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/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 查看和管理全局配置](#510-查看和管理全局配置)
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` | 查看全局配置 | 5.10 |
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. 遍历所有配置项(以 `CONFIG_DEFINITIONS` 为单一数据源),逐项展示:
1021
- - 配置项名称(粗体)
1076
+ 2. 列出所有配置项供用户选择(`Enquirer.Select`),每项显示:
1077
+ - 配置项名称
1022
1078
  - 当前值(布尔值绿色/黄色,字符串青色)
1023
1079
  - 配置项描述(灰色)
1024
- 3. 输出配置文件路径,提示用户可直接编辑
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
- claudeCodeCommand: claude
1035
- Claude Code CLI 启动指令
1093
+ **参数:**
1036
1094
 
1037
- autoPullPush: false
1038
- merge 成功后是否自动执行 git pull git push
1095
+ | 参数 | 必填 | 说明 |
1096
+ | ---- | ---- | ---- |
1097
+ | `key` | 否 | 配置项名称(不传则进入交互式模式) |
1098
+ | `value` | 否 | 配置值(传了 `key` 时必填) |
1039
1099
 
1040
- confirmDestructiveOps: true
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "2.14.0",
3
+ "version": "2.15.0",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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
- const choices = keys.map((k) => ({
127
- name: k,
128
- message: `${k}: ${formatConfigValue(config[k])} ${chalk.dim(`— ${CONFIG_DESCRIPTIONS[k]}`)}`,
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({
@@ -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
- * 优先级:命令行参数 > 全局配置 > 默认值 0
40
- * @param {string | undefined} optionValue - 命令行传入的并发数字符串
41
- * @param {number} configValue - 全局配置中的默认并发数
42
- * @returns {number} 解析后的并发数,0 表示不限制
43
+ * 从任务文件解析出分支名列表
44
+ * -b 参数时使用自动编号,否则使用文件中每个任务块的独立分支名
45
+ * @param {RunOptions} options - 命令选项
46
+ * @param {number} entryCount - 任务条目数量
47
+ * @param {Array<{branch?: string}>} entries - 解析出的任务条目(含可选分支名)
48
+ * @returns {string[]} 分支名列表
43
49
  */
44
- function parseConcurrency(optionValue: string | undefined, configValue: number): number {
45
- if (optionValue === undefined) {
46
- return configValue;
47
- }
48
-
49
- const parsed = parseInt(optionValue, 10);
50
- if (Number.isNaN(parsed) || parsed < 0) {
51
- throw new ClawtError(MESSAGES.CONCURRENCY_INVALID);
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
- return parsed;
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.map((t) => t.trim()).filter(Boolean);
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
@@ -37,7 +37,7 @@ export const CONFIG_DEFINITIONS: ConfigDefinitions = {
37
37
  },
38
38
  aliases: {
39
39
  defaultValue: {} as Record<string, string>,
40
- description: '命令别名映射(通过 clawt alias 命令管理)',
40
+ description: '命令别名映射',
41
41
  },
42
42
  };
43
43
 
@@ -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';
@@ -1,3 +1,6 @@
1
+ /** 对象类型配置项禁用提示(如 aliases 需通过专用命令管理) */
2
+ export const CONFIG_ALIAS_DISABLED_HINT = '(通过 clawt alias 命令管理)';
3
+
1
4
  /** config 命令专属提示消息 */
2
5
  export const CONFIG_CMD_MESSAGES = {
3
6
  /** 配置已恢复为默认值 */
@@ -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;
@@ -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 命令选项 */
@@ -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
+ }
@@ -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
 
@@ -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>` 为分支名,其余行为任务描述