Skip to main content

Adding Commands

Adding Commands

Internal Extension

Carry one user-facing operation from Typer input to tested core behavior without mixing responsibilities.

Commandapp/cli/commands/<feature>/

Category

Developer Guide

Quick Command

options -> model -> resolver -> command -> core

Typerargument modelconfiguration resolvertests

Overview

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

Declares Typer callbacks, reads application context, builds resolved arguments, and delegates to a core service or pipeline.

options.py

Defines reusable Annotated Typer options, aliases, validation, completion, help text, and help-panel grouping.

models.py

Defines typed dataclasses for the effective arguments consumed after resolution.

resolver.py

Applies CLI -> project config -> built-in default precedence and returns the typed argument model.
Recommended package layout
text
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.py

Implementation Flow

Workflow Timeline

1
Define the public contract
Choose the command path, required inputs, safe defaults, side effects, help text, and whether global dry-run applies.
completed
2
Create typed options and a result model
Keep CLI declarations reusable and make the resolver return one explicit dataclass.
completed
3
Resolve effective values
Use ConfigLoader.resolve for supported project defaults and leave CLI option defaults unspecified when fallback is intended.
current
4
Delegate from the callback
Merge global AppContext values, construct the required core service or pipeline, and avoid implementing domain logic in command.py.
pending
5
Register and test
Expose the command through the root application and cover every layer before updating public documentation.
pending

Resolver Contract

Resolvers make precedence visible and independently testable.

Illustrative resolver
python
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.


Continue