clawt 2.15.0 → 2.16.1
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 +51 -7
- package/dist/postinstall.js +12 -2
- package/docs/spec.md +50 -6
- package/package.json +1 -1
- package/src/commands/remove.ts +4 -0
- package/src/commands/validate.ts +38 -0
- package/src/constants/messages/remove.ts +2 -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/formatter.ts +8 -0
- package/src/utils/index.ts +2 -2
- package/src/utils/shell.ts +21 -1
- package/tests/unit/commands/remove.test.ts +7 -0
- 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
|
|
@@ -253,7 +261,9 @@ var REMOVE_MESSAGES = {
|
|
|
253
261
|
${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
254
262
|
/** 批量移除部分失败 */
|
|
255
263
|
REMOVE_PARTIAL_FAILURE: (failures) => `\u4EE5\u4E0B worktree \u79FB\u9664\u5931\u8D25\uFF1A
|
|
256
|
-
${failures.map((f) => ` \u2717 ${f.path}: ${f.error}`).join("\n")}
|
|
264
|
+
${failures.map((f) => ` \u2717 ${f.path}: ${f.error}`).join("\n")}`,
|
|
265
|
+
/** 用户选择保留本地分支 */
|
|
266
|
+
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
267
|
};
|
|
258
268
|
|
|
259
269
|
// src/constants/messages/reset.ts
|
|
@@ -521,7 +531,7 @@ function enableConsoleTransport() {
|
|
|
521
531
|
}
|
|
522
532
|
|
|
523
533
|
// src/utils/shell.ts
|
|
524
|
-
import { execSync, execFileSync, spawn } from "child_process";
|
|
534
|
+
import { execSync, execFileSync, spawn, spawnSync } from "child_process";
|
|
525
535
|
function execCommand(command, options) {
|
|
526
536
|
logger.debug(`\u6267\u884C\u547D\u4EE4: ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ""}`);
|
|
527
537
|
const result = execSync(command, {
|
|
@@ -555,6 +565,14 @@ function execCommandWithInput(command, args, options) {
|
|
|
555
565
|
});
|
|
556
566
|
return result.trim();
|
|
557
567
|
}
|
|
568
|
+
function runCommandInherited(command, options) {
|
|
569
|
+
logger.debug(`\u6267\u884C\u547D\u4EE4(inherit): ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ""}`);
|
|
570
|
+
return spawnSync(command, {
|
|
571
|
+
cwd: options?.cwd,
|
|
572
|
+
stdio: "inherit",
|
|
573
|
+
shell: true
|
|
574
|
+
});
|
|
575
|
+
}
|
|
558
576
|
|
|
559
577
|
// src/utils/git.ts
|
|
560
578
|
import { basename } from "path";
|
|
@@ -746,6 +764,9 @@ function printWarning(message) {
|
|
|
746
764
|
function printInfo(message) {
|
|
747
765
|
console.log(message);
|
|
748
766
|
}
|
|
767
|
+
function printHint(message) {
|
|
768
|
+
console.log(chalk2.hex("#FF8C00")(message));
|
|
769
|
+
}
|
|
749
770
|
function printSeparator() {
|
|
750
771
|
console.log(MESSAGES.SEPARATOR);
|
|
751
772
|
}
|
|
@@ -1000,7 +1021,7 @@ function parseConcurrency(optionValue, configValue) {
|
|
|
1000
1021
|
import Enquirer from "enquirer";
|
|
1001
1022
|
|
|
1002
1023
|
// src/utils/claude.ts
|
|
1003
|
-
import { spawnSync } from "child_process";
|
|
1024
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1004
1025
|
import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
|
|
1005
1026
|
import { join as join3 } from "path";
|
|
1006
1027
|
|
|
@@ -1109,7 +1130,7 @@ function launchInteractiveClaude(worktree, options = {}) {
|
|
|
1109
1130
|
printInfo(` \u6A21\u5F0F: ${hasPreviousSession ? "\u7EE7\u7EED\u4E0A\u6B21\u5BF9\u8BDD" : "\u65B0\u5BF9\u8BDD"}`);
|
|
1110
1131
|
}
|
|
1111
1132
|
printInfo("");
|
|
1112
|
-
const result =
|
|
1133
|
+
const result = spawnSync2(cmd, args, {
|
|
1113
1134
|
cwd: worktree.path,
|
|
1114
1135
|
stdio: "inherit"
|
|
1115
1136
|
});
|
|
@@ -1779,7 +1800,7 @@ async function executeBatchTasks(worktrees, tasks, concurrency) {
|
|
|
1779
1800
|
// src/utils/dry-run.ts
|
|
1780
1801
|
import chalk4 from "chalk";
|
|
1781
1802
|
import { join as join5 } from "path";
|
|
1782
|
-
var DRY_RUN_TASK_DESC_MAX_LENGTH =
|
|
1803
|
+
var DRY_RUN_TASK_DESC_MAX_LENGTH = 80;
|
|
1783
1804
|
function truncateTaskDesc(task) {
|
|
1784
1805
|
const oneLine = task.replace(/\n/g, " ").trim();
|
|
1785
1806
|
if (oneLine.length <= DRY_RUN_TASK_DESC_MAX_LENGTH) {
|
|
@@ -2046,6 +2067,9 @@ async function handleRemove(options) {
|
|
|
2046
2067
|
let shouldDeleteBranch = autoDelete;
|
|
2047
2068
|
if (!autoDelete) {
|
|
2048
2069
|
shouldDeleteBranch = await confirmAction("\u662F\u5426\u540C\u65F6\u5220\u9664\u5BF9\u5E94\u7684\u672C\u5730\u5206\u652F\uFF1F");
|
|
2070
|
+
if (!shouldDeleteBranch) {
|
|
2071
|
+
printHint(MESSAGES.REMOVE_BRANCHES_KEPT);
|
|
2072
|
+
}
|
|
2049
2073
|
}
|
|
2050
2074
|
const failures = [];
|
|
2051
2075
|
for (const wt of worktreesToRemove) {
|
|
@@ -2227,7 +2251,7 @@ var VALIDATE_RESOLVE_MESSAGES = {
|
|
|
2227
2251
|
noMatch: MESSAGES.VALIDATE_NO_MATCH
|
|
2228
2252
|
};
|
|
2229
2253
|
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) => {
|
|
2254
|
+
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
2255
|
await handleValidate(options);
|
|
2232
2256
|
});
|
|
2233
2257
|
}
|
|
@@ -2368,6 +2392,23 @@ function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, project
|
|
|
2368
2392
|
}
|
|
2369
2393
|
printSuccess(MESSAGES.INCREMENTAL_VALIDATE_SUCCESS(branchName));
|
|
2370
2394
|
}
|
|
2395
|
+
function executeRunCommand(command, mainWorktreePath) {
|
|
2396
|
+
printInfo("");
|
|
2397
|
+
printInfo(MESSAGES.VALIDATE_RUN_START(command));
|
|
2398
|
+
printSeparator();
|
|
2399
|
+
const result = runCommandInherited(command, { cwd: mainWorktreePath });
|
|
2400
|
+
printSeparator();
|
|
2401
|
+
if (result.error) {
|
|
2402
|
+
printError(MESSAGES.VALIDATE_RUN_ERROR(command, result.error.message));
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
const exitCode = result.status ?? 1;
|
|
2406
|
+
if (exitCode === 0) {
|
|
2407
|
+
printSuccess(MESSAGES.VALIDATE_RUN_SUCCESS(command));
|
|
2408
|
+
} else {
|
|
2409
|
+
printError(MESSAGES.VALIDATE_RUN_FAILED(command, exitCode));
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2371
2412
|
async function handleValidate(options) {
|
|
2372
2413
|
if (options.clean) {
|
|
2373
2414
|
await handleValidateClean(options);
|
|
@@ -2399,6 +2440,9 @@ async function handleValidate(options) {
|
|
|
2399
2440
|
}
|
|
2400
2441
|
handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
2401
2442
|
}
|
|
2443
|
+
if (options.run) {
|
|
2444
|
+
executeRunCommand(options.run, mainWorktreePath);
|
|
2445
|
+
}
|
|
2402
2446
|
}
|
|
2403
2447
|
|
|
2404
2448
|
// 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
|
|
@@ -245,7 +253,9 @@ var REMOVE_MESSAGES = {
|
|
|
245
253
|
${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
246
254
|
/** 批量移除部分失败 */
|
|
247
255
|
REMOVE_PARTIAL_FAILURE: (failures) => `\u4EE5\u4E0B worktree \u79FB\u9664\u5931\u8D25\uFF1A
|
|
248
|
-
${failures.map((f) => ` \u2717 ${f.path}: ${f.error}`).join("\n")}
|
|
256
|
+
${failures.map((f) => ` \u2717 ${f.path}: ${f.error}`).join("\n")}`,
|
|
257
|
+
/** 用户选择保留本地分支 */
|
|
258
|
+
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"
|
|
249
259
|
};
|
|
250
260
|
|
|
251
261
|
// src/constants/messages/reset.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/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
|
@@ -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
|
}
|
|
@@ -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;
|
|
@@ -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/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,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,
|
|
@@ -49,7 +49,7 @@ export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } fro
|
|
|
49
49
|
export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
|
|
50
50
|
export { createWorktrees, getProjectWorktrees, getProjectWorktreeDir, cleanupWorktrees, getWorktreeStatus, createWorktreesByBranches } from './worktree.js';
|
|
51
51
|
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';
|
|
52
|
+
export { printSuccess, printError, printWarning, printInfo, printHint, printSeparator, printDoubleSeparator, confirmAction, confirmDestructiveAction, formatWorktreeStatus, isWorktreeIdle, formatDuration } from './formatter.js';
|
|
53
53
|
export { ensureDir, removeEmptyDir } from './fs.js';
|
|
54
54
|
export { multilineInput } from './prompt.js';
|
|
55
55
|
export { launchInteractiveClaude, hasClaudeSessionHistory, launchInteractiveClaudeInNewTerminal } from './claude.js';
|
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
|
+
}
|
|
@@ -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 () => {
|
|
@@ -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
|
+
});
|