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
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
|
+
}
|