clawt 2.10.0 → 2.10.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.
Files changed (40) hide show
  1. package/.claude/agent-memory/docs-sync-updater/MEMORY.md +9 -6
  2. package/.claude/agents/docs-sync-updater.md +11 -0
  3. package/README.md +63 -290
  4. package/dist/index.js +203 -104
  5. package/dist/postinstall.js +242 -0
  6. package/docs/spec.md +27 -14
  7. package/package.json +1 -1
  8. package/src/commands/remove.ts +21 -28
  9. package/src/constants/index.ts +1 -1
  10. package/src/constants/messages/common.ts +41 -0
  11. package/src/constants/messages/config.ts +5 -0
  12. package/src/constants/messages/create.ts +5 -0
  13. package/src/constants/messages/index.ts +29 -0
  14. package/src/constants/messages/merge.ts +42 -0
  15. package/src/constants/messages/remove.ts +15 -0
  16. package/src/constants/messages/reset.ts +7 -0
  17. package/src/constants/messages/resume.ts +12 -0
  18. package/src/constants/messages/run.ts +16 -0
  19. package/src/constants/messages/status.ts +25 -0
  20. package/src/constants/messages/sync.ts +24 -0
  21. package/src/constants/messages/validate.ts +25 -0
  22. package/src/utils/index.ts +2 -2
  23. package/src/utils/worktree-matcher.ts +92 -0
  24. package/tests/unit/commands/config.test.ts +110 -0
  25. package/tests/unit/commands/create.test.ts +115 -0
  26. package/tests/unit/commands/list.test.ts +118 -0
  27. package/tests/unit/commands/merge.test.ts +323 -0
  28. package/tests/unit/commands/remove.test.ts +240 -0
  29. package/tests/unit/commands/reset.test.ts +124 -0
  30. package/tests/unit/commands/resume.test.ts +91 -0
  31. package/tests/unit/commands/run.test.ts +207 -0
  32. package/tests/unit/commands/status.test.ts +214 -0
  33. package/tests/unit/commands/sync.test.ts +208 -0
  34. package/tests/unit/commands/validate.test.ts +382 -0
  35. package/tests/unit/constants/messages.test.ts +1 -1
  36. package/tests/unit/utils/config.test.ts +21 -1
  37. package/tests/unit/utils/formatter.test.ts +44 -1
  38. package/tests/unit/utils/git.test.ts +44 -0
  39. package/tests/unit/utils/validate-snapshot.test.ts +25 -0
  40. package/tests/unit/utils/worktree-matcher.test.ts +81 -5
package/docs/spec.md CHANGED
@@ -171,7 +171,7 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
171
171
  | `clawt run` | 批量创建 worktree + 启动 Claude Code 执行任务 | 5.2 |
172
172
  | `clawt validate` | 在主 worktree 验证某个 worktree 分支的变更 | 5.4 |
173
173
  | `clawt merge` | 合并某个已验证的 worktree 分支到主 worktree | 5.6 |
174
- | `clawt remove` | 移除 worktree(支持单个/批量/全部) | 5.5 |
174
+ | `clawt remove` | 移除 worktree(支持模糊匹配/多选/全部) | 5.5 |
175
175
  | `clawt list` | 列出当前项目所有 worktree(支持 `--json` 格式输出) | 5.8 |
176
176
  | `clawt config` | 查看全局配置 | 5.10 |
177
177
  | `clawt config reset` | 将配置恢复为默认值 | 5.10 |
@@ -574,29 +574,42 @@ git apply --cached < patch
574
574
  **命令:**
575
575
 
576
576
  ```bash
577
- clawt remove [options]
577
+ # 移除当前项目所有 worktree
578
+ clawt remove --all
579
+
580
+ # 指定分支名(支持模糊匹配)
581
+ clawt remove -b <branchName>
582
+
583
+ # 不指定参数(列出所有分支供多选)
584
+ clawt remove
578
585
  ```
579
586
 
580
587
  **参数:**
581
588
 
582
- | 参数 | 说明 |
583
- | --------- | ---------------------------------------------------------- |
584
- | `--all` | 移除当前项目 (`~/.clawt/worktrees/<project>/`) 下所有 worktree |
585
- | `-b <branchName>` | 移除匹配 branchName 或 branchName-* 的 worktree |
586
-
587
- **三种移除粒度:**
589
+ | 参数 | 必填 | 说明 |
590
+ | --------- | ---- | ---------------------------------------------------------------------- |
591
+ | `--all` | 否 | 移除当前项目 (`~/.clawt/worktrees/<project>/`) 下所有 worktree |
592
+ | `-b` | | 指定分支名(支持模糊匹配,不传则列出所有分支供多选) |
588
593
 
589
- | 粒度 | 命令示例 | 移除范围 |
590
- | ---- | ---------------------------------------- | ------------------------------------------------------------- |
591
- | 全部 | `clawt remove --all` | `~/.clawt/worktrees/<project>/` 下所有 worktree |
592
- | 分支 | `clawt remove -b feature-scheme` | `~/.clawt/worktrees/<project>/feature-scheme-*` 的所有 worktree |
593
- | 单个 | `clawt remove -b feature-scheme-2` | 仅移除 `feature-scheme-2` 对应的 worktree(完整分支名精确匹配) |
594
+ > **提示:** 不传 `--all` 也不传 `-b` 时,会列出当前项目所有 worktree 供交互式多选。
594
595
 
595
596
  **运行流程:**
596
597
 
597
598
  1. **主 worktree 校验** (2.1)
598
599
  2. **获取项目名** (2.2)
599
- 3. 根据参数确定要移除的 worktree 列表
600
+ 3. **确定待移除的 worktree 列表**:
601
+ - **指定 `--all`** → 选中当前项目所有 worktree
602
+ - **未指定 `--all`** → 通过 `resolveTargetWorktrees` 解析目标 worktree(多选版本),匹配策略如下:
603
+ - **未传 `-b` 参数**:
604
+ - 无可用 worktree → 报错退出
605
+ - 仅 1 个 worktree → 直接使用,无需选择
606
+ - 多个 worktree → 通过交互式多选列表(Enquirer.MultiSelect)让用户选择(空格选择,回车确认)
607
+ - **传了 `-b` 参数**:
608
+ 1. **精确匹配优先**:在 worktree 列表中查找分支名完全相同的 worktree,找到则直接使用
609
+ 2. **模糊匹配**(子串匹配,大小写不敏感):
610
+ - 唯一匹配 → 直接使用
611
+ - 多个匹配 → 通过交互式多选列表让用户从匹配结果中选择
612
+ 3. **无匹配** → 报错退出,并列出所有可用分支名
600
613
  4. 列出即将移除的 worktree 及对应分支:
601
614
 
602
615
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "2.10.0",
3
+ "version": "2.10.1",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -19,7 +19,17 @@ import {
19
19
  confirmAction,
20
20
  removeSnapshot,
21
21
  removeProjectSnapshots,
22
+ resolveTargetWorktrees,
22
23
  } from '../utils/index.js';
24
+ import type { WorktreeMultiResolveMessages } from '../utils/index.js';
25
+
26
+ /** remove 命令的分支解析消息配置 */
27
+ const REMOVE_RESOLVE_MESSAGES: WorktreeMultiResolveMessages = {
28
+ noWorktrees: MESSAGES.REMOVE_NO_WORKTREES,
29
+ selectBranch: MESSAGES.REMOVE_SELECT_BRANCH,
30
+ multipleMatches: MESSAGES.REMOVE_MULTIPLE_MATCHES,
31
+ noMatch: MESSAGES.REMOVE_NO_MATCH,
32
+ };
23
33
 
24
34
  /**
25
35
  * 注册 remove 命令:移除 worktree
@@ -30,38 +40,12 @@ export function registerRemoveCommand(program: Command): void {
30
40
  .command('remove')
31
41
  .description('移除 worktree(支持单个/批量/全部)')
32
42
  .option('--all', '移除当前项目下所有 worktree')
33
- .option('-b, --branch <branchName>', '指定分支名(完整分支名精确匹配)')
43
+ .option('-b, --branch <branchName>', '指定分支名(支持模糊匹配,不传则列出所有分支)')
34
44
  .action(async (options: RemoveOptions) => {
35
45
  await handleRemove(options);
36
46
  });
37
47
  }
38
48
 
39
- /**
40
- * 根据参数确定要移除的 worktree 列表
41
- * @param {RemoveOptions} options - 命令选项
42
- * @returns {Array<{path: string, branch: string}>} 待移除的 worktree 列表
43
- */
44
- function resolveWorktreesToRemove(options: RemoveOptions): Array<{ path: string; branch: string }> {
45
- const allWorktrees = getProjectWorktrees();
46
-
47
- if (options.all) {
48
- return allWorktrees;
49
- }
50
-
51
- if (!options.branch) {
52
- throw new ClawtError('请指定 --all 或 -b <branchName> 参数');
53
- }
54
-
55
- // 分支级移除:匹配 branchName 或 branchName-*
56
- const matched = allWorktrees.filter(
57
- (wt) => wt.branch === options.branch || wt.branch.startsWith(`${options.branch}-`),
58
- );
59
- if (matched.length === 0) {
60
- throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
61
- }
62
- return matched;
63
- }
64
-
65
49
  /**
66
50
  * 执行 remove 命令的核心逻辑
67
51
  * @param {RemoveOptions} options - 命令选项
@@ -72,7 +56,16 @@ async function handleRemove(options: RemoveOptions): Promise<void> {
72
56
  const projectName = getProjectName();
73
57
  logger.info(`remove 命令执行,项目: ${projectName}`);
74
58
 
75
- const worktreesToRemove = resolveWorktreesToRemove(options);
59
+ const allWorktrees = getProjectWorktrees();
60
+
61
+ // 确定待移除的 worktree 列表
62
+ let worktreesToRemove;
63
+ if (options.all) {
64
+ worktreesToRemove = allWorktrees;
65
+ } else {
66
+ // 通过 fuzzy search 解析目标 worktree(精确匹配 / 模糊匹配 / 交互多选)
67
+ worktreesToRemove = await resolveTargetWorktrees(allWorktrees, REMOVE_RESOLVE_MESSAGES, options.branch);
68
+ }
76
69
 
77
70
  if (worktreesToRemove.length === 0) {
78
71
  printInfo(MESSAGES.NO_WORKTREES);
@@ -1,6 +1,6 @@
1
1
  export { CLAWT_HOME, CONFIG_PATH, LOGS_DIR, WORKTREES_DIR, VALIDATE_SNAPSHOTS_DIR } from './paths.js';
2
2
  export { INVALID_BRANCH_CHARS } from './branch.js';
3
- export { MESSAGES } from './messages.js';
3
+ export { MESSAGES } from './messages/index.js';
4
4
  export { EXIT_CODES } from './exitCodes.js';
5
5
  export { ENABLE_BRACKETED_PASTE, DISABLE_BRACKETED_PASTE, PASTE_THRESHOLD_MS } from './terminal.js';
6
6
  export { DEFAULT_CONFIG, CONFIG_DESCRIPTIONS, APPEND_SYSTEM_PROMPT } from './config.js';
@@ -0,0 +1,41 @@
1
+ /** 通用/共享提示消息 */
2
+ export const COMMON_MESSAGES = {
3
+ /** 不在主 worktree 根目录 */
4
+ NOT_MAIN_WORKTREE: '请在主 worktree 的根目录下执行 clawt',
5
+ /** Git 未安装 */
6
+ GIT_NOT_INSTALLED: 'Git 未安装或不在 PATH 中,请先安装 Git',
7
+ /** Claude Code CLI 未安装 */
8
+ CLAUDE_NOT_INSTALLED: 'Claude Code CLI 未安装,请先安装:npm install -g @anthropic-ai/claude-code',
9
+ /** 分支已存在 */
10
+ BRANCH_EXISTS: (name: string) => `分支 ${name} 已存在,无法创建`,
11
+ /** 分支名清理后为空 */
12
+ BRANCH_NAME_EMPTY: (original: string) =>
13
+ `分支名 "${original}" 中不包含合法字符,无法创建分支`,
14
+ /** 分支名被转换 */
15
+ BRANCH_SANITIZED: (original: string, sanitized: string) =>
16
+ `分支名已转换: ${original} → ${sanitized}`,
17
+ /** worktree 创建成功 */
18
+ WORKTREE_CREATED: (count: number) => `✓ 已创建 ${count} 个 worktree`,
19
+ /** worktree 移除成功 */
20
+ WORKTREE_REMOVED: (path: string) => `✓ 已移除 worktree: ${path}`,
21
+ /** 没有 worktree */
22
+ NO_WORKTREES: '(无 worktree)',
23
+ /** 目标 worktree 不存在 */
24
+ WORKTREE_NOT_FOUND: (name: string) => `worktree ${name} 不存在`,
25
+ /** 主 worktree 有未提交更改 */
26
+ MAIN_WORKTREE_DIRTY: '主 worktree 有未提交的更改,请先处理',
27
+ /** 目标 worktree 无更改 */
28
+ TARGET_WORKTREE_CLEAN: '该 worktree 的分支上没有任何更改,无需验证',
29
+ /** 用户取消破坏性操作 */
30
+ DESTRUCTIVE_OP_CANCELLED: '已取消操作',
31
+ /** 请提供提交信息 */
32
+ COMMIT_MESSAGE_REQUIRED: '请提供提交信息(-m 参数)',
33
+ /** 配置文件损坏,已重新生成默认配置 */
34
+ CONFIG_CORRUPTED: '配置文件损坏或无法解析,已重新生成默认配置',
35
+ /** worktree 状态获取失败 */
36
+ WORKTREE_STATUS_UNAVAILABLE: '(状态不可用)',
37
+ /** 分隔线 */
38
+ SEPARATOR: '────────────────────────────────────────',
39
+ /** 粗分隔线 */
40
+ DOUBLE_SEPARATOR: '════════════════════════════════════════',
41
+ } as const;
@@ -0,0 +1,5 @@
1
+ /** config 命令专属提示消息 */
2
+ export const CONFIG_CMD_MESSAGES = {
3
+ /** 配置已恢复为默认值 */
4
+ CONFIG_RESET_SUCCESS: '✓ 配置已恢复为默认值',
5
+ } as const;
@@ -0,0 +1,5 @@
1
+ /** create 命令专属提示消息 */
2
+ export const CREATE_MESSAGES = {
3
+ /** 创建数量参数无效 */
4
+ INVALID_COUNT: (value: string) => `无效的创建数量: "${value}",请输入正整数`,
5
+ } as const;
@@ -0,0 +1,29 @@
1
+ import { COMMON_MESSAGES } from './common.js';
2
+ import { RUN_MESSAGES } from './run.js';
3
+ import { CREATE_MESSAGES } from './create.js';
4
+ import { MERGE_MESSAGES } from './merge.js';
5
+ import { VALIDATE_MESSAGES } from './validate.js';
6
+ import { SYNC_MESSAGES } from './sync.js';
7
+ import { RESUME_MESSAGES } from './resume.js';
8
+ import { REMOVE_MESSAGES } from './remove.js';
9
+ import { RESET_MESSAGES } from './reset.js';
10
+ import { CONFIG_CMD_MESSAGES } from './config.js';
11
+ import { STATUS_MESSAGES } from './status.js';
12
+
13
+ /**
14
+ * 提示消息模板
15
+ * 合并所有子模块的消息,保持扁平结构以兼容现有的 MESSAGES.XXX 访问方式
16
+ */
17
+ export const MESSAGES = {
18
+ ...COMMON_MESSAGES,
19
+ ...RUN_MESSAGES,
20
+ ...CREATE_MESSAGES,
21
+ ...MERGE_MESSAGES,
22
+ ...VALIDATE_MESSAGES,
23
+ ...SYNC_MESSAGES,
24
+ ...RESUME_MESSAGES,
25
+ ...REMOVE_MESSAGES,
26
+ ...RESET_MESSAGES,
27
+ ...CONFIG_CMD_MESSAGES,
28
+ ...STATUS_MESSAGES,
29
+ } as const;
@@ -0,0 +1,42 @@
1
+ /** merge 命令专属提示消息 */
2
+ export const MERGE_MESSAGES = {
3
+ /** merge 成功 */
4
+ MERGE_SUCCESS: (branch: string, message: string, pushed: boolean) =>
5
+ `✓ 分支 ${branch} 已成功合并到当前分支\n 提交信息: ${message}${pushed ? '\n 已推送到远程仓库' : ''}`,
6
+ /** merge 成功(无提交信息,目标 worktree 已提交过) */
7
+ MERGE_SUCCESS_NO_MESSAGE: (branch: string, pushed: boolean) =>
8
+ `✓ 分支 ${branch} 已成功合并到当前分支${pushed ? '\n 已推送到远程仓库' : ''}`,
9
+ /** merge 冲突 */
10
+ MERGE_CONFLICT: '合并存在冲突,请手动处理:\n 解决冲突后执行 git add . && git merge --continue',
11
+ /** merge 后清理 worktree 和分支成功 */
12
+ WORKTREE_CLEANED: (branch: string) => `✓ 已清理 worktree 和分支: ${branch}`,
13
+ /** 目标 worktree 有未提交修改但未指定 -m */
14
+ TARGET_WORKTREE_DIRTY_NO_MESSAGE: '目标 worktree 有未提交的修改,请通过 -m 参数提供提交信息',
15
+ /** 目标 worktree 既干净又无本地提交 */
16
+ TARGET_WORKTREE_NO_CHANGES: '目标 worktree 没有任何可合并的变更(工作区干净且无本地提交)',
17
+ /** merge 命令检测到 validate 状态的提示 */
18
+ MERGE_VALIDATE_STATE_HINT: (branch: string) =>
19
+ `主 worktree 可能存在 validate 残留状态,可先执行 clawt validate -b ${branch} --clean 清理`,
20
+ /** merge 检测到 auto-save 提交,提示用户是否压缩 */
21
+ MERGE_SQUASH_PROMPT: '检测到 sync 产生的临时提交,是否将所有提交压缩为一个?\n 压缩后变更将保留在目标worktree的暂存区,需要重新提交(可使用 Claude Code Cli或其他工具生成提交信息)',
22
+ /** squash 完成且通过 -m 直接提交后的提示 */
23
+ MERGE_SQUASH_COMMITTED: (branch: string) =>
24
+ `✓ 已将分支 ${branch} 的所有提交压缩为一个`,
25
+ /** squash 完成但未提供 -m,提示用户自行提交 */
26
+ MERGE_SQUASH_PENDING: (worktreePath: string, branch: string) =>
27
+ `✓ 已将所有提交压缩到暂存区\n 请在目标 worktree 中提交后重新执行 merge:\n cd ${worktreePath}\n 提交完成后执行:clawt merge -b ${branch}`,
28
+ /** merge 后 pull 冲突 */
29
+ PULL_CONFLICT:
30
+ '自动 pull 时发生冲突,merge 已完成但远程同步失败\n 请手动解决冲突:\n 解决冲突后执行 git add . && git commit\n 然后执行 git push 推送到远程',
31
+ /** push 失败 */
32
+ PUSH_FAILED: '自动 push 失败,merge 和 pull 已完成\n 请手动执行 git push',
33
+ /** merge 无可用 worktree */
34
+ MERGE_NO_WORKTREES: '当前项目没有可用的 worktree,请先通过 clawt run 或 clawt create 创建',
35
+ /** merge 模糊匹配无结果,列出可用分支 */
36
+ MERGE_NO_MATCH: (name: string, branches: string[]) =>
37
+ `未找到与 "${name}" 匹配的分支\n 可用分支:\n${branches.map((b) => ` - ${b}`).join('\n')}`,
38
+ /** merge 交互选择提示 */
39
+ MERGE_SELECT_BRANCH: '请选择要合并的分支',
40
+ /** merge 模糊匹配到多个结果提示 */
41
+ MERGE_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择:`,
42
+ } as const;
@@ -0,0 +1,15 @@
1
+ /** remove 命令专属提示消息 */
2
+ export const REMOVE_MESSAGES = {
3
+ /** remove 无可用 worktree */
4
+ REMOVE_NO_WORKTREES: '当前项目没有可用的 worktree,无需移除',
5
+ /** remove 多选交互提示 */
6
+ REMOVE_SELECT_BRANCH: '请选择要移除的分支(空格选择,回车确认)',
7
+ /** remove 模糊匹配到多个结果提示 */
8
+ REMOVE_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择要移除的(空格选择,回车确认):`,
9
+ /** remove 模糊匹配无结果,列出可用分支 */
10
+ REMOVE_NO_MATCH: (name: string, branches: string[]) =>
11
+ `未找到与 "${name}" 匹配的分支\n 可用分支:\n${branches.map((b) => ` - ${b}`).join('\n')}`,
12
+ /** 批量移除部分失败 */
13
+ REMOVE_PARTIAL_FAILURE: (failures: Array<{ path: string; error: string }>) =>
14
+ `以下 worktree 移除失败:\n${failures.map((f) => ` ✗ ${f.path}: ${f.error}`).join('\n')}`,
15
+ } as const;
@@ -0,0 +1,7 @@
1
+ /** reset 命令专属提示消息 */
2
+ export const RESET_MESSAGES = {
3
+ /** reset 成功 */
4
+ RESET_SUCCESS: '✓ 主 worktree 工作区和暂存区已重置',
5
+ /** reset 时工作区和暂存区已干净 */
6
+ RESET_ALREADY_CLEAN: '主 worktree 工作区和暂存区已是干净状态,无需重置',
7
+ } as const;
@@ -0,0 +1,12 @@
1
+ /** resume 命令专属提示消息 */
2
+ export const RESUME_MESSAGES = {
3
+ /** resume 无可用 worktree */
4
+ RESUME_NO_WORKTREES: '当前项目没有可用的 worktree,请先通过 clawt run 或 clawt create 创建',
5
+ /** resume 模糊匹配无结果,列出可用分支 */
6
+ RESUME_NO_MATCH: (name: string, branches: string[]) =>
7
+ `未找到与 "${name}" 匹配的分支\n 可用分支:\n${branches.map((b) => ` - ${b}`).join('\n')}`,
8
+ /** resume 交互选择提示 */
9
+ RESUME_SELECT_BRANCH: '请选择要恢复的分支',
10
+ /** resume 模糊匹配到多个结果提示 */
11
+ RESUME_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择:`,
12
+ } as const;
@@ -0,0 +1,16 @@
1
+ /** run 命令专属提示消息 */
2
+ export const RUN_MESSAGES = {
3
+ /** 分支已存在时提示使用 resume */
4
+ BRANCH_EXISTS_USE_RESUME: (name: string) =>
5
+ `分支 ${name} 已存在,请使用 clawt resume -b ${name} 恢复会话`,
6
+ /** 检测到用户中断 */
7
+ INTERRUPTED: '检测到退出指令,已停止 Claude Code 任务',
8
+ /** 中断后自动清理完成 */
9
+ INTERRUPT_AUTO_CLEANED: (count: number) => `✓ 已自动清理 ${count} 个 worktree 和对应分支`,
10
+ /** 中断后手动确认清理 */
11
+ INTERRUPT_CONFIRM_CLEANUP: '是否移除刚刚创建的 worktree 和对应分支?',
12
+ /** 中断后清理完成 */
13
+ INTERRUPT_CLEANED: (count: number) => `✓ 已清理 ${count} 个 worktree 和对应分支`,
14
+ /** 中断后保留 worktree */
15
+ INTERRUPT_KEPT: '已保留 worktree,可稍后使用 clawt remove 手动清理',
16
+ } as const;
@@ -0,0 +1,25 @@
1
+ /** status 命令专属提示消息 */
2
+ export const STATUS_MESSAGES = {
3
+ /** status 命令标题 */
4
+ STATUS_TITLE: (projectName: string) => `项目状态总览: ${projectName}`,
5
+ /** status 主 worktree 区块标题 */
6
+ STATUS_MAIN_SECTION: '主 Worktree',
7
+ /** status worktrees 区块标题 */
8
+ STATUS_WORKTREES_SECTION: 'Worktree 列表',
9
+ /** status 快照区块标题 */
10
+ STATUS_SNAPSHOTS_SECTION: '未清理的 Validate 快照',
11
+ /** status 无 worktree */
12
+ STATUS_NO_WORKTREES: '(无活跃 worktree)',
13
+ /** status 无未清理快照 */
14
+ STATUS_NO_SNAPSHOTS: '(无未清理的快照)',
15
+ /** status 变更状态:已提交 */
16
+ STATUS_CHANGE_COMMITTED: '已提交',
17
+ /** status 变更状态:未提交修改 */
18
+ STATUS_CHANGE_UNCOMMITTED: '未提交修改',
19
+ /** status 变更状态:合并冲突 */
20
+ STATUS_CHANGE_CONFLICT: '合并冲突',
21
+ /** status 变更状态:无变更 */
22
+ STATUS_CHANGE_CLEAN: '无变更',
23
+ /** status 快照对应 worktree 已不存在 */
24
+ STATUS_SNAPSHOT_ORPHANED: '(对应 worktree 已不存在)',
25
+ } as const;
@@ -0,0 +1,24 @@
1
+ /** sync 命令专属提示消息 */
2
+ export const SYNC_MESSAGES = {
3
+ /** sync 自动保存未提交变更 */
4
+ SYNC_AUTO_COMMITTED: (branch: string) =>
5
+ `已自动保存 ${branch} 分支的未提交变更`,
6
+ /** sync 开始合并 */
7
+ SYNC_MERGING: (targetBranch: string, mainBranch: string) =>
8
+ `正在将 ${mainBranch} 合并到 ${targetBranch} ...`,
9
+ /** sync 成功 */
10
+ SYNC_SUCCESS: (targetBranch: string, mainBranch: string) =>
11
+ `✓ 已将 ${mainBranch} 的最新代码同步到 ${targetBranch}`,
12
+ /** sync 冲突 */
13
+ SYNC_CONFLICT: (worktreePath: string) =>
14
+ `合并存在冲突,请进入目标 worktree 手动解决:\n cd ${worktreePath}\n 解决冲突后执行 git add . && git merge --continue\n clawt validate -b <branch> 验证变更`,
15
+ /** sync 无可用 worktree */
16
+ SYNC_NO_WORKTREES: '当前项目没有可用的 worktree,请先通过 clawt run 或 clawt create 创建',
17
+ /** sync 模糊匹配无结果,列出可用分支 */
18
+ SYNC_NO_MATCH: (name: string, branches: string[]) =>
19
+ `未找到与 "${name}" 匹配的分支\n 可用分支:\n${branches.map((b) => ` - ${b}`).join('\n')}`,
20
+ /** sync 交互选择提示 */
21
+ SYNC_SELECT_BRANCH: '请选择要同步的分支',
22
+ /** sync 模糊匹配到多个结果提示 */
23
+ SYNC_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择:`,
24
+ } as const;
@@ -0,0 +1,25 @@
1
+ /** validate 命令专属提示消息 */
2
+ export const VALIDATE_MESSAGES = {
3
+ /** validate 成功 */
4
+ VALIDATE_SUCCESS: (branch: string) =>
5
+ `✓ 已将分支 ${branch} 的变更应用到主 worktree\n 可以开始验证了`,
6
+ /** 增量 validate 成功提示 */
7
+ INCREMENTAL_VALIDATE_SUCCESS: (branch: string) =>
8
+ `✓ 已将分支 ${branch} 的最新变更应用到主 worktree(增量模式)\n 暂存区 = 上次快照,工作目录 = 最新变更`,
9
+ /** 增量 validate 降级为全量模式提示 */
10
+ INCREMENTAL_VALIDATE_FALLBACK: '增量对比失败,已降级为全量模式',
11
+ /** validate 状态已清理 */
12
+ VALIDATE_CLEANED: (branch: string) => `✓ 分支 ${branch} 的 validate 状态已清理`,
13
+ /** validate patch apply 失败,提示用户同步主分支 */
14
+ VALIDATE_PATCH_APPLY_FAILED: (branch: string) =>
15
+ `变更迁移失败:目标分支与主分支差异过大\n 请先执行 clawt sync -b ${branch} 同步主分支后重试`,
16
+ /** validate 无可用 worktree */
17
+ VALIDATE_NO_WORKTREES: '当前项目没有可用的 worktree,请先通过 clawt run 或 clawt create 创建',
18
+ /** validate 模糊匹配无结果,列出可用分支 */
19
+ VALIDATE_NO_MATCH: (name: string, branches: string[]) =>
20
+ `未找到与 "${name}" 匹配的分支\n 可用分支:\n${branches.map((b) => ` - ${b}`).join('\n')}`,
21
+ /** validate 交互选择提示 */
22
+ VALIDATE_SELECT_BRANCH: '请选择要验证的分支',
23
+ /** validate 模糊匹配到多个结果提示 */
24
+ VALIDATE_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择:`,
25
+ } as const;
@@ -54,5 +54,5 @@ export { ensureDir, removeEmptyDir } from './fs.js';
54
54
  export { multilineInput } from './prompt.js';
55
55
  export { launchInteractiveClaude } from './claude.js';
56
56
  export { getSnapshotPath, hasSnapshot, readSnapshotTreeHash, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots, getProjectSnapshotBranches } from './validate-snapshot.js';
57
- export { findExactMatch, findFuzzyMatches, promptSelectBranch, resolveTargetWorktree } from './worktree-matcher.js';
58
- export type { WorktreeResolveMessages } from './worktree-matcher.js';
57
+ export { findExactMatch, findFuzzyMatches, promptSelectBranch, promptMultiSelectBranches, resolveTargetWorktree, resolveTargetWorktrees } from './worktree-matcher.js';
58
+ export type { WorktreeResolveMessages, WorktreeMultiResolveMessages } from './worktree-matcher.js';
@@ -38,6 +38,21 @@ export function findFuzzyMatches(worktrees: WorktreeInfo[], keyword: string): Wo
38
38
  return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
39
39
  }
40
40
 
41
+ /**
42
+ * 多选场景下的分支解析消息文案配置
43
+ * 与 WorktreeResolveMessages 类似,但用于需要多选的命令(如 remove)
44
+ */
45
+ export interface WorktreeMultiResolveMessages {
46
+ /** 无可用 worktree 时的错误消息 */
47
+ noWorktrees: string;
48
+ /** 未传分支名时的多选交互提示 */
49
+ selectBranch: string;
50
+ /** 模糊匹配到多个结果时的多选交互提示 */
51
+ multipleMatches: (keyword: string) => string;
52
+ /** 无匹配结果时的错误消息 */
53
+ noMatch: (keyword: string, branches: string[]) => string;
54
+ }
55
+
41
56
  /**
42
57
  * 通过交互式列表让用户从 worktree 列表中选择一个分支
43
58
  * @param {WorktreeInfo[]} worktrees - 可供选择的 worktree 列表
@@ -57,6 +72,83 @@ export async function promptSelectBranch(worktrees: WorktreeInfo[], message: str
57
72
  return worktrees.find((wt) => wt.branch === selectedBranch)!;
58
73
  }
59
74
 
75
+ /**
76
+ * 通过交互式多选列表让用户从 worktree 列表中选择多个分支
77
+ * 用户可通过空格键选择/取消,回车键确认
78
+ * @param {WorktreeInfo[]} worktrees - 可供选择的 worktree 列表
79
+ * @param {string} message - 选择提示信息
80
+ * @returns {Promise<WorktreeInfo[]>} 用户选择的 worktree 列表
81
+ */
82
+ export async function promptMultiSelectBranches(worktrees: WorktreeInfo[], message: string): Promise<WorktreeInfo[]> {
83
+ // @ts-expect-error enquirer 类型声明未导出 MultiSelect 类,但运行时存在
84
+ const selectedBranches: string[] = await new Enquirer.MultiSelect({
85
+ message,
86
+ choices: worktrees.map((wt) => ({
87
+ name: wt.branch,
88
+ message: wt.branch,
89
+ })),
90
+ // 使用空心圆/实心圆作为选中指示符
91
+ symbols: {
92
+ indicator: { on: '●', off: '○' },
93
+ },
94
+ }).run();
95
+
96
+ return worktrees.filter((wt) => selectedBranches.includes(wt.branch));
97
+ }
98
+
99
+ /**
100
+ * 根据用户输入解析目标 worktree(多选版本)
101
+ * 匹配策略:精确匹配 → 模糊匹配(唯一直接使用,多个交互多选) → 无匹配报错
102
+ * 不传分支名时列出所有可用分支供多选
103
+ * @param {WorktreeInfo[]} worktrees - 可用的 worktree 列表
104
+ * @param {WorktreeMultiResolveMessages} messages - 命令专属的消息文案
105
+ * @param {string} [branchName] - 用户输入的分支名(可选)
106
+ * @returns {Promise<WorktreeInfo[]>} 解析后的目标 worktree 列表
107
+ * @throws {ClawtError} 无可用 worktree 或无匹配结果时抛出
108
+ */
109
+ export async function resolveTargetWorktrees(
110
+ worktrees: WorktreeInfo[],
111
+ messages: WorktreeMultiResolveMessages,
112
+ branchName?: string,
113
+ ): Promise<WorktreeInfo[]> {
114
+ // 无可用 worktree,直接报错
115
+ if (worktrees.length === 0) {
116
+ throw new ClawtError(messages.noWorktrees);
117
+ }
118
+
119
+ // 未传 -b 参数:列出所有分支供多选
120
+ if (!branchName) {
121
+ // 只有一个 worktree 时直接使用,无需选择
122
+ if (worktrees.length === 1) {
123
+ return [worktrees[0]];
124
+ }
125
+ return promptMultiSelectBranches(worktrees, messages.selectBranch);
126
+ }
127
+
128
+ // 1. 精确匹配优先
129
+ const exactMatch = findExactMatch(worktrees, branchName);
130
+ if (exactMatch) {
131
+ return [exactMatch];
132
+ }
133
+
134
+ // 2. 模糊匹配
135
+ const fuzzyMatches = findFuzzyMatches(worktrees, branchName);
136
+
137
+ // 2a. 唯一匹配,直接使用
138
+ if (fuzzyMatches.length === 1) {
139
+ return [fuzzyMatches[0]];
140
+ }
141
+
142
+ // 2b. 多个匹配,交互多选
143
+ if (fuzzyMatches.length > 1) {
144
+ return promptMultiSelectBranches(fuzzyMatches, messages.multipleMatches(branchName));
145
+ }
146
+
147
+ // 3. 无匹配,抛出错误并列出所有可用分支
148
+ const allBranches = worktrees.map((wt) => wt.branch);
149
+ throw new ClawtError(messages.noMatch(branchName, allBranches));
150
+ }
151
+
60
152
  /**
61
153
  * 根据用户输入解析目标 worktree
62
154
  * 匹配策略:精确匹配 → 模糊匹配(唯一直接使用,多个交互选择) → 无匹配报错
@@ -0,0 +1,110 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { Command } from 'commander';
3
+
4
+ // mock 依赖模块
5
+ vi.mock('../../../src/logger/index.js', () => ({
6
+ logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
7
+ }));
8
+
9
+ vi.mock('../../../src/utils/index.js', () => ({
10
+ loadConfig: vi.fn(),
11
+ writeDefaultConfig: vi.fn(),
12
+ printInfo: vi.fn(),
13
+ printSuccess: vi.fn(),
14
+ printSeparator: vi.fn(),
15
+ confirmDestructiveAction: vi.fn(),
16
+ }));
17
+
18
+ vi.mock('../../../src/constants/index.js', () => ({
19
+ CONFIG_PATH: '/mock/.clawt/config.json',
20
+ DEFAULT_CONFIG: {
21
+ claudeCodeCommand: 'claude',
22
+ autoDeleteBranch: false,
23
+ autoPullPush: false,
24
+ confirmDestructiveOps: true,
25
+ },
26
+ CONFIG_DESCRIPTIONS: {
27
+ claudeCodeCommand: 'Claude Code CLI 命令',
28
+ autoDeleteBranch: '自动删除分支',
29
+ autoPullPush: '自动 pull/push',
30
+ confirmDestructiveOps: '破坏性操作确认',
31
+ },
32
+ MESSAGES: {
33
+ CONFIG_RESET_SUCCESS: '配置已恢复为默认值',
34
+ DESTRUCTIVE_OP_CANCELLED: '已取消操作',
35
+ },
36
+ }));
37
+
38
+ import { registerConfigCommand } from '../../../src/commands/config.js';
39
+ import { loadConfig, writeDefaultConfig, printInfo, printSuccess, confirmDestructiveAction } from '../../../src/utils/index.js';
40
+
41
+ const mockedLoadConfig = vi.mocked(loadConfig);
42
+ const mockedWriteDefaultConfig = vi.mocked(writeDefaultConfig);
43
+ const mockedPrintInfo = vi.mocked(printInfo);
44
+ const mockedPrintSuccess = vi.mocked(printSuccess);
45
+ const mockedConfirmDestructiveAction = vi.mocked(confirmDestructiveAction);
46
+
47
+ beforeEach(() => {
48
+ mockedLoadConfig.mockReset();
49
+ mockedWriteDefaultConfig.mockReset();
50
+ mockedPrintInfo.mockReset();
51
+ mockedPrintSuccess.mockReset();
52
+ mockedConfirmDestructiveAction.mockReset();
53
+ });
54
+
55
+ describe('registerConfigCommand', () => {
56
+ it('注册 config 命令和 config reset 子命令', () => {
57
+ const program = new Command();
58
+ registerConfigCommand(program);
59
+ const configCmd = program.commands.find((c) => c.name() === 'config');
60
+ expect(configCmd).toBeDefined();
61
+ const resetCmd = configCmd!.commands.find((c) => c.name() === 'reset');
62
+ expect(resetCmd).toBeDefined();
63
+ });
64
+ });
65
+
66
+ describe('handleConfig(通过 action 间接测试)', () => {
67
+ it('展示配置列表', () => {
68
+ mockedLoadConfig.mockReturnValue({
69
+ claudeCodeCommand: 'claude',
70
+ autoDeleteBranch: false,
71
+ autoPullPush: false,
72
+ confirmDestructiveOps: true,
73
+ });
74
+
75
+ const program = new Command();
76
+ program.exitOverride();
77
+ registerConfigCommand(program);
78
+ program.parse(['config'], { from: 'user' });
79
+
80
+ expect(mockedLoadConfig).toHaveBeenCalled();
81
+ // 应输出配置信息
82
+ expect(mockedPrintInfo).toHaveBeenCalled();
83
+ });
84
+ });
85
+
86
+ describe('handleConfigReset(通过 action 间接测试)', () => {
87
+ it('用户确认后恢复默认配置', async () => {
88
+ mockedConfirmDestructiveAction.mockResolvedValue(true);
89
+
90
+ const program = new Command();
91
+ program.exitOverride();
92
+ registerConfigCommand(program);
93
+ await program.parseAsync(['config', 'reset'], { from: 'user' });
94
+
95
+ expect(mockedWriteDefaultConfig).toHaveBeenCalled();
96
+ expect(mockedPrintSuccess).toHaveBeenCalled();
97
+ });
98
+
99
+ it('用户取消操作时不写入', async () => {
100
+ mockedConfirmDestructiveAction.mockResolvedValue(false);
101
+
102
+ const program = new Command();
103
+ program.exitOverride();
104
+ registerConfigCommand(program);
105
+ await program.parseAsync(['config', 'reset'], { from: 'user' });
106
+
107
+ expect(mockedWriteDefaultConfig).not.toHaveBeenCalled();
108
+ expect(mockedPrintInfo).toHaveBeenCalled();
109
+ });
110
+ });