Reusable CLI framework for project automation
Project description
clickwork
Reusable CLI framework for project automation. Build project-specific CLIs with plugin discovery, layered config, subprocess helpers, and common utilities -- so your commands focus on business logic, not boilerplate.
Status: 1.0 stable. The public API is documented in docs/API_POLICY.md and covered by SemVer: breaking changes require a major bump and removals carry a one-minor deprecation runway. All features are driven by real orbit-admin needs -- no speculative abstractions.
Upgrading from 0.2.x? See docs/MIGRATING.md for the complete before/after diff.
Installation
# From PyPI (preferred)
uv pip install "clickwork>=1.0,<2"
# or, pinning to a git tag if you need a ref PyPI doesn't expose
uv pip install "git+https://github.com/qubitrenegade/clickwork.git@v1.0.0"
For local development alongside a consumer project:
git clone https://github.com/qubitrenegade/clickwork.git
cd your-project
uv pip install -e ../clickwork
Quick Start
1. Create your entry point
#!/usr/bin/env python3
"""my-tool: Project automation CLI."""
from pathlib import Path
from clickwork import create_cli
commands_dir = Path(__file__).resolve().parent / "commands"
cli = create_cli(name="my-tool", commands_dir=commands_dir)
if __name__ == "__main__":
cli()
2. Write a command
Drop a .py file in your commands/ directory. Export a Click command or
group as cli:
# commands/deploy.py
import click
from clickwork import pass_cli_context, CliContext
@click.command()
@click.argument("target")
@pass_cli_context
def deploy(ctx: CliContext, target: str):
"""Deploy a component to the active environment."""
ctx.require("wrangler")
account_id = ctx.config.get("cloudflare.account_id")
ctx.run(["wrangler", "deploy", "--account-id", account_id])
cli = deploy
3. Run it
# Dev mode (directory scanning)
python tools/my-tool.py deploy site
# With flags
python tools/my-tool.py --env staging --dry-run deploy site
# Help
python tools/my-tool.py --help
python tools/my-tool.py deploy --help
See the sample plugin for a complete working example with subcommand groups.
Documentation
- Guide -- Step-by-step tutorial: building a CLI, adding config, using subprocess helpers, distributing as a package, testing your commands.
- Plugins -- 15-minute walkthrough for shipping
a pip-installable plugin via the
clickwork.commandsentry-point group. - Architecture -- Design decisions, module responsibilities, security model, and the reasoning behind non-obvious choices.
- Security -- What clickwork defends against, what it leaves to the CLI author, threat model assumptions, and how to report vulnerabilities.
- Migrating 0.2.x to 1.0 -- Breaking changes, new opt-in surfaces, and concrete before/after diffs for upgraders.
- API Policy -- The 1.0 public surface: which symbols are covered by SemVer, deprecation runway, supported Python and Click ranges.
- LLM Reference -- Compact, LLM-oriented
cheat sheet of the public surface with a "Common Footguns" section
(patching prereqs,
ClickExceptionrouting, CliRunner streams, secrets-in-argv,bash -crisks, etc.). Useful whether you're an AI agent generating clickwork code or a human skimming for gotchas.
Features
Plugin Discovery
Two mechanisms, selected via discovery_mode:
-
Directory scanning (
dev): Imports.pyfiles fromcommands_dir, registers any that export acliattribute (Click command or group). Subdirectories likelib/are skipped. Used for local development. -
Entry points (
installed): Reads theclickwork.commandsentry point group from installed packages. Used for distributed plugins. -
Auto mode (default): Uses directory scanning if
commands_direxists on disk, plus entry points from installed packages. Local commands win on name conflicts (with an info log).
Layered Config
TOML-based configuration with cascading precedence (highest wins):
- Environment variables -- explicit mappings (
CLOUDFLARE_ACCOUNT_ID) or auto-prefixed (MY_TOOL_BUCKET) - Env-specific section --
[env.staging]in repo config, selected via--envflag or{PROJECT_NAME}_ENVenv var - Default section --
[default]in repo config (.my-tool.toml) - User-level config --
~/.config/my-tool/config.toml
# .my-tool.toml
[default]
r2.bucket = "releases-staging"
[env.production]
r2.bucket = "releases-prod"
cloudflare.account_id = "prod-xyz"
Optional schema validation with required keys, defaults, type checking, and secret-in-repo-config rejection:
CONFIG_SCHEMA = {
"cloudflare.account_id": {
"required": True,
"env": "CLOUDFLARE_ACCOUNT_ID",
},
"api_token": {
"secret": True, # Rejected if found in repo config
"env": "MY_TOOL_API_TOKEN",
},
}
cli = create_cli(name="my-tool", commands_dir=..., config_schema=CONFIG_SCHEMA)
Subprocess Helpers
Commands get ctx.run(), ctx.capture(), and ctx.run_with_confirm():
# Mutating command -- respects --dry-run
ctx.run(["wrangler", "deploy"])
# Read-only -- always executes, even in dry-run
output = ctx.capture(["git", "rev-parse", "HEAD"])
# Destructive -- prompts for confirmation first
ctx.run_with_confirm(["rm", "-rf", "dist/"], "Delete build artifacts?")
- All helpers accept argv lists only (never strings) to prevent shell injection
run()streams output in real-time, raisesCliProcessErroron failurecapture()returns stripped stdout, always executes (read-only convention)- Secrets passed via
env=parameter, not argv (visible inps) - SIGINT forwarded to child processes before propagating
Global Flags
Every CLI built with create_cli() gets these flags automatically:
| Flag | Description |
|---|---|
--verbose / -v |
Increase log verbosity (-vv for debug) |
--quiet / -q |
Suppress non-error output (mutually exclusive with -v) |
--dry-run |
Preview actions without executing |
--env |
Select config environment (e.g., --env staging) |
--yes / -y |
Skip confirmation prompts (for CI) |
Typed Context
CliContext is a dataclass passed to every command via @pass_cli_context.
It holds config, flags, logger, and convenience methods:
@click.command()
@pass_cli_context
def my_command(ctx: CliContext):
ctx.config.get("some.key") # Resolved config value
ctx.env # Selected environment
ctx.dry_run # True if --dry-run
ctx.run(["echo", "hello"]) # Subprocess helper
ctx.require("docker") # Prerequisite check
ctx.confirm("Continue?") # TTY-aware prompt
Secret Safety
Secretwrapper type redacts values instr(),repr(),f-strings,vars(), and pickle- User config files checked for owner-only permissions (0o600)
- Keys tagged
secret: Truein schema are rejected if found in repo config - Subprocess secrets passed via env vars, not argv
Prerequisite Checking
ctx.require("docker") # Is it on PATH?
ctx.require("gh", authenticated=True) # Is it on PATH AND authenticated?
Known auth checks are built in for gh, gcloud, and aws. Extensible via
clickwork.prereqs.AUTH_CHECKS.
Architecture
your-project/
tools/
my-tool.py # Entry point: create_cli(name="my-tool", commands_dir=...)
commands/
deploy.py # cli = click.command()(deploy_fn)
runner.py # cli = click.group()(runner_group)
lib/
helpers.py # Shared code (not auto-discovered)
.my-tool.toml # Repo-level config
The framework (clickwork) provides:
| Module | Responsibility |
|---|---|
cli.py |
create_cli() factory, global flags, context wiring |
discovery.py |
Plugin discovery (directory + entry points) |
config.py |
Layered TOML config, schema validation |
process.py |
run(), capture(), run_with_confirm() |
prereqs.py |
require() binary/auth checks |
prompts.py |
confirm(), TTY detection |
_logging.py |
Logging setup, verbosity levels |
platform.py |
Platform detection, repo root finding |
_types.py |
CliContext, Secret, CliProcessError |
Development
See CONTRIBUTING.md for the canonical local setup
(uv sync --extra dev), the four-command verification suite that
matches CI, test-writing pointers, PR conventions, and review
expectations. The section below is a quick pytest reference for
contributors who already have a venv.
# Run tests (after uv sync --extra dev)
uv run pytest tests/unit/ -v # Fast unit tests
uv run pytest tests/integration/ -v # Slower integration tests (creates venvs)
uv run pytest tests/ -v # Everything
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file clickwork-1.0.0.tar.gz.
File metadata
- Download URL: clickwork-1.0.0.tar.gz
- Upload date:
- Size: 301.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
76b83302f472db4ccc21e8a9f7dc37a4c52212af028fcbd316978d2caed554ce
|
|
| MD5 |
d0de2b704443b557c0d7965155f0a40a
|
|
| BLAKE2b-256 |
86b0cc7268df715bd7602194aba00a91a458ce8091a95008827e964b844b7bbb
|
Provenance
The following attestation bundles were made for clickwork-1.0.0.tar.gz:
Publisher:
publish.yml on qubitrenegade/clickwork
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
clickwork-1.0.0.tar.gz -
Subject digest:
76b83302f472db4ccc21e8a9f7dc37a4c52212af028fcbd316978d2caed554ce - Sigstore transparency entry: 1340058708
- Sigstore integration time:
-
Permalink:
qubitrenegade/clickwork@946e49497b8afbbf7caa125a39be138689a33ea2 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/qubitrenegade
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@946e49497b8afbbf7caa125a39be138689a33ea2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file clickwork-1.0.0-py3-none-any.whl.
File metadata
- Download URL: clickwork-1.0.0-py3-none-any.whl
- Upload date:
- Size: 113.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f6c90303dcdf1ca361d93e44f911d2bfab3daeb10f104c95afd10117e20ff68e
|
|
| MD5 |
cafd5d42afdbe60688aa605905ecec67
|
|
| BLAKE2b-256 |
d0a27383db2bf83f7b638a818b054350433c3f94f92d5c8d9a9e6b5aa0aa9e51
|
Provenance
The following attestation bundles were made for clickwork-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on qubitrenegade/clickwork
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
clickwork-1.0.0-py3-none-any.whl -
Subject digest:
f6c90303dcdf1ca361d93e44f911d2bfab3daeb10f104c95afd10117e20ff68e - Sigstore transparency entry: 1340058711
- Sigstore integration time:
-
Permalink:
qubitrenegade/clickwork@946e49497b8afbbf7caa125a39be138689a33ea2 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/qubitrenegade
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@946e49497b8afbbf7caa125a39be138689a33ea2 -
Trigger Event:
push
-
Statement type: