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.
Files changed (40) hide show
  1. package/README.md +22 -13
  2. package/dist/index.js +141 -56
  3. package/dist/postinstall.js +15 -2
  4. package/package.json +1 -1
  5. package/src/commands/cover-validate.ts +2 -4
  6. package/src/commands/create.ts +5 -2
  7. package/src/commands/home.ts +3 -4
  8. package/src/commands/init.ts +7 -3
  9. package/src/commands/list.ts +2 -2
  10. package/src/commands/merge.ts +7 -2
  11. package/src/commands/remove.ts +2 -4
  12. package/src/commands/reset.ts +2 -4
  13. package/src/commands/resume.ts +2 -2
  14. package/src/commands/run.ts +4 -2
  15. package/src/commands/status.ts +24 -3
  16. package/src/commands/sync.ts +4 -4
  17. package/src/commands/validate.ts +3 -7
  18. package/src/constants/interactive-panel.ts +2 -2
  19. package/src/constants/messages/common.ts +8 -0
  20. package/src/constants/messages/index.ts +2 -2
  21. package/src/constants/messages/interactive-panel.ts +27 -0
  22. package/src/constants/messages/status.ts +9 -0
  23. package/src/types/status.ts +4 -0
  24. package/src/utils/index.ts +2 -2
  25. package/src/utils/interactive-panel-render.ts +28 -2
  26. package/src/utils/project-config.ts +39 -2
  27. package/src/utils/validation.ts +50 -0
  28. package/tests/unit/commands/cover-validate.test.ts +3 -1
  29. package/tests/unit/commands/create.test.ts +7 -5
  30. package/tests/unit/commands/init.test.ts +4 -1
  31. package/tests/unit/commands/list.test.ts +5 -5
  32. package/tests/unit/commands/merge.test.ts +3 -1
  33. package/tests/unit/commands/remove.test.ts +5 -3
  34. package/tests/unit/commands/reset.test.ts +3 -1
  35. package/tests/unit/commands/resume.test.ts +5 -5
  36. package/tests/unit/commands/run.test.ts +3 -1
  37. package/tests/unit/commands/status.test.ts +7 -1
  38. package/tests/unit/commands/sync.test.ts +3 -1
  39. package/tests/unit/commands/validate.test.ts +3 -1
  40. package/tests/unit/utils/validation.test.ts +43 -1
@@ -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
- validateMainWorktree,
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
- validateMainWorktree();
60
- requireProjectConfig();
58
+ runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
61
59
 
62
60
  const projectName = getProjectName();
63
61
  logger.info(`remove 命令执行,项目: ${projectName}`);
@@ -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
- validateMainWorktree,
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
- validateMainWorktree();
35
- requireProjectConfig();
33
+ runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
36
34
 
37
35
  const mainWorktreePath = getGitTopLevel();
38
36
  logger.info('reset 命令执行');
@@ -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
- validateMainWorktree,
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
- validateMainWorktree();
50
+ runPreChecks({ mainWorktree: true, headExists: true });
51
51
  validateClaudeCodeInstalled();
52
52
 
53
53
  logger.info(`resume 命令执行,分支过滤: ${options.branch ?? '(无)'}`);
@@ -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
- validateMainWorktree,
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
- validateMainWorktree();
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
 
@@ -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
- validateMainWorktree,
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
- validateMainWorktree();
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
 
@@ -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
- validateMainWorktree,
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
- validateMainWorktree();
131
- requireProjectConfig();
130
+ runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
131
+ await guardMainWorkBranch();
132
132
 
133
133
  logger.info(`sync 命令执行,分支: ${options.branch ?? '(未指定)'}`);
134
134
 
@@ -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
- validateMainWorktree,
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
- validateMainWorktree();
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
- validateMainWorktree();
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 = 4;
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;
@@ -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 快照信息 */
@@ -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) + 快照摘要 + 顶部分隔线 + 底部分隔线 + 底栏(后四项 = PANEL_FIXED_ROWS)
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
@@ -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
- validateMainWorktree: vi.fn(),
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
- validateMainWorktree: vi.fn(),
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 { validateMainWorktree, createWorktrees, printSuccess } from '../../../src/utils/index.js';
41
+ import { runPreChecks, createWorktrees, printSuccess } from '../../../src/utils/index.js';
40
42
 
41
- const mockedValidateMainWorktree = vi.mocked(validateMainWorktree);
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
- mockedValidateMainWorktree.mockReset();
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(mockedValidateMainWorktree).toHaveBeenCalled();
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
- validateMainWorktree: vi.fn(),
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';