clawt 3.5.1 → 3.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -250,7 +250,19 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
250
250
  VALIDATE_BRANCH_NOT_FOUND: (validateBranch, branch) => `\u9A8C\u8BC1\u5206\u652F ${validateBranch} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C clawt create \u6216 clawt run \u521B\u5EFA\u5206\u652F ${branch}`,
251
251
  /** validate 成功(含验证分支信息) */
252
252
  VALIDATE_SUCCESS_WITH_BRANCH: (branch, validateBranch) => `\u2713 \u5DF2\u5207\u6362\u5230\u9A8C\u8BC1\u5206\u652F ${validateBranch} \u5E76\u5E94\u7528\u5206\u652F ${branch} \u7684\u53D8\u66F4
253
- \u53EF\u4EE5\u5F00\u59CB\u9A8C\u8BC1\u4E86`
253
+ \u53EF\u4EE5\u5F00\u59CB\u9A8C\u8BC1\u4E86`,
254
+ /** 错误信息已复制到剪贴板提示 */
255
+ VALIDATE_RUN_ERROR_COPIED: "\u2702 \u9519\u8BEF\u4FE1\u606F\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F",
256
+ /** 剪贴板复制失败提示 */
257
+ VALIDATE_RUN_ERROR_COPY_FAILED: "\u26A0 \u9519\u8BEF\u4FE1\u606F\u590D\u5236\u5230\u526A\u8D34\u677F\u5931\u8D25",
258
+ /** 单命令(含 && 链)剪贴板错误格式 */
259
+ VALIDATE_CLIPBOARD_SINGLE_ERROR: (command, stderr) => `${command} \u6307\u4EE4\u6267\u884C\u51FA\u9519\uFF0C\u9519\u8BEF\u4FE1\u606F\uFF1A
260
+ ${stderr}`,
261
+ /** 并行命令中单个命令的剪贴板错误格式 */
262
+ VALIDATE_CLIPBOARD_PARALLEL_ERROR: (command, stderr) => `${command} \u6307\u4EE4\u6267\u884C\u51FA\u9519\uFF0C\u9519\u8BEF\u4FE1\u606F\uFF1A
263
+ ${stderr}`,
264
+ /** 多个错误之间的分隔符 */
265
+ VALIDATE_CLIPBOARD_SEPARATOR: "\n\n---\n\n"
254
266
  };
255
267
 
256
268
  // src/constants/messages/sync.ts
@@ -979,26 +991,83 @@ function parseParallelCommands(commandString) {
979
991
  const parts = escaped.split("&");
980
992
  return parts.map((part) => part.replace(new RegExp(placeholder, "g"), "&&").trim()).filter((part) => part.length > 0);
981
993
  }
982
- function runParallelCommands(commands, options) {
983
- const promises = commands.map((command) => {
984
- return new Promise((resolve4) => {
985
- logger.debug(`\u5E76\u884C\u542F\u52A8\u547D\u4EE4: ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ""}`);
986
- const child = spawn(command, {
987
- cwd: options?.cwd,
988
- stdio: "inherit",
989
- shell: true
990
- });
991
- child.on("error", (err) => {
992
- resolve4({ command, exitCode: 1, error: err.message });
994
+ function spawnWithStderrCapture(command, options) {
995
+ return new Promise((resolve4) => {
996
+ const child = spawn(command, {
997
+ cwd: options?.cwd,
998
+ stdio: ["inherit", "inherit", "pipe"],
999
+ shell: true
1000
+ });
1001
+ const stderrChunks = [];
1002
+ child.stderr?.on("data", (chunk) => {
1003
+ process.stderr.write(chunk);
1004
+ stderrChunks.push(chunk);
1005
+ });
1006
+ child.on("error", (err) => {
1007
+ resolve4({
1008
+ exitCode: 1,
1009
+ error: err.message,
1010
+ stderr: Buffer.concat(stderrChunks).toString("utf-8")
993
1011
  });
994
- child.on("close", (code) => {
995
- resolve4({ command, exitCode: code ?? 1 });
1012
+ });
1013
+ child.on("close", (code) => {
1014
+ resolve4({
1015
+ exitCode: code ?? 1,
1016
+ stderr: Buffer.concat(stderrChunks).toString("utf-8")
996
1017
  });
997
1018
  });
998
1019
  });
1020
+ }
1021
+ function runCommandWithStderrCapture(command, options) {
1022
+ logger.debug(`\u6267\u884C\u547D\u4EE4(stderr\u6355\u83B7): ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ""}`);
1023
+ return spawnWithStderrCapture(command, options);
1024
+ }
1025
+ function runParallelCommandsWithStderrCapture(commands, options) {
1026
+ const promises = commands.map(async (command) => {
1027
+ logger.debug(`\u5E76\u884C\u542F\u52A8\u547D\u4EE4(stderr\u6355\u83B7): ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ""}`);
1028
+ const result = await spawnWithStderrCapture(command, options);
1029
+ return { command, ...result };
1030
+ });
999
1031
  return Promise.all(promises);
1000
1032
  }
1001
1033
 
1034
+ // src/utils/clipboard.ts
1035
+ import { spawnSync as spawnSync2 } from "child_process";
1036
+ function getClipboardCommand() {
1037
+ switch (process.platform) {
1038
+ case "darwin":
1039
+ return { command: "pbcopy", args: [] };
1040
+ case "linux":
1041
+ return { command: "xclip", args: ["-selection", "clipboard"] };
1042
+ case "win32":
1043
+ return { command: "clip", args: [] };
1044
+ default:
1045
+ return null;
1046
+ }
1047
+ }
1048
+ function copyToClipboard(text) {
1049
+ try {
1050
+ const clipboardCmd = getClipboardCommand();
1051
+ if (!clipboardCmd) {
1052
+ logger.debug(`\u4E0D\u652F\u6301\u7684\u5E73\u53F0: ${process.platform}\uFF0C\u8DF3\u8FC7\u526A\u8D34\u677F\u590D\u5236`);
1053
+ return false;
1054
+ }
1055
+ const result = spawnSync2(clipboardCmd.command, clipboardCmd.args, {
1056
+ input: text,
1057
+ encoding: "utf-8",
1058
+ stdio: ["pipe", "pipe", "pipe"]
1059
+ });
1060
+ if (result.status !== 0) {
1061
+ logger.debug(`\u526A\u8D34\u677F\u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF0C\u9000\u51FA\u7801: ${result.status}`);
1062
+ return false;
1063
+ }
1064
+ return true;
1065
+ } catch (error) {
1066
+ logger.debug(`\u526A\u8D34\u677F\u590D\u5236\u5F02\u5E38: ${error.message}`);
1067
+ return false;
1068
+ }
1069
+ }
1070
+
1002
1071
  // src/utils/git-core.ts
1003
1072
  import { basename } from "path";
1004
1073
  import { execSync as execSync2, execFileSync as execFileSync2 } from "child_process";
@@ -1757,7 +1826,7 @@ function parseConcurrency(optionValue, configValue) {
1757
1826
  import Enquirer2 from "enquirer";
1758
1827
 
1759
1828
  // src/utils/claude.ts
1760
- import { spawnSync as spawnSync2 } from "child_process";
1829
+ import { spawnSync as spawnSync3 } from "child_process";
1761
1830
  import { existsSync as existsSync7, readdirSync as readdirSync3 } from "fs";
1762
1831
  import { join as join5 } from "path";
1763
1832
 
@@ -1866,7 +1935,7 @@ function launchInteractiveClaude(worktree, options = {}) {
1866
1935
  printInfo(` \u6A21\u5F0F: ${hasPreviousSession ? "\u7EE7\u7EED\u4E0A\u6B21\u5BF9\u8BDD" : "\u65B0\u5BF9\u8BDD"}`);
1867
1936
  }
1868
1937
  printInfo("");
1869
- const result = spawnSync2(cmd, args, {
1938
+ const result = spawnSync3(cmd, args, {
1870
1939
  cwd: worktree.path,
1871
1940
  stdio: "inherit"
1872
1941
  });
@@ -3227,39 +3296,66 @@ async function checkForUpdates(currentVersion) {
3227
3296
  }
3228
3297
 
3229
3298
  // src/utils/validate-runner.ts
3230
- function executeSingleCommand(command, mainWorktreePath) {
3299
+ function handleErrorClipboard(clipboardContent) {
3300
+ const success = copyToClipboard(clipboardContent);
3301
+ if (success) {
3302
+ printInfo(MESSAGES.VALIDATE_RUN_ERROR_COPIED);
3303
+ } else {
3304
+ printWarning(MESSAGES.VALIDATE_RUN_ERROR_COPY_FAILED);
3305
+ }
3306
+ }
3307
+ function buildSingleErrorClipboard(command, stderr, exitCode) {
3308
+ if (stderr.trim()) {
3309
+ return MESSAGES.VALIDATE_CLIPBOARD_SINGLE_ERROR(command, stderr.trim());
3310
+ }
3311
+ return `${command} \u6307\u4EE4\u6267\u884C\u51FA\u9519\uFF0C\u9000\u51FA\u7801: ${exitCode}`;
3312
+ }
3313
+ async function executeSingleCommand(command, mainWorktreePath) {
3231
3314
  printInfo(MESSAGES.VALIDATE_RUN_START(command));
3232
3315
  printSeparator();
3233
- const result = runCommandInherited(command, { cwd: mainWorktreePath });
3316
+ const result = await runCommandWithStderrCapture(command, { cwd: mainWorktreePath });
3234
3317
  printSeparator();
3235
3318
  if (result.error) {
3236
- printError(MESSAGES.VALIDATE_RUN_ERROR(command, result.error.message));
3319
+ printError(MESSAGES.VALIDATE_RUN_ERROR(command, result.error));
3320
+ const clipboardContent = MESSAGES.VALIDATE_CLIPBOARD_SINGLE_ERROR(command, result.error);
3321
+ handleErrorClipboard(clipboardContent);
3237
3322
  return;
3238
3323
  }
3239
- const exitCode = result.status ?? 1;
3240
- if (exitCode === 0) {
3324
+ if (result.exitCode === 0) {
3241
3325
  printSuccess(MESSAGES.VALIDATE_RUN_SUCCESS(command));
3242
3326
  } else {
3243
- printError(MESSAGES.VALIDATE_RUN_FAILED(command, exitCode));
3327
+ printError(MESSAGES.VALIDATE_RUN_FAILED(command, result.exitCode));
3328
+ const clipboardContent = buildSingleErrorClipboard(command, result.stderr, result.exitCode);
3329
+ handleErrorClipboard(clipboardContent);
3244
3330
  }
3245
3331
  }
3246
3332
  function reportParallelResults(results) {
3247
3333
  printSeparator();
3248
3334
  const successCount = results.filter((r) => r.exitCode === 0 && !r.error).length;
3249
3335
  const failedCount = results.length - successCount;
3336
+ const errorClipboardParts = [];
3250
3337
  for (const result of results) {
3251
3338
  if (result.error) {
3252
3339
  printError(MESSAGES.VALIDATE_PARALLEL_CMD_ERROR(result.command, result.error));
3340
+ errorClipboardParts.push(
3341
+ MESSAGES.VALIDATE_CLIPBOARD_PARALLEL_ERROR(result.command, result.error)
3342
+ );
3253
3343
  } else if (result.exitCode === 0) {
3254
3344
  printSuccess(MESSAGES.VALIDATE_PARALLEL_CMD_SUCCESS(result.command));
3255
3345
  } else {
3256
3346
  printError(MESSAGES.VALIDATE_PARALLEL_CMD_FAILED(result.command, result.exitCode));
3347
+ const errorContent = result.stderr.trim() ? result.stderr.trim() : `\u9000\u51FA\u7801: ${result.exitCode}`;
3348
+ errorClipboardParts.push(
3349
+ MESSAGES.VALIDATE_CLIPBOARD_PARALLEL_ERROR(result.command, errorContent)
3350
+ );
3257
3351
  }
3258
3352
  }
3259
3353
  if (failedCount === 0) {
3260
3354
  printSuccess(MESSAGES.VALIDATE_PARALLEL_RUN_ALL_SUCCESS(results.length));
3261
3355
  } else {
3262
3356
  printError(MESSAGES.VALIDATE_PARALLEL_RUN_SUMMARY(successCount, failedCount));
3357
+ const clipboardContent = errorClipboardParts.join(MESSAGES.VALIDATE_CLIPBOARD_SEPARATOR);
3358
+ handleErrorClipboard(clipboardContent);
3263
3359
  }
3264
3360
  }
3265
3361
  async function executeParallelCommands(commands, mainWorktreePath) {
@@ -3268,14 +3364,14 @@ async function executeParallelCommands(commands, mainWorktreePath) {
3268
3364
  printInfo(MESSAGES.VALIDATE_PARALLEL_CMD_START(i + 1, commands.length, commands[i]));
3269
3365
  }
3270
3366
  printSeparator();
3271
- const results = await runParallelCommands(commands, { cwd: mainWorktreePath });
3367
+ const results = await runParallelCommandsWithStderrCapture(commands, { cwd: mainWorktreePath });
3272
3368
  reportParallelResults(results);
3273
3369
  }
3274
3370
  async function executeRunCommand(command, mainWorktreePath) {
3275
3371
  printInfo("");
3276
3372
  const commands = parseParallelCommands(command);
3277
3373
  if (commands.length <= 1) {
3278
- executeSingleCommand(commands[0] || command, mainWorktreePath);
3374
+ await executeSingleCommand(commands[0] || command, mainWorktreePath);
3279
3375
  } else {
3280
3376
  await executeParallelCommands(commands, mainWorktreePath);
3281
3377
  }
@@ -5709,7 +5805,8 @@ process.on("unhandledRejection", (reason) => {
5709
5805
  });
5710
5806
  async function main() {
5711
5807
  await program.parseAsync(process.argv);
5712
- if (config.autoUpdate) {
5808
+ const isCompletionCommand = process.argv[2] === "completion";
5809
+ if (config.autoUpdate && !isCompletionCommand) {
5713
5810
  await checkForUpdates(version);
5714
5811
  }
5715
5812
  }
@@ -241,7 +241,19 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
241
241
  VALIDATE_BRANCH_NOT_FOUND: (validateBranch, branch) => `\u9A8C\u8BC1\u5206\u652F ${validateBranch} \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u6267\u884C clawt create \u6216 clawt run \u521B\u5EFA\u5206\u652F ${branch}`,
242
242
  /** validate 成功(含验证分支信息) */
243
243
  VALIDATE_SUCCESS_WITH_BRANCH: (branch, validateBranch) => `\u2713 \u5DF2\u5207\u6362\u5230\u9A8C\u8BC1\u5206\u652F ${validateBranch} \u5E76\u5E94\u7528\u5206\u652F ${branch} \u7684\u53D8\u66F4
244
- \u53EF\u4EE5\u5F00\u59CB\u9A8C\u8BC1\u4E86`
244
+ \u53EF\u4EE5\u5F00\u59CB\u9A8C\u8BC1\u4E86`,
245
+ /** 错误信息已复制到剪贴板提示 */
246
+ VALIDATE_RUN_ERROR_COPIED: "\u2702 \u9519\u8BEF\u4FE1\u606F\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F",
247
+ /** 剪贴板复制失败提示 */
248
+ VALIDATE_RUN_ERROR_COPY_FAILED: "\u26A0 \u9519\u8BEF\u4FE1\u606F\u590D\u5236\u5230\u526A\u8D34\u677F\u5931\u8D25",
249
+ /** 单命令(含 && 链)剪贴板错误格式 */
250
+ VALIDATE_CLIPBOARD_SINGLE_ERROR: (command, stderr) => `${command} \u6307\u4EE4\u6267\u884C\u51FA\u9519\uFF0C\u9519\u8BEF\u4FE1\u606F\uFF1A
251
+ ${stderr}`,
252
+ /** 并行命令中单个命令的剪贴板错误格式 */
253
+ VALIDATE_CLIPBOARD_PARALLEL_ERROR: (command, stderr) => `${command} \u6307\u4EE4\u6267\u884C\u51FA\u9519\uFF0C\u9519\u8BEF\u4FE1\u606F\uFF1A
254
+ ${stderr}`,
255
+ /** 多个错误之间的分隔符 */
256
+ VALIDATE_CLIPBOARD_SEPARATOR: "\n\n---\n\n"
245
257
  };
246
258
 
247
259
  // src/constants/messages/sync.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "3.5.1",
3
+ "version": "3.5.3",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -71,4 +71,16 @@ export const VALIDATE_MESSAGES = {
71
71
  /** validate 成功(含验证分支信息) */
72
72
  VALIDATE_SUCCESS_WITH_BRANCH: (branch: string, validateBranch: string) =>
73
73
  `✓ 已切换到验证分支 ${validateBranch} 并应用分支 ${branch} 的变更\n 可以开始验证了`,
74
+ /** 错误信息已复制到剪贴板提示 */
75
+ VALIDATE_RUN_ERROR_COPIED: '✂ 错误信息已复制到剪贴板',
76
+ /** 剪贴板复制失败提示 */
77
+ VALIDATE_RUN_ERROR_COPY_FAILED: '⚠ 错误信息复制到剪贴板失败',
78
+ /** 单命令(含 && 链)剪贴板错误格式 */
79
+ VALIDATE_CLIPBOARD_SINGLE_ERROR: (command: string, stderr: string) =>
80
+ `${command} 指令执行出错,错误信息:\n${stderr}`,
81
+ /** 并行命令中单个命令的剪贴板错误格式 */
82
+ VALIDATE_CLIPBOARD_PARALLEL_ERROR: (command: string, stderr: string) =>
83
+ `${command} 指令执行出错,错误信息:\n${stderr}`,
84
+ /** 多个错误之间的分隔符 */
85
+ VALIDATE_CLIPBOARD_SEPARATOR: '\n\n---\n\n',
74
86
  } as const;
package/src/index.ts CHANGED
@@ -100,7 +100,10 @@ process.on('unhandledRejection', (reason) => {
100
100
  async function main(): Promise<void> {
101
101
  await program.parseAsync(process.argv);
102
102
 
103
- if (config.autoUpdate) {
103
+ // completion 子命令的 stdout 会被 shell 的 source 捕获并执行,
104
+ // 更新提示会污染 stdout 导致 shell 报错,因此跳过更新检查
105
+ const isCompletionCommand = process.argv[2] === 'completion';
106
+ if (config.autoUpdate && !isCompletionCommand) {
104
107
  await checkForUpdates(version);
105
108
  }
106
109
  }
@@ -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
+ }
@@ -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,
@@ -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);