get-shit-done(GSD)更新机制深度解析:从版本检测到补丁回放的完整升级链路
/gsd:update 是 get-shit-done(GSD)系统内置的自我升级命令,负责通过 npm 检测新版本、拉取并展示变更日志、征得用户确认后执行带缓存清理的干净安装。本文以 get-shit-done/workflows/update.md 为骨架,结合仓库中的命令定义、版本检查脚本、SessionStart Hook 与测试用例,完整拆解这条升级链路的八个步骤,并说明 --sync、--reapply 两个扩展标志的用途,帮助读者理解 GSD 如何在多 AI 运行时(Claude Code、OpenAI Codex、Gemini CLI、Kilo、OpenCode)下安全地完成自我更新,同时保住用户的本地自定义内容。
一、命令入口:/gsd:update 的定义与标志路由
/gsd:update 的命令契约定义在 commands/gsd/update.md 中,其 frontmatter 声明了参数提示 [--sync | --reapply],并允许使用 Read、Write、Edit、Bash、Glob、Grep、AskUserQuestion 等工具。命令的 execution_context 直接指向 get-shit-done/workflows/update.md,即本文要解析的核心工作流。
命令根据 $ARGUMENTS 的第一个 token 做三路路由:
| 参数 | 行为 |
|---|---|
--sync |
剥离该标志后转交 sync-skills 工作流,跨运行时根目录同步受管 GSD skills,支持 --from、--to、--dry-run、--apply 子标志,保证多运行时用户在更新后保持一致 |
--reapply |
转交 reapply-patches 工作流,用三方比较(原始基线、用户修改备份、新安装版本)把用户本地修改合并回新版本 |
| (无标志) | 标准更新:检查新版本 → 展示变更日志 → 执行安装 |
标准更新路径是本文的主角,其完整流程在 update.md 的 <process> 中分为八个 step:
get_installed_version:检测本地/全局安装并校验完整性check_latest_version:通过确定性脚本查询 npm 最新版本compare_versions:比较已装版本与最新版本show_changes_and_confirm:更新前拉取并展示变更日志,获取用户确认backup_custom_files:安装前备份 GSD 管理目录内的用户自建文件run_update:按检测到的安装类型执行干净安装并清理更新缓存display_result:格式化完成消息并提示重启运行时check_local_patches:检查安装器是否备份了被本地修改的文件
<success_criteria> 则定义了成功的硬性标准:正确读取已装版本、经 npm 检查最新版本、已是最新时跳过、在更新前展示变更日志、展示干净安装警告、获得用户确认、更新成功执行、展示重启提醒。
二、Step 1:安装检测——在多运行时与自定义 config-dir 下的精确定位
更新流程的第一步是回答三个问题:GSD 装在哪、是本地还是全局安装、属于哪个运行时。其核心是解析 execution_context 中的路径推导 PREFERRED_CONFIG_DIR 与 PREFERRED_RUNTIME:
- 路径含
/get-shit-done/workflows/update.md时,剥掉该后缀得到PREFERRED_CONFIG_DIR - 路径含
/.codex/→codex;/.gemini/antigravity/→antigravity;/.gemini/→gemini - 路径含
/.config/kilo/、/.kilo/,或 config dir 含kilo.json/kilo.jsonc→kilo - 路径含
/.config/opencode/、/.opencode/,或 config dir 含opencode.json/opencode.jsonc→opencode - 否则默认 →
claude
这样设计的意义在于:使用自定义 --config-dir 安装的用户,其配置目录不在任何默认路径下,必须优先信任 execution_context 指向的目录,而不是机械地扫默认目录。同时,PREFERRED_RUNTIME 作为第一个被检查的运行时,保证 /gsd:update 精确命中发起调用的运行时,而不是误更新同机器上其他运行时的副本。
2.1 运行时候选与优先级重排
工作流用数组存储 <runtime>:<config-dir> 候选(用数组而非空格分隔字符串,是为了在 bash 与 zsh 下都正确迭代,修复了 #1173):
RUNTIME_DIRS=( "claude:.claude" "opencode:.config/opencode" "opencode:.opencode" "antigravity:.gemini/antigravity" "gemini:.gemini" "kilo:.config/kilo" "kilo:.kilo" "codex:.codex" )
如果 PREFERRED_RUNTIME 尚未推导出来,则依次探测各运行时的环境变量:CODEX_HOME、ANTIGRAVITY_CONFIG_DIR、GEMINI_CONFIG_DIR、KILO_CONFIG_DIR/KILO_CONFIG、OPENCODE_CONFIG_DIR/OPENCODE_CONFIG、CLAUDE_CONFIG_DIR,全部缺失时回退 claude。KILO_CONFIG_DIR → dirname(KILO_CONFIG) → XDG_CONFIG_HOME/kilo → ~/.config/kilo 的优先级必须与安装器保持一致。
随后,preferred runtime 的条目被提到数组最前(ORDERED_RUNTIME_DIRS/ORDERED_ENV_RUNTIME_DIRS),保证优先检查目标运行时,其余运行时按固定顺序兜底。
2.2 快路径:execution_context 已指向有效安装
如果 PREFERRED_CONFIG_DIR 下存在 get-shit-done/VERSION 或 get-shit-done/workflows/update.md 标记文件,直接信任该路径并快速返回,覆盖了不在默认目录下的自定义 --config-dir 安装。这段逻辑里有一个值得注意的细节:Windows(Git Bash)下 pwd 返回 POSIX 风格 /c/Users/...,而 PREFERRED_CONFIG_DIR 可能携带 C:/Users/...,因此先用 normalize_path() 把盘符路径统一转成 POSIX 形式再比较,避免本地/全局误判。快路径必须输出四行结果(4-line output contract,源自 #2993 代码评审),否则下游 check_latest_version 会把安装误判为 UNKNOWN。
2.3 慢路径:扫描本地与全局
当没有可信任的 execution_context 路径时,工作流按序扫描:
- 本地优先:遍历
ORDERED_RUNTIME_DIRS,检查./$dir/get-shit-done/VERSION或update.md标记; - 全局(环境变量派生):遍历
ENV_RUNTIME_DIRS(由CLAUDE_CONFIG_DIR、GEMINI_CONFIG_DIR、KILO_CONFIG_DIR/KILO_CONFIG/XDG_CONFIG_HOME、OPENCODE_CONFIG_DIR/OPENCODE_CONFIG/XDG_CONFIG_HOME、CODEX_HOME构造的绝对路径候选); - 全局(默认位置):若上一步未命中,再扫
$HOME/$dir默认目录; - 本地/全局判定:仅当本地与全局解析出的目录不同(防止
CWD=$HOME时误判)且VERSION文件通过^[0-9]+\.[0-9]+\.[0-9]+正则校验时才认定IS_LOCAL=true。
最终输出四行契约:
行 1:已装版本(0.0.0 表示未知版本)
行 2:安装范围(LOCAL / GLOBAL / UNKNOWN)
行 3:目标运行时(claude / opencode / gemini / kilo / codex)
行 4:解析出的 GSD 配置目录(如 /Users/me/.claude),UNKNOWN 时为空;记为 GSD_DIR 传给后续步骤
若同时检测到多个运行时安装且无法从 execution_context 判定发起者,工作流要求先询问用户更新哪个运行时,而不是擅自挑选。若 VERSION 文件缺失,则按版本 0.0.0 对待,直接进入全新安装流程,并提示用户“你的安装不包含版本跟踪”。
三、Step 2:确定性 npm 版本检查——包名是常量,不是自由选择
查询最新版本必须走仓库自带的确定性脚本 get-shit-done/bin/check-latest-version.cjs,严禁直接运行 npm view 或 npm search。这条禁令的由来记录在脚本头注释与测试 tests/bug-2992-check-latest-version.test.cjs 中:此前工作流以自然语言指示 LLM“运行 npm view get-shit-done-cc version”,执行模型可能偷懒或臆造出 @get-shit-done/cli、get-shit-done-cli、gsd 等错误包名,这些查询要么 404,要么更糟——命中无关的抢注(typosquat)包。因此:
// Hardcoded. Do not parameterise — the whole point of this script is that
// the package name is not a runtime choice for the caller.
const PACKAGE_NAME = 'get-shit-done-cc';
脚本的返回值是结构化 JSON { ok, version, reason, detail? },reason 取自有穷枚举 CHECK_REASON:ok、fail_npm_failed、fail_invalid_output。它通过 shell 投影层 execNpm(['view', PACKAGE_NAME, 'version'], { timeout: 15_000 }) 执行 npm,15 秒上限防止注册表挂起阻塞整个 /gsd:update(#2993 代码评审要求),并区分三种失败形态:
- 超时(status 为 null、signal 被设置、stderr 为空)→
detail: npm timed out (signal: SIGTERM) - npm 真实报错(stderr 非空)→
detail取裁剪后的 stderr - 其他非零退出 →
detail: npm exited non-zero
SEMVER_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/ 严格校验输出必须是合法 semver(含预发布版本),否则返回 fail_invalid_output。测试用例用可注入的 spawn 伪造 npm 输出,验证了成功路径、404 路径、超时路径、HTML 页面误作 stdout 的路径以及预发布版本 1.40.0-rc.1 的接受逻辑,且断言 PACKAGE_NAME 恒等于 get-shit-done-cc、没有任何调用方能覆盖它。
工作流侧通过 node "$GSD_DIR/get-shit-done/bin/check-latest-version.cjs" --json 调用并仅在脚本真正执行时用 jq 解析;当 GSD_DIR 为空(范围 UNKNOWN)时跳过检查,用无操作值种子化 LATEST_OK=false、LATEST_REASON="no_install_detected",避免下游把未设置的 LATEST_RESULT 误当成网络检查失败(#2993 反馈)。当 LATEST_OK 不为 true 或退出码非零时,工作流打印 Couldn't check for updates (reason: ...) 并提供手动更新命令 npx -y --package=get-shit-done-cc@latest -- get-shit-done-cc --global 后退出。
四、Step 3:版本比较——三种分支三种结局
拿到已装版本与最新版本后,compare_versions 处理三种情况:
已装 == 最新:直接输出 You're already on the latest version. 并退出,不打扰用户。
已装 > 最新:判定为开发版安装。工作流会打印“你领先于最新发布——这看起来是开发版安装”,并特别给出状态栏出现 ⚠ dev install — re-run installer to sync hooks 警告时的修复指引:这代表 hook 文件比 VERSION 文件旧,应从开发分支重新运行 node bin/install.js --global --claude。明确警告此时不要用 /gsd:update——它只会安装 npm 发布版(A.B.C)并把开发版降级。
已装 < 最新:进入 Step 4,先展示“有什么新东西”再安装。
五、Step 4:变更日志展示与用户确认——先看再装
更新可用时,工作流先从 GitHub raw URL 拉取 changelog,提取已装版本与最新版本之间的条目,在更新之前展示预览,同时给出干净安装的明确警告:
## GSD Update Available
**Installed:** 1.5.10
**Latest:** 1.5.15
### What's New
────────────────────────────────────────────────────────────
## [1.5.15] - 2026-01-20
### Added
- Feature X
## [1.5.14] - 2026-01-18
### Fixed
- Bug fix Y
────────────────────────────────────────────────────────────
⚠️ Note: The installer performs a clean install of GSD folders:
- `commands/gsd/` will be wiped and replaced
- `get-shit-done/` will be wiped and replaced
- `agents/gsd-*` files will be replaced
警告明确列出会被清空替换的目录(相对检测到的运行时安装位置),同时强调其他位置的用户自定义内容会被保留:不在 commands/gsd/ 下的自定义命令、不以 gsd- 前缀命名的自定义 agent、自定义 hooks、用户的 CLAUDE.md 文件。被直接修改过的 GSD 文件则自动备份到 gsd-local-patches/,更新后可用 /gsd:update --reapply 重新应用。
交互确认环节使用 AskUserQuestion(问题“Proceed with update?”,选项“Yes, update now”/“No, cancel”)。文本模式(配置中 workflow.text_mode: true 或命令行 --text 标志,即 $ARGUMENTS 含 --text 或 init JSON 的 text_mode 为 true)下,每个 AskUserQuestion 被替换为带编号的纯文本列表,让用户输入数字选择——这是为非 Claude 运行时(OpenAI Codex、Gemini CLI 等不支持 AskUserQuestion 的运行时)准备的必要降级方案。用户取消则直接退出。
六、Step 5:自定义文件备份——安装器不认识的文件先救出来
干净安装会清空 GSD 管理目录,但用户自行添加、安装器不知道的文件会被一并删除。因此安装前需要检测这类文件:存在于磁盘但不在 gsd-file-manifest.json 清单中的文件。
工作流特别强调:不要用 bash 路径裁剪(${filepath#$RUNTIME_DIR/})或内联 node -e require()——当 $RUNTIME_DIR 未设置时这些写法会失败,裁剪出的相对路径也可能与 manifest 键格式不匹配,导致即使存在自定义文件也统计出 CUSTOM_COUNT=0(bug #1997)。正确做法是优先用 gsd-sdk query detect-custom-files,否则用随附的 gsd-tools.cjs detect-custom-files,二者都通过 Node.js path.relative() 可靠解析路径。
流程为:先按 INSTALL_SCOPE 确定 RUNTIME_DIR(LOCAL 用 LOCAL_DIR,GLOBAL 用 GLOBAL_DIR,否则置空并跳过本步);再执行 detect-custom-files 拿到 {custom_files, custom_count} JSON;若 CUSTOM_COUNT > 0,把每个文件复制到 $RUNTIME_DIR/gsd-user-files-backup/(保留相对路径层级,单文件复制失败仅告警不中断):
BACKUP_DIR="$RUNTIME_DIR/gsd-user-files-backup"
mkdir -p "$BACKUP_DIR"
完成后提示用户:
⚠️ Found N custom file(s) inside GSD-managed directories.
These have been backed up to gsd-user-files-backup/ before the update.
Restore them after the update if needed.
七、Step 6:执行更新——按安装类型路由并清理更新缓存
安装命令由 Step 1 检测出的目标运行时与安装范围共同决定,RUNTIME_FLAG="--$TARGET_RUNTIME":
| 安装范围 | 执行命令 |
|---|---|
| LOCAL | npx -y --package=get-shit-done-cc@latest -- get-shit-done-cc "$RUNTIME_FLAG" --local |
| GLOBAL | npx -y --package=get-shit-done-cc@latest -- get-shit-done-cc "$RUNTIME_FLAG" --global |
| UNKNOWN | npx -y --package=get-shit-done-cc@latest -- get-shit-done-cc --claude --global |
安装失败则展示错误并退出。成功后进入缓存清理环节——这是很多人忽略但至关重要的一步:SessionStart Hook hooks/gsd-check-update.js 每次会话在后台检查更新并把结果写入缓存,状态栏据此显示 ⬆ /gsd:update 指示器;如果更新后不清理缓存,这个过期的“有新版本”指示会一直赖着不走。
清理策略是宁可多清不可漏清:依次遍历 PREFERRED_CONFIG_DIR、CLAUDE_CONFIG_DIR、GEMINI_CONFIG_DIR、Kilo 与 OpenCode 的环境变量/XDG 路径、CODEX_HOME 构造的目录集合,删除各目录下 cache/gsd-update-check.json;再兜底清理全部默认运行时目录(.claude、.config/opencode、.opencode、.gemini/antigravity、.gemini、.config/kilo、.kilo、.codex)的本地与 $HOME 全局两份缓存。最后还要清理 hook 写入的共享工具无关缓存 ~/.cache/gsd/gsd-update-check.json(#2784):gsd-check-update.js 使用这个跨运行时统一的缓存目录,正是为了避免多运行时解析不一致导致 check 写一处、statusline 读另一处(#1421)。
从 hooks/gsd-check-update-worker.js 可以看到后台检查的实现:父 hook 以 detached: true 派生 worker 子进程(Windows 上必须 detached 才能正确脱离),通过环境变量传入缓存文件与本地/全局 VERSION 文件路径;worker 用 isNewer() 做三段式 semver 比较(剥离 -beta.1 等预发布后缀防止 Number() 产生 NaN),顺带检查 13 个受管 hook 的版本头(// gsd-hook-version: ... 注释,兼容 JS 与 bash 两种注释风格)以报告 stale hooks,最后通过 npm view get-shit-done-cc version(Windows 下 shell: true 经 cmd.exe 解析 npm.cmd,POSIX 保持无 shell 派生)把 {update_available, installed, latest, checked, stale_hooks} 写入缓存文件。
八、Step 7:结果展示——完成消息与重启提醒
安装完成后输出格式化完成框,由于 changelog 已在确认环节展示,这里不再重复:
╔═══════════════════════════════════════════════════════════╗
║ GSD Updated: v1.5.10 → v1.5.15 ║
╚═══════════════════════════════════════════════════════════╝
⚠️ Restart your runtime to pick up the new commands.
[View full changelog](https://github.com/gsd-build/get-shit-done/blob/main/CHANGELOG.md)
重启提醒是硬性要求:新命令、新 hook 只有重启运行时才会被加载。仓库内实际的版本发布记录可对照 CHANGELOG.md 中的历次条目,例如 /gsd:update 会始终安装最新包版本、尊重本地/全局安装位置、先展示 changelog 再征询确认,以及状态栏在存在新版本时显示 ⬆ /gsd:update 指示器。
九、Step 8:本地补丁检查与 --reapply 回放
更新完成后,工作流检查配置目录中是否存在 gsd-local-patches/backup-meta.json。若安装器在更新前检测并备份了被本地修改的文件,则提示:
Local patches were backed up before the update.
Run `/gsd:update --reapply` to merge your modifications into the new version.
--reapply 分支对应 get-shit-done/workflows/reapply-patches.md,其核心是三方比较:
- 原始基线(pristine baseline):更新前 GSD 发布版中的原始文件
- 用户版本:备份在
gsd-local-patches/中的修改后文件 - 新版本:更新后新装的文件
合并规则清晰:仅用户改过的区块 → 采用用户版本;仅上游改过的区块 → 接受上游版本;双方都改 → 标记 CONFLICT 展示双方内容请用户裁决;都没改 → 使用新版本。
基线来源按优先级选择:最可靠的是 backup-meta.json 中记录的 pristine_hashes(文件在更新前 GSD 发布版中的 SHA-256)配合 git 历史定位正确基线提交——直接用 git log --diff-filter=A 找“首次添加该文件”的提交在经历过多次更新循环的仓库里是错误的基线;其次是安装时保存的 gsd-pristine/ 快照目录;两者皆无时降级为强化启发式的两路比较。工作流还设定了不可违背的不变量:凡是进入 gsd-local-patches/ 的文件,必然被安装器的哈希比较判定为已修改,因此“无自定义内容”永远不是合法结论,拿不准时必须归类为 CONFLICT 而非 SKIP。
合并后还有两道验证闸门:5a 是绑定闸门——运行确定性验证脚本 verify-reapply-patches 对应的 get-shit-done/bin/verify-reapply-patches.cjs,结构化对比用户新增行是否真实存在于合并结果,任何缺失即非零退出并阻止后续清理(bug #2969 曾抓到自由文本“verified: yes”表格在未做实际内容检查时反复误报);5b 是咨询闸门——人工复核 Step 4 产出的 Hunk Verification Table,防止脚本回归或基线缺失时 verified: no 的行悄悄漏过。两者构成纵深防御,宁可误报中断(可恢复)也不容忍静默丢失内容(不可恢复)。此外还处理 gsd-pristine/ 快照与 backup-meta.json 哈希不一致的漂移场景(#3657),漂移时必须停下并向用户提供三种解决路径。
十、测试保障:升级链路的行为契约
这条链路的可靠性由专门测试锁定。除 tests/bug-2992-check-latest-version.test.cjs 外,仓库还有 tests/bug-3516-reapply-patches-gsd-update-filter.test.cjs(验证 reapply 时正确过滤 gsd:update 相关提交)、tests/enh-2937-statusline-context-position.test.cjs(状态栏上下文定位)与 tests/feat-2795-update-banner.test.cjs(更新横幅)。结合 hooks/ 目录下 14 个受管 hook 文件(gsd-check-update.js、gsd-statusline.js、gsd-update-banner.js、gsd-session-state.sh 等)与安装器 bin/install.js,可以完整印证:检查是后台异步的、提示是状态栏/横幅的、执行是确认后原子的、回滚是补丁回放的。
结语
GSD 的 /gsd:update 并非一条“装完就走”的简单命令,而是一条精心设计的自我维护闭环:多运行时与自定义目录的精确探测、包名常量化的防注入版本检查、先展示后确认的用户交互、清单外自定义文件的预先抢救、跨运行时缓存清理、以及基于三方比较与双重验证闸门的本地补丁回放。理解这条链路,不仅能让你在升级 GSD 时从容应对各类提示与告警,也为设计“自带安全升级机制”的 CLI 工具提供了可直接借鉴的工程范式——尤其是“LLM 驱动的流程必须用确定性脚本锁住关键常量”和“宁可多次备份也不让用户内容静默丢失”这两条设计取舍。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00