Skip to main content

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: Pre-1.0 (0.x). API is unstable. All features are driven by real orbit-admin needs -- no speculative abstractions.

Installation

# From PyPI (preferred)
uv pip install "clickwork==0.2.0"
# 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@v0.2.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.
  • Architecture -- Design decisions, module responsibilities, security model, and the reasoning behind non-obvious choices.

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

git clone https://github.com/qubitrenegade/clickwork.git
cd clickwork
uv venv && uv pip install -e ".[dev]"

# Run tests
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-0.2.0.tar.gz (177.4 kB view details)

Uploaded Source

Built Distribution

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

clickwork-0.2.0-py3-none-any.whl (87.4 kB view details)

Uploaded Python 3

File details

Details for the file clickwork-0.2.0.tar.gz.

File metadata

  • Download URL: clickwork-0.2.0.tar.gz
  • Upload date:
  • Size: 177.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for clickwork-0.2.0.tar.gz
Algorithm Hash digest
SHA256 ecba7df305d501e167f6a89e0d8e513baf226de08176a5c105a9e40601c49f18
MD5 ac59dd6b81c00466a68b14453eda68f4
BLAKE2b-256 c6183492a1685ed753a1cd0b77b159477bccef25beee4490012cc4af6210aa3d

See more details on using hashes here.

Provenance

The following attestation bundles were made for clickwork-0.2.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-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: clickwork-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 87.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for clickwork-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ab3e3ef39ffddaf5308bd70e7d7fdfdffe97a66bbf5425cc9f6fd5af6bada776
MD5 6f5a26497df85b9f11fe28e737d3858d
BLAKE2b-256 87bdc91c7fa4a10b8186687b0802442d148caa4f337ae8c79437c7aaaa1c98c9

See more details on using hashes here.

Provenance

The following attestation bundles were made for clickwork-0.2.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