首页
/ get-shit-done 安装器迁移层:从变更集发布说明到全运行时安全升级的实现解析

get-shit-done 安装器迁移层:从变更集发布说明到全运行时安全升级的实现解析

2026-09-07 11:16:51作者:余洋婵Anita

导读

本文围绕 get-shit-done(GSD)仓库中变更集 .changeset/vivid-foxes-romp.md 记录的发布特性展开:将"安装器迁移"(installer migrations)接入 Claude Code、Codex、Cursor、Gemini、OpenCode 等 15 个支持运行时的常规安装/更新流程,并落地六项关键能力——基线扫描(baseline scanning)、折叠式动作报告(collapsed action reporting)、受阻动作保护(blocked-action protection)、事务式回滚(transactional rollback)、安装状态持久化(install-state persistence)与锁保护执行(lock-guarded execution)。读完本文,你将掌握这套迁移层的完整设计目标、迁移记录与动作契约、plan/apply/rollback 执行流水线、安装入口接线方式,以及如何为退役一个安装产物编写合规的迁移与配套测试。

为什么需要一个显式的迁移层

GSD 安装器在此之前其实已经具备多项升级行为:替换由 GSD 托管的命令、技能、Agent、Hook 与引擎文件;在替换前备份本地改动过的托管文件;保留已知的用户自持产物;清理旧 Hook 文件与 Hook 注册项;改写各运行时专有的配置格式;以及部分回滚失败的 Codex 安装。

问题在于这些行为散落在各个安装分支(install branches)中。对孤立的缺陷修复而言这足够灵活,但一旦涉及特性退役就变得危险:未来某个改动可能把文件从发布包中移除,却在已安装目录中留下陈旧的旧副本;或者因为某个文件恰好位于 GSD 托管目录下,就被误当成 GSD 产物而删除用户自建文件。安装器迁移层就是为了让升级行为显式化、可评审、可重复而引入的统一机制(架构文档)。

六个设计目标

  1. 默认保护用户数据;
  2. 特性退役时移除陈旧的 GSD 托管文件;
  3. 破坏性动作在运行前必须可见;
  4. 记录已发生的动作,避免后续安装重复执行同一迁移;
  5. 即使各运行时的具体文件不同,也给予一致的安全模型;
  6. 让迁移编写足够小,使贡献者愿意使用迁移而非再写一段一次性清理代码。

非目标

它不是通用包管理器,不是数据库迁移系统,不会自动推断每一种历史安装布局,不删除任意用户文件,也不会一步取代现有安装变换逻辑。

变更集发布内容速览

变更集记录为 type: Added、关联 PR 3402,正文精确概括了本特性的交付范围:

将安装器迁移接入每个受支持运行时的常规安装流程,在包物化(package materialization)之前完成:基线扫描、折叠式动作报告、受阻动作保护、事务式回滚、安装状态持久化与锁保护执行。

逐词拆解即可得到正文的核心骨架,本文以下各节分别对应其中一项或多项能力展开说明。

核心术语与文件分类模型

在设计文档中定义了五组关键术语,是理解迁移安全边界的前提:

  • 托管文件(Managed file):由 GSD 安装并记录在安装清单(install manifest)中的文件。未改动时可自动替换;被本地改动过则必须备份或合并。
  • 用户自持文件(User-owned file):由用户工作流或用户直接创建维护的文件。即使位于 GSD 目录下也绝不能因位置而删除。
  • 未知文件(Unknown file):位于安装根目录下、既不在清单中也没有被归类为用户自持的文件。默认保留,除非迁移通过专门检测器证明其为陈旧 GSD 产物。
  • 迁移(Migration):一个版本化的变更集,可检查当前安装、产出计划(plan),并在安全检查通过后应用计划。
  • 计划(Plan):一系列拟议的文件系统与配置动作的清单,可安全展示给用户,描述"将要发生什么、为什么",不触碰磁盘。

实现中的文件分类函数位于 get-shit-done/bin/lib/installer-migrations.cjsclassifyArtifact 依据清单中记录的哈希把某个路径划分为五种分类之一:managed-pristine(托管且哈希一致)、managed-modified(托管但被本地改动)、managed-missing(清单在录但文件缺失)、unknown(不在清单)、missing。计划的生成与动作保护都建立在这套分类之上。

状态持久化:File Manifest 与 Install State

File Manifest(既有基线)

现有清单仍是所有权基线,记录已安装的 GSD 版本、安装模式以及发行版托管文件的哈希。其不变式非常严格:

  • 发行版托管文件必须在清单中跟踪;
  • 用户自持文件必须被保留且不纳入清单哈希;
  • 同一路径不能同时属于两类。

Install State(新增状态记录)

安装器在清单旁写入 gsd-install-state.json(实现常量见 installer-migrations.cjs),设计文档给出的结构如下:

{
  "schema": 1,
  "runtime": "codex",
  "scope": "global",
  "installed_version": "1.50.0",
  "install_mode": "full",
  "applied_migrations": [
    {
      "id": "2026-05-11-codex-hooks-layout",
      "package_version": "1.50.0",
      "checksum": "sha256:...",
      "applied_at": "2026-05-11T00:00:00.000Z"
    }
  ]
}

实现中的写入做了严格原子写:先把内容写入 gsd-install-state.json.tmp-<pid>-<ts>,再通过 rename 落盘,绝不允许半写状态残留(源码注释明确要求绕过 platformWriteSync,因为它在 rename 失败时回退为直接写,会破坏该不变式)。checksum 由迁移定义本身计算(sha256: 前缀)。若某个已应用迁移的校验和发生变化,安装器必须告警并拒绝静默重跑;修复方向的新增逻辑应使用新的迁移 id 而非改动旧记录(assertAppliedMigrationChecksums 会直接抛错提示 "create a new fix-forward migration id")。

迁移记录契约(Migration Record)

每个迁移导出一份普通记录加纯规划逻辑。必填字段:

module.exports = {
  id: '2026-05-11-runtime-layout-example',
  title: 'Move legacy commands into runtime skills',
  description: 'Move legacy runtime command files into the generated skill layout.',
  introducedIn: '1.50.0',
  runtimes: ['claude', 'codex', 'gemini'],
  scopes: ['global', 'local'],
  destructive: true,
  plan(ctx) {
    return [];
  }
};

作者守卫模块会拒绝缺少 idtitledescriptionintroducedInscopesdestructiveplan 的记录。runtimes 是唯一可选的字段——它仅在迁移刻意对每个运行时共享时省略;scopes 必须始终显式,防止作者无意中扩大 local/global 行为。从源码看,installer-migrations.cjsdiscoverInstallerMigrations 会扫描 installer-migrations 目录下按文件名排序的所有 .cjs 文件并逐条校验记录。

plan(ctx) 与辅助谓词

plan(ctx) 接收一个包含 runtime、scope、目标目录、上一份清单、安装状态、包清单与文件系统助手的安装上下文,返回动作列表。它绝不能改磁盘。迁移可使用的谓词包括:

  • isManaged(relPath)
  • isUserOwned(relPath)
  • hashMatchesManifest(relPath)
  • exists(relPath)
  • readJson(relPath)
  • readToml(relPath)

动作类型:迁移产出的最小集合

迁移只产出少量动作类型,执行器统一负责变更、备份、回滚与报告:

动作类型 语义与适用场景
remove-managed 仅当路径确证为 GSD 托管且与上一份清单哈希一致时删除;必须携带 ownershipEvidence 说明所有权依据。适用于退役 Hook、旧生成 Agent、废弃命令文件、陈旧运行时产物。
backup-and-remove 托管文件与清单不一致时先备份再删除,用户可查看报告与备份。适用于退役"用户可能打过补丁"的托管文件。
move-managed 将托管路径移动到新的托管路径;若源文件被本地改动则升级为 backup-and-move 或冲突。适用于命令目录迁入技能布局这类布局迁移。
rewrite-config 通过解析器或结构化辅助改写结构化配置文件(首期实现 rewrite-json);字符串替换只允许用于窄范围 marker 块且需覆盖行尾与顺序变体的测试。适用于运行时配置、Hook 注册、特性开关、生成式 Agent 注册块。每个 rewrite-json 动作都必须带 ownershipEvidence 与引用运行时配置契约注册表的 runtimeContract
preserve-user 声明路径为用户自持、必须在目录整体替换中存活的动作;dry-run 中仅信息性展示,apply 时在所有权已知时转为透传/恢复。适用于 profile、偏好、手写指令等。
record-baseline 首次基线扫描中把清单托管文件记入基线而不改动它。仅首次基线扫描器可用。
baseline-preserve-user 记录已知安装面下发现的用户自持或未知文件,不改动它;未知文件默认落到此动作。仅首次基线扫描器可用。
prompt-user 中止非交互式破坏性迁移,在交互模式询问(preserve / back up / remove / move,默认 preserve)。适用于分类歧义且猜测可能丢数据的情形。

计划阶段内置两层保护逻辑(源码见 planInstallerMigrations):当迁移请求 remove-managed 但文件分类为 managed-modified 时自动升级为 backup-and-remove;分类为 unknown 时自动降级为 preserve-userprompt-user 一律进入受阻集合;分类为 unknown 且动作类型不属于 rewrite-json/record-baseline/baseline-preserve-user 的也会进入受阻集合。

事务式执行流程:plan → apply → journal → rollback

安装器在物化新包内容之前运行迁移。设计文档给出了完整流程:

  1. 构建安装上下文;
  2. 读取上一份清单与安装状态;
  3. 为可能触碰的路径构建运行前快照;
  4. 按 runtime、scope 与已应用状态发现待执行迁移;
  5. 向每个待执行迁移询问计划;
  6. 合并并校验计划;
  7. 以 dry-run 形式打印计划;
  8. 应用安全的非交互动作;
  9. 对歧义动作提示或停止;
  10. 写入新包内容;
  11. 写入新清单与安装状态;
  12. 报告备份、保留文件、移除的陈旧文件与跳过的动作。

installer-migrations.cjs 中,runInstallerMigrations 是其入口:先 acquireInstallMigrationLock 取锁;无动作时调用 markPendingMigrationsApplied 直接落安装状态;存在受阻动作时返回(不执行任何 apply);否则调用 applyInstallerMigrationPlan。apply 期间:

  • gsd-migration-journal/<runId>.json 记录运行日志(journal),每条动作附状态:removedrewrittenrecordedpreservedmissing 等;
  • gsd-migration-journal/<runId>-rollback/ 保存每个被触碰文件的原字节快照;
  • backup-and-remove 额外在 <runId>-backups/ 保留可检查的备份;
  • 全部动作完成后才原子写入新的 install-state;
  • 任何一步失败:按 journal 逆序把原字节复制回原位(绝不删除当前安装运行未创建或未修改的文件),恢复先前的 install-state 字节,最后清理 journal 目录;回滚是尽力而为(best-effort),但不完整时必须大声失败(抛出含 rollbackFailuresmigration rollback incomplete 错误)。

锁保护执行

acquireInstallMigrationLock 实现 gsd-install-migration.lock 的排他互斥,默认超时 30 秒。锁文件内写入持有者 pid 与获取时间;遇到 EEXIST 时先读锁内 pid 并用 process.kill(pid, 0) 探测存活状态,只有确认持有进程已死(ESRCH)或就是本进程时才回收陈旧锁并重试,否则等待到超时后抛出 "installer migration lock is held"。Windows 下释放锁用 unlinkSync 而非吞掉 EPERM 的 rmSync,保证释放失败会浮出表面(对应 tests/bug-3670-cursor-local-install-migration-lock.test.cjs 覆盖的回归场景)。

接入常规安装入口(变更集核心交付)

变更集最核心的一句话是 "for every supported runtime" 与 "before package materialization"。Phase 4 集成把上面这套 runner 接入常规 install/update 入口,覆盖全部 15 个运行时:Claude Code、Antigravity、Augment、Cline、CodeBuddy、Codex、Copilot、Cursor、Gemini、Hermes Agent、Kilo、OpenCode、Qwen Code、Trae 与 Windsurf。

bin/install.js 的安装函数可见其接线方式:

  • 声明 installerMigrationResult 状态并注册回滚闭包 rollbackInstallerMigrations(变更前文件已备份、后续 materialization 失败时回滚迁移结果,见 L8014 附近);
  • 安装路径上调用 runInstallerMigrations({ configDir: targetDir })(同时传递 baselineScan: true 之类参数,见 L8033 / L8213 两处调用);
  • 打印 reportInstallerMigrationResult 的折叠式报告行(L7812 起);
  • 非 TTY 场景(典型如 Claude Code 的 /gsd-update)先经 resolveInstallerMigrationPromptsForNonTty 解析受阻动作(L8241);
  • 最后 assertInstallerMigrationsUnblocked 断言无残留受阻项(L8266),受阻时在写入任何新包文件前直接失败

也就是说,迁移运行先于包物化;安装状态只在包物化与收尾成功后才持久化;runner 返回受阻动作时安装器在落新文件前失败——这正是变更集所述 "install-state persistence" 与 "blocked-action protection" 在入口处的体现。

折叠式动作报告(collapsed action reporting)

"Collapsed" 针对的是首次基线扫描可能产生海量逐文件动作的问题。summarizeInstallerMigrationResultinstaller-migration-report.cjs)把 record-baselinebaseline-preserve-user 分别折叠为一行汇总:"N managed baseline files(recorded)" 与 "N user baseline files(preserved)",其余真实动作保持逐文件行。这样首次安装不会被数百行基线噪音淹没,同时升级类动作依旧可见。reportInstallerMigrationResult 最终以 ✓ Installer migrations 头逐行输出 label、relPath 与 reason。

Dry Run:同一套规划器

迁移 runner 支持 dry-run 模式:打印计划后不做任何改动退出。输出按风险分组:

  • will preserve
  • will replace unchanged managed files
  • will remove stale managed files
  • will back up locally modified files
  • needs user choice
  • blocked

关键不变式:dry-run 与 apply 共用同一套规划器,不允许存在独立的"仅预览"代码路径。

受阻动作的非交互解析与保护

无 TTY 的自动更新无法逐文件交互询问。resolveInstallerMigrationPromptsForNonTty 按既定优先级消解受阻动作:

  1. 操作员覆盖:设置环境变量 GSD_INSTALLER_MIGRATION_RESOLVE=keep|remove,对所有支持该选项的动作生效;
  2. 非 TTY 安全默认分类classifyPromptUserAction):
    • 陈旧 SDK 构建产物(get-shit-done/sdk/{dist,src}/ 下路径,每次安装都会重新生成,删除无损)→ remove
    • 面向用户的技能锚点(skills/gsd-*/SKILL.md)→ keep,保护用户内容;
    • 发行包内白名单内置 Hook(hooks/ 下以 BUNDLED_GSD_HOOK_FILES 白名单为准的 13 个文件)→ remove,因为首装基线后安装器会立即写入新版;
  3. 其余一律走硬断言assertInstallerMigrationsUnblocked 抛出分组错误,按 reason 汇总文件数、给出最多 3 个示例路径与可用选项,并提示通过 GSD_INSTALLER_MIGRATION_RESOLVE 或 TTY 交互解决。

白名单为何必要:若不限制为精确文件名集合,^hooks/gsd-[^/]+\.(js|sh|...)$ 这类形状正则同样会命中用户自写自定义 Hook 与旧版退役内置 Hook,自动删除即静默数据丢失。相关回归覆盖见 tests/bug-3628-bundled-hook-classifier-whitelist.test.cjs(双向断言:白名单缺文件或发行多了未白名单文件都会让 CI 失败)。

首次基线迁移:旧安装的逃生舱

首条迁移记录 2026-05-11-first-time-baseline-scan(实现在 000-first-time-baseline.cjs)不做任何破坏性修复,只分类既有安装。它按运行时已知安装面(如 claude 的 get-shit-done/commands/gsd/skills/agents/hooks/settings.json;codex 另含 config.toml/hooks.json)递归扫描,跳过内部顶层文件后逐项分类:

  • manifest 托管且 pristine/modified → record-baseline
  • 已知安装器生成的 Agent 文件(与仓库 agents/gsd-*.md 名称集合比对)→ record-baseline
  • 已知用户自持路径或非 gsd- 前缀的 skills/agents 子项 → baseline-preserve-user
  • 长得像 GSD 但清单无法证明的陈旧文件 → prompt-userkeep/remove);
  • 其余未知文件 → baseline-preserve-user

runner 仅在调用方传 baselineScan: true 时激活该记录;否则该记录不产出动作,常规迁移发现的意外文件继续走保护逻辑。这为早于完整迁移跟踪的旧安装提供了可评审的重分配/清理计划,而无需安装器完美推断每一次历史版本过渡。

后续已内置的两条显式迁移可作范例:001-legacy-orphan-files.cjs(把孤立的旧 Hook/文件清理收编为首个显式迁移)与 002-codex-legacy-hooks-json.cjs(Codex hooks.json 的结构化清理——GSD 只证明个别生成 Hook 命令的所有权,不拥有整个文件,故走 rewrite-json 且仅改写自有部分)。

安全策略

  • 所有权:绝不删除未知文件。未知文件默认保留,除非迁移内含能证明其为陈旧 GSD 产物的专门检测器。
  • 改动检测:路径在上一份清单中时——哈希一致 = 未改动的托管文件;哈希不一致 = 本地改动过的托管文件;缺失 = 用户已自行删除、应保持删除(除非迁移明确需要重建)。
  • 用户自持产物:用户自持产物只定义一次,被保留逻辑与清单写入逻辑共同消费;新增用户自持产物必须附带回归测试,证明其在重装后被保留且不出现在清单中。
  • 配置文件:运行时配置是混合所有权。GSD 可能拥有 marker 块、生成式 Agent 区块或 Hook 条目,但除非文件本就是 GSD 专用文件,否则不拥有整个文件。配置迁移只删除或改写自有部分。

运行时配置契约注册表

设计文档维护了一张覆盖全部运行时的配置契约注册表(docs/installer-migrations.md 中的 Runtime Configuration Contract Registry),作为触碰宿主运行时配置的迁移的唯一权威依据。每行记录五个维度:What(GSD 安装的调用面/Agent/技能/规则/Hook/配置面)、Where(安装器瞄准的 global/local 根目录)、When(install/upgrade/uninstall/迁移触点)、Who(周边用户配置的所有权边界)、Why(上游加载器契约或当前 GSD 兼容垫片)。要点包括:

  • Claude Code:GSD 拥有生成的技能、本地命令、gsd-* Agent、Hook 文件与 settings.json 中的 GSD hook/statusLine 条目(文档核验时间 2026-05-11);
  • Codex:GSD 拥有生成技能、生成的 Agent TOML、agents.gsd-* 配置区段、GSD 添加的 [features].hooks(canonical 键;遗留别名 codex_hooks 被识别并前向迁移,#3566)与 GSD Hook 条目,兼容哨兵为 Codex 0.130.0 的 features.hooks 键;
  • Gemini CLI:GSD 拥有生成的 TOML 命令/Agent/Hook 与 GSD settings 条目;本地命令副本在全局 GSD 命令已存在时可跳过;
  • 若干运行时(Antigravity、Windsurf、Trae 等)上游文档不完整或受限,注册表将这些行标记为 source-limited / 高风险,重写此类迁移需新主源或带测试的安装器级探针;
  • 文档还要求:配置改写优先用结构化解析器(JSON/JSONC/TOML/YAML);marker 块重写需要行尾与顺序变体测试;上游文档或 CLI 模式变化时更新快照日期、更新 docs/ARCHITECTURE.md 并为新形态补测试后再改迁移行为。

为迁移作者准备的编写工作流

当 PR 要移除或移动安装产物时,PR 必须包含:

  1. 一条迁移记录;
  2. dry-run 计划输出的测试;
  3. apply 行为的测试;
  4. 本地改动托管文件的测试;
  5. 变更路径附近用户自持文件的测试;
  6. 若影响用户可见的安装行为,更新发布说明。

作者必须在迁移文件中回答:被退役的是哪种旧产物或配置形态?如何证明其为 GSD 所有?用户改过会怎样?缺失会怎样?影响哪些 runtime 与 scope?非交互安装中该动作是否安全?

测试矩阵与实现顺序

迁移 runner 的任何改动都应覆盖:无历史状态的全新安装、清单一致的重装、有待执行迁移的升级、本地改动过的托管文件、GSD 目录下的未知文件、被整体抹掉目录下的用户自持文件、apply 失败后的回滚、适用的 global/local 安装 scope、路径序列化时的 Windows 分隔符、以及配置改写时的 CRLF 输入。仓库中对应实现于 tests/installer-migrations.test.cjstests/installer-migration-report.test.cjstests/installer-migration-install-integration.test.cjs(后者即变更集所述的 all-runtime 安装矩阵,逐一演练安全托管清理与受阻用户选择产物)。

建议实现顺序与迁移层在仓库中的演化一致:先抽离围绕清单与用户自持列表的所有权助手 → 安装状态读写助手 → 迁移发现与校验和 → 纯规划器 dry-run → 带日志的 executor → 把孤立 Hook/文件清理迁入首条显式迁移 → 把一次结构化配置改写迁入 runner → 基线分类器 → 之后凡移动/改名/退役产物的新安装 PR 必须配迁移。该顺序让首版保持小巧:既有安装器继续物化文件,而迁移 runner 接管清理、分类与可评审的破坏性变更。

设计借鉴(Prior Art)

该设计明确借鉴了既有升级系统:Flyway 的版本化迁移(按校验和跟踪的有序、一次执行)、Flyway 的 dry-run 预览;Liquibase 的 changeset 与前置条件(受当前系统状态门控的声明式变更);Debian conffile 策略(保留本地配置并区分包所有权与用户所有权);npm 生命周期脚本作为打包语境参考,但因卸载与升级上下文受限而不足以充当迁移机制。

上述全部内容——从基线扫描、折叠报告、受阻保护、事务回滚、安装状态持久化到锁保护执行——共同构成了变更集 vivid-foxes-romp.md 一句话背后完整的安装器迁移架构,并已通过 bin/install.js 接入全部 15 个运行时的常规安装与更新入口。对贡献者而言,新增迁移的正确起点是阅读 docs/installer-migrations.md 的注册表行,复用已有 executor 而非追加一次性清理块。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391