feat(cli): port subcommands (search/index/find-related/init/savings/mcp) from semble#15
Conversation
There was a problem hiding this comment.
Code Review
This pull request ports the CLI implementation from Python to TypeScript (src/cli.ts) and introduces a comprehensive test suite in src/cli.test.ts. The review feedback suggests improving the CLI's robustness and testability by throwing errors instead of calling process.exit(1) directly in _runInit (which also simplifies the unit tests), wrapping runCli in a try/catch block to prevent unhandled crashes, and updating the index command to properly resolve and pass content types using the --content flag.
There was a problem hiding this comment.
5 issues found across 2 files
Architecture diagram
sequenceDiagram
participant CLI as CLI Entrypoint
participant Parser as parseArgs()
participant AgentFile as Agent File Loader
participant Index as CspIndex
participant Stats as formatSavingsReport
participant MCP as MCP Server
participant FS as File System
participant Git as Git Remote
Note over CLI,Git: Subcommand dispatch flow
CLI->>Parser: parseArgs(argv)
Parser-->>CLI: { command, positional, flags }
alt command = "init"
CLI->>CLI: _coerceAgent(flags.agent)
CLI->>AgentFile: _readAgentFile(agent)
AgentFile->>FS: readFile via import.meta.url
FS-->>AgentFile: agent markdown content
AgentFile-->>CLI: markdown string
CLI->>FS: stat(_agentPath)
alt file exists AND not --force
FS-->>CLI: exists
CLI->>CLI: process.exit(1) with error
else not exists OR --force
CLI->>FS: mkdir + writeFile
FS-->>CLI: written
CLI->>CLI: process.stdout.write "Created ..."
end
end
alt command = "search"
CLI->>CLI: _getNumberFlag(topK)
CLI->>CLI: _resolveContent(flags)
alt --index provided
CLI->>Index: fromPath(path, content) or fromGit(url, content)
else path provided
CLI->>Index: fromPath(path, content)
end
Index-->>CLI: CspIndex instance
CLI->>Index: search(query, { topK })
Index-->>CLI: SearchResult[]
alt empty results
CLI->>CLI: JSON output {"error": "No results found."}
else results exist
CLI->>CLI: formatResults(results) → JSON to stdout
end
end
alt command = "index"
CLI->>CLI: isGitUrl(path)?
alt git URL
CLI->>Git: CspIndex.fromGit(url, { includeTextFiles })
Git-->>Index: index built from remote
else local path
CLI->>FS: CspIndex.fromPath(path, { includeTextFiles })
FS-->>Index: index built from files
end
Index->>FS: index.save(out)
FS-->>Index: saved
end
alt command = "find-related"
CLI->>CLI: validate line is /^-?\d+$/
alt invalid line
CLI->>CLI: process.exit(1) "line must be an integer"
end
CLI->>Index: fromPath(path, content) or fromGit(url, content)
CLI->>Index: search(similarity, { topK })
Index-->>CLI: results to stdout
end
alt command = "savings"
CLI->>Stats: formatSavings({ verbose })
Stats-->>CLI: report string
CLI->>CLI: stdout report
end
alt command = "mcp"
CLI->>MCP: serve(path, { ref, content })
MCP-->>CLI: server started
end
alt command = "--help" or "-h"
CLI->>CLI: _printHelp() → stdout
end
Note over CLI,Git: Key flags parsed via parseArgs
Note over Parser: --flag value, --flag=value, boolean<br/>short -k, multi-value --content<br/>-- end-of-options
Note over CLI: Flags: --top-k/-k, --content, --index<br/>--agent/-a, --force, -o/--out, --ref<br/>--verbose, --include-text-files (deprecated)
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
- _runInit: throw Error instead of process.exit(1); callers handle exit code - _runIndex: accept content: ContentType[] (was includeTextFiles boolean) - index dispatch: resolve --content / --include-text-files like other commands - runCli: wrap dispatch in try/catch; surface user-friendly errors as exit 1 - unknown subcommand: return exit 1 (was 0); print 'Unknown command' to stderr - tests: use rejects.toThrow for _runInit (no process.exit mocking) - tests: cover unknown subcommand, invalid --agent, invalid --content, runCli-level translation of init rejection, and index --content forwarding Deferred: greedy --content parsing, --agent codex (also missing upstream).
amondnet
left a comment
There was a problem hiding this comment.
Applied 8, deferred 2.
Applied (a911c84):
- gemini #3318894235 / cubic #3318943092 —
_runInitthrowsErrorinstead ofprocess.exit(1) - gemini #3318894249 —
_runIndexacceptscontent: ContentType[] - gemini #3318894280 —
indexdispatch resolves--content/--include-text-files - gemini #3318894296 — test simplified to
rejects.toThrow - gemini #3318894307 / cubic #3318943095 —
runCliwraps dispatch intry/catch; user-friendly stderr + exit 1 - cubic #3318943072 — unknown subcommands now exit 1 (was 0)
New tests: unknown subcommand, invalid --agent, invalid --content, runCli-level translation of init collision, csp index --content content forwarding. bun test src/cli.test.ts → 34 pass, 0 fail.
Deferred (matches upstream semble; tracked in PR description's deferred-findings list):
- cubic #3318943061 — greedy
--contentparsing (matchesargparse nargs="+") - cubic #3318943068 —
--agent codex(upstream sembleAgentenum also omits codex; nosrc/agents/codex.mdtemplate exists yet)
Unit 15 —
src/cli.tsReplaces the placeholder
src/cli.tswith a port ofsrc/semble/cli.py, plus a newsrc/cli.test.tscovering the parser, the init flow, search dispatch, savings, mcp routing, and find-related validation.Subcommands implemented
csp search <query> [path]— flags:--top-k/-k,--index,--content code|docs|config|all,--include-text-files(deprecated)csp index <path> -o <out>— flag:--include-text-filescsp find-related <file_path> <line> [path]— flags:--top-k,--index,--contentcsp init [--agent claude|copilot|cursor|gemini|kiro|opencode] [--force]— writes to.<agent>/agents/csp-search.md(or.github/agents/csp-search.mdfor copilot)csp savings [--verbose]csp mcp [path] [--ref] [--content ...]— starts MCP server (explicit subcommand, divergence from semble's bare-binary default)CLI parser
Commander isn't yet in
package.json(Unit 0 territory), so this port uses a small purpose-builtparseArgsthat handles:--flag value,--flag=value, boolean--flag-k 5,-o idx)--content code docs--end-of-options markerrunCli(argv, options)accepts injection points for the CLI's external dependencies (fromPath,fromGit,readIndex,serveMcp,formatSavings,readAgentFile, etc.), making the surface unit-testable without real indexing or model loads.Agent markdown loading
Agent files live at
src/agents/<agent>.mdand are loaded at runtime vianew URL('./agents/${agent}.md', import.meta.url)+fs.readFile. For the published package,tsdown --unbundlemay not copy markdown automatically — if that turns out to be the case, a follow-up should inline the markdown into TS (export const CLAUDE_MD = '...') or configure tsdown to copy assets.Tests (
src/cli.test.ts, 29 tests, all passing)Agentenum values_agentPathfor claude/copilot/cursor/opencodeparseArgsfor sub-command + positional,--flag value,--flag=value, boolean, multi-value--content, short-k_resolveContentfor default/all/include-text-files/multiple/unknownrunCli --helpmentions every subcommandcsp init --agent claudewrites.claude/agents/csp-search.mdto a tmp cwdcsp init --agent copilotwrites.github/agents/csp-search.mdcsp initwithout--forceerrors if file exists (verifies original content preserved); with--forceoverwritescsp search foo .callsindex.search('foo', { topK })with the right topKformatResults, JSON-stringifiedcsp savings/--verboseforwards the flagcsp mcpdispatches to the injectedserveMcpwith path/ref/content (locks in the dispatch fix below)csp find-related src/auth.ts 42abc .errors with exit 1 andline must be an integer(locks in the validation fix below)Code-review findings fixed in this PR
csp mcpwas misrouted to help —'mcp'was missing fromCLI_DISPATCH_ARGS, so the routing predicate fell into the "unknown subcommand → print help" branch. Added'mcp'to the set. Verified with new test +bun run --bun src/cli.ts mcpnow reaches the serve stub._runMcphelper — removed; themcpbranch inlines the call toservedirectly.Number.parseIntaccepted trailing junk infind-related <line>(42abcsilently parsed as 42, diverging from semble's argparsetype=int). Added an/^-?\d+$/validation gate beforeparseInt.Deferred findings (follow-up issue worth filing)
--content— multi-value parsing for--contentgreedily consumes every following non-dash token (matches semble's argparsenargs="+"behavior).csp search foo --content code .swallows the trailing.path. Mitigation: README examples place--contentat the end. Long-term fix is to limit to known choices or require explicit=.--help—csp savings --helpruns the savings command instead of showing help; per-subcommand help isn't implemented.invokedDirectlycheck usesendsWith('/cli.ts'), which won't match backslash-style argv on Windows. Bun on Windows would mostly normalize, but worth replacing withimport.meta.mainwhen available.--ref -release(refs starting with a dash) misparses as boolean. Unusual but documented for completeness.Stubs / sibling integration
The following files are stubs to make the CLI typecheck and let the tests run; they are NOT part of this PR (intentionally left untracked) and belong to other units:
src/types.ts—ContentType,Chunk,SearchResultsrc/utils.ts—isGitUrl,resolveChunk,formatResultssrc/stats.ts—formatSavingsReportsrc/indexing/index.ts—CspIndexclass with stubbed static methodssrc/mcp/server.ts—servesrc/agents/*.md— initial port of upstream agent markdown withsemble→csprenamesIf the parallel unit owners create the same files, the stubs here can be discarded —
src/cli.tsonly depends on the exported names listed in the task brief.Local typechecking note
tsconfig.jsoncurrently lacksallowImportingTsExtensions, which makestsc --noEmitreject the.tsimports the project uses. I confirmed locally that adding"allowImportingTsExtensions": trueresolves it, but per task scope I did not modifytsconfig.json. Bun runs the file fine without that flag.Verification
Summary by cubic
Replaces the placeholder CLI with a TypeScript port of the
sembleCLI, addingsearch,index,find-related,init,savings, andmcp, plus 29 unit tests. Improves error handling and makes--contentconsistent across commands.New Features
search,index,find-related,init,savings,mcp.parseArgswith--flag value,--flag=value, short flags, multi-value--content, and--.--index; outputs JSON.initwrites agent file to.<agent>/agents/csp-search.md(Copilot to.github/agents/csp-search.md);mcpstarts the MCP server with--refand--content.Bug Fixes
mcpnow routes to the server instead of showing help.find-relatedrequires<line>to be an integer (rejects values like42abc).index,search,find-related, andmcpall resolve--content(code|docs|config|all);--include-text-filesis deprecated (use--content all).runClinow returns exit 1 with a stderr message on errors (e.g., unknown subcommand, invalid--agent/--content);_runInitthrows and is translated to exit 1.Written for commit a911c84. Summary will update on new commits.