clawt 2.20.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/.claude/agents/docs-sync-updater.md +29 -11
  2. package/README.md +19 -30
  3. package/dist/index.js +1127 -222
  4. package/dist/postinstall.js +73 -8
  5. package/docs/alias.md +108 -0
  6. package/docs/completion.md +55 -0
  7. package/docs/config-file.md +43 -0
  8. package/docs/config.md +91 -0
  9. package/docs/create.md +85 -0
  10. package/docs/init.md +65 -0
  11. package/docs/list.md +67 -0
  12. package/docs/log.md +67 -0
  13. package/docs/merge.md +137 -0
  14. package/docs/notification.md +94 -0
  15. package/docs/projects.md +135 -0
  16. package/docs/remove.md +79 -0
  17. package/docs/reset.md +35 -0
  18. package/docs/resume.md +99 -0
  19. package/docs/run.md +146 -0
  20. package/docs/spec.md +157 -1906
  21. package/docs/status.md +298 -0
  22. package/docs/sync.md +114 -0
  23. package/docs/update-check.md +95 -0
  24. package/docs/validate.md +368 -0
  25. package/package.json +1 -1
  26. package/src/commands/alias.ts +1 -1
  27. package/src/commands/create.ts +10 -5
  28. package/src/commands/init.ts +75 -0
  29. package/src/commands/list.ts +1 -1
  30. package/src/commands/merge.ts +11 -4
  31. package/src/commands/remove.ts +10 -3
  32. package/src/commands/reset.ts +3 -0
  33. package/src/commands/resume.ts +1 -1
  34. package/src/commands/run.ts +9 -3
  35. package/src/commands/status.ts +14 -5
  36. package/src/commands/sync.ts +18 -6
  37. package/src/commands/validate.ts +46 -52
  38. package/src/constants/branch.ts +3 -0
  39. package/src/constants/config.ts +1 -1
  40. package/src/constants/index.ts +14 -2
  41. package/src/constants/interactive-panel.ts +44 -0
  42. package/src/constants/messages/completion.ts +1 -1
  43. package/src/constants/messages/create.ts +3 -0
  44. package/src/constants/messages/index.ts +4 -0
  45. package/src/constants/messages/init.ts +18 -0
  46. package/src/constants/messages/interactive-panel.ts +61 -0
  47. package/src/constants/messages/remove.ts +2 -0
  48. package/src/constants/messages/sync.ts +3 -0
  49. package/src/constants/messages/validate.ts +6 -0
  50. package/src/constants/paths.ts +3 -0
  51. package/src/index.ts +2 -0
  52. package/src/types/command.ts +9 -1
  53. package/src/types/index.ts +2 -1
  54. package/src/types/projectConfig.ts +5 -0
  55. package/src/utils/config.ts +2 -1
  56. package/src/utils/git.ts +18 -0
  57. package/src/utils/index.ts +9 -1
  58. package/src/utils/interactive-panel-render.ts +315 -0
  59. package/src/utils/interactive-panel.ts +590 -0
  60. package/src/utils/json.ts +67 -0
  61. package/src/utils/project-config.ts +77 -0
  62. package/src/utils/validate-branch.ts +166 -0
  63. package/src/utils/worktree-matcher.ts +2 -2
  64. package/src/utils/worktree.ts +6 -2
  65. package/tests/unit/commands/create.test.ts +20 -16
  66. package/tests/unit/commands/init.test.ts +146 -0
  67. package/tests/unit/commands/merge.test.ts +7 -1
  68. package/tests/unit/commands/remove.test.ts +4 -0
  69. package/tests/unit/commands/reset.test.ts +2 -0
  70. package/tests/unit/commands/run.test.ts +2 -0
  71. package/tests/unit/commands/sync.test.ts +6 -0
  72. package/tests/unit/commands/validate.test.ts +13 -0
  73. package/tests/unit/utils/config.test.ts +2 -2
  74. package/tests/unit/utils/project-config.test.ts +136 -0
  75. package/tests/unit/utils/update-checker.test.ts +28 -7
  76. package/tests/unit/utils/validate-branch.test.ts +272 -0
  77. package/tests/unit/utils/worktree.test.ts +6 -0
@@ -0,0 +1,368 @@
1
+ ### 5.4 在主 Worktree 验证其他分支
2
+
3
+ **命令:**
4
+
5
+ ```bash
6
+ # 指定分支名(支持模糊匹配)
7
+ clawt validate -b <branchName> [--clean] [-r <command>]
8
+
9
+ # 不指定分支名(列出所有分支供选择)
10
+ clawt validate [--clean] [-r <command>]
11
+ ```
12
+
13
+ **参数:**
14
+
15
+ | 参数 | 必填 | 说明 |
16
+ | ------------- | ---- | ------------------------------------------------------------------------ |
17
+ | `-b` | 否 | 要验证的 worktree 分支名(支持模糊匹配,不传则列出所有分支供选择) |
18
+ | `--clean` | 否 | 清理 validate 状态(重置主 worktree 并删除快照) |
19
+ | `-r, --run` | 否 | validate 成功后在主 worktree 中执行的命令(如测试、构建等) |
20
+
21
+ > **限制:** 单次只能验证一个分支,不支持批量验证。
22
+
23
+ **背景说明:**
24
+
25
+ Git worktree 不会包含 `node_modules`、`.venv` 等依赖文件,每次安装依赖耗时较长。利用 `git diff HEAD...branch --binary`(三点 diff)可以获取目标分支自分叉点以来的全量变更(包含已提交和未提交的修改),将其作为 patch 应用到主 worktree 中进行测试,无需重新安装依赖。
26
+
27
+ **验证分支机制:**
28
+
29
+ validate 不再在主工作分支上直接 apply patch,而是先切换到目标分支对应的**验证分支**(`clawt-validate-<branchName>`),再 apply patch。验证分支的 HEAD 不会随主工作分支推进,因此 patch apply 永远不会冲突。详见 [2.5 验证分支](#25-验证分支)。
30
+
31
+ **快照机制:**
32
+
33
+ validate 命令引入了**快照(snapshot)机制**来支持增量对比。每次 validate 执行成功后,会将当前全量变更通过 `git write-tree` 保存为 git tree 对象,并将 tree hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.tree`),同时将验证分支的 HEAD commit hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.head`),用于增量 validate 时对齐基准。当再次执行 validate 时,如果验证分支 HEAD 未变化(正常情况),通过 `git read-tree` 将上次快照的 tree 对象载入暂存区;如果验证分支 HEAD 已变化(sync 后重建了验证分支),则将旧变更 patch(旧 tree 相对于旧 HEAD 的差异)重放到当前 HEAD 暂存区上,避免新旧 tree 基准不同导致 diff 混入 HEAD 变化的内容。最终用户可通过 `git diff` 查看两次 validate 之间的增量差异。
34
+
35
+ **运行流程:**
36
+
37
+ #### `--clean` 模式
38
+
39
+ 当指定 `--clean` 选项时,执行清理逻辑后直接返回,不进入常规 validate 流程:
40
+
41
+ 1. **主 worktree 校验** (2.1)
42
+ 2. **解析目标 worktree**:通过模糊匹配解析目标分支(匹配策略同下文常规 validate 流程中的描述)
43
+ 3. 如果配置项 `confirmDestructiveOps` 为 `true`,提示确认(显示即将执行的危险指令和操作后果),用户取消则退出
44
+ 4. 如果主 worktree 有未提交更改,执行 `git reset --hard` + `git clean -fd` 清空
45
+ 5. **(新增)** 确保当前处于主工作分支上(`ensureOnMainWorkBranch`):如果在验证分支上,清理后切回;如果在其他分支上,交互处理脏工作区后切回
46
+ 6. 删除对应分支的快照文件
47
+ 7. 输出清理成功提示
48
+
49
+ #### 首次 validate(无历史快照)
50
+
51
+ ##### 步骤 0:解析目标 worktree
52
+
53
+ 根据 `-b` 参数解析目标 worktree,匹配策略如下:
54
+
55
+ - **未传 `-b` 参数**:
56
+ - 获取当前项目所有 worktree
57
+ - 无可用 worktree → 报错退出
58
+ - 仅 1 个 worktree → 直接使用,无需选择
59
+ - 多个 worktree → 通过交互式列表(Enquirer.Select)让用户选择
60
+ - **传了 `-b` 参数**:
61
+ 1. **精确匹配优先**:在 worktree 列表中查找分支名完全相同的 worktree,找到则直接使用
62
+ 2. **模糊匹配**(子串匹配,大小写不敏感):
63
+ - 唯一匹配 → 直接使用
64
+ - 多个匹配 → 通过交互式列表让用户从匹配结果中选择
65
+ 3. **无匹配** → 报错退出,并列出所有可用分支名
66
+
67
+ ##### 步骤 1:检测目标分支变更
68
+
69
+ 统一检测目标 worktree 的未提交修改和已提交 commit:
70
+
71
+ ```bash
72
+ # 检测未提交修改
73
+ cd ~/.clawt/worktrees/<project>/<branchName>
74
+ git status --porcelain
75
+
76
+ # 检测已提交 commit(在主 worktree 中执行)
77
+ cd <主 worktree 路径>
78
+ git log HEAD..<branchName> --oneline
79
+ ```
80
+
81
+ - **两者均无** → 输出提示 `该 worktree 的分支上没有任何更改,无需验证`,退出
82
+ - **至少有一项** → 继续
83
+
84
+ ##### 步骤 2:检测主 worktree 工作区状态
85
+
86
+ 执行 `git status --porcelain`,判断主 worktree 是否有未提交的更改。
87
+
88
+ - **无更改** → 进入步骤 3
89
+ - **有更改** → 提示用户选择处理方式,使用交互式选择(方向键切换,回车确认):
90
+
91
+ ```
92
+ ⚠ 当前分支有未提交的更改,请选择处理方式:
93
+
94
+ ❯ reset - 丢弃所有更改 (git reset --hard HEAD && git clean -fd)
95
+ stash - 暂存更改 (git add . && git stash)
96
+ exit - 退出,手动处理
97
+ ```
98
+
99
+ | 选项 | 执行命令 | 默认 |
100
+ | ------- | ----------------------------------------- | ---- |
101
+ | `reset` | `git reset --hard HEAD && git clean -fd` | 是 |
102
+ | `stash` | `git add . && git stash push -m "clawt:auto-stash"` | 否 |
103
+ | `exit` | 退出程序 | 否 |
104
+
105
+ 执行完毕后,通过 `git status --porcelain` 再次检测状态,确保工作区干净。如果仍然不干净,报错退出。
106
+
107
+ ##### 步骤 3:切换到验证分支
108
+
109
+ ```bash
110
+ cd <主 worktree 路径>
111
+ git checkout clawt-validate-<branchName>
112
+ ```
113
+
114
+ 如果验证分支不存在,直接报错退出:
115
+
116
+ ```
117
+ 验证分支 clawt-validate-<branchName> 不存在,请先执行 clawt create 或 clawt run 创建分支 <branchName>
118
+ ```
119
+
120
+ ##### 步骤 4:通过 patch 迁移目标分支全量变更
121
+
122
+ 使用三点 diff(`git diff HEAD...branchName --binary`)获取目标分支自分叉点以来的全量变更。如果目标 worktree 有未提交修改,先做临时 commit 以便 diff 能捕获全部变更,diff 完成后撤销临时 commit 恢复原状。
123
+
124
+ ```bash
125
+ # 如果有未提交修改,先临时提交
126
+ cd ~/.clawt/worktrees/<project>/<branchName>
127
+ git add .
128
+ git commit -m "clawt:temp-commit-for-validate"
129
+
130
+ # 在主 worktree(已切换到验证分支)中执行三点 diff
131
+ cd <主 worktree 路径>
132
+ git diff HEAD...<branchName> --binary | git apply
133
+
134
+ # 撤销临时 commit,恢复目标 worktree 原状
135
+ cd ~/.clawt/worktrees/<project>/<branchName>
136
+ git reset --soft HEAD~1
137
+ git restore --staged .
138
+ ```
139
+
140
+ > 由于验证分支的 HEAD 与目标分支的创建基点一致,patch apply **永远不会冲突**。
141
+ > 此步骤结束后,目标 worktree 的代码保持原样,主 worktree 工作目录包含目标分支的全量变更。
142
+ > 如果 patch apply 失败(兜底场景),`migrateChangesViaPatch` 返回 `{ success: false }`,进入自动 sync 交互流程(见下文 [patch apply 失败后的自动 sync 流程](#patch-apply-失败后的自动-sync-流程))。
143
+
144
+ ##### patch apply 失败后的自动 sync 流程
145
+
146
+ 当 patch apply 失败时,validate 不再直接退出,而是先通过 `ensureOnMainWorkBranch()` 确保主 worktree 切回主工作分支,然后通过 `handlePatchApplyFailure()` 函数进入交互流程:
147
+
148
+ 1. **询问用户**:提示 `是否立即执行 sync 同步主分支到 <branchName>?`
149
+ 2. **用户拒绝** → 输出提示 `请手动执行 clawt sync -b <branchName> 同步主分支后重试`,退出
150
+ 3. **用户确认** → 调用 `executeSyncForBranch(targetWorktreePath, branchName)` 自动执行 sync,sync 的结果(成功/冲突)由 `executeSyncForBranch` 内部输出,`handlePatchApplyFailure` 不做额外判断,validate 流程结束(用户需重新执行 validate)
151
+
152
+ > `executeSyncForBranch` 为 sync 命令抽取的核心操作函数(见 [5.12](#512-将主分支代码同步到目标-worktree)),供 validate 等命令复用。
153
+
154
+ **实现要点:**
155
+
156
+ - `migrateChangesViaPatch()` 返回类型从 `void` 改为 `{ success: boolean }`,patch apply 失败时返回 `{ success: false }` 而非抛出异常
157
+ - `handleFirstValidate()` 和 `handleIncrementalValidate()` 从同步函数改为 `async` 函数,以支持交互式确认
158
+ - `handlePatchApplyFailure()` 为新增的异步函数(`src/commands/validate.ts`),负责 patch 失败后的交互逻辑
159
+ - 消息常量:`MESSAGES.VALIDATE_CONFIRM_AUTO_SYNC`、`MESSAGES.VALIDATE_AUTO_SYNC_START`、`MESSAGES.VALIDATE_AUTO_SYNC_DECLINED`(`src/constants/messages/validate.ts`)
160
+
161
+ ##### 步骤 5:保存快照为 git tree 对象
162
+
163
+ 将主 worktree 工作目录的全量变更保存为 git tree 对象,同时记录验证分支的 HEAD commit hash:
164
+
165
+ ```bash
166
+ git add .
167
+ git write-tree # → 返回 tree hash,写入 ~/.clawt/validate-snapshots/<project>/<branchName>.tree
168
+ git rev-parse HEAD # → 返回验证分支的 HEAD commit hash,写入 ~/.clawt/validate-snapshots/<project>/<branchName>.head
169
+ git restore --staged .
170
+ ```
171
+
172
+ > 此处保存的 HEAD commit hash 是验证分支的 HEAD(即创建时的基点),而非主工作分支的 HEAD。
173
+ > 结果:暂存区=空,工作目录=全量变更。
174
+
175
+ ##### 步骤 6:输出成功提示
176
+
177
+ ```
178
+ ✓ 已切换到验证分支 clawt-validate-feature-scheme-1 并应用分支 feature-scheme-1 的变更
179
+ 可以开始验证了
180
+ ```
181
+
182
+ ##### 步骤 7:执行 `--run` 命令(可选)
183
+
184
+ 如果用户传入了 `-r, --run` 选项,在 validate 成功后自动在主 worktree 中执行指定命令:
185
+
186
+ ```bash
187
+ # 示例:单命令
188
+ clawt validate -b feature-scheme-1 -r "npm test"
189
+
190
+ # 示例:并行执行多个命令(& 为并行分隔符)
191
+ clawt validate -b feature-scheme-1 -r "pnpm test & pnpm build"
192
+ ```
193
+
194
+ **执行说明:**
195
+
196
+ - 命令执行失败(退出码非 0 或进程启动失败)**不影响** validate 本身的结果,仅输出提示信息
197
+ - `--clean` 模式下传入 `--run` 会被忽略(只执行 clean 逻辑)
198
+
199
+ **命令解析规则:**
200
+
201
+ `-r` 选项支持通过 `&` 将多个命令并行执行。解析由 `parseParallelCommands()`(`src/utils/shell.ts`)负责:
202
+
203
+ 1. 先将命令字符串中的 `&&` 临时替换为占位符,避免被误拆
204
+ 2. 按单个 `&` 分割为多个独立命令
205
+ 3. 还原占位符为 `&&`,去除首尾空白,过滤空串
206
+
207
+ | 输入示例 | 解析结果 | 执行方式 |
208
+ | -------- | -------- | -------- |
209
+ | `"npm test"` | `["npm test"]` | 单命令,同步执行(`spawnSync` + `inherit`) |
210
+ | `"npm lint && npm test"` | `["npm lint && npm test"]` | 单命令(`&&` 不拆分),同步执行 |
211
+ | `"npm test & npm build"` | `["npm test", "npm build"]` | 并行执行(`spawn` + `Promise.all`) |
212
+ | `"npm lint && npm test & npm build"` | `["npm lint && npm test", "npm build"]` | 并行执行 2 个命令 |
213
+
214
+ **单命令执行:**
215
+
216
+ 当解析后只有 1 个命令时,通过 `spawnSync` + `inherit` stdio 模式同步执行,输出实时显示在终端。
217
+
218
+ **并行命令执行:**
219
+
220
+ 当解析后有多个命令时,通过 `runParallelCommands()`(`src/utils/shell.ts`)执行:
221
+
222
+ - 每个命令通过 Node.js `spawn` 以 shell 模式启动,`stdio: 'inherit'`
223
+ - 使用 `Promise.all` 等待全部命令完成
224
+ - 完成后汇总输出各命令的执行结果
225
+
226
+ **向后兼容性:**
227
+
228
+ - `-r "npm test"` — 单命令,走原有同步路径,行为无变化
229
+ - `-r "npm lint && npm test"` — `&&` 不拆分,走原有同步路径,行为无变化
230
+ - `-r "npm test & npm build"` — **新行为**:并行执行,等全部完成后汇总
231
+
232
+ **输出格式:**
233
+
234
+ ```
235
+ # 单命令执行成功
236
+ 正在主 worktree 中执行命令: npm test
237
+ ────────────────────────────────────────
238
+ ... 命令的实时输出 ...
239
+ ────────────────────────────────────────
240
+ ✓ 命令执行完成: npm test,退出码: 0
241
+
242
+ # 单命令执行失败(退出码非 0)
243
+ 正在主 worktree 中执行命令: npm test
244
+ ────────────────────────────────────────
245
+ ... 命令的实时输出 ...
246
+ ────────────────────────────────────────
247
+ ✗ 命令执行完成: npm test,退出码: 1
248
+
249
+ # 单命令执行出错(进程启动失败)
250
+ 正在主 worktree 中执行命令: nonexistent
251
+ ────────────────────────────────────────
252
+ ────────────────────────────────────────
253
+ ✗ 命令执行出错: spawn ENOENT
254
+
255
+ # 并行命令执行(全部成功)
256
+ 正在并行执行 2 个命令...
257
+ [1/2] pnpm test
258
+ [2/2] pnpm build
259
+ ────────────────────────────────────────
260
+ ... 各命令的实时输出(交错显示) ...
261
+ ────────────────────────────────────────
262
+ ✓ pnpm test
263
+ ✓ pnpm build
264
+ ✓ 全部 2 个命令执行成功
265
+
266
+ # 并行命令执行(部分失败)
267
+ 正在并行执行 2 个命令...
268
+ [1/2] pnpm test
269
+ [2/2] pnpm build
270
+ ────────────────────────────────────────
271
+ ... 各命令的实时输出(交错显示) ...
272
+ ────────────────────────────────────────
273
+ ✗ pnpm test(退出码: 1)
274
+ ✓ pnpm build
275
+ 共 2 个命令,1 个成功,1 个失败
276
+ ```
277
+
278
+ **实现要点:**
279
+
280
+ - 命令解析:`parseParallelCommands()`(`src/utils/shell.ts`)
281
+ - 并行执行:`runParallelCommands()`(`src/utils/shell.ts`),返回 `ParallelCommandResult[]`
282
+ - 结果汇总:`reportParallelResults()`(`src/commands/validate.ts`)
283
+ - 消息常量:`MESSAGES.VALIDATE_PARALLEL_*` 系列(`src/constants/messages/validate.ts`)
284
+
285
+ #### 增量 validate(存在历史快照)
286
+
287
+ 当 `~/.clawt/validate-snapshots/<project>/<branchName>.tree` 存在时,自动进入增量模式:
288
+
289
+ ##### 步骤 1:读取旧快照
290
+
291
+ 在清空主 worktree 之前,读取上次保存的快照 tree hash 及当时的 HEAD commit hash。
292
+
293
+ ##### 步骤 2:确保主 worktree 干净
294
+
295
+ 如果主 worktree 有残留状态,直接执行 `git reset --hard` + `git clean -fd` 兜底清理(无交互,用户交互已在 `handleValidate` 主函数中通过 `handleDirtyMainWorktree` 完成)。
296
+
297
+ ##### 步骤 3:切换到验证分支
298
+
299
+ 如果当前已在该验证分支上(上次 validate 后未切回),跳过。如果当前在另一个验证分支上(验证了分支 A,现在要验证分支 B),直接切换:
300
+
301
+ ```bash
302
+ git checkout clawt-validate-<branchName>
303
+ ```
304
+
305
+ ##### 步骤 4:从目标分支获取最新全量变更
306
+
307
+ 通过 patch 方式从目标分支获取最新全量变更(流程同首次 validate 的步骤 4)。如果 patch apply 失败,同样进入自动 sync 交互流程(见首次 validate 的 [patch apply 失败后的自动 sync 流程](#patch-apply-失败后的自动-sync-流程)),validate 流程提前结束。
308
+
309
+ ##### 步骤 5:保存最新快照为 git tree 对象
310
+
311
+ 将最新全量变更保存为新的 tree 对象(覆盖旧快照),同时记录验证分支的 HEAD commit hash(流程同首次 validate 的步骤 5)。
312
+
313
+ ##### 步骤 6:将旧变更状态载入暂存区
314
+
315
+ 由于验证分支的 HEAD 不会变化,`oldHeadCommitHash` 与 `currentHeadCommitHash` 始终一致(除非执行了 sync 重建验证分支),因此:
316
+
317
+ **正常情况(HEAD 未变化):**
318
+
319
+ 直接通过 `git read-tree` 将旧 tree 对象载入暂存区:
320
+
321
+ ```bash
322
+ git read-tree <旧 tree hash>
323
+ ```
324
+
325
+ - **读取成功** → 结果:暂存区=上次快照,工作目录=最新全量变更(用户可通过 `git diff` 查看增量差异)
326
+ - **读取失败**(tree 对象可能被 git gc 回收)→ 降级为全量模式,暂存区保持为空,等同于首次 validate 的结果
327
+
328
+ > 这是最常见的路径。相比重构前,正常情况不再需要处理 HEAD 变化的复杂逻辑,代码路径更简单、更可靠。
329
+
330
+ **sync 后(HEAD 变化,验证分支已重建):**
331
+
332
+ 此时旧 tree 对象基于旧 HEAD,直接 read-tree 会导致 diff 混入 HEAD 变化的内容。需要将旧变更 patch(旧 tree 相对于旧 HEAD 的差异)重放到当前 HEAD 暂存区上:
333
+
334
+ ```bash
335
+ # 获取旧 HEAD 对应的 tree hash
336
+ git rev-parse <旧 HEAD commit hash>^{tree} # → 旧 HEAD tree hash
337
+
338
+ # 提取旧变更 patch(旧 HEAD tree → 旧快照 tree 的差异)
339
+ git diff-tree -p --binary <旧 HEAD tree hash> <旧快照 tree hash>
340
+
341
+ # 检测 patch 能否无冲突地应用到暂存区
342
+ git apply --cached --check < patch
343
+
344
+ # 无冲突:apply --cached 到当前 HEAD 暂存区
345
+ git apply --cached < patch
346
+ ```
347
+
348
+ - **patch 为空**(旧变更为空)→ 暂存区保持干净
349
+ - **无冲突** → apply --cached 到当前 HEAD 暂存区,结果与正常情况一致
350
+ - **有冲突** → 降级为全量模式(暂存区保持为空),等同于首次 validate 的结果
351
+
352
+ ##### 步骤 7:输出成功提示
353
+
354
+ ```
355
+ # 增量模式成功
356
+ ✓ 已将分支 feature-scheme-1 的最新变更应用到主 worktree(增量模式)
357
+ 暂存区 = 上次快照,工作目录 = 最新变更
358
+
359
+ # 增量降级为全量
360
+ ✓ 已切换到验证分支 clawt-validate-feature-scheme-1 并应用分支 feature-scheme-1 的变更
361
+ 可以开始验证了
362
+ ```
363
+
364
+ ##### 步骤 8:执行 `--run` 命令(可选)
365
+
366
+ 与首次 validate 的步骤 7 相同,增量 validate 成功后也会执行 `-r, --run` 指定的命令。
367
+
368
+ ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "2.20.0",
3
+ "version": "3.1.0",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -104,7 +104,7 @@ function handleAliasRemove(alias: string): void {
104
104
  export function registerAliasCommand(program: Command): void {
105
105
  const aliasCmd = program
106
106
  .command('alias')
107
- .description('管理命令别名')
107
+ .description('管理命令别名(列出 / 设置 / 移除)')
108
108
  .action(() => {
109
109
  handleAliasList();
110
110
  });
@@ -6,23 +6,25 @@ import type { CreateOptions } from '../types/index.js';
6
6
  import {
7
7
  validateMainWorktree,
8
8
  createWorktrees,
9
+ ensureOnMainWorkBranch,
10
+ getValidateBranchName,
9
11
  printSuccess,
10
12
  printInfo,
11
13
  printSeparator,
12
14
  } from '../utils/index.js';
13
15
 
14
16
  /**
15
- * 注册 create 命令:批量创建 worktree 及对应分支
17
+ * 注册 create 命令:批量创建 worktree 及对应分支(含验证分支)
16
18
  * @param {Command} program - Commander 实例
17
19
  */
18
20
  export function registerCreateCommand(program: Command): void {
19
21
  program
20
22
  .command('create')
21
- .description('批量创建 worktree 及对应分支')
23
+ .description('批量创建 worktree 及对应分支(含验证分支)')
22
24
  .requiredOption('-b, --branch <branchName>', '分支名')
23
25
  .option('-n, --number <count>', '创建数量', '1')
24
- .action((options: CreateOptions) => {
25
- handleCreate(options);
26
+ .action(async (options: CreateOptions) => {
27
+ await handleCreate(options);
26
28
  });
27
29
  }
28
30
 
@@ -30,9 +32,11 @@ export function registerCreateCommand(program: Command): void {
30
32
  * 执行 create 命令的核心逻辑
31
33
  * @param {CreateOptions} options - 命令选项
32
34
  */
33
- function handleCreate(options: CreateOptions): void {
35
+ async function handleCreate(options: CreateOptions): Promise<void> {
34
36
  validateMainWorktree();
35
37
 
38
+ await ensureOnMainWorkBranch();
39
+
36
40
  const count = Number(options.number);
37
41
 
38
42
  // 校验创建数量必须为正整数
@@ -54,6 +58,7 @@ function handleCreate(options: CreateOptions): void {
54
58
  printInfo(`目录路径${index + 1}:`);
55
59
  printInfo(` ${wt.path}`);
56
60
  printInfo(` 分支名: ${wt.branch}`);
61
+ printInfo(` 验证分支: ${getValidateBranchName(wt.branch)}`);
57
62
  printSeparator();
58
63
  });
59
64
  }
@@ -0,0 +1,75 @@
1
+ import type { Command } from 'commander';
2
+ import { Command as Cmd } from 'commander';
3
+ import { logger } from '../logger/index.js';
4
+ import { MESSAGES } from '../constants/index.js';
5
+ import type { InitOptions } from '../types/index.js';
6
+ import {
7
+ validateMainWorktree,
8
+ getCurrentBranch,
9
+ loadProjectConfig,
10
+ saveProjectConfig,
11
+ requireProjectConfig,
12
+ printSuccess,
13
+ printInfo,
14
+ safeStringify,
15
+ } from '../utils/index.js';
16
+
17
+ /**
18
+ * 注册 init 命令:初始化项目级配置,设置主工作分支
19
+ * @param {Command} program - Commander 实例
20
+ */
21
+ export function registerInitCommand(program: Command): void {
22
+ const initCmd = program
23
+ .command('init')
24
+ .description('初始化项目级配置,设置主工作分支')
25
+ .option('-b, --branch <branchName>', '指定主工作分支名(默认使用当前分支)')
26
+ .action(async (options: InitOptions) => {
27
+ await handleInit(options);
28
+ });
29
+
30
+ // 注册 show 子命令:展示当前项目配置
31
+ initCmd.addCommand(
32
+ new Cmd('show')
33
+ .description('展示当前项目的 init 配置')
34
+ .action(() => {
35
+ handleInitShow();
36
+ }),
37
+ );
38
+ }
39
+
40
+ /**
41
+ * 处理 init show 子命令:以 JSON 格式展示当前项目完整配置
42
+ */
43
+ function handleInitShow(): void {
44
+ validateMainWorktree();
45
+ const config = requireProjectConfig();
46
+ const configJson = safeStringify(config);
47
+ printInfo(MESSAGES.INIT_SHOW(configJson));
48
+ }
49
+
50
+ /**
51
+ * 执行 init 命令的核心逻辑
52
+ * 无论是否已初始化,始终执行设置/切换主工作分支
53
+ * 有 -b 参数时:使用指定分支
54
+ * 无 -b 参数时:使用当前分支
55
+ * @param {InitOptions} options - 命令选项
56
+ */
57
+ async function handleInit(options: InitOptions): Promise<void> {
58
+ validateMainWorktree();
59
+
60
+ const existingConfig = loadProjectConfig();
61
+
62
+ // 确定分支名:优先使用 -b 参数,否则使用当前分支
63
+ const branchName = options.branch || getCurrentBranch();
64
+
65
+ logger.info(`init 命令执行,主工作分支: ${branchName}`);
66
+
67
+ // 保存项目配置
68
+ saveProjectConfig({ clawtMainWorkBranch: branchName });
69
+
70
+ if (existingConfig) {
71
+ printSuccess(MESSAGES.INIT_UPDATED(existingConfig.clawtMainWorkBranch, branchName));
72
+ } else {
73
+ printSuccess(MESSAGES.INIT_SUCCESS(branchName));
74
+ }
75
+ }
@@ -21,7 +21,7 @@ import {
21
21
  export function registerListCommand(program: Command): void {
22
22
  program
23
23
  .command('list')
24
- .description('列出当前项目所有 worktree')
24
+ .description('列出当前项目所有 worktree(支持 --json 格式输出)')
25
25
  .option('--json', '以 JSON 格式输出')
26
26
  .action((options: ListOptions) => {
27
27
  handleList(options);
@@ -27,8 +27,12 @@ import {
27
27
  hasCommitWithMessage,
28
28
  gitMergeBase,
29
29
  gitResetSoftTo,
30
- getCurrentBranch,
30
+ gitResetHard,
31
+ gitCleanForce,
32
+ gitCheckout,
31
33
  resolveTargetWorktree,
34
+ getMainWorkBranch,
35
+ ensureOnMainWorkBranch,
32
36
  } from '../utils/index.js';
33
37
  import type { WorktreeResolveMessages } from '../utils/index.js';
34
38
 
@@ -40,8 +44,8 @@ export function registerMergeCommand(program: Command): void {
40
44
  program
41
45
  .command('merge')
42
46
  .description('合并某个已验证的 worktree 分支到主 worktree')
43
- .option('-b, --branch <branchName>', '要合并的分支名(支持模糊匹配,不传则列出所有分支)')
44
- .option('-m, --message <message>', '提交信息(工作区有修改时必填)')
47
+ .option('-b, --branch <branchName>', '要合并的分支名(支持模糊匹配,不传则列出所有分支供选择)')
48
+ .option('-m, --message <commitMessage>', '提交信息(目标 worktree 工作区有修改时必填)')
45
49
  .action(async (options: MergeOptions) => {
46
50
  await handleMerge(options);
47
51
  });
@@ -82,7 +86,7 @@ async function handleSquashIfNeeded(
82
86
  }
83
87
 
84
88
  // 获取当前主分支名并计算分叉点
85
- const mainBranch = getCurrentBranch(mainWorktreePath);
89
+ const mainBranch = getMainWorkBranch();
86
90
  const mergeBase = gitMergeBase(mainBranch, branchName, mainWorktreePath);
87
91
  logger.info(`squash: merge-base = ${mergeBase}, 分支 = ${branchName}`);
88
92
 
@@ -135,6 +139,9 @@ async function handleMerge(options: MergeOptions): Promise<void> {
135
139
 
136
140
  const mainWorktreePath = getGitTopLevel();
137
141
 
142
+ // 确保当前在主工作分支上
143
+ await ensureOnMainWorkBranch(mainWorktreePath);
144
+
138
145
  logger.info(`merge 命令执行,分支: ${options.branch ?? '(未指定)'},提交信息: ${options.message ?? '(未提供)'}`);
139
146
 
140
147
  // 解析目标 worktree(精确匹配 / 模糊匹配 / 交互选择)
@@ -21,6 +21,9 @@ import {
21
21
  removeSnapshot,
22
22
  removeProjectSnapshots,
23
23
  resolveTargetWorktrees,
24
+ getValidateBranchName,
25
+ deleteValidateBranch,
26
+ ensureOnMainWorkBranch,
24
27
  } from '../utils/index.js';
25
28
  import type { WorktreeMultiResolveMessages } from '../utils/index.js';
26
29
 
@@ -39,7 +42,7 @@ const REMOVE_RESOLVE_MESSAGES: WorktreeMultiResolveMessages = {
39
42
  export function registerRemoveCommand(program: Command): void {
40
43
  program
41
44
  .command('remove')
42
- .description('移除 worktree(支持单个/批量/全部)')
45
+ .description('移除 worktree(支持模糊匹配/多选/全部)')
43
46
  .option('--all', '移除当前项目下所有 worktree')
44
47
  .option('-b, --branch <branchName>', '指定分支名(支持模糊匹配,不传则列出所有分支)')
45
48
  .action(async (options: RemoveOptions) => {
@@ -76,7 +79,7 @@ async function handleRemove(options: RemoveOptions): Promise<void> {
76
79
  // 列出即将移除的 worktree
77
80
  printInfo('即将移除以下 worktree 及本地分支:\n');
78
81
  worktreesToRemove.forEach((wt, index) => {
79
- printInfo(` ${index + 1}. ${wt.path} → 分支: ${wt.branch}`);
82
+ printInfo(` ${index + 1}. ${wt.path} → 分支: ${wt.branch} 验证分支: ${getValidateBranchName(wt.branch)}`);
80
83
  });
81
84
  printInfo('');
82
85
 
@@ -85,7 +88,7 @@ async function handleRemove(options: RemoveOptions): Promise<void> {
85
88
  let shouldDeleteBranch = autoDelete;
86
89
 
87
90
  if (!autoDelete) {
88
- shouldDeleteBranch = await confirmAction('是否同时删除对应的本地分支?');
91
+ shouldDeleteBranch = await confirmAction(MESSAGES.REMOVE_CONFIRM_DELETE_BRANCHES);
89
92
  if (!shouldDeleteBranch) {
90
93
  printHint(MESSAGES.REMOVE_BRANCHES_KEPT);
91
94
  }
@@ -95,10 +98,14 @@ async function handleRemove(options: RemoveOptions): Promise<void> {
95
98
  const failures: Array<{ path: string; error: string }> = [];
96
99
  for (const wt of worktreesToRemove) {
97
100
  try {
101
+ // 确保当前在主工作分支上
102
+ await ensureOnMainWorkBranch();
98
103
  removeWorktreeByPath(wt.path);
99
104
  if (shouldDeleteBranch) {
100
105
  deleteBranch(wt.branch);
101
106
  }
107
+ // 删除对应的验证分支
108
+ deleteValidateBranch(wt.branch);
102
109
  // 清理该分支对应的 validate 快照
103
110
  removeSnapshot(projectName, wt.branch);
104
111
  printSuccess(MESSAGES.WORKTREE_REMOVED(wt.path));
@@ -11,6 +11,7 @@ import {
11
11
  confirmDestructiveAction,
12
12
  printSuccess,
13
13
  printInfo,
14
+ requireProjectConfig,
14
15
  } from '../utils/index.js';
15
16
 
16
17
  /**
@@ -31,6 +32,8 @@ export function registerResetCommand(program: Command): void {
31
32
  */
32
33
  async function handleReset(): Promise<void> {
33
34
  validateMainWorktree();
35
+ requireProjectConfig();
36
+
34
37
  const mainWorktreePath = getGitTopLevel();
35
38
  logger.info('reset 命令执行');
36
39
 
@@ -33,7 +33,7 @@ const RESUME_RESOLVE_MESSAGES: WorktreeMultiResolveMessages = {
33
33
  export function registerResumeCommand(program: Command): void {
34
34
  program
35
35
  .command('resume')
36
- .description('在已有 worktree 中恢复 Claude Code 交互式会话')
36
+ .description('在已有 worktree 中恢复 Claude Code 会话(支持多选批量恢复)')
37
37
  .option('-b, --branch <branchName>', '要恢复的分支名(支持模糊匹配,不传则列出所有分支)')
38
38
  .action(async (options: ResumeOptions) => {
39
39
  await handleResume(options);