clawt 3.5.0 → 3.5.2

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.
@@ -36,6 +36,8 @@ export interface MergeOptions {
36
36
  branch?: string;
37
37
  /** 提交信息(目标 worktree 工作区有修改时必填) */
38
38
  message?: string;
39
+ /** 遇到冲突直接调用 AI 解决,不再询问 */
40
+ auto?: boolean;
39
41
  }
40
42
 
41
43
  /** remove 命令选项 */
@@ -18,6 +18,10 @@ export interface ClawtConfig {
18
18
  aliases: Record<string, string>;
19
19
  /** 是否启用自动更新检查 */
20
20
  autoUpdate: boolean;
21
+ /** merge 冲突时的解决模式:ask(询问)、auto(自动 AI 解决)、manual(手动解决) */
22
+ conflictResolveMode: 'ask' | 'auto' | 'manual';
23
+ /** Claude Code 冲突解决超时时间(毫秒),默认 300000(5 分钟) */
24
+ conflictResolveTimeoutMs: number;
21
25
  }
22
26
 
23
27
  /** 单个配置项的完整定义(默认值 + 描述) */
@@ -0,0 +1,52 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { logger } from '../logger/index.js';
3
+
4
+ /**
5
+ * 根据当前操作系统获取剪贴板写入命令
6
+ * @returns {{ command: string, args: string[] } | null} 剪贴板命令配置,不支持的平台返回 null
7
+ */
8
+ function getClipboardCommand(): { command: string; args: string[] } | null {
9
+ switch (process.platform) {
10
+ case 'darwin':
11
+ return { command: 'pbcopy', args: [] };
12
+ case 'linux':
13
+ return { command: 'xclip', args: ['-selection', 'clipboard'] };
14
+ case 'win32':
15
+ return { command: 'clip', args: [] };
16
+ default:
17
+ return null;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * 将文本复制到系统剪贴板
23
+ * 跨平台支持:macOS (pbcopy)、Linux (xclip)、Windows (clip)
24
+ * 失败时静默返回 false,不影响主流程
25
+ * @param {string} text - 要复制到剪贴板的文本
26
+ * @returns {boolean} 复制是否成功
27
+ */
28
+ export function copyToClipboard(text: string): boolean {
29
+ try {
30
+ const clipboardCmd = getClipboardCommand();
31
+ if (!clipboardCmd) {
32
+ logger.debug(`不支持的平台: ${process.platform},跳过剪贴板复制`);
33
+ return false;
34
+ }
35
+
36
+ const result = spawnSync(clipboardCmd.command, clipboardCmd.args, {
37
+ input: text,
38
+ encoding: 'utf-8',
39
+ stdio: ['pipe', 'pipe', 'pipe'],
40
+ });
41
+
42
+ if (result.status !== 0) {
43
+ logger.debug(`剪贴板命令执行失败,退出码: ${result.status}`);
44
+ return false;
45
+ }
46
+
47
+ return true;
48
+ } catch (error) {
49
+ logger.debug(`剪贴板复制异常: ${(error as Error).message}`);
50
+ return false;
51
+ }
52
+ }
@@ -0,0 +1,170 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { logger } from '../logger/index.js';
3
+ import { ClawtError } from '../errors/index.js';
4
+ import { getConfigValue } from './config.js';
5
+ import { getConflictFiles, gitAddFiles, gitMergeContinue } from './git.js';
6
+ import { printInfo, printSuccess, printWarning } from './formatter.js';
7
+ import { confirmAction } from './formatter.js';
8
+ import { MESSAGES } from '../constants/index.js';
9
+ import { CONFLICT_RESOLVE_PROMPT } from '../constants/ai-prompts.js';
10
+
11
+ /** 默认 Claude Code 冲突解决超时时间(毫秒) */
12
+ const DEFAULT_CONFLICT_RESOLVE_TIMEOUT_MS = 300000;
13
+
14
+ /**
15
+ * 构建发送给 Claude Code 的冲突解决 prompt
16
+ * Claude Code 会自动读取当前工作目录下的冲突文件,无需手动传入
17
+ * @returns {string} 纯指令性 AI prompt
18
+ */
19
+ export function buildConflictResolvePrompt(): string {
20
+ return CONFLICT_RESOLVE_PROMPT;
21
+ }
22
+
23
+ /**
24
+ * 获取冲突解决超时时间(毫秒)
25
+ * 优先使用配置项,未配置时使用默认值
26
+ * @returns {number} 超时时间(毫秒)
27
+ */
28
+ function getConflictResolveTimeout(): number {
29
+ const configValue = getConfigValue('conflictResolveTimeoutMs');
30
+ if (typeof configValue === 'number' && configValue > 0) {
31
+ return configValue;
32
+ }
33
+ return DEFAULT_CONFLICT_RESOLVE_TIMEOUT_MS;
34
+ }
35
+
36
+ /**
37
+ * 调用 Claude Code CLI 以非交互模式解决冲突
38
+ * 使用 execFileSync 数组参数形式避免 shell 注入
39
+ * @param {string} prompt - AI prompt
40
+ * @param {string} cwd - 工作目录
41
+ * @returns {string} Claude Code 的输出
42
+ */
43
+ export function invokeClaudeForConflictResolve(prompt: string, cwd: string): string {
44
+ const args = ['-p', prompt, '--permission-mode', 'bypassPermissions'];
45
+
46
+ logger.info(`调用 Claude Code 解决冲突,命令: claude -p "..." --permission-mode bypassPermissions`);
47
+
48
+ try {
49
+ const output = execFileSync('claude', args, {
50
+ cwd,
51
+ encoding: 'utf-8',
52
+ stdio: ['pipe', 'pipe', 'pipe'],
53
+ timeout: getConflictResolveTimeout(),
54
+ });
55
+ return output;
56
+ } catch (error: unknown) {
57
+ const errMsg = error instanceof Error ? error.message : String(error);
58
+ logger.error(`Claude Code 冲突解决失败: ${errMsg}`);
59
+ throw new ClawtError(MESSAGES.MERGE_CONFLICT_AI_FAILED(errMsg));
60
+ }
61
+ }
62
+
63
+ /**
64
+ * 执行 AI 辅助冲突解决的完整流程
65
+ * 1. 获取冲突文件列表
66
+ * 2. 构建 AI prompt
67
+ * 3. 调用 Claude Code 解决冲突
68
+ * 4. 检查冲突是否解决
69
+ * 5. git add 已解决的文件并 merge --continue
70
+ * @param {string} currentBranch - 当前分支名
71
+ * @param {string} incomingBranch - 传入分支名
72
+ * @param {string} cwd - 工作目录
73
+ * @returns {boolean} 是否所有冲突都已解决
74
+ */
75
+ export function resolveConflictsWithAI(
76
+ currentBranch: string,
77
+ incomingBranch: string,
78
+ cwd: string,
79
+ ): boolean {
80
+ const conflictFiles = getConflictFiles(cwd);
81
+ if (conflictFiles.length === 0) {
82
+ return true;
83
+ }
84
+
85
+ printInfo(MESSAGES.MERGE_CONFLICT_AI_START(conflictFiles.length));
86
+
87
+ // 构建 prompt 并调用 Claude Code,捕获异常提供恢复提示
88
+ const prompt = buildConflictResolvePrompt();
89
+ try {
90
+ invokeClaudeForConflictResolve(prompt, cwd);
91
+ } catch (error) {
92
+ // AI 调用失败时输出友好提示,告知用户可手动解决或重试
93
+ const errMsg = error instanceof ClawtError ? error.message : String(error);
94
+ printWarning(errMsg);
95
+ return false;
96
+ }
97
+
98
+ // 检查冲突是否已解决
99
+ const remainingConflicts = getConflictFiles(cwd);
100
+
101
+ if (remainingConflicts.length === 0) {
102
+ // 所有冲突已解决,git add 所有原冲突文件并完成 merge
103
+ gitAddFiles(conflictFiles, cwd);
104
+ gitMergeContinue(cwd);
105
+ printSuccess(MESSAGES.MERGE_CONFLICT_AI_SUCCESS);
106
+ return true;
107
+ }
108
+
109
+ // 部分冲突已解决,git add 已解决的文件
110
+ const resolvedFiles = conflictFiles.filter((f) => !remainingConflicts.includes(f));
111
+ if (resolvedFiles.length > 0) {
112
+ gitAddFiles(resolvedFiles, cwd);
113
+ }
114
+
115
+ printWarning(MESSAGES.MERGE_CONFLICT_AI_PARTIAL(remainingConflicts.length));
116
+ return false;
117
+ }
118
+
119
+ /**
120
+ * 根据配置和命令选项判断冲突解决模式
121
+ * @param {boolean} [autoFlag] - --auto 命令行参数
122
+ * @returns {'ask' | 'auto' | 'manual'} 冲突解决模式
123
+ */
124
+ export function determineConflictResolveMode(autoFlag?: boolean): 'ask' | 'auto' | 'manual' {
125
+ // --auto 命令行参数优先级最高
126
+ if (autoFlag === true) {
127
+ return 'auto';
128
+ }
129
+
130
+ const configMode = getConfigValue('conflictResolveMode');
131
+ if (configMode === 'auto' || configMode === 'manual') {
132
+ return configMode;
133
+ }
134
+
135
+ return 'ask';
136
+ }
137
+
138
+ /**
139
+ * 处理合并冲突的入口函数
140
+ * 根据冲突解决模式决定处理方式:auto 直接 AI 解决,ask 询问用户,manual 抛出错误
141
+ * @param {string} currentBranch - 当前分支名
142
+ * @param {string} incomingBranch - 传入分支名
143
+ * @param {string} cwd - 工作目录
144
+ * @param {boolean} [autoFlag] - --auto 命令行参数
145
+ * @returns {Promise<boolean>} 是否成功解决了所有冲突
146
+ */
147
+ export async function handleMergeConflict(
148
+ currentBranch: string,
149
+ incomingBranch: string,
150
+ cwd: string,
151
+ autoFlag?: boolean,
152
+ ): Promise<boolean> {
153
+ const mode = determineConflictResolveMode(autoFlag);
154
+
155
+ if (mode === 'manual') {
156
+ throw new ClawtError(MESSAGES.MERGE_CONFLICT_MANUAL);
157
+ }
158
+
159
+ if (mode === 'auto') {
160
+ return resolveConflictsWithAI(currentBranch, incomingBranch, cwd);
161
+ }
162
+
163
+ // mode === 'ask'
164
+ const shouldUseAI = await confirmAction(MESSAGES.MERGE_CONFLICT_ASK_AI);
165
+ if (!shouldUseAI) {
166
+ throw new ClawtError(MESSAGES.MERGE_CONFLICT_MANUAL);
167
+ }
168
+
169
+ return resolveConflictsWithAI(currentBranch, incomingBranch, cwd);
170
+ }
@@ -1,5 +1,5 @@
1
1
  import { basename } from 'node:path';
2
- import { execSync } from 'node:child_process';
2
+ import { execSync, execFileSync } from 'node:child_process';
3
3
  import { execCommand, execCommandWithInput } from './shell.js';
4
4
  import { logger } from '../logger/index.js';
5
5
 
@@ -367,3 +367,51 @@ export function gitApplyCachedCheck(patchContent: Buffer, cwd?: string): boolean
367
367
  return false;
368
368
  }
369
369
  }
370
+
371
+ /**
372
+ * 获取冲突文件列表
373
+ * 通过 git status --porcelain 解析 UU/AA/DD 等冲突标记
374
+ * @param {string} [cwd] - 工作目录
375
+ * @returns {string[]} 冲突文件路径列表
376
+ */
377
+ export function getConflictFiles(cwd?: string): string[] {
378
+ const status = getStatusPorcelain(cwd);
379
+ if (!status) return [];
380
+ return status
381
+ .split('\n')
382
+ .filter((line) => /^(UU|AA|DD|DU|UD|AU|UA)/.test(line))
383
+ .map((line) => line.slice(3));
384
+ }
385
+
386
+ /**
387
+ * git add 指定文件
388
+ * 使用 execFileSync 数组参数形式避免文件名中特殊字符导致的注入风险
389
+ * @param {string[]} files - 文件路径列表
390
+ * @param {string} [cwd] - 工作目录
391
+ */
392
+ export function gitAddFiles(files: string[], cwd?: string): void {
393
+ if (files.length === 0) return;
394
+ const args = ['add', '--', ...files];
395
+ logger.debug(`执行命令: git ${args.join(' ')}${cwd ? ` (cwd: ${cwd})` : ''}`);
396
+ execFileSync('git', args, {
397
+ cwd,
398
+ encoding: 'utf-8',
399
+ stdio: ['pipe', 'pipe', 'pipe'],
400
+ });
401
+ }
402
+
403
+ /**
404
+ * git merge --continue(非交互式)
405
+ * @param {string} [cwd] - 工作目录
406
+ */
407
+ export function gitMergeContinue(cwd?: string): void {
408
+ execCommand('GIT_EDITOR=true git merge --continue', { cwd });
409
+ }
410
+
411
+ /**
412
+ * git merge --abort
413
+ * @param {string} [cwd] - 工作目录
414
+ */
415
+ export function gitMergeAbort(cwd?: string): void {
416
+ execCommand('git merge --abort', { cwd });
417
+ }
@@ -1,5 +1,6 @@
1
- export { execCommand, spawnProcess, killAllChildProcesses, execCommandWithInput, runCommandInherited, parseParallelCommands, runParallelCommands } from './shell.js';
2
- export type { ParallelCommandResult } from './shell.js';
1
+ export { execCommand, spawnProcess, killAllChildProcesses, execCommandWithInput, runCommandInherited, parseParallelCommands, runParallelCommands, runCommandWithStderrCapture, runParallelCommandsWithStderrCapture } from './shell.js';
2
+ export type { ParallelCommandResult, CommandResultWithStderr, ParallelCommandResultWithStderr } from './shell.js';
3
+ export { copyToClipboard } from './clipboard.js';
3
4
  export {
4
5
  getGitCommonDir,
5
6
  getGitTopLevel,
@@ -48,6 +49,10 @@ export {
48
49
  getBranchCreatedAt,
49
50
  gitCheckout,
50
51
  createBranch,
52
+ getConflictFiles,
53
+ gitAddFiles,
54
+ gitMergeContinue,
55
+ gitMergeAbort,
51
56
  } from './git.js';
52
57
  export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
53
58
  export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled, validateHeadExists, validateWorkingDirClean, runPreChecks } from './validation.js';
@@ -59,7 +64,7 @@ export { ensureDir, removeEmptyDir, calculateDirSize } from './fs.js';
59
64
  export { multilineInput } from './prompt.js';
60
65
  export { launchInteractiveClaude, hasClaudeSessionHistory, launchInteractiveClaudeInNewTerminal } from './claude.js';
61
66
  export { getSnapshotPath, hasSnapshot, getSnapshotModifiedTime, readSnapshotTreeHash, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots, getProjectSnapshotBranches } from './validate-snapshot.js';
62
- export { findExactMatch, findFuzzyMatches, promptSelectBranch, promptMultiSelectBranches, promptGroupedMultiSelectBranches, resolveTargetWorktree, resolveTargetWorktrees, groupWorktreesByDate, buildGroupedChoices, buildGroupMembershipMap, formatRelativeDate, getWorktreeCreatedDate, getWorktreeCreatedTime } from './worktree-matcher.js';
67
+ export { findExactMatch, findFuzzyMatches, promptGroupedMultiSelectBranches, resolveTargetWorktree, resolveTargetWorktrees, groupWorktreesByDate, buildGroupedChoices, buildGroupMembershipMap, formatRelativeDate, getWorktreeCreatedDate, getWorktreeCreatedTime } from './worktree-matcher.js';
63
68
  export type { WorktreeResolveMessages, WorktreeMultiResolveMessages } from './worktree-matcher.js';
64
69
  export { ProgressRenderer } from './progress.js';
65
70
  export { parseTaskFile, loadTaskFile, parseTasksFromOptions } from './task-file.js';
@@ -79,4 +84,5 @@ export { migrateChangesViaPatch, computeCurrentTreeHash, saveCurrentSnapshotTree
79
84
  export { InteractivePanel } from './interactive-panel.js';
80
85
  export { buildPanelFrame, buildGroupedWorktreeLines, buildDisplayOrder, renderDateSeparator, renderWorktreeBlock, renderSnapshotSummary, renderFooter, calculateVisibleRows } from './interactive-panel-render.js';
81
86
  export type { PanelLine } from './interactive-panel-render.js';
87
+ export { buildConflictResolvePrompt, invokeClaudeForConflictResolve, resolveConflictsWithAI, determineConflictResolveMode, handleMergeConflict } from './conflict-resolver.js';
82
88
 
@@ -11,6 +11,22 @@ export interface ParallelCommandResult {
11
11
  error?: string;
12
12
  }
13
13
 
14
+ /** 带 stderr 捕获的命令执行结果 */
15
+ export interface CommandResultWithStderr {
16
+ /** 进程退出码 */
17
+ exitCode: number;
18
+ /** 进程启动失败时的错误信息 */
19
+ error?: string;
20
+ /** 捕获的 stderr 输出内容 */
21
+ stderr: string;
22
+ }
23
+
24
+ /** 带 stderr 捕获的并行命令执行结果 */
25
+ export interface ParallelCommandResultWithStderr extends ParallelCommandResult {
26
+ /** 捕获的 stderr 输出内容 */
27
+ stderr: string;
28
+ }
29
+
14
30
  /**
15
31
  * 同步执行 shell 命令并返回 stdout
16
32
  * @param {string} command - 要执行的命令
@@ -157,3 +173,83 @@ export function runParallelCommands(
157
173
 
158
174
  return Promise.all(promises);
159
175
  }
176
+
177
+ /**
178
+ * 以 shell 模式启动子进程,捕获 stderr 同时实时回显到终端
179
+ * stdout 直接继承父进程(实时输出),stderr 通过 pipe 捕获并同步回显
180
+ * @param {string} command - 要执行的命令字符串
181
+ * @param {object} options - 可选配置
182
+ * @param {string} options.cwd - 工作目录
183
+ * @returns {Promise<{ exitCode: number, error?: string, stderr: string }>} 退出码、错误信息和 stderr 内容
184
+ */
185
+ function spawnWithStderrCapture(
186
+ command: string,
187
+ options?: { cwd?: string },
188
+ ): Promise<{ exitCode: number; error?: string; stderr: string }> {
189
+ return new Promise((resolve) => {
190
+ const child = spawn(command, {
191
+ cwd: options?.cwd,
192
+ stdio: ['inherit', 'inherit', 'pipe'],
193
+ shell: true,
194
+ });
195
+
196
+ const stderrChunks: Buffer[] = [];
197
+
198
+ child.stderr?.on('data', (chunk: Buffer) => {
199
+ // 实时回显到终端
200
+ process.stderr.write(chunk);
201
+ // 累积到 buffer
202
+ stderrChunks.push(chunk);
203
+ });
204
+
205
+ child.on('error', (err) => {
206
+ resolve({
207
+ exitCode: 1,
208
+ error: err.message,
209
+ stderr: Buffer.concat(stderrChunks).toString('utf-8'),
210
+ });
211
+ });
212
+
213
+ child.on('close', (code) => {
214
+ resolve({
215
+ exitCode: code ?? 1,
216
+ stderr: Buffer.concat(stderrChunks).toString('utf-8'),
217
+ });
218
+ });
219
+ });
220
+ }
221
+
222
+ /**
223
+ * 异步执行命令,捕获 stderr 同时实时回显到终端
224
+ * @param {string} command - 要执行的命令字符串
225
+ * @param {object} options - 可选配置
226
+ * @param {string} options.cwd - 工作目录
227
+ * @returns {Promise<CommandResultWithStderr>} 包含退出码和 stderr 内容的结果
228
+ */
229
+ export function runCommandWithStderrCapture(
230
+ command: string,
231
+ options?: { cwd?: string },
232
+ ): Promise<CommandResultWithStderr> {
233
+ logger.debug(`执行命令(stderr捕获): ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`);
234
+ return spawnWithStderrCapture(command, options);
235
+ }
236
+
237
+ /**
238
+ * 并行执行多个命令,捕获每个命令的 stderr 同时实时回显到终端
239
+ * @param {string[]} commands - 要并行执行的命令数组
240
+ * @param {object} options - 可选配置
241
+ * @param {string} options.cwd - 工作目录
242
+ * @returns {Promise<ParallelCommandResultWithStderr[]>} 各命令的执行结果(含 stderr)
243
+ */
244
+ export function runParallelCommandsWithStderrCapture(
245
+ commands: string[],
246
+ options?: { cwd?: string },
247
+ ): Promise<ParallelCommandResultWithStderr[]> {
248
+ const promises = commands.map(async (command): Promise<ParallelCommandResultWithStderr> => {
249
+ logger.debug(`并行启动命令(stderr捕获): ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`);
250
+ const result = await spawnWithStderrCapture(command, options);
251
+ return { command, ...result };
252
+ });
253
+
254
+ return Promise.all(promises);
255
+ }
@@ -3,57 +3,101 @@ import {
3
3
  printInfo,
4
4
  printSuccess,
5
5
  printError,
6
+ printWarning,
6
7
  printSeparator,
7
- runCommandInherited,
8
8
  parseParallelCommands,
9
- runParallelCommands,
10
9
  } from './index.js';
11
- import type { ParallelCommandResult } from './index.js';
10
+ import { runCommandWithStderrCapture, runParallelCommandsWithStderrCapture } from './shell.js';
11
+ import type { ParallelCommandResultWithStderr } from './shell.js';
12
+ import { copyToClipboard } from './clipboard.js';
12
13
 
13
14
  /**
14
- * 执行单个命令(同步方式,保持原有行为不变)
15
+ * 处理命令执行失败后的剪贴板复制逻辑
16
+ * 将格式化的错误信息复制到系统剪贴板,并输出操作结果提示
17
+ * @param {string} clipboardContent - 要复制到剪贴板的完整错误信息
18
+ */
19
+ function handleErrorClipboard(clipboardContent: string): void {
20
+ const success = copyToClipboard(clipboardContent);
21
+ if (success) {
22
+ printInfo(MESSAGES.VALIDATE_RUN_ERROR_COPIED);
23
+ } else {
24
+ printWarning(MESSAGES.VALIDATE_RUN_ERROR_COPY_FAILED);
25
+ }
26
+ }
27
+
28
+ /**
29
+ * 构建单命令失败时的剪贴板内容
30
+ * @param {string} command - 失败的命令
31
+ * @param {string} stderr - 捕获的 stderr 内容
32
+ * @param {number} exitCode - 进程退出码
33
+ * @returns {string} 格式化后的剪贴板内容
34
+ */
35
+ function buildSingleErrorClipboard(command: string, stderr: string, exitCode: number): string {
36
+ if (stderr.trim()) {
37
+ return MESSAGES.VALIDATE_CLIPBOARD_SINGLE_ERROR(command, stderr.trim());
38
+ }
39
+ return `${command} 指令执行出错,退出码: ${exitCode}`;
40
+ }
41
+
42
+ /**
43
+ * 执行单个命令(异步方式,捕获 stderr 并在失败时复制到剪贴板)
15
44
  * @param {string} command - 要执行的命令字符串
16
45
  * @param {string} mainWorktreePath - 主 worktree 路径
17
46
  */
18
- function executeSingleCommand(command: string, mainWorktreePath: string): void {
47
+ async function executeSingleCommand(command: string, mainWorktreePath: string): Promise<void> {
19
48
  printInfo(MESSAGES.VALIDATE_RUN_START(command));
20
49
  printSeparator();
21
50
 
22
- const result = runCommandInherited(command, { cwd: mainWorktreePath });
51
+ const result = await runCommandWithStderrCapture(command, { cwd: mainWorktreePath });
23
52
 
24
53
  printSeparator();
25
54
 
26
55
  if (result.error) {
27
56
  // 进程启动失败(如命令不存在)
28
- printError(MESSAGES.VALIDATE_RUN_ERROR(command, result.error.message));
57
+ printError(MESSAGES.VALIDATE_RUN_ERROR(command, result.error));
58
+ const clipboardContent = MESSAGES.VALIDATE_CLIPBOARD_SINGLE_ERROR(command, result.error);
59
+ handleErrorClipboard(clipboardContent);
29
60
  return;
30
61
  }
31
62
 
32
- const exitCode = result.status ?? 1;
33
- if (exitCode === 0) {
63
+ if (result.exitCode === 0) {
34
64
  printSuccess(MESSAGES.VALIDATE_RUN_SUCCESS(command));
35
65
  } else {
36
- printError(MESSAGES.VALIDATE_RUN_FAILED(command, exitCode));
66
+ printError(MESSAGES.VALIDATE_RUN_FAILED(command, result.exitCode));
67
+ const clipboardContent = buildSingleErrorClipboard(command, result.stderr, result.exitCode);
68
+ handleErrorClipboard(clipboardContent);
37
69
  }
38
70
  }
39
71
 
40
72
  /**
41
- * 汇总输出并行命令的执行结果
42
- * @param {ParallelCommandResult[]} results - 各命令的执行结果数组
73
+ * 汇总输出并行命令的执行结果,并将失败命令的错误信息复制到剪贴板
74
+ * @param {ParallelCommandResultWithStderr[]} results - 各命令的执行结果数组
43
75
  */
44
- function reportParallelResults(results: ParallelCommandResult[]): void {
76
+ function reportParallelResults(results: ParallelCommandResultWithStderr[]): void {
45
77
  printSeparator();
46
78
 
47
79
  const successCount = results.filter((r) => r.exitCode === 0 && !r.error).length;
48
80
  const failedCount = results.length - successCount;
81
+ const errorClipboardParts: string[] = [];
49
82
 
50
83
  for (const result of results) {
51
84
  if (result.error) {
52
85
  printError(MESSAGES.VALIDATE_PARALLEL_CMD_ERROR(result.command, result.error));
86
+ // 收集错误信息用于剪贴板
87
+ errorClipboardParts.push(
88
+ MESSAGES.VALIDATE_CLIPBOARD_PARALLEL_ERROR(result.command, result.error),
89
+ );
53
90
  } else if (result.exitCode === 0) {
54
91
  printSuccess(MESSAGES.VALIDATE_PARALLEL_CMD_SUCCESS(result.command));
55
92
  } else {
56
93
  printError(MESSAGES.VALIDATE_PARALLEL_CMD_FAILED(result.command, result.exitCode));
94
+ // 收集错误信息用于剪贴板
95
+ const errorContent = result.stderr.trim()
96
+ ? result.stderr.trim()
97
+ : `退出码: ${result.exitCode}`;
98
+ errorClipboardParts.push(
99
+ MESSAGES.VALIDATE_CLIPBOARD_PARALLEL_ERROR(result.command, errorContent),
100
+ );
57
101
  }
58
102
  }
59
103
 
@@ -61,6 +105,9 @@ function reportParallelResults(results: ParallelCommandResult[]): void {
61
105
  printSuccess(MESSAGES.VALIDATE_PARALLEL_RUN_ALL_SUCCESS(results.length));
62
106
  } else {
63
107
  printError(MESSAGES.VALIDATE_PARALLEL_RUN_SUMMARY(successCount, failedCount));
108
+ // 将所有失败命令的错误信息拼接后一次性复制到剪贴板
109
+ const clipboardContent = errorClipboardParts.join(MESSAGES.VALIDATE_CLIPBOARD_SEPARATOR);
110
+ handleErrorClipboard(clipboardContent);
64
111
  }
65
112
  }
66
113
 
@@ -78,7 +125,7 @@ async function executeParallelCommands(commands: string[], mainWorktreePath: str
78
125
 
79
126
  printSeparator();
80
127
 
81
- const results = await runParallelCommands(commands, { cwd: mainWorktreePath });
128
+ const results = await runParallelCommandsWithStderrCapture(commands, { cwd: mainWorktreePath });
82
129
 
83
130
  reportParallelResults(results);
84
131
  }
@@ -96,8 +143,8 @@ export async function executeRunCommand(command: string, mainWorktreePath: strin
96
143
  const commands = parseParallelCommands(command);
97
144
 
98
145
  if (commands.length <= 1) {
99
- // 单命令(包括含 && 的串行命令),走原有同步路径
100
- executeSingleCommand(commands[0] || command, mainWorktreePath);
146
+ // 单命令(包括含 && 的串行命令),走异步路径并捕获 stderr
147
+ await executeSingleCommand(commands[0] || command, mainWorktreePath);
101
148
  } else {
102
149
  // 多命令,并行执行
103
150
  await executeParallelCommands(commands, mainWorktreePath);