The Capuzzella CLI: A Complete Guide

Maurice Wipf · March 12, 2026

Capuzzella ships a CLI that wraps its HTTP API into shell commands. It's a thin client — no local state, no config files, no database connections. Every command is an authenticated HTTP request to a running Capuzzella server. This post covers setup, every command group, and the design decisions behind the tool.

Architecture

The CLI is a single file: bin/capuzzella, roughly 260 lines of JavaScript with a Bun shebang. There is no CLI framework — no Commander, no Yargs, no oclif. Argument parsing is manual: two-word commands (pages list, schedule add) are matched first, then single-word commands (publish, whoami). The remaining arguments are passed to the handler as a flat array.

Every handler calls a shared request(method, path, body) function that appends the path to the base URL, sets the Authorization header, and parses the response as JSON or text depending on Content-Type. Non-2xx responses print the error and exit with code 1.

This means the CLI has no opinion about your server's location, authentication backend, or deployment model. It works equally well against localhost:3000 during development and a production instance behind a reverse proxy. It also means you can replace the CLI entirely with curl — every command maps 1:1 to an API endpoint.

Setup

Two environment variables control the CLI:

Variable Required Default Purpose
CAPUZZELLA_API_KEY Yes Bearer token for API authentication
CAPUZZELLA_URL No http://localhost:3000 Base URL of the Capuzzella server

If CAPUZZELLA_API_KEY is not set, the CLI exits immediately with a helpful message. Trailing slashes on CAPUZZELLA_URL are stripped automatically. There is no config file, no .capuzzellarc, no capuzzella.config.js. Environment variables are the only input.

To install the CLI globally:

bun link

This makes the capuzzella command available system-wide. You can also run it locally without linking:

bun run bin/capuzzella -- <command>

Command Groups

The CLI organizes its commands into five groups: identity, pages, publishing, scheduling, and key management. Each group maps directly to a section of the REST API.

Identity

capuzzella whoami

Calls GET /api/me and prints the role, key name, key ID, and associated email for the current API key. Useful for verifying which key is active in your shell session, especially when switching between environments.

Pages

Page paths are always relative and end in .html. Nested paths use forward slashes: blog/post.html, services/consulting.html.

Command API Call Description
pages list GET /api/pages Lists all draft pages as JSON
pages get <path> GET /api/pages/<path> Prints the raw HTML of a page
pages save <path> --file <file> PUT /api/pages/<path> Reads a local file and uploads it as a page
pages delete <path> DELETE /api/pages/<path> Deletes both the draft and published copy

pages get detects whether the response contains an html field and prints raw HTML if so, otherwise falls back to pretty-printed JSON. This makes it easy to pipe page content into other tools:

capuzzella pages get index.html > backup.html

pages save requires the --file flag. It reads the file from disk using Bun's Bun.file() API, checks that the file exists, and sends the contents as the html field in a JSON body. This is a create-or-update operation — if the page path doesn't exist, it's created; if it does, it's overwritten.

Publishing

Capuzzella's content model is built on two directories: drafts/ and public/. Publishing copies a page from drafts to public. Unpublishing removes the public copy while keeping the draft intact.

Command API Call Description
publish all POST /publish/ Publishes every draft at once
publish <path> POST /publish/<path> Publishes a single page
unpublish <path> DELETE /publish/<path> Removes the published copy
status <path> GET /publish/status/<path> Shows isPublished and hasUnpublishedChanges

The status command is the one you'll reach for most often. It returns two booleans: whether the page is currently published, and whether the draft has diverged from the published version. A typical workflow looks like:

capuzzella pages save blog/post.html --file ./post.html
capuzzella status blog/post.html
capuzzella publish blog/post.html

Scheduled Publishing

Scheduled publishing lets you queue a page to go live at a specific time. The server stores schedules in SQLite and runs a background check to publish pages when their scheduled time arrives.

Command API Call Description
schedule list GET /publish/schedule?status=pending Lists pending schedules
schedule list all GET /publish/schedule Lists all schedules (including completed and cancelled)
schedule add <path> --at <datetime> POST /publish/schedule Schedules a page for future publish
schedule get <id> GET /publish/schedule/<id> Gets details of a scheduled publish
schedule cancel <id> DELETE /publish/schedule/<id> Cancels a pending schedule

The --at flag takes an ISO 8601 datetime string. Timestamps are stored as Unix epoch seconds internally, but the CLI converts them back to ISO format for display. The schedule list output shows each schedule on a single line:

capuzzella schedule add blog/launch.html --at 2026-04-01T09:00:00Z
# Scheduled #3: blog/launch.html will publish at 2026-04-01T09:00:00.000Z

capuzzella schedule list
#   #3  blog/launch.html  →  2026-04-01T09:00:00.000Z  [pending]

API Key Management

These commands require an admin-role API key. They let you manage access to the Capuzzella API without touching the server's admin UI.

Command API Call Description
keys list GET /settings/api-keys Lists all API keys (hashed, not plaintext)
keys create --name <n> --role <r> [--rate-limit <n>] POST /settings/api-keys Creates a key and prints it once
keys revoke <id> POST /settings/api-keys/<id>/revoke Revokes a key (disables without deleting)
keys delete <id> POST /settings/api-keys/<id>/delete Permanently deletes a key

When creating a key, the plaintext value (prefixed cap_) is printed exactly once. The server stores only the hash. If you lose it, you create a new one. The --role flag accepts editor, admin, or viewer. The --rate-limit flag defaults to 60 requests per minute if omitted.

capuzzella keys create --name "CI pipeline" --role editor --rate-limit 120
# API key created. Copy it now — it will not be shown again:
#   cap_abc123...

Revoking a key is a soft disable — the key record stays but stops authenticating. Deleting is permanent. Both are useful in different situations: revoke when you suspect a leak and want to investigate, delete when cleaning up old integrations.

Design Decisions

Why No CLI Framework

Commander, Yargs, and oclif are all capable tools. But for a CLI with ~20 commands and no complex option parsing, they add more weight than value. The manual two-word/single-word dispatch is 10 lines of code. Adding a framework would introduce a dependency tree, increase startup time, and make the source harder to read for a marginal gain in help text formatting.

Why a Thin Client

The CLI does no local caching, no local file tracking, no diffing. Every command is a stateless HTTP call. This means the CLI never gets out of sync with the server. There's no capuzzella pull / capuzzella push state machine to debug. You run a command, it talks to the server, you get a result.

The trade-off is obvious: you need a running server. But since Capuzzella is designed to run as a persistent service (not a build tool you invoke occasionally), this is the right trade-off. The server is always there.

Why Environment Variables Over Config Files

Config files are convenient for complex setups. But for two values — a URL and a key — environment variables are simpler, more portable, and more secure. They work in shell scripts, CI pipelines, Docker containers, and .env files without any parsing logic. They don't risk being committed to version control (unlike a .capuzzellarc with an API key in it).

Scripting and CI Integration

Because every command returns structured JSON and exits with code 0 on success or 1 on failure, the CLI slots into shell scripts and CI pipelines naturally:

#!/bin/bash
set -e

# Deploy all HTML files from a build directory
for file in dist/*.html; do
  page=$(basename "$file")
  capuzzella pages save "$page" --file "$file"
  capuzzella publish "$page"
  echo "Published $page"
done

In a GitHub Actions workflow, you'd set CAPUZZELLA_API_KEY and CAPUZZELLA_URL as repository secrets, install Bun, link the CLI, and run your deployment script. The CLI's stateless design means there's no setup step beyond exporting two environment variables.

Full Command Reference

Run capuzzella --help (or capuzzella with no arguments) to print the built-in reference:

Capuzzella CLI

Usage: capuzzella <command> [options]

Commands:
  whoami                                  Show current key role and identity

  pages list                              List all pages
  pages get <path>                        Get page HTML
  pages save <path> --file <file>         Save page from local file
  pages delete <path>                     Delete a page

  publish all                             Publish all drafts
  publish <path>                          Publish a single page
  unpublish <path>                        Unpublish a page
  status <path>                           Check publish status

  schedule list                           List pending scheduled publishes
  schedule list all                       List all scheduled publishes
  schedule add <path> --at <datetime>     Schedule a page for future publish
  schedule get <id>                       Get details of a scheduled publish
  schedule cancel <id>                    Cancel a pending scheduled publish

  keys list                               List API keys
  keys create --name <n> --role <r>       Create a new API key
  keys revoke <id>                        Revoke an API key
  keys delete <id>                        Delete an API key

Environment:
  CAPUZZELLA_API_KEY    API key (required)
  CAPUZZELLA_URL        Server URL (default: http://localhost:3000)