mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-02 15:04:37 +01:00
* feat(release): regression cuts pass the 72-hour floor like security cuts; weekly wording follows the twice-weekly cadence Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: a regression anchor needs a published release; an unmoved exception tag reruns only with its recorded reason; a version may end the sentence Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * RESTAMP: 1.91.291.0 -> 1.91.292.0 (stamped; queue_mode=stamp-at-merge base=origin/main@6f27f0c269cb policy=base origin/main@6f27f0c269cb:.gstack/ship-policy.json) STAMP_VERSION: 1.91.292.0 stage version: done (wrote 1.91.292.0) stage release_tool: done (bun scripts/gen-agents-digest.ts) stage changelog: done (Unreleased -> 1.91.292.0) stage stamp_paths: skipped (no tentative mention newer than the fork version 1.91.291.0 and none of base 1.91.291.0 on a line this branch added) stage mirror: skipped (no mirror in policy) stage apply: done (4 file(s)) FILES: CHANGELOG.md VERSION agents-digest/gstack-AGENTS.md package.json MERGE_CONDITION: sha=40a8df86de0bda12cedea7dbdc744b90f4b1d5eb GSTACK_RESULT: skill=restamp status=complete run=/workspace/gstack/.gstack/tmp/restamp-journal.json Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
426 lines
24 KiB
Cheetah
426 lines
24 KiB
Cheetah
---
|
|
name: gstack-upgrade
|
|
version: 1.1.0
|
|
description: |
|
|
Upgrade gstack to the latest version. 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:
|
|
- "upgrade the tools"
|
|
- "update the tools"
|
|
- "gee stack upgrade"
|
|
- "g stack upgrade"
|
|
triggers:
|
|
- upgrade gstack
|
|
- update gstack version
|
|
- get latest gstack
|
|
allowed-tools:
|
|
- Bash
|
|
- Read
|
|
- Write
|
|
- AskUserQuestion
|
|
---
|
|
|
|
# /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?"
|
|
Four fields (`UPGRADE_AVAILABLE <old> <old>+<n> <sha>`, main moved): ask "gstack main is **{n} commit(s)** past v{old} (at {sha}). 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. On a four-field line use the SHA (fourth field) as `{new}`, so the snooze ends when main moves.
|
|
|
|
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=$({{BIN_DIR}}/gstack-paths --get GSTACK_STATE_ROOT 2>/dev/null) && _SRC=$(awk -F '\t' '$(1) == "{{HOST_ID}}" && $(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 `UPSTREAM_REF` and `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, through `bin/gstack-ff-update`, the same code the
|
|
session-update auto-upgrade uses: local edits that merge cleanly ride over the
|
|
update, and an edit that conflicts (or a staged change, or an unmerged file)
|
|
is saved in a rescue ref instead of being left as conflict markers:
|
|
```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; }
|
|
git fetch origin
|
|
# Channel ref (docs/releases.md): origin/stable for a team install, else origin/main
|
|
_CH=$(bash bin/gstack-update-channel.sh --fetch-ref 2>&1) || _CH=origin/main
|
|
echo "$_CH"; UPSTREAM_REF=$(printf '%s\n' "$_CH" | tail -1)
|
|
echo "UPSTREAM_REF=${UPSTREAM_REF:=origin/main}"
|
|
# Same pre-advance check as the auto-updater: the incoming release's Bun floor,
|
|
# judged by bash whatever this tool shell is (zsh does not word-split).
|
|
HOLD=$(UPSTREAM_REF="$UPSTREAM_REF" bash -c '. bin/gstack-bun-version.sh && gstack_bun_incoming_hold . "$UPSTREAM_REF"' 2>/dev/null) && { case "$HOLD" in bun-version-unreadable*) echo "BUN_VERSION_UNREADABLE: $HOLD; nothing was changed" >&2 ;; *) echo "BUN_TOO_OLD: $HOLD; nothing was changed" >&2 ;; esac; exit 1; }
|
|
PRE_UPGRADE_COMMIT=$(git rev-parse HEAD)
|
|
echo "PRE_UPGRADE_COMMIT=$PRE_UPGRADE_COMMIT"
|
|
# Past the release (by ancestry, or by VERSION in a depth-1 clone): wait for stable; nothing moves back.
|
|
if [ "$UPSTREAM_REF" = origin/stable ] && [ "$(git rev-parse origin/stable)" != "$PRE_UPGRADE_COMMIT" ] && ! git merge-base --is-ancestor HEAD origin/stable 2>/dev/null \
|
|
&& { git merge-base --is-ancestor origin/stable HEAD 2>/dev/null || [ "$(printf '%s\n%s\n' "$(git show origin/stable:VERSION)" "$(cat VERSION)" | sort -V | tail -1)" = "$(cat VERSION)" ]; }; then echo "AHEAD_OF_STABLE"; exit 0; fi
|
|
FF=$(bash bin/gstack-ff-update . "$UPSTREAM_REF"); FF_RC=$?
|
|
echo "$FF"
|
|
case "$FF_RC" in
|
|
0) if {{SETUP_COMMAND}} --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 ;;
|
|
4) echo "FF_REFUSED" ;;
|
|
*) echo "FF_STOPPED: $FF" >&2; exit 1 ;;
|
|
esac
|
|
```
|
|
|
|
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. When the line before it reads
|
|
`rescued <old> <new> refs/gstack/rescue/<oid> unmerged=<0|1>`, tell the user
|
|
their conflicting local edits were saved there and relay the `Inspect`,
|
|
`Export` and `Delete` commands the script printed (export into a scratch
|
|
checkout with `git apply -3`, never `stash apply` in the install;
|
|
`unmerged=1` means the rescue needs manual resolution).
|
|
On `FF_STOPPED`, STOP and relay the reason: nothing was changed.
|
|
On `AHEAD_OF_STABLE`, STOP: the checkout is already past the latest stable release, so nothing changes until the next cut (`gstack-config set update_channel main` follows every merge). On a `fetching origin/stable failed` line above, STOP and relay it; nothing was changed.
|
|
`ff-busy` means another gstack update is running (retry in a minute);
|
|
`autostash-unrecoverable` names what is in the way (an untracked file the
|
|
release adds, or another git command); `index-unreadable` means the checkout's
|
|
index is damaged.
|
|
On `BUN_TOO_OLD`, STOP: tell the user to run `bun upgrade`, then /gstack-upgrade again.
|
|
On `BUN_VERSION_UNREADABLE`, STOP: `bun --version` did not print a version gstack can read, so `bun upgrade` may not run either; tell the user to reinstall Bun from https://bun.sh (`curl -fsSL https://bun.sh/install | bash` in Git Bash or a macOS/Linux terminal), check that `bun --version` prints a version such as 1.4.2, then run /gstack-upgrade again (docs/troubleshooting.md#bun-version-unreadable).
|
|
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` (local commits or a diverged history); 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 "${UPSTREAM_REF:?}"..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; }
|
|
# Regenerable render-footprint dirt (setup re-renders it) never goes into the stash.
|
|
git checkout -- 'SKILL.md' '*/SKILL.md' '*/sections/*.md' 2>/dev/null || true
|
|
STASH_OUTPUT=$(git stash 2>&1)
|
|
git reset --hard "${UPSTREAM_REF:?UPSTREAM_REF is not set: substitute the value Step 4 printed}"
|
|
{{SETUP_COMMAND}} --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_COMMAND}} --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)
|
|
if [ "$_RESOLVED_LOCAL" != "$_RESOLVED_PRIMARY" ]; then
|
|
LOCAL_GSTACK="$_ROOT/.claude/skills/gstack"
|
|
fi
|
|
fi
|
|
_TEAM_MODE=$(~/.claude/skills/gstack/bin/gstack-config get team_mode 2>/dev/null || echo "false")
|
|
echo "LOCAL_GSTACK=$LOCAL_GSTACK"
|
|
echo "TEAM_MODE=$_TEAM_MODE"
|
|
```
|
|
|
|
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is `true`:** Remove the vendored copy. Team mode uses the global install as the single source of truth.
|
|
|
|
```bash
|
|
(cd -- "${LOCAL_GSTACK:?LOCAL_GSTACK is not set: re-run the detection block above and substitute the printed path}" && [ -f VERSION ] && [ -f setup ] && [ -f bin/gstack-config ]) || { echo "ERROR: LOCAL_GSTACK=${LOCAL_GSTACK:-} is not a gstack copy; nothing was changed." >&2; exit 1; }
|
|
_ROOT=${LOCAL_GSTACK%/.claude/skills/gstack}
|
|
{ [ "$_ROOT" != "$LOCAL_GSTACK" ] && cd -- "${_ROOT:?}" && _p=$(git rev-parse --show-prefix 2>/dev/null) && [ -z "$_p" ] && [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" = true ]; } || { echo "ERROR: $LOCAL_GSTACK is not the vendored copy at the root of a git repository; nothing was changed." >&2; exit 1; }
|
|
git rm -r --cached .claude/skills/gstack/ 2>/dev/null || true
|
|
if ! grep -qF '.claude/skills/gstack/' .gitignore 2>/dev/null; then
|
|
echo '.claude/skills/gstack/' >> .gitignore
|
|
fi
|
|
rm -rf "$LOCAL_GSTACK"
|
|
```
|
|
Tell user: "Removed vendored copy at `$LOCAL_GSTACK` (team mode active — global install is the source of truth). Commit the `.gitignore` change when ready."
|
|
|
|
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`:** Update it by copying from the freshly-upgraded primary install (same approach as README vendored install):
|
|
```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." >&2; exit 1; }
|
|
(cd -- "${LOCAL_GSTACK:?LOCAL_GSTACK is not set: re-run the detection block above and substitute the printed path}" && [ -f VERSION ] && [ -f setup ] && [ -f bin/gstack-config ]) || { echo "ERROR: LOCAL_GSTACK=${LOCAL_GSTACK:-} is not a gstack copy; nothing was changed." >&2; exit 1; }
|
|
[ -e "$LOCAL_GSTACK.bak" ] && { echo "ERROR: stale vendored backup; inspect it before retrying." >&2; exit 1; }
|
|
mv "$LOCAL_GSTACK" "$LOCAL_GSTACK.bak" || exit 1
|
|
if cp -Rf "$INSTALL_DIR" "$LOCAL_GSTACK" && rm -rf "$LOCAL_GSTACK/.git" && (cd "$LOCAL_GSTACK" && {{SETUP_COMMAND}} --refresh-registered); then
|
|
rm -rf "$LOCAL_GSTACK.bak"
|
|
echo "LOCAL_SYNC_OK"
|
|
else
|
|
rm -rf "$LOCAL_GSTACK"
|
|
mv "$LOCAL_GSTACK.bak" "$LOCAL_GSTACK" || { echo "ERROR: restore failed; backup retained." >&2; exit 1; }
|
|
echo "ERROR: sync failed; previous vendored copy restored." >&2
|
|
exit 1
|
|
fi
|
|
```
|
|
Only on `LOCAL_SYNC_OK`, tell user: "Also updated vendored copy at `$LOCAL_GSTACK` — commit `.claude/skills/gstack/` when you're ready." Otherwise stop and report the recovery outcome; do not continue migrations or announce success.
|
|
|
|
### Step 4.75: Run version migrations
|
|
|
|
After `./setup`, run the migrations in `gstack-upgrade/migrations/` (idempotent
|
|
`v{VERSION}.sh` scripts; CONTRIBUTING.md says how to add one) newer than the old
|
|
version: they fix state `./setup` alone cannot (stale config, orphaned files).
|
|
|
|
```bash
|
|
MIGRATIONS_DIR="${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}/gstack-upgrade/migrations"
|
|
: "${OLD_VERSION:?OLD_VERSION is not set: substitute the value Step 3 printed}"
|
|
if [ -d "$MIGRATIONS_DIR" ]; then
|
|
for migration in $(find "$MIGRATIONS_DIR" -maxdepth 1 -name 'v*.sh' -type f 2>/dev/null | sort -V); do
|
|
m_ver="$(basename "$migration" .sh | sed 's/^v//')"
|
|
if [ "$OLD_VERSION" != "unknown" ] && [ "$(printf '%s\n%s' "$OLD_VERSION" "$m_ver" | sort -V | head -1)" = "$OLD_VERSION" ] && [ "$OLD_VERSION" != "$m_ver" ]; then
|
|
echo "Running migration $m_ver..."
|
|
# Without GSTACK_INSTALL_DIR a migration that cleans the install assumes ~/.claude/skills/gstack.
|
|
GSTACK_INSTALL_DIR="$INSTALL_DIR" bash "$migration" || echo " Warning: migration $m_ver had errors (non-fatal)"
|
|
fi
|
|
done
|
|
fi
|
|
```
|
|
|
|
### Step 4.8: Stop any stale daemon (unconditional)
|
|
|
|
A browse daemon started before the upgrade keeps running the old binary (the
|
|
reset and `./setup` do not stop it), so always run this step with the install
|
|
directory from Step 2.
|
|
|
|
```bash
|
|
INSTALL_DIR_PLACEHOLDER="<install dir from Step 2>"
|
|
NEW_HASH=$(cat "$INSTALL_DIR_PLACEHOLDER/browse/dist/.version" 2>/dev/null || echo "")
|
|
_STATE_FILE="${BROWSE_STATE_FILE:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/browse.json}"
|
|
if [ -z "$NEW_HASH" ] || [ ! -f "$_STATE_FILE" ]; then
|
|
echo "DAEMON_CHECK=none (no state file or no fresh build hash)"
|
|
else
|
|
DAEMON_PID=$(sed -n 's/.*"pid"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$_STATE_FILE" | head -1)
|
|
DAEMON_PORT=$(sed -n 's/.*"port"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$_STATE_FILE" | head -1)
|
|
OLD_HASH=$(sed -n 's/.*"binaryVersion"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$_STATE_FILE" | head -1)
|
|
if [ -z "$DAEMON_PID" ] || ! kill -0 "$DAEMON_PID" 2>/dev/null; then
|
|
echo "DAEMON_CHECK=dead (no live daemon to stop)"
|
|
elif [ "$OLD_HASH" = "$NEW_HASH" ]; then
|
|
echo "DAEMON_CHECK=current (daemon already runs the new binary)"
|
|
elif curl -fsS --max-time 2 "http://127.0.0.1:$DAEMON_PORT/health" 2>/dev/null | grep -q '"status":"healthy"'; then
|
|
echo "DAEMON_CHECK=stale-responsive pid=$DAEMON_PID hash=${OLD_HASH:-unknown} -> $NEW_HASH"
|
|
"$INSTALL_DIR_PLACEHOLDER/browse/dist/browse" stop && echo "DAEMON_STOPPED=yes"
|
|
else
|
|
echo "DAEMON_CHECK=stale-busy pid=$DAEMON_PID hash=${OLD_HASH:-unknown} -> $NEW_HASH"
|
|
fi
|
|
fi
|
|
```
|
|
|
|
Substitute the install directory from Step 2, then act on `DAEMON_CHECK`:
|
|
|
|
1. **`stale-responsive` + `DAEMON_STOPPED=yes`:** Tell the user "Stopped the
|
|
old browse daemon (binary {OLD_HASH} → {NEW_HASH}). The next browse command
|
|
starts a fresh daemon on the new binary."
|
|
2. **`stale-busy`:** The daemon runs the old binary but is mid-work — DEFER to
|
|
it, never kill a busy daemon during upgrade. Tell the user: "A browse daemon
|
|
is still running the pre-upgrade binary ({OLD_HASH} → {NEW_HASH}) but is
|
|
busy right now. When it finishes, stop it with `browse stop` — or force it
|
|
immediately with `browse --force-restart stop` (loses that session's
|
|
tabs/cookies/logins)."
|
|
3. **`none` / `dead` / `current`:** say nothing.
|
|
|
|
### Step 5: Write marker + clear cache
|
|
|
|
```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}"
|
|
mkdir -p "$GSTACK_STATE_ROOT"
|
|
echo "$OLD_VERSION" > "$GSTACK_STATE_ROOT"/just-upgraded-from
|
|
rm -f "$GSTACK_STATE_ROOT"/last-update-check
|
|
rm -f "$GSTACK_STATE_ROOT"/update-snoozed
|
|
```
|
|
|
|
### Step 6: Show What's New
|
|
|
|
Read `$INSTALL_DIR/CHANGELOG.md`. Find all version entries between the old version and the new version. Summarize as 5-7 bullets grouped by theme, user-facing changes first.
|
|
|
|
Four fields: summarize `git -C "$INSTALL_DIR" log --oneline <PRE_UPGRADE_COMMIT>..HEAD` (≤7 bullets) as "gstack v{old}, main at {sha}".
|
|
|
|
Format:
|
|
```
|
|
gstack v{new} — upgraded from v{old}!
|
|
|
|
What's new:
|
|
- [bullet 1]
|
|
- [bullet 2]
|
|
- ...
|
|
|
|
Happy shipping!
|
|
```
|
|
|
|
### Step 7: Continue
|
|
|
|
Continue with the skill the user originally invoked; the upgrade is done.
|
|
|
|
---
|
|
|
|
## Standalone usage
|
|
|
|
When invoked directly as `/gstack-upgrade` (not from a preamble):
|
|
|
|
1. Force a fresh update check (bypass cache):
|
|
```bash
|
|
~/.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || \
|
|
.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || true
|
|
```
|
|
|
|
2. If `UPGRADE_AVAILABLE` (three or four fields): follow Steps 2-6 above.
|
|
|
|
If `CHECK_FAILED ...`: the remote version could not be read, so the update
|
|
status is unknown. Show the line (it names the URL that failed) and tell the
|
|
user the check failed; never report "already on the latest version". Stop.
|
|
|
|
3. If no output (primary is up to date): check for a stale local vendored copy.
|
|
|
|
Run the Step 2 bash block above to detect the primary install type and directory (`INSTALL_TYPE` and `INSTALL_DIR`). Then run the Step 4.5 detection bash block above to check for a local vendored copy (`LOCAL_GSTACK`) and team mode status (`TEAM_MODE`).
|
|
|
|
**If `LOCAL_GSTACK` is empty** (no local vendored copy): tell the user "You're already on the latest version (v{version})."
|
|
|
|
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is `true`:** Remove the vendored copy using the Step 4.5 team-mode removal bash block above. Tell user: "Global v{version} is up to date. Removed stale vendored copy (team mode active). Commit the `.gitignore` change when ready."
|
|
|
|
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`**, compare versions:
|
|
```bash
|
|
PRIMARY_VER=$(cat "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}/VERSION" 2>/dev/null || echo "unknown")
|
|
LOCAL_VER=$(cat "${LOCAL_GSTACK:?LOCAL_GSTACK is not set: re-run the Step 4.5 detection block}/VERSION" 2>/dev/null || echo "unknown")
|
|
echo "PRIMARY=$PRIMARY_VER LOCAL=$LOCAL_VER"
|
|
```
|
|
|
|
**If versions differ:** follow the Step 4.5 sync bash block above to update the local copy from the primary. Tell user: "Global v{PRIMARY_VER} is up to date. Updated local vendored copy from v{LOCAL_VER} → v{PRIMARY_VER}. Commit `.claude/skills/gstack/` when you're ready."
|
|
|
|
**If versions match:** tell the user "You're on the latest version (v{PRIMARY_VER}). Global and local vendored copy are both up to date."
|