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.
- package/.claude/agent-memory/docs-sync-updater/MEMORY.md +6 -2
- package/README.md +18 -3
- package/dist/index.js +94 -70
- package/docs/spec.md +26 -5
- package/package.json +1 -1
- package/src/commands/resume.ts +13 -93
- package/src/commands/validate.ts +31 -19
- package/src/constants/messages.ts +9 -0
- package/src/types/command.ts +2 -2
- package/src/utils/index.ts +2 -0
- package/src/utils/worktree-matcher.ts +111 -0
|
@@ -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
|
-
-
|
|
69
|
-
-
|
|
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
|
-
|
|
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
|
|
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").
|
|
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
|
-
|
|
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 ${
|
|
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,
|
|
1222
|
-
printSuccess(MESSAGES.VALIDATE_CLEANED(
|
|
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
|
|
1270
|
-
const
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
1315
|
+
handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
1292
1316
|
}
|
|
1293
1317
|
}
|
|
1294
1318
|
|
|
1295
1319
|
// src/commands/merge.ts
|
|
1296
|
-
import { join as
|
|
1297
|
-
import { existsSync as
|
|
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 =
|
|
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 (!
|
|
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
|
|
1435
|
-
import { join as
|
|
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 =
|
|
1464
|
-
if (!
|
|
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` |
|
|
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.
|
|
377
|
-
3.
|
|
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
package/src/commands/resume.ts
CHANGED
|
@@ -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
|
|
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
|
|
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);
|
package/src/commands/validate.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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 并删除分支 ${
|
|
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,
|
|
215
|
+
removeSnapshot(projectName, branchName);
|
|
203
216
|
|
|
204
|
-
printSuccess(MESSAGES.VALIDATE_CLEANED(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
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;
|
package/src/types/command.ts
CHANGED
package/src/utils/index.ts
CHANGED
|
@@ -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
|
+
}
|