CLI Adapter Boundary

The CLI is an adapter around the application, not the internal API. Its job is to parse terminal arguments, create typed request models, call the CodeAlmanac app, and render results [@cli-main] [@dispatch-wiki]. Product behavior belongs in services and workflows assembled by the Composition Root, while terminal wording belongs in render modules.

This boundary is split into three responsibilities: parser modules define public command syntax, dispatch modules translate parsed arguments into service or workflow calls, and render modules own human and JSON output. The root files are intentionally small facades so command-family modules can grow without turning one CLI file into the product center [@parser-root] [@dispatch-root] [@render-root].

Entrypoint#

main() builds the parser, parses argv, and constructs the app with create_app(). A CliSyntaxError at parse time renders a guided syntax screen and returns exit code 2 before the app is ever touched [@cli-main]. Once parsing succeeds, main() hands the parsed args, the app, and a start timestamp to execute() in cli/execution.py, which is the actual product-error boundary [@cli-main] [@cli-execution].

execute() calls the root dispatch(args, app) and translates what comes back into stderr text and an exit code: a CodeAlmanacError or Pydantic ValidationError prints codealmanac: ... to stderr and returns 1; a KeyboardInterrupt is re-raised after capture (exit code 130); any other exception is captured as a telemetry exception before it propagates [@cli-execution]. execute() also calls capture_command() after every path, successful or not, which is why command telemetry lives at this layer rather than in main() — see Telemetry for the event shape. See Terminal Output and the error and exit code contract for how the syntax-error path renders and why it gets its own exit code.

That small entrypoint enforces the adapter role. The CLI owns process-level concerns: argument parsing, stderr for user-facing failures, and integer exit codes. It does not construct stores, choose integrations, or run lifecycle logic directly.

Parser Boundary#

cli/parser/root.py creates the codealmanac parser, installs --version, and registers command families through add_run_commands, add_wiki_commands, and add_admin_commands [@parser-root]. The public command metavar lists the full visible command surface: init, ingest, garden, sync, list, search, show, topics, health, validate, reindex, serve, tag, untag, config, setup, uninstall, doctor, update, jobs, and automation [@parser-root].

Parser modules should describe syntax only. For example, the root parser knows that command families exist, but it does not know how search calls the index or how setup writes instructions. That keeps public command shape separate from product action.

Dispatch Boundary#

cli/dispatch/root.py routes by command family. It checks whether the command is a run, wiki, or admin command and delegates to that family dispatcher [@dispatch-root]. The architecture tests require this root dispatcher to stay short and to avoid importing request models such as IngestRequest or SearchPagesRequest [@architecture-tests].

Command-family dispatchers perform the adapter translation. Wiki search builds a SearchPagesRequest from the current directory, --wiki, query text, topic flags, mention filter, and limit, then calls app.search.search(...) and renders the result [@dispatch-wiki]. Show, health, validate, reindex, tag, and untag follow the same pattern: build the owning service request, call the app, render, and return an exit code [@dispatch-wiki].

Render Boundary#

cli/render/root.py is a re-export facade for dispatcher stability. It imports render functions from specific modules such as repositories, run commands, sync, and wiki renderers, then exposes them through __all__ [@render-root]. It does not render text itself.

The tests protect this shape. They require render root to stay a facade, wiki render to stay split by output family, admin render to stay split by output family, and dispatch files to stay split by command domain [@architecture-tests]. This prevents a common CLI failure mode where parsing, service calls, JSON formatting, and terminal prose collapse into one file.

Change Rule#

When adding a CLI command, put syntax in the parser family, request construction in the dispatch family, and output in the render family. The command should cross into the core through Request Models, then return through a renderer. That keeps the CLI easy to test and keeps future server or worker entrypoints free to call the same services without shelling through terminal code.