clawt 2.14.0 → 2.15.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/.claude/agent-memory/docs-sync-updater/MEMORY.md +8 -3
- package/README.md +43 -3
- package/dist/index.js +162 -49
- package/dist/postinstall.js +18 -2
- package/docs/spec.md +109 -23
- package/package.json +1 -1
- package/src/commands/config.ts +10 -5
- package/src/commands/run.ts +68 -21
- package/src/constants/config.ts +1 -1
- package/src/constants/index.ts +1 -0
- package/src/constants/messages/config.ts +3 -0
- package/src/constants/messages/index.ts +3 -1
- package/src/constants/messages/run.ts +16 -0
- package/src/types/command.ts +2 -0
- package/src/utils/config.ts +20 -0
- package/src/utils/dry-run.ts +89 -0
- package/src/utils/index.ts +3 -2
- package/src/utils/task-file.ts +18 -0
- package/tests/unit/commands/config.test.ts +34 -3
- package/tests/unit/commands/resume.test.ts +9 -9
- package/tests/unit/commands/run.test.ts +160 -6
- 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
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
- [5.7 默认配置文件](#57-默认配置文件)
|
|
22
22
|
- [5.8 获取当前项目所有 Worktree](#58-获取当前项目所有-worktree)
|
|
23
23
|
- [5.9 日志系统](#59-日志系统)
|
|
24
|
-
- [5.10
|
|
24
|
+
- [5.10 交互式查看和修改全局配置](#510-交互式查看和修改全局配置)
|
|
25
25
|
- [5.11 在已有 Worktree 中恢复会话](#511-在已有-worktree-中恢复会话)
|
|
26
26
|
- [5.12 将主分支代码同步到目标 Worktree](#512-将主分支代码同步到目标-worktree)
|
|
27
27
|
- [5.13 重置主 Worktree 工作区和暂存区](#513-重置主-worktree-工作区和暂存区)
|
|
@@ -174,7 +174,9 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
|
|
|
174
174
|
| `clawt merge` | 合并某个已验证的 worktree 分支到主 worktree | 5.6 |
|
|
175
175
|
| `clawt remove` | 移除 worktree(支持模糊匹配/多选/全部) | 5.5 |
|
|
176
176
|
| `clawt list` | 列出当前项目所有 worktree(支持 `--json` 格式输出) | 5.8 |
|
|
177
|
-
| `clawt config` |
|
|
177
|
+
| `clawt config` | 交互式查看和修改全局配置(等同于 `config set`) | 5.10 |
|
|
178
|
+
| `clawt config set` | 修改配置项(无参数进入交互式,有参数直接设置) | 5.10 |
|
|
179
|
+
| `clawt config get` | 获取单个配置项的值 | 5.10 |
|
|
178
180
|
| `clawt config reset` | 将配置恢复为默认值 | 5.10 |
|
|
179
181
|
| `clawt resume` | 在已有 worktree 中恢复 Claude Code 会话(支持多选批量恢复) | 5.11 |
|
|
180
182
|
| `clawt sync` | 将主分支最新代码同步到目标 worktree | 5.12 |
|
|
@@ -276,6 +278,7 @@ clawt run -b <branchName>
|
|
|
276
278
|
| `--tasks` | 否 | 任务描述(可多次指定,每个 --tasks 对应一个任务,任务数量即 worktree 数量)。不传则在 worktree 中打开 Claude Code 交互式界面 |
|
|
277
279
|
| `-f` | 否 | 从任务文件读取任务列表(与 `--tasks` 互斥) |
|
|
278
280
|
| `-c` | 否 | 最大并发数,`0` 表示不限制 |
|
|
281
|
+
| `--dry-run` | 否 | 试运行模式,仅输出预览信息不实际执行 |
|
|
279
282
|
|
|
280
283
|
**互斥约束:**
|
|
281
284
|
|
|
@@ -349,6 +352,51 @@ clawt run -b <branchName>
|
|
|
349
352
|
|
|
350
353
|
**注意:** 当 `n = 1` 时(只有一个任务),worktree 目录命名规则同 **5.1**(不加 `-1` 后缀)。
|
|
351
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
|
+
|
|
352
400
|
---
|
|
353
401
|
|
|
354
402
|
### 5.3 任务完成通知机制
|
|
@@ -1000,51 +1048,82 @@ clawt validate -b feature-scheme --debug
|
|
|
1000
1048
|
|
|
1001
1049
|
---
|
|
1002
1050
|
|
|
1003
|
-
### 5.10
|
|
1051
|
+
### 5.10 交互式查看和修改全局配置
|
|
1004
1052
|
|
|
1005
1053
|
**命令:**
|
|
1006
1054
|
|
|
1007
1055
|
```bash
|
|
1008
|
-
#
|
|
1056
|
+
# 交互式修改配置(等同于 config set 无参数)
|
|
1009
1057
|
clawt config
|
|
1010
1058
|
|
|
1059
|
+
# 修改配置项(无参数进入交互式,有参数直接设置)
|
|
1060
|
+
clawt config set [key] [value]
|
|
1061
|
+
|
|
1062
|
+
# 获取单个配置项的值
|
|
1063
|
+
clawt config get <key>
|
|
1064
|
+
|
|
1011
1065
|
# 将配置恢复为默认值
|
|
1012
1066
|
clawt config reset
|
|
1013
1067
|
```
|
|
1014
1068
|
|
|
1015
|
-
####
|
|
1069
|
+
#### 交互式修改配置(`config` / `config set`)
|
|
1070
|
+
|
|
1071
|
+
直接执行 `clawt config` 或 `clawt config set`(不带参数)进入交互式配置修改模式。
|
|
1016
1072
|
|
|
1017
1073
|
**运行流程:**
|
|
1018
1074
|
|
|
1019
1075
|
1. 读取全局配置文件 `~/.clawt/config.json`
|
|
1020
|
-
2.
|
|
1021
|
-
-
|
|
1076
|
+
2. 列出所有配置项供用户选择(`Enquirer.Select`),每项显示:
|
|
1077
|
+
- 配置项名称
|
|
1022
1078
|
- 当前值(布尔值绿色/黄色,字符串青色)
|
|
1023
1079
|
- 配置项描述(灰色)
|
|
1024
|
-
|
|
1080
|
+
- 对象类型配置项(如 `aliases`)标灰不可选,提示用户通过专用命令管理
|
|
1081
|
+
3. 用户选择某个配置项后,根据值类型自动选择提示策略:
|
|
1082
|
+
- **boolean 类型** → `Select`(true / false)
|
|
1083
|
+
- **number 类型** → `Input`(带数字校验)
|
|
1084
|
+
- **string 类型 + 有 `allowedValues`** → `Select`(枚举列表)
|
|
1085
|
+
- **string 类型 + 无 `allowedValues`** → `Input`(自由输入)
|
|
1086
|
+
4. 将修改后的配置持久化到配置文件
|
|
1087
|
+
5. 输出成功提示:`✓ <key> 已设置为 <value>`
|
|
1025
1088
|
|
|
1026
|
-
|
|
1089
|
+
#### 直接设置配置项(`config set <key> <value>`)
|
|
1027
1090
|
|
|
1028
|
-
|
|
1029
|
-
配置文件路径: ~/.clawt/config.json
|
|
1030
|
-
────────────────────────────────────────
|
|
1031
|
-
autoDeleteBranch: false
|
|
1032
|
-
移除 worktree 时是否自动删除对应本地分支
|
|
1091
|
+
当带参数执行 `clawt config set <key> <value>` 时,直接修改指定配置项。
|
|
1033
1092
|
|
|
1034
|
-
|
|
1035
|
-
Claude Code CLI 启动指令
|
|
1093
|
+
**参数:**
|
|
1036
1094
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1095
|
+
| 参数 | 必填 | 说明 |
|
|
1096
|
+
| ---- | ---- | ---- |
|
|
1097
|
+
| `key` | 否 | 配置项名称(不传则进入交互式模式) |
|
|
1098
|
+
| `value` | 否 | 配置值(传了 `key` 时必填) |
|
|
1039
1099
|
|
|
1040
|
-
|
|
1041
|
-
执行破坏性操作(reset、validate --clean)前是否提示确认
|
|
1100
|
+
**运行流程:**
|
|
1042
1101
|
|
|
1043
|
-
|
|
1102
|
+
1. 校验 `key` 是否为有效的配置项名称(基于 `DEFAULT_CONFIG` 的键列表),无效则输出错误及可用配置项列表
|
|
1103
|
+
2. 校验 `value` 是否缺失,缺失则提示用法:`clawt config set <key> <value>`
|
|
1104
|
+
3. 根据目标配置项的类型解析并校验值:
|
|
1105
|
+
- **boolean** → 仅接受 `true` 或 `false`
|
|
1106
|
+
- **number** → `Number()` 解析,`NaN` 报错
|
|
1107
|
+
- **string + 有 `allowedValues`** → 校验值是否在枚举列表中
|
|
1108
|
+
- **string + 无 `allowedValues`** → 无额外校验
|
|
1109
|
+
4. 加载配置、修改目标项、持久化
|
|
1110
|
+
5. 输出成功提示:`✓ <key> 已设置为 <value>`
|
|
1044
1111
|
|
|
1045
|
-
|
|
1112
|
+
#### 获取单个配置项(`config get <key>`)
|
|
1046
1113
|
|
|
1047
|
-
|
|
1114
|
+
**参数:**
|
|
1115
|
+
|
|
1116
|
+
| 参数 | 必填 | 说明 |
|
|
1117
|
+
| ---- | ---- | ---- |
|
|
1118
|
+
| `key` | 是 | 配置项名称 |
|
|
1119
|
+
|
|
1120
|
+
**运行流程:**
|
|
1121
|
+
|
|
1122
|
+
1. 校验 `key` 是否为有效的配置项名称,无效则输出错误及可用配置项列表
|
|
1123
|
+
2. 读取配置文件,获取目标配置项的值
|
|
1124
|
+
3. 输出:`<key> = <value>`
|
|
1125
|
+
|
|
1126
|
+
#### 恢复默认配置(`config reset`)
|
|
1048
1127
|
|
|
1049
1128
|
**运行流程:**
|
|
1050
1129
|
|
|
@@ -1052,6 +1131,13 @@ clawt config reset
|
|
|
1052
1131
|
2. 将默认配置写入 `~/.clawt/config.json`(覆盖现有配置文件)
|
|
1053
1132
|
3. 输出成功提示:`✓ 配置已恢复为默认值`
|
|
1054
1133
|
|
|
1134
|
+
**实现要点:**
|
|
1135
|
+
|
|
1136
|
+
- 配置项类型定义:`ConfigItemDefinition` 新增可选字段 `allowedValues`(`readonly string[]`),仅对 string 类型有效,用于枚举值校验和交互式 Select 提示
|
|
1137
|
+
- 值解析与提示策略:`src/utils/config-strategy.ts` 中的 `parseConfigValue()`(CLI 字符串解析)和 `promptConfigValue()`(交互式提示),基于类型和 `allowedValues` 自动分发
|
|
1138
|
+
- `saveConfig(config)`:`src/utils/config.ts` 中新增的通用配置写入函数,将完整配置对象持久化到文件
|
|
1139
|
+
- `formatConfigValue(value)`:支持 boolean、string、number、对象类型(如 `aliases`,按键值对逐行展示)的格式化显示
|
|
1140
|
+
|
|
1055
1141
|
---
|
|
1056
1142
|
|
|
1057
1143
|
### 5.11 在已有 Worktree 中恢复会话
|
package/package.json
CHANGED
package/src/commands/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import Enquirer from 'enquirer';
|
|
4
|
-
import { DEFAULT_CONFIG, CONFIG_DESCRIPTIONS, MESSAGES } from '../constants/index.js';
|
|
4
|
+
import { DEFAULT_CONFIG, CONFIG_DESCRIPTIONS, MESSAGES, CONFIG_ALIAS_DISABLED_HINT } from '../constants/index.js';
|
|
5
5
|
import { logger } from '../logger/index.js';
|
|
6
6
|
import {
|
|
7
7
|
loadConfig,
|
|
@@ -123,10 +123,15 @@ async function handleInteractiveConfigSet(): Promise<void> {
|
|
|
123
123
|
logger.info('config set 命令执行,进入交互式配置');
|
|
124
124
|
|
|
125
125
|
// 构建选择列表,显示配置项名称、当前值和描述
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
// 对象类型配置项(如 aliases)标灰不可选,提示用户通过专用命令管理
|
|
127
|
+
const choices = keys.map((k) => {
|
|
128
|
+
const isObject = typeof DEFAULT_CONFIG[k] === 'object';
|
|
129
|
+
return {
|
|
130
|
+
name: k,
|
|
131
|
+
message: `${k}: ${isObject ? chalk.dim(JSON.stringify(config[k])) : formatConfigValue(config[k])} ${chalk.dim(`— ${CONFIG_DESCRIPTIONS[k]}`)}`,
|
|
132
|
+
...(isObject && { disabled: CONFIG_ALIAS_DISABLED_HINT }),
|
|
133
|
+
};
|
|
134
|
+
});
|
|
130
135
|
|
|
131
136
|
// @ts-expect-error enquirer 类型声明未导出 Select 类,但运行时存在
|
|
132
137
|
const selectedKey: keyof ClawtConfig = await new Enquirer.Select({
|
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/constants/config.ts
CHANGED
package/src/constants/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { CLAWT_HOME, CONFIG_PATH, LOGS_DIR, WORKTREES_DIR, VALIDATE_SNAPSHOTS_DIR, CLAUDE_PROJECTS_DIR } from './paths.js';
|
|
2
2
|
export { INVALID_BRANCH_CHARS } from './branch.js';
|
|
3
3
|
export { MESSAGES } from './messages/index.js';
|
|
4
|
+
export { CONFIG_ALIAS_DISABLED_HINT } from './messages/index.js';
|
|
4
5
|
export { EXIT_CODES } from './exitCodes.js';
|
|
5
6
|
export { ENABLE_BRACKETED_PASTE, DISABLE_BRACKETED_PASTE, PASTE_THRESHOLD_MS, VALID_TERMINAL_APPS, ITERM2_APP_PATH } from './terminal.js';
|
|
6
7
|
export { DEFAULT_CONFIG, CONFIG_DESCRIPTIONS, CONFIG_DEFINITIONS, APPEND_SYSTEM_PROMPT } from './config.js';
|
|
@@ -7,7 +7,9 @@ import { SYNC_MESSAGES } from './sync.js';
|
|
|
7
7
|
import { RESUME_MESSAGES } from './resume.js';
|
|
8
8
|
import { REMOVE_MESSAGES } from './remove.js';
|
|
9
9
|
import { RESET_MESSAGES } from './reset.js';
|
|
10
|
-
import { CONFIG_CMD_MESSAGES } from './config.js';
|
|
10
|
+
import { CONFIG_CMD_MESSAGES, CONFIG_ALIAS_DISABLED_HINT } from './config.js';
|
|
11
|
+
|
|
12
|
+
export { CONFIG_ALIAS_DISABLED_HINT };
|
|
11
13
|
import { STATUS_MESSAGES } from './status.js';
|
|
12
14
|
import { ALIAS_MESSAGES } from './alias.js';
|
|
13
15
|
|
|
@@ -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;
|
package/src/types/command.ts
CHANGED
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 = 70;
|
|
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
|
@@ -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/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>` 为分支名,其余行为任务描述
|