clawt 2.10.1 → 2.11.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.
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;
@@ -71,7 +72,33 @@ var RUN_MESSAGES = {
71
72
  /** 中断后清理完成 */
72
73
  INTERRUPT_CLEANED: (count) => `\u2713 \u5DF2\u6E05\u7406 ${count} \u4E2A worktree \u548C\u5BF9\u5E94\u5206\u652F`,
73
74
  /** 中断后保留 worktree */
74
- INTERRUPT_KEPT: "\u5DF2\u4FDD\u7559 worktree\uFF0C\u53EF\u7A0D\u540E\u4F7F\u7528 clawt remove \u624B\u52A8\u6E05\u7406"
75
+ INTERRUPT_KEPT: "\u5DF2\u4FDD\u7559 worktree\uFF0C\u53EF\u7A0D\u540E\u4F7F\u7528 clawt remove \u624B\u52A8\u6E05\u7406",
76
+ /** 非 TTY 环境降级输出:任务启动 */
77
+ PROGRESS_TASK_STARTED: (index, total, branch, path) => `[${index}/${total}] ${branch} \u542F\u52A8 ${path}`,
78
+ /** 非 TTY 环境降级输出:任务完成 */
79
+ PROGRESS_TASK_DONE: (index, total, branch, duration, cost, path) => `[${index}/${total}] ${branch} \u2713 \u5B8C\u6210 ${duration} ${cost} ${path}`,
80
+ /** 非 TTY 环境降级输出:任务失败 */
81
+ PROGRESS_TASK_FAILED: (index, total, branch, duration, path) => `[${index}/${total}] ${branch} \u2717 \u5931\u8D25 ${duration} ${path}`,
82
+ /** 并发限制提示 */
83
+ CONCURRENCY_INFO: (concurrency, total) => `\u5E76\u53D1\u9650\u5236: ${concurrency}\uFF0C\u5171 ${total} \u4E2A\u4EFB\u52A1`,
84
+ /** 并发数无效提示 */
85
+ CONCURRENCY_INVALID: "\u5E76\u53D1\u6570\u5FC5\u987B\u4E3A\u6B63\u6574\u6570",
86
+ /** 任务文件不存在 */
87
+ TASK_FILE_NOT_FOUND: (path) => `\u4EFB\u52A1\u6587\u4EF6\u4E0D\u5B58\u5728: ${path}`,
88
+ /** 任务文件中没有解析到有效任务 */
89
+ TASK_FILE_EMPTY: "\u4EFB\u52A1\u6587\u4EF6\u4E2D\u6CA1\u6709\u89E3\u6790\u5230\u6709\u6548\u4EFB\u52A1",
90
+ /** 任务文件中某个任务块缺少分支名 */
91
+ TASK_FILE_MISSING_BRANCH: (blockIndex) => `\u4EFB\u52A1\u6587\u4EF6\u7B2C ${blockIndex} \u4E2A\u4EFB\u52A1\u5757\u7F3A\u5C11\u5206\u652F\u540D\uFF08# branch: ...\uFF09`,
92
+ /** 任务文件中某个任务块缺少任务描述 */
93
+ TASK_FILE_MISSING_TASK: (branch) => `\u4EFB\u52A1\u6587\u4EF6\u4E2D\u5206\u652F ${branch} \u7F3A\u5C11\u4EFB\u52A1\u63CF\u8FF0`,
94
+ /** 任务文件中某个任务块缺少任务描述(无分支名时按索引定位) */
95
+ TASK_FILE_MISSING_TASK_BY_INDEX: (blockIndex) => `\u4EFB\u52A1\u6587\u4EF6\u7B2C ${blockIndex} \u4E2A\u4EFB\u52A1\u5757\u7F3A\u5C11\u4EFB\u52A1\u63CF\u8FF0`,
96
+ /** --file 和 --tasks 不能同时使用 */
97
+ FILE_AND_TASKS_CONFLICT: "--file \u548C --tasks \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528",
98
+ /** 任务文件加载成功 */
99
+ TASK_FILE_LOADED: (count, path) => `\u2713 \u4ECE ${path} \u52A0\u8F7D\u4E86 ${count} \u4E2A\u4EFB\u52A1`,
100
+ /** 未指定 -b 或 -f */
101
+ BRANCH_OR_FILE_REQUIRED: "\u8BF7\u6307\u5B9A -b \u5206\u652F\u540D\u6216 -f \u4EFB\u52A1\u6587\u4EF6"
75
102
  };
76
103
 
77
104
  // src/constants/messages/create.ts
@@ -288,6 +315,10 @@ var CONFIG_DEFINITIONS = {
288
315
  confirmDestructiveOps: {
289
316
  defaultValue: true,
290
317
  description: "\u6267\u884C\u7834\u574F\u6027\u64CD\u4F5C\uFF08reset\u3001validate --clean\uFF09\u524D\u662F\u5426\u63D0\u793A\u786E\u8BA4"
318
+ },
319
+ maxConcurrency: {
320
+ defaultValue: 0,
321
+ description: "run \u547D\u4EE4\u9ED8\u8BA4\u6700\u5927\u5E76\u53D1\u6570\uFF0C0 \u8868\u793A\u4E0D\u9650\u5236"
291
322
  }
292
323
  };
293
324
  function deriveDefaultConfig(definitions) {
@@ -311,6 +342,32 @@ var AUTO_SAVE_COMMIT_MESSAGE = "chore: auto-save before sync";
311
342
  // src/constants/logger.ts
312
343
  var DEBUG_TIMESTAMP_FORMAT = "HH:mm:ss.SSS";
313
344
 
345
+ // src/constants/progress.ts
346
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
347
+ var SPINNER_INTERVAL_MS = 100;
348
+ var CURSOR_UP = (n) => `\x1B[${n}A`;
349
+ var CLEAR_LINE = "\x1B[0K";
350
+ var CURSOR_HIDE = "\x1B[?25l";
351
+ var CURSOR_SHOW = "\x1B[?25h";
352
+ var TASK_STATUS_ICONS = {
353
+ /** 排队中 */
354
+ PENDING: "\u25E6",
355
+ /** 完成 */
356
+ DONE: "\u2713",
357
+ /** 失败 */
358
+ FAILED: "\u2717"
359
+ };
360
+ var TASK_STATUS_LABELS = {
361
+ /** 排队中 */
362
+ PENDING: "\u6392\u961F\u4E2D",
363
+ /** 运行中 */
364
+ RUNNING: "\u8FD0\u884C\u4E2D",
365
+ /** 完成 */
366
+ DONE: "\u5B8C\u6210",
367
+ /** 失败 */
368
+ FAILED: "\u5931\u8D25"
369
+ };
370
+
314
371
  // src/errors/index.ts
315
372
  var ClawtError = class extends Error {
316
373
  /** 退出码 */
@@ -616,14 +673,14 @@ function printDoubleSeparator() {
616
673
  console.log(MESSAGES.DOUBLE_SEPARATOR);
617
674
  }
618
675
  function confirmAction(question) {
619
- return new Promise((resolve) => {
676
+ return new Promise((resolve2) => {
620
677
  const rl = createInterface({
621
678
  input: process.stdin,
622
679
  output: process.stdout
623
680
  });
624
681
  rl.question(`${question} (y/N) `, (answer) => {
625
682
  rl.close();
626
- resolve(answer.toLowerCase() === "y");
683
+ resolve2(answer.toLowerCase() === "y");
627
684
  });
628
685
  });
629
686
  }
@@ -650,6 +707,15 @@ function formatWorktreeStatus(status) {
650
707
  }
651
708
  return parts.join(" ");
652
709
  }
710
+ function formatDuration(ms) {
711
+ const totalSeconds = ms / 1e3;
712
+ if (totalSeconds < 60) {
713
+ return `${totalSeconds.toFixed(1)}s`;
714
+ }
715
+ const minutes = Math.floor(totalSeconds / 60);
716
+ const seconds = Math.floor(totalSeconds % 60);
717
+ return `${minutes}m${String(seconds).padStart(2, "0")}s`;
718
+ }
653
719
 
654
720
  // src/utils/branch.ts
655
721
  function sanitizeBranchName(branchName) {
@@ -742,6 +808,19 @@ function createWorktrees(branchName, count) {
742
808
  }
743
809
  return results;
744
810
  }
811
+ function createWorktreesByBranches(branchNames) {
812
+ validateBranchesNotExist(branchNames);
813
+ const projectDir = getProjectWorktreeDir();
814
+ ensureDir(projectDir);
815
+ const results = [];
816
+ for (const name of branchNames) {
817
+ const worktreePath = join2(projectDir, name);
818
+ createWorktree(name, worktreePath);
819
+ results.push({ path: worktreePath, branch: name });
820
+ logger.info(`worktree \u521B\u5EFA\u5B8C\u6210: ${worktreePath} (\u5206\u652F: ${name})`);
821
+ }
822
+ return results;
823
+ }
745
824
  function getProjectWorktrees() {
746
825
  const projectDir = getProjectWorktreeDir();
747
826
  if (!existsSync3(projectDir)) {
@@ -826,7 +905,21 @@ import Enquirer from "enquirer";
826
905
 
827
906
  // src/utils/claude.ts
828
907
  import { spawnSync } from "child_process";
829
- function launchInteractiveClaude(worktree) {
908
+ import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
909
+ import { join as join3 } from "path";
910
+ function encodeClaudeProjectPath(absolutePath) {
911
+ return absolutePath.replace(/[^a-zA-Z0-9]/g, "-");
912
+ }
913
+ function hasClaudeSessionHistory(worktreePath) {
914
+ const encodedName = encodeClaudeProjectPath(worktreePath);
915
+ const projectDir = join3(CLAUDE_PROJECTS_DIR, encodedName);
916
+ if (!existsSync5(projectDir)) {
917
+ return false;
918
+ }
919
+ const entries = readdirSync3(projectDir);
920
+ return entries.some((entry) => entry.endsWith(".jsonl"));
921
+ }
922
+ function launchInteractiveClaude(worktree, options = {}) {
830
923
  const commandStr = getConfigValue("claudeCodeCommand");
831
924
  const parts = commandStr.split(/\s+/).filter(Boolean);
832
925
  const cmd = parts[0];
@@ -835,10 +928,17 @@ function launchInteractiveClaude(worktree) {
835
928
  "--append-system-prompt",
836
929
  APPEND_SYSTEM_PROMPT
837
930
  ];
931
+ const hasPreviousSession = options.autoContinue === true && hasClaudeSessionHistory(worktree.path);
932
+ if (hasPreviousSession) {
933
+ args.push("--continue");
934
+ }
838
935
  printInfo(`\u6B63\u5728 worktree \u4E2D\u542F\u52A8 Claude Code \u4EA4\u4E92\u5F0F\u754C\u9762...`);
839
936
  printInfo(` \u5206\u652F: ${worktree.branch}`);
840
937
  printInfo(` \u8DEF\u5F84: ${worktree.path}`);
841
938
  printInfo(` \u6307\u4EE4: ${commandStr}`);
939
+ if (options.autoContinue) {
940
+ printInfo(` \u6A21\u5F0F: ${hasPreviousSession ? "\u7EE7\u7EED\u4E0A\u6B21\u5BF9\u8BDD" : "\u65B0\u5BF9\u8BDD"}`);
941
+ }
842
942
  printInfo("");
843
943
  const result = spawnSync(cmd, args, {
844
944
  cwd: worktree.path,
@@ -853,29 +953,29 @@ function launchInteractiveClaude(worktree) {
853
953
  }
854
954
 
855
955
  // src/utils/validate-snapshot.ts
856
- import { join as join3 } from "path";
857
- import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync3, rmdirSync as rmdirSync2 } from "fs";
956
+ import { join as join4 } from "path";
957
+ import { existsSync as existsSync6, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync4, rmdirSync as rmdirSync2 } from "fs";
858
958
  function getSnapshotPath(projectName, branchName) {
859
- return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
959
+ return join4(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
860
960
  }
861
961
  function getSnapshotHeadPath(projectName, branchName) {
862
- return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
962
+ return join4(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
863
963
  }
864
964
  function hasSnapshot(projectName, branchName) {
865
- return existsSync5(getSnapshotPath(projectName, branchName));
965
+ return existsSync6(getSnapshotPath(projectName, branchName));
866
966
  }
867
967
  function readSnapshot(projectName, branchName) {
868
968
  const snapshotPath = getSnapshotPath(projectName, branchName);
869
969
  const headPath = getSnapshotHeadPath(projectName, branchName);
870
970
  logger.debug(`\u8BFB\u53D6 validate \u5FEB\u7167: ${snapshotPath}`);
871
- const treeHash = existsSync5(snapshotPath) ? readFileSync2(snapshotPath, "utf-8").trim() : "";
872
- const headCommitHash = existsSync5(headPath) ? readFileSync2(headPath, "utf-8").trim() : "";
971
+ const treeHash = existsSync6(snapshotPath) ? readFileSync2(snapshotPath, "utf-8").trim() : "";
972
+ const headCommitHash = existsSync6(headPath) ? readFileSync2(headPath, "utf-8").trim() : "";
873
973
  return { treeHash, headCommitHash };
874
974
  }
875
975
  function writeSnapshot(projectName, branchName, treeHash, headCommitHash) {
876
976
  const snapshotPath = getSnapshotPath(projectName, branchName);
877
977
  const headPath = getSnapshotHeadPath(projectName, branchName);
878
- const snapshotDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
978
+ const snapshotDir = join4(VALIDATE_SNAPSHOTS_DIR, projectName);
879
979
  ensureDir(snapshotDir);
880
980
  writeFileSync2(snapshotPath, treeHash, "utf-8");
881
981
  writeFileSync2(headPath, headCommitHash, "utf-8");
@@ -884,31 +984,31 @@ function writeSnapshot(projectName, branchName, treeHash, headCommitHash) {
884
984
  function removeSnapshot(projectName, branchName) {
885
985
  const snapshotPath = getSnapshotPath(projectName, branchName);
886
986
  const headPath = getSnapshotHeadPath(projectName, branchName);
887
- if (existsSync5(snapshotPath)) {
987
+ if (existsSync6(snapshotPath)) {
888
988
  unlinkSync(snapshotPath);
889
989
  logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${snapshotPath}`);
890
990
  }
891
- if (existsSync5(headPath)) {
991
+ if (existsSync6(headPath)) {
892
992
  unlinkSync(headPath);
893
993
  logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${headPath}`);
894
994
  }
895
995
  }
896
996
  function getProjectSnapshotBranches(projectName) {
897
- const projectDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
898
- if (!existsSync5(projectDir)) {
997
+ const projectDir = join4(VALIDATE_SNAPSHOTS_DIR, projectName);
998
+ if (!existsSync6(projectDir)) {
899
999
  return [];
900
1000
  }
901
- const files = readdirSync3(projectDir);
1001
+ const files = readdirSync4(projectDir);
902
1002
  return files.filter((f) => f.endsWith(".tree")).map((f) => f.replace(/\.tree$/, ""));
903
1003
  }
904
1004
  function removeProjectSnapshots(projectName) {
905
- const projectDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
906
- if (!existsSync5(projectDir)) {
1005
+ const projectDir = join4(VALIDATE_SNAPSHOTS_DIR, projectName);
1006
+ if (!existsSync6(projectDir)) {
907
1007
  return;
908
1008
  }
909
- const files = readdirSync3(projectDir);
1009
+ const files = readdirSync4(projectDir);
910
1010
  for (const file of files) {
911
- unlinkSync(join3(projectDir, file));
1011
+ unlinkSync(join4(projectDir, file));
912
1012
  }
913
1013
  try {
914
1014
  rmdirSync2(projectDir);
@@ -999,8 +1099,467 @@ async function resolveTargetWorktree(worktrees, messages, branchName) {
999
1099
  throw new ClawtError(messages.noMatch(branchName, allBranches));
1000
1100
  }
1001
1101
 
1002
- // src/commands/list.ts
1102
+ // src/utils/progress-render.ts
1003
1103
  import chalk3 from "chalk";
1104
+ function getMaxBranchWidth(tasks) {
1105
+ return Math.max(...tasks.map((t) => t.branch.length));
1106
+ }
1107
+ function renderTaskLine(task, total, maxBranchWidth, spinnerChar) {
1108
+ const indexStr = `[${task.index}/${total}]`;
1109
+ const branchStr = task.branch.padEnd(maxBranchWidth);
1110
+ switch (task.status) {
1111
+ case "pending": {
1112
+ return `${indexStr} ${branchStr} ${chalk3.gray(TASK_STATUS_ICONS.PENDING)} ${chalk3.gray(TASK_STATUS_LABELS.PENDING)} ${chalk3.dim(task.path)}`;
1113
+ }
1114
+ case "running": {
1115
+ const elapsed = formatDuration(Date.now() - task.startedAt);
1116
+ return `${indexStr} ${branchStr} ${chalk3.cyan(spinnerChar)} ${chalk3.cyan(TASK_STATUS_LABELS.RUNNING)} ${chalk3.gray(elapsed)} ${chalk3.dim(task.path)}`;
1117
+ }
1118
+ case "done": {
1119
+ const duration = task.durationMs != null ? formatDuration(task.durationMs) : "N/A";
1120
+ const cost = task.costUsd != null ? `$${task.costUsd.toFixed(2)}` : "";
1121
+ return `${indexStr} ${branchStr} ${chalk3.green(TASK_STATUS_ICONS.DONE)} ${chalk3.green(TASK_STATUS_LABELS.DONE)} ${chalk3.gray(duration)} ${chalk3.yellow(cost)} ${chalk3.dim(task.path)}`;
1122
+ }
1123
+ case "failed": {
1124
+ const duration = task.durationMs != null ? formatDuration(task.durationMs) : "N/A";
1125
+ return `${indexStr} ${branchStr} ${chalk3.red(TASK_STATUS_ICONS.FAILED)} ${chalk3.red(TASK_STATUS_LABELS.FAILED)} ${chalk3.gray(duration)} ${chalk3.dim(task.path)}`;
1126
+ }
1127
+ }
1128
+ }
1129
+ function renderSummaryLine(tasks, total) {
1130
+ const running = tasks.filter((t) => t.status === "running").length;
1131
+ const done = tasks.filter((t) => t.status === "done").length;
1132
+ const failed = tasks.filter((t) => t.status === "failed").length;
1133
+ const pending = tasks.filter((t) => t.status === "pending").length;
1134
+ const parts = [];
1135
+ if (running > 0) parts.push(chalk3.cyan(`${running}/${total} ${TASK_STATUS_LABELS.RUNNING}`));
1136
+ if (done > 0) parts.push(chalk3.green(`${done}/${total} ${TASK_STATUS_LABELS.DONE}`));
1137
+ if (failed > 0) parts.push(chalk3.red(`${failed}/${total} ${TASK_STATUS_LABELS.FAILED}`));
1138
+ if (pending > 0) parts.push(chalk3.gray(`${pending}/${total} ${TASK_STATUS_LABELS.PENDING}`));
1139
+ return `[${parts.join(", ")}]`;
1140
+ }
1141
+
1142
+ // src/utils/progress.ts
1143
+ var ProgressRenderer = class {
1144
+ /** 所有任务的进度状态 */
1145
+ tasks;
1146
+ /** 总任务数 */
1147
+ total;
1148
+ /** 当前 spinner 帧索引 */
1149
+ frameIndex;
1150
+ /** 定时器引用 */
1151
+ timer;
1152
+ /** 是否为 TTY 环境 */
1153
+ isTTY;
1154
+ /** 已渲染的行数(用于回退光标) */
1155
+ renderedLineCount;
1156
+ /** 是否已停止 */
1157
+ stopped;
1158
+ /** 是否存在排队任务(启用汇总行渲染) */
1159
+ hasPendingTasks;
1160
+ /**
1161
+ * 创建进度面板渲染器
1162
+ * @param {string[]} branches - 分支名列表,顺序对应任务列表
1163
+ * @param {string[]} paths - worktree 路径列表,完成/失败后显示
1164
+ * @param {boolean} [allRunning=true] - 是否将所有任务初始化为 running 状态,false 时初始化为 pending
1165
+ */
1166
+ constructor(branches, paths, allRunning = true) {
1167
+ const now = Date.now();
1168
+ this.total = branches.length;
1169
+ this.frameIndex = 0;
1170
+ this.timer = null;
1171
+ this.isTTY = !!process.stdout.isTTY;
1172
+ this.renderedLineCount = 0;
1173
+ this.stopped = false;
1174
+ this.hasPendingTasks = !allRunning;
1175
+ this.tasks = branches.map((branch, i) => ({
1176
+ index: i + 1,
1177
+ branch,
1178
+ path: paths[i],
1179
+ status: allRunning ? "running" : "pending",
1180
+ startedAt: allRunning ? now : 0,
1181
+ finishedAt: null,
1182
+ lastActiveAt: allRunning ? now : 0,
1183
+ durationMs: null,
1184
+ costUsd: null
1185
+ }));
1186
+ }
1187
+ /**
1188
+ * 启动定时渲染循环
1189
+ * TTY 模式下每 SPINNER_INTERVAL_MS 毫秒刷新一次面板
1190
+ * 非 TTY 模式下输出状态为 running 的任务的启动信息
1191
+ */
1192
+ start() {
1193
+ if (this.stopped) return;
1194
+ if (!this.isTTY) {
1195
+ for (const task of this.tasks) {
1196
+ if (task.status === "running") {
1197
+ console.log(MESSAGES.PROGRESS_TASK_STARTED(task.index, this.total, task.branch, task.path));
1198
+ }
1199
+ }
1200
+ return;
1201
+ }
1202
+ process.stdout.write(CURSOR_HIDE);
1203
+ this.render();
1204
+ this.timer = setInterval(() => {
1205
+ this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
1206
+ this.render();
1207
+ }, SPINNER_INTERVAL_MS);
1208
+ if (this.timer.unref) {
1209
+ this.timer.unref();
1210
+ }
1211
+ }
1212
+ /**
1213
+ * 更新指定任务的最后活动时间戳
1214
+ * 当 child.stderr 有输出时调用,表示任务仍然活跃
1215
+ * @param {number} index - 任务索引(从 0 开始)
1216
+ */
1217
+ updateActivity(index) {
1218
+ this.tasks[index].lastActiveAt = Date.now();
1219
+ }
1220
+ /**
1221
+ * 标记指定任务为运行中状态
1222
+ * 将 pending 任务标记为 running 并设置启动时间戳
1223
+ * @param {number} index - 任务索引(从 0 开始)
1224
+ */
1225
+ markRunning(index) {
1226
+ const task = this.tasks[index];
1227
+ const now = Date.now();
1228
+ task.status = "running";
1229
+ task.startedAt = now;
1230
+ task.lastActiveAt = now;
1231
+ if (!this.isTTY) {
1232
+ console.log(MESSAGES.PROGRESS_TASK_STARTED(task.index, this.total, task.branch, task.path));
1233
+ }
1234
+ }
1235
+ /**
1236
+ * 标记指定任务为完成状态
1237
+ * @param {number} index - 任务索引(从 0 开始)
1238
+ * @param {number} durationMs - 耗时(毫秒)
1239
+ * @param {number} costUsd - 费用(美元)
1240
+ */
1241
+ markDone(index, durationMs, costUsd) {
1242
+ const task = this.tasks[index];
1243
+ task.status = "done";
1244
+ task.finishedAt = Date.now();
1245
+ task.durationMs = durationMs;
1246
+ task.costUsd = costUsd;
1247
+ if (!this.isTTY) {
1248
+ const duration = formatDuration(durationMs);
1249
+ const cost = `$${costUsd.toFixed(2)}`;
1250
+ console.log(MESSAGES.PROGRESS_TASK_DONE(task.index, this.total, task.branch, duration, cost, task.path));
1251
+ }
1252
+ }
1253
+ /**
1254
+ * 标记指定任务为失败状态
1255
+ * @param {number} index - 任务索引(从 0 开始)
1256
+ * @param {number} durationMs - 耗时(毫秒)
1257
+ */
1258
+ markFailed(index, durationMs) {
1259
+ const task = this.tasks[index];
1260
+ task.status = "failed";
1261
+ task.finishedAt = Date.now();
1262
+ task.durationMs = durationMs;
1263
+ if (!this.isTTY) {
1264
+ const duration = formatDuration(durationMs);
1265
+ console.log(MESSAGES.PROGRESS_TASK_FAILED(task.index, this.total, task.branch, duration, task.path));
1266
+ }
1267
+ }
1268
+ /**
1269
+ * 停止渲染循环并恢复光标
1270
+ * 在所有任务完成或 SIGINT 中断时调用
1271
+ */
1272
+ stop() {
1273
+ if (this.stopped) return;
1274
+ this.stopped = true;
1275
+ if (this.timer) {
1276
+ clearInterval(this.timer);
1277
+ this.timer = null;
1278
+ }
1279
+ if (this.isTTY) {
1280
+ this.render();
1281
+ process.stdout.write(CURSOR_SHOW);
1282
+ }
1283
+ }
1284
+ /**
1285
+ * 执行一次完整的面板渲染
1286
+ * 先回退光标到面板起始位置,再逐行输出
1287
+ */
1288
+ render() {
1289
+ const maxBranchWidth = getMaxBranchWidth(this.tasks);
1290
+ const spinnerChar = SPINNER_FRAMES[this.frameIndex];
1291
+ const lines = this.tasks.map((task) => renderTaskLine(task, this.total, maxBranchWidth, spinnerChar));
1292
+ if (this.hasPendingTasks) {
1293
+ lines.push(renderSummaryLine(this.tasks, this.total));
1294
+ }
1295
+ if (this.renderedLineCount > 0) {
1296
+ process.stdout.write(CURSOR_UP(this.renderedLineCount));
1297
+ }
1298
+ for (const line of lines) {
1299
+ process.stdout.write(`${line}${CLEAR_LINE}
1300
+ `);
1301
+ }
1302
+ this.renderedLineCount = lines.length;
1303
+ }
1304
+ };
1305
+
1306
+ // src/utils/task-file.ts
1307
+ import { resolve } from "path";
1308
+ import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
1309
+ var TASK_BLOCK_REGEX = /<!-- CLAWT-TASKS:START -->([\s\S]*?)<!-- CLAWT-TASKS:END -->/g;
1310
+ var BRANCH_LINE_REGEX = /^#\s*branch:\s*(.+)$/;
1311
+ function parseTaskFile(content, options) {
1312
+ const branchRequired = options?.branchRequired ?? true;
1313
+ const entries = [];
1314
+ let match;
1315
+ let blockIndex = 0;
1316
+ TASK_BLOCK_REGEX.lastIndex = 0;
1317
+ while ((match = TASK_BLOCK_REGEX.exec(content)) !== null) {
1318
+ blockIndex++;
1319
+ const blockContent = match[1].trim();
1320
+ const lines = blockContent.split("\n");
1321
+ let branch;
1322
+ const taskLines = [];
1323
+ for (const line of lines) {
1324
+ const branchMatch = line.trim().match(BRANCH_LINE_REGEX);
1325
+ if (branchMatch) {
1326
+ branch = branchMatch[1].trim();
1327
+ } else {
1328
+ taskLines.push(line);
1329
+ }
1330
+ }
1331
+ if (branchRequired && !branch) {
1332
+ throw new ClawtError(MESSAGES.TASK_FILE_MISSING_BRANCH(blockIndex));
1333
+ }
1334
+ const task = taskLines.join("\n").trim();
1335
+ if (!task) {
1336
+ throw new ClawtError(
1337
+ branch ? MESSAGES.TASK_FILE_MISSING_TASK(branch) : MESSAGES.TASK_FILE_MISSING_TASK_BY_INDEX(blockIndex)
1338
+ );
1339
+ }
1340
+ entries.push({ branch, task });
1341
+ }
1342
+ return entries;
1343
+ }
1344
+ function loadTaskFile(filePath, options) {
1345
+ const absolutePath = resolve(filePath);
1346
+ if (!existsSync7(absolutePath)) {
1347
+ throw new ClawtError(MESSAGES.TASK_FILE_NOT_FOUND(absolutePath));
1348
+ }
1349
+ const content = readFileSync3(absolutePath, "utf-8");
1350
+ const entries = parseTaskFile(content, options);
1351
+ if (entries.length === 0) {
1352
+ throw new ClawtError(MESSAGES.TASK_FILE_EMPTY);
1353
+ }
1354
+ return entries;
1355
+ }
1356
+
1357
+ // src/utils/task-executor.ts
1358
+ function executeClaudeTask(worktree, task) {
1359
+ const child = spawnProcess(
1360
+ "claude",
1361
+ ["-p", task, "--output-format", "json", "--permission-mode", "bypassPermissions"],
1362
+ {
1363
+ cwd: worktree.path,
1364
+ // stdin 必须设置为 'ignore',不能用 'pipe'
1365
+ // 原因:claude -p 是非交互模式,不需要 stdin 输入。如果 stdin 为 'pipe',
1366
+ // 父进程会创建一个可写流连接到子进程但从不写入也不关闭,
1367
+ // claude 检测到 stdin 是管道后会尝试读取输入,导致进程永远卡住
1368
+ stdio: ["ignore", "pipe", "pipe"]
1369
+ }
1370
+ );
1371
+ const promise = new Promise((resolve2) => {
1372
+ let stdout = "";
1373
+ let stderr = "";
1374
+ child.stdout?.on("data", (data) => {
1375
+ stdout += data.toString();
1376
+ });
1377
+ child.stderr?.on("data", (data) => {
1378
+ stderr += data.toString();
1379
+ });
1380
+ child.on("close", (code) => {
1381
+ let result = null;
1382
+ let success = code === 0;
1383
+ try {
1384
+ if (stdout.trim()) {
1385
+ result = JSON.parse(stdout.trim());
1386
+ success = !result.is_error;
1387
+ }
1388
+ } catch {
1389
+ logger.warn(`\u89E3\u6790 Claude Code \u8F93\u51FA\u5931\u8D25: ${stdout.substring(0, 200)}`);
1390
+ }
1391
+ resolve2({
1392
+ task,
1393
+ branch: worktree.branch,
1394
+ worktreePath: worktree.path,
1395
+ success,
1396
+ result,
1397
+ error: success ? void 0 : stderr || "\u4EFB\u52A1\u6267\u884C\u5931\u8D25"
1398
+ });
1399
+ });
1400
+ child.on("error", (err) => {
1401
+ resolve2({
1402
+ task,
1403
+ branch: worktree.branch,
1404
+ worktreePath: worktree.path,
1405
+ success: false,
1406
+ result: null,
1407
+ error: err.message
1408
+ });
1409
+ });
1410
+ });
1411
+ return { child, promise };
1412
+ }
1413
+ function printTaskSummary(summary) {
1414
+ printDoubleSeparator();
1415
+ printInfo(`\u5168\u90E8\u4EFB\u52A1\u5DF2\u5B8C\u6210 (${summary.total}/${summary.total})`);
1416
+ printInfo(` \u6210\u529F: ${summary.succeeded}`);
1417
+ printInfo(` \u5931\u8D25: ${summary.failed}`);
1418
+ printInfo(` \u603B\u8017\u65F6: ${(summary.totalDurationMs / 1e3).toFixed(1)}s`);
1419
+ printInfo(` \u603B\u82B1\u8D39: $${summary.totalCostUsd.toFixed(2)}`);
1420
+ printDoubleSeparator();
1421
+ }
1422
+ async function handleInterruptCleanup(worktrees) {
1423
+ const autoDelete = getConfigValue("autoDeleteBranch");
1424
+ if (autoDelete) {
1425
+ cleanupWorktrees(worktrees);
1426
+ printSuccess(MESSAGES.INTERRUPT_AUTO_CLEANED(worktrees.length));
1427
+ return;
1428
+ }
1429
+ const shouldClean = await confirmAction(MESSAGES.INTERRUPT_CONFIRM_CLEANUP);
1430
+ if (shouldClean) {
1431
+ cleanupWorktrees(worktrees);
1432
+ printSuccess(MESSAGES.INTERRUPT_CLEANED(worktrees.length));
1433
+ } else {
1434
+ printInfo(MESSAGES.INTERRUPT_KEPT);
1435
+ }
1436
+ }
1437
+ function updateRendererStatus(renderer, index, result, startTime) {
1438
+ if (result.success) {
1439
+ renderer.markDone(
1440
+ index,
1441
+ result.result?.duration_ms ?? Date.now() - startTime,
1442
+ result.result?.total_cost_usd ?? 0
1443
+ );
1444
+ } else {
1445
+ renderer.markFailed(
1446
+ index,
1447
+ result.result?.duration_ms ?? Date.now() - startTime
1448
+ );
1449
+ }
1450
+ }
1451
+ async function executeWithConcurrency(worktrees, tasks, concurrency, renderer, startTime, isInterrupted, childProcesses) {
1452
+ const total = tasks.length;
1453
+ const results = new Array(total);
1454
+ let nextIndex = 0;
1455
+ let completedCount = 0;
1456
+ return new Promise((resolve2) => {
1457
+ function launchNext() {
1458
+ if (nextIndex >= total || isInterrupted()) return;
1459
+ const index = nextIndex;
1460
+ nextIndex++;
1461
+ const wt = worktrees[index];
1462
+ const task = tasks[index];
1463
+ logger.info(`\u542F\u52A8\u4EFB\u52A1 ${index + 1}: ${task} (worktree: ${wt.path})`);
1464
+ renderer.markRunning(index);
1465
+ const handle = executeClaudeTask(wt, task);
1466
+ childProcesses.push(handle.child);
1467
+ handle.child.stderr?.on("data", () => {
1468
+ renderer.updateActivity(index);
1469
+ });
1470
+ handle.promise.then((result) => {
1471
+ results[index] = result;
1472
+ completedCount++;
1473
+ if (!isInterrupted()) {
1474
+ updateRendererStatus(renderer, index, result, startTime);
1475
+ }
1476
+ launchNext();
1477
+ if (completedCount === total) {
1478
+ resolve2(results);
1479
+ }
1480
+ });
1481
+ }
1482
+ const initialBatch = Math.min(concurrency, total);
1483
+ for (let i = 0; i < initialBatch; i++) {
1484
+ launchNext();
1485
+ }
1486
+ });
1487
+ }
1488
+ async function executeAllParallel(worktrees, tasks, renderer, startTime, isInterrupted, childProcesses) {
1489
+ const handles = worktrees.map((wt, index) => {
1490
+ const task = tasks[index];
1491
+ logger.info(`\u542F\u52A8\u4EFB\u52A1 ${index + 1}: ${task} (worktree: ${wt.path})`);
1492
+ const handle = executeClaudeTask(wt, task);
1493
+ childProcesses.push(handle.child);
1494
+ handle.child.stderr?.on("data", () => {
1495
+ renderer.updateActivity(index);
1496
+ });
1497
+ return handle;
1498
+ });
1499
+ const results = await Promise.all(
1500
+ handles.map(
1501
+ (handle, index) => handle.promise.then((result) => {
1502
+ if (!isInterrupted()) {
1503
+ updateRendererStatus(renderer, index, result, startTime);
1504
+ }
1505
+ return result;
1506
+ })
1507
+ )
1508
+ );
1509
+ return results;
1510
+ }
1511
+ async function executeBatchTasks(worktrees, tasks, concurrency) {
1512
+ const count = tasks.length;
1513
+ if (concurrency > 0) {
1514
+ printInfo(MESSAGES.CONCURRENCY_INFO(concurrency, count));
1515
+ printInfo("");
1516
+ }
1517
+ const startTime = Date.now();
1518
+ const branches = worktrees.map((wt) => wt.branch);
1519
+ const paths = worktrees.map((wt) => wt.path);
1520
+ const allRunning = concurrency === 0;
1521
+ const renderer = new ProgressRenderer(branches, paths, allRunning);
1522
+ renderer.start();
1523
+ let interrupted = false;
1524
+ const isInterrupted = () => interrupted;
1525
+ const childProcesses = [];
1526
+ const sigintHandler = async () => {
1527
+ if (interrupted) return;
1528
+ interrupted = true;
1529
+ renderer.stop();
1530
+ printInfo("");
1531
+ printWarning(MESSAGES.INTERRUPTED);
1532
+ killAllChildProcesses(childProcesses);
1533
+ await Promise.allSettled(childProcesses.map(
1534
+ (cp) => new Promise((resolve2) => {
1535
+ if (cp.exitCode !== null) {
1536
+ resolve2();
1537
+ } else {
1538
+ cp.on("close", () => resolve2());
1539
+ }
1540
+ })
1541
+ ));
1542
+ await handleInterruptCleanup(worktrees);
1543
+ process.exit(1);
1544
+ };
1545
+ process.on("SIGINT", sigintHandler);
1546
+ const results = concurrency > 0 ? await executeWithConcurrency(worktrees, tasks, concurrency, renderer, startTime, isInterrupted, childProcesses) : await executeAllParallel(worktrees, tasks, renderer, startTime, isInterrupted, childProcesses);
1547
+ renderer.stop();
1548
+ process.removeListener("SIGINT", sigintHandler);
1549
+ if (interrupted) return;
1550
+ const totalDurationMs = Date.now() - startTime;
1551
+ const summary = {
1552
+ total: results.length,
1553
+ succeeded: results.filter((r) => r.success).length,
1554
+ failed: results.filter((r) => !r.success).length,
1555
+ totalDurationMs,
1556
+ totalCostUsd: results.reduce((sum, r) => sum + (r.result?.total_cost_usd ?? 0), 0)
1557
+ };
1558
+ printTaskSummary(summary);
1559
+ }
1560
+
1561
+ // src/commands/list.ts
1562
+ import chalk4 from "chalk";
1004
1563
  function registerListCommand(program2) {
1005
1564
  program2.command("list").description("\u5217\u51FA\u5F53\u524D\u9879\u76EE\u6240\u6709 worktree").option("--json", "\u4EE5 JSON \u683C\u5F0F\u8F93\u51FA").action((options) => {
1006
1565
  handleList(options);
@@ -1037,12 +1596,12 @@ function printListAsText(projectName, worktrees) {
1037
1596
  for (const wt of worktrees) {
1038
1597
  const status = getWorktreeStatus(wt);
1039
1598
  const isIdle = status ? isWorktreeIdle(status) : false;
1040
- const pathDisplay = isIdle ? chalk3.hex("#FF8C00")(wt.path) : wt.path;
1599
+ const pathDisplay = isIdle ? chalk4.hex("#FF8C00")(wt.path) : wt.path;
1041
1600
  printInfo(` ${pathDisplay} [${wt.branch}]`);
1042
1601
  if (status) {
1043
1602
  printInfo(` ${formatWorktreeStatus(status)}`);
1044
1603
  } else {
1045
- printInfo(` ${chalk3.yellow(MESSAGES.WORKTREE_STATUS_UNAVAILABLE)}`);
1604
+ printInfo(` ${chalk4.yellow(MESSAGES.WORKTREE_STATUS_UNAVAILABLE)}`);
1046
1605
  }
1047
1606
  printInfo("");
1048
1607
  }
@@ -1145,110 +1704,48 @@ async function handleRemove(options) {
1145
1704
 
1146
1705
  // src/commands/run.ts
1147
1706
  function registerRunCommand(program2) {
1148
- program2.command("run").description("\u6279\u91CF\u521B\u5EFA worktree \u5E76\u542F\u52A8 Claude Code \u6267\u884C\u4EFB\u52A1").requiredOption("-b, --branch <branchName>", "\u5206\u652F\u540D").option("--tasks <task...>", "\u4EFB\u52A1\u5217\u8868\uFF08\u53EF\u591A\u6B21\u6307\u5B9A\uFF09\uFF0C\u4E0D\u4F20\u5219\u5728 worktree \u4E2D\u6253\u5F00 Claude Code \u4EA4\u4E92\u5F0F\u754C\u9762").action(async (options) => {
1707
+ program2.command("run").description("\u6279\u91CF\u521B\u5EFA worktree \u5E76\u542F\u52A8 Claude Code \u6267\u884C\u4EFB\u52A1").option("-b, --branch <branchName>", "\u5206\u652F\u540D").option("--tasks <task...>", "\u4EFB\u52A1\u5217\u8868\uFF08\u53EF\u591A\u6B21\u6307\u5B9A\uFF09\uFF0C\u4E0D\u4F20\u5219\u5728 worktree \u4E2D\u6253\u5F00 Claude Code \u4EA4\u4E92\u5F0F\u754C\u9762").option("-c, --concurrency <n>", "\u6700\u5927\u5E76\u53D1\u6570\uFF0C0 \u8868\u793A\u4E0D\u9650\u5236").option("-f, --file <path>", "\u4ECE\u4EFB\u52A1\u6587\u4EF6\u8BFB\u53D6\u4EFB\u52A1\u5217\u8868\uFF08\u4E0E --tasks \u4E92\u65A5\uFF09").action(async (options) => {
1149
1708
  await handleRun(options);
1150
1709
  });
1151
1710
  }
1152
- function executeClaudeTask(worktree, task) {
1153
- const child = spawnProcess(
1154
- "claude",
1155
- ["-p", task, "--output-format", "json", "--permission-mode", "bypassPermissions"],
1156
- {
1157
- cwd: worktree.path,
1158
- // stdin 必须设置为 'ignore',不能用 'pipe'
1159
- // 原因:claude -p 是非交互模式,不需要 stdin 输入。如果 stdin 为 'pipe',
1160
- // 父进程会创建一个可写流连接到子进程但从不写入也不关闭,
1161
- // claude 检测到 stdin 是管道后会尝试读取输入,导致进程永远卡住
1162
- stdio: ["ignore", "pipe", "pipe"]
1163
- }
1164
- );
1165
- const promise = new Promise((resolve) => {
1166
- let stdout = "";
1167
- let stderr = "";
1168
- child.stdout?.on("data", (data) => {
1169
- stdout += data.toString();
1170
- });
1171
- child.stderr?.on("data", (data) => {
1172
- stderr += data.toString();
1173
- });
1174
- child.on("close", (code) => {
1175
- let result = null;
1176
- let success = code === 0;
1177
- try {
1178
- if (stdout.trim()) {
1179
- result = JSON.parse(stdout.trim());
1180
- success = !result.is_error;
1181
- }
1182
- } catch {
1183
- logger.warn(`\u89E3\u6790 Claude Code \u8F93\u51FA\u5931\u8D25: ${stdout.substring(0, 200)}`);
1184
- }
1185
- resolve({
1186
- task,
1187
- branch: worktree.branch,
1188
- worktreePath: worktree.path,
1189
- success,
1190
- result,
1191
- error: success ? void 0 : stderr || "\u4EFB\u52A1\u6267\u884C\u5931\u8D25"
1192
- });
1193
- });
1194
- child.on("error", (err) => {
1195
- resolve({
1196
- task,
1197
- branch: worktree.branch,
1198
- worktreePath: worktree.path,
1199
- success: false,
1200
- result: null,
1201
- error: err.message
1202
- });
1203
- });
1204
- });
1205
- return { child, promise };
1206
- }
1207
- function printTaskNotification(taskResult) {
1208
- const { success, worktreePath, branch, result } = taskResult;
1209
- const status = success ? "\u5B8C\u6210" : "\u5931\u8D25";
1210
- const icon = success ? "\u2713" : "\u2717";
1211
- const durationStr = result ? `${(result.duration_ms / 1e3).toFixed(1)}s` : "N/A";
1212
- const costStr = result ? `$${result.total_cost_usd.toFixed(2)}` : "N/A";
1213
- const resultStr = success ? "success" : "failed";
1214
- if (success) {
1215
- printSuccess(`${icon} [${status}] worktree: ${worktreePath}`);
1216
- } else {
1217
- printError(`${icon} [${status}] worktree: ${worktreePath}`);
1711
+ function parseConcurrency(optionValue, configValue) {
1712
+ if (optionValue === void 0) {
1713
+ return configValue;
1218
1714
  }
1219
- printInfo(` \u5206\u652F: ${branch}`);
1220
- printInfo(` \u8017\u65F6: ${durationStr}`);
1221
- printInfo(` \u82B1\u8D39: ${costStr}`);
1222
- printInfo(` \u7ED3\u679C: ${resultStr}`);
1223
- printSeparator();
1224
- }
1225
- function printTaskSummary(summary) {
1226
- printDoubleSeparator();
1227
- printInfo(`\u5168\u90E8\u4EFB\u52A1\u5DF2\u5B8C\u6210 (${summary.total}/${summary.total})`);
1228
- printInfo(` \u6210\u529F: ${summary.succeeded}`);
1229
- printInfo(` \u5931\u8D25: ${summary.failed}`);
1230
- printInfo(` \u603B\u8017\u65F6: ${(summary.totalDurationMs / 1e3).toFixed(1)}s`);
1231
- printInfo(` \u603B\u82B1\u8D39: $${summary.totalCostUsd.toFixed(2)}`);
1232
- printDoubleSeparator();
1233
- }
1234
- async function handleInterruptCleanup(worktrees) {
1235
- const autoDelete = getConfigValue("autoDeleteBranch");
1236
- if (autoDelete) {
1237
- cleanupWorktrees(worktrees);
1238
- printSuccess(MESSAGES.INTERRUPT_AUTO_CLEANED(worktrees.length));
1239
- return;
1715
+ const parsed = parseInt(optionValue, 10);
1716
+ if (Number.isNaN(parsed) || parsed < 0) {
1717
+ throw new ClawtError(MESSAGES.CONCURRENCY_INVALID);
1240
1718
  }
1241
- const shouldClean = await confirmAction(MESSAGES.INTERRUPT_CONFIRM_CLEANUP);
1242
- if (shouldClean) {
1243
- cleanupWorktrees(worktrees);
1244
- printSuccess(MESSAGES.INTERRUPT_CLEANED(worktrees.length));
1719
+ return parsed;
1720
+ }
1721
+ async function handleRunFromFile(options) {
1722
+ const branchRequired = !options.branch;
1723
+ const entries = loadTaskFile(options.file, { branchRequired });
1724
+ printSuccess(MESSAGES.TASK_FILE_LOADED(entries.length, options.file));
1725
+ const tasks = entries.map((e) => e.task);
1726
+ let worktrees;
1727
+ if (options.branch) {
1728
+ worktrees = createWorktrees(options.branch, entries.length);
1245
1729
  } else {
1246
- printInfo(MESSAGES.INTERRUPT_KEPT);
1730
+ const branches = entries.map((e) => sanitizeBranchName(e.branch));
1731
+ worktrees = createWorktreesByBranches(branches);
1247
1732
  }
1733
+ const concurrency = parseConcurrency(options.concurrency, getConfigValue("maxConcurrency"));
1734
+ logger.info(`run \u547D\u4EE4\uFF08\u6587\u4EF6\u6A21\u5F0F\uFF09\u6267\u884C\uFF0C\u4EFB\u52A1\u6570: ${entries.length}\uFF0C\u5E76\u53D1\u6570: ${concurrency || "\u4E0D\u9650\u5236"}`);
1735
+ await executeBatchTasks(worktrees, tasks, concurrency);
1248
1736
  }
1249
1737
  async function handleRun(options) {
1250
1738
  validateMainWorktree();
1251
1739
  validateClaudeCodeInstalled();
1740
+ if (options.file && options.tasks) {
1741
+ throw new ClawtError(MESSAGES.FILE_AND_TASKS_CONFLICT);
1742
+ }
1743
+ if (options.file) {
1744
+ return handleRunFromFile(options);
1745
+ }
1746
+ if (!options.branch) {
1747
+ throw new ClawtError(MESSAGES.BRANCH_OR_FILE_REQUIRED);
1748
+ }
1252
1749
  if (!options.tasks || options.tasks.length === 0) {
1253
1750
  const sanitized = sanitizeBranchName(options.branch);
1254
1751
  if (checkBranchExists(sanitized)) {
@@ -1265,52 +1762,10 @@ async function handleRun(options) {
1265
1762
  throw new ClawtError("\u4EFB\u52A1\u5217\u8868\u4E0D\u80FD\u4E3A\u7A7A");
1266
1763
  }
1267
1764
  const count = tasks.length;
1268
- logger.info(`run \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch}\uFF0C\u4EFB\u52A1\u6570: ${count}`);
1765
+ const concurrency = parseConcurrency(options.concurrency, getConfigValue("maxConcurrency"));
1766
+ logger.info(`run \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch}\uFF0C\u4EFB\u52A1\u6570: ${count}\uFF0C\u5E76\u53D1\u6570: ${concurrency || "\u4E0D\u9650\u5236"}`);
1269
1767
  const worktrees = createWorktrees(options.branch, count);
1270
- printSuccess(MESSAGES.WORKTREE_CREATED(worktrees.length));
1271
- for (const wt of worktrees) {
1272
- printInfo(` \u5206\u652F: ${wt.branch} \u8DEF\u5F84: ${wt.path}`);
1273
- }
1274
- printInfo("");
1275
- const startTime = Date.now();
1276
- const handles = worktrees.map((wt, index) => {
1277
- const task = tasks[index];
1278
- logger.info(`\u542F\u52A8\u4EFB\u52A1 ${index + 1}: ${task} (worktree: ${wt.path})`);
1279
- return executeClaudeTask(wt, task);
1280
- });
1281
- const childProcesses = handles.map((h) => h.child);
1282
- let interrupted = false;
1283
- const sigintHandler = async () => {
1284
- if (interrupted) return;
1285
- interrupted = true;
1286
- printInfo("");
1287
- printWarning(MESSAGES.INTERRUPTED);
1288
- killAllChildProcesses(childProcesses);
1289
- await Promise.allSettled(handles.map((h) => h.promise));
1290
- await handleInterruptCleanup(worktrees);
1291
- process.exit(1);
1292
- };
1293
- process.on("SIGINT", sigintHandler);
1294
- const taskPromises = handles.map(
1295
- (handle) => handle.promise.then((result) => {
1296
- if (!interrupted) {
1297
- printTaskNotification(result);
1298
- }
1299
- return result;
1300
- })
1301
- );
1302
- const results = await Promise.all(taskPromises);
1303
- process.removeListener("SIGINT", sigintHandler);
1304
- if (interrupted) return;
1305
- const totalDurationMs = Date.now() - startTime;
1306
- const summary = {
1307
- total: results.length,
1308
- succeeded: results.filter((r) => r.success).length,
1309
- failed: results.filter((r) => !r.success).length,
1310
- totalDurationMs,
1311
- totalCostUsd: results.reduce((sum, r) => sum + (r.result?.total_cost_usd ?? 0), 0)
1312
- };
1313
- printTaskSummary(summary);
1768
+ await executeBatchTasks(worktrees, tasks, concurrency);
1314
1769
  }
1315
1770
 
1316
1771
  // src/commands/resume.ts
@@ -1331,7 +1786,7 @@ async function handleResume(options) {
1331
1786
  logger.info(`resume \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch ?? "(\u672A\u6307\u5B9A)"}`);
1332
1787
  const worktrees = getProjectWorktrees();
1333
1788
  const worktree = await resolveTargetWorktree(worktrees, RESUME_RESOLVE_MESSAGES, options.branch);
1334
- launchInteractiveClaude(worktree);
1789
+ launchInteractiveClaude(worktree, { autoContinue: true });
1335
1790
  }
1336
1791
 
1337
1792
  // src/commands/validate.ts
@@ -1637,7 +2092,7 @@ async function handleMerge(options) {
1637
2092
  }
1638
2093
 
1639
2094
  // src/commands/config.ts
1640
- import chalk4 from "chalk";
2095
+ import chalk5 from "chalk";
1641
2096
  function registerConfigCommand(program2) {
1642
2097
  const configCmd = program2.command("config").description("\u67E5\u770B\u548C\u7BA1\u7406\u5168\u5C40\u914D\u7F6E").action(() => {
1643
2098
  handleConfig();
@@ -1650,7 +2105,7 @@ function handleConfig() {
1650
2105
  const config = loadConfig();
1651
2106
  logger.info("config \u547D\u4EE4\u6267\u884C\uFF0C\u5C55\u793A\u5168\u5C40\u914D\u7F6E");
1652
2107
  printInfo(`
1653
- ${chalk4.dim("\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84:")} ${CONFIG_PATH}
2108
+ ${chalk5.dim("\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84:")} ${CONFIG_PATH}
1654
2109
  `);
1655
2110
  printSeparator();
1656
2111
  const keys = Object.keys(DEFAULT_CONFIG);
@@ -1660,8 +2115,8 @@ ${chalk4.dim("\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84:")} ${CONFIG_PATH}
1660
2115
  const description = CONFIG_DESCRIPTIONS[key];
1661
2116
  const formattedValue = formatConfigValue(value);
1662
2117
  if (i === 0) printInfo("");
1663
- printInfo(` ${chalk4.bold(key)}: ${formattedValue}`);
1664
- printInfo(` ${chalk4.dim(description)}`);
2118
+ printInfo(` ${chalk5.bold(key)}: ${formattedValue}`);
2119
+ printInfo(` ${chalk5.dim(description)}`);
1665
2120
  printInfo("");
1666
2121
  }
1667
2122
  printSeparator();
@@ -1681,9 +2136,9 @@ async function handleConfigReset() {
1681
2136
  }
1682
2137
  function formatConfigValue(value) {
1683
2138
  if (typeof value === "boolean") {
1684
- return value ? chalk4.green("true") : chalk4.yellow("false");
2139
+ return value ? chalk5.green("true") : chalk5.yellow("false");
1685
2140
  }
1686
- return chalk4.cyan(String(value));
2141
+ return chalk5.cyan(String(value));
1687
2142
  }
1688
2143
 
1689
2144
  // src/commands/sync.ts
@@ -1770,7 +2225,7 @@ async function handleReset() {
1770
2225
  }
1771
2226
 
1772
2227
  // src/commands/status.ts
1773
- import chalk5 from "chalk";
2228
+ import chalk6 from "chalk";
1774
2229
  function registerStatusCommand(program2) {
1775
2230
  program2.command("status").description("\u663E\u793A\u9879\u76EE\u5168\u5C40\u72B6\u6001\u603B\u89C8").option("--json", "\u4EE5 JSON \u683C\u5F0F\u8F93\u51FA").action((options) => {
1776
2231
  handleStatus(options);
@@ -1866,7 +2321,7 @@ function printStatusAsJson(result) {
1866
2321
  }
1867
2322
  function printStatusAsText(result) {
1868
2323
  printDoubleSeparator();
1869
- printInfo(` ${chalk5.bold.cyan(MESSAGES.STATUS_TITLE(result.main.projectName))}`);
2324
+ printInfo(` ${chalk6.bold.cyan(MESSAGES.STATUS_TITLE(result.main.projectName))}`);
1870
2325
  printDoubleSeparator();
1871
2326
  printInfo("");
1872
2327
  printMainSection(result.main);
@@ -1879,17 +2334,17 @@ function printStatusAsText(result) {
1879
2334
  printDoubleSeparator();
1880
2335
  }
1881
2336
  function printMainSection(main) {
1882
- printInfo(` ${chalk5.bold("\u25C6")} ${chalk5.bold(MESSAGES.STATUS_MAIN_SECTION)}`);
1883
- printInfo(` \u5206\u652F: ${chalk5.bold(main.branch)}`);
2337
+ printInfo(` ${chalk6.bold("\u25C6")} ${chalk6.bold(MESSAGES.STATUS_MAIN_SECTION)}`);
2338
+ printInfo(` \u5206\u652F: ${chalk6.bold(main.branch)}`);
1884
2339
  if (main.isClean) {
1885
- printInfo(` \u72B6\u6001: ${chalk5.green("\u2713 \u5E72\u51C0")}`);
2340
+ printInfo(` \u72B6\u6001: ${chalk6.green("\u2713 \u5E72\u51C0")}`);
1886
2341
  } else {
1887
- printInfo(` \u72B6\u6001: ${chalk5.yellow("\u2717 \u6709\u672A\u63D0\u4EA4\u4FEE\u6539")}`);
2342
+ printInfo(` \u72B6\u6001: ${chalk6.yellow("\u2717 \u6709\u672A\u63D0\u4EA4\u4FEE\u6539")}`);
1888
2343
  }
1889
2344
  printInfo("");
1890
2345
  }
1891
2346
  function printWorktreesSection(worktrees, total) {
1892
- printInfo(` ${chalk5.bold("\u25C6")} ${chalk5.bold(MESSAGES.STATUS_WORKTREES_SECTION)} (${total} \u4E2A)`);
2347
+ printInfo(` ${chalk6.bold("\u25C6")} ${chalk6.bold(MESSAGES.STATUS_WORKTREES_SECTION)} (${total} \u4E2A)`);
1893
2348
  printInfo("");
1894
2349
  if (worktrees.length === 0) {
1895
2350
  printInfo(` ${MESSAGES.STATUS_NO_WORKTREES}`);
@@ -1901,39 +2356,39 @@ function printWorktreesSection(worktrees, total) {
1901
2356
  }
1902
2357
  function printWorktreeItem(wt) {
1903
2358
  const statusLabel = formatChangeStatusLabel(wt.changeStatus);
1904
- printInfo(` ${chalk5.bold("\u25CF")} ${chalk5.bold(wt.branch)} [${statusLabel}]`);
2359
+ printInfo(` ${chalk6.bold("\u25CF")} ${chalk6.bold(wt.branch)} [${statusLabel}]`);
1905
2360
  const parts = [];
1906
2361
  if (wt.insertions > 0 || wt.deletions > 0) {
1907
- parts.push(`${chalk5.green(`+${wt.insertions}`)} ${chalk5.red(`-${wt.deletions}`)}`);
2362
+ parts.push(`${chalk6.green(`+${wt.insertions}`)} ${chalk6.red(`-${wt.deletions}`)}`);
1908
2363
  }
1909
2364
  if (wt.commitsAhead > 0) {
1910
- parts.push(chalk5.yellow(`${wt.commitsAhead} \u4E2A\u672C\u5730\u63D0\u4EA4`));
2365
+ parts.push(chalk6.yellow(`${wt.commitsAhead} \u4E2A\u672C\u5730\u63D0\u4EA4`));
1911
2366
  }
1912
2367
  if (wt.commitsBehind > 0) {
1913
- parts.push(chalk5.yellow(`\u843D\u540E\u4E3B\u5206\u652F ${wt.commitsBehind} \u4E2A\u63D0\u4EA4`));
2368
+ parts.push(chalk6.yellow(`\u843D\u540E\u4E3B\u5206\u652F ${wt.commitsBehind} \u4E2A\u63D0\u4EA4`));
1914
2369
  } else {
1915
- parts.push(chalk5.green("\u4E0E\u4E3B\u5206\u652F\u540C\u6B65"));
2370
+ parts.push(chalk6.green("\u4E0E\u4E3B\u5206\u652F\u540C\u6B65"));
1916
2371
  }
1917
2372
  printInfo(` ${parts.join(" ")}`);
1918
2373
  if (wt.hasSnapshot) {
1919
- printInfo(` ${chalk5.blue("\u6709 validate \u5FEB\u7167")}`);
2374
+ printInfo(` ${chalk6.blue("\u6709 validate \u5FEB\u7167")}`);
1920
2375
  }
1921
2376
  printInfo("");
1922
2377
  }
1923
2378
  function formatChangeStatusLabel(status) {
1924
2379
  switch (status) {
1925
2380
  case "committed":
1926
- return chalk5.green(MESSAGES.STATUS_CHANGE_COMMITTED);
2381
+ return chalk6.green(MESSAGES.STATUS_CHANGE_COMMITTED);
1927
2382
  case "uncommitted":
1928
- return chalk5.yellow(MESSAGES.STATUS_CHANGE_UNCOMMITTED);
2383
+ return chalk6.yellow(MESSAGES.STATUS_CHANGE_UNCOMMITTED);
1929
2384
  case "conflict":
1930
- return chalk5.red(MESSAGES.STATUS_CHANGE_CONFLICT);
2385
+ return chalk6.red(MESSAGES.STATUS_CHANGE_CONFLICT);
1931
2386
  case "clean":
1932
- return chalk5.gray(MESSAGES.STATUS_CHANGE_CLEAN);
2387
+ return chalk6.gray(MESSAGES.STATUS_CHANGE_CLEAN);
1933
2388
  }
1934
2389
  }
1935
2390
  function printSnapshotsSection(snapshots) {
1936
- printInfo(` ${chalk5.bold("\u25C6")} ${chalk5.bold(MESSAGES.STATUS_SNAPSHOTS_SECTION)} (${snapshots.length} \u4E2A)`);
2391
+ printInfo(` ${chalk6.bold("\u25C6")} ${chalk6.bold(MESSAGES.STATUS_SNAPSHOTS_SECTION)} (${snapshots.length} \u4E2A)`);
1937
2392
  printInfo("");
1938
2393
  if (snapshots.length === 0) {
1939
2394
  printInfo(` ${MESSAGES.STATUS_NO_SNAPSHOTS}`);
@@ -1941,8 +2396,8 @@ function printSnapshotsSection(snapshots) {
1941
2396
  return;
1942
2397
  }
1943
2398
  for (const snap of snapshots) {
1944
- const orphanLabel = snap.worktreeExists ? "" : ` ${chalk5.yellow(MESSAGES.STATUS_SNAPSHOT_ORPHANED)}`;
1945
- const icon = snap.worktreeExists ? chalk5.blue("\u25CF") : chalk5.yellow("\u26A0");
2399
+ const orphanLabel = snap.worktreeExists ? "" : ` ${chalk6.yellow(MESSAGES.STATUS_SNAPSHOT_ORPHANED)}`;
2400
+ const icon = snap.worktreeExists ? chalk6.blue("\u25CF") : chalk6.yellow("\u26A0");
1946
2401
  printInfo(` ${icon} ${snap.branch}${orphanLabel}`);
1947
2402
  }
1948
2403
  printInfo("");