clawt 2.16.3 → 2.16.5
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 +13 -1
- package/README.md +4 -0
- package/dist/index.js +379 -119
- package/dist/postinstall.js +12 -3
- package/docs/spec.md +97 -12
- package/package.json +2 -1
- package/src/commands/sync.ts +34 -14
- package/src/commands/validate.ts +50 -8
- package/src/constants/index.ts +12 -2
- package/src/constants/messages/run.ts +4 -4
- package/src/constants/messages/validate.ts +12 -0
- package/src/constants/progress.ts +36 -6
- package/src/utils/index.ts +2 -0
- package/src/utils/progress-render.ts +77 -13
- package/src/utils/progress.ts +110 -24
- package/src/utils/stream-parser.ts +251 -0
- package/src/utils/task-executor.ts +61 -27
- package/tests/unit/utils/progress-render.test.ts +96 -0
- package/tests/unit/utils/progress.test.ts +391 -10
- package/tests/unit/utils/stream-parser.test.ts +375 -0
package/dist/postinstall.js
CHANGED
|
@@ -68,9 +68,9 @@ var RUN_MESSAGES = {
|
|
|
68
68
|
/** 非 TTY 环境降级输出:任务启动 */
|
|
69
69
|
PROGRESS_TASK_STARTED: (index, total, branch, path) => `[${index}/${total}] ${branch} \u542F\u52A8 ${path}`,
|
|
70
70
|
/** 非 TTY 环境降级输出:任务完成 */
|
|
71
|
-
PROGRESS_TASK_DONE: (index, total, branch, duration, cost,
|
|
71
|
+
PROGRESS_TASK_DONE: (index, total, branch, duration, cost, detail) => `[${index}/${total}] ${branch} \u2713 \u5B8C\u6210 ${duration} ${cost} ${detail}`,
|
|
72
72
|
/** 非 TTY 环境降级输出:任务失败 */
|
|
73
|
-
PROGRESS_TASK_FAILED: (index, total, branch, duration,
|
|
73
|
+
PROGRESS_TASK_FAILED: (index, total, branch, duration, detail) => `[${index}/${total}] ${branch} \u2717 \u5931\u8D25 ${duration} ${detail}`,
|
|
74
74
|
/** 并发限制提示 */
|
|
75
75
|
CONCURRENCY_INFO: (concurrency, total) => `\u5E76\u53D1\u9650\u5236: ${concurrency}\uFF0C\u5171 ${total} \u4E2A\u4EFB\u52A1`,
|
|
76
76
|
/** 并发数无效提示 */
|
|
@@ -203,7 +203,16 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
|
203
203
|
/** 并行执行中单个命令失败 */
|
|
204
204
|
VALIDATE_PARALLEL_CMD_FAILED: (command, exitCode) => ` \u2717 ${command}\uFF08\u9000\u51FA\u7801: ${exitCode}\uFF09`,
|
|
205
205
|
/** 并行执行中单个命令启动失败 */
|
|
206
|
-
VALIDATE_PARALLEL_CMD_ERROR: (command, errorMessage) => ` \u2717 ${command}\uFF08\u9519\u8BEF: ${errorMessage}\uFF09
|
|
206
|
+
VALIDATE_PARALLEL_CMD_ERROR: (command, errorMessage) => ` \u2717 ${command}\uFF08\u9519\u8BEF: ${errorMessage}\uFF09`,
|
|
207
|
+
/** patch apply 失败后询问用户是否执行 sync */
|
|
208
|
+
VALIDATE_CONFIRM_AUTO_SYNC: (branch) => `\u662F\u5426\u7ACB\u5373\u6267\u884C sync \u540C\u6B65\u4E3B\u5206\u652F\u5230 ${branch}\uFF1F`,
|
|
209
|
+
/** 自动 sync 开始提示 */
|
|
210
|
+
VALIDATE_AUTO_SYNC_START: (branch) => `\u6B63\u5728\u81EA\u52A8\u540C\u6B65\u4E3B\u5206\u652F\u5230 ${branch} ...`,
|
|
211
|
+
/** 自动 sync 存在冲突,无法重试 */
|
|
212
|
+
VALIDATE_AUTO_SYNC_CONFLICT: (worktreePath) => `\u540C\u6B65\u5B58\u5728\u51B2\u7A81\uFF0C\u8BF7\u8FDB\u5165\u76EE\u6807 worktree \u624B\u52A8\u89E3\u51B3\u51B2\u7A81\u540E\u91CD\u8BD5
|
|
213
|
+
cd ${worktreePath}`,
|
|
214
|
+
/** 用户拒绝自动 sync */
|
|
215
|
+
VALIDATE_AUTO_SYNC_DECLINED: (branch) => `\u8BF7\u624B\u52A8\u6267\u884C clawt sync -b ${branch} \u540C\u6B65\u4E3B\u5206\u652F\u540E\u91CD\u8BD5`
|
|
207
216
|
};
|
|
208
217
|
|
|
209
218
|
// src/constants/messages/sync.ts
|
package/docs/spec.md
CHANGED
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
| CLI 框架 | Commander.js |
|
|
47
47
|
| 日志库 | winston (按日期滚动文件) |
|
|
48
48
|
| 交互式 | enquirer (选项选择/确认对话) |
|
|
49
|
+
| 终端宽度 | string-width (ANSI 安全的字符宽度计算) |
|
|
49
50
|
| 测试 | Vitest + @vitest/coverage-v8 |
|
|
50
51
|
| 构建 | tsup / tsc |
|
|
51
52
|
| 分发 | pnpm 全局安装 (`pnpm add -g clawt`) |
|
|
@@ -338,8 +339,9 @@ clawt run -b <branchName>
|
|
|
338
339
|
4. 通过公共函数 `executeBatchTasks`(`src/utils/task-executor.ts`)启动批量任务执行,该函数负责进度面板渲染、SIGINT 中断处理、并发控制和汇总输出。对每个 worktree 并行启动 Claude Code CLI:
|
|
339
340
|
```bash
|
|
340
341
|
cd ~/.clawt/worktrees/<project>/<branchName>-<i>
|
|
341
|
-
claude -p "<tasks[i]>" --output-format json --permission-mode bypassPermissions
|
|
342
|
+
claude -p "<tasks[i]>" --output-format stream-json --verbose --permission-mode bypassPermissions
|
|
342
343
|
```
|
|
344
|
+
使用 `stream-json` 格式可实时获取 Claude Code 的流式事件(工具调用、文本输出、最终结果),用于在进度面板中显示每个任务的实时活动描述和结果预览。流式事件解析由 `src/utils/stream-parser.ts` 负责。
|
|
343
345
|
5. 进入**事件监听通知**阶段(见 [5.3](#53-任务完成通知机制))
|
|
344
346
|
6. **中断处理(Ctrl+C / SIGINT)**
|
|
345
347
|
- 监听 `SIGINT` 信号,用户按下 Ctrl+C 时触发
|
|
@@ -405,7 +407,7 @@ clawt run -b <branchName>
|
|
|
405
407
|
|
|
406
408
|
**机制说明:**
|
|
407
409
|
|
|
408
|
-
Claude Code CLI 以 `--output-format json`
|
|
410
|
+
Claude Code CLI 以 `--output-format stream-json --verbose` 运行时,stdout 会持续输出 JSON 行(每行一个事件),包括 `system`、`assistant`(含 `tool_use` 和 `text`)、`user`(含 `tool_result`)等类型。任务结束时输出 `type: "result"` 事件:
|
|
409
411
|
|
|
410
412
|
```json
|
|
411
413
|
{
|
|
@@ -423,12 +425,22 @@ Claude Code CLI 以 `--output-format json` 运行时,退出后会在 stdout
|
|
|
423
425
|
}
|
|
424
426
|
```
|
|
425
427
|
|
|
428
|
+
**流式事件解析(`src/utils/stream-parser.ts`):**
|
|
429
|
+
|
|
430
|
+
由于 stdout 的 `data` 事件可能在行中间切割,使用 `createLineBuffer()` 行缓冲器拼接完整行后,通过 `parseStreamLine()` 解析为 `StreamEvent` 对象,再由 `parseStreamEvent()` 提取活动信息(`ParsedActivity`):
|
|
431
|
+
|
|
432
|
+
- **`tool_use` 类型**:提取工具名和文件路径/命令参数,格式如 `Read index.ts`、`Bash ls -la`
|
|
433
|
+
- **`text` 类型**:提取文本片段,格式如 `思考中: 让我分析一下`
|
|
434
|
+
- **`result` 类型**:构造 `ClaudeCodeResult` 对象,提取耗时、费用、结果文本等
|
|
435
|
+
|
|
436
|
+
活动描述文本最大长度为 `ACTIVITY_TEXT_MAX_LENGTH`(30 字符),超出后截断并追加省略号。结果预览文本最大长度为 `RESULT_PREVIEW_MAX_LENGTH`(40 字符)。
|
|
437
|
+
|
|
426
438
|
**事件监听与通知流程:**
|
|
427
439
|
|
|
428
440
|
1. 为每个 Claude Code 子进程维护状态(运行中 / 已完成 / 已失败)
|
|
429
441
|
2. 监听每个子进程的 `close` 事件(基于 Node.js `ChildProcess` 的事件驱动机制)
|
|
430
|
-
3. 当某个子进程触发 `close`
|
|
431
|
-
4. 在主 worktree 的 clawt
|
|
442
|
+
3. 当某个子进程触发 `close` 事件时,解析流式输出中最后的 `result` 事件
|
|
443
|
+
4. 在主 worktree 的 clawt 终端实时输出通知。TTY 环境下使用进度面板,进度面板每个任务行第二列显示 worktree 路径(终端可点击跳转),运行中显示实时活动描述,完成/失败后显示结果预览:
|
|
432
444
|
|
|
433
445
|
```
|
|
434
446
|
✓ [完成] worktree: ~/.clawt/worktrees/main-project/feature-scheme-1
|
|
@@ -452,6 +464,39 @@ Claude Code CLI 以 `--output-format json` 运行时,退出后会在 stdout
|
|
|
452
464
|
════════════════════════════════════════
|
|
453
465
|
```
|
|
454
466
|
|
|
467
|
+
#### 进度面板渲染机制
|
|
468
|
+
|
|
469
|
+
进度面板由 `ProgressRenderer`(`src/utils/progress.ts`)负责渲染,渲染函数拆分到 `src/utils/progress-render.ts`。
|
|
470
|
+
|
|
471
|
+
**TTY 模式渲染策略(备选屏幕缓冲区):**
|
|
472
|
+
|
|
473
|
+
- **进入备选屏幕**:`start()` 时通过 `ALT_SCREEN_ENTER`(`\x1B[?1049h`)进入终端备选屏幕缓冲区,隔离进度面板与主屏幕内容
|
|
474
|
+
- **禁用行换行**:通过 `LINE_WRAP_DISABLE`(`\x1B[?7l`)防止超长行自动折行,配合按终端宽度截断保证每行只占一行
|
|
475
|
+
- **每帧渲染**:使用 `CLEAR_SCREEN` + `CURSOR_HOME` 清屏后完全重绘,无需计算 `CURSOR_UP` 回退量,不受终端 reflow 影响
|
|
476
|
+
- **防闪烁**:每帧渲染使用 Synchronized Output(`SYNC_OUTPUT_START` / `SYNC_OUTPUT_END`),终端缓冲全部输出后一次性刷新
|
|
477
|
+
- **行宽截断**:通过 `truncateToTerminalWidth()`(`src/utils/progress-render.ts`)将含 ANSI 转义码的字符串截断到终端可见列数,使用 `string-width` 库正确计算中文/emoji 宽度
|
|
478
|
+
- **终端 resize 响应**:监听 `process.stdout` 的 `resize` 事件,窗口宽度变化时立即触发重绘
|
|
479
|
+
- **退出时恢复**:`stop()` 时恢复行换行、显示光标、退出备选屏幕,然后在主屏幕上重新输出最终面板状态(备选屏幕内容不保留)
|
|
480
|
+
- **异常退出兜底**:注册 `process.on('exit')` 处理器,确保即使异常退出也能恢复终端状态
|
|
481
|
+
|
|
482
|
+
**任务行格式:**
|
|
483
|
+
|
|
484
|
+
```
|
|
485
|
+
[1/3] /path/to/worktree ⠹ 运行中 1m23s Read index.ts
|
|
486
|
+
[2/3] /path/to/worktree ✓ 完成 2m05s $0.08 任务已成功完成
|
|
487
|
+
[3/3] /path/to/worktree ● 排队中
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
- 第二列为 worktree 路径(`path.padEnd(maxPathWidth)` 对齐)
|
|
491
|
+
- 运行中状态:末尾显示实时活动描述文本(如工具名+文件名、思考中+文本片段)
|
|
492
|
+
- 完成/失败状态:末尾显示结果预览文本(从 `ClaudeCodeResult.result` 提取,最大 40 字符)
|
|
493
|
+
|
|
494
|
+
**非 TTY 降级模式:**
|
|
495
|
+
|
|
496
|
+
- 启动时输出 `[1/3] branch 启动 path`
|
|
497
|
+
- 完成时输出 `[1/3] branch ✓ 完成 duration cost detail`(`detail` 优先使用结果预览,无则回退到路径)
|
|
498
|
+
- 失败时输出 `[1/3] branch ✗ 失败 duration detail`
|
|
499
|
+
|
|
455
500
|
---
|
|
456
501
|
|
|
457
502
|
### 5.4 在主 Worktree 验证其他分支
|
|
@@ -576,7 +621,26 @@ git restore --staged .
|
|
|
576
621
|
```
|
|
577
622
|
|
|
578
623
|
> 此步骤结束后,目标 worktree 的代码保持原样,主 worktree 工作目录包含目标分支的全量变更。
|
|
579
|
-
> 如果 patch apply
|
|
624
|
+
> 如果 patch apply 失败(目标分支与主分支差异过大),`migrateChangesViaPatch` 返回 `{ success: false }`,进入自动 sync 交互流程(见下文 [patch apply 失败后的自动 sync 流程](#patch-apply-失败后的自动-sync-流程))。
|
|
625
|
+
|
|
626
|
+
##### patch apply 失败后的自动 sync 流程
|
|
627
|
+
|
|
628
|
+
当 patch apply 失败时,validate 不再直接退出,而是通过 `handlePatchApplyFailure()` 函数进入交互流程:
|
|
629
|
+
|
|
630
|
+
1. **询问用户**:提示 `是否立即执行 sync 同步主分支到 <branchName>?`
|
|
631
|
+
2. **用户拒绝** → 输出提示 `请手动执行 clawt sync -b <branchName> 同步主分支后重试`,退出
|
|
632
|
+
3. **用户确认** → 调用 `executeSyncForBranch(targetWorktreePath, branchName)` 自动执行 sync
|
|
633
|
+
- **sync 成功** → validate 流程结束(用户需重新执行 validate)
|
|
634
|
+
- **sync 存在冲突** → 输出提示 `同步存在冲突,请进入目标 worktree 手动解决冲突后重试`,退出
|
|
635
|
+
|
|
636
|
+
> `executeSyncForBranch` 为 sync 命令抽取的核心操作函数(见 [5.12](#512-将主分支代码同步到目标-worktree)),供 validate 等命令复用。
|
|
637
|
+
|
|
638
|
+
**实现要点:**
|
|
639
|
+
|
|
640
|
+
- `migrateChangesViaPatch()` 返回类型从 `void` 改为 `{ success: boolean }`,patch apply 失败时返回 `{ success: false }` 而非抛出异常
|
|
641
|
+
- `handleFirstValidate()` 和 `handleIncrementalValidate()` 从同步函数改为 `async` 函数,以支持交互式确认
|
|
642
|
+
- `handlePatchApplyFailure()` 为新增的异步函数(`src/commands/validate.ts`),负责 patch 失败后的交互逻辑
|
|
643
|
+
- 消息常量:`MESSAGES.VALIDATE_CONFIRM_AUTO_SYNC`、`MESSAGES.VALIDATE_AUTO_SYNC_START`、`MESSAGES.VALIDATE_AUTO_SYNC_CONFLICT`、`MESSAGES.VALIDATE_AUTO_SYNC_DECLINED`(`src/constants/messages/validate.ts`)
|
|
580
644
|
|
|
581
645
|
##### 步骤 4:保存快照为 git tree 对象
|
|
582
646
|
|
|
@@ -715,7 +779,7 @@ clawt validate -b feature-scheme-1 -r "pnpm test & pnpm build"
|
|
|
715
779
|
|
|
716
780
|
##### 步骤 3:从目标分支获取最新全量变更
|
|
717
781
|
|
|
718
|
-
通过 patch 方式从目标分支获取最新全量变更(流程同首次 validate 的步骤 3
|
|
782
|
+
通过 patch 方式从目标分支获取最新全量变更(流程同首次 validate 的步骤 3)。如果 patch apply 失败,同样进入自动 sync 交互流程(见首次 validate 的 [patch apply 失败后的自动 sync 流程](#patch-apply-失败后的自动-sync-流程)),validate 流程提前结束。
|
|
719
783
|
|
|
720
784
|
##### 步骤 4:保存最新快照为 git tree 对象
|
|
721
785
|
|
|
@@ -1356,16 +1420,36 @@ clawt sync
|
|
|
1356
1420
|
- 唯一匹配 → 直接使用
|
|
1357
1421
|
- 多个匹配 → 通过交互式列表让用户从匹配结果中选择
|
|
1358
1422
|
3. **无匹配** → 报错退出,并列出所有可用分支名
|
|
1359
|
-
3.
|
|
1360
|
-
|
|
1423
|
+
3. 调用 `executeSyncForBranch(targetWorktreePath, branch)` 执行核心同步逻辑
|
|
1424
|
+
|
|
1425
|
+
#### `executeSyncForBranch` — sync 核心操作函数
|
|
1426
|
+
|
|
1427
|
+
`executeSyncForBranch(targetWorktreePath: string, branch: string): SyncResult` 是从 `handleSync` 中抽取的核心同步逻辑,不包含 worktree 解析交互,供 validate 等命令复用。
|
|
1428
|
+
|
|
1429
|
+
**接口定义:**
|
|
1430
|
+
|
|
1431
|
+
```typescript
|
|
1432
|
+
/** sync 核心操作的执行结果 */
|
|
1433
|
+
export interface SyncResult {
|
|
1434
|
+
/** 是否同步成功 */
|
|
1435
|
+
success: boolean;
|
|
1436
|
+
/** 是否存在合并冲突 */
|
|
1437
|
+
hasConflict: boolean;
|
|
1438
|
+
}
|
|
1439
|
+
```
|
|
1440
|
+
|
|
1441
|
+
**执行流程:**
|
|
1442
|
+
|
|
1443
|
+
1. **获取主分支名**:通过 `git rev-parse --abbrev-ref HEAD` 获取主 worktree 当前分支名(不硬编码 main/master)
|
|
1444
|
+
2. **自动保存未提交变更**:检查目标 worktree 是否有未提交修改
|
|
1361
1445
|
- 有修改 → 自动执行 `git add . && git commit -m "<AUTO_SAVE_COMMIT_MESSAGE>"` 保存变更(commit message 由常量 `AUTO_SAVE_COMMIT_MESSAGE` 定义,值为 `chore: auto-save before sync`,同时用于 merge 命令的 squash 检测)
|
|
1362
1446
|
- 无修改 → 跳过
|
|
1363
|
-
|
|
1447
|
+
3. **在目标 worktree 中合并主分支**:
|
|
1364
1448
|
```bash
|
|
1365
1449
|
cd ~/.clawt/worktrees/<project>/<branchName>
|
|
1366
1450
|
git merge <mainBranch>
|
|
1367
1451
|
```
|
|
1368
|
-
|
|
1452
|
+
4. **冲突处理**:
|
|
1369
1453
|
- **有冲突** → 输出警告,提示用户进入目标 worktree 手动解决:
|
|
1370
1454
|
```
|
|
1371
1455
|
合并存在冲突,请进入目标 worktree 手动解决:
|
|
@@ -1373,9 +1457,10 @@ clawt sync
|
|
|
1373
1457
|
解决冲突后执行 git add . && git merge --continue
|
|
1374
1458
|
clawt validate -b <branch> 验证变更
|
|
1375
1459
|
```
|
|
1460
|
+
- 返回 `{ success: false, hasConflict: true }`
|
|
1376
1461
|
- **无冲突** → 继续
|
|
1377
|
-
|
|
1378
|
-
|
|
1462
|
+
5. **清除 validate 快照**:合并成功后,如果该分支存在 validate 快照(`.tree` 和 `.head` 文件),自动删除(代码基础已变化,旧快照无效)
|
|
1463
|
+
6. **输出成功提示**并返回 `{ success: true, hasConflict: false }`:
|
|
1379
1464
|
```
|
|
1380
1465
|
✓ 已将 <mainBranch> 的最新代码同步到 <branchName>
|
|
1381
1466
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawt",
|
|
3
|
-
"version": "2.16.
|
|
3
|
+
"version": "2.16.5",
|
|
4
4
|
"description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"chalk": "^5.4.1",
|
|
20
20
|
"commander": "^13.1.0",
|
|
21
21
|
"enquirer": "^2.4.1",
|
|
22
|
+
"string-width": "^8.2.0",
|
|
22
23
|
"winston": "^3.17.0",
|
|
23
24
|
"winston-daily-rotate-file": "^5.0.0"
|
|
24
25
|
},
|
package/src/commands/sync.ts
CHANGED
|
@@ -77,21 +77,22 @@ function mergeMainBranch(worktreePath: string, mainBranch: string): boolean {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/** sync 核心操作的执行结果 */
|
|
81
|
+
export interface SyncResult {
|
|
82
|
+
/** 是否同步成功 */
|
|
83
|
+
success: boolean;
|
|
84
|
+
/** 是否存在合并冲突 */
|
|
85
|
+
hasConflict: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
80
88
|
/**
|
|
81
|
-
* 执行 sync
|
|
82
|
-
*
|
|
83
|
-
* @param {
|
|
89
|
+
* 执行 sync 核心操作(检查未提交→自动保存→merge 主分支→清除快照)
|
|
90
|
+
* 不包含 worktree 解析交互,供 validate 等命令复用
|
|
91
|
+
* @param {string} targetWorktreePath - 目标 worktree 路径
|
|
92
|
+
* @param {string} branch - 分支名
|
|
93
|
+
* @returns {SyncResult} sync 执行结果
|
|
84
94
|
*/
|
|
85
|
-
|
|
86
|
-
validateMainWorktree();
|
|
87
|
-
|
|
88
|
-
logger.info(`sync 命令执行,分支: ${options.branch ?? '(未指定)'}`);
|
|
89
|
-
|
|
90
|
-
// 解析目标 worktree(精确匹配 / 模糊匹配 / 交互选择)
|
|
91
|
-
const worktrees = getProjectWorktrees();
|
|
92
|
-
const worktree = await resolveTargetWorktree(worktrees, SYNC_RESOLVE_MESSAGES, options.branch);
|
|
93
|
-
const { path: targetWorktreePath, branch } = worktree;
|
|
94
|
-
|
|
95
|
+
export function executeSyncForBranch(targetWorktreePath: string, branch: string): SyncResult {
|
|
95
96
|
// 获取主分支名(不硬编码 main/master)
|
|
96
97
|
const mainWorktreePath = getGitTopLevel();
|
|
97
98
|
const mainBranch = getCurrentBranch(mainWorktreePath);
|
|
@@ -107,7 +108,7 @@ async function handleSync(options: SyncOptions): Promise<void> {
|
|
|
107
108
|
|
|
108
109
|
if (hasConflict) {
|
|
109
110
|
printWarning(MESSAGES.SYNC_CONFLICT(targetWorktreePath));
|
|
110
|
-
return;
|
|
111
|
+
return { success: false, hasConflict: true };
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
// 合并成功后清除该分支的 validate 快照(代码基础已变化,旧快照无效)
|
|
@@ -118,4 +119,23 @@ async function handleSync(options: SyncOptions): Promise<void> {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
printSuccess(MESSAGES.SYNC_SUCCESS(branch, mainBranch));
|
|
122
|
+
return { success: true, hasConflict: false };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 执行 sync 命令的核心逻辑
|
|
127
|
+
* 将主分支最新代码同步到目标 worktree
|
|
128
|
+
* @param {SyncOptions} options - 命令选项
|
|
129
|
+
*/
|
|
130
|
+
async function handleSync(options: SyncOptions): Promise<void> {
|
|
131
|
+
validateMainWorktree();
|
|
132
|
+
|
|
133
|
+
logger.info(`sync 命令执行,分支: ${options.branch ?? '(未指定)'}`);
|
|
134
|
+
|
|
135
|
+
// 解析目标 worktree(精确匹配 / 模糊匹配 / 交互选择)
|
|
136
|
+
const worktrees = getProjectWorktrees();
|
|
137
|
+
const worktree = await resolveTargetWorktree(worktrees, SYNC_RESOLVE_MESSAGES, options.branch);
|
|
138
|
+
const { path: targetWorktreePath, branch } = worktree;
|
|
139
|
+
|
|
140
|
+
executeSyncForBranch(targetWorktreePath, branch);
|
|
121
141
|
}
|
package/src/commands/validate.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { logger } from '../logger/index.js';
|
|
|
4
4
|
import { ClawtError } from '../errors/index.js';
|
|
5
5
|
import { MESSAGES } from '../constants/index.js';
|
|
6
6
|
import type { ValidateOptions } from '../types/index.js';
|
|
7
|
+
import { executeSyncForBranch } from './sync.js';
|
|
7
8
|
import {
|
|
8
9
|
validateMainWorktree,
|
|
9
10
|
getProjectName,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
writeSnapshot,
|
|
34
35
|
removeSnapshot,
|
|
35
36
|
confirmDestructiveAction,
|
|
37
|
+
confirmAction,
|
|
36
38
|
printSuccess,
|
|
37
39
|
printError,
|
|
38
40
|
printWarning,
|
|
@@ -121,8 +123,9 @@ async function handleDirtyMainWorktree(mainWorktreePath: string): Promise<void>
|
|
|
121
123
|
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
122
124
|
* @param {string} branchName - 分支名
|
|
123
125
|
* @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
|
|
126
|
+
* @returns {{ success: boolean }} patch 迁移结果
|
|
124
127
|
*/
|
|
125
|
-
function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: string, branchName: string, hasUncommitted: boolean):
|
|
128
|
+
function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: string, branchName: string, hasUncommitted: boolean): { success: boolean } {
|
|
126
129
|
let didTempCommit = false;
|
|
127
130
|
|
|
128
131
|
try {
|
|
@@ -143,9 +146,11 @@ function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: st
|
|
|
143
146
|
} catch (error) {
|
|
144
147
|
logger.warn(`patch apply 失败: ${error}`);
|
|
145
148
|
printWarning(MESSAGES.VALIDATE_PATCH_APPLY_FAILED(branchName));
|
|
146
|
-
|
|
149
|
+
return { success: false };
|
|
147
150
|
}
|
|
148
151
|
}
|
|
152
|
+
|
|
153
|
+
return { success: true };
|
|
149
154
|
} finally {
|
|
150
155
|
// 确保临时 commit 一定会被撤销,恢复目标 worktree 原状
|
|
151
156
|
// 每个操作独立 try-catch,避免前一个失败导致后续操作不执行
|
|
@@ -164,6 +169,31 @@ function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: st
|
|
|
164
169
|
}
|
|
165
170
|
}
|
|
166
171
|
|
|
172
|
+
/**
|
|
173
|
+
* patch apply 失败后的交互处理:询问用户是否自动执行 sync
|
|
174
|
+
* @param {string} targetWorktreePath - 目标 worktree 路径
|
|
175
|
+
* @param {string} branchName - 分支名
|
|
176
|
+
*/
|
|
177
|
+
async function handlePatchApplyFailure(targetWorktreePath: string, branchName: string): Promise<void> {
|
|
178
|
+
// 询问用户是否自动执行 sync
|
|
179
|
+
const confirmed = await confirmAction(MESSAGES.VALIDATE_CONFIRM_AUTO_SYNC(branchName));
|
|
180
|
+
|
|
181
|
+
if (!confirmed) {
|
|
182
|
+
// 用户拒绝自动 sync
|
|
183
|
+
printWarning(MESSAGES.VALIDATE_AUTO_SYNC_DECLINED(branchName));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 用户确认,执行 sync
|
|
188
|
+
printInfo(MESSAGES.VALIDATE_AUTO_SYNC_START(branchName));
|
|
189
|
+
const syncResult = executeSyncForBranch(targetWorktreePath, branchName);
|
|
190
|
+
|
|
191
|
+
if (syncResult.hasConflict) {
|
|
192
|
+
// sync 存在冲突,提示用户手动解决
|
|
193
|
+
printWarning(MESSAGES.VALIDATE_AUTO_SYNC_CONFLICT(targetWorktreePath));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
167
197
|
/**
|
|
168
198
|
* 保存当前主 worktree 工作目录变更为 git tree 对象快照
|
|
169
199
|
* 操作序列:git add . → git write-tree → git restore --staged .
|
|
@@ -231,9 +261,15 @@ async function handleValidateClean(options: ValidateOptions): Promise<void> {
|
|
|
231
261
|
* @param {string} branchName - 分支名
|
|
232
262
|
* @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
|
|
233
263
|
*/
|
|
234
|
-
function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): void {
|
|
264
|
+
async function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): Promise<void> {
|
|
235
265
|
// 通过 patch 迁移目标分支全量变更到主 worktree
|
|
236
|
-
migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
266
|
+
const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
267
|
+
|
|
268
|
+
if (!result.success) {
|
|
269
|
+
// patch 失败,询问用户是否自动 sync
|
|
270
|
+
await handlePatchApplyFailure(targetWorktreePath, branchName);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
237
273
|
|
|
238
274
|
// 保存快照为 git tree 对象
|
|
239
275
|
saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
|
|
@@ -250,7 +286,7 @@ function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: strin
|
|
|
250
286
|
* @param {string} branchName - 分支名
|
|
251
287
|
* @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
|
|
252
288
|
*/
|
|
253
|
-
function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): void {
|
|
289
|
+
async function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): Promise<void> {
|
|
254
290
|
// 步骤 1:读取旧快照(tree hash + 当时的 HEAD commit hash)
|
|
255
291
|
const { treeHash: oldTreeHash, headCommitHash: oldHeadCommitHash } = readSnapshot(projectName, branchName);
|
|
256
292
|
|
|
@@ -262,7 +298,13 @@ function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath:
|
|
|
262
298
|
}
|
|
263
299
|
|
|
264
300
|
// 步骤 3:通过 patch 从目标分支获取最新全量变更
|
|
265
|
-
migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
301
|
+
const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
302
|
+
|
|
303
|
+
if (!result.success) {
|
|
304
|
+
// patch 失败,询问用户是否自动 sync
|
|
305
|
+
await handlePatchApplyFailure(targetWorktreePath, branchName);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
266
308
|
|
|
267
309
|
// 步骤 4:保存最新快照为 git tree 对象(同时记录当前 HEAD)
|
|
268
310
|
saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
|
|
@@ -441,14 +483,14 @@ async function handleValidate(options: ValidateOptions): Promise<void> {
|
|
|
441
483
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
442
484
|
await handleDirtyMainWorktree(mainWorktreePath);
|
|
443
485
|
}
|
|
444
|
-
handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
486
|
+
await handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
445
487
|
} else {
|
|
446
488
|
// 首次模式:先确保主 worktree 干净
|
|
447
489
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
448
490
|
await handleDirtyMainWorktree(mainWorktreePath);
|
|
449
491
|
}
|
|
450
492
|
|
|
451
|
-
handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
493
|
+
await handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
452
494
|
}
|
|
453
495
|
|
|
454
496
|
// validate 成功后执行用户指定的命令
|
package/src/constants/index.ts
CHANGED
|
@@ -10,11 +10,21 @@ export { DEBUG_LOG_PREFIX, DEBUG_TIMESTAMP_FORMAT } from './logger.js';
|
|
|
10
10
|
export {
|
|
11
11
|
SPINNER_FRAMES,
|
|
12
12
|
SPINNER_INTERVAL_MS,
|
|
13
|
-
CURSOR_UP,
|
|
14
|
-
CLEAR_LINE,
|
|
15
13
|
CURSOR_HIDE,
|
|
16
14
|
CURSOR_SHOW,
|
|
17
15
|
TASK_STATUS_ICONS,
|
|
18
16
|
TASK_STATUS_LABELS,
|
|
17
|
+
ACTIVITY_TEXT_MAX_LENGTH,
|
|
18
|
+
TEXT_ACTIVITY_PREFIX,
|
|
19
|
+
RESULT_PREVIEW_MAX_LENGTH,
|
|
20
|
+
DEFAULT_TERMINAL_COLUMNS,
|
|
21
|
+
LINE_WRAP_DISABLE,
|
|
22
|
+
LINE_WRAP_ENABLE,
|
|
23
|
+
SYNC_OUTPUT_START,
|
|
24
|
+
SYNC_OUTPUT_END,
|
|
25
|
+
ALT_SCREEN_ENTER,
|
|
26
|
+
ALT_SCREEN_LEAVE,
|
|
27
|
+
CLEAR_SCREEN,
|
|
28
|
+
CURSOR_HOME,
|
|
19
29
|
} from './progress.js';
|
|
20
30
|
export { SELECT_ALL_NAME, SELECT_ALL_LABEL } from './prompt.js';
|
|
@@ -17,11 +17,11 @@ export const RUN_MESSAGES = {
|
|
|
17
17
|
PROGRESS_TASK_STARTED: (index: number, total: number, branch: string, path: string) =>
|
|
18
18
|
`[${index}/${total}] ${branch} 启动 ${path}`,
|
|
19
19
|
/** 非 TTY 环境降级输出:任务完成 */
|
|
20
|
-
PROGRESS_TASK_DONE: (index: number, total: number, branch: string, duration: string, cost: string,
|
|
21
|
-
`[${index}/${total}] ${branch} ✓ 完成 ${duration} ${cost} ${
|
|
20
|
+
PROGRESS_TASK_DONE: (index: number, total: number, branch: string, duration: string, cost: string, detail: string) =>
|
|
21
|
+
`[${index}/${total}] ${branch} ✓ 完成 ${duration} ${cost} ${detail}`,
|
|
22
22
|
/** 非 TTY 环境降级输出:任务失败 */
|
|
23
|
-
PROGRESS_TASK_FAILED: (index: number, total: number, branch: string, duration: string,
|
|
24
|
-
`[${index}/${total}] ${branch} ✗ 失败 ${duration} ${
|
|
23
|
+
PROGRESS_TASK_FAILED: (index: number, total: number, branch: string, duration: string, detail: string) =>
|
|
24
|
+
`[${index}/${total}] ${branch} ✗ 失败 ${duration} ${detail}`,
|
|
25
25
|
/** 并发限制提示 */
|
|
26
26
|
CONCURRENCY_INFO: (concurrency: number, total: number) =>
|
|
27
27
|
`并发限制: ${concurrency},共 ${total} 个任务`,
|
|
@@ -53,4 +53,16 @@ export const VALIDATE_MESSAGES = {
|
|
|
53
53
|
/** 并行执行中单个命令启动失败 */
|
|
54
54
|
VALIDATE_PARALLEL_CMD_ERROR: (command: string, errorMessage: string) =>
|
|
55
55
|
` ✗ ${command}(错误: ${errorMessage})`,
|
|
56
|
+
/** patch apply 失败后询问用户是否执行 sync */
|
|
57
|
+
VALIDATE_CONFIRM_AUTO_SYNC: (branch: string) =>
|
|
58
|
+
`是否立即执行 sync 同步主分支到 ${branch}?`,
|
|
59
|
+
/** 自动 sync 开始提示 */
|
|
60
|
+
VALIDATE_AUTO_SYNC_START: (branch: string) =>
|
|
61
|
+
`正在自动同步主分支到 ${branch} ...`,
|
|
62
|
+
/** 自动 sync 存在冲突,无法重试 */
|
|
63
|
+
VALIDATE_AUTO_SYNC_CONFLICT: (worktreePath: string) =>
|
|
64
|
+
`同步存在冲突,请进入目标 worktree 手动解决冲突后重试\n cd ${worktreePath}`,
|
|
65
|
+
/** 用户拒绝自动 sync */
|
|
66
|
+
VALIDATE_AUTO_SYNC_DECLINED: (branch: string) =>
|
|
67
|
+
`请手动执行 clawt sync -b ${branch} 同步主分支后重试`,
|
|
56
68
|
} as const;
|
|
@@ -4,12 +4,6 @@ export const SPINNER_FRAMES: readonly string[] = ['⠋', '⠙', '⠹', '⠸', '
|
|
|
4
4
|
/** spinner 刷新间隔(毫秒) */
|
|
5
5
|
export const SPINNER_INTERVAL_MS = 100;
|
|
6
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
7
|
/** ANSI 转义:隐藏光标 */
|
|
14
8
|
export const CURSOR_HIDE = '\x1B[?25l';
|
|
15
9
|
|
|
@@ -37,3 +31,39 @@ export const TASK_STATUS_LABELS = {
|
|
|
37
31
|
/** 失败 */
|
|
38
32
|
FAILED: '失败',
|
|
39
33
|
} as const;
|
|
34
|
+
|
|
35
|
+
/** 活动描述文本的最大字符数 */
|
|
36
|
+
export const ACTIVITY_TEXT_MAX_LENGTH = 30;
|
|
37
|
+
|
|
38
|
+
/** 文本类型活动的前缀 */
|
|
39
|
+
export const TEXT_ACTIVITY_PREFIX = '思考中';
|
|
40
|
+
|
|
41
|
+
/** 结果预览文本的最大字符数 */
|
|
42
|
+
export const RESULT_PREVIEW_MAX_LENGTH = 40;
|
|
43
|
+
|
|
44
|
+
/** 终端宽度的默认值(无法获取时的回退值) */
|
|
45
|
+
export const DEFAULT_TERMINAL_COLUMNS = 80;
|
|
46
|
+
|
|
47
|
+
/** ANSI 转义:禁用终端自动行换行(超出宽度的内容被截断而非折行) */
|
|
48
|
+
export const LINE_WRAP_DISABLE = '\x1B[?7l';
|
|
49
|
+
|
|
50
|
+
/** ANSI 转义:恢复终端自动行换行 */
|
|
51
|
+
export const LINE_WRAP_ENABLE = '\x1B[?7h';
|
|
52
|
+
|
|
53
|
+
/** ANSI 转义:开启同步输出模式(终端缓冲所有输出直到关闭,防止渲染闪烁) */
|
|
54
|
+
export const SYNC_OUTPUT_START = '\x1B[?2026h';
|
|
55
|
+
|
|
56
|
+
/** ANSI 转义:关闭同步输出模式(终端一次性刷新缓冲区内容) */
|
|
57
|
+
export const SYNC_OUTPUT_END = '\x1B[?2026l';
|
|
58
|
+
|
|
59
|
+
/** ANSI 转义:进入备选屏幕缓冲区(同时保存光标位置) */
|
|
60
|
+
export const ALT_SCREEN_ENTER = '\x1B[?1049h';
|
|
61
|
+
|
|
62
|
+
/** ANSI 转义:离开备选屏幕缓冲区(同时恢复光标位置) */
|
|
63
|
+
export const ALT_SCREEN_LEAVE = '\x1B[?1049l';
|
|
64
|
+
|
|
65
|
+
/** ANSI 转义:清除整个屏幕 */
|
|
66
|
+
export const CLEAR_SCREEN = '\x1B[2J';
|
|
67
|
+
|
|
68
|
+
/** ANSI 转义:光标移到屏幕左上角 (1,1) */
|
|
69
|
+
export const CURSOR_HOME = '\x1B[H';
|
package/src/utils/index.ts
CHANGED
|
@@ -61,6 +61,8 @@ export type { WorktreeResolveMessages, WorktreeMultiResolveMessages } from './wo
|
|
|
61
61
|
export { ProgressRenderer } from './progress.js';
|
|
62
62
|
export { parseTaskFile, loadTaskFile, parseTasksFromOptions } from './task-file.js';
|
|
63
63
|
export { executeBatchTasks } from './task-executor.js';
|
|
64
|
+
export { createLineBuffer, parseStreamLine, parseStreamEvent, formatActivityText, truncateText } from './stream-parser.js';
|
|
65
|
+
export type { ParsedActivity, StreamEvent, LineBuffer } from './stream-parser.js';
|
|
64
66
|
export { detectTerminalApp, openCommandInNewTerminalTab } from './terminal.js';
|
|
65
67
|
export { truncateTaskDesc, printDryRunPreview } from './dry-run.js';
|
|
66
68
|
export { applyAliases } from './alias.js';
|