clawt 2.6.0 → 2.7.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.
@@ -9,6 +9,7 @@
9
9
  - merge 命令对应 `5.6 合并验证过的分支`,流程按步骤编号描述
10
10
  - config 命令对应 `5.10 查看全局配置`,只读展示配置
11
11
  - resume 命令对应 `5.11 在已有 Worktree 中恢复会话`,支持模糊匹配和交互式分支选择(-b 可选)
12
+ - validate 命令对应 `5.4 在主 Worktree 验证其他分支`,-b 可选,支持模糊匹配(与 resume 共享匹配逻辑)
12
13
  - 配置项说明在 `5.7 默认配置文件` 章节的表格中
13
14
  - 更新模式:新增步骤时追加编号,配置项影响范围变化时更新说明列
14
15
 
@@ -65,8 +66,11 @@ Notes:
65
66
  - resume 和 run(交互式模式)共用 `launchInteractiveClaude()`,该函数从 run.ts 提取到 src/utils/claude.ts
66
67
  - `claudeCodeCommand` 配置项同时影响 run 交互式模式和 resume 命令
67
68
  - reset 命令与 validate --clean 的区别:reset 不删除快照文件,validate --clean 会删除快照
68
- - resume 的 `-b` 参数为可选,核心函数 `resolveTargetWorktree()` 封装匹配策略:精确→模糊(子串,大小写不敏感)→交互选择
69
- - resume 的交互式选择使用 Enquirer.Select(`promptSelectBranch()`),消息常量在 `MESSAGES.RESUME_*`
69
+ - `resolveTargetWorktree()` 是 resume 和 validate 共用的分支匹配函数(在 src/utils/worktree-matcher.ts)
70
+ - `WorktreeResolveMessages` 接口实现命令间消息解耦,每个命令传入各自的提示文案
71
+ - resume 的消息常量在 `MESSAGES.RESUME_*`,validate 的消息常量在 `MESSAGES.VALIDATE_*`
72
+ - resume 和 validate 的 `-b` 参数均为可选,匹配策略一致:精确→模糊(子串,大小写不敏感)→交互选择
73
+ - validate 的交互式选择和 resume 使用同一个 `promptSelectBranch()`(Enquirer.Select)
70
74
 
71
75
  ## validate 快照机制
72
76
 
package/README.md CHANGED
@@ -107,29 +107,44 @@ clawt resume
107
107
  ### `clawt validate` — 在主 worktree 验证分支变更
108
108
 
109
109
  ```bash
110
+ # 指定分支名(支持模糊匹配)
110
111
  clawt validate -b <branchName> [--clean]
112
+
113
+ # 不指定分支名(列出所有分支供选择)
114
+ clawt validate [--clean]
111
115
  ```
112
116
 
113
117
  | 参数 | 必填 | 说明 |
114
118
  | ---- | ---- | ---- |
115
- | `-b` | | 要验证的分支名 |
119
+ | `-b` | | 要验证的分支名(支持模糊匹配,不传则列出所有分支供选择) |
116
120
  | `--clean` | 否 | 清理 validate 状态(重置主 worktree 并删除快照) |
117
121
 
118
122
  将目标 worktree 的变更通过 `git diff`(三点 diff)迁移到主 worktree,方便在主 worktree 中直接测试,无需重新安装依赖。同时检测未提交修改和已提交 commit,确保所有变更都能被捕获。
119
123
 
124
+ **分支匹配策略:**
125
+ - 传 `-b` 时,优先精确匹配分支名;未精确匹配则进行模糊匹配(子串匹配,大小写不敏感);模糊匹配到多个时通过交互列表选择;无匹配时报错并列出所有可用分支
126
+ - 不传 `-b` 时,列出当前项目所有可用分支供交互式选择(仅 1 个时自动使用)
127
+
120
128
  支持增量模式:首次 validate 后会自动保存快照(通过 `git write-tree` 将变更存储为 git tree 对象,并记录当前 HEAD commit hash),再次 validate 同一分支时会将上次快照载入暂存区、最新变更保留在工作目录,用户可通过 `git diff` 查看两次 validate 之间的增量差异。当主分支 HEAD 发生变化(如合并了其他分支)时,会自动将旧变更 patch 重放到当前 HEAD 暂存区上,避免 diff 混入 HEAD 变化的内容;若 patch 存在冲突则自动降级为全量模式。使用 `--clean` 可清理 validate 状态(重置主 worktree 并删除快照文件)。
121
129
 
122
130
  > **提示:** 如果 validate 时 patch apply 失败(目标分支与主分支差异过大),可先执行 `clawt sync -b <branchName>` 同步主分支后重试。
123
131
 
124
132
  ```bash
125
- # 首次验证
133
+ # 精确匹配分支名
126
134
  clawt validate -b feature-scheme-1
127
135
 
136
+ # 模糊匹配(匹配包含 "scheme" 的分支)
137
+ clawt validate -b scheme
138
+
139
+ # 交互式选择所有分支
140
+ clawt validate
141
+
128
142
  # 再次验证(增量模式,可通过 git diff 查看增量差异)
129
143
  clawt validate -b feature-scheme-1
130
144
 
131
- # 清理 validate 状态
145
+ # 清理 validate 状态(同样支持模糊匹配)
132
146
  clawt validate -b feature-scheme-1 --clean
147
+ clawt validate --clean
133
148
  ```
134
149
 
135
150
  ### `clawt sync` — 将主分支代码同步到目标 worktree
package/dist/index.js CHANGED
@@ -134,7 +134,17 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
134
134
  /** resume 交互选择提示 */
135
135
  RESUME_SELECT_BRANCH: "\u8BF7\u9009\u62E9\u8981\u6062\u590D\u7684\u5206\u652F",
136
136
  /** resume 模糊匹配到多个结果提示 */
137
- RESUME_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`
137
+ RESUME_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`,
138
+ /** validate 无可用 worktree */
139
+ VALIDATE_NO_WORKTREES: "\u5F53\u524D\u9879\u76EE\u6CA1\u6709\u53EF\u7528\u7684 worktree\uFF0C\u8BF7\u5148\u901A\u8FC7 clawt run \u6216 clawt create \u521B\u5EFA",
140
+ /** validate 模糊匹配无结果,列出可用分支 */
141
+ VALIDATE_NO_MATCH: (name, branches) => `\u672A\u627E\u5230\u4E0E "${name}" \u5339\u914D\u7684\u5206\u652F
142
+ \u53EF\u7528\u5206\u652F\uFF1A
143
+ ${branches.map((b) => ` - ${b}`).join("\n")}`,
144
+ /** validate 交互选择提示 */
145
+ VALIDATE_SELECT_BRANCH: "\u8BF7\u9009\u62E9\u8981\u9A8C\u8BC1\u7684\u5206\u652F",
146
+ /** validate 模糊匹配到多个结果提示 */
147
+ VALIDATE_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`
138
148
  };
139
149
 
140
150
  // src/constants/exitCodes.ts
@@ -742,6 +752,50 @@ function removeProjectSnapshots(projectName) {
742
752
  logger.info(`\u5DF2\u5220\u9664\u9879\u76EE ${projectName} \u7684\u6240\u6709 validate \u5FEB\u7167`);
743
753
  }
744
754
 
755
+ // src/utils/worktree-matcher.ts
756
+ import Enquirer2 from "enquirer";
757
+ function findExactMatch(worktrees, branchName) {
758
+ return worktrees.find((wt) => wt.branch === branchName);
759
+ }
760
+ function findFuzzyMatches(worktrees, keyword) {
761
+ const lowerKeyword = keyword.toLowerCase();
762
+ return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
763
+ }
764
+ async function promptSelectBranch(worktrees, message) {
765
+ const selectedBranch = await new Enquirer2.Select({
766
+ message,
767
+ choices: worktrees.map((wt) => ({
768
+ name: wt.branch,
769
+ message: wt.branch
770
+ }))
771
+ }).run();
772
+ return worktrees.find((wt) => wt.branch === selectedBranch);
773
+ }
774
+ async function resolveTargetWorktree(worktrees, messages, branchName) {
775
+ if (worktrees.length === 0) {
776
+ throw new ClawtError(messages.noWorktrees);
777
+ }
778
+ if (!branchName) {
779
+ if (worktrees.length === 1) {
780
+ return worktrees[0];
781
+ }
782
+ return promptSelectBranch(worktrees, messages.selectBranch);
783
+ }
784
+ const exactMatch = findExactMatch(worktrees, branchName);
785
+ if (exactMatch) {
786
+ return exactMatch;
787
+ }
788
+ const fuzzyMatches = findFuzzyMatches(worktrees, branchName);
789
+ if (fuzzyMatches.length === 1) {
790
+ return fuzzyMatches[0];
791
+ }
792
+ if (fuzzyMatches.length > 1) {
793
+ return promptSelectBranch(fuzzyMatches, messages.multipleMatches(branchName));
794
+ }
795
+ const allBranches = worktrees.map((wt) => wt.branch);
796
+ throw new ClawtError(messages.noMatch(branchName, allBranches));
797
+ }
798
+
745
799
  // src/commands/list.ts
746
800
  import chalk2 from "chalk";
747
801
  function registerListCommand(program2) {
@@ -1059,68 +1113,36 @@ async function handleRun(options) {
1059
1113
  }
1060
1114
 
1061
1115
  // src/commands/resume.ts
1062
- import Enquirer2 from "enquirer";
1116
+ var RESUME_RESOLVE_MESSAGES = {
1117
+ noWorktrees: MESSAGES.RESUME_NO_WORKTREES,
1118
+ selectBranch: MESSAGES.RESUME_SELECT_BRANCH,
1119
+ multipleMatches: MESSAGES.RESUME_MULTIPLE_MATCHES,
1120
+ noMatch: MESSAGES.RESUME_NO_MATCH
1121
+ };
1063
1122
  function registerResumeCommand(program2) {
1064
1123
  program2.command("resume").description("\u5728\u5DF2\u6709 worktree \u4E2D\u6062\u590D Claude Code \u4EA4\u4E92\u5F0F\u4F1A\u8BDD").option("-b, --branch <branchName>", "\u8981\u6062\u590D\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) => {
1065
1124
  await handleResume(options);
1066
1125
  });
1067
1126
  }
1068
- function findExactMatch(worktrees, branchName) {
1069
- return worktrees.find((wt) => wt.branch === branchName);
1070
- }
1071
- function findFuzzyMatches(worktrees, keyword) {
1072
- const lowerKeyword = keyword.toLowerCase();
1073
- return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
1074
- }
1075
- async function promptSelectBranch(worktrees, message) {
1076
- const selectedBranch = await new Enquirer2.Select({
1077
- message,
1078
- choices: worktrees.map((wt) => ({
1079
- name: wt.branch,
1080
- message: wt.branch
1081
- }))
1082
- }).run();
1083
- return worktrees.find((wt) => wt.branch === selectedBranch);
1084
- }
1085
- async function resolveTargetWorktree(branchName) {
1086
- const worktrees = getProjectWorktrees();
1087
- if (worktrees.length === 0) {
1088
- throw new ClawtError(MESSAGES.RESUME_NO_WORKTREES);
1089
- }
1090
- if (!branchName) {
1091
- if (worktrees.length === 1) {
1092
- return worktrees[0];
1093
- }
1094
- return promptSelectBranch(worktrees, MESSAGES.RESUME_SELECT_BRANCH);
1095
- }
1096
- const exactMatch = findExactMatch(worktrees, branchName);
1097
- if (exactMatch) {
1098
- return exactMatch;
1099
- }
1100
- const fuzzyMatches = findFuzzyMatches(worktrees, branchName);
1101
- if (fuzzyMatches.length === 1) {
1102
- return fuzzyMatches[0];
1103
- }
1104
- if (fuzzyMatches.length > 1) {
1105
- return promptSelectBranch(fuzzyMatches, MESSAGES.RESUME_MULTIPLE_MATCHES(branchName));
1106
- }
1107
- const allBranches = worktrees.map((wt) => wt.branch);
1108
- throw new ClawtError(MESSAGES.RESUME_NO_MATCH(branchName, allBranches));
1109
- }
1110
1127
  async function handleResume(options) {
1111
1128
  validateMainWorktree();
1112
1129
  validateClaudeCodeInstalled();
1113
1130
  logger.info(`resume \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch ?? "(\u672A\u6307\u5B9A)"}`);
1114
- const worktree = await resolveTargetWorktree(options.branch);
1131
+ const worktrees = getProjectWorktrees();
1132
+ const worktree = await resolveTargetWorktree(worktrees, RESUME_RESOLVE_MESSAGES, options.branch);
1115
1133
  launchInteractiveClaude(worktree);
1116
1134
  }
1117
1135
 
1118
1136
  // src/commands/validate.ts
1119
- import { join as join4 } from "path";
1120
- import { existsSync as existsSync6 } from "fs";
1121
1137
  import Enquirer3 from "enquirer";
1138
+ var VALIDATE_RESOLVE_MESSAGES = {
1139
+ noWorktrees: MESSAGES.VALIDATE_NO_WORKTREES,
1140
+ selectBranch: MESSAGES.VALIDATE_SELECT_BRANCH,
1141
+ multipleMatches: MESSAGES.VALIDATE_MULTIPLE_MATCHES,
1142
+ noMatch: MESSAGES.VALIDATE_NO_MATCH
1143
+ };
1122
1144
  function registerValidateCommand(program2) {
1123
- program2.command("validate").description("\u5728\u4E3B worktree \u9A8C\u8BC1\u67D0\u4E2A worktree \u5206\u652F\u7684\u53D8\u66F4").requiredOption("-b, --branch <branchName>", "\u8981\u9A8C\u8BC1\u7684\u5206\u652F\u540D").option("--clean", "\u6E05\u7406 validate \u72B6\u6001\uFF08\u91CD\u7F6E\u4E3B worktree \u5E76\u5220\u9664\u5FEB\u7167\uFF09").action(async (options) => {
1145
+ program2.command("validate").description("\u5728\u4E3B worktree \u9A8C\u8BC1\u67D0\u4E2A worktree \u5206\u652F\u7684\u53D8\u66F4").option("-b, --branch <branchName>", "\u8981\u9A8C\u8BC1\u7684\u5206\u652F\u540D\uFF08\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF0C\u4E0D\u4F20\u5219\u5217\u51FA\u6240\u6709\u5206\u652F\uFF09").option("--clean", "\u6E05\u7406 validate \u72B6\u6001\uFF08\u91CD\u7F6E\u4E3B worktree \u5E76\u5220\u9664\u5FEB\u7167\uFF09").action(async (options) => {
1124
1146
  await handleValidate(options);
1125
1147
  });
1126
1148
  }
@@ -1203,11 +1225,14 @@ async function handleValidateClean(options) {
1203
1225
  validateMainWorktree();
1204
1226
  const projectName = getProjectName();
1205
1227
  const mainWorktreePath = getGitTopLevel();
1206
- logger.info(`validate --clean \u6267\u884C\uFF0C\u5206\u652F: ${options.branch}`);
1228
+ const worktrees = getProjectWorktrees();
1229
+ const worktree = await resolveTargetWorktree(worktrees, VALIDATE_RESOLVE_MESSAGES, options.branch);
1230
+ const branchName = worktree.branch;
1231
+ logger.info(`validate --clean \u6267\u884C\uFF0C\u5206\u652F: ${branchName}`);
1207
1232
  if (getConfigValue("confirmDestructiveOps")) {
1208
1233
  const confirmed = await confirmDestructiveAction(
1209
1234
  "git reset --hard + git clean -fd",
1210
- `\u91CD\u7F6E\u4E3B worktree \u5E76\u5220\u9664\u5206\u652F ${options.branch} \u7684 validate \u5FEB\u7167`
1235
+ `\u91CD\u7F6E\u4E3B worktree \u5E76\u5220\u9664\u5206\u652F ${branchName} \u7684 validate \u5FEB\u7167`
1211
1236
  );
1212
1237
  if (!confirmed) {
1213
1238
  printInfo(MESSAGES.DESTRUCTIVE_OP_CANCELLED);
@@ -1218,8 +1243,8 @@ async function handleValidateClean(options) {
1218
1243
  gitResetHard(mainWorktreePath);
1219
1244
  gitCleanForce(mainWorktreePath);
1220
1245
  }
1221
- removeSnapshot(projectName, options.branch);
1222
- printSuccess(MESSAGES.VALIDATE_CLEANED(options.branch));
1246
+ removeSnapshot(projectName, branchName);
1247
+ printSuccess(MESSAGES.VALIDATE_CLEANED(branchName));
1223
1248
  }
1224
1249
  function handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
1225
1250
  migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
@@ -1266,35 +1291,34 @@ async function handleValidate(options) {
1266
1291
  validateMainWorktree();
1267
1292
  const projectName = getProjectName();
1268
1293
  const mainWorktreePath = getGitTopLevel();
1269
- const projectDir = getProjectWorktreeDir();
1270
- const targetWorktreePath = join4(projectDir, options.branch);
1271
- logger.info(`validate \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch}`);
1272
- if (!existsSync6(targetWorktreePath)) {
1273
- throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
1274
- }
1294
+ const worktrees = getProjectWorktrees();
1295
+ const worktree = await resolveTargetWorktree(worktrees, VALIDATE_RESOLVE_MESSAGES, options.branch);
1296
+ const branchName = worktree.branch;
1297
+ const targetWorktreePath = worktree.path;
1298
+ logger.info(`validate \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${branchName}`);
1275
1299
  const hasUncommitted = !isWorkingDirClean(targetWorktreePath);
1276
- const hasCommitted = hasLocalCommits(options.branch, mainWorktreePath);
1300
+ const hasCommitted = hasLocalCommits(branchName, mainWorktreePath);
1277
1301
  if (!hasUncommitted && !hasCommitted) {
1278
1302
  printInfo(MESSAGES.TARGET_WORKTREE_CLEAN);
1279
1303
  return;
1280
1304
  }
1281
- const isIncremental = hasSnapshot(projectName, options.branch);
1305
+ const isIncremental = hasSnapshot(projectName, branchName);
1282
1306
  if (isIncremental) {
1283
1307
  if (!isWorkingDirClean(mainWorktreePath)) {
1284
1308
  await handleDirtyMainWorktree(mainWorktreePath);
1285
1309
  }
1286
- handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, options.branch, hasUncommitted);
1310
+ handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
1287
1311
  } else {
1288
1312
  if (!isWorkingDirClean(mainWorktreePath)) {
1289
1313
  await handleDirtyMainWorktree(mainWorktreePath);
1290
1314
  }
1291
- handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, options.branch, hasUncommitted);
1315
+ handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
1292
1316
  }
1293
1317
  }
1294
1318
 
1295
1319
  // src/commands/merge.ts
1296
- import { join as join5 } from "path";
1297
- import { existsSync as existsSync7 } from "fs";
1320
+ import { join as join4 } from "path";
1321
+ import { existsSync as existsSync6 } from "fs";
1298
1322
  function registerMergeCommand(program2) {
1299
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) => {
1300
1324
  await handleMerge(options);
@@ -1336,9 +1360,9 @@ async function handleMerge(options) {
1336
1360
  validateMainWorktree();
1337
1361
  const mainWorktreePath = getGitTopLevel();
1338
1362
  const projectDir = getProjectWorktreeDir();
1339
- const targetWorktreePath = join5(projectDir, options.branch);
1363
+ const targetWorktreePath = join4(projectDir, options.branch);
1340
1364
  logger.info(`merge \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch}\uFF0C\u63D0\u4EA4\u4FE1\u606F: ${options.message ?? "(\u672A\u63D0\u4F9B)"}`);
1341
- if (!existsSync7(targetWorktreePath)) {
1365
+ if (!existsSync6(targetWorktreePath)) {
1342
1366
  throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
1343
1367
  }
1344
1368
  const projectName = getProjectName();
@@ -1431,8 +1455,8 @@ function formatConfigValue(value) {
1431
1455
  }
1432
1456
 
1433
1457
  // src/commands/sync.ts
1434
- import { existsSync as existsSync8 } from "fs";
1435
- import { join as join6 } from "path";
1458
+ import { existsSync as existsSync7 } from "fs";
1459
+ import { join as join5 } from "path";
1436
1460
  function registerSyncCommand(program2) {
1437
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) => {
1438
1462
  await handleSync(options);
@@ -1460,8 +1484,8 @@ async function handleSync(options) {
1460
1484
  const { branch } = options;
1461
1485
  logger.info(`sync \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${branch}`);
1462
1486
  const projectWorktreeDir = getProjectWorktreeDir();
1463
- const targetWorktreePath = join6(projectWorktreeDir, branch);
1464
- if (!existsSync8(targetWorktreePath)) {
1487
+ const targetWorktreePath = join5(projectWorktreeDir, branch);
1488
+ if (!existsSync7(targetWorktreePath)) {
1465
1489
  throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(branch));
1466
1490
  }
1467
1491
  const mainWorktreePath = getGitTopLevel();
package/docs/spec.md CHANGED
@@ -346,14 +346,18 @@ Claude Code CLI 以 `--output-format json` 运行时,退出后会在 stdout
346
346
  **命令:**
347
347
 
348
348
  ```bash
349
+ # 指定分支名(支持模糊匹配)
349
350
  clawt validate -b <branchName> [--clean]
351
+
352
+ # 不指定分支名(列出所有分支供选择)
353
+ clawt validate [--clean]
350
354
  ```
351
355
 
352
356
  **参数:**
353
357
 
354
358
  | 参数 | 必填 | 说明 |
355
359
  | --------- | ---- | ------------------------------------------------------------------------ |
356
- | `-b` | | 要验证的 worktree 分支名(例如 `feature-scheme-1`) |
360
+ | `-b` | | 要验证的 worktree 分支名(支持模糊匹配,不传则列出所有分支供选择) |
357
361
  | `--clean` | 否 | 清理 validate 状态(重置主 worktree 并删除快照) |
358
362
 
359
363
  > **限制:** 单次只能验证一个分支,不支持批量验证。
@@ -373,13 +377,30 @@ validate 命令引入了**快照(snapshot)机制**来支持增量对比。
373
377
  当指定 `--clean` 选项时,执行清理逻辑后直接返回,不进入常规 validate 流程:
374
378
 
375
379
  1. **主 worktree 校验** (2.1)
376
- 2. 如果配置项 `confirmDestructiveOps` `true`,提示确认(显示即将执行的危险指令和操作后果),用户取消则退出
377
- 3. 如果主 worktree 有未提交更改,执行 `git reset --hard` + `git clean -fd` 清空
378
- 4. 删除对应分支的快照文件
379
- 5. 输出清理成功提示
380
+ 2. **解析目标 worktree**:通过模糊匹配解析目标分支(匹配策略同下文常规 validate 流程中的描述)
381
+ 3. 如果配置项 `confirmDestructiveOps` `true`,提示确认(显示即将执行的危险指令和操作后果),用户取消则退出
382
+ 4. 如果主 worktree 有未提交更改,执行 `git reset --hard` + `git clean -fd` 清空
383
+ 5. 删除对应分支的快照文件
384
+ 6. 输出清理成功提示
380
385
 
381
386
  #### 首次 validate(无历史快照)
382
387
 
388
+ ##### 步骤 0:解析目标 worktree
389
+
390
+ 根据 `-b` 参数解析目标 worktree,匹配策略如下:
391
+
392
+ - **未传 `-b` 参数**:
393
+ - 获取当前项目所有 worktree
394
+ - 无可用 worktree → 报错退出
395
+ - 仅 1 个 worktree → 直接使用,无需选择
396
+ - 多个 worktree → 通过交互式列表(Enquirer.Select)让用户选择
397
+ - **传了 `-b` 参数**:
398
+ 1. **精确匹配优先**:在 worktree 列表中查找分支名完全相同的 worktree,找到则直接使用
399
+ 2. **模糊匹配**(子串匹配,大小写不敏感):
400
+ - 唯一匹配 → 直接使用
401
+ - 多个匹配 → 通过交互式列表让用户从匹配结果中选择
402
+ 3. **无匹配** → 报错退出,并列出所有可用分支名
403
+
383
404
  ##### 步骤 1:检测主 worktree 工作区状态
384
405
 
385
406
  执行 `git status --porcelain`,判断主 worktree 是否有未提交的更改。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,15 +1,23 @@
1
1
  import type { Command } from 'commander';
2
- import Enquirer from 'enquirer';
3
2
  import { logger } from '../logger/index.js';
4
- import { ClawtError } from '../errors/index.js';
5
3
  import { MESSAGES } from '../constants/index.js';
6
- import type { ResumeOptions, WorktreeInfo } from '../types/index.js';
4
+ import type { ResumeOptions } from '../types/index.js';
7
5
  import {
8
6
  validateMainWorktree,
9
7
  validateClaudeCodeInstalled,
10
8
  getProjectWorktrees,
11
9
  launchInteractiveClaude,
10
+ resolveTargetWorktree,
12
11
  } from '../utils/index.js';
12
+ import type { WorktreeResolveMessages } from '../utils/index.js';
13
+
14
+ /** resume 命令的分支解析消息配置 */
15
+ const RESUME_RESOLVE_MESSAGES: WorktreeResolveMessages = {
16
+ noWorktrees: MESSAGES.RESUME_NO_WORKTREES,
17
+ selectBranch: MESSAGES.RESUME_SELECT_BRANCH,
18
+ multipleMatches: MESSAGES.RESUME_MULTIPLE_MATCHES,
19
+ noMatch: MESSAGES.RESUME_NO_MATCH,
20
+ };
13
21
 
14
22
  /**
15
23
  * 注册 resume 命令:在已有 worktree 中恢复 Claude Code 会话
@@ -25,95 +33,6 @@ export function registerResumeCommand(program: Command): void {
25
33
  });
26
34
  }
27
35
 
28
- /**
29
- * 在 worktree 列表中精确匹配分支名
30
- * @param {WorktreeInfo[]} worktrees - worktree 列表
31
- * @param {string} branchName - 目标分支名
32
- * @returns {WorktreeInfo | undefined} 匹配的 worktree,未找到返回 undefined
33
- */
34
- function findExactMatch(worktrees: WorktreeInfo[], branchName: string): WorktreeInfo | undefined {
35
- return worktrees.find((wt) => wt.branch === branchName);
36
- }
37
-
38
- /**
39
- * 在 worktree 列表中进行模糊匹配(子串匹配,大小写不敏感)
40
- * @param {WorktreeInfo[]} worktrees - worktree 列表
41
- * @param {string} keyword - 匹配关键词
42
- * @returns {WorktreeInfo[]} 匹配到的 worktree 列表
43
- */
44
- function findFuzzyMatches(worktrees: WorktreeInfo[], keyword: string): WorktreeInfo[] {
45
- const lowerKeyword = keyword.toLowerCase();
46
- return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
47
- }
48
-
49
- /**
50
- * 通过交互式列表让用户从 worktree 列表中选择一个分支
51
- * @param {WorktreeInfo[]} worktrees - 可供选择的 worktree 列表
52
- * @param {string} message - 选择提示信息
53
- * @returns {Promise<WorktreeInfo>} 用户选择的 worktree
54
- */
55
- async function promptSelectBranch(worktrees: WorktreeInfo[], message: string): Promise<WorktreeInfo> {
56
- // @ts-expect-error enquirer 类型声明未导出 Select 类,但运行时存在
57
- const selectedBranch: string = await new Enquirer.Select({
58
- message,
59
- choices: worktrees.map((wt) => ({
60
- name: wt.branch,
61
- message: wt.branch,
62
- })),
63
- }).run();
64
-
65
- return worktrees.find((wt) => wt.branch === selectedBranch)!;
66
- }
67
-
68
- /**
69
- * 根据用户输入解析目标 worktree
70
- * 匹配策略:精确匹配 → 模糊匹配(唯一直接使用,多个交互选择) → 无匹配报错
71
- * 不传分支名时列出所有可用分支供选择
72
- * @param {string} [branchName] - 用户输入的分支名(可选)
73
- * @returns {Promise<WorktreeInfo>} 解析后的目标 worktree
74
- * @throws {ClawtError} 无可用 worktree 或无匹配结果时抛出
75
- */
76
- async function resolveTargetWorktree(branchName?: string): Promise<WorktreeInfo> {
77
- const worktrees = getProjectWorktrees();
78
-
79
- // 无可用 worktree,直接报错
80
- if (worktrees.length === 0) {
81
- throw new ClawtError(MESSAGES.RESUME_NO_WORKTREES);
82
- }
83
-
84
- // 未传 -b 参数:列出所有分支供选择
85
- if (!branchName) {
86
- // 只有一个 worktree 时直接使用,无需选择
87
- if (worktrees.length === 1) {
88
- return worktrees[0];
89
- }
90
- return promptSelectBranch(worktrees, MESSAGES.RESUME_SELECT_BRANCH);
91
- }
92
-
93
- // 1. 精确匹配优先
94
- const exactMatch = findExactMatch(worktrees, branchName);
95
- if (exactMatch) {
96
- return exactMatch;
97
- }
98
-
99
- // 2. 模糊匹配
100
- const fuzzyMatches = findFuzzyMatches(worktrees, branchName);
101
-
102
- // 2a. 唯一匹配,直接使用
103
- if (fuzzyMatches.length === 1) {
104
- return fuzzyMatches[0];
105
- }
106
-
107
- // 2b. 多个匹配,交互选择
108
- if (fuzzyMatches.length > 1) {
109
- return promptSelectBranch(fuzzyMatches, MESSAGES.RESUME_MULTIPLE_MATCHES(branchName));
110
- }
111
-
112
- // 3. 无匹配,抛出错误并列出所有可用分支
113
- const allBranches = worktrees.map((wt) => wt.branch);
114
- throw new ClawtError(MESSAGES.RESUME_NO_MATCH(branchName, allBranches));
115
- }
116
-
117
36
  /**
118
37
  * 执行 resume 命令的核心逻辑
119
38
  * 解析目标 worktree 并恢复 Claude Code 会话
@@ -126,7 +45,8 @@ async function handleResume(options: ResumeOptions): Promise<void> {
126
45
  logger.info(`resume 命令执行,分支: ${options.branch ?? '(未指定)'}`);
127
46
 
128
47
  // 解析目标 worktree(精确匹配 / 模糊匹配 / 交互选择)
129
- const worktree = await resolveTargetWorktree(options.branch);
48
+ const worktrees = getProjectWorktrees();
49
+ const worktree = await resolveTargetWorktree(worktrees, RESUME_RESOLVE_MESSAGES, options.branch);
130
50
 
131
51
  // 启动 Claude Code 交互式界面
132
52
  launchInteractiveClaude(worktree);
@@ -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 Enquirer from 'enquirer';
5
3
  import { logger } from '../logger/index.js';
6
4
  import { ClawtError } from '../errors/index.js';
@@ -10,7 +8,7 @@ import {
10
8
  validateMainWorktree,
11
9
  getProjectName,
12
10
  getGitTopLevel,
13
- getProjectWorktreeDir,
11
+ getProjectWorktrees,
14
12
  getConfigValue,
15
13
  isWorkingDirClean,
16
14
  gitAddAll,
@@ -38,7 +36,17 @@ import {
38
36
  printSuccess,
39
37
  printWarning,
40
38
  printInfo,
39
+ resolveTargetWorktree,
41
40
  } from '../utils/index.js';
41
+ import type { WorktreeResolveMessages } from '../utils/index.js';
42
+
43
+ /** validate 命令的分支解析消息配置 */
44
+ const VALIDATE_RESOLVE_MESSAGES: WorktreeResolveMessages = {
45
+ noWorktrees: MESSAGES.VALIDATE_NO_WORKTREES,
46
+ selectBranch: MESSAGES.VALIDATE_SELECT_BRANCH,
47
+ multipleMatches: MESSAGES.VALIDATE_MULTIPLE_MATCHES,
48
+ noMatch: MESSAGES.VALIDATE_NO_MATCH,
49
+ };
42
50
 
43
51
  /**
44
52
  * 注册 validate 命令:在主 worktree 验证其他分支的变更
@@ -48,7 +56,7 @@ export function registerValidateCommand(program: Command): void {
48
56
  program
49
57
  .command('validate')
50
58
  .description('在主 worktree 验证某个 worktree 分支的变更')
51
- .requiredOption('-b, --branch <branchName>', '要验证的分支名')
59
+ .option('-b, --branch <branchName>', '要验证的分支名(支持模糊匹配,不传则列出所有分支)')
52
60
  .option('--clean', '清理 validate 状态(重置主 worktree 并删除快照)')
53
61
  .action(async (options: ValidateOptions) => {
54
62
  await handleValidate(options);
@@ -178,13 +186,18 @@ async function handleValidateClean(options: ValidateOptions): Promise<void> {
178
186
  const projectName = getProjectName();
179
187
  const mainWorktreePath = getGitTopLevel();
180
188
 
181
- logger.info(`validate --clean 执行,分支: ${options.branch}`);
189
+ // 通过模糊匹配解析目标 worktree
190
+ const worktrees = getProjectWorktrees();
191
+ const worktree = await resolveTargetWorktree(worktrees, VALIDATE_RESOLVE_MESSAGES, options.branch);
192
+ const branchName = worktree.branch;
193
+
194
+ logger.info(`validate --clean 执行,分支: ${branchName}`);
182
195
 
183
196
  // 根据配置决定是否需要确认
184
197
  if (getConfigValue('confirmDestructiveOps')) {
185
198
  const confirmed = await confirmDestructiveAction(
186
199
  'git reset --hard + git clean -fd',
187
- `重置主 worktree 并删除分支 ${options.branch} 的 validate 快照`,
200
+ `重置主 worktree 并删除分支 ${branchName} 的 validate 快照`,
188
201
  );
189
202
  if (!confirmed) {
190
203
  printInfo(MESSAGES.DESTRUCTIVE_OP_CANCELLED);
@@ -199,9 +212,9 @@ async function handleValidateClean(options: ValidateOptions): Promise<void> {
199
212
  }
200
213
 
201
214
  // 删除对应的快照文件
202
- removeSnapshot(projectName, options.branch);
215
+ removeSnapshot(projectName, branchName);
203
216
 
204
- printSuccess(MESSAGES.VALIDATE_CLEANED(options.branch));
217
+ printSuccess(MESSAGES.VALIDATE_CLEANED(branchName));
205
218
  }
206
219
 
207
220
  /**
@@ -302,19 +315,18 @@ async function handleValidate(options: ValidateOptions): Promise<void> {
302
315
 
303
316
  const projectName = getProjectName();
304
317
  const mainWorktreePath = getGitTopLevel();
305
- const projectDir = getProjectWorktreeDir();
306
- const targetWorktreePath = join(projectDir, options.branch);
307
318
 
308
- logger.info(`validate 命令执行,分支: ${options.branch}`);
319
+ // 通过模糊匹配解析目标 worktree
320
+ const worktrees = getProjectWorktrees();
321
+ const worktree = await resolveTargetWorktree(worktrees, VALIDATE_RESOLVE_MESSAGES, options.branch);
322
+ const branchName = worktree.branch;
323
+ const targetWorktreePath = worktree.path;
309
324
 
310
- // 检查目标 worktree 是否存在
311
- if (!existsSync(targetWorktreePath)) {
312
- throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
313
- }
325
+ logger.info(`validate 命令执行,分支: ${branchName}`);
314
326
 
315
327
  // 统一检测未提交修改 + 已提交 commit
316
328
  const hasUncommitted = !isWorkingDirClean(targetWorktreePath);
317
- const hasCommitted = hasLocalCommits(options.branch, mainWorktreePath);
329
+ const hasCommitted = hasLocalCommits(branchName, mainWorktreePath);
318
330
 
319
331
  if (!hasUncommitted && !hasCommitted) {
320
332
  printInfo(MESSAGES.TARGET_WORKTREE_CLEAN);
@@ -322,20 +334,20 @@ async function handleValidate(options: ValidateOptions): Promise<void> {
322
334
  }
323
335
 
324
336
  // 判断是否为增量 validate(tree 对象不依赖主分支 HEAD,无需一致性校验)
325
- const isIncremental = hasSnapshot(projectName, options.branch);
337
+ const isIncremental = hasSnapshot(projectName, branchName);
326
338
 
327
339
  if (isIncremental) {
328
340
  // 增量模式:主 worktree 有残留状态时让用户选择处理方式
329
341
  if (!isWorkingDirClean(mainWorktreePath)) {
330
342
  await handleDirtyMainWorktree(mainWorktreePath);
331
343
  }
332
- handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, options.branch, hasUncommitted);
344
+ handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
333
345
  } else {
334
346
  // 首次模式:先确保主 worktree 干净
335
347
  if (!isWorkingDirClean(mainWorktreePath)) {
336
348
  await handleDirtyMainWorktree(mainWorktreePath);
337
349
  }
338
350
 
339
- handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, options.branch, hasUncommitted);
351
+ handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
340
352
  }
341
353
  }
@@ -119,4 +119,13 @@ export const MESSAGES = {
119
119
  RESUME_SELECT_BRANCH: '请选择要恢复的分支',
120
120
  /** resume 模糊匹配到多个结果提示 */
121
121
  RESUME_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择:`,
122
+ /** validate 无可用 worktree */
123
+ VALIDATE_NO_WORKTREES: '当前项目没有可用的 worktree,请先通过 clawt run 或 clawt create 创建',
124
+ /** validate 模糊匹配无结果,列出可用分支 */
125
+ VALIDATE_NO_MATCH: (name: string, branches: string[]) =>
126
+ `未找到与 "${name}" 匹配的分支\n 可用分支:\n${branches.map((b) => ` - ${b}`).join('\n')}`,
127
+ /** validate 交互选择提示 */
128
+ VALIDATE_SELECT_BRANCH: '请选择要验证的分支',
129
+ /** validate 模糊匹配到多个结果提示 */
130
+ VALIDATE_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择:`,
122
131
  } as const;
@@ -16,8 +16,8 @@ export interface RunOptions {
16
16
 
17
17
  /** validate 命令选项 */
18
18
  export interface ValidateOptions {
19
- /** 要验证的分支名 */
20
- branch: string;
19
+ /** 要验证的分支名(可选,支持模糊匹配,不传则列出所有分支供选择) */
20
+ branch?: string;
21
21
  /** 清理 validate 状态 */
22
22
  clean?: boolean;
23
23
  }
@@ -53,3 +53,5 @@ export { ensureDir, removeEmptyDir } from './fs.js';
53
53
  export { multilineInput } from './prompt.js';
54
54
  export { launchInteractiveClaude } from './claude.js';
55
55
  export { getSnapshotPath, hasSnapshot, readSnapshotTreeHash, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots } from './validate-snapshot.js';
56
+ export { findExactMatch, findFuzzyMatches, promptSelectBranch, resolveTargetWorktree } from './worktree-matcher.js';
57
+ export type { WorktreeResolveMessages } from './worktree-matcher.js';
@@ -0,0 +1,111 @@
1
+ import Enquirer from 'enquirer';
2
+ import { ClawtError } from '../errors/index.js';
3
+ import type { WorktreeInfo } from '../types/index.js';
4
+
5
+ /**
6
+ * 分支解析时使用的消息文案配置
7
+ * 通过此接口实现命令间的消息解耦,不同命令可传入各自的提示文案
8
+ */
9
+ export interface WorktreeResolveMessages {
10
+ /** 无可用 worktree 时的错误消息 */
11
+ noWorktrees: string;
12
+ /** 未传分支名时的交互选择提示 */
13
+ selectBranch: string;
14
+ /** 模糊匹配到多个结果时的交互选择提示 */
15
+ multipleMatches: (keyword: string) => string;
16
+ /** 无匹配结果时的错误消息 */
17
+ noMatch: (keyword: string, branches: string[]) => string;
18
+ }
19
+
20
+ /**
21
+ * 在 worktree 列表中精确匹配分支名
22
+ * @param {WorktreeInfo[]} worktrees - worktree 列表
23
+ * @param {string} branchName - 目标分支名
24
+ * @returns {WorktreeInfo | undefined} 匹配的 worktree,未找到返回 undefined
25
+ */
26
+ export function findExactMatch(worktrees: WorktreeInfo[], branchName: string): WorktreeInfo | undefined {
27
+ return worktrees.find((wt) => wt.branch === branchName);
28
+ }
29
+
30
+ /**
31
+ * 在 worktree 列表中进行模糊匹配(子串匹配,大小写不敏感)
32
+ * @param {WorktreeInfo[]} worktrees - worktree 列表
33
+ * @param {string} keyword - 匹配关键词
34
+ * @returns {WorktreeInfo[]} 匹配到的 worktree 列表
35
+ */
36
+ export function findFuzzyMatches(worktrees: WorktreeInfo[], keyword: string): WorktreeInfo[] {
37
+ const lowerKeyword = keyword.toLowerCase();
38
+ return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
39
+ }
40
+
41
+ /**
42
+ * 通过交互式列表让用户从 worktree 列表中选择一个分支
43
+ * @param {WorktreeInfo[]} worktrees - 可供选择的 worktree 列表
44
+ * @param {string} message - 选择提示信息
45
+ * @returns {Promise<WorktreeInfo>} 用户选择的 worktree
46
+ */
47
+ export async function promptSelectBranch(worktrees: WorktreeInfo[], message: string): Promise<WorktreeInfo> {
48
+ // @ts-expect-error enquirer 类型声明未导出 Select 类,但运行时存在
49
+ const selectedBranch: string = await new Enquirer.Select({
50
+ message,
51
+ choices: worktrees.map((wt) => ({
52
+ name: wt.branch,
53
+ message: wt.branch,
54
+ })),
55
+ }).run();
56
+
57
+ return worktrees.find((wt) => wt.branch === selectedBranch)!;
58
+ }
59
+
60
+ /**
61
+ * 根据用户输入解析目标 worktree
62
+ * 匹配策略:精确匹配 → 模糊匹配(唯一直接使用,多个交互选择) → 无匹配报错
63
+ * 不传分支名时列出所有可用分支供选择
64
+ * @param {WorktreeInfo[]} worktrees - 可用的 worktree 列表
65
+ * @param {WorktreeResolveMessages} messages - 命令专属的消息文案
66
+ * @param {string} [branchName] - 用户输入的分支名(可选)
67
+ * @returns {Promise<WorktreeInfo>} 解析后的目标 worktree
68
+ * @throws {ClawtError} 无可用 worktree 或无匹配结果时抛出
69
+ */
70
+ export async function resolveTargetWorktree(
71
+ worktrees: WorktreeInfo[],
72
+ messages: WorktreeResolveMessages,
73
+ branchName?: string,
74
+ ): Promise<WorktreeInfo> {
75
+ // 无可用 worktree,直接报错
76
+ if (worktrees.length === 0) {
77
+ throw new ClawtError(messages.noWorktrees);
78
+ }
79
+
80
+ // 未传 -b 参数:列出所有分支供选择
81
+ if (!branchName) {
82
+ // 只有一个 worktree 时直接使用,无需选择
83
+ if (worktrees.length === 1) {
84
+ return worktrees[0];
85
+ }
86
+ return promptSelectBranch(worktrees, messages.selectBranch);
87
+ }
88
+
89
+ // 1. 精确匹配优先
90
+ const exactMatch = findExactMatch(worktrees, branchName);
91
+ if (exactMatch) {
92
+ return exactMatch;
93
+ }
94
+
95
+ // 2. 模糊匹配
96
+ const fuzzyMatches = findFuzzyMatches(worktrees, branchName);
97
+
98
+ // 2a. 唯一匹配,直接使用
99
+ if (fuzzyMatches.length === 1) {
100
+ return fuzzyMatches[0];
101
+ }
102
+
103
+ // 2b. 多个匹配,交互选择
104
+ if (fuzzyMatches.length > 1) {
105
+ return promptSelectBranch(fuzzyMatches, messages.multipleMatches(branchName));
106
+ }
107
+
108
+ // 3. 无匹配,抛出错误并列出所有可用分支
109
+ const allBranches = worktrees.map((wt) => wt.branch);
110
+ throw new ClawtError(messages.noMatch(branchName, allBranches));
111
+ }