clawt 3.4.3 → 3.4.5
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/dist/index.js +132 -108
- package/dist/postinstall.js +12 -0
- package/package.json +1 -1
- package/src/commands/cover-validate.ts +1 -1
- package/src/commands/create.ts +2 -7
- package/src/commands/home.ts +1 -1
- package/src/commands/init.ts +2 -2
- package/src/commands/list.ts +4 -4
- package/src/commands/merge.ts +2 -10
- package/src/commands/remove.ts +1 -1
- package/src/commands/reset.ts +1 -1
- package/src/commands/resume.ts +2 -3
- package/src/commands/run.ts +3 -9
- package/src/commands/status.ts +3 -16
- package/src/commands/sync.ts +2 -3
- package/src/commands/validate.ts +2 -2
- package/src/constants/index.ts +1 -0
- package/src/constants/interactive-panel.ts +2 -0
- package/src/constants/messages/interactive-panel.ts +1 -0
- package/src/constants/pre-checks.ts +30 -0
- package/src/utils/index.ts +4 -3
- package/src/utils/interactive-panel.ts +13 -0
- package/src/utils/project-config.ts +2 -24
- package/src/utils/validate-branch.ts +9 -4
- package/src/utils/validation.ts +55 -18
- package/src/utils/worktree-matcher.ts +15 -0
- package/tests/unit/commands/create.test.ts +11 -7
- package/tests/unit/commands/list.test.ts +18 -14
- package/tests/unit/commands/merge.test.ts +27 -23
- package/tests/unit/commands/resume.test.ts +14 -11
- package/tests/unit/commands/run.test.ts +29 -25
- package/tests/unit/commands/status.test.ts +58 -54
- package/tests/unit/commands/sync.test.ts +18 -14
- package/tests/unit/utils/validate-branch.test.ts +22 -1
package/src/commands/status.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
hasLocalCommits,
|
|
17
17
|
getSnapshotModifiedTime,
|
|
18
18
|
getProjectSnapshotBranches,
|
|
19
|
-
|
|
19
|
+
getWorktreeCreatedTime,
|
|
20
20
|
formatRelativeTime,
|
|
21
21
|
printInfo,
|
|
22
22
|
printDoubleSeparator,
|
|
@@ -46,7 +46,7 @@ export function registerStatusCommand(program: Command): void {
|
|
|
46
46
|
* @param {StatusOptions} options - 命令选项
|
|
47
47
|
*/
|
|
48
48
|
async function handleStatus(options: StatusOptions): Promise<void> {
|
|
49
|
-
runPreChecks({
|
|
49
|
+
await runPreChecks({ requireMainWorktree: true, requireHead: true });
|
|
50
50
|
|
|
51
51
|
// 交互式面板模式
|
|
52
52
|
if (options.interactive) {
|
|
@@ -116,7 +116,7 @@ function collectWorktreeDetailedStatus(worktree: WorktreeInfo, projectName: stri
|
|
|
116
116
|
const changeStatus = detectChangeStatus(worktree);
|
|
117
117
|
const { commitsAhead, commitsBehind } = countCommitDivergence(worktree.branch);
|
|
118
118
|
const { insertions, deletions } = countDiffStat(worktree.path);
|
|
119
|
-
const createdAt =
|
|
119
|
+
const createdAt = getWorktreeCreatedTime(worktree.path);
|
|
120
120
|
|
|
121
121
|
return {
|
|
122
122
|
path: worktree.path,
|
|
@@ -183,19 +183,6 @@ function countDiffStat(worktreePath: string): { insertions: number; deletions: n
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
/**
|
|
187
|
-
* 获取分支的创建时间
|
|
188
|
-
* @param {string} branchName - 分支名
|
|
189
|
-
* @returns {string | null} ISO 8601 格式的创建时间,获取失败时返回 null
|
|
190
|
-
*/
|
|
191
|
-
function resolveBranchCreatedAt(branchName: string): string | null {
|
|
192
|
-
try {
|
|
193
|
-
return getBranchCreatedAt(branchName);
|
|
194
|
-
} catch {
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
186
|
/**
|
|
200
187
|
* 获取分支的 validate 快照修改时间
|
|
201
188
|
* @param {string} projectName - 项目名
|
package/src/commands/sync.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { logger } from '../logger/index.js';
|
|
|
3
3
|
import { ClawtError } from '../errors/index.js';
|
|
4
4
|
import { MESSAGES, AUTO_SAVE_COMMIT_MESSAGE } from '../constants/index.js';
|
|
5
5
|
import type { SyncOptions } from '../types/index.js';
|
|
6
|
+
import { PRE_CHECK_SYNC } from '../constants/index.js';
|
|
6
7
|
import {
|
|
7
8
|
runPreChecks,
|
|
8
9
|
getGitTopLevel,
|
|
@@ -19,7 +20,6 @@ import {
|
|
|
19
20
|
getMainWorkBranch,
|
|
20
21
|
rebuildValidateBranch,
|
|
21
22
|
getValidateBranchName,
|
|
22
|
-
guardMainWorkBranch,
|
|
23
23
|
} from '../utils/index.js';
|
|
24
24
|
import type { WorktreeResolveMessages } from '../utils/index.js';
|
|
25
25
|
|
|
@@ -127,8 +127,7 @@ export async function executeSyncForBranch(targetWorktreePath: string, branch: s
|
|
|
127
127
|
* @param {SyncOptions} options - 命令选项
|
|
128
128
|
*/
|
|
129
129
|
async function handleSync(options: SyncOptions): Promise<void> {
|
|
130
|
-
runPreChecks(
|
|
131
|
-
await guardMainWorkBranch();
|
|
130
|
+
await runPreChecks(PRE_CHECK_SYNC);
|
|
132
131
|
|
|
133
132
|
logger.info(`sync 命令执行,分支: ${options.branch ?? '(未指定)'}`);
|
|
134
133
|
|
package/src/commands/validate.ts
CHANGED
|
@@ -97,7 +97,7 @@ async function handlePatchApplyFailure(targetWorktreePath: string, branchName: s
|
|
|
97
97
|
* @param {ValidateOptions} options - 命令选项
|
|
98
98
|
*/
|
|
99
99
|
async function handleValidateClean(options: ValidateOptions): Promise<void> {
|
|
100
|
-
runPreChecks({
|
|
100
|
+
await runPreChecks({ requireMainWorktree: true, requireHead: true, requireProjectConfig: true });
|
|
101
101
|
|
|
102
102
|
const projectName = getProjectName();
|
|
103
103
|
const mainWorktreePath = getGitTopLevel();
|
|
@@ -261,7 +261,7 @@ async function handleValidate(options: ValidateOptions): Promise<void> {
|
|
|
261
261
|
return;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
runPreChecks({
|
|
264
|
+
await runPreChecks({ requireMainWorktree: true, requireHead: true, requireProjectConfig: true });
|
|
265
265
|
|
|
266
266
|
const projectName = getProjectName();
|
|
267
267
|
const mainWorktreePath = getGitTopLevel();
|
package/src/constants/index.ts
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PreCheckOptions } from '../utils/validation.js';
|
|
2
|
+
|
|
3
|
+
/** create 命令:主 worktree + HEAD + 切换分支 + 工作区干净 */
|
|
4
|
+
export const PRE_CHECK_CREATE: PreCheckOptions = {
|
|
5
|
+
requireMainWorktree: true, requireHead: true,
|
|
6
|
+
ensureOnClawtMainWorkBranch: true, requireCleanWorkingDir: true,
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
/** run 命令正常模式(不含 requireClaudeCode,因为 claudeCode 在 dryRun 分支之后才需要) */
|
|
10
|
+
export const PRE_CHECK_RUN: PreCheckOptions = { ...PRE_CHECK_CREATE } as const;
|
|
11
|
+
|
|
12
|
+
/** run dry-run 模式 */
|
|
13
|
+
export const PRE_CHECK_DRY_RUN: PreCheckOptions = { requireMainWorktree: true, requireHead: true } as const;
|
|
14
|
+
|
|
15
|
+
/** merge 命令 */
|
|
16
|
+
export const PRE_CHECK_MERGE: PreCheckOptions = {
|
|
17
|
+
requireMainWorktree: true, requireHead: true,
|
|
18
|
+
ensureOnClawtMainWorkBranch: true,
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
/** sync 命令 */
|
|
22
|
+
export const PRE_CHECK_SYNC: PreCheckOptions = {
|
|
23
|
+
requireMainWorktree: true, requireHead: true, requireProjectConfig: true,
|
|
24
|
+
ensureOnClawtMainWorkBranch: true,
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
/** resume 命令 */
|
|
28
|
+
export const PRE_CHECK_RESUME: PreCheckOptions = {
|
|
29
|
+
requireMainWorktree: true, requireHead: true, requireClaudeCode: true,
|
|
30
|
+
} as const;
|
package/src/utils/index.ts
CHANGED
|
@@ -50,7 +50,8 @@ export {
|
|
|
50
50
|
createBranch,
|
|
51
51
|
} from './git.js';
|
|
52
52
|
export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
|
|
53
|
-
export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled, validateHeadExists, runPreChecks } from './validation.js';
|
|
53
|
+
export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled, validateHeadExists, validateWorkingDirClean, runPreChecks } from './validation.js';
|
|
54
|
+
export type { PreCheckOptions } from './validation.js';
|
|
54
55
|
export { createWorktrees, getProjectWorktrees, getProjectWorktreeDir, cleanupWorktrees, getWorktreeStatus, createWorktreesByBranches } from './worktree.js';
|
|
55
56
|
export { loadConfig, writeDefaultConfig, writeConfig, saveConfig, getConfigValue, ensureClawtDirs, parseConcurrency } from './config.js';
|
|
56
57
|
export { printSuccess, printError, printWarning, printInfo, printHint, printSeparator, printDoubleSeparator, confirmAction, confirmDestructiveAction, formatWorktreeStatus, isWorktreeIdle, formatDuration, formatRelativeTime, formatDiskSize, formatLocalISOString } from './formatter.js';
|
|
@@ -58,7 +59,7 @@ export { ensureDir, removeEmptyDir, calculateDirSize } from './fs.js';
|
|
|
58
59
|
export { multilineInput } from './prompt.js';
|
|
59
60
|
export { launchInteractiveClaude, hasClaudeSessionHistory, launchInteractiveClaudeInNewTerminal } from './claude.js';
|
|
60
61
|
export { getSnapshotPath, hasSnapshot, getSnapshotModifiedTime, readSnapshotTreeHash, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots, getProjectSnapshotBranches } from './validate-snapshot.js';
|
|
61
|
-
export { findExactMatch, findFuzzyMatches, promptSelectBranch, promptMultiSelectBranches, promptGroupedMultiSelectBranches, resolveTargetWorktree, resolveTargetWorktrees, groupWorktreesByDate, buildGroupedChoices, buildGroupMembershipMap, formatRelativeDate, getWorktreeCreatedDate } from './worktree-matcher.js';
|
|
62
|
+
export { findExactMatch, findFuzzyMatches, promptSelectBranch, promptMultiSelectBranches, promptGroupedMultiSelectBranches, resolveTargetWorktree, resolveTargetWorktrees, groupWorktreesByDate, buildGroupedChoices, buildGroupMembershipMap, formatRelativeDate, getWorktreeCreatedDate, getWorktreeCreatedTime } from './worktree-matcher.js';
|
|
62
63
|
export type { WorktreeResolveMessages, WorktreeMultiResolveMessages } from './worktree-matcher.js';
|
|
63
64
|
export { ProgressRenderer } from './progress.js';
|
|
64
65
|
export { parseTaskFile, loadTaskFile, parseTasksFromOptions } from './task-file.js';
|
|
@@ -70,7 +71,7 @@ export { truncateTaskDesc, printDryRunPreview } from './dry-run.js';
|
|
|
70
71
|
export { applyAliases } from './alias.js';
|
|
71
72
|
export { isValidConfigKey, getValidConfigKeys, parseConfigValue, promptConfigValue, formatConfigValue, interactiveConfigEditor } from './config-strategy.js';
|
|
72
73
|
export { checkForUpdates } from './update-checker.js';
|
|
73
|
-
export { getProjectConfigPath, loadProjectConfig, saveProjectConfig, requireProjectConfig, getMainWorkBranch, guardMainWorkBranchExists,
|
|
74
|
+
export { getProjectConfigPath, loadProjectConfig, saveProjectConfig, requireProjectConfig, getMainWorkBranch, guardMainWorkBranchExists, getValidateRunCommand } from './project-config.js';
|
|
74
75
|
export { getValidateBranchName, createValidateBranch, deleteValidateBranch, rebuildValidateBranch, ensureOnMainWorkBranch, handleDirtyWorkingDir } from './validate-branch.js';
|
|
75
76
|
export { safeStringify } from './json.js';
|
|
76
77
|
export { executeRunCommand } from './validate-runner.js';
|
|
@@ -281,6 +281,11 @@ export class InteractivePanel {
|
|
|
281
281
|
this.executeOperation(() => this.handleSync());
|
|
282
282
|
return;
|
|
283
283
|
}
|
|
284
|
+
|
|
285
|
+
if (key === PANEL_SHORTCUT_KEYS.COVER) {
|
|
286
|
+
this.executeOperation(() => this.handleCover());
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
284
289
|
}
|
|
285
290
|
|
|
286
291
|
/**
|
|
@@ -571,6 +576,14 @@ export class InteractivePanel {
|
|
|
571
576
|
runCommandInherited(`clawt sync -b ${branch}`);
|
|
572
577
|
}
|
|
573
578
|
|
|
579
|
+
/**
|
|
580
|
+
* 执行覆盖操作
|
|
581
|
+
* cover 命令从主 worktree 当前所在的验证分支名自动推导目标分支
|
|
582
|
+
*/
|
|
583
|
+
private handleCover(): void {
|
|
584
|
+
runCommandInherited('clawt cover');
|
|
585
|
+
}
|
|
586
|
+
|
|
574
587
|
/**
|
|
575
588
|
* 等待用户按回车键
|
|
576
589
|
* @returns {Promise<void>} 用户按回车时 resolve
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { PROJECTS_CONFIG_DIR, MESSAGES
|
|
3
|
+
import { PROJECTS_CONFIG_DIR, MESSAGES } from '../constants/index.js';
|
|
4
4
|
import { ClawtError } from '../errors/index.js';
|
|
5
5
|
import { logger } from '../logger/index.js';
|
|
6
|
-
import { getProjectName, checkBranchExists
|
|
6
|
+
import { getProjectName, checkBranchExists } from './git.js';
|
|
7
7
|
import { ensureDir } from './fs.js';
|
|
8
|
-
import { printWarning, confirmAction } from './formatter.js';
|
|
9
8
|
import type { ProjectConfig } from '../types/index.js';
|
|
10
9
|
|
|
11
10
|
/**
|
|
@@ -92,27 +91,6 @@ export function guardMainWorkBranchExists(cwd?: string): void {
|
|
|
92
91
|
}
|
|
93
92
|
}
|
|
94
93
|
|
|
95
|
-
/**
|
|
96
|
-
* 守卫检测:验证配置中的主工作分支是否有效
|
|
97
|
-
* 分支不存在 → 抛出 ClawtError(致命错误)
|
|
98
|
-
* 当前分支与配置分支不一致且非验证分支 → 警告并交互确认是否继续
|
|
99
|
-
* @param {string} [cwd] - 工作目录
|
|
100
|
-
*/
|
|
101
|
-
export async function guardMainWorkBranch(cwd?: string): Promise<void> {
|
|
102
|
-
guardMainWorkBranchExists(cwd);
|
|
103
|
-
|
|
104
|
-
const config = requireProjectConfig();
|
|
105
|
-
const mainBranch = config.clawtMainWorkBranch;
|
|
106
|
-
const currentBranch = getCurrentBranch(cwd);
|
|
107
|
-
if (currentBranch !== mainBranch && !currentBranch.startsWith(VALIDATE_BRANCH_PREFIX)) {
|
|
108
|
-
printWarning(MESSAGES.GUARD_BRANCH_MISMATCH(mainBranch, currentBranch));
|
|
109
|
-
const confirmed = await confirmAction('是否继续执行?');
|
|
110
|
-
if (!confirmed) {
|
|
111
|
-
throw new ClawtError(MESSAGES.DESTRUCTIVE_OP_CANCELLED);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
94
|
/**
|
|
117
95
|
* 从项目配置中获取 validate 成功后自动执行的命令
|
|
118
96
|
* @returns {string | undefined} 配置的命令字符串,未配置时返回 undefined
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import Enquirer from 'enquirer';
|
|
2
|
-
import { VALIDATE_BRANCH_PREFIX } from '../constants/index.js';
|
|
2
|
+
import { VALIDATE_BRANCH_PREFIX, MESSAGES } from '../constants/index.js';
|
|
3
3
|
import { logger } from '../logger/index.js';
|
|
4
4
|
import { checkBranchExists, createBranch, deleteBranch, getCurrentBranch, gitCheckout, gitResetHard, gitCleanForce, isWorkingDirClean, gitAddAll, gitStashPush } from './git.js';
|
|
5
5
|
import { getMainWorkBranch } from './project-config.js';
|
|
6
|
-
import { printWarning } from './formatter.js';
|
|
6
|
+
import { printWarning, confirmAction } from './formatter.js';
|
|
7
7
|
import { ClawtError } from '../errors/index.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -133,7 +133,7 @@ export async function handleDirtyWorkingDir(cwd?: string): Promise<void> {
|
|
|
133
133
|
* 三种情况:
|
|
134
134
|
* 1. 已在主工作分支上 → 直接返回
|
|
135
135
|
* 2. 在验证分支上 → 清理工作区后自动切回主工作分支
|
|
136
|
-
* 3. 在其他分支上 →
|
|
136
|
+
* 3. 在其他分支上 → 警告并确认后,处理脏工作区再切换到主工作分支
|
|
137
137
|
* @param {string} [cwd] - 工作目录
|
|
138
138
|
*/
|
|
139
139
|
export async function ensureOnMainWorkBranch(cwd?: string): Promise<void> {
|
|
@@ -157,7 +157,12 @@ export async function ensureOnMainWorkBranch(cwd?: string): Promise<void> {
|
|
|
157
157
|
return;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
//
|
|
160
|
+
// 当前在其他分支上,警告并确认后处理脏工作区再切换
|
|
161
|
+
printWarning(MESSAGES.GUARD_BRANCH_MISMATCH(mainBranch, currentBranch));
|
|
162
|
+
const confirmed = await confirmAction('是否继续执行?');
|
|
163
|
+
if (!confirmed) {
|
|
164
|
+
throw new ClawtError(MESSAGES.DESTRUCTIVE_OP_CANCELLED);
|
|
165
|
+
}
|
|
161
166
|
logger.info(`当前在分支 ${currentBranch} 上,需切换到主工作分支 ${mainBranch}`);
|
|
162
167
|
if (!isWorkingDirClean(cwd)) {
|
|
163
168
|
await handleDirtyWorkingDir(cwd);
|
package/src/utils/validation.ts
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import { MESSAGES } from '../constants/index.js';
|
|
2
2
|
import { ClawtError } from '../errors/index.js';
|
|
3
3
|
import { execCommand } from './shell.js';
|
|
4
|
-
import { getGitCommonDir } from './git.js';
|
|
4
|
+
import { getGitCommonDir, isWorkingDirClean } from './git.js';
|
|
5
5
|
import { requireProjectConfig, guardMainWorkBranchExists } from './project-config.js';
|
|
6
|
+
import { ensureOnMainWorkBranch } from './validate-branch.js';
|
|
6
7
|
|
|
7
8
|
/** 统一前置校验选项 */
|
|
8
|
-
interface PreCheckOptions {
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
export interface PreCheckOptions {
|
|
10
|
+
/** 校验当前目录是否在主 worktree 根目录 */
|
|
11
|
+
requireMainWorktree?: boolean;
|
|
11
12
|
/** 校验 HEAD 是否存在(仓库有至少一次 commit) */
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
|
|
13
|
+
requireHead?: boolean;
|
|
14
|
+
/** 校验项目配置文件是否存在且合法 */
|
|
15
|
+
requireProjectConfig?: boolean;
|
|
16
|
+
/** 校验配置中的主工作分支在 git 仓库中是否存在 */
|
|
17
|
+
requireMainBranchExists?: boolean;
|
|
18
|
+
/** 确保当前在主工作分支上,不在则自动切换 */
|
|
19
|
+
ensureOnClawtMainWorkBranch?: boolean;
|
|
20
|
+
/** 校验主分支工作区和暂存区是否干净 */
|
|
21
|
+
requireCleanWorkingDir?: boolean;
|
|
22
|
+
/** 校验 Claude Code CLI 是否已安装 */
|
|
23
|
+
requireClaudeCode?: boolean;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
/**
|
|
@@ -38,6 +45,7 @@ export function validateMainWorktree(): void {
|
|
|
38
45
|
|
|
39
46
|
/**
|
|
40
47
|
* 校验 Git 是否已安装
|
|
48
|
+
* @deprecated 当前无调用方,保留函数不删除
|
|
41
49
|
* @throws {ClawtError} Git 未安装时抛出
|
|
42
50
|
*/
|
|
43
51
|
export function validateGitInstalled(): void {
|
|
@@ -73,26 +81,55 @@ export function validateHeadExists(): void {
|
|
|
73
81
|
}
|
|
74
82
|
}
|
|
75
83
|
|
|
84
|
+
/**
|
|
85
|
+
* 校验主分支工作区和暂存区是否干净
|
|
86
|
+
* 当存在未提交的更改时抛出错误,防止基于脏状态创建 worktree
|
|
87
|
+
* @throws {ClawtError} 工作区或暂存区不干净时抛出
|
|
88
|
+
*/
|
|
89
|
+
export function validateWorkingDirClean(): void {
|
|
90
|
+
if (!isWorkingDirClean()) {
|
|
91
|
+
throw new ClawtError(MESSAGES.MAIN_WORKTREE_DIRTY);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
76
95
|
/**
|
|
77
96
|
* 统一前置校验入口,按需执行各项校验
|
|
97
|
+
* 执行顺序:同步快速校验 → 异步交互校验 → 依赖前序结果的校验
|
|
78
98
|
* @param {PreCheckOptions} options - 校验选项
|
|
79
|
-
* @param {boolean} [options.
|
|
80
|
-
* @param {boolean} [options.
|
|
81
|
-
* @param {boolean} [options.
|
|
82
|
-
* @param {boolean} [options.
|
|
99
|
+
* @param {boolean} [options.requireMainWorktree] - 校验当前目录是否在主 worktree 根目录
|
|
100
|
+
* @param {boolean} [options.requireHead] - 校验 HEAD 是否存在
|
|
101
|
+
* @param {boolean} [options.requireProjectConfig] - 校验项目配置文件是否存在且合法
|
|
102
|
+
* @param {boolean} [options.requireMainBranchExists] - 校验配置中的主工作分支在 git 仓库中是否存在
|
|
103
|
+
* @param {boolean} [options.ensureOnClawtMainWorkBranch] - 确保当前在主工作分支上,不在则自动切换
|
|
104
|
+
* @param {boolean} [options.requireCleanWorkingDir] - 校验主分支工作区和暂存区是否干净
|
|
105
|
+
* @param {boolean} [options.requireClaudeCode] - 校验 Claude Code CLI 是否已安装
|
|
83
106
|
* @throws {ClawtError} 任一校验未通过时抛出
|
|
84
107
|
*/
|
|
85
|
-
export function runPreChecks(options: PreCheckOptions): void {
|
|
86
|
-
|
|
108
|
+
export async function runPreChecks(options: PreCheckOptions): Promise<void> {
|
|
109
|
+
// 阶段 1:同步快速校验
|
|
110
|
+
if (options.requireMainWorktree) {
|
|
87
111
|
validateMainWorktree();
|
|
88
112
|
}
|
|
89
|
-
if (options.
|
|
113
|
+
if (options.requireHead) {
|
|
90
114
|
validateHeadExists();
|
|
91
115
|
}
|
|
92
|
-
if (options.
|
|
116
|
+
if (options.requireProjectConfig) {
|
|
93
117
|
requireProjectConfig();
|
|
94
118
|
}
|
|
95
|
-
if (options.
|
|
119
|
+
if (options.requireMainBranchExists) {
|
|
96
120
|
guardMainWorkBranchExists();
|
|
97
121
|
}
|
|
122
|
+
|
|
123
|
+
// 阶段 2:异步交互校验
|
|
124
|
+
if (options.ensureOnClawtMainWorkBranch) {
|
|
125
|
+
await ensureOnMainWorkBranch();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 阶段 3:依赖前序结果的校验
|
|
129
|
+
if (options.requireCleanWorkingDir) {
|
|
130
|
+
validateWorkingDirClean();
|
|
131
|
+
}
|
|
132
|
+
if (options.requireClaudeCode) {
|
|
133
|
+
validateClaudeCodeInstalled();
|
|
134
|
+
}
|
|
98
135
|
}
|
|
@@ -310,6 +310,21 @@ export function getWorktreeCreatedDate(dirPath: string): string | null {
|
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
/**
|
|
314
|
+
* 获取 worktree 目录的创建时间(ISO 8601 格式)
|
|
315
|
+
* 通过文件系统的 birthtime 获取目录实际创建时间,保留小时/分钟级精度
|
|
316
|
+
* @param {string} dirPath - worktree 目录路径
|
|
317
|
+
* @returns {string | null} ISO 8601 格式的时间字符串,无法获取时返回 null
|
|
318
|
+
*/
|
|
319
|
+
export function getWorktreeCreatedTime(dirPath: string): string | null {
|
|
320
|
+
try {
|
|
321
|
+
const stat = statSync(dirPath);
|
|
322
|
+
return stat.birthtime.toISOString();
|
|
323
|
+
} catch {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
313
328
|
/**
|
|
314
329
|
* 将 YYYY-MM-DD 日期字符串格式化为中文相对日期描述
|
|
315
330
|
* 基于自然日计算,适用于日期分组场景
|
|
@@ -15,13 +15,17 @@ vi.mock('../../../src/errors/index.js', () => ({
|
|
|
15
15
|
},
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
|
-
vi.mock('../../../src/constants/index.js', () =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
18
|
+
vi.mock('../../../src/constants/index.js', async (importOriginal) => {
|
|
19
|
+
const actual = await importOriginal<typeof import('../../../src/constants/index.js')>();
|
|
20
|
+
return {
|
|
21
|
+
...actual,
|
|
22
|
+
MESSAGES: {
|
|
23
|
+
INVALID_COUNT: (val: string) => `数量必须为正整数: ${val}`,
|
|
24
|
+
WORKTREE_CREATED: (count: number) => `✓ 已创建 ${count} 个 worktree`,
|
|
25
|
+
},
|
|
26
|
+
EXIT_CODES: { SUCCESS: 0, ERROR: 1, ARGUMENT_ERROR: 2 },
|
|
27
|
+
};
|
|
28
|
+
});
|
|
25
29
|
|
|
26
30
|
vi.mock('../../../src/utils/index.js', () => ({
|
|
27
31
|
runPreChecks: vi.fn(),
|
|
@@ -5,12 +5,16 @@ vi.mock('../../../src/logger/index.js', () => ({
|
|
|
5
5
|
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
6
6
|
}));
|
|
7
7
|
|
|
8
|
-
vi.mock('../../../src/constants/index.js', () =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
vi.mock('../../../src/constants/index.js', async (importOriginal) => {
|
|
9
|
+
const actual = await importOriginal<typeof import('../../../src/constants/index.js')>();
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
MESSAGES: {
|
|
13
|
+
NO_WORKTREES: '(无 worktree)',
|
|
14
|
+
WORKTREE_STATUS_UNAVAILABLE: '(状态不可用)',
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
});
|
|
14
18
|
|
|
15
19
|
vi.mock('../../../src/utils/index.js', () => ({
|
|
16
20
|
runPreChecks: vi.fn(),
|
|
@@ -49,20 +53,20 @@ describe('registerListCommand', () => {
|
|
|
49
53
|
});
|
|
50
54
|
|
|
51
55
|
describe('handleList', () => {
|
|
52
|
-
it('无 worktree 时文本输出', () => {
|
|
56
|
+
it('无 worktree 时文本输出', async () => {
|
|
53
57
|
mockedGetProjectName.mockReturnValue('test-project');
|
|
54
58
|
mockedGetProjectWorktrees.mockReturnValue([]);
|
|
55
59
|
|
|
56
60
|
const program = new Command();
|
|
57
61
|
program.exitOverride();
|
|
58
62
|
registerListCommand(program);
|
|
59
|
-
program.
|
|
63
|
+
await program.parseAsync(['list'], { from: 'user' });
|
|
60
64
|
|
|
61
65
|
expect(mockedRunPreChecks).toHaveBeenCalled();
|
|
62
66
|
expect(mockedPrintInfo).toHaveBeenCalled();
|
|
63
67
|
});
|
|
64
68
|
|
|
65
|
-
it('有 worktree 时文本输出', () => {
|
|
69
|
+
it('有 worktree 时文本输出', async () => {
|
|
66
70
|
mockedGetProjectName.mockReturnValue('test-project');
|
|
67
71
|
mockedGetProjectWorktrees.mockReturnValue([
|
|
68
72
|
{ path: '/path/feature', branch: 'feature' },
|
|
@@ -74,12 +78,12 @@ describe('handleList', () => {
|
|
|
74
78
|
const program = new Command();
|
|
75
79
|
program.exitOverride();
|
|
76
80
|
registerListCommand(program);
|
|
77
|
-
program.
|
|
81
|
+
await program.parseAsync(['list'], { from: 'user' });
|
|
78
82
|
|
|
79
83
|
expect(mockedGetWorktreeStatus).toHaveBeenCalled();
|
|
80
84
|
});
|
|
81
85
|
|
|
82
|
-
it('--json 输出 JSON 格式', () => {
|
|
86
|
+
it('--json 输出 JSON 格式', async () => {
|
|
83
87
|
mockedGetProjectName.mockReturnValue('test-project');
|
|
84
88
|
mockedGetProjectWorktrees.mockReturnValue([
|
|
85
89
|
{ path: '/path/feature', branch: 'feature' },
|
|
@@ -89,7 +93,7 @@ describe('handleList', () => {
|
|
|
89
93
|
const program = new Command();
|
|
90
94
|
program.exitOverride();
|
|
91
95
|
registerListCommand(program);
|
|
92
|
-
program.
|
|
96
|
+
await program.parseAsync(['list', '--json'], { from: 'user' });
|
|
93
97
|
|
|
94
98
|
// 应通过 console.log 输出 JSON
|
|
95
99
|
const jsonCall = consoleSpy.mock.calls.find((call) => {
|
|
@@ -101,7 +105,7 @@ describe('handleList', () => {
|
|
|
101
105
|
expect(parsed.total).toBe(1);
|
|
102
106
|
});
|
|
103
107
|
|
|
104
|
-
it('worktree 状态不可用时显示提示', () => {
|
|
108
|
+
it('worktree 状态不可用时显示提示', async () => {
|
|
105
109
|
mockedGetProjectName.mockReturnValue('test-project');
|
|
106
110
|
mockedGetProjectWorktrees.mockReturnValue([
|
|
107
111
|
{ path: '/path/feature', branch: 'feature' },
|
|
@@ -111,7 +115,7 @@ describe('handleList', () => {
|
|
|
111
115
|
const program = new Command();
|
|
112
116
|
program.exitOverride();
|
|
113
117
|
registerListCommand(program);
|
|
114
|
-
program.
|
|
118
|
+
await program.parseAsync(['list'], { from: 'user' });
|
|
115
119
|
|
|
116
120
|
expect(mockedGetWorktreeStatus).toHaveBeenCalled();
|
|
117
121
|
});
|
|
@@ -15,29 +15,33 @@ vi.mock('../../../src/errors/index.js', () => ({
|
|
|
15
15
|
},
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
|
-
vi.mock('../../../src/constants/index.js', () =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
18
|
+
vi.mock('../../../src/constants/index.js', async (importOriginal) => {
|
|
19
|
+
const actual = await importOriginal<typeof import('../../../src/constants/index.js')>();
|
|
20
|
+
return {
|
|
21
|
+
...actual,
|
|
22
|
+
MESSAGES: {
|
|
23
|
+
MERGE_NO_WORKTREES: '没有可用的 worktree',
|
|
24
|
+
MERGE_SELECT_BRANCH: '选择要合并的分支',
|
|
25
|
+
MERGE_MULTIPLE_MATCHES: (keyword: string) => `找到多个匹配 "${keyword}" 的分支`,
|
|
26
|
+
MERGE_NO_MATCH: (keyword: string, branches: string[]) => `未找到匹配 "${keyword}" 的分支`,
|
|
27
|
+
MERGE_SQUASH_PROMPT: '是否压缩提交?',
|
|
28
|
+
MERGE_SQUASH_COMMITTED: (branch: string) => `已压缩提交: ${branch}`,
|
|
29
|
+
MERGE_SQUASH_PENDING: (path: string, branch: string) => `请手动提交: ${path}`,
|
|
30
|
+
MERGE_VALIDATE_STATE_HINT: (branch: string) => `分支 ${branch} 存在 validate 状态`,
|
|
31
|
+
MAIN_WORKTREE_DIRTY: '主 worktree 有未提交的更改',
|
|
32
|
+
TARGET_WORKTREE_DIRTY_NO_MESSAGE: (worktreePath: string) =>
|
|
33
|
+
`${worktreePath} 有未提交修改,请提供 -m 参数`,
|
|
34
|
+
TARGET_WORKTREE_NO_CHANGES: '没有可合并的变更',
|
|
35
|
+
MERGE_CONFLICT: '合并冲突',
|
|
36
|
+
PULL_CONFLICT: 'pull 冲突',
|
|
37
|
+
PUSH_FAILED: 'push 失败',
|
|
38
|
+
MERGE_SUCCESS: (branch: string, message: string, autoPullPush: boolean) => `合并成功: ${branch}`,
|
|
39
|
+
MERGE_SUCCESS_NO_MESSAGE: (branch: string, autoPullPush: boolean) => `合并成功: ${branch}`,
|
|
40
|
+
WORKTREE_CLEANED: (branch: string) => `已清理: ${branch}`,
|
|
41
|
+
},
|
|
42
|
+
AUTO_SAVE_COMMIT_MESSAGE: 'clawt:auto-save',
|
|
43
|
+
};
|
|
44
|
+
});
|
|
41
45
|
|
|
42
46
|
vi.mock('../../../src/utils/index.js', () => ({
|
|
43
47
|
runPreChecks: vi.fn(),
|
|
@@ -5,16 +5,20 @@ vi.mock('../../../src/logger/index.js', () => ({
|
|
|
5
5
|
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
6
6
|
}));
|
|
7
7
|
|
|
8
|
-
vi.mock('../../../src/constants/index.js', () =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
8
|
+
vi.mock('../../../src/constants/index.js', async (importOriginal) => {
|
|
9
|
+
const actual = await importOriginal<typeof import('../../../src/constants/index.js')>();
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
MESSAGES: {
|
|
13
|
+
RESUME_NO_WORKTREES: '没有可用的 worktree',
|
|
14
|
+
RESUME_SELECT_BRANCH: '选择要恢复的分支',
|
|
15
|
+
RESUME_MULTIPLE_MATCHES: (keyword: string) => `找到多个匹配 "${keyword}" 的分支`,
|
|
16
|
+
RESUME_NO_MATCH: (keyword: string, branches: string[]) => `未找到匹配 "${keyword}" 的分支`,
|
|
17
|
+
RESUME_ALL_CONFIRM: (count: number) => `确认恢复 ${count} 个分支?`,
|
|
18
|
+
RESUME_ALL_SUCCESS: (count: number) => `已恢复 ${count} 个分支`,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
});
|
|
18
22
|
|
|
19
23
|
vi.mock('../../../src/utils/index.js', () => ({
|
|
20
24
|
runPreChecks: vi.fn(),
|
|
@@ -91,7 +95,6 @@ describe('handleResume', () => {
|
|
|
91
95
|
await program.parseAsync(['resume', '-b', 'feature'], { from: 'user' });
|
|
92
96
|
|
|
93
97
|
expect(mockedRunPreChecks).toHaveBeenCalled();
|
|
94
|
-
expect(mockedValidateClaudeCodeInstalled).toHaveBeenCalled();
|
|
95
98
|
expect(mockedResolveTargetWorktrees).toHaveBeenCalled();
|
|
96
99
|
expect(mockedPromptGroupedMultiSelectBranches).not.toHaveBeenCalled();
|
|
97
100
|
expect(mockedLaunchInteractiveClaude).toHaveBeenCalledWith(worktree, { autoContinue: true });
|