Adding Commands
Adding Commands
Carry one user-facing operation from Typer input to tested core behavior without mixing responsibilities.
app/cli/commands/<feature>/Category
Quick Command
options -> model -> resolver -> command -> coreOverview
An active Custy command normally separates user-facing Typer declarations from resolved values and core behavior. Preserve that separation even when a small feature could technically fit in one file; it keeps configuration precedence, testing, and command registration understandable.
Command Package Shape
Four Focused Modules
command.py
options.py
models.py
resolver.py
app/cli/commands/example/
|- **init**.py
|- command.py
|- models.py
|- options.py
- resolver.py
tests/cli/commands/example/
|- test_command_example.py
|- test_models_example.py
|- test_options_example.py
- test_resolver_example.pyImplementation Flow
Workflow Timeline
Resolver Contract
Resolvers make precedence visible and independently testable.from app.cli.commands.example.models import ExampleArgs
def resolve_example_args(config, cli_args) -> ExampleArgs:
"""Resolve effective arguments for the example command."""
return ExampleArgs(
output_file=config.resolve(
cli_args.output_file,
["cli", "paths", "example_output"],
"example.txt",
),
)
The configuration path is relative to [tool.custy]. Keep the packaged
configuration template, resolver key, consuming code, tests, and public
configuration reference synchronized.
Register the Command
For a new command module, export its command module from
app/cli/commands/__init__.py. Then choose one root registration style in
app/cli/main.py:
app.command(name="example")(example_command.example)for one direct top-level command;app.add_typer(example_command.app, name="example")for a group containing subcommands.
Registration determines the public CLI path. A pipeline profile with the same
name does not automatically create a CLI command, and registering a pipeline
step does not expose a public custy run choice.
Command Callback Checklist
Before You Begin
- Read the shared configurationRequired
Use get_config() rather than parsing TOML inside the command.
- Create raw CLI argumentsRequired
Map Typer values into the existing CliArgs boundary or another explicit input model.
- Resolve effective argumentsRequired
Call the feature resolver once and use the returned dataclass downstream.
- Merge global contextRequired
Carry dry-run, debug, and log-level values from AppContext when the delegated core flow supports them.
- Delegate business behaviorRequired
Invoke a focused core service or construct the appropriate registered pipeline.
- Return actionable failuresRequired
Preserve project exception context and give users a corrective next step.
Test the Complete Boundary
- Verify the command or group appears under the intended public path.
- Verify option names, aliases, types, defaults, validation, and completion metadata.
- Verify the dataclass stores the effective values with useful types.
- Verify explicit CLI values win, configuration fills omitted values, and the built-in fallback is used last.
- Mock the delegated core boundary and verify the callback passes the expected arguments and global context.
- Add an integration or regression test when registration, pipeline composition, filesystem state, or Git behavior crosses modules.