clawt 3.5.1 → 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.
- package/dist/index.js +120 -24
- package/dist/postinstall.js +13 -1
- package/package.json +1 -1
- package/src/constants/messages/validate.ts +12 -0
- package/src/utils/clipboard.ts +52 -0
- package/src/utils/index.ts +3 -2
- package/src/utils/shell.ts +96 -0
- package/src/utils/validate-runner.ts +63 -16
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
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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
|
-
|
|
995
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/dist/postinstall.js
CHANGED
|
@@ -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
|
@@ -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;
|
|
@@ -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
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -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,
|
package/src/utils/shell.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 {
|
|
73
|
+
* 汇总输出并行命令的执行结果,并将失败命令的错误信息复制到剪贴板
|
|
74
|
+
* @param {ParallelCommandResultWithStderr[]} results - 各命令的执行结果数组
|
|
43
75
|
*/
|
|
44
|
-
function reportParallelResults(results:
|
|
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
|
|
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);
|