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
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "clawt",
3
+ "version": "1.0.0",
4
+ "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "clawt": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsup",
12
+ "dev": "tsup --watch",
13
+ "postinstall": "node dist/postinstall.js"
14
+ },
15
+ "keywords": [
16
+ "claude",
17
+ "worktree",
18
+ "git",
19
+ "cli"
20
+ ],
21
+ "author": "",
22
+ "license": "ISC",
23
+ "dependencies": {
24
+ "chalk": "^5.4.1",
25
+ "commander": "^13.1.0",
26
+ "enquirer": "^2.4.1",
27
+ "winston": "^3.17.0",
28
+ "winston-daily-rotate-file": "^5.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.13.1",
32
+ "tsup": "^8.3.6",
33
+ "typescript": "^5.7.3"
34
+ },
35
+ "engines": {
36
+ "node": ">=18"
37
+ }
38
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * postinstall 脚本:npm 全局安装后初始化 ~/.clawt/ 目录
3
+ */
4
+
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { homedir } from 'node:os';
8
+ import { DEFAULT_CONFIG } from '../src/constants/index.js';
9
+
10
+ /** clawt 主目录 */
11
+ const CLAWT_HOME = join(homedir(), '.clawt');
12
+ /** 配置文件路径 */
13
+ const CONFIG_PATH = join(CLAWT_HOME, 'config.json');
14
+ /** 日志目录 */
15
+ const LOGS_DIR = join(CLAWT_HOME, 'logs');
16
+ /** worktree 目录 */
17
+ const WORKTREES_DIR = join(CLAWT_HOME, 'worktrees');
18
+
19
+ /**
20
+ * 确保目录存在,不存在则递归创建
21
+ * @param {string} dirPath - 目录路径
22
+ */
23
+ function ensureDirectory(dirPath: string): void {
24
+ if (!existsSync(dirPath)) {
25
+ mkdirSync(dirPath, { recursive: true });
26
+ }
27
+ }
28
+
29
+ /**
30
+ * 读取已有的用户配置文件,解析失败时返回 null
31
+ * @param {string} configPath - 配置文件路径
32
+ * @returns {Record<string, unknown> | null} 解析后的配置对象,失败返回 null
33
+ */
34
+ function loadExistingConfig(configPath: string): Record<string, unknown> | null {
35
+ if (!existsSync(configPath)) {
36
+ return null;
37
+ }
38
+ try {
39
+ const raw = readFileSync(configPath, 'utf-8');
40
+ return JSON.parse(raw) as Record<string, unknown>;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * 将用户配置与默认配置合并:
48
+ * - 新版本新增的 key → 使用默认值补充
49
+ * - 用户已有的 key → 保留用户值不覆盖
50
+ * - 新版本已移除的 key → 从用户配置中删除
51
+ * @param {Record<string, unknown>} existing - 用户已有配置
52
+ * @param {Record<string, unknown>} defaults - 新版本默认配置
53
+ * @returns {Record<string, unknown>} 合并后的配置对象
54
+ */
55
+ function mergeConfig(
56
+ existing: Record<string, unknown>,
57
+ defaults: Record<string, unknown>,
58
+ ): Record<string, unknown> {
59
+ const merged: Record<string, unknown> = {};
60
+
61
+ // 以默认配置的 key 为基准,保留用户已有值,补充新增默认值
62
+ for (const key of Object.keys(defaults)) {
63
+ merged[key] = key in existing ? existing[key] : defaults[key];
64
+ }
65
+
66
+ // 默认配置中不存在的 key 不会被带入,即完成了旧配置的清理
67
+
68
+ return merged;
69
+ }
70
+
71
+ /**
72
+ * 写入配置文件并输出提示
73
+ * @param {string} configPath - 配置文件路径
74
+ * @param {Record<string, unknown>} config - 配置对象
75
+ * @param {string} message - 输出的提示信息
76
+ */
77
+ function writeConfig(configPath: string, config: Record<string, unknown>, message: string): void {
78
+ writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
79
+ console.log(message);
80
+ }
81
+
82
+ /**
83
+ * 同步配置文件:不存在则创建,已存在则合并
84
+ * @param {string} configPath - 配置文件路径
85
+ * @param {Record<string, unknown>} defaultConfig - 默认配置
86
+ */
87
+ function syncConfig(configPath: string, defaultConfig: Record<string, unknown>): void {
88
+ const existing = loadExistingConfig(configPath);
89
+
90
+ if (!existing) {
91
+ writeConfig(configPath, defaultConfig, `✓ 已创建默认配置文件: ${configPath}`);
92
+ return;
93
+ }
94
+
95
+ const merged = mergeConfig(existing, defaultConfig);
96
+
97
+ // 仅在配置发生变化时才写入
98
+ if (JSON.stringify(existing) !== JSON.stringify(merged)) {
99
+ writeConfig(configPath, merged, `✓ 已更新配置文件: ${configPath}`);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * 初始化 ~/.clawt/ 目录结构和默认配置
105
+ */
106
+ function init(): void {
107
+ ensureDirectory(CLAWT_HOME);
108
+ ensureDirectory(LOGS_DIR);
109
+ ensureDirectory(WORKTREES_DIR);
110
+
111
+ syncConfig(CONFIG_PATH, DEFAULT_CONFIG as unknown as Record<string, unknown>);
112
+
113
+ console.log('✓ clawt 初始化完成');
114
+ }
115
+
116
+ init();
@@ -0,0 +1,49 @@
1
+ import type { Command } from 'commander';
2
+ import { MESSAGES } from '../constants/index.js';
3
+ import { logger } from '../logger/index.js';
4
+ import type { CreateOptions } from '../types/index.js';
5
+ import {
6
+ validateMainWorktree,
7
+ createWorktrees,
8
+ printSuccess,
9
+ printInfo,
10
+ printSeparator,
11
+ } from '../utils/index.js';
12
+
13
+ /**
14
+ * 注册 create 命令:批量创建 worktree 及对应分支
15
+ * @param {Command} program - Commander 实例
16
+ */
17
+ export function registerCreateCommand(program: Command): void {
18
+ program
19
+ .command('create')
20
+ .description('批量创建 worktree 及对应分支')
21
+ .requiredOption('-b, --branch <branchName>', '分支名')
22
+ .option('-n, --number <count>', '创建数量', '1')
23
+ .action((options: CreateOptions) => {
24
+ handleCreate(options);
25
+ });
26
+ }
27
+
28
+ /**
29
+ * 执行 create 命令的核心逻辑
30
+ * @param {CreateOptions} options - 命令选项
31
+ */
32
+ function handleCreate(options: CreateOptions): void {
33
+ validateMainWorktree();
34
+
35
+ const count = Number(options.number);
36
+ logger.info(`create 命令执行,分支: ${options.branch},数量: ${count}`);
37
+
38
+ const worktrees = createWorktrees(options.branch, count);
39
+
40
+ printSuccess(MESSAGES.WORKTREE_CREATED(worktrees.length));
41
+ printInfo('');
42
+
43
+ worktrees.forEach((wt, index) => {
44
+ printInfo(`目录路径${index + 1}:`);
45
+ printInfo(` ${wt.path}`);
46
+ printInfo(` 分支名: ${wt.branch}`);
47
+ printSeparator();
48
+ });
49
+ }
@@ -0,0 +1,45 @@
1
+ import type { Command } from 'commander';
2
+ import { MESSAGES } from '../constants/index.js';
3
+ import { logger } from '../logger/index.js';
4
+ import {
5
+ validateMainWorktree,
6
+ getProjectName,
7
+ getProjectWorktrees,
8
+ printInfo,
9
+ } from '../utils/index.js';
10
+
11
+ /**
12
+ * 注册 list 命令:列出当前项目所有 worktree
13
+ * @param {Command} program - Commander 实例
14
+ */
15
+ export function registerListCommand(program: Command): void {
16
+ program
17
+ .command('list')
18
+ .description('列出当前项目所有 worktree')
19
+ .action(() => {
20
+ handleList();
21
+ });
22
+ }
23
+
24
+ /**
25
+ * 执行 list 命令的核心逻辑
26
+ */
27
+ function handleList(): void {
28
+ validateMainWorktree();
29
+
30
+ const projectName = getProjectName();
31
+ const worktrees = getProjectWorktrees();
32
+
33
+ logger.info(`list 命令执行,项目: ${projectName},共 ${worktrees.length} 个 worktree`);
34
+
35
+ printInfo(`当前项目: ${projectName}\n`);
36
+
37
+ if (worktrees.length === 0) {
38
+ printInfo(` ${MESSAGES.NO_WORKTREES}`);
39
+ } else {
40
+ for (const wt of worktrees) {
41
+ printInfo(` ${wt.path} [${wt.branch}]`);
42
+ }
43
+ printInfo(`\n共 ${worktrees.length} 个 worktree`);
44
+ }
45
+ }
@@ -0,0 +1,142 @@
1
+ import type { Command } from 'commander';
2
+ import { join } from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+ import { logger } from '../logger/index.js';
5
+ import { ClawtError } from '../errors/index.js';
6
+ import { MESSAGES } from '../constants/index.js';
7
+ import type { MergeOptions } from '../types/index.js';
8
+ import {
9
+ validateMainWorktree,
10
+ getGitTopLevel,
11
+ getProjectWorktreeDir,
12
+ isWorkingDirClean,
13
+ gitAddAll,
14
+ gitCommit,
15
+ gitMerge,
16
+ hasMergeConflict,
17
+ gitPull,
18
+ gitPush,
19
+ hasLocalCommits,
20
+ printSuccess,
21
+ printInfo,
22
+ getConfigValue,
23
+ confirmAction,
24
+ cleanupWorktrees,
25
+ } from '../utils/index.js';
26
+
27
+ /**
28
+ * 注册 merge 命令:合并验证过的分支到主 worktree
29
+ * @param {Command} program - Commander 实例
30
+ */
31
+ export function registerMergeCommand(program: Command): void {
32
+ program
33
+ .command('merge')
34
+ .description('合并某个已验证的 worktree 分支到主 worktree')
35
+ .requiredOption('-b, --branch <branchName>', '要合并的分支名')
36
+ .option('-m, --message <message>', '提交信息(工作区有修改时必填)')
37
+ .action(async (options: MergeOptions) => {
38
+ await handleMerge(options);
39
+ });
40
+ }
41
+
42
+ /**
43
+ * 判断 merge 成功后是否需要清理 worktree 和分支
44
+ * 如果全局配置了 autoDeleteBranch 则直接返回 true,否则询问用户
45
+ * @param {string} branchName - 分支名
46
+ * @returns {Promise<boolean>} 是否清理
47
+ */
48
+ async function shouldCleanupAfterMerge(branchName: string): Promise<boolean> {
49
+ const autoDelete = getConfigValue('autoDeleteBranch');
50
+ if (autoDelete) {
51
+ printInfo(`已配置自动删除,merge 成功后将自动清理 worktree 和分支: ${branchName}`);
52
+ return true;
53
+ }
54
+ return confirmAction(`merge 成功后是否删除对应的 worktree 和分支 (${branchName})?`);
55
+ }
56
+
57
+ /**
58
+ * 清理已合并的 worktree 和对应分支
59
+ * @param {string} worktreePath - worktree 目录路径
60
+ * @param {string} branchName - 分支名
61
+ */
62
+ function cleanupWorktreeAndBranch(worktreePath: string, branchName: string): void {
63
+ cleanupWorktrees([{ path: worktreePath, branch: branchName }]);
64
+ printSuccess(MESSAGES.WORKTREE_CLEANED(branchName));
65
+ }
66
+
67
+ /**
68
+ * 执行 merge 命令的核心逻辑
69
+ * @param {MergeOptions} options - 命令选项
70
+ */
71
+ async function handleMerge(options: MergeOptions): Promise<void> {
72
+ validateMainWorktree();
73
+
74
+ const mainWorktreePath = getGitTopLevel();
75
+ const projectDir = getProjectWorktreeDir();
76
+ const targetWorktreePath = join(projectDir, options.branch);
77
+
78
+ logger.info(`merge 命令执行,分支: ${options.branch},提交信息: ${options.message ?? '(未提供)'}`);
79
+
80
+ // 检查目标 worktree 是否存在
81
+ if (!existsSync(targetWorktreePath)) {
82
+ throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
83
+ }
84
+
85
+ // 步骤 3:主 worktree 状态检测
86
+ if (!isWorkingDirClean(mainWorktreePath)) {
87
+ throw new ClawtError(MESSAGES.MAIN_WORKTREE_DIRTY);
88
+ }
89
+
90
+ // merge 前确认是否清理 worktree 和分支
91
+ const shouldCleanup = await shouldCleanupAfterMerge(options.branch);
92
+
93
+ // 步骤 4:根据目标 worktree 状态决定是否需要提交
94
+ const targetClean = isWorkingDirClean(targetWorktreePath);
95
+
96
+ if (!targetClean) {
97
+ // 目标 worktree 有未提交修改,必须提供 -m
98
+ if (!options.message) {
99
+ throw new ClawtError(MESSAGES.TARGET_WORKTREE_DIRTY_NO_MESSAGE);
100
+ }
101
+ gitAddAll(targetWorktreePath);
102
+ gitCommit(options.message, targetWorktreePath);
103
+ } else {
104
+ // 目标 worktree 干净,检查是否有本地提交
105
+ if (!hasLocalCommits(options.branch, mainWorktreePath)) {
106
+ throw new ClawtError(MESSAGES.TARGET_WORKTREE_NO_CHANGES);
107
+ }
108
+ // 有本地提交,跳过提交步骤,直接合并
109
+ }
110
+
111
+ // 步骤 5:回到主 worktree 进行合并
112
+ try {
113
+ gitMerge(options.branch, mainWorktreePath);
114
+ } catch (error) {
115
+ // 检查是否有冲突
116
+ if (hasMergeConflict(mainWorktreePath)) {
117
+ throw new ClawtError(MESSAGES.MERGE_CONFLICT);
118
+ }
119
+ throw error;
120
+ }
121
+
122
+ // 步骤 6:冲突检测(二次确认)
123
+ if (hasMergeConflict(mainWorktreePath)) {
124
+ throw new ClawtError(MESSAGES.MERGE_CONFLICT);
125
+ }
126
+
127
+ // 步骤 7:推送
128
+ gitPull(mainWorktreePath);
129
+ gitPush(mainWorktreePath);
130
+
131
+ // 步骤 8:输出成功提示(根据是否有 message 选择对应模板)
132
+ if (options.message) {
133
+ printSuccess(MESSAGES.MERGE_SUCCESS(options.branch, options.message));
134
+ } else {
135
+ printSuccess(MESSAGES.MERGE_SUCCESS_NO_MESSAGE(options.branch));
136
+ }
137
+
138
+ // 步骤 9:merge 成功后清理 worktree 和分支
139
+ if (shouldCleanup) {
140
+ cleanupWorktreeAndBranch(targetWorktreePath, options.branch);
141
+ }
142
+ }
@@ -0,0 +1,127 @@
1
+ import type { Command } from 'commander';
2
+ import { join } from 'node:path';
3
+ import { existsSync, readdirSync } from 'node:fs';
4
+ import { logger } from '../logger/index.js';
5
+ import { ClawtError } from '../errors/index.js';
6
+ import { MESSAGES } from '../constants/index.js';
7
+ import type { RemoveOptions } from '../types/index.js';
8
+ import {
9
+ validateMainWorktree,
10
+ getProjectName,
11
+ getProjectWorktreeDir,
12
+ getProjectWorktrees,
13
+ removeWorktreeByPath,
14
+ deleteBranch,
15
+ getConfigValue,
16
+ gitWorktreePrune,
17
+ removeEmptyDir,
18
+ printInfo,
19
+ printSuccess,
20
+ confirmAction,
21
+ } from '../utils/index.js';
22
+
23
+ /**
24
+ * 注册 remove 命令:移除 worktree
25
+ * @param {Command} program - Commander 实例
26
+ */
27
+ export function registerRemoveCommand(program: Command): void {
28
+ program
29
+ .command('remove')
30
+ .description('移除 worktree(支持单个/批量/全部)')
31
+ .option('--all', '移除当前项目下所有 worktree')
32
+ .option('-b, --branch <branchName>', '指定分支名')
33
+ .option('-i, --index <index>', '指定索引(配合 -b 使用)')
34
+ .action(async (options: RemoveOptions) => {
35
+ await handleRemove(options);
36
+ });
37
+ }
38
+
39
+ /**
40
+ * 根据参数确定要移除的 worktree 列表
41
+ * @param {RemoveOptions} options - 命令选项
42
+ * @returns {Array<{path: string, branch: string}>} 待移除的 worktree 列表
43
+ */
44
+ function resolveWorktreesToRemove(options: RemoveOptions): Array<{ path: string; branch: string }> {
45
+ const projectDir = getProjectWorktreeDir();
46
+ const allWorktrees = getProjectWorktrees();
47
+
48
+ if (options.all) {
49
+ return allWorktrees;
50
+ }
51
+
52
+ if (!options.branch) {
53
+ throw new ClawtError('请指定 --all 或 -b <branchName> 参数');
54
+ }
55
+
56
+ if (options.index !== undefined) {
57
+ // 单个移除:branchName-<index>
58
+ const targetName = `${options.branch}-${options.index}`;
59
+ const targetPath = join(projectDir, targetName);
60
+ const found = allWorktrees.find((wt) => wt.path === targetPath);
61
+ if (!found) {
62
+ throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(targetName));
63
+ }
64
+ return [found];
65
+ }
66
+
67
+ // 分支级移除:匹配 branchName 或 branchName-*
68
+ const matched = allWorktrees.filter(
69
+ (wt) => wt.branch === options.branch || wt.branch.startsWith(`${options.branch}-`),
70
+ );
71
+ if (matched.length === 0) {
72
+ throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch!));
73
+ }
74
+ return matched;
75
+ }
76
+
77
+ /**
78
+ * 执行 remove 命令的核心逻辑
79
+ * @param {RemoveOptions} options - 命令选项
80
+ */
81
+ async function handleRemove(options: RemoveOptions): Promise<void> {
82
+ validateMainWorktree();
83
+
84
+ const projectName = getProjectName();
85
+ logger.info(`remove 命令执行,项目: ${projectName}`);
86
+
87
+ const worktreesToRemove = resolveWorktreesToRemove(options);
88
+
89
+ if (worktreesToRemove.length === 0) {
90
+ printInfo(MESSAGES.NO_WORKTREES);
91
+ return;
92
+ }
93
+
94
+ // 列出即将移除的 worktree
95
+ printInfo('即将移除以下 worktree 及本地分支:\n');
96
+ worktreesToRemove.forEach((wt, index) => {
97
+ printInfo(` ${index + 1}. ${wt.path} → 分支: ${wt.branch}`);
98
+ });
99
+ printInfo('');
100
+
101
+ // 判断是否需要删除分支
102
+ const autoDelete = getConfigValue('autoDeleteBranch');
103
+ let shouldDeleteBranch = autoDelete;
104
+
105
+ if (!autoDelete) {
106
+ shouldDeleteBranch = await confirmAction('是否同时删除对应的本地分支?');
107
+ }
108
+
109
+ // 执行移除
110
+ for (const wt of worktreesToRemove) {
111
+ try {
112
+ removeWorktreeByPath(wt.path);
113
+ if (shouldDeleteBranch) {
114
+ deleteBranch(wt.branch);
115
+ }
116
+ printSuccess(MESSAGES.WORKTREE_REMOVED(wt.path));
117
+ } catch (error) {
118
+ logger.error(`移除 worktree 失败: ${wt.path} - ${error}`);
119
+ throw error;
120
+ }
121
+ }
122
+
123
+ // 清理 worktree 并清除空目录
124
+ gitWorktreePrune();
125
+ const projectDir = getProjectWorktreeDir();
126
+ removeEmptyDir(projectDir);
127
+ }