clawt 2.20.0 → 3.1.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/agents/docs-sync-updater.md +29 -11
- package/README.md +19 -30
- package/dist/index.js +1127 -222
- package/dist/postinstall.js +73 -8
- package/docs/alias.md +108 -0
- package/docs/completion.md +55 -0
- package/docs/config-file.md +43 -0
- package/docs/config.md +91 -0
- package/docs/create.md +85 -0
- package/docs/init.md +65 -0
- package/docs/list.md +67 -0
- package/docs/log.md +67 -0
- package/docs/merge.md +137 -0
- package/docs/notification.md +94 -0
- package/docs/projects.md +135 -0
- package/docs/remove.md +79 -0
- package/docs/reset.md +35 -0
- package/docs/resume.md +99 -0
- package/docs/run.md +146 -0
- package/docs/spec.md +157 -1906
- package/docs/status.md +298 -0
- package/docs/sync.md +114 -0
- package/docs/update-check.md +95 -0
- package/docs/validate.md +368 -0
- package/package.json +1 -1
- package/src/commands/alias.ts +1 -1
- package/src/commands/create.ts +10 -5
- package/src/commands/init.ts +75 -0
- package/src/commands/list.ts +1 -1
- package/src/commands/merge.ts +11 -4
- package/src/commands/remove.ts +10 -3
- package/src/commands/reset.ts +3 -0
- package/src/commands/resume.ts +1 -1
- package/src/commands/run.ts +9 -3
- package/src/commands/status.ts +14 -5
- package/src/commands/sync.ts +18 -6
- package/src/commands/validate.ts +46 -52
- package/src/constants/branch.ts +3 -0
- package/src/constants/config.ts +1 -1
- package/src/constants/index.ts +14 -2
- package/src/constants/interactive-panel.ts +44 -0
- package/src/constants/messages/completion.ts +1 -1
- package/src/constants/messages/create.ts +3 -0
- package/src/constants/messages/index.ts +4 -0
- package/src/constants/messages/init.ts +18 -0
- package/src/constants/messages/interactive-panel.ts +61 -0
- package/src/constants/messages/remove.ts +2 -0
- package/src/constants/messages/sync.ts +3 -0
- package/src/constants/messages/validate.ts +6 -0
- package/src/constants/paths.ts +3 -0
- package/src/index.ts +2 -0
- package/src/types/command.ts +9 -1
- package/src/types/index.ts +2 -1
- package/src/types/projectConfig.ts +5 -0
- package/src/utils/config.ts +2 -1
- package/src/utils/git.ts +18 -0
- package/src/utils/index.ts +9 -1
- package/src/utils/interactive-panel-render.ts +315 -0
- package/src/utils/interactive-panel.ts +590 -0
- package/src/utils/json.ts +67 -0
- package/src/utils/project-config.ts +77 -0
- package/src/utils/validate-branch.ts +166 -0
- package/src/utils/worktree-matcher.ts +2 -2
- package/src/utils/worktree.ts +6 -2
- package/tests/unit/commands/create.test.ts +20 -16
- package/tests/unit/commands/init.test.ts +146 -0
- package/tests/unit/commands/merge.test.ts +7 -1
- package/tests/unit/commands/remove.test.ts +4 -0
- package/tests/unit/commands/reset.test.ts +2 -0
- package/tests/unit/commands/run.test.ts +2 -0
- package/tests/unit/commands/sync.test.ts +6 -0
- package/tests/unit/commands/validate.test.ts +13 -0
- package/tests/unit/utils/config.test.ts +2 -2
- package/tests/unit/utils/project-config.test.ts +136 -0
- package/tests/unit/utils/update-checker.test.ts +28 -7
- package/tests/unit/utils/validate-branch.test.ts +272 -0
- package/tests/unit/utils/worktree.test.ts +6 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { PROJECTS_CONFIG_DIR, MESSAGES } from '../constants/index.js';
|
|
4
|
+
import { ClawtError } from '../errors/index.js';
|
|
5
|
+
import { logger } from '../logger/index.js';
|
|
6
|
+
import { getProjectName } from './git.js';
|
|
7
|
+
import { ensureDir } from './fs.js';
|
|
8
|
+
import type { ProjectConfig } from '../types/index.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 获取项目配置文件路径
|
|
12
|
+
* @param {string} projectName - 项目名
|
|
13
|
+
* @returns {string} 配置文件路径
|
|
14
|
+
*/
|
|
15
|
+
export function getProjectConfigPath(projectName: string): string {
|
|
16
|
+
return join(PROJECTS_CONFIG_DIR, projectName, 'config.json');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 加载当前项目的配置
|
|
21
|
+
* @returns {ProjectConfig | null} 项目配置,不存在时返回 null
|
|
22
|
+
*/
|
|
23
|
+
export function loadProjectConfig(): ProjectConfig | null {
|
|
24
|
+
const projectName = getProjectName();
|
|
25
|
+
const configPath = getProjectConfigPath(projectName);
|
|
26
|
+
|
|
27
|
+
if (!existsSync(configPath)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
33
|
+
return JSON.parse(raw) as ProjectConfig;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
logger.warn(`项目配置文件解析失败: ${error}`);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 保存项目配置
|
|
42
|
+
* @param {ProjectConfig} config - 要保存的项目配置
|
|
43
|
+
*/
|
|
44
|
+
export function saveProjectConfig(config: ProjectConfig): void {
|
|
45
|
+
const projectName = getProjectName();
|
|
46
|
+
const configPath = getProjectConfigPath(projectName);
|
|
47
|
+
// 确保项目子目录存在
|
|
48
|
+
const projectDir = join(PROJECTS_CONFIG_DIR, projectName);
|
|
49
|
+
ensureDir(projectDir);
|
|
50
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
51
|
+
logger.info(`项目配置已保存: ${configPath}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 获取当前项目配置,不存在则抛出错误
|
|
56
|
+
* @returns {ProjectConfig} 项目配置
|
|
57
|
+
*/
|
|
58
|
+
export function requireProjectConfig(): ProjectConfig {
|
|
59
|
+
const config = loadProjectConfig();
|
|
60
|
+
if (!config) {
|
|
61
|
+
throw new ClawtError(MESSAGES.PROJECT_NOT_INITIALIZED);
|
|
62
|
+
}
|
|
63
|
+
// 校验配置文件中是否包含 clawtMainWorkBranch 字段
|
|
64
|
+
if (!config.clawtMainWorkBranch) {
|
|
65
|
+
throw new ClawtError(MESSAGES.PROJECT_CONFIG_MISSING_BRANCH);
|
|
66
|
+
}
|
|
67
|
+
return config;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 从项目配置中获取主工作分支名
|
|
72
|
+
* @returns {string} 主工作分支名
|
|
73
|
+
*/
|
|
74
|
+
export function getMainWorkBranch(): string {
|
|
75
|
+
const config = requireProjectConfig();
|
|
76
|
+
return config.clawtMainWorkBranch;
|
|
77
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import Enquirer from 'enquirer';
|
|
2
|
+
import { VALIDATE_BRANCH_PREFIX } from '../constants/index.js';
|
|
3
|
+
import { logger } from '../logger/index.js';
|
|
4
|
+
import { checkBranchExists, createBranch, deleteBranch, getCurrentBranch, gitCheckout, gitResetHard, gitCleanForce, isWorkingDirClean, gitAddAll, gitStashPush } from './git.js';
|
|
5
|
+
import { getMainWorkBranch } from './project-config.js';
|
|
6
|
+
import { printWarning } from './formatter.js';
|
|
7
|
+
import { ClawtError } from '../errors/index.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 生成验证分支名
|
|
11
|
+
* @param {string} branchName - 原始分支名
|
|
12
|
+
* @returns {string} 验证分支名
|
|
13
|
+
*/
|
|
14
|
+
export function getValidateBranchName(branchName: string): string {
|
|
15
|
+
return `${VALIDATE_BRANCH_PREFIX}${branchName}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 创建验证分支(如果不存在)
|
|
20
|
+
* @param {string} branchName - 原始分支名
|
|
21
|
+
* @param {string} [cwd] - 工作目录
|
|
22
|
+
*/
|
|
23
|
+
export function createValidateBranch(branchName: string, cwd?: string): void {
|
|
24
|
+
const validateBranchName = getValidateBranchName(branchName);
|
|
25
|
+
|
|
26
|
+
if (checkBranchExists(validateBranchName, cwd)) {
|
|
27
|
+
logger.info(`验证分支已存在,跳过创建: ${validateBranchName}`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
createBranch(validateBranchName, cwd);
|
|
32
|
+
logger.info(`验证分支已创建: ${validateBranchName}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 删除验证分支(如果存在)
|
|
37
|
+
* @param {string} branchName - 原始分支名
|
|
38
|
+
* @param {string} [cwd] - 工作目录
|
|
39
|
+
*/
|
|
40
|
+
export function deleteValidateBranch(branchName: string, cwd?: string): void {
|
|
41
|
+
const validateBranchName = getValidateBranchName(branchName);
|
|
42
|
+
|
|
43
|
+
if (!checkBranchExists(validateBranchName, cwd)) {
|
|
44
|
+
logger.info(`验证分支不存在,跳过删除: ${validateBranchName}`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
deleteBranch(validateBranchName, cwd);
|
|
49
|
+
logger.info(`验证分支已删除: ${validateBranchName}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 重建验证分支(先删除再创建)
|
|
54
|
+
* 确保在主工作分支上创建验证分支,处理三种情况:
|
|
55
|
+
* 1. 已在主工作分支上 → 直接重建
|
|
56
|
+
* 2. 在验证分支上 → 清理工作区后自动切回主工作分支
|
|
57
|
+
* 3. 在其他分支上 → 检查工作区是否干净,不干净则交互处理后切回主工作分支
|
|
58
|
+
* @param {string} branchName - 原始分支名
|
|
59
|
+
* @param {string} [cwd] - 工作目录
|
|
60
|
+
*/
|
|
61
|
+
export async function rebuildValidateBranch(branchName: string, cwd?: string): Promise<void> {
|
|
62
|
+
const mainBranch = getMainWorkBranch();
|
|
63
|
+
const currentBranch = getCurrentBranch(cwd);
|
|
64
|
+
|
|
65
|
+
if (currentBranch === mainBranch) {
|
|
66
|
+
// 已在主工作分支上,无需切换
|
|
67
|
+
} else if (currentBranch.startsWith(VALIDATE_BRANCH_PREFIX)) {
|
|
68
|
+
// 在验证分支上,验证分支的修改是可丢弃的,直接清理后切回
|
|
69
|
+
gitResetHard(cwd);
|
|
70
|
+
gitCleanForce(cwd);
|
|
71
|
+
gitCheckout(mainBranch, cwd);
|
|
72
|
+
} else {
|
|
73
|
+
// 在其他分支上,需确保工作区干净后切回主工作分支
|
|
74
|
+
if (!isWorkingDirClean(cwd)) {
|
|
75
|
+
await handleDirtyWorkingDir(cwd);
|
|
76
|
+
}
|
|
77
|
+
gitCheckout(mainBranch, cwd);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
deleteValidateBranch(branchName, cwd);
|
|
81
|
+
createValidateBranch(branchName, cwd);
|
|
82
|
+
logger.info(`验证分支已重建: ${getValidateBranchName(branchName)}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 处理工作区有未提交更改的情况
|
|
87
|
+
* 提供交互式选择:reset(丢弃所有更改)、stash(暂存更改)、exit(退出让用户手动处理)
|
|
88
|
+
* @param {string} [cwd] - 工作目录
|
|
89
|
+
*/
|
|
90
|
+
export async function handleDirtyWorkingDir(cwd?: string): Promise<void> {
|
|
91
|
+
printWarning('当前分支有未提交的更改,请选择处理方式:\n');
|
|
92
|
+
|
|
93
|
+
// @ts-expect-error enquirer 类型声明未导出 Select 类,但运行时存在
|
|
94
|
+
const choice = await new Enquirer.Select({
|
|
95
|
+
message: '选择处理方式',
|
|
96
|
+
choices: [
|
|
97
|
+
{
|
|
98
|
+
name: 'reset',
|
|
99
|
+
message: 'reset - 丢弃所有更改 (git reset --hard HEAD && git clean -fd)',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'stash',
|
|
103
|
+
message: 'stash - 暂存更改 (git add . && git stash)',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'exit',
|
|
107
|
+
message: 'exit - 退出,手动处理',
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
initial: 0,
|
|
111
|
+
}).run();
|
|
112
|
+
|
|
113
|
+
if (choice === 'exit') {
|
|
114
|
+
throw new ClawtError('用户选择退出,请手动处理工作区更改后重试');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (choice === 'reset') {
|
|
118
|
+
gitResetHard(cwd);
|
|
119
|
+
gitCleanForce(cwd);
|
|
120
|
+
} else if (choice === 'stash') {
|
|
121
|
+
gitAddAll(cwd);
|
|
122
|
+
gitStashPush('clawt:auto-stash', cwd);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 再次检查是否干净
|
|
126
|
+
if (!isWorkingDirClean(cwd)) {
|
|
127
|
+
throw new ClawtError('工作区仍然不干净,请手动处理');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 确保当前在主工作分支上
|
|
133
|
+
* 三种情况:
|
|
134
|
+
* 1. 已在主工作分支上 → 直接返回
|
|
135
|
+
* 2. 在验证分支上 → 清理工作区后自动切回主工作分支
|
|
136
|
+
* 3. 在其他分支上 → 处理脏工作区后切换到主工作分支
|
|
137
|
+
* @param {string} [cwd] - 工作目录
|
|
138
|
+
*/
|
|
139
|
+
export async function ensureOnMainWorkBranch(cwd?: string): Promise<void> {
|
|
140
|
+
const mainBranch = getMainWorkBranch();
|
|
141
|
+
const currentBranch = getCurrentBranch(cwd);
|
|
142
|
+
|
|
143
|
+
// 已在主工作分支上,无需操作
|
|
144
|
+
if (currentBranch === mainBranch) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 当前在验证分支上,清理工作区后自动切回
|
|
149
|
+
if (currentBranch.startsWith(VALIDATE_BRANCH_PREFIX)) {
|
|
150
|
+
logger.info(`当前在验证分支 ${currentBranch} 上,自动切回主工作分支 ${mainBranch}`);
|
|
151
|
+
// 验证分支上的修改是可丢弃的,直接清理
|
|
152
|
+
if (!isWorkingDirClean(cwd)) {
|
|
153
|
+
gitResetHard(cwd);
|
|
154
|
+
gitCleanForce(cwd);
|
|
155
|
+
}
|
|
156
|
+
gitCheckout(mainBranch, cwd);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 当前在其他分支上,处理脏工作区后切换
|
|
161
|
+
logger.info(`当前在分支 ${currentBranch} 上,需切换到主工作分支 ${mainBranch}`);
|
|
162
|
+
if (!isWorkingDirClean(cwd)) {
|
|
163
|
+
await handleDirtyWorkingDir(cwd);
|
|
164
|
+
}
|
|
165
|
+
gitCheckout(mainBranch, cwd);
|
|
166
|
+
}
|
|
@@ -301,7 +301,7 @@ function formatLocalDate(date: Date): string {
|
|
|
301
301
|
* @param {string} dirPath - worktree 目录路径
|
|
302
302
|
* @returns {string | null} YYYY-MM-DD 格式的本地日期字符串,无法获取时返回 null
|
|
303
303
|
*/
|
|
304
|
-
function getWorktreeCreatedDate(dirPath: string): string | null {
|
|
304
|
+
export function getWorktreeCreatedDate(dirPath: string): string | null {
|
|
305
305
|
try {
|
|
306
306
|
const stat = statSync(dirPath);
|
|
307
307
|
return formatLocalDate(stat.birthtime);
|
|
@@ -316,7 +316,7 @@ function getWorktreeCreatedDate(dirPath: string): string | null {
|
|
|
316
316
|
* @param {string} dateStr - YYYY-MM-DD 格式的日期字符串
|
|
317
317
|
* @returns {string} 中文相对日期描述,如"今天"、"昨天"、"3 天前"
|
|
318
318
|
*/
|
|
319
|
-
function formatRelativeDate(dateStr: string): string {
|
|
319
|
+
export function formatRelativeDate(dateStr: string): string {
|
|
320
320
|
const today = formatLocalDate(new Date());
|
|
321
321
|
const todayMs = new Date(today).getTime();
|
|
322
322
|
const targetMs = new Date(dateStr).getTime();
|
package/src/utils/worktree.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { logger } from '../logger/index.js';
|
|
|
5
5
|
import { createWorktree as gitCreateWorktree, getProjectName, gitWorktreeList, removeWorktreeByPath, deleteBranch, gitWorktreePrune, getCommitCountAhead, getDiffStat, isWorkingDirClean } from './git.js';
|
|
6
6
|
import { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
|
|
7
7
|
import { ensureDir, removeEmptyDir } from './fs.js';
|
|
8
|
+
import { createValidateBranch, deleteValidateBranch } from './validate-branch.js';
|
|
8
9
|
import type { WorktreeInfo, WorktreeStatus } from '../types/index.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -37,11 +38,12 @@ export function createWorktrees(branchName: string, count: number): WorktreeInfo
|
|
|
37
38
|
const projectDir = getProjectWorktreeDir();
|
|
38
39
|
ensureDir(projectDir);
|
|
39
40
|
|
|
40
|
-
// 5. 串行创建 worktree
|
|
41
|
+
// 5. 串行创建 worktree 及对应验证分支
|
|
41
42
|
const results: WorktreeInfo[] = [];
|
|
42
43
|
for (const name of branchNames) {
|
|
43
44
|
const worktreePath = join(projectDir, name);
|
|
44
45
|
gitCreateWorktree(name, worktreePath);
|
|
46
|
+
createValidateBranch(name);
|
|
45
47
|
results.push({ path: worktreePath, branch: name });
|
|
46
48
|
logger.info(`worktree 创建完成: ${worktreePath} (分支: ${name})`);
|
|
47
49
|
}
|
|
@@ -64,11 +66,12 @@ export function createWorktreesByBranches(branchNames: string[]): WorktreeInfo[]
|
|
|
64
66
|
const projectDir = getProjectWorktreeDir();
|
|
65
67
|
ensureDir(projectDir);
|
|
66
68
|
|
|
67
|
-
// 3. 串行创建 worktree
|
|
69
|
+
// 3. 串行创建 worktree 及对应验证分支
|
|
68
70
|
const results: WorktreeInfo[] = [];
|
|
69
71
|
for (const name of branchNames) {
|
|
70
72
|
const worktreePath = join(projectDir, name);
|
|
71
73
|
gitCreateWorktree(name, worktreePath);
|
|
74
|
+
createValidateBranch(name);
|
|
72
75
|
results.push({ path: worktreePath, branch: name });
|
|
73
76
|
logger.info(`worktree 创建完成: ${worktreePath} (分支: ${name})`);
|
|
74
77
|
}
|
|
@@ -123,6 +126,7 @@ export function cleanupWorktrees(worktrees: WorktreeInfo[]): void {
|
|
|
123
126
|
try {
|
|
124
127
|
removeWorktreeByPath(wt.path);
|
|
125
128
|
deleteBranch(wt.branch);
|
|
129
|
+
deleteValidateBranch(wt.branch);
|
|
126
130
|
logger.info(`已清理 worktree 和分支: ${wt.branch}`);
|
|
127
131
|
} catch (error) {
|
|
128
132
|
logger.error(`清理 worktree 失败: ${wt.path} - ${error}`);
|
|
@@ -26,6 +26,10 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
26
26
|
vi.mock('../../../src/utils/index.js', () => ({
|
|
27
27
|
validateMainWorktree: vi.fn(),
|
|
28
28
|
createWorktrees: vi.fn(),
|
|
29
|
+
getConfigValue: vi.fn().mockReturnValue(true),
|
|
30
|
+
requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
|
|
31
|
+
ensureOnMainWorkBranch: vi.fn().mockResolvedValue(undefined),
|
|
32
|
+
getValidateBranchName: vi.fn((name: string) => `clawt-validate-${name}`),
|
|
29
33
|
printSuccess: vi.fn(),
|
|
30
34
|
printInfo: vi.fn(),
|
|
31
35
|
printSeparator: vi.fn(),
|
|
@@ -54,7 +58,7 @@ describe('registerCreateCommand', () => {
|
|
|
54
58
|
});
|
|
55
59
|
|
|
56
60
|
describe('handleCreate', () => {
|
|
57
|
-
it('成功创建 worktree', () => {
|
|
61
|
+
it('成功创建 worktree', async () => {
|
|
58
62
|
mockedCreateWorktrees.mockReturnValue([
|
|
59
63
|
{ path: '/path/feature', branch: 'feature' },
|
|
60
64
|
]);
|
|
@@ -62,14 +66,14 @@ describe('handleCreate', () => {
|
|
|
62
66
|
const program = new Command();
|
|
63
67
|
program.exitOverride();
|
|
64
68
|
registerCreateCommand(program);
|
|
65
|
-
program.
|
|
69
|
+
await program.parseAsync(['create', '-b', 'feature'], { from: 'user' });
|
|
66
70
|
|
|
67
71
|
expect(mockedValidateMainWorktree).toHaveBeenCalled();
|
|
68
72
|
expect(mockedCreateWorktrees).toHaveBeenCalledWith('feature', 1);
|
|
69
73
|
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
70
74
|
});
|
|
71
75
|
|
|
72
|
-
it('支持 -n 指定创建数量', () => {
|
|
76
|
+
it('支持 -n 指定创建数量', async () => {
|
|
73
77
|
mockedCreateWorktrees.mockReturnValue([
|
|
74
78
|
{ path: '/path/feature-1', branch: 'feature-1' },
|
|
75
79
|
{ path: '/path/feature-2', branch: 'feature-2' },
|
|
@@ -78,38 +82,38 @@ describe('handleCreate', () => {
|
|
|
78
82
|
const program = new Command();
|
|
79
83
|
program.exitOverride();
|
|
80
84
|
registerCreateCommand(program);
|
|
81
|
-
program.
|
|
85
|
+
await program.parseAsync(['create', '-b', 'feature', '-n', '2'], { from: 'user' });
|
|
82
86
|
|
|
83
87
|
expect(mockedCreateWorktrees).toHaveBeenCalledWith('feature', 2);
|
|
84
88
|
});
|
|
85
89
|
|
|
86
|
-
it('无效数量抛出 ClawtError', () => {
|
|
90
|
+
it('无效数量抛出 ClawtError', async () => {
|
|
87
91
|
const program = new Command();
|
|
88
92
|
program.exitOverride();
|
|
89
93
|
registerCreateCommand(program);
|
|
90
94
|
|
|
91
|
-
expect(
|
|
92
|
-
program.
|
|
93
|
-
|
|
95
|
+
await expect(
|
|
96
|
+
program.parseAsync(['create', '-b', 'feature', '-n', 'abc'], { from: 'user' }),
|
|
97
|
+
).rejects.toThrow();
|
|
94
98
|
});
|
|
95
99
|
|
|
96
|
-
it('数量为 0 时抛出 ClawtError', () => {
|
|
100
|
+
it('数量为 0 时抛出 ClawtError', async () => {
|
|
97
101
|
const program = new Command();
|
|
98
102
|
program.exitOverride();
|
|
99
103
|
registerCreateCommand(program);
|
|
100
104
|
|
|
101
|
-
expect(
|
|
102
|
-
program.
|
|
103
|
-
|
|
105
|
+
await expect(
|
|
106
|
+
program.parseAsync(['create', '-b', 'feature', '-n', '0'], { from: 'user' }),
|
|
107
|
+
).rejects.toThrow();
|
|
104
108
|
});
|
|
105
109
|
|
|
106
|
-
it('负数数量抛出 ClawtError', () => {
|
|
110
|
+
it('负数数量抛出 ClawtError', async () => {
|
|
107
111
|
const program = new Command();
|
|
108
112
|
program.exitOverride();
|
|
109
113
|
registerCreateCommand(program);
|
|
110
114
|
|
|
111
|
-
expect(
|
|
112
|
-
program.
|
|
113
|
-
|
|
115
|
+
await expect(
|
|
116
|
+
program.parseAsync(['create', '-b', 'feature', '-n', '-1'], { from: 'user' }),
|
|
117
|
+
).rejects.toThrow();
|
|
114
118
|
});
|
|
115
119
|
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
|
|
4
|
+
vi.mock('../../../src/logger/index.js', () => ({
|
|
5
|
+
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock('../../../src/constants/index.js', () => ({
|
|
9
|
+
MESSAGES: {
|
|
10
|
+
INIT_SUCCESS: (branch: string) => `✓ 项目初始化成功,主工作分支设置为: ${branch}`,
|
|
11
|
+
INIT_UPDATED: (oldBranch: string, newBranch: string) => `✓ 已将主工作分支从 ${oldBranch} 更新为 ${newBranch}`,
|
|
12
|
+
INIT_SHOW: (configJson: string) => `当前项目配置:\n${configJson}`,
|
|
13
|
+
PROJECT_NOT_INITIALIZED: '项目尚未初始化,请先执行 clawt init 设置主工作分支',
|
|
14
|
+
PROJECT_CONFIG_MISSING_BRANCH: '项目配置缺少主工作分支信息,请重新执行 clawt init 设置主工作分支',
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock('../../../src/utils/index.js', () => ({
|
|
19
|
+
validateMainWorktree: vi.fn(),
|
|
20
|
+
getCurrentBranch: vi.fn().mockReturnValue('main'),
|
|
21
|
+
loadProjectConfig: vi.fn(),
|
|
22
|
+
saveProjectConfig: vi.fn(),
|
|
23
|
+
requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
|
|
24
|
+
printSuccess: vi.fn(),
|
|
25
|
+
printInfo: vi.fn(),
|
|
26
|
+
safeStringify: vi.fn((value: unknown, indent: number = 2) => JSON.stringify(value, null, indent)),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
import { registerInitCommand } from '../../../src/commands/init.js';
|
|
30
|
+
import {
|
|
31
|
+
loadProjectConfig,
|
|
32
|
+
saveProjectConfig,
|
|
33
|
+
requireProjectConfig,
|
|
34
|
+
printSuccess,
|
|
35
|
+
printInfo,
|
|
36
|
+
getCurrentBranch,
|
|
37
|
+
} from '../../../src/utils/index.js';
|
|
38
|
+
|
|
39
|
+
const mockedLoadProjectConfig = vi.mocked(loadProjectConfig);
|
|
40
|
+
const mockedSaveProjectConfig = vi.mocked(saveProjectConfig);
|
|
41
|
+
const mockedRequireProjectConfig = vi.mocked(requireProjectConfig);
|
|
42
|
+
const mockedPrintSuccess = vi.mocked(printSuccess);
|
|
43
|
+
const mockedPrintInfo = vi.mocked(printInfo);
|
|
44
|
+
const mockedGetCurrentBranch = vi.mocked(getCurrentBranch);
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
mockedLoadProjectConfig.mockReset();
|
|
48
|
+
mockedSaveProjectConfig.mockReset();
|
|
49
|
+
mockedRequireProjectConfig.mockReset();
|
|
50
|
+
mockedRequireProjectConfig.mockReturnValue({ clawtMainWorkBranch: 'main' });
|
|
51
|
+
mockedPrintSuccess.mockReset();
|
|
52
|
+
mockedPrintInfo.mockReset();
|
|
53
|
+
mockedGetCurrentBranch.mockReturnValue('main');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('registerInitCommand', () => {
|
|
57
|
+
it('注册 init 命令', () => {
|
|
58
|
+
const program = new Command();
|
|
59
|
+
registerInitCommand(program);
|
|
60
|
+
const cmd = program.commands.find((c) => c.name() === 'init');
|
|
61
|
+
expect(cmd).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('注册 init show 子命令', () => {
|
|
65
|
+
const program = new Command();
|
|
66
|
+
registerInitCommand(program);
|
|
67
|
+
const initCmd = program.commands.find((c) => c.name() === 'init');
|
|
68
|
+
const showCmd = initCmd?.commands.find((c) => c.name() === 'show');
|
|
69
|
+
expect(showCmd).toBeDefined();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('handleInit', () => {
|
|
74
|
+
it('无参数且已初始化时使用当前分支切换主工作分支', async () => {
|
|
75
|
+
mockedLoadProjectConfig.mockReturnValue({ clawtMainWorkBranch: 'develop' });
|
|
76
|
+
|
|
77
|
+
const program = new Command();
|
|
78
|
+
program.exitOverride();
|
|
79
|
+
registerInitCommand(program);
|
|
80
|
+
await program.parseAsync(['init'], { from: 'user' });
|
|
81
|
+
|
|
82
|
+
expect(mockedSaveProjectConfig).toHaveBeenCalledWith({ clawtMainWorkBranch: 'main' });
|
|
83
|
+
expect(mockedPrintSuccess).toHaveBeenCalledWith(
|
|
84
|
+
expect.stringContaining('develop'),
|
|
85
|
+
);
|
|
86
|
+
expect(mockedPrintSuccess).toHaveBeenCalledWith(
|
|
87
|
+
expect.stringContaining('main'),
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('无参数且未初始化时使用当前分支初始化', async () => {
|
|
92
|
+
mockedLoadProjectConfig.mockReturnValue(null);
|
|
93
|
+
|
|
94
|
+
const program = new Command();
|
|
95
|
+
program.exitOverride();
|
|
96
|
+
registerInitCommand(program);
|
|
97
|
+
await program.parseAsync(['init'], { from: 'user' });
|
|
98
|
+
|
|
99
|
+
expect(mockedSaveProjectConfig).toHaveBeenCalledWith({ clawtMainWorkBranch: 'main' });
|
|
100
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('有 -b 参数时设置指定分支', async () => {
|
|
104
|
+
mockedLoadProjectConfig.mockReturnValue(null);
|
|
105
|
+
|
|
106
|
+
const program = new Command();
|
|
107
|
+
program.exitOverride();
|
|
108
|
+
registerInitCommand(program);
|
|
109
|
+
await program.parseAsync(['init', '-b', 'develop'], { from: 'user' });
|
|
110
|
+
|
|
111
|
+
expect(mockedSaveProjectConfig).toHaveBeenCalledWith({ clawtMainWorkBranch: 'develop' });
|
|
112
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('有 -b 参数且已初始化时更新配置', async () => {
|
|
116
|
+
mockedLoadProjectConfig.mockReturnValue({ clawtMainWorkBranch: 'main' });
|
|
117
|
+
|
|
118
|
+
const program = new Command();
|
|
119
|
+
program.exitOverride();
|
|
120
|
+
registerInitCommand(program);
|
|
121
|
+
await program.parseAsync(['init', '-b', 'develop'], { from: 'user' });
|
|
122
|
+
|
|
123
|
+
expect(mockedSaveProjectConfig).toHaveBeenCalledWith({ clawtMainWorkBranch: 'develop' });
|
|
124
|
+
// 验证 INIT_UPDATED 传入了旧分支名和新分支名
|
|
125
|
+
expect(mockedPrintSuccess).toHaveBeenCalledWith(
|
|
126
|
+
expect.stringContaining('main'),
|
|
127
|
+
);
|
|
128
|
+
expect(mockedPrintSuccess).toHaveBeenCalledWith(
|
|
129
|
+
expect.stringContaining('develop'),
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('handleInitShow (show 子命令)', () => {
|
|
135
|
+
it('clawt init show 展示当前配置', async () => {
|
|
136
|
+
mockedRequireProjectConfig.mockReturnValue({ clawtMainWorkBranch: 'develop' });
|
|
137
|
+
|
|
138
|
+
const program = new Command();
|
|
139
|
+
program.exitOverride();
|
|
140
|
+
registerInitCommand(program);
|
|
141
|
+
await program.parseAsync(['init', 'show'], { from: 'user' });
|
|
142
|
+
|
|
143
|
+
expect(mockedPrintInfo).toHaveBeenCalled();
|
|
144
|
+
expect(mockedSaveProjectConfig).not.toHaveBeenCalled();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -64,7 +64,13 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
64
64
|
gitMergeBase: vi.fn(),
|
|
65
65
|
gitResetSoftTo: vi.fn(),
|
|
66
66
|
getCurrentBranch: vi.fn(),
|
|
67
|
+
gitResetHard: vi.fn(),
|
|
68
|
+
gitCleanForce: vi.fn(),
|
|
69
|
+
gitCheckout: vi.fn(),
|
|
67
70
|
resolveTargetWorktree: vi.fn(),
|
|
71
|
+
requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
|
|
72
|
+
getMainWorkBranch: vi.fn().mockReturnValue('main'),
|
|
73
|
+
ensureOnMainWorkBranch: vi.fn(),
|
|
68
74
|
}));
|
|
69
75
|
|
|
70
76
|
import { registerMergeCommand } from '../../../src/commands/merge.js';
|
|
@@ -175,7 +181,7 @@ describe('handleMerge', () => {
|
|
|
175
181
|
|
|
176
182
|
it('目标 worktree 有未提交修改且提供 -m 时先提交再合并', async () => {
|
|
177
183
|
mockedIsWorkingDirClean
|
|
178
|
-
.mockReturnValueOnce(true) // 主 worktree
|
|
184
|
+
.mockReturnValueOnce(true) // 主 worktree 状态检测:干净
|
|
179
185
|
.mockReturnValueOnce(false); // 目标 worktree 不干净
|
|
180
186
|
mockedConfirmAction.mockResolvedValue(false); // 不清理
|
|
181
187
|
|
|
@@ -46,6 +46,10 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
46
46
|
removeSnapshot: vi.fn(),
|
|
47
47
|
removeProjectSnapshots: vi.fn(),
|
|
48
48
|
resolveTargetWorktrees: vi.fn(),
|
|
49
|
+
requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
|
|
50
|
+
getValidateBranchName: vi.fn((name: string) => `clawt-validate-${name}`),
|
|
51
|
+
deleteValidateBranch: vi.fn(),
|
|
52
|
+
ensureOnMainWorkBranch: vi.fn(),
|
|
49
53
|
}));
|
|
50
54
|
|
|
51
55
|
import { registerRemoveCommand } from '../../../src/commands/remove.js';
|
|
@@ -23,6 +23,8 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
23
23
|
confirmDestructiveAction: vi.fn(),
|
|
24
24
|
printSuccess: vi.fn(),
|
|
25
25
|
printInfo: vi.fn(),
|
|
26
|
+
requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
|
|
27
|
+
switchBackIfOnValidateBranch: vi.fn(),
|
|
26
28
|
}));
|
|
27
29
|
|
|
28
30
|
import { registerResetCommand } from '../../../src/commands/reset.js';
|
|
@@ -71,6 +71,8 @@ vi.mock('../../../src/utils/index.js', async (importOriginal) => {
|
|
|
71
71
|
parseTasksFromOptions: vi.fn(),
|
|
72
72
|
createWorktreesByBranches: vi.fn(),
|
|
73
73
|
printDryRunPreview: vi.fn(),
|
|
74
|
+
requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
|
|
75
|
+
ensureOnMainWorkBranch: vi.fn().mockResolvedValue(undefined),
|
|
74
76
|
};
|
|
75
77
|
});
|
|
76
78
|
|
|
@@ -25,6 +25,7 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
25
25
|
SYNC_MERGING: (branch: string, mainBranch: string) => `正在将 ${mainBranch} 合并到 ${branch}...`,
|
|
26
26
|
SYNC_CONFLICT: (path: string) => `合并冲突,请手动解决: ${path}`,
|
|
27
27
|
SYNC_SUCCESS: (branch: string, mainBranch: string) => `✓ 已将 ${mainBranch} 同步到 ${branch}`,
|
|
28
|
+
SYNC_VALIDATE_BRANCH_REBUILT: (validateBranch: string) => `验证分支 ${validateBranch} 已重建`,
|
|
28
29
|
},
|
|
29
30
|
AUTO_SAVE_COMMIT_MESSAGE: 'clawt:auto-save',
|
|
30
31
|
}));
|
|
@@ -46,6 +47,11 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
46
47
|
printInfo: vi.fn(),
|
|
47
48
|
printWarning: vi.fn(),
|
|
48
49
|
resolveTargetWorktree: vi.fn(),
|
|
50
|
+
requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
|
|
51
|
+
getMainWorkBranch: vi.fn().mockReturnValue('main'),
|
|
52
|
+
rebuildValidateBranch: vi.fn(),
|
|
53
|
+
getValidateBranchName: vi.fn((name: string) => `clawt-validate-${name}`),
|
|
54
|
+
ensureOnMainWorkBranch: vi.fn(),
|
|
49
55
|
}));
|
|
50
56
|
|
|
51
57
|
import { registerSyncCommand } from '../../../src/commands/sync.js';
|
|
@@ -40,6 +40,11 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
40
40
|
VALIDATE_PARALLEL_CMD_FAILED: (cmd: string, code: number) => ` ✗ ${cmd}(退出码: ${code})`,
|
|
41
41
|
VALIDATE_PARALLEL_CMD_ERROR: (cmd: string, msg: string) => ` ✗ ${cmd}(错误: ${msg})`,
|
|
42
42
|
SEPARATOR: '────',
|
|
43
|
+
VALIDATE_BRANCH_NOT_FOUND: (validateBranch: string, branch: string) => `验证分支 ${validateBranch} 不存在`,
|
|
44
|
+
VALIDATE_SUCCESS_WITH_BRANCH: (branch: string, validateBranch: string) => `✓ 已切换到验证分支 ${validateBranch} 并验证 ${branch}`,
|
|
45
|
+
VALIDATE_CONFIRM_AUTO_SYNC: (branch: string) => `是否自动 sync ${branch}`,
|
|
46
|
+
VALIDATE_AUTO_SYNC_DECLINED: (branch: string) => `已跳过 ${branch} 的自动 sync`,
|
|
47
|
+
VALIDATE_AUTO_SYNC_START: (branch: string) => `正在自动 sync ${branch}`,
|
|
43
48
|
},
|
|
44
49
|
}));
|
|
45
50
|
|
|
@@ -79,6 +84,7 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
79
84
|
writeSnapshot: vi.fn(),
|
|
80
85
|
removeSnapshot: vi.fn(),
|
|
81
86
|
confirmDestructiveAction: vi.fn(),
|
|
87
|
+
confirmAction: vi.fn(),
|
|
82
88
|
printSuccess: vi.fn(),
|
|
83
89
|
printWarning: vi.fn(),
|
|
84
90
|
printInfo: vi.fn(),
|
|
@@ -88,6 +94,13 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
88
94
|
printSeparator: vi.fn(),
|
|
89
95
|
parseParallelCommands: vi.fn(),
|
|
90
96
|
runParallelCommands: vi.fn(),
|
|
97
|
+
requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
|
|
98
|
+
getValidateBranchName: vi.fn((name: string) => `clawt-validate-${name}`),
|
|
99
|
+
gitCheckout: vi.fn(),
|
|
100
|
+
ensureOnMainWorkBranch: vi.fn(),
|
|
101
|
+
handleDirtyWorkingDir: vi.fn(),
|
|
102
|
+
checkBranchExists: vi.fn().mockReturnValue(true),
|
|
103
|
+
getCurrentBranch: vi.fn().mockReturnValue('main'),
|
|
91
104
|
}));
|
|
92
105
|
|
|
93
106
|
import { registerValidateCommand } from '../../../src/commands/validate.js';
|
|
@@ -90,9 +90,9 @@ describe('writeConfig', () => {
|
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
describe('ensureClawtDirs', () => {
|
|
93
|
-
it('
|
|
93
|
+
it('确保四个全局目录存在', () => {
|
|
94
94
|
ensureClawtDirs();
|
|
95
|
-
expect(mockedEnsureDir).toHaveBeenCalledTimes(
|
|
95
|
+
expect(mockedEnsureDir).toHaveBeenCalledTimes(4);
|
|
96
96
|
});
|
|
97
97
|
});
|
|
98
98
|
|