Using the CLI

The CLI is the product. There is no console you have to open, no wizard that owns a step, and nothing you can do in a dashboard that you cannot do from a terminal — which is also why an agent can drive the whole kit without a single screenshot.

This page is what every command shares. What one command does is on its own page.

The shape

pithy <command> [subcommand] [args] [--flags]

Verb first, noun second, and the binary is always pithy:

pithy init
pithy add auth
pithy worker add admin-api
pithy migrate --env staging
pithy deploy --env prod

Flags every command honors

FlagMeaning
--jsonOne line, one object, on stdout. Also suppresses every prompt
--help, -hHelp for any command
--version, -vThe installed version, from anywhere before a -- separator

Long flags are kebab-case, short flags are a single letter, booleans default to false, and a value passes with either = or a space — --env=prod and --env prod are the same thing.

--version works after a subcommand as well as before one. That is deliberate: citty, the framework underneath, answers its own version builtin only when it is the sole argument, so pithy add --version would have run add instead. The rule lives in one place and applies everywhere.

--env, and why it is short

--env takes dev, staging, or prod. It defaults to dev, because every command is safe there. It is validated at the flag, before anything loads.

It is not production. The environment sits verbatim in the middle of every Cloudflare name the project composes — <project>-<env>-<thing> — so each of its characters costs one character of project name, one for one. --env production is answered with an error naming prod.

A custom environment is allowed: live, eu-prod. It is held to the same character set and to a hard maximum of seven characters, the length of staging. Every project-name budget is derived against that seven, and a provisioned project cannot be renamed, so a longer environment is refused rather than quietly shrinking a cap other projects were already accepted under.

Which environments a project has is declared once, as environments in the root pithy.config.ts — ["staging", "prod"] unless it says otherwise. A command that deploys or provisions refuses an --env the project does not declare, and names the ones it does. dev is never declared, because it is local and always there.

--worker, and when you need it

Every Worker lives in apps/<name>/ and owns its own config. There is no root Worker.

  • Commands that wire one Worker take --worker <name>: add, remove. In a single-Worker project the flag is optional. In a project with several, the CLI prompts at a terminal and fails with an actionable error under --json rather than guessing — wiring a capability into the wrong Worker puts its bindings and its Durable Object class migrations on the wrong script.
  • Commands that operate on the project fan out over every Worker and accept --worker to narrow: migrate, seed, upgrade, doctor, env.

Two Workers share a resource by declaring the same binding name. Two Workers that both declare DB are backed by one D1; a Worker that wants its own declares COLLAB_DB.

One command refuses --json

pithy remove is destructive, so it is manual and interactive only. Passing --json fast-fails with a clear error before anything changes, and its --drop confirmations are typed at a real terminal. There is no headless path, on purpose.

Automated teardown of an ephemeral environment is a different command — the pithy feature lifecycle — rather than a flag on this one.

The p. shortcut

Two characters, ending in the brand mark:

pithy alias

p. init, p. add auth, p. deploy. It writes a delimited block into your shell’s rc file, detected from $SHELL and the platform, and it is idempotent — a second run reports Already pithy. and changes nothing. --status reports it, --remove deletes exactly the block that was added.

pithy init offers it once, at the end, and only to a human at a terminal.

What output looks like

Work in progress carries a status arrow. Completion is one word. Errors lead with the problem and follow with the action.

$ pithy add auth
▸ Installing @pithy-sh/auth...
▸ Updating wrangler.jsonc...

Done.

There is no emoji, no celebration, and no successfully completed in 2.3s. A genuinely long command appends the timing minimally — Done. (3.2s) — and nothing else does.

An error is two lines:

Couldn't connect to Cloudflare API.
Check your CLOUDFLARE_API_TOKEN and try again.

No stack traces reach you; a stack trace means a CLI bug rather than a project problem, and those keep theirs. Multi-row output is whitespace-aligned columns with no borders — no ASCII art, no Unicode box drawing. The whitespace is the layout.

Color is decided once, for the whole program, and respects NO_COLOR, a non-TTY stdout, and CI. Piping pithy --help into something gets you plain text, including from the parts of the help that citty renders rather than Pithy.

Prompts, and what they can answer for you

A prompt shows its default in brackets — [Y/n] means default yes, [y/N] means default no.

A prompt that can be answered from your account, is. pithy init and pithy worker add ask where a Worker will answer, per environment, and offer the account’s real Cloudflare zones rather than a free-text field. A typo fails at init with a list of what exists, instead of at deploy with a Cloudflare error to decode. Where the account cannot be reached — no credentials, a token without Zone:Read, an offline laptop — the prompt says so in one line and falls back to free text. Scaffolding has never required the network.

Every one of those is skippable, and a non-interactive run asks nothing at all.

The update notifier

Pithy checks for a new CLI version in the background and never updates anything by itself.

  • At most once every 24 hours, cached in the config directory’s state.json.
  • Off-thread, after the command’s real work has finished. It never blocks anything.
  • Written to stderr, after Done., and only when stderr is a TTY — so it never pollutes a pipe or a CI log.
  • Only for a minor or major release. A patch is suppressed unless the release is flagged as a security fix, because patch noise trains people to ignore the surface entirely.
  • A failed check silently keeps the cached value. An update check must never break the CLI.

Turn it off with PITHY_NO_UPDATE_NOTIFIER=1, or persistently with pithy doctor --disable-notifier. pithy doctor --enable-notifier puts it back.

Every command

CommandWhat it does
pithy initScaffold a new project and its first Worker
pithy addInstall a capability into one Worker
pithy removeUnwire a capability. Manual only
pithy devRun the whole local environment under one supervisor
pithy migrateRun each Worker’s migration registry against an environment
pithy seedLoad fixtures from your own Zod schemas
pithy provisionCreate an environment’s Cloudflare resources and migrate
pithy deployShip every Worker, then prove which version answered
pithy envReport environments, bindings, ids and dashboard links
pithy workerAdd, list, remove, rename and sync the Workers under apps/
pithy uiScaffold a front end into a Worker and wire it end to end
pithy secretsCreate, rotate, edit and provision the secret registry
pithy tokenMint, list and revoke scoped Cloudflare API tokens
pithy doctorCheck the toolchain, the project, and for a new version
pithy upgradeReconcile package-served capabilities with their manifests
pithy aliasInstall or remove the p. shortcut
pithy featureFeature environment lifecycle: create, sync, destroy
pithy dashboardRegister, rotate and revoke a management client’s access
pithy emailProvision mail, send test messages, inspect the job log
pithy mediaProvision and manage the media backends
pithy storageProvision and manage the R2 bucket behind storage
pithy vectorProvision Vectorize indexes and reprocess embeddings
pithy paymentsProvision and inspect the payment rails
pithy supportProvision and manage the support inbox
pithy testersCohorts, invitations and tester rosters
pithy turnstileProvision the humanity check

Every page carries the same six sections — synopsis, flags, what it does, --json, errors, examples — so a command’s contract is specified in one place and specified completely. A test in the kit holds each page to naming every key its command’s --json payload emits, which is why the reference does not drift from the code.

ESC