- name
- gstack-upgrade
- version
- 1.1.0
- description
- Upgrade gstack to the latest version.
- triggers
- ["upgrade gstack","update gstack version","get latest gstack"]
- allowed-tools
- ["Bash","Read","Write","AskUserQuestion"]
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
## When to invoke this skill
Detects global vs vendored install,
runs the upgrade, and shows what's new. Use when asked to "upgrade gstack",
"update gstack", or "get latest version".
Voice triggers (speech-to-text aliases): "upgrade the tools", "update the tools", "gee stack upgrade", "g stack upgrade".
# /gstack-upgrade
Upgrade gstack to the latest version and show what's new.
## Inline upgrade flow
This section is referenced by all skill preambles when they detect `UPGRADE_AVAILABLE`.
### Step 1: Ask the user (or auto-upgrade)
First, check if auto-upgrade is enabled:
```bash
_AUTO=""
[ "${GSTACK_AUTO_UPGRADE:-}" = "1" ] && _AUTO="true"
[ -z "$_AUTO" ] && _AUTO=$(~/.claude/skills/gstack/bin/gstack-config get auto_upgrade 2>/dev/null || true)
echo "AUTO_UPGRADE=$_AUTO"
```
**If `AUTO_UPGRADE=true` or `AUTO_UPGRADE=1`:** Skip AskUserQuestion. Log "Auto-upgrading gstack v{old} → v{new}..." and proceed directly to Step 2. On setup failure, follow Step 4's install-specific recovery: vendored installs restore their backup; git installs stop with the pre-upgrade commit recorded, without a destructive reset. Never claim restoration unless it actually succeeded.
**Otherwise**, use AskUserQuestion:
- Question: "gstack **v{new}** is available (you're on v{old}). Upgrade now?"
- Options: ["Yes, upgrade now", "Always keep me up to date", "Not now", "Never ask again"]
**If "Yes, upgrade now":** Proceed to Step 2.
**If "Always keep me up to date":**
```bash
~/.claude/skills/gstack/bin/gstack-config set auto_upgrade true
```
Tell user: "Auto-upgrade enabled. Future updates will install automatically." Then proceed to Step 2.
**If "Not now":** Write snooze state with escalating backoff (first snooze = 24h, second = 48h, third+ = 1 week), then continue with the current skill. Do not mention the upgrade again.
```bash
GSTACK_STATE_ROOT=$(~/.claude/skills/gstack/bin/gstack-paths --get GSTACK_STATE_ROOT); : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}"
_SNOOZE_FILE="$GSTACK_STATE_ROOT/update-snoozed"
_REMOTE_VER="{new}"
_CUR_LEVEL=0
if [ -f "$_SNOOZE_FILE" ]; then
_SNOOZED_VER=$(awk '{print $(1)}' "$_SNOOZE_FILE")
if [ "$_SNOOZED_VER" = "$_REMOTE_VER" ]; 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 "$_REMOTE_VER $_NEW_LEVEL $(date +%s)" > "$_SNOOZE_FILE"
```
Note: `{new}` is the remote version from the `UPGRADE_AVAILABLE` output — substitute it from the update check result.
Tell user the snooze duration: "Next reminder in 24h" (or 48h or 1 week, depending on level). Tip: "Set `auto_upgrade: true` in `~/.gstack/config.yaml` for automatic upgrades."
**If "Never ask again":**
```bash
~/.claude/skills/gstack/bin/gstack-config set update_check false
```
Tell user: "Update checks disabled. Run `~/.claude/skills/gstack/bin/gstack-config set update_check true` to re-enable."
Continue with the current skill.
### Step 2: Detect install type
```bash
if [ -d "$HOME/.claude/skills/gstack/.git" ]; then
INSTALL_TYPE="global-git"
INSTALL_DIR="$HOME/.claude/skills/gstack"
elif [ -d "$HOME/.gstack/repos/gstack/.git" ]; then
INSTALL_TYPE="global-git"
INSTALL_DIR="$HOME/.gstack/repos/gstack"
elif [ -d ".claude/skills/gstack/.git" ]; then
INSTALL_TYPE="local-git"
INSTALL_DIR=".claude/skills/gstack"
elif [ -d ".agents/skills/gstack/.git" ]; then
INSTALL_TYPE="local-git"
INSTALL_DIR=".agents/skills/gstack"
elif [ -d ".claude/skills/gstack" ]; then
INSTALL_TYPE="vendored"
INSTALL_DIR=".claude/skills/gstack"
elif [ -d "$HOME/.claude/skills/gstack" ]; then
INSTALL_TYPE="vendored-global"
INSTALL_DIR="$HOME/.claude/skills/gstack"
elif _SRC=$(~/.claude/skills/gstack/bin/gstack-paths --get GSTACK_STATE_ROOT 2>/dev/null) && _SRC=$(awk -F '\t' '$(1) == "claude" && $(6) != "-" { print $(6); exit }' "$_SRC/installs.tsv" 2>/dev/null) && [ -d "$_SRC/.git" ]; then
# The install registry names the checkout setup activated for this host (e.g. a ~/gstack clone).
INSTALL_TYPE="global-git"
INSTALL_DIR="$_SRC"
else
echo "ERROR: gstack not found"
exit 1
fi
INSTALL_DIR=$(cd -- "$INSTALL_DIR" && pwd -P) || { echo "ERROR: cannot enter the gstack install directory" >&2; exit 1; }
echo "Install type: $INSTALL_TYPE at $INSTALL_DIR"
```
The install type and the absolute directory printed above are used in all subsequent steps.
Carry `INSTALL_TYPE`, `INSTALL_DIR`, `OLD_VERSION`, and later `NEW_VERSION` forward explicitly: tool calls may run in fresh shells, so start each later block by assigning them from the captured output (for example `INSTALL_DIR=/abs/path/printed/above`). Do not rely on a prior call's working directory or shell variables. Every block that changes files first checks `INSTALL_DIR` and stops before any git command when it is unset, empty, missing, unreadable, or not gstack's own checkout.
### Step 3: Save old version
Use the install directory from Step 2's output below:
```bash
OLD_VERSION=$(cat "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}/VERSION" 2>/dev/null || echo "unknown")
echo "OLD_VERSION=$OLD_VERSION"
```
### Step 4: Upgrade
Use the install type and directory detected in Step 2:
**For git installs** (global-git, local-git):
Fast-forward first — the same policy the session-update auto-upgrade
uses. `--autostash` carries local edits over the pull; render-footprint dirt
is discarded first because it is regenerable and poisons stashes:
```bash
cd -- "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}" || exit 1
{ [ -f VERSION ] && [ -f setup ] && [ -f bin/gstack-config ] && _p=$(git rev-parse --show-prefix 2>/dev/null) && [ -z "$_p" ] && [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" = true ]; } || { echo "ERROR: $INSTALL_DIR is not a gstack checkout; nothing was changed. Re-run Step 2." >&2; exit 1; }
# Discard render-footprint dirt: older gbrain-enabled installs rendered
# generated SKILL.md / sections files IN PLACE. They are regenerable (setup
# re-renders to ~/.gstack/render), so discarding is lossless.
git checkout -- 'SKILL.md' '*/SKILL.md' '*/sections/*.md' 2>/dev/null || true
git fetch origin
# Same pre-advance check as the auto-updater: the incoming release's Bun floor.
HOLD=$(. bin/gstack-bun-version.sh 2>/dev/null && gstack_bun_incoming_hold . origin/main) && { echo "BUN_TOO_OLD: $HOLD; nothing was changed" >&2; exit 1; }
PRE_UPGRADE_COMMIT=$(git rev-parse HEAD)
echo "PRE_UPGRADE_COMMIT=$PRE_UPGRADE_COMMIT"
if git pull --ff-only --autostash origin main; then
if ./setup --refresh-registered; then echo "FF_OK"; else echo "SETUP_FAILED: git update succeeded; stop and inspect setup output (previous commit: $PRE_UPGRADE_COMMIT)" >&2; exit 1; fi
else
echo "FF_REFUSED"
fi
```
Setup's `--refresh-registered` refreshes every install this checkout
registered (each host, global and this project's), prints the source first,
and ends with an `Upgrade summary` of one row per install. Relay those rows
as printed. A `failed` row means that host kept its previous install; give
its retry command and never say every host was refreshed. A `skipped` row
belongs to another checkout or project; pass on its command, do not run it.
If the output ends with `FF_OK`, the upgrade is done — skip the fallback
below entirely.
On `BUN_TOO_OLD`, STOP: tell the user to run `bun upgrade`, then /gstack-upgrade again.
On `SETUP_FAILED`, STOP; keep user changes and report the recovery commit. There is no `.bak` on the git path. Do not enter the divergence fallback merely because setup failed. Enter it only on `FF_REFUSED`, after inspecting the pull error; network/auth failures stop for repair, not reset.
**Fallback (ff-only refused — local commits or divergence).** `git reset
--hard` DESTROYS things: a clean tree with unpushed local commits still loses
those commits. Gate it:
1. Run `git status --porcelain` and `git rev-list origin/main..HEAD --oneline`
in `$INSTALL_DIR`.
2. If BOTH are empty, the reset is provably safe — run the fallback block
below without asking.
3. Otherwise ask via AskUserQuestion (one-way door — destructive), listing
exactly what will be discarded: each dirty file and each unpushed commit
by hash + subject. Options: **A)** Discard them and upgrade (reset) —
requires the explicit letter; **B)** Abort the upgrade so the user can
rescue their work first (recommended when local commits exist). Never
proceed on a vague reply.
```bash
cd -- "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}" || exit 1
{ [ -f VERSION ] && [ -f setup ] && [ -f bin/gstack-config ] && _p=$(git rev-parse --show-prefix 2>/dev/null) && [ -z "$_p" ] && [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" = true ]; } || { echo "ERROR: $INSTALL_DIR is not a gstack checkout; nothing was changed. Re-run Step 2." >&2; exit 1; }
STASH_OUTPUT=$(git stash 2>&1)
git reset --hard origin/main
./setup --refresh-registered
```
If `$STASH_OUTPUT` contains "Saved working directory", warn the user: "Note: local changes were stashed (any modified generated SKILL.md/sections files were discarded first — they regenerate on setup). Run `git stash pop` in the skill directory to restore your own changes."
**For vendored installs** (vendored, vendored-global):
```bash
(cd -- "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}" && [ -f VERSION ] && [ -f setup ] && [ -f bin/gstack-config ]) || { echo "ERROR: INSTALL_DIR=${INSTALL_DIR:-} is not a gstack install; nothing was changed. Re-run Step 2." >&2; exit 1; }
# A stale .bak from a previously crashed upgrade would make the mv below NEST
# the live install inside it and the failure-restore arm would "restore" the
# stale backup. It may also be the only good copy from that crashed run —
# abort and let the human inspect, never delete it silently.
[ -e "$INSTALL_DIR.bak" ] && { echo "ERROR: stale backup exists at $INSTALL_DIR.bak (from a previous failed upgrade?) — inspect it, salvage/remove it, then re-run." >&2; exit 1; }
TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/gstack-upgrade.XXXXXX") || { echo "ERROR: mktemp failed — aborting upgrade (install untouched)." >&2; exit 1; }
git clone --depth 1 https://github.com/garrytan/gstack.git "$TMP_DIR/gstack" || { echo "ERROR: clone failed — aborting upgrade (install untouched)." >&2; rm -rf "$TMP_DIR"; exit 1; }
mv "$INSTALL_DIR" "$INSTALL_DIR.bak" || { rm -rf "$TMP_DIR"; exit 1; }
if mv "$TMP_DIR/gstack" "$INSTALL_DIR"; then
if (cd "$INSTALL_DIR" && ./setup --refresh-registered); then
rm -rf "$INSTALL_DIR.bak" "$TMP_DIR"
else
rm -rf "$INSTALL_DIR"
mv "$INSTALL_DIR.bak" "$INSTALL_DIR" || { echo "ERROR: restore failed; backup retained." >&2; exit 1; }
rm -rf "$TMP_DIR"
echo "ERROR: setup failed; previous install restored." >&2
exit 1
fi
else
mv "$INSTALL_DIR.bak" "$INSTALL_DIR"
echo "ERROR: swap failed — previous install restored; upgrade aborted." >&2
rm -rf "$TMP_DIR"
exit 1
fi
```
### Step 4.5: Handle local vendored copy
Use the install directory from Step 2. Check if there's also a local vendored copy, and whether team mode is active:
```bash
_RESOLVED_PRIMARY=$(cd -- "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}" && pwd -P) || exit 1
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
LOCAL_GSTACK=""
if [ -n "$_ROOT" ] && [ -d "$_ROOT/.claude/skills/gstack" ]; then
_RESOLVED_LOCAL=$(cd "$_ROOT/.claude/skills/gstack" && pwd -P)
Ver en GitHub