clawt 2.6.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,9 +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
+ - validate 命令对应 `5.4 在主 Worktree 验证其他分支`,-b 可选,支持模糊匹配(与 resume 共享匹配逻辑)
13
+ - sync 命令对应 `5.12 将主分支代码同步到目标 Worktree`,-b 可选,支持模糊匹配(与 resume/validate/merge 共享匹配逻辑)
12
14
  - 配置项说明在 `5.7 默认配置文件` 章节的表格中
13
15
  - 更新模式:新增步骤时追加编号,配置项影响范围变化时更新说明列
14
16
 
@@ -65,8 +67,11 @@ Notes:
65
67
  - resume 和 run(交互式模式)共用 `launchInteractiveClaude()`,该函数从 run.ts 提取到 src/utils/claude.ts
66
68
  - `claudeCodeCommand` 配置项同时影响 run 交互式模式和 resume 命令
67
69
  - reset 命令与 validate --clean 的区别:reset 不删除快照文件,validate --clean 会删除快照
68
- - resume 的 `-b` 参数为可选,核心函数 `resolveTargetWorktree()` 封装匹配策略:精确→模糊(子串,大小写不敏感)→交互选择
69
- - resume 的交互式选择使用 Enquirer.Select(`promptSelectBranch()`),消息常量在 `MESSAGES.RESUME_*`
70
+ - `resolveTargetWorktree()` 是 resume、validate、merge 和 sync 共用的分支匹配函数(在 src/utils/worktree-matcher.ts)
71
+ - `WorktreeResolveMessages` 接口实现命令间消息解耦,每个命令传入各自的提示文案
72
+ - resume 的消息常量在 `MESSAGES.RESUME_*`,validate 的消息常量在 `MESSAGES.VALIDATE_*`,merge 的消息常量在 `MESSAGES.MERGE_*`,sync 的消息常量在 `MESSAGES.SYNC_*`
73
+ - resume、validate、merge 和 sync 的 `-b` 参数均为可选,匹配策略一致:精确→模糊(子串,大小写不敏感)→交互选择
74
+ - validate 的交互式选择和 resume 使用同一个 `promptSelectBranch()`(Enquirer.Select)
70
75
 
71
76
  ## validate 快照机制
72
77
 
package/README.md CHANGED
@@ -107,67 +107,110 @@ 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
136
151
 
137
152
  ```bash
153
+ # 指定分支名(支持模糊匹配)
138
154
  clawt sync -b <branchName>
155
+
156
+ # 不指定分支名(列出所有分支供选择)
157
+ clawt sync
139
158
  ```
140
159
 
141
160
  | 参数 | 必填 | 说明 |
142
161
  | ---- | ---- | ---- |
143
- | `-b` | | 要同步的分支名 |
162
+ | `-b` | | 要同步的分支名(支持模糊匹配,不传则列出所有分支供选择) |
144
163
 
145
164
  将主分支最新代码合并到目标 worktree 的分支中。如果目标 worktree 有未提交的修改,会自动保存后再合并。存在冲突时会提示用户手动解决。合并成功后会自动清除该分支的 validate 快照(代码基础已变化,旧快照无效)。
146
165
 
166
+ **分支匹配策略:**
167
+ - 传 `-b` 时,优先精确匹配分支名;未精确匹配则进行模糊匹配(子串匹配,大小写不敏感);模糊匹配到多个时通过交互列表选择;无匹配时报错并列出所有可用分支
168
+ - 不传 `-b` 时,列出当前项目所有可用分支供交互式选择(仅 1 个时自动使用)
169
+
147
170
  ```bash
148
- # 将主分支最新代码同步到目标 worktree
171
+ # 精确匹配分支名
149
172
  clawt sync -b feature-scheme-1
173
+
174
+ # 模糊匹配(匹配包含 "scheme" 的分支)
175
+ clawt sync -b scheme
176
+
177
+ # 交互式选择所有分支
178
+ clawt sync
150
179
  ```
151
180
 
152
181
  ### `clawt merge` — 合并分支到主 worktree
153
182
 
154
183
  ```bash
184
+ # 指定分支名(支持模糊匹配)
155
185
  clawt merge -b <branchName> [-m <commitMessage>]
186
+
187
+ # 不指定分支名(列出所有分支供选择)
188
+ clawt merge [-m <commitMessage>]
156
189
  ```
157
190
 
158
191
  | 参数 | 必填 | 说明 |
159
192
  | ---- | ---- | ---- |
160
- | `-b` | | 要合并的分支名 |
193
+ | `-b` | | 要合并的分支名(支持模糊匹配,不传则列出所有分支供选择) |
161
194
  | `-m` | 否 | 提交信息(目标 worktree 工作区有修改时必填) |
162
195
 
163
196
  将目标 worktree 的变更合并到主 worktree 的当前分支。如果配置了 `autoPullPush: true`,合并后会自动推送到远程仓库。如果目标 worktree 工作区有未提交的修改,需要通过 `-m` 提供提交信息;如果目标 worktree 已经提交过(工作区干净但有本地提交),可以省略 `-m` 直接合并。merge 成功后会询问是否清理对应的 worktree 和分支(如果配置了 `autoDeleteBranch: true` 则自动清理)。
164
197
 
198
+ **分支匹配策略:**
199
+ - 传 `-b` 时,优先精确匹配分支名;未精确匹配则进行模糊匹配(子串匹配,大小写不敏感);模糊匹配到多个时通过交互列表选择;无匹配时报错并列出所有可用分支
200
+ - 不传 `-b` 时,列出当前项目所有可用分支供交互式选择(仅 1 个时自动使用)
201
+
165
202
  如果检测到目标分支存在 `clawt sync` 产生的临时提交(auto-save commit),会自动提示是否将所有提交压缩(squash)为一个。用户选择压缩后,所有 commit 会被 reset 到暂存区:如果提供了 `-m` 则直接提交并继续合并流程;如果未提供 `-m` 则提示用户前往目标 worktree 自行提交后重新执行 merge。
166
203
 
167
204
  ```bash
168
- # 目标 worktree 有未提交修改,需提供 -m
205
+ # 精确匹配,目标 worktree 有未提交修改,需提供 -m
169
206
  clawt merge -b feature-scheme-1 -m "feat: 实现用户登录功能"
170
207
 
208
+ # 模糊匹配(匹配包含 "scheme" 的分支)
209
+ clawt merge -b scheme
210
+
211
+ # 交互式选择所有分支
212
+ clawt merge
213
+
171
214
  # 目标 worktree 已提交过,可省略 -m
172
215
  clawt merge -b feature-scheme-1
173
216
  ```
package/dist/index.js CHANGED
@@ -134,7 +134,37 @@ ${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`,
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`
138
168
  };
139
169
 
140
170
  // src/constants/exitCodes.ts
@@ -742,6 +772,50 @@ function removeProjectSnapshots(projectName) {
742
772
  logger.info(`\u5DF2\u5220\u9664\u9879\u76EE ${projectName} \u7684\u6240\u6709 validate \u5FEB\u7167`);
743
773
  }
744
774
 
775
+ // src/utils/worktree-matcher.ts
776
+ import Enquirer2 from "enquirer";
777
+ function findExactMatch(worktrees, branchName) {
778
+ return worktrees.find((wt) => wt.branch === branchName);
779
+ }
780
+ function findFuzzyMatches(worktrees, keyword) {
781
+ const lowerKeyword = keyword.toLowerCase();
782
+ return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
783
+ }
784
+ async function promptSelectBranch(worktrees, message) {
785
+ const selectedBranch = await new Enquirer2.Select({
786
+ message,
787
+ choices: worktrees.map((wt) => ({
788
+ name: wt.branch,
789
+ message: wt.branch
790
+ }))
791
+ }).run();
792
+ return worktrees.find((wt) => wt.branch === selectedBranch);
793
+ }
794
+ async function resolveTargetWorktree(worktrees, messages, branchName) {
795
+ if (worktrees.length === 0) {
796
+ throw new ClawtError(messages.noWorktrees);
797
+ }
798
+ if (!branchName) {
799
+ if (worktrees.length === 1) {
800
+ return worktrees[0];
801
+ }
802
+ return promptSelectBranch(worktrees, messages.selectBranch);
803
+ }
804
+ const exactMatch = findExactMatch(worktrees, branchName);
805
+ if (exactMatch) {
806
+ return exactMatch;
807
+ }
808
+ const fuzzyMatches = findFuzzyMatches(worktrees, branchName);
809
+ if (fuzzyMatches.length === 1) {
810
+ return fuzzyMatches[0];
811
+ }
812
+ if (fuzzyMatches.length > 1) {
813
+ return promptSelectBranch(fuzzyMatches, messages.multipleMatches(branchName));
814
+ }
815
+ const allBranches = worktrees.map((wt) => wt.branch);
816
+ throw new ClawtError(messages.noMatch(branchName, allBranches));
817
+ }
818
+
745
819
  // src/commands/list.ts
746
820
  import chalk2 from "chalk";
747
821
  function registerListCommand(program2) {
@@ -1059,68 +1133,36 @@ async function handleRun(options) {
1059
1133
  }
1060
1134
 
1061
1135
  // src/commands/resume.ts
1062
- import Enquirer2 from "enquirer";
1136
+ var RESUME_RESOLVE_MESSAGES = {
1137
+ noWorktrees: MESSAGES.RESUME_NO_WORKTREES,
1138
+ selectBranch: MESSAGES.RESUME_SELECT_BRANCH,
1139
+ multipleMatches: MESSAGES.RESUME_MULTIPLE_MATCHES,
1140
+ noMatch: MESSAGES.RESUME_NO_MATCH
1141
+ };
1063
1142
  function registerResumeCommand(program2) {
1064
1143
  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
1144
  await handleResume(options);
1066
1145
  });
1067
1146
  }
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
1147
  async function handleResume(options) {
1111
1148
  validateMainWorktree();
1112
1149
  validateClaudeCodeInstalled();
1113
1150
  logger.info(`resume \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch ?? "(\u672A\u6307\u5B9A)"}`);
1114
- const worktree = await resolveTargetWorktree(options.branch);
1151
+ const worktrees = getProjectWorktrees();
1152
+ const worktree = await resolveTargetWorktree(worktrees, RESUME_RESOLVE_MESSAGES, options.branch);
1115
1153
  launchInteractiveClaude(worktree);
1116
1154
  }
1117
1155
 
1118
1156
  // src/commands/validate.ts
1119
- import { join as join4 } from "path";
1120
- import { existsSync as existsSync6 } from "fs";
1121
1157
  import Enquirer3 from "enquirer";
1158
+ var VALIDATE_RESOLVE_MESSAGES = {
1159
+ noWorktrees: MESSAGES.VALIDATE_NO_WORKTREES,
1160
+ selectBranch: MESSAGES.VALIDATE_SELECT_BRANCH,
1161
+ multipleMatches: MESSAGES.VALIDATE_MULTIPLE_MATCHES,
1162
+ noMatch: MESSAGES.VALIDATE_NO_MATCH
1163
+ };
1122
1164
  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) => {
1165
+ 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
1166
  await handleValidate(options);
1125
1167
  });
1126
1168
  }
@@ -1203,11 +1245,14 @@ async function handleValidateClean(options) {
1203
1245
  validateMainWorktree();
1204
1246
  const projectName = getProjectName();
1205
1247
  const mainWorktreePath = getGitTopLevel();
1206
- logger.info(`validate --clean \u6267\u884C\uFF0C\u5206\u652F: ${options.branch}`);
1248
+ const worktrees = getProjectWorktrees();
1249
+ const worktree = await resolveTargetWorktree(worktrees, VALIDATE_RESOLVE_MESSAGES, options.branch);
1250
+ const branchName = worktree.branch;
1251
+ logger.info(`validate --clean \u6267\u884C\uFF0C\u5206\u652F: ${branchName}`);
1207
1252
  if (getConfigValue("confirmDestructiveOps")) {
1208
1253
  const confirmed = await confirmDestructiveAction(
1209
1254
  "git reset --hard + git clean -fd",
1210
- `\u91CD\u7F6E\u4E3B worktree \u5E76\u5220\u9664\u5206\u652F ${options.branch} \u7684 validate \u5FEB\u7167`
1255
+ `\u91CD\u7F6E\u4E3B worktree \u5E76\u5220\u9664\u5206\u652F ${branchName} \u7684 validate \u5FEB\u7167`
1211
1256
  );
1212
1257
  if (!confirmed) {
1213
1258
  printInfo(MESSAGES.DESTRUCTIVE_OP_CANCELLED);
@@ -1218,8 +1263,8 @@ async function handleValidateClean(options) {
1218
1263
  gitResetHard(mainWorktreePath);
1219
1264
  gitCleanForce(mainWorktreePath);
1220
1265
  }
1221
- removeSnapshot(projectName, options.branch);
1222
- printSuccess(MESSAGES.VALIDATE_CLEANED(options.branch));
1266
+ removeSnapshot(projectName, branchName);
1267
+ printSuccess(MESSAGES.VALIDATE_CLEANED(branchName));
1223
1268
  }
1224
1269
  function handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
1225
1270
  migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
@@ -1266,40 +1311,43 @@ async function handleValidate(options) {
1266
1311
  validateMainWorktree();
1267
1312
  const projectName = getProjectName();
1268
1313
  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
- }
1314
+ const worktrees = getProjectWorktrees();
1315
+ const worktree = await resolveTargetWorktree(worktrees, VALIDATE_RESOLVE_MESSAGES, options.branch);
1316
+ const branchName = worktree.branch;
1317
+ const targetWorktreePath = worktree.path;
1318
+ logger.info(`validate \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${branchName}`);
1275
1319
  const hasUncommitted = !isWorkingDirClean(targetWorktreePath);
1276
- const hasCommitted = hasLocalCommits(options.branch, mainWorktreePath);
1320
+ const hasCommitted = hasLocalCommits(branchName, mainWorktreePath);
1277
1321
  if (!hasUncommitted && !hasCommitted) {
1278
1322
  printInfo(MESSAGES.TARGET_WORKTREE_CLEAN);
1279
1323
  return;
1280
1324
  }
1281
- const isIncremental = hasSnapshot(projectName, options.branch);
1325
+ const isIncremental = hasSnapshot(projectName, branchName);
1282
1326
  if (isIncremental) {
1283
1327
  if (!isWorkingDirClean(mainWorktreePath)) {
1284
1328
  await handleDirtyMainWorktree(mainWorktreePath);
1285
1329
  }
1286
- handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, options.branch, hasUncommitted);
1330
+ handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
1287
1331
  } else {
1288
1332
  if (!isWorkingDirClean(mainWorktreePath)) {
1289
1333
  await handleDirtyMainWorktree(mainWorktreePath);
1290
1334
  }
1291
- handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, options.branch, hasUncommitted);
1335
+ handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
1292
1336
  }
1293
1337
  }
1294
1338
 
1295
1339
  // src/commands/merge.ts
1296
- import { join as join5 } from "path";
1297
- import { existsSync as existsSync7 } from "fs";
1298
1340
  function registerMergeCommand(program2) {
1299
- 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) => {
1300
1342
  await handleMerge(options);
1301
1343
  });
1302
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
+ };
1303
1351
  async function handleSquashIfNeeded(targetWorktreePath, mainWorktreePath, branchName, commitMessage) {
1304
1352
  if (!hasCommitWithMessage(branchName, AUTO_SAVE_COMMIT_MESSAGE, mainWorktreePath)) {
1305
1353
  return false;
@@ -1335,20 +1383,18 @@ function cleanupWorktreeAndBranch(worktreePath, branchName) {
1335
1383
  async function handleMerge(options) {
1336
1384
  validateMainWorktree();
1337
1385
  const mainWorktreePath = getGitTopLevel();
1338
- const projectDir = getProjectWorktreeDir();
1339
- const targetWorktreePath = join5(projectDir, options.branch);
1340
- 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)) {
1342
- throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
1343
- }
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;
1344
1390
  const projectName = getProjectName();
1345
1391
  if (!isWorkingDirClean(mainWorktreePath)) {
1346
- if (hasSnapshot(projectName, options.branch)) {
1347
- printWarning(MESSAGES.MERGE_VALIDATE_STATE_HINT(options.branch));
1392
+ if (hasSnapshot(projectName, branch)) {
1393
+ printWarning(MESSAGES.MERGE_VALIDATE_STATE_HINT(branch));
1348
1394
  }
1349
1395
  throw new ClawtError(MESSAGES.MAIN_WORKTREE_DIRTY);
1350
1396
  }
1351
- const shouldExit = await handleSquashIfNeeded(targetWorktreePath, mainWorktreePath, options.branch, options.message);
1397
+ const shouldExit = await handleSquashIfNeeded(targetWorktreePath, mainWorktreePath, branch, options.message);
1352
1398
  if (shouldExit) {
1353
1399
  return;
1354
1400
  }
@@ -1360,12 +1406,12 @@ async function handleMerge(options) {
1360
1406
  gitAddAll(targetWorktreePath);
1361
1407
  gitCommit(options.message, targetWorktreePath);
1362
1408
  } else {
1363
- if (!hasLocalCommits(options.branch, mainWorktreePath)) {
1409
+ if (!hasLocalCommits(branch, mainWorktreePath)) {
1364
1410
  throw new ClawtError(MESSAGES.TARGET_WORKTREE_NO_CHANGES);
1365
1411
  }
1366
1412
  }
1367
1413
  try {
1368
- gitMerge(options.branch, mainWorktreePath);
1414
+ gitMerge(branch, mainWorktreePath);
1369
1415
  } catch (error) {
1370
1416
  if (hasMergeConflict(mainWorktreePath)) {
1371
1417
  throw new ClawtError(MESSAGES.MERGE_CONFLICT);
@@ -1383,16 +1429,16 @@ async function handleMerge(options) {
1383
1429
  printInfo("\u5DF2\u8DF3\u8FC7\u81EA\u52A8 pull/push\uFF0C\u8BF7\u624B\u52A8\u6267\u884C git pull && git push");
1384
1430
  }
1385
1431
  if (options.message) {
1386
- printSuccess(MESSAGES.MERGE_SUCCESS(options.branch, options.message, autoPullPush));
1432
+ printSuccess(MESSAGES.MERGE_SUCCESS(branch, options.message, autoPullPush));
1387
1433
  } else {
1388
- printSuccess(MESSAGES.MERGE_SUCCESS_NO_MESSAGE(options.branch, autoPullPush));
1434
+ printSuccess(MESSAGES.MERGE_SUCCESS_NO_MESSAGE(branch, autoPullPush));
1389
1435
  }
1390
- const shouldCleanup = await shouldCleanupAfterMerge(options.branch);
1436
+ const shouldCleanup = await shouldCleanupAfterMerge(branch);
1391
1437
  if (shouldCleanup) {
1392
- cleanupWorktreeAndBranch(targetWorktreePath, options.branch);
1438
+ cleanupWorktreeAndBranch(targetWorktreePath, branch);
1393
1439
  }
1394
- if (hasSnapshot(projectName, options.branch)) {
1395
- removeSnapshot(projectName, options.branch);
1440
+ if (hasSnapshot(projectName, branch)) {
1441
+ removeSnapshot(projectName, branch);
1396
1442
  }
1397
1443
  }
1398
1444
 
@@ -1431,13 +1477,17 @@ function formatConfigValue(value) {
1431
1477
  }
1432
1478
 
1433
1479
  // src/commands/sync.ts
1434
- import { existsSync as existsSync8 } from "fs";
1435
- import { join as join6 } from "path";
1436
1480
  function registerSyncCommand(program2) {
1437
- 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) => {
1438
1482
  await handleSync(options);
1439
1483
  });
1440
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
+ };
1441
1491
  function autoSaveChanges(worktreePath, branch) {
1442
1492
  gitAddAll(worktreePath);
1443
1493
  gitCommit(AUTO_SAVE_COMMIT_MESSAGE, worktreePath);
@@ -1457,13 +1507,10 @@ function mergeMainBranch(worktreePath, mainBranch) {
1457
1507
  }
1458
1508
  async function handleSync(options) {
1459
1509
  validateMainWorktree();
1460
- const { branch } = options;
1461
- logger.info(`sync \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${branch}`);
1462
- const projectWorktreeDir = getProjectWorktreeDir();
1463
- const targetWorktreePath = join6(projectWorktreeDir, branch);
1464
- if (!existsSync8(targetWorktreePath)) {
1465
- throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(branch));
1466
- }
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;
1467
1514
  const mainWorktreePath = getGitTopLevel();
1468
1515
  const mainBranch = getCurrentBranch(mainWorktreePath);
1469
1516
  if (!isWorkingDirClean(targetWorktreePath)) {