1. Your first command¶
By the end of this page you'll have a working projectctl tail-logs
command and understand how clickwork finds commands on disk.
Scaffold the project¶
uv init --package . gives you a modern pyproject.toml and a
src/projectctl/ layout. Create the commands/ directory inside
the package (that's where commands_dir will point):
Wire up the CLI¶
Create src/projectctl/__main__.py:
And src/projectctl/cli.py:
from pathlib import Path
from clickwork import create_cli
cli = create_cli(
name="projectctl",
commands_dir=Path(__file__).parent / "commands",
)
commands_dir is typed as pathlib.Path (discovery calls .is_dir()
and .glob() on it). Path(__file__).parent / "commands" resolves
relative to this cli.py so the command works regardless of what
directory you run python -m projectctl from.
Write the first command¶
Create src/projectctl/commands/tail_logs.py:
from pathlib import Path
import click
@click.command(name="tail-logs")
@click.argument("path", type=click.Path(path_type=Path, exists=True))
@click.option("-n", "--lines", default=20, show_default=True,
help="How many lines from the tail.")
def cli(path: Path, lines: int) -> None:
"""Tail a log file."""
content = path.read_text().splitlines()
for line in content[-lines:]:
click.echo(line)
Note: the attribute MUST be named cli — that's what clickwork's
discovery looks for. The Click name= kwarg controls how the
subcommand appears on the command line.
Install the project into the venv¶
This creates .venv/ and installs projectctl in editable mode plus
clickwork.
Add clickwork as a dep if uv init didn't:
Run it¶
Create a sample log:
Then:
Expected:
Next¶
In Adding a plugin you'll add a second
command, but via a separate installable package rather than another
file in commands/.