clawt 3.4.6 → 3.5.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.
Files changed (53) hide show
  1. package/.claude/settings.local.json +12 -0
  2. package/README.md +0 -4
  3. package/dist/index.js +583 -314
  4. package/dist/postinstall.js +37 -2
  5. package/docs/alias.md +7 -1
  6. package/docs/completion.md +1 -1
  7. package/docs/config.md +4 -3
  8. package/docs/cover-validate.md +4 -3
  9. package/docs/create.md +28 -12
  10. package/docs/home.md +12 -8
  11. package/docs/init.md +16 -9
  12. package/docs/list.md +13 -7
  13. package/docs/merge.md +12 -12
  14. package/docs/remove.md +24 -13
  15. package/docs/reset.md +6 -4
  16. package/docs/resume.md +3 -4
  17. package/docs/status.md +75 -30
  18. package/docs/sync.md +26 -26
  19. package/docs/validate.md +13 -7
  20. package/package.json +1 -1
  21. package/src/commands/merge.ts +20 -5
  22. package/src/commands/tasks.ts +51 -0
  23. package/src/constants/ai-prompts.ts +14 -0
  24. package/src/constants/config.ts +9 -0
  25. package/src/constants/index.ts +4 -0
  26. package/src/constants/interactive-panel.ts +6 -0
  27. package/src/constants/messages/index.ts +4 -2
  28. package/src/constants/messages/interactive-panel.ts +12 -0
  29. package/src/constants/messages/merge.ts +15 -0
  30. package/src/constants/messages/tasks.ts +9 -0
  31. package/src/constants/tasks-template.ts +28 -0
  32. package/src/index.ts +2 -0
  33. package/src/types/command.ts +8 -0
  34. package/src/types/config.ts +4 -0
  35. package/src/types/index.ts +1 -1
  36. package/src/utils/conflict-resolver.ts +170 -0
  37. package/src/utils/formatter.ts +19 -0
  38. package/src/utils/git-branch.ts +116 -0
  39. package/src/utils/git-core.ts +417 -0
  40. package/src/utils/git-worktree.ts +40 -0
  41. package/src/utils/git.ts +3 -521
  42. package/src/utils/index.ts +7 -2
  43. package/src/utils/interactive-panel-render.ts +12 -6
  44. package/src/utils/interactive-panel-state.ts +137 -0
  45. package/src/utils/interactive-panel.ts +44 -188
  46. package/src/utils/keyboard-controller.ts +48 -0
  47. package/src/utils/ui-prompts.ts +240 -0
  48. package/src/utils/worktree-matcher.ts +21 -251
  49. package/tests/unit/commands/merge.test.ts +59 -3
  50. package/tests/unit/commands/tasks.test.ts +153 -0
  51. package/tests/unit/utils/conflict-resolver.test.ts +250 -0
  52. package/tests/unit/utils/formatter.test.ts +26 -1
  53. package/src/constants/messages.ts +0 -179
@@ -0,0 +1,417 @@
1
+ import { basename } from 'node:path';
2
+ import { execSync, execFileSync } from 'node:child_process';
3
+ import { execCommand, execCommandWithInput } from './shell.js';
4
+ import { logger } from '../logger/index.js';
5
+
6
+ /**
7
+ * 获取 git common dir(用于判断是否为主 worktree)
8
+ * @param {string} cwd - 工作目录
9
+ * @returns {string} git common dir 路径
10
+ */
11
+ export function getGitCommonDir(cwd?: string): string {
12
+ return execCommand('git rev-parse --git-common-dir', { cwd });
13
+ }
14
+
15
+ /**
16
+ * 获取 git 仓库根目录的绝对路径
17
+ * @param {string} cwd - 工作目录
18
+ * @returns {string} 仓库根目录路径
19
+ */
20
+ export function getGitTopLevel(cwd?: string): string {
21
+ return execCommand('git rev-parse --show-toplevel', { cwd });
22
+ }
23
+
24
+ /**
25
+ * 获取项目名(仓库根目录名称)
26
+ * @param {string} cwd - 工作目录
27
+ * @returns {string} 项目名
28
+ */
29
+ export function getProjectName(cwd?: string): string {
30
+ const topLevel = getGitTopLevel(cwd);
31
+ return basename(topLevel);
32
+ }
33
+
34
+ /**
35
+ * 获取工作区状态(git status --porcelain)
36
+ * @param {string} cwd - 工作目录
37
+ * @returns {string} porcelain 格式输出,为空表示干净
38
+ */
39
+ export function getStatusPorcelain(cwd?: string): string {
40
+ return execCommand('git status --porcelain', { cwd });
41
+ }
42
+
43
+ /**
44
+ * 判断工作区是否干净
45
+ * @param {string} cwd - 工作目录
46
+ * @returns {boolean} 是否干净
47
+ */
48
+ export function isWorkingDirClean(cwd?: string): boolean {
49
+ return getStatusPorcelain(cwd) === '';
50
+ }
51
+
52
+ /**
53
+ * git add 所有文件
54
+ * @param {string} cwd - 工作目录
55
+ */
56
+ export function gitAddAll(cwd?: string): void {
57
+ execCommand('git add .', { cwd });
58
+ }
59
+
60
+ /**
61
+ * git commit
62
+ * @param {string} message - 提交信息
63
+ * @param {string} cwd - 工作目录
64
+ */
65
+ export function gitCommit(message: string, cwd?: string): void {
66
+ execCommand(`git commit -m '${message.replace(/'/g, "'\\''")}'`, { cwd });
67
+ }
68
+
69
+ /**
70
+ * git merge
71
+ * @param {string} branchName - 要合并的分支名
72
+ * @param {string} cwd - 工作目录
73
+ */
74
+ export function gitMerge(branchName: string, cwd?: string): void {
75
+ execCommand(`git merge ${branchName}`, { cwd });
76
+ }
77
+
78
+ /**
79
+ * 检查是否有合并冲突
80
+ * @param {string} cwd - 工作目录
81
+ * @returns {boolean} 是否有冲突
82
+ */
83
+ export function hasMergeConflict(cwd?: string): boolean {
84
+ const status = getStatusPorcelain(cwd);
85
+ // UU = 双方修改冲突, AA = 双方新增冲突, DD = 双方删除
86
+ return status.split('\n').some((line) => /^(UU|AA|DD|DU|UD|AU|UA)/.test(line));
87
+ }
88
+
89
+ /**
90
+ * git pull
91
+ * @param {string} cwd - 工作目录
92
+ */
93
+ export function gitPull(cwd?: string): void {
94
+ execCommand('git pull', { cwd });
95
+ }
96
+
97
+ /**
98
+ * git push
99
+ * @param {string} cwd - 工作目录
100
+ */
101
+ export function gitPush(cwd?: string): void {
102
+ execCommand('git push', { cwd });
103
+ }
104
+
105
+ /**
106
+ * git reset --hard HEAD
107
+ * @param {string} cwd - 工作目录
108
+ */
109
+ export function gitResetHard(cwd?: string): void {
110
+ execCommand('git reset --hard HEAD', { cwd });
111
+ }
112
+
113
+ /**
114
+ * git clean -fd(删除未跟踪文件)
115
+ * @param {string} cwd - 工作目录
116
+ */
117
+ export function gitCleanForce(cwd?: string): void {
118
+ execCommand('git clean -fd', { cwd });
119
+ }
120
+
121
+ /**
122
+ * git stash push -m <message>
123
+ * @param {string} message - stash 消息
124
+ * @param {string} cwd - 工作目录
125
+ */
126
+ export function gitStashPush(message: string, cwd?: string): void {
127
+ execCommand(`git stash push -m "${message}"`, { cwd });
128
+ }
129
+
130
+ /**
131
+ * git stash apply
132
+ * @param {string} cwd - 工作目录
133
+ */
134
+ export function gitStashApply(cwd?: string): void {
135
+ execCommand('git stash apply', { cwd });
136
+ }
137
+
138
+ /**
139
+ * git stash pop stash@{index}
140
+ * @param {number} index - stash 索引
141
+ * @param {string} cwd - 工作目录
142
+ */
143
+ export function gitStashPop(index: number = 0, cwd?: string): void {
144
+ execCommand(`git stash pop stash@{${index}}`, { cwd });
145
+ }
146
+
147
+ /**
148
+ * git stash drop stash@{index}
149
+ * @param {number} index - stash 索引
150
+ * @param {string} [cwd] - 工作目录
151
+ */
152
+ export function gitStashDrop(index: number = 0, cwd?: string): void {
153
+ execCommand(`git stash drop stash@{${index}}`, { cwd });
154
+ }
155
+
156
+ /**
157
+ * git stash list
158
+ * @param {string} cwd - 工作目录
159
+ * @returns {string} stash 列表输出
160
+ */
161
+ export function gitStashList(cwd?: string): string {
162
+ try {
163
+ return execCommand('git stash list', { cwd });
164
+ } catch {
165
+ return '';
166
+ }
167
+ }
168
+
169
+ /**
170
+ * git restore --staged .
171
+ * @param {string} cwd - 工作目录
172
+ */
173
+ export function gitRestoreStaged(cwd?: string): void {
174
+ execCommand('git restore --staged .', { cwd });
175
+ }
176
+
177
+ /**
178
+ * 解析 git diff --shortstat 输出,提取新增行数和删除行数
179
+ * @param {string} output - shortstat 输出字符串
180
+ * @returns {{ insertions: number; deletions: number }} 新增和删除行数
181
+ */
182
+ export function parseShortStat(output: string): { insertions: number; deletions: number } {
183
+ let insertions = 0;
184
+ let deletions = 0;
185
+
186
+ const insertMatch = output.match(/(\d+)\s+insertion/);
187
+ if (insertMatch) {
188
+ insertions = parseInt(insertMatch[1], 10);
189
+ }
190
+
191
+ const deleteMatch = output.match(/(\d+)\s+deletion/);
192
+ if (deleteMatch) {
193
+ deletions = parseInt(deleteMatch[1], 10);
194
+ }
195
+
196
+ return { insertions, deletions };
197
+ }
198
+
199
+ /**
200
+ * 获取 worktree 中工作区和暂存区的变更统计
201
+ * @param {string} worktreePath - worktree 目录路径
202
+ * @returns {{ insertions: number; deletions: number }} 新增和删除行数
203
+ */
204
+ export function getDiffStat(worktreePath: string): { insertions: number; deletions: number } {
205
+ // 工作区和暂存区相对于 HEAD 的变更
206
+ const output = execCommand('git diff --shortstat HEAD', { cwd: worktreePath });
207
+ return parseShortStat(output);
208
+ }
209
+
210
+ /**
211
+ * 获取暂存区相对于 HEAD 的完整 diff(含二进制文件)
212
+ * 注意:返回原始输出不做 trim,保留 patch 格式完整性
213
+ * @param {string} [cwd] - 工作目录
214
+ * @returns {Buffer} diff 原始输出(Buffer 格式,保留二进制数据完整性)
215
+ */
216
+ export function gitDiffCachedBinary(cwd?: string): Buffer {
217
+ logger.debug(`执行命令: git diff --cached --binary${cwd ? ` (cwd: ${cwd})` : ''}`);
218
+ return execSync('git diff --cached --binary', {
219
+ cwd,
220
+ stdio: ['pipe', 'pipe', 'pipe'],
221
+ });
222
+ }
223
+
224
+ /**
225
+ * 将 patch 内容通过 stdin 应用到暂存区
226
+ * @param {Buffer} patchContent - patch 内容(Buffer 格式)
227
+ * @param {string} [cwd] - 工作目录
228
+ */
229
+ export function gitApplyCachedFromStdin(patchContent: Buffer, cwd?: string): void {
230
+ execCommandWithInput('git', ['apply', '--cached'], { input: patchContent, cwd });
231
+ }
232
+
233
+ /**
234
+ * 获取当前 HEAD 的 commit hash
235
+ * @param {string} [cwd] - 工作目录
236
+ * @returns {string} commit hash
237
+ */
238
+ export function getHeadCommitHash(cwd?: string): string {
239
+ return execCommand('git rev-parse HEAD', { cwd });
240
+ }
241
+
242
+ /**
243
+ * 获取目标分支相对于当前分支的已提交变更(含二进制文件)
244
+ * 使用三点 diff(HEAD...branchName)获取自分叉点以来的变更
245
+ * @param {string} branchName - 目标分支名
246
+ * @param {string} [cwd] - 工作目录(应在主 worktree 中执行)
247
+ * @returns {Buffer} diff 原始输出
248
+ */
249
+ export function gitDiffBinaryAgainstBranch(branchName: string, cwd?: string): Buffer {
250
+ logger.debug(`执行命令: git diff HEAD...${branchName} --binary${cwd ? ` (cwd: ${cwd})` : ''}`);
251
+ return execSync(`git diff HEAD...${branchName} --binary`, {
252
+ cwd,
253
+ stdio: ['pipe', 'pipe', 'pipe'],
254
+ });
255
+ }
256
+
257
+ /**
258
+ * 将 patch 内容通过 stdin 应用到工作目录(不带 --cached)
259
+ * @param {Buffer} patchContent - patch 内容
260
+ * @param {string} [cwd] - 工作目录
261
+ */
262
+ export function gitApplyFromStdin(patchContent: Buffer, cwd?: string): void {
263
+ execCommandWithInput('git', ['apply'], { input: patchContent, cwd });
264
+ }
265
+
266
+ /**
267
+ * git reset --soft HEAD~<count>,撤销 commit 但保留变更在暂存区
268
+ * @param {number} count - 撤销的 commit 数量
269
+ * @param {string} [cwd] - 工作目录
270
+ */
271
+ export function gitResetSoft(count: number = 1, cwd?: string): void {
272
+ execCommand(`git reset --soft HEAD~${count}`, { cwd });
273
+ }
274
+
275
+ /**
276
+ * 获取两个分支的分叉点(merge-base)
277
+ * @param {string} branchA - 分支 A
278
+ * @param {string} branchB - 分支 B
279
+ * @param {string} [cwd] - 工作目录
280
+ * @returns {string} merge-base 的 commit hash
281
+ */
282
+ export function gitMergeBase(branchA: string, branchB: string, cwd?: string): string {
283
+ return execCommand(`git merge-base ${branchA} ${branchB}`, { cwd });
284
+ }
285
+
286
+ /**
287
+ * 检查目标分支相对于当前分支是否存在指定前缀的 commit message
288
+ * 通过 git log HEAD..<branch> 遍历所有新增 commit 的 message
289
+ * @param {string} branchName - 目标分支名
290
+ * @param {string} messagePrefix - 要匹配的 commit message 前缀
291
+ * @param {string} [cwd] - 工作目录
292
+ * @returns {boolean} 是否存在匹配的 commit
293
+ */
294
+ export function hasCommitWithMessage(branchName: string, messagePrefix: string, cwd?: string): boolean {
295
+ try {
296
+ const output = execCommand(`git log HEAD..${branchName} --format=%s`, { cwd });
297
+ if (!output.trim()) return false;
298
+ return output.trim().split('\n').some((msg) => msg.startsWith(messagePrefix));
299
+ } catch {
300
+ return false;
301
+ }
302
+ }
303
+
304
+ /**
305
+ * git reset --soft <commitHash>,将指定 commit 之后的所有提交撤销到暂存区
306
+ * @param {string} commitHash - 目标 commit hash(reset 到此处)
307
+ * @param {string} [cwd] - 工作目录
308
+ */
309
+ export function gitResetSoftTo(commitHash: string, cwd?: string): void {
310
+ execCommand(`git reset --soft ${commitHash}`, { cwd });
311
+ }
312
+
313
+ /**
314
+ * 将当前暂存区内容写入 git tree 对象并返回其 hash
315
+ * @param {string} [cwd] - 工作目录
316
+ * @returns {string} tree 对象的 hash
317
+ */
318
+ export function gitWriteTree(cwd?: string): string {
319
+ return execCommand('git write-tree', { cwd });
320
+ }
321
+
322
+ /**
323
+ * 将指定 tree 对象的内容载入暂存区(不影响工作目录)
324
+ * @param {string} treeHash - tree 对象的 hash
325
+ * @param {string} [cwd] - 工作目录
326
+ */
327
+ export function gitReadTree(treeHash: string, cwd?: string): void {
328
+ execCommand(`git read-tree ${treeHash}`, { cwd });
329
+ }
330
+
331
+ /**
332
+ * 获取指定 commit 对应的 tree 对象 hash
333
+ * @param {string} commitHash - commit hash
334
+ * @param {string} [cwd] - 工作目录
335
+ * @returns {string} tree 对象的 hash
336
+ */
337
+ export function getCommitTreeHash(commitHash: string, cwd?: string): string {
338
+ return execCommand(`git rev-parse ${commitHash}^{tree}`, { cwd });
339
+ }
340
+
341
+ /**
342
+ * 获取两个 tree 对象之间的 diff(patch 格式,含二进制)
343
+ * @param {string} baseTreeHash - 基准 tree hash
344
+ * @param {string} targetTreeHash - 目标 tree hash
345
+ * @param {string} [cwd] - 工作目录
346
+ * @returns {Buffer} diff patch 内容
347
+ */
348
+ export function gitDiffTree(baseTreeHash: string, targetTreeHash: string, cwd?: string): Buffer {
349
+ logger.debug(`执行命令: git diff-tree -p --binary ${baseTreeHash} ${targetTreeHash}${cwd ? ` (cwd: ${cwd})` : ''}`);
350
+ return execSync(`git diff-tree -p --binary ${baseTreeHash} ${targetTreeHash}`, {
351
+ cwd,
352
+ stdio: ['pipe', 'pipe', 'pipe'],
353
+ });
354
+ }
355
+
356
+ /**
357
+ * 检测 patch 能否无冲突地应用到暂存区(干运行,不实际修改)
358
+ * @param {Buffer} patchContent - patch 内容(Buffer 格式)
359
+ * @param {string} [cwd] - 工作目录
360
+ * @returns {boolean} patch 能否成功应用
361
+ */
362
+ export function gitApplyCachedCheck(patchContent: Buffer, cwd?: string): boolean {
363
+ try {
364
+ execCommandWithInput('git', ['apply', '--cached', '--check'], { input: patchContent, cwd });
365
+ return true;
366
+ } catch {
367
+ return false;
368
+ }
369
+ }
370
+
371
+ /**
372
+ * 获取冲突文件列表
373
+ * 通过 git status --porcelain 解析 UU/AA/DD 等冲突标记
374
+ * @param {string} [cwd] - 工作目录
375
+ * @returns {string[]} 冲突文件路径列表
376
+ */
377
+ export function getConflictFiles(cwd?: string): string[] {
378
+ const status = getStatusPorcelain(cwd);
379
+ if (!status) return [];
380
+ return status
381
+ .split('\n')
382
+ .filter((line) => /^(UU|AA|DD|DU|UD|AU|UA)/.test(line))
383
+ .map((line) => line.slice(3));
384
+ }
385
+
386
+ /**
387
+ * git add 指定文件
388
+ * 使用 execFileSync 数组参数形式避免文件名中特殊字符导致的注入风险
389
+ * @param {string[]} files - 文件路径列表
390
+ * @param {string} [cwd] - 工作目录
391
+ */
392
+ export function gitAddFiles(files: string[], cwd?: string): void {
393
+ if (files.length === 0) return;
394
+ const args = ['add', '--', ...files];
395
+ logger.debug(`执行命令: git ${args.join(' ')}${cwd ? ` (cwd: ${cwd})` : ''}`);
396
+ execFileSync('git', args, {
397
+ cwd,
398
+ encoding: 'utf-8',
399
+ stdio: ['pipe', 'pipe', 'pipe'],
400
+ });
401
+ }
402
+
403
+ /**
404
+ * git merge --continue(非交互式)
405
+ * @param {string} [cwd] - 工作目录
406
+ */
407
+ export function gitMergeContinue(cwd?: string): void {
408
+ execCommand('GIT_EDITOR=true git merge --continue', { cwd });
409
+ }
410
+
411
+ /**
412
+ * git merge --abort
413
+ * @param {string} [cwd] - 工作目录
414
+ */
415
+ export function gitMergeAbort(cwd?: string): void {
416
+ execCommand('git merge --abort', { cwd });
417
+ }
@@ -0,0 +1,40 @@
1
+ import { execCommand } from './shell.js';
2
+ import { logger } from '../logger/index.js';
3
+
4
+ /**
5
+ * 创建 worktree 并同时创建新分支
6
+ * @param {string} branchName - 新分支名
7
+ * @param {string} worktreePath - worktree 目录路径
8
+ * @param {string} cwd - 工作目录
9
+ */
10
+ export function createWorktree(branchName: string, worktreePath: string, cwd?: string): void {
11
+ logger.info(`创建 worktree: ${worktreePath}`);
12
+ execCommand(`git worktree add -b ${branchName} "${worktreePath}"`, { cwd });
13
+ }
14
+
15
+ /**
16
+ * 强制移除 worktree
17
+ * @param {string} worktreePath - worktree 目录路径
18
+ * @param {string} cwd - 工作目录
19
+ */
20
+ export function removeWorktreeByPath(worktreePath: string, cwd?: string): void {
21
+ logger.info(`移除 worktree: ${worktreePath}`);
22
+ execCommand(`git worktree remove -f "${worktreePath}"`, { cwd });
23
+ }
24
+
25
+ /**
26
+ * 获取 git worktree list 的输出
27
+ * @param {string} cwd - 工作目录
28
+ * @returns {string} worktree 列表
29
+ */
30
+ export function gitWorktreeList(cwd?: string): string {
31
+ return execCommand('git worktree list', { cwd });
32
+ }
33
+
34
+ /**
35
+ * 执行 git worktree prune
36
+ * @param {string} cwd - 工作目录
37
+ */
38
+ export function gitWorktreePrune(cwd?: string): void {
39
+ execCommand('git worktree prune', { cwd });
40
+ }