clawt 2.6.0 → 2.7.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 -3
- package/README.md +50 -7
- package/dist/index.js +139 -92
- package/docs/spec.md +77 -26
- package/package.json +1 -1
- package/src/commands/merge.ts +28 -22
- package/src/commands/resume.ts +13 -93
- package/src/commands/sync.ts +17 -13
- package/src/commands/validate.ts +31 -19
- package/src/constants/messages.ts +27 -0
- package/src/types/command.ts +6 -6
- package/src/utils/index.ts +2 -0
- package/src/utils/worktree-matcher.ts +111 -0
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
- 完整的软件规格说明,包含 7 大章节
|
|
7
7
|
- 命令流程在 `5. 需求场景详细设计` 下,每个命令一个子章节(5.1-5.13)
|
|
8
8
|
- run 命令对应 `5.2 批量创建 Worktree + 执行 Claude Code 任务`,流程按步骤编号描述
|
|
9
|
-
- merge 命令对应 `5.6
|
|
9
|
+
- merge 命令对应 `5.6 合并验证过的分支`,-b 可选,支持模糊匹配(与 resume/validate 共享匹配逻辑),流程按步骤编号描述
|
|
10
10
|
- config 命令对应 `5.10 查看全局配置`,只读展示配置
|
|
11
11
|
- resume 命令对应 `5.11 在已有 Worktree 中恢复会话`,支持模糊匹配和交互式分支选择(-b 可选)
|
|
12
|
+
- validate 命令对应 `5.4 在主 Worktree 验证其他分支`,-b 可选,支持模糊匹配(与 resume 共享匹配逻辑)
|
|
13
|
+
- sync 命令对应 `5.12 将主分支代码同步到目标 Worktree`,-b 可选,支持模糊匹配(与 resume/validate/merge 共享匹配逻辑)
|
|
12
14
|
- 配置项说明在 `5.7 默认配置文件` 章节的表格中
|
|
13
15
|
- 更新模式:新增步骤时追加编号,配置项影响范围变化时更新说明列
|
|
14
16
|
|
|
@@ -65,8 +67,11 @@ Notes:
|
|
|
65
67
|
- resume 和 run(交互式模式)共用 `launchInteractiveClaude()`,该函数从 run.ts 提取到 src/utils/claude.ts
|
|
66
68
|
- `claudeCodeCommand` 配置项同时影响 run 交互式模式和 resume 命令
|
|
67
69
|
- reset 命令与 validate --clean 的区别:reset 不删除快照文件,validate --clean 会删除快照
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
+
- `resolveTargetWorktree()` 是 resume、validate、merge 和 sync 共用的分支匹配函数(在 src/utils/worktree-matcher.ts)
|
|
71
|
+
- `WorktreeResolveMessages` 接口实现命令间消息解耦,每个命令传入各自的提示文案
|
|
72
|
+
- resume 的消息常量在 `MESSAGES.RESUME_*`,validate 的消息常量在 `MESSAGES.VALIDATE_*`,merge 的消息常量在 `MESSAGES.MERGE_*`,sync 的消息常量在 `MESSAGES.SYNC_*`
|
|
73
|
+
- resume、validate、merge 和 sync 的 `-b` 参数均为可选,匹配策略一致:精确→模糊(子串,大小写不敏感)→交互选择
|
|
74
|
+
- validate 的交互式选择和 resume 使用同一个 `promptSelectBranch()`(Enquirer.Select)
|
|
70
75
|
|
|
71
76
|
## validate 快照机制
|
|
72
77
|
|
package/README.md
CHANGED
|
@@ -107,67 +107,110 @@ 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
|
|
|
124
|
+
**分支匹配策略:**
|
|
125
|
+
- 传 `-b` 时,优先精确匹配分支名;未精确匹配则进行模糊匹配(子串匹配,大小写不敏感);模糊匹配到多个时通过交互列表选择;无匹配时报错并列出所有可用分支
|
|
126
|
+
- 不传 `-b` 时,列出当前项目所有可用分支供交互式选择(仅 1 个时自动使用)
|
|
127
|
+
|
|
120
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
|
|
136
151
|
|
|
137
152
|
```bash
|
|
153
|
+
# 指定分支名(支持模糊匹配)
|
|
138
154
|
clawt sync -b <branchName>
|
|
155
|
+
|
|
156
|
+
# 不指定分支名(列出所有分支供选择)
|
|
157
|
+
clawt sync
|
|
139
158
|
```
|
|
140
159
|
|
|
141
160
|
| 参数 | 必填 | 说明 |
|
|
142
161
|
| ---- | ---- | ---- |
|
|
143
|
-
| `-b` |
|
|
162
|
+
| `-b` | 否 | 要同步的分支名(支持模糊匹配,不传则列出所有分支供选择) |
|
|
144
163
|
|
|
145
164
|
将主分支最新代码合并到目标 worktree 的分支中。如果目标 worktree 有未提交的修改,会自动保存后再合并。存在冲突时会提示用户手动解决。合并成功后会自动清除该分支的 validate 快照(代码基础已变化,旧快照无效)。
|
|
146
165
|
|
|
166
|
+
**分支匹配策略:**
|
|
167
|
+
- 传 `-b` 时,优先精确匹配分支名;未精确匹配则进行模糊匹配(子串匹配,大小写不敏感);模糊匹配到多个时通过交互列表选择;无匹配时报错并列出所有可用分支
|
|
168
|
+
- 不传 `-b` 时,列出当前项目所有可用分支供交互式选择(仅 1 个时自动使用)
|
|
169
|
+
|
|
147
170
|
```bash
|
|
148
|
-
#
|
|
171
|
+
# 精确匹配分支名
|
|
149
172
|
clawt sync -b feature-scheme-1
|
|
173
|
+
|
|
174
|
+
# 模糊匹配(匹配包含 "scheme" 的分支)
|
|
175
|
+
clawt sync -b scheme
|
|
176
|
+
|
|
177
|
+
# 交互式选择所有分支
|
|
178
|
+
clawt sync
|
|
150
179
|
```
|
|
151
180
|
|
|
152
181
|
### `clawt merge` — 合并分支到主 worktree
|
|
153
182
|
|
|
154
183
|
```bash
|
|
184
|
+
# 指定分支名(支持模糊匹配)
|
|
155
185
|
clawt merge -b <branchName> [-m <commitMessage>]
|
|
186
|
+
|
|
187
|
+
# 不指定分支名(列出所有分支供选择)
|
|
188
|
+
clawt merge [-m <commitMessage>]
|
|
156
189
|
```
|
|
157
190
|
|
|
158
191
|
| 参数 | 必填 | 说明 |
|
|
159
192
|
| ---- | ---- | ---- |
|
|
160
|
-
| `-b` |
|
|
193
|
+
| `-b` | 否 | 要合并的分支名(支持模糊匹配,不传则列出所有分支供选择) |
|
|
161
194
|
| `-m` | 否 | 提交信息(目标 worktree 工作区有修改时必填) |
|
|
162
195
|
|
|
163
196
|
将目标 worktree 的变更合并到主 worktree 的当前分支。如果配置了 `autoPullPush: true`,合并后会自动推送到远程仓库。如果目标 worktree 工作区有未提交的修改,需要通过 `-m` 提供提交信息;如果目标 worktree 已经提交过(工作区干净但有本地提交),可以省略 `-m` 直接合并。merge 成功后会询问是否清理对应的 worktree 和分支(如果配置了 `autoDeleteBranch: true` 则自动清理)。
|
|
164
197
|
|
|
198
|
+
**分支匹配策略:**
|
|
199
|
+
- 传 `-b` 时,优先精确匹配分支名;未精确匹配则进行模糊匹配(子串匹配,大小写不敏感);模糊匹配到多个时通过交互列表选择;无匹配时报错并列出所有可用分支
|
|
200
|
+
- 不传 `-b` 时,列出当前项目所有可用分支供交互式选择(仅 1 个时自动使用)
|
|
201
|
+
|
|
165
202
|
如果检测到目标分支存在 `clawt sync` 产生的临时提交(auto-save commit),会自动提示是否将所有提交压缩(squash)为一个。用户选择压缩后,所有 commit 会被 reset 到暂存区:如果提供了 `-m` 则直接提交并继续合并流程;如果未提供 `-m` 则提示用户前往目标 worktree 自行提交后重新执行 merge。
|
|
166
203
|
|
|
167
204
|
```bash
|
|
168
|
-
#
|
|
205
|
+
# 精确匹配,目标 worktree 有未提交修改,需提供 -m
|
|
169
206
|
clawt merge -b feature-scheme-1 -m "feat: 实现用户登录功能"
|
|
170
207
|
|
|
208
|
+
# 模糊匹配(匹配包含 "scheme" 的分支)
|
|
209
|
+
clawt merge -b scheme
|
|
210
|
+
|
|
211
|
+
# 交互式选择所有分支
|
|
212
|
+
clawt merge
|
|
213
|
+
|
|
171
214
|
# 目标 worktree 已提交过,可省略 -m
|
|
172
215
|
clawt merge -b feature-scheme-1
|
|
173
216
|
```
|
package/dist/index.js
CHANGED
|
@@ -134,7 +134,37 @@ ${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`,
|
|
148
|
+
/** merge 无可用 worktree */
|
|
149
|
+
MERGE_NO_WORKTREES: "\u5F53\u524D\u9879\u76EE\u6CA1\u6709\u53EF\u7528\u7684 worktree\uFF0C\u8BF7\u5148\u901A\u8FC7 clawt run \u6216 clawt create \u521B\u5EFA",
|
|
150
|
+
/** merge 模糊匹配无结果,列出可用分支 */
|
|
151
|
+
MERGE_NO_MATCH: (name, branches) => `\u672A\u627E\u5230\u4E0E "${name}" \u5339\u914D\u7684\u5206\u652F
|
|
152
|
+
\u53EF\u7528\u5206\u652F\uFF1A
|
|
153
|
+
${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
154
|
+
/** merge 交互选择提示 */
|
|
155
|
+
MERGE_SELECT_BRANCH: "\u8BF7\u9009\u62E9\u8981\u5408\u5E76\u7684\u5206\u652F",
|
|
156
|
+
/** merge 模糊匹配到多个结果提示 */
|
|
157
|
+
MERGE_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`,
|
|
158
|
+
/** sync 无可用 worktree */
|
|
159
|
+
SYNC_NO_WORKTREES: "\u5F53\u524D\u9879\u76EE\u6CA1\u6709\u53EF\u7528\u7684 worktree\uFF0C\u8BF7\u5148\u901A\u8FC7 clawt run \u6216 clawt create \u521B\u5EFA",
|
|
160
|
+
/** sync 模糊匹配无结果,列出可用分支 */
|
|
161
|
+
SYNC_NO_MATCH: (name, branches) => `\u672A\u627E\u5230\u4E0E "${name}" \u5339\u914D\u7684\u5206\u652F
|
|
162
|
+
\u53EF\u7528\u5206\u652F\uFF1A
|
|
163
|
+
${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
164
|
+
/** sync 交互选择提示 */
|
|
165
|
+
SYNC_SELECT_BRANCH: "\u8BF7\u9009\u62E9\u8981\u540C\u6B65\u7684\u5206\u652F",
|
|
166
|
+
/** sync 模糊匹配到多个结果提示 */
|
|
167
|
+
SYNC_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`
|
|
138
168
|
};
|
|
139
169
|
|
|
140
170
|
// src/constants/exitCodes.ts
|
|
@@ -742,6 +772,50 @@ function removeProjectSnapshots(projectName) {
|
|
|
742
772
|
logger.info(`\u5DF2\u5220\u9664\u9879\u76EE ${projectName} \u7684\u6240\u6709 validate \u5FEB\u7167`);
|
|
743
773
|
}
|
|
744
774
|
|
|
775
|
+
// src/utils/worktree-matcher.ts
|
|
776
|
+
import Enquirer2 from "enquirer";
|
|
777
|
+
function findExactMatch(worktrees, branchName) {
|
|
778
|
+
return worktrees.find((wt) => wt.branch === branchName);
|
|
779
|
+
}
|
|
780
|
+
function findFuzzyMatches(worktrees, keyword) {
|
|
781
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
782
|
+
return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
|
|
783
|
+
}
|
|
784
|
+
async function promptSelectBranch(worktrees, message) {
|
|
785
|
+
const selectedBranch = await new Enquirer2.Select({
|
|
786
|
+
message,
|
|
787
|
+
choices: worktrees.map((wt) => ({
|
|
788
|
+
name: wt.branch,
|
|
789
|
+
message: wt.branch
|
|
790
|
+
}))
|
|
791
|
+
}).run();
|
|
792
|
+
return worktrees.find((wt) => wt.branch === selectedBranch);
|
|
793
|
+
}
|
|
794
|
+
async function resolveTargetWorktree(worktrees, messages, branchName) {
|
|
795
|
+
if (worktrees.length === 0) {
|
|
796
|
+
throw new ClawtError(messages.noWorktrees);
|
|
797
|
+
}
|
|
798
|
+
if (!branchName) {
|
|
799
|
+
if (worktrees.length === 1) {
|
|
800
|
+
return worktrees[0];
|
|
801
|
+
}
|
|
802
|
+
return promptSelectBranch(worktrees, messages.selectBranch);
|
|
803
|
+
}
|
|
804
|
+
const exactMatch = findExactMatch(worktrees, branchName);
|
|
805
|
+
if (exactMatch) {
|
|
806
|
+
return exactMatch;
|
|
807
|
+
}
|
|
808
|
+
const fuzzyMatches = findFuzzyMatches(worktrees, branchName);
|
|
809
|
+
if (fuzzyMatches.length === 1) {
|
|
810
|
+
return fuzzyMatches[0];
|
|
811
|
+
}
|
|
812
|
+
if (fuzzyMatches.length > 1) {
|
|
813
|
+
return promptSelectBranch(fuzzyMatches, messages.multipleMatches(branchName));
|
|
814
|
+
}
|
|
815
|
+
const allBranches = worktrees.map((wt) => wt.branch);
|
|
816
|
+
throw new ClawtError(messages.noMatch(branchName, allBranches));
|
|
817
|
+
}
|
|
818
|
+
|
|
745
819
|
// src/commands/list.ts
|
|
746
820
|
import chalk2 from "chalk";
|
|
747
821
|
function registerListCommand(program2) {
|
|
@@ -1059,68 +1133,36 @@ async function handleRun(options) {
|
|
|
1059
1133
|
}
|
|
1060
1134
|
|
|
1061
1135
|
// src/commands/resume.ts
|
|
1062
|
-
|
|
1136
|
+
var RESUME_RESOLVE_MESSAGES = {
|
|
1137
|
+
noWorktrees: MESSAGES.RESUME_NO_WORKTREES,
|
|
1138
|
+
selectBranch: MESSAGES.RESUME_SELECT_BRANCH,
|
|
1139
|
+
multipleMatches: MESSAGES.RESUME_MULTIPLE_MATCHES,
|
|
1140
|
+
noMatch: MESSAGES.RESUME_NO_MATCH
|
|
1141
|
+
};
|
|
1063
1142
|
function registerResumeCommand(program2) {
|
|
1064
1143
|
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) => {
|
|
1065
1144
|
await handleResume(options);
|
|
1066
1145
|
});
|
|
1067
1146
|
}
|
|
1068
|
-
function findExactMatch(worktrees, branchName) {
|
|
1069
|
-
return worktrees.find((wt) => wt.branch === branchName);
|
|
1070
|
-
}
|
|
1071
|
-
function findFuzzyMatches(worktrees, keyword) {
|
|
1072
|
-
const lowerKeyword = keyword.toLowerCase();
|
|
1073
|
-
return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
|
|
1074
|
-
}
|
|
1075
|
-
async function promptSelectBranch(worktrees, message) {
|
|
1076
|
-
const selectedBranch = await new Enquirer2.Select({
|
|
1077
|
-
message,
|
|
1078
|
-
choices: worktrees.map((wt) => ({
|
|
1079
|
-
name: wt.branch,
|
|
1080
|
-
message: wt.branch
|
|
1081
|
-
}))
|
|
1082
|
-
}).run();
|
|
1083
|
-
return worktrees.find((wt) => wt.branch === selectedBranch);
|
|
1084
|
-
}
|
|
1085
|
-
async function resolveTargetWorktree(branchName) {
|
|
1086
|
-
const worktrees = getProjectWorktrees();
|
|
1087
|
-
if (worktrees.length === 0) {
|
|
1088
|
-
throw new ClawtError(MESSAGES.RESUME_NO_WORKTREES);
|
|
1089
|
-
}
|
|
1090
|
-
if (!branchName) {
|
|
1091
|
-
if (worktrees.length === 1) {
|
|
1092
|
-
return worktrees[0];
|
|
1093
|
-
}
|
|
1094
|
-
return promptSelectBranch(worktrees, MESSAGES.RESUME_SELECT_BRANCH);
|
|
1095
|
-
}
|
|
1096
|
-
const exactMatch = findExactMatch(worktrees, branchName);
|
|
1097
|
-
if (exactMatch) {
|
|
1098
|
-
return exactMatch;
|
|
1099
|
-
}
|
|
1100
|
-
const fuzzyMatches = findFuzzyMatches(worktrees, branchName);
|
|
1101
|
-
if (fuzzyMatches.length === 1) {
|
|
1102
|
-
return fuzzyMatches[0];
|
|
1103
|
-
}
|
|
1104
|
-
if (fuzzyMatches.length > 1) {
|
|
1105
|
-
return promptSelectBranch(fuzzyMatches, MESSAGES.RESUME_MULTIPLE_MATCHES(branchName));
|
|
1106
|
-
}
|
|
1107
|
-
const allBranches = worktrees.map((wt) => wt.branch);
|
|
1108
|
-
throw new ClawtError(MESSAGES.RESUME_NO_MATCH(branchName, allBranches));
|
|
1109
|
-
}
|
|
1110
1147
|
async function handleResume(options) {
|
|
1111
1148
|
validateMainWorktree();
|
|
1112
1149
|
validateClaudeCodeInstalled();
|
|
1113
1150
|
logger.info(`resume \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch ?? "(\u672A\u6307\u5B9A)"}`);
|
|
1114
|
-
const
|
|
1151
|
+
const worktrees = getProjectWorktrees();
|
|
1152
|
+
const worktree = await resolveTargetWorktree(worktrees, RESUME_RESOLVE_MESSAGES, options.branch);
|
|
1115
1153
|
launchInteractiveClaude(worktree);
|
|
1116
1154
|
}
|
|
1117
1155
|
|
|
1118
1156
|
// src/commands/validate.ts
|
|
1119
|
-
import { join as join4 } from "path";
|
|
1120
|
-
import { existsSync as existsSync6 } from "fs";
|
|
1121
1157
|
import Enquirer3 from "enquirer";
|
|
1158
|
+
var VALIDATE_RESOLVE_MESSAGES = {
|
|
1159
|
+
noWorktrees: MESSAGES.VALIDATE_NO_WORKTREES,
|
|
1160
|
+
selectBranch: MESSAGES.VALIDATE_SELECT_BRANCH,
|
|
1161
|
+
multipleMatches: MESSAGES.VALIDATE_MULTIPLE_MATCHES,
|
|
1162
|
+
noMatch: MESSAGES.VALIDATE_NO_MATCH
|
|
1163
|
+
};
|
|
1122
1164
|
function registerValidateCommand(program2) {
|
|
1123
|
-
program2.command("validate").description("\u5728\u4E3B worktree \u9A8C\u8BC1\u67D0\u4E2A worktree \u5206\u652F\u7684\u53D8\u66F4").
|
|
1165
|
+
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) => {
|
|
1124
1166
|
await handleValidate(options);
|
|
1125
1167
|
});
|
|
1126
1168
|
}
|
|
@@ -1203,11 +1245,14 @@ async function handleValidateClean(options) {
|
|
|
1203
1245
|
validateMainWorktree();
|
|
1204
1246
|
const projectName = getProjectName();
|
|
1205
1247
|
const mainWorktreePath = getGitTopLevel();
|
|
1206
|
-
|
|
1248
|
+
const worktrees = getProjectWorktrees();
|
|
1249
|
+
const worktree = await resolveTargetWorktree(worktrees, VALIDATE_RESOLVE_MESSAGES, options.branch);
|
|
1250
|
+
const branchName = worktree.branch;
|
|
1251
|
+
logger.info(`validate --clean \u6267\u884C\uFF0C\u5206\u652F: ${branchName}`);
|
|
1207
1252
|
if (getConfigValue("confirmDestructiveOps")) {
|
|
1208
1253
|
const confirmed = await confirmDestructiveAction(
|
|
1209
1254
|
"git reset --hard + git clean -fd",
|
|
1210
|
-
`\u91CD\u7F6E\u4E3B worktree \u5E76\u5220\u9664\u5206\u652F ${
|
|
1255
|
+
`\u91CD\u7F6E\u4E3B worktree \u5E76\u5220\u9664\u5206\u652F ${branchName} \u7684 validate \u5FEB\u7167`
|
|
1211
1256
|
);
|
|
1212
1257
|
if (!confirmed) {
|
|
1213
1258
|
printInfo(MESSAGES.DESTRUCTIVE_OP_CANCELLED);
|
|
@@ -1218,8 +1263,8 @@ async function handleValidateClean(options) {
|
|
|
1218
1263
|
gitResetHard(mainWorktreePath);
|
|
1219
1264
|
gitCleanForce(mainWorktreePath);
|
|
1220
1265
|
}
|
|
1221
|
-
removeSnapshot(projectName,
|
|
1222
|
-
printSuccess(MESSAGES.VALIDATE_CLEANED(
|
|
1266
|
+
removeSnapshot(projectName, branchName);
|
|
1267
|
+
printSuccess(MESSAGES.VALIDATE_CLEANED(branchName));
|
|
1223
1268
|
}
|
|
1224
1269
|
function handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
|
|
1225
1270
|
migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
@@ -1266,40 +1311,43 @@ async function handleValidate(options) {
|
|
|
1266
1311
|
validateMainWorktree();
|
|
1267
1312
|
const projectName = getProjectName();
|
|
1268
1313
|
const mainWorktreePath = getGitTopLevel();
|
|
1269
|
-
const
|
|
1270
|
-
const
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
}
|
|
1314
|
+
const worktrees = getProjectWorktrees();
|
|
1315
|
+
const worktree = await resolveTargetWorktree(worktrees, VALIDATE_RESOLVE_MESSAGES, options.branch);
|
|
1316
|
+
const branchName = worktree.branch;
|
|
1317
|
+
const targetWorktreePath = worktree.path;
|
|
1318
|
+
logger.info(`validate \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${branchName}`);
|
|
1275
1319
|
const hasUncommitted = !isWorkingDirClean(targetWorktreePath);
|
|
1276
|
-
const hasCommitted = hasLocalCommits(
|
|
1320
|
+
const hasCommitted = hasLocalCommits(branchName, mainWorktreePath);
|
|
1277
1321
|
if (!hasUncommitted && !hasCommitted) {
|
|
1278
1322
|
printInfo(MESSAGES.TARGET_WORKTREE_CLEAN);
|
|
1279
1323
|
return;
|
|
1280
1324
|
}
|
|
1281
|
-
const isIncremental = hasSnapshot(projectName,
|
|
1325
|
+
const isIncremental = hasSnapshot(projectName, branchName);
|
|
1282
1326
|
if (isIncremental) {
|
|
1283
1327
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
1284
1328
|
await handleDirtyMainWorktree(mainWorktreePath);
|
|
1285
1329
|
}
|
|
1286
|
-
handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName,
|
|
1330
|
+
handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
1287
1331
|
} else {
|
|
1288
1332
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
1289
1333
|
await handleDirtyMainWorktree(mainWorktreePath);
|
|
1290
1334
|
}
|
|
1291
|
-
handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName,
|
|
1335
|
+
handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
1292
1336
|
}
|
|
1293
1337
|
}
|
|
1294
1338
|
|
|
1295
1339
|
// src/commands/merge.ts
|
|
1296
|
-
import { join as join5 } from "path";
|
|
1297
|
-
import { existsSync as existsSync7 } from "fs";
|
|
1298
1340
|
function registerMergeCommand(program2) {
|
|
1299
|
-
program2.command("merge").description("\u5408\u5E76\u67D0\u4E2A\u5DF2\u9A8C\u8BC1\u7684 worktree \u5206\u652F\u5230\u4E3B worktree").
|
|
1341
|
+
program2.command("merge").description("\u5408\u5E76\u67D0\u4E2A\u5DF2\u9A8C\u8BC1\u7684 worktree \u5206\u652F\u5230\u4E3B worktree").option("-b, --branch <branchName>", "\u8981\u5408\u5E76\u7684\u5206\u652F\u540D\uFF08\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF0C\u4E0D\u4F20\u5219\u5217\u51FA\u6240\u6709\u5206\u652F\uFF09").option("-m, --message <message>", "\u63D0\u4EA4\u4FE1\u606F\uFF08\u5DE5\u4F5C\u533A\u6709\u4FEE\u6539\u65F6\u5FC5\u586B\uFF09").action(async (options) => {
|
|
1300
1342
|
await handleMerge(options);
|
|
1301
1343
|
});
|
|
1302
1344
|
}
|
|
1345
|
+
var MERGE_RESOLVE_MESSAGES = {
|
|
1346
|
+
noWorktrees: MESSAGES.MERGE_NO_WORKTREES,
|
|
1347
|
+
selectBranch: MESSAGES.MERGE_SELECT_BRANCH,
|
|
1348
|
+
multipleMatches: MESSAGES.MERGE_MULTIPLE_MATCHES,
|
|
1349
|
+
noMatch: MESSAGES.MERGE_NO_MATCH
|
|
1350
|
+
};
|
|
1303
1351
|
async function handleSquashIfNeeded(targetWorktreePath, mainWorktreePath, branchName, commitMessage) {
|
|
1304
1352
|
if (!hasCommitWithMessage(branchName, AUTO_SAVE_COMMIT_MESSAGE, mainWorktreePath)) {
|
|
1305
1353
|
return false;
|
|
@@ -1335,20 +1383,18 @@ function cleanupWorktreeAndBranch(worktreePath, branchName) {
|
|
|
1335
1383
|
async function handleMerge(options) {
|
|
1336
1384
|
validateMainWorktree();
|
|
1337
1385
|
const mainWorktreePath = getGitTopLevel();
|
|
1338
|
-
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
|
|
1343
|
-
}
|
|
1386
|
+
logger.info(`merge \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch ?? "(\u672A\u6307\u5B9A)"}\uFF0C\u63D0\u4EA4\u4FE1\u606F: ${options.message ?? "(\u672A\u63D0\u4F9B)"}`);
|
|
1387
|
+
const worktrees = getProjectWorktrees();
|
|
1388
|
+
const worktree = await resolveTargetWorktree(worktrees, MERGE_RESOLVE_MESSAGES, options.branch);
|
|
1389
|
+
const { path: targetWorktreePath, branch } = worktree;
|
|
1344
1390
|
const projectName = getProjectName();
|
|
1345
1391
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
1346
|
-
if (hasSnapshot(projectName,
|
|
1347
|
-
printWarning(MESSAGES.MERGE_VALIDATE_STATE_HINT(
|
|
1392
|
+
if (hasSnapshot(projectName, branch)) {
|
|
1393
|
+
printWarning(MESSAGES.MERGE_VALIDATE_STATE_HINT(branch));
|
|
1348
1394
|
}
|
|
1349
1395
|
throw new ClawtError(MESSAGES.MAIN_WORKTREE_DIRTY);
|
|
1350
1396
|
}
|
|
1351
|
-
const shouldExit = await handleSquashIfNeeded(targetWorktreePath, mainWorktreePath,
|
|
1397
|
+
const shouldExit = await handleSquashIfNeeded(targetWorktreePath, mainWorktreePath, branch, options.message);
|
|
1352
1398
|
if (shouldExit) {
|
|
1353
1399
|
return;
|
|
1354
1400
|
}
|
|
@@ -1360,12 +1406,12 @@ async function handleMerge(options) {
|
|
|
1360
1406
|
gitAddAll(targetWorktreePath);
|
|
1361
1407
|
gitCommit(options.message, targetWorktreePath);
|
|
1362
1408
|
} else {
|
|
1363
|
-
if (!hasLocalCommits(
|
|
1409
|
+
if (!hasLocalCommits(branch, mainWorktreePath)) {
|
|
1364
1410
|
throw new ClawtError(MESSAGES.TARGET_WORKTREE_NO_CHANGES);
|
|
1365
1411
|
}
|
|
1366
1412
|
}
|
|
1367
1413
|
try {
|
|
1368
|
-
gitMerge(
|
|
1414
|
+
gitMerge(branch, mainWorktreePath);
|
|
1369
1415
|
} catch (error) {
|
|
1370
1416
|
if (hasMergeConflict(mainWorktreePath)) {
|
|
1371
1417
|
throw new ClawtError(MESSAGES.MERGE_CONFLICT);
|
|
@@ -1383,16 +1429,16 @@ async function handleMerge(options) {
|
|
|
1383
1429
|
printInfo("\u5DF2\u8DF3\u8FC7\u81EA\u52A8 pull/push\uFF0C\u8BF7\u624B\u52A8\u6267\u884C git pull && git push");
|
|
1384
1430
|
}
|
|
1385
1431
|
if (options.message) {
|
|
1386
|
-
printSuccess(MESSAGES.MERGE_SUCCESS(
|
|
1432
|
+
printSuccess(MESSAGES.MERGE_SUCCESS(branch, options.message, autoPullPush));
|
|
1387
1433
|
} else {
|
|
1388
|
-
printSuccess(MESSAGES.MERGE_SUCCESS_NO_MESSAGE(
|
|
1434
|
+
printSuccess(MESSAGES.MERGE_SUCCESS_NO_MESSAGE(branch, autoPullPush));
|
|
1389
1435
|
}
|
|
1390
|
-
const shouldCleanup = await shouldCleanupAfterMerge(
|
|
1436
|
+
const shouldCleanup = await shouldCleanupAfterMerge(branch);
|
|
1391
1437
|
if (shouldCleanup) {
|
|
1392
|
-
cleanupWorktreeAndBranch(targetWorktreePath,
|
|
1438
|
+
cleanupWorktreeAndBranch(targetWorktreePath, branch);
|
|
1393
1439
|
}
|
|
1394
|
-
if (hasSnapshot(projectName,
|
|
1395
|
-
removeSnapshot(projectName,
|
|
1440
|
+
if (hasSnapshot(projectName, branch)) {
|
|
1441
|
+
removeSnapshot(projectName, branch);
|
|
1396
1442
|
}
|
|
1397
1443
|
}
|
|
1398
1444
|
|
|
@@ -1431,13 +1477,17 @@ function formatConfigValue(value) {
|
|
|
1431
1477
|
}
|
|
1432
1478
|
|
|
1433
1479
|
// src/commands/sync.ts
|
|
1434
|
-
import { existsSync as existsSync8 } from "fs";
|
|
1435
|
-
import { join as join6 } from "path";
|
|
1436
1480
|
function registerSyncCommand(program2) {
|
|
1437
|
-
program2.command("sync").description("\u5C06\u4E3B\u5206\u652F\u6700\u65B0\u4EE3\u7801\u540C\u6B65\u5230\u76EE\u6807 worktree").
|
|
1481
|
+
program2.command("sync").description("\u5C06\u4E3B\u5206\u652F\u6700\u65B0\u4EE3\u7801\u540C\u6B65\u5230\u76EE\u6807 worktree").option("-b, --branch <branchName>", "\u8981\u540C\u6B65\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) => {
|
|
1438
1482
|
await handleSync(options);
|
|
1439
1483
|
});
|
|
1440
1484
|
}
|
|
1485
|
+
var SYNC_RESOLVE_MESSAGES = {
|
|
1486
|
+
noWorktrees: MESSAGES.SYNC_NO_WORKTREES,
|
|
1487
|
+
selectBranch: MESSAGES.SYNC_SELECT_BRANCH,
|
|
1488
|
+
multipleMatches: MESSAGES.SYNC_MULTIPLE_MATCHES,
|
|
1489
|
+
noMatch: MESSAGES.SYNC_NO_MATCH
|
|
1490
|
+
};
|
|
1441
1491
|
function autoSaveChanges(worktreePath, branch) {
|
|
1442
1492
|
gitAddAll(worktreePath);
|
|
1443
1493
|
gitCommit(AUTO_SAVE_COMMIT_MESSAGE, worktreePath);
|
|
@@ -1457,13 +1507,10 @@ function mergeMainBranch(worktreePath, mainBranch) {
|
|
|
1457
1507
|
}
|
|
1458
1508
|
async function handleSync(options) {
|
|
1459
1509
|
validateMainWorktree();
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
const
|
|
1463
|
-
const
|
|
1464
|
-
if (!existsSync8(targetWorktreePath)) {
|
|
1465
|
-
throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(branch));
|
|
1466
|
-
}
|
|
1510
|
+
logger.info(`sync \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch ?? "(\u672A\u6307\u5B9A)"}`);
|
|
1511
|
+
const worktrees = getProjectWorktrees();
|
|
1512
|
+
const worktree = await resolveTargetWorktree(worktrees, SYNC_RESOLVE_MESSAGES, options.branch);
|
|
1513
|
+
const { path: targetWorktreePath, branch } = worktree;
|
|
1467
1514
|
const mainWorktreePath = getGitTopLevel();
|
|
1468
1515
|
const mainBranch = getCurrentBranch(mainWorktreePath);
|
|
1469
1516
|
if (!isWorkingDirClean(targetWorktreePath)) {
|