clawt 3.4.5 → 3.5.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.
Files changed (45) hide show
  1. package/.claude/settings.local.json +12 -0
  2. package/README.md +0 -4
  3. package/dist/index.js +430 -306
  4. package/dist/postinstall.js +12 -1
  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/init.ts +6 -2
  22. package/src/commands/tasks.ts +51 -0
  23. package/src/constants/index.ts +3 -0
  24. package/src/constants/interactive-panel.ts +6 -0
  25. package/src/constants/messages/index.ts +4 -2
  26. package/src/constants/messages/interactive-panel.ts +12 -0
  27. package/src/constants/messages/tasks.ts +9 -0
  28. package/src/constants/tasks-template.ts +28 -0
  29. package/src/index.ts +2 -0
  30. package/src/types/command.ts +6 -0
  31. package/src/types/index.ts +1 -1
  32. package/src/utils/formatter.ts +19 -0
  33. package/src/utils/git-branch.ts +116 -0
  34. package/src/utils/git-core.ts +369 -0
  35. package/src/utils/git-worktree.ts +40 -0
  36. package/src/utils/git.ts +3 -521
  37. package/src/utils/index.ts +1 -1
  38. package/src/utils/interactive-panel-render.ts +12 -6
  39. package/src/utils/interactive-panel-state.ts +137 -0
  40. package/src/utils/interactive-panel.ts +44 -188
  41. package/src/utils/keyboard-controller.ts +48 -0
  42. package/src/utils/ui-prompts.ts +240 -0
  43. package/src/utils/worktree-matcher.ts +21 -251
  44. package/tests/unit/commands/tasks.test.ts +153 -0
  45. package/tests/unit/utils/formatter.test.ts +26 -1
@@ -0,0 +1,116 @@
1
+ import { execCommand } from './shell.js';
2
+ import { logger } from '../logger/index.js';
3
+
4
+ /**
5
+ * 检查本地分支是否存在
6
+ * @param {string} branchName - 分支名
7
+ * @param {string} cwd - 工作目录
8
+ * @returns {boolean} 分支是否存在
9
+ */
10
+ export function checkBranchExists(branchName: string, cwd?: string): boolean {
11
+ try {
12
+ execCommand(`git show-ref --verify refs/heads/${branchName}`, { cwd });
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * 强制删除本地分支
21
+ * @param {string} branchName - 分支名
22
+ * @param {string} cwd - 工作目录
23
+ */
24
+ export function deleteBranch(branchName: string, cwd?: string): void {
25
+ logger.info(`删除分支: ${branchName}`);
26
+ execCommand(`git branch -D ${branchName}`, { cwd });
27
+ }
28
+
29
+ /**
30
+ * 检查目标分支相对于当前分支是否有本地提交
31
+ * @param {string} branchName - 目标分支名
32
+ * @param {string} cwd - 工作目录
33
+ * @returns {boolean} 是否有本地提交
34
+ */
35
+ export function hasLocalCommits(branchName: string, cwd?: string): boolean {
36
+ try {
37
+ const output = execCommand(`git log HEAD..${branchName} --oneline`, { cwd });
38
+ return output.trim() !== '';
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * 获取目标分支相对于当前分支的新增提交数
46
+ * @param {string} branchName - 目标分支名
47
+ * @param {string} [cwd] - 工作目录
48
+ * @returns {number} 新增提交数
49
+ */
50
+ export function getCommitCountAhead(branchName: string, cwd?: string): number {
51
+ const output = execCommand(`git rev-list --count HEAD..${branchName}`, { cwd });
52
+ return parseInt(output, 10) || 0;
53
+ }
54
+
55
+ /**
56
+ * 获取目标分支落后于当前分支的提交数
57
+ * 即当前分支有多少提交是目标分支没有的
58
+ * @param {string} branchName - 目标分支名
59
+ * @param {string} [cwd] - 工作目录
60
+ * @returns {number} 落后的提交数
61
+ */
62
+ export function getCommitCountBehind(branchName: string, cwd?: string): number {
63
+ try {
64
+ const output = execCommand(`git rev-list --count ${branchName}..HEAD`, { cwd });
65
+ return parseInt(output, 10) || 0;
66
+ } catch {
67
+ return 0;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * 获取当前分支名
73
+ * @param {string} [cwd] - 工作目录
74
+ * @returns {string} 当前分支名
75
+ */
76
+ export function getCurrentBranch(cwd?: string): string {
77
+ return execCommand('git rev-parse --abbrev-ref HEAD', { cwd });
78
+ }
79
+
80
+ /**
81
+ * 获取分支的创建时间(通过 reflog 获取分支创建时的时间戳)
82
+ * reflog 的最后一条记录即为分支创建时的记录
83
+ * @param {string} branchName - 目标分支名
84
+ * @param {string} [cwd] - 工作目录
85
+ * @returns {string | null} ISO 8601 格式的时间字符串,无法获取时返回 null
86
+ */
87
+ export function getBranchCreatedAt(branchName: string, cwd?: string): string | null {
88
+ try {
89
+ const output = execCommand(`git reflog show ${branchName} --format=%cI`, { cwd });
90
+ if (!output.trim()) return null;
91
+ // 取最后一行,即分支创建时的 reflog 记录
92
+ const lines = output.trim().split('\n');
93
+ const lastLine = lines[lines.length - 1];
94
+ return lastLine || null;
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * 切换到指定分支
102
+ * @param {string} branchName - 目标分支名
103
+ * @param {string} [cwd] - 工作目录
104
+ */
105
+ export function gitCheckout(branchName: string, cwd?: string): void {
106
+ execCommand(`git checkout ${branchName}`, { cwd });
107
+ }
108
+
109
+ /**
110
+ * 创建本地分支(不切换)
111
+ * @param {string} branchName - 新分支名
112
+ * @param {string} [cwd] - 工作目录
113
+ */
114
+ export function createBranch(branchName: string, cwd?: string): void {
115
+ execCommand(`git branch ${branchName}`, { cwd });
116
+ }
@@ -0,0 +1,369 @@
1
+ import { basename } from 'node:path';
2
+ import { execSync } 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
+ }
@@ -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
+ }