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)