Getting Started¶
Requirements¶
- Python 3.13+
- uv
Installation¶
Running the CLI¶
Global flags¶
These flags work in front of any subcommand:
# Enable DEBUG logging for the duration of the command
uv run cli-app --verbose command example-command hello
# Emit machine-readable JSON instead of Rich text
uv run cli-app --output-format json command example-command hello
# Pipe stdin into a command
echo "hello" | uv run cli-app command example-command
Shell completion¶
# Install completion for the current shell
uv run cli-app completion install
# Or target a specific shell
uv run cli-app completion install --shell zsh
# Print the script without installing
uv run cli-app completion show
Development Commands¶
Tasks are available via taskipy — run with uv run task <name>:
uv run task lint # ruff check .
uv run task fmt # ruff format .
uv run task typecheck # mypy src/
uv run task test # pytest (parallel, 80% coverage enforced)
uv run task test-fast # pytest --no-cov -n auto
uv run task audit # pip-audit dependency audit
Or run the tools directly:
uv run pytest # full suite
uv run pytest --no-cov # skip coverage (faster)
uv run pytest tests/path/to/test.py # single file
Versioning & Changelog¶
Commit messages follow Conventional Commits — enforced by the commit-msg pre-commit hook.
uv run cz bump # bump version, update CHANGELOG, create tag
uv run cz changelog # update CHANGELOG without bumping
uv run cz changelog --dry-run
Bump type is inferred from commits: fix: → patch · feat: → minor · feat!: / BREAKING CHANGE: → major.
Adding a Command Group¶
- Create
src/cli_app/cli/commands/my_command.py:
import structlog
from typer import Context, Typer
from cli_app.utils.console import get_console
from cli_app.utils.output import OutputFormat, render_output
app = Typer()
console = get_console()
log = structlog.get_logger()
@app.command()
def my_action(ctx: Context, name: str) -> None:
"""Do something."""
log.debug("my_action called", name=name)
fmt = ctx.obj.get("output_format", OutputFormat.text) if ctx.obj else OutputFormat.text
render_output(
{"name": name},
fmt,
text_render=lambda: console.print(f"Hello, [bold]{name}[/bold]!"),
)
- Export it from
src/cli_app/cli/commands/__init__.py:
- Register it in
src/cli_app/cli/app.py:
Stdin support¶
Use read_stdin_if_piped() to accept piped input as a fallback when an argument is omitted:
from cli_app.utils.stdin import read_stdin_if_piped
@app.command()
def process(ctx: Context, text: str | None = None) -> None:
resolved = text if text is not None else read_stdin_if_piped()
if not resolved:
raise typer.Exit(1)
...
Configuration¶
Behaviour can be overridden via environment variables or a .env file:
| Prefix | Controls |
|---|---|
CLI_APP_CONSOLE_* | Rich console (theme, colors, width) |
CLI_APP_LOG_* | Log level, file path, rotation, JSON format |