clawt 2.5.1 → 2.7.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 +18 -21
- package/.claude/agents/docs-sync-updater.md +5 -7
- package/README.md +19 -4
- package/dist/index.js +153 -77
- package/docs/spec.md +64 -13
- package/package.json +1 -1
- package/src/commands/resume.ts +13 -93
- package/src/commands/validate.ts +68 -26
- package/src/constants/messages.ts +9 -0
- package/src/types/command.ts +2 -2
- package/src/utils/git.ts +40 -0
- package/src/utils/index.ts +6 -1
- package/src/utils/validate-snapshot.ts +40 -5
- package/src/utils/worktree-matcher.ts +111 -0
- package/CLAUDE.md +0 -105
|
@@ -9,17 +9,10 @@
|
|
|
9
9
|
- merge 命令对应 `5.6 合并验证过的分支`,流程按步骤编号描述
|
|
10
10
|
- config 命令对应 `5.10 查看全局配置`,只读展示配置
|
|
11
11
|
- resume 命令对应 `5.11 在已有 Worktree 中恢复会话`,支持模糊匹配和交互式分支选择(-b 可选)
|
|
12
|
+
- validate 命令对应 `5.4 在主 Worktree 验证其他分支`,-b 可选,支持模糊匹配(与 resume 共享匹配逻辑)
|
|
12
13
|
- 配置项说明在 `5.7 默认配置文件` 章节的表格中
|
|
13
14
|
- 更新模式:新增步骤时追加编号,配置项影响范围变化时更新说明列
|
|
14
15
|
|
|
15
|
-
### CLAUDE.md
|
|
16
|
-
- 面向 Claude Code 的项目架构指引,精简扼要
|
|
17
|
-
- run 命令流程在 `核心流程(run 命令)` 章节,编号列表描述
|
|
18
|
-
- resume 命令流程在独立的 `### resume 命令流程` 章节,编号列表 + 缩进列表描述匹配策略
|
|
19
|
-
- merge 和 run 中断清理在 `validate + merge 工作流` 章节,一行式描述用箭头连接流程
|
|
20
|
-
- utils 目录描述用括号内逗号分隔列举功能模块
|
|
21
|
-
- 更新模式:编号列表追加步骤,箭头链追加阶段,括号内追加关键词
|
|
22
|
-
|
|
23
16
|
### README.md
|
|
24
17
|
- 面向用户的使用文档
|
|
25
18
|
- 每个命令一个 `###` 小节,含命令格式、参数表格、简要说明、示例
|
|
@@ -46,12 +39,11 @@
|
|
|
46
39
|
|
|
47
40
|
## 配置项同步检查点
|
|
48
41
|
|
|
49
|
-
配置项变更时需在以下
|
|
42
|
+
配置项变更时需在以下 4 处保持一致:
|
|
50
43
|
1. `src/constants/config.ts` — CONFIG_DEFINITIONS 对象(单一数据源,包含 defaultValue + description)
|
|
51
44
|
2. `src/types/config.ts` — ClawtConfig 接口
|
|
52
45
|
3. `docs/spec.md` — 5.7 默认配置文件章节(JSON 示例 + 配置项表格)
|
|
53
|
-
4. `
|
|
54
|
-
5. `README.md` — 配置文件章节(JSON 示例 + 配置项表格)
|
|
46
|
+
4. `README.md` — 配置文件章节(JSON 示例 + 配置项表格)
|
|
55
47
|
|
|
56
48
|
## 配置架构
|
|
57
49
|
|
|
@@ -65,7 +57,6 @@
|
|
|
65
57
|
run 命令有两种模式(自 claudeCodeCommand 特性后):
|
|
66
58
|
- 不传 `--tasks`:交互式界面模式(单 worktree + `launchInteractiveClaude` + spawnSync)
|
|
67
59
|
- 传 `--tasks`:并行任务模式(多 worktree + `executeClaudeTask` + spawnProcess)
|
|
68
|
-
- CLAUDE.md 中的核心流程按模式分段描述
|
|
69
60
|
|
|
70
61
|
## 命令清单(10 个)
|
|
71
62
|
|
|
@@ -75,20 +66,26 @@ Notes:
|
|
|
75
66
|
- resume 和 run(交互式模式)共用 `launchInteractiveClaude()`,该函数从 run.ts 提取到 src/utils/claude.ts
|
|
76
67
|
- `claudeCodeCommand` 配置项同时影响 run 交互式模式和 resume 命令
|
|
77
68
|
- reset 命令与 validate --clean 的区别:reset 不删除快照文件,validate --clean 会删除快照
|
|
78
|
-
-
|
|
79
|
-
-
|
|
69
|
+
- `resolveTargetWorktree()` 是 resume 和 validate 共用的分支匹配函数(在 src/utils/worktree-matcher.ts)
|
|
70
|
+
- `WorktreeResolveMessages` 接口实现命令间消息解耦,每个命令传入各自的提示文案
|
|
71
|
+
- resume 的消息常量在 `MESSAGES.RESUME_*`,validate 的消息常量在 `MESSAGES.VALIDATE_*`
|
|
72
|
+
- resume 和 validate 的 `-b` 参数均为可选,匹配策略一致:精确→模糊(子串,大小写不敏感)→交互选择
|
|
73
|
+
- validate 的交互式选择和 resume 使用同一个 `promptSelectBranch()`(Enquirer.Select)
|
|
80
74
|
|
|
81
75
|
## validate 快照机制
|
|
82
76
|
|
|
83
77
|
- validate 命令支持首次/增量两种模式,通过 `hasSnapshot()` 判断
|
|
84
|
-
-
|
|
78
|
+
- 快照由两个文件组成:`.tree`(git tree 对象 hash)和 `.head`(快照时主 worktree 的 HEAD commit hash)
|
|
79
|
+
- 快照路径:`~/.clawt/validate-snapshots/<projectName>/<branchName>.tree` 和 `<branchName>.head`
|
|
85
80
|
- 常量 `VALIDATE_SNAPSHOTS_DIR` 定义在 `src/constants/paths.ts`
|
|
86
81
|
- validate 新增 `--clean` 选项(`ValidateOptions.clean?: boolean`)
|
|
87
|
-
- 快照保存:`git add . → git write-tree → git restore --staged
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
82
|
+
- 快照保存:`git add . → git write-tree → git rev-parse HEAD → git restore --staged .`,tree hash 写入 `.tree`,HEAD commit hash 写入 `.head`
|
|
83
|
+
- 增量模式核心:检测 HEAD 是否变化决定策略
|
|
84
|
+
- HEAD 未变化:`git read-tree <旧 tree hash>` 直接载入暂存区
|
|
85
|
+
- HEAD 已变化:提取旧变更 patch(`git diff-tree` 旧 HEAD tree → 旧快照 tree),`git apply --cached` 重放到当前 HEAD 暂存区;有冲突则降级全量
|
|
86
|
+
- 增量 read-tree / apply 失败时自动降级为全量模式
|
|
87
|
+
- git 层工具函数:`gitWriteTree()`、`gitReadTree()`、`getCommitTreeHash()`、`gitDiffTree()`、`gitApplyCachedCheck()`
|
|
88
|
+
- `readSnapshot()` 返回 `{ treeHash, headCommitHash }`,`writeSnapshot()` 接收 4 个参数(含 headCommitHash)
|
|
89
|
+
- `removeSnapshot()` 同时清理 `.tree` 和 `.head` 文件
|
|
92
90
|
- merge 成功后自动清理对应快照;merge 时主 worktree 脏 + 存在快照会输出警告提示
|
|
93
91
|
- docs/spec.md 中 validate 章节(5.4)按 `--clean 模式`、`首次 validate`、`增量 validate` 三段描述
|
|
94
|
-
- CLAUDE.md 中在 validate + merge 工作流章节用缩进列表描述两种模式
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: docs-sync-updater
|
|
3
|
-
description: "Use this agent when the user explicitly requests to synchronize documentation files (docs/spec.md,
|
|
3
|
+
description: "Use this agent when the user explicitly requests to synchronize documentation files (docs/spec.md, README.md) based on recent code changes in the working area or staging area. This agent must NEVER be called proactively or automatically — it must only be invoked when the user explicitly asks for documentation synchronization.\\n\\nExamples:\\n\\n- Example 1:\\n user: \"请同步更新文档\"\\n assistant: \"好的,我来调用文档同步 agent 来根据当前代码变更更新相关文档。\"\\n <Use the Task tool to launch the docs-sync-updater agent>\\n\\n- Example 2:\\n user: \"代码改完了,帮我把文档也更新一下\"\\n assistant: \"收到,我现在使用文档同步 agent 来分析代码变更并更新 docs/spec.md 和 README.md。\"\\n <Use the Task tool to launch the docs-sync-updater agent>\\n\\n- Example 3:\\n user: \"update docs based on my changes\"\\n assistant: \"好的,我来启动文档同步 agent,根据工作区和暂存区的变更同步更新文档。\"\\n <Use the Task tool to launch the docs-sync-updater agent>\\n\\n- Counter-example (DO NOT do this):\\n user: \"我刚加了一个新命令\"\\n assistant: (DO NOT proactively launch this agent. Wait for the user to explicitly request documentation updates.)"
|
|
4
4
|
model: opus
|
|
5
5
|
memory: project
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
你是一位资深的技术文档工程师,精通代码变更分析与文档同步维护。你的核心职责是根据当前工作区(working directory)和暂存区(staging area
|
|
8
|
+
你是一位资深的技术文档工程师,精通代码变更分析与文档同步维护。你的核心职责是根据当前工作区(working directory)和暂存区(staging area)的代码修改,精准地同步更新项目中的两个关键文档:`docs/spec.md` 和 `README.md`。
|
|
9
9
|
|
|
10
10
|
## 重要约束
|
|
11
11
|
|
|
@@ -33,15 +33,13 @@ memory: project
|
|
|
33
33
|
### 第二步:阅读现有文档
|
|
34
34
|
|
|
35
35
|
1. 读取 `docs/spec.md` 的当前内容(如果存在)。
|
|
36
|
-
2. 读取 `
|
|
37
|
-
3.
|
|
38
|
-
4. 理解每个文档的结构、风格和覆盖范围。
|
|
36
|
+
2. 读取 `README.md` 的当前内容(如果存在)。
|
|
37
|
+
3. 理解每个文档的结构、风格和覆盖范围。
|
|
39
38
|
|
|
40
39
|
### 第三步:确定需要更新的内容
|
|
41
40
|
|
|
42
41
|
对每个文档,判断代码变更是否影响其内容:
|
|
43
42
|
|
|
44
|
-
- **`CLAUDE.md`**:项目架构说明、命令列表、核心流程、目录层级、关键约定、构建命令等。当新增命令、修改架构、调整目录结构、修改构建流程时需要更新。
|
|
45
43
|
- **`docs/spec.md`**:项目规格说明文档。当功能需求、技术规格、API 设计发生变化时需要更新。
|
|
46
44
|
- **`README.md`**:用户面向的项目说明。当用户可见的功能、安装方式、使用方法、命令参数发生变化时需要更新。
|
|
47
45
|
|
|
@@ -83,7 +81,7 @@ memory: project
|
|
|
83
81
|
- 如果工作区和暂存区都没有变更,检查最近的提交并告知用户当前没有未提交的变更,询问是否基于最近提交更新。
|
|
84
82
|
- 如果某个文档文件不存在,告知用户并询问是否需要创建。
|
|
85
83
|
- 如果变更内容过于复杂或不确定如何反映到文档中,列出你的理解并询问用户确认。
|
|
86
|
-
- 如果变更只涉及代码重构而不改变功能,可能不需要更新面向用户的文档(README.md
|
|
84
|
+
- 如果变更只涉及代码重构而不改变功能,可能不需要更新面向用户的文档(README.md),但可能需要更新规格文档(docs/spec.md)。
|
|
87
85
|
|
|
88
86
|
**Update your agent memory** as you discover documentation patterns, document structure conventions, terminology usage, and relationships between code modules and their documentation sections. This builds up institutional knowledge across conversations. Write concise notes about what you found and where.
|
|
89
87
|
|
package/README.md
CHANGED
|
@@ -107,29 +107,44 @@ clawt resume
|
|
|
107
107
|
### `clawt validate` — 在主 worktree 验证分支变更
|
|
108
108
|
|
|
109
109
|
```bash
|
|
110
|
+
# 指定分支名(支持模糊匹配)
|
|
110
111
|
clawt validate -b <branchName> [--clean]
|
|
112
|
+
|
|
113
|
+
# 不指定分支名(列出所有分支供选择)
|
|
114
|
+
clawt validate [--clean]
|
|
111
115
|
```
|
|
112
116
|
|
|
113
117
|
| 参数 | 必填 | 说明 |
|
|
114
118
|
| ---- | ---- | ---- |
|
|
115
|
-
| `-b` |
|
|
119
|
+
| `-b` | 否 | 要验证的分支名(支持模糊匹配,不传则列出所有分支供选择) |
|
|
116
120
|
| `--clean` | 否 | 清理 validate 状态(重置主 worktree 并删除快照) |
|
|
117
121
|
|
|
118
122
|
将目标 worktree 的变更通过 `git diff`(三点 diff)迁移到主 worktree,方便在主 worktree 中直接测试,无需重新安装依赖。同时检测未提交修改和已提交 commit,确保所有变更都能被捕获。
|
|
119
123
|
|
|
120
|
-
|
|
124
|
+
**分支匹配策略:**
|
|
125
|
+
- 传 `-b` 时,优先精确匹配分支名;未精确匹配则进行模糊匹配(子串匹配,大小写不敏感);模糊匹配到多个时通过交互列表选择;无匹配时报错并列出所有可用分支
|
|
126
|
+
- 不传 `-b` 时,列出当前项目所有可用分支供交互式选择(仅 1 个时自动使用)
|
|
127
|
+
|
|
128
|
+
支持增量模式:首次 validate 后会自动保存快照(通过 `git write-tree` 将变更存储为 git tree 对象,并记录当前 HEAD commit hash),再次 validate 同一分支时会将上次快照载入暂存区、最新变更保留在工作目录,用户可通过 `git diff` 查看两次 validate 之间的增量差异。当主分支 HEAD 发生变化(如合并了其他分支)时,会自动将旧变更 patch 重放到当前 HEAD 暂存区上,避免 diff 混入 HEAD 变化的内容;若 patch 存在冲突则自动降级为全量模式。使用 `--clean` 可清理 validate 状态(重置主 worktree 并删除快照文件)。
|
|
121
129
|
|
|
122
130
|
> **提示:** 如果 validate 时 patch apply 失败(目标分支与主分支差异过大),可先执行 `clawt sync -b <branchName>` 同步主分支后重试。
|
|
123
131
|
|
|
124
132
|
```bash
|
|
125
|
-
#
|
|
133
|
+
# 精确匹配分支名
|
|
126
134
|
clawt validate -b feature-scheme-1
|
|
127
135
|
|
|
136
|
+
# 模糊匹配(匹配包含 "scheme" 的分支)
|
|
137
|
+
clawt validate -b scheme
|
|
138
|
+
|
|
139
|
+
# 交互式选择所有分支
|
|
140
|
+
clawt validate
|
|
141
|
+
|
|
128
142
|
# 再次验证(增量模式,可通过 git diff 查看增量差异)
|
|
129
143
|
clawt validate -b feature-scheme-1
|
|
130
144
|
|
|
131
|
-
# 清理 validate
|
|
145
|
+
# 清理 validate 状态(同样支持模糊匹配)
|
|
132
146
|
clawt validate -b feature-scheme-1 --clean
|
|
147
|
+
clawt validate --clean
|
|
133
148
|
```
|
|
134
149
|
|
|
135
150
|
### `clawt sync` — 将主分支代码同步到目标 worktree
|
package/dist/index.js
CHANGED
|
@@ -134,7 +134,17 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
|
134
134
|
/** resume 交互选择提示 */
|
|
135
135
|
RESUME_SELECT_BRANCH: "\u8BF7\u9009\u62E9\u8981\u6062\u590D\u7684\u5206\u652F",
|
|
136
136
|
/** resume 模糊匹配到多个结果提示 */
|
|
137
|
-
RESUME_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A
|
|
137
|
+
RESUME_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`,
|
|
138
|
+
/** validate 无可用 worktree */
|
|
139
|
+
VALIDATE_NO_WORKTREES: "\u5F53\u524D\u9879\u76EE\u6CA1\u6709\u53EF\u7528\u7684 worktree\uFF0C\u8BF7\u5148\u901A\u8FC7 clawt run \u6216 clawt create \u521B\u5EFA",
|
|
140
|
+
/** validate 模糊匹配无结果,列出可用分支 */
|
|
141
|
+
VALIDATE_NO_MATCH: (name, branches) => `\u672A\u627E\u5230\u4E0E "${name}" \u5339\u914D\u7684\u5206\u652F
|
|
142
|
+
\u53EF\u7528\u5206\u652F\uFF1A
|
|
143
|
+
${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
144
|
+
/** validate 交互选择提示 */
|
|
145
|
+
VALIDATE_SELECT_BRANCH: "\u8BF7\u9009\u62E9\u8981\u9A8C\u8BC1\u7684\u5206\u652F",
|
|
146
|
+
/** validate 模糊匹配到多个结果提示 */
|
|
147
|
+
VALIDATE_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`
|
|
138
148
|
};
|
|
139
149
|
|
|
140
150
|
// src/constants/exitCodes.ts
|
|
@@ -368,9 +378,15 @@ function getDiffStat(worktreePath) {
|
|
|
368
378
|
const output = execCommand("git diff --shortstat HEAD", { cwd: worktreePath });
|
|
369
379
|
return parseShortStat(output);
|
|
370
380
|
}
|
|
381
|
+
function gitApplyCachedFromStdin(patchContent, cwd) {
|
|
382
|
+
execCommandWithInput("git", ["apply", "--cached"], { input: patchContent, cwd });
|
|
383
|
+
}
|
|
371
384
|
function getCurrentBranch(cwd) {
|
|
372
385
|
return execCommand("git rev-parse --abbrev-ref HEAD", { cwd });
|
|
373
386
|
}
|
|
387
|
+
function getHeadCommitHash(cwd) {
|
|
388
|
+
return execCommand("git rev-parse HEAD", { cwd });
|
|
389
|
+
}
|
|
374
390
|
function gitDiffBinaryAgainstBranch(branchName, cwd) {
|
|
375
391
|
logger.debug(`\u6267\u884C\u547D\u4EE4: git diff HEAD...${branchName} --binary${cwd ? ` (cwd: ${cwd})` : ""}`);
|
|
376
392
|
return execSync2(`git diff HEAD...${branchName} --binary`, {
|
|
@@ -405,6 +421,24 @@ function gitWriteTree(cwd) {
|
|
|
405
421
|
function gitReadTree(treeHash, cwd) {
|
|
406
422
|
execCommand(`git read-tree ${treeHash}`, { cwd });
|
|
407
423
|
}
|
|
424
|
+
function getCommitTreeHash(commitHash, cwd) {
|
|
425
|
+
return execCommand(`git rev-parse ${commitHash}^{tree}`, { cwd });
|
|
426
|
+
}
|
|
427
|
+
function gitDiffTree(baseTreeHash, targetTreeHash, cwd) {
|
|
428
|
+
logger.debug(`\u6267\u884C\u547D\u4EE4: git diff-tree -p --binary ${baseTreeHash} ${targetTreeHash}${cwd ? ` (cwd: ${cwd})` : ""}`);
|
|
429
|
+
return execSync2(`git diff-tree -p --binary ${baseTreeHash} ${targetTreeHash}`, {
|
|
430
|
+
cwd,
|
|
431
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
function gitApplyCachedCheck(patchContent, cwd) {
|
|
435
|
+
try {
|
|
436
|
+
execCommandWithInput("git", ["apply", "--cached", "--check"], { input: patchContent, cwd });
|
|
437
|
+
return true;
|
|
438
|
+
} catch {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
408
442
|
|
|
409
443
|
// src/utils/formatter.ts
|
|
410
444
|
import chalk from "chalk";
|
|
@@ -667,27 +701,40 @@ import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync
|
|
|
667
701
|
function getSnapshotPath(projectName, branchName) {
|
|
668
702
|
return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
|
|
669
703
|
}
|
|
704
|
+
function getSnapshotHeadPath(projectName, branchName) {
|
|
705
|
+
return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
|
|
706
|
+
}
|
|
670
707
|
function hasSnapshot(projectName, branchName) {
|
|
671
708
|
return existsSync5(getSnapshotPath(projectName, branchName));
|
|
672
709
|
}
|
|
673
|
-
function
|
|
710
|
+
function readSnapshot(projectName, branchName) {
|
|
674
711
|
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
712
|
+
const headPath = getSnapshotHeadPath(projectName, branchName);
|
|
675
713
|
logger.debug(`\u8BFB\u53D6 validate \u5FEB\u7167: ${snapshotPath}`);
|
|
676
|
-
|
|
714
|
+
const treeHash = existsSync5(snapshotPath) ? readFileSync2(snapshotPath, "utf-8").trim() : "";
|
|
715
|
+
const headCommitHash = existsSync5(headPath) ? readFileSync2(headPath, "utf-8").trim() : "";
|
|
716
|
+
return { treeHash, headCommitHash };
|
|
677
717
|
}
|
|
678
|
-
function writeSnapshot(projectName, branchName, treeHash) {
|
|
718
|
+
function writeSnapshot(projectName, branchName, treeHash, headCommitHash) {
|
|
679
719
|
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
720
|
+
const headPath = getSnapshotHeadPath(projectName, branchName);
|
|
680
721
|
const snapshotDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
681
722
|
ensureDir(snapshotDir);
|
|
682
723
|
writeFileSync2(snapshotPath, treeHash, "utf-8");
|
|
683
|
-
|
|
724
|
+
writeFileSync2(headPath, headCommitHash, "utf-8");
|
|
725
|
+
logger.info(`\u5DF2\u4FDD\u5B58 validate \u5FEB\u7167: ${snapshotPath}, ${headPath}`);
|
|
684
726
|
}
|
|
685
727
|
function removeSnapshot(projectName, branchName) {
|
|
686
728
|
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
729
|
+
const headPath = getSnapshotHeadPath(projectName, branchName);
|
|
687
730
|
if (existsSync5(snapshotPath)) {
|
|
688
731
|
unlinkSync(snapshotPath);
|
|
689
732
|
logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${snapshotPath}`);
|
|
690
733
|
}
|
|
734
|
+
if (existsSync5(headPath)) {
|
|
735
|
+
unlinkSync(headPath);
|
|
736
|
+
logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${headPath}`);
|
|
737
|
+
}
|
|
691
738
|
}
|
|
692
739
|
function removeProjectSnapshots(projectName) {
|
|
693
740
|
const projectDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
@@ -705,6 +752,50 @@ function removeProjectSnapshots(projectName) {
|
|
|
705
752
|
logger.info(`\u5DF2\u5220\u9664\u9879\u76EE ${projectName} \u7684\u6240\u6709 validate \u5FEB\u7167`);
|
|
706
753
|
}
|
|
707
754
|
|
|
755
|
+
// src/utils/worktree-matcher.ts
|
|
756
|
+
import Enquirer2 from "enquirer";
|
|
757
|
+
function findExactMatch(worktrees, branchName) {
|
|
758
|
+
return worktrees.find((wt) => wt.branch === branchName);
|
|
759
|
+
}
|
|
760
|
+
function findFuzzyMatches(worktrees, keyword) {
|
|
761
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
762
|
+
return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
|
|
763
|
+
}
|
|
764
|
+
async function promptSelectBranch(worktrees, message) {
|
|
765
|
+
const selectedBranch = await new Enquirer2.Select({
|
|
766
|
+
message,
|
|
767
|
+
choices: worktrees.map((wt) => ({
|
|
768
|
+
name: wt.branch,
|
|
769
|
+
message: wt.branch
|
|
770
|
+
}))
|
|
771
|
+
}).run();
|
|
772
|
+
return worktrees.find((wt) => wt.branch === selectedBranch);
|
|
773
|
+
}
|
|
774
|
+
async function resolveTargetWorktree(worktrees, messages, branchName) {
|
|
775
|
+
if (worktrees.length === 0) {
|
|
776
|
+
throw new ClawtError(messages.noWorktrees);
|
|
777
|
+
}
|
|
778
|
+
if (!branchName) {
|
|
779
|
+
if (worktrees.length === 1) {
|
|
780
|
+
return worktrees[0];
|
|
781
|
+
}
|
|
782
|
+
return promptSelectBranch(worktrees, messages.selectBranch);
|
|
783
|
+
}
|
|
784
|
+
const exactMatch = findExactMatch(worktrees, branchName);
|
|
785
|
+
if (exactMatch) {
|
|
786
|
+
return exactMatch;
|
|
787
|
+
}
|
|
788
|
+
const fuzzyMatches = findFuzzyMatches(worktrees, branchName);
|
|
789
|
+
if (fuzzyMatches.length === 1) {
|
|
790
|
+
return fuzzyMatches[0];
|
|
791
|
+
}
|
|
792
|
+
if (fuzzyMatches.length > 1) {
|
|
793
|
+
return promptSelectBranch(fuzzyMatches, messages.multipleMatches(branchName));
|
|
794
|
+
}
|
|
795
|
+
const allBranches = worktrees.map((wt) => wt.branch);
|
|
796
|
+
throw new ClawtError(messages.noMatch(branchName, allBranches));
|
|
797
|
+
}
|
|
798
|
+
|
|
708
799
|
// src/commands/list.ts
|
|
709
800
|
import chalk2 from "chalk";
|
|
710
801
|
function registerListCommand(program2) {
|
|
@@ -1022,68 +1113,36 @@ async function handleRun(options) {
|
|
|
1022
1113
|
}
|
|
1023
1114
|
|
|
1024
1115
|
// src/commands/resume.ts
|
|
1025
|
-
|
|
1116
|
+
var RESUME_RESOLVE_MESSAGES = {
|
|
1117
|
+
noWorktrees: MESSAGES.RESUME_NO_WORKTREES,
|
|
1118
|
+
selectBranch: MESSAGES.RESUME_SELECT_BRANCH,
|
|
1119
|
+
multipleMatches: MESSAGES.RESUME_MULTIPLE_MATCHES,
|
|
1120
|
+
noMatch: MESSAGES.RESUME_NO_MATCH
|
|
1121
|
+
};
|
|
1026
1122
|
function registerResumeCommand(program2) {
|
|
1027
1123
|
program2.command("resume").description("\u5728\u5DF2\u6709 worktree \u4E2D\u6062\u590D Claude Code \u4EA4\u4E92\u5F0F\u4F1A\u8BDD").option("-b, --branch <branchName>", "\u8981\u6062\u590D\u7684\u5206\u652F\u540D\uFF08\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF0C\u4E0D\u4F20\u5219\u5217\u51FA\u6240\u6709\u5206\u652F\uFF09").action(async (options) => {
|
|
1028
1124
|
await handleResume(options);
|
|
1029
1125
|
});
|
|
1030
1126
|
}
|
|
1031
|
-
function findExactMatch(worktrees, branchName) {
|
|
1032
|
-
return worktrees.find((wt) => wt.branch === branchName);
|
|
1033
|
-
}
|
|
1034
|
-
function findFuzzyMatches(worktrees, keyword) {
|
|
1035
|
-
const lowerKeyword = keyword.toLowerCase();
|
|
1036
|
-
return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
|
|
1037
|
-
}
|
|
1038
|
-
async function promptSelectBranch(worktrees, message) {
|
|
1039
|
-
const selectedBranch = await new Enquirer2.Select({
|
|
1040
|
-
message,
|
|
1041
|
-
choices: worktrees.map((wt) => ({
|
|
1042
|
-
name: wt.branch,
|
|
1043
|
-
message: wt.branch
|
|
1044
|
-
}))
|
|
1045
|
-
}).run();
|
|
1046
|
-
return worktrees.find((wt) => wt.branch === selectedBranch);
|
|
1047
|
-
}
|
|
1048
|
-
async function resolveTargetWorktree(branchName) {
|
|
1049
|
-
const worktrees = getProjectWorktrees();
|
|
1050
|
-
if (worktrees.length === 0) {
|
|
1051
|
-
throw new ClawtError(MESSAGES.RESUME_NO_WORKTREES);
|
|
1052
|
-
}
|
|
1053
|
-
if (!branchName) {
|
|
1054
|
-
if (worktrees.length === 1) {
|
|
1055
|
-
return worktrees[0];
|
|
1056
|
-
}
|
|
1057
|
-
return promptSelectBranch(worktrees, MESSAGES.RESUME_SELECT_BRANCH);
|
|
1058
|
-
}
|
|
1059
|
-
const exactMatch = findExactMatch(worktrees, branchName);
|
|
1060
|
-
if (exactMatch) {
|
|
1061
|
-
return exactMatch;
|
|
1062
|
-
}
|
|
1063
|
-
const fuzzyMatches = findFuzzyMatches(worktrees, branchName);
|
|
1064
|
-
if (fuzzyMatches.length === 1) {
|
|
1065
|
-
return fuzzyMatches[0];
|
|
1066
|
-
}
|
|
1067
|
-
if (fuzzyMatches.length > 1) {
|
|
1068
|
-
return promptSelectBranch(fuzzyMatches, MESSAGES.RESUME_MULTIPLE_MATCHES(branchName));
|
|
1069
|
-
}
|
|
1070
|
-
const allBranches = worktrees.map((wt) => wt.branch);
|
|
1071
|
-
throw new ClawtError(MESSAGES.RESUME_NO_MATCH(branchName, allBranches));
|
|
1072
|
-
}
|
|
1073
1127
|
async function handleResume(options) {
|
|
1074
1128
|
validateMainWorktree();
|
|
1075
1129
|
validateClaudeCodeInstalled();
|
|
1076
1130
|
logger.info(`resume \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch ?? "(\u672A\u6307\u5B9A)"}`);
|
|
1077
|
-
const
|
|
1131
|
+
const worktrees = getProjectWorktrees();
|
|
1132
|
+
const worktree = await resolveTargetWorktree(worktrees, RESUME_RESOLVE_MESSAGES, options.branch);
|
|
1078
1133
|
launchInteractiveClaude(worktree);
|
|
1079
1134
|
}
|
|
1080
1135
|
|
|
1081
1136
|
// src/commands/validate.ts
|
|
1082
|
-
import { join as join4 } from "path";
|
|
1083
|
-
import { existsSync as existsSync6 } from "fs";
|
|
1084
1137
|
import Enquirer3 from "enquirer";
|
|
1138
|
+
var VALIDATE_RESOLVE_MESSAGES = {
|
|
1139
|
+
noWorktrees: MESSAGES.VALIDATE_NO_WORKTREES,
|
|
1140
|
+
selectBranch: MESSAGES.VALIDATE_SELECT_BRANCH,
|
|
1141
|
+
multipleMatches: MESSAGES.VALIDATE_MULTIPLE_MATCHES,
|
|
1142
|
+
noMatch: MESSAGES.VALIDATE_NO_MATCH
|
|
1143
|
+
};
|
|
1085
1144
|
function registerValidateCommand(program2) {
|
|
1086
|
-
program2.command("validate").description("\u5728\u4E3B worktree \u9A8C\u8BC1\u67D0\u4E2A worktree \u5206\u652F\u7684\u53D8\u66F4").
|
|
1145
|
+
program2.command("validate").description("\u5728\u4E3B worktree \u9A8C\u8BC1\u67D0\u4E2A worktree \u5206\u652F\u7684\u53D8\u66F4").option("-b, --branch <branchName>", "\u8981\u9A8C\u8BC1\u7684\u5206\u652F\u540D\uFF08\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF0C\u4E0D\u4F20\u5219\u5217\u51FA\u6240\u6709\u5206\u652F\uFF09").option("--clean", "\u6E05\u7406 validate \u72B6\u6001\uFF08\u91CD\u7F6E\u4E3B worktree \u5E76\u5220\u9664\u5FEB\u7167\uFF09").action(async (options) => {
|
|
1087
1146
|
await handleValidate(options);
|
|
1088
1147
|
});
|
|
1089
1148
|
}
|
|
@@ -1158,18 +1217,22 @@ function saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName) {
|
|
|
1158
1217
|
gitAddAll(mainWorktreePath);
|
|
1159
1218
|
const treeHash = gitWriteTree(mainWorktreePath);
|
|
1160
1219
|
gitRestoreStaged(mainWorktreePath);
|
|
1161
|
-
|
|
1220
|
+
const headCommitHash = getHeadCommitHash(mainWorktreePath);
|
|
1221
|
+
writeSnapshot(projectName, branchName, treeHash, headCommitHash);
|
|
1162
1222
|
return treeHash;
|
|
1163
1223
|
}
|
|
1164
1224
|
async function handleValidateClean(options) {
|
|
1165
1225
|
validateMainWorktree();
|
|
1166
1226
|
const projectName = getProjectName();
|
|
1167
1227
|
const mainWorktreePath = getGitTopLevel();
|
|
1168
|
-
|
|
1228
|
+
const worktrees = getProjectWorktrees();
|
|
1229
|
+
const worktree = await resolveTargetWorktree(worktrees, VALIDATE_RESOLVE_MESSAGES, options.branch);
|
|
1230
|
+
const branchName = worktree.branch;
|
|
1231
|
+
logger.info(`validate --clean \u6267\u884C\uFF0C\u5206\u652F: ${branchName}`);
|
|
1169
1232
|
if (getConfigValue("confirmDestructiveOps")) {
|
|
1170
1233
|
const confirmed = await confirmDestructiveAction(
|
|
1171
1234
|
"git reset --hard + git clean -fd",
|
|
1172
|
-
`\u91CD\u7F6E\u4E3B worktree \u5E76\u5220\u9664\u5206\u652F ${
|
|
1235
|
+
`\u91CD\u7F6E\u4E3B worktree \u5E76\u5220\u9664\u5206\u652F ${branchName} \u7684 validate \u5FEB\u7167`
|
|
1173
1236
|
);
|
|
1174
1237
|
if (!confirmed) {
|
|
1175
1238
|
printInfo(MESSAGES.DESTRUCTIVE_OP_CANCELLED);
|
|
@@ -1180,8 +1243,8 @@ async function handleValidateClean(options) {
|
|
|
1180
1243
|
gitResetHard(mainWorktreePath);
|
|
1181
1244
|
gitCleanForce(mainWorktreePath);
|
|
1182
1245
|
}
|
|
1183
|
-
removeSnapshot(projectName,
|
|
1184
|
-
printSuccess(MESSAGES.VALIDATE_CLEANED(
|
|
1246
|
+
removeSnapshot(projectName, branchName);
|
|
1247
|
+
printSuccess(MESSAGES.VALIDATE_CLEANED(branchName));
|
|
1185
1248
|
}
|
|
1186
1249
|
function handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
|
|
1187
1250
|
migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
@@ -1189,7 +1252,7 @@ function handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName,
|
|
|
1189
1252
|
printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
|
|
1190
1253
|
}
|
|
1191
1254
|
function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
|
|
1192
|
-
const oldTreeHash =
|
|
1255
|
+
const { treeHash: oldTreeHash, headCommitHash: oldHeadCommitHash } = readSnapshot(projectName, branchName);
|
|
1193
1256
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
1194
1257
|
gitResetHard(mainWorktreePath);
|
|
1195
1258
|
gitCleanForce(mainWorktreePath);
|
|
@@ -1197,7 +1260,21 @@ function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, project
|
|
|
1197
1260
|
migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
1198
1261
|
saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
|
|
1199
1262
|
try {
|
|
1200
|
-
|
|
1263
|
+
const currentHeadCommitHash = getHeadCommitHash(mainWorktreePath);
|
|
1264
|
+
if (oldHeadCommitHash && oldHeadCommitHash !== currentHeadCommitHash) {
|
|
1265
|
+
const oldHeadTreeHash = getCommitTreeHash(oldHeadCommitHash, mainWorktreePath);
|
|
1266
|
+
const oldChangePatch = gitDiffTree(oldHeadTreeHash, oldTreeHash, mainWorktreePath);
|
|
1267
|
+
if (oldChangePatch.length > 0 && gitApplyCachedCheck(oldChangePatch, mainWorktreePath)) {
|
|
1268
|
+
gitApplyCachedFromStdin(oldChangePatch, mainWorktreePath);
|
|
1269
|
+
} else if (oldChangePatch.length > 0) {
|
|
1270
|
+
logger.warn("\u65E7\u53D8\u66F4 patch \u4E0E\u5F53\u524D HEAD \u51B2\u7A81\uFF0C\u964D\u7EA7\u4E3A\u5168\u91CF\u6A21\u5F0F");
|
|
1271
|
+
printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
|
|
1272
|
+
printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
} else {
|
|
1276
|
+
gitReadTree(oldTreeHash, mainWorktreePath);
|
|
1277
|
+
}
|
|
1201
1278
|
} catch (error) {
|
|
1202
1279
|
logger.warn(`\u589E\u91CF read-tree \u5931\u8D25: ${error}`);
|
|
1203
1280
|
printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
|
|
@@ -1214,35 +1291,34 @@ async function handleValidate(options) {
|
|
|
1214
1291
|
validateMainWorktree();
|
|
1215
1292
|
const projectName = getProjectName();
|
|
1216
1293
|
const mainWorktreePath = getGitTopLevel();
|
|
1217
|
-
const
|
|
1218
|
-
const
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
}
|
|
1294
|
+
const worktrees = getProjectWorktrees();
|
|
1295
|
+
const worktree = await resolveTargetWorktree(worktrees, VALIDATE_RESOLVE_MESSAGES, options.branch);
|
|
1296
|
+
const branchName = worktree.branch;
|
|
1297
|
+
const targetWorktreePath = worktree.path;
|
|
1298
|
+
logger.info(`validate \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${branchName}`);
|
|
1223
1299
|
const hasUncommitted = !isWorkingDirClean(targetWorktreePath);
|
|
1224
|
-
const hasCommitted = hasLocalCommits(
|
|
1300
|
+
const hasCommitted = hasLocalCommits(branchName, mainWorktreePath);
|
|
1225
1301
|
if (!hasUncommitted && !hasCommitted) {
|
|
1226
1302
|
printInfo(MESSAGES.TARGET_WORKTREE_CLEAN);
|
|
1227
1303
|
return;
|
|
1228
1304
|
}
|
|
1229
|
-
const isIncremental = hasSnapshot(projectName,
|
|
1305
|
+
const isIncremental = hasSnapshot(projectName, branchName);
|
|
1230
1306
|
if (isIncremental) {
|
|
1231
1307
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
1232
1308
|
await handleDirtyMainWorktree(mainWorktreePath);
|
|
1233
1309
|
}
|
|
1234
|
-
handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName,
|
|
1310
|
+
handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
1235
1311
|
} else {
|
|
1236
1312
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
1237
1313
|
await handleDirtyMainWorktree(mainWorktreePath);
|
|
1238
1314
|
}
|
|
1239
|
-
handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName,
|
|
1315
|
+
handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
1240
1316
|
}
|
|
1241
1317
|
}
|
|
1242
1318
|
|
|
1243
1319
|
// src/commands/merge.ts
|
|
1244
|
-
import { join as
|
|
1245
|
-
import { existsSync as
|
|
1320
|
+
import { join as join4 } from "path";
|
|
1321
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1246
1322
|
function registerMergeCommand(program2) {
|
|
1247
1323
|
program2.command("merge").description("\u5408\u5E76\u67D0\u4E2A\u5DF2\u9A8C\u8BC1\u7684 worktree \u5206\u652F\u5230\u4E3B worktree").requiredOption("-b, --branch <branchName>", "\u8981\u5408\u5E76\u7684\u5206\u652F\u540D").option("-m, --message <message>", "\u63D0\u4EA4\u4FE1\u606F\uFF08\u5DE5\u4F5C\u533A\u6709\u4FEE\u6539\u65F6\u5FC5\u586B\uFF09").action(async (options) => {
|
|
1248
1324
|
await handleMerge(options);
|
|
@@ -1284,9 +1360,9 @@ async function handleMerge(options) {
|
|
|
1284
1360
|
validateMainWorktree();
|
|
1285
1361
|
const mainWorktreePath = getGitTopLevel();
|
|
1286
1362
|
const projectDir = getProjectWorktreeDir();
|
|
1287
|
-
const targetWorktreePath =
|
|
1363
|
+
const targetWorktreePath = join4(projectDir, options.branch);
|
|
1288
1364
|
logger.info(`merge \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch}\uFF0C\u63D0\u4EA4\u4FE1\u606F: ${options.message ?? "(\u672A\u63D0\u4F9B)"}`);
|
|
1289
|
-
if (!
|
|
1365
|
+
if (!existsSync6(targetWorktreePath)) {
|
|
1290
1366
|
throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
|
|
1291
1367
|
}
|
|
1292
1368
|
const projectName = getProjectName();
|
|
@@ -1379,8 +1455,8 @@ function formatConfigValue(value) {
|
|
|
1379
1455
|
}
|
|
1380
1456
|
|
|
1381
1457
|
// src/commands/sync.ts
|
|
1382
|
-
import { existsSync as
|
|
1383
|
-
import { join as
|
|
1458
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1459
|
+
import { join as join5 } from "path";
|
|
1384
1460
|
function registerSyncCommand(program2) {
|
|
1385
1461
|
program2.command("sync").description("\u5C06\u4E3B\u5206\u652F\u6700\u65B0\u4EE3\u7801\u540C\u6B65\u5230\u76EE\u6807 worktree").requiredOption("-b, --branch <branchName>", "\u8981\u540C\u6B65\u7684\u5206\u652F\u540D").action(async (options) => {
|
|
1386
1462
|
await handleSync(options);
|
|
@@ -1408,8 +1484,8 @@ async function handleSync(options) {
|
|
|
1408
1484
|
const { branch } = options;
|
|
1409
1485
|
logger.info(`sync \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${branch}`);
|
|
1410
1486
|
const projectWorktreeDir = getProjectWorktreeDir();
|
|
1411
|
-
const targetWorktreePath =
|
|
1412
|
-
if (!
|
|
1487
|
+
const targetWorktreePath = join5(projectWorktreeDir, branch);
|
|
1488
|
+
if (!existsSync7(targetWorktreePath)) {
|
|
1413
1489
|
throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(branch));
|
|
1414
1490
|
}
|
|
1415
1491
|
const mainWorktreePath = getGitTopLevel();
|