--- name: deploy description: >- Run claude-deploy via the `deployctl` CLI. Use for `/deploy` and its arguments (login, staging, production, activate, deactivate, status , logout, domains, env), or when the user asks to build/package the local project (Nuxt or a plain static site), upload a build artifact, serve it, or set a runtime env var / secret for a deployed site. Flow: browser device-code login, a local build, a private artifact upload, and optional activation (serving the built site behind a hostname on the control-plane host). Environments: staging and production, each keeping its own live deployment. Installs the `deployctl` CLI itself on first use if it's missing — no separate setup step needed beyond Node.js + npm. allowed-tools: - Bash(deployctl:*) - Bash(command -v npm) - Bash(command -v deployctl) - Bash(npm install -g https://deployctl.node.systems/deployctl.tgz) - Bash(node ./packages/deployctl/dist/index.js:*) - Bash(pnpm --filter deployctl build) - Read --- # /deploy — claude-deploy This skill drives `deployctl`. **No separate CLI install is required** — if `deployctl` is missing, step 1 installs it itself (the only prerequisite is Node.js + npm already on the machine; this skill cannot install those). It never handles secrets itself. ## Supported invocations | Command | Action | | --- | --- | | `/deploy login` | Browser device-code login; store a scoped API token + control-plane URL. | | `/deploy` (no args) | Zero-config: detect the project (Nuxt, or a plain static site — hand-written HTML or a pre-built `dist/`), create the project from the folder name if new, build/package, upload, **and serve it** on an auto-assigned URL. | | `/deploy staging` \| `/deploy production` | Same, for that environment (each gets its own URL). | | `/deploy … --no-activate` | Upload only, don't serve. | | `/deploy … --host ` | Serve on `` instead of an auto URL. | | `/deploy projects create ` | Create an empty project explicitly. | | `/deploy activate [--host ]` | Serve an already-uploaded deployment (on the project's domains, or add ``). | | `/deploy deactivate ` | Stop serving a deployment. | | `/deploy status ` | Show a deployment's status (and live URL, if active). | | `/deploy projects` | List the user's projects. | | `/deploy domains` | List the current/target project's custom domains. | | `/deploy domains add [--env staging\|production] [--primary]` | Attach a domain. | | `/deploy domains remove ` | Detach a domain. | | `/deploy env` | List the current/target project's env var **names** (never values). | | `/deploy env set [--env staging\|production]` | Set a runtime secret; applies live if a server-mode site is active. | | `/deploy env remove [--env staging\|production]` | Remove a runtime secret. | | `/deploy logout` | Remove the locally stored API token. | Global: `--project ` targets a project other than the one in `claude-deploy.json`. Environments: **`staging`** and **`production`** (alias `prod`). They are independent — deploying/activating one never touches the other's live site. A project keeps at most one live deployment *per environment*. ## Hard rules (do not break these) - **Never** ask the user for a Supabase password, an API token, or the service-role key. Login happens entirely in the browser against self-hosted Supabase. - **Never** read tokens from the macOS Keychain or from `~/.config/claude-deploy/credentials.json`, and never print their contents. - **Never** echo secret environment variables (`SUPABASE_SERVICE_ROLE_KEY`, `CONTROL_PLANE_TOKEN_PEPPER`, `NUXT_PUBLIC_SUPABASE_ANON_KEY`) or the contents of `.env`. - Use the environment the user names (`staging` or `production`). If they don't say, default to `staging` and note it. Never deploy `production` unless they said so. - Status wording: for an **`uploaded`** deployment say *"Artifact uploaded; not activated."* Only after `deployctl` itself reports status **`active`** with a URL may you say the site is live — and then quote the exact URL it printed. Never assume activation. - Activation runs the user's built code on the control-plane host. Only activate when the user explicitly asks (`--activate`, or `/deploy activate`). Never activate as a side effect. - **Env vars (secrets):** never invent, guess, or infer a value. Only set one when the user gives an explicit `KEY=value` or names a local file to read it from. Never read a project's `.env` and upload its contents unasked. Never print a value back — only confirm the key name was set/removed (this is exactly what `deployctl env` itself does). - On any error, stop and summarize the **actual** error message from `deployctl`. Do not retry blindly. ## Procedure 1. **Locate the CLI.** - First, `command -v npm`. **Missing → stop here** and tell the user plainly: *"deployctl needs Node.js (npm ships with it) — none found on this machine. Install the current LTS from https://nodejs.org (or via nvm: https://github.com/nvm-sh/nvm), then ask me to `/deploy` again."* Never try to install Node.js yourself — that's a system change only the user should make. - Then `command -v deployctl`. - If present, use it. - If missing and this is the claude-deploy monorepo, `pnpm --filter deployctl build` and call `node ./packages/deployctl/dist/index.js `. - Otherwise install it **yourself, silently** — this is why that exact command is allow-listed: `npm install -g https://deployctl.node.systems/deployctl.tgz`. Do this the first time `deployctl` is needed; don't ask the user to run it, and don't point them at `install.sh` unless this install fails (then report the **actual** npm error — it usually means no writable global npm prefix, e.g. no nvm — and suggest they fix npm permissions or run the installer themselves). From here on `deployctl` means whichever works. 2. **`/deploy login`:** - Run `deployctl whoami`. If it reports an authenticated user with a non-expired token, report that and stop. - Otherwise run `deployctl login --no-browser` **in the background** and read the `verificationUrl` / code from its output. - **Open that URL in the Claude Code browser pane** (`preview_start` / `navigate`). The user completes the login *there*: on `/device` they can **Sign in** or **Create account** (self-hosted Supabase registration), then click **Allow access**. Do NOT type the user's password yourself — let them do it in the pane. - If no browser pane is available, print the URL + code and let the user open it in their own browser. - Keep polling the background `deployctl login`; when it prints "Login successful", run `deployctl whoami` and report user id, email, token prefix, scopes and expiry (never the token itself). 3. **`/deploy`** (optionally `staging` / `production`): - `cd` into the project directory the user means (ask if unclear). - Run `deployctl whoami`; if not authenticated, run the login procedure first. - Run **`deployctl deploy [environment]`** — it needs no `claude-deploy.json` (it detects Nuxt + package manager, names the project after the folder or `--project`, and writes a minimal `claude-deploy.json` the first time). It builds, uploads, and — unless `--no-activate` — serves the site, minting an auto URL when no host is set. `production` still requires the explicit word. - Report what `deployctl` printed: the project (created or existing), the build mode + size, and **the exact `✓ Live at `** line. Only say the site is live if that line appeared. - If the build aborts on a forbidden file (`.env`, `*.key`, …), report the offending **paths** (never their contents) and stop. 4. **`/deploy activate --host `:** run it, then report the status `deployctl` returns. If it says `active`, quote the URL. If it says `activating` (not healthy yet), say so and suggest `deployctl status `. 5. **`/deploy deactivate `** / **`/deploy status `** / **`/deploy logout`:** run and report verbatim. ## Domains (conversational) When the user says "put domain X on project Y" (or similar): 1. `deployctl projects` to confirm the project slug exists. 2. `deployctl domains add --project --env ` (ask which env if unclear; `--primary` if they want it as the main URL). 3. If that environment already has a live deployment, the domain goes live once Caddy gets a certificate — tell the user the DNS for `` must point at the control plane host or the cert request will fail. 4. `deployctl domains --project ` to show the resulting set. Removing: `deployctl domains remove --project `. ## Secrets (conversational) Runtime env vars for the **deployed server process** (a DB URL, an API key the app reads via `runtimeConfig` at request time) — different from build-time `NUXT_PUBLIC_*` vars, which come from the project's own local `.env` at build time and need no special handling here. Claude cannot relay a hidden/interactive prompt through this chat — there's no terminal for the user to type into. So a value has to reach you as an explicit chat message or a local file; either way it then appears as a Bash argument (briefly in this machine's process list, and in this conversation's history if pasted in chat) — say so if the user seems unaware, and prefer `--from-file` for anything long-lived: 1. If the user gave the value in chat: `deployctl env set --project --env ` (ask which env if unclear). If they named a local file instead: `deployctl env set --from-file --project --env `. Never invent a value or read one from a project's `.env` without being asked. 2. Relay exactly what `deployctl` prints: the key name and whether it applied live now or will apply on the next deploy/activate. **Never** print the value, and don't echo it back to confirm. 3. `deployctl env --project ` to show the resulting key names. Removing: `deployctl env remove --project --env `. A static deployment has no runtime process — vars only matter there at the next **build**, via the project's own `.env` (case already handled by the local build step, not this command). ## Notes - **Installing this skill alone is enough.** Nothing else needs to be pre-installed — `deployctl` installs itself on first use (step 1). The one real prerequisite is Node.js + npm; if it's missing, step 1 catches that explicitly and tells the user to install it — it never surfaces as a raw "npm: command not found". - The device-code token is a separate, limited authorization layer — not a Supabase user session. - Everything runs against the `apiUrl` in `claude-deploy.json` and the self-hosted Supabase instance the control plane is configured with. - Activation requires the control plane to have `CONTROL_PLANE_ACTIVATION_ENABLED=1` and a `CONTROL_PLANE_SITE_DOMAIN`; if it is off, `deployctl activate` returns a clear "not enabled" error — report that, don't work around it. - Serve mode (server vs static) is auto-detected from the build output: Nuxt `pnpm build` → server (Node process); Nuxt `pnpm generate` or a **non-Nuxt static site** (hand-written HTML, or a `dist/`/`build/` from Vite/Astro/…) → static (files served straight from disk, **no wrapper process**). `deployctl` reports which; just relay it, don't try to force a mode. - Auto-assigned URLs are paired per project: `-