clawt 2.7.0 → 2.7.1

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.
@@ -6,10 +6,11 @@
6
6
  - 完整的软件规格说明,包含 7 大章节
7
7
  - 命令流程在 `5. 需求场景详细设计` 下,每个命令一个子章节(5.1-5.13)
8
8
  - run 命令对应 `5.2 批量创建 Worktree + 执行 Claude Code 任务`,流程按步骤编号描述
9
- - merge 命令对应 `5.6 合并验证过的分支`,流程按步骤编号描述
9
+ - merge 命令对应 `5.6 合并验证过的分支`,-b 可选,支持模糊匹配(与 resume/validate 共享匹配逻辑),流程按步骤编号描述
10
10
  - config 命令对应 `5.10 查看全局配置`,只读展示配置
11
11
  - resume 命令对应 `5.11 在已有 Worktree 中恢复会话`,支持模糊匹配和交互式分支选择(-b 可选)
12
12
  - validate 命令对应 `5.4 在主 Worktree 验证其他分支`,-b 可选,支持模糊匹配(与 resume 共享匹配逻辑)
13
+ - sync 命令对应 `5.12 将主分支代码同步到目标 Worktree`,-b 可选,支持模糊匹配(与 resume/validate/merge 共享匹配逻辑)
13
14
  - 配置项说明在 `5.7 默认配置文件` 章节的表格中
14
15
  - 更新模式:新增步骤时追加编号,配置项影响范围变化时更新说明列
15
16
 
@@ -66,10 +67,10 @@ Notes:
66
67
  - resume 和 run(交互式模式)共用 `launchInteractiveClaude()`,该函数从 run.ts 提取到 src/utils/claude.ts
67
68
  - `claudeCodeCommand` 配置项同时影响 run 交互式模式和 resume 命令
68
69
  - reset 命令与 validate --clean 的区别:reset 不删除快照文件,validate --clean 会删除快照
69
- - `resolveTargetWorktree()` 是 resume 和 validate 共用的分支匹配函数(在 src/utils/worktree-matcher.ts)
70
+ - `resolveTargetWorktree()` 是 resume、validate、mergesync 共用的分支匹配函数(在 src/utils/worktree-matcher.ts)
70
71
  - `WorktreeResolveMessages` 接口实现命令间消息解耦,每个命令传入各自的提示文案
71
- - resume 的消息常量在 `MESSAGES.RESUME_*`,validate 的消息常量在 `MESSAGES.VALIDATE_*`
72
- - resume 和 validate 的 `-b` 参数均为可选,匹配策略一致:精确→模糊(子串,大小写不敏感)→交互选择
72
+ - resume 的消息常量在 `MESSAGES.RESUME_*`,validate 的消息常量在 `MESSAGES.VALIDATE_*`,merge 的消息常量在 `MESSAGES.MERGE_*`,sync 的消息常量在 `MESSAGES.SYNC_*`
73
+ - resume、validate、mergesync 的 `-b` 参数均为可选,匹配策略一致:精确→模糊(子串,大小写不敏感)→交互选择
73
74
  - validate 的交互式选择和 resume 使用同一个 `promptSelectBranch()`(Enquirer.Select)
74
75
 
75
76
  ## validate 快照机制
package/README.md CHANGED
@@ -150,39 +150,67 @@ clawt validate --clean
150
150
  ### `clawt sync` — 将主分支代码同步到目标 worktree
151
151
 
152
152
  ```bash
153
+ # 指定分支名(支持模糊匹配)
153
154
  clawt sync -b <branchName>
155
+
156
+ # 不指定分支名(列出所有分支供选择)
157
+ clawt sync
154
158
  ```
155
159
 
156
160
  | 参数 | 必填 | 说明 |
157
161
  | ---- | ---- | ---- |
158
- | `-b` | | 要同步的分支名 |
162
+ | `-b` | | 要同步的分支名(支持模糊匹配,不传则列出所有分支供选择) |
159
163
 
160
164
  将主分支最新代码合并到目标 worktree 的分支中。如果目标 worktree 有未提交的修改,会自动保存后再合并。存在冲突时会提示用户手动解决。合并成功后会自动清除该分支的 validate 快照(代码基础已变化,旧快照无效)。
161
165
 
166
+ **分支匹配策略:**
167
+ - 传 `-b` 时,优先精确匹配分支名;未精确匹配则进行模糊匹配(子串匹配,大小写不敏感);模糊匹配到多个时通过交互列表选择;无匹配时报错并列出所有可用分支
168
+ - 不传 `-b` 时,列出当前项目所有可用分支供交互式选择(仅 1 个时自动使用)
169
+
162
170
  ```bash
163
- # 将主分支最新代码同步到目标 worktree
171
+ # 精确匹配分支名
164
172
  clawt sync -b feature-scheme-1
173
+
174
+ # 模糊匹配(匹配包含 "scheme" 的分支)
175
+ clawt sync -b scheme
176
+
177
+ # 交互式选择所有分支
178
+ clawt sync
165
179
  ```
166
180
 
167
181
  ### `clawt merge` — 合并分支到主 worktree
168
182
 
169
183
  ```bash
184
+ # 指定分支名(支持模糊匹配)
170
185
  clawt merge -b <branchName> [-m <commitMessage>]
186
+
187
+ # 不指定分支名(列出所有分支供选择)
188
+ clawt merge [-m <commitMessage>]
171
189
  ```
172
190
 
173
191
  | 参数 | 必填 | 说明 |
174
192
  | ---- | ---- | ---- |
175
- | `-b` | | 要合并的分支名 |
193
+ | `-b` | | 要合并的分支名(支持模糊匹配,不传则列出所有分支供选择) |
176
194
  | `-m` | 否 | 提交信息(目标 worktree 工作区有修改时必填) |
177
195
 
178
196
  将目标 worktree 的变更合并到主 worktree 的当前分支。如果配置了 `autoPullPush: true`,合并后会自动推送到远程仓库。如果目标 worktree 工作区有未提交的修改,需要通过 `-m` 提供提交信息;如果目标 worktree 已经提交过(工作区干净但有本地提交),可以省略 `-m` 直接合并。merge 成功后会询问是否清理对应的 worktree 和分支(如果配置了 `autoDeleteBranch: true` 则自动清理)。
179
197
 
198
+ **分支匹配策略:**
199
+ - 传 `-b` 时,优先精确匹配分支名;未精确匹配则进行模糊匹配(子串匹配,大小写不敏感);模糊匹配到多个时通过交互列表选择;无匹配时报错并列出所有可用分支
200
+ - 不传 `-b` 时,列出当前项目所有可用分支供交互式选择(仅 1 个时自动使用)
201
+
180
202
  如果检测到目标分支存在 `clawt sync` 产生的临时提交(auto-save commit),会自动提示是否将所有提交压缩(squash)为一个。用户选择压缩后,所有 commit 会被 reset 到暂存区:如果提供了 `-m` 则直接提交并继续合并流程;如果未提供 `-m` 则提示用户前往目标 worktree 自行提交后重新执行 merge。
181
203
 
182
204
  ```bash
183
- # 目标 worktree 有未提交修改,需提供 -m
205
+ # 精确匹配,目标 worktree 有未提交修改,需提供 -m
184
206
  clawt merge -b feature-scheme-1 -m "feat: 实现用户登录功能"
185
207
 
208
+ # 模糊匹配(匹配包含 "scheme" 的分支)
209
+ clawt merge -b scheme
210
+
211
+ # 交互式选择所有分支
212
+ clawt merge
213
+
186
214
  # 目标 worktree 已提交过,可省略 -m
187
215
  clawt merge -b feature-scheme-1
188
216
  ```
package/dist/index.js CHANGED
@@ -144,7 +144,27 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
144
144
  /** validate 交互选择提示 */
145
145
  VALIDATE_SELECT_BRANCH: "\u8BF7\u9009\u62E9\u8981\u9A8C\u8BC1\u7684\u5206\u652F",
146
146
  /** validate 模糊匹配到多个结果提示 */
147
- VALIDATE_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`
147
+ VALIDATE_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`,
148
+ /** merge 无可用 worktree */
149
+ MERGE_NO_WORKTREES: "\u5F53\u524D\u9879\u76EE\u6CA1\u6709\u53EF\u7528\u7684 worktree\uFF0C\u8BF7\u5148\u901A\u8FC7 clawt run \u6216 clawt create \u521B\u5EFA",
150
+ /** merge 模糊匹配无结果,列出可用分支 */
151
+ MERGE_NO_MATCH: (name, branches) => `\u672A\u627E\u5230\u4E0E "${name}" \u5339\u914D\u7684\u5206\u652F
152
+ \u53EF\u7528\u5206\u652F\uFF1A
153
+ ${branches.map((b) => ` - ${b}`).join("\n")}`,
154
+ /** merge 交互选择提示 */
155
+ MERGE_SELECT_BRANCH: "\u8BF7\u9009\u62E9\u8981\u5408\u5E76\u7684\u5206\u652F",
156
+ /** merge 模糊匹配到多个结果提示 */
157
+ MERGE_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`,
158
+ /** sync 无可用 worktree */
159
+ SYNC_NO_WORKTREES: "\u5F53\u524D\u9879\u76EE\u6CA1\u6709\u53EF\u7528\u7684 worktree\uFF0C\u8BF7\u5148\u901A\u8FC7 clawt run \u6216 clawt create \u521B\u5EFA",
160
+ /** sync 模糊匹配无结果,列出可用分支 */
161
+ SYNC_NO_MATCH: (name, branches) => `\u672A\u627E\u5230\u4E0E "${name}" \u5339\u914D\u7684\u5206\u652F
162
+ \u53EF\u7528\u5206\u652F\uFF1A
163
+ ${branches.map((b) => ` - ${b}`).join("\n")}`,
164
+ /** sync 交互选择提示 */
165
+ SYNC_SELECT_BRANCH: "\u8BF7\u9009\u62E9\u8981\u540C\u6B65\u7684\u5206\u652F",
166
+ /** sync 模糊匹配到多个结果提示 */
167
+ SYNC_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`
148
168
  };
149
169
 
150
170
  // src/constants/exitCodes.ts
@@ -1317,13 +1337,17 @@ async function handleValidate(options) {
1317
1337
  }
1318
1338
 
1319
1339
  // src/commands/merge.ts
1320
- import { join as join4 } from "path";
1321
- import { existsSync as existsSync6 } from "fs";
1322
1340
  function registerMergeCommand(program2) {
1323
- program2.command("merge").description("\u5408\u5E76\u67D0\u4E2A\u5DF2\u9A8C\u8BC1\u7684 worktree \u5206\u652F\u5230\u4E3B worktree").requiredOption("-b, --branch <branchName>", "\u8981\u5408\u5E76\u7684\u5206\u652F\u540D").option("-m, --message <message>", "\u63D0\u4EA4\u4FE1\u606F\uFF08\u5DE5\u4F5C\u533A\u6709\u4FEE\u6539\u65F6\u5FC5\u586B\uFF09").action(async (options) => {
1341
+ program2.command("merge").description("\u5408\u5E76\u67D0\u4E2A\u5DF2\u9A8C\u8BC1\u7684 worktree \u5206\u652F\u5230\u4E3B worktree").option("-b, --branch <branchName>", "\u8981\u5408\u5E76\u7684\u5206\u652F\u540D\uFF08\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF0C\u4E0D\u4F20\u5219\u5217\u51FA\u6240\u6709\u5206\u652F\uFF09").option("-m, --message <message>", "\u63D0\u4EA4\u4FE1\u606F\uFF08\u5DE5\u4F5C\u533A\u6709\u4FEE\u6539\u65F6\u5FC5\u586B\uFF09").action(async (options) => {
1324
1342
  await handleMerge(options);
1325
1343
  });
1326
1344
  }
1345
+ var MERGE_RESOLVE_MESSAGES = {
1346
+ noWorktrees: MESSAGES.MERGE_NO_WORKTREES,
1347
+ selectBranch: MESSAGES.MERGE_SELECT_BRANCH,
1348
+ multipleMatches: MESSAGES.MERGE_MULTIPLE_MATCHES,
1349
+ noMatch: MESSAGES.MERGE_NO_MATCH
1350
+ };
1327
1351
  async function handleSquashIfNeeded(targetWorktreePath, mainWorktreePath, branchName, commitMessage) {
1328
1352
  if (!hasCommitWithMessage(branchName, AUTO_SAVE_COMMIT_MESSAGE, mainWorktreePath)) {
1329
1353
  return false;
@@ -1359,20 +1383,18 @@ function cleanupWorktreeAndBranch(worktreePath, branchName) {
1359
1383
  async function handleMerge(options) {
1360
1384
  validateMainWorktree();
1361
1385
  const mainWorktreePath = getGitTopLevel();
1362
- const projectDir = getProjectWorktreeDir();
1363
- const targetWorktreePath = join4(projectDir, options.branch);
1364
- logger.info(`merge \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch}\uFF0C\u63D0\u4EA4\u4FE1\u606F: ${options.message ?? "(\u672A\u63D0\u4F9B)"}`);
1365
- if (!existsSync6(targetWorktreePath)) {
1366
- throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
1367
- }
1386
+ logger.info(`merge \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch ?? "(\u672A\u6307\u5B9A)"}\uFF0C\u63D0\u4EA4\u4FE1\u606F: ${options.message ?? "(\u672A\u63D0\u4F9B)"}`);
1387
+ const worktrees = getProjectWorktrees();
1388
+ const worktree = await resolveTargetWorktree(worktrees, MERGE_RESOLVE_MESSAGES, options.branch);
1389
+ const { path: targetWorktreePath, branch } = worktree;
1368
1390
  const projectName = getProjectName();
1369
1391
  if (!isWorkingDirClean(mainWorktreePath)) {
1370
- if (hasSnapshot(projectName, options.branch)) {
1371
- printWarning(MESSAGES.MERGE_VALIDATE_STATE_HINT(options.branch));
1392
+ if (hasSnapshot(projectName, branch)) {
1393
+ printWarning(MESSAGES.MERGE_VALIDATE_STATE_HINT(branch));
1372
1394
  }
1373
1395
  throw new ClawtError(MESSAGES.MAIN_WORKTREE_DIRTY);
1374
1396
  }
1375
- const shouldExit = await handleSquashIfNeeded(targetWorktreePath, mainWorktreePath, options.branch, options.message);
1397
+ const shouldExit = await handleSquashIfNeeded(targetWorktreePath, mainWorktreePath, branch, options.message);
1376
1398
  if (shouldExit) {
1377
1399
  return;
1378
1400
  }
@@ -1384,12 +1406,12 @@ async function handleMerge(options) {
1384
1406
  gitAddAll(targetWorktreePath);
1385
1407
  gitCommit(options.message, targetWorktreePath);
1386
1408
  } else {
1387
- if (!hasLocalCommits(options.branch, mainWorktreePath)) {
1409
+ if (!hasLocalCommits(branch, mainWorktreePath)) {
1388
1410
  throw new ClawtError(MESSAGES.TARGET_WORKTREE_NO_CHANGES);
1389
1411
  }
1390
1412
  }
1391
1413
  try {
1392
- gitMerge(options.branch, mainWorktreePath);
1414
+ gitMerge(branch, mainWorktreePath);
1393
1415
  } catch (error) {
1394
1416
  if (hasMergeConflict(mainWorktreePath)) {
1395
1417
  throw new ClawtError(MESSAGES.MERGE_CONFLICT);
@@ -1407,16 +1429,16 @@ async function handleMerge(options) {
1407
1429
  printInfo("\u5DF2\u8DF3\u8FC7\u81EA\u52A8 pull/push\uFF0C\u8BF7\u624B\u52A8\u6267\u884C git pull && git push");
1408
1430
  }
1409
1431
  if (options.message) {
1410
- printSuccess(MESSAGES.MERGE_SUCCESS(options.branch, options.message, autoPullPush));
1432
+ printSuccess(MESSAGES.MERGE_SUCCESS(branch, options.message, autoPullPush));
1411
1433
  } else {
1412
- printSuccess(MESSAGES.MERGE_SUCCESS_NO_MESSAGE(options.branch, autoPullPush));
1434
+ printSuccess(MESSAGES.MERGE_SUCCESS_NO_MESSAGE(branch, autoPullPush));
1413
1435
  }
1414
- const shouldCleanup = await shouldCleanupAfterMerge(options.branch);
1436
+ const shouldCleanup = await shouldCleanupAfterMerge(branch);
1415
1437
  if (shouldCleanup) {
1416
- cleanupWorktreeAndBranch(targetWorktreePath, options.branch);
1438
+ cleanupWorktreeAndBranch(targetWorktreePath, branch);
1417
1439
  }
1418
- if (hasSnapshot(projectName, options.branch)) {
1419
- removeSnapshot(projectName, options.branch);
1440
+ if (hasSnapshot(projectName, branch)) {
1441
+ removeSnapshot(projectName, branch);
1420
1442
  }
1421
1443
  }
1422
1444
 
@@ -1455,13 +1477,17 @@ function formatConfigValue(value) {
1455
1477
  }
1456
1478
 
1457
1479
  // src/commands/sync.ts
1458
- import { existsSync as existsSync7 } from "fs";
1459
- import { join as join5 } from "path";
1460
1480
  function registerSyncCommand(program2) {
1461
- program2.command("sync").description("\u5C06\u4E3B\u5206\u652F\u6700\u65B0\u4EE3\u7801\u540C\u6B65\u5230\u76EE\u6807 worktree").requiredOption("-b, --branch <branchName>", "\u8981\u540C\u6B65\u7684\u5206\u652F\u540D").action(async (options) => {
1481
+ program2.command("sync").description("\u5C06\u4E3B\u5206\u652F\u6700\u65B0\u4EE3\u7801\u540C\u6B65\u5230\u76EE\u6807 worktree").option("-b, --branch <branchName>", "\u8981\u540C\u6B65\u7684\u5206\u652F\u540D\uFF08\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF0C\u4E0D\u4F20\u5219\u5217\u51FA\u6240\u6709\u5206\u652F\uFF09").action(async (options) => {
1462
1482
  await handleSync(options);
1463
1483
  });
1464
1484
  }
1485
+ var SYNC_RESOLVE_MESSAGES = {
1486
+ noWorktrees: MESSAGES.SYNC_NO_WORKTREES,
1487
+ selectBranch: MESSAGES.SYNC_SELECT_BRANCH,
1488
+ multipleMatches: MESSAGES.SYNC_MULTIPLE_MATCHES,
1489
+ noMatch: MESSAGES.SYNC_NO_MATCH
1490
+ };
1465
1491
  function autoSaveChanges(worktreePath, branch) {
1466
1492
  gitAddAll(worktreePath);
1467
1493
  gitCommit(AUTO_SAVE_COMMIT_MESSAGE, worktreePath);
@@ -1481,13 +1507,10 @@ function mergeMainBranch(worktreePath, mainBranch) {
1481
1507
  }
1482
1508
  async function handleSync(options) {
1483
1509
  validateMainWorktree();
1484
- const { branch } = options;
1485
- logger.info(`sync \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${branch}`);
1486
- const projectWorktreeDir = getProjectWorktreeDir();
1487
- const targetWorktreePath = join5(projectWorktreeDir, branch);
1488
- if (!existsSync7(targetWorktreePath)) {
1489
- throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(branch));
1490
- }
1510
+ logger.info(`sync \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch ?? "(\u672A\u6307\u5B9A)"}`);
1511
+ const worktrees = getProjectWorktrees();
1512
+ const worktree = await resolveTargetWorktree(worktrees, SYNC_RESOLVE_MESSAGES, options.branch);
1513
+ const { path: targetWorktreePath, branch } = worktree;
1491
1514
  const mainWorktreePath = getGitTopLevel();
1492
1515
  const mainBranch = getCurrentBranch(mainWorktreePath);
1493
1516
  if (!isWorkingDirClean(targetWorktreePath)) {
package/docs/spec.md CHANGED
@@ -622,41 +622,57 @@ git branch -D <branchName>
622
622
  **命令:**
623
623
 
624
624
  ```bash
625
+ # 指定分支名(支持模糊匹配)
625
626
  clawt merge -b <branchName> [-m <commitMessage>]
627
+
628
+ # 不指定分支名(列出所有分支供选择)
629
+ clawt merge [-m <commitMessage>]
626
630
  ```
627
631
 
628
632
  **参数:**
629
633
 
630
- | 参数 | 必填 | 说明 |
631
- | ---- | ---- | ---------------------------------------- |
632
- | `-b` | | 要合并的分支名 |
633
- | `-m` | 否 | 提交信息(目标 worktree 工作区有修改时必填) |
634
+ | 参数 | 必填 | 说明 |
635
+ | ---- | ---- | ------------------------------------------------------------------------ |
636
+ | `-b` | | 要合并的分支名(支持模糊匹配,不传则列出所有分支供选择) |
637
+ | `-m` | 否 | 提交信息(目标 worktree 工作区有修改时必填) |
634
638
 
635
639
  **运行流程:**
636
640
 
637
641
  1. **主 worktree 校验** (2.1)
638
- 2. **主 worktree 状态检测**
642
+ 2. **解析目标 worktree**:根据 `-b` 参数解析目标 worktree,匹配策略如下:
643
+ - **未传 `-b` 参数**:
644
+ - 获取当前项目所有 worktree
645
+ - 无可用 worktree → 报错退出
646
+ - 仅 1 个 worktree → 直接使用,无需选择
647
+ - 多个 worktree → 通过交互式列表(Enquirer.Select)让用户选择
648
+ - **传了 `-b` 参数**:
649
+ 1. **精确匹配优先**:在 worktree 列表中查找分支名完全相同的 worktree,找到则直接使用
650
+ 2. **模糊匹配**(子串匹配,大小写不敏感):
651
+ - 唯一匹配 → 直接使用
652
+ - 多个匹配 → 通过交互式列表让用户从匹配结果中选择
653
+ 3. **无匹配** → 报错退出,并列出所有可用分支名
654
+ 3. **主 worktree 状态检测**
639
655
  - 执行 `git status --porcelain`
640
656
  - 如果有更改:
641
657
  - 如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.tree`),额外输出警告提示用户可先执行 `clawt validate -b <branchName> --clean` 清理
642
658
  - 提示 `主 worktree 有未提交的更改,请先处理`,退出
643
659
  - 无更改 → 继续
644
- 3. **Squash 检测与执行(auto-save 临时提交压缩)**
660
+ 4. **Squash 检测与执行(auto-save 临时提交压缩)**
645
661
  - 通过 `git log HEAD..<branchName> --format=%s` 检查目标分支是否存在以 `AUTO_SAVE_COMMIT_MESSAGE`(`chore: auto-save before sync`)为前缀的 commit
646
- - **不存在** → 跳过,进入步骤 4
662
+ - **不存在** → 跳过,进入步骤 5
647
663
  - **存在** → 提示用户是否将所有提交压缩为一个:
648
664
  ```
649
665
  检测到 sync 产生的临时提交,是否将所有提交压缩为一个?
650
666
  压缩后变更将保留在目标worktree的暂存区,需要重新提交
651
667
  ```
652
- - **用户选择不压缩** → 跳过,进入步骤 4
668
+ - **用户选择不压缩** → 跳过,进入步骤 5
653
669
  - **用户选择压缩** →
654
670
  1. 获取主分支名(`git rev-parse --abbrev-ref HEAD`)
655
671
  2. 计算分叉点:`git merge-base <mainBranch> <branchName>`
656
672
  3. 在目标 worktree 中执行 `git reset --soft <merge-base>`,将所有 commit 撤销到暂存区
657
- 4. 如果用户提供了 `-m` → 直接在目标 worktree 执行 `git commit -m '<commitMessage>'`,输出成功提示,继续步骤 4
673
+ 4. 如果用户提供了 `-m` → 直接在目标 worktree 执行 `git commit -m '<commitMessage>'`,输出成功提示,继续步骤 5
658
674
  5. 如果用户未提供 `-m` → 提示用户前往目标 worktree 自行提交后重新执行 `clawt merge`,**退出流程**
659
- 4. **根据目标 worktree 状态决定是否需要提交**
675
+ 5. **根据目标 worktree 状态决定是否需要提交**
660
676
  - 检测目标 worktree 工作区是否干净(`git status --porcelain`)
661
677
  - **工作区有未提交修改**:
662
678
  - 如果用户未提供 `-m`,提示 `目标 worktree 有未提交的修改,请通过 -m 参数提供提交信息`,退出
@@ -670,22 +686,22 @@ clawt merge -b <branchName> [-m <commitMessage>]
670
686
  - 检查目标分支相对于主分支是否有本地提交(`git log HEAD..<branchName> --oneline`)
671
687
  - 有本地提交 → 跳过提交步骤,直接进入合并
672
688
  - 无本地提交 → 提示 `目标 worktree 没有任何可合并的变更(工作区干净且无本地提交)`,退出
673
- 5. **回到主 worktree 进行合并**
689
+ 6. **回到主 worktree 进行合并**
674
690
  ```bash
675
691
  cd <主 worktree 路径>
676
692
  git merge <branchName>
677
693
  ```
678
- 6. **冲突检测**
694
+ 7. **冲突检测**
679
695
  - 检查 merge 退出码及 `git status` 是否存在冲突
680
696
  - **有冲突** → 提示 `合并存在冲突,请手动处理`,退出
681
697
  - **无冲突** → 继续
682
- 7. **推送(受 `autoPullPush` 配置控制)**
698
+ 8. **推送(受 `autoPullPush` 配置控制)**
683
699
  ```bash
684
700
  # 仅当 autoPullPush 为 true 时执行
685
701
  git pull
686
702
  git push
687
703
  ```
688
- 8. **输出成功提示**
704
+ 9. **输出成功提示**
689
705
 
690
706
  ```
691
707
  # 提供了 -m 且已推送时
@@ -705,7 +721,7 @@ clawt merge -b <branchName> [-m <commitMessage>]
705
721
  ✓ 分支 feature-scheme-1 已成功合并到当前分支
706
722
  ```
707
723
 
708
- 9. **merge 成功后确认并清理 worktree 和分支(可选)**
724
+ 10. **merge 成功后确认并清理 worktree 和分支(可选)**
709
725
  - 如果配置文件中 `autoDeleteBranch` 为 `true`,自动执行清理
710
726
  - 否则交互式询问用户是否清理
711
727
  - 用户确认后,依次执行:
@@ -720,7 +736,7 @@ clawt merge -b <branchName> [-m <commitMessage>]
720
736
  ```
721
737
  - 输出清理成功提示:`✓ 已清理 worktree 和分支: <branchName>`
722
738
 
723
- 10. **清理 validate 快照**
739
+ 11. **清理 validate 快照**
724
740
  - merge 成功后,如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.tree` 和 `<branchName>.head`),自动删除这些快照文件(merge 成功后快照已无意义)
725
741
 
726
742
  > **注意:** 清理确认和清理操作均在 merge 成功后执行。只有 merge 成功才会询问用户是否清理 worktree 和分支,避免 merge 冲突时用户被提前询问造成困惑。
@@ -951,14 +967,18 @@ clawt resume
951
967
  **命令:**
952
968
 
953
969
  ```bash
970
+ # 指定分支名(支持模糊匹配)
954
971
  clawt sync -b <branchName>
972
+
973
+ # 不指定分支名(列出所有分支供选择)
974
+ clawt sync
955
975
  ```
956
976
 
957
977
  **参数:**
958
978
 
959
- | 参数 | 必填 | 说明 |
960
- | ---- | ---- | ----------------------------------------------------- |
961
- | `-b` | | 要同步的分支名(对应已有 worktree 的分支) |
979
+ | 参数 | 必填 | 说明 |
980
+ | ---- | ---- | ------------------------------------------------------------------------ |
981
+ | `-b` | | 要同步的分支名(支持模糊匹配,不传则列出所有分支供选择) |
962
982
 
963
983
  **使用场景:**
964
984
 
@@ -967,8 +987,18 @@ clawt sync -b <branchName>
967
987
  **运行流程:**
968
988
 
969
989
  1. **主 worktree 校验** (2.1)
970
- 2. **检查目标 worktree 是否存在**:确认 `~/.clawt/worktrees/<project>/<branchName>` 目录存在
971
- - 不存在 报错退出
990
+ 2. **解析目标 worktree**:根据 `-b` 参数解析目标 worktree,匹配策略如下:
991
+ - **未传 `-b` 参数**:
992
+ - 获取当前项目所有 worktree
993
+ - 无可用 worktree → 报错退出
994
+ - 仅 1 个 worktree → 直接使用,无需选择
995
+ - 多个 worktree → 通过交互式列表(Enquirer.Select)让用户选择
996
+ - **传了 `-b` 参数**:
997
+ 1. **精确匹配优先**:在 worktree 列表中查找分支名完全相同的 worktree,找到则直接使用
998
+ 2. **模糊匹配**(子串匹配,大小写不敏感):
999
+ - 唯一匹配 → 直接使用
1000
+ - 多个匹配 → 通过交互式列表让用户从匹配结果中选择
1001
+ 3. **无匹配** → 报错退出,并列出所有可用分支名
972
1002
  3. **获取主分支名**:通过 `git rev-parse --abbrev-ref HEAD` 获取主 worktree 当前分支名(不硬编码 main/master)
973
1003
  4. **自动保存未提交变更**:检查目标 worktree 是否有未提交修改
974
1004
  - 有修改 → 自动执行 `git add . && git commit -m "<AUTO_SAVE_COMMIT_MESSAGE>"` 保存变更(commit message 由常量 `AUTO_SAVE_COMMIT_MESSAGE` 定义,值为 `chore: auto-save before sync`,同时用于 merge 命令的 squash 检测)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "2.7.0",
3
+ "version": "2.7.1",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,4 @@
1
1
  import type { Command } from 'commander';
2
- import { join } from 'node:path';
3
- import { existsSync } from 'node:fs';
4
2
  import { logger } from '../logger/index.js';
5
3
  import { ClawtError } from '../errors/index.js';
6
4
  import { MESSAGES, AUTO_SAVE_COMMIT_MESSAGE } from '../constants/index.js';
@@ -9,7 +7,7 @@ import {
9
7
  validateMainWorktree,
10
8
  getProjectName,
11
9
  getGitTopLevel,
12
- getProjectWorktreeDir,
10
+ getProjectWorktrees,
13
11
  isWorkingDirClean,
14
12
  gitAddAll,
15
13
  gitCommit,
@@ -30,7 +28,9 @@ import {
30
28
  gitMergeBase,
31
29
  gitResetSoftTo,
32
30
  getCurrentBranch,
31
+ resolveTargetWorktree,
33
32
  } from '../utils/index.js';
33
+ import type { WorktreeResolveMessages } from '../utils/index.js';
34
34
 
35
35
  /**
36
36
  * 注册 merge 命令:合并验证过的分支到主 worktree
@@ -40,13 +40,21 @@ export function registerMergeCommand(program: Command): void {
40
40
  program
41
41
  .command('merge')
42
42
  .description('合并某个已验证的 worktree 分支到主 worktree')
43
- .requiredOption('-b, --branch <branchName>', '要合并的分支名')
43
+ .option('-b, --branch <branchName>', '要合并的分支名(支持模糊匹配,不传则列出所有分支)')
44
44
  .option('-m, --message <message>', '提交信息(工作区有修改时必填)')
45
45
  .action(async (options: MergeOptions) => {
46
46
  await handleMerge(options);
47
47
  });
48
48
  }
49
49
 
50
+ /** merge 命令的分支解析消息配置 */
51
+ const MERGE_RESOLVE_MESSAGES: WorktreeResolveMessages = {
52
+ noWorktrees: MESSAGES.MERGE_NO_WORKTREES,
53
+ selectBranch: MESSAGES.MERGE_SELECT_BRANCH,
54
+ multipleMatches: MESSAGES.MERGE_MULTIPLE_MATCHES,
55
+ noMatch: MESSAGES.MERGE_NO_MATCH,
56
+ };
57
+
50
58
  /**
51
59
  * 检测并处理目标分支的 auto-save 提交压缩
52
60
  * 如果检测到 sync 产生的临时提交,提示用户是否将所有提交压缩为一个
@@ -126,29 +134,27 @@ async function handleMerge(options: MergeOptions): Promise<void> {
126
134
  validateMainWorktree();
127
135
 
128
136
  const mainWorktreePath = getGitTopLevel();
129
- const projectDir = getProjectWorktreeDir();
130
- const targetWorktreePath = join(projectDir, options.branch);
131
137
 
132
- logger.info(`merge 命令执行,分支: ${options.branch},提交信息: ${options.message ?? '(未提供)'}`);
138
+ logger.info(`merge 命令执行,分支: ${options.branch ?? '(未指定)'},提交信息: ${options.message ?? '(未提供)'}`);
133
139
 
134
- // 检查目标 worktree 是否存在
135
- if (!existsSync(targetWorktreePath)) {
136
- throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
137
- }
140
+ // 解析目标 worktree(精确匹配 / 模糊匹配 / 交互选择)
141
+ const worktrees = getProjectWorktrees();
142
+ const worktree = await resolveTargetWorktree(worktrees, MERGE_RESOLVE_MESSAGES, options.branch);
143
+ const { path: targetWorktreePath, branch } = worktree;
138
144
 
139
145
  const projectName = getProjectName();
140
146
 
141
147
  // 步骤 3:主 worktree 状态检测
142
148
  if (!isWorkingDirClean(mainWorktreePath)) {
143
149
  // 如果存在 validate 快照状态,提示用户先清理
144
- if (hasSnapshot(projectName, options.branch)) {
145
- printWarning(MESSAGES.MERGE_VALIDATE_STATE_HINT(options.branch));
150
+ if (hasSnapshot(projectName, branch)) {
151
+ printWarning(MESSAGES.MERGE_VALIDATE_STATE_HINT(branch));
146
152
  }
147
153
  throw new ClawtError(MESSAGES.MAIN_WORKTREE_DIRTY);
148
154
  }
149
155
 
150
156
  // 步骤 3.5:检测是否需要 squash(sync 临时提交压缩)
151
- const shouldExit = await handleSquashIfNeeded(targetWorktreePath, mainWorktreePath, options.branch, options.message);
157
+ const shouldExit = await handleSquashIfNeeded(targetWorktreePath, mainWorktreePath, branch, options.message);
152
158
  if (shouldExit) {
153
159
  return;
154
160
  }
@@ -165,7 +171,7 @@ async function handleMerge(options: MergeOptions): Promise<void> {
165
171
  gitCommit(options.message, targetWorktreePath);
166
172
  } else {
167
173
  // 目标 worktree 干净,检查是否有本地提交
168
- if (!hasLocalCommits(options.branch, mainWorktreePath)) {
174
+ if (!hasLocalCommits(branch, mainWorktreePath)) {
169
175
  throw new ClawtError(MESSAGES.TARGET_WORKTREE_NO_CHANGES);
170
176
  }
171
177
  // 有本地提交,跳过提交步骤,直接合并
@@ -173,7 +179,7 @@ async function handleMerge(options: MergeOptions): Promise<void> {
173
179
 
174
180
  // 步骤 5:回到主 worktree 进行合并
175
181
  try {
176
- gitMerge(options.branch, mainWorktreePath);
182
+ gitMerge(branch, mainWorktreePath);
177
183
  } catch (error) {
178
184
  // 检查是否有冲突
179
185
  if (hasMergeConflict(mainWorktreePath)) {
@@ -198,19 +204,19 @@ async function handleMerge(options: MergeOptions): Promise<void> {
198
204
 
199
205
  // 步骤 8:输出成功提示(根据是否有 message 选择对应模板)
200
206
  if (options.message) {
201
- printSuccess(MESSAGES.MERGE_SUCCESS(options.branch, options.message, autoPullPush));
207
+ printSuccess(MESSAGES.MERGE_SUCCESS(branch, options.message, autoPullPush));
202
208
  } else {
203
- printSuccess(MESSAGES.MERGE_SUCCESS_NO_MESSAGE(options.branch, autoPullPush));
209
+ printSuccess(MESSAGES.MERGE_SUCCESS_NO_MESSAGE(branch, autoPullPush));
204
210
  }
205
211
 
206
212
  // 步骤 9:merge 成功后确认并清理 worktree 和分支
207
- const shouldCleanup = await shouldCleanupAfterMerge(options.branch);
213
+ const shouldCleanup = await shouldCleanupAfterMerge(branch);
208
214
  if (shouldCleanup) {
209
- cleanupWorktreeAndBranch(targetWorktreePath, options.branch);
215
+ cleanupWorktreeAndBranch(targetWorktreePath, branch);
210
216
  }
211
217
 
212
218
  // 步骤 10:清理 validate 快照(merge 成功后快照已无意义)
213
- if (hasSnapshot(projectName, options.branch)) {
214
- removeSnapshot(projectName, options.branch);
219
+ if (hasSnapshot(projectName, branch)) {
220
+ removeSnapshot(projectName, branch);
215
221
  }
216
222
  }
@@ -1,5 +1,3 @@
1
- import { existsSync } from 'node:fs';
2
- import { join } from 'node:path';
3
1
  import type { Command } from 'commander';
4
2
  import { logger } from '../logger/index.js';
5
3
  import { ClawtError } from '../errors/index.js';
@@ -9,7 +7,7 @@ import {
9
7
  validateMainWorktree,
10
8
  getGitTopLevel,
11
9
  getProjectName,
12
- getProjectWorktreeDir,
10
+ getProjectWorktrees,
13
11
  isWorkingDirClean,
14
12
  gitAddAll,
15
13
  gitCommit,
@@ -21,7 +19,9 @@ import {
21
19
  printSuccess,
22
20
  printInfo,
23
21
  printWarning,
22
+ resolveTargetWorktree,
24
23
  } from '../utils/index.js';
24
+ import type { WorktreeResolveMessages } from '../utils/index.js';
25
25
 
26
26
  /**
27
27
  * 注册 sync 命令:将主分支最新代码同步到目标 worktree
@@ -31,12 +31,20 @@ export function registerSyncCommand(program: Command): void {
31
31
  program
32
32
  .command('sync')
33
33
  .description('将主分支最新代码同步到目标 worktree')
34
- .requiredOption('-b, --branch <branchName>', '要同步的分支名')
34
+ .option('-b, --branch <branchName>', '要同步的分支名(支持模糊匹配,不传则列出所有分支)')
35
35
  .action(async (options: SyncOptions) => {
36
36
  await handleSync(options);
37
37
  });
38
38
  }
39
39
 
40
+ /** sync 命令的分支解析消息配置 */
41
+ const SYNC_RESOLVE_MESSAGES: WorktreeResolveMessages = {
42
+ noWorktrees: MESSAGES.SYNC_NO_WORKTREES,
43
+ selectBranch: MESSAGES.SYNC_SELECT_BRANCH,
44
+ multipleMatches: MESSAGES.SYNC_MULTIPLE_MATCHES,
45
+ noMatch: MESSAGES.SYNC_NO_MATCH,
46
+ };
47
+
40
48
  /**
41
49
  * 自动保存目标 worktree 中的未提交变更
42
50
  * @param {string} worktreePath - 目标 worktree 路径
@@ -77,16 +85,12 @@ function mergeMainBranch(worktreePath: string, mainBranch: string): boolean {
77
85
  async function handleSync(options: SyncOptions): Promise<void> {
78
86
  validateMainWorktree();
79
87
 
80
- const { branch } = options;
81
- logger.info(`sync 命令执行,分支: ${branch}`);
82
-
83
- // 检查目标 worktree 是否存在
84
- const projectWorktreeDir = getProjectWorktreeDir();
85
- const targetWorktreePath = join(projectWorktreeDir, branch);
88
+ logger.info(`sync 命令执行,分支: ${options.branch ?? '(未指定)'}`);
86
89
 
87
- if (!existsSync(targetWorktreePath)) {
88
- throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(branch));
89
- }
90
+ // 解析目标 worktree(精确匹配 / 模糊匹配 / 交互选择)
91
+ const worktrees = getProjectWorktrees();
92
+ const worktree = await resolveTargetWorktree(worktrees, SYNC_RESOLVE_MESSAGES, options.branch);
93
+ const { path: targetWorktreePath, branch } = worktree;
90
94
 
91
95
  // 获取主分支名(不硬编码 main/master)
92
96
  const mainWorktreePath = getGitTopLevel();
@@ -128,4 +128,22 @@ export const MESSAGES = {
128
128
  VALIDATE_SELECT_BRANCH: '请选择要验证的分支',
129
129
  /** validate 模糊匹配到多个结果提示 */
130
130
  VALIDATE_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择:`,
131
+ /** merge 无可用 worktree */
132
+ MERGE_NO_WORKTREES: '当前项目没有可用的 worktree,请先通过 clawt run 或 clawt create 创建',
133
+ /** merge 模糊匹配无结果,列出可用分支 */
134
+ MERGE_NO_MATCH: (name: string, branches: string[]) =>
135
+ `未找到与 "${name}" 匹配的分支\n 可用分支:\n${branches.map((b) => ` - ${b}`).join('\n')}`,
136
+ /** merge 交互选择提示 */
137
+ MERGE_SELECT_BRANCH: '请选择要合并的分支',
138
+ /** merge 模糊匹配到多个结果提示 */
139
+ MERGE_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择:`,
140
+ /** sync 无可用 worktree */
141
+ SYNC_NO_WORKTREES: '当前项目没有可用的 worktree,请先通过 clawt run 或 clawt create 创建',
142
+ /** sync 模糊匹配无结果,列出可用分支 */
143
+ SYNC_NO_MATCH: (name: string, branches: string[]) =>
144
+ `未找到与 "${name}" 匹配的分支\n 可用分支:\n${branches.map((b) => ` - ${b}`).join('\n')}`,
145
+ /** sync 交互选择提示 */
146
+ SYNC_SELECT_BRANCH: '请选择要同步的分支',
147
+ /** sync 模糊匹配到多个结果提示 */
148
+ SYNC_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择:`,
131
149
  } as const;
@@ -24,8 +24,8 @@ export interface ValidateOptions {
24
24
 
25
25
  /** merge 命令选项 */
26
26
  export interface MergeOptions {
27
- /** 要合并的分支名 */
28
- branch: string;
27
+ /** 要合并的分支名(可选,支持模糊匹配,不传则列出所有分支供选择) */
28
+ branch?: string;
29
29
  /** 提交信息(工作区有修改时必填) */
30
30
  message?: string;
31
31
  }
@@ -46,8 +46,8 @@ export interface ResumeOptions {
46
46
 
47
47
  /** sync 命令选项 */
48
48
  export interface SyncOptions {
49
- /** 要同步的分支名 */
50
- branch: string;
49
+ /** 要同步的分支名(可选,支持模糊匹配,不传则列出所有分支供选择) */
50
+ branch?: string;
51
51
  }
52
52
 
53
53
  /** list 命令选项 */