clawt 2.16.0 → 2.16.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/README.md +8 -1
- package/dist/index.js +88 -5
- package/dist/postinstall.js +18 -2
- package/docs/spec.md +69 -5
- package/package.json +1 -1
- package/src/commands/remove.ts +4 -0
- package/src/commands/validate.ts +73 -6
- package/src/constants/messages/remove.ts +2 -0
- package/src/constants/messages/validate.ts +21 -0
- package/src/utils/formatter.ts +8 -0
- package/src/utils/index.ts +3 -2
- package/src/utils/shell.ts +65 -0
- package/tests/unit/commands/remove.test.ts +7 -0
- package/tests/unit/commands/validate.test.ts +103 -0
- package/tests/unit/utils/shell.test.ts +42 -1
package/README.md
CHANGED
|
@@ -123,11 +123,18 @@ clawt validate -b <branch> # 将变更迁移到主 worktree
|
|
|
123
123
|
clawt validate -b <branch> --clean # 清理 validate 状态
|
|
124
124
|
clawt validate -b <branch> -r "npm test" # validate 成功后自动运行测试
|
|
125
125
|
clawt validate -b <branch> -r "npm run build" # validate 成功后自动构建
|
|
126
|
+
clawt validate -b <branch> -r "pnpm test & pnpm build" # 并行执行多个命令
|
|
126
127
|
```
|
|
127
128
|
|
|
128
129
|
支持增量模式:再次 validate 同一分支时,可通过 `git diff` 查看两次之间的增量差异。
|
|
129
130
|
|
|
130
|
-
`-r, --run` 选项可在 validate 成功后自动在主 worktree 中执行指定命令(如测试、构建等),命令执行失败不影响 validate
|
|
131
|
+
`-r, --run` 选项可在 validate 成功后自动在主 worktree 中执行指定命令(如测试、构建等),命令执行失败不影响 validate 结果。支持用 `&` 分隔多个命令并行执行:
|
|
132
|
+
|
|
133
|
+
| 用法 | 行为 |
|
|
134
|
+
| ---- | ---- |
|
|
135
|
+
| `-r "npm test"` | 单命令,同步执行 |
|
|
136
|
+
| `-r "npm lint && npm test"` | `&&` 不拆分,同步执行 |
|
|
137
|
+
| `-r "pnpm test & pnpm build"` | 并行执行,等全部完成后汇总结果 |
|
|
131
138
|
|
|
132
139
|
### `clawt sync` — 同步主分支代码到目标 worktree
|
|
133
140
|
|
package/dist/index.js
CHANGED
|
@@ -197,7 +197,21 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
|
197
197
|
/** --run 命令执行失败(退出码非 0) */
|
|
198
198
|
VALIDATE_RUN_FAILED: (command, exitCode) => `\u2717 \u547D\u4EE4\u6267\u884C\u5B8C\u6210: ${command}\uFF0C\u9000\u51FA\u7801: ${exitCode}`,
|
|
199
199
|
/** --run 命令执行异常(进程启动失败等) */
|
|
200
|
-
VALIDATE_RUN_ERROR: (command, errorMessage) => `\u2717 \u547D\u4EE4\u6267\u884C\u51FA\u9519: ${errorMessage}
|
|
200
|
+
VALIDATE_RUN_ERROR: (command, errorMessage) => `\u2717 \u547D\u4EE4\u6267\u884C\u51FA\u9519: ${errorMessage}`,
|
|
201
|
+
/** 并行命令开始执行提示 */
|
|
202
|
+
VALIDATE_PARALLEL_RUN_START: (count) => `\u6B63\u5728\u5E76\u884C\u6267\u884C ${count} \u4E2A\u547D\u4EE4...`,
|
|
203
|
+
/** 并行执行中单个命令开始提示(带序号) */
|
|
204
|
+
VALIDATE_PARALLEL_CMD_START: (index, total, command) => `[${index}/${total}] ${command}`,
|
|
205
|
+
/** 并行执行全部成功汇总提示 */
|
|
206
|
+
VALIDATE_PARALLEL_RUN_ALL_SUCCESS: (count) => `\u2713 \u5168\u90E8 ${count} \u4E2A\u547D\u4EE4\u6267\u884C\u6210\u529F`,
|
|
207
|
+
/** 并行执行部分失败汇总提示 */
|
|
208
|
+
VALIDATE_PARALLEL_RUN_SUMMARY: (successCount, failedCount) => `\u5171 ${successCount + failedCount} \u4E2A\u547D\u4EE4\uFF0C${successCount} \u4E2A\u6210\u529F\uFF0C${failedCount} \u4E2A\u5931\u8D25`,
|
|
209
|
+
/** 并行执行中单个命令成功 */
|
|
210
|
+
VALIDATE_PARALLEL_CMD_SUCCESS: (command) => ` \u2713 ${command}`,
|
|
211
|
+
/** 并行执行中单个命令失败 */
|
|
212
|
+
VALIDATE_PARALLEL_CMD_FAILED: (command, exitCode) => ` \u2717 ${command}\uFF08\u9000\u51FA\u7801: ${exitCode}\uFF09`,
|
|
213
|
+
/** 并行执行中单个命令启动失败 */
|
|
214
|
+
VALIDATE_PARALLEL_CMD_ERROR: (command, errorMessage) => ` \u2717 ${command}\uFF08\u9519\u8BEF: ${errorMessage}\uFF09`
|
|
201
215
|
};
|
|
202
216
|
|
|
203
217
|
// src/constants/messages/sync.ts
|
|
@@ -261,7 +275,9 @@ var REMOVE_MESSAGES = {
|
|
|
261
275
|
${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
262
276
|
/** 批量移除部分失败 */
|
|
263
277
|
REMOVE_PARTIAL_FAILURE: (failures) => `\u4EE5\u4E0B worktree \u79FB\u9664\u5931\u8D25\uFF1A
|
|
264
|
-
${failures.map((f) => ` \u2717 ${f.path}: ${f.error}`).join("\n")}
|
|
278
|
+
${failures.map((f) => ` \u2717 ${f.path}: ${f.error}`).join("\n")}`,
|
|
279
|
+
/** 用户选择保留本地分支 */
|
|
280
|
+
REMOVE_BRANCHES_KEPT: "\u5DF2\u4FDD\u7559\u672C\u5730\u5206\u652F\uFF0C\u53EF\u7A0D\u540E\u4F7F\u7528 git branch -D <\u5206\u652F\u540D> \u624B\u52A8\u5220\u9664"
|
|
265
281
|
};
|
|
266
282
|
|
|
267
283
|
// src/constants/messages/reset.ts
|
|
@@ -571,6 +587,31 @@ function runCommandInherited(command, options) {
|
|
|
571
587
|
shell: true
|
|
572
588
|
});
|
|
573
589
|
}
|
|
590
|
+
function parseParallelCommands(commandString) {
|
|
591
|
+
const placeholder = "\0AND\0";
|
|
592
|
+
const escaped = commandString.replace(/&&/g, placeholder);
|
|
593
|
+
const parts = escaped.split("&");
|
|
594
|
+
return parts.map((part) => part.replace(new RegExp(placeholder, "g"), "&&").trim()).filter((part) => part.length > 0);
|
|
595
|
+
}
|
|
596
|
+
function runParallelCommands(commands, options) {
|
|
597
|
+
const promises = commands.map((command) => {
|
|
598
|
+
return new Promise((resolve2) => {
|
|
599
|
+
logger.debug(`\u5E76\u884C\u542F\u52A8\u547D\u4EE4: ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ""}`);
|
|
600
|
+
const child = spawn(command, {
|
|
601
|
+
cwd: options?.cwd,
|
|
602
|
+
stdio: "inherit",
|
|
603
|
+
shell: true
|
|
604
|
+
});
|
|
605
|
+
child.on("error", (err) => {
|
|
606
|
+
resolve2({ command, exitCode: 1, error: err.message });
|
|
607
|
+
});
|
|
608
|
+
child.on("close", (code) => {
|
|
609
|
+
resolve2({ command, exitCode: code ?? 1 });
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
return Promise.all(promises);
|
|
614
|
+
}
|
|
574
615
|
|
|
575
616
|
// src/utils/git.ts
|
|
576
617
|
import { basename } from "path";
|
|
@@ -762,6 +803,9 @@ function printWarning(message) {
|
|
|
762
803
|
function printInfo(message) {
|
|
763
804
|
console.log(message);
|
|
764
805
|
}
|
|
806
|
+
function printHint(message) {
|
|
807
|
+
console.log(chalk2.hex("#FF8C00")(message));
|
|
808
|
+
}
|
|
765
809
|
function printSeparator() {
|
|
766
810
|
console.log(MESSAGES.SEPARATOR);
|
|
767
811
|
}
|
|
@@ -2062,6 +2106,9 @@ async function handleRemove(options) {
|
|
|
2062
2106
|
let shouldDeleteBranch = autoDelete;
|
|
2063
2107
|
if (!autoDelete) {
|
|
2064
2108
|
shouldDeleteBranch = await confirmAction("\u662F\u5426\u540C\u65F6\u5220\u9664\u5BF9\u5E94\u7684\u672C\u5730\u5206\u652F\uFF1F");
|
|
2109
|
+
if (!shouldDeleteBranch) {
|
|
2110
|
+
printHint(MESSAGES.REMOVE_BRANCHES_KEPT);
|
|
2111
|
+
}
|
|
2065
2112
|
}
|
|
2066
2113
|
const failures = [];
|
|
2067
2114
|
for (const wt of worktreesToRemove) {
|
|
@@ -2384,8 +2431,7 @@ function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, project
|
|
|
2384
2431
|
}
|
|
2385
2432
|
printSuccess(MESSAGES.INCREMENTAL_VALIDATE_SUCCESS(branchName));
|
|
2386
2433
|
}
|
|
2387
|
-
function
|
|
2388
|
-
printInfo("");
|
|
2434
|
+
function executeSingleCommand(command, mainWorktreePath) {
|
|
2389
2435
|
printInfo(MESSAGES.VALIDATE_RUN_START(command));
|
|
2390
2436
|
printSeparator();
|
|
2391
2437
|
const result = runCommandInherited(command, { cwd: mainWorktreePath });
|
|
@@ -2401,6 +2447,43 @@ function executeRunCommand(command, mainWorktreePath) {
|
|
|
2401
2447
|
printError(MESSAGES.VALIDATE_RUN_FAILED(command, exitCode));
|
|
2402
2448
|
}
|
|
2403
2449
|
}
|
|
2450
|
+
function reportParallelResults(results) {
|
|
2451
|
+
printSeparator();
|
|
2452
|
+
const successCount = results.filter((r) => r.exitCode === 0 && !r.error).length;
|
|
2453
|
+
const failedCount = results.length - successCount;
|
|
2454
|
+
for (const result of results) {
|
|
2455
|
+
if (result.error) {
|
|
2456
|
+
printError(MESSAGES.VALIDATE_PARALLEL_CMD_ERROR(result.command, result.error));
|
|
2457
|
+
} else if (result.exitCode === 0) {
|
|
2458
|
+
printSuccess(MESSAGES.VALIDATE_PARALLEL_CMD_SUCCESS(result.command));
|
|
2459
|
+
} else {
|
|
2460
|
+
printError(MESSAGES.VALIDATE_PARALLEL_CMD_FAILED(result.command, result.exitCode));
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
if (failedCount === 0) {
|
|
2464
|
+
printSuccess(MESSAGES.VALIDATE_PARALLEL_RUN_ALL_SUCCESS(results.length));
|
|
2465
|
+
} else {
|
|
2466
|
+
printError(MESSAGES.VALIDATE_PARALLEL_RUN_SUMMARY(successCount, failedCount));
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
async function executeParallelCommands(commands, mainWorktreePath) {
|
|
2470
|
+
printInfo(MESSAGES.VALIDATE_PARALLEL_RUN_START(commands.length));
|
|
2471
|
+
for (let i = 0; i < commands.length; i++) {
|
|
2472
|
+
printInfo(MESSAGES.VALIDATE_PARALLEL_CMD_START(i + 1, commands.length, commands[i]));
|
|
2473
|
+
}
|
|
2474
|
+
printSeparator();
|
|
2475
|
+
const results = await runParallelCommands(commands, { cwd: mainWorktreePath });
|
|
2476
|
+
reportParallelResults(results);
|
|
2477
|
+
}
|
|
2478
|
+
async function executeRunCommand(command, mainWorktreePath) {
|
|
2479
|
+
printInfo("");
|
|
2480
|
+
const commands = parseParallelCommands(command);
|
|
2481
|
+
if (commands.length <= 1) {
|
|
2482
|
+
executeSingleCommand(commands[0] || command, mainWorktreePath);
|
|
2483
|
+
} else {
|
|
2484
|
+
await executeParallelCommands(commands, mainWorktreePath);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2404
2487
|
async function handleValidate(options) {
|
|
2405
2488
|
if (options.clean) {
|
|
2406
2489
|
await handleValidateClean(options);
|
|
@@ -2433,7 +2516,7 @@ async function handleValidate(options) {
|
|
|
2433
2516
|
handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
2434
2517
|
}
|
|
2435
2518
|
if (options.run) {
|
|
2436
|
-
executeRunCommand(options.run, mainWorktreePath);
|
|
2519
|
+
await executeRunCommand(options.run, mainWorktreePath);
|
|
2437
2520
|
}
|
|
2438
2521
|
}
|
|
2439
2522
|
|
package/dist/postinstall.js
CHANGED
|
@@ -189,7 +189,21 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
|
189
189
|
/** --run 命令执行失败(退出码非 0) */
|
|
190
190
|
VALIDATE_RUN_FAILED: (command, exitCode) => `\u2717 \u547D\u4EE4\u6267\u884C\u5B8C\u6210: ${command}\uFF0C\u9000\u51FA\u7801: ${exitCode}`,
|
|
191
191
|
/** --run 命令执行异常(进程启动失败等) */
|
|
192
|
-
VALIDATE_RUN_ERROR: (command, errorMessage) => `\u2717 \u547D\u4EE4\u6267\u884C\u51FA\u9519: ${errorMessage}
|
|
192
|
+
VALIDATE_RUN_ERROR: (command, errorMessage) => `\u2717 \u547D\u4EE4\u6267\u884C\u51FA\u9519: ${errorMessage}`,
|
|
193
|
+
/** 并行命令开始执行提示 */
|
|
194
|
+
VALIDATE_PARALLEL_RUN_START: (count) => `\u6B63\u5728\u5E76\u884C\u6267\u884C ${count} \u4E2A\u547D\u4EE4...`,
|
|
195
|
+
/** 并行执行中单个命令开始提示(带序号) */
|
|
196
|
+
VALIDATE_PARALLEL_CMD_START: (index, total, command) => `[${index}/${total}] ${command}`,
|
|
197
|
+
/** 并行执行全部成功汇总提示 */
|
|
198
|
+
VALIDATE_PARALLEL_RUN_ALL_SUCCESS: (count) => `\u2713 \u5168\u90E8 ${count} \u4E2A\u547D\u4EE4\u6267\u884C\u6210\u529F`,
|
|
199
|
+
/** 并行执行部分失败汇总提示 */
|
|
200
|
+
VALIDATE_PARALLEL_RUN_SUMMARY: (successCount, failedCount) => `\u5171 ${successCount + failedCount} \u4E2A\u547D\u4EE4\uFF0C${successCount} \u4E2A\u6210\u529F\uFF0C${failedCount} \u4E2A\u5931\u8D25`,
|
|
201
|
+
/** 并行执行中单个命令成功 */
|
|
202
|
+
VALIDATE_PARALLEL_CMD_SUCCESS: (command) => ` \u2713 ${command}`,
|
|
203
|
+
/** 并行执行中单个命令失败 */
|
|
204
|
+
VALIDATE_PARALLEL_CMD_FAILED: (command, exitCode) => ` \u2717 ${command}\uFF08\u9000\u51FA\u7801: ${exitCode}\uFF09`,
|
|
205
|
+
/** 并行执行中单个命令启动失败 */
|
|
206
|
+
VALIDATE_PARALLEL_CMD_ERROR: (command, errorMessage) => ` \u2717 ${command}\uFF08\u9519\u8BEF: ${errorMessage}\uFF09`
|
|
193
207
|
};
|
|
194
208
|
|
|
195
209
|
// src/constants/messages/sync.ts
|
|
@@ -253,7 +267,9 @@ var REMOVE_MESSAGES = {
|
|
|
253
267
|
${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
254
268
|
/** 批量移除部分失败 */
|
|
255
269
|
REMOVE_PARTIAL_FAILURE: (failures) => `\u4EE5\u4E0B worktree \u79FB\u9664\u5931\u8D25\uFF1A
|
|
256
|
-
${failures.map((f) => ` \u2717 ${f.path}: ${f.error}`).join("\n")}
|
|
270
|
+
${failures.map((f) => ` \u2717 ${f.path}: ${f.error}`).join("\n")}`,
|
|
271
|
+
/** 用户选择保留本地分支 */
|
|
272
|
+
REMOVE_BRANCHES_KEPT: "\u5DF2\u4FDD\u7559\u672C\u5730\u5206\u652F\uFF0C\u53EF\u7A0D\u540E\u4F7F\u7528 git branch -D <\u5206\u652F\u540D> \u624B\u52A8\u5220\u9664"
|
|
257
273
|
};
|
|
258
274
|
|
|
259
275
|
// src/constants/messages/reset.ts
|
package/docs/spec.md
CHANGED
|
@@ -603,40 +603,104 @@ git restore --staged .
|
|
|
603
603
|
如果用户传入了 `-r, --run` 选项,在 validate 成功后自动在主 worktree 中执行指定命令:
|
|
604
604
|
|
|
605
605
|
```bash
|
|
606
|
-
#
|
|
606
|
+
# 示例:单命令
|
|
607
607
|
clawt validate -b feature-scheme-1 -r "npm test"
|
|
608
|
+
|
|
609
|
+
# 示例:并行执行多个命令(& 为并行分隔符)
|
|
610
|
+
clawt validate -b feature-scheme-1 -r "pnpm test & pnpm build"
|
|
608
611
|
```
|
|
609
612
|
|
|
610
613
|
**执行说明:**
|
|
611
614
|
|
|
612
|
-
- 命令通过 `spawnSync` + `inherit` stdio 模式在主 worktree 中执行,输出实时显示在终端
|
|
613
615
|
- 命令执行失败(退出码非 0 或进程启动失败)**不影响** validate 本身的结果,仅输出提示信息
|
|
614
616
|
- `--clean` 模式下传入 `--run` 会被忽略(只执行 clean 逻辑)
|
|
615
617
|
|
|
618
|
+
**命令解析规则:**
|
|
619
|
+
|
|
620
|
+
`-r` 选项支持通过 `&` 将多个命令并行执行。解析由 `parseParallelCommands()`(`src/utils/shell.ts`)负责:
|
|
621
|
+
|
|
622
|
+
1. 先将命令字符串中的 `&&` 临时替换为占位符,避免被误拆
|
|
623
|
+
2. 按单个 `&` 分割为多个独立命令
|
|
624
|
+
3. 还原占位符为 `&&`,去除首尾空白,过滤空串
|
|
625
|
+
|
|
626
|
+
| 输入示例 | 解析结果 | 执行方式 |
|
|
627
|
+
| -------- | -------- | -------- |
|
|
628
|
+
| `"npm test"` | `["npm test"]` | 单命令,同步执行(`spawnSync` + `inherit`) |
|
|
629
|
+
| `"npm lint && npm test"` | `["npm lint && npm test"]` | 单命令(`&&` 不拆分),同步执行 |
|
|
630
|
+
| `"npm test & npm build"` | `["npm test", "npm build"]` | 并行执行(`spawn` + `Promise.all`) |
|
|
631
|
+
| `"npm lint && npm test & npm build"` | `["npm lint && npm test", "npm build"]` | 并行执行 2 个命令 |
|
|
632
|
+
|
|
633
|
+
**单命令执行:**
|
|
634
|
+
|
|
635
|
+
当解析后只有 1 个命令时,通过 `spawnSync` + `inherit` stdio 模式同步执行,输出实时显示在终端。
|
|
636
|
+
|
|
637
|
+
**并行命令执行:**
|
|
638
|
+
|
|
639
|
+
当解析后有多个命令时,通过 `runParallelCommands()`(`src/utils/shell.ts`)执行:
|
|
640
|
+
|
|
641
|
+
- 每个命令通过 Node.js `spawn` 以 shell 模式启动,`stdio: 'inherit'`
|
|
642
|
+
- 使用 `Promise.all` 等待全部命令完成
|
|
643
|
+
- 完成后汇总输出各命令的执行结果
|
|
644
|
+
|
|
645
|
+
**向后兼容性:**
|
|
646
|
+
|
|
647
|
+
- `-r "npm test"` — 单命令,走原有同步路径,行为无变化
|
|
648
|
+
- `-r "npm lint && npm test"` — `&&` 不拆分,走原有同步路径,行为无变化
|
|
649
|
+
- `-r "npm test & npm build"` — **新行为**:并行执行,等全部完成后汇总
|
|
650
|
+
|
|
616
651
|
**输出格式:**
|
|
617
652
|
|
|
618
653
|
```
|
|
619
|
-
#
|
|
654
|
+
# 单命令执行成功
|
|
620
655
|
正在主 worktree 中执行命令: npm test
|
|
621
656
|
────────────────────────────────────────
|
|
622
657
|
... 命令的实时输出 ...
|
|
623
658
|
────────────────────────────────────────
|
|
624
659
|
✓ 命令执行完成: npm test,退出码: 0
|
|
625
660
|
|
|
626
|
-
#
|
|
661
|
+
# 单命令执行失败(退出码非 0)
|
|
627
662
|
正在主 worktree 中执行命令: npm test
|
|
628
663
|
────────────────────────────────────────
|
|
629
664
|
... 命令的实时输出 ...
|
|
630
665
|
────────────────────────────────────────
|
|
631
666
|
✗ 命令执行完成: npm test,退出码: 1
|
|
632
667
|
|
|
633
|
-
#
|
|
668
|
+
# 单命令执行出错(进程启动失败)
|
|
634
669
|
正在主 worktree 中执行命令: nonexistent
|
|
635
670
|
────────────────────────────────────────
|
|
636
671
|
────────────────────────────────────────
|
|
637
672
|
✗ 命令执行出错: spawn ENOENT
|
|
673
|
+
|
|
674
|
+
# 并行命令执行(全部成功)
|
|
675
|
+
正在并行执行 2 个命令...
|
|
676
|
+
[1/2] pnpm test
|
|
677
|
+
[2/2] pnpm build
|
|
678
|
+
────────────────────────────────────────
|
|
679
|
+
... 各命令的实时输出(交错显示) ...
|
|
680
|
+
────────────────────────────────────────
|
|
681
|
+
✓ pnpm test
|
|
682
|
+
✓ pnpm build
|
|
683
|
+
✓ 全部 2 个命令执行成功
|
|
684
|
+
|
|
685
|
+
# 并行命令执行(部分失败)
|
|
686
|
+
正在并行执行 2 个命令...
|
|
687
|
+
[1/2] pnpm test
|
|
688
|
+
[2/2] pnpm build
|
|
689
|
+
────────────────────────────────────────
|
|
690
|
+
... 各命令的实时输出(交错显示) ...
|
|
691
|
+
────────────────────────────────────────
|
|
692
|
+
✗ pnpm test(退出码: 1)
|
|
693
|
+
✓ pnpm build
|
|
694
|
+
共 2 个命令,1 个成功,1 个失败
|
|
638
695
|
```
|
|
639
696
|
|
|
697
|
+
**实现要点:**
|
|
698
|
+
|
|
699
|
+
- 命令解析:`parseParallelCommands()`(`src/utils/shell.ts`)
|
|
700
|
+
- 并行执行:`runParallelCommands()`(`src/utils/shell.ts`),返回 `ParallelCommandResult[]`
|
|
701
|
+
- 结果汇总:`reportParallelResults()`(`src/commands/validate.ts`)
|
|
702
|
+
- 消息常量:`MESSAGES.VALIDATE_PARALLEL_*` 系列(`src/constants/messages/validate.ts`)
|
|
703
|
+
|
|
640
704
|
#### 增量 validate(存在历史快照)
|
|
641
705
|
|
|
642
706
|
当 `~/.clawt/validate-snapshots/<project>/<branchName>.tree` 存在时,自动进入增量模式:
|
package/package.json
CHANGED
package/src/commands/remove.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
printInfo,
|
|
17
17
|
printSuccess,
|
|
18
18
|
printError,
|
|
19
|
+
printHint,
|
|
19
20
|
confirmAction,
|
|
20
21
|
removeSnapshot,
|
|
21
22
|
removeProjectSnapshots,
|
|
@@ -85,6 +86,9 @@ async function handleRemove(options: RemoveOptions): Promise<void> {
|
|
|
85
86
|
|
|
86
87
|
if (!autoDelete) {
|
|
87
88
|
shouldDeleteBranch = await confirmAction('是否同时删除对应的本地分支?');
|
|
89
|
+
if (!shouldDeleteBranch) {
|
|
90
|
+
printHint(MESSAGES.REMOVE_BRANCHES_KEPT);
|
|
91
|
+
}
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
// 执行移除,收集失败项
|
package/src/commands/validate.ts
CHANGED
|
@@ -40,8 +40,10 @@ import {
|
|
|
40
40
|
printSeparator,
|
|
41
41
|
resolveTargetWorktree,
|
|
42
42
|
runCommandInherited,
|
|
43
|
+
parseParallelCommands,
|
|
44
|
+
runParallelCommands,
|
|
43
45
|
} from '../utils/index.js';
|
|
44
|
-
import type { WorktreeResolveMessages } from '../utils/index.js';
|
|
46
|
+
import type { WorktreeResolveMessages, ParallelCommandResult } from '../utils/index.js';
|
|
45
47
|
|
|
46
48
|
/** validate 命令的分支解析消息配置 */
|
|
47
49
|
const VALIDATE_RESOLVE_MESSAGES: WorktreeResolveMessages = {
|
|
@@ -305,13 +307,11 @@ function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath:
|
|
|
305
307
|
}
|
|
306
308
|
|
|
307
309
|
/**
|
|
308
|
-
*
|
|
309
|
-
* 命令执行失败不影响 validate 本身的结果,仅输出提示
|
|
310
|
+
* 执行单个命令(同步方式,保持原有行为不变)
|
|
310
311
|
* @param {string} command - 要执行的命令字符串
|
|
311
312
|
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
312
313
|
*/
|
|
313
|
-
function
|
|
314
|
-
printInfo('');
|
|
314
|
+
function executeSingleCommand(command: string, mainWorktreePath: string): void {
|
|
315
315
|
printInfo(MESSAGES.VALIDATE_RUN_START(command));
|
|
316
316
|
printSeparator();
|
|
317
317
|
|
|
@@ -333,6 +333,73 @@ function executeRunCommand(command: string, mainWorktreePath: string): void {
|
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
+
/**
|
|
337
|
+
* 汇总输出并行命令的执行结果
|
|
338
|
+
* @param {ParallelCommandResult[]} results - 各命令的执行结果数组
|
|
339
|
+
*/
|
|
340
|
+
function reportParallelResults(results: ParallelCommandResult[]): void {
|
|
341
|
+
printSeparator();
|
|
342
|
+
|
|
343
|
+
const successCount = results.filter((r) => r.exitCode === 0 && !r.error).length;
|
|
344
|
+
const failedCount = results.length - successCount;
|
|
345
|
+
|
|
346
|
+
for (const result of results) {
|
|
347
|
+
if (result.error) {
|
|
348
|
+
printError(MESSAGES.VALIDATE_PARALLEL_CMD_ERROR(result.command, result.error));
|
|
349
|
+
} else if (result.exitCode === 0) {
|
|
350
|
+
printSuccess(MESSAGES.VALIDATE_PARALLEL_CMD_SUCCESS(result.command));
|
|
351
|
+
} else {
|
|
352
|
+
printError(MESSAGES.VALIDATE_PARALLEL_CMD_FAILED(result.command, result.exitCode));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (failedCount === 0) {
|
|
357
|
+
printSuccess(MESSAGES.VALIDATE_PARALLEL_RUN_ALL_SUCCESS(results.length));
|
|
358
|
+
} else {
|
|
359
|
+
printError(MESSAGES.VALIDATE_PARALLEL_RUN_SUMMARY(successCount, failedCount));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* 并行执行多个命令并汇总结果
|
|
365
|
+
* @param {string[]} commands - 要并行执行的命令数组
|
|
366
|
+
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
367
|
+
*/
|
|
368
|
+
async function executeParallelCommands(commands: string[], mainWorktreePath: string): Promise<void> {
|
|
369
|
+
printInfo(MESSAGES.VALIDATE_PARALLEL_RUN_START(commands.length));
|
|
370
|
+
|
|
371
|
+
for (let i = 0; i < commands.length; i++) {
|
|
372
|
+
printInfo(MESSAGES.VALIDATE_PARALLEL_CMD_START(i + 1, commands.length, commands[i]));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
printSeparator();
|
|
376
|
+
|
|
377
|
+
const results = await runParallelCommands(commands, { cwd: mainWorktreePath });
|
|
378
|
+
|
|
379
|
+
reportParallelResults(results);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* 在主 worktree 中执行用户指定的命令
|
|
384
|
+
* 根据命令字符串中的 & 分隔符决定是单命令执行还是并行执行
|
|
385
|
+
* 命令执行失败不影响 validate 本身的结果,仅输出提示
|
|
386
|
+
* @param {string} command - 要执行的命令字符串
|
|
387
|
+
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
388
|
+
*/
|
|
389
|
+
async function executeRunCommand(command: string, mainWorktreePath: string): Promise<void> {
|
|
390
|
+
printInfo('');
|
|
391
|
+
|
|
392
|
+
const commands = parseParallelCommands(command);
|
|
393
|
+
|
|
394
|
+
if (commands.length <= 1) {
|
|
395
|
+
// 单命令(包括含 && 的串行命令),走原有同步路径
|
|
396
|
+
executeSingleCommand(commands[0] || command, mainWorktreePath);
|
|
397
|
+
} else {
|
|
398
|
+
// 多命令,并行执行
|
|
399
|
+
await executeParallelCommands(commands, mainWorktreePath);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
336
403
|
/**
|
|
337
404
|
* 执行 validate 命令的核心逻辑
|
|
338
405
|
* @param {ValidateOptions} options - 命令选项
|
|
@@ -386,6 +453,6 @@ async function handleValidate(options: ValidateOptions): Promise<void> {
|
|
|
386
453
|
|
|
387
454
|
// validate 成功后执行用户指定的命令
|
|
388
455
|
if (options.run) {
|
|
389
|
-
executeRunCommand(options.run, mainWorktreePath);
|
|
456
|
+
await executeRunCommand(options.run, mainWorktreePath);
|
|
390
457
|
}
|
|
391
458
|
}
|
|
@@ -12,4 +12,6 @@ export const REMOVE_MESSAGES = {
|
|
|
12
12
|
/** 批量移除部分失败 */
|
|
13
13
|
REMOVE_PARTIAL_FAILURE: (failures: Array<{ path: string; error: string }>) =>
|
|
14
14
|
`以下 worktree 移除失败:\n${failures.map((f) => ` ✗ ${f.path}: ${f.error}`).join('\n')}`,
|
|
15
|
+
/** 用户选择保留本地分支 */
|
|
16
|
+
REMOVE_BRANCHES_KEPT: '已保留本地分支,可稍后使用 git branch -D <分支名> 手动删除',
|
|
15
17
|
} as const;
|
|
@@ -32,4 +32,25 @@ export const VALIDATE_MESSAGES = {
|
|
|
32
32
|
/** --run 命令执行异常(进程启动失败等) */
|
|
33
33
|
VALIDATE_RUN_ERROR: (command: string, errorMessage: string) =>
|
|
34
34
|
`✗ 命令执行出错: ${errorMessage}`,
|
|
35
|
+
/** 并行命令开始执行提示 */
|
|
36
|
+
VALIDATE_PARALLEL_RUN_START: (count: number) =>
|
|
37
|
+
`正在并行执行 ${count} 个命令...`,
|
|
38
|
+
/** 并行执行中单个命令开始提示(带序号) */
|
|
39
|
+
VALIDATE_PARALLEL_CMD_START: (index: number, total: number, command: string) =>
|
|
40
|
+
`[${index}/${total}] ${command}`,
|
|
41
|
+
/** 并行执行全部成功汇总提示 */
|
|
42
|
+
VALIDATE_PARALLEL_RUN_ALL_SUCCESS: (count: number) =>
|
|
43
|
+
`✓ 全部 ${count} 个命令执行成功`,
|
|
44
|
+
/** 并行执行部分失败汇总提示 */
|
|
45
|
+
VALIDATE_PARALLEL_RUN_SUMMARY: (successCount: number, failedCount: number) =>
|
|
46
|
+
`共 ${successCount + failedCount} 个命令,${successCount} 个成功,${failedCount} 个失败`,
|
|
47
|
+
/** 并行执行中单个命令成功 */
|
|
48
|
+
VALIDATE_PARALLEL_CMD_SUCCESS: (command: string) =>
|
|
49
|
+
` ✓ ${command}`,
|
|
50
|
+
/** 并行执行中单个命令失败 */
|
|
51
|
+
VALIDATE_PARALLEL_CMD_FAILED: (command: string, exitCode: number) =>
|
|
52
|
+
` ✗ ${command}(退出码: ${exitCode})`,
|
|
53
|
+
/** 并行执行中单个命令启动失败 */
|
|
54
|
+
VALIDATE_PARALLEL_CMD_ERROR: (command: string, errorMessage: string) =>
|
|
55
|
+
` ✗ ${command}(错误: ${errorMessage})`,
|
|
35
56
|
} as const;
|
package/src/utils/formatter.ts
CHANGED
|
@@ -35,6 +35,14 @@ export function printInfo(message: string): void {
|
|
|
35
35
|
console.log(message);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* 输出提示信息(橙色)
|
|
40
|
+
* @param {string} message - 消息内容
|
|
41
|
+
*/
|
|
42
|
+
export function printHint(message: string): void {
|
|
43
|
+
console.log(chalk.hex('#FF8C00')(message));
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
/**
|
|
39
47
|
* 输出分隔线
|
|
40
48
|
*/
|
package/src/utils/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { execCommand, spawnProcess, killAllChildProcesses, execCommandWithInput, runCommandInherited } from './shell.js';
|
|
1
|
+
export { execCommand, spawnProcess, killAllChildProcesses, execCommandWithInput, runCommandInherited, parseParallelCommands, runParallelCommands } from './shell.js';
|
|
2
|
+
export type { ParallelCommandResult } from './shell.js';
|
|
2
3
|
export {
|
|
3
4
|
getGitCommonDir,
|
|
4
5
|
getGitTopLevel,
|
|
@@ -49,7 +50,7 @@ export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } fro
|
|
|
49
50
|
export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
|
|
50
51
|
export { createWorktrees, getProjectWorktrees, getProjectWorktreeDir, cleanupWorktrees, getWorktreeStatus, createWorktreesByBranches } from './worktree.js';
|
|
51
52
|
export { loadConfig, writeDefaultConfig, writeConfig, saveConfig, getConfigValue, ensureClawtDirs, parseConcurrency } from './config.js';
|
|
52
|
-
export { printSuccess, printError, printWarning, printInfo, printSeparator, printDoubleSeparator, confirmAction, confirmDestructiveAction, formatWorktreeStatus, isWorktreeIdle, formatDuration } from './formatter.js';
|
|
53
|
+
export { printSuccess, printError, printWarning, printInfo, printHint, printSeparator, printDoubleSeparator, confirmAction, confirmDestructiveAction, formatWorktreeStatus, isWorktreeIdle, formatDuration } from './formatter.js';
|
|
53
54
|
export { ensureDir, removeEmptyDir } from './fs.js';
|
|
54
55
|
export { multilineInput } from './prompt.js';
|
|
55
56
|
export { launchInteractiveClaude, hasClaudeSessionHistory, launchInteractiveClaudeInNewTerminal } from './claude.js';
|
package/src/utils/shell.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { execSync, execFileSync, spawn, spawnSync, type ChildProcess, type SpawnSyncReturns, type StdioOptions } from 'node:child_process';
|
|
2
2
|
import { logger } from '../logger/index.js';
|
|
3
3
|
|
|
4
|
+
/** 并行命令执行的单个结果 */
|
|
5
|
+
export interface ParallelCommandResult {
|
|
6
|
+
/** 执行的命令字符串 */
|
|
7
|
+
command: string;
|
|
8
|
+
/** 进程退出码 */
|
|
9
|
+
exitCode: number;
|
|
10
|
+
/** 进程启动失败时的错误信息 */
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
/**
|
|
5
15
|
* 同步执行 shell 命令并返回 stdout
|
|
6
16
|
* @param {string} command - 要执行的命令
|
|
@@ -92,3 +102,58 @@ export function runCommandInherited(
|
|
|
92
102
|
shell: true,
|
|
93
103
|
});
|
|
94
104
|
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 解析命令字符串中的并行分隔符 `&`,将其拆分为多个独立命令
|
|
108
|
+
* `&&` 不会被拆分(属于 shell 的串行逻辑与操作符)
|
|
109
|
+
* @param {string} commandString - 命令字符串
|
|
110
|
+
* @returns {string[]} 拆分后的命令数组
|
|
111
|
+
*/
|
|
112
|
+
export function parseParallelCommands(commandString: string): string[] {
|
|
113
|
+
// 将 && 临时替换为占位符,避免被 & 分割逻辑误拆
|
|
114
|
+
const placeholder = '\x00AND\x00';
|
|
115
|
+
const escaped = commandString.replace(/&&/g, placeholder);
|
|
116
|
+
|
|
117
|
+
// 按单个 & 分割
|
|
118
|
+
const parts = escaped.split('&');
|
|
119
|
+
|
|
120
|
+
// 还原占位符为 &&,并去除首尾空白
|
|
121
|
+
return parts
|
|
122
|
+
.map((part) => part.replace(new RegExp(placeholder, 'g'), '&&').trim())
|
|
123
|
+
.filter((part) => part.length > 0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 并行执行多个命令,等待全部完成后返回结果
|
|
128
|
+
* 每个命令通过 spawn 以 shell 模式启动,stdio 继承父进程(实时输出到终端)
|
|
129
|
+
* @param {string[]} commands - 要并行执行的命令数组
|
|
130
|
+
* @param {object} options - 可选配置
|
|
131
|
+
* @param {string} options.cwd - 工作目录
|
|
132
|
+
* @returns {Promise<ParallelCommandResult[]>} 各命令的执行结果
|
|
133
|
+
*/
|
|
134
|
+
export function runParallelCommands(
|
|
135
|
+
commands: string[],
|
|
136
|
+
options?: { cwd?: string },
|
|
137
|
+
): Promise<ParallelCommandResult[]> {
|
|
138
|
+
const promises = commands.map((command) => {
|
|
139
|
+
return new Promise<ParallelCommandResult>((resolve) => {
|
|
140
|
+
logger.debug(`并行启动命令: ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`);
|
|
141
|
+
|
|
142
|
+
const child = spawn(command, {
|
|
143
|
+
cwd: options?.cwd,
|
|
144
|
+
stdio: 'inherit',
|
|
145
|
+
shell: true,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
child.on('error', (err) => {
|
|
149
|
+
resolve({ command, exitCode: 1, error: err.message });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
child.on('close', (code) => {
|
|
153
|
+
resolve({ command, exitCode: code ?? 1 });
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return Promise.all(promises);
|
|
159
|
+
}
|
|
@@ -24,6 +24,7 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
24
24
|
REMOVE_SELECT_BRANCH: '请选择要移除的分支(空格选择,回车确认)',
|
|
25
25
|
REMOVE_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支`,
|
|
26
26
|
REMOVE_NO_MATCH: (name: string, branches: string[]) => `未找到与 "${name}" 匹配的分支,可用:${branches.join(', ')}`,
|
|
27
|
+
REMOVE_BRANCHES_KEPT: '已保留本地分支,可稍后使用 git branch -D <分支名> 手动删除',
|
|
27
28
|
},
|
|
28
29
|
}));
|
|
29
30
|
|
|
@@ -40,6 +41,7 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
40
41
|
printInfo: vi.fn(),
|
|
41
42
|
printSuccess: vi.fn(),
|
|
42
43
|
printError: vi.fn(),
|
|
44
|
+
printHint: vi.fn(),
|
|
43
45
|
confirmAction: vi.fn(),
|
|
44
46
|
removeSnapshot: vi.fn(),
|
|
45
47
|
removeProjectSnapshots: vi.fn(),
|
|
@@ -59,6 +61,7 @@ import {
|
|
|
59
61
|
removeProjectSnapshots,
|
|
60
62
|
printSuccess,
|
|
61
63
|
printError,
|
|
64
|
+
printHint,
|
|
62
65
|
resolveTargetWorktrees,
|
|
63
66
|
} from '../../../src/utils/index.js';
|
|
64
67
|
|
|
@@ -72,6 +75,7 @@ const mockedRemoveSnapshot = vi.mocked(removeSnapshot);
|
|
|
72
75
|
const mockedRemoveProjectSnapshots = vi.mocked(removeProjectSnapshots);
|
|
73
76
|
const mockedPrintSuccess = vi.mocked(printSuccess);
|
|
74
77
|
const mockedPrintError = vi.mocked(printError);
|
|
78
|
+
const mockedPrintHint = vi.mocked(printHint);
|
|
75
79
|
const mockedResolveTargetWorktrees = vi.mocked(resolveTargetWorktrees);
|
|
76
80
|
|
|
77
81
|
beforeEach(() => {
|
|
@@ -86,6 +90,7 @@ beforeEach(() => {
|
|
|
86
90
|
mockedRemoveProjectSnapshots.mockReset();
|
|
87
91
|
mockedPrintSuccess.mockReset();
|
|
88
92
|
mockedPrintError.mockReset();
|
|
93
|
+
mockedPrintHint.mockReset();
|
|
89
94
|
mockedResolveTargetWorktrees.mockReset();
|
|
90
95
|
});
|
|
91
96
|
|
|
@@ -199,6 +204,8 @@ describe('handleRemove', () => {
|
|
|
199
204
|
expect(mockedConfirmAction).toHaveBeenCalled();
|
|
200
205
|
// 用户拒绝删除分支
|
|
201
206
|
expect(mockedDeleteBranch).not.toHaveBeenCalled();
|
|
207
|
+
// 应提示用户分支已保留
|
|
208
|
+
expect(mockedPrintHint).toHaveBeenCalledWith('已保留本地分支,可稍后使用 git branch -D <分支名> 手动删除');
|
|
202
209
|
});
|
|
203
210
|
|
|
204
211
|
it('-b 指定不存在的分支时 resolveTargetWorktrees 抛出错误', async () => {
|
|
@@ -32,6 +32,13 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
32
32
|
VALIDATE_RUN_SUCCESS: (cmd: string) => `✓ 命令执行完成: ${cmd}`,
|
|
33
33
|
VALIDATE_RUN_FAILED: (cmd: string, code: number) => `✗ 命令失败: ${cmd},退出码: ${code}`,
|
|
34
34
|
VALIDATE_RUN_ERROR: (cmd: string, msg: string) => `✗ 命令出错: ${msg}`,
|
|
35
|
+
VALIDATE_PARALLEL_RUN_START: (count: number) => `正在并行执行 ${count} 个命令...`,
|
|
36
|
+
VALIDATE_PARALLEL_CMD_START: (index: number, total: number, cmd: string) => `[${index}/${total}] ${cmd}`,
|
|
37
|
+
VALIDATE_PARALLEL_RUN_ALL_SUCCESS: (count: number) => `✓ 全部 ${count} 个命令执行成功`,
|
|
38
|
+
VALIDATE_PARALLEL_RUN_SUMMARY: (s: number, f: number) => `共 ${s + f} 个命令,${s} 个成功,${f} 个失败`,
|
|
39
|
+
VALIDATE_PARALLEL_CMD_SUCCESS: (cmd: string) => ` ✓ ${cmd}`,
|
|
40
|
+
VALIDATE_PARALLEL_CMD_FAILED: (cmd: string, code: number) => ` ✗ ${cmd}(退出码: ${code})`,
|
|
41
|
+
VALIDATE_PARALLEL_CMD_ERROR: (cmd: string, msg: string) => ` ✗ ${cmd}(错误: ${msg})`,
|
|
35
42
|
SEPARATOR: '────',
|
|
36
43
|
},
|
|
37
44
|
}));
|
|
@@ -79,6 +86,8 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
79
86
|
runCommandInherited: vi.fn(),
|
|
80
87
|
printError: vi.fn(),
|
|
81
88
|
printSeparator: vi.fn(),
|
|
89
|
+
parseParallelCommands: vi.fn(),
|
|
90
|
+
runParallelCommands: vi.fn(),
|
|
82
91
|
}));
|
|
83
92
|
|
|
84
93
|
import { registerValidateCommand } from '../../../src/commands/validate.js';
|
|
@@ -116,6 +125,8 @@ import {
|
|
|
116
125
|
runCommandInherited,
|
|
117
126
|
printError,
|
|
118
127
|
printSeparator,
|
|
128
|
+
parseParallelCommands,
|
|
129
|
+
runParallelCommands,
|
|
119
130
|
} from '../../../src/utils/index.js';
|
|
120
131
|
|
|
121
132
|
const mockedGetProjectName = vi.mocked(getProjectName);
|
|
@@ -151,6 +162,8 @@ const mockedPrintWarning = vi.mocked(printWarning);
|
|
|
151
162
|
const mockedRunCommandInherited = vi.mocked(runCommandInherited);
|
|
152
163
|
const mockedPrintError = vi.mocked(printError);
|
|
153
164
|
const mockedPrintSeparator = vi.mocked(printSeparator);
|
|
165
|
+
const mockedParseParallelCommands = vi.mocked(parseParallelCommands);
|
|
166
|
+
const mockedRunParallelCommands = vi.mocked(runParallelCommands);
|
|
154
167
|
|
|
155
168
|
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
156
169
|
|
|
@@ -188,6 +201,10 @@ beforeEach(() => {
|
|
|
188
201
|
mockedRunCommandInherited.mockReset();
|
|
189
202
|
mockedPrintError.mockReset();
|
|
190
203
|
mockedPrintSeparator.mockReset();
|
|
204
|
+
mockedParseParallelCommands.mockReset();
|
|
205
|
+
mockedRunParallelCommands.mockReset();
|
|
206
|
+
// 默认让 parseParallelCommands 返回单命令数组,保持旧测试兼容
|
|
207
|
+
mockedParseParallelCommands.mockImplementation((cmd: string) => [cmd]);
|
|
191
208
|
});
|
|
192
209
|
|
|
193
210
|
describe('registerValidateCommand', () => {
|
|
@@ -516,3 +533,89 @@ describe('--run 选项', () => {
|
|
|
516
533
|
expect(mockedRunCommandInherited).not.toHaveBeenCalled();
|
|
517
534
|
});
|
|
518
535
|
});
|
|
536
|
+
|
|
537
|
+
describe('--run 并行命令', () => {
|
|
538
|
+
/** 设置首次 validate 成功的公共 mock */
|
|
539
|
+
function setupSuccessfulFirstValidate(): void {
|
|
540
|
+
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
541
|
+
mockedHasLocalCommits.mockReturnValue(true);
|
|
542
|
+
mockedHasSnapshot.mockReturnValue(false);
|
|
543
|
+
mockedGitDiffBinaryAgainstBranch.mockReturnValue(Buffer.from('diff'));
|
|
544
|
+
mockedGitWriteTree.mockReturnValue('treehash');
|
|
545
|
+
mockedGetHeadCommitHash.mockReturnValue('headhash');
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
it('& 分隔的命令触发并行执行', async () => {
|
|
549
|
+
setupSuccessfulFirstValidate();
|
|
550
|
+
mockedParseParallelCommands.mockReturnValue(['pnpm test', 'pnpm build']);
|
|
551
|
+
mockedRunParallelCommands.mockResolvedValue([
|
|
552
|
+
{ command: 'pnpm test', exitCode: 0 },
|
|
553
|
+
{ command: 'pnpm build', exitCode: 0 },
|
|
554
|
+
]);
|
|
555
|
+
|
|
556
|
+
const program = new Command();
|
|
557
|
+
program.exitOverride();
|
|
558
|
+
registerValidateCommand(program);
|
|
559
|
+
await program.parseAsync(['validate', '-b', 'feature', '-r', 'pnpm test & pnpm build'], { from: 'user' });
|
|
560
|
+
|
|
561
|
+
// 应该调用并行执行而非同步执行
|
|
562
|
+
expect(mockedRunParallelCommands).toHaveBeenCalledWith(['pnpm test', 'pnpm build'], { cwd: '/repo' });
|
|
563
|
+
expect(mockedRunCommandInherited).not.toHaveBeenCalled();
|
|
564
|
+
// 全部成功,printSuccess 被调用(validate 成功 + 各命令成功 + 汇总成功)
|
|
565
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('并行执行部分失败时输出错误汇总', async () => {
|
|
569
|
+
setupSuccessfulFirstValidate();
|
|
570
|
+
mockedParseParallelCommands.mockReturnValue(['pnpm test', 'pnpm build']);
|
|
571
|
+
mockedRunParallelCommands.mockResolvedValue([
|
|
572
|
+
{ command: 'pnpm test', exitCode: 1 },
|
|
573
|
+
{ command: 'pnpm build', exitCode: 0 },
|
|
574
|
+
]);
|
|
575
|
+
|
|
576
|
+
const program = new Command();
|
|
577
|
+
program.exitOverride();
|
|
578
|
+
registerValidateCommand(program);
|
|
579
|
+
await program.parseAsync(['validate', '-b', 'feature', '-r', 'pnpm test & pnpm build'], { from: 'user' });
|
|
580
|
+
|
|
581
|
+
expect(mockedRunParallelCommands).toHaveBeenCalled();
|
|
582
|
+
// 部分失败,应有错误输出
|
|
583
|
+
expect(mockedPrintError).toHaveBeenCalled();
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it('单命令走原有同步路径不触发并行', async () => {
|
|
587
|
+
setupSuccessfulFirstValidate();
|
|
588
|
+
mockedParseParallelCommands.mockReturnValue(['npm test']);
|
|
589
|
+
mockedRunCommandInherited.mockReturnValue({
|
|
590
|
+
pid: 0, output: [], stdout: Buffer.alloc(0), stderr: Buffer.alloc(0),
|
|
591
|
+
status: 0, signal: null, error: undefined,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const program = new Command();
|
|
595
|
+
program.exitOverride();
|
|
596
|
+
registerValidateCommand(program);
|
|
597
|
+
await program.parseAsync(['validate', '-b', 'feature', '-r', 'npm test'], { from: 'user' });
|
|
598
|
+
|
|
599
|
+
// 单命令应该走同步路径
|
|
600
|
+
expect(mockedRunCommandInherited).toHaveBeenCalledWith('npm test', { cwd: '/repo' });
|
|
601
|
+
expect(mockedRunParallelCommands).not.toHaveBeenCalled();
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('&& 命令不触发并行执行', async () => {
|
|
605
|
+
setupSuccessfulFirstValidate();
|
|
606
|
+
mockedParseParallelCommands.mockReturnValue(['pnpm lint && pnpm test']);
|
|
607
|
+
mockedRunCommandInherited.mockReturnValue({
|
|
608
|
+
pid: 0, output: [], stdout: Buffer.alloc(0), stderr: Buffer.alloc(0),
|
|
609
|
+
status: 0, signal: null, error: undefined,
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const program = new Command();
|
|
613
|
+
program.exitOverride();
|
|
614
|
+
registerValidateCommand(program);
|
|
615
|
+
await program.parseAsync(['validate', '-b', 'feature', '-r', 'pnpm lint && pnpm test'], { from: 'user' });
|
|
616
|
+
|
|
617
|
+
// && 不拆分,走同步路径
|
|
618
|
+
expect(mockedRunCommandInherited).toHaveBeenCalledWith('pnpm lint && pnpm test', { cwd: '/repo' });
|
|
619
|
+
expect(mockedRunParallelCommands).not.toHaveBeenCalled();
|
|
620
|
+
});
|
|
621
|
+
});
|
|
@@ -13,7 +13,7 @@ vi.mock('../../../src/logger/index.js', () => ({
|
|
|
13
13
|
}));
|
|
14
14
|
|
|
15
15
|
import { execSync, execFileSync, spawn } from 'node:child_process';
|
|
16
|
-
import { execCommand, execCommandWithInput, spawnProcess, killAllChildProcesses } from '../../../src/utils/shell.js';
|
|
16
|
+
import { execCommand, execCommandWithInput, spawnProcess, killAllChildProcesses, parseParallelCommands } from '../../../src/utils/shell.js';
|
|
17
17
|
|
|
18
18
|
const mockedExecSync = vi.mocked(execSync);
|
|
19
19
|
const mockedExecFileSync = vi.mocked(execFileSync);
|
|
@@ -106,3 +106,44 @@ describe('killAllChildProcesses', () => {
|
|
|
106
106
|
expect(() => killAllChildProcesses([])).not.toThrow();
|
|
107
107
|
});
|
|
108
108
|
});
|
|
109
|
+
|
|
110
|
+
describe('parseParallelCommands', () => {
|
|
111
|
+
it('单个命令返回包含该命令的数组', () => {
|
|
112
|
+
expect(parseParallelCommands('pnpm test')).toEqual(['pnpm test']);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('使用 & 分隔的多个命令被正确拆分', () => {
|
|
116
|
+
expect(parseParallelCommands('pnpm test & pnpm build')).toEqual(['pnpm test', 'pnpm build']);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('&& 不会被拆分,保持为单条命令', () => {
|
|
120
|
+
expect(parseParallelCommands('pnpm lint && pnpm test')).toEqual(['pnpm lint && pnpm test']);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('混合场景:&& 和 & 同时存在', () => {
|
|
124
|
+
expect(parseParallelCommands('pnpm lint && pnpm test & pnpm build')).toEqual([
|
|
125
|
+
'pnpm lint && pnpm test',
|
|
126
|
+
'pnpm build',
|
|
127
|
+
]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('多个 & 分隔的命令', () => {
|
|
131
|
+
expect(parseParallelCommands('cmd1 & cmd2 & cmd3')).toEqual(['cmd1', 'cmd2', 'cmd3']);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('空字符串返回空数组', () => {
|
|
135
|
+
expect(parseParallelCommands('')).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('去除命令首尾空白', () => {
|
|
139
|
+
expect(parseParallelCommands(' pnpm test & pnpm build ')).toEqual(['pnpm test', 'pnpm build']);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('多个 && 不拆分', () => {
|
|
143
|
+
expect(parseParallelCommands('cmd1 && cmd2 && cmd3')).toEqual(['cmd1 && cmd2 && cmd3']);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('尾部 & 后无内容时过滤空字符串', () => {
|
|
147
|
+
expect(parseParallelCommands('pnpm test & ')).toEqual(['pnpm test']);
|
|
148
|
+
});
|
|
149
|
+
});
|