clawt 2.10.1 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agent-memory/docs-sync-updater/MEMORY.md +5 -1
- package/README.md +25 -2
- package/dist/index.js +603 -170
- package/dist/postinstall.js +31 -1
- package/docs/spec.md +57 -8
- package/package.json +1 -1
- package/src/commands/run.ts +68 -206
- package/src/constants/config.ts +4 -0
- package/src/constants/index.ts +10 -0
- package/src/constants/messages/run.ts +30 -0
- package/src/constants/progress.ts +39 -0
- package/src/types/command.ts +4 -0
- package/src/types/config.ts +2 -0
- package/src/types/index.ts +1 -0
- package/src/types/taskFile.ts +13 -0
- package/src/utils/formatter.ts +16 -0
- package/src/utils/index.ts +6 -2
- package/src/utils/progress-render.ts +90 -0
- package/src/utils/progress.ts +213 -0
- package/src/utils/task-executor.ts +365 -0
- package/src/utils/task-file.ts +87 -0
- package/src/utils/worktree.ts +27 -0
- package/tests/unit/commands/run.test.ts +259 -10
- package/tests/unit/constants/config.test.ts +1 -0
- package/tests/unit/utils/formatter.test.ts +27 -1
- package/tests/unit/utils/progress.test.ts +255 -0
- package/tests/unit/utils/task-file.test.ts +236 -0
- package/tests/unit/utils/worktree.test.ts +26 -1
package/dist/index.js
CHANGED
|
@@ -71,7 +71,33 @@ var RUN_MESSAGES = {
|
|
|
71
71
|
/** 中断后清理完成 */
|
|
72
72
|
INTERRUPT_CLEANED: (count) => `\u2713 \u5DF2\u6E05\u7406 ${count} \u4E2A worktree \u548C\u5BF9\u5E94\u5206\u652F`,
|
|
73
73
|
/** 中断后保留 worktree */
|
|
74
|
-
INTERRUPT_KEPT: "\u5DF2\u4FDD\u7559 worktree\uFF0C\u53EF\u7A0D\u540E\u4F7F\u7528 clawt remove \u624B\u52A8\u6E05\u7406"
|
|
74
|
+
INTERRUPT_KEPT: "\u5DF2\u4FDD\u7559 worktree\uFF0C\u53EF\u7A0D\u540E\u4F7F\u7528 clawt remove \u624B\u52A8\u6E05\u7406",
|
|
75
|
+
/** 非 TTY 环境降级输出:任务启动 */
|
|
76
|
+
PROGRESS_TASK_STARTED: (index, total, branch, path) => `[${index}/${total}] ${branch} \u542F\u52A8 ${path}`,
|
|
77
|
+
/** 非 TTY 环境降级输出:任务完成 */
|
|
78
|
+
PROGRESS_TASK_DONE: (index, total, branch, duration, cost, path) => `[${index}/${total}] ${branch} \u2713 \u5B8C\u6210 ${duration} ${cost} ${path}`,
|
|
79
|
+
/** 非 TTY 环境降级输出:任务失败 */
|
|
80
|
+
PROGRESS_TASK_FAILED: (index, total, branch, duration, path) => `[${index}/${total}] ${branch} \u2717 \u5931\u8D25 ${duration} ${path}`,
|
|
81
|
+
/** 并发限制提示 */
|
|
82
|
+
CONCURRENCY_INFO: (concurrency, total) => `\u5E76\u53D1\u9650\u5236: ${concurrency}\uFF0C\u5171 ${total} \u4E2A\u4EFB\u52A1`,
|
|
83
|
+
/** 并发数无效提示 */
|
|
84
|
+
CONCURRENCY_INVALID: "\u5E76\u53D1\u6570\u5FC5\u987B\u4E3A\u6B63\u6574\u6570",
|
|
85
|
+
/** 任务文件不存在 */
|
|
86
|
+
TASK_FILE_NOT_FOUND: (path) => `\u4EFB\u52A1\u6587\u4EF6\u4E0D\u5B58\u5728: ${path}`,
|
|
87
|
+
/** 任务文件中没有解析到有效任务 */
|
|
88
|
+
TASK_FILE_EMPTY: "\u4EFB\u52A1\u6587\u4EF6\u4E2D\u6CA1\u6709\u89E3\u6790\u5230\u6709\u6548\u4EFB\u52A1",
|
|
89
|
+
/** 任务文件中某个任务块缺少分支名 */
|
|
90
|
+
TASK_FILE_MISSING_BRANCH: (blockIndex) => `\u4EFB\u52A1\u6587\u4EF6\u7B2C ${blockIndex} \u4E2A\u4EFB\u52A1\u5757\u7F3A\u5C11\u5206\u652F\u540D\uFF08# branch: ...\uFF09`,
|
|
91
|
+
/** 任务文件中某个任务块缺少任务描述 */
|
|
92
|
+
TASK_FILE_MISSING_TASK: (branch) => `\u4EFB\u52A1\u6587\u4EF6\u4E2D\u5206\u652F ${branch} \u7F3A\u5C11\u4EFB\u52A1\u63CF\u8FF0`,
|
|
93
|
+
/** 任务文件中某个任务块缺少任务描述(无分支名时按索引定位) */
|
|
94
|
+
TASK_FILE_MISSING_TASK_BY_INDEX: (blockIndex) => `\u4EFB\u52A1\u6587\u4EF6\u7B2C ${blockIndex} \u4E2A\u4EFB\u52A1\u5757\u7F3A\u5C11\u4EFB\u52A1\u63CF\u8FF0`,
|
|
95
|
+
/** --file 和 --tasks 不能同时使用 */
|
|
96
|
+
FILE_AND_TASKS_CONFLICT: "--file \u548C --tasks \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528",
|
|
97
|
+
/** 任务文件加载成功 */
|
|
98
|
+
TASK_FILE_LOADED: (count, path) => `\u2713 \u4ECE ${path} \u52A0\u8F7D\u4E86 ${count} \u4E2A\u4EFB\u52A1`,
|
|
99
|
+
/** 未指定 -b 或 -f */
|
|
100
|
+
BRANCH_OR_FILE_REQUIRED: "\u8BF7\u6307\u5B9A -b \u5206\u652F\u540D\u6216 -f \u4EFB\u52A1\u6587\u4EF6"
|
|
75
101
|
};
|
|
76
102
|
|
|
77
103
|
// src/constants/messages/create.ts
|
|
@@ -288,6 +314,10 @@ var CONFIG_DEFINITIONS = {
|
|
|
288
314
|
confirmDestructiveOps: {
|
|
289
315
|
defaultValue: true,
|
|
290
316
|
description: "\u6267\u884C\u7834\u574F\u6027\u64CD\u4F5C\uFF08reset\u3001validate --clean\uFF09\u524D\u662F\u5426\u63D0\u793A\u786E\u8BA4"
|
|
317
|
+
},
|
|
318
|
+
maxConcurrency: {
|
|
319
|
+
defaultValue: 0,
|
|
320
|
+
description: "run \u547D\u4EE4\u9ED8\u8BA4\u6700\u5927\u5E76\u53D1\u6570\uFF0C0 \u8868\u793A\u4E0D\u9650\u5236"
|
|
291
321
|
}
|
|
292
322
|
};
|
|
293
323
|
function deriveDefaultConfig(definitions) {
|
|
@@ -311,6 +341,32 @@ var AUTO_SAVE_COMMIT_MESSAGE = "chore: auto-save before sync";
|
|
|
311
341
|
// src/constants/logger.ts
|
|
312
342
|
var DEBUG_TIMESTAMP_FORMAT = "HH:mm:ss.SSS";
|
|
313
343
|
|
|
344
|
+
// src/constants/progress.ts
|
|
345
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
346
|
+
var SPINNER_INTERVAL_MS = 100;
|
|
347
|
+
var CURSOR_UP = (n) => `\x1B[${n}A`;
|
|
348
|
+
var CLEAR_LINE = "\x1B[0K";
|
|
349
|
+
var CURSOR_HIDE = "\x1B[?25l";
|
|
350
|
+
var CURSOR_SHOW = "\x1B[?25h";
|
|
351
|
+
var TASK_STATUS_ICONS = {
|
|
352
|
+
/** 排队中 */
|
|
353
|
+
PENDING: "\u25E6",
|
|
354
|
+
/** 完成 */
|
|
355
|
+
DONE: "\u2713",
|
|
356
|
+
/** 失败 */
|
|
357
|
+
FAILED: "\u2717"
|
|
358
|
+
};
|
|
359
|
+
var TASK_STATUS_LABELS = {
|
|
360
|
+
/** 排队中 */
|
|
361
|
+
PENDING: "\u6392\u961F\u4E2D",
|
|
362
|
+
/** 运行中 */
|
|
363
|
+
RUNNING: "\u8FD0\u884C\u4E2D",
|
|
364
|
+
/** 完成 */
|
|
365
|
+
DONE: "\u5B8C\u6210",
|
|
366
|
+
/** 失败 */
|
|
367
|
+
FAILED: "\u5931\u8D25"
|
|
368
|
+
};
|
|
369
|
+
|
|
314
370
|
// src/errors/index.ts
|
|
315
371
|
var ClawtError = class extends Error {
|
|
316
372
|
/** 退出码 */
|
|
@@ -616,14 +672,14 @@ function printDoubleSeparator() {
|
|
|
616
672
|
console.log(MESSAGES.DOUBLE_SEPARATOR);
|
|
617
673
|
}
|
|
618
674
|
function confirmAction(question) {
|
|
619
|
-
return new Promise((
|
|
675
|
+
return new Promise((resolve2) => {
|
|
620
676
|
const rl = createInterface({
|
|
621
677
|
input: process.stdin,
|
|
622
678
|
output: process.stdout
|
|
623
679
|
});
|
|
624
680
|
rl.question(`${question} (y/N) `, (answer) => {
|
|
625
681
|
rl.close();
|
|
626
|
-
|
|
682
|
+
resolve2(answer.toLowerCase() === "y");
|
|
627
683
|
});
|
|
628
684
|
});
|
|
629
685
|
}
|
|
@@ -650,6 +706,15 @@ function formatWorktreeStatus(status) {
|
|
|
650
706
|
}
|
|
651
707
|
return parts.join(" ");
|
|
652
708
|
}
|
|
709
|
+
function formatDuration(ms) {
|
|
710
|
+
const totalSeconds = ms / 1e3;
|
|
711
|
+
if (totalSeconds < 60) {
|
|
712
|
+
return `${totalSeconds.toFixed(1)}s`;
|
|
713
|
+
}
|
|
714
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
715
|
+
const seconds = Math.floor(totalSeconds % 60);
|
|
716
|
+
return `${minutes}m${String(seconds).padStart(2, "0")}s`;
|
|
717
|
+
}
|
|
653
718
|
|
|
654
719
|
// src/utils/branch.ts
|
|
655
720
|
function sanitizeBranchName(branchName) {
|
|
@@ -742,6 +807,19 @@ function createWorktrees(branchName, count) {
|
|
|
742
807
|
}
|
|
743
808
|
return results;
|
|
744
809
|
}
|
|
810
|
+
function createWorktreesByBranches(branchNames) {
|
|
811
|
+
validateBranchesNotExist(branchNames);
|
|
812
|
+
const projectDir = getProjectWorktreeDir();
|
|
813
|
+
ensureDir(projectDir);
|
|
814
|
+
const results = [];
|
|
815
|
+
for (const name of branchNames) {
|
|
816
|
+
const worktreePath = join2(projectDir, name);
|
|
817
|
+
createWorktree(name, worktreePath);
|
|
818
|
+
results.push({ path: worktreePath, branch: name });
|
|
819
|
+
logger.info(`worktree \u521B\u5EFA\u5B8C\u6210: ${worktreePath} (\u5206\u652F: ${name})`);
|
|
820
|
+
}
|
|
821
|
+
return results;
|
|
822
|
+
}
|
|
745
823
|
function getProjectWorktrees() {
|
|
746
824
|
const projectDir = getProjectWorktreeDir();
|
|
747
825
|
if (!existsSync3(projectDir)) {
|
|
@@ -999,8 +1077,467 @@ async function resolveTargetWorktree(worktrees, messages, branchName) {
|
|
|
999
1077
|
throw new ClawtError(messages.noMatch(branchName, allBranches));
|
|
1000
1078
|
}
|
|
1001
1079
|
|
|
1002
|
-
// src/
|
|
1080
|
+
// src/utils/progress-render.ts
|
|
1003
1081
|
import chalk3 from "chalk";
|
|
1082
|
+
function getMaxBranchWidth(tasks) {
|
|
1083
|
+
return Math.max(...tasks.map((t) => t.branch.length));
|
|
1084
|
+
}
|
|
1085
|
+
function renderTaskLine(task, total, maxBranchWidth, spinnerChar) {
|
|
1086
|
+
const indexStr = `[${task.index}/${total}]`;
|
|
1087
|
+
const branchStr = task.branch.padEnd(maxBranchWidth);
|
|
1088
|
+
switch (task.status) {
|
|
1089
|
+
case "pending": {
|
|
1090
|
+
return `${indexStr} ${branchStr} ${chalk3.gray(TASK_STATUS_ICONS.PENDING)} ${chalk3.gray(TASK_STATUS_LABELS.PENDING)} ${chalk3.dim(task.path)}`;
|
|
1091
|
+
}
|
|
1092
|
+
case "running": {
|
|
1093
|
+
const elapsed = formatDuration(Date.now() - task.startedAt);
|
|
1094
|
+
return `${indexStr} ${branchStr} ${chalk3.cyan(spinnerChar)} ${chalk3.cyan(TASK_STATUS_LABELS.RUNNING)} ${chalk3.gray(elapsed)} ${chalk3.dim(task.path)}`;
|
|
1095
|
+
}
|
|
1096
|
+
case "done": {
|
|
1097
|
+
const duration = task.durationMs != null ? formatDuration(task.durationMs) : "N/A";
|
|
1098
|
+
const cost = task.costUsd != null ? `$${task.costUsd.toFixed(2)}` : "";
|
|
1099
|
+
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)}`;
|
|
1100
|
+
}
|
|
1101
|
+
case "failed": {
|
|
1102
|
+
const duration = task.durationMs != null ? formatDuration(task.durationMs) : "N/A";
|
|
1103
|
+
return `${indexStr} ${branchStr} ${chalk3.red(TASK_STATUS_ICONS.FAILED)} ${chalk3.red(TASK_STATUS_LABELS.FAILED)} ${chalk3.gray(duration)} ${chalk3.dim(task.path)}`;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
function renderSummaryLine(tasks, total) {
|
|
1108
|
+
const running = tasks.filter((t) => t.status === "running").length;
|
|
1109
|
+
const done = tasks.filter((t) => t.status === "done").length;
|
|
1110
|
+
const failed = tasks.filter((t) => t.status === "failed").length;
|
|
1111
|
+
const pending = tasks.filter((t) => t.status === "pending").length;
|
|
1112
|
+
const parts = [];
|
|
1113
|
+
if (running > 0) parts.push(chalk3.cyan(`${running}/${total} ${TASK_STATUS_LABELS.RUNNING}`));
|
|
1114
|
+
if (done > 0) parts.push(chalk3.green(`${done}/${total} ${TASK_STATUS_LABELS.DONE}`));
|
|
1115
|
+
if (failed > 0) parts.push(chalk3.red(`${failed}/${total} ${TASK_STATUS_LABELS.FAILED}`));
|
|
1116
|
+
if (pending > 0) parts.push(chalk3.gray(`${pending}/${total} ${TASK_STATUS_LABELS.PENDING}`));
|
|
1117
|
+
return `[${parts.join(", ")}]`;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/utils/progress.ts
|
|
1121
|
+
var ProgressRenderer = class {
|
|
1122
|
+
/** 所有任务的进度状态 */
|
|
1123
|
+
tasks;
|
|
1124
|
+
/** 总任务数 */
|
|
1125
|
+
total;
|
|
1126
|
+
/** 当前 spinner 帧索引 */
|
|
1127
|
+
frameIndex;
|
|
1128
|
+
/** 定时器引用 */
|
|
1129
|
+
timer;
|
|
1130
|
+
/** 是否为 TTY 环境 */
|
|
1131
|
+
isTTY;
|
|
1132
|
+
/** 已渲染的行数(用于回退光标) */
|
|
1133
|
+
renderedLineCount;
|
|
1134
|
+
/** 是否已停止 */
|
|
1135
|
+
stopped;
|
|
1136
|
+
/** 是否存在排队任务(启用汇总行渲染) */
|
|
1137
|
+
hasPendingTasks;
|
|
1138
|
+
/**
|
|
1139
|
+
* 创建进度面板渲染器
|
|
1140
|
+
* @param {string[]} branches - 分支名列表,顺序对应任务列表
|
|
1141
|
+
* @param {string[]} paths - worktree 路径列表,完成/失败后显示
|
|
1142
|
+
* @param {boolean} [allRunning=true] - 是否将所有任务初始化为 running 状态,false 时初始化为 pending
|
|
1143
|
+
*/
|
|
1144
|
+
constructor(branches, paths, allRunning = true) {
|
|
1145
|
+
const now = Date.now();
|
|
1146
|
+
this.total = branches.length;
|
|
1147
|
+
this.frameIndex = 0;
|
|
1148
|
+
this.timer = null;
|
|
1149
|
+
this.isTTY = !!process.stdout.isTTY;
|
|
1150
|
+
this.renderedLineCount = 0;
|
|
1151
|
+
this.stopped = false;
|
|
1152
|
+
this.hasPendingTasks = !allRunning;
|
|
1153
|
+
this.tasks = branches.map((branch, i) => ({
|
|
1154
|
+
index: i + 1,
|
|
1155
|
+
branch,
|
|
1156
|
+
path: paths[i],
|
|
1157
|
+
status: allRunning ? "running" : "pending",
|
|
1158
|
+
startedAt: allRunning ? now : 0,
|
|
1159
|
+
finishedAt: null,
|
|
1160
|
+
lastActiveAt: allRunning ? now : 0,
|
|
1161
|
+
durationMs: null,
|
|
1162
|
+
costUsd: null
|
|
1163
|
+
}));
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* 启动定时渲染循环
|
|
1167
|
+
* TTY 模式下每 SPINNER_INTERVAL_MS 毫秒刷新一次面板
|
|
1168
|
+
* 非 TTY 模式下输出状态为 running 的任务的启动信息
|
|
1169
|
+
*/
|
|
1170
|
+
start() {
|
|
1171
|
+
if (this.stopped) return;
|
|
1172
|
+
if (!this.isTTY) {
|
|
1173
|
+
for (const task of this.tasks) {
|
|
1174
|
+
if (task.status === "running") {
|
|
1175
|
+
console.log(MESSAGES.PROGRESS_TASK_STARTED(task.index, this.total, task.branch, task.path));
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
process.stdout.write(CURSOR_HIDE);
|
|
1181
|
+
this.render();
|
|
1182
|
+
this.timer = setInterval(() => {
|
|
1183
|
+
this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
|
|
1184
|
+
this.render();
|
|
1185
|
+
}, SPINNER_INTERVAL_MS);
|
|
1186
|
+
if (this.timer.unref) {
|
|
1187
|
+
this.timer.unref();
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* 更新指定任务的最后活动时间戳
|
|
1192
|
+
* 当 child.stderr 有输出时调用,表示任务仍然活跃
|
|
1193
|
+
* @param {number} index - 任务索引(从 0 开始)
|
|
1194
|
+
*/
|
|
1195
|
+
updateActivity(index) {
|
|
1196
|
+
this.tasks[index].lastActiveAt = Date.now();
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* 标记指定任务为运行中状态
|
|
1200
|
+
* 将 pending 任务标记为 running 并设置启动时间戳
|
|
1201
|
+
* @param {number} index - 任务索引(从 0 开始)
|
|
1202
|
+
*/
|
|
1203
|
+
markRunning(index) {
|
|
1204
|
+
const task = this.tasks[index];
|
|
1205
|
+
const now = Date.now();
|
|
1206
|
+
task.status = "running";
|
|
1207
|
+
task.startedAt = now;
|
|
1208
|
+
task.lastActiveAt = now;
|
|
1209
|
+
if (!this.isTTY) {
|
|
1210
|
+
console.log(MESSAGES.PROGRESS_TASK_STARTED(task.index, this.total, task.branch, task.path));
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* 标记指定任务为完成状态
|
|
1215
|
+
* @param {number} index - 任务索引(从 0 开始)
|
|
1216
|
+
* @param {number} durationMs - 耗时(毫秒)
|
|
1217
|
+
* @param {number} costUsd - 费用(美元)
|
|
1218
|
+
*/
|
|
1219
|
+
markDone(index, durationMs, costUsd) {
|
|
1220
|
+
const task = this.tasks[index];
|
|
1221
|
+
task.status = "done";
|
|
1222
|
+
task.finishedAt = Date.now();
|
|
1223
|
+
task.durationMs = durationMs;
|
|
1224
|
+
task.costUsd = costUsd;
|
|
1225
|
+
if (!this.isTTY) {
|
|
1226
|
+
const duration = formatDuration(durationMs);
|
|
1227
|
+
const cost = `$${costUsd.toFixed(2)}`;
|
|
1228
|
+
console.log(MESSAGES.PROGRESS_TASK_DONE(task.index, this.total, task.branch, duration, cost, task.path));
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* 标记指定任务为失败状态
|
|
1233
|
+
* @param {number} index - 任务索引(从 0 开始)
|
|
1234
|
+
* @param {number} durationMs - 耗时(毫秒)
|
|
1235
|
+
*/
|
|
1236
|
+
markFailed(index, durationMs) {
|
|
1237
|
+
const task = this.tasks[index];
|
|
1238
|
+
task.status = "failed";
|
|
1239
|
+
task.finishedAt = Date.now();
|
|
1240
|
+
task.durationMs = durationMs;
|
|
1241
|
+
if (!this.isTTY) {
|
|
1242
|
+
const duration = formatDuration(durationMs);
|
|
1243
|
+
console.log(MESSAGES.PROGRESS_TASK_FAILED(task.index, this.total, task.branch, duration, task.path));
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* 停止渲染循环并恢复光标
|
|
1248
|
+
* 在所有任务完成或 SIGINT 中断时调用
|
|
1249
|
+
*/
|
|
1250
|
+
stop() {
|
|
1251
|
+
if (this.stopped) return;
|
|
1252
|
+
this.stopped = true;
|
|
1253
|
+
if (this.timer) {
|
|
1254
|
+
clearInterval(this.timer);
|
|
1255
|
+
this.timer = null;
|
|
1256
|
+
}
|
|
1257
|
+
if (this.isTTY) {
|
|
1258
|
+
this.render();
|
|
1259
|
+
process.stdout.write(CURSOR_SHOW);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* 执行一次完整的面板渲染
|
|
1264
|
+
* 先回退光标到面板起始位置,再逐行输出
|
|
1265
|
+
*/
|
|
1266
|
+
render() {
|
|
1267
|
+
const maxBranchWidth = getMaxBranchWidth(this.tasks);
|
|
1268
|
+
const spinnerChar = SPINNER_FRAMES[this.frameIndex];
|
|
1269
|
+
const lines = this.tasks.map((task) => renderTaskLine(task, this.total, maxBranchWidth, spinnerChar));
|
|
1270
|
+
if (this.hasPendingTasks) {
|
|
1271
|
+
lines.push(renderSummaryLine(this.tasks, this.total));
|
|
1272
|
+
}
|
|
1273
|
+
if (this.renderedLineCount > 0) {
|
|
1274
|
+
process.stdout.write(CURSOR_UP(this.renderedLineCount));
|
|
1275
|
+
}
|
|
1276
|
+
for (const line of lines) {
|
|
1277
|
+
process.stdout.write(`${line}${CLEAR_LINE}
|
|
1278
|
+
`);
|
|
1279
|
+
}
|
|
1280
|
+
this.renderedLineCount = lines.length;
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
// src/utils/task-file.ts
|
|
1285
|
+
import { resolve } from "path";
|
|
1286
|
+
import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
|
|
1287
|
+
var TASK_BLOCK_REGEX = /<!-- CLAWT-TASKS:START -->([\s\S]*?)<!-- CLAWT-TASKS:END -->/g;
|
|
1288
|
+
var BRANCH_LINE_REGEX = /^#\s*branch:\s*(.+)$/;
|
|
1289
|
+
function parseTaskFile(content, options) {
|
|
1290
|
+
const branchRequired = options?.branchRequired ?? true;
|
|
1291
|
+
const entries = [];
|
|
1292
|
+
let match;
|
|
1293
|
+
let blockIndex = 0;
|
|
1294
|
+
TASK_BLOCK_REGEX.lastIndex = 0;
|
|
1295
|
+
while ((match = TASK_BLOCK_REGEX.exec(content)) !== null) {
|
|
1296
|
+
blockIndex++;
|
|
1297
|
+
const blockContent = match[1].trim();
|
|
1298
|
+
const lines = blockContent.split("\n");
|
|
1299
|
+
let branch;
|
|
1300
|
+
const taskLines = [];
|
|
1301
|
+
for (const line of lines) {
|
|
1302
|
+
const branchMatch = line.trim().match(BRANCH_LINE_REGEX);
|
|
1303
|
+
if (branchMatch) {
|
|
1304
|
+
branch = branchMatch[1].trim();
|
|
1305
|
+
} else {
|
|
1306
|
+
taskLines.push(line);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
if (branchRequired && !branch) {
|
|
1310
|
+
throw new ClawtError(MESSAGES.TASK_FILE_MISSING_BRANCH(blockIndex));
|
|
1311
|
+
}
|
|
1312
|
+
const task = taskLines.join("\n").trim();
|
|
1313
|
+
if (!task) {
|
|
1314
|
+
throw new ClawtError(
|
|
1315
|
+
branch ? MESSAGES.TASK_FILE_MISSING_TASK(branch) : MESSAGES.TASK_FILE_MISSING_TASK_BY_INDEX(blockIndex)
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
entries.push({ branch, task });
|
|
1319
|
+
}
|
|
1320
|
+
return entries;
|
|
1321
|
+
}
|
|
1322
|
+
function loadTaskFile(filePath, options) {
|
|
1323
|
+
const absolutePath = resolve(filePath);
|
|
1324
|
+
if (!existsSync6(absolutePath)) {
|
|
1325
|
+
throw new ClawtError(MESSAGES.TASK_FILE_NOT_FOUND(absolutePath));
|
|
1326
|
+
}
|
|
1327
|
+
const content = readFileSync3(absolutePath, "utf-8");
|
|
1328
|
+
const entries = parseTaskFile(content, options);
|
|
1329
|
+
if (entries.length === 0) {
|
|
1330
|
+
throw new ClawtError(MESSAGES.TASK_FILE_EMPTY);
|
|
1331
|
+
}
|
|
1332
|
+
return entries;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// src/utils/task-executor.ts
|
|
1336
|
+
function executeClaudeTask(worktree, task) {
|
|
1337
|
+
const child = spawnProcess(
|
|
1338
|
+
"claude",
|
|
1339
|
+
["-p", task, "--output-format", "json", "--permission-mode", "bypassPermissions"],
|
|
1340
|
+
{
|
|
1341
|
+
cwd: worktree.path,
|
|
1342
|
+
// stdin 必须设置为 'ignore',不能用 'pipe'
|
|
1343
|
+
// 原因:claude -p 是非交互模式,不需要 stdin 输入。如果 stdin 为 'pipe',
|
|
1344
|
+
// 父进程会创建一个可写流连接到子进程但从不写入也不关闭,
|
|
1345
|
+
// claude 检测到 stdin 是管道后会尝试读取输入,导致进程永远卡住
|
|
1346
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1347
|
+
}
|
|
1348
|
+
);
|
|
1349
|
+
const promise = new Promise((resolve2) => {
|
|
1350
|
+
let stdout = "";
|
|
1351
|
+
let stderr = "";
|
|
1352
|
+
child.stdout?.on("data", (data) => {
|
|
1353
|
+
stdout += data.toString();
|
|
1354
|
+
});
|
|
1355
|
+
child.stderr?.on("data", (data) => {
|
|
1356
|
+
stderr += data.toString();
|
|
1357
|
+
});
|
|
1358
|
+
child.on("close", (code) => {
|
|
1359
|
+
let result = null;
|
|
1360
|
+
let success = code === 0;
|
|
1361
|
+
try {
|
|
1362
|
+
if (stdout.trim()) {
|
|
1363
|
+
result = JSON.parse(stdout.trim());
|
|
1364
|
+
success = !result.is_error;
|
|
1365
|
+
}
|
|
1366
|
+
} catch {
|
|
1367
|
+
logger.warn(`\u89E3\u6790 Claude Code \u8F93\u51FA\u5931\u8D25: ${stdout.substring(0, 200)}`);
|
|
1368
|
+
}
|
|
1369
|
+
resolve2({
|
|
1370
|
+
task,
|
|
1371
|
+
branch: worktree.branch,
|
|
1372
|
+
worktreePath: worktree.path,
|
|
1373
|
+
success,
|
|
1374
|
+
result,
|
|
1375
|
+
error: success ? void 0 : stderr || "\u4EFB\u52A1\u6267\u884C\u5931\u8D25"
|
|
1376
|
+
});
|
|
1377
|
+
});
|
|
1378
|
+
child.on("error", (err) => {
|
|
1379
|
+
resolve2({
|
|
1380
|
+
task,
|
|
1381
|
+
branch: worktree.branch,
|
|
1382
|
+
worktreePath: worktree.path,
|
|
1383
|
+
success: false,
|
|
1384
|
+
result: null,
|
|
1385
|
+
error: err.message
|
|
1386
|
+
});
|
|
1387
|
+
});
|
|
1388
|
+
});
|
|
1389
|
+
return { child, promise };
|
|
1390
|
+
}
|
|
1391
|
+
function printTaskSummary(summary) {
|
|
1392
|
+
printDoubleSeparator();
|
|
1393
|
+
printInfo(`\u5168\u90E8\u4EFB\u52A1\u5DF2\u5B8C\u6210 (${summary.total}/${summary.total})`);
|
|
1394
|
+
printInfo(` \u6210\u529F: ${summary.succeeded}`);
|
|
1395
|
+
printInfo(` \u5931\u8D25: ${summary.failed}`);
|
|
1396
|
+
printInfo(` \u603B\u8017\u65F6: ${(summary.totalDurationMs / 1e3).toFixed(1)}s`);
|
|
1397
|
+
printInfo(` \u603B\u82B1\u8D39: $${summary.totalCostUsd.toFixed(2)}`);
|
|
1398
|
+
printDoubleSeparator();
|
|
1399
|
+
}
|
|
1400
|
+
async function handleInterruptCleanup(worktrees) {
|
|
1401
|
+
const autoDelete = getConfigValue("autoDeleteBranch");
|
|
1402
|
+
if (autoDelete) {
|
|
1403
|
+
cleanupWorktrees(worktrees);
|
|
1404
|
+
printSuccess(MESSAGES.INTERRUPT_AUTO_CLEANED(worktrees.length));
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
const shouldClean = await confirmAction(MESSAGES.INTERRUPT_CONFIRM_CLEANUP);
|
|
1408
|
+
if (shouldClean) {
|
|
1409
|
+
cleanupWorktrees(worktrees);
|
|
1410
|
+
printSuccess(MESSAGES.INTERRUPT_CLEANED(worktrees.length));
|
|
1411
|
+
} else {
|
|
1412
|
+
printInfo(MESSAGES.INTERRUPT_KEPT);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
function updateRendererStatus(renderer, index, result, startTime) {
|
|
1416
|
+
if (result.success) {
|
|
1417
|
+
renderer.markDone(
|
|
1418
|
+
index,
|
|
1419
|
+
result.result?.duration_ms ?? Date.now() - startTime,
|
|
1420
|
+
result.result?.total_cost_usd ?? 0
|
|
1421
|
+
);
|
|
1422
|
+
} else {
|
|
1423
|
+
renderer.markFailed(
|
|
1424
|
+
index,
|
|
1425
|
+
result.result?.duration_ms ?? Date.now() - startTime
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
async function executeWithConcurrency(worktrees, tasks, concurrency, renderer, startTime, isInterrupted, childProcesses) {
|
|
1430
|
+
const total = tasks.length;
|
|
1431
|
+
const results = new Array(total);
|
|
1432
|
+
let nextIndex = 0;
|
|
1433
|
+
let completedCount = 0;
|
|
1434
|
+
return new Promise((resolve2) => {
|
|
1435
|
+
function launchNext() {
|
|
1436
|
+
if (nextIndex >= total || isInterrupted()) return;
|
|
1437
|
+
const index = nextIndex;
|
|
1438
|
+
nextIndex++;
|
|
1439
|
+
const wt = worktrees[index];
|
|
1440
|
+
const task = tasks[index];
|
|
1441
|
+
logger.info(`\u542F\u52A8\u4EFB\u52A1 ${index + 1}: ${task} (worktree: ${wt.path})`);
|
|
1442
|
+
renderer.markRunning(index);
|
|
1443
|
+
const handle = executeClaudeTask(wt, task);
|
|
1444
|
+
childProcesses.push(handle.child);
|
|
1445
|
+
handle.child.stderr?.on("data", () => {
|
|
1446
|
+
renderer.updateActivity(index);
|
|
1447
|
+
});
|
|
1448
|
+
handle.promise.then((result) => {
|
|
1449
|
+
results[index] = result;
|
|
1450
|
+
completedCount++;
|
|
1451
|
+
if (!isInterrupted()) {
|
|
1452
|
+
updateRendererStatus(renderer, index, result, startTime);
|
|
1453
|
+
}
|
|
1454
|
+
launchNext();
|
|
1455
|
+
if (completedCount === total) {
|
|
1456
|
+
resolve2(results);
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
const initialBatch = Math.min(concurrency, total);
|
|
1461
|
+
for (let i = 0; i < initialBatch; i++) {
|
|
1462
|
+
launchNext();
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
async function executeAllParallel(worktrees, tasks, renderer, startTime, isInterrupted, childProcesses) {
|
|
1467
|
+
const handles = worktrees.map((wt, index) => {
|
|
1468
|
+
const task = tasks[index];
|
|
1469
|
+
logger.info(`\u542F\u52A8\u4EFB\u52A1 ${index + 1}: ${task} (worktree: ${wt.path})`);
|
|
1470
|
+
const handle = executeClaudeTask(wt, task);
|
|
1471
|
+
childProcesses.push(handle.child);
|
|
1472
|
+
handle.child.stderr?.on("data", () => {
|
|
1473
|
+
renderer.updateActivity(index);
|
|
1474
|
+
});
|
|
1475
|
+
return handle;
|
|
1476
|
+
});
|
|
1477
|
+
const results = await Promise.all(
|
|
1478
|
+
handles.map(
|
|
1479
|
+
(handle, index) => handle.promise.then((result) => {
|
|
1480
|
+
if (!isInterrupted()) {
|
|
1481
|
+
updateRendererStatus(renderer, index, result, startTime);
|
|
1482
|
+
}
|
|
1483
|
+
return result;
|
|
1484
|
+
})
|
|
1485
|
+
)
|
|
1486
|
+
);
|
|
1487
|
+
return results;
|
|
1488
|
+
}
|
|
1489
|
+
async function executeBatchTasks(worktrees, tasks, concurrency) {
|
|
1490
|
+
const count = tasks.length;
|
|
1491
|
+
if (concurrency > 0) {
|
|
1492
|
+
printInfo(MESSAGES.CONCURRENCY_INFO(concurrency, count));
|
|
1493
|
+
printInfo("");
|
|
1494
|
+
}
|
|
1495
|
+
const startTime = Date.now();
|
|
1496
|
+
const branches = worktrees.map((wt) => wt.branch);
|
|
1497
|
+
const paths = worktrees.map((wt) => wt.path);
|
|
1498
|
+
const allRunning = concurrency === 0;
|
|
1499
|
+
const renderer = new ProgressRenderer(branches, paths, allRunning);
|
|
1500
|
+
renderer.start();
|
|
1501
|
+
let interrupted = false;
|
|
1502
|
+
const isInterrupted = () => interrupted;
|
|
1503
|
+
const childProcesses = [];
|
|
1504
|
+
const sigintHandler = async () => {
|
|
1505
|
+
if (interrupted) return;
|
|
1506
|
+
interrupted = true;
|
|
1507
|
+
renderer.stop();
|
|
1508
|
+
printInfo("");
|
|
1509
|
+
printWarning(MESSAGES.INTERRUPTED);
|
|
1510
|
+
killAllChildProcesses(childProcesses);
|
|
1511
|
+
await Promise.allSettled(childProcesses.map(
|
|
1512
|
+
(cp) => new Promise((resolve2) => {
|
|
1513
|
+
if (cp.exitCode !== null) {
|
|
1514
|
+
resolve2();
|
|
1515
|
+
} else {
|
|
1516
|
+
cp.on("close", () => resolve2());
|
|
1517
|
+
}
|
|
1518
|
+
})
|
|
1519
|
+
));
|
|
1520
|
+
await handleInterruptCleanup(worktrees);
|
|
1521
|
+
process.exit(1);
|
|
1522
|
+
};
|
|
1523
|
+
process.on("SIGINT", sigintHandler);
|
|
1524
|
+
const results = concurrency > 0 ? await executeWithConcurrency(worktrees, tasks, concurrency, renderer, startTime, isInterrupted, childProcesses) : await executeAllParallel(worktrees, tasks, renderer, startTime, isInterrupted, childProcesses);
|
|
1525
|
+
renderer.stop();
|
|
1526
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
1527
|
+
if (interrupted) return;
|
|
1528
|
+
const totalDurationMs = Date.now() - startTime;
|
|
1529
|
+
const summary = {
|
|
1530
|
+
total: results.length,
|
|
1531
|
+
succeeded: results.filter((r) => r.success).length,
|
|
1532
|
+
failed: results.filter((r) => !r.success).length,
|
|
1533
|
+
totalDurationMs,
|
|
1534
|
+
totalCostUsd: results.reduce((sum, r) => sum + (r.result?.total_cost_usd ?? 0), 0)
|
|
1535
|
+
};
|
|
1536
|
+
printTaskSummary(summary);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// src/commands/list.ts
|
|
1540
|
+
import chalk4 from "chalk";
|
|
1004
1541
|
function registerListCommand(program2) {
|
|
1005
1542
|
program2.command("list").description("\u5217\u51FA\u5F53\u524D\u9879\u76EE\u6240\u6709 worktree").option("--json", "\u4EE5 JSON \u683C\u5F0F\u8F93\u51FA").action((options) => {
|
|
1006
1543
|
handleList(options);
|
|
@@ -1037,12 +1574,12 @@ function printListAsText(projectName, worktrees) {
|
|
|
1037
1574
|
for (const wt of worktrees) {
|
|
1038
1575
|
const status = getWorktreeStatus(wt);
|
|
1039
1576
|
const isIdle = status ? isWorktreeIdle(status) : false;
|
|
1040
|
-
const pathDisplay = isIdle ?
|
|
1577
|
+
const pathDisplay = isIdle ? chalk4.hex("#FF8C00")(wt.path) : wt.path;
|
|
1041
1578
|
printInfo(` ${pathDisplay} [${wt.branch}]`);
|
|
1042
1579
|
if (status) {
|
|
1043
1580
|
printInfo(` ${formatWorktreeStatus(status)}`);
|
|
1044
1581
|
} else {
|
|
1045
|
-
printInfo(` ${
|
|
1582
|
+
printInfo(` ${chalk4.yellow(MESSAGES.WORKTREE_STATUS_UNAVAILABLE)}`);
|
|
1046
1583
|
}
|
|
1047
1584
|
printInfo("");
|
|
1048
1585
|
}
|
|
@@ -1145,110 +1682,48 @@ async function handleRemove(options) {
|
|
|
1145
1682
|
|
|
1146
1683
|
// src/commands/run.ts
|
|
1147
1684
|
function registerRunCommand(program2) {
|
|
1148
|
-
program2.command("run").description("\u6279\u91CF\u521B\u5EFA worktree \u5E76\u542F\u52A8 Claude Code \u6267\u884C\u4EFB\u52A1").
|
|
1685
|
+
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
1686
|
await handleRun(options);
|
|
1150
1687
|
});
|
|
1151
1688
|
}
|
|
1152
|
-
function
|
|
1153
|
-
|
|
1154
|
-
|
|
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}`);
|
|
1689
|
+
function parseConcurrency(optionValue, configValue) {
|
|
1690
|
+
if (optionValue === void 0) {
|
|
1691
|
+
return configValue;
|
|
1218
1692
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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;
|
|
1693
|
+
const parsed = parseInt(optionValue, 10);
|
|
1694
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
1695
|
+
throw new ClawtError(MESSAGES.CONCURRENCY_INVALID);
|
|
1240
1696
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1697
|
+
return parsed;
|
|
1698
|
+
}
|
|
1699
|
+
async function handleRunFromFile(options) {
|
|
1700
|
+
const branchRequired = !options.branch;
|
|
1701
|
+
const entries = loadTaskFile(options.file, { branchRequired });
|
|
1702
|
+
printSuccess(MESSAGES.TASK_FILE_LOADED(entries.length, options.file));
|
|
1703
|
+
const tasks = entries.map((e) => e.task);
|
|
1704
|
+
let worktrees;
|
|
1705
|
+
if (options.branch) {
|
|
1706
|
+
worktrees = createWorktrees(options.branch, entries.length);
|
|
1245
1707
|
} else {
|
|
1246
|
-
|
|
1708
|
+
const branches = entries.map((e) => sanitizeBranchName(e.branch));
|
|
1709
|
+
worktrees = createWorktreesByBranches(branches);
|
|
1247
1710
|
}
|
|
1711
|
+
const concurrency = parseConcurrency(options.concurrency, getConfigValue("maxConcurrency"));
|
|
1712
|
+
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"}`);
|
|
1713
|
+
await executeBatchTasks(worktrees, tasks, concurrency);
|
|
1248
1714
|
}
|
|
1249
1715
|
async function handleRun(options) {
|
|
1250
1716
|
validateMainWorktree();
|
|
1251
1717
|
validateClaudeCodeInstalled();
|
|
1718
|
+
if (options.file && options.tasks) {
|
|
1719
|
+
throw new ClawtError(MESSAGES.FILE_AND_TASKS_CONFLICT);
|
|
1720
|
+
}
|
|
1721
|
+
if (options.file) {
|
|
1722
|
+
return handleRunFromFile(options);
|
|
1723
|
+
}
|
|
1724
|
+
if (!options.branch) {
|
|
1725
|
+
throw new ClawtError(MESSAGES.BRANCH_OR_FILE_REQUIRED);
|
|
1726
|
+
}
|
|
1252
1727
|
if (!options.tasks || options.tasks.length === 0) {
|
|
1253
1728
|
const sanitized = sanitizeBranchName(options.branch);
|
|
1254
1729
|
if (checkBranchExists(sanitized)) {
|
|
@@ -1265,52 +1740,10 @@ async function handleRun(options) {
|
|
|
1265
1740
|
throw new ClawtError("\u4EFB\u52A1\u5217\u8868\u4E0D\u80FD\u4E3A\u7A7A");
|
|
1266
1741
|
}
|
|
1267
1742
|
const count = tasks.length;
|
|
1268
|
-
|
|
1743
|
+
const concurrency = parseConcurrency(options.concurrency, getConfigValue("maxConcurrency"));
|
|
1744
|
+
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
1745
|
const worktrees = createWorktrees(options.branch, count);
|
|
1270
|
-
|
|
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);
|
|
1746
|
+
await executeBatchTasks(worktrees, tasks, concurrency);
|
|
1314
1747
|
}
|
|
1315
1748
|
|
|
1316
1749
|
// src/commands/resume.ts
|
|
@@ -1637,7 +2070,7 @@ async function handleMerge(options) {
|
|
|
1637
2070
|
}
|
|
1638
2071
|
|
|
1639
2072
|
// src/commands/config.ts
|
|
1640
|
-
import
|
|
2073
|
+
import chalk5 from "chalk";
|
|
1641
2074
|
function registerConfigCommand(program2) {
|
|
1642
2075
|
const configCmd = program2.command("config").description("\u67E5\u770B\u548C\u7BA1\u7406\u5168\u5C40\u914D\u7F6E").action(() => {
|
|
1643
2076
|
handleConfig();
|
|
@@ -1650,7 +2083,7 @@ function handleConfig() {
|
|
|
1650
2083
|
const config = loadConfig();
|
|
1651
2084
|
logger.info("config \u547D\u4EE4\u6267\u884C\uFF0C\u5C55\u793A\u5168\u5C40\u914D\u7F6E");
|
|
1652
2085
|
printInfo(`
|
|
1653
|
-
${
|
|
2086
|
+
${chalk5.dim("\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84:")} ${CONFIG_PATH}
|
|
1654
2087
|
`);
|
|
1655
2088
|
printSeparator();
|
|
1656
2089
|
const keys = Object.keys(DEFAULT_CONFIG);
|
|
@@ -1660,8 +2093,8 @@ ${chalk4.dim("\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84:")} ${CONFIG_PATH}
|
|
|
1660
2093
|
const description = CONFIG_DESCRIPTIONS[key];
|
|
1661
2094
|
const formattedValue = formatConfigValue(value);
|
|
1662
2095
|
if (i === 0) printInfo("");
|
|
1663
|
-
printInfo(` ${
|
|
1664
|
-
printInfo(` ${
|
|
2096
|
+
printInfo(` ${chalk5.bold(key)}: ${formattedValue}`);
|
|
2097
|
+
printInfo(` ${chalk5.dim(description)}`);
|
|
1665
2098
|
printInfo("");
|
|
1666
2099
|
}
|
|
1667
2100
|
printSeparator();
|
|
@@ -1681,9 +2114,9 @@ async function handleConfigReset() {
|
|
|
1681
2114
|
}
|
|
1682
2115
|
function formatConfigValue(value) {
|
|
1683
2116
|
if (typeof value === "boolean") {
|
|
1684
|
-
return value ?
|
|
2117
|
+
return value ? chalk5.green("true") : chalk5.yellow("false");
|
|
1685
2118
|
}
|
|
1686
|
-
return
|
|
2119
|
+
return chalk5.cyan(String(value));
|
|
1687
2120
|
}
|
|
1688
2121
|
|
|
1689
2122
|
// src/commands/sync.ts
|
|
@@ -1770,7 +2203,7 @@ async function handleReset() {
|
|
|
1770
2203
|
}
|
|
1771
2204
|
|
|
1772
2205
|
// src/commands/status.ts
|
|
1773
|
-
import
|
|
2206
|
+
import chalk6 from "chalk";
|
|
1774
2207
|
function registerStatusCommand(program2) {
|
|
1775
2208
|
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
2209
|
handleStatus(options);
|
|
@@ -1866,7 +2299,7 @@ function printStatusAsJson(result) {
|
|
|
1866
2299
|
}
|
|
1867
2300
|
function printStatusAsText(result) {
|
|
1868
2301
|
printDoubleSeparator();
|
|
1869
|
-
printInfo(` ${
|
|
2302
|
+
printInfo(` ${chalk6.bold.cyan(MESSAGES.STATUS_TITLE(result.main.projectName))}`);
|
|
1870
2303
|
printDoubleSeparator();
|
|
1871
2304
|
printInfo("");
|
|
1872
2305
|
printMainSection(result.main);
|
|
@@ -1879,17 +2312,17 @@ function printStatusAsText(result) {
|
|
|
1879
2312
|
printDoubleSeparator();
|
|
1880
2313
|
}
|
|
1881
2314
|
function printMainSection(main) {
|
|
1882
|
-
printInfo(` ${
|
|
1883
|
-
printInfo(` \u5206\u652F: ${
|
|
2315
|
+
printInfo(` ${chalk6.bold("\u25C6")} ${chalk6.bold(MESSAGES.STATUS_MAIN_SECTION)}`);
|
|
2316
|
+
printInfo(` \u5206\u652F: ${chalk6.bold(main.branch)}`);
|
|
1884
2317
|
if (main.isClean) {
|
|
1885
|
-
printInfo(` \u72B6\u6001: ${
|
|
2318
|
+
printInfo(` \u72B6\u6001: ${chalk6.green("\u2713 \u5E72\u51C0")}`);
|
|
1886
2319
|
} else {
|
|
1887
|
-
printInfo(` \u72B6\u6001: ${
|
|
2320
|
+
printInfo(` \u72B6\u6001: ${chalk6.yellow("\u2717 \u6709\u672A\u63D0\u4EA4\u4FEE\u6539")}`);
|
|
1888
2321
|
}
|
|
1889
2322
|
printInfo("");
|
|
1890
2323
|
}
|
|
1891
2324
|
function printWorktreesSection(worktrees, total) {
|
|
1892
|
-
printInfo(` ${
|
|
2325
|
+
printInfo(` ${chalk6.bold("\u25C6")} ${chalk6.bold(MESSAGES.STATUS_WORKTREES_SECTION)} (${total} \u4E2A)`);
|
|
1893
2326
|
printInfo("");
|
|
1894
2327
|
if (worktrees.length === 0) {
|
|
1895
2328
|
printInfo(` ${MESSAGES.STATUS_NO_WORKTREES}`);
|
|
@@ -1901,39 +2334,39 @@ function printWorktreesSection(worktrees, total) {
|
|
|
1901
2334
|
}
|
|
1902
2335
|
function printWorktreeItem(wt) {
|
|
1903
2336
|
const statusLabel = formatChangeStatusLabel(wt.changeStatus);
|
|
1904
|
-
printInfo(` ${
|
|
2337
|
+
printInfo(` ${chalk6.bold("\u25CF")} ${chalk6.bold(wt.branch)} [${statusLabel}]`);
|
|
1905
2338
|
const parts = [];
|
|
1906
2339
|
if (wt.insertions > 0 || wt.deletions > 0) {
|
|
1907
|
-
parts.push(`${
|
|
2340
|
+
parts.push(`${chalk6.green(`+${wt.insertions}`)} ${chalk6.red(`-${wt.deletions}`)}`);
|
|
1908
2341
|
}
|
|
1909
2342
|
if (wt.commitsAhead > 0) {
|
|
1910
|
-
parts.push(
|
|
2343
|
+
parts.push(chalk6.yellow(`${wt.commitsAhead} \u4E2A\u672C\u5730\u63D0\u4EA4`));
|
|
1911
2344
|
}
|
|
1912
2345
|
if (wt.commitsBehind > 0) {
|
|
1913
|
-
parts.push(
|
|
2346
|
+
parts.push(chalk6.yellow(`\u843D\u540E\u4E3B\u5206\u652F ${wt.commitsBehind} \u4E2A\u63D0\u4EA4`));
|
|
1914
2347
|
} else {
|
|
1915
|
-
parts.push(
|
|
2348
|
+
parts.push(chalk6.green("\u4E0E\u4E3B\u5206\u652F\u540C\u6B65"));
|
|
1916
2349
|
}
|
|
1917
2350
|
printInfo(` ${parts.join(" ")}`);
|
|
1918
2351
|
if (wt.hasSnapshot) {
|
|
1919
|
-
printInfo(` ${
|
|
2352
|
+
printInfo(` ${chalk6.blue("\u6709 validate \u5FEB\u7167")}`);
|
|
1920
2353
|
}
|
|
1921
2354
|
printInfo("");
|
|
1922
2355
|
}
|
|
1923
2356
|
function formatChangeStatusLabel(status) {
|
|
1924
2357
|
switch (status) {
|
|
1925
2358
|
case "committed":
|
|
1926
|
-
return
|
|
2359
|
+
return chalk6.green(MESSAGES.STATUS_CHANGE_COMMITTED);
|
|
1927
2360
|
case "uncommitted":
|
|
1928
|
-
return
|
|
2361
|
+
return chalk6.yellow(MESSAGES.STATUS_CHANGE_UNCOMMITTED);
|
|
1929
2362
|
case "conflict":
|
|
1930
|
-
return
|
|
2363
|
+
return chalk6.red(MESSAGES.STATUS_CHANGE_CONFLICT);
|
|
1931
2364
|
case "clean":
|
|
1932
|
-
return
|
|
2365
|
+
return chalk6.gray(MESSAGES.STATUS_CHANGE_CLEAN);
|
|
1933
2366
|
}
|
|
1934
2367
|
}
|
|
1935
2368
|
function printSnapshotsSection(snapshots) {
|
|
1936
|
-
printInfo(` ${
|
|
2369
|
+
printInfo(` ${chalk6.bold("\u25C6")} ${chalk6.bold(MESSAGES.STATUS_SNAPSHOTS_SECTION)} (${snapshots.length} \u4E2A)`);
|
|
1937
2370
|
printInfo("");
|
|
1938
2371
|
if (snapshots.length === 0) {
|
|
1939
2372
|
printInfo(` ${MESSAGES.STATUS_NO_SNAPSHOTS}`);
|
|
@@ -1941,8 +2374,8 @@ function printSnapshotsSection(snapshots) {
|
|
|
1941
2374
|
return;
|
|
1942
2375
|
}
|
|
1943
2376
|
for (const snap of snapshots) {
|
|
1944
|
-
const orphanLabel = snap.worktreeExists ? "" : ` ${
|
|
1945
|
-
const icon = snap.worktreeExists ?
|
|
2377
|
+
const orphanLabel = snap.worktreeExists ? "" : ` ${chalk6.yellow(MESSAGES.STATUS_SNAPSHOT_ORPHANED)}`;
|
|
2378
|
+
const icon = snap.worktreeExists ? chalk6.blue("\u25CF") : chalk6.yellow("\u26A0");
|
|
1946
2379
|
printInfo(` ${icon} ${snap.branch}${orphanLabel}`);
|
|
1947
2380
|
}
|
|
1948
2381
|
printInfo("");
|