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.
Files changed (43) hide show
  1. package/.claude/agent-memory/docs-sync-updater/MEMORY.md +48 -0
  2. package/.claude/agents/docs-sync-updater.md +128 -0
  3. package/CLAUDE.md +71 -0
  4. package/README.md +168 -0
  5. package/dist/index.js +923 -0
  6. package/dist/postinstall.js +71 -0
  7. package/docs/spec.md +710 -0
  8. package/package.json +38 -0
  9. package/scripts/postinstall.ts +116 -0
  10. package/src/commands/create.ts +49 -0
  11. package/src/commands/list.ts +45 -0
  12. package/src/commands/merge.ts +142 -0
  13. package/src/commands/remove.ts +127 -0
  14. package/src/commands/run.ts +310 -0
  15. package/src/commands/validate.ts +137 -0
  16. package/src/constants/branch.ts +6 -0
  17. package/src/constants/config.ts +8 -0
  18. package/src/constants/exitCodes.ts +9 -0
  19. package/src/constants/index.ts +6 -0
  20. package/src/constants/messages.ts +61 -0
  21. package/src/constants/paths.ts +14 -0
  22. package/src/constants/terminal.ts +13 -0
  23. package/src/errors/index.ts +20 -0
  24. package/src/index.ts +55 -0
  25. package/src/logger/index.ts +34 -0
  26. package/src/types/claudeCode.ts +14 -0
  27. package/src/types/command.ts +39 -0
  28. package/src/types/config.ts +7 -0
  29. package/src/types/index.ts +5 -0
  30. package/src/types/taskResult.ts +31 -0
  31. package/src/types/worktree.ts +7 -0
  32. package/src/utils/branch.ts +51 -0
  33. package/src/utils/config.ts +35 -0
  34. package/src/utils/formatter.ts +67 -0
  35. package/src/utils/fs.ts +28 -0
  36. package/src/utils/git.ts +243 -0
  37. package/src/utils/index.ts +35 -0
  38. package/src/utils/prompt.ts +18 -0
  39. package/src/utils/shell.ts +53 -0
  40. package/src/utils/validation.ts +48 -0
  41. package/src/utils/worktree.ts +107 -0
  42. package/tsconfig.json +17 -0
  43. package/tsup.config.ts +25 -0
@@ -0,0 +1,14 @@
1
+ /** Claude Code CLI 输出的 JSON 结果 */
2
+ export interface ClaudeCodeResult {
3
+ type: string;
4
+ subtype: string;
5
+ is_error: boolean;
6
+ duration_ms: number;
7
+ duration_api_ms: number;
8
+ num_turns: number;
9
+ result: string;
10
+ stop_reason: string;
11
+ session_id: string;
12
+ total_cost_usd: number;
13
+ usage: Record<string, unknown>;
14
+ }
@@ -0,0 +1,39 @@
1
+ /** create 命令选项 */
2
+ export interface CreateOptions {
3
+ /** 分支名 */
4
+ branch: string;
5
+ /** 创建数量,默认 1 */
6
+ number: number;
7
+ }
8
+
9
+ /** run 命令选项 */
10
+ export interface RunOptions {
11
+ /** 分支名 */
12
+ branch: string;
13
+ /** 任务列表(支持多次 --tasks 传入),不传则在 worktree 中打开 Claude Code 交互式界面 */
14
+ tasks?: string[];
15
+ }
16
+
17
+ /** validate 命令选项 */
18
+ export interface ValidateOptions {
19
+ /** 要验证的分支名 */
20
+ branch: string;
21
+ }
22
+
23
+ /** merge 命令选项 */
24
+ export interface MergeOptions {
25
+ /** 要合并的分支名 */
26
+ branch: string;
27
+ /** 提交信息(工作区有修改时必填) */
28
+ message?: string;
29
+ }
30
+
31
+ /** remove 命令选项 */
32
+ export interface RemoveOptions {
33
+ /** 移除所有 worktree */
34
+ all?: boolean;
35
+ /** 分支名 */
36
+ branch?: string;
37
+ /** 指定索引 */
38
+ index?: number;
39
+ }
@@ -0,0 +1,7 @@
1
+ /** clawt 全局配置 */
2
+ export interface ClawtConfig {
3
+ /** 移除 worktree 时是否自动删除对应本地分支 */
4
+ autoDeleteBranch: boolean;
5
+ /** Claude Code CLI 启动指令(不传 --tasks 时在 worktree 中直接打开交互式界面) */
6
+ claudeCodeCommand: string;
7
+ }
@@ -0,0 +1,5 @@
1
+ export type { ClawtConfig } from './config.js';
2
+ export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions } from './command.js';
3
+ export type { WorktreeInfo } from './worktree.js';
4
+ export type { ClaudeCodeResult } from './claudeCode.js';
5
+ export type { TaskResult, TaskSummary } from './taskResult.js';
@@ -0,0 +1,31 @@
1
+ import type { ClaudeCodeResult } from './claudeCode.js';
2
+
3
+ /** 单个任务的执行结果 */
4
+ export interface TaskResult {
5
+ /** 任务描述 */
6
+ task: string;
7
+ /** 分支名 */
8
+ branch: string;
9
+ /** worktree 路径 */
10
+ worktreePath: string;
11
+ /** 是否成功 */
12
+ success: boolean;
13
+ /** Claude Code 的输出结果 */
14
+ result: ClaudeCodeResult | null;
15
+ /** 错误信息(失败时) */
16
+ error?: string;
17
+ }
18
+
19
+ /** 所有任务的汇总信息 */
20
+ export interface TaskSummary {
21
+ /** 总任务数 */
22
+ total: number;
23
+ /** 成功数 */
24
+ succeeded: number;
25
+ /** 失败数 */
26
+ failed: number;
27
+ /** 总耗时(毫秒) */
28
+ totalDurationMs: number;
29
+ /** 总花费(美元) */
30
+ totalCostUsd: number;
31
+ }
@@ -0,0 +1,7 @@
1
+ /** worktree 信息 */
2
+ export interface WorktreeInfo {
3
+ /** worktree 路径 */
4
+ path: string;
5
+ /** 分支名 */
6
+ branch: string;
7
+ }
@@ -0,0 +1,51 @@
1
+ import { INVALID_BRANCH_CHARS, MESSAGES } from '../constants/index.js';
2
+ import { logger } from '../logger/index.js';
3
+ import { printWarning } from './formatter.js';
4
+ import { checkBranchExists } from './git.js';
5
+ import { ClawtError } from '../errors/index.js';
6
+
7
+ /**
8
+ * 将分支名中的非法字符替换为 '-',并压缩连续 '-'、去除首尾 '-'
9
+ * @param {string} branchName - 原始分支名
10
+ * @returns {string} 清理后的合法分支名
11
+ */
12
+ export function sanitizeBranchName(branchName: string): string {
13
+ const sanitized = branchName
14
+ .replace(INVALID_BRANCH_CHARS, '-')
15
+ .replace(/-{2,}/g, '-')
16
+ .replace(/^-|-$/g, '');
17
+
18
+ if (sanitized !== branchName) {
19
+ logger.warn(MESSAGES.BRANCH_SANITIZED(branchName, sanitized));
20
+ printWarning(MESSAGES.BRANCH_SANITIZED(branchName, sanitized));
21
+ }
22
+
23
+ return sanitized;
24
+ }
25
+
26
+ /**
27
+ * 根据基础分支名和数量生成分支名数组
28
+ * n=1 时返回 [branchName],n>1 时返回 [branchName-1, ..., branchName-n]
29
+ * @param {string} branchName - 基础分支名
30
+ * @param {number} count - 数量
31
+ * @returns {string[]} 分支名数组
32
+ */
33
+ export function generateBranchNames(branchName: string, count: number): string[] {
34
+ if (count === 1) {
35
+ return [branchName];
36
+ }
37
+ return Array.from({ length: count }, (_, i) => `${branchName}-${i + 1}`);
38
+ }
39
+
40
+ /**
41
+ * 校验所有分支名是否都不存在,有任何一个存在则抛出错误
42
+ * @param {string[]} branchNames - 分支名数组
43
+ * @throws {ClawtError} 分支已存在时抛出
44
+ */
45
+ export function validateBranchesNotExist(branchNames: string[]): void {
46
+ for (const name of branchNames) {
47
+ if (checkBranchExists(name)) {
48
+ throw new ClawtError(MESSAGES.BRANCH_EXISTS(name));
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,35 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { CONFIG_PATH, CLAWT_HOME, LOGS_DIR, WORKTREES_DIR, DEFAULT_CONFIG } from '../constants/index.js';
3
+ import { ensureDir } from './fs.js';
4
+ import type { ClawtConfig } from '../types/index.js';
5
+
6
+ /**
7
+ * 加载全局配置,不存在则返回默认配置
8
+ * @returns {ClawtConfig} 配置对象
9
+ */
10
+ export function loadConfig(): ClawtConfig {
11
+ if (!existsSync(CONFIG_PATH)) {
12
+ return { ...DEFAULT_CONFIG };
13
+ }
14
+ const raw = readFileSync(CONFIG_PATH, 'utf-8');
15
+ return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
16
+ }
17
+
18
+ /**
19
+ * 获取配置中指定字段的值
20
+ * @param {keyof ClawtConfig} key - 配置字段名
21
+ * @returns {ClawtConfig[keyof ClawtConfig]} 配置值
22
+ */
23
+ export function getConfigValue<K extends keyof ClawtConfig>(key: K): ClawtConfig[K] {
24
+ const config = loadConfig();
25
+ return config[key];
26
+ }
27
+
28
+ /**
29
+ * 确保 clawt 全局目录结构存在
30
+ */
31
+ export function ensureClawtDirs(): void {
32
+ ensureDir(CLAWT_HOME);
33
+ ensureDir(LOGS_DIR);
34
+ ensureDir(WORKTREES_DIR);
35
+ }
@@ -0,0 +1,67 @@
1
+ import chalk from 'chalk';
2
+ import { MESSAGES } from '../constants/index.js';
3
+ import { createInterface } from 'node:readline';
4
+
5
+ /**
6
+ * 输出成功信息
7
+ * @param {string} message - 消息内容
8
+ */
9
+ export function printSuccess(message: string): void {
10
+ console.log(chalk.green(message));
11
+ }
12
+
13
+ /**
14
+ * 输出错误信息
15
+ * @param {string} message - 消息内容
16
+ */
17
+ export function printError(message: string): void {
18
+ console.error(chalk.red(`✗ ${message}`));
19
+ }
20
+
21
+ /**
22
+ * 输出警告信息
23
+ * @param {string} message - 消息内容
24
+ */
25
+ export function printWarning(message: string): void {
26
+ console.log(chalk.yellow(`⚠ ${message}`));
27
+ }
28
+
29
+ /**
30
+ * 输出普通信息
31
+ * @param {string} message - 消息内容
32
+ */
33
+ export function printInfo(message: string): void {
34
+ console.log(message);
35
+ }
36
+
37
+ /**
38
+ * 输出分隔线
39
+ */
40
+ export function printSeparator(): void {
41
+ console.log(MESSAGES.SEPARATOR);
42
+ }
43
+
44
+ /**
45
+ * 输出粗分隔线
46
+ */
47
+ export function printDoubleSeparator(): void {
48
+ console.log(MESSAGES.DOUBLE_SEPARATOR);
49
+ }
50
+
51
+ /**
52
+ * 简易 yes/no 确认(非交互式场景使用)
53
+ * @param {string} question - 确认问题
54
+ * @returns {Promise<boolean>} 用户是否确认
55
+ */
56
+ export function confirmAction(question: string): Promise<boolean> {
57
+ return new Promise((resolve) => {
58
+ const rl = createInterface({
59
+ input: process.stdin,
60
+ output: process.stdout,
61
+ });
62
+ rl.question(`${question} (y/N) `, (answer) => {
63
+ rl.close();
64
+ resolve(answer.toLowerCase() === 'y');
65
+ });
66
+ });
67
+ }
@@ -0,0 +1,28 @@
1
+ import { existsSync, mkdirSync, readdirSync, rmdirSync } from 'node:fs';
2
+
3
+ /**
4
+ * 确保目录存在,不存在则递归创建
5
+ * @param {string} dirPath - 目录路径
6
+ */
7
+ export function ensureDir(dirPath: string): void {
8
+ if (!existsSync(dirPath)) {
9
+ mkdirSync(dirPath, { recursive: true });
10
+ }
11
+ }
12
+
13
+ /**
14
+ * 如果目录为空则删除
15
+ * @param {string} dirPath - 目录路径
16
+ * @returns {boolean} 是否删除了目录
17
+ */
18
+ export function removeEmptyDir(dirPath: string): boolean {
19
+ if (!existsSync(dirPath)) {
20
+ return false;
21
+ }
22
+ const entries = readdirSync(dirPath);
23
+ if (entries.length === 0) {
24
+ rmdirSync(dirPath);
25
+ return true;
26
+ }
27
+ return false;
28
+ }
@@ -0,0 +1,243 @@
1
+ import { basename } from 'node:path';
2
+ import { execCommand } from './shell.js';
3
+ import { logger } from '../logger/index.js';
4
+
5
+ /**
6
+ * 获取 git common dir(用于判断是否为主 worktree)
7
+ * @param {string} cwd - 工作目录
8
+ * @returns {string} git common dir 路径
9
+ */
10
+ export function getGitCommonDir(cwd?: string): string {
11
+ return execCommand('git rev-parse --git-common-dir', { cwd });
12
+ }
13
+
14
+ /**
15
+ * 获取 git 仓库根目录的绝对路径
16
+ * @param {string} cwd - 工作目录
17
+ * @returns {string} 仓库根目录路径
18
+ */
19
+ export function getGitTopLevel(cwd?: string): string {
20
+ return execCommand('git rev-parse --show-toplevel', { cwd });
21
+ }
22
+
23
+ /**
24
+ * 获取项目名(仓库根目录名称)
25
+ * @param {string} cwd - 工作目录
26
+ * @returns {string} 项目名
27
+ */
28
+ export function getProjectName(cwd?: string): string {
29
+ const topLevel = getGitTopLevel(cwd);
30
+ return basename(topLevel);
31
+ }
32
+
33
+ /**
34
+ * 检查本地分支是否存在
35
+ * @param {string} branchName - 分支名
36
+ * @param {string} cwd - 工作目录
37
+ * @returns {boolean} 分支是否存在
38
+ */
39
+ export function checkBranchExists(branchName: string, cwd?: string): boolean {
40
+ try {
41
+ execCommand(`git show-ref --verify refs/heads/${branchName}`, { cwd });
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * 创建 worktree 并同时创建新分支
50
+ * @param {string} branchName - 新分支名
51
+ * @param {string} worktreePath - worktree 目录路径
52
+ * @param {string} cwd - 工作目录
53
+ */
54
+ export function createWorktree(branchName: string, worktreePath: string, cwd?: string): void {
55
+ logger.info(`创建 worktree: ${worktreePath}`);
56
+ execCommand(`git worktree add -b ${branchName} "${worktreePath}"`, { cwd });
57
+ }
58
+
59
+ /**
60
+ * 强制移除 worktree
61
+ * @param {string} worktreePath - worktree 目录路径
62
+ * @param {string} cwd - 工作目录
63
+ */
64
+ export function removeWorktreeByPath(worktreePath: string, cwd?: string): void {
65
+ logger.info(`移除 worktree: ${worktreePath}`);
66
+ execCommand(`git worktree remove -f "${worktreePath}"`, { cwd });
67
+ }
68
+
69
+ /**
70
+ * 强制删除本地分支
71
+ * @param {string} branchName - 分支名
72
+ * @param {string} cwd - 工作目录
73
+ */
74
+ export function deleteBranch(branchName: string, cwd?: string): void {
75
+ logger.info(`删除分支: ${branchName}`);
76
+ execCommand(`git branch -D ${branchName}`, { cwd });
77
+ }
78
+
79
+ /**
80
+ * 获取工作区状态(git status --porcelain)
81
+ * @param {string} cwd - 工作目录
82
+ * @returns {string} porcelain 格式输出,为空表示干净
83
+ */
84
+ export function getStatusPorcelain(cwd?: string): string {
85
+ return execCommand('git status --porcelain', { cwd });
86
+ }
87
+
88
+ /**
89
+ * 判断工作区是否干净
90
+ * @param {string} cwd - 工作目录
91
+ * @returns {boolean} 是否干净
92
+ */
93
+ export function isWorkingDirClean(cwd?: string): boolean {
94
+ return getStatusPorcelain(cwd) === '';
95
+ }
96
+
97
+ /**
98
+ * git add 所有文件
99
+ * @param {string} cwd - 工作目录
100
+ */
101
+ export function gitAddAll(cwd?: string): void {
102
+ execCommand('git add .', { cwd });
103
+ }
104
+
105
+ /**
106
+ * git commit
107
+ * @param {string} message - 提交信息
108
+ * @param {string} cwd - 工作目录
109
+ */
110
+ export function gitCommit(message: string, cwd?: string): void {
111
+ execCommand(`git commit -m '${message.replace(/'/g, "'\\''")}'`, { cwd });
112
+ }
113
+
114
+ /**
115
+ * git merge
116
+ * @param {string} branchName - 要合并的分支名
117
+ * @param {string} cwd - 工作目录
118
+ */
119
+ export function gitMerge(branchName: string, cwd?: string): void {
120
+ execCommand(`git merge ${branchName}`, { cwd });
121
+ }
122
+
123
+ /**
124
+ * 检查是否有合并冲突
125
+ * @param {string} cwd - 工作目录
126
+ * @returns {boolean} 是否有冲突
127
+ */
128
+ export function hasMergeConflict(cwd?: string): boolean {
129
+ const status = getStatusPorcelain(cwd);
130
+ // UU = 双方修改冲突, AA = 双方新增冲突, DD = 双方删除
131
+ return status.split('\n').some((line) => /^(UU|AA|DD|DU|UD|AU|UA)/.test(line));
132
+ }
133
+
134
+ /**
135
+ * git pull
136
+ * @param {string} cwd - 工作目录
137
+ */
138
+ export function gitPull(cwd?: string): void {
139
+ execCommand('git pull', { cwd });
140
+ }
141
+
142
+ /**
143
+ * git push
144
+ * @param {string} cwd - 工作目录
145
+ */
146
+ export function gitPush(cwd?: string): void {
147
+ execCommand('git push', { cwd });
148
+ }
149
+
150
+ /**
151
+ * git reset --hard HEAD
152
+ * @param {string} cwd - 工作目录
153
+ */
154
+ export function gitResetHard(cwd?: string): void {
155
+ execCommand('git reset --hard HEAD', { cwd });
156
+ }
157
+
158
+ /**
159
+ * git clean -fd(删除未跟踪文件)
160
+ * @param {string} cwd - 工作目录
161
+ */
162
+ export function gitCleanForce(cwd?: string): void {
163
+ execCommand('git clean -fd', { cwd });
164
+ }
165
+
166
+ /**
167
+ * git stash push -m <message>
168
+ * @param {string} message - stash 消息
169
+ * @param {string} cwd - 工作目录
170
+ */
171
+ export function gitStashPush(message: string, cwd?: string): void {
172
+ execCommand(`git stash push -m "${message}"`, { cwd });
173
+ }
174
+
175
+ /**
176
+ * git stash apply
177
+ * @param {string} cwd - 工作目录
178
+ */
179
+ export function gitStashApply(cwd?: string): void {
180
+ execCommand('git stash apply', { cwd });
181
+ }
182
+
183
+ /**
184
+ * git stash pop stash@{index}
185
+ * @param {number} index - stash 索引
186
+ * @param {string} cwd - 工作目录
187
+ */
188
+ export function gitStashPop(index: number = 0, cwd?: string): void {
189
+ execCommand(`git stash pop stash@{${index}}`, { cwd });
190
+ }
191
+
192
+ /**
193
+ * git stash list
194
+ * @param {string} cwd - 工作目录
195
+ * @returns {string} stash 列表输出
196
+ */
197
+ export function gitStashList(cwd?: string): string {
198
+ try {
199
+ return execCommand('git stash list', { cwd });
200
+ } catch {
201
+ return '';
202
+ }
203
+ }
204
+
205
+ /**
206
+ * git restore --staged .
207
+ * @param {string} cwd - 工作目录
208
+ */
209
+ export function gitRestoreStaged(cwd?: string): void {
210
+ execCommand('git restore --staged .', { cwd });
211
+ }
212
+
213
+ /**
214
+ * 获取 git worktree list 的输出
215
+ * @param {string} cwd - 工作目录
216
+ * @returns {string} worktree 列表
217
+ */
218
+ export function gitWorktreeList(cwd?: string): string {
219
+ return execCommand('git worktree list', { cwd });
220
+ }
221
+
222
+ /**
223
+ * 执行 git worktree prune
224
+ * @param {string} cwd - 工作目录
225
+ */
226
+ export function gitWorktreePrune(cwd?: string): void {
227
+ execCommand('git worktree prune', { cwd });
228
+ }
229
+
230
+ /**
231
+ * 检查目标分支相对于当前分支是否有本地提交
232
+ * @param {string} branchName - 目标分支名
233
+ * @param {string} cwd - 工作目录
234
+ * @returns {boolean} 是否有本地提交
235
+ */
236
+ export function hasLocalCommits(branchName: string, cwd?: string): boolean {
237
+ try {
238
+ const output = execCommand(`git log HEAD..${branchName} --oneline`, { cwd });
239
+ return output.trim() !== '';
240
+ } catch {
241
+ return false;
242
+ }
243
+ }
@@ -0,0 +1,35 @@
1
+ export { execCommand, spawnProcess, killAllChildProcesses } from './shell.js';
2
+ export {
3
+ getGitCommonDir,
4
+ getGitTopLevel,
5
+ getProjectName,
6
+ checkBranchExists,
7
+ createWorktree,
8
+ removeWorktreeByPath,
9
+ deleteBranch,
10
+ getStatusPorcelain,
11
+ isWorkingDirClean,
12
+ gitAddAll,
13
+ gitCommit,
14
+ gitMerge,
15
+ hasMergeConflict,
16
+ gitPull,
17
+ gitPush,
18
+ gitResetHard,
19
+ gitCleanForce,
20
+ gitStashPush,
21
+ gitStashApply,
22
+ gitStashPop,
23
+ gitStashList,
24
+ gitRestoreStaged,
25
+ gitWorktreeList,
26
+ gitWorktreePrune,
27
+ hasLocalCommits,
28
+ } from './git.js';
29
+ export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
30
+ export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
31
+ export { createWorktrees, getProjectWorktrees, getProjectWorktreeDir, cleanupWorktrees } from './worktree.js';
32
+ export { loadConfig, getConfigValue, ensureClawtDirs } from './config.js';
33
+ export { printSuccess, printError, printWarning, printInfo, printSeparator, printDoubleSeparator, confirmAction } from './formatter.js';
34
+ export { ensureDir, removeEmptyDir } from './fs.js';
35
+ export { multilineInput } from './prompt.js';
@@ -0,0 +1,18 @@
1
+ import Enquirer from 'enquirer';
2
+
3
+ /**
4
+ * 多行交互式输入框
5
+ * 使用 enquirer 的 Input prompt(multiline 模式)实现
6
+ * @param {string} message - 提示信息
7
+ * @returns {Promise<string>} 用户输入的文本内容
8
+ */
9
+ export async function multilineInput(message: string): Promise<string> {
10
+ // @ts-expect-error enquirer 类型声明未导出 Input 类,但运行时存在
11
+ const prompt = new Enquirer.Input({
12
+ message,
13
+ multiline: true,
14
+ });
15
+
16
+ const result: string = await prompt.run();
17
+ return result;
18
+ }
@@ -0,0 +1,53 @@
1
+ import { execSync, spawn, type ChildProcess, type StdioOptions } from 'node:child_process';
2
+ import { logger } from '../logger/index.js';
3
+
4
+ /**
5
+ * 同步执行 shell 命令并返回 stdout
6
+ * @param {string} command - 要执行的命令
7
+ * @param {object} options - 可选配置
8
+ * @param {string} options.cwd - 工作目录
9
+ * @returns {string} 命令的标准输出(已 trim)
10
+ * @throws {Error} 命令执行失败时抛出
11
+ */
12
+ export function execCommand(command: string, options?: { cwd?: string }): string {
13
+ logger.debug(`执行命令: ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`);
14
+ const result = execSync(command, {
15
+ cwd: options?.cwd,
16
+ encoding: 'utf-8',
17
+ stdio: ['pipe', 'pipe', 'pipe'],
18
+ });
19
+ return result.trim();
20
+ }
21
+
22
+ /**
23
+ * 以子进程方式异步执行命令
24
+ * @param {string} command - 要执行的命令
25
+ * @param {string[]} args - 命令参数
26
+ * @param {object} options - 可选配置
27
+ * @param {string} options.cwd - 工作目录
28
+ * @param {StdioOptions} options.stdio - stdio 配置,默认 ['pipe', 'pipe', 'pipe']
29
+ * @returns {ChildProcess} 子进程实例
30
+ */
31
+ export function spawnProcess(
32
+ command: string,
33
+ args: string[],
34
+ options?: { cwd?: string; stdio?: StdioOptions },
35
+ ): ChildProcess {
36
+ logger.debug(`启动子进程: ${command} ${args.join(' ')}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`);
37
+ return spawn(command, args, {
38
+ cwd: options?.cwd,
39
+ stdio: options?.stdio ?? ['pipe', 'pipe', 'pipe'],
40
+ });
41
+ }
42
+
43
+ /**
44
+ * 终止所有正在运行的子进程
45
+ * @param {ChildProcess[]} children - 子进程列表
46
+ */
47
+ export function killAllChildProcesses(children: ChildProcess[]): void {
48
+ for (const child of children) {
49
+ if (!child.killed) {
50
+ child.kill('SIGTERM');
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,48 @@
1
+ import { MESSAGES } from '../constants/index.js';
2
+ import { ClawtError } from '../errors/index.js';
3
+ import { execCommand } from './shell.js';
4
+ import { getGitCommonDir } from './git.js';
5
+
6
+ /**
7
+ * 校验当前目录是否为主 worktree 的根目录
8
+ * 条件:git rev-parse --git-common-dir === ".git"
9
+ * @throws {ClawtError} 不在主 worktree 根目录时抛出
10
+ */
11
+ export function validateMainWorktree(): void {
12
+ try {
13
+ const gitCommonDir = getGitCommonDir();
14
+ if (gitCommonDir !== '.git') {
15
+ throw new ClawtError(MESSAGES.NOT_MAIN_WORKTREE);
16
+ }
17
+ } catch (error) {
18
+ if (error instanceof ClawtError) {
19
+ throw error;
20
+ }
21
+ // git 命令执行失败,可能不在 git 仓库中
22
+ throw new ClawtError(MESSAGES.NOT_MAIN_WORKTREE);
23
+ }
24
+ }
25
+
26
+ /**
27
+ * 校验 Git 是否已安装
28
+ * @throws {ClawtError} Git 未安装时抛出
29
+ */
30
+ export function validateGitInstalled(): void {
31
+ try {
32
+ execCommand('git --version');
33
+ } catch {
34
+ throw new ClawtError(MESSAGES.GIT_NOT_INSTALLED);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * 校验 Claude Code CLI 是否已安装
40
+ * @throws {ClawtError} Claude Code CLI 未安装时抛出
41
+ */
42
+ export function validateClaudeCodeInstalled(): void {
43
+ try {
44
+ execCommand('claude --version');
45
+ } catch {
46
+ throw new ClawtError(MESSAGES.CLAUDE_NOT_INSTALLED);
47
+ }
48
+ }