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/.claude/agent-memory/docs-sync-updater/MEMORY.md +8 -2
- package/README.md +27 -2
- package/dist/index.js +644 -189
- package/dist/postinstall.js +32 -1
- package/docs/spec.md +59 -8
- package/package.json +1 -1
- package/src/commands/resume.ts +2 -2
- package/src/commands/run.ts +68 -206
- package/src/constants/config.ts +4 -0
- package/src/constants/index.ts +11 -1
- package/src/constants/messages/run.ts +30 -0
- package/src/constants/paths.ts +3 -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/claude.ts +50 -2
- package/src/utils/formatter.ts +16 -0
- package/src/utils/index.ts +7 -3
- 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/resume.test.ts +1 -1
- package/tests/unit/commands/run.test.ts +259 -10
- package/tests/unit/constants/config.test.ts +1 -0
- package/tests/unit/utils/claude.test.ts +115 -1
- 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
|
@@ -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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
857
|
-
import { existsSync as
|
|
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
|
|
959
|
+
return join4(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
|
|
860
960
|
}
|
|
861
961
|
function getSnapshotHeadPath(projectName, branchName) {
|
|
862
|
-
return
|
|
962
|
+
return join4(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
|
|
863
963
|
}
|
|
864
964
|
function hasSnapshot(projectName, branchName) {
|
|
865
|
-
return
|
|
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 =
|
|
872
|
-
const headCommitHash =
|
|
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 =
|
|
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 (
|
|
987
|
+
if (existsSync6(snapshotPath)) {
|
|
888
988
|
unlinkSync(snapshotPath);
|
|
889
989
|
logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${snapshotPath}`);
|
|
890
990
|
}
|
|
891
|
-
if (
|
|
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 =
|
|
898
|
-
if (!
|
|
997
|
+
const projectDir = join4(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
998
|
+
if (!existsSync6(projectDir)) {
|
|
899
999
|
return [];
|
|
900
1000
|
}
|
|
901
|
-
const files =
|
|
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 =
|
|
906
|
-
if (!
|
|
1005
|
+
const projectDir = join4(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
1006
|
+
if (!existsSync6(projectDir)) {
|
|
907
1007
|
return;
|
|
908
1008
|
}
|
|
909
|
-
const files =
|
|
1009
|
+
const files = readdirSync4(projectDir);
|
|
910
1010
|
for (const file of files) {
|
|
911
|
-
unlinkSync(
|
|
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/
|
|
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 ?
|
|
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(` ${
|
|
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").
|
|
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
|
|
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}`);
|
|
1711
|
+
function parseConcurrency(optionValue, configValue) {
|
|
1712
|
+
if (optionValue === void 0) {
|
|
1713
|
+
return configValue;
|
|
1218
1714
|
}
|
|
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;
|
|
1715
|
+
const parsed = parseInt(optionValue, 10);
|
|
1716
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
1717
|
+
throw new ClawtError(MESSAGES.CONCURRENCY_INVALID);
|
|
1240
1718
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
${
|
|
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(` ${
|
|
1664
|
-
printInfo(` ${
|
|
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 ?
|
|
2139
|
+
return value ? chalk5.green("true") : chalk5.yellow("false");
|
|
1685
2140
|
}
|
|
1686
|
-
return
|
|
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
|
|
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(` ${
|
|
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(` ${
|
|
1883
|
-
printInfo(` \u5206\u652F: ${
|
|
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: ${
|
|
2340
|
+
printInfo(` \u72B6\u6001: ${chalk6.green("\u2713 \u5E72\u51C0")}`);
|
|
1886
2341
|
} else {
|
|
1887
|
-
printInfo(` \u72B6\u6001: ${
|
|
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(` ${
|
|
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(` ${
|
|
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(`${
|
|
2362
|
+
parts.push(`${chalk6.green(`+${wt.insertions}`)} ${chalk6.red(`-${wt.deletions}`)}`);
|
|
1908
2363
|
}
|
|
1909
2364
|
if (wt.commitsAhead > 0) {
|
|
1910
|
-
parts.push(
|
|
2365
|
+
parts.push(chalk6.yellow(`${wt.commitsAhead} \u4E2A\u672C\u5730\u63D0\u4EA4`));
|
|
1911
2366
|
}
|
|
1912
2367
|
if (wt.commitsBehind > 0) {
|
|
1913
|
-
parts.push(
|
|
2368
|
+
parts.push(chalk6.yellow(`\u843D\u540E\u4E3B\u5206\u652F ${wt.commitsBehind} \u4E2A\u63D0\u4EA4`));
|
|
1914
2369
|
} else {
|
|
1915
|
-
parts.push(
|
|
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(` ${
|
|
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
|
|
2381
|
+
return chalk6.green(MESSAGES.STATUS_CHANGE_COMMITTED);
|
|
1927
2382
|
case "uncommitted":
|
|
1928
|
-
return
|
|
2383
|
+
return chalk6.yellow(MESSAGES.STATUS_CHANGE_UNCOMMITTED);
|
|
1929
2384
|
case "conflict":
|
|
1930
|
-
return
|
|
2385
|
+
return chalk6.red(MESSAGES.STATUS_CHANGE_CONFLICT);
|
|
1931
2386
|
case "clean":
|
|
1932
|
-
return
|
|
2387
|
+
return chalk6.gray(MESSAGES.STATUS_CHANGE_CLEAN);
|
|
1933
2388
|
}
|
|
1934
2389
|
}
|
|
1935
2390
|
function printSnapshotsSection(snapshots) {
|
|
1936
|
-
printInfo(` ${
|
|
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 ? "" : ` ${
|
|
1945
|
-
const icon = snap.worktreeExists ?
|
|
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("");
|