A small reference project that demonstrates a Click-based Python CLI with a simple plugin-style command discovery mechanism and modern tooling.
- CLI framework: Click
- Dynamic command loading: commands are discovered from the
src/cli/commands/tree and loaded lazily at runtime - Package management: uv for fast dependency management
- Code quality: Ruff (lint + format) and Pyright (type checking)
- Testing: pytest (with coverage support)
- Security: pip-audit and bandit
- Automation: a Makefile with common tasks
Install uv.
Create / sync the project environment (this installs the project in editable mode, including the mycli console script):
make setupuv run mycli --help
uv run mycli compute add 1 2
uv run mycli net ping 1.1.1.1 --count 1Notes:
- If you skip
make setup, you can runuv sync --locked --all-extras --devdirectly. - To install explicitly (editable) and see post-install hints, run
make install. - The Python-module equivalent of
mycli --helpis:uv run python -m cli.main --help.
This repository includes example commands:
compute add <a> <b>compute sub <a> <b>net ping <host> [--count N]
Commands are discovered from:
src/cli/commands/<...nested command path...>/
Each plugin directory must contain:
entry.py(required): exportscli, aclick.Command(typically created with@click.command())meta.yaml(required): a YAML mapping containing a non-emptyshort_helpstring
Supported meta.yaml keys:
short_help(required)help_group(optional, default:Commands)enabled(optional, default:true)hidden(optional, default:false; forced totruewhenenabled: false)packaged(optional; used for packaging workflows)no_args_is_help(optional, default:false)
Compatibility note: legacy aliases (shortHelp, HelpSummary, HelpGroup) are still accepted by the loader.
At runtime, the root command lists available command groups and loads subcommands only when invoked.
Dot-prefixed and __-prefixed directories are ignored during command discovery.
uv run mycli admin new-command mul --short-help "Multiply two integers."
uv run mycli admin new-command mul --parent compute --short-help "Multiply two integers."
uv run mycli admin new-command issue --parent github.repo --short-help "Manage repository issues."--parent uses dot-notation to create nested command paths (github.repo => github/repo).
Any missing parent groups are scaffolded automatically.
If you want to generate plugins into a different directory during development, set:
<ENV_PREFIX>COMMANDS_DIR(used by theadmin new-commandhelper)<ENV_PREFIX>REBRAND_PROJECT_ROOT(used by theadmin rebrandhelper)
The environment variable prefix is configurable in pyproject.toml:
[tool.mycli]
env_prefix = "mycli_"
name = "mycli"
cli_name = "mycli"With the default prefix this resolves to mycli_COMMANDS_DIR.
name: branded display name used in CLI metadata output (for example, banner/version text)cli_name: command name used as the CLI program name
With the default prefix, supported override variables are:
mycli_COMMANDS_DIRmycli_REBRAND_PROJECT_ROOT
make format
make lint
make test
make coverage
make scanThe Makefile supports environment-specific configuration via .env files:
.env(base).env.local(local overrides, gitignored).env.$(ENV)(environment-specific; e.g..env.dev,.env.prod)
Example:
ENV=prod make lint.
├── src/
│ └── cli/
│ ├── commands/ # Dynamic command plugins
│ ├── dev.py # Developer utilities ("mycli dev …")
│ ├── loader.py # Command discovery + lazy loader
│ └── main.py # Console entry point
├── tests/
├── pyproject.toml
├── Makefile
└── README.md
MIT