A Python CLI reference project built with Click and a plugin-style command loader. It is designed as a starting point for CLIs that need dynamic command discovery, user-extensible commands, and a practical developer toolchain.
This project demonstrates how to:
- build a nested CLI using Click
- discover commands from both built-in and user command trees
- lazily load command modules at runtime
- scaffold and rebrand commands using built-in admin utilities
- develop and ship with uv, Ruff, Pyright, pytest, Bandit, and Make
- Python 3.12+
- uv
Create/sync the environment and install dependencies:
make setupInstall the package in editable mode (optional, but useful when developing command entry points):
make installShow top-level help:
uv run mcli --helpRun sample commands:
uv run mcli samples add 1 2
uv run mcli samples sub 5 3
uv run mcli samples ping 1.1.1.1 --count 1Run admin utilities:
uv run mcli admin new-command mul --short-help "Multiply two integers."
uv run mcli admin rm-command mul
uv run mcli admin rebrand --name "My CLI" --cli mycliModule-entry equivalent:
uv run python -m cli.main --helpsamples— demo commands (add,sub,ping)admin— project utilities (new-command,rm-command,rebrand)
Commands are discovered from both trees:
src/cli/commands/<...nested command path...>/
$HOME/.<package-name>/commands/<...nested command path...>/
<package-name> is derived from project metadata (see src/cli/utils/metadata.py).
If the same command path exists in both trees, the user command wins.
Each command directory should provide:
entry.pyexportingclimeta.yamlwith a non-emptyshort_help
Supported meta.yaml keys:
short_help(required)help_group(optional, defaultCommands)enabled(optional, defaulttrue)hidden(optional, defaultfalse; forcedtruewhen disabled)packaged(optional; used by packaging workflows)no_args_is_help(optional, defaultfalse)
Legacy aliases (shortHelp, HelpSummary, HelpGroup) are accepted.
Create nested commands using dot-notation parents:
uv run mcli admin new-command issue --parent github.repo --short-help "Manage repository issues."Use --user with admin new-command or admin rm-command to target the per-user command tree at $HOME/.<package-name>/commands.
Without --user, admin new-command respects MCLI_COMMANDS_DIR when it is set and otherwise falls back to the per-user tree. admin rm-command respects MCLI_COMMANDS_DIR when it is set and otherwise targets the packaged command tree. You can override paths with environment variables using the configured prefix from pyproject.toml:
MCLI_COMMANDS_DIRMCLI_REBRAND_PROJECT_ROOT
Common tasks:
make help
make format
make lint
make test
make coverage
make build
make scanEnvironment loading for Make targets (later files override earlier):
.env-build.env-build.local.env-build.$(ENV)
Example:
ENV=prod make lint- Read CONTRIBUTING.md.
- Install pre-commit hooks:
uv run pre-commit install- Before opening a PR, run formatting, linting, tests, and build checks:
make format lint test build.
├── src/cli/
│ ├── commands/ # Built-in command plugins
│ ├── loader.py # Discovery + lazy loading
│ ├── main.py # CLI entry point
│ └── utils/
├── tests/
├── pyproject.toml
├── Makefile
└── README.md
MIT