【免费下载链接】gsd-core
Git. Ship. Done - Core
导读
本文围绕 gsd-core 的「可选更新横幅(update banner opt-in)」特性展开,讲解 GSD 安装器如何为拒绝或未安装 GSD statusline 的用户提供一个可选的SessionStart钩子,使其在会话启动时借助已有的~/.cache/gsd/gsd-update-check.json缓存静默地获知新版本可用。读完本文,你将掌握该特性的触发条件、缓存数据契约、横幅输出与限流逻辑、以及与gsd-statusline.js的职责划分,并能基于源码定位对应实现与测试,理解其「优雅降级、绝不打断会话启动」的设计原则。
该特性由 hooks/gsd-update-banner.js 实现,对应的变更记录见 .changeset/archived/update-banner-opt-in.md。
一、特性背景:为什么需要「可选」更新横幅
GSD(Git. Ship. Done)在安装时会注册一组托管钩子(managed hooks),其中 hooks/gsd-statusline.js 负责在会话中渲染状态栏(statusline),并在其中内嵌更新提示(⬆ /gsd:update与陈旧钩子警告,见该文件中约第 930 行起对缓存的读取与evaluateUpdateCache的调用)。也就是说,安装了 statusline 的用户不需要额外的更新提醒机制——状态栏本身就会消费更新缓存。
但有一类用户会在安装器询问时拒绝安装或保留非 GSD 的 statusline。对这类用户而言,更新信息没有呈现入口。于是本特性提供了一条**可选的(opt-in)**补充通道:当安装器检测到用户拒绝/保留非 GSD statusline 时,会额外注册一个SessionStart横幅钩子,通过systemMessage信封在会话启动时呈现更新可用信息。
从 hooks/gsd-update-banner.js 头部注释可以确认三条关键设计约束:
- 注册即 Opt-in:
bin/install.js仅在用户拒绝安装(或替换)GSD statusline 时注册该钩子,「SessionStart条目的存在本身就是 opt-in」,不存在独立的运行时开关; - 复用已有缓存:横幅不自己联网检查,而是读取
gsd-check-update-worker.js写入的~/.cache/gsd/下缓存文件(per-package 命名); - 以 issue #2795 为设计依据,保证更新提示在多种运行时(Claude、OpenCode、Kilo、Gemini 等)下的一致呈现。
二、数据来源:更新检查缓存(update-check cache)契约
横幅本身是「只读消费者」,真正的更新探测由后台 worker 完成。整条链路为:
SessionStart 主钩子→ 派生子进程 worker → 写缓存文件 →gsd-update-banner.js(或 statusline)读取
2.1 后台检查:gsd-check-update.js
hooks/gsd-check-update.js 是标准SessionStart钩子(每会话运行一次),其职责:
- 通过
detectConfigDir探测运行时配置目录,依次支持CLAUDE_CONFIG_DIR环境变量覆盖,以及.claude、.gemini、.config/kilo、.kilo、.config/opencode、.opencode等目录; - 计算项目级/全局级
VERSION文件路径(gsd-core/VERSION,项目优先、全局兜底); - 统一将缓存目录收敛到
~/.cache/gsd/(os.homedir() + '.cache/gsd'),该目录在写入前用fs.mkdirSync(..., { recursive: true })确保存在; - 通过
spawn(process.execPath, [workerPath], { stdio: 'ignore', detached: true, windowsHide: true })派生出独立的后台 worker,并通过环境变量GSD_CACHE_FILE、GSD_PROJECT_VERSION_FILE、GSD_GLOBAL_VERSION_FILE传递路径,最后child.unref()分离子进程。
之所以拆成独立 worker 文件而非node -e '<inline code>',源码注释明确说明是为了避免模板字符串正则转义问题,并让 worker 可以独立测试。
2.2 写入端:gsd-check-update-worker.js
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, };各字段含义:
| 字段 | 说明 |
|---|---|
update_available | 布尔值,latest存在且isSemverNewer(latest, installed)为真时为true |
installed | 当前安装版本,从项目级/全局级VERSION文件读取(项目优先),缺省为'0.0.0' |
latest | 通过checkLatestVersion()查询到的最新版本;非ok结果下为'unknown' |
checked | 检查发生的 Unix 时间戳(秒) |
stale_hooks | 陈旧托管钩子数组(比对每个钩子文件头部的gsd-hook-version注释与已安装版本),无则为undefined |
package_name | 包身份标识,来自包身份缝(Package Identity seam),用于下游的「血统守卫」 |
值得注意的实现细节:
- 版本来源:worker 依次读取项目级与全局级
VERSION文件;项目目录优先(本地安装优先于全局安装)。 - 陈旧钩子检测:仅扫描
MANAGED_HOOKS清单(来自 hooks/managed-hooks-registry.cjs)中列出的文件;被移除特性的孤儿文件(如gsd-intel-*.js)不会列入,以免产生永久性陈旧警告(#1750)。gsd-update-banner.js正是该清单中的第 42 项。 - 原子写入:为避免与并发读取者(statusline、横幅、多个运行时 worker)发生撕裂读,worker 采用「同目录临时文件 +
renameSync」的原子发布策略(#4091):先写cacheFile + '.tmp-' + process.pid,再renameSync到位,失败时尽力清理临时文件。 - 血统字段:
package_name由打包的 Package Identity 缝提供(而不是从../package.json向上查找,后者在已安装树中解析不到名字,#2544/#378)。
2.3 缓存文件命名
缓存文件名由包身份决定。updateCacheFileName由package-identity.cjs提供;从 tests/gsd-statusline.test.cjs(第 2343–2345 行)可以确认:
PACKAGE_NAME为@opengsd/gsd-core;updateCacheFileName为gsd-update-check-opengsd-gsd-core.json(per-package 命名,避免多运行时/多包冲突)。
也就是说,横幅实际读取的路径为~/.cache/gsd/gsd-update-check-opengsd-gsd-core.json,与变更记录中的~/.cache/gsd/gsd-update-check.json是「通用回退名 + 包专属名」的关系——当运行库缺失时退回到通用文件名(见下文降级设计)。
三、横幅实现:gsd-update-banner.js 的核心逻辑
hooks/gsd-update-banner.js 是可选的SessionStart钩子,其运行流程可概括为三个纯函数 + 一个主入口:
readCache(cacheFile)—— 读取并解析缓存;buildBannerOutput(state)—— 依据缓存状态决策「输出 or 静默」;shouldSuppressFailureWarning(sentinelFile, nowSeconds)/recordFailureWarning(...)—— 失败诊断的 24h 限流;main()—— 组装以上逻辑,向stdout输出 JSON 信封。
3.1 输出契约:systemMessage信封
横幅的输出格式与 statusline 的更新段保持一致的 JSON 信封:
{ "systemMessage": "GSD update available: 1.2.0 → 1.3.0. Run /gsd:update." }其中版本号取自缓存的installed/latest(缺失时显示'unknown')。buildBannerOutput是纯函数(无 I/O),返回null表示「本会话静默、什么都不输出」。
3.2 决策矩阵
buildBannerOutput(state)的判定顺序如下:
| 缓存状态 | 结果 |
|---|---|
缓存文件存在但JSON.parse失败(parseError = true)且未在限流窗口内 | 输出{ systemMessage: 'GSD update check failed.' } |
同上但处于限流窗口(suppressFailureWarning = true) | 返回null(静默) |
缓存缺失/不可读(cache = null) | 返回null(静默) |
缓存存在但package_name缺失或与PACKAGE_NAME不一致(血统守卫失败) | 返回null(视为不可信缓存) |
update_available不为真 | 返回null(静默) |
| 以上均通过 | 输出更新可用横幅 |
要点解读:
- 更新可用时才有横幅:
update_available: false时完全静默,符合「更新已是最新时不打扰」的定位; - 血统守卫(lineage guard):
package_name必须存在且与当前包一致(!cache.package_name || cache.package_name !== PACKAGE_NAME即拒绝)。缺失package_name的缓存被视为「早于血统追踪的旧记录」,一律不可信——该逻辑与 statusline 中evaluateUpdateCache的守卫完全一致; - 解析失败限流:只有「文件存在但 JSON 损坏」这一类错误才会触发一次性诊断横幅,且同一错误 24 小时内最多提示一次(
RATE_LIMIT_SECONDS = 24 * 60 * 60),避免坏缓存文件在每个会话都打扰用户。
3.3 失败限流的实现
限流通过哨兵文件~/.cache/gsd/banner-failure-warned-at实现:
shouldSuppressFailureWarning(sentinelFile, nowSeconds):读取哨兵文件中的时间戳,若nowSeconds - last < RATE_LIMIT_SECONDS则返回true;文件缺失或内容非有限数字时返回false(放行提示);- 当
parseError且未限流时,main()先以recursive: true确保缓存目录存在(覆盖「目录被清空」的首次运行场景),再写入当前时间戳(recordFailureWarning,尽力而为,失败仅意味着下个会话重新提示)。
四、优雅降级:构建失败绝不打断会话启动
横幅是「可选」的SessionStart钩子,因此它对失败的态度是降级而非崩溃(#3582)。具体表现为:
gsd-update-banner.js顶部对运行库采用try/require/ensureRuntimeBuild/require/catch形状:gsd-core/bin/lib/package-identity.cjs是 tsc 构建产物(ADR-457),在未执行npm run build:lib的裸插件市场/git-clone 安装中不存在;此时PACKAGE_NAME保持null,updateCacheFileName保持通用回退名'gsd-update-check.json';- 由于
PACKAGE_NAME为null,buildBannerOutput的血统守卫!cache.package_name || cache.package_name !== PACKAGE_NAME恒为真,缓存永远被判定为不可信,main()自然落入「什么都不打印」的静默路径——无需单独的分支代码; - 读取、限流、写哨兵等环节均有 try/catch 兜底,任何 I/O 失败都只影响「本会话是否提示」,不会让会话启动失败。
同样的降级形状也独立存在于gsd-check-update.js与gsd-check-update-worker.js中。源码注释说明这一重复是刻意的:脚本 scripts/lint-hooks-runtime-build-seam.cjs 按文件逐一要求ensure-runtime-build.cjs的require/调用与编译产物require字面量同时出现在该文件自身中,提取公共 helper 会让字面量 require 移出文件,破坏该 lint 的文本扫描。
五、与 statusline 的职责划分
同一份缓存有两个消费端,判定逻辑保持一致:
| 维度 | gsd-statusline.js | gsd-update-banner.js |
|---|---|---|
| 消费入口 | 状态栏渲染(会话持续可见) | 可选的SessionStart一次性横幅 |
| 决策函数 | evaluateUpdateCache(cache) | buildBannerOutput(state) |
| 血统守卫 | package_name缺失或与PACKAGE_NAME不一致 → 视为无缓存 | 同上,返回null |
| 更新提示 | 状态栏内嵌⬆ /gsd:update与⚠ stale hooks段 | systemMessage: 'GSD update available: X → Y. Run /gsd:update.' |
| 使用前提 | 已安装 GSD statusline | 用户拒绝/保留非 GSD statusline(由安装器注册) |
两者共享同一个 per-package 缓存文件(~/.cache/gsd/gsd-update-check-opengsd-gsd-core.json),因此不会出现「检查写入一个位置、读取另一个位置」的多运行时错位问题(#607/#1421)。
六、测试验证与源码佐证
仓库对buildBannerOutput的行为有直接的单测覆盖,位于 tests/gsd-statusline.test.cjs(第 2361–2409 行「buildBannerOutput lineage guard」分组):
- 外来血统被拒绝:缓存
package_name为'get-shit-done-cc'时返回null(foreign lineage must be rejected); - 同源血统输出横幅:
package_name为'@opengsd/gsd-core'且update_available: true时返回信封,且systemMessage同时包含installed、latest与/gsd:update; - 缺失血统视为不可信:缓存无
package_name字段时返回null。
同一测试文件中还断言了包身份常量:PACKAGE_NAME === '@opengsd/gsd-core'、updateCacheFileName === 'gsd-update-check-opengsd-gsd-core.json'。此外,hooks/managed-hooks-registry.cjs 将gsd-update-banner.js列入托管钩子清单(第 42 项),worker 会将其纳入陈旧钩子扫描,保证升级后横幅脚本自身也能被识别为待更新对象。
七、适用前提与使用说明
本特性是可选注册的,其生效链路为:
- 运行安装器(如
npx get-shit-done-cc等)时,在 statusline 询问处选择拒绝安装/保留非 GSD statusline; - 安装器因此注册
gsd-update-banner.js为SessionStart钩子(SessionStart条目的存在即 opt-in); - 每会话由
gsd-check-update.js派生子进程 worker 检查更新并写缓存; - 横幅钩子读取缓存,仅在「有更新」或「缓存损坏且 24h 未提示过」时输出
systemMessage信封,其余情况静默; - 卸载时通过
npx get-shit-done-cc --uninstall干净移除,该钩子随托管钩子一并清理。
需要说明的边界:
- 若已安装 GSD statusline,则无需本横幅——状态栏已内嵌更新提示,横幅不会重复注册;
- 横幅不主动联网,只消费 worker 写入的缓存;缓存缺失、损坏、血统不符或已是最新版本时均静默;
- 源码注释强调「
SessionStart条目的存在本身就是 opt-in,没有单独的运行时开关」,因此不要试图通过环境变量或配置文件单独开关横幅——其开关完全由安装时的选择决定; - 本文所描述的文件路径(
~/.cache/gsd/、哨兵文件、缓存文件名)以当前仓库实现为准,若想深入阅读,可继续查看 hooks/gsd-update-banner.js、hooks/gsd-check-update.js、hooks/gsd-check-update-worker.js 与 tests/gsd-statusline.test.cjs。
【免费下载链接】gsd-core
Git. Ship. Done - Core
相关推荐
get-shit-done 不装默认 statusline 时如何启用 gsd-update-banner 更新提示
get shit done 不装默认 statusline 时如何启用 gsd update banner 更新提示 在 get shit done(GSD)中
人工智能AI 应用提示工程开发工具工作流自动化AI AgentGitHub_Trending/re/review-prompts高级调试技巧:解决复杂的代码问题
GitHub_Trending/re/review prompts高级调试技巧:解决复杂的代码问题 GitHub_Trending/re/review prom
FIFA 23实时编辑器完全指南:新手快速掌握游戏修改技巧
FIFA 23实时编辑器完全指南:新手快速掌握游戏修改技巧 FIFA 23 Live Editor是一款功能强大的游戏实时编辑工具,专为FIFA 23玩家设计,
游戏开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考