get-shit-done 自更新链路加固:用 `--package=` 与 `--` 分隔符根治 npx 缓存陈旧与 Bash 工具令牌误路由问题
get-shit-done(GSD)通过
/gsd:update命令与update.md工作流完成自更新,其核心动作是调用 npx 拉取并执行 npm 包get-shit-done-cc。历史上裸写npx -y get-shit-done-cc@latest存在两类失效:一类是 npx 命中本地缓存、拉回的并非@latest而是一个更旧的版本;另一类是 Bash 工具包装层错误路由@令牌,报出Unknown command: "get-shit-done-cc@latest"。本文以修复记录 .changeset/fix-3130-update-npx-robust.md 为主体,结合仓库中的更新工作流、回归测试与安装器实现,完整剖析这次加固的技术细节。读完你既能理解npx -y --package=<pkg>@latest -- <bin>这个鲁棒调用形态为何有效,也能在自己的 Agent 自更新或“npx 执行一次性命令”场景中复用它。
一次“缺陷修复型 Changeset”究竟在讲什么
.changeset 目录存放的是按 PR 拆分、发布时统一合并进 CHANGELOG 的碎片(见 .changeset/README.md)。每个碎片头部包含 type 与 pr frontmatter,正文一句话描述面向用户的可见变化。本文要讲的碎片即 fix-3130-update-npx-robust.md:
type: Fixed:这是一次缺陷修复;pr: 3130:对应 GitHub issue/PR #3130;- 正文点明:
update.md中的 npx 调用被加固,以规避**缓存陈旧(cache-stale)**与 Bash 工具令牌路由失败两类故障;三个同源调用(local、global、unknown/fallback)统一改为npx -y --package=get-shit-done-cc@latest -- get-shit-done-cc。
这个碎片虽然只有寥寥几句,但它浓缩了一个真实的自更新可靠性问题:当“更新命令本身被写进一个由 Agent 驱动的文档工作流,并经由 Bash 工具执行”时,命令的写法必须同时对抗包管理器的缓存行为与工具链的参数路由规则。下面逐层展开。
上下文:GSD 是如何“更新自己”的
在深入命令形态之前,先锚定这段命令在整条链路中的位置。
- 用户或状态行触发斜杠命令
/gsd:update,其命令定义为 commands/gsd/update.md,负责检测本地/全局安装、比对 npm 最新版、拉取并展示 changelog、请求用户确认、执行更新并清缓存、提醒重启。 - 命令主体路由到执行上下文 get-shit-done/workflows/update.md。该工作流是一份高度结构化、给 Agent 逐步执行的 Markdown 过程文档,包含
get_installed_version、check_latest_version、compare_versions、show_changes_and_confirm、backup_custom_files、run_update、display_result、check_local_patches等步骤。 run_update步骤真正执行安装。npm 包名与可执行文件来自根 package.json:包名为get-shit-done-cc,其bin字段把同名命令指向bin/install.js——也就是说npx get-shit-done-cc <flags>的本质是临时拉包后运行安装器,由 bin/install.js 根据--global/--local、--claude/--codex/...等参数把文件安装进对应运行时的配置目录。
从源码结构可以推断,这是一条“Agent 读了文档 → 由 Bash 工具执行 npx → npx 跑安装器写文件”的链式调用,任何一环对参数的解析与裸写命令假设不一致,整次更新就会失败——这正是 #3130 要解决的问题。
裸 npx 调用的两个失效模式
回归测试 tests/bug-3130-update-npx-robust-invocation.test.cjs 的头部注释把旧形态 npx -y get-shit-done-cc@latest 的两个故障写得非常直白:
模式 1:缓存陈旧(cache-stale)。 裸写的 npx -y get-shit-done-cc@latest 会命中 npx 的本地缓存,可能返回一个比 @latest 更旧的版本。也就是说,用户以为在升级,实际 npx 悄悄给了缓存里那份老包,导致更新“静默失效”——版本号没变、changelog 没有、甚至可能与安装器检测到的已装版本比较后误判“已是最新”。
模式 2:令牌路由(token-routing)。 get-shit-done-cc@latest 里带 @ 的 specifier 在经由某些 Bash 工具包装层执行时会被错误路由,npm 收到的是 get-shit-done-cc@latest 这个字符串本身,于是报出:
Unknown command: "get-shit-done-cc@latest"
更新流程在这一步直接中断。注意这类错误发生在 npm 解析参数之前——是调用链把“应该作为安装目标包名的 specifier”与“可执行的命令名”混为一谈造成的。
加固形态:npx -y --package=...@latest -- <bin>
修复后的统一形态是:
npx -y --package=get-shit-done-cc@latest -- get-shit-done-cc
两条关键语法各自的职责(依据 fix-3130-update-npx-robust.md 与测试注释):
--package=get-shit-done-cc@latest强制走全新 registry 拉取。它把“要安装的包”显式声明为--package=参数,绕过 npx 的本地缓存命中,从而解决“拿不到 @latest”的缓存陈旧问题。-y保持自动确认,避免交互阻塞 Agent 驱动的执行。--分隔符明确划分“npx 自身参数”与“要运行的命令及其参数”。--之后的第一段get-shit-done-cc是待执行命令、随后的内容(如--global)才是传给该命令的参数。这样@令牌就不会再被上游包装层当作“命令名的一部分”错误路由,Unknown command不再出现。
写作提醒:上述关于
--package=绕过缓存、--防止令牌误路由的机制说明,来自项目文档与测试注释的表述,引用时以此为准;若要在自己的脚本中依赖这些行为,建议实测你的 npx 版本与调用链。
三个“兄弟调用”:local / global / unknown 全覆盖
在 get-shit-done/workflows/update.md 的 run_update 步骤中,工作流先根据第一步 get_installed_version 探测到的安装范围(INSTALL_SCOPE)拼出 RUNTIME_FLAG,再分别落到三种调用。三种情况统一替换为加固形态,这正是碎片所说的“all three sibling invocations”。
本地安装(--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
未知安装(fallback)——没探测到任何安装,退回 Claude Code 全局安装:
npx -y --package=get-shit-done-cc@latest -- get-shit-done-cc --claude --global
(上述命令原文分别位于 update.md 第 520-541 行 附近。)
$RUNTIME_FLAG 由 TARGET_RUNTIME 拼出(--$TARGET_RUNTIME),而运行时取值在 get_installed_version 步骤中推导,覆盖 claude、opencode、gemini、kilo、codex 等;环境变量优先级(如 CODEX_HOME、GEMINI_CONFIG_DIR、OPENCODE_CONFIG_DIR)也各有专门处理。安装完成后,工作流还会跨 preferred/env/默认目录清理 cache/gsd-update-check.json 以及共享缓存 ~/.cache/gsd/gsd-update-check.json,避免状态行残留“⬆ /gsd:update”的陈旧提示。
补充一条手动的逃生通道:当版本检查失败(如 npm 暂不可用)时,工作流直接建议用户手动执行 npx -y --package=get-shit-done-cc@latest -- get-shit-done-cc --global(见 update.md 第 329 行)——同一加固形态也用于人类手动升级路径。若完全不依赖 npm(发布中断或直接基于源码仓库开发),则可参考 docs/manual-update.md,用 git pull --rebase && node scripts/build-hooks.js && node bin/install.js --claude --global 的方式绕过 npx。
确定性约束:为什么包名不能“让 Agent 现场编”
加固 npx 调用并非孤立的语法修补,它与相邻的可靠性设计相互配合。check_latest_version 步骤明确规定不要直接运行 npm view 或 npm search,而是调用确定性脚本 get-shit-done/bin/check-latest-version.cjs。
该脚本背后的动机(#2992)与 #3130 高度同源:当“npm 查询目标包名”由 LLM 在运行时自由发挥时,模型可能拼出 @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: bool, version: string, reason: string, detail?: string },reason 使用受控枚举(ok / fail_npm_failed / fail_invalid_output),版本需通过语义化版本正则校验,并内置 15 秒超时上限,防止 registry 挂起阻塞 /gsd:update。可以看出,GSD 的自我更新设计遵循一条清晰原则:包名、命令形态这类“会影响对哪个包动手”的决策一律固化进代码/文档常量,不让 Agent 在执行时自由发挥;#3130 是把这一原则从“包名查询”延伸到了“安装调用参数路由”。
回归防线:测试如何“守住”文档形态
加固不能只靠一次改稿,仓库为此在 tests/bug-3130-update-npx-robust-invocation.test.cjs 中建立了两条文本级回归守卫,直接把工作流文档当成被测契约:
- 断言不存在旧形态:用
npx -y get-shit-done-cc@latest[^\n]*扫描 update.md,匹配到任何裸写形式即失败——“一旦有人回退成 cache-stale 形态,CI 立刻报警”。 - 断言新形态至少出现 3 次:用
npx -y --package=get-shit-done-cc@latest -- get-shit-done-cc统计加固形态出现次数,要求>= 3,正好对应 local、global、unknown 三个兄弟调用,防止后续维护时漏改某个分支。
测试注释还特意说明这是“允许的文档结构契约测试”——项目对“测试直接 grep 源码”通常设限(见仓库测试目录中大量 lint-no-source-grep* 规则),但对这类“产品工作流文档的调用形态”保持白名单放行,因为这里文档本身就是运行时被 Agent 执行的指令。这一对测试构成了典型的“防回退护栏”:语法修复 + 两条文本断言 + changeset 记录,共同保证 #3130 的修复不会在未来的重构里悄悄消失。
可迁移的工程结论
从这次修复能沉淀出几条不依赖具体项目的通用经验:
- 用
--package=显式声明要安装的包,别靠“把 specifier 当命令的一部分”的裸写。npx pkg@latest形态在交互终端里顺手,但在“缓存可能陈旧 + 命令经包装层路由”的环境中,npx -y --package=pkg@latest -- bin更能保证拉取的是真@latest、执行的是真bin。 --是参数路由的“防火墙”。凡是“npx 选项之后还有可执行命令 + 传给它的参数”,都应使用--分隔,避免上游工具把带@、-等特殊字符的令牌误当作命令名。- 凡是“影响对哪个目标动手”的常量,都要固化成代码/文档约束(包名、命令形态),并结合结构断言测试防止回归——这是从 #2992(包名确定性)与 #3130(命令形态确定性)中一致体现出来的设计取向。
- 多个分支调用同步改造。同一条更新逻辑往往有 local/global/fallback 等多个出口,修复时逐一核对,并让测试断言“分支数量级”的覆盖,避免只修了最常见的路径。
关键文件索引
| 角色 | 相对路径 |
|---|---|
| 本次修复的 changelog 碎片 | .changeset/fix-3130-update-npx-robust.md |
| 更新工作流(三条加固命令所在) | get-shit-done/workflows/update.md |
/gsd:update 斜杠命令 |
commands/gsd/update.md |
| 回归守卫测试 | tests/bug-3130-update-npx-robust-invocation.test.cjs |
| 确定性版本检查脚本 | get-shit-done/bin/check-latest-version.cjs |
| 安装器(npm bin 目标) | bin/install.js |
| 包名与 bin 映射 | package.json |
| 不依赖 npm 的手动更新流程 | docs/manual-update.md |
| changeset 碎片约定 | .changeset/README.md |
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
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