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,28 @@
1
+ /** 任务模板默认输出目录 */
2
+ export const TASK_TEMPLATE_OUTPUT_DIR = 'clawt/tasks';
3
+
4
+ /** 任务模板文件名前缀 */
5
+ export const TASK_TEMPLATE_FILENAME_PREFIX = 'clawt-tasks';
6
+
7
+ /** 任务模板文件内容 */
8
+ export const TASK_TEMPLATE_CONTENT = `# Clawt 任务文件
9
+ #
10
+ # 使用方法: clawt run -f tasks.md
11
+ # 格式说明: 标签外的文本会被忽略,每个任务用 START/END 标签包裹
12
+ #
13
+ # 规则:
14
+ # 1. 每个任务块用 <!-- CLAWT-TASKS:START --> 和 <!-- CLAWT-TASKS:END --> 包裹
15
+ # 2. 块内 # branch: <分支名> 声明分支名(使用 -b 参数时可省略)
16
+ # 3. 块内其余行为任务描述(支持多行)
17
+
18
+ <!-- CLAWT-TASKS:START -->
19
+ # branch: feat-example-1
20
+ 在这里写第一个任务的描述
21
+ <!-- CLAWT-TASKS:END -->
22
+
23
+ <!-- CLAWT-TASKS:START -->
24
+ # branch: feat-example-2
25
+ 在这里写第二个任务的描述
26
+ 支持多行描述
27
+ <!-- CLAWT-TASKS:END -->
28
+ `;
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ import { registerProjectsCommand } from './commands/projects.js';
21
21
  import { registerCompletionCommand } from './commands/completion.js';
22
22
  import { registerInitCommand } from './commands/init.js';
23
23
  import { registerHomeCommand } from './commands/home.js';
24
+ import { registerTasksCommand } from './commands/tasks.js';
24
25
 
25
26
  // 从 package.json 读取版本号,避免硬编码
26
27
  const require = createRequire(import.meta.url);
@@ -62,6 +63,7 @@ registerProjectsCommand(program);
62
63
  registerCompletionCommand(program);
63
64
  registerInitCommand(program);
64
65
  registerHomeCommand(program);
66
+ registerTasksCommand(program);
65
67
 
66
68
  // 加载配置并应用命令别名
67
69
  const config = loadConfig();
@@ -36,6 +36,8 @@ export interface MergeOptions {
36
36
  branch?: string;
37
37
  /** 提交信息(目标 worktree 工作区有修改时必填) */
38
38
  message?: string;
39
+ /** 遇到冲突直接调用 AI 解决,不再询问 */
40
+ auto?: boolean;
39
41
  }
40
42
 
41
43
  /** remove 命令选项 */
@@ -85,3 +87,9 @@ export interface InitOptions {
85
87
  /** 指定主工作分支名(可选,默认使用当前分支) */
86
88
  branch?: string;
87
89
  }
90
+
91
+ /** tasks init 命令选项 */
92
+ export interface TasksInitOptions {
93
+ /** 输出文件路径(可选,默认 tasks.md) */
94
+ path?: string;
95
+ }
@@ -18,6 +18,10 @@ export interface ClawtConfig {
18
18
  aliases: Record<string, string>;
19
19
  /** 是否启用自动更新检查 */
20
20
  autoUpdate: boolean;
21
+ /** merge 冲突时的解决模式:ask(询问)、auto(自动 AI 解决)、manual(手动解决) */
22
+ conflictResolveMode: 'ask' | 'auto' | 'manual';
23
+ /** Claude Code 冲突解决超时时间(毫秒),默认 300000(5 分钟) */
24
+ conflictResolveTimeoutMs: number;
21
25
  }
22
26
 
23
27
  /** 单个配置项的完整定义(默认值 + 描述) */
@@ -1,5 +1,5 @@
1
1
  export type { ClawtConfig, ConfigItemDefinition, ConfigDefinitions } from './config.js';
2
- export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions, ResumeOptions, SyncOptions, ListOptions, StatusOptions, ProjectsOptions, InitOptions } from './command.js';
2
+ export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions, ResumeOptions, SyncOptions, ListOptions, StatusOptions, ProjectsOptions, InitOptions, TasksInitOptions } from './command.js';
3
3
  export type { WorktreeInfo, WorktreeStatus } from './worktree.js';
4
4
  export type { ClaudeCodeResult } from './claudeCode.js';
5
5
  export type { TaskResult, TaskSummary } from './taskResult.js';
@@ -0,0 +1,170 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { logger } from '../logger/index.js';
3
+ import { ClawtError } from '../errors/index.js';
4
+ import { getConfigValue } from './config.js';
5
+ import { getConflictFiles, gitAddFiles, gitMergeContinue } from './git.js';
6
+ import { printInfo, printSuccess, printWarning } from './formatter.js';
7
+ import { confirmAction } from './formatter.js';
8
+ import { MESSAGES } from '../constants/index.js';
9
+ import { CONFLICT_RESOLVE_PROMPT } from '../constants/ai-prompts.js';
10
+
11
+ /** 默认 Claude Code 冲突解决超时时间(毫秒) */
12
+ const DEFAULT_CONFLICT_RESOLVE_TIMEOUT_MS = 300000;
13
+
14
+ /**
15
+ * 构建发送给 Claude Code 的冲突解决 prompt
16
+ * Claude Code 会自动读取当前工作目录下的冲突文件,无需手动传入
17
+ * @returns {string} 纯指令性 AI prompt
18
+ */
19
+ export function buildConflictResolvePrompt(): string {
20
+ return CONFLICT_RESOLVE_PROMPT;
21
+ }
22
+
23
+ /**
24
+ * 获取冲突解决超时时间(毫秒)
25
+ * 优先使用配置项,未配置时使用默认值
26
+ * @returns {number} 超时时间(毫秒)
27
+ */
28
+ function getConflictResolveTimeout(): number {
29
+ const configValue = getConfigValue('conflictResolveTimeoutMs');
30
+ if (typeof configValue === 'number' && configValue > 0) {
31
+ return configValue;
32
+ }
33
+ return DEFAULT_CONFLICT_RESOLVE_TIMEOUT_MS;
34
+ }
35
+
36
+ /**
37
+ * 调用 Claude Code CLI 以非交互模式解决冲突
38
+ * 使用 execFileSync 数组参数形式避免 shell 注入
39
+ * @param {string} prompt - AI prompt
40
+ * @param {string} cwd - 工作目录
41
+ * @returns {string} Claude Code 的输出
42
+ */
43
+ export function invokeClaudeForConflictResolve(prompt: string, cwd: string): string {
44
+ const args = ['-p', prompt, '--permission-mode', 'bypassPermissions'];
45
+
46
+ logger.info(`调用 Claude Code 解决冲突,命令: claude -p "..." --permission-mode bypassPermissions`);
47
+
48
+ try {
49
+ const output = execFileSync('claude', args, {
50
+ cwd,
51
+ encoding: 'utf-8',
52
+ stdio: ['pipe', 'pipe', 'pipe'],
53
+ timeout: getConflictResolveTimeout(),
54
+ });
55
+ return output;
56
+ } catch (error: unknown) {
57
+ const errMsg = error instanceof Error ? error.message : String(error);
58
+ logger.error(`Claude Code 冲突解决失败: ${errMsg}`);
59
+ throw new ClawtError(MESSAGES.MERGE_CONFLICT_AI_FAILED(errMsg));
60
+ }
61
+ }
62
+
63
+ /**
64
+ * 执行 AI 辅助冲突解决的完整流程
65
+ * 1. 获取冲突文件列表
66
+ * 2. 构建 AI prompt
67
+ * 3. 调用 Claude Code 解决冲突
68
+ * 4. 检查冲突是否解决
69
+ * 5. git add 已解决的文件并 merge --continue
70
+ * @param {string} currentBranch - 当前分支名
71
+ * @param {string} incomingBranch - 传入分支名
72
+ * @param {string} cwd - 工作目录
73
+ * @returns {boolean} 是否所有冲突都已解决
74
+ */
75
+ export function resolveConflictsWithAI(
76
+ currentBranch: string,
77
+ incomingBranch: string,
78
+ cwd: string,
79
+ ): boolean {
80
+ const conflictFiles = getConflictFiles(cwd);
81
+ if (conflictFiles.length === 0) {
82
+ return true;
83
+ }
84
+
85
+ printInfo(MESSAGES.MERGE_CONFLICT_AI_START(conflictFiles.length));
86
+
87
+ // 构建 prompt 并调用 Claude Code,捕获异常提供恢复提示
88
+ const prompt = buildConflictResolvePrompt();
89
+ try {
90
+ invokeClaudeForConflictResolve(prompt, cwd);
91
+ } catch (error) {
92
+ // AI 调用失败时输出友好提示,告知用户可手动解决或重试
93
+ const errMsg = error instanceof ClawtError ? error.message : String(error);
94
+ printWarning(errMsg);
95
+ return false;
96
+ }
97
+
98
+ // 检查冲突是否已解决
99
+ const remainingConflicts = getConflictFiles(cwd);
100
+
101
+ if (remainingConflicts.length === 0) {
102
+ // 所有冲突已解决,git add 所有原冲突文件并完成 merge
103
+ gitAddFiles(conflictFiles, cwd);
104
+ gitMergeContinue(cwd);
105
+ printSuccess(MESSAGES.MERGE_CONFLICT_AI_SUCCESS);
106
+ return true;
107
+ }
108
+
109
+ // 部分冲突已解决,git add 已解决的文件
110
+ const resolvedFiles = conflictFiles.filter((f) => !remainingConflicts.includes(f));
111
+ if (resolvedFiles.length > 0) {
112
+ gitAddFiles(resolvedFiles, cwd);
113
+ }
114
+
115
+ printWarning(MESSAGES.MERGE_CONFLICT_AI_PARTIAL(remainingConflicts.length));
116
+ return false;
117
+ }
118
+
119
+ /**
120
+ * 根据配置和命令选项判断冲突解决模式
121
+ * @param {boolean} [autoFlag] - --auto 命令行参数
122
+ * @returns {'ask' | 'auto' | 'manual'} 冲突解决模式
123
+ */
124
+ export function determineConflictResolveMode(autoFlag?: boolean): 'ask' | 'auto' | 'manual' {
125
+ // --auto 命令行参数优先级最高
126
+ if (autoFlag === true) {
127
+ return 'auto';
128
+ }
129
+
130
+ const configMode = getConfigValue('conflictResolveMode');
131
+ if (configMode === 'auto' || configMode === 'manual') {
132
+ return configMode;
133
+ }
134
+
135
+ return 'ask';
136
+ }
137
+
138
+ /**
139
+ * 处理合并冲突的入口函数
140
+ * 根据冲突解决模式决定处理方式:auto 直接 AI 解决,ask 询问用户,manual 抛出错误
141
+ * @param {string} currentBranch - 当前分支名
142
+ * @param {string} incomingBranch - 传入分支名
143
+ * @param {string} cwd - 工作目录
144
+ * @param {boolean} [autoFlag] - --auto 命令行参数
145
+ * @returns {Promise<boolean>} 是否成功解决了所有冲突
146
+ */
147
+ export async function handleMergeConflict(
148
+ currentBranch: string,
149
+ incomingBranch: string,
150
+ cwd: string,
151
+ autoFlag?: boolean,
152
+ ): Promise<boolean> {
153
+ const mode = determineConflictResolveMode(autoFlag);
154
+
155
+ if (mode === 'manual') {
156
+ throw new ClawtError(MESSAGES.MERGE_CONFLICT_MANUAL);
157
+ }
158
+
159
+ if (mode === 'auto') {
160
+ return resolveConflictsWithAI(currentBranch, incomingBranch, cwd);
161
+ }
162
+
163
+ // mode === 'ask'
164
+ const shouldUseAI = await confirmAction(MESSAGES.MERGE_CONFLICT_ASK_AI);
165
+ if (!shouldUseAI) {
166
+ throw new ClawtError(MESSAGES.MERGE_CONFLICT_MANUAL);
167
+ }
168
+
169
+ return resolveConflictsWithAI(currentBranch, incomingBranch, cwd);
170
+ }
@@ -230,3 +230,22 @@ export function formatLocalISOString(date: Date): string {
230
230
 
231
231
  return `${iso}${sign}${hours}:${minutes}`;
232
232
  }
233
+
234
+ /**
235
+ * 生成任务模板文件名,格式:clawt-tasks-YYYY-MM-DD-HH-mm-ss.md
236
+ * @param {string} prefix - 文件名前缀
237
+ * @returns {string} 带时间戳的文件名
238
+ */
239
+ export function generateTaskFilename(prefix: string): string {
240
+ const now = new Date();
241
+ const pad = (n: number) => String(n).padStart(2, '0');
242
+ const timestamp = [
243
+ now.getFullYear(),
244
+ pad(now.getMonth() + 1),
245
+ pad(now.getDate()),
246
+ pad(now.getHours()),
247
+ pad(now.getMinutes()),
248
+ pad(now.getSeconds()),
249
+ ].join('-');
250
+ return `${prefix}-${timestamp}.md`;
251
+ }
@@ -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
+ }