【免费下载链接】gsd-core
Git. Ship. Done - Core
本篇指南聚焦 gsd-core 项目(Git. Ship. Done — Core)在包重命名迁移后的一项关键修复:更新检查缓存从"多包共享"改为"按包隔离 + 血缘(lineage)校验",同时安装器内置了对旧包get-shit-done-cc遗留物(残留 hooks、commands、skills 与过期缓存)的自动检测与清理。读完本文,你将理解"永久性⬆ /gsd:update假升级提示"的成因与修复原理,掌握通过--dry-run安全预览清理计划、执行自动清理或手动回退的完整操作流程,并能从源码与测试层面验证这一机制的可靠性。
本文以仓库中该变更的 changeset 记录(.changeset/archived/nimble-eagles-romp.md)与配套操作文档 docs/cleanup-get-shit-done-cc.md 为主体,结合 hooks/gsd-check-update.js、gsd-core/bin/lib/legacy-cleanup.cjs 等源码与 tests/legacy-cleanup.test.cjs 测试展开。
背景:包重命名引发的"假升级"问题
从get-shit-done-cc到@opengsd/gsd-core
gsd-core 的前身包名为get-shit-done-cc。在包重命名为@opengsd/gsd-core时(仓库 issue #607),版本计数器被重置:旧包get-shit-done-cc已经发布到1.42.x,而新包@opengsd/gsd-core从1.2.0重新开始计数。
这种版本号"断崖"本身没问题,但它与旧的更新检查机制叠加后产生了一个长期困扰用户的 bug:如果旧包仍以任何形式残留在某个运行时的配置目录中(例如~/.gemini),旧包的更新检查进程会把一个更高的latest版本写入所有包共享的更新缓存文件~/.cache/gsd/gsd-update-check.json,而旧版本的新工具会无条件信任这份缓存中的写入。结果就是状态栏(statusline)永久显示一个并不存在的升级提示⬆ /gsd:update——用户明明已经安装了最新版@opengsd/gsd-core,却永远被告知有更新。
问题机理:共享缓存为何能被"投毒"
从当前源码可以还原出旧机制的脆弱点:
- 更新检查由 SessionStart 钩子触发,hooks/gsd-check-update.js 中定义缓存文件名,并通过环境变量把缓存路径传给后台工作进程 hooks/gsd-check-update-worker.js。
- 缓存目录是工具无关的共享目录
~/.cache/gsd(源码注释明确说明这是为了规避"多运行时解析不一致"——statusline 从一个运行时的缓存读、check-update 写到另一个运行时缓存的问题,#1421)。 - 旧机制下缓存文件名固定(
gsd-update-check.json),且缓存内容不携带写入者身份。任何安装在该机器上的、旧包的更新检查进程都可以往同一个文件里写,读取端(statusline、banner)无法区分这条记录是哪个包写的,于是一份来自get-shit-done-cc的高版本号记录被新包读取,产生假升级提示。
修复一:更新检查缓存按包隔离并携带血缘字段
这次变更(PR #611)对更新检查机制做了两处根治:
按包隔离的缓存文件名
缓存文件名从固定的gsd-update-check.json改为按包命名的文件名。包身份的单一事实来源是 gsd-core/bin/lib/package-identity.cjs(由 scripts/generate-package-identity.cjs 从package.json生成,issue #498):
const cacheSlug = "opengsd-gsd-core"; const updateCacheFileName = "gsd-update-check-opengsd-gsd-core.json";即当前包的缓存文件名是gsd-update-check-opengsd-gsd-core.json,而旧包的gsd-update-check.json不再被新包读取。写入与读取两端都通过require('../gsd-core/bin/lib/package-identity.cjs')拿到同一个文件名常量,保证写方与读方一致。变更前的 changeset 原文将这一点表述为"per-package filename(gsd-update-check-<slug>.json)carrying apackage_namelineage field"。
值得注意的一个工程细节:在插件市场直装 / git clone 后未执行构建的场景下,package-identity.cjs作为 tsc 构建产物可能不存在(#3582),因此 hooks/gsd-check-update.js 与 hooks/gsd-check-update-worker.js 都做了降级处理——先尝试ensureRuntimeBuild()自建产物,失败则回退到通用文件名gsd-update-check.json并保持写读两端一致,而不是让 SessionStart 钩子崩溃。
缓存记录携带package_name血缘字段,读取端校验
后台工作进程 hooks/gsd-check-update-worker.js 写入的缓存记录结构如下:
const result = { update_available: latest && isSemverNewer(latest, installed), installed, latest: latest || 'unknown', checked: Math.floor(Date.now() / 1000), stale_hooks: staleHooks.length > 0 ? staleHooks : undefined, package_name: PACKAGE_NAME, };package_name字段即"血缘标识"。读取端不再盲目信任缓存,而是先做校验。以可选的 SessionStart 横幅钩子 hooks/gsd-update-banner.js 为例:
// Lineage guard: package_name must be present and match this package. // Absent package_name means the cache predates lineage tracking — treat as untrusted. if (!cache.package_name || cache.package_name !== PACKAGE_NAME) return null;- 缓存中
package_name缺失(缓存早于血缘追踪机制产生)→ 视为不可信,不输出任何提示; package_name存在但与当前包不匹配(例如来自旧包get-shit-done-cc的记录)→ 同样拒绝。
这样一来,任何其他包(包括旧包get-shit-done-cc)都无法再污染当前包的升级提示,同时"多运行时可见性"得到保留:同一个包在 15 个受支持运行时之间仍然共享同一份按包隔离的缓存。
原子写入,避免撕裂读
由于缓存文件按包共享、可能被多个运行时的 statusline/banner 读取端并发解析,工作进程在写入时采用原子发布(#4091):先写唯一同目录临时文件(<cacheFile>.tmp-<pid>),再renameSync到位——读者只会看到旧记录或新记录,绝不会看到写了一半的撕裂内容。/gsd:update命令则会清空全部 15 个受支持运行时的缓存(即删除按包命名的缓存文件),让下一次会话重新评估。
修复二:安装器自动检测并清理旧包遗留物
与缓存隔离配套,安装器现在在每次安装时自动检测并移除旧包get-shit-done-cc遗留的构件。核心实现位于 gsd-core/bin/lib/legacy-cleanup.cjs,设计遵循"纯扫描函数 + 薄 IO 应用层"的风格,并通过opts.fs/opts.logger注入点实现无真实文件系统触碰的单元测试(issue #607)。
扫描范围与判定规则
planLegacyCleanup(configDirs, opts)对每个运行时配置目录执行三类扫描:
1.hooks/与commands/子树的内容引用扫描(content-references-old-package)
- 仅扫描扩展名为
.js/.cjs/.mjs/.sh的代码文件(CODE_EXTENSIONS); - 判定条件是文件内容包含旧包名字符串信号
gsd-core+-cc; - 刻意排除Markdown、JSON、TOML、YAML 等文档/配置文件——因为它们可能在历史与引用语境中合法地提及旧包名(例如 CHANGELOG.md),扫描它们曾导致安装器误删刚安装好的
gsd-core/CHANGELOG.md,破坏安装; - 同时刻意排除
gsd-core/子树本身——当前包自己的基础设施(CHANGELOG.md、本文件等)就存放在那里,安装时本就会被覆盖,扫描它会造成"自我删除"误判。
2.skills/gsd-*目录的过期路径引用扫描(stale-get-shit-done-path)
- 旧版安装写出的 SKILL.md 文件中内嵌了指向旧运行时配置目录的路径,例如
@$HOME/.codex/get-shit-done/workflows/docs-update.md(#1453); - 重命名后正确路径应为
@$HOME/.codex/gsd-core/workflows/docs-update.md,残留的过期技能文件会被运行时错误拾取、破坏会话; - 该扫描仅针对
gsd-*前缀的 GSD 托管技能目录中的.md文件,判定其内容包含/<legacy>/路径片段;非gsd-前缀的用户技能目录一律不触碰。
3. 旧共享缓存文件(legacy-shared-cache)
- 即旧包写出的固定名缓存
~/.cache/gsd/gsd-update-check.json; - 在
--config-dir重定向安装场景下,扫描根改为重定向的 scope 根而非默认 home(#3799),避免误删仍在服役的安装的缓存。
扫描结果按路径去重、确定性排序,返回{ path, reason }[]计划。三处判定规则的源码注释还记录了数据丢失回归测试对应的历史教训(见下文"测试佐证")。
用户资产保护
清理设计上有一条硬边界:用户自有资产绝不触碰。isDevPreferencesPath()保证任何路径段为dev-preferences或文件名为dev-preferences.md的文件一律跳过;用户自定义的 agents、非gsd-前缀的技能目录、以及任何不由 GSD 托管的文件都不在清理计划内。这与 docs/manual-update.md 中安装器"仅干净地替换 GSD 托管目录"的行为一致(自定义 agent、commands/gsd/之外的命令、CLAUDE.md、自定义 hooks 均被保留)。
三步实操:从预览到应用
下面完整给出官方操作文档 docs/cleanup-get-shit-done-cc.md 的流程。适用场景:状态栏持续显示⬆ /gsd:update提示,而@opengsd/gsd-core实际已是最新——即怀疑旧包遗留物仍在投毒更新提示。
第一步:预览清理计划(--dry-run)
执行安装器并附加--dry-run,即可看到将要发生的全部变更而不修改任何文件:
npx -y --package=@opengsd/gsd-core@latest -- gsd-core --claude --global --dry-run命令会打印移除计划——每个文件路径及其删除原因——并列明将清除的过期更新缓存文件,然后直接退出。从 bin/install.js 的 CLI 分发逻辑看,--dry-run模式下cleanupLegacyGsdCc({ dryRun: true })是"预览唯一事实来源":它同时覆盖旧共享缓存与按包缓存两条路径的预览,且--dry-run只预览遗留清理,不预览--uninstall(源码中有明确提示文案)。
若你使用的不是 Claude Code,把--claude换成对应运行时的旗标即可,完整旗标表见 docs/manual-update.md(下表为该文档原文继承):
| Runtime | Flag |
|---|---|
| Claude Code | --claude |
| OpenCode | --opencode |
| Kilo | --kilo |
| Codex | --codex |
| Copilot | --copilot |
| Cursor | --cursor |
| Windsurf | --windsurf |
| Augment | --augment |
| All runtimes | --all |
项目级安装请把--global换成--local。
第二步:执行自动清理
确认预览结果无误后,去掉--dry-run重跑同一命令:
npx -y --package=@opengsd/gsd-core@latest -- gsd-core --claude --global安装器将依次完成:
- 在所有运行时配置目录(
~/.claude、~/.gemini、~/.codex、~/.config/opencode、~/.kilo等)中检测旧包遗留构件; - 移除孤儿 hooks、commands以及任何引用旧包名的代码文件(判定规则见上文
legacy-cleanup.cjs的CODE_EXTENSIONS与内容信号扫描); - 移除
skills/gsd-*下内嵌旧路径的技能文件(stale-get-shit-done-path); - 清除过期的共享更新缓存(旧包写出的
gsd-update-check.json); - 同时清除当前包的按包缓存(
gsd-update-check-opengsd-gsd-core.json,即updateCacheFileName),使下次会话重新评估钩子版本; - 保留用户自有资产:
dev-preferences.md、自定义 agents、以及任何不由 GSD 托管的文件。
实现层面对删除操作也做了健壮性处理:applyLegacyCleanup在 Windows 平台遇到EBUSY/EPERM(常见于 Defender 扫描)时最多重试 3 次、每次同步等待 100ms,其余平台与非重试错误则立即停止(gsd-core/bin/lib/legacy-cleanup.cjs)。清理失败绝不会中止安装——安装器只记录警告并继续(bin/install.js 中cleanupLegacyGsdCc的调用被 try/catch 包裹)。
第三步:手动回退方案
如果安装器在你的环境中无法解析get-shit-done-cc,或你更倾向于手工清理,可按以下步骤操作(继承自 docs/cleanup-get-shit-done-cc.md):
逐个检查运行时配置目录中旧包留下的
gsd-core/子树:ls ~/.claude/gsd-core/ ls ~/.gemini/gsd-core/ ls ~/.codex/gsd-core/ ls ~/.config/opencode/gsd-core/ ls ~/.kilo/gsd-core/新包安装在相同路径下,因此:仅当该运行时尚未运行过新安装器时,才删除这些由
get-shit-done-cc写入的目录。卸载旧包(若它仍然可解析):
npx get-shit-done-cc --uninstall删除过期的共享缓存:
rm -f ~/.cache/gsd/gsd-update-check.json
说明:以上命令属于用户在自己机器上的运维操作,仓库本身为只读,本文仅介绍查看与配置方式。
第四步:验证
打开一个新终端会话(或重启你的 AI 运行时),状态栏中的⬆ /gsd:update提示应不再出现。可进一步确认安装版本:
npx @opengsd/gsd-core@latest -- gsd-core --version注意:在非 npm 安装 / 源码仓库直用的场景下,官方手册 docs/manual-update.md 还提供了手工更新流程(git pull --rebase→node scripts/build-hooks.js→node bin/install.js --claude --global→ 清除更新缓存),其中第 4 步"清除更新缓存使状态栏指示器复位"与本文主题直接相关——新版安装器(含按包缓存清理)运行后,这一手工步骤通常不再需要。
源码与测试佐证
关键的代码锚点
| 关注点 | 位置 |
|---|---|
| 按包缓存文件名常量 | gsd-core/bin/lib/package-identity.cjs(updateCacheFileName、cacheSlug) |
缓存写入端(package_name血缘字段、原子写入) | hooks/gsd-check-update-worker.js |
| 缓存读取端血缘校验(lineage guard) | hooks/gsd-update-banner.js |
| 多运行时配置目录检测 | hooks/gsd-check-update.js |
| 遗留清理扫描与应用 | gsd-core/bin/lib/legacy-cleanup.cjs |
安装器--dry-run分发与清理接入 | bin/install.js |
测试如何锁定行为
tests/legacy-cleanup.test.cjs 使用真实临时目录端到端验证清理逻辑,其中几个关键回归测试直接对应本变更的工程质量:
- 内容信号扫描:
hooks/下内容包含旧包信号的钩子文件被标记content-references-old-package;内容干净的同名文件不被标记; - 数据丢失回归(#607):用户自定义
gsd-*.js/gsd-*.sh钩子只要不包含旧包内容,就绝不进入清理计划——此前"按名称匹配孤儿钩子"的规则会误删用户钩子,这是本次变更修复的核心数据丢失风险; - 自我删除回归(#607):
gsd-core/子树内的代码文件即使包含信号字符串也不被标记(子树不扫描),防止安装器自删; - Markdown 永不标记:文档/配置文件合法引用旧包名时不得被删除(防止误删新装的 CHANGELOG.md)。
此外,tests/package-identity.test.cjs、tests/gsd-check-update-worker-platform-gate.test.cjs、tests/installer-migration-report.test.cjs 等测试分别覆盖包身份常量一致性、工作进程降级路径与安装器迁移报告输出,共同构成对这一修复的守护网。
小结
gsd-core在 PR #611 中的这项变更,从两个层面根治了包重命名带来的"永久假升级"问题:更新检查缓存按包隔离并携带package_name血缘字段、读取端严格校验,杜绝跨包投毒;安装器内置旧包遗留物扫描清理并提供--dry-run预览,让迁移残留自动、安全、可预期地被清除。对用户而言,遇到⬆ /gsd:update假提示时,一条带--dry-run的安装命令即可预览清理计划,随后去掉--dry-run一键完成清理;对开发者而言,planLegacyCleanup/applyLegacyCleanup的纯函数 + 薄 IO 分层、明确的扫描边界与用户资产保护、以及针对历史数据丢失事故的回归测试,都是可复用的工程范式。
【免费下载链接】gsd-core
Git. Ship. Done - Core
相关推荐
GSD Core 安装器迁移 003:升级时安全清理遗留 `get-shit-done/` 运行时目录的完整实现解析
GSD Core 安装器迁移 003:升级时安全清理遗留 get shit done/ 运行时目录的完整实现解析 GSD Core(@opengsd/gsd c
get-shit-done (GSD) Skill Surface 剪枝机制修复:applySurface 在集群禁用时清除 ~/.claude/skills 下遗留的 gsd-*/ 目录
get shit done GSD Skill Surface 剪枝机制修复:applySurface 在集群禁用时清除 ~/.claude/skills 下遗
人工智能AI 应用提示工程开发工具工作流自动化AI Agentgsd-core 工作流安装兜底提示迁移:从废弃的 get-shit-done-cc 切换到 @opengsd/get-shit-done-redux 安装命令
gsd core 工作流安装兜底提示迁移:从废弃的 get shit done cc 切换到 @opengsd/get shit done redux 安装命令
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考