---
name: pstack-upgrade
preamble-tier: 1
version: 0.1.0
description: |
  Detects the user's installed pstack version vs the latest marketplace
  version, prints CHANGELOG/release notes for the gap, runs versioned
  on-disk migrations, and tells the user exactly which `/plugin update`
  + restart steps to perform (Claude Code can't trigger the plugin
  update from inside a skill). Other pstack skills' preambles surface
  a one-line nudge when an upgrade is available.
  Use when asked to "upgrade pstack", "update pstack",
  "/pstack-upgrade", or "get latest pstack".
triggers:
  - /pstack-upgrade
  - upgrade pstack
  - update pstack
  - get latest pstack
  - pstack version
allowed-tools:
  - Bash
  - Read
  - Write
  - Edit
  - AskUserQuestion
---

# pstack-upgrade

Single command that closes the upgrade loop for pstack itself:

1. What version am I on?
2. What's the latest?
3. What's new in between?
4. What state on my disk needs adjusting?
5. What do I (the human) need to run / restart, since Claude Code can't
   invoke `/plugin update` or quit the app on its own?

Borrows the snooze + auto-upgrade ergonomics from gstack's
`/gstack-upgrade`, adapted for the **Claude Code plugin** install shape:
the actual binary refresh happens via `/plugin update pstack@pstack`,
which only the user can invoke.

## Args

```
/pstack-upgrade                # full interactive flow
/pstack-upgrade --check-only   # version check + what's-new; no prompts, no migrations
/pstack-upgrade --auto         # non-interactive: skip prompts, run migrations, print restart steps
```

`--auto` still prints the `/plugin update` instructions at the end —
the user has to run that line themselves.

## Steps

### 0. Preamble — surface the nudge

```bash
"${CLAUDE_SKILL_DIR}/../../scripts/pstack-update-check.sh" 2>/dev/null || true
```

(Silent when there's nothing to say.)

### 1. Detect installed + latest

```bash
read -r INSTALLED LATEST < <("${CLAUDE_SKILL_DIR}/../../scripts/pstack-version.sh" --force 2>/dev/null || echo "unknown unknown")
echo "Installed: v$INSTALLED"
echo "Latest:    v$LATEST"
```

Decision matrix:

- `INSTALLED == LATEST` → "pstack v$INSTALLED is the latest version." Exit.
- `INSTALLED == unknown` → "Couldn't read the installed plugin version
  from `~/.claude/plugins/installed_plugins.json`. Make sure pstack is
  installed via `/plugin install pstack@pstack`, then re-run." Exit.
- `LATEST == unknown` → "Couldn't reach `marketplace.json` on GitHub
  (offline?). Try again once you have network." Exit.
- `INSTALLED > LATEST` (semver) → "You're on v$INSTALLED, ahead of the
  marketplace's v$LATEST — looks like a dev build. Skipping upgrade."
  Continue to Step 4 anyway (migrations may still apply for a
  hand-installed version).
- Otherwise → continue to Step 2.

### 2. Show what's new

Prefer `gh` (cleaner formatting); fall back to GitHub REST.

```bash
RELEASE_BODY=$(gh release view "v$LATEST" --repo periant-llc/pstack --json body --jq .body 2>/dev/null \
  || curl -fsS "https://api.github.com/repos/periant-llc/pstack/releases/tags/v$LATEST" 2>/dev/null \
       | python3 -c "import json,sys; print(json.load(sys.stdin).get('body',''))" 2>/dev/null)

if [ -n "$RELEASE_BODY" ]; then
  echo "What's new in v$LATEST:"
  echo "$RELEASE_BODY" | head -50
  LINES=$(echo "$RELEASE_BODY" | wc -l | tr -d ' ')
  if [ "$LINES" -gt 50 ]; then
    echo "(truncated — full notes: https://github.com/periant-llc/pstack/releases/tag/v$LATEST)"
  fi
else
  echo "Could not fetch release notes for v$LATEST."
  echo "See: https://github.com/periant-llc/pstack/releases/tag/v$LATEST"
fi
```

If multiple versions are being skipped (e.g., upgrading 1.0.0 → 1.2.0),
also call out `https://github.com/periant-llc/pstack/releases` for the
intermediate notes.

### 3. Ask the user

Skip this step if `--auto` or `--check-only` was passed. For `--auto`,
proceed to Step 4 directly. For `--check-only`, exit after Step 2.

Use `AskUserQuestion`:

- **Question:** "pstack v$LATEST is available (you're on v$INSTALLED). What would you like to do?"
- **Options (single-select):**
  1. **Upgrade now** — proceed to Step 4.
  2. **Always auto-upgrade** — set `auto_upgrade: true` in `~/.pstack/config.json`, then Step 4. (Caveat to surface in the option description: pstack still can't run `/plugin update` for you; "auto" means migrations + restart-instructions run without prompting.)
  3. **Not now** — escalating snooze (24h → 48h → 1w), then exit.
  4. **Never ask again** — set `update_check: false`, then exit.

**Handling "Always auto-upgrade":**

```bash
python3 - <<'PY'
import json
from pathlib import Path
p = Path.home() / ".pstack" / "config.json"
p.parent.mkdir(parents=True, exist_ok=True)
data = json.loads(p.read_text()) if p.exists() else {}
data["auto_upgrade"] = True
p.write_text(json.dumps(data, indent=2) + "\n")
PY
echo "Auto-upgrade enabled. Future runs of /pstack-upgrade will skip the prompt."
```

**Handling "Not now":**

```bash
SNOOZE_FILE="$HOME/.pstack/state/upgrade-snoozed"
mkdir -p "$(dirname "$SNOOZE_FILE")"
CUR_LEVEL=0
if [ -f "$SNOOZE_FILE" ]; then
  SNOOZED_VER=$(awk '{print $1}' "$SNOOZE_FILE")
  if [ "$SNOOZED_VER" = "$LATEST" ]; then
    CUR_LEVEL=$(awk '{print $2}' "$SNOOZE_FILE")
    case "$CUR_LEVEL" in *[!0-9]*|"") CUR_LEVEL=0 ;; esac
  fi
fi
NEW_LEVEL=$((CUR_LEVEL + 1))
[ "$NEW_LEVEL" -gt 3 ] && NEW_LEVEL=3
echo "$LATEST $NEW_LEVEL $(date +%s)" > "$SNOOZE_FILE"
case "$NEW_LEVEL" in
  1) echo "Snoozed for 24 hours." ;;
  2) echo "Snoozed for 48 hours." ;;
  *) echo "Snoozed for 1 week."   ;;
esac
echo "Tip: set \"auto_upgrade\": true in ~/.pstack/config.json to skip future prompts."
```

Exit after writing snooze.

**Handling "Never ask again":**

```bash
python3 - <<'PY'
import json
from pathlib import Path
p = Path.home() / ".pstack" / "config.json"
p.parent.mkdir(parents=True, exist_ok=True)
data = json.loads(p.read_text()) if p.exists() else {}
data["update_check"] = False
p.write_text(json.dumps(data, indent=2) + "\n")
PY
echo "Update checks disabled. Re-enable with:"
echo "  python3 -c 'import json,os; p=os.path.expanduser(\"~/.pstack/config.json\"); d=json.load(open(p)); d[\"update_check\"]=True; open(p,\"w\").write(json.dumps(d,indent=2))'"
```

Exit.

### 4. Run pre-update migrations

```bash
"${CLAUDE_SKILL_DIR}/../../scripts/pstack-run-migrations.sh" "$INSTALLED" "$LATEST"
```

The runner prints what it's doing. If `$INSTALLED` is `unknown`, the
runner skips with a one-line explanation.

### 5. Tell the user what to do

```
✓ Migrations complete. To finish the upgrade:

   1. Run:    /plugin update pstack@pstack
   2. Reload: VS Code → Cmd+Shift+P → "Reload Window"
              Claude Desktop → quit + reopen the app
   3. Verify: /pstack-upgrade --check-only

Claude Code can't run /plugin update or restart itself from inside a
skill — that's why steps 1 and 2 are yours.
```

Also clear the snooze + cached version so the next check is fresh:

```bash
rm -f "$HOME/.pstack/state/upgrade-snoozed" "$HOME/.pstack/state/last-update-check"
```

### 6. (--check-only) Short summary

When invoked with `--check-only`, after Step 2 print a one-line summary
and exit without prompting / migrations / restart instructions:

- `INSTALLED == LATEST` → `pstack v$INSTALLED — up to date.`
- `LATEST > INSTALLED` → `pstack v$INSTALLED installed; v$LATEST available. Run /pstack-upgrade to apply.`
- `INSTALLED > LATEST` → `pstack v$INSTALLED (ahead of marketplace v$LATEST — dev build).`

## How the preamble nudge works

Each pstack skill's body includes this one-liner near the top:

```bash
"${CLAUDE_SKILL_DIR}/../../scripts/pstack-update-check.sh" 2>/dev/null || true
```

`pstack-update-check.sh` is silent unless **all** of these are true:

- An upgrade is available (`LATEST > INSTALLED`, both detectable).
- `update_check` is not `false` in `~/.pstack/config.json`.
- There is no active snooze for the same `LATEST` version.

When the nudge fires, it prints one line:

```
ℹ pstack v<latest> is available (you're on v<installed>). Run /pstack-upgrade for details.
```

The version check is cached for 4 hours by default
(`PSTACK_UPDATE_CHECK_INTERVAL_HOURS` env var to override).

## Files this skill owns

- `plugins/pstack/skills/pstack-upgrade/SKILL.md` (this file)
- `plugins/pstack/scripts/pstack-version.sh` — installed/latest detection
  + 4-hour cache
- `plugins/pstack/scripts/pstack-update-check.sh` — silent-or-one-line
  preamble nudge
- `plugins/pstack/scripts/pstack-run-migrations.sh` — runs everything
  in `(OLD, NEW]` from the migrations directory
- `plugins/pstack/migrations/v*.sh` — versioned, idempotent state fixes
- `~/.pstack/config.json` — top-level `auto_upgrade` + `update_check`
- `~/.pstack/state/last-update-check` — cached latest version
- `~/.pstack/state/upgrade-snoozed` — snooze state

## Do not

- Do not attempt to run `/plugin update pstack@pstack` from inside this
  skill — Claude Code rejects programmatic invocations of `/plugin`.
  The user runs it.
- Do not skip migrations on `--auto`. The whole point of `--auto` is to
  do *everything we can* without prompting; migrations are a yes.
- Do not modify `~/.pstack/state/upgrade-snoozed` from this skill except
  in the "Not now" branch and the post-upgrade cleanup. Other skills'
  preambles read it but never write it.
- Do not call `gh` for the latest-version lookup. The marketplace.json
  fetch via `curl` is more reliable across environments (Desktop, CLI,
  Codespaces) and doesn't depend on the user being logged in.
