clawt 1.1.1 → 1.3.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 +26 -3
- package/CLAUDE.md +18 -7
- package/README.md +30 -3
- package/dist/index.js +208 -56
- package/dist/postinstall.js +1 -0
- package/docs/spec.md +127 -14
- package/package.json +1 -1
- package/src/commands/merge.ts +15 -0
- package/src/commands/resume.ts +60 -0
- package/src/commands/run.ts +2 -37
- package/src/commands/validate.ts +156 -28
- package/src/constants/index.ts +1 -1
- package/src/constants/messages.ts +12 -0
- package/src/constants/paths.ts +3 -0
- package/src/index.ts +2 -0
- package/src/types/command.ts +8 -0
- package/src/types/index.ts +1 -1
- package/src/utils/claude.ts +41 -0
- package/src/utils/git.ts +25 -1
- package/src/utils/index.ts +5 -1
- package/src/utils/shell.ts +22 -1
- package/src/utils/validate-snapshot.ts +89 -0
|
@@ -4,16 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
### docs/spec.md
|
|
6
6
|
- 完整的软件规格说明,包含 7 大章节
|
|
7
|
-
- 命令流程在 `5. 需求场景详细设计` 下,每个命令一个子章节(5.1-5.
|
|
7
|
+
- 命令流程在 `5. 需求场景详细设计` 下,每个命令一个子章节(5.1-5.11)
|
|
8
8
|
- run 命令对应 `5.2 批量创建 Worktree + 执行 Claude Code 任务`,流程按步骤编号描述
|
|
9
9
|
- merge 命令对应 `5.6 合并验证过的分支`,流程按步骤编号描述
|
|
10
10
|
- config 命令对应 `5.10 查看全局配置`,只读展示配置
|
|
11
|
+
- resume 命令对应 `5.11 在已有 Worktree 中恢复会话`,查找已有 worktree 并启动交互式界面
|
|
11
12
|
- 配置项说明在 `5.7 默认配置文件` 章节的表格中
|
|
12
13
|
- 更新模式:新增步骤时追加编号,配置项影响范围变化时更新说明列
|
|
13
14
|
|
|
14
15
|
### CLAUDE.md
|
|
15
16
|
- 面向 Claude Code 的项目架构指引,精简扼要
|
|
16
17
|
- run 命令流程在 `核心流程(run 命令)` 章节,编号列表描述
|
|
18
|
+
- resume 命令流程在独立的 `### resume 命令流程` 章节,编号列表描述
|
|
17
19
|
- merge 和 run 中断清理在 `validate + merge 工作流` 章节,一行式描述用箭头连接流程
|
|
18
20
|
- utils 目录描述用括号内逗号分隔列举功能模块
|
|
19
21
|
- 更新模式:编号列表追加步骤,箭头链追加阶段,括号内追加关键词
|
|
@@ -27,10 +29,13 @@
|
|
|
27
29
|
## 关键约定
|
|
28
30
|
- `autoDeleteBranch` 配置项影响三处:remove 命令、merge 命令、run 中断清理
|
|
29
31
|
- merge 的清理确认在 merge 操作之前询问(避免交互中断),但清理在 merge 成功后执行
|
|
32
|
+
- merge 成功后自动清理对应的 validate 快照(hasSnapshot + removeSnapshot)
|
|
30
33
|
- run 的中断清理在所有子进程退出后执行
|
|
31
34
|
- 文档中文风格,技术术语保留英文(worktree, merge, branch, SIGINT 等)
|
|
32
35
|
- cleanupWorktrees 是 merge 和 run 共用的公共清理函数(在 src/utils/worktree.ts)
|
|
36
|
+
- `launchInteractiveClaude` 是 run(交互式模式)和 resume 共用的公共函数(在 src/utils/claude.ts)
|
|
33
37
|
- killAllChildProcesses 是 run 专用的子进程终止函数(在 src/utils/shell.ts)
|
|
38
|
+
- validate 快照管理函数在 `src/utils/validate-snapshot.ts`,被 validate 和 merge 两个命令使用
|
|
34
39
|
|
|
35
40
|
## 配置项同步检查点
|
|
36
41
|
|
|
@@ -55,6 +60,24 @@ run 命令有两种模式(自 claudeCodeCommand 特性后):
|
|
|
55
60
|
- 传 `--tasks`:并行任务模式(多 worktree + `executeClaudeTask` + spawnProcess)
|
|
56
61
|
- CLAUDE.md 中的核心流程按模式分段描述
|
|
57
62
|
|
|
58
|
-
## 命令清单(
|
|
63
|
+
## 命令清单(8 个)
|
|
59
64
|
|
|
60
|
-
`create`、`run`、`list`、`remove`、`validate`、`merge`、`config`
|
|
65
|
+
`create`、`run`、`resume`、`list`、`remove`、`validate`、`merge`、`config`
|
|
66
|
+
|
|
67
|
+
Notes:
|
|
68
|
+
- resume 和 run(交互式模式)共用 `launchInteractiveClaude()`,该函数从 run.ts 提取到 src/utils/claude.ts
|
|
69
|
+
- `claudeCodeCommand` 配置项同时影响 run 交互式模式和 resume 命令
|
|
70
|
+
|
|
71
|
+
## validate 快照机制
|
|
72
|
+
|
|
73
|
+
- validate 命令支持首次/增量两种模式,通过 `hasSnapshot()` 判断
|
|
74
|
+
- 快照路径:`~/.clawt/validate-snapshots/<projectName>/<branchName>.patch`
|
|
75
|
+
- 常量 `VALIDATE_SNAPSHOTS_DIR` 定义在 `src/constants/paths.ts`
|
|
76
|
+
- validate 新增 `--clean` 选项(`ValidateOptions.clean?: boolean`)
|
|
77
|
+
- 增量模式核心:旧 patch 应用到暂存区 + 新全量变更在工作目录 → `git diff` 可查看增量差异
|
|
78
|
+
- 增量 apply 失败时自动降级为全量模式
|
|
79
|
+
- shell 层新增 `execCommandWithInput()`(`execFileSync` + stdin),用于 `gitApplyCachedFromStdin()`
|
|
80
|
+
- git 层新增 `gitDiffCachedBinary()`(返回 Buffer)和 `gitApplyCachedFromStdin()`
|
|
81
|
+
- merge 成功后自动清理对应快照;merge 时主 worktree 脏 + 存在快照会输出警告提示
|
|
82
|
+
- docs/spec.md 中 validate 章节(5.4)按 `--clean 模式`、`首次 validate`、`增量 validate` 三段描述
|
|
83
|
+
- CLAUDE.md 中在 validate + merge 工作流章节用缩进列表描述两种模式
|
package/CLAUDE.md
CHANGED
|
@@ -24,7 +24,7 @@ npm i -g . # 本地全局安装进行测试
|
|
|
24
24
|
|
|
25
25
|
每个命令为独立文件 `src/commands/<name>.ts`,导出 `registerXxxCommand(program)` 函数,在 `src/index.ts` 中统一注册到 Commander。命令内部逻辑封装在对应的 `handleXxx` 函数中。
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
八个命令:`create`、`run`、`resume`、`list`、`remove`、`validate`、`merge`、`config`。
|
|
28
28
|
|
|
29
29
|
### 核心流程(run 命令)
|
|
30
30
|
|
|
@@ -46,17 +46,28 @@ run 命令有两种模式:
|
|
|
46
46
|
5. 每个任务完成时实时输出通知,全部完成后输出汇总
|
|
47
47
|
6. SIGINT(Ctrl+C)中断处理:`killAllChildProcesses()` 终止所有子进程 → 等待退出 → `handleInterruptCleanup()` 根据 `autoDeleteBranch` 配置自动或交互式清理 worktree 和分支
|
|
48
48
|
|
|
49
|
+
### resume 命令流程
|
|
50
|
+
|
|
51
|
+
1. `validateMainWorktree()` 确认在主 worktree 根目录
|
|
52
|
+
2. `validateClaudeCodeInstalled()` 确认 claude CLI 可用
|
|
53
|
+
3. `findWorktreeByBranch()` 在当前项目的 worktree 列表中按分支名查找已有 worktree
|
|
54
|
+
4. `launchInteractiveClaude()` 在目标 worktree 中启动 Claude Code 交互式界面
|
|
55
|
+
|
|
49
56
|
### validate + merge 工作流
|
|
50
57
|
|
|
51
|
-
- `validate`:将目标 worktree 的变更通过 git stash 迁移到主 worktree,便于在主 worktree
|
|
52
|
-
-
|
|
58
|
+
- `validate`:将目标 worktree 的变更通过 git stash 迁移到主 worktree,便于在主 worktree 中测试。支持两种模式:
|
|
59
|
+
- **首次 validate**(无历史快照):stash 迁移 → 保存纯净快照 patch → 结果:暂存区=空,工作目录=全量变更
|
|
60
|
+
- **增量 validate**(存在历史快照):读取旧 patch → 清空主 worktree → stash 迁移最新变更 → 保存新快照 → 旧 patch 应用到暂存区 → 结果:暂存区=上次快照,工作目录=最新变更(可通过 `git diff` 查看增量差异)
|
|
61
|
+
- `--clean` 选项:重置主 worktree + 删除对应快照文件
|
|
62
|
+
- 快照存储路径:`~/.clawt/validate-snapshots/<projectName>/<branchName>.patch`
|
|
63
|
+
- `merge`:检测目标 worktree 状态(有修改则需 `-m` 提交,已提交则跳过,无变更则报错)→ 合并到主 worktree → pull → push → 可选清理 worktree 和分支(受 `autoDeleteBranch` 配置或交互式确认控制)→ 清理对应的 validate 快照
|
|
53
64
|
- `run` 中断清理:Ctrl+C 终止所有子进程后,根据 `autoDeleteBranch` 配置自动清理或交互式确认清理本次创建的 worktree 和分支
|
|
54
65
|
|
|
55
66
|
### 目录层级
|
|
56
67
|
|
|
57
68
|
- `src/commands/` — 各命令的注册与处理逻辑
|
|
58
|
-
- `src/utils/` — 工具函数(git 操作、shell 执行与子进程管理、分支名处理、worktree
|
|
59
|
-
- `src/constants/` —
|
|
69
|
+
- `src/utils/` — 工具函数(git 操作、shell 执行与子进程管理、分支名处理、worktree 管理与批量清理、配置、格式化输出、交互式输入、Claude Code 交互式启动、validate 快照管理)
|
|
70
|
+
- `src/constants/` — 常量定义(路径、退出码、消息模板、分支规则、配置默认值、终端控制序列、validate 快照目录)
|
|
60
71
|
- `src/types/` — TypeScript 类型定义
|
|
61
72
|
- `src/errors/` — 自定义 `ClawtError` 错误类(携带退出码)
|
|
62
73
|
- `src/logger/` — winston 日志(按日期滚动,写入 `~/.clawt/logs/`)
|
|
@@ -65,7 +76,7 @@ run 命令有两种模式:
|
|
|
65
76
|
|
|
66
77
|
- 所有命令执行前都会调用 `validateMainWorktree()` 确保在主 worktree 根目录(`git rev-parse --git-common-dir === ".git"`)
|
|
67
78
|
- Worktree 统一存放在 `~/.clawt/worktrees/<projectName>/` 下
|
|
68
|
-
- 全局配置文件 `~/.clawt/config.json`,postinstall 时自动创建/合并,包含 `autoDeleteBranch`(是否自动删除分支)、`claudeCodeCommand`(Claude Code CLI
|
|
69
|
-
- shell 命令执行有同步(`execCommand` → `execSync
|
|
79
|
+
- 全局配置文件 `~/.clawt/config.json`,postinstall 时自动创建/合并,包含 `autoDeleteBranch`(是否自动删除分支)、`claudeCodeCommand`(Claude Code CLI 启动指令,用于 `run` 和 `resume` 的交互式界面)、`autoPullPush`(merge 后是否自动 pull/push)三个配置项。配置项以 `CONFIG_DEFINITIONS` 为单一数据源,`DEFAULT_CONFIG` 和 `CONFIG_DESCRIPTIONS` 均从中派生
|
|
80
|
+
- shell 命令执行有同步(`execCommand` → `execSync`)、异步(`spawnProcess` → `spawn`)和同步带 stdin(`execCommandWithInput` → `execFileSync`)三种方式
|
|
70
81
|
- 项目为纯 ESM(`"type": "module"`),模块导入需带 `.js` 后缀
|
|
71
82
|
- 分支名特殊字符会被 `sanitizeBranchName()` 自动清理
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ npm i -g clawt
|
|
|
12
12
|
|
|
13
13
|
- Node.js >= 18
|
|
14
14
|
- Git >= 2.15
|
|
15
|
-
- Claude Code CLI
|
|
15
|
+
- Claude Code CLI(`clawt run` 和 `clawt resume` 需要)
|
|
16
16
|
|
|
17
17
|
## 使用前提
|
|
18
18
|
|
|
@@ -73,20 +73,47 @@ clawt run -b feature-scheme \
|
|
|
73
73
|
clawt run -b feature-login
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
### `clawt resume` — 在已有 worktree 中恢复 Claude Code 会话
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
clawt resume -b <branchName>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| 参数 | 必填 | 说明 |
|
|
83
|
+
| ---- | ---- | ---- |
|
|
84
|
+
| `-b` | 是 | 要恢复的分支名 |
|
|
85
|
+
|
|
86
|
+
在之前通过 `clawt run` 或 `clawt create` 创建的 worktree 中重新打开 Claude Code 交互式界面,继续之前的工作。启动命令由配置项 `claudeCodeCommand` 指定(默认 `claude`)。
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# 在已有 worktree 中恢复会话
|
|
90
|
+
clawt resume -b feature-login
|
|
91
|
+
```
|
|
92
|
+
|
|
76
93
|
### `clawt validate` — 在主 worktree 验证分支变更
|
|
77
94
|
|
|
78
95
|
```bash
|
|
79
|
-
clawt validate -b <branchName>
|
|
96
|
+
clawt validate -b <branchName> [--clean]
|
|
80
97
|
```
|
|
81
98
|
|
|
82
99
|
| 参数 | 必填 | 说明 |
|
|
83
100
|
| ---- | ---- | ---- |
|
|
84
101
|
| `-b` | 是 | 要验证的分支名 |
|
|
102
|
+
| `--clean` | 否 | 清理 validate 状态(重置主 worktree 并删除快照) |
|
|
85
103
|
|
|
86
104
|
将目标 worktree 的变更通过 `git stash` 迁移到主 worktree,方便在主 worktree 中直接测试,无需重新安装依赖。
|
|
87
105
|
|
|
106
|
+
支持增量模式:首次 validate 后会自动保存快照,再次 validate 同一分支时会将上次快照应用到暂存区、最新变更保留在工作目录,用户可通过 `git diff` 查看两次 validate 之间的增量差异。使用 `--clean` 可清理 validate 状态(重置主 worktree 并删除快照文件)。
|
|
107
|
+
|
|
88
108
|
```bash
|
|
109
|
+
# 首次验证
|
|
89
110
|
clawt validate -b feature-scheme-1
|
|
111
|
+
|
|
112
|
+
# 再次验证(增量模式,可通过 git diff 查看增量差异)
|
|
113
|
+
clawt validate -b feature-scheme-1
|
|
114
|
+
|
|
115
|
+
# 清理 validate 状态
|
|
116
|
+
clawt validate -b feature-scheme-1 --clean
|
|
90
117
|
```
|
|
91
118
|
|
|
92
119
|
### `clawt merge` — 合并分支到主 worktree
|
|
@@ -162,7 +189,7 @@ clawt config
|
|
|
162
189
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
163
190
|
| ------ | ---- | ------ | ---- |
|
|
164
191
|
| `autoDeleteBranch` | `boolean` | `false` | 移除 worktree 时自动删除对应本地分支;merge 成功后自动清理 worktree 和分支;run 中断后自动清理本次创建的 worktree 和分支 |
|
|
165
|
-
| `claudeCodeCommand` | `string` | `"claude"` | Claude Code CLI 启动指令,用于 `clawt run` 不传 `--tasks`
|
|
192
|
+
| `claudeCodeCommand` | `string` | `"claude"` | Claude Code CLI 启动指令,用于 `clawt run` 不传 `--tasks` 时和 `clawt resume` 在 worktree 中打开交互式界面 |
|
|
166
193
|
| `autoPullPush` | `boolean` | `false` | merge 成功后是否自动执行 git pull 和 git push |
|
|
167
194
|
|
|
168
195
|
## 分支名规则
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ var CLAWT_HOME = join(homedir(), ".clawt");
|
|
|
13
13
|
var CONFIG_PATH = join(CLAWT_HOME, "config.json");
|
|
14
14
|
var LOGS_DIR = join(CLAWT_HOME, "logs");
|
|
15
15
|
var WORKTREES_DIR = join(CLAWT_HOME, "worktrees");
|
|
16
|
+
var VALIDATE_SNAPSHOTS_DIR = join(CLAWT_HOME, "validate-snapshots");
|
|
16
17
|
|
|
17
18
|
// src/constants/branch.ts
|
|
18
19
|
var INVALID_BRANCH_CHARS = /[\/\\.\s~:*?[\]^]+/g;
|
|
@@ -81,7 +82,18 @@ var MESSAGES = {
|
|
|
81
82
|
/** 创建数量参数无效 */
|
|
82
83
|
INVALID_COUNT: (value) => `\u65E0\u6548\u7684\u521B\u5EFA\u6570\u91CF: "${value}"\uFF0C\u8BF7\u8F93\u5165\u6B63\u6574\u6570`,
|
|
83
84
|
/** worktree 状态获取失败 */
|
|
84
|
-
WORKTREE_STATUS_UNAVAILABLE: "(\u72B6\u6001\u4E0D\u53EF\u7528)"
|
|
85
|
+
WORKTREE_STATUS_UNAVAILABLE: "(\u72B6\u6001\u4E0D\u53EF\u7528)",
|
|
86
|
+
/** 增量 validate 成功提示 */
|
|
87
|
+
INCREMENTAL_VALIDATE_SUCCESS: (branch) => `\u2713 \u5DF2\u5C06\u5206\u652F ${branch} \u7684\u6700\u65B0\u53D8\u66F4\u5E94\u7528\u5230\u4E3B worktree\uFF08\u589E\u91CF\u6A21\u5F0F\uFF09
|
|
88
|
+
\u6682\u5B58\u533A = \u4E0A\u6B21\u5FEB\u7167\uFF0C\u5DE5\u4F5C\u76EE\u5F55 = \u6700\u65B0\u53D8\u66F4`,
|
|
89
|
+
/** 增量 validate 降级为全量模式提示 */
|
|
90
|
+
INCREMENTAL_VALIDATE_FALLBACK: "\u589E\u91CF\u5BF9\u6BD4\u5931\u8D25\uFF0C\u5DF2\u964D\u7EA7\u4E3A\u5168\u91CF\u6A21\u5F0F",
|
|
91
|
+
/** validate 状态已清理 */
|
|
92
|
+
VALIDATE_CLEANED: (branch) => `\u2713 \u5206\u652F ${branch} \u7684 validate \u72B6\u6001\u5DF2\u6E05\u7406`,
|
|
93
|
+
/** 增量 validate 检测到脏状态,即将清空 */
|
|
94
|
+
INCREMENTAL_VALIDATE_RESET: "\u68C0\u6D4B\u5230\u4E0A\u6B21 validate \u7684\u6B8B\u7559\u72B6\u6001\uFF0C\u5C06\u6E05\u7A7A\u4E3B worktree \u5E76\u91CD\u65B0\u5E94\u7528",
|
|
95
|
+
/** merge 命令检测到 validate 状态的提示 */
|
|
96
|
+
MERGE_VALIDATE_STATE_HINT: (branch) => `\u4E3B worktree \u53EF\u80FD\u5B58\u5728 validate \u6B8B\u7559\u72B6\u6001\uFF0C\u53EF\u5148\u6267\u884C clawt validate -b ${branch} --clean \u6E05\u7406`
|
|
85
97
|
};
|
|
86
98
|
|
|
87
99
|
// src/constants/exitCodes.ts
|
|
@@ -168,7 +180,7 @@ var logger = winston.createLogger({
|
|
|
168
180
|
});
|
|
169
181
|
|
|
170
182
|
// src/utils/shell.ts
|
|
171
|
-
import { execSync, spawn } from "child_process";
|
|
183
|
+
import { execSync, execFileSync, spawn } from "child_process";
|
|
172
184
|
function execCommand(command, options) {
|
|
173
185
|
logger.debug(`\u6267\u884C\u547D\u4EE4: ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ""}`);
|
|
174
186
|
const result = execSync(command, {
|
|
@@ -192,9 +204,20 @@ function killAllChildProcesses(children) {
|
|
|
192
204
|
}
|
|
193
205
|
}
|
|
194
206
|
}
|
|
207
|
+
function execCommandWithInput(command, args, options) {
|
|
208
|
+
logger.debug(`\u6267\u884C\u547D\u4EE4(stdin): ${command} ${args.join(" ")}${options.cwd ? ` (cwd: ${options.cwd})` : ""}`);
|
|
209
|
+
const result = execFileSync(command, args, {
|
|
210
|
+
cwd: options.cwd,
|
|
211
|
+
input: options.input,
|
|
212
|
+
encoding: "utf-8",
|
|
213
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
214
|
+
});
|
|
215
|
+
return result.trim();
|
|
216
|
+
}
|
|
195
217
|
|
|
196
218
|
// src/utils/git.ts
|
|
197
219
|
import { basename } from "path";
|
|
220
|
+
import { execSync as execSync2 } from "child_process";
|
|
198
221
|
function getGitCommonDir(cwd) {
|
|
199
222
|
return execCommand("git rev-parse --git-common-dir", { cwd });
|
|
200
223
|
}
|
|
@@ -316,6 +339,16 @@ function getDiffStat(branchName, worktreePath, cwd) {
|
|
|
316
339
|
deletions: committed.deletions + uncommitted.deletions
|
|
317
340
|
};
|
|
318
341
|
}
|
|
342
|
+
function gitDiffCachedBinary(cwd) {
|
|
343
|
+
logger.debug(`\u6267\u884C\u547D\u4EE4: git diff --cached --binary${cwd ? ` (cwd: ${cwd})` : ""}`);
|
|
344
|
+
return execSync2("git diff --cached --binary", {
|
|
345
|
+
cwd,
|
|
346
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
function gitApplyCachedFromStdin(patchContent, cwd) {
|
|
350
|
+
execCommandWithInput("git", ["apply", "--cached"], { input: patchContent, cwd });
|
|
351
|
+
}
|
|
319
352
|
|
|
320
353
|
// src/utils/formatter.ts
|
|
321
354
|
import chalk from "chalk";
|
|
@@ -537,6 +570,63 @@ function ensureClawtDirs() {
|
|
|
537
570
|
// src/utils/prompt.ts
|
|
538
571
|
import Enquirer from "enquirer";
|
|
539
572
|
|
|
573
|
+
// src/utils/claude.ts
|
|
574
|
+
import { spawnSync } from "child_process";
|
|
575
|
+
function launchInteractiveClaude(worktree) {
|
|
576
|
+
const commandStr = getConfigValue("claudeCodeCommand");
|
|
577
|
+
const parts = commandStr.split(/\s+/).filter(Boolean);
|
|
578
|
+
const cmd = parts[0];
|
|
579
|
+
const args = [
|
|
580
|
+
...parts.slice(1),
|
|
581
|
+
"--append-system-prompt",
|
|
582
|
+
APPEND_SYSTEM_PROMPT
|
|
583
|
+
];
|
|
584
|
+
printInfo(`\u6B63\u5728 worktree \u4E2D\u542F\u52A8 Claude Code \u4EA4\u4E92\u5F0F\u754C\u9762...`);
|
|
585
|
+
printInfo(` \u5206\u652F: ${worktree.branch}`);
|
|
586
|
+
printInfo(` \u8DEF\u5F84: ${worktree.path}`);
|
|
587
|
+
printInfo(` \u6307\u4EE4: ${commandStr}`);
|
|
588
|
+
printInfo("");
|
|
589
|
+
const result = spawnSync(cmd, args, {
|
|
590
|
+
cwd: worktree.path,
|
|
591
|
+
stdio: "inherit"
|
|
592
|
+
});
|
|
593
|
+
if (result.error) {
|
|
594
|
+
throw new ClawtError(`\u542F\u52A8 Claude Code \u5931\u8D25: ${result.error.message}`);
|
|
595
|
+
}
|
|
596
|
+
if (result.status !== null && result.status !== 0) {
|
|
597
|
+
printWarning(`Claude Code \u9000\u51FA\u7801: ${result.status}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/utils/validate-snapshot.ts
|
|
602
|
+
import { join as join3 } from "path";
|
|
603
|
+
import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync3, rmdirSync as rmdirSync2 } from "fs";
|
|
604
|
+
function getSnapshotPath(projectName, branchName) {
|
|
605
|
+
return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.patch`);
|
|
606
|
+
}
|
|
607
|
+
function hasSnapshot(projectName, branchName) {
|
|
608
|
+
return existsSync5(getSnapshotPath(projectName, branchName));
|
|
609
|
+
}
|
|
610
|
+
function readSnapshot(projectName, branchName) {
|
|
611
|
+
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
612
|
+
logger.debug(`\u8BFB\u53D6 validate \u5FEB\u7167: ${snapshotPath}`);
|
|
613
|
+
return readFileSync2(snapshotPath);
|
|
614
|
+
}
|
|
615
|
+
function writeSnapshot(projectName, branchName, patch) {
|
|
616
|
+
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
617
|
+
const snapshotDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
618
|
+
ensureDir(snapshotDir);
|
|
619
|
+
writeFileSync2(snapshotPath, patch);
|
|
620
|
+
logger.info(`\u5DF2\u4FDD\u5B58 validate \u5FEB\u7167: ${snapshotPath}`);
|
|
621
|
+
}
|
|
622
|
+
function removeSnapshot(projectName, branchName) {
|
|
623
|
+
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
624
|
+
if (existsSync5(snapshotPath)) {
|
|
625
|
+
unlinkSync(snapshotPath);
|
|
626
|
+
logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${snapshotPath}`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
540
630
|
// src/commands/list.ts
|
|
541
631
|
import chalk2 from "chalk";
|
|
542
632
|
function registerListCommand(program2) {
|
|
@@ -596,7 +686,7 @@ function handleCreate(options) {
|
|
|
596
686
|
}
|
|
597
687
|
|
|
598
688
|
// src/commands/remove.ts
|
|
599
|
-
import { join as
|
|
689
|
+
import { join as join4 } from "path";
|
|
600
690
|
function registerRemoveCommand(program2) {
|
|
601
691
|
program2.command("remove").description("\u79FB\u9664 worktree\uFF08\u652F\u6301\u5355\u4E2A/\u6279\u91CF/\u5168\u90E8\uFF09").option("--all", "\u79FB\u9664\u5F53\u524D\u9879\u76EE\u4E0B\u6240\u6709 worktree").option("-b, --branch <branchName>", "\u6307\u5B9A\u5206\u652F\u540D").option("-i, --index <index>", "\u6307\u5B9A\u7D22\u5F15\uFF08\u914D\u5408 -b \u4F7F\u7528\uFF09").action(async (options) => {
|
|
602
692
|
await handleRemove(options);
|
|
@@ -613,7 +703,7 @@ function resolveWorktreesToRemove(options) {
|
|
|
613
703
|
}
|
|
614
704
|
if (options.index !== void 0) {
|
|
615
705
|
const targetName = `${options.branch}-${options.index}`;
|
|
616
|
-
const targetPath =
|
|
706
|
+
const targetPath = join4(projectDir, targetName);
|
|
617
707
|
const found = allWorktrees.find((wt) => wt.path === targetPath);
|
|
618
708
|
if (!found) {
|
|
619
709
|
throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(targetName));
|
|
@@ -665,37 +755,11 @@ async function handleRemove(options) {
|
|
|
665
755
|
}
|
|
666
756
|
|
|
667
757
|
// src/commands/run.ts
|
|
668
|
-
import { spawnSync } from "child_process";
|
|
669
758
|
function registerRunCommand(program2) {
|
|
670
759
|
program2.command("run").description("\u6279\u91CF\u521B\u5EFA worktree \u5E76\u542F\u52A8 Claude Code \u6267\u884C\u4EFB\u52A1").requiredOption("-b, --branch <branchName>", "\u5206\u652F\u540D").option("--tasks <task...>", "\u4EFB\u52A1\u5217\u8868\uFF08\u53EF\u591A\u6B21\u6307\u5B9A\uFF09\uFF0C\u4E0D\u4F20\u5219\u5728 worktree \u4E2D\u6253\u5F00 Claude Code \u4EA4\u4E92\u5F0F\u754C\u9762").action(async (options) => {
|
|
671
760
|
await handleRun(options);
|
|
672
761
|
});
|
|
673
762
|
}
|
|
674
|
-
function launchInteractiveClaude(worktree) {
|
|
675
|
-
const commandStr = getConfigValue("claudeCodeCommand");
|
|
676
|
-
const parts = commandStr.split(/\s+/).filter(Boolean);
|
|
677
|
-
const cmd = parts[0];
|
|
678
|
-
const args = [
|
|
679
|
-
...parts.slice(1),
|
|
680
|
-
"--append-system-prompt",
|
|
681
|
-
APPEND_SYSTEM_PROMPT
|
|
682
|
-
];
|
|
683
|
-
printInfo(`\u6B63\u5728 worktree \u4E2D\u542F\u52A8 Claude Code \u4EA4\u4E92\u5F0F\u754C\u9762...`);
|
|
684
|
-
printInfo(` \u5206\u652F: ${worktree.branch}`);
|
|
685
|
-
printInfo(` \u8DEF\u5F84: ${worktree.path}`);
|
|
686
|
-
printInfo(` \u6307\u4EE4: ${commandStr}`);
|
|
687
|
-
printInfo("");
|
|
688
|
-
const result = spawnSync(cmd, args, {
|
|
689
|
-
cwd: worktree.path,
|
|
690
|
-
stdio: "inherit"
|
|
691
|
-
});
|
|
692
|
-
if (result.error) {
|
|
693
|
-
throw new ClawtError(`\u542F\u52A8 Claude Code \u5931\u8D25: ${result.error.message}`);
|
|
694
|
-
}
|
|
695
|
-
if (result.status !== null && result.status !== 0) {
|
|
696
|
-
printWarning(`Claude Code \u9000\u51FA\u7801: ${result.status}`);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
763
|
function executeClaudeTask(worktree, task) {
|
|
700
764
|
const child = spawnProcess(
|
|
701
765
|
"claude",
|
|
@@ -856,12 +920,34 @@ async function handleRun(options) {
|
|
|
856
920
|
printTaskSummary(summary);
|
|
857
921
|
}
|
|
858
922
|
|
|
923
|
+
// src/commands/resume.ts
|
|
924
|
+
function registerResumeCommand(program2) {
|
|
925
|
+
program2.command("resume").description("\u5728\u5DF2\u6709 worktree \u4E2D\u6062\u590D Claude Code \u4EA4\u4E92\u5F0F\u4F1A\u8BDD").requiredOption("-b, --branch <branchName>", "\u8981\u6062\u590D\u7684\u5206\u652F\u540D").action(async (options) => {
|
|
926
|
+
await handleResume(options);
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
function findWorktreeByBranch(branchName) {
|
|
930
|
+
const worktrees = getProjectWorktrees();
|
|
931
|
+
const matched = worktrees.find((wt) => wt.branch === branchName);
|
|
932
|
+
if (!matched) {
|
|
933
|
+
throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(branchName));
|
|
934
|
+
}
|
|
935
|
+
return matched;
|
|
936
|
+
}
|
|
937
|
+
async function handleResume(options) {
|
|
938
|
+
validateMainWorktree();
|
|
939
|
+
validateClaudeCodeInstalled();
|
|
940
|
+
logger.info(`resume \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch}`);
|
|
941
|
+
const worktree = findWorktreeByBranch(options.branch);
|
|
942
|
+
launchInteractiveClaude(worktree);
|
|
943
|
+
}
|
|
944
|
+
|
|
859
945
|
// src/commands/validate.ts
|
|
860
|
-
import { join as
|
|
861
|
-
import { existsSync as
|
|
946
|
+
import { join as join5 } from "path";
|
|
947
|
+
import { existsSync as existsSync6 } from "fs";
|
|
862
948
|
import Enquirer2 from "enquirer";
|
|
863
949
|
function registerValidateCommand(program2) {
|
|
864
|
-
program2.command("validate").description("\u5728\u4E3B worktree \u9A8C\u8BC1\u67D0\u4E2A worktree \u5206\u652F\u7684\u53D8\u66F4").requiredOption("-b, --branch <branchName>", "\u8981\u9A8C\u8BC1\u7684\u5206\u652F\u540D").action(async (options) => {
|
|
950
|
+
program2.command("validate").description("\u5728\u4E3B worktree \u9A8C\u8BC1\u67D0\u4E2A worktree \u5206\u652F\u7684\u53D8\u66F4").requiredOption("-b, --branch <branchName>", "\u8981\u9A8C\u8BC1\u7684\u5206\u652F\u540D").option("--clean", "\u6E05\u7406 validate \u72B6\u6001\uFF08\u91CD\u7F6E\u4E3B worktree \u5E76\u5220\u9664\u5FEB\u7167\uFF09").action(async (options) => {
|
|
865
951
|
await handleValidate(options);
|
|
866
952
|
});
|
|
867
953
|
}
|
|
@@ -899,24 +985,8 @@ async function handleDirtyMainWorktree(mainWorktreePath) {
|
|
|
899
985
|
throw new ClawtError("\u5DE5\u4F5C\u533A\u4ECD\u7136\u4E0D\u5E72\u51C0\uFF0C\u8BF7\u624B\u52A8\u5904\u7406");
|
|
900
986
|
}
|
|
901
987
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
const projectName = getProjectName();
|
|
905
|
-
const mainWorktreePath = getGitTopLevel();
|
|
906
|
-
const projectDir = getProjectWorktreeDir();
|
|
907
|
-
const targetWorktreePath = join4(projectDir, options.branch);
|
|
908
|
-
logger.info(`validate \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch}`);
|
|
909
|
-
if (!existsSync5(targetWorktreePath)) {
|
|
910
|
-
throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
|
|
911
|
-
}
|
|
912
|
-
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
913
|
-
await handleDirtyMainWorktree(mainWorktreePath);
|
|
914
|
-
}
|
|
915
|
-
if (isWorkingDirClean(targetWorktreePath)) {
|
|
916
|
-
printInfo(MESSAGES.TARGET_WORKTREE_CLEAN);
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
const stashMessage = `clawt:validate:${options.branch}`;
|
|
988
|
+
function migrateChangesViaStash(targetWorktreePath, mainWorktreePath, branchName) {
|
|
989
|
+
const stashMessage = `clawt:validate:${branchName}`;
|
|
920
990
|
gitAddAll(targetWorktreePath);
|
|
921
991
|
gitStashPush(stashMessage, targetWorktreePath);
|
|
922
992
|
gitStashApply(targetWorktreePath);
|
|
@@ -927,12 +997,86 @@ async function handleValidate(options) {
|
|
|
927
997
|
throw new ClawtError(MESSAGES.STASH_CHANGED);
|
|
928
998
|
}
|
|
929
999
|
gitStashPop(0, mainWorktreePath);
|
|
930
|
-
|
|
1000
|
+
}
|
|
1001
|
+
function saveCurrentSnapshotPatch(mainWorktreePath, projectName, branchName) {
|
|
1002
|
+
gitAddAll(mainWorktreePath);
|
|
1003
|
+
const patch = gitDiffCachedBinary(mainWorktreePath);
|
|
1004
|
+
gitRestoreStaged(mainWorktreePath);
|
|
1005
|
+
writeSnapshot(projectName, branchName, patch);
|
|
1006
|
+
return patch;
|
|
1007
|
+
}
|
|
1008
|
+
function handleValidateClean(options) {
|
|
1009
|
+
validateMainWorktree();
|
|
1010
|
+
const projectName = getProjectName();
|
|
1011
|
+
const mainWorktreePath = getGitTopLevel();
|
|
1012
|
+
logger.info(`validate --clean \u6267\u884C\uFF0C\u5206\u652F: ${options.branch}`);
|
|
1013
|
+
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
1014
|
+
gitResetHard(mainWorktreePath);
|
|
1015
|
+
gitCleanForce(mainWorktreePath);
|
|
1016
|
+
}
|
|
1017
|
+
removeSnapshot(projectName, options.branch);
|
|
1018
|
+
printSuccess(MESSAGES.VALIDATE_CLEANED(options.branch));
|
|
1019
|
+
}
|
|
1020
|
+
function handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName) {
|
|
1021
|
+
migrateChangesViaStash(targetWorktreePath, mainWorktreePath, branchName);
|
|
1022
|
+
saveCurrentSnapshotPatch(mainWorktreePath, projectName, branchName);
|
|
1023
|
+
printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
|
|
1024
|
+
}
|
|
1025
|
+
function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName) {
|
|
1026
|
+
const oldPatch = readSnapshot(projectName, branchName);
|
|
1027
|
+
printInfo(MESSAGES.INCREMENTAL_VALIDATE_RESET);
|
|
1028
|
+
gitResetHard(mainWorktreePath);
|
|
1029
|
+
gitCleanForce(mainWorktreePath);
|
|
1030
|
+
migrateChangesViaStash(targetWorktreePath, mainWorktreePath, branchName);
|
|
1031
|
+
const newPatch = saveCurrentSnapshotPatch(mainWorktreePath, projectName, branchName);
|
|
1032
|
+
if (oldPatch.length > 0) {
|
|
1033
|
+
try {
|
|
1034
|
+
gitApplyCachedFromStdin(oldPatch, mainWorktreePath);
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
logger.warn(`\u589E\u91CF apply \u5931\u8D25: ${error}`);
|
|
1037
|
+
printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
|
|
1038
|
+
printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
printSuccess(MESSAGES.INCREMENTAL_VALIDATE_SUCCESS(branchName));
|
|
1043
|
+
}
|
|
1044
|
+
async function handleValidate(options) {
|
|
1045
|
+
if (options.clean) {
|
|
1046
|
+
handleValidateClean(options);
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
validateMainWorktree();
|
|
1050
|
+
const projectName = getProjectName();
|
|
1051
|
+
const mainWorktreePath = getGitTopLevel();
|
|
1052
|
+
const projectDir = getProjectWorktreeDir();
|
|
1053
|
+
const targetWorktreePath = join5(projectDir, options.branch);
|
|
1054
|
+
logger.info(`validate \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch}`);
|
|
1055
|
+
if (!existsSync6(targetWorktreePath)) {
|
|
1056
|
+
throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
|
|
1057
|
+
}
|
|
1058
|
+
const isIncremental = hasSnapshot(projectName, options.branch);
|
|
1059
|
+
if (isIncremental) {
|
|
1060
|
+
if (isWorkingDirClean(targetWorktreePath)) {
|
|
1061
|
+
printInfo(MESSAGES.TARGET_WORKTREE_CLEAN);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, options.branch);
|
|
1065
|
+
} else {
|
|
1066
|
+
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
1067
|
+
await handleDirtyMainWorktree(mainWorktreePath);
|
|
1068
|
+
}
|
|
1069
|
+
if (isWorkingDirClean(targetWorktreePath)) {
|
|
1070
|
+
printInfo(MESSAGES.TARGET_WORKTREE_CLEAN);
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, options.branch);
|
|
1074
|
+
}
|
|
931
1075
|
}
|
|
932
1076
|
|
|
933
1077
|
// src/commands/merge.ts
|
|
934
|
-
import { join as
|
|
935
|
-
import { existsSync as
|
|
1078
|
+
import { join as join6 } from "path";
|
|
1079
|
+
import { existsSync as existsSync7 } from "fs";
|
|
936
1080
|
function registerMergeCommand(program2) {
|
|
937
1081
|
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) => {
|
|
938
1082
|
await handleMerge(options);
|
|
@@ -954,12 +1098,16 @@ async function handleMerge(options) {
|
|
|
954
1098
|
validateMainWorktree();
|
|
955
1099
|
const mainWorktreePath = getGitTopLevel();
|
|
956
1100
|
const projectDir = getProjectWorktreeDir();
|
|
957
|
-
const targetWorktreePath =
|
|
1101
|
+
const targetWorktreePath = join6(projectDir, options.branch);
|
|
958
1102
|
logger.info(`merge \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch}\uFF0C\u63D0\u4EA4\u4FE1\u606F: ${options.message ?? "(\u672A\u63D0\u4F9B)"}`);
|
|
959
|
-
if (!
|
|
1103
|
+
if (!existsSync7(targetWorktreePath)) {
|
|
960
1104
|
throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
|
|
961
1105
|
}
|
|
1106
|
+
const projectName = getProjectName();
|
|
962
1107
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
1108
|
+
if (hasSnapshot(projectName, options.branch)) {
|
|
1109
|
+
printWarning(MESSAGES.MERGE_VALIDATE_STATE_HINT(options.branch));
|
|
1110
|
+
}
|
|
963
1111
|
throw new ClawtError(MESSAGES.MAIN_WORKTREE_DIRTY);
|
|
964
1112
|
}
|
|
965
1113
|
const shouldCleanup = await shouldCleanupAfterMerge(options.branch);
|
|
@@ -1000,6 +1148,9 @@ async function handleMerge(options) {
|
|
|
1000
1148
|
if (shouldCleanup) {
|
|
1001
1149
|
cleanupWorktreeAndBranch(targetWorktreePath, options.branch);
|
|
1002
1150
|
}
|
|
1151
|
+
if (hasSnapshot(projectName, options.branch)) {
|
|
1152
|
+
removeSnapshot(projectName, options.branch);
|
|
1153
|
+
}
|
|
1003
1154
|
}
|
|
1004
1155
|
|
|
1005
1156
|
// src/commands/config.ts
|
|
@@ -1046,6 +1197,7 @@ registerListCommand(program);
|
|
|
1046
1197
|
registerCreateCommand(program);
|
|
1047
1198
|
registerRemoveCommand(program);
|
|
1048
1199
|
registerRunCommand(program);
|
|
1200
|
+
registerResumeCommand(program);
|
|
1049
1201
|
registerValidateCommand(program);
|
|
1050
1202
|
registerMergeCommand(program);
|
|
1051
1203
|
registerConfigCommand(program);
|
package/dist/postinstall.js
CHANGED
|
@@ -10,6 +10,7 @@ var CLAWT_HOME = join(homedir(), ".clawt");
|
|
|
10
10
|
var CONFIG_PATH = join(CLAWT_HOME, "config.json");
|
|
11
11
|
var LOGS_DIR = join(CLAWT_HOME, "logs");
|
|
12
12
|
var WORKTREES_DIR = join(CLAWT_HOME, "worktrees");
|
|
13
|
+
var VALIDATE_SNAPSHOTS_DIR = join(CLAWT_HOME, "validate-snapshots");
|
|
13
14
|
|
|
14
15
|
// src/constants/config.ts
|
|
15
16
|
var CONFIG_DEFINITIONS = {
|