clawt 2.15.0 → 2.16.0
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 +6 -2
- package/dist/index.js +42 -6
- package/dist/postinstall.js +9 -1
- package/docs/spec.md +50 -6
- package/package.json +1 -1
- package/src/commands/validate.ts +38 -0
- package/src/constants/messages/validate.ts +10 -0
- package/src/types/command.ts +2 -0
- package/src/utils/dry-run.ts +1 -1
- package/src/utils/index.ts +1 -1
- package/src/utils/shell.ts +21 -1
- package/tests/unit/commands/validate.test.ts +136 -0
package/README.md
CHANGED
|
@@ -119,12 +119,16 @@ clawt create -b <branch> -n 3 # 批量创建 3 个
|
|
|
119
119
|
### `clawt validate` — 在主 worktree 中验证分支变更
|
|
120
120
|
|
|
121
121
|
```bash
|
|
122
|
-
clawt validate -b <branch>
|
|
123
|
-
clawt validate -b <branch> --clean
|
|
122
|
+
clawt validate -b <branch> # 将变更迁移到主 worktree 测试
|
|
123
|
+
clawt validate -b <branch> --clean # 清理 validate 状态
|
|
124
|
+
clawt validate -b <branch> -r "npm test" # validate 成功后自动运行测试
|
|
125
|
+
clawt validate -b <branch> -r "npm run build" # validate 成功后自动构建
|
|
124
126
|
```
|
|
125
127
|
|
|
126
128
|
支持增量模式:再次 validate 同一分支时,可通过 `git diff` 查看两次之间的增量差异。
|
|
127
129
|
|
|
130
|
+
`-r, --run` 选项可在 validate 成功后自动在主 worktree 中执行指定命令(如测试、构建等),命令执行失败不影响 validate 结果。
|
|
131
|
+
|
|
128
132
|
### `clawt sync` — 同步主分支代码到目标 worktree
|
|
129
133
|
|
|
130
134
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -189,7 +189,15 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
|
189
189
|
/** validate 交互选择提示 */
|
|
190
190
|
VALIDATE_SELECT_BRANCH: "\u8BF7\u9009\u62E9\u8981\u9A8C\u8BC1\u7684\u5206\u652F",
|
|
191
191
|
/** validate 模糊匹配到多个结果提示 */
|
|
192
|
-
VALIDATE_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A
|
|
192
|
+
VALIDATE_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`,
|
|
193
|
+
/** --run 命令开始执行提示 */
|
|
194
|
+
VALIDATE_RUN_START: (command) => `\u6B63\u5728\u4E3B worktree \u4E2D\u6267\u884C\u547D\u4EE4: ${command}`,
|
|
195
|
+
/** --run 命令执行成功(退出码 0) */
|
|
196
|
+
VALIDATE_RUN_SUCCESS: (command) => `\u2713 \u547D\u4EE4\u6267\u884C\u5B8C\u6210: ${command}\uFF0C\u9000\u51FA\u7801: 0`,
|
|
197
|
+
/** --run 命令执行失败(退出码非 0) */
|
|
198
|
+
VALIDATE_RUN_FAILED: (command, exitCode) => `\u2717 \u547D\u4EE4\u6267\u884C\u5B8C\u6210: ${command}\uFF0C\u9000\u51FA\u7801: ${exitCode}`,
|
|
199
|
+
/** --run 命令执行异常(进程启动失败等) */
|
|
200
|
+
VALIDATE_RUN_ERROR: (command, errorMessage) => `\u2717 \u547D\u4EE4\u6267\u884C\u51FA\u9519: ${errorMessage}`
|
|
193
201
|
};
|
|
194
202
|
|
|
195
203
|
// src/constants/messages/sync.ts
|
|
@@ -521,7 +529,7 @@ function enableConsoleTransport() {
|
|
|
521
529
|
}
|
|
522
530
|
|
|
523
531
|
// src/utils/shell.ts
|
|
524
|
-
import { execSync, execFileSync, spawn } from "child_process";
|
|
532
|
+
import { execSync, execFileSync, spawn, spawnSync } from "child_process";
|
|
525
533
|
function execCommand(command, options) {
|
|
526
534
|
logger.debug(`\u6267\u884C\u547D\u4EE4: ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ""}`);
|
|
527
535
|
const result = execSync(command, {
|
|
@@ -555,6 +563,14 @@ function execCommandWithInput(command, args, options) {
|
|
|
555
563
|
});
|
|
556
564
|
return result.trim();
|
|
557
565
|
}
|
|
566
|
+
function runCommandInherited(command, options) {
|
|
567
|
+
logger.debug(`\u6267\u884C\u547D\u4EE4(inherit): ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ""}`);
|
|
568
|
+
return spawnSync(command, {
|
|
569
|
+
cwd: options?.cwd,
|
|
570
|
+
stdio: "inherit",
|
|
571
|
+
shell: true
|
|
572
|
+
});
|
|
573
|
+
}
|
|
558
574
|
|
|
559
575
|
// src/utils/git.ts
|
|
560
576
|
import { basename } from "path";
|
|
@@ -1000,7 +1016,7 @@ function parseConcurrency(optionValue, configValue) {
|
|
|
1000
1016
|
import Enquirer from "enquirer";
|
|
1001
1017
|
|
|
1002
1018
|
// src/utils/claude.ts
|
|
1003
|
-
import { spawnSync } from "child_process";
|
|
1019
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1004
1020
|
import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
|
|
1005
1021
|
import { join as join3 } from "path";
|
|
1006
1022
|
|
|
@@ -1109,7 +1125,7 @@ function launchInteractiveClaude(worktree, options = {}) {
|
|
|
1109
1125
|
printInfo(` \u6A21\u5F0F: ${hasPreviousSession ? "\u7EE7\u7EED\u4E0A\u6B21\u5BF9\u8BDD" : "\u65B0\u5BF9\u8BDD"}`);
|
|
1110
1126
|
}
|
|
1111
1127
|
printInfo("");
|
|
1112
|
-
const result =
|
|
1128
|
+
const result = spawnSync2(cmd, args, {
|
|
1113
1129
|
cwd: worktree.path,
|
|
1114
1130
|
stdio: "inherit"
|
|
1115
1131
|
});
|
|
@@ -1779,7 +1795,7 @@ async function executeBatchTasks(worktrees, tasks, concurrency) {
|
|
|
1779
1795
|
// src/utils/dry-run.ts
|
|
1780
1796
|
import chalk4 from "chalk";
|
|
1781
1797
|
import { join as join5 } from "path";
|
|
1782
|
-
var DRY_RUN_TASK_DESC_MAX_LENGTH =
|
|
1798
|
+
var DRY_RUN_TASK_DESC_MAX_LENGTH = 80;
|
|
1783
1799
|
function truncateTaskDesc(task) {
|
|
1784
1800
|
const oneLine = task.replace(/\n/g, " ").trim();
|
|
1785
1801
|
if (oneLine.length <= DRY_RUN_TASK_DESC_MAX_LENGTH) {
|
|
@@ -2227,7 +2243,7 @@ var VALIDATE_RESOLVE_MESSAGES = {
|
|
|
2227
2243
|
noMatch: MESSAGES.VALIDATE_NO_MATCH
|
|
2228
2244
|
};
|
|
2229
2245
|
function registerValidateCommand(program2) {
|
|
2230
|
-
program2.command("validate").description("\u5728\u4E3B worktree \u9A8C\u8BC1\u67D0\u4E2A worktree \u5206\u652F\u7684\u53D8\u66F4").option("-b, --branch <branchName>", "\u8981\u9A8C\u8BC1\u7684\u5206\u652F\u540D\uFF08\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF0C\u4E0D\u4F20\u5219\u5217\u51FA\u6240\u6709\u5206\u652F\uFF09").option("--clean", "\u6E05\u7406 validate \u72B6\u6001\uFF08\u91CD\u7F6E\u4E3B worktree \u5E76\u5220\u9664\u5FEB\u7167\uFF09").action(async (options) => {
|
|
2246
|
+
program2.command("validate").description("\u5728\u4E3B worktree \u9A8C\u8BC1\u67D0\u4E2A worktree \u5206\u652F\u7684\u53D8\u66F4").option("-b, --branch <branchName>", "\u8981\u9A8C\u8BC1\u7684\u5206\u652F\u540D\uFF08\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF0C\u4E0D\u4F20\u5219\u5217\u51FA\u6240\u6709\u5206\u652F\uFF09").option("--clean", "\u6E05\u7406 validate \u72B6\u6001\uFF08\u91CD\u7F6E\u4E3B worktree \u5E76\u5220\u9664\u5FEB\u7167\uFF09").option("-r, --run <command>", "validate \u6210\u529F\u540E\u5728\u4E3B worktree \u4E2D\u6267\u884C\u7684\u547D\u4EE4").action(async (options) => {
|
|
2231
2247
|
await handleValidate(options);
|
|
2232
2248
|
});
|
|
2233
2249
|
}
|
|
@@ -2368,6 +2384,23 @@ function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, project
|
|
|
2368
2384
|
}
|
|
2369
2385
|
printSuccess(MESSAGES.INCREMENTAL_VALIDATE_SUCCESS(branchName));
|
|
2370
2386
|
}
|
|
2387
|
+
function executeRunCommand(command, mainWorktreePath) {
|
|
2388
|
+
printInfo("");
|
|
2389
|
+
printInfo(MESSAGES.VALIDATE_RUN_START(command));
|
|
2390
|
+
printSeparator();
|
|
2391
|
+
const result = runCommandInherited(command, { cwd: mainWorktreePath });
|
|
2392
|
+
printSeparator();
|
|
2393
|
+
if (result.error) {
|
|
2394
|
+
printError(MESSAGES.VALIDATE_RUN_ERROR(command, result.error.message));
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
const exitCode = result.status ?? 1;
|
|
2398
|
+
if (exitCode === 0) {
|
|
2399
|
+
printSuccess(MESSAGES.VALIDATE_RUN_SUCCESS(command));
|
|
2400
|
+
} else {
|
|
2401
|
+
printError(MESSAGES.VALIDATE_RUN_FAILED(command, exitCode));
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2371
2404
|
async function handleValidate(options) {
|
|
2372
2405
|
if (options.clean) {
|
|
2373
2406
|
await handleValidateClean(options);
|
|
@@ -2399,6 +2432,9 @@ async function handleValidate(options) {
|
|
|
2399
2432
|
}
|
|
2400
2433
|
handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
2401
2434
|
}
|
|
2435
|
+
if (options.run) {
|
|
2436
|
+
executeRunCommand(options.run, mainWorktreePath);
|
|
2437
|
+
}
|
|
2402
2438
|
}
|
|
2403
2439
|
|
|
2404
2440
|
// src/commands/merge.ts
|
package/dist/postinstall.js
CHANGED
|
@@ -181,7 +181,15 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
|
181
181
|
/** validate 交互选择提示 */
|
|
182
182
|
VALIDATE_SELECT_BRANCH: "\u8BF7\u9009\u62E9\u8981\u9A8C\u8BC1\u7684\u5206\u652F",
|
|
183
183
|
/** validate 模糊匹配到多个结果提示 */
|
|
184
|
-
VALIDATE_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A
|
|
184
|
+
VALIDATE_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`,
|
|
185
|
+
/** --run 命令开始执行提示 */
|
|
186
|
+
VALIDATE_RUN_START: (command) => `\u6B63\u5728\u4E3B worktree \u4E2D\u6267\u884C\u547D\u4EE4: ${command}`,
|
|
187
|
+
/** --run 命令执行成功(退出码 0) */
|
|
188
|
+
VALIDATE_RUN_SUCCESS: (command) => `\u2713 \u547D\u4EE4\u6267\u884C\u5B8C\u6210: ${command}\uFF0C\u9000\u51FA\u7801: 0`,
|
|
189
|
+
/** --run 命令执行失败(退出码非 0) */
|
|
190
|
+
VALIDATE_RUN_FAILED: (command, exitCode) => `\u2717 \u547D\u4EE4\u6267\u884C\u5B8C\u6210: ${command}\uFF0C\u9000\u51FA\u7801: ${exitCode}`,
|
|
191
|
+
/** --run 命令执行异常(进程启动失败等) */
|
|
192
|
+
VALIDATE_RUN_ERROR: (command, errorMessage) => `\u2717 \u547D\u4EE4\u6267\u884C\u51FA\u9519: ${errorMessage}`
|
|
185
193
|
};
|
|
186
194
|
|
|
187
195
|
// src/constants/messages/sync.ts
|
package/docs/spec.md
CHANGED
|
@@ -460,18 +460,19 @@ Claude Code CLI 以 `--output-format json` 运行时,退出后会在 stdout
|
|
|
460
460
|
|
|
461
461
|
```bash
|
|
462
462
|
# 指定分支名(支持模糊匹配)
|
|
463
|
-
clawt validate -b <branchName> [--clean]
|
|
463
|
+
clawt validate -b <branchName> [--clean] [-r <command>]
|
|
464
464
|
|
|
465
465
|
# 不指定分支名(列出所有分支供选择)
|
|
466
|
-
clawt validate [--clean]
|
|
466
|
+
clawt validate [--clean] [-r <command>]
|
|
467
467
|
```
|
|
468
468
|
|
|
469
469
|
**参数:**
|
|
470
470
|
|
|
471
|
-
| 参数
|
|
472
|
-
|
|
|
473
|
-
| `-b`
|
|
474
|
-
| `--clean`
|
|
471
|
+
| 参数 | 必填 | 说明 |
|
|
472
|
+
| ------------- | ---- | ------------------------------------------------------------------------ |
|
|
473
|
+
| `-b` | 否 | 要验证的 worktree 分支名(支持模糊匹配,不传则列出所有分支供选择) |
|
|
474
|
+
| `--clean` | 否 | 清理 validate 状态(重置主 worktree 并删除快照) |
|
|
475
|
+
| `-r, --run` | 否 | validate 成功后在主 worktree 中执行的命令(如测试、构建等) |
|
|
475
476
|
|
|
476
477
|
> **限制:** 单次只能验证一个分支,不支持批量验证。
|
|
477
478
|
|
|
@@ -597,6 +598,45 @@ git restore --staged .
|
|
|
597
598
|
可以开始验证了
|
|
598
599
|
```
|
|
599
600
|
|
|
601
|
+
##### 步骤 6:执行 `--run` 命令(可选)
|
|
602
|
+
|
|
603
|
+
如果用户传入了 `-r, --run` 选项,在 validate 成功后自动在主 worktree 中执行指定命令:
|
|
604
|
+
|
|
605
|
+
```bash
|
|
606
|
+
# 示例
|
|
607
|
+
clawt validate -b feature-scheme-1 -r "npm test"
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
**执行说明:**
|
|
611
|
+
|
|
612
|
+
- 命令通过 `spawnSync` + `inherit` stdio 模式在主 worktree 中执行,输出实时显示在终端
|
|
613
|
+
- 命令执行失败(退出码非 0 或进程启动失败)**不影响** validate 本身的结果,仅输出提示信息
|
|
614
|
+
- `--clean` 模式下传入 `--run` 会被忽略(只执行 clean 逻辑)
|
|
615
|
+
|
|
616
|
+
**输出格式:**
|
|
617
|
+
|
|
618
|
+
```
|
|
619
|
+
# 命令执行成功
|
|
620
|
+
正在主 worktree 中执行命令: npm test
|
|
621
|
+
────────────────────────────────────────
|
|
622
|
+
... 命令的实时输出 ...
|
|
623
|
+
────────────────────────────────────────
|
|
624
|
+
✓ 命令执行完成: npm test,退出码: 0
|
|
625
|
+
|
|
626
|
+
# 命令执行失败(退出码非 0)
|
|
627
|
+
正在主 worktree 中执行命令: npm test
|
|
628
|
+
────────────────────────────────────────
|
|
629
|
+
... 命令的实时输出 ...
|
|
630
|
+
────────────────────────────────────────
|
|
631
|
+
✗ 命令执行完成: npm test,退出码: 1
|
|
632
|
+
|
|
633
|
+
# 命令执行出错(进程启动失败)
|
|
634
|
+
正在主 worktree 中执行命令: nonexistent
|
|
635
|
+
────────────────────────────────────────
|
|
636
|
+
────────────────────────────────────────
|
|
637
|
+
✗ 命令执行出错: spawn ENOENT
|
|
638
|
+
```
|
|
639
|
+
|
|
600
640
|
#### 增量 validate(存在历史快照)
|
|
601
641
|
|
|
602
642
|
当 `~/.clawt/validate-snapshots/<project>/<branchName>.tree` 存在时,自动进入增量模式:
|
|
@@ -666,6 +706,10 @@ git apply --cached < patch
|
|
|
666
706
|
可以开始验证了
|
|
667
707
|
```
|
|
668
708
|
|
|
709
|
+
##### 步骤 7:执行 `--run` 命令(可选)
|
|
710
|
+
|
|
711
|
+
与首次 validate 的步骤 6 相同,增量 validate 成功后也会执行 `-r, --run` 指定的命令。
|
|
712
|
+
|
|
669
713
|
---
|
|
670
714
|
|
|
671
715
|
### 5.5 移除 Worktree
|
package/package.json
CHANGED
package/src/commands/validate.ts
CHANGED
|
@@ -34,9 +34,12 @@ import {
|
|
|
34
34
|
removeSnapshot,
|
|
35
35
|
confirmDestructiveAction,
|
|
36
36
|
printSuccess,
|
|
37
|
+
printError,
|
|
37
38
|
printWarning,
|
|
38
39
|
printInfo,
|
|
40
|
+
printSeparator,
|
|
39
41
|
resolveTargetWorktree,
|
|
42
|
+
runCommandInherited,
|
|
40
43
|
} from '../utils/index.js';
|
|
41
44
|
import type { WorktreeResolveMessages } from '../utils/index.js';
|
|
42
45
|
|
|
@@ -58,6 +61,7 @@ export function registerValidateCommand(program: Command): void {
|
|
|
58
61
|
.description('在主 worktree 验证某个 worktree 分支的变更')
|
|
59
62
|
.option('-b, --branch <branchName>', '要验证的分支名(支持模糊匹配,不传则列出所有分支)')
|
|
60
63
|
.option('--clean', '清理 validate 状态(重置主 worktree 并删除快照)')
|
|
64
|
+
.option('-r, --run <command>', 'validate 成功后在主 worktree 中执行的命令')
|
|
61
65
|
.action(async (options: ValidateOptions) => {
|
|
62
66
|
await handleValidate(options);
|
|
63
67
|
});
|
|
@@ -300,6 +304,35 @@ function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath:
|
|
|
300
304
|
printSuccess(MESSAGES.INCREMENTAL_VALIDATE_SUCCESS(branchName));
|
|
301
305
|
}
|
|
302
306
|
|
|
307
|
+
/**
|
|
308
|
+
* 在主 worktree 中执行用户指定的命令
|
|
309
|
+
* 命令执行失败不影响 validate 本身的结果,仅输出提示
|
|
310
|
+
* @param {string} command - 要执行的命令字符串
|
|
311
|
+
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
312
|
+
*/
|
|
313
|
+
function executeRunCommand(command: string, mainWorktreePath: string): void {
|
|
314
|
+
printInfo('');
|
|
315
|
+
printInfo(MESSAGES.VALIDATE_RUN_START(command));
|
|
316
|
+
printSeparator();
|
|
317
|
+
|
|
318
|
+
const result = runCommandInherited(command, { cwd: mainWorktreePath });
|
|
319
|
+
|
|
320
|
+
printSeparator();
|
|
321
|
+
|
|
322
|
+
if (result.error) {
|
|
323
|
+
// 进程启动失败(如命令不存在)
|
|
324
|
+
printError(MESSAGES.VALIDATE_RUN_ERROR(command, result.error.message));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const exitCode = result.status ?? 1;
|
|
329
|
+
if (exitCode === 0) {
|
|
330
|
+
printSuccess(MESSAGES.VALIDATE_RUN_SUCCESS(command));
|
|
331
|
+
} else {
|
|
332
|
+
printError(MESSAGES.VALIDATE_RUN_FAILED(command, exitCode));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
303
336
|
/**
|
|
304
337
|
* 执行 validate 命令的核心逻辑
|
|
305
338
|
* @param {ValidateOptions} options - 命令选项
|
|
@@ -350,4 +383,9 @@ async function handleValidate(options: ValidateOptions): Promise<void> {
|
|
|
350
383
|
|
|
351
384
|
handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
352
385
|
}
|
|
386
|
+
|
|
387
|
+
// validate 成功后执行用户指定的命令
|
|
388
|
+
if (options.run) {
|
|
389
|
+
executeRunCommand(options.run, mainWorktreePath);
|
|
390
|
+
}
|
|
353
391
|
}
|
|
@@ -22,4 +22,14 @@ export const VALIDATE_MESSAGES = {
|
|
|
22
22
|
VALIDATE_SELECT_BRANCH: '请选择要验证的分支',
|
|
23
23
|
/** validate 模糊匹配到多个结果提示 */
|
|
24
24
|
VALIDATE_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择:`,
|
|
25
|
+
/** --run 命令开始执行提示 */
|
|
26
|
+
VALIDATE_RUN_START: (command: string) => `正在主 worktree 中执行命令: ${command}`,
|
|
27
|
+
/** --run 命令执行成功(退出码 0) */
|
|
28
|
+
VALIDATE_RUN_SUCCESS: (command: string) => `✓ 命令执行完成: ${command},退出码: 0`,
|
|
29
|
+
/** --run 命令执行失败(退出码非 0) */
|
|
30
|
+
VALIDATE_RUN_FAILED: (command: string, exitCode: number) =>
|
|
31
|
+
`✗ 命令执行完成: ${command},退出码: ${exitCode}`,
|
|
32
|
+
/** --run 命令执行异常(进程启动失败等) */
|
|
33
|
+
VALIDATE_RUN_ERROR: (command: string, errorMessage: string) =>
|
|
34
|
+
`✗ 命令执行出错: ${errorMessage}`,
|
|
25
35
|
} as const;
|
package/src/types/command.ts
CHANGED
package/src/utils/dry-run.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { getProjectWorktreeDir } from './worktree.js';
|
|
|
6
6
|
import { printInfo, printDoubleSeparator, printSeparator } from './formatter.js';
|
|
7
7
|
|
|
8
8
|
/** dry-run 模式下任务描述的最大显示长度 */
|
|
9
|
-
const DRY_RUN_TASK_DESC_MAX_LENGTH =
|
|
9
|
+
const DRY_RUN_TASK_DESC_MAX_LENGTH = 80;
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* 截取任务描述,超出最大长度时末尾加省略号
|
package/src/utils/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { execCommand, spawnProcess, killAllChildProcesses, execCommandWithInput } from './shell.js';
|
|
1
|
+
export { execCommand, spawnProcess, killAllChildProcesses, execCommandWithInput, runCommandInherited } from './shell.js';
|
|
2
2
|
export {
|
|
3
3
|
getGitCommonDir,
|
|
4
4
|
getGitTopLevel,
|
package/src/utils/shell.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execSync, execFileSync, spawn, type ChildProcess, type StdioOptions } from 'node:child_process';
|
|
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
4
|
/**
|
|
@@ -72,3 +72,23 @@ export function execCommandWithInput(command: string, args: string[], options: {
|
|
|
72
72
|
});
|
|
73
73
|
return result.trim();
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 同步执行命令,继承父进程的 stdio(实时输出到终端)
|
|
78
|
+
* 使用 shell 模式以支持管道、重定向等 shell 语法
|
|
79
|
+
* @param {string} command - 要执行的命令字符串
|
|
80
|
+
* @param {object} options - 可选配置
|
|
81
|
+
* @param {string} options.cwd - 工作目录
|
|
82
|
+
* @returns {SpawnSyncReturns<Buffer>} spawnSync 的返回结果
|
|
83
|
+
*/
|
|
84
|
+
export function runCommandInherited(
|
|
85
|
+
command: string,
|
|
86
|
+
options?: { cwd?: string },
|
|
87
|
+
): SpawnSyncReturns<Buffer> {
|
|
88
|
+
logger.debug(`执行命令(inherit): ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`);
|
|
89
|
+
return spawnSync(command, {
|
|
90
|
+
cwd: options?.cwd,
|
|
91
|
+
stdio: 'inherit',
|
|
92
|
+
shell: true,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -28,6 +28,11 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
28
28
|
INCREMENTAL_VALIDATE_SUCCESS: (branch: string) => `✓ 增量验证 ${branch}`,
|
|
29
29
|
INCREMENTAL_VALIDATE_FALLBACK: '降级为全量模式',
|
|
30
30
|
DESTRUCTIVE_OP_CANCELLED: '已取消操作',
|
|
31
|
+
VALIDATE_RUN_START: (cmd: string) => `正在执行命令: ${cmd}`,
|
|
32
|
+
VALIDATE_RUN_SUCCESS: (cmd: string) => `✓ 命令执行完成: ${cmd}`,
|
|
33
|
+
VALIDATE_RUN_FAILED: (cmd: string, code: number) => `✗ 命令失败: ${cmd},退出码: ${code}`,
|
|
34
|
+
VALIDATE_RUN_ERROR: (cmd: string, msg: string) => `✗ 命令出错: ${msg}`,
|
|
35
|
+
SEPARATOR: '────',
|
|
31
36
|
},
|
|
32
37
|
}));
|
|
33
38
|
|
|
@@ -71,6 +76,9 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
71
76
|
printWarning: vi.fn(),
|
|
72
77
|
printInfo: vi.fn(),
|
|
73
78
|
resolveTargetWorktree: vi.fn(),
|
|
79
|
+
runCommandInherited: vi.fn(),
|
|
80
|
+
printError: vi.fn(),
|
|
81
|
+
printSeparator: vi.fn(),
|
|
74
82
|
}));
|
|
75
83
|
|
|
76
84
|
import { registerValidateCommand } from '../../../src/commands/validate.js';
|
|
@@ -105,6 +113,9 @@ import {
|
|
|
105
113
|
gitApplyCachedCheck,
|
|
106
114
|
gitApplyCachedFromStdin,
|
|
107
115
|
printWarning,
|
|
116
|
+
runCommandInherited,
|
|
117
|
+
printError,
|
|
118
|
+
printSeparator,
|
|
108
119
|
} from '../../../src/utils/index.js';
|
|
109
120
|
|
|
110
121
|
const mockedGetProjectName = vi.mocked(getProjectName);
|
|
@@ -137,6 +148,9 @@ const mockedGitDiffTree = vi.mocked(gitDiffTree);
|
|
|
137
148
|
const mockedGitApplyCachedCheck = vi.mocked(gitApplyCachedCheck);
|
|
138
149
|
const mockedGitApplyCachedFromStdin = vi.mocked(gitApplyCachedFromStdin);
|
|
139
150
|
const mockedPrintWarning = vi.mocked(printWarning);
|
|
151
|
+
const mockedRunCommandInherited = vi.mocked(runCommandInherited);
|
|
152
|
+
const mockedPrintError = vi.mocked(printError);
|
|
153
|
+
const mockedPrintSeparator = vi.mocked(printSeparator);
|
|
140
154
|
|
|
141
155
|
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
142
156
|
|
|
@@ -171,6 +185,9 @@ beforeEach(() => {
|
|
|
171
185
|
mockedGitApplyCachedCheck.mockReset();
|
|
172
186
|
mockedGitApplyCachedFromStdin.mockReset();
|
|
173
187
|
mockedPrintWarning.mockReset();
|
|
188
|
+
mockedRunCommandInherited.mockReset();
|
|
189
|
+
mockedPrintError.mockReset();
|
|
190
|
+
mockedPrintSeparator.mockReset();
|
|
174
191
|
});
|
|
175
192
|
|
|
176
193
|
describe('registerValidateCommand', () => {
|
|
@@ -380,3 +397,122 @@ describe('增量 validate', () => {
|
|
|
380
397
|
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
381
398
|
});
|
|
382
399
|
});
|
|
400
|
+
|
|
401
|
+
describe('--run 选项', () => {
|
|
402
|
+
/** 设置首次 validate 成功的公共 mock */
|
|
403
|
+
function setupSuccessfulFirstValidate(): void {
|
|
404
|
+
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
405
|
+
mockedHasLocalCommits.mockReturnValue(true);
|
|
406
|
+
mockedHasSnapshot.mockReturnValue(false);
|
|
407
|
+
mockedGitDiffBinaryAgainstBranch.mockReturnValue(Buffer.from('diff'));
|
|
408
|
+
mockedGitWriteTree.mockReturnValue('treehash');
|
|
409
|
+
mockedGetHeadCommitHash.mockReturnValue('headhash');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** 构造 spawnSync 返回值的辅助函数 */
|
|
413
|
+
function createSpawnResult(overrides: { status?: number | null; error?: Error }) {
|
|
414
|
+
return {
|
|
415
|
+
pid: 0,
|
|
416
|
+
output: [],
|
|
417
|
+
stdout: Buffer.alloc(0),
|
|
418
|
+
stderr: Buffer.alloc(0),
|
|
419
|
+
status: overrides.status ?? null,
|
|
420
|
+
signal: null,
|
|
421
|
+
error: overrides.error,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
it('validate 成功后执行 --run 指定的命令', async () => {
|
|
426
|
+
setupSuccessfulFirstValidate();
|
|
427
|
+
mockedRunCommandInherited.mockReturnValue(createSpawnResult({ status: 0 }));
|
|
428
|
+
|
|
429
|
+
const program = new Command();
|
|
430
|
+
program.exitOverride();
|
|
431
|
+
registerValidateCommand(program);
|
|
432
|
+
await program.parseAsync(['validate', '-b', 'feature', '-r', 'npm test'], { from: 'user' });
|
|
433
|
+
|
|
434
|
+
expect(mockedRunCommandInherited).toHaveBeenCalledWith('npm test', { cwd: '/repo' });
|
|
435
|
+
expect(mockedPrintSuccess).toHaveBeenCalledTimes(2);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('--run 命令失败时输出错误信息但不抛异常', async () => {
|
|
439
|
+
setupSuccessfulFirstValidate();
|
|
440
|
+
mockedRunCommandInherited.mockReturnValue(createSpawnResult({ status: 1 }));
|
|
441
|
+
|
|
442
|
+
const program = new Command();
|
|
443
|
+
program.exitOverride();
|
|
444
|
+
registerValidateCommand(program);
|
|
445
|
+
await program.parseAsync(['validate', '-b', 'feature', '--run', 'npm test'], { from: 'user' });
|
|
446
|
+
|
|
447
|
+
expect(mockedRunCommandInherited).toHaveBeenCalledWith('npm test', { cwd: '/repo' });
|
|
448
|
+
expect(mockedPrintError).toHaveBeenCalled();
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('--run 命令进程启动失败时输出错误信息', async () => {
|
|
452
|
+
setupSuccessfulFirstValidate();
|
|
453
|
+
mockedRunCommandInherited.mockReturnValue(createSpawnResult({ error: new Error('spawn ENOENT') }));
|
|
454
|
+
|
|
455
|
+
const program = new Command();
|
|
456
|
+
program.exitOverride();
|
|
457
|
+
registerValidateCommand(program);
|
|
458
|
+
await program.parseAsync(['validate', '-b', 'feature', '-r', 'nonexistent'], { from: 'user' });
|
|
459
|
+
|
|
460
|
+
expect(mockedPrintError).toHaveBeenCalled();
|
|
461
|
+
// validate 成功 + run 出错,printSuccess 只被调用 1 次(validate 成功)
|
|
462
|
+
expect(mockedPrintSuccess).toHaveBeenCalledTimes(1);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('未传 --run 时不执行任何命令', async () => {
|
|
466
|
+
setupSuccessfulFirstValidate();
|
|
467
|
+
|
|
468
|
+
const program = new Command();
|
|
469
|
+
program.exitOverride();
|
|
470
|
+
registerValidateCommand(program);
|
|
471
|
+
await program.parseAsync(['validate', '-b', 'feature'], { from: 'user' });
|
|
472
|
+
|
|
473
|
+
expect(mockedRunCommandInherited).not.toHaveBeenCalled();
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('--clean 与 --run 同时传入时只执行 clean 不执行 run', async () => {
|
|
477
|
+
mockedGetConfigValue.mockReturnValue(false);
|
|
478
|
+
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
479
|
+
|
|
480
|
+
const program = new Command();
|
|
481
|
+
program.exitOverride();
|
|
482
|
+
registerValidateCommand(program);
|
|
483
|
+
await program.parseAsync(['validate', '--clean', '-b', 'feature', '-r', 'npm test'], { from: 'user' });
|
|
484
|
+
|
|
485
|
+
expect(mockedRemoveSnapshot).toHaveBeenCalled();
|
|
486
|
+
expect(mockedRunCommandInherited).not.toHaveBeenCalled();
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('增量 validate 成功后也执行 --run 命令', async () => {
|
|
490
|
+
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
491
|
+
mockedHasLocalCommits.mockReturnValue(true);
|
|
492
|
+
mockedHasSnapshot.mockReturnValue(true);
|
|
493
|
+
mockedReadSnapshot.mockReturnValue({ treeHash: 'oldtree', headCommitHash: 'headhash' });
|
|
494
|
+
mockedGetHeadCommitHash.mockReturnValue('headhash');
|
|
495
|
+
mockedGitDiffBinaryAgainstBranch.mockReturnValue(Buffer.from('diff'));
|
|
496
|
+
mockedGitWriteTree.mockReturnValue('newtree');
|
|
497
|
+
mockedRunCommandInherited.mockReturnValue(createSpawnResult({ status: 0 }));
|
|
498
|
+
|
|
499
|
+
const program = new Command();
|
|
500
|
+
program.exitOverride();
|
|
501
|
+
registerValidateCommand(program);
|
|
502
|
+
await program.parseAsync(['validate', '-b', 'feature', '-r', 'npm test'], { from: 'user' });
|
|
503
|
+
|
|
504
|
+
expect(mockedRunCommandInherited).toHaveBeenCalledWith('npm test', { cwd: '/repo' });
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('目标分支无变更时不执行 --run', async () => {
|
|
508
|
+
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
509
|
+
mockedHasLocalCommits.mockReturnValue(false);
|
|
510
|
+
|
|
511
|
+
const program = new Command();
|
|
512
|
+
program.exitOverride();
|
|
513
|
+
registerValidateCommand(program);
|
|
514
|
+
await program.parseAsync(['validate', '-b', 'feature', '-r', 'npm test'], { from: 'user' });
|
|
515
|
+
|
|
516
|
+
expect(mockedRunCommandInherited).not.toHaveBeenCalled();
|
|
517
|
+
});
|
|
518
|
+
});
|