关于 gstack-upgrade
gstack-upgrade 是用于更新 gstack 并说明变更内容的技能。它处理 Git 和 vendored 安装,会检查本地更改,并为更新失败提供恢复指引。
主要功能
- 01检查并应用 gstack 更新
- 02支持 Git 和 vendored 安装
- 03保留或报告冲突的本地更改
- 04刷新已注册的安装
适用场景
- 01有新版本时更新 gstack
- 02更新中止或失败时查看恢复步骤
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ garrytan/ gstack --skill gstack-upgrade安装后在 Claude Code 中输入/gstack-upgrade确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
SKILL.md.tmplmigrations/v0.15.2.0.shv0.16.2.0.shv1.0.0.0.shv1.1.3.0.shv1.17.0.0.shv1.27.0.0.shv1.37.0.0.shv1.38.1.0.shv1.40.0.0.shv1.58.0.0.shv1.65.0.0.shv1.67.0.0.shv1.78.0.0.shv1.86.0.0.shv1.91.19.0.shv1.91.65.0.sh详细介绍
/gstack-upgrade
将 gstack 升级到最新版本,并查看新增内容。
内联升级流程
所有技能前置内容在检测到 UPGRADE_AVAILABLE 时都会引用本节。
步骤 1:询问用户(或自动升级)
首先,检查是否启用了自动升级:
_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"
如果 AUTO_UPGRADE=true 或 AUTO_UPGRADE=1: 跳过 AskUserQuestion。记录“正在将 gstack 从 v{old} 升级到 v{new}……”,然后直接继续步骤 2。如果设置失败,请按照步骤 4 中针对安装类型的恢复流程操作:vendored 安装恢复其备份;git 安装停止,并记录升级前的提交,不执行破坏性重置。除非确实恢复成功,否则绝不要声称已恢复。
否则,使用 AskUserQuestion:
- 问题:“gstack v{new} 已推出(你正在使用 v{old})。现在升级吗?”
四字段形式(
UPGRADE_AVAILABLE <old> <old>+<n> <sha>,main 有更新):询问“gstack main 比 v{old} 领先 {n} 个提交(当前为 {sha})。现在升级吗?” - 选项:["是,现在升级", "始终让我保持最新", "暂不", "不再询问"]
如果选择“是,现在升级”: 继续步骤 2。
如果选择“始终让我保持最新”:
~/.claude/skills/gstack/bin/gstack-config set auto_upgrade true
告知用户:“已启用自动升级。今后将自动安装更新。”然后继续步骤 2。
如果选择“暂不”: 写入延后状态,并采用逐步延长的间隔(第一次延后 = 24 小时,第二次 = 48 小时,第三次及之后 = 1 周),然后继续使用当前技能。不要再提及此次升级。
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"
注意:{new} 是 UPGRADE_AVAILABLE 输出中的远程版本——请从更新检查结果中替换该值。对于四字段形式的行,使用 SHA(第四个字段)作为 {new},这样 main 更新后延后状态就会结束。
告知用户延后时长:“下次提醒时间:24 小时后”(或根据级别显示 48 小时后或 1 周后)。提示:“在 ~/.gstack/config.yaml 中设置 auto_upgrade: true 可启用自动升级。”
如果选择“不再询问”:
~/.claude/skills/gstack/bin/gstack-config set update_check false
告知用户:“已禁用更新检查。运行 ~/.claude/skills/gstack/bin/gstack-config set update_check true 可重新启用。”
继续使用当前技能。
步骤 2:检测安装类型
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"
上方输出的安装类型和绝对目录将在后续所有步骤中使用。
明确传递 INSTALL_TYPE、INSTALL_DIR、OLD_VERSION,以及之后的 UPSTREAM_REF 和 NEW_VERSION:工具调用可能会在新的 shell 中运行,因此后续每个代码块开始时都要从已捕获的输出中为这些变量赋值(例如 INSTALL_DIR=/abs/path/printed/above)。不要依赖先前调用的工作目录或 shell 变量。每个更改文件的代码块都必须先检查 INSTALL_DIR;如果其未设置、为空、目录不存在、不可读,或不是 gstack 自身的检出目录,则停止且不要执行任何 git 命令。
步骤 3:保存旧版本
使用步骤 2 输出中的安装目录:
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"
步骤 4:升级
使用步骤 2 中检测到的安装类型和目录:
对于 git 安装(global-git、local-git):
先通过 bin/gstack-ff-update 快进更新;它与 session-update 自动升级使用相同的代码:可以干净合并的本地编辑会随更新保留,而发生冲突的编辑(或暂存的更改、未合并的文件)会保存到救援引用中,而不是留下冲突标记:
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 --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 的 --refresh-registered 会刷新此检出注册的每个安装(每个主机、全局安装和此项目的安装),先打印来源,最后输出每个安装各占一行的 Upgrade summary。按原样转达这些行。failed 行表示该主机保留了之前的安装;请提供其重试命令,切勿声称所有主机都已刷新。skipped 行属于其他检出或项目;转达其命令,但不要运行。
如果输出以 FF_OK 结尾,则升级已完成——完全跳过下面的回退流程。如果该行之前显示 rescued <old> <new> refs/gstack/rescue/<oid> unmerged=<0|1>,请告知用户,其有冲突的本地编辑已保存到该位置,并转达脚本打印的 Inspect、Export 和 Delete 命令(导出到临时检出,并使用 git apply -3;切勿在安装目录中使用 stash apply;unmerged=1 表示救援内容需要手动解决)。
遇到 FF_STOPPED 时,停止并转达原因:没有任何更改。
遇到 AHEAD_OF_STABLE 时,停止:此检出已领先于每周发布版本,因此在下次发布之前不会有任何更改(每次合并后都会执行 gstack-config set update_channel main)。如果上方出现 fetching origin/stable failed 行,停止并转达该行;没有任何更改。
ff-busy 表示另一个 gstack 更新正在运行(请在一分钟后重试);autostash-unrecoverable 会指出阻碍更新的内容(发布版本新增的未跟踪文件,或另一个 git 命令);index-unreadable 表示检出的索引已损坏。
遇到 BUN_TOO_OLD 时,停止:告知用户运行 bun upgrade,然后再次运行 /gstack-upgrade。
遇到 BUN_VERSION_UNREADABLE 时,停止:bun --version 未输出 gstack 能识别的版本,因此 bun upgrade 也可能无法运行;告知用户从 https://bun.sh 重新安装 Bun(在 Git Bash 或 macOS/Linux 终端中运行 curl -fsSL https://bun.sh/install | bash),确认 bun --version 输出类似 1.4.2 的版本,然后再次运行 /gstack-upgrade(docs/troubleshooting.md#bun-version-unreadable)。
遇到 SETUP_FAILED 时,停止;保留用户更改并报告恢复提交。git 路径上没有 .bak。切勿仅因 setup 失败就进入分歧回退流程。只有遇到 FF_REFUSED(本地提交或历史分歧)时才进入该流程;网络/身份验证失败应停止并修复,不要重置。
回退(仅快进被拒绝——本地提交或分歧)。 git reset --hard 会销毁内容:即使工作区干净,未推送的本地提交仍会丢失。请先确认:
- 在
$INSTALL_DIR中运行git status --porcelain和git rev-list "${UPSTREAM_REF:?}"..HEAD --oneline。 - 如果两者都为空,则可以确定重置是安全的——无需询问,直接运行下面的回退代码块。
- 否则,通过 AskUserQuestion 询问(不可逆操作——有破坏性),准确列出将被丢弃的内容:每个有更改的文件,以及每个未推送提交的哈希值和主题。选项:A) 丢弃这些内容并升级(重置)——必须明确回复字母;B) 取消升级,让用户先自行挽救工作(存在本地提交时推荐)。回复含糊时绝不可继续。
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 --refresh-registered
如果 $STASH_OUTPUT 包含 "Saved working directory",请提醒用户:"注意:本地更改已暂存(任何已修改的生成文件 SKILL.md/sections 文件都已先行丢弃——它们会在 setup 时重新生成)。运行 git stash pop,在技能目录中恢复你自己的更改。"
对于 vendored 安装(vendored、vendored-global):
(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
第 4.5 步:处理本地 vendored 副本
使用第 2 步中的安装目录。检查是否还有本地 vendored 副本,以及团队模式是否已启用:
_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"
如果 LOCAL_GSTACK 非空且 TEAM_MODE 为 true: 删除 vendored 副本。团队模式使用全局安装作为唯一事实来源。
(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"
告知用户:“已删除 $LOCAL_GSTACK 处的 vendored 副本(团队模式已启用——全局安装是唯一事实来源)。准备好后,请提交 .gitignore 更改。”
如果 LOCAL_GSTACK 非空且 TEAM_MODE 不为 true: 从刚升级的主安装复制内容来更新它(采用与 README vendored 安装相同的方法):
(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 --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
仅当 LOCAL_SYNC_OK 时,告知用户:“也已更新 $LOCAL_GSTACK 处的 vendored 副本——准备好后请提交 .claude/skills/gstack/。”否则停止并报告恢复结果;不要继续迁移或宣布成功。
第 4.75 步:运行版本迁移
运行 ./setup 后,执行 gstack-upgrade/migrations/ 中比旧版本更新的迁移(幂等的
v{VERSION}.sh 脚本;CONTRIBUTING.md 说明了如何添加迁移脚本)。这些迁移会修复仅靠 ./setup 无法处理的状态问题(过时的配置、孤立文件)。
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
第 4.8 步:停止任何过时的守护进程(无条件)
升级前启动的浏览守护进程仍在运行旧二进制文件(重置和 ./setup 都不会停止它),因此始终使用第 2 步中的安装目录执行此步骤。
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
替换为第 2 步中的安装目录,然后根据 DAEMON_CHECK 采取相应操作:
stale-responsive+DAEMON_STOPPED=yes: 告知用户:“已停止旧版浏览守护进程(二进制文件 {OLD_HASH} → {NEW_HASH})。下一条浏览命令会使用新二进制文件启动一个全新的守护进程。”stale-busy: 守护进程正在运行旧二进制文件,但正忙于处理任务——交由它继续运行,升级期间绝不要杀掉忙碌的守护进程。告知用户:“浏览守护进程仍在运行升级前的二进制文件({OLD_HASH} → {NEW_HASH}),但当前正忙。任务完成后,使用browse stop停止它——或者立即使用browse --force-restart stop强制停止(会丢失该会话的标签页、Cookie 和登录状态)。”none/dead/current: 不作说明。
第 5 步:写入标记并清除缓存
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
第 6 步:展示更新内容
读取 $INSTALL_DIR/CHANGELOG.md。找出旧版本和新版本之间的所有版本条目。按主题分组总结为 5–7 条要点,并优先介绍面向用户的更改。
四个字段:将 git -C "$INSTALL_DIR" log --oneline <PRE_UPGRADE_COMMIT>..HEAD 的结果(≤7 条要点)总结为“gstack v{old},main 位于 {sha}”。
格式:
gstack v{new} — upgraded from v{old}!
What's new:
- [bullet 1]
- [bullet 2]
- ...
Happy shipping!
第 7 步:继续
继续执行用户最初调用的技能;升级已完成。
单独使用
如果直接调用 /gstack-upgrade(而非从前置提示中调用):
- 强制执行全新更新检查(绕过缓存):
~/.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || \
.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || true
-
如果为
UPGRADE_AVAILABLE(三个或四个字段):执行上文的步骤 2-6。如果为
CHECK_FAILED ...:无法读取远程版本,因此更新状态未知。显示该行(其中列出了检查失败的 URL),并告知用户检查失败;绝不要报告“已是最新版本”。停止。 -
如果没有输出(主安装已是最新版本):检查是否存在过时的本地 vendored 副本。
运行上文的步骤 2 bash 代码块,以检测主安装类型和目录(INSTALL_TYPE 和 INSTALL_DIR)。然后运行上文的步骤 4.5 检测 bash 代码块,以检查是否存在本地 vendored 副本(LOCAL_GSTACK)以及团队模式状态(TEAM_MODE)。
如果 LOCAL_GSTACK 为空(没有本地 vendored 副本):告知用户“您已是最新版本(v{version})。”
**如果 LOCAL_GSTACK 非空且 TEAM_MODE 为 true:**使用上文的步骤 4.5 团队模式移除 bash 代码块删除 vendored 副本。告知用户:“全局 v{version} 已是最新版本。已移除过时的 vendored 副本(团队模式已启用)。准备好后提交 .gitignore 更改。”
如果 LOCAL_GSTACK 非空且 TEAM_MODE 不为 true,比较版本:
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"
**如果版本不同:**按照上文的步骤 4.5 同步 bash 代码块,将本地副本更新为主安装的版本。告知用户:“全局 v{PRIMARY_VER} 已是最新版本。已将本地 vendored 副本从 v{LOCAL_VER} 更新为 v{PRIMARY_VER}。准备好后提交 .claude/skills/gstack/。”
**如果版本相同:**告知用户“您已是最新版本(v{PRIMARY_VER})。全局和本地 vendored 副本均已是最新版本。”