Skip to main content

Reusable CLI framework for project automation

Project description

clickwork

PyPI Python Versions License: MIT

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.commands entry-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, ClickException routing, CliRunner streams, secrets-in-argv, bash -c risks, 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 .py files from commands_dir, registers any that export a cli attribute (Click command or group). Subdirectories like lib/ are skipped. Used for local development.

  • Entry points (installed): Reads the clickwork.commands entry point group from installed packages. Used for distributed plugins.

  • Auto mode (default): Uses directory scanning if commands_dir exists 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):

  1. Environment variables -- explicit mappings (CLOUDFLARE_ACCOUNT_ID) or auto-prefixed (MY_TOOL_BUCKET)
  2. Env-specific section -- [env.staging] in repo config, selected via --env flag or {PROJECT_NAME}_ENV env var
  3. Default section -- [default] in repo config (.my-tool.toml)
  4. 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, raises CliProcessError on failure
  • capture() returns stripped stdout, always executes (read-only convention)
  • Secrets passed via env= parameter, not argv (visible in ps)
  • 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

  • Secret wrapper type redacts values in str(), repr(), f-strings, vars(), and pickle
  • User config files checked for owner-only permissions (0o600)
  • Keys tagged secret: True in 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

clickwork-1.0.0.tar.gz (301.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

clickwork-1.0.0-py3-none-any.whl (113.7 kB view details)

Uploaded Python 3

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

Hashes for clickwork-1.0.0.tar.gz
Algorithm Hash digest
SHA256 76b83302f472db4ccc21e8a9f7dc37a4c52212af028fcbd316978d2caed554ce
MD5 d0de2b704443b557c0d7965155f0a40a
BLAKE2b-256 86b0cc7268df715bd7602194aba00a91a458ce8091a95008827e964b844b7bbb

See more details on using hashes here.

Provenance

The following attestation bundles were made for clickwork-1.0.0.tar.gz:

Publisher: publish.yml on qubitrenegade/clickwork

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for clickwork-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f6c90303dcdf1ca361d93e44f911d2bfab3daeb10f104c95afd10117e20ff68e
MD5 cafd5d42afdbe60688aa605905ecec67
BLAKE2b-256 d0a27383db2bf83f7b638a818b054350433c3f94f92d5c8d9a9e6b5aa0aa9e51

See more details on using hashes here.

Provenance

The following attestation bundles were made for clickwork-1.0.0-py3-none-any.whl:

Publisher: publish.yml on qubitrenegade/clickwork

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page