ADR-0008: Click over Typer¶
Status¶
Accepted
Context¶
td needs a Python CLI framework. The realistic options:
- Click. Mature, explicit, decorator-based. Exposes the command tree as inspectable objects.
- Typer. Built on top of Click. Uses type hints and function signatures to generate CLI commands with less boilerplate for simple cases.
- argparse. Standard library. Verbose. No ecosystem for the richer features below.
- Custom. Full control, massive maintenance burden. Unjustifiable for a side project.
The deciding factor is what the framework has to support beyond basic argument parsing:
- Rich, JSON, and Plain output modes with uniform formatting
- Shell completion generation for bash, zsh, and fish
- A machine-readable schema of the entire command tree (ADR-0006)
- Error interception at a single chokepoint
- A custom
Groupsubclass that catches and formats every exception
All of these require owning the command tree: walking it at runtime to generate artifacts, intercepting method calls on the Group, and threading a shared output formatter through every command context. Typer abstracts the command tree behind its decorator and type-hint machinery, which makes these walks awkward or impossible without reaching into Typer's private internals. Click exposes the tree directly and expects you to walk it.
Decision¶
Use Click directly. Implement a custom TdGroup(click.Group) subclass
in cli/__init__.py whose invoke() method catches every exception,
maps API errors through map_api_exception(), and renders them via
OutputFormatter.
Do not layer Typer on top. The "less boilerplate" benefit is real but small, and it costs direct access to the command tree that drives schema generation (ADR-0006), shell completions, and uniform error handling.
Consequences¶
Positive. Every command is a click.Command object we can
inspect at runtime. td schema walks the tree directly. Shell
completions are generated from the same objects. Error handling is
uniform because TdGroup.invoke() is the single chokepoint for
command execution. All commands flow through OutputFormatter via
ctx.obj["formatter"].
Negative. More boilerplate per command than Typer. Every flag
and argument is an explicit @click.option or @click.argument
decoration. No type-driven auto-generation from function signatures.
Reversibility. Adopting Typer later would require rewriting
every command decorator and rewriting schema.py to navigate
Typer's structure instead of Click's. Not a reversible decision
once commands accumulate. This ADR exists so a future refactor
does not attempt the migration without understanding what breaks.