clawt 2.10.1 → 2.11.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/.claude/agent-memory/docs-sync-updater/MEMORY.md +8 -2
- package/README.md +27 -2
- package/dist/index.js +644 -189
- package/dist/postinstall.js +32 -1
- package/docs/spec.md +59 -8
- package/package.json +1 -1
- package/src/commands/resume.ts +2 -2
- package/src/commands/run.ts +68 -206
- package/src/constants/config.ts +4 -0
- package/src/constants/index.ts +11 -1
- package/src/constants/messages/run.ts +30 -0
- package/src/constants/paths.ts +3 -0
- package/src/constants/progress.ts +39 -0
- package/src/types/command.ts +4 -0
- package/src/types/config.ts +2 -0
- package/src/types/index.ts +1 -0
- package/src/types/taskFile.ts +13 -0
- package/src/utils/claude.ts +50 -2
- package/src/utils/formatter.ts +16 -0
- package/src/utils/index.ts +7 -3
- package/src/utils/progress-render.ts +90 -0
- package/src/utils/progress.ts +213 -0
- package/src/utils/task-executor.ts +365 -0
- package/src/utils/task-file.ts +87 -0
- package/src/utils/worktree.ts +27 -0
- package/tests/unit/commands/resume.test.ts +1 -1
- package/tests/unit/commands/run.test.ts +259 -10
- package/tests/unit/constants/config.test.ts +1 -0
- package/tests/unit/utils/claude.test.ts +115 -1
- package/tests/unit/utils/formatter.test.ts +27 -1
- package/tests/unit/utils/progress.test.ts +255 -0
- package/tests/unit/utils/task-file.test.ts +236 -0
- package/tests/unit/utils/worktree.test.ts +26 -1
package/dist/postinstall.js
CHANGED
|
@@ -9,6 +9,7 @@ var CONFIG_PATH = join(CLAWT_HOME, "config.json");
|
|
|
9
9
|
var LOGS_DIR = join(CLAWT_HOME, "logs");
|
|
10
10
|
var WORKTREES_DIR = join(CLAWT_HOME, "worktrees");
|
|
11
11
|
var VALIDATE_SNAPSHOTS_DIR = join(CLAWT_HOME, "validate-snapshots");
|
|
12
|
+
var CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
|
|
12
13
|
|
|
13
14
|
// src/constants/messages/common.ts
|
|
14
15
|
var COMMON_MESSAGES = {
|
|
@@ -63,7 +64,33 @@ var RUN_MESSAGES = {
|
|
|
63
64
|
/** 中断后清理完成 */
|
|
64
65
|
INTERRUPT_CLEANED: (count) => `\u2713 \u5DF2\u6E05\u7406 ${count} \u4E2A worktree \u548C\u5BF9\u5E94\u5206\u652F`,
|
|
65
66
|
/** 中断后保留 worktree */
|
|
66
|
-
INTERRUPT_KEPT: "\u5DF2\u4FDD\u7559 worktree\uFF0C\u53EF\u7A0D\u540E\u4F7F\u7528 clawt remove \u624B\u52A8\u6E05\u7406"
|
|
67
|
+
INTERRUPT_KEPT: "\u5DF2\u4FDD\u7559 worktree\uFF0C\u53EF\u7A0D\u540E\u4F7F\u7528 clawt remove \u624B\u52A8\u6E05\u7406",
|
|
68
|
+
/** 非 TTY 环境降级输出:任务启动 */
|
|
69
|
+
PROGRESS_TASK_STARTED: (index, total, branch, path) => `[${index}/${total}] ${branch} \u542F\u52A8 ${path}`,
|
|
70
|
+
/** 非 TTY 环境降级输出:任务完成 */
|
|
71
|
+
PROGRESS_TASK_DONE: (index, total, branch, duration, cost, path) => `[${index}/${total}] ${branch} \u2713 \u5B8C\u6210 ${duration} ${cost} ${path}`,
|
|
72
|
+
/** 非 TTY 环境降级输出:任务失败 */
|
|
73
|
+
PROGRESS_TASK_FAILED: (index, total, branch, duration, path) => `[${index}/${total}] ${branch} \u2717 \u5931\u8D25 ${duration} ${path}`,
|
|
74
|
+
/** 并发限制提示 */
|
|
75
|
+
CONCURRENCY_INFO: (concurrency, total) => `\u5E76\u53D1\u9650\u5236: ${concurrency}\uFF0C\u5171 ${total} \u4E2A\u4EFB\u52A1`,
|
|
76
|
+
/** 并发数无效提示 */
|
|
77
|
+
CONCURRENCY_INVALID: "\u5E76\u53D1\u6570\u5FC5\u987B\u4E3A\u6B63\u6574\u6570",
|
|
78
|
+
/** 任务文件不存在 */
|
|
79
|
+
TASK_FILE_NOT_FOUND: (path) => `\u4EFB\u52A1\u6587\u4EF6\u4E0D\u5B58\u5728: ${path}`,
|
|
80
|
+
/** 任务文件中没有解析到有效任务 */
|
|
81
|
+
TASK_FILE_EMPTY: "\u4EFB\u52A1\u6587\u4EF6\u4E2D\u6CA1\u6709\u89E3\u6790\u5230\u6709\u6548\u4EFB\u52A1",
|
|
82
|
+
/** 任务文件中某个任务块缺少分支名 */
|
|
83
|
+
TASK_FILE_MISSING_BRANCH: (blockIndex) => `\u4EFB\u52A1\u6587\u4EF6\u7B2C ${blockIndex} \u4E2A\u4EFB\u52A1\u5757\u7F3A\u5C11\u5206\u652F\u540D\uFF08# branch: ...\uFF09`,
|
|
84
|
+
/** 任务文件中某个任务块缺少任务描述 */
|
|
85
|
+
TASK_FILE_MISSING_TASK: (branch) => `\u4EFB\u52A1\u6587\u4EF6\u4E2D\u5206\u652F ${branch} \u7F3A\u5C11\u4EFB\u52A1\u63CF\u8FF0`,
|
|
86
|
+
/** 任务文件中某个任务块缺少任务描述(无分支名时按索引定位) */
|
|
87
|
+
TASK_FILE_MISSING_TASK_BY_INDEX: (blockIndex) => `\u4EFB\u52A1\u6587\u4EF6\u7B2C ${blockIndex} \u4E2A\u4EFB\u52A1\u5757\u7F3A\u5C11\u4EFB\u52A1\u63CF\u8FF0`,
|
|
88
|
+
/** --file 和 --tasks 不能同时使用 */
|
|
89
|
+
FILE_AND_TASKS_CONFLICT: "--file \u548C --tasks \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528",
|
|
90
|
+
/** 任务文件加载成功 */
|
|
91
|
+
TASK_FILE_LOADED: (count, path) => `\u2713 \u4ECE ${path} \u52A0\u8F7D\u4E86 ${count} \u4E2A\u4EFB\u52A1`,
|
|
92
|
+
/** 未指定 -b 或 -f */
|
|
93
|
+
BRANCH_OR_FILE_REQUIRED: "\u8BF7\u6307\u5B9A -b \u5206\u652F\u540D\u6216 -f \u4EFB\u52A1\u6587\u4EF6"
|
|
67
94
|
};
|
|
68
95
|
|
|
69
96
|
// src/constants/messages/create.ts
|
|
@@ -269,6 +296,10 @@ var CONFIG_DEFINITIONS = {
|
|
|
269
296
|
confirmDestructiveOps: {
|
|
270
297
|
defaultValue: true,
|
|
271
298
|
description: "\u6267\u884C\u7834\u574F\u6027\u64CD\u4F5C\uFF08reset\u3001validate --clean\uFF09\u524D\u662F\u5426\u63D0\u793A\u786E\u8BA4"
|
|
299
|
+
},
|
|
300
|
+
maxConcurrency: {
|
|
301
|
+
defaultValue: 0,
|
|
302
|
+
description: "run \u547D\u4EE4\u9ED8\u8BA4\u6700\u5927\u5E76\u53D1\u6570\uFF0C0 \u8868\u793A\u4E0D\u9650\u5236"
|
|
272
303
|
}
|
|
273
304
|
};
|
|
274
305
|
function deriveDefaultConfig(definitions) {
|
package/docs/spec.md
CHANGED
|
@@ -168,7 +168,7 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
|
|
|
168
168
|
| 命令 | 说明 | 对应场景 |
|
|
169
169
|
| --------------------- | ---------------------------------------------- | -------- |
|
|
170
170
|
| `clawt create` | 批量创建 worktree 及对应分支 | 5.1 |
|
|
171
|
-
| `clawt run` | 批量创建 worktree + 启动 Claude Code
|
|
171
|
+
| `clawt run` | 批量创建 worktree + 启动 Claude Code 执行任务(支持任务文件) | 5.2 |
|
|
172
172
|
| `clawt validate` | 在主 worktree 验证某个 worktree 分支的变更 | 5.4 |
|
|
173
173
|
| `clawt merge` | 合并某个已验证的 worktree 分支到主 worktree | 5.6 |
|
|
174
174
|
| `clawt remove` | 移除 worktree(支持模糊匹配/多选/全部) | 5.5 |
|
|
@@ -259,7 +259,10 @@ clawt create -b <branchName> [-n <count>]
|
|
|
259
259
|
# 方式一:通过 --tasks 参数直接指定任务(多任务并行)
|
|
260
260
|
clawt run -b <branchName> --tasks <task1> --tasks <task2> --tasks <task3>
|
|
261
261
|
|
|
262
|
-
#
|
|
262
|
+
# 方式二:通过 -f 从任务文件读取任务列表
|
|
263
|
+
clawt run -f <path>
|
|
264
|
+
|
|
265
|
+
# 方式三:不传 --tasks 也不传 -f,在 worktree 中打开 Claude Code 交互式界面
|
|
263
266
|
clawt run -b <branchName>
|
|
264
267
|
```
|
|
265
268
|
|
|
@@ -267,21 +270,67 @@ clawt run -b <branchName>
|
|
|
267
270
|
|
|
268
271
|
| 参数 | 必填 | 说明 |
|
|
269
272
|
| --------- | ---- | ----------------------------------------------------------- |
|
|
270
|
-
| `-b` |
|
|
273
|
+
| `-b` | 否 | 分支名(使用 `-f` 时可选,否则必填) |
|
|
271
274
|
| `--tasks` | 否 | 任务描述(可多次指定,每个 --tasks 对应一个任务,任务数量即 worktree 数量)。不传则在 worktree 中打开 Claude Code 交互式界面 |
|
|
275
|
+
| `-f` | 否 | 从任务文件读取任务列表(与 `--tasks` 互斥) |
|
|
276
|
+
| `-c` | 否 | 最大并发数,`0` 表示不限制 |
|
|
277
|
+
|
|
278
|
+
**互斥约束:**
|
|
279
|
+
|
|
280
|
+
- `--file` 和 `--tasks` **不能同时使用**
|
|
281
|
+
- 非 `-f` 模式必须指定 `-b`
|
|
272
282
|
|
|
273
283
|
**交互式 Claude Code 界面模式:**
|
|
274
284
|
|
|
275
|
-
当不传 `--tasks` 时,会创建单个 worktree,然后通过 `spawnSync` + `inherit stdio` 在该 worktree 中直接启动 Claude Code CLI 交互式界面,让用户与 Claude Code 直接交互。
|
|
285
|
+
当不传 `--tasks` 也不传 `-f` 时,会创建单个 worktree,然后通过 `spawnSync` + `inherit stdio` 在该 worktree 中直接启动 Claude Code CLI 交互式界面,让用户与 Claude Code 直接交互。
|
|
276
286
|
|
|
277
287
|
启动命令通过配置项 `claudeCodeCommand`(默认值 `claude`)指定,支持自定义命令及参数。
|
|
278
288
|
|
|
279
|
-
|
|
289
|
+
#### 任务文件格式
|
|
290
|
+
|
|
291
|
+
任务文件使用 Markdown 文件中嵌入 HTML 注释标签的自定义格式,标签外的任何文本都不会被解析。
|
|
292
|
+
|
|
293
|
+
```markdown
|
|
294
|
+
这里可以写任何说明文字,会被忽略
|
|
295
|
+
|
|
296
|
+
<!-- CLAWT-TASKS:START -->
|
|
297
|
+
# branch: feat-login
|
|
298
|
+
实现用户登录功能
|
|
299
|
+
<!-- CLAWT-TASKS:END -->
|
|
300
|
+
|
|
301
|
+
<!-- CLAWT-TASKS:START -->
|
|
302
|
+
# branch: fix-bug
|
|
303
|
+
修复内存泄漏问题
|
|
304
|
+
这是多行任务描述
|
|
305
|
+
可以写很多行
|
|
306
|
+
<!-- CLAWT-TASKS:END -->
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**格式规则:**
|
|
310
|
+
|
|
311
|
+
1. **任务块界定**:每个任务用 `<!-- CLAWT-TASKS:START -->` 和 `<!-- CLAWT-TASKS:END -->` 包裹
|
|
312
|
+
2. **分支名声明**:块内必须有一行 `# branch: <分支名>`(冒号前后的空格可灵活)
|
|
313
|
+
3. **任务描述**:块内除分支名行以外的所有行,合并为任务描述(支持多行)
|
|
314
|
+
4. **块外内容忽略**:标签外的任何文本都不会被解析
|
|
315
|
+
5. **必填校验**:每个块必须包含任务描述;分支名默认必填,但使用 `-b` 参数时分支名为可选(会被忽略,用 `-b` 值自动编号)
|
|
316
|
+
|
|
317
|
+
**解析实现:** `src/utils/task-file.ts` 中的 `parseTaskFile()` 和 `loadTaskFile()` 函数,类型定义为 `TaskFileEntry`(`src/types/taskFile.ts`)。
|
|
318
|
+
|
|
319
|
+
#### 任务文件模式运行流程
|
|
320
|
+
|
|
321
|
+
使用 `-f` 时的执行路径(`handleRun` → `handleRunFromFile`):
|
|
322
|
+
|
|
323
|
+
1. 调用 `loadTaskFile(options.file)` 读取解析文件
|
|
324
|
+
2. **有 `-b` 参数**:忽略文件中的分支名,用 `-b` 值自动编号创建 worktree(`createWorktrees(branch, count)`)
|
|
325
|
+
3. **无 `-b` 参数**:使用文件中每个任务的独立分支名,先 `sanitizeBranchName` 清理后调用 `createWorktreesByBranches(branches)`
|
|
326
|
+
4. 调用 `executeBatchTasks(worktrees, tasks, concurrency)` 执行
|
|
327
|
+
|
|
328
|
+
#### --tasks 模式运行流程
|
|
280
329
|
|
|
281
330
|
1. 若传了 `--tasks`,解析得到任务数组 `tasks[]`;若未传,先检测分支是否已存在(已存在则提示使用 `clawt resume -b <branchName>` 恢复会话),然后创建单个 worktree 并启动 Claude Code 交互式界面(流程结束,不进入后续并行执行阶段)
|
|
282
331
|
2. `n = tasks.length`
|
|
283
332
|
3. 按照 **5.1** 的流程创建 `n` 个 worktree
|
|
284
|
-
4.
|
|
333
|
+
4. 通过公共函数 `executeBatchTasks`(`src/utils/task-executor.ts`)启动批量任务执行,该函数负责进度面板渲染、SIGINT 中断处理、并发控制和汇总输出。对每个 worktree 并行启动 Claude Code CLI:
|
|
285
334
|
```bash
|
|
286
335
|
cd ~/.clawt/worktrees/<project>/<branchName>-<i>
|
|
287
336
|
claude -p "<tasks[i]>" --output-format json --permission-mode bypassPermissions
|
|
@@ -329,7 +378,7 @@ Claude Code CLI 以 `--output-format json` 运行时,退出后会在 stdout
|
|
|
329
378
|
1. 为每个 Claude Code 子进程维护状态(运行中 / 已完成 / 已失败)
|
|
330
379
|
2. 监听每个子进程的 `close` 事件(基于 Node.js `ChildProcess` 的事件驱动机制)
|
|
331
380
|
3. 当某个子进程触发 `close` 事件时,解析其 stdout 输出的 JSON
|
|
332
|
-
4. 在主 worktree 的 clawt
|
|
381
|
+
4. 在主 worktree 的 clawt 终端实时输出通知(进度面板每个任务行末尾显示 worktree 路径,终端可点击跳转):
|
|
333
382
|
|
|
334
383
|
```
|
|
335
384
|
✓ [完成] worktree: ~/.clawt/worktrees/main-project/feature-scheme-1
|
|
@@ -1039,6 +1088,8 @@ clawt resume
|
|
|
1039
1088
|
|
|
1040
1089
|
启动命令通过配置项 `claudeCodeCommand`(默认值 `claude`)指定,与 `clawt run` 不传 `--tasks` 时的交互式界面行为一致。
|
|
1041
1090
|
|
|
1091
|
+
**会话自动续接:** 启动前会自动检测该 worktree 是否存在 Claude Code 历史会话(通过检查 `~/.claude/projects/<encoded-path>/` 下是否有 `.jsonl` 文件判断),如果存在则自动追加 `--continue` 参数继续上次对话,否则打开新对话。启动信息中会显示当前模式("继续上次对话"或"新对话")。路径编码规则:将绝对路径中所有非字母数字字符替换为 `-`(与 Claude Code 源码的编码逻辑一致)。
|
|
1092
|
+
|
|
1042
1093
|
---
|
|
1043
1094
|
|
|
1044
1095
|
### 5.12 将主分支代码同步到目标 Worktree
|
|
@@ -1316,7 +1367,7 @@ clawt status [--json]
|
|
|
1316
1367
|
- 测试辅助文件:
|
|
1317
1368
|
- `tests/helpers/setup.ts`:全局 setup,禁用 chalk 颜色输出避免 ANSI 转义码干扰断言
|
|
1318
1369
|
- `tests/helpers/fixtures.ts`:测试数据工厂,提供 `createWorktreeInfo()`、`createWorktreeStatus()`、`createWorktreeList()` 等工厂函数
|
|
1319
|
-
- 覆盖范围:`src/` 下的 `utils/`、`errors/`、`constants/`
|
|
1370
|
+
- 覆盖范围:`src/` 下的 `commands/`、`utils/`、`errors/`、`constants/` 全部关键模块
|
|
1320
1371
|
- 覆盖率统计排除项:`src/index.ts`(入口文件)、`src/types/**`(类型定义)、`src/logger/**`(日志模块)
|
|
1321
1372
|
- npm 脚本:
|
|
1322
1373
|
- `npm test`:执行全部测试(`vitest run`)
|
package/package.json
CHANGED
package/src/commands/resume.ts
CHANGED
|
@@ -48,6 +48,6 @@ async function handleResume(options: ResumeOptions): Promise<void> {
|
|
|
48
48
|
const worktrees = getProjectWorktrees();
|
|
49
49
|
const worktree = await resolveTargetWorktree(worktrees, RESUME_RESOLVE_MESSAGES, options.branch);
|
|
50
50
|
|
|
51
|
-
// 启动 Claude Code
|
|
52
|
-
launchInteractiveClaude(worktree);
|
|
51
|
+
// 启动 Claude Code 交互式界面(resume 自动续接历史会话)
|
|
52
|
+
launchInteractiveClaude(worktree, { autoContinue: true });
|
|
53
53
|
}
|
package/src/commands/run.ts
CHANGED
|
@@ -1,27 +1,20 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
-
import type { ChildProcess } from 'node:child_process';
|
|
3
2
|
import { logger } from '../logger/index.js';
|
|
4
3
|
import { ClawtError } from '../errors/index.js';
|
|
5
4
|
import { MESSAGES } from '../constants/index.js';
|
|
6
|
-
import type { RunOptions,
|
|
5
|
+
import type { RunOptions, WorktreeInfo } from '../types/index.js';
|
|
7
6
|
import {
|
|
8
7
|
validateMainWorktree,
|
|
9
8
|
validateClaudeCodeInstalled,
|
|
10
9
|
createWorktrees,
|
|
10
|
+
createWorktreesByBranches,
|
|
11
11
|
sanitizeBranchName,
|
|
12
12
|
checkBranchExists,
|
|
13
|
-
spawnProcess,
|
|
14
|
-
killAllChildProcesses,
|
|
15
|
-
cleanupWorktrees,
|
|
16
13
|
getConfigValue,
|
|
17
14
|
printSuccess,
|
|
18
|
-
printError,
|
|
19
|
-
printWarning,
|
|
20
|
-
printInfo,
|
|
21
|
-
printSeparator,
|
|
22
|
-
printDoubleSeparator,
|
|
23
|
-
confirmAction,
|
|
24
15
|
launchInteractiveClaude,
|
|
16
|
+
loadTaskFile,
|
|
17
|
+
executeBatchTasks,
|
|
25
18
|
} from '../utils/index.js';
|
|
26
19
|
|
|
27
20
|
/**
|
|
@@ -32,165 +25,93 @@ export function registerRunCommand(program: Command): void {
|
|
|
32
25
|
program
|
|
33
26
|
.command('run')
|
|
34
27
|
.description('批量创建 worktree 并启动 Claude Code 执行任务')
|
|
35
|
-
.
|
|
28
|
+
.option('-b, --branch <branchName>', '分支名')
|
|
36
29
|
.option('--tasks <task...>', '任务列表(可多次指定),不传则在 worktree 中打开 Claude Code 交互式界面')
|
|
30
|
+
.option('-c, --concurrency <n>', '最大并发数,0 表示不限制')
|
|
31
|
+
.option('-f, --file <path>', '从任务文件读取任务列表(与 --tasks 互斥)')
|
|
37
32
|
.action(async (options: RunOptions) => {
|
|
38
33
|
await handleRun(options);
|
|
39
34
|
});
|
|
40
35
|
}
|
|
41
36
|
|
|
42
|
-
/** executeClaudeTask 的返回结构,包含子进程引用和结果 Promise */
|
|
43
|
-
interface ClaudeTaskHandle {
|
|
44
|
-
/** 子进程实例,用于在中断时终止 */
|
|
45
|
-
child: ChildProcess;
|
|
46
|
-
/** 任务结果 Promise */
|
|
47
|
-
promise: Promise<TaskResult>;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
37
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* @param {string}
|
|
54
|
-
* @
|
|
38
|
+
* 解析并发数参数
|
|
39
|
+
* 优先级:命令行参数 > 全局配置 > 默认值 0
|
|
40
|
+
* @param {string | undefined} optionValue - 命令行传入的并发数字符串
|
|
41
|
+
* @param {number} configValue - 全局配置中的默认并发数
|
|
42
|
+
* @returns {number} 解析后的并发数,0 表示不限制
|
|
55
43
|
*/
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
['-p', task, '--output-format', 'json', '--permission-mode', 'bypassPermissions'],
|
|
60
|
-
{
|
|
61
|
-
cwd: worktree.path,
|
|
62
|
-
// stdin 必须设置为 'ignore',不能用 'pipe'
|
|
63
|
-
// 原因:claude -p 是非交互模式,不需要 stdin 输入。如果 stdin 为 'pipe',
|
|
64
|
-
// 父进程会创建一个可写流连接到子进程但从不写入也不关闭,
|
|
65
|
-
// claude 检测到 stdin 是管道后会尝试读取输入,导致进程永远卡住
|
|
66
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
67
|
-
},
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
const promise = new Promise<TaskResult>((resolve) => {
|
|
71
|
-
let stdout = '';
|
|
72
|
-
let stderr = '';
|
|
73
|
-
|
|
74
|
-
child.stdout?.on('data', (data: Buffer) => {
|
|
75
|
-
stdout += data.toString();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
child.stderr?.on('data', (data: Buffer) => {
|
|
79
|
-
stderr += data.toString();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
child.on('close', (code) => {
|
|
83
|
-
let result: ClaudeCodeResult | null = null;
|
|
84
|
-
let success = code === 0;
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
if (stdout.trim()) {
|
|
88
|
-
result = JSON.parse(stdout.trim()) as ClaudeCodeResult;
|
|
89
|
-
success = !result.is_error;
|
|
90
|
-
}
|
|
91
|
-
} catch {
|
|
92
|
-
logger.warn(`解析 Claude Code 输出失败: ${stdout.substring(0, 200)}`);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
resolve({
|
|
96
|
-
task,
|
|
97
|
-
branch: worktree.branch,
|
|
98
|
-
worktreePath: worktree.path,
|
|
99
|
-
success,
|
|
100
|
-
result,
|
|
101
|
-
error: success ? undefined : stderr || '任务执行失败',
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
child.on('error', (err) => {
|
|
106
|
-
resolve({
|
|
107
|
-
task,
|
|
108
|
-
branch: worktree.branch,
|
|
109
|
-
worktreePath: worktree.path,
|
|
110
|
-
success: false,
|
|
111
|
-
result: null,
|
|
112
|
-
error: err.message,
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
return { child, promise };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* 输出单个任务完成通知
|
|
122
|
-
* @param {TaskResult} taskResult - 任务结果
|
|
123
|
-
*/
|
|
124
|
-
function printTaskNotification(taskResult: TaskResult): void {
|
|
125
|
-
const { success, worktreePath, branch, result } = taskResult;
|
|
126
|
-
const status = success ? '完成' : '失败';
|
|
127
|
-
const icon = success ? '✓' : '✗';
|
|
128
|
-
const durationStr = result ? `${(result.duration_ms / 1000).toFixed(1)}s` : 'N/A';
|
|
129
|
-
const costStr = result ? `$${result.total_cost_usd.toFixed(2)}` : 'N/A';
|
|
130
|
-
const resultStr = success ? 'success' : 'failed';
|
|
131
|
-
|
|
132
|
-
if (success) {
|
|
133
|
-
printSuccess(`${icon} [${status}] worktree: ${worktreePath}`);
|
|
134
|
-
} else {
|
|
135
|
-
printError(`${icon} [${status}] worktree: ${worktreePath}`);
|
|
44
|
+
function parseConcurrency(optionValue: string | undefined, configValue: number): number {
|
|
45
|
+
if (optionValue === undefined) {
|
|
46
|
+
return configValue;
|
|
136
47
|
}
|
|
137
|
-
printInfo(` 分支: ${branch}`);
|
|
138
|
-
printInfo(` 耗时: ${durationStr}`);
|
|
139
|
-
printInfo(` 花费: ${costStr}`);
|
|
140
|
-
printInfo(` 结果: ${resultStr}`);
|
|
141
|
-
printSeparator();
|
|
142
|
-
}
|
|
143
48
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
printDoubleSeparator();
|
|
150
|
-
printInfo(`全部任务已完成 (${summary.total}/${summary.total})`);
|
|
151
|
-
printInfo(` 成功: ${summary.succeeded}`);
|
|
152
|
-
printInfo(` 失败: ${summary.failed}`);
|
|
153
|
-
printInfo(` 总耗时: ${(summary.totalDurationMs / 1000).toFixed(1)}s`);
|
|
154
|
-
printInfo(` 总花费: $${summary.totalCostUsd.toFixed(2)}`);
|
|
155
|
-
printDoubleSeparator();
|
|
49
|
+
const parsed = parseInt(optionValue, 10);
|
|
50
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
51
|
+
throw new ClawtError(MESSAGES.CONCURRENCY_INVALID);
|
|
52
|
+
}
|
|
53
|
+
return parsed;
|
|
156
54
|
}
|
|
157
55
|
|
|
158
56
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* @param {WorktreeInfo[]} worktrees - 本次创建的 worktree 列表
|
|
57
|
+
* 处理从任务文件执行的逻辑
|
|
58
|
+
* @param {RunOptions} options - 命令选项(包含 file 字段)
|
|
162
59
|
*/
|
|
163
|
-
async function
|
|
164
|
-
|
|
60
|
+
async function handleRunFromFile(options: RunOptions): Promise<void> {
|
|
61
|
+
// 有 -b 参数时,文件中的分支名为可选
|
|
62
|
+
const branchRequired = !options.branch;
|
|
63
|
+
// 加载并解析任务文件
|
|
64
|
+
const entries = loadTaskFile(options.file!, { branchRequired });
|
|
65
|
+
printSuccess(MESSAGES.TASK_FILE_LOADED(entries.length, options.file!));
|
|
165
66
|
|
|
166
|
-
|
|
167
|
-
// 全局配置了自动删除,直接清理
|
|
168
|
-
cleanupWorktrees(worktrees);
|
|
169
|
-
printSuccess(MESSAGES.INTERRUPT_AUTO_CLEANED(worktrees.length));
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
67
|
+
const tasks = entries.map((e) => e.task);
|
|
172
68
|
|
|
173
|
-
|
|
174
|
-
const shouldClean = await confirmAction(MESSAGES.INTERRUPT_CONFIRM_CLEANUP);
|
|
69
|
+
let worktrees: WorktreeInfo[];
|
|
175
70
|
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
|
|
71
|
+
if (options.branch) {
|
|
72
|
+
// 有 -b 参数:忽略文件中的分支名,用 -b 自动编号
|
|
73
|
+
worktrees = createWorktrees(options.branch, entries.length);
|
|
179
74
|
} else {
|
|
180
|
-
|
|
75
|
+
// 无 -b 参数:使用文件中每个任务的独立分支名
|
|
76
|
+
const branches = entries.map((e) => sanitizeBranchName(e.branch!));
|
|
77
|
+
worktrees = createWorktreesByBranches(branches);
|
|
181
78
|
}
|
|
79
|
+
|
|
80
|
+
// 解析并发数
|
|
81
|
+
const concurrency = parseConcurrency(options.concurrency, getConfigValue('maxConcurrency'));
|
|
82
|
+
|
|
83
|
+
logger.info(`run 命令(文件模式)执行,任务数: ${entries.length},并发数: ${concurrency || '不限制'}`);
|
|
84
|
+
|
|
85
|
+
await executeBatchTasks(worktrees, tasks, concurrency);
|
|
182
86
|
}
|
|
183
87
|
|
|
184
88
|
/**
|
|
185
89
|
* 执行 run 命令的核心逻辑
|
|
186
|
-
*
|
|
187
|
-
*
|
|
90
|
+
* 支持三种模式:
|
|
91
|
+
* 1. -f 任务文件模式
|
|
92
|
+
* 2. --tasks 命令行任务模式
|
|
93
|
+
* 3. 无任务参数时打开交互式界面
|
|
188
94
|
* @param {RunOptions} options - 命令选项
|
|
189
95
|
*/
|
|
190
96
|
async function handleRun(options: RunOptions): Promise<void> {
|
|
191
97
|
validateMainWorktree();
|
|
192
98
|
validateClaudeCodeInstalled();
|
|
193
99
|
|
|
100
|
+
// 互斥校验:--file 和 --tasks 不能同时使用
|
|
101
|
+
if (options.file && options.tasks) {
|
|
102
|
+
throw new ClawtError(MESSAGES.FILE_AND_TASKS_CONFLICT);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --file 模式
|
|
106
|
+
if (options.file) {
|
|
107
|
+
return handleRunFromFile(options);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 非 --file 模式必须指定 -b
|
|
111
|
+
if (!options.branch) {
|
|
112
|
+
throw new ClawtError(MESSAGES.BRANCH_OR_FILE_REQUIRED);
|
|
113
|
+
}
|
|
114
|
+
|
|
194
115
|
// 未传 --tasks 时,创建单个 worktree 并打开 Claude Code 交互式界面
|
|
195
116
|
if (!options.tasks || options.tasks.length === 0) {
|
|
196
117
|
// 分支已存在时,提示用户使用 resume 恢复会话
|
|
@@ -214,73 +135,14 @@ async function handleRun(options: RunOptions): Promise<void> {
|
|
|
214
135
|
}
|
|
215
136
|
|
|
216
137
|
const count = tasks.length;
|
|
217
|
-
logger.info(`run 命令执行,分支: ${options.branch},任务数: ${count}`);
|
|
218
138
|
|
|
219
|
-
//
|
|
220
|
-
const
|
|
221
|
-
printSuccess(MESSAGES.WORKTREE_CREATED(worktrees.length));
|
|
222
|
-
for (const wt of worktrees) {
|
|
223
|
-
printInfo(` 分支: ${wt.branch} 路径: ${wt.path}`);
|
|
224
|
-
}
|
|
225
|
-
printInfo('');
|
|
139
|
+
// 解析并发数:命令行参数 > 全局配置 > 默认值 0
|
|
140
|
+
const concurrency = parseConcurrency(options.concurrency, getConfigValue('maxConcurrency'));
|
|
226
141
|
|
|
227
|
-
|
|
228
|
-
const startTime = Date.now();
|
|
229
|
-
const handles = worktrees.map((wt, index) => {
|
|
230
|
-
const task = tasks[index];
|
|
231
|
-
logger.info(`启动任务 ${index + 1}: ${task} (worktree: ${wt.path})`);
|
|
232
|
-
return executeClaudeTask(wt, task);
|
|
233
|
-
});
|
|
142
|
+
logger.info(`run 命令执行,分支: ${options.branch},任务数: ${count},并发数: ${concurrency || '不限制'}`);
|
|
234
143
|
|
|
235
|
-
//
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
// 监听 SIGINT(Ctrl+C),终止所有子进程并触发清理流程
|
|
239
|
-
let interrupted = false;
|
|
240
|
-
const sigintHandler = async () => {
|
|
241
|
-
if (interrupted) return;
|
|
242
|
-
interrupted = true;
|
|
243
|
-
|
|
244
|
-
printInfo('');
|
|
245
|
-
printWarning(MESSAGES.INTERRUPTED);
|
|
246
|
-
killAllChildProcesses(childProcesses);
|
|
247
|
-
|
|
248
|
-
// 等待所有子进程退出后再执行清理
|
|
249
|
-
await Promise.allSettled(handles.map((h) => h.promise));
|
|
250
|
-
|
|
251
|
-
await handleInterruptCleanup(worktrees);
|
|
252
|
-
process.exit(1);
|
|
253
|
-
};
|
|
254
|
-
process.on('SIGINT', sigintHandler);
|
|
255
|
-
|
|
256
|
-
const taskPromises = handles.map((handle) =>
|
|
257
|
-
handle.promise.then((result) => {
|
|
258
|
-
// 被中断时不再输出通知
|
|
259
|
-
if (!interrupted) {
|
|
260
|
-
printTaskNotification(result);
|
|
261
|
-
}
|
|
262
|
-
return result;
|
|
263
|
-
}),
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
const results = await Promise.all(taskPromises);
|
|
267
|
-
|
|
268
|
-
// 正常完成,移除 SIGINT 监听器
|
|
269
|
-
process.removeListener('SIGINT', sigintHandler);
|
|
270
|
-
|
|
271
|
-
// 被中断时不输出汇总(已在 sigintHandler 中处理退出)
|
|
272
|
-
if (interrupted) return;
|
|
273
|
-
|
|
274
|
-
const totalDurationMs = Date.now() - startTime;
|
|
275
|
-
|
|
276
|
-
// 汇总
|
|
277
|
-
const summary: TaskSummary = {
|
|
278
|
-
total: results.length,
|
|
279
|
-
succeeded: results.filter((r) => r.success).length,
|
|
280
|
-
failed: results.filter((r) => !r.success).length,
|
|
281
|
-
totalDurationMs,
|
|
282
|
-
totalCostUsd: results.reduce((sum, r) => sum + (r.result?.total_cost_usd ?? 0), 0),
|
|
283
|
-
};
|
|
144
|
+
// 创建 worktree
|
|
145
|
+
const worktrees = createWorktrees(options.branch, count);
|
|
284
146
|
|
|
285
|
-
|
|
147
|
+
await executeBatchTasks(worktrees, tasks, concurrency);
|
|
286
148
|
}
|
package/src/constants/config.ts
CHANGED
package/src/constants/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { CLAWT_HOME, CONFIG_PATH, LOGS_DIR, WORKTREES_DIR, VALIDATE_SNAPSHOTS_DIR } from './paths.js';
|
|
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
4
|
export { EXIT_CODES } from './exitCodes.js';
|
|
@@ -6,3 +6,13 @@ export { ENABLE_BRACKETED_PASTE, DISABLE_BRACKETED_PASTE, PASTE_THRESHOLD_MS } f
|
|
|
6
6
|
export { DEFAULT_CONFIG, CONFIG_DESCRIPTIONS, APPEND_SYSTEM_PROMPT } from './config.js';
|
|
7
7
|
export { AUTO_SAVE_COMMIT_MESSAGE } from './git.js';
|
|
8
8
|
export { DEBUG_LOG_PREFIX, DEBUG_TIMESTAMP_FORMAT } from './logger.js';
|
|
9
|
+
export {
|
|
10
|
+
SPINNER_FRAMES,
|
|
11
|
+
SPINNER_INTERVAL_MS,
|
|
12
|
+
CURSOR_UP,
|
|
13
|
+
CLEAR_LINE,
|
|
14
|
+
CURSOR_HIDE,
|
|
15
|
+
CURSOR_SHOW,
|
|
16
|
+
TASK_STATUS_ICONS,
|
|
17
|
+
TASK_STATUS_LABELS,
|
|
18
|
+
} from './progress.js';
|
|
@@ -13,4 +13,34 @@ export const RUN_MESSAGES = {
|
|
|
13
13
|
INTERRUPT_CLEANED: (count: number) => `✓ 已清理 ${count} 个 worktree 和对应分支`,
|
|
14
14
|
/** 中断后保留 worktree */
|
|
15
15
|
INTERRUPT_KEPT: '已保留 worktree,可稍后使用 clawt remove 手动清理',
|
|
16
|
+
/** 非 TTY 环境降级输出:任务启动 */
|
|
17
|
+
PROGRESS_TASK_STARTED: (index: number, total: number, branch: string, path: string) =>
|
|
18
|
+
`[${index}/${total}] ${branch} 启动 ${path}`,
|
|
19
|
+
/** 非 TTY 环境降级输出:任务完成 */
|
|
20
|
+
PROGRESS_TASK_DONE: (index: number, total: number, branch: string, duration: string, cost: string, path: string) =>
|
|
21
|
+
`[${index}/${total}] ${branch} ✓ 完成 ${duration} ${cost} ${path}`,
|
|
22
|
+
/** 非 TTY 环境降级输出:任务失败 */
|
|
23
|
+
PROGRESS_TASK_FAILED: (index: number, total: number, branch: string, duration: string, path: string) =>
|
|
24
|
+
`[${index}/${total}] ${branch} ✗ 失败 ${duration} ${path}`,
|
|
25
|
+
/** 并发限制提示 */
|
|
26
|
+
CONCURRENCY_INFO: (concurrency: number, total: number) =>
|
|
27
|
+
`并发限制: ${concurrency},共 ${total} 个任务`,
|
|
28
|
+
/** 并发数无效提示 */
|
|
29
|
+
CONCURRENCY_INVALID: '并发数必须为正整数',
|
|
30
|
+
/** 任务文件不存在 */
|
|
31
|
+
TASK_FILE_NOT_FOUND: (path: string) => `任务文件不存在: ${path}`,
|
|
32
|
+
/** 任务文件中没有解析到有效任务 */
|
|
33
|
+
TASK_FILE_EMPTY: '任务文件中没有解析到有效任务',
|
|
34
|
+
/** 任务文件中某个任务块缺少分支名 */
|
|
35
|
+
TASK_FILE_MISSING_BRANCH: (blockIndex: number) => `任务文件第 ${blockIndex} 个任务块缺少分支名(# branch: ...)`,
|
|
36
|
+
/** 任务文件中某个任务块缺少任务描述 */
|
|
37
|
+
TASK_FILE_MISSING_TASK: (branch: string) => `任务文件中分支 ${branch} 缺少任务描述`,
|
|
38
|
+
/** 任务文件中某个任务块缺少任务描述(无分支名时按索引定位) */
|
|
39
|
+
TASK_FILE_MISSING_TASK_BY_INDEX: (blockIndex: number) => `任务文件第 ${blockIndex} 个任务块缺少任务描述`,
|
|
40
|
+
/** --file 和 --tasks 不能同时使用 */
|
|
41
|
+
FILE_AND_TASKS_CONFLICT: '--file 和 --tasks 不能同时使用',
|
|
42
|
+
/** 任务文件加载成功 */
|
|
43
|
+
TASK_FILE_LOADED: (count: number, path: string) => `✓ 从 ${path} 加载了 ${count} 个任务`,
|
|
44
|
+
/** 未指定 -b 或 -f */
|
|
45
|
+
BRANCH_OR_FILE_REQUIRED: '请指定 -b 分支名或 -f 任务文件',
|
|
16
46
|
} as const;
|
package/src/constants/paths.ts
CHANGED
|
@@ -15,3 +15,6 @@ export const WORKTREES_DIR = join(CLAWT_HOME, 'worktrees');
|
|
|
15
15
|
|
|
16
16
|
/** validate 快照目录 ~/.clawt/validate-snapshots/ */
|
|
17
17
|
export const VALIDATE_SNAPSHOTS_DIR = join(CLAWT_HOME, 'validate-snapshots');
|
|
18
|
+
|
|
19
|
+
/** Claude Code 项目会话目录 ~/.claude/projects/ */
|
|
20
|
+
export const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Braille spinner 帧序列 */
|
|
2
|
+
export const SPINNER_FRAMES: readonly string[] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
3
|
+
|
|
4
|
+
/** spinner 刷新间隔(毫秒) */
|
|
5
|
+
export const SPINNER_INTERVAL_MS = 100;
|
|
6
|
+
|
|
7
|
+
/** ANSI 转义:光标上移 n 行 */
|
|
8
|
+
export const CURSOR_UP = (n: number): string => `\x1B[${n}A`;
|
|
9
|
+
|
|
10
|
+
/** ANSI 转义:清除从光标到行尾 */
|
|
11
|
+
export const CLEAR_LINE = '\x1B[0K';
|
|
12
|
+
|
|
13
|
+
/** ANSI 转义:隐藏光标 */
|
|
14
|
+
export const CURSOR_HIDE = '\x1B[?25l';
|
|
15
|
+
|
|
16
|
+
/** ANSI 转义:显示光标 */
|
|
17
|
+
export const CURSOR_SHOW = '\x1B[?25h';
|
|
18
|
+
|
|
19
|
+
/** 任务状态图标 */
|
|
20
|
+
export const TASK_STATUS_ICONS = {
|
|
21
|
+
/** 排队中 */
|
|
22
|
+
PENDING: '◦',
|
|
23
|
+
/** 完成 */
|
|
24
|
+
DONE: '✓',
|
|
25
|
+
/** 失败 */
|
|
26
|
+
FAILED: '✗',
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
/** 任务状态标签 */
|
|
30
|
+
export const TASK_STATUS_LABELS = {
|
|
31
|
+
/** 排队中 */
|
|
32
|
+
PENDING: '排队中',
|
|
33
|
+
/** 运行中 */
|
|
34
|
+
RUNNING: '运行中',
|
|
35
|
+
/** 完成 */
|
|
36
|
+
DONE: '完成',
|
|
37
|
+
/** 失败 */
|
|
38
|
+
FAILED: '失败',
|
|
39
|
+
} as const;
|
package/src/types/command.ts
CHANGED
|
@@ -12,6 +12,10 @@ export interface RunOptions {
|
|
|
12
12
|
branch: string;
|
|
13
13
|
/** 任务列表(支持多次 --tasks 传入),不传则在 worktree 中打开 Claude Code 交互式界面 */
|
|
14
14
|
tasks?: string[];
|
|
15
|
+
/** 最大并发数(Commander 传入为字符串),0 表示不限制 */
|
|
16
|
+
concurrency?: string;
|
|
17
|
+
/** 任务文件路径(与 --tasks 互斥) */
|
|
18
|
+
file?: string;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
/** validate 命令选项 */
|
package/src/types/config.ts
CHANGED