Adding a CLI command¶
Every CLI command is one file under src/jellycell/cli/commands/. The typer app auto-registers them.
Pattern¶
# src/jellycell/cli/commands/mycmd.py
from __future__ import annotations
import json
from pathlib import Path
import typer
from pydantic import BaseModel
from rich.console import Console
from jellycell.cli.app import app
from jellycell.paths import Project
console = Console()
class MyCmdReport(BaseModel):
"""JSON output shape for `jellycell mycmd`. Spec §10.1 schema contract."""
schema_version: int = 1
project: str
result: str
@app.command("mycmd")
def mycmd(
path: Path = typer.Argument(..., help="Thing to operate on"),
json_output: bool = typer.Option(False, "--json", help="Emit JSON to stdout"),
) -> None:
"""One-line summary. Shown in `jellycell --help`."""
project = Project.from_path(Path.cwd())
# ... do the work ...
report = MyCmdReport(project=project.root.name, result="ok")
if json_output:
typer.echo(report.model_dump_json())
else:
console.print(f"[green]ok[/green] {report.result}")
Rules¶
--jsonis mandatory. Every command that produces data emits a pydantic model withschema_version: 1. Bump the version if the schema breaks.Use
richfor human output; rawtyper.echofor JSON. Never mix them.Errors on stderr.
console.print(..., file=sys.stderr)ortyper.echo(..., err=True).Project.from_path(Path.cwd())discovers the root. Don’t accept raw paths into the project — always go throughProject.Document the command in docs/cli-reference.md once
sphinxcontrib-typerauto-generates it, this is automatic.
Testing¶
Unit test:
tests/unit/test_cli_mycmd.py— mockProject, assert behavior.Integration:
tests/integration/test_cli_mycmd.py— usetyper.testing.CliRunneragainst a realsample_projectfixture.