diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 224b7ac..69c2910 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -5,14 +5,14 @@ }, "metadata": { "description": "Deploy and manage apps on Deploio (nine.ch PaaS) — skills for deployment, management, debugging, backing services, and CI/CD.", - "version": "1.1.0" + "version": "1.5.0" }, "plugins": [ { "name": "deploio", "source": "./", "description": "Five skills for deploying and managing Deploio apps: first-time deploy, day-to-day management, debugging, backing service provisioning, and CI/CD pipeline setup.", - "version": "1.1.0", + "version": "1.5.0", "license": "MIT", "author": { "name": "renuo.ch" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index e2d34d6..a6d2d14 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "deploio", - "version": "1.1.1", + "version": "1.5.0", "description": "Deploy and manage apps on Deploio (nine.ch PaaS) using nctl. Five skills covering first-time deployment, day-to-day management, debugging, backing service provisioning (PostgreSQL, Redis, S3), and CI/CD setup.", "license": "MIT", "author": { diff --git a/CHANGELOG.md b/CHANGELOG.md index 144014f..624ad37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Version --- +## [1.5.0] — 2026-04-29 + +### Added +- **`auth_stale` blocker** — gather-context distinguishes a stale JWT (expired/corrupt session token) from never-authenticated and emits a verbatim friendly user-facing message telling them to run `nctl auth login`. New `nctl auth errors` section in `shared/troubleshooting.md`. +- **`SubagentStart` nctl version probe** — when the deploio-cli agent is spawned (i.e. just before any nctl call), a hook checks that nctl is installed and meets the plugin's required version (1.16.0). If missing or outdated, the user sees an install/upgrade advisory before any work begins. +- **`RELEASING.md` checklist** — documents the four files that must move together on release (plugin.json, marketplace.json with its two version fields, changed SKILL.md metadata blocks, CHANGELOG.md), with a verification shell block to catch drift before commit. + +### Changed +- **Project scoping** — every nctl call from executor and monitor agents now carries `--project=`. The agent no longer runs `nctl auth set-project`, which mutated the user's global kubeconfig and silently broke other concurrent shells using the same nctl config. The executor spec drops the legacy `project_suffix` field and the standalone `org` field — `--project=` is authoritative. +- **Destructive-command guard now blocks every `nctl auth` subcommand except `whoami`.** The skills prompt the user to run `nctl auth login` / `nctl auth set-org` themselves, with an upfront warning that those commands disrupt any other concurrent session sharing the same nctl config. Both install paths use the same shell-based hard block (the prompt-based variant `hooks/guard-destructive.md` was removed). +- **Hook wiring converged on `hooks/hooks.json` as the single source of truth.** Both hooks (destructive-guard, nctl-probe) declared in one place. Marketplace installs read it directly; flat installs merge the substituted block into `settings.json` via jq, preserving any user-owned hooks. Agent frontmatter no longer carries an inline `hooks:` block. +- **Hook path resolution** uses `${CLAUDE_PLUGIN_ROOT}/hooks/...` in source. Marketplace installs resolve the env var; flat installs substitute the absolute path so global installs no longer lose the guard outside the source repo. +- **`nctl --version`** (works on old and new nctl) replaces `nctl version` (fails on releases below 1.12) in gather-context. + +### Fixed +- `install.sh` exit code on local source — the cleanup trap returned 1 under `set -e` when `LOCAL_SRC` was set, breaking CI and scripted installs even on success. +- `install.sh` jq merge is genuinely idempotent and surfaces failures: temp files cleaned up via trap, jq errors abort the install rather than silently leaving the file unchanged with a misleading success message. + +--- + ## [1.1.0] — 2026-03-23 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 6bb38ec..e9a7761 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,4 +54,4 @@ Nine docs: [docs.nine.ch](https://docs.nine.ch) ## Versioning -Bump `"version"` in `.claude-plugin/plugin.json` and push to `main` — the GitHub Actions workflow tags and releases automatically. +See `RELEASING.md` at the repo root for the full checklist. Four files move together: `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json` (two places — `metadata.version` and `plugins[0].version`), each changed skill's `SKILL.md` `metadata.version`, and a new `CHANGELOG.md` entry. Pushing to `main` after the bump tags + releases automatically. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fad727a..d2919e9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,9 +17,7 @@ Cross-skill troubleshooting patterns go in `skills/shared/troubleshooting.md`. ## Releasing a new version -Bump `"version"` in `.claude-plugin/plugin.json`, commit, and push to `main`. The GitHub Actions workflow creates the git tag and GitHub release automatically. - -Version format: `MAJOR.MINOR.PATCH` following [semver](https://semver.org). +See [RELEASING.md](RELEASING.md) for the full checklist. Short version: four files (`plugin.json`, `marketplace.json`, the changed `SKILL.md`s, and `CHANGELOG.md`) move together; pushing to `main` triggers the autorelease. ## Reporting issues diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..f3a4788 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,92 @@ +# Releasing + +Cutting a release means moving four files in lockstep, then pushing to `main`. The GitHub Actions workflow at `.github/workflows/` watches `.claude-plugin/plugin.json` and tags + releases automatically once it lands on `main` — but only the *plugin* version. Everything else (marketplace listing, changelog, per-skill metadata) is your responsibility. + +This document is the checklist. Run through it top-to-bottom every time. + +## When to release + +- Bug fix, security patch, doc-only change → **PATCH** (`1.2.0` → `1.2.1`) +- New skill, new hook, new command, new feature, additive behavior change → **MINOR** (`1.2.1` → `1.3.0`) +- Removed / renamed skill, breaking change to an executor agent's task spec, hook event surface change that breaks downstream automation → **MAJOR** (`1.x.y` → `2.0.0`) + +The agent ↔ skill executor spec (e.g. `task: deploy` fields) is an internal contract — bump MINOR when its shape changes, since the plugin still works end-to-end. Reserve MAJOR for things a *user* would feel. + +## All versions move together + +The plugin, marketplace metadata, marketplace plugin entry, and every skill share the **same version number**. On every release, bump all of them — even skills whose contents didn't change in this cycle. + +This is a deliberate policy. Per-skill versions used to be decoupled from the plugin (each bumped only when that skill's contents changed), but the seven version numbers across the repo could end up several minor releases apart, making it impossible to tell at a glance which release a user was on without reading every metadata block. Aligned versioning trades skill-level history fidelity for one obvious answer to "which release is this". + +If the plugin is ever split into separately-installable units, decoupling will need to come back. Until then: one number, everywhere. + +## The four files + +| File | What to update | +|---|---| +| `.claude-plugin/plugin.json` | `"version"` (one occurrence) | +| `.claude-plugin/marketplace.json` | `"version"` in **two** places: `metadata.version` and `plugins[0].version` — both must match | +| `skills//SKILL.md` | `metadata.version` in **every skill** — all five move together with the plugin | +| `CHANGELOG.md` | New entry at the top (under the header), dated today, with `### Added` / `### Changed` / `### Fixed` sections matching what's in the diff | + +## Step-by-step + +1. **Decide the version.** Read the diff since the last tag (`git log $(git describe --tags --abbrev=0)..HEAD --oneline`) and pick PATCH / MINOR / MAJOR per the table above. Pick the next number from the current `plugin.json` value, not from `marketplace.json` (the source of truth is `plugin.json` since the autorelease watches it). + +2. **Edit the four files** in the order they're listed above. For each, save and check the change is the *only* version-string change in that file (`git diff `). + +3. **Run the verification block** (below). It checks the three plugin-level versions agree and that CHANGELOG names the new version. Fix anything it reports before continuing. + +4. **Run the project's existing tests / installs** if the changes touched anything user-facing. At minimum: + - `bash -n install.sh && bash -n uninstall.sh` (syntax check the install scripts) + - A round-trip flat install into a scratch dir followed by uninstall, to confirm the install scripts still work + - For hook changes, a manual smoke test of each hook script (`echo '{"tool_input":{...}}' | bash hooks/.sh`) + +5. **Commit** with subject `chore(release): ` and a body that mirrors the CHANGELOG entry (or just summarizes it). + +6. **Push to `main`.** The GitHub Actions workflow tags and creates the GitHub release. Watch the run pass before considering the release shipped. + +## Verification block + +Run this from the repo root before committing — it surfaces drift between the four files: + +```bash +PV=$(jq -r '.version' .claude-plugin/plugin.json) +MM=$(jq -r '.metadata.version' .claude-plugin/marketplace.json) +MP=$(jq -r '.plugins[0].version' .claude-plugin/marketplace.json) +CL=$(grep -m1 -oE '\[[0-9]+\.[0-9]+\.[0-9]+\]' CHANGELOG.md | tr -d '[]') + +printf "plugin.json: %s\n" "$PV" +printf "marketplace.json metadata: %s\n" "$MM" +printf "marketplace.json plugins: %s\n" "$MP" +printf "CHANGELOG.md (top entry): %s\n" "$CL" + +OK=true +[ "$PV" = "$MM" ] && [ "$PV" = "$MP" ] && [ "$PV" = "$CL" ] || OK=false + +echo +echo "Skill versions:" +for f in skills/*/SKILL.md; do + v=$(awk '/^metadata:/{m=1; next} m && /^ version:/{print $2; exit}' "$f") + marker=""; [ "$v" = "$PV" ] || { marker=" (mismatch)"; OK=false; } + printf " %-32s %s%s\n" "$(basename "$(dirname "$f")")" "$v" "$marker" +done + +echo +if $OK; then + echo "OK — all versions agree on $PV" +else + echo "DRIFT — fix before committing" + exit 1 +fi +``` + +## After push + +- Check the GitHub Actions run on the merge commit. If it fails, the tag isn't created — fix forward. +- Confirm the new tag exists: `git fetch --tags && git tag -l ''`. +- The marketplace registry picks up the new `marketplace.json` on its next sync. There's no separate publish step. + +## If you need to roll back + +Don't delete the tag — push a follow-up release with the fix. Tags are immutable contracts; deleting one breaks anyone who already installed at that version. diff --git a/agents/deploio-cli.md b/agents/deploio-cli.md index 156dd82..d409808 100644 --- a/agents/deploio-cli.md +++ b/agents/deploio-cli.md @@ -5,12 +5,6 @@ model: inherit permissionMode: bypassPermissions color: blue tools: Bash, Read -hooks: - PreToolUse: - - matcher: "Bash" - hooks: - - type: command - command: ".claude/hooks/deploio-guard-destructive.sh" --- You are a Deploio CLI expert. You execute nctl commands precisely, handle errors gracefully, and report clean status summaries back to the coordinator. @@ -19,9 +13,9 @@ You are a Deploio CLI expert. You execute nctl commands precisely, handle errors --- -## Project naming rule +## Project scoping rule -Project is always `-` (e.g. `renuotest` + `mcp-server` → `renuotest-mcp-server`). Before any `--project=`, verify `` has the org prefix; if not, prepend the active org from `nctl auth whoami`. +The coordinator passes the full project name (`-`) in the task spec. **Pass `--project=` on every nctl command that takes it.** Never run `nctl auth set-project` or any other `nctl auth` subcommand (only `nctl auth whoami` is allowed, and it's read-only): they mutate the user's global nctl state and silently break concurrent shells. If you receive a project name without an org prefix, that's a coordinator bug — report it and stop, do not guess. --- @@ -31,15 +25,14 @@ Memorise these. Never guess or substitute: | Intent | Correct command | |---|---| -| Create app | `nctl create app ` | -| Inspect app | `nctl get app ` | -| Update app | `nctl update app ` | -| Poll logs (agent use) | `nctl logs app --type --since 10s` — do NOT use `-f` in agents (blocks forever) | -| Check status | `nctl get app ` or `nctl get app -o yaml` — do NOT use `--watch` in agents (blocks forever) | -| Check releases | `nctl get releases` — shows all releases with their STATUS column | -| Create project | `nctl create project -` | -| Set active project | `nctl auth set-project -` | -| Current identity | `nctl auth whoami` | +| Create app | `nctl create app --project=` | +| Inspect app | `nctl get app --project=` | +| Update app | `nctl update app --project=` | +| Poll logs (agent use) | `nctl logs app --project= --type --since 10s` — do NOT use `-f` in agents (blocks forever) | +| Check status | `nctl get app --project=` or `... -o yaml` — do NOT use `--watch` in agents (blocks forever) | +| Check releases | `nctl get releases --project=` — shows all releases with their STATUS column | +| Create project | `nctl create project ` | +| Current identity | `nctl auth whoami` (read-only — the only `nctl auth` subcommand permitted) | --- @@ -53,10 +46,18 @@ task: gather-context Run these immediately in parallel — first action, no preamble: ```bash nctl --version 2>&1 +nctl auth whoami 2>&1 git remote -v 2>&1 git branch --show-current 2>&1 ``` +Interpret `nctl auth whoami` output: +- Success → parse `active_org` (the entry marked `*`) and `available_orgs` (the full list). +- Output contains `failed to parse JWT token` (or a similar token-parse error) → set `blockers: ["auth_stale"]`. The session token is expired or corrupt; only the user can fix it by running `nctl auth login` in their own terminal. +- Generic auth failure with no JWT mention (e.g. never logged in) → set `blockers: ["nctl_not_authenticated"]`. + +Never run `nctl auth login` from the agent — it's blocked by the destructive-guard hook, and even if it weren't, it would alter the user's global session. + Also read the project root to detect app type: | File | App type | Default port | @@ -71,11 +72,14 @@ Also read the project root to detect app type: Report back to coordinator: ``` nctl_installed: true | false +nctl_version: +active_org: +available_orgs: [, ...] remote_url: branch: app_type: Rails | Node.js | Python | PHP | Go | Docker | unknown port: -blockers: [nctl-missing | no-remote | ...] +blockers: [nctl_missing | nctl_outdated | nctl_not_authenticated | auth_stale | no_remote | ...] ``` --- @@ -85,7 +89,8 @@ blockers: [nctl-missing | no-remote | ...] Spec: ``` task: deploy -project_suffix: +org: # for reporting only — never run nctl auth set-org +project: # full - string — pass to every --project flag app: git_remote: branch: @@ -95,14 +100,14 @@ env_vars: KEY=VALUE, ... (optional) deploy_job: "" (optional) ``` -### Step 0: Verify auth and resolve org — run this first, right now +### Step 0: Verify auth — run this first, right now ```bash nctl auth whoami ``` -- If it fails: run `nctl auth login` (opens browser), then re-run. -- Extract the active org. Full project name = `-`. +- If it fails or output contains "failed to parse JWT token", report back to the coordinator with `blocker: auth_stale` so the user can run `nctl auth login` themselves. Do not run `nctl auth login` from the agent — it's blocked by the destructive-command guard and would mutate the user's global session anyway. +- Confirm the active org from the output matches the `org` field in the spec. If it doesn't match, stop and report a mismatch to the coordinator — do not run `nctl auth set-org`. ### Step 1: Resolve git credentials @@ -117,22 +122,22 @@ Deploio pulls code from the git remote — it never receives code directly from **Public repo:** try without credentials first; request if access fails. -### Step 2: Set up the project +### Step 2: Ensure the project exists ```bash -nctl auth set-project - +nctl get project 2>&1 ``` -If project not found, create it first: +- Found → proceed +- Not found → create it (no auth state change involved): ```bash -nctl create project - -nctl auth set-project - +nctl create project ``` ### Step 3: Check for app name collision ```bash -nctl get app +nctl get app --project= ``` - Not found → proceed @@ -158,6 +163,7 @@ If the file doesn't exist and the app uses credentials, report this to the coord ```bash nctl create app \ + --project= \ --git-url= \ --git-revision= \ [--dockerfile] # if build=docker @@ -177,20 +183,20 @@ The build and release are separate stages. The app phase turning "Running" means Poll every 10 seconds: ```bash -nctl get releases 2>&1 +nctl get releases --project= 2>&1 ``` Look for the release row for ``. Wait until its `STATUS` column shows `Running`. If it shows `failed` or stays `progressing` for more than 5 minutes, check logs: ```bash -nctl logs app --type app --since 30s 2>&1 -nctl logs app --type deploy_job --since 30s 2>&1 +nctl logs app --project= --type app --since 30s 2>&1 +nctl logs app --project= --type deploy_job --since 30s 2>&1 ``` ### Step 7: Verify app health After the release reaches Running, confirm the app booted cleanly: ```bash -nctl logs app --type app --since 30s 2>&1 +nctl logs app --project= --type app --since 30s 2>&1 ``` Look for healthy signals: "Listening on port", "Booted in", "Running on". @@ -203,11 +209,11 @@ If boot errors are present, report them as a failure with the log excerpt — do Run both commands — do not skip either: ```bash -nctl get app +nctl get app --project= ``` ```bash -nctl get app --basic-auth-credentials +nctl get app --project= --basic-auth-credentials ``` The second command prints `username:password`. Embed the credentials directly in the URL so the user can click it: @@ -242,27 +248,22 @@ Spec: ``` task: monitor-logs app: -project_suffix: +project: # full - string — pass to every --project flag ``` Your job is to keep the user informed while the executor agent works. The app may not exist yet when you start — retry until it does. -### Step 0: Set up project context — run this first, right now +### Step 0: Sanity-check auth — run this first, right now ```bash nctl auth whoami ``` -Extract the org. Full project name = `-`. Then: -```bash -nctl auth set-project - -``` - -If the project doesn't exist yet, wait 10 seconds and retry — the executor may be creating it in parallel. +This is read-only — it confirms the user is authenticated and exposes the active org. Never run any other `nctl auth` subcommand: `set-project`/`set-org`/`login` are blocked by the destructive guard and would mutate the user's global session anyway. Use `--project=` on every command instead. ### Step 1: Wait for app to exist -Poll every 5 seconds until `nctl get app ` succeeds (exit 0). +Poll every 5 seconds until `nctl get app --project=` succeeds (exit 0). If the project itself doesn't exist yet, the command will fail — that's fine; the executor may be creating it in parallel. Retry. ### Step 2: Poll build logs in short windows @@ -270,7 +271,7 @@ Do NOT use `-f` (it blocks forever and prevents you from reporting). Instead, po ```bash # Poll loop — run this repeatedly until build is complete -nctl logs app --type build --since 10s 2>&1 +nctl logs app --project= --type build --since 10s 2>&1 ``` After each poll, report a one-line summary to the coordinator: @@ -282,7 +283,7 @@ After each poll, report a one-line summary to the coordinator: Detect build completion by checking app status between polls: ```bash -nctl get app 2>&1 +nctl get app --project= 2>&1 ``` Stop polling build logs when status moves past the build phase. @@ -291,7 +292,7 @@ Stop polling build logs when status moves past the build phase. Same pattern: ```bash -nctl logs app --type deploy_job --since 10s 2>&1 +nctl logs app --project= --type deploy_job --since 10s 2>&1 ``` Report: @@ -304,8 +305,8 @@ Report: After build completes, check release status and app boot: ```bash -nctl get releases 2>&1 -nctl logs app --type app --since 15s 2>&1 +nctl get releases --project= 2>&1 +nctl logs app --project= --type app --since 15s 2>&1 ``` Look for healthy boot signals ("Listening on port", "Booted in") or failure signals ("Error", "Exception", crash loops). @@ -329,7 +330,9 @@ Stop once the release STATUS is `Running` and boot is confirmed, or a failure is |---|---| | SSH protocol error | Switch to HTTPS + gh token (see step 1) | | Repository access denied | Report back — coordinator needs credentials from user | -| Project not found (warning) | Create project, then re-set | +| Project not found | Create with `nctl create project ` and retry the original command with `--project=` | +| `whoami` says "failed to parse JWT token..." or similar | Report `blocker: auth_stale` to coordinator — user must run `nctl auth login` themselves (the agent cannot; it's blocked by the destructive guard) | +| Org in `whoami` doesn't match `org` in spec | Report mismatch to coordinator — do not run `nctl auth set-org` (blocked by guard, and would break the user's other shells) | | App name collision | Report collision + suggest `-2` | | SECRET_KEY_BASE placeholder in env_vars | Generate with `openssl rand -hex 64` before creating app | | Release stuck in `progressing` > 5min | Check app and deploy_job logs, report with excerpt | diff --git a/hooks/check-nctl-version.sh b/hooks/check-nctl-version.sh new file mode 100755 index 0000000..2ef8a27 --- /dev/null +++ b/hooks/check-nctl-version.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# Deploio nctl version probe. +# Wired as a SubagentStart hook with matcher=deploio-cli — fires the moment +# Claude spawns the deploio-cli agent, just before any nctl call. Anything +# this script writes to stdout becomes context for the spawning Claude (not +# direct output to the user's terminal), so the message is phrased as an +# instruction for Claude to relay to the user. +# +# The minimum is the lowest nctl release the Deploio skills are tested +# against. Bump it here when the skills start to depend on a newer nctl +# feature. Plugin version lives in .claude-plugin/plugin.json. +MIN_VERSION="1.16.0" + +# Drain stdin — the host writes the tool-input payload here even when we +# don't consume it; leaving it pending can stall the host process. +cat >/dev/null + +version_lt() { + [ "$1" != "$2" ] && \ + [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" = "$1" ] +} + +if ! command -v nctl >/dev/null 2>&1; then + cat <<'EOF' +[deploio nctl probe] nctl is not installed on this machine — the Deploio +skills cannot deploy or manage apps until the user installs it. + +Before doing any work, lead with this message to the user verbatim: + + The Deploio CLI (`nctl`) isn't installed yet. Please install it and ask + me to retry: + macOS: brew install ninech/tap/nctl + Other: https://github.com/ninech/nctl +EOF + exit 0 +fi + +INSTALLED=$(nctl --version 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1) +if [ -z "$INSTALLED" ]; then + exit 0 +fi + +if version_lt "$INSTALLED" "$MIN_VERSION"; then + cat < -Risk: - -This cannot be undone. Do you want to proceed? - → Yes, I understand the risk — run it - → No, cancel -``` - -Wait for the user's explicit confirmation before allowing the command to run. - -## If the command does not match - -Allow it silently. Do not output anything. diff --git a/hooks/guard-destructive.sh b/hooks/guard-destructive.sh index 6a923a9..0e9340a 100755 --- a/hooks/guard-destructive.sh +++ b/hooks/guard-destructive.sh @@ -21,6 +21,17 @@ if echo "$COMMAND" | grep -qE '\bnctl\s+delete\b'; then exit 2 fi +# --- Global auth-state mutations -------------------------------------------- +# Block any `nctl auth ` except `whoami`. These mutate the user's +# kubeconfig / active project / active org / session token globally — silently +# breaking any other shell session using the same nctl config. The user runs +# these themselves; the agent only reads state via `nctl auth whoami`. +if echo "$COMMAND" | grep -qE '\bnctl\s+auth\b' && ! echo "$COMMAND" | grep -qE '\bnctl\s+auth\s+whoami\b'; then + SUBCMD=$(echo "$COMMAND" | grep -oE '\bnctl\s+auth(\s+\S+)?' | awk '{print $3}') + echo "BLOCKED: 'nctl auth ${SUBCMD:-}' changes global nctl state (active org/project or session token) and silently breaks any other shell using the same nctl config. Ask the user to run it manually in their terminal." >&2 + exit 2 +fi + # --- Scale to zero (stops serving traffic) ---------------------------------- # Anchor on the flag itself rather than the subcommand — `nctl update app` # and `nctl update application` are both valid, and we want to catch both. diff --git a/hooks/hooks.json b/hooks/hooks.json index 0dfb4d7..4fb6273 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -5,8 +5,19 @@ "matcher": "Bash", "hooks": [ { - "type": "prompt", - "prompt": "${CLAUDE_PLUGIN_ROOT}/hooks/guard-destructive.md" + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/guard-destructive.sh" + } + ] + } + ], + "SubagentStart": [ + { + "matcher": "deploio-cli", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/check-nctl-version.sh" } ] } diff --git a/install.sh b/install.sh index 283ffed..394046b 100755 --- a/install.sh +++ b/install.sh @@ -31,7 +31,13 @@ info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } ok() { printf '\033[1;32m==>\033[0m %s\n' "$*"; } fail() { printf '\033[1;31mError:\033[0m %s\n' "$*" >&2; exit 1; } -cleanup() { [ -z "${LOCAL_SRC:-}" ] && [ -d "${tmpdir:-}" ] && rm -rf "$tmpdir"; } +# Don't end on a falsy short-circuit — under `set -e` + EXIT trap, the trap's +# own non-zero return becomes the script's exit status, even on success. +cleanup() { + if [ -z "${LOCAL_SRC:-}" ] && [ -d "${tmpdir:-}" ]; then + rm -rf "$tmpdir" + fi +} trap cleanup EXIT # --- resolve install scope -------------------------------------------------- @@ -106,6 +112,66 @@ info "Installing hooks..." mkdir -p "$CLAUDE_DIR/hooks" cp "$src/hooks/guard-destructive.sh" "$CLAUDE_DIR/hooks/deploio-guard-destructive.sh" chmod +x "$CLAUDE_DIR/hooks/deploio-guard-destructive.sh" +cp "$src/hooks/check-nctl-version.sh" "$CLAUDE_DIR/hooks/deploio-check-nctl-version.sh" +chmod +x "$CLAUDE_DIR/hooks/deploio-check-nctl-version.sh" + +# Wire the plugin's hook entries into the user's settings.json. Marketplace +# installs read hooks/hooks.json directly (with ${CLAUDE_PLUGIN_ROOT} resolved +# at hook-exec time). Flat installs have no plugin context, so we substitute +# every ${CLAUDE_PLUGIN_ROOT}/hooks/.sh with the absolute, deploio- +# prefixed install path and merge the result into settings.json. Re-running +# the installer is idempotent: prior entries pointing into $CLAUDE_DIR/hooks/ +# are stripped before the new ones are appended. +SETTINGS="$CLAUDE_DIR/settings.json" +HOOK_PREFIX="$CLAUDE_DIR/hooks/" + +# Build the substituted hooks block. Two replacements: env-var → absolute +# path, and source script name → installed (prefixed) script name. +NEW_HOOKS_JSON=$(sed \ + -e "s|\${CLAUDE_PLUGIN_ROOT}/hooks/guard-destructive\.sh|${HOOK_PREFIX}deploio-guard-destructive.sh|g" \ + -e "s|\${CLAUDE_PLUGIN_ROOT}/hooks/check-nctl-version\.sh|${HOOK_PREFIX}deploio-check-nctl-version.sh|g" \ + "$src/hooks/hooks.json") + +if ! command -v jq >/dev/null 2>&1; then + info "jq not found — to enable Deploio hooks, copy this block into $SETTINGS:" + echo "$NEW_HOOKS_JSON" +else + hooks_tmp=$(mktemp) + # Make sure the temp file is cleaned up even if jq fails or the script + # exits unexpectedly. The cleanup trap from the top of the script handles + # tmpdir; this trap appends to it for hooks_tmp. + trap 'rm -f "$hooks_tmp"; cleanup' EXIT + + # If settings.json doesn't exist yet, start from an empty object. + jq_input="$SETTINGS" + if [ ! -f "$SETTINGS" ]; then + jq_input=$(mktemp) + echo '{}' > "$jq_input" + trap 'rm -f "$hooks_tmp" "$jq_input"; cleanup' EXIT + fi + + if echo "$NEW_HOOKS_JSON" | jq -s --arg prefix "$HOOK_PREFIX" ' + # Strip any existing entry that already references one of our installed + # hook scripts (idempotent re-install). + def strip_deploio: + map(select( + (.hooks // []) | any(((.command // .prompt) // "") | startswith($prefix)) | not + )); + + .[0] as $existing | .[1] as $new | + ($existing | .hooks //= {}) | + reduce ($new.hooks | keys[]) as $event (.; + .hooks[$event] = (((.hooks[$event] // []) | strip_deploio) + $new.hooks[$event]) + ) + ' "$jq_input" - > "$hooks_tmp"; then + mv "$hooks_tmp" "$SETTINGS" + ok "Wired Deploio hooks into $SETTINGS" + else + fail "jq failed to merge hooks into $SETTINGS — file left unchanged" + fi + + trap cleanup EXIT +fi # --- install commands ------------------------------------------------------- @@ -123,7 +189,8 @@ ok "Deploio Claude Code skills installed!" echo "" echo " Agent: $CLAUDE_DIR/agents/deploio-cli.md" echo " Skills: $CLAUDE_DIR/skills/deploio-{deploy,manage,debug,provision,ci-cd}/" -echo " Hooks: $CLAUDE_DIR/hooks/deploio-guard-destructive.sh" +echo " Hooks: $CLAUDE_DIR/hooks/deploio-guard-destructive.sh + $CLAUDE_DIR/hooks/deploio-check-nctl-version.sh" echo " Commands: $CLAUDE_DIR/commands/{deploy,debug}.md" echo "" echo " Make sure nctl is installed and authenticated:" diff --git a/skills/deploio-ci-cd/SKILL.md b/skills/deploio-ci-cd/SKILL.md index 859eb50..a8b88dc 100644 --- a/skills/deploio-ci-cd/SKILL.md +++ b/skills/deploio-ci-cd/SKILL.md @@ -3,7 +3,7 @@ name: deploio-ci-cd description: Sets up automated deployments to Deploio from CI/CD pipelines using nctl service accounts. This skill should be triggered when the user wants to automate deployments: "GitHub Actions Deploio", "CI/CD for Deploio", "auto-deploy to Deploio", "deploy on push", "automate deployments", "configure deployment pipeline", "automate releases", "GitLab CI deploy", "CircleCI deploy", "Deploio in pipeline". Covers service account creation, credential management, GitHub Actions workflow templates, and multi-environment (staging/production) patterns. Works with any CI system. Do NOT use for manual one-time deployments (use deploio-deploy or deploio-manage). license: MIT metadata: - version: 1.3.0 + version: 1.5.0 --- # Deploio: CI/CD Setup diff --git a/skills/deploio-debug/SKILL.md b/skills/deploio-debug/SKILL.md index 2a6f297..54d9f1c 100644 --- a/skills/deploio-debug/SKILL.md +++ b/skills/deploio-debug/SKILL.md @@ -3,7 +3,7 @@ name: deploio-debug description: Diagnoses and fixes Deploio app problems — crashes, build failures, release errors, and runtime errors. This skill should be triggered when something is broken or wrong: "app crashed", "deploy failing", "build error", "release failed", "app not starting", "getting 500 errors", "getting 503 errors", "bad gateway", "migrations failed", "why is my app broken", "something went wrong after deploy", "app keeps restarting", "app is slow", "high memory usage", "OOM", "performance issue". Gathers logs and state automatically, presents a diagnosis, then applies fixes directly. Do NOT use for routine log monitoring or opening a Rails console on a healthy app (use deploio-manage), first-time deployment (use deploio-deploy), or provisioning services (use deploio-provision). license: MIT metadata: - version: 1.4.0 + version: 1.5.0 --- # Deploio: Debugging and Troubleshooting diff --git a/skills/deploio-deploy/SKILL.md b/skills/deploio-deploy/SKILL.md index b3b140a..7c8644c 100644 --- a/skills/deploio-deploy/SKILL.md +++ b/skills/deploio-deploy/SKILL.md @@ -3,7 +3,7 @@ name: deploio-deploy description: Handles first-time deployment of an app to Deploio — from git URL to live HTTPS URL. This skill should be triggered when deploying a new app for the first time: "deploy my app on Deploio", "create a Deploio app", "how do I deploy to Deploio", "host on Deploio", "push to Deploio", "new app on Deploio", "first deploy to Deploio", "set up a new Deploio app", or setting up a new Deploio app from scratch. Covers auth, project setup, git credential resolution, buildpack/Dockerfile detection, and build monitoring. Do NOT use for apps already running on Deploio (use deploio-manage to update them). license: MIT metadata: - version: 1.3.0 + version: 1.5.0 --- # Deploio: First-Time App Deployment @@ -45,11 +45,11 @@ hints: ### What the agent must do -**Constraints:** Do not run any `nctl` commands during context gathering — nctl commands may require auth and will produce misleading errors before the project exists. Use only: git commands, file system reads, and `nctl version`. +**Constraints:** Do not run any `nctl` commands during context gathering — nctl commands may require auth and will produce misleading errors before the project exists. Use only: git commands, file system reads, and `nctl --version`. **Detection steps:** -1. `nctl version` → set `nctl_installed: true/false`; require at least v1.16.0 for Deploio support +1. `nctl --version` → set `nctl_installed: true/false`; require at least v1.16.0 for Deploio support. (The plugin's SessionStart hook also surfaces this at the start of the session, but re-check here so the gather-context output is self-contained.) 2. Use the `remote_url` hint if provided; otherwise run `git remote get-url origin` → set `remote_url` 3. `git branch --show-current` → set `branch` 4. Read project files to detect `app_type` and `port` (framework details live in `references/.md`): @@ -98,7 +98,16 @@ hints: } ``` -`blockers` is a list of strings, e.g. `["nctl not installed", "no git remote"]`. +`blockers` is a list of canonical strings the coordinator maps to user-facing guidance: + +| Blocker | Trigger | Coordinator action | +|---|---|---| +| `nctl_missing` | `nctl --version` not found | Phase 2: ask user to install nctl | +| `nctl_outdated` | nctl version below v1.16.0 | Phase 2: ask user to upgrade | +| `nctl_not_authenticated` | `whoami` errors, no JWT mention | Phase 2: ask user to run `nctl auth login` | +| `auth_stale` | `whoami` output contains `failed to parse JWT token` | Phase 2: surface explicit stale-token message (see below) | +| `no_remote` | `git remote get-url origin` fails | Phase 2: ask user to add remote and push | +| `app_type_unknown` | None of the framework files matched | Phase 2: ask user what runtime they use | --- @@ -122,7 +131,25 @@ git push -u origin main ``` Continue once the user confirms it's pushed. -**nctl not authenticated:** Ask the user to run `nctl auth login` (opens browser OAuth) then `nctl auth set-project `. +**nctl not authenticated** (`nctl_not_authenticated`): The user has never logged in or their session was wiped. Show this message verbatim: + +> Your nctl session isn't authenticated. Run this in your terminal to log in +> (a browser window will open): +> +> nctl auth login +> +> Once it's done, ask me to deploy again. + +**Stale auth token** (`auth_stale` — `nctl auth whoami` output contains `failed to parse JWT token`): The session token is expired or corrupt. Don't paraphrase the raw nctl error — translate it into the message below: + +> Your local nctl auth token is stale (the session expired or got corrupted). +> Run this in your terminal to refresh it: +> +> nctl auth login +> +> Then ask me to deploy again — I'll pick up where we left off. + +The skill never runs `nctl auth login` or `nctl auth set-*` itself — those mutate the user's global nctl session and would silently break any other shell using the same config. Project scoping is handled per-command with `--project=` on the executor side; the user does not need to run `nctl auth set-project`. **app_type unknown:** Ask the user what runtime their app uses before proceeding. @@ -160,12 +187,18 @@ Your account has access to multiple Deploio organizations: 2. acme-staging Which organization should this app be deployed to? -(Or run `nctl auth set-org ` first to set a default.) + +If you'd like to make a default permanently, run this in your terminal: + nctl auth set-org +⚠ Heads up: that command changes the active org globally — any other shell or + Claude session you have open using the same nctl config will switch with it. + This deploy doesn't need it: I'll scope every command with --project so your + current state stays untouched. ``` -Set `selected_org` from the user's answer. +Set `selected_org` from the user's answer. **Never run `nctl auth set-org` or `nctl auth set-project` from the skill or agent** — they're blocked by the destructive-guard hook precisely because they mutate global nctl state. -> **Terminology note:** Deploio calls the top-level grouping an **organization** (set with `nctl auth set-org`). The app lives within the organization. Do not use the word "project" when referring to this selection — it confuses users who see organization names in `nctl auth whoami` output. +> **Terminology note:** Deploio calls the top-level grouping an **organization**. The app lives within the organization. Do not use the word "project" when referring to this selection — it confuses users who see organization names in `nctl auth whoami` output. --- @@ -251,7 +284,7 @@ Once confirmed, use `TaskCreate` to create a task named "Deploying ``" ``` task: deploy -org: +project: - # full project string — agent passes to every --project flag (scoping is authoritative; agent does not need org separately) app: git_remote: branch: @@ -279,13 +312,18 @@ For Rails apps, if `SECRET_KEY_BASE` is not already set by the user, the executo ### nctl commands the executor will run (in order) +The executor never runs `nctl auth login` or `nctl auth set-*` — those are blocked by the destructive-guard hook and would mutate the user's global nctl session. Instead it verifies auth with read-only `nctl auth whoami`, and scopes every other command with `--project=`. If `whoami` fails or reports a stale JWT, the executor reports `auth_stale` back and the coordinator asks the user to run `nctl auth login` themselves. + ```bash -# 1. Authenticate (skip if already logged in) -nctl auth login -nctl auth set-project +# 1. Verify auth (read-only) +nctl auth whoami + +# 2. Ensure the project exists, create if missing +nctl get project 2>&1 || nctl create project -# 2. Create the app +# 3. Create the app — every command carries --project= nctl create app \ + --project= \ --git-url= \ --git-revision= \ [--git-sub-path=] # monorepos only @@ -308,9 +346,9 @@ nctl create app \ [--health-probe-period-seconds=] # default 10, min 1 [--hosts=] # custom domains at creation time -# 3. Get the live URL and (if basic_auth) credentials +# 4. Get the live URL and (if basic_auth) credentials nctl get app --project= -[nctl get app --basic-auth-credentials] # if basic_auth is true +[nctl get app --project= --basic-auth-credentials] # if basic_auth is true ``` **Env var syntax:** Multiple env vars can be passed either as repeated flags (`--env=KEY1=VAL1 --env=KEY2=VAL2`) or semicolon-separated in a single flag (`--env='KEY1=VAL1;KEY2=VAL2'`). Both forms are accepted. The same syntax applies to `--build-env`. @@ -323,7 +361,7 @@ On failure, report back: `{ "status": "failed", "error": "", ``` task: monitor-logs app: -org: +project: - # full project string — agent passes to every --project flag termination: stop when executor reports success or failure, or after 20 minutes — whichever comes first ``` diff --git a/skills/deploio-provision/SKILL.md b/skills/deploio-provision/SKILL.md index 83a4602..0f6fec4 100644 --- a/skills/deploio-provision/SKILL.md +++ b/skills/deploio-provision/SKILL.md @@ -3,7 +3,7 @@ name: deploio-provision description: Provisions and connects managed backing services to Deploio apps — PostgreSQL, MySQL, Redis-compatible KVS, OpenSearch, and S3-compatible object storage. This skill should be triggered when the user needs to add a database, cache, or storage service: "add postgres to Deploio", "provision MySQL", "need a database on Deploio", "add Redis", "create KVS", "set up object storage", "add OpenSearch", "connect database to Deploio app", "add Sidekiq", "background jobs on Deploio", "file uploads on Deploio", "object storage Deploio". Handles creation, credential extraction, env var injection, and connection verification. Do NOT use for app config changes unrelated to backing services (use deploio-manage). license: MIT metadata: - version: 1.3.0 + version: 1.5.0 --- # Deploio: Provisioning Backing Services diff --git a/skills/shared/troubleshooting.md b/skills/shared/troubleshooting.md index 65b93e8..33713d5 100644 --- a/skills/shared/troubleshooting.md +++ b/skills/shared/troubleshooting.md @@ -53,3 +53,11 @@ Common problems, their root causes, and how to fix them. Referenced by `deploio- | Symptom | Likely cause | Fix | |---|---|---| | Auth error on git pull | Private repo, no credentials | Add SSH key (`--git-ssh-private-key-from-file`) or HTTPS token (`--git-username`/`--git-password`) | + +## nctl auth errors + +| Symptom | Likely cause | Fix | +|---|---|---| +| `nctl auth whoami`: `failed to parse JWT token` | Local kubeconfig token expired or corrupt | Tell the user: "Your local nctl auth token is stale — run `nctl auth login` in your terminal." Skill/agent never run this. | +| `nctl auth whoami` errors with no JWT mention | User never logged in, or kubeconfig wiped | Tell the user to run `nctl auth login` once in their terminal. | +| Org in `whoami` doesn't match the spec org | Active org changed in another shell | Tell the user — never run `nctl auth set-org` from the skill or agent (concurrent sessions sharing the same nctl config would silently break). | diff --git a/uninstall.sh b/uninstall.sh index c92c83a..7ed888f 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -84,6 +84,28 @@ remove_dir "$CLAUDE_DIR/skills/shared" # --- hooks ------------------------------------------------------------------ remove_file "$CLAUDE_DIR/hooks/deploio-guard-destructive.sh" +remove_file "$CLAUDE_DIR/hooks/deploio-check-nctl-version.sh" + +# Strip every settings.json hook entry pointing at our hooks/ directory. +# Best-effort: needs jq; without jq we leave the entries in place (they're +# harmless once the scripts are gone — they'll just fail silently). +SETTINGS="$CLAUDE_DIR/settings.json" +HOOK_PREFIX="$CLAUDE_DIR/hooks/" +if [ -f "$SETTINGS" ] && command -v jq >/dev/null 2>&1; then + tmp=$(mktemp) + if jq --arg prefix "$HOOK_PREFIX" ' + def strip_deploio: + map(select( + (.hooks // []) | any(((.command // .prompt) // "") | startswith($prefix)) | not + )); + .hooks //= {} | + .hooks |= with_entries(.value |= strip_deploio) + ' "$SETTINGS" > "$tmp" && mv "$tmp" "$SETTINGS"; then + info "Stripped Deploio hook entries from $SETTINGS" + else + rm -f "$tmp" + fi +fi # --- commands ---------------------------------------------------------------