clawt 2.14.1 → 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 +29 -2
- package/dist/index.js +194 -50
- package/dist/postinstall.js +26 -2
- package/docs/spec.md +96 -6
- package/package.json +1 -1
- package/src/commands/run.ts +68 -21
- package/src/commands/validate.ts +38 -0
- package/src/constants/messages/run.ts +16 -0
- package/src/constants/messages/validate.ts +10 -0
- package/src/types/command.ts +4 -0
- package/src/utils/config.ts +20 -0
- package/src/utils/dry-run.ts +89 -0
- package/src/utils/index.ts +4 -3
- package/src/utils/shell.ts +21 -1
- package/src/utils/task-file.ts +18 -0
- package/tests/unit/commands/resume.test.ts +9 -9
- package/tests/unit/commands/run.test.ts +160 -6
- package/tests/unit/commands/validate.test.ts +136 -0
- package/tests/unit/constants/messages.test.ts +5 -1
- package/tests/unit/utils/config.test.ts +23 -1
- package/tests/unit/utils/dry-run.test.ts +124 -0
- package/tests/unit/utils/task-file.test.ts +26 -1
package/docs/spec.md
CHANGED
|
@@ -278,6 +278,7 @@ clawt run -b <branchName>
|
|
|
278
278
|
| `--tasks` | 否 | 任务描述(可多次指定,每个 --tasks 对应一个任务,任务数量即 worktree 数量)。不传则在 worktree 中打开 Claude Code 交互式界面 |
|
|
279
279
|
| `-f` | 否 | 从任务文件读取任务列表(与 `--tasks` 互斥) |
|
|
280
280
|
| `-c` | 否 | 最大并发数,`0` 表示不限制 |
|
|
281
|
+
| `--dry-run` | 否 | 试运行模式,仅输出预览信息不实际执行 |
|
|
281
282
|
|
|
282
283
|
**互斥约束:**
|
|
283
284
|
|
|
@@ -351,6 +352,51 @@ clawt run -b <branchName>
|
|
|
351
352
|
|
|
352
353
|
**注意:** 当 `n = 1` 时(只有一个任务),worktree 目录命名规则同 **5.1**(不加 `-1` 后缀)。
|
|
353
354
|
|
|
355
|
+
#### `--dry-run` 预览模式
|
|
356
|
+
|
|
357
|
+
传入 `--dry-run` 时不实际创建 worktree 和执行任务,仅输出预览信息供用户确认。预览由 `printDryRunPreview()`(`src/utils/dry-run.ts`)负责渲染。
|
|
358
|
+
|
|
359
|
+
**输出格式:**
|
|
360
|
+
|
|
361
|
+
```
|
|
362
|
+
════════════════════════════════════════
|
|
363
|
+
Dry Run 预览
|
|
364
|
+
════════════════════════════════════════
|
|
365
|
+
任务数: 3 │ 并发数: 不限制 │ Worktree: ~/.clawt/worktrees/project
|
|
366
|
+
────────────────────────────────────────
|
|
367
|
+
✓ [1/3] feat-login
|
|
368
|
+
路径: ~/.clawt/worktrees/project/feat-login
|
|
369
|
+
任务: 实现登录功能
|
|
370
|
+
|
|
371
|
+
⚠ [2/3] feat-signup — 分支 feat-signup 已存在
|
|
372
|
+
路径: ~/.clawt/worktrees/project/feat-signup
|
|
373
|
+
任务: 实现注册功能
|
|
374
|
+
|
|
375
|
+
✓ [3/3] fix-bug
|
|
376
|
+
路径: ~/.clawt/worktrees/project/fix-bug
|
|
377
|
+
任务: 修复内存泄漏
|
|
378
|
+
|
|
379
|
+
════════════════════════════════════════
|
|
380
|
+
✓ 预览完成,无冲突。移除 --dry-run 即可正式执行。
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**格式规则:**
|
|
384
|
+
|
|
385
|
+
1. **标题区**:双线分隔符包裹标题 `Dry Run 预览`
|
|
386
|
+
2. **摘要行**:任务数、并发数、Worktree 目录路径合并为一行,用灰色 `│` 分隔;交互式模式(无 `--tasks`)会额外追加模式信息
|
|
387
|
+
3. **分支列表**:
|
|
388
|
+
- 正常分支:行首绿色 `✓` + 序号 + 青色分支名
|
|
389
|
+
- 冲突分支:行首黄色 `⚠` + 序号 + 黄色分支名 + 灰色 `—` + 黄色警告文本(如 `分支 xxx 已存在`),警告合并在序号行
|
|
390
|
+
4. **路径/任务行**:2 空格缩进,灰色标签前缀(`路径:` / `任务:`)
|
|
391
|
+
5. **任务描述截断**:超过 70 字符时末尾加 `...`,多行合并为单行
|
|
392
|
+
6. **结尾**:双线分隔符后根据冲突情况输出结论——无冲突时绿色 `✓` 提示,有冲突时黄色 `⚠` 警告
|
|
393
|
+
|
|
394
|
+
**实现要点:**
|
|
395
|
+
|
|
396
|
+
- 常量定义在 `src/constants/messages/run.ts`(`DRY_RUN_*` 系列)
|
|
397
|
+
- `DRY_RUN_WORKTREE_DIR` 前缀为 `Worktree:`(简短形式)
|
|
398
|
+
- `truncateTaskDesc()` 负责截断任务描述(最大长度 70 字符)
|
|
399
|
+
|
|
354
400
|
---
|
|
355
401
|
|
|
356
402
|
### 5.3 任务完成通知机制
|
|
@@ -414,18 +460,19 @@ Claude Code CLI 以 `--output-format json` 运行时,退出后会在 stdout
|
|
|
414
460
|
|
|
415
461
|
```bash
|
|
416
462
|
# 指定分支名(支持模糊匹配)
|
|
417
|
-
clawt validate -b <branchName> [--clean]
|
|
463
|
+
clawt validate -b <branchName> [--clean] [-r <command>]
|
|
418
464
|
|
|
419
465
|
# 不指定分支名(列出所有分支供选择)
|
|
420
|
-
clawt validate [--clean]
|
|
466
|
+
clawt validate [--clean] [-r <command>]
|
|
421
467
|
```
|
|
422
468
|
|
|
423
469
|
**参数:**
|
|
424
470
|
|
|
425
|
-
| 参数
|
|
426
|
-
|
|
|
427
|
-
| `-b`
|
|
428
|
-
| `--clean`
|
|
471
|
+
| 参数 | 必填 | 说明 |
|
|
472
|
+
| ------------- | ---- | ------------------------------------------------------------------------ |
|
|
473
|
+
| `-b` | 否 | 要验证的 worktree 分支名(支持模糊匹配,不传则列出所有分支供选择) |
|
|
474
|
+
| `--clean` | 否 | 清理 validate 状态(重置主 worktree 并删除快照) |
|
|
475
|
+
| `-r, --run` | 否 | validate 成功后在主 worktree 中执行的命令(如测试、构建等) |
|
|
429
476
|
|
|
430
477
|
> **限制:** 单次只能验证一个分支,不支持批量验证。
|
|
431
478
|
|
|
@@ -551,6 +598,45 @@ git restore --staged .
|
|
|
551
598
|
可以开始验证了
|
|
552
599
|
```
|
|
553
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
|
+
|
|
554
640
|
#### 增量 validate(存在历史快照)
|
|
555
641
|
|
|
556
642
|
当 `~/.clawt/validate-snapshots/<project>/<branchName>.tree` 存在时,自动进入增量模式:
|
|
@@ -620,6 +706,10 @@ git apply --cached < patch
|
|
|
620
706
|
可以开始验证了
|
|
621
707
|
```
|
|
622
708
|
|
|
709
|
+
##### 步骤 7:执行 `--run` 命令(可选)
|
|
710
|
+
|
|
711
|
+
与首次 validate 的步骤 6 相同,增量 validate 成功后也会执行 `-r, --run` 指定的命令。
|
|
712
|
+
|
|
623
713
|
---
|
|
624
714
|
|
|
625
715
|
### 5.5 移除 Worktree
|
package/package.json
CHANGED
package/src/commands/run.ts
CHANGED
|
@@ -9,12 +9,16 @@ import {
|
|
|
9
9
|
createWorktrees,
|
|
10
10
|
createWorktreesByBranches,
|
|
11
11
|
sanitizeBranchName,
|
|
12
|
+
generateBranchNames,
|
|
12
13
|
checkBranchExists,
|
|
13
14
|
getConfigValue,
|
|
15
|
+
parseConcurrency,
|
|
14
16
|
printSuccess,
|
|
15
17
|
launchInteractiveClaude,
|
|
16
18
|
loadTaskFile,
|
|
19
|
+
parseTasksFromOptions,
|
|
17
20
|
executeBatchTasks,
|
|
21
|
+
printDryRunPreview,
|
|
18
22
|
} from '../utils/index.js';
|
|
19
23
|
|
|
20
24
|
/**
|
|
@@ -29,28 +33,32 @@ export function registerRunCommand(program: Command): void {
|
|
|
29
33
|
.option('--tasks <task...>', '任务列表(可多次指定),不传则在 worktree 中打开 Claude Code 交互式界面')
|
|
30
34
|
.option('-c, --concurrency <n>', '最大并发数,0 表示不限制')
|
|
31
35
|
.option('-f, --file <path>', '从任务文件读取任务列表(与 --tasks 互斥)')
|
|
36
|
+
.option('-d, --dry-run', '预览模式,仅展示任务计划不实际执行')
|
|
32
37
|
.action(async (options: RunOptions) => {
|
|
33
38
|
await handleRun(options);
|
|
34
39
|
});
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* @param {
|
|
41
|
-
* @param {number}
|
|
42
|
-
* @
|
|
43
|
+
* 从任务文件解析出分支名列表
|
|
44
|
+
* 有 -b 参数时使用自动编号,否则使用文件中每个任务块的独立分支名
|
|
45
|
+
* @param {RunOptions} options - 命令选项
|
|
46
|
+
* @param {number} entryCount - 任务条目数量
|
|
47
|
+
* @param {Array<{branch?: string}>} entries - 解析出的任务条目(含可选分支名)
|
|
48
|
+
* @returns {string[]} 分支名列表
|
|
43
49
|
*/
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
function resolveBranchNamesFromFile(
|
|
51
|
+
options: RunOptions,
|
|
52
|
+
entryCount: number,
|
|
53
|
+
entries: Array<{ branch?: string }>,
|
|
54
|
+
): string[] {
|
|
55
|
+
if (options.branch) {
|
|
56
|
+
// 有 -b 参数:忽略文件中的分支名,用 -b 自动编号
|
|
57
|
+
const sanitized = sanitizeBranchName(options.branch);
|
|
58
|
+
return generateBranchNames(sanitized, entryCount);
|
|
52
59
|
}
|
|
53
|
-
|
|
60
|
+
// 无 -b 参数:使用文件中每个任务的独立分支名
|
|
61
|
+
return entries.map((e) => sanitizeBranchName(e.branch!));
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
/**
|
|
@@ -85,6 +93,22 @@ async function handleRunFromFile(options: RunOptions): Promise<void> {
|
|
|
85
93
|
await executeBatchTasks(worktrees, tasks, concurrency);
|
|
86
94
|
}
|
|
87
95
|
|
|
96
|
+
/**
|
|
97
|
+
* 处理 dry-run 模式下从任务文件读取的逻辑
|
|
98
|
+
* @param {RunOptions} options - 命令选项(包含 file 字段)
|
|
99
|
+
*/
|
|
100
|
+
function handleDryRunFromFile(options: RunOptions): void {
|
|
101
|
+
const branchRequired = !options.branch;
|
|
102
|
+
const entries = loadTaskFile(options.file!, { branchRequired });
|
|
103
|
+
printSuccess(MESSAGES.TASK_FILE_LOADED(entries.length, options.file!));
|
|
104
|
+
|
|
105
|
+
const tasks = entries.map((e) => e.task);
|
|
106
|
+
const branchNames = resolveBranchNamesFromFile(options, entries.length, entries);
|
|
107
|
+
const concurrency = parseConcurrency(options.concurrency, getConfigValue('maxConcurrency'));
|
|
108
|
+
|
|
109
|
+
printDryRunPreview(branchNames, tasks, concurrency);
|
|
110
|
+
}
|
|
111
|
+
|
|
88
112
|
/**
|
|
89
113
|
* 执行 run 命令的核心逻辑
|
|
90
114
|
* 支持三种模式:
|
|
@@ -95,13 +119,41 @@ async function handleRunFromFile(options: RunOptions): Promise<void> {
|
|
|
95
119
|
*/
|
|
96
120
|
async function handleRun(options: RunOptions): Promise<void> {
|
|
97
121
|
validateMainWorktree();
|
|
98
|
-
validateClaudeCodeInstalled();
|
|
99
122
|
|
|
100
123
|
// 互斥校验:--file 和 --tasks 不能同时使用
|
|
101
124
|
if (options.file && options.tasks) {
|
|
102
125
|
throw new ClawtError(MESSAGES.FILE_AND_TASKS_CONFLICT);
|
|
103
126
|
}
|
|
104
127
|
|
|
128
|
+
// dry-run 模式:仅解析和展示任务计划,不实际创建 worktree 或启动 Claude Code
|
|
129
|
+
if (options.dryRun) {
|
|
130
|
+
// dry-run 不需要校验 Claude Code 是否安装
|
|
131
|
+
if (options.file) {
|
|
132
|
+
return handleDryRunFromFile(options);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!options.branch) {
|
|
136
|
+
throw new ClawtError(MESSAGES.BRANCH_OR_FILE_REQUIRED);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const sanitized = sanitizeBranchName(options.branch);
|
|
140
|
+
|
|
141
|
+
if (!options.tasks || options.tasks.length === 0) {
|
|
142
|
+
// 交互式模式 dry-run:展示单个 worktree 信息
|
|
143
|
+
printDryRunPreview([sanitized], [], 0);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const tasks = parseTasksFromOptions(options.tasks);
|
|
148
|
+
const branchNames = generateBranchNames(sanitized, tasks.length);
|
|
149
|
+
const concurrency = parseConcurrency(options.concurrency, getConfigValue('maxConcurrency'));
|
|
150
|
+
printDryRunPreview(branchNames, tasks, concurrency);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 正常执行模式需要校验 Claude Code
|
|
155
|
+
validateClaudeCodeInstalled();
|
|
156
|
+
|
|
105
157
|
// --file 模式
|
|
106
158
|
if (options.file) {
|
|
107
159
|
return handleRunFromFile(options);
|
|
@@ -128,12 +180,7 @@ async function handleRun(options: RunOptions): Promise<void> {
|
|
|
128
180
|
return;
|
|
129
181
|
}
|
|
130
182
|
|
|
131
|
-
const tasks = options.tasks
|
|
132
|
-
|
|
133
|
-
if (tasks.length === 0) {
|
|
134
|
-
throw new ClawtError('任务列表不能为空');
|
|
135
|
-
}
|
|
136
|
-
|
|
183
|
+
const tasks = parseTasksFromOptions(options.tasks);
|
|
137
184
|
const count = tasks.length;
|
|
138
185
|
|
|
139
186
|
// 解析并发数:命令行参数 > 全局配置 > 默认值 0
|
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
|
}
|
|
@@ -43,4 +43,20 @@ export const RUN_MESSAGES = {
|
|
|
43
43
|
TASK_FILE_LOADED: (count: number, path: string) => `✓ 从 ${path} 加载了 ${count} 个任务`,
|
|
44
44
|
/** 未指定 -b 或 -f */
|
|
45
45
|
BRANCH_OR_FILE_REQUIRED: '请指定 -b 分支名或 -f 任务文件',
|
|
46
|
+
/** dry-run 预览标题 */
|
|
47
|
+
DRY_RUN_TITLE: 'Dry Run 预览',
|
|
48
|
+
/** dry-run 任务数量 */
|
|
49
|
+
DRY_RUN_TASK_COUNT: (count: number) => `任务数: ${count}`,
|
|
50
|
+
/** dry-run 并发数 */
|
|
51
|
+
DRY_RUN_CONCURRENCY: (concurrency: number) => `并发数: ${concurrency === 0 ? '不限制' : concurrency}`,
|
|
52
|
+
/** dry-run worktree 目录 */
|
|
53
|
+
DRY_RUN_WORKTREE_DIR: (dir: string) => `Worktree: ${dir}`,
|
|
54
|
+
/** dry-run 分支已存在警告 */
|
|
55
|
+
DRY_RUN_BRANCH_EXISTS_WARNING: (name: string) => `分支 ${name} 已存在`,
|
|
56
|
+
/** dry-run 交互式模式提示(无任务描述) */
|
|
57
|
+
DRY_RUN_INTERACTIVE_MODE: '模式: 交互式(无预设任务)',
|
|
58
|
+
/** dry-run 预览完成且无冲突 */
|
|
59
|
+
DRY_RUN_READY: '预览完成,无冲突。移除 --dry-run 即可正式执行。',
|
|
60
|
+
/** dry-run 存在分支冲突 */
|
|
61
|
+
DRY_RUN_HAS_CONFLICT: '存在分支冲突,实际执行时将会报错。请先处理冲突的分支。',
|
|
46
62
|
} 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
|
@@ -16,6 +16,8 @@ export interface RunOptions {
|
|
|
16
16
|
concurrency?: string;
|
|
17
17
|
/** 任务文件路径(与 --tasks 互斥) */
|
|
18
18
|
file?: string;
|
|
19
|
+
/** 预览模式,仅展示任务计划不实际执行 */
|
|
20
|
+
dryRun?: boolean;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/** validate 命令选项 */
|
|
@@ -24,6 +26,8 @@ export interface ValidateOptions {
|
|
|
24
26
|
branch?: string;
|
|
25
27
|
/** 清理 validate 状态 */
|
|
26
28
|
clean?: boolean;
|
|
29
|
+
/** validate 成功后在主 worktree 中执行的命令 */
|
|
30
|
+
run?: string;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
/** merge 命令选项 */
|
package/src/utils/config.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { CONFIG_PATH, CLAWT_HOME, LOGS_DIR, WORKTREES_DIR, DEFAULT_CONFIG, MESSAGES } from '../constants/index.js';
|
|
3
|
+
import { ClawtError } from '../errors/index.js';
|
|
3
4
|
import { ensureDir } from './fs.js';
|
|
4
5
|
import { logger } from '../logger/index.js';
|
|
5
6
|
import type { ClawtConfig } from '../types/index.js';
|
|
@@ -65,3 +66,22 @@ export function ensureClawtDirs(): void {
|
|
|
65
66
|
ensureDir(LOGS_DIR);
|
|
66
67
|
ensureDir(WORKTREES_DIR);
|
|
67
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 解析并发数参数
|
|
72
|
+
* 优先级:命令行参数 > 全局配置 > 默认值 0
|
|
73
|
+
* @param {string | undefined} optionValue - 命令行传入的并发数字符串
|
|
74
|
+
* @param {number} configValue - 全局配置中的默认并发数
|
|
75
|
+
* @returns {number} 解析后的并发数,0 表示不限制
|
|
76
|
+
*/
|
|
77
|
+
export function parseConcurrency(optionValue: string | undefined, configValue: number): number {
|
|
78
|
+
if (optionValue === undefined) {
|
|
79
|
+
return configValue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const parsed = parseInt(optionValue, 10);
|
|
83
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
84
|
+
throw new ClawtError(MESSAGES.CONCURRENCY_INVALID);
|
|
85
|
+
}
|
|
86
|
+
return parsed;
|
|
87
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { MESSAGES } from '../constants/index.js';
|
|
4
|
+
import { checkBranchExists } from './git.js';
|
|
5
|
+
import { getProjectWorktreeDir } from './worktree.js';
|
|
6
|
+
import { printInfo, printDoubleSeparator, printSeparator } from './formatter.js';
|
|
7
|
+
|
|
8
|
+
/** dry-run 模式下任务描述的最大显示长度 */
|
|
9
|
+
const DRY_RUN_TASK_DESC_MAX_LENGTH = 80;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 截取任务描述,超出最大长度时末尾加省略号
|
|
13
|
+
* 多行内容会被合并为单行显示
|
|
14
|
+
* @param {string} task - 原始任务描述
|
|
15
|
+
* @returns {string} 截取后的任务描述
|
|
16
|
+
*/
|
|
17
|
+
export function truncateTaskDesc(task: string): string {
|
|
18
|
+
// 将换行替换为空格,保证单行显示
|
|
19
|
+
const oneLine = task.replace(/\n/g, ' ').trim();
|
|
20
|
+
if (oneLine.length <= DRY_RUN_TASK_DESC_MAX_LENGTH) {
|
|
21
|
+
return oneLine;
|
|
22
|
+
}
|
|
23
|
+
return oneLine.slice(0, DRY_RUN_TASK_DESC_MAX_LENGTH) + '...';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 输出 dry-run 预览信息,展示将要创建的 worktree 列表和任务摘要
|
|
28
|
+
* @param {string[]} branchNames - 分支名列表
|
|
29
|
+
* @param {string[]} tasks - 任务描述列表(与 branchNames 等长,交互式模式可为空数组)
|
|
30
|
+
* @param {number} concurrency - 并发数
|
|
31
|
+
*/
|
|
32
|
+
export function printDryRunPreview(branchNames: string[], tasks: string[], concurrency: number): void {
|
|
33
|
+
const projectDir = getProjectWorktreeDir();
|
|
34
|
+
const isInteractive = tasks.length === 0;
|
|
35
|
+
|
|
36
|
+
// 标题区
|
|
37
|
+
printDoubleSeparator();
|
|
38
|
+
printInfo(` ${chalk.bold(MESSAGES.DRY_RUN_TITLE)}`);
|
|
39
|
+
printDoubleSeparator();
|
|
40
|
+
|
|
41
|
+
// 摘要行:合并为一行,用 │ 分隔
|
|
42
|
+
const summaryParts = [
|
|
43
|
+
MESSAGES.DRY_RUN_TASK_COUNT(branchNames.length),
|
|
44
|
+
MESSAGES.DRY_RUN_CONCURRENCY(concurrency),
|
|
45
|
+
MESSAGES.DRY_RUN_WORKTREE_DIR(projectDir),
|
|
46
|
+
];
|
|
47
|
+
if (isInteractive) {
|
|
48
|
+
summaryParts.push(MESSAGES.DRY_RUN_INTERACTIVE_MODE);
|
|
49
|
+
}
|
|
50
|
+
printInfo(summaryParts.join(chalk.gray(' │ ')));
|
|
51
|
+
|
|
52
|
+
printSeparator();
|
|
53
|
+
|
|
54
|
+
// 逐个展示 worktree 信息
|
|
55
|
+
let hasConflict = false;
|
|
56
|
+
for (let i = 0; i < branchNames.length; i++) {
|
|
57
|
+
const branch = branchNames[i];
|
|
58
|
+
const worktreePath = join(projectDir, branch);
|
|
59
|
+
const exists = checkBranchExists(branch);
|
|
60
|
+
|
|
61
|
+
if (exists) hasConflict = true;
|
|
62
|
+
|
|
63
|
+
// 序号行:正常用绿色 ✓,冲突用黄色 ⚠ + 警告文本
|
|
64
|
+
const indexLabel = `[${i + 1}/${branchNames.length}]`;
|
|
65
|
+
if (exists) {
|
|
66
|
+
printInfo(`${chalk.yellow('⚠')} ${indexLabel} ${chalk.yellow(branch)} ${chalk.gray('—')} ${chalk.yellow(MESSAGES.DRY_RUN_BRANCH_EXISTS_WARNING(branch))}`);
|
|
67
|
+
} else {
|
|
68
|
+
printInfo(`${chalk.green('✓')} ${indexLabel} ${chalk.cyan(branch)}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 路径行(2空格缩进)
|
|
72
|
+
printInfo(` ${chalk.gray('路径:')} ${worktreePath}`);
|
|
73
|
+
|
|
74
|
+
// 任务描述行(2空格缩进,非交互式模式)
|
|
75
|
+
if (!isInteractive) {
|
|
76
|
+
printInfo(` ${chalk.gray('任务:')} ${truncateTaskDesc(tasks[i])}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
printInfo('');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 结尾
|
|
83
|
+
printDoubleSeparator();
|
|
84
|
+
if (hasConflict) {
|
|
85
|
+
printInfo(chalk.yellow(`⚠ ${MESSAGES.DRY_RUN_HAS_CONFLICT}`));
|
|
86
|
+
} else {
|
|
87
|
+
printInfo(chalk.green(`✓ ${MESSAGES.DRY_RUN_READY}`));
|
|
88
|
+
}
|
|
89
|
+
}
|
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,
|
|
@@ -48,7 +48,7 @@ export {
|
|
|
48
48
|
export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
|
|
49
49
|
export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
|
|
50
50
|
export { createWorktrees, getProjectWorktrees, getProjectWorktreeDir, cleanupWorktrees, getWorktreeStatus, createWorktreesByBranches } from './worktree.js';
|
|
51
|
-
export { loadConfig, writeDefaultConfig, writeConfig, saveConfig, getConfigValue, ensureClawtDirs } from './config.js';
|
|
51
|
+
export { loadConfig, writeDefaultConfig, writeConfig, saveConfig, getConfigValue, ensureClawtDirs, parseConcurrency } from './config.js';
|
|
52
52
|
export { printSuccess, printError, printWarning, printInfo, 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';
|
|
@@ -57,9 +57,10 @@ export { getSnapshotPath, hasSnapshot, readSnapshotTreeHash, readSnapshot, write
|
|
|
57
57
|
export { findExactMatch, findFuzzyMatches, promptSelectBranch, promptMultiSelectBranches, resolveTargetWorktree, resolveTargetWorktrees } from './worktree-matcher.js';
|
|
58
58
|
export type { WorktreeResolveMessages, WorktreeMultiResolveMessages } from './worktree-matcher.js';
|
|
59
59
|
export { ProgressRenderer } from './progress.js';
|
|
60
|
-
export { parseTaskFile, loadTaskFile } from './task-file.js';
|
|
60
|
+
export { parseTaskFile, loadTaskFile, parseTasksFromOptions } from './task-file.js';
|
|
61
61
|
export { executeBatchTasks } from './task-executor.js';
|
|
62
62
|
export { detectTerminalApp, openCommandInNewTerminalTab } from './terminal.js';
|
|
63
|
+
export { truncateTaskDesc, printDryRunPreview } from './dry-run.js';
|
|
63
64
|
export { applyAliases } from './alias.js';
|
|
64
65
|
export { isValidConfigKey, getValidConfigKeys, parseConfigValue, promptConfigValue, formatConfigValue } from './config-strategy.js';
|
|
65
66
|
|
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
|
+
}
|
package/src/utils/task-file.ts
CHANGED
|
@@ -10,6 +10,24 @@ const TASK_BLOCK_REGEX = /<!-- CLAWT-TASKS:START -->([\s\S]*?)<!-- CLAWT-TASKS:E
|
|
|
10
10
|
/** 匹配分支名行的正则:# branch: <name> */
|
|
11
11
|
const BRANCH_LINE_REGEX = /^#\s*branch:\s*(.+)$/;
|
|
12
12
|
|
|
13
|
+
/** 任务列表为空时的错误提示 */
|
|
14
|
+
const EMPTY_TASKS_MESSAGE = '任务列表不能为空';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 从命令行 --tasks 选项中解析出有效的任务列表
|
|
18
|
+
* 去除首尾空白并过滤空字符串,结果为空时抛出错误
|
|
19
|
+
* @param {string[]} rawTasks - 原始任务字符串数组
|
|
20
|
+
* @returns {string[]} 过滤后的有效任务列表
|
|
21
|
+
* @throws {ClawtError} 任务列表为空时抛出
|
|
22
|
+
*/
|
|
23
|
+
export function parseTasksFromOptions(rawTasks: string[]): string[] {
|
|
24
|
+
const tasks = rawTasks.map((t) => t.trim()).filter(Boolean);
|
|
25
|
+
if (tasks.length === 0) {
|
|
26
|
+
throw new ClawtError(EMPTY_TASKS_MESSAGE);
|
|
27
|
+
}
|
|
28
|
+
return tasks;
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
/**
|
|
14
32
|
* 解析任务文件内容,提取所有任务块
|
|
15
33
|
* 每个块内 `# branch: <name>` 为分支名,其余行为任务描述
|