clawt 1.0.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 +48 -0
- package/.claude/agents/docs-sync-updater.md +128 -0
- package/CLAUDE.md +71 -0
- package/README.md +168 -0
- package/dist/index.js +923 -0
- package/dist/postinstall.js +71 -0
- package/docs/spec.md +710 -0
- package/package.json +38 -0
- package/scripts/postinstall.ts +116 -0
- package/src/commands/create.ts +49 -0
- package/src/commands/list.ts +45 -0
- package/src/commands/merge.ts +142 -0
- package/src/commands/remove.ts +127 -0
- package/src/commands/run.ts +310 -0
- package/src/commands/validate.ts +137 -0
- package/src/constants/branch.ts +6 -0
- package/src/constants/config.ts +8 -0
- package/src/constants/exitCodes.ts +9 -0
- package/src/constants/index.ts +6 -0
- package/src/constants/messages.ts +61 -0
- package/src/constants/paths.ts +14 -0
- package/src/constants/terminal.ts +13 -0
- package/src/errors/index.ts +20 -0
- package/src/index.ts +55 -0
- package/src/logger/index.ts +34 -0
- package/src/types/claudeCode.ts +14 -0
- package/src/types/command.ts +39 -0
- package/src/types/config.ts +7 -0
- package/src/types/index.ts +5 -0
- package/src/types/taskResult.ts +31 -0
- package/src/types/worktree.ts +7 -0
- package/src/utils/branch.ts +51 -0
- package/src/utils/config.ts +35 -0
- package/src/utils/formatter.ts +67 -0
- package/src/utils/fs.ts +28 -0
- package/src/utils/git.ts +243 -0
- package/src/utils/index.ts +35 -0
- package/src/utils/prompt.ts +18 -0
- package/src/utils/shell.ts +53 -0
- package/src/utils/validation.ts +48 -0
- package/src/utils/worktree.ts +107 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +25 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import type { ChildProcess } from 'node:child_process';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { logger } from '../logger/index.js';
|
|
5
|
+
import { ClawtError } from '../errors/index.js';
|
|
6
|
+
import { MESSAGES } from '../constants/index.js';
|
|
7
|
+
import type { RunOptions, ClaudeCodeResult, TaskResult, TaskSummary } from '../types/index.js';
|
|
8
|
+
import {
|
|
9
|
+
validateMainWorktree,
|
|
10
|
+
validateClaudeCodeInstalled,
|
|
11
|
+
createWorktrees,
|
|
12
|
+
spawnProcess,
|
|
13
|
+
killAllChildProcesses,
|
|
14
|
+
cleanupWorktrees,
|
|
15
|
+
getConfigValue,
|
|
16
|
+
printSuccess,
|
|
17
|
+
printError,
|
|
18
|
+
printWarning,
|
|
19
|
+
printInfo,
|
|
20
|
+
printSeparator,
|
|
21
|
+
printDoubleSeparator,
|
|
22
|
+
confirmAction,
|
|
23
|
+
} from '../utils/index.js';
|
|
24
|
+
import type { WorktreeInfo } from '../types/index.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 注册 run 命令:批量创建 worktree 并执行 Claude Code 任务
|
|
28
|
+
* @param {Command} program - Commander 实例
|
|
29
|
+
*/
|
|
30
|
+
export function registerRunCommand(program: Command): void {
|
|
31
|
+
program
|
|
32
|
+
.command('run')
|
|
33
|
+
.description('批量创建 worktree 并启动 Claude Code 执行任务')
|
|
34
|
+
.requiredOption('-b, --branch <branchName>', '分支名')
|
|
35
|
+
.option('--tasks <task...>', '任务列表(可多次指定),不传则在 worktree 中打开 Claude Code 交互式界面')
|
|
36
|
+
.action(async (options: RunOptions) => {
|
|
37
|
+
await handleRun(options);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 在指定 worktree 中启动 Claude Code CLI 交互式界面
|
|
43
|
+
* 使用 spawnSync + inherit stdio,让用户直接与 Claude Code 交互
|
|
44
|
+
* @param {WorktreeInfo} worktree - worktree 信息
|
|
45
|
+
*/
|
|
46
|
+
function launchInteractiveClaude(worktree: WorktreeInfo): void {
|
|
47
|
+
const commandStr = getConfigValue('claudeCodeCommand');
|
|
48
|
+
const parts = commandStr.split(/\s+/).filter(Boolean);
|
|
49
|
+
const cmd = parts[0];
|
|
50
|
+
const args = parts.slice(1);
|
|
51
|
+
|
|
52
|
+
printInfo(`正在 worktree 中启动 Claude Code 交互式界面...`);
|
|
53
|
+
printInfo(` 分支: ${worktree.branch}`);
|
|
54
|
+
printInfo(` 路径: ${worktree.path}`);
|
|
55
|
+
printInfo(` 指令: ${commandStr}`);
|
|
56
|
+
printInfo('');
|
|
57
|
+
|
|
58
|
+
const result = spawnSync(cmd, args, {
|
|
59
|
+
cwd: worktree.path,
|
|
60
|
+
stdio: 'inherit',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (result.error) {
|
|
64
|
+
throw new ClawtError(`启动 Claude Code 失败: ${result.error.message}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (result.status !== null && result.status !== 0) {
|
|
68
|
+
printWarning(`Claude Code 退出码: ${result.status}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** executeClaudeTask 的返回结构,包含子进程引用和结果 Promise */
|
|
73
|
+
interface ClaudeTaskHandle {
|
|
74
|
+
/** 子进程实例,用于在中断时终止 */
|
|
75
|
+
child: ChildProcess;
|
|
76
|
+
/** 任务结果 Promise */
|
|
77
|
+
promise: Promise<TaskResult>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 在指定 worktree 中执行 Claude Code 任务
|
|
82
|
+
* @param {WorktreeInfo} worktree - worktree 信息
|
|
83
|
+
* @param {string} task - 任务描述
|
|
84
|
+
* @returns {ClaudeTaskHandle} 包含子进程引用和结果 Promise
|
|
85
|
+
*/
|
|
86
|
+
function executeClaudeTask(worktree: WorktreeInfo, task: string): ClaudeTaskHandle {
|
|
87
|
+
const child = spawnProcess(
|
|
88
|
+
'claude',
|
|
89
|
+
['-p', task, '--output-format', 'json', '--permission-mode', 'bypassPermissions'],
|
|
90
|
+
{
|
|
91
|
+
cwd: worktree.path,
|
|
92
|
+
// stdin 必须设置为 'ignore',不能用 'pipe'
|
|
93
|
+
// 原因:claude -p 是非交互模式,不需要 stdin 输入。如果 stdin 为 'pipe',
|
|
94
|
+
// 父进程会创建一个可写流连接到子进程但从不写入也不关闭,
|
|
95
|
+
// claude 检测到 stdin 是管道后会尝试读取输入,导致进程永远卡住
|
|
96
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const promise = new Promise<TaskResult>((resolve) => {
|
|
101
|
+
let stdout = '';
|
|
102
|
+
let stderr = '';
|
|
103
|
+
|
|
104
|
+
child.stdout?.on('data', (data: Buffer) => {
|
|
105
|
+
stdout += data.toString();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
child.stderr?.on('data', (data: Buffer) => {
|
|
109
|
+
stderr += data.toString();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
child.on('close', (code) => {
|
|
113
|
+
let result: ClaudeCodeResult | null = null;
|
|
114
|
+
let success = code === 0;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
if (stdout.trim()) {
|
|
118
|
+
result = JSON.parse(stdout.trim()) as ClaudeCodeResult;
|
|
119
|
+
success = !result.is_error;
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
logger.warn(`解析 Claude Code 输出失败: ${stdout.substring(0, 200)}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
resolve({
|
|
126
|
+
task,
|
|
127
|
+
branch: worktree.branch,
|
|
128
|
+
worktreePath: worktree.path,
|
|
129
|
+
success,
|
|
130
|
+
result,
|
|
131
|
+
error: success ? undefined : stderr || '任务执行失败',
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
child.on('error', (err) => {
|
|
136
|
+
resolve({
|
|
137
|
+
task,
|
|
138
|
+
branch: worktree.branch,
|
|
139
|
+
worktreePath: worktree.path,
|
|
140
|
+
success: false,
|
|
141
|
+
result: null,
|
|
142
|
+
error: err.message,
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return { child, promise };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 输出单个任务完成通知
|
|
152
|
+
* @param {TaskResult} taskResult - 任务结果
|
|
153
|
+
*/
|
|
154
|
+
function printTaskNotification(taskResult: TaskResult): void {
|
|
155
|
+
const { success, worktreePath, branch, result } = taskResult;
|
|
156
|
+
const status = success ? '完成' : '失败';
|
|
157
|
+
const icon = success ? '✓' : '✗';
|
|
158
|
+
const durationStr = result ? `${(result.duration_ms / 1000).toFixed(1)}s` : 'N/A';
|
|
159
|
+
const costStr = result ? `$${result.total_cost_usd.toFixed(2)}` : 'N/A';
|
|
160
|
+
const resultStr = success ? 'success' : 'failed';
|
|
161
|
+
|
|
162
|
+
if (success) {
|
|
163
|
+
printSuccess(`${icon} [${status}] worktree: ${worktreePath}`);
|
|
164
|
+
} else {
|
|
165
|
+
printError(`${icon} [${status}] worktree: ${worktreePath}`);
|
|
166
|
+
}
|
|
167
|
+
printInfo(` 分支: ${branch}`);
|
|
168
|
+
printInfo(` 耗时: ${durationStr}`);
|
|
169
|
+
printInfo(` 花费: ${costStr}`);
|
|
170
|
+
printInfo(` 结果: ${resultStr}`);
|
|
171
|
+
printSeparator();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 输出所有任务的汇总信息
|
|
176
|
+
* @param {TaskSummary} summary - 汇总信息
|
|
177
|
+
*/
|
|
178
|
+
function printTaskSummary(summary: TaskSummary): void {
|
|
179
|
+
printDoubleSeparator();
|
|
180
|
+
printInfo(`全部任务已完成 (${summary.total}/${summary.total})`);
|
|
181
|
+
printInfo(` 成功: ${summary.succeeded}`);
|
|
182
|
+
printInfo(` 失败: ${summary.failed}`);
|
|
183
|
+
printInfo(` 总耗时: ${(summary.totalDurationMs / 1000).toFixed(1)}s`);
|
|
184
|
+
printInfo(` 总花费: $${summary.totalCostUsd.toFixed(2)}`);
|
|
185
|
+
printDoubleSeparator();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 处理用户中断(Ctrl+C)后的清理流程
|
|
190
|
+
* 根据全局配置决定自动清理或交互式确认
|
|
191
|
+
* @param {WorktreeInfo[]} worktrees - 本次创建的 worktree 列表
|
|
192
|
+
*/
|
|
193
|
+
async function handleInterruptCleanup(worktrees: WorktreeInfo[]): Promise<void> {
|
|
194
|
+
const autoDelete = getConfigValue('autoDeleteBranch');
|
|
195
|
+
|
|
196
|
+
if (autoDelete) {
|
|
197
|
+
// 全局配置了自动删除,直接清理
|
|
198
|
+
cleanupWorktrees(worktrees);
|
|
199
|
+
printSuccess(MESSAGES.INTERRUPT_AUTO_CLEANED(worktrees.length));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 交互式确认是否清理
|
|
204
|
+
const shouldClean = await confirmAction(MESSAGES.INTERRUPT_CONFIRM_CLEANUP);
|
|
205
|
+
|
|
206
|
+
if (shouldClean) {
|
|
207
|
+
cleanupWorktrees(worktrees);
|
|
208
|
+
printSuccess(MESSAGES.INTERRUPT_CLEANED(worktrees.length));
|
|
209
|
+
} else {
|
|
210
|
+
printInfo(MESSAGES.INTERRUPT_KEPT);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 执行 run 命令的核心逻辑
|
|
216
|
+
* 不传 --tasks 时创建单个 worktree 并打开 Claude Code 交互式界面
|
|
217
|
+
* 传 --tasks 时批量创建 worktree 并并行执行任务
|
|
218
|
+
* @param {RunOptions} options - 命令选项
|
|
219
|
+
*/
|
|
220
|
+
async function handleRun(options: RunOptions): Promise<void> {
|
|
221
|
+
validateMainWorktree();
|
|
222
|
+
validateClaudeCodeInstalled();
|
|
223
|
+
|
|
224
|
+
// 未传 --tasks 时,创建单个 worktree 并打开 Claude Code 交互式界面
|
|
225
|
+
if (!options.tasks || options.tasks.length === 0) {
|
|
226
|
+
const worktrees = createWorktrees(options.branch, 1);
|
|
227
|
+
const worktree = worktrees[0];
|
|
228
|
+
printSuccess(MESSAGES.WORKTREE_CREATED(1));
|
|
229
|
+
|
|
230
|
+
launchInteractiveClaude(worktree);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const tasks = options.tasks.map((t) => t.trim()).filter(Boolean);
|
|
235
|
+
|
|
236
|
+
if (tasks.length === 0) {
|
|
237
|
+
throw new ClawtError('任务列表不能为空');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const count = tasks.length;
|
|
241
|
+
logger.info(`run 命令执行,分支: ${options.branch},任务数: ${count}`);
|
|
242
|
+
|
|
243
|
+
// 创建 worktree
|
|
244
|
+
const worktrees = createWorktrees(options.branch, count);
|
|
245
|
+
printSuccess(MESSAGES.WORKTREE_CREATED(worktrees.length));
|
|
246
|
+
for (const wt of worktrees) {
|
|
247
|
+
printInfo(` 分支: ${wt.branch} 路径: ${wt.path}`);
|
|
248
|
+
}
|
|
249
|
+
printInfo('');
|
|
250
|
+
|
|
251
|
+
// 并行执行 Claude Code 任务,每个完成时实时通知
|
|
252
|
+
const startTime = Date.now();
|
|
253
|
+
const handles = worktrees.map((wt, index) => {
|
|
254
|
+
const task = tasks[index];
|
|
255
|
+
logger.info(`启动任务 ${index + 1}: ${task} (worktree: ${wt.path})`);
|
|
256
|
+
return executeClaudeTask(wt, task);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// 收集所有子进程引用,用于中断时终止
|
|
260
|
+
const childProcesses = handles.map((h) => h.child);
|
|
261
|
+
|
|
262
|
+
// 监听 SIGINT(Ctrl+C),终止所有子进程并触发清理流程
|
|
263
|
+
let interrupted = false;
|
|
264
|
+
const sigintHandler = async () => {
|
|
265
|
+
if (interrupted) return;
|
|
266
|
+
interrupted = true;
|
|
267
|
+
|
|
268
|
+
printInfo('');
|
|
269
|
+
printWarning(MESSAGES.INTERRUPTED);
|
|
270
|
+
killAllChildProcesses(childProcesses);
|
|
271
|
+
|
|
272
|
+
// 等待所有子进程退出后再执行清理
|
|
273
|
+
await Promise.allSettled(handles.map((h) => h.promise));
|
|
274
|
+
|
|
275
|
+
await handleInterruptCleanup(worktrees);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
};
|
|
278
|
+
process.on('SIGINT', sigintHandler);
|
|
279
|
+
|
|
280
|
+
const taskPromises = handles.map((handle) =>
|
|
281
|
+
handle.promise.then((result) => {
|
|
282
|
+
// 被中断时不再输出通知
|
|
283
|
+
if (!interrupted) {
|
|
284
|
+
printTaskNotification(result);
|
|
285
|
+
}
|
|
286
|
+
return result;
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const results = await Promise.all(taskPromises);
|
|
291
|
+
|
|
292
|
+
// 正常完成,移除 SIGINT 监听器
|
|
293
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
294
|
+
|
|
295
|
+
// 被中断时不输出汇总(已在 sigintHandler 中处理退出)
|
|
296
|
+
if (interrupted) return;
|
|
297
|
+
|
|
298
|
+
const totalDurationMs = Date.now() - startTime;
|
|
299
|
+
|
|
300
|
+
// 汇总
|
|
301
|
+
const summary: TaskSummary = {
|
|
302
|
+
total: results.length,
|
|
303
|
+
succeeded: results.filter((r) => r.success).length,
|
|
304
|
+
failed: results.filter((r) => !r.success).length,
|
|
305
|
+
totalDurationMs,
|
|
306
|
+
totalCostUsd: results.reduce((sum, r) => sum + (r.result?.total_cost_usd ?? 0), 0),
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
printTaskSummary(summary);
|
|
310
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import Enquirer from 'enquirer';
|
|
5
|
+
import { logger } from '../logger/index.js';
|
|
6
|
+
import { ClawtError } from '../errors/index.js';
|
|
7
|
+
import { MESSAGES } from '../constants/index.js';
|
|
8
|
+
import type { ValidateOptions } from '../types/index.js';
|
|
9
|
+
import {
|
|
10
|
+
validateMainWorktree,
|
|
11
|
+
getProjectName,
|
|
12
|
+
getGitTopLevel,
|
|
13
|
+
getProjectWorktreeDir,
|
|
14
|
+
isWorkingDirClean,
|
|
15
|
+
gitAddAll,
|
|
16
|
+
gitStashPush,
|
|
17
|
+
gitStashApply,
|
|
18
|
+
gitStashPop,
|
|
19
|
+
gitStashList,
|
|
20
|
+
gitRestoreStaged,
|
|
21
|
+
gitResetHard,
|
|
22
|
+
gitCleanForce,
|
|
23
|
+
getStatusPorcelain,
|
|
24
|
+
printSuccess,
|
|
25
|
+
printError,
|
|
26
|
+
printWarning,
|
|
27
|
+
printInfo,
|
|
28
|
+
} from '../utils/index.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 注册 validate 命令:在主 worktree 验证其他分支的变更
|
|
32
|
+
* @param {Command} program - Commander 实例
|
|
33
|
+
*/
|
|
34
|
+
export function registerValidateCommand(program: Command): void {
|
|
35
|
+
program
|
|
36
|
+
.command('validate')
|
|
37
|
+
.description('在主 worktree 验证某个 worktree 分支的变更')
|
|
38
|
+
.requiredOption('-b, --branch <branchName>', '要验证的分支名')
|
|
39
|
+
.action(async (options: ValidateOptions) => {
|
|
40
|
+
await handleValidate(options);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 处理主 worktree 工作区有未提交更改的情况
|
|
46
|
+
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
47
|
+
*/
|
|
48
|
+
async function handleDirtyMainWorktree(mainWorktreePath: string): Promise<void> {
|
|
49
|
+
printWarning('主 worktree 当前分支有未提交的更改,请选择处理方式:\n');
|
|
50
|
+
|
|
51
|
+
// @ts-expect-error enquirer 类型声明未导出 Select 类,但运行时存在
|
|
52
|
+
const choice = await new Enquirer.Select({
|
|
53
|
+
message: '选择处理方式',
|
|
54
|
+
choices: [
|
|
55
|
+
{
|
|
56
|
+
name: 'reset',
|
|
57
|
+
message: 'reset (推荐) - 丢弃所有更改 (git reset --hard HEAD && git clean -fd)',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'stash',
|
|
61
|
+
message: 'stash - 暂存更改 (git add . && git stash)',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'exit',
|
|
65
|
+
message: 'exit - 退出,手动处理',
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
initial: 0,
|
|
69
|
+
}).run();
|
|
70
|
+
|
|
71
|
+
if (choice === 'exit') {
|
|
72
|
+
throw new ClawtError('用户选择退出');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (choice === 'reset') {
|
|
76
|
+
gitResetHard(mainWorktreePath);
|
|
77
|
+
gitCleanForce(mainWorktreePath);
|
|
78
|
+
} else if (choice === 'stash') {
|
|
79
|
+
gitAddAll(mainWorktreePath);
|
|
80
|
+
gitStashPush('clawt:auto-stash', mainWorktreePath);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 再次检查是否干净
|
|
84
|
+
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
85
|
+
throw new ClawtError('工作区仍然不干净,请手动处理');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 执行 validate 命令的核心逻辑
|
|
91
|
+
* @param {ValidateOptions} options - 命令选项
|
|
92
|
+
*/
|
|
93
|
+
async function handleValidate(options: ValidateOptions): Promise<void> {
|
|
94
|
+
validateMainWorktree();
|
|
95
|
+
|
|
96
|
+
const projectName = getProjectName();
|
|
97
|
+
const mainWorktreePath = getGitTopLevel();
|
|
98
|
+
const projectDir = getProjectWorktreeDir();
|
|
99
|
+
const targetWorktreePath = join(projectDir, options.branch);
|
|
100
|
+
|
|
101
|
+
logger.info(`validate 命令执行,分支: ${options.branch}`);
|
|
102
|
+
|
|
103
|
+
// 检查目标 worktree 是否存在
|
|
104
|
+
if (!existsSync(targetWorktreePath)) {
|
|
105
|
+
throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 步骤 1:检测主 worktree 工作区状态
|
|
109
|
+
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
110
|
+
await handleDirtyMainWorktree(mainWorktreePath);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 步骤 2:在目标 worktree 中创建 stash
|
|
114
|
+
if (isWorkingDirClean(targetWorktreePath)) {
|
|
115
|
+
printInfo(MESSAGES.TARGET_WORKTREE_CLEAN);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const stashMessage = `clawt:validate:${options.branch}`;
|
|
120
|
+
gitAddAll(targetWorktreePath);
|
|
121
|
+
gitStashPush(stashMessage, targetWorktreePath);
|
|
122
|
+
gitStashApply(targetWorktreePath);
|
|
123
|
+
gitRestoreStaged(targetWorktreePath);
|
|
124
|
+
|
|
125
|
+
// 步骤 3:在主 worktree 应用 stash
|
|
126
|
+
const stashList = gitStashList(mainWorktreePath);
|
|
127
|
+
const firstLine = stashList.split('\n')[0] || '';
|
|
128
|
+
|
|
129
|
+
if (!firstLine.includes(stashMessage)) {
|
|
130
|
+
throw new ClawtError(MESSAGES.STASH_CHANGED);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
gitStashPop(0, mainWorktreePath);
|
|
134
|
+
|
|
135
|
+
// 步骤 4:输出成功提示
|
|
136
|
+
printSuccess(MESSAGES.VALIDATE_SUCCESS(options.branch));
|
|
137
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { CLAWT_HOME, CONFIG_PATH, LOGS_DIR, WORKTREES_DIR } from './paths.js';
|
|
2
|
+
export { INVALID_BRANCH_CHARS } from './branch.js';
|
|
3
|
+
export { MESSAGES } from './messages.js';
|
|
4
|
+
export { EXIT_CODES } from './exitCodes.js';
|
|
5
|
+
export { ENABLE_BRACKETED_PASTE, DISABLE_BRACKETED_PASTE, PASTE_THRESHOLD_MS } from './terminal.js';
|
|
6
|
+
export { DEFAULT_CONFIG } from './config.js';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** 提示消息模板 */
|
|
2
|
+
export const MESSAGES = {
|
|
3
|
+
/** 不在主 worktree 根目录 */
|
|
4
|
+
NOT_MAIN_WORKTREE: '请在主 worktree 的根目录下执行 clawt',
|
|
5
|
+
/** Git 未安装 */
|
|
6
|
+
GIT_NOT_INSTALLED: 'Git 未安装或不在 PATH 中,请先安装 Git',
|
|
7
|
+
/** Claude Code CLI 未安装 */
|
|
8
|
+
CLAUDE_NOT_INSTALLED: 'Claude Code CLI 未安装,请先安装:npm install -g @anthropic-ai/claude-code',
|
|
9
|
+
/** 分支已存在 */
|
|
10
|
+
BRANCH_EXISTS: (name: string) => `分支 ${name} 已存在,无法创建`,
|
|
11
|
+
/** 分支名被转换 */
|
|
12
|
+
BRANCH_SANITIZED: (original: string, sanitized: string) =>
|
|
13
|
+
`分支名已转换: ${original} → ${sanitized}`,
|
|
14
|
+
/** worktree 创建成功 */
|
|
15
|
+
WORKTREE_CREATED: (count: number) => `✓ 已创建 ${count} 个 worktree`,
|
|
16
|
+
/** worktree 移除成功 */
|
|
17
|
+
WORKTREE_REMOVED: (path: string) => `✓ 已移除 worktree: ${path}`,
|
|
18
|
+
/** 没有 worktree */
|
|
19
|
+
NO_WORKTREES: '(无 worktree)',
|
|
20
|
+
/** 目标 worktree 不存在 */
|
|
21
|
+
WORKTREE_NOT_FOUND: (name: string) => `worktree ${name} 不存在`,
|
|
22
|
+
/** 主 worktree 有未提交更改 */
|
|
23
|
+
MAIN_WORKTREE_DIRTY: '主 worktree 有未提交的更改,请先处理',
|
|
24
|
+
/** 目标 worktree 无更改 */
|
|
25
|
+
TARGET_WORKTREE_CLEAN: '该 worktree 的分支上没有任何更改,无需验证',
|
|
26
|
+
/** stash 已变更 */
|
|
27
|
+
STASH_CHANGED: 'git stash list 已变更,请重新执行',
|
|
28
|
+
/** validate 成功 */
|
|
29
|
+
VALIDATE_SUCCESS: (branch: string) =>
|
|
30
|
+
`✓ 已将分支 ${branch} 的变更应用到主 worktree\n 可以开始验证了`,
|
|
31
|
+
/** merge 成功 */
|
|
32
|
+
MERGE_SUCCESS: (branch: string, message: string) =>
|
|
33
|
+
`✓ 分支 ${branch} 已成功合并到当前分支\n 提交信息: ${message}\n 已推送到远程仓库`,
|
|
34
|
+
/** merge 成功(无提交信息,目标 worktree 已提交过) */
|
|
35
|
+
MERGE_SUCCESS_NO_MESSAGE: (branch: string) =>
|
|
36
|
+
`✓ 分支 ${branch} 已成功合并到当前分支\n 已推送到远程仓库`,
|
|
37
|
+
/** merge 冲突 */
|
|
38
|
+
MERGE_CONFLICT: '合并存在冲突,请手动处理',
|
|
39
|
+
/** merge 后清理 worktree 和分支成功 */
|
|
40
|
+
WORKTREE_CLEANED: (branch: string) => `✓ 已清理 worktree 和分支: ${branch}`,
|
|
41
|
+
/** 请提供提交信息 */
|
|
42
|
+
COMMIT_MESSAGE_REQUIRED: '请提供提交信息(-m 参数)',
|
|
43
|
+
/** 目标 worktree 有未提交修改但未指定 -m */
|
|
44
|
+
TARGET_WORKTREE_DIRTY_NO_MESSAGE: '目标 worktree 有未提交的修改,请通过 -m 参数提供提交信息',
|
|
45
|
+
/** 目标 worktree 既干净又无本地提交 */
|
|
46
|
+
TARGET_WORKTREE_NO_CHANGES: '目标 worktree 没有任何可合并的变更(工作区干净且无本地提交)',
|
|
47
|
+
/** 检测到用户中断 */
|
|
48
|
+
INTERRUPTED: '检测到退出指令,已停止 Claude Code 任务',
|
|
49
|
+
/** 中断后自动清理完成 */
|
|
50
|
+
INTERRUPT_AUTO_CLEANED: (count: number) => `✓ 已自动清理 ${count} 个 worktree 和对应分支`,
|
|
51
|
+
/** 中断后手动确认清理 */
|
|
52
|
+
INTERRUPT_CONFIRM_CLEANUP: '是否移除刚刚创建的 worktree 和对应分支?',
|
|
53
|
+
/** 中断后清理完成 */
|
|
54
|
+
INTERRUPT_CLEANED: (count: number) => `✓ 已清理 ${count} 个 worktree 和对应分支`,
|
|
55
|
+
/** 中断后保留 worktree */
|
|
56
|
+
INTERRUPT_KEPT: '已保留 worktree,可稍后使用 clawt remove 手动清理',
|
|
57
|
+
/** 分隔线 */
|
|
58
|
+
SEPARATOR: '────────────────────────────────────────',
|
|
59
|
+
/** 粗分隔线 */
|
|
60
|
+
DOUBLE_SEPARATOR: '════════════════════════════════════════',
|
|
61
|
+
} as const;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/** clawt 主目录 ~/.clawt/ */
|
|
5
|
+
export const CLAWT_HOME = join(homedir(), '.clawt');
|
|
6
|
+
|
|
7
|
+
/** 全局配置文件路径 ~/.clawt/config.json */
|
|
8
|
+
export const CONFIG_PATH = join(CLAWT_HOME, 'config.json');
|
|
9
|
+
|
|
10
|
+
/** 日志目录 ~/.clawt/logs/ */
|
|
11
|
+
export const LOGS_DIR = join(CLAWT_HOME, 'logs');
|
|
12
|
+
|
|
13
|
+
/** worktree 统一存放目录 ~/.clawt/worktrees/ */
|
|
14
|
+
export const WORKTREES_DIR = join(CLAWT_HOME, 'worktrees');
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** 启用 Bracketed Paste Mode 的 ANSI 转义序列 */
|
|
2
|
+
export const ENABLE_BRACKETED_PASTE = '\x1b[?2004h';
|
|
3
|
+
|
|
4
|
+
/** 禁用 Bracketed Paste Mode 的 ANSI 转义序列 */
|
|
5
|
+
export const DISABLE_BRACKETED_PASTE = '\x1b[?2004l';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 粘贴检测的时间阈值(毫秒)
|
|
9
|
+
* 粘贴时字符在同一事件循环内批量到达,间隔接近 0ms
|
|
10
|
+
* 手动按键间隔通常 > 50ms
|
|
11
|
+
* 作为 Bracketed Paste Mode 不可用时的降级方案
|
|
12
|
+
*/
|
|
13
|
+
export const PASTE_THRESHOLD_MS = 10;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { EXIT_CODES } from '../constants/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Clawt 自定义错误类
|
|
5
|
+
* 携带退出码信息,用于统一错误处理
|
|
6
|
+
*/
|
|
7
|
+
export class ClawtError extends Error {
|
|
8
|
+
/** 退出码 */
|
|
9
|
+
public readonly exitCode: number;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} message - 错误消息
|
|
13
|
+
* @param {number} exitCode - 退出码,默认为 EXIT_CODES.ERROR (1)
|
|
14
|
+
*/
|
|
15
|
+
constructor(message: string, exitCode: number = EXIT_CODES.ERROR) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'ClawtError';
|
|
18
|
+
this.exitCode = exitCode;
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { ClawtError } from './errors/index.js';
|
|
3
|
+
import { logger } from './logger/index.js';
|
|
4
|
+
import { EXIT_CODES } from './constants/index.js';
|
|
5
|
+
import { printError, ensureClawtDirs } from './utils/index.js';
|
|
6
|
+
import { registerListCommand } from './commands/list.js';
|
|
7
|
+
import { registerCreateCommand } from './commands/create.js';
|
|
8
|
+
import { registerRemoveCommand } from './commands/remove.js';
|
|
9
|
+
import { registerRunCommand } from './commands/run.js';
|
|
10
|
+
import { registerValidateCommand } from './commands/validate.js';
|
|
11
|
+
import { registerMergeCommand } from './commands/merge.js';
|
|
12
|
+
|
|
13
|
+
// 确保全局目录结构存在
|
|
14
|
+
ensureClawtDirs();
|
|
15
|
+
|
|
16
|
+
const program = new Command();
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.name('clawt')
|
|
20
|
+
.description('本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具')
|
|
21
|
+
.version('1.0.0');
|
|
22
|
+
|
|
23
|
+
// 注册所有命令
|
|
24
|
+
registerListCommand(program);
|
|
25
|
+
registerCreateCommand(program);
|
|
26
|
+
registerRemoveCommand(program);
|
|
27
|
+
registerRunCommand(program);
|
|
28
|
+
registerValidateCommand(program);
|
|
29
|
+
registerMergeCommand(program);
|
|
30
|
+
|
|
31
|
+
// 全局未捕获异常处理
|
|
32
|
+
process.on('uncaughtException', (error) => {
|
|
33
|
+
if (error instanceof ClawtError) {
|
|
34
|
+
printError(error.message);
|
|
35
|
+
logger.error(error.message);
|
|
36
|
+
process.exit(error.exitCode);
|
|
37
|
+
}
|
|
38
|
+
printError(error.message || '未知错误');
|
|
39
|
+
logger.error(`未捕获异常: ${error.message}\n${error.stack}`);
|
|
40
|
+
process.exit(EXIT_CODES.ERROR);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
process.on('unhandledRejection', (reason) => {
|
|
44
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
45
|
+
if (error instanceof ClawtError) {
|
|
46
|
+
printError(error.message);
|
|
47
|
+
logger.error(error.message);
|
|
48
|
+
process.exit(error.exitCode);
|
|
49
|
+
}
|
|
50
|
+
printError(error.message || '未知错误');
|
|
51
|
+
logger.error(`未处理的 Promise 拒绝: ${error.message}`);
|
|
52
|
+
process.exit(EXIT_CODES.ERROR);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import winston from 'winston';
|
|
2
|
+
import DailyRotateFile from 'winston-daily-rotate-file';
|
|
3
|
+
import { LOGS_DIR } from '../constants/index.js';
|
|
4
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
// 确保日志目录存在
|
|
7
|
+
if (!existsSync(LOGS_DIR)) {
|
|
8
|
+
mkdirSync(LOGS_DIR, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** 日志格式:[2025-02-06 14:30:22] [INFO] 消息内容 */
|
|
12
|
+
const logFormat = winston.format.printf(({ level, message, timestamp }) => {
|
|
13
|
+
const upperLevel = level.toUpperCase().padEnd(5);
|
|
14
|
+
return `[${timestamp}] [${upperLevel}] ${message}`;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/** 日志滚动传输:按日期滚动,保留 30 天,单文件最大 10MB */
|
|
18
|
+
const dailyRotateTransport = new DailyRotateFile({
|
|
19
|
+
dirname: LOGS_DIR,
|
|
20
|
+
filename: 'clawt-%DATE%.log',
|
|
21
|
+
datePattern: 'YYYY-MM-DD',
|
|
22
|
+
maxSize: '10m',
|
|
23
|
+
maxFiles: '30d',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/** winston 日志实例 */
|
|
27
|
+
export const logger = winston.createLogger({
|
|
28
|
+
level: 'debug',
|
|
29
|
+
format: winston.format.combine(
|
|
30
|
+
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
|
31
|
+
logFormat,
|
|
32
|
+
),
|
|
33
|
+
transports: [dailyRotateTransport],
|
|
34
|
+
});
|