clawt 2.11.0 → 2.12.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.
@@ -8,7 +8,7 @@
8
8
  - run 命令对应 `5.2 批量创建 Worktree + 执行 Claude Code 任务`,流程按步骤编号描述
9
9
  - merge 命令对应 `5.6 合并验证过的分支`,-b 可选,支持模糊匹配(与 resume/validate 共享匹配逻辑),流程按步骤编号描述
10
10
  - config 命令对应 `5.10 查看和管理全局配置`,包含查看配置和 config reset 子命令两部分(使用 `####` 子标题区分)
11
- - resume 命令对应 `5.11 在已有 Worktree 中恢复会话`,支持模糊匹配和交互式分支选择(-b 可选)
11
+ - resume 命令对应 `5.11 在已有 Worktree 中恢复会话`,统一使用多选交互(resolveTargetWorktrees),选 1 个当前终端恢复,选多个在独立终端 Tab 批量恢复(-b 可选)
12
12
  - validate 命令对应 `5.4 在主 Worktree 验证其他分支`,-b 可选,支持模糊匹配(与 resume 共享匹配逻辑)
13
13
  - sync 命令对应 `5.12 将主分支代码同步到目标 Worktree`,-b 可选,支持模糊匹配(与 resume/validate/merge 共享匹配逻辑)
14
14
  - status 命令对应 `5.14 项目全局状态总览`,支持 `--json` 格式输出,展示主 worktree 状态、各 worktree 详细状态、未清理快照
@@ -35,7 +35,13 @@
35
35
  - remove 批量操作时收集错误继续处理,最后汇总报告
36
36
  - 文档中文风格,技术术语保留英文(worktree, merge, branch, SIGINT 等)
37
37
  - cleanupWorktrees 是 merge 和 run 共用的公共清理函数(在 src/utils/worktree.ts)
38
- - `launchInteractiveClaude` 是 run(交互式模式)和 resume 共用的公共函数(在 src/utils/claude.ts
38
+ - `launchInteractiveClaude` 是 run(交互式模式)和 resume(单选模式)共用的公共函数(在 src/utils/claude.ts),启动前自动检测会话历史并追加 `--continue`
39
+ - `launchInteractiveClaudeInNewTerminal` 是 resume 批量模式使用的函数(在 src/utils/claude.ts),通过 AppleScript 在新终端 Tab 中启动
40
+ - `buildClaudeCommand` 构建完整 shell 命令字符串(在 src/utils/claude.ts),被 `launchInteractiveClaudeInNewTerminal` 调用
41
+ - `openCommandInNewTerminalTab` 终端 Tab 管理函数(在 src/utils/terminal.ts),支持 iTerm2 和 Terminal.app
42
+ - `detectTerminalApp` 终端类型检测函数(在 src/utils/terminal.ts),读取 `terminalApp` 配置项
43
+ - `hasClaudeSessionHistory` 检测 `~/.claude/projects/<encoded-path>/` 下是否有 `.jsonl` 文件(在 src/utils/claude.ts)
44
+ - `CLAUDE_PROJECTS_DIR` 常量(`~/.claude/projects/`)定义在 `src/constants/paths.ts`
39
45
  - killAllChildProcesses 是 run 专用的子进程终止函数(在 src/utils/shell.ts)
40
46
  - validate 快照管理函数在 `src/utils/validate-snapshot.ts`,被 validate、merge、remove 和 status 四个命令使用
41
47
  - `confirmDestructiveAction` 在 `src/utils/formatter.ts`,被 reset、validate --clean 和 config reset 使用
@@ -74,14 +80,17 @@ Notes:
74
80
  - resume 和 run(交互式模式)共用 `launchInteractiveClaude()`,该函数从 run.ts 提取到 src/utils/claude.ts
75
81
  - `claudeCodeCommand` 配置项同时影响 run 交互式模式和 resume 命令
76
82
  - reset 命令与 validate --clean 的区别:reset 不删除快照文件,validate --clean 会删除快照
77
- - `resolveTargetWorktree()` 是 resume、validate、merge 和 sync 共用的单选分支匹配函数(在 src/utils/worktree-matcher.ts)
78
- - `resolveTargetWorktrees()` 是多选分支匹配函数(在 src/utils/worktree-matcher.ts),目前被 remove 命令使用
83
+ - `resolveTargetWorktree()` 是 validate、merge 和 sync 共用的单选分支匹配函数(在 src/utils/worktree-matcher.ts)
84
+ - `resolveTargetWorktrees()` 是多选分支匹配函数(在 src/utils/worktree-matcher.ts),被 remove 和 resume 命令使用
79
85
  - `WorktreeResolveMessages` 接口用于单选命令的消息解耦,`WorktreeMultiResolveMessages` 接口用于多选命令的消息解耦
80
86
  - `promptSelectBranch()`(Enquirer.Select)用于单选交互,`promptMultiSelectBranches()`(Enquirer.MultiSelect)用于多选交互
81
- - resume 的消息常量在 `MESSAGES.RESUME_*`,validate 的消息常量在 `MESSAGES.VALIDATE_*`,merge 的消息常量在 `MESSAGES.MERGE_*`,sync 的消息常量在 `MESSAGES.SYNC_*`,status 的消息常量在 `MESSAGES.STATUS_*`,remove 的 fuzzy search 消息在 `MESSAGES.REMOVE_*`
82
- - resumevalidate、merge 和 sync 的 `-b` 参数均为可选,匹配策略一致:精确→模糊(子串,大小写不敏感)→交互单选
87
+ - resume 的消息常量在 `MESSAGES.RESUME_*`(含 RESUME_ALL_CONFIRM / RESUME_ALL_SUCCESS 等批量恢复消息),validate 的消息常量在 `MESSAGES.VALIDATE_*`,merge 的消息常量在 `MESSAGES.MERGE_*`,sync 的消息常量在 `MESSAGES.SYNC_*`,status 的消息常量在 `MESSAGES.STATUS_*`,remove 的 fuzzy search 消息在 `MESSAGES.REMOVE_*`
88
+ - resume 使用多选匹配策略:精确→模糊→交互多选(与 remove 一致),validate、merge 和 sync 使用单选匹配策略:精确→模糊→交互单选
83
89
  - remove 的 `-b` 参数可选,匹配策略:精确→模糊→交互多选;不传 `-b` 时列出所有分支供多选
84
- - validate 的交互式选择和 resume 使用同一个 `promptSelectBranch()`(Enquirer.Select);remove 使用 `promptMultiSelectBranches()`(Enquirer.MultiSelect)
90
+ - validate 的交互式选择使用 `promptSelectBranch()`(Enquirer.Select);resume 和 remove 使用 `promptMultiSelectBranches()`(Enquirer.MultiSelect)
91
+ - `promptMultiSelectBranches` 支持「全选」选项(顶部 [select-all]),通过扩展 MultiSelect 覆写 space() 实现全选 toggle
92
+ - `SELECT_ALL_NAME` 和 `SELECT_ALL_LABEL` 常量定义在 `src/constants/prompt.ts`
93
+ - `VALID_TERMINAL_APPS` 和 `ITERM2_APP_PATH` 常量定义在 `src/constants/terminal.ts`
85
94
 
86
95
  ## validate 快照机制
87
96
 
package/README.md CHANGED
@@ -80,9 +80,15 @@ clawt run -f tasks.md -b feat
80
80
 
81
81
  ```bash
82
82
  clawt resume -b <branch> # 指定分支
83
- clawt resume # 交互式选择
83
+ clawt resume # 交互式多选
84
84
  ```
85
85
 
86
+ 支持多选:选 1 个在当前终端恢复,选多个自动在独立终端 Tab 中批量恢复(仅 macOS)。
87
+
88
+ 如果目标 worktree 存在历史会话,会自动继续上次对话(`--continue`)。
89
+
90
+ > **注意:** 使用 Terminal.app 批量恢复时,需要在「系统设置 → 隐私与安全性 → 辅助功能」中授权终端应用。iTerm2 无需额外授权。终端类型可通过配置项 `terminalApp` 指定。
91
+
86
92
  ### `clawt create` — 仅创建 worktree(不执行任务)
87
93
 
88
94
  ```bash
@@ -157,6 +163,8 @@ clawt config reset # 恢复默认配置
157
163
  | `claudeCodeCommand` | `"claude"` | Claude Code CLI 启动命令 |
158
164
  | `autoPullPush` | `false` | merge 后自动 pull/push |
159
165
  | `confirmDestructiveOps` | `true` | 破坏性操作前确认 |
166
+ | `maxConcurrency` | `0` | run 命令最大并发数,`0` 为不限制 |
167
+ | `terminalApp` | `"auto"` | 批量 resume 使用的终端:`auto` / `iterm2` / `terminal` |
160
168
 
161
169
  ## 全局选项
162
170
 
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ var CONFIG_PATH = join(CLAWT_HOME, "config.json");
14
14
  var LOGS_DIR = join(CLAWT_HOME, "logs");
15
15
  var WORKTREES_DIR = join(CLAWT_HOME, "worktrees");
16
16
  var VALIDATE_SNAPSHOTS_DIR = join(CLAWT_HOME, "validate-snapshots");
17
+ var CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
17
18
 
18
19
  // src/constants/branch.ts
19
20
  var INVALID_BRANCH_CHARS = /[\/\\.\s~:*?[\]^]+/g;
@@ -208,10 +209,18 @@ var RESUME_MESSAGES = {
208
209
  RESUME_NO_MATCH: (name, branches) => `\u672A\u627E\u5230\u4E0E "${name}" \u5339\u914D\u7684\u5206\u652F
209
210
  \u53EF\u7528\u5206\u652F\uFF1A
210
211
  ${branches.map((b) => ` - ${b}`).join("\n")}`,
211
- /** resume 交互选择提示 */
212
- RESUME_SELECT_BRANCH: "\u8BF7\u9009\u62E9\u8981\u6062\u590D\u7684\u5206\u652F",
213
- /** resume 模糊匹配到多个结果提示 */
214
- RESUME_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`
212
+ /** resume 多选交互提示 */
213
+ RESUME_SELECT_BRANCH: "\u8BF7\u9009\u62E9\u8981\u6062\u590D\u7684\u5206\u652F\uFF08\u7A7A\u683C\u9009\u62E9\uFF0C\u56DE\u8F66\u786E\u8BA4\uFF09",
214
+ /** resume 模糊匹配到多个结果的多选提示 */
215
+ RESUME_MULTIPLE_MATCHES: (keyword) => `"${keyword}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\u8981\u6062\u590D\u7684\uFF1A`,
216
+ /** 批量 resume 确认提示 */
217
+ RESUME_ALL_CONFIRM: (count) => `\u5373\u5C06\u5728 ${count} \u4E2A\u72EC\u7ACB\u7EC8\u7AEF Tab \u4E2D\u6062\u590D Claude Code \u4F1A\u8BDD\uFF0C\u662F\u5426\u7EE7\u7EED\uFF1F`,
218
+ /** 批量 resume 完成提示 */
219
+ RESUME_ALL_SUCCESS: (count) => `\u5DF2\u5728 ${count} \u4E2A\u7EC8\u7AEF Tab \u4E2D\u542F\u52A8 Claude Code \u4F1A\u8BDD`,
220
+ /** 批量 resume 非 macOS 平台提示 */
221
+ RESUME_ALL_PLATFORM_UNSUPPORTED: "\u6279\u91CF resume \u76EE\u524D\u4EC5\u652F\u6301 macOS \u5E73\u53F0\uFF08\u901A\u8FC7 AppleScript \u6253\u5F00\u7EC8\u7AEF Tab\uFF09",
222
+ /** 批量 resume 无匹配分支提示 */
223
+ RESUME_ALL_NO_MATCH: (keyword) => `\u672A\u627E\u5230\u4E0E "${keyword}" \u5339\u914D\u7684\u5206\u652F`
215
224
  };
216
225
 
217
226
  // src/constants/messages/remove.ts
@@ -296,6 +305,10 @@ var EXIT_CODES = {
296
305
  ARGUMENT_ERROR: 2
297
306
  };
298
307
 
308
+ // src/constants/terminal.ts
309
+ var VALID_TERMINAL_APPS = ["auto", "iterm2", "terminal"];
310
+ var ITERM2_APP_PATH = "/Applications/iTerm.app";
311
+
299
312
  // src/constants/config.ts
300
313
  var APPEND_SYSTEM_PROMPT = "After the code execution is completed, it is prohibited to build the project for verification.";
301
314
  var CONFIG_DEFINITIONS = {
@@ -318,6 +331,10 @@ var CONFIG_DEFINITIONS = {
318
331
  maxConcurrency: {
319
332
  defaultValue: 0,
320
333
  description: "run \u547D\u4EE4\u9ED8\u8BA4\u6700\u5927\u5E76\u53D1\u6570\uFF0C0 \u8868\u793A\u4E0D\u9650\u5236"
334
+ },
335
+ terminalApp: {
336
+ defaultValue: "auto",
337
+ description: "\u6279\u91CF resume \u4F7F\u7528\u7684\u7EC8\u7AEF\u5E94\u7528\uFF1Aauto\uFF08\u81EA\u52A8\u68C0\u6D4B\uFF09\u3001iterm2\u3001terminal\uFF08macOS\uFF09"
321
338
  }
322
339
  };
323
340
  function deriveDefaultConfig(definitions) {
@@ -367,6 +384,10 @@ var TASK_STATUS_LABELS = {
367
384
  FAILED: "\u5931\u8D25"
368
385
  };
369
386
 
387
+ // src/constants/prompt.ts
388
+ var SELECT_ALL_NAME = "__select_all__";
389
+ var SELECT_ALL_LABEL = "[select-all]";
390
+
370
391
  // src/errors/index.ts
371
392
  var ClawtError = class extends Error {
372
393
  /** 退出码 */
@@ -904,7 +925,94 @@ import Enquirer from "enquirer";
904
925
 
905
926
  // src/utils/claude.ts
906
927
  import { spawnSync } from "child_process";
907
- function launchInteractiveClaude(worktree) {
928
+ import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
929
+ import { join as join3 } from "path";
930
+
931
+ // src/utils/terminal.ts
932
+ import { execFileSync as execFileSync2 } from "child_process";
933
+ import { existsSync as existsSync5 } from "fs";
934
+ function isITerm2Installed() {
935
+ return existsSync5(ITERM2_APP_PATH);
936
+ }
937
+ function detectTerminalApp() {
938
+ const configured = getConfigValue("terminalApp");
939
+ if (configured === "iterm2" || configured === "terminal") {
940
+ return configured;
941
+ }
942
+ if (!VALID_TERMINAL_APPS.includes(configured)) {
943
+ logger.warn(`terminalApp \u914D\u7F6E\u503C "${configured}" \u65E0\u6548\uFF0C\u6709\u6548\u503C: ${VALID_TERMINAL_APPS.join(", ")}\uFF0C\u5C06\u4F7F\u7528\u81EA\u52A8\u68C0\u6D4B`);
944
+ }
945
+ if (isITerm2Installed()) {
946
+ return "iterm2";
947
+ }
948
+ return "terminal";
949
+ }
950
+ function escapeAppleScriptString(str) {
951
+ return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
952
+ }
953
+ function buildTerminalAppleScript(command, title) {
954
+ const escapedCommand = escapeAppleScriptString(command);
955
+ const escapedTitle = escapeAppleScriptString(title);
956
+ return `
957
+ tell application "Terminal"
958
+ activate
959
+ tell application "System Events" to tell process "Terminal" to keystroke "t" using command down
960
+ delay 0.3
961
+ do script "${escapedCommand}" in front window's selected tab
962
+ set custom title of front window's selected tab to "${escapedTitle}"
963
+ end tell
964
+ `.trim();
965
+ }
966
+ function buildITermAppleScript(command, title) {
967
+ const escapedCommand = escapeAppleScriptString(command);
968
+ const escapedTitle = escapeAppleScriptString(title);
969
+ return `
970
+ tell application "iTerm"
971
+ activate
972
+ tell current window
973
+ create tab with default profile
974
+ tell current session
975
+ set name to "${escapedTitle}"
976
+ write text "${escapedCommand}"
977
+ end tell
978
+ end tell
979
+ end tell
980
+ `.trim();
981
+ }
982
+ function openCommandInNewTerminalTab(command, tabTitle) {
983
+ if (process.platform !== "darwin") {
984
+ throw new ClawtError("\u6279\u91CF resume \u76EE\u524D\u4EC5\u652F\u6301 macOS \u5E73\u53F0\uFF08\u901A\u8FC7 AppleScript \u6253\u5F00\u7EC8\u7AEF Tab\uFF09");
985
+ }
986
+ const terminalApp = detectTerminalApp();
987
+ const script = terminalApp === "iterm2" ? buildITermAppleScript(command, tabTitle) : buildTerminalAppleScript(command, tabTitle);
988
+ logger.debug(`\u6253\u5F00\u7EC8\u7AEF Tab [${terminalApp}]: ${tabTitle}`);
989
+ logger.debug(`\u6267\u884C\u547D\u4EE4: ${command}`);
990
+ try {
991
+ execFileSync2("osascript", ["-e", script], {
992
+ encoding: "utf-8",
993
+ stdio: ["pipe", "pipe", "pipe"]
994
+ });
995
+ } catch (error) {
996
+ const message = error instanceof Error ? error.message : String(error);
997
+ const accessibilityHint = terminalApp === "terminal" ? "\n\u63D0\u793A\uFF1ATerminal.app \u9700\u8981\u8F85\u52A9\u529F\u80FD\u6743\u9650\uFF0C\u8BF7\u5728\u300C\u7CFB\u7EDF\u8BBE\u7F6E \u2192 \u9690\u79C1\u4E0E\u5B89\u5168\u6027 \u2192 \u8F85\u52A9\u529F\u80FD\u300D\u4E2D\u6388\u6743\u7EC8\u7AEF\u5E94\u7528" : "";
998
+ throw new ClawtError(`\u6253\u5F00\u7EC8\u7AEF Tab \u5931\u8D25: ${message}${accessibilityHint}`);
999
+ }
1000
+ }
1001
+
1002
+ // src/utils/claude.ts
1003
+ function encodeClaudeProjectPath(absolutePath) {
1004
+ return absolutePath.replace(/[^a-zA-Z0-9]/g, "-");
1005
+ }
1006
+ function hasClaudeSessionHistory(worktreePath) {
1007
+ const encodedName = encodeClaudeProjectPath(worktreePath);
1008
+ const projectDir = join3(CLAUDE_PROJECTS_DIR, encodedName);
1009
+ if (!existsSync6(projectDir)) {
1010
+ return false;
1011
+ }
1012
+ const entries = readdirSync3(projectDir);
1013
+ return entries.some((entry) => entry.endsWith(".jsonl"));
1014
+ }
1015
+ function launchInteractiveClaude(worktree, options = {}) {
908
1016
  const commandStr = getConfigValue("claudeCodeCommand");
909
1017
  const parts = commandStr.split(/\s+/).filter(Boolean);
910
1018
  const cmd = parts[0];
@@ -913,10 +1021,17 @@ function launchInteractiveClaude(worktree) {
913
1021
  "--append-system-prompt",
914
1022
  APPEND_SYSTEM_PROMPT
915
1023
  ];
1024
+ const hasPreviousSession = options.autoContinue === true && hasClaudeSessionHistory(worktree.path);
1025
+ if (hasPreviousSession) {
1026
+ args.push("--continue");
1027
+ }
916
1028
  printInfo(`\u6B63\u5728 worktree \u4E2D\u542F\u52A8 Claude Code \u4EA4\u4E92\u5F0F\u754C\u9762...`);
917
1029
  printInfo(` \u5206\u652F: ${worktree.branch}`);
918
1030
  printInfo(` \u8DEF\u5F84: ${worktree.path}`);
919
1031
  printInfo(` \u6307\u4EE4: ${commandStr}`);
1032
+ if (options.autoContinue) {
1033
+ printInfo(` \u6A21\u5F0F: ${hasPreviousSession ? "\u7EE7\u7EED\u4E0A\u6B21\u5BF9\u8BDD" : "\u65B0\u5BF9\u8BDD"}`);
1034
+ }
920
1035
  printInfo("");
921
1036
  const result = spawnSync(cmd, args, {
922
1037
  cwd: worktree.path,
@@ -929,31 +1044,47 @@ function launchInteractiveClaude(worktree) {
929
1044
  printWarning(`Claude Code \u9000\u51FA\u7801: ${result.status}`);
930
1045
  }
931
1046
  }
1047
+ function escapeShellSingleQuote(str) {
1048
+ return str.replace(/'/g, "'\\''");
1049
+ }
1050
+ function buildClaudeCommand(worktree, hasPreviousSession) {
1051
+ const commandStr = getConfigValue("claudeCodeCommand");
1052
+ const escapedPath = escapeShellSingleQuote(worktree.path);
1053
+ const escapedPrompt = escapeShellSingleQuote(APPEND_SYSTEM_PROMPT);
1054
+ const continueFlag = hasPreviousSession ? " --continue" : "";
1055
+ return `cd '${escapedPath}' && ${commandStr} --append-system-prompt '${escapedPrompt}'${continueFlag}`;
1056
+ }
1057
+ function launchInteractiveClaudeInNewTerminal(worktree, hasPreviousSession) {
1058
+ const command = buildClaudeCommand(worktree, hasPreviousSession);
1059
+ const modeLabel = hasPreviousSession ? "\u7EE7\u7EED" : "\u65B0\u5BF9\u8BDD";
1060
+ const tabTitle = `clawt: ${worktree.branch}`;
1061
+ openCommandInNewTerminalTab(command, tabTitle);
1062
+ }
932
1063
 
933
1064
  // src/utils/validate-snapshot.ts
934
- import { join as join3 } from "path";
935
- import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync3, rmdirSync as rmdirSync2 } from "fs";
1065
+ import { join as join4 } from "path";
1066
+ import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync4, rmdirSync as rmdirSync2 } from "fs";
936
1067
  function getSnapshotPath(projectName, branchName) {
937
- return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
1068
+ return join4(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
938
1069
  }
939
1070
  function getSnapshotHeadPath(projectName, branchName) {
940
- return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
1071
+ return join4(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
941
1072
  }
942
1073
  function hasSnapshot(projectName, branchName) {
943
- return existsSync5(getSnapshotPath(projectName, branchName));
1074
+ return existsSync7(getSnapshotPath(projectName, branchName));
944
1075
  }
945
1076
  function readSnapshot(projectName, branchName) {
946
1077
  const snapshotPath = getSnapshotPath(projectName, branchName);
947
1078
  const headPath = getSnapshotHeadPath(projectName, branchName);
948
1079
  logger.debug(`\u8BFB\u53D6 validate \u5FEB\u7167: ${snapshotPath}`);
949
- const treeHash = existsSync5(snapshotPath) ? readFileSync2(snapshotPath, "utf-8").trim() : "";
950
- const headCommitHash = existsSync5(headPath) ? readFileSync2(headPath, "utf-8").trim() : "";
1080
+ const treeHash = existsSync7(snapshotPath) ? readFileSync2(snapshotPath, "utf-8").trim() : "";
1081
+ const headCommitHash = existsSync7(headPath) ? readFileSync2(headPath, "utf-8").trim() : "";
951
1082
  return { treeHash, headCommitHash };
952
1083
  }
953
1084
  function writeSnapshot(projectName, branchName, treeHash, headCommitHash) {
954
1085
  const snapshotPath = getSnapshotPath(projectName, branchName);
955
1086
  const headPath = getSnapshotHeadPath(projectName, branchName);
956
- const snapshotDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
1087
+ const snapshotDir = join4(VALIDATE_SNAPSHOTS_DIR, projectName);
957
1088
  ensureDir(snapshotDir);
958
1089
  writeFileSync2(snapshotPath, treeHash, "utf-8");
959
1090
  writeFileSync2(headPath, headCommitHash, "utf-8");
@@ -962,31 +1093,31 @@ function writeSnapshot(projectName, branchName, treeHash, headCommitHash) {
962
1093
  function removeSnapshot(projectName, branchName) {
963
1094
  const snapshotPath = getSnapshotPath(projectName, branchName);
964
1095
  const headPath = getSnapshotHeadPath(projectName, branchName);
965
- if (existsSync5(snapshotPath)) {
1096
+ if (existsSync7(snapshotPath)) {
966
1097
  unlinkSync(snapshotPath);
967
1098
  logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${snapshotPath}`);
968
1099
  }
969
- if (existsSync5(headPath)) {
1100
+ if (existsSync7(headPath)) {
970
1101
  unlinkSync(headPath);
971
1102
  logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${headPath}`);
972
1103
  }
973
1104
  }
974
1105
  function getProjectSnapshotBranches(projectName) {
975
- const projectDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
976
- if (!existsSync5(projectDir)) {
1106
+ const projectDir = join4(VALIDATE_SNAPSHOTS_DIR, projectName);
1107
+ if (!existsSync7(projectDir)) {
977
1108
  return [];
978
1109
  }
979
- const files = readdirSync3(projectDir);
1110
+ const files = readdirSync4(projectDir);
980
1111
  return files.filter((f) => f.endsWith(".tree")).map((f) => f.replace(/\.tree$/, ""));
981
1112
  }
982
1113
  function removeProjectSnapshots(projectName) {
983
- const projectDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
984
- if (!existsSync5(projectDir)) {
1114
+ const projectDir = join4(VALIDATE_SNAPSHOTS_DIR, projectName);
1115
+ if (!existsSync7(projectDir)) {
985
1116
  return;
986
1117
  }
987
- const files = readdirSync3(projectDir);
1118
+ const files = readdirSync4(projectDir);
988
1119
  for (const file of files) {
989
- unlinkSync(join3(projectDir, file));
1120
+ unlinkSync(join4(projectDir, file));
990
1121
  }
991
1122
  try {
992
1123
  rmdirSync2(projectDir);
@@ -1015,12 +1146,37 @@ async function promptSelectBranch(worktrees, message) {
1015
1146
  return worktrees.find((wt) => wt.branch === selectedBranch);
1016
1147
  }
1017
1148
  async function promptMultiSelectBranches(worktrees, message) {
1018
- const selectedBranches = await new Enquirer2.MultiSelect({
1149
+ const branchChoices = worktrees.map((wt) => ({
1150
+ name: wt.branch,
1151
+ message: wt.branch
1152
+ }));
1153
+ const choices = [
1154
+ { name: SELECT_ALL_NAME, message: SELECT_ALL_LABEL },
1155
+ ...branchChoices
1156
+ ];
1157
+ const MultiSelect = Enquirer2.MultiSelect;
1158
+ class MultiSelectWithSelectAll extends MultiSelect {
1159
+ space() {
1160
+ if (!this.focused) return;
1161
+ if (this.focused.name === SELECT_ALL_NAME) {
1162
+ const willEnable = !this.focused.enabled;
1163
+ for (const ch of this.choices) {
1164
+ ch.enabled = willEnable;
1165
+ }
1166
+ return this.render();
1167
+ }
1168
+ this.toggle(this.focused);
1169
+ const selectAllChoice = this.choices.find((ch) => ch.name === SELECT_ALL_NAME);
1170
+ const branchItems = this.choices.filter((ch) => ch.name !== SELECT_ALL_NAME);
1171
+ if (selectAllChoice) {
1172
+ selectAllChoice.enabled = branchItems.every((ch) => ch.enabled);
1173
+ }
1174
+ return this.render();
1175
+ }
1176
+ }
1177
+ const selectedBranches = await new MultiSelectWithSelectAll({
1019
1178
  message,
1020
- choices: worktrees.map((wt) => ({
1021
- name: wt.branch,
1022
- message: wt.branch
1023
- })),
1179
+ choices,
1024
1180
  // 使用空心圆/实心圆作为选中指示符
1025
1181
  symbols: {
1026
1182
  indicator: { on: "\u25CF", off: "\u25CB" }
@@ -1283,7 +1439,7 @@ var ProgressRenderer = class {
1283
1439
 
1284
1440
  // src/utils/task-file.ts
1285
1441
  import { resolve } from "path";
1286
- import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
1442
+ import { existsSync as existsSync8, readFileSync as readFileSync3 } from "fs";
1287
1443
  var TASK_BLOCK_REGEX = /<!-- CLAWT-TASKS:START -->([\s\S]*?)<!-- CLAWT-TASKS:END -->/g;
1288
1444
  var BRANCH_LINE_REGEX = /^#\s*branch:\s*(.+)$/;
1289
1445
  function parseTaskFile(content, options) {
@@ -1321,7 +1477,7 @@ function parseTaskFile(content, options) {
1321
1477
  }
1322
1478
  function loadTaskFile(filePath, options) {
1323
1479
  const absolutePath = resolve(filePath);
1324
- if (!existsSync6(absolutePath)) {
1480
+ if (!existsSync8(absolutePath)) {
1325
1481
  throw new ClawtError(MESSAGES.TASK_FILE_NOT_FOUND(absolutePath));
1326
1482
  }
1327
1483
  const content = readFileSync3(absolutePath, "utf-8");
@@ -1761,10 +1917,44 @@ function registerResumeCommand(program2) {
1761
1917
  async function handleResume(options) {
1762
1918
  validateMainWorktree();
1763
1919
  validateClaudeCodeInstalled();
1764
- logger.info(`resume \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch ?? "(\u672A\u6307\u5B9A)"}`);
1920
+ logger.info(`resume \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F\u8FC7\u6EE4: ${options.branch ?? "(\u65E0)"}`);
1765
1921
  const worktrees = getProjectWorktrees();
1766
- const worktree = await resolveTargetWorktree(worktrees, RESUME_RESOLVE_MESSAGES, options.branch);
1767
- launchInteractiveClaude(worktree);
1922
+ const targetWorktrees = await resolveTargetWorktrees(worktrees, RESUME_RESOLVE_MESSAGES, options.branch);
1923
+ if (targetWorktrees.length === 0) {
1924
+ return;
1925
+ }
1926
+ if (targetWorktrees.length === 1) {
1927
+ launchInteractiveClaude(targetWorktrees[0], { autoContinue: true });
1928
+ } else {
1929
+ await handleBatchResume(targetWorktrees);
1930
+ }
1931
+ }
1932
+ function printBatchResumePreview(worktrees, sessionMap) {
1933
+ printInfo("\u5373\u5C06\u6062\u590D\u7684\u5206\u652F\uFF1A");
1934
+ for (const wt of worktrees) {
1935
+ const modeLabel = sessionMap.get(wt.path) ? "\u7EE7\u7EED\u4E0A\u6B21\u5BF9\u8BDD" : "\u65B0\u5BF9\u8BDD";
1936
+ printInfo(` - ${wt.branch} (${modeLabel})`);
1937
+ }
1938
+ printInfo("");
1939
+ }
1940
+ function buildSessionMap(worktrees) {
1941
+ const map = /* @__PURE__ */ new Map();
1942
+ for (const wt of worktrees) {
1943
+ map.set(wt.path, hasClaudeSessionHistory(wt.path));
1944
+ }
1945
+ return map;
1946
+ }
1947
+ async function handleBatchResume(worktrees) {
1948
+ const sessionMap = buildSessionMap(worktrees);
1949
+ printBatchResumePreview(worktrees, sessionMap);
1950
+ const confirmed = await confirmAction(MESSAGES.RESUME_ALL_CONFIRM(worktrees.length));
1951
+ if (!confirmed) {
1952
+ return;
1953
+ }
1954
+ for (const wt of worktrees) {
1955
+ launchInteractiveClaudeInNewTerminal(wt, sessionMap.get(wt.path) ?? false);
1956
+ }
1957
+ printSuccess(MESSAGES.RESUME_ALL_SUCCESS(worktrees.length));
1768
1958
  }
1769
1959
 
1770
1960
  // src/commands/validate.ts
@@ -9,6 +9,7 @@ var CONFIG_PATH = join(CLAWT_HOME, "config.json");
9
9
  var LOGS_DIR = join(CLAWT_HOME, "logs");
10
10
  var WORKTREES_DIR = join(CLAWT_HOME, "worktrees");
11
11
  var VALIDATE_SNAPSHOTS_DIR = join(CLAWT_HOME, "validate-snapshots");
12
+ var CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
12
13
 
13
14
  // src/constants/messages/common.ts
14
15
  var COMMON_MESSAGES = {
@@ -200,10 +201,18 @@ var RESUME_MESSAGES = {
200
201
  RESUME_NO_MATCH: (name, branches) => `\u672A\u627E\u5230\u4E0E "${name}" \u5339\u914D\u7684\u5206\u652F
201
202
  \u53EF\u7528\u5206\u652F\uFF1A
202
203
  ${branches.map((b) => ` - ${b}`).join("\n")}`,
203
- /** resume 交互选择提示 */
204
- RESUME_SELECT_BRANCH: "\u8BF7\u9009\u62E9\u8981\u6062\u590D\u7684\u5206\u652F",
205
- /** resume 模糊匹配到多个结果提示 */
206
- RESUME_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`
204
+ /** resume 多选交互提示 */
205
+ RESUME_SELECT_BRANCH: "\u8BF7\u9009\u62E9\u8981\u6062\u590D\u7684\u5206\u652F\uFF08\u7A7A\u683C\u9009\u62E9\uFF0C\u56DE\u8F66\u786E\u8BA4\uFF09",
206
+ /** resume 模糊匹配到多个结果的多选提示 */
207
+ RESUME_MULTIPLE_MATCHES: (keyword) => `"${keyword}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\u8981\u6062\u590D\u7684\uFF1A`,
208
+ /** 批量 resume 确认提示 */
209
+ RESUME_ALL_CONFIRM: (count) => `\u5373\u5C06\u5728 ${count} \u4E2A\u72EC\u7ACB\u7EC8\u7AEF Tab \u4E2D\u6062\u590D Claude Code \u4F1A\u8BDD\uFF0C\u662F\u5426\u7EE7\u7EED\uFF1F`,
210
+ /** 批量 resume 完成提示 */
211
+ RESUME_ALL_SUCCESS: (count) => `\u5DF2\u5728 ${count} \u4E2A\u7EC8\u7AEF Tab \u4E2D\u542F\u52A8 Claude Code \u4F1A\u8BDD`,
212
+ /** 批量 resume 非 macOS 平台提示 */
213
+ RESUME_ALL_PLATFORM_UNSUPPORTED: "\u6279\u91CF resume \u76EE\u524D\u4EC5\u652F\u6301 macOS \u5E73\u53F0\uFF08\u901A\u8FC7 AppleScript \u6253\u5F00\u7EC8\u7AEF Tab\uFF09",
214
+ /** 批量 resume 无匹配分支提示 */
215
+ RESUME_ALL_NO_MATCH: (keyword) => `\u672A\u627E\u5230\u4E0E "${keyword}" \u5339\u914D\u7684\u5206\u652F`
207
216
  };
208
217
 
209
218
  // src/constants/messages/remove.ts
@@ -299,6 +308,10 @@ var CONFIG_DEFINITIONS = {
299
308
  maxConcurrency: {
300
309
  defaultValue: 0,
301
310
  description: "run \u547D\u4EE4\u9ED8\u8BA4\u6700\u5927\u5E76\u53D1\u6570\uFF0C0 \u8868\u793A\u4E0D\u9650\u5236"
311
+ },
312
+ terminalApp: {
313
+ defaultValue: "auto",
314
+ description: "\u6279\u91CF resume \u4F7F\u7528\u7684\u7EC8\u7AEF\u5E94\u7528\uFF1Aauto\uFF08\u81EA\u52A8\u68C0\u6D4B\uFF09\u3001iterm2\u3001terminal\uFF08macOS\uFF09"
302
315
  }
303
316
  };
304
317
  function deriveDefaultConfig(definitions) {
package/docs/spec.md CHANGED
@@ -175,7 +175,7 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
175
175
  | `clawt list` | 列出当前项目所有 worktree(支持 `--json` 格式输出) | 5.8 |
176
176
  | `clawt config` | 查看全局配置 | 5.10 |
177
177
  | `clawt config reset` | 将配置恢复为默认值 | 5.10 |
178
- | `clawt resume` | 在已有 worktree 中恢复 Claude Code 交互式会话 | 5.11 |
178
+ | `clawt resume` | 在已有 worktree 中恢复 Claude Code 会话(支持多选批量恢复) | 5.11 |
179
179
  | `clawt sync` | 将主分支最新代码同步到目标 worktree | 5.12 |
180
180
  | `clawt reset` | 重置主 worktree 工作区和暂存区 | 5.13 |
181
181
  | `clawt status` | 显示项目全局状态总览(支持 `--json` 格式输出) | 5.14 |
@@ -840,7 +840,9 @@ clawt merge [-m <commitMessage>]
840
840
  "autoDeleteBranch": false,
841
841
  "claudeCodeCommand": "claude",
842
842
  "autoPullPush": false,
843
- "confirmDestructiveOps": true
843
+ "confirmDestructiveOps": true,
844
+ "maxConcurrency": 0,
845
+ "terminalApp": "auto"
844
846
  }
845
847
  ```
846
848
 
@@ -852,6 +854,8 @@ clawt merge [-m <commitMessage>]
852
854
  | `claudeCodeCommand` | `string` | `"claude"` | Claude Code CLI 启动指令,用于 `clawt run` 不传 `--tasks` 时和 `clawt resume` 在 worktree 中打开交互式界面 |
853
855
  | `autoPullPush` | `boolean` | `false` | merge 成功后是否自动执行 git pull 和 git push |
854
856
  | `confirmDestructiveOps` | `boolean` | `true` | 执行破坏性操作(reset、validate --clean、config reset)前是否提示确认 |
857
+ | `maxConcurrency` | `number` | `0` | run 命令默认最大并发数,`0` 表示不限制 |
858
+ | `terminalApp` | `string` | `"auto"` | 批量 resume 使用的终端应用:`auto`(自动检测)、`iterm2`、`terminal`(macOS) |
855
859
 
856
860
  ---
857
861
 
@@ -1054,7 +1058,7 @@ clawt config reset
1054
1058
  # 指定分支名(支持模糊匹配)
1055
1059
  clawt resume -b <branchName>
1056
1060
 
1057
- # 不指定分支名(列出所有分支供选择)
1061
+ # 不指定分支名(列出所有分支供多选)
1058
1062
  clawt resume
1059
1063
  ```
1060
1064
 
@@ -1062,32 +1066,59 @@ clawt resume
1062
1066
 
1063
1067
  | 参数 | 必填 | 说明 |
1064
1068
  | ---- | ---- | ----------------------------------------------------- |
1065
- | `-b` | 否 | 要恢复的分支名(支持模糊匹配,不传则列出所有分支供选择) |
1069
+ | `-b` | 否 | 要恢复的分支名(支持模糊匹配,不传则列出所有分支供多选) |
1066
1070
 
1067
1071
  **使用场景:**
1068
1072
 
1069
- 当用户之前通过 `clawt run` 或 `clawt create` 创建了 worktree 但会话已结束,希望在该 worktree 中重新打开 Claude Code 交互式界面继续工作。
1073
+ 当用户之前通过 `clawt run` 或 `clawt create` 创建了 worktree 但会话已结束,希望在该 worktree 中重新打开 Claude Code 交互式界面继续工作。支持一次选中多个分支,自动在独立终端 Tab 中批量恢复。
1070
1074
 
1071
1075
  **运行流程:**
1072
1076
 
1073
1077
  1. **主 worktree 校验** (2.1)
1074
1078
  2. **Claude Code CLI 校验**:确认 `claude` CLI 可用
1075
- 3. **解析目标 worktree**:根据 `-b` 参数解析目标 worktree,匹配策略如下:
1079
+ 3. **解析目标 worktree(多选模式)**:统一使用 `resolveTargetWorktrees`(多选版本)解析目标 worktree,匹配策略如下:
1076
1080
  - **未传 `-b` 参数**:
1077
1081
  - 获取当前项目所有 worktree
1078
1082
  - 无可用 worktree → 报错退出
1079
1083
  - 仅 1 个 worktree → 直接使用,无需选择
1080
- - 多个 worktree → 通过交互式列表(Enquirer.Select)让用户选择
1084
+ - 多个 worktree → 通过交互式多选列表(Enquirer.MultiSelect)让用户选择(空格选择,回车确认),顶部提供「全选」选项
1081
1085
  - **传了 `-b` 参数**:
1082
1086
  1. **精确匹配优先**:在 worktree 列表中查找分支名完全相同的 worktree,找到则直接使用
1083
1087
  2. **模糊匹配**(子串匹配,大小写不敏感):
1084
1088
  - 唯一匹配 → 直接使用
1085
- - 多个匹配 → 通过交互式列表让用户从匹配结果中选择
1089
+ - 多个匹配 → 通过交互式多选列表让用户从匹配结果中选择
1086
1090
  3. **无匹配** → 报错退出,并列出所有可用分支名
1087
- 4. **启动 Claude Code 交互式界面**:通过 `launchInteractiveClaude()` 在目标 worktree 中启动 Claude Code CLI 交互式界面(使用 `spawnSync` + `inherit stdio`)
1091
+ 4. **根据选中数量自动分发**:
1092
+ - **用户未选择任何分支** → 直接退出
1093
+ - **选中 1 个** → 在当前终端恢复(同原有行为),通过 `launchInteractiveClaude()` 启动(使用 `spawnSync` + `inherit stdio`)
1094
+ - **选中多个** → 进入批量恢复流程(见下文)
1095
+
1096
+ **批量恢复流程:**
1097
+
1098
+ 1. **计算会话状态**:一次性遍历所有选中的 worktree,通过 `hasClaudeSessionHistory()` 检测是否存在历史会话,构建 sessionMap 避免重复 I/O
1099
+ 2. **输出预览**:列出即将恢复的分支及其会话状态("继续上次对话"或"新对话")
1100
+ 3. **用户确认**:提示即将在 N 个独立终端 Tab 中恢复会话,等待用户确认
1101
+ 4. **逐个在新终端 Tab 中启动**:通过 `launchInteractiveClaudeInNewTerminal()` 构建 shell 命令并通过 AppleScript 在新终端 Tab 中执行
1102
+ 5. **输出完成提示**
1103
+
1104
+ **终端 Tab 管理:**
1105
+
1106
+ 批量恢复通过 `openCommandInNewTerminalTab()`(`src/utils/terminal.ts`)在新终端 Tab 中启动 Claude Code。终端类型由配置项 `terminalApp` 控制:
1107
+
1108
+ | 配置值 | 行为 |
1109
+ | ---------- | ------------------------------------------------------------ |
1110
+ | `auto` | 自动检测:优先检测 iTerm2 是否已安装(`/Applications/iTerm.app`),已安装则使用 iTerm2,否则降级到 Terminal.app |
1111
+ | `iterm2` | 强制使用 iTerm2 |
1112
+ | `terminal` | 强制使用 Terminal.app |
1113
+
1114
+ **平台限制:** 批量恢复目前仅支持 macOS 平台(通过 AppleScript 打开终端 Tab)。非 macOS 平台会抛出错误。
1115
+
1116
+ **权限要求:** Terminal.app 通过 System Events 模拟键盘操作(`Cmd+T`)新建 Tab,需要在「系统设置 → 隐私与安全性 → 辅助功能」中授权终端应用。iTerm2 使用原生 AppleScript 接口,无需辅助功能权限。
1088
1117
 
1089
1118
  启动命令通过配置项 `claudeCodeCommand`(默认值 `claude`)指定,与 `clawt run` 不传 `--tasks` 时的交互式界面行为一致。
1090
1119
 
1120
+ **会话自动续接:** 启动前会自动检测该 worktree 是否存在 Claude Code 历史会话(通过检查 `~/.claude/projects/<encoded-path>/` 下是否有 `.jsonl` 文件判断),如果存在则自动追加 `--continue` 参数继续上次对话,否则打开新对话。启动信息中会显示当前模式("继续上次对话"或"新对话")。路径编码规则:将绝对路径中所有非字母数字字符替换为 `-`(与 Claude Code 源码的编码逻辑一致)。
1121
+
1091
1122
  ---
1092
1123
 
1093
1124
  ### 5.12 将主分支代码同步到目标 Worktree
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",