clawt 3.4.1 → 3.4.3
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/README.md +22 -13
- package/dist/index.js +141 -56
- package/dist/postinstall.js +15 -2
- package/package.json +1 -1
- package/src/commands/cover-validate.ts +2 -4
- package/src/commands/create.ts +5 -2
- package/src/commands/home.ts +3 -4
- package/src/commands/init.ts +7 -3
- package/src/commands/list.ts +2 -2
- package/src/commands/merge.ts +7 -2
- package/src/commands/remove.ts +2 -4
- package/src/commands/reset.ts +2 -4
- package/src/commands/resume.ts +2 -2
- package/src/commands/run.ts +4 -2
- package/src/commands/status.ts +24 -3
- package/src/commands/sync.ts +4 -4
- package/src/commands/validate.ts +3 -7
- package/src/constants/interactive-panel.ts +2 -2
- package/src/constants/messages/common.ts +8 -0
- package/src/constants/messages/index.ts +2 -2
- package/src/constants/messages/interactive-panel.ts +27 -0
- package/src/constants/messages/status.ts +9 -0
- package/src/types/status.ts +4 -0
- package/src/utils/index.ts +2 -2
- package/src/utils/interactive-panel-render.ts +28 -2
- package/src/utils/project-config.ts +39 -2
- package/src/utils/validation.ts +50 -0
- package/tests/unit/commands/cover-validate.test.ts +3 -1
- package/tests/unit/commands/create.test.ts +7 -5
- package/tests/unit/commands/init.test.ts +4 -1
- package/tests/unit/commands/list.test.ts +5 -5
- package/tests/unit/commands/merge.test.ts +3 -1
- package/tests/unit/commands/remove.test.ts +5 -3
- package/tests/unit/commands/reset.test.ts +3 -1
- package/tests/unit/commands/resume.test.ts +5 -5
- package/tests/unit/commands/run.test.ts +3 -1
- package/tests/unit/commands/status.test.ts +7 -1
- package/tests/unit/commands/sync.test.ts +3 -1
- package/tests/unit/commands/validate.test.ts +3 -1
- package/tests/unit/utils/validation.test.ts +43 -1
package/src/commands/remove.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { ClawtError } from '../errors/index.js';
|
|
|
4
4
|
import { MESSAGES } from '../constants/index.js';
|
|
5
5
|
import type { RemoveOptions } from '../types/index.js';
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
runPreChecks,
|
|
8
8
|
getProjectName,
|
|
9
9
|
getProjectWorktreeDir,
|
|
10
10
|
getProjectWorktrees,
|
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
resolveTargetWorktrees,
|
|
24
24
|
getValidateBranchName,
|
|
25
25
|
deleteValidateBranch,
|
|
26
|
-
requireProjectConfig,
|
|
27
26
|
getCurrentBranch,
|
|
28
27
|
} from '../utils/index.js';
|
|
29
28
|
import type { WorktreeMultiResolveMessages } from '../utils/index.js';
|
|
@@ -56,8 +55,7 @@ export function registerRemoveCommand(program: Command): void {
|
|
|
56
55
|
* @param {RemoveOptions} options - 命令选项
|
|
57
56
|
*/
|
|
58
57
|
async function handleRemove(options: RemoveOptions): Promise<void> {
|
|
59
|
-
|
|
60
|
-
requireProjectConfig();
|
|
58
|
+
runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
|
|
61
59
|
|
|
62
60
|
const projectName = getProjectName();
|
|
63
61
|
logger.info(`remove 命令执行,项目: ${projectName}`);
|
package/src/commands/reset.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Command } from 'commander';
|
|
|
2
2
|
import { logger } from '../logger/index.js';
|
|
3
3
|
import { MESSAGES } from '../constants/index.js';
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
runPreChecks,
|
|
6
6
|
getGitTopLevel,
|
|
7
7
|
getConfigValue,
|
|
8
8
|
isWorkingDirClean,
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
confirmDestructiveAction,
|
|
12
12
|
printSuccess,
|
|
13
13
|
printInfo,
|
|
14
|
-
requireProjectConfig,
|
|
15
14
|
} from '../utils/index.js';
|
|
16
15
|
|
|
17
16
|
/**
|
|
@@ -31,8 +30,7 @@ export function registerResetCommand(program: Command): void {
|
|
|
31
30
|
* 执行 reset 命令:重置主 worktree 工作区和暂存区
|
|
32
31
|
*/
|
|
33
32
|
async function handleReset(): Promise<void> {
|
|
34
|
-
|
|
35
|
-
requireProjectConfig();
|
|
33
|
+
runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
|
|
36
34
|
|
|
37
35
|
const mainWorktreePath = getGitTopLevel();
|
|
38
36
|
logger.info('reset 命令执行');
|
package/src/commands/resume.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { MESSAGES } from '../constants/index.js';
|
|
|
4
4
|
import type { ResumeOptions } from '../types/index.js';
|
|
5
5
|
import type { WorktreeInfo } from '../types/index.js';
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
runPreChecks,
|
|
8
8
|
validateClaudeCodeInstalled,
|
|
9
9
|
getProjectWorktrees,
|
|
10
10
|
launchInteractiveClaude,
|
|
@@ -47,7 +47,7 @@ export function registerResumeCommand(program: Command): void {
|
|
|
47
47
|
* @param {ResumeOptions} options - 命令选项
|
|
48
48
|
*/
|
|
49
49
|
async function handleResume(options: ResumeOptions): Promise<void> {
|
|
50
|
-
|
|
50
|
+
runPreChecks({ mainWorktree: true, headExists: true });
|
|
51
51
|
validateClaudeCodeInstalled();
|
|
52
52
|
|
|
53
53
|
logger.info(`resume 命令执行,分支过滤: ${options.branch ?? '(无)'}`);
|
package/src/commands/run.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { ClawtError } from '../errors/index.js';
|
|
|
4
4
|
import { MESSAGES } from '../constants/index.js';
|
|
5
5
|
import type { RunOptions, WorktreeInfo } from '../types/index.js';
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
runPreChecks,
|
|
8
8
|
validateClaudeCodeInstalled,
|
|
9
9
|
createWorktrees,
|
|
10
10
|
createWorktreesByBranches,
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
executeBatchTasks,
|
|
21
21
|
printDryRunPreview,
|
|
22
22
|
ensureOnMainWorkBranch,
|
|
23
|
+
guardMainWorkBranch,
|
|
23
24
|
} from '../utils/index.js';
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -119,10 +120,11 @@ function handleDryRunFromFile(options: RunOptions): void {
|
|
|
119
120
|
* @param {RunOptions} options - 命令选项
|
|
120
121
|
*/
|
|
121
122
|
async function handleRun(options: RunOptions): Promise<void> {
|
|
122
|
-
|
|
123
|
+
runPreChecks({ mainWorktree: true, headExists: true });
|
|
123
124
|
|
|
124
125
|
// dry-run 模式跳过项目配置前置校验
|
|
125
126
|
if (!options.dryRun) {
|
|
127
|
+
await guardMainWorkBranch();
|
|
126
128
|
await ensureOnMainWorkBranch();
|
|
127
129
|
}
|
|
128
130
|
|
package/src/commands/status.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { MESSAGES } from '../constants/index.js';
|
|
3
|
+
import { MESSAGES, VALIDATE_BRANCH_PREFIX } from '../constants/index.js';
|
|
4
4
|
import { logger } from '../logger/index.js';
|
|
5
5
|
import type { StatusOptions, WorktreeDetailedStatus, MainWorktreeStatus, SnapshotSummary, StatusResult, WorktreeInfo } from '../types/index.js';
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
runPreChecks,
|
|
8
8
|
getProjectName,
|
|
9
9
|
getCurrentBranch,
|
|
10
10
|
isWorkingDirClean,
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
printDoubleSeparator,
|
|
23
23
|
printSeparator,
|
|
24
24
|
InteractivePanel,
|
|
25
|
+
loadProjectConfig,
|
|
26
|
+
checkBranchExists,
|
|
25
27
|
} from '../utils/index.js';
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -44,7 +46,7 @@ export function registerStatusCommand(program: Command): void {
|
|
|
44
46
|
* @param {StatusOptions} options - 命令选项
|
|
45
47
|
*/
|
|
46
48
|
async function handleStatus(options: StatusOptions): Promise<void> {
|
|
47
|
-
|
|
49
|
+
runPreChecks({ mainWorktree: true, headExists: true });
|
|
48
50
|
|
|
49
51
|
// 交互式面板模式
|
|
50
52
|
if (options.interactive) {
|
|
@@ -74,11 +76,18 @@ export function collectStatus(): StatusResult {
|
|
|
74
76
|
const currentBranch = getCurrentBranch();
|
|
75
77
|
const isClean = isWorkingDirClean();
|
|
76
78
|
|
|
79
|
+
// 配置分支信息收集
|
|
80
|
+
const projectConfig = loadProjectConfig();
|
|
81
|
+
const configuredMainBranch = projectConfig?.clawtMainWorkBranch || null;
|
|
82
|
+
const configuredBranchExists = configuredMainBranch ? checkBranchExists(configuredMainBranch) : null;
|
|
83
|
+
|
|
77
84
|
// 主 worktree 状态
|
|
78
85
|
const main: MainWorktreeStatus = {
|
|
79
86
|
branch: currentBranch,
|
|
80
87
|
isClean,
|
|
81
88
|
projectName,
|
|
89
|
+
configuredMainBranch,
|
|
90
|
+
configuredBranchExists,
|
|
82
91
|
};
|
|
83
92
|
|
|
84
93
|
// 各 worktree 详细状态
|
|
@@ -265,6 +274,18 @@ function printMainSection(main: MainWorktreeStatus): void {
|
|
|
265
274
|
} else {
|
|
266
275
|
printInfo(` 状态: ${chalk.yellow('✗ 有未提交修改')}`);
|
|
267
276
|
}
|
|
277
|
+
|
|
278
|
+
// 配置分支信息展示
|
|
279
|
+
if (main.configuredMainBranch !== null) {
|
|
280
|
+
if (main.configuredBranchExists === false) {
|
|
281
|
+
printInfo(` ${chalk.red(MESSAGES.STATUS_CONFIGURED_BRANCH_DELETED(main.configuredMainBranch))}`);
|
|
282
|
+
} else if (main.branch !== main.configuredMainBranch && !main.branch.startsWith(VALIDATE_BRANCH_PREFIX)) {
|
|
283
|
+
printInfo(` ${chalk.yellow(MESSAGES.STATUS_CONFIGURED_BRANCH_MISMATCH(main.configuredMainBranch))}`);
|
|
284
|
+
} else {
|
|
285
|
+
printInfo(` ${chalk.gray(MESSAGES.STATUS_CONFIGURED_BRANCH(main.configuredMainBranch))}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
268
289
|
printInfo('');
|
|
269
290
|
}
|
|
270
291
|
|
package/src/commands/sync.ts
CHANGED
|
@@ -4,7 +4,7 @@ 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
6
|
import {
|
|
7
|
-
|
|
7
|
+
runPreChecks,
|
|
8
8
|
getGitTopLevel,
|
|
9
9
|
getProjectWorktrees,
|
|
10
10
|
isWorkingDirClean,
|
|
@@ -16,10 +16,10 @@ import {
|
|
|
16
16
|
printInfo,
|
|
17
17
|
printWarning,
|
|
18
18
|
resolveTargetWorktree,
|
|
19
|
-
requireProjectConfig,
|
|
20
19
|
getMainWorkBranch,
|
|
21
20
|
rebuildValidateBranch,
|
|
22
21
|
getValidateBranchName,
|
|
22
|
+
guardMainWorkBranch,
|
|
23
23
|
} from '../utils/index.js';
|
|
24
24
|
import type { WorktreeResolveMessages } from '../utils/index.js';
|
|
25
25
|
|
|
@@ -127,8 +127,8 @@ 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
|
-
|
|
131
|
-
|
|
130
|
+
runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
|
|
131
|
+
await guardMainWorkBranch();
|
|
132
132
|
|
|
133
133
|
logger.info(`sync 命令执行,分支: ${options.branch ?? '(未指定)'}`);
|
|
134
134
|
|
package/src/commands/validate.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { MESSAGES } from '../constants/index.js';
|
|
|
4
4
|
import type { ValidateOptions } from '../types/index.js';
|
|
5
5
|
import { executeSyncForBranch } from './sync.js';
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
runPreChecks,
|
|
8
8
|
getProjectName,
|
|
9
9
|
getGitTopLevel,
|
|
10
10
|
getProjectWorktrees,
|
|
@@ -25,7 +25,6 @@ import {
|
|
|
25
25
|
printWarning,
|
|
26
26
|
printInfo,
|
|
27
27
|
resolveTargetWorktree,
|
|
28
|
-
requireProjectConfig,
|
|
29
28
|
ensureOnMainWorkBranch,
|
|
30
29
|
handleDirtyWorkingDir,
|
|
31
30
|
getValidateRunCommand,
|
|
@@ -98,9 +97,7 @@ async function handlePatchApplyFailure(targetWorktreePath: string, branchName: s
|
|
|
98
97
|
* @param {ValidateOptions} options - 命令选项
|
|
99
98
|
*/
|
|
100
99
|
async function handleValidateClean(options: ValidateOptions): Promise<void> {
|
|
101
|
-
|
|
102
|
-
// 显式前置校验:确保项目已初始化
|
|
103
|
-
requireProjectConfig();
|
|
100
|
+
runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
|
|
104
101
|
|
|
105
102
|
const projectName = getProjectName();
|
|
106
103
|
const mainWorktreePath = getGitTopLevel();
|
|
@@ -264,8 +261,7 @@ async function handleValidate(options: ValidateOptions): Promise<void> {
|
|
|
264
261
|
return;
|
|
265
262
|
}
|
|
266
263
|
|
|
267
|
-
|
|
268
|
-
requireProjectConfig();
|
|
264
|
+
runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
|
|
269
265
|
|
|
270
266
|
const projectName = getProjectName();
|
|
271
267
|
const mainWorktreePath = getGitTopLevel();
|
|
@@ -40,5 +40,5 @@ export const PANEL_SHORTCUT_KEYS = {
|
|
|
40
40
|
/** 日期分隔线前缀 */
|
|
41
41
|
export const PANEL_DATE_SEPARATOR_PREFIX = '════';
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
export const PANEL_FIXED_ROWS =
|
|
43
|
+
/** 固定占用行数(配置分支信息 + 快照摘要 + 顶部分隔线 + 底部分隔线 + 底栏) */
|
|
44
|
+
export const PANEL_FIXED_ROWS = 5;
|
|
@@ -6,6 +6,8 @@ export const COMMON_MESSAGES = {
|
|
|
6
6
|
GIT_NOT_INSTALLED: 'Git 未安装或不在 PATH 中,请先安装 Git',
|
|
7
7
|
/** Claude Code CLI 未安装 */
|
|
8
8
|
CLAUDE_NOT_INSTALLED: 'Claude Code CLI 未安装,请先安装:npm install -g @anthropic-ai/claude-code',
|
|
9
|
+
/** HEAD 不存在(仓库无任何 commit) */
|
|
10
|
+
HEAD_NOT_FOUND: '当前仓库尚未创建任何提交,请先执行 git commit 创建首次提交后再使用 clawt',
|
|
9
11
|
/** 分支已存在 */
|
|
10
12
|
BRANCH_EXISTS: (name: string) => `分支 ${name} 已存在,无法创建`,
|
|
11
13
|
/** 分支名清理后为空 */
|
|
@@ -38,4 +40,10 @@ export const COMMON_MESSAGES = {
|
|
|
38
40
|
SEPARATOR: '────────────────────────────────────────',
|
|
39
41
|
/** 粗分隔线 */
|
|
40
42
|
DOUBLE_SEPARATOR: '════════════════════════════════════════',
|
|
43
|
+
/** 守卫检测:配置的主工作分支已不存在 */
|
|
44
|
+
GUARD_BRANCH_NOT_EXISTS: (branchName: string) =>
|
|
45
|
+
`配置的主工作分支 ${branchName} 已不存在,请执行 clawt init 重新设置主工作分支`,
|
|
46
|
+
/** 守卫检测:当前分支与配置的主工作分支不一致 */
|
|
47
|
+
GUARD_BRANCH_MISMATCH: (configuredBranch: string, currentBranch: string) =>
|
|
48
|
+
`当前分支 ${currentBranch} 与配置的主工作分支 ${configuredBranch} 不一致,如需更新请执行 clawt init`,
|
|
41
49
|
} as const;
|
|
@@ -16,11 +16,11 @@ import { UPDATE_MESSAGES, UPDATE_COMMANDS } from './update.js';
|
|
|
16
16
|
import { INIT_MESSAGES } from './init.js';
|
|
17
17
|
import { COVER_VALIDATE_MESSAGES } from './cover-validate.js';
|
|
18
18
|
import { HOME_MESSAGES } from './home.js';
|
|
19
|
-
import { PANEL_FOOTER_SHORTCUTS, PANEL_FOOTER_COUNTDOWN, PANEL_OVERFLOW_DOWN_HINT, PANEL_OVERFLOW_UP_HINT, PANEL_SNAPSHOT_SUMMARY, PANEL_NO_WORKTREES as PANEL_NO_WORKTREES_MSG, PANEL_PRESS_ENTER_TO_RETURN, PANEL_NOT_TTY, PANEL_TITLE } from './interactive-panel.js';
|
|
19
|
+
import { PANEL_FOOTER_SHORTCUTS, PANEL_FOOTER_COUNTDOWN, PANEL_OVERFLOW_DOWN_HINT, PANEL_OVERFLOW_UP_HINT, PANEL_SNAPSHOT_SUMMARY, PANEL_NO_WORKTREES as PANEL_NO_WORKTREES_MSG, PANEL_PRESS_ENTER_TO_RETURN, PANEL_NOT_TTY, PANEL_TITLE, PANEL_CONFIGURED_BRANCH, PANEL_CONFIGURED_BRANCH_DELETED, PANEL_CONFIGURED_BRANCH_MISMATCH, PANEL_NOT_INITIALIZED } from './interactive-panel.js';
|
|
20
20
|
|
|
21
21
|
export { CONFIG_ALIAS_DISABLED_HINT };
|
|
22
22
|
export { UPDATE_MESSAGES, UPDATE_COMMANDS };
|
|
23
|
-
export { PANEL_FOOTER_SHORTCUTS, PANEL_FOOTER_COUNTDOWN, PANEL_OVERFLOW_DOWN_HINT, PANEL_OVERFLOW_UP_HINT, PANEL_SNAPSHOT_SUMMARY, PANEL_NO_WORKTREES_MSG, PANEL_PRESS_ENTER_TO_RETURN, PANEL_NOT_TTY, PANEL_TITLE };
|
|
23
|
+
export { PANEL_FOOTER_SHORTCUTS, PANEL_FOOTER_COUNTDOWN, PANEL_OVERFLOW_DOWN_HINT, PANEL_OVERFLOW_UP_HINT, PANEL_SNAPSHOT_SUMMARY, PANEL_NO_WORKTREES_MSG, PANEL_PRESS_ENTER_TO_RETURN, PANEL_NOT_TTY, PANEL_TITLE, PANEL_CONFIGURED_BRANCH, PANEL_CONFIGURED_BRANCH_DELETED, PANEL_CONFIGURED_BRANCH_MISMATCH, PANEL_NOT_INITIALIZED };
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* 提示消息模板
|
|
@@ -59,3 +59,30 @@ export const PANEL_NOT_TTY = '交互式面板需要 TTY 终端环境,请直接
|
|
|
59
59
|
* @returns {string} 格式化的标题
|
|
60
60
|
*/
|
|
61
61
|
export const PANEL_TITLE = (projectName: string): string => chalk.bold.cyan(`项目状态总览: ${projectName}`);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 面板配置分支信息(正常)
|
|
65
|
+
* @param {string} branchName - 分支名
|
|
66
|
+
* @returns {string} 格式化的分支信息
|
|
67
|
+
*/
|
|
68
|
+
export const PANEL_CONFIGURED_BRANCH = (branchName: string): string =>
|
|
69
|
+
chalk.gray(`主工作分支: ${branchName}`);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 面板配置分支信息(分支已删除)
|
|
73
|
+
* @param {string} branchName - 分支名
|
|
74
|
+
* @returns {string} 格式化的分支信息
|
|
75
|
+
*/
|
|
76
|
+
export const PANEL_CONFIGURED_BRANCH_DELETED = (branchName: string): string =>
|
|
77
|
+
chalk.red(`✗ 主工作分支: ${branchName}(已不存在)`);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 面板配置分支信息(分支不一致)
|
|
81
|
+
* @param {string} branchName - 分支名
|
|
82
|
+
* @returns {string} 格式化的分支信息
|
|
83
|
+
*/
|
|
84
|
+
export const PANEL_CONFIGURED_BRANCH_MISMATCH = (branchName: string): string =>
|
|
85
|
+
chalk.yellow(`⚠ 主工作分支: ${branchName}(不一致)`);
|
|
86
|
+
|
|
87
|
+
/** 面板配置分支信息(未初始化) */
|
|
88
|
+
export const PANEL_NOT_INITIALIZED = chalk.gray('未初始化(执行 clawt init 设置主工作分支)');
|
|
@@ -30,4 +30,13 @@ export const STATUS_MESSAGES = {
|
|
|
30
30
|
STATUS_LAST_VALIDATED: (relativeTime: string) => `上次验证: ${relativeTime}`,
|
|
31
31
|
/** status 未验证警示 */
|
|
32
32
|
STATUS_NOT_VALIDATED: '✗ 未验证',
|
|
33
|
+
/** status 配置的主工作分支(正常状态) */
|
|
34
|
+
STATUS_CONFIGURED_BRANCH: (branchName: string) =>
|
|
35
|
+
`主工作分支: ${branchName}`,
|
|
36
|
+
/** status 配置的主工作分支已不存在 */
|
|
37
|
+
STATUS_CONFIGURED_BRANCH_DELETED: (branchName: string) =>
|
|
38
|
+
`✗ 主工作分支: ${branchName}(已不存在,请执行 clawt init 重新设置)`,
|
|
39
|
+
/** status 当前分支与配置的主工作分支不一致 */
|
|
40
|
+
STATUS_CONFIGURED_BRANCH_MISMATCH: (branchName: string) =>
|
|
41
|
+
`⚠ 主工作分支: ${branchName}(当前分支不一致,如需更新请执行 clawt init)`,
|
|
33
42
|
} as const;
|
package/src/types/status.ts
CHANGED
|
@@ -28,6 +28,10 @@ export interface MainWorktreeStatus {
|
|
|
28
28
|
isClean: boolean;
|
|
29
29
|
/** 项目名 */
|
|
30
30
|
projectName: string;
|
|
31
|
+
/** 配置的主工作分支名(项目未初始化时为 null) */
|
|
32
|
+
configuredMainBranch: string | null;
|
|
33
|
+
/** 配置的主工作分支是否存在(项目未初始化时为 null) */
|
|
34
|
+
configuredBranchExists: boolean | null;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
/** validate 快照信息 */
|
package/src/utils/index.ts
CHANGED
|
@@ -50,7 +50,7 @@ export {
|
|
|
50
50
|
createBranch,
|
|
51
51
|
} from './git.js';
|
|
52
52
|
export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
|
|
53
|
-
export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
|
|
53
|
+
export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled, validateHeadExists, runPreChecks } from './validation.js';
|
|
54
54
|
export { createWorktrees, getProjectWorktrees, getProjectWorktreeDir, cleanupWorktrees, getWorktreeStatus, createWorktreesByBranches } from './worktree.js';
|
|
55
55
|
export { loadConfig, writeDefaultConfig, writeConfig, saveConfig, getConfigValue, ensureClawtDirs, parseConcurrency } from './config.js';
|
|
56
56
|
export { printSuccess, printError, printWarning, printInfo, printHint, printSeparator, printDoubleSeparator, confirmAction, confirmDestructiveAction, formatWorktreeStatus, isWorktreeIdle, formatDuration, formatRelativeTime, formatDiskSize, formatLocalISOString } from './formatter.js';
|
|
@@ -70,7 +70,7 @@ export { truncateTaskDesc, printDryRunPreview } from './dry-run.js';
|
|
|
70
70
|
export { applyAliases } from './alias.js';
|
|
71
71
|
export { isValidConfigKey, getValidConfigKeys, parseConfigValue, promptConfigValue, formatConfigValue, interactiveConfigEditor } from './config-strategy.js';
|
|
72
72
|
export { checkForUpdates } from './update-checker.js';
|
|
73
|
-
export { getProjectConfigPath, loadProjectConfig, saveProjectConfig, requireProjectConfig, getMainWorkBranch, getValidateRunCommand } from './project-config.js';
|
|
73
|
+
export { getProjectConfigPath, loadProjectConfig, saveProjectConfig, requireProjectConfig, getMainWorkBranch, guardMainWorkBranchExists, guardMainWorkBranch, getValidateRunCommand } from './project-config.js';
|
|
74
74
|
export { getValidateBranchName, createValidateBranch, deleteValidateBranch, rebuildValidateBranch, ensureOnMainWorkBranch, handleDirtyWorkingDir } from './validate-branch.js';
|
|
75
75
|
export { safeStringify } from './json.js';
|
|
76
76
|
export { executeRunCommand } from './validate-runner.js';
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
PANEL_DATE_SEPARATOR_PREFIX,
|
|
8
8
|
PANEL_FIXED_ROWS,
|
|
9
9
|
UNKNOWN_DATE_GROUP,
|
|
10
|
+
VALIDATE_BRANCH_PREFIX,
|
|
10
11
|
} from '../constants/index.js';
|
|
11
12
|
import {
|
|
12
13
|
PANEL_FOOTER_SHORTCUTS,
|
|
@@ -16,8 +17,12 @@ import {
|
|
|
16
17
|
PANEL_SNAPSHOT_SUMMARY,
|
|
17
18
|
PANEL_NO_WORKTREES_MSG,
|
|
18
19
|
PANEL_TITLE,
|
|
20
|
+
PANEL_CONFIGURED_BRANCH,
|
|
21
|
+
PANEL_CONFIGURED_BRANCH_DELETED,
|
|
22
|
+
PANEL_CONFIGURED_BRANCH_MISMATCH,
|
|
23
|
+
PANEL_NOT_INITIALIZED,
|
|
19
24
|
} from '../constants/messages/index.js';
|
|
20
|
-
import type { StatusResult, WorktreeDetailedStatus } from '../types/index.js';
|
|
25
|
+
import type { StatusResult, WorktreeDetailedStatus, MainWorktreeStatus } from '../types/index.js';
|
|
21
26
|
import { formatRelativeTime, groupWorktreesByDate, formatRelativeDate } from './index.js';
|
|
22
27
|
|
|
23
28
|
/** 面板行类型 */
|
|
@@ -73,6 +78,9 @@ export function buildPanelFrame(
|
|
|
73
78
|
// 标题行
|
|
74
79
|
lines.push(PANEL_TITLE(statusResult.main.projectName));
|
|
75
80
|
|
|
81
|
+
// 配置分支信息行
|
|
82
|
+
lines.push(renderConfiguredBranchLine(statusResult.main));
|
|
83
|
+
|
|
76
84
|
// 快照摘要行
|
|
77
85
|
lines.push(renderSnapshotSummary(statusResult.snapshots.total, statusResult.snapshots.orphaned));
|
|
78
86
|
|
|
@@ -283,6 +291,24 @@ function formatChangeStatusLabel(status: WorktreeDetailedStatus['changeStatus'])
|
|
|
283
291
|
}
|
|
284
292
|
}
|
|
285
293
|
|
|
294
|
+
/**
|
|
295
|
+
* 渲染配置的主工作分支信息行(面板模式)
|
|
296
|
+
* @param {MainWorktreeStatus} main - 主 worktree 状态
|
|
297
|
+
* @returns {string} 格式化的配置分支信息
|
|
298
|
+
*/
|
|
299
|
+
function renderConfiguredBranchLine(main: MainWorktreeStatus): string {
|
|
300
|
+
if (main.configuredMainBranch === null) {
|
|
301
|
+
return PANEL_NOT_INITIALIZED;
|
|
302
|
+
}
|
|
303
|
+
if (main.configuredBranchExists === false) {
|
|
304
|
+
return PANEL_CONFIGURED_BRANCH_DELETED(main.configuredMainBranch);
|
|
305
|
+
}
|
|
306
|
+
if (main.branch !== main.configuredMainBranch && !main.branch.startsWith(VALIDATE_BRANCH_PREFIX)) {
|
|
307
|
+
return PANEL_CONFIGURED_BRANCH_MISMATCH(main.configuredMainBranch);
|
|
308
|
+
}
|
|
309
|
+
return PANEL_CONFIGURED_BRANCH(main.configuredMainBranch);
|
|
310
|
+
}
|
|
311
|
+
|
|
286
312
|
/**
|
|
287
313
|
* 渲染快照摘要行
|
|
288
314
|
* @param {number} total - 快照总数
|
|
@@ -309,7 +335,7 @@ export function renderFooter(countdown: number): string {
|
|
|
309
335
|
* @returns {number} 滚动区域总行数
|
|
310
336
|
*/
|
|
311
337
|
export function calculateVisibleRows(terminalRows: number): number {
|
|
312
|
-
// 固定行:标题(1) + 快照摘要 + 顶部分隔线 + 底部分隔线 +
|
|
338
|
+
// 固定行:标题(1) + 配置分支信息 + 快照摘要 + 顶部分隔线 + 底部分隔线 + 底栏(后五项 = PANEL_FIXED_ROWS)
|
|
313
339
|
const fixedRows = PANEL_FIXED_ROWS + 1;
|
|
314
340
|
return Math.max(terminalRows - fixedRows, 3);
|
|
315
341
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { PROJECTS_CONFIG_DIR, MESSAGES } from '../constants/index.js';
|
|
3
|
+
import { PROJECTS_CONFIG_DIR, MESSAGES, VALIDATE_BRANCH_PREFIX } from '../constants/index.js';
|
|
4
4
|
import { ClawtError } from '../errors/index.js';
|
|
5
5
|
import { logger } from '../logger/index.js';
|
|
6
|
-
import { getProjectName } from './git.js';
|
|
6
|
+
import { getProjectName, checkBranchExists, getCurrentBranch } from './git.js';
|
|
7
7
|
import { ensureDir } from './fs.js';
|
|
8
|
+
import { printWarning, confirmAction } from './formatter.js';
|
|
8
9
|
import type { ProjectConfig } from '../types/index.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -76,6 +77,42 @@ export function getMainWorkBranch(): string {
|
|
|
76
77
|
return config.clawtMainWorkBranch;
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
/**
|
|
81
|
+
* 守卫检测:仅验证配置中的主工作分支是否存在
|
|
82
|
+
* 分支不存在 → 抛出 ClawtError(致命错误)
|
|
83
|
+
* 适用于不需要分支一致性检测的场景(如 home 命令)
|
|
84
|
+
* @param {string} [cwd] - 工作目录
|
|
85
|
+
*/
|
|
86
|
+
export function guardMainWorkBranchExists(cwd?: string): void {
|
|
87
|
+
const config = requireProjectConfig();
|
|
88
|
+
const mainBranch = config.clawtMainWorkBranch;
|
|
89
|
+
|
|
90
|
+
if (!checkBranchExists(mainBranch, cwd)) {
|
|
91
|
+
throw new ClawtError(MESSAGES.GUARD_BRANCH_NOT_EXISTS(mainBranch));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
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
|
+
|
|
79
116
|
/**
|
|
80
117
|
* 从项目配置中获取 validate 成功后自动执行的命令
|
|
81
118
|
* @returns {string | undefined} 配置的命令字符串,未配置时返回 undefined
|
package/src/utils/validation.ts
CHANGED
|
@@ -2,6 +2,19 @@ import { MESSAGES } from '../constants/index.js';
|
|
|
2
2
|
import { ClawtError } from '../errors/index.js';
|
|
3
3
|
import { execCommand } from './shell.js';
|
|
4
4
|
import { getGitCommonDir } from './git.js';
|
|
5
|
+
import { requireProjectConfig, guardMainWorkBranchExists } from './project-config.js';
|
|
6
|
+
|
|
7
|
+
/** 统一前置校验选项 */
|
|
8
|
+
interface PreCheckOptions {
|
|
9
|
+
/** 校验是否在主 worktree 根目录 */
|
|
10
|
+
mainWorktree?: boolean;
|
|
11
|
+
/** 校验 HEAD 是否存在(仓库有至少一次 commit) */
|
|
12
|
+
headExists?: boolean;
|
|
13
|
+
/** 校验项目是否已初始化(配置文件存在) */
|
|
14
|
+
projectConfig?: boolean;
|
|
15
|
+
/** 校验配置的主工作分支是否存在 */
|
|
16
|
+
branchExists?: boolean;
|
|
17
|
+
}
|
|
5
18
|
|
|
6
19
|
/**
|
|
7
20
|
* 校验当前目录是否为主 worktree 的根目录
|
|
@@ -46,3 +59,40 @@ export function validateClaudeCodeInstalled(): void {
|
|
|
46
59
|
throw new ClawtError(MESSAGES.CLAUDE_NOT_INSTALLED);
|
|
47
60
|
}
|
|
48
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 校验 HEAD 是否存在(仓库是否有至少一次 commit)
|
|
65
|
+
* git init 后未做任何 commit 时,HEAD 不指向有效引用
|
|
66
|
+
* @throws {ClawtError} HEAD 不存在时抛出
|
|
67
|
+
*/
|
|
68
|
+
export function validateHeadExists(): void {
|
|
69
|
+
try {
|
|
70
|
+
execCommand('git rev-parse --verify HEAD');
|
|
71
|
+
} catch {
|
|
72
|
+
throw new ClawtError(MESSAGES.HEAD_NOT_FOUND);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 统一前置校验入口,按需执行各项校验
|
|
78
|
+
* @param {PreCheckOptions} options - 校验选项
|
|
79
|
+
* @param {boolean} [options.mainWorktree] - 校验是否在主 worktree 根目录
|
|
80
|
+
* @param {boolean} [options.headExists] - 校验 HEAD 是否存在
|
|
81
|
+
* @param {boolean} [options.projectConfig] - 校验项目是否已初始化
|
|
82
|
+
* @param {boolean} [options.branchExists] - 校验配置的主工作分支是否存在
|
|
83
|
+
* @throws {ClawtError} 任一校验未通过时抛出
|
|
84
|
+
*/
|
|
85
|
+
export function runPreChecks(options: PreCheckOptions): void {
|
|
86
|
+
if (options.mainWorktree) {
|
|
87
|
+
validateMainWorktree();
|
|
88
|
+
}
|
|
89
|
+
if (options.headExists) {
|
|
90
|
+
validateHeadExists();
|
|
91
|
+
}
|
|
92
|
+
if (options.projectConfig) {
|
|
93
|
+
requireProjectConfig();
|
|
94
|
+
}
|
|
95
|
+
if (options.branchExists) {
|
|
96
|
+
guardMainWorkBranchExists();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -29,7 +29,7 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
29
29
|
}));
|
|
30
30
|
|
|
31
31
|
vi.mock('../../../src/utils/index.js', () => ({
|
|
32
|
-
|
|
32
|
+
runPreChecks: vi.fn(),
|
|
33
33
|
requireProjectConfig: vi.fn(),
|
|
34
34
|
getProjectName: vi.fn().mockReturnValue('test-project'),
|
|
35
35
|
getGitTopLevel: vi.fn().mockReturnValue('/repo'),
|
|
@@ -48,6 +48,8 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
48
48
|
printInfo: vi.fn(),
|
|
49
49
|
isWorkingDirClean: vi.fn().mockReturnValue(false),
|
|
50
50
|
confirmAction: vi.fn().mockResolvedValue(true),
|
|
51
|
+
guardMainWorkBranch: vi.fn().mockResolvedValue(undefined),
|
|
52
|
+
guardMainWorkBranchExists: vi.fn(),
|
|
51
53
|
}));
|
|
52
54
|
|
|
53
55
|
import { registerCoverValidateCommand, extractTargetBranchName, findTargetWorktreePath, computeIncrementalPatch } from '../../../src/commands/cover-validate.js';
|
|
@@ -24,7 +24,7 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
24
24
|
}));
|
|
25
25
|
|
|
26
26
|
vi.mock('../../../src/utils/index.js', () => ({
|
|
27
|
-
|
|
27
|
+
runPreChecks: vi.fn(),
|
|
28
28
|
createWorktrees: vi.fn(),
|
|
29
29
|
getConfigValue: vi.fn().mockReturnValue(true),
|
|
30
30
|
requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
|
|
@@ -33,17 +33,19 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
33
33
|
printSuccess: vi.fn(),
|
|
34
34
|
printInfo: vi.fn(),
|
|
35
35
|
printSeparator: vi.fn(),
|
|
36
|
+
guardMainWorkBranch: vi.fn().mockResolvedValue(undefined),
|
|
37
|
+
guardMainWorkBranchExists: vi.fn(),
|
|
36
38
|
}));
|
|
37
39
|
|
|
38
40
|
import { registerCreateCommand } from '../../../src/commands/create.js';
|
|
39
|
-
import {
|
|
41
|
+
import { runPreChecks, createWorktrees, printSuccess } from '../../../src/utils/index.js';
|
|
40
42
|
|
|
41
|
-
const
|
|
43
|
+
const mockedRunPreChecks = vi.mocked(runPreChecks);
|
|
42
44
|
const mockedCreateWorktrees = vi.mocked(createWorktrees);
|
|
43
45
|
const mockedPrintSuccess = vi.mocked(printSuccess);
|
|
44
46
|
|
|
45
47
|
beforeEach(() => {
|
|
46
|
-
|
|
48
|
+
mockedRunPreChecks.mockReset();
|
|
47
49
|
mockedCreateWorktrees.mockReset();
|
|
48
50
|
mockedPrintSuccess.mockReset();
|
|
49
51
|
});
|
|
@@ -68,7 +70,7 @@ describe('handleCreate', () => {
|
|
|
68
70
|
registerCreateCommand(program);
|
|
69
71
|
await program.parseAsync(['create', '-b', 'feature'], { from: 'user' });
|
|
70
72
|
|
|
71
|
-
expect(
|
|
73
|
+
expect(mockedRunPreChecks).toHaveBeenCalled();
|
|
72
74
|
expect(mockedCreateWorktrees).toHaveBeenCalledWith('feature', 1);
|
|
73
75
|
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
74
76
|
});
|
|
@@ -22,7 +22,8 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
22
22
|
}));
|
|
23
23
|
|
|
24
24
|
vi.mock('../../../src/utils/index.js', () => ({
|
|
25
|
-
|
|
25
|
+
runPreChecks: vi.fn(),
|
|
26
|
+
validateHeadExists: vi.fn(),
|
|
26
27
|
getCurrentBranch: vi.fn().mockReturnValue('main'),
|
|
27
28
|
loadProjectConfig: vi.fn(),
|
|
28
29
|
saveProjectConfig: vi.fn(),
|
|
@@ -31,6 +32,8 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
31
32
|
printInfo: vi.fn(),
|
|
32
33
|
safeStringify: vi.fn((value: unknown, indent: number = 2) => JSON.stringify(value, null, indent)),
|
|
33
34
|
interactiveConfigEditor: vi.fn(),
|
|
35
|
+
guardMainWorkBranch: vi.fn().mockResolvedValue(undefined),
|
|
36
|
+
guardMainWorkBranchExists: vi.fn(),
|
|
34
37
|
}));
|
|
35
38
|
|
|
36
39
|
import { registerInitCommand } from '../../../src/commands/init.js';
|