clawt 2.16.3 → 2.16.5
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 +13 -1
- package/README.md +4 -0
- package/dist/index.js +379 -119
- package/dist/postinstall.js +12 -3
- package/docs/spec.md +97 -12
- package/package.json +2 -1
- package/src/commands/sync.ts +34 -14
- package/src/commands/validate.ts +50 -8
- package/src/constants/index.ts +12 -2
- package/src/constants/messages/run.ts +4 -4
- package/src/constants/messages/validate.ts +12 -0
- package/src/constants/progress.ts +36 -6
- package/src/utils/index.ts +2 -0
- package/src/utils/progress-render.ts +77 -13
- package/src/utils/progress.ts +110 -24
- package/src/utils/stream-parser.ts +251 -0
- package/src/utils/task-executor.ts +61 -27
- package/tests/unit/utils/progress-render.test.ts +96 -0
- package/tests/unit/utils/progress.test.ts +391 -10
- package/tests/unit/utils/stream-parser.test.ts +375 -0
|
@@ -7,6 +7,8 @@ import { cleanupWorktrees } from './worktree.js';
|
|
|
7
7
|
import { getConfigValue } from './config.js';
|
|
8
8
|
import { printSuccess, printWarning, printInfo, printDoubleSeparator, confirmAction } from './formatter.js';
|
|
9
9
|
import { ProgressRenderer } from './progress.js';
|
|
10
|
+
import { createLineBuffer, parseStreamLine, parseStreamEvent, truncateText } from './stream-parser.js';
|
|
11
|
+
import { RESULT_PREVIEW_MAX_LENGTH } from '../constants/index.js';
|
|
10
12
|
|
|
11
13
|
/** executeClaudeTask 的返回结构,包含子进程引用和结果 Promise */
|
|
12
14
|
interface ClaudeTaskHandle {
|
|
@@ -17,15 +19,23 @@ interface ClaudeTaskHandle {
|
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
|
-
*
|
|
22
|
+
* 活动更新回调函数类型
|
|
23
|
+
* @param {string} activityText - 活动描述文本
|
|
24
|
+
*/
|
|
25
|
+
type ActivityCallback = (activityText: string) => void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 在指定 worktree 中执行 Claude Code 任务,使用 stream-json 格式获取实时事件
|
|
21
29
|
* @param {WorktreeInfo} worktree - worktree 信息
|
|
22
30
|
* @param {string} task - 任务描述
|
|
31
|
+
* @param {ActivityCallback} [onActivity] - 活动更新回调(可选)
|
|
23
32
|
* @returns {ClaudeTaskHandle} 包含子进程引用和结果 Promise
|
|
24
33
|
*/
|
|
25
|
-
function executeClaudeTask(worktree: WorktreeInfo, task: string): ClaudeTaskHandle {
|
|
34
|
+
function executeClaudeTask(worktree: WorktreeInfo, task: string, onActivity?: ActivityCallback): ClaudeTaskHandle {
|
|
35
|
+
// 旧版使用 --output-format json,现改为 stream-json --verbose 以支持实时活动信息
|
|
26
36
|
const child = spawnProcess(
|
|
27
37
|
'claude',
|
|
28
|
-
['-p', task, '--output-format', 'json', '--permission-mode', 'bypassPermissions'],
|
|
38
|
+
['-p', task, '--output-format', 'stream-json', '--verbose', '--permission-mode', 'bypassPermissions'],
|
|
29
39
|
{
|
|
30
40
|
cwd: worktree.path,
|
|
31
41
|
// stdin 必须设置为 'ignore',不能用 'pipe'
|
|
@@ -37,11 +47,27 @@ function executeClaudeTask(worktree: WorktreeInfo, task: string): ClaudeTaskHand
|
|
|
37
47
|
);
|
|
38
48
|
|
|
39
49
|
const promise = new Promise<TaskResult>((resolve) => {
|
|
40
|
-
let stdout = '';
|
|
41
50
|
let stderr = '';
|
|
51
|
+
let finalResult: ClaudeCodeResult | null = null;
|
|
52
|
+
const lineBuffer = createLineBuffer();
|
|
42
53
|
|
|
43
54
|
child.stdout?.on('data', (data: Buffer) => {
|
|
44
|
-
|
|
55
|
+
const lines = lineBuffer.push(data.toString());
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
const event = parseStreamLine(line);
|
|
58
|
+
if (!event) continue;
|
|
59
|
+
|
|
60
|
+
const parsed = parseStreamEvent(event);
|
|
61
|
+
if (!parsed) continue;
|
|
62
|
+
|
|
63
|
+
if (parsed.kind === 'result') {
|
|
64
|
+
// 最终结果事件
|
|
65
|
+
finalResult = parsed.result ?? null;
|
|
66
|
+
} else if (parsed.activityText && onActivity) {
|
|
67
|
+
// 活动事件,回调通知
|
|
68
|
+
onActivity(parsed.activityText);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
45
71
|
});
|
|
46
72
|
|
|
47
73
|
child.stderr?.on('data', (data: Buffer) => {
|
|
@@ -49,16 +75,21 @@ function executeClaudeTask(worktree: WorktreeInfo, task: string): ClaudeTaskHand
|
|
|
49
75
|
});
|
|
50
76
|
|
|
51
77
|
child.on('close', (code) => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
78
|
+
// 处理缓冲区中可能残留的最后一行
|
|
79
|
+
const remaining = lineBuffer.flush();
|
|
80
|
+
if (remaining) {
|
|
81
|
+
const event = parseStreamLine(remaining);
|
|
82
|
+
if (event) {
|
|
83
|
+
const parsed = parseStreamEvent(event);
|
|
84
|
+
if (parsed?.kind === 'result') {
|
|
85
|
+
finalResult = parsed.result ?? null;
|
|
86
|
+
}
|
|
59
87
|
}
|
|
60
|
-
}
|
|
61
|
-
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let success = code === 0;
|
|
91
|
+
if (finalResult) {
|
|
92
|
+
success = !finalResult.is_error;
|
|
62
93
|
}
|
|
63
94
|
|
|
64
95
|
resolve({
|
|
@@ -66,7 +97,7 @@ function executeClaudeTask(worktree: WorktreeInfo, task: string): ClaudeTaskHand
|
|
|
66
97
|
branch: worktree.branch,
|
|
67
98
|
worktreePath: worktree.path,
|
|
68
99
|
success,
|
|
69
|
-
result,
|
|
100
|
+
result: finalResult,
|
|
70
101
|
error: success ? undefined : stderr || '任务执行失败',
|
|
71
102
|
});
|
|
72
103
|
});
|
|
@@ -134,16 +165,25 @@ async function handleInterruptCleanup(worktrees: WorktreeInfo[]): Promise<void>
|
|
|
134
165
|
* @param {number} startTime - 任务批次启动时间戳
|
|
135
166
|
*/
|
|
136
167
|
function updateRendererStatus(renderer: ProgressRenderer, index: number, result: TaskResult, startTime: number): void {
|
|
168
|
+
// 从 ClaudeCodeResult.result 中提取结果预览文本
|
|
169
|
+
// 先清理换行符和多余空白(结果文本可能包含多行),再截断到最大长度
|
|
170
|
+
const rawResultText = result.result?.result;
|
|
171
|
+
const resultPreview = rawResultText
|
|
172
|
+
? truncateText(rawResultText.replace(/[\n\r\t]+/g, ' ').trim(), RESULT_PREVIEW_MAX_LENGTH)
|
|
173
|
+
: undefined;
|
|
174
|
+
|
|
137
175
|
if (result.success) {
|
|
138
176
|
renderer.markDone(
|
|
139
177
|
index,
|
|
140
178
|
result.result?.duration_ms ?? (Date.now() - startTime),
|
|
141
179
|
result.result?.total_cost_usd ?? 0,
|
|
180
|
+
resultPreview,
|
|
142
181
|
);
|
|
143
182
|
} else {
|
|
144
183
|
renderer.markFailed(
|
|
145
184
|
index,
|
|
146
185
|
result.result?.duration_ms ?? (Date.now() - startTime),
|
|
186
|
+
resultPreview,
|
|
147
187
|
);
|
|
148
188
|
}
|
|
149
189
|
}
|
|
@@ -192,13 +232,10 @@ async function executeWithConcurrency(
|
|
|
192
232
|
// 标记为运行中
|
|
193
233
|
renderer.markRunning(index);
|
|
194
234
|
|
|
195
|
-
const handle = executeClaudeTask(wt, task)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// 监听 stderr 输出,更新任务活动时间戳
|
|
199
|
-
handle.child.stderr?.on('data', () => {
|
|
200
|
-
renderer.updateActivity(index);
|
|
235
|
+
const handle = executeClaudeTask(wt, task, (activityText) => {
|
|
236
|
+
renderer.updateActivityText(index, activityText);
|
|
201
237
|
});
|
|
238
|
+
childProcesses.push(handle.child);
|
|
202
239
|
|
|
203
240
|
handle.promise.then((result) => {
|
|
204
241
|
results[index] = result;
|
|
@@ -248,13 +285,10 @@ async function executeAllParallel(
|
|
|
248
285
|
const handles = worktrees.map((wt, index) => {
|
|
249
286
|
const task = tasks[index];
|
|
250
287
|
logger.info(`启动任务 ${index + 1}: ${task} (worktree: ${wt.path})`);
|
|
251
|
-
const handle = executeClaudeTask(wt, task)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
// 监听 stderr 输出,更新任务活动时间戳
|
|
255
|
-
handle.child.stderr?.on('data', () => {
|
|
256
|
-
renderer.updateActivity(index);
|
|
288
|
+
const handle = executeClaudeTask(wt, task, (activityText) => {
|
|
289
|
+
renderer.updateActivityText(index, activityText);
|
|
257
290
|
});
|
|
291
|
+
childProcesses.push(handle.child);
|
|
258
292
|
|
|
259
293
|
return handle;
|
|
260
294
|
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { truncateToTerminalWidth } from '../../../src/utils/progress-render.js';
|
|
4
|
+
|
|
5
|
+
describe('truncateToTerminalWidth', () => {
|
|
6
|
+
describe('纯文本截断', () => {
|
|
7
|
+
it('文本未超限时原样返回', () => {
|
|
8
|
+
const text = 'hello world';
|
|
9
|
+
const result = truncateToTerminalWidth(text, 20);
|
|
10
|
+
expect(result).toBe('hello world');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('文本恰好等于宽度时原样返回', () => {
|
|
14
|
+
const text = 'abcde';
|
|
15
|
+
const result = truncateToTerminalWidth(text, 5);
|
|
16
|
+
expect(result).toBe('abcde');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('文本超限时截断到指定宽度', () => {
|
|
20
|
+
const text = 'hello world, this is a long string';
|
|
21
|
+
const result = truncateToTerminalWidth(text, 11);
|
|
22
|
+
// 截断后追加 ANSI 重置序列
|
|
23
|
+
expect(result).toBe('hello world\x1B[0m');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('空字符串直接返回', () => {
|
|
27
|
+
const result = truncateToTerminalWidth('', 10);
|
|
28
|
+
expect(result).toBe('');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('宽度为 0 时返回空内容加重置', () => {
|
|
32
|
+
const result = truncateToTerminalWidth('abc', 0);
|
|
33
|
+
expect(result).toBe('\x1B[0m');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('含 ANSI 颜色码的截断', () => {
|
|
38
|
+
it('含颜色的文本未超限时原样返回', () => {
|
|
39
|
+
// chalk.red('hi') 的可见宽度只有 2
|
|
40
|
+
const text = chalk.red('hi');
|
|
41
|
+
const result = truncateToTerminalWidth(text, 10);
|
|
42
|
+
expect(result).toBe(text);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('含颜色的文本超限时正确截断', () => {
|
|
46
|
+
// 构造一个已知可见宽度的带颜色字符串
|
|
47
|
+
const text = chalk.red('abcdefghij'); // 可见宽度 10
|
|
48
|
+
const result = truncateToTerminalWidth(text, 5);
|
|
49
|
+
// 截断后的可见内容应只有 5 个字符
|
|
50
|
+
const stripped = result.replace(/\x1B\[[0-9;]*m/g, '');
|
|
51
|
+
expect(stripped.length).toBe(5);
|
|
52
|
+
expect(stripped).toBe('abcde');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('多段颜色混合时正确截断', () => {
|
|
56
|
+
const text = chalk.red('abc') + chalk.green('defgh');
|
|
57
|
+
const result = truncateToTerminalWidth(text, 5);
|
|
58
|
+
const stripped = result.replace(/\x1B\[[0-9;]*m/g, '');
|
|
59
|
+
expect(stripped.length).toBe(5);
|
|
60
|
+
expect(stripped).toBe('abcde');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('截断后追加 ANSI 重置序列', () => {
|
|
64
|
+
const text = chalk.red('abcdefghij');
|
|
65
|
+
const result = truncateToTerminalWidth(text, 5);
|
|
66
|
+
// 应以 \x1B[0m 结尾
|
|
67
|
+
expect(result).toMatch(/\x1B\[0m$/);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('中文/宽字符截断', () => {
|
|
72
|
+
it('中文字符占 2 列宽度', () => {
|
|
73
|
+
const text = '你好世界测试';
|
|
74
|
+
const result = truncateToTerminalWidth(text, 8);
|
|
75
|
+
// 4 个中文字符 = 8 列宽
|
|
76
|
+
const stripped = result.replace(/\x1B\[[0-9;]*m/g, '');
|
|
77
|
+
expect(stripped).toBe('你好世界');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('宽度刚好不够放下一个中文字符时不截断一半', () => {
|
|
81
|
+
const text = '你好世界';
|
|
82
|
+
const result = truncateToTerminalWidth(text, 5);
|
|
83
|
+
// 5 列只能放 2 个中文字符(4列),第 3 个字符需要 6 列放不下
|
|
84
|
+
const stripped = result.replace(/\x1B\[[0-9;]*m/g, '');
|
|
85
|
+
expect(stripped).toBe('你好');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('中英混合文本正确截断', () => {
|
|
89
|
+
const text = 'hi你好world';
|
|
90
|
+
const result = truncateToTerminalWidth(text, 6);
|
|
91
|
+
// 'h'=1, 'i'=1, '你'=2, '好'=2 => 总计 6
|
|
92
|
+
const stripped = result.replace(/\x1B\[[0-9;]*m/g, '');
|
|
93
|
+
expect(stripped).toBe('hi你好');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|