"""The one module in `core/` that knows about Typer. **Why this is not a violation of the layering.** The invariant D-263 states is that typer/click appear only in `main.py` and `router.py` — and its purpose is that a *service* must never know it was called from a CLI. A shared group class is transport by definition; the alternative is copying the same subclass into every `router.py`, where the copies drift and only some domains end up enumerating their verbs. So the invariant is refined rather than broken: no typer/click in service.py, schemas.py or helpers.py — ever. transport lives in main.py, router.py, and this module. Keep that bound. If something here stops being about *transport*, it belongs somewhere else. """ from __future__ import annotations import typer from typer.core import TyperGroup class ReachDomainGroup(TyperGroup): """A domain group whose unknown-verb error names the verbs that exist. Click's default is `No such command 'x'` — which tells you that you are wrong without telling you what would be right. That is the closed-set gap D-263 measured in pql (an invalid status rejected without naming the six valid ones), and the fix is nearly free: the verb list is already registered on the group, so enumerating it costs a sort. """ def resolve_command(self, ctx: typer.Context, args: list[str]): if args and self.get_command(ctx, args[0]) is None: listed = ", ".join(sorted(self.list_commands(ctx))) ctx.fail(f"unknown command {args[0]!r}\n\nChoose one of: {listed}") return super().resolve_command(ctx, args) def domain(name: str, help: str) -> typer.Typer: """Build a domain's Typer app with the house settings applied. Every domain router should use this rather than calling `typer.Typer` directly, so the settings that are easy to forget are not per-router decisions: - `cls=ReachDomainGroup` so unknown verbs enumerate. - `rich_markup_mode=None` — load-bearing, not cosmetic: it keeps `rich` and `pygments` off the import path, and stops typer drawing box-art help even when stdout is a pipe, which would litter hook logs. - `no_args_is_help` so a bare `reach ` says what it can do. Note the caller still needs a `@app.callback()` on the router: Typer collapses a single-command app into a bare command, and without the callback `reach ` fails with "unexpected extra argument". """ return typer.Typer( name=name, help=help, cls=ReachDomainGroup, no_args_is_help=True, add_completion=False, rich_markup_mode=None, )