How to add a command
This guide walks through adding a new top-level command to the gitmastery CLI. The CLI uses Click, a Python library for building command-line interfaces.
We’ll use a simple greet command as an example throughout.
This is for demonstration purposes. The
greetcommand is not a real feature and should not be merged into the codebase. When adding real commands, follow the same steps but implement the actual functionality needed.
1. Create the command file
Create a new file under app/app/commands/:
app/app/commands/greet.py
Every command is a Python function decorated with @click.command(). Use the shared output helpers from app.utils.click instead of print().
# app/app/commands/greet.py
import click
from app.utils.click import info, success
@click.command()
@click.argument("name")
def greet(name: str) -> None:
"""
Greets the user by name.
"""
info(f"Hello, {name}!")
success("Greeting complete.")
Output helpers
| Helper | When to use |
|---|---|
info(msg) | Normal status messages |
success(msg) | Command completed successfully |
warn(msg) | Non-fatal issues or warnings |
error(msg) | Fatal issues — exits immediately |
2. Register the command in cli.py
Open app/app/cli.py and add two things:
- Import your command at the top.
- Add it to the
commandslist instart().
# app/app/cli.py
# 1. Import
from app.commands.greet import greet # add this
# 2. Register
def start() -> None:
commands = [check, download, greet, progress, setup, verify, version] # add greet
for command in commands:
cli.add_command(command)
cli(obj={})
3. Verify it works locally
Run the CLI directly from source to confirm the command appears and works:
uv run python main.py greet Alice
Expected output:
INFO Hello, Alice!
SUCCESS Greeting complete.
Also check it shows in --help:
uv run python main.py --help
4. Add an E2E test
Every new command needs an E2E test. Create a new file under tests/e2e/commands/:
tests/e2e/commands/test_greet.py
# tests/e2e/commands/test_greet.py
from pathlib import Path
from ..runner import BinaryRunner
def test_greet(runner: BinaryRunner, gitmastery_root: Path) -> None:
"""greet prints the expected message."""
res = runner.run(["greet", "Alice"], cwd=gitmastery_root)
res.assert_success()
res.assert_stdout_contains("Hello, Alice!")
RunResult assertion methods
| Method | Description |
|---|---|
.assert_success() | Asserts exit code is 0 |
.assert_stdout_contains(text) | Asserts stdout contains an exact substring |
.assert_stdout_matches(pattern) | Asserts stdout matches a regex pattern |
5. Run the E2E tests
Build the binary first, then run the suite:
# Build
uv run pyinstaller --onefile main.py --name gitmastery
# Set binary path (Unix)
export GITMASTERY_BINARY="$PWD/dist/gitmastery"
# Set binary path (Windows, PowerShell)
$env:GITMASTERY_BINARY = "$PWD\dist\gitmastery.exe"
# Run only your new test
uv run pytest tests/e2e/commands/test_greet.py -v
# Run the full E2E suite
uv run pytest tests/e2e/ -v
All tests should pass before opening a pull request.
Command group (optional)
If you want to add a command that has subcommands (like gitmastery progress show), use @click.group() and register subcommands with .add_command().
# app/app/commands/greet.py
import click
from app.utils.click import info
@click.group()
def greet() -> None:
"""Greet people in different ways."""
pass
@click.command()
@click.argument("name")
def hello(name: str) -> None:
"""Say hello."""
info(f"Hello, {name}!")
@click.command()
@click.argument("name")
def goodbye(name: str) -> None:
"""Say goodbye."""
info(f"Goodbye, {name}!")
greet.add_command(hello)
greet.add_command(goodbye)
Register greet the same way in cli.py. The user then runs:
gitmastery greet hello Alice
gitmastery greet goodbye Alice
Checklist
Before opening a pull request for a new command:
- Command file created under
app/app/commands/ - Command registered in
app/app/cli.py - Command works locally with
uv run python main.py - E2E test file created under
tests/e2e/commands/ - E2E tests pass with the built binary
See E2E testing flow for a full explanation of the test infrastructure and how to write more complex tests.