clawt 3.4.2 → 3.4.4

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 CHANGED
@@ -30,6 +30,8 @@ var COMMON_MESSAGES = {
30
30
  GIT_NOT_INSTALLED: "Git \u672A\u5B89\u88C5\u6216\u4E0D\u5728 PATH \u4E2D\uFF0C\u8BF7\u5148\u5B89\u88C5 Git",
31
31
  /** Claude Code CLI 未安装 */
32
32
  CLAUDE_NOT_INSTALLED: "Claude Code CLI \u672A\u5B89\u88C5\uFF0C\u8BF7\u5148\u5B89\u88C5\uFF1Anpm install -g @anthropic-ai/claude-code",
33
+ /** HEAD 不存在(仓库无任何 commit) */
34
+ HEAD_NOT_FOUND: "\u5F53\u524D\u4ED3\u5E93\u5C1A\u672A\u521B\u5EFA\u4EFB\u4F55\u63D0\u4EA4\uFF0C\u8BF7\u5148\u6267\u884C git commit \u521B\u5EFA\u9996\u6B21\u63D0\u4EA4\u540E\u518D\u4F7F\u7528 clawt",
33
35
  /** 分支已存在 */
34
36
  BRANCH_EXISTS: (name) => `\u5206\u652F ${name} \u5DF2\u5B58\u5728\uFF0C\u65E0\u6CD5\u521B\u5EFA`,
35
37
  /** 分支名清理后为空 */
@@ -1229,31 +1231,9 @@ function validateBranchesNotExist(branchNames) {
1229
1231
  }
1230
1232
  }
1231
1233
 
1232
- // src/utils/validation.ts
1233
- function validateMainWorktree() {
1234
- try {
1235
- const gitCommonDir = getGitCommonDir();
1236
- if (gitCommonDir !== ".git") {
1237
- throw new ClawtError(MESSAGES.NOT_MAIN_WORKTREE);
1238
- }
1239
- } catch (error) {
1240
- if (error instanceof ClawtError) {
1241
- throw error;
1242
- }
1243
- throw new ClawtError(MESSAGES.NOT_MAIN_WORKTREE);
1244
- }
1245
- }
1246
- function validateClaudeCodeInstalled() {
1247
- try {
1248
- execCommand("claude --version");
1249
- } catch {
1250
- throw new ClawtError(MESSAGES.CLAUDE_NOT_INSTALLED);
1251
- }
1252
- }
1253
-
1254
- // src/utils/worktree.ts
1255
- import { join as join4 } from "path";
1256
- import { existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
1234
+ // src/utils/project-config.ts
1235
+ import { existsSync as existsSync3, readFileSync, writeFileSync } from "fs";
1236
+ import { join as join3 } from "path";
1257
1237
 
1258
1238
  // src/utils/fs.ts
1259
1239
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, rmdirSync, statSync } from "fs";
@@ -1294,12 +1274,7 @@ function calculateDirSize(dirPath) {
1294
1274
  return totalSize;
1295
1275
  }
1296
1276
 
1297
- // src/utils/validate-branch.ts
1298
- import Enquirer from "enquirer";
1299
-
1300
1277
  // src/utils/project-config.ts
1301
- import { existsSync as existsSync3, readFileSync, writeFileSync } from "fs";
1302
- import { join as join3 } from "path";
1303
1278
  function getProjectConfigPath(projectName) {
1304
1279
  return join3(PROJECTS_CONFIG_DIR, projectName, "config.json");
1305
1280
  }
@@ -1364,7 +1339,60 @@ function getValidateRunCommand() {
1364
1339
  return config2?.validateRunCommand || void 0;
1365
1340
  }
1366
1341
 
1342
+ // src/utils/validation.ts
1343
+ function validateMainWorktree() {
1344
+ try {
1345
+ const gitCommonDir = getGitCommonDir();
1346
+ if (gitCommonDir !== ".git") {
1347
+ throw new ClawtError(MESSAGES.NOT_MAIN_WORKTREE);
1348
+ }
1349
+ } catch (error) {
1350
+ if (error instanceof ClawtError) {
1351
+ throw error;
1352
+ }
1353
+ throw new ClawtError(MESSAGES.NOT_MAIN_WORKTREE);
1354
+ }
1355
+ }
1356
+ function validateClaudeCodeInstalled() {
1357
+ try {
1358
+ execCommand("claude --version");
1359
+ } catch {
1360
+ throw new ClawtError(MESSAGES.CLAUDE_NOT_INSTALLED);
1361
+ }
1362
+ }
1363
+ function validateHeadExists() {
1364
+ try {
1365
+ execCommand("git rev-parse --verify HEAD");
1366
+ } catch {
1367
+ throw new ClawtError(MESSAGES.HEAD_NOT_FOUND);
1368
+ }
1369
+ }
1370
+ function validateWorkingDirClean() {
1371
+ if (!isWorkingDirClean()) {
1372
+ throw new ClawtError(MESSAGES.MAIN_WORKTREE_DIRTY);
1373
+ }
1374
+ }
1375
+ function runPreChecks(options) {
1376
+ if (options.mainWorktree) {
1377
+ validateMainWorktree();
1378
+ }
1379
+ if (options.headExists) {
1380
+ validateHeadExists();
1381
+ }
1382
+ if (options.projectConfig) {
1383
+ requireProjectConfig();
1384
+ }
1385
+ if (options.branchExists) {
1386
+ guardMainWorkBranchExists();
1387
+ }
1388
+ }
1389
+
1390
+ // src/utils/worktree.ts
1391
+ import { join as join4 } from "path";
1392
+ import { existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
1393
+
1367
1394
  // src/utils/validate-branch.ts
1395
+ import Enquirer from "enquirer";
1368
1396
  function getValidateBranchName(branchName) {
1369
1397
  return `${VALIDATE_BRANCH_PREFIX}${branchName}`;
1370
1398
  }
@@ -3795,7 +3823,7 @@ function registerListCommand(program2) {
3795
3823
  });
3796
3824
  }
3797
3825
  function handleList(options) {
3798
- validateMainWorktree();
3826
+ runPreChecks({ mainWorktree: true });
3799
3827
  const projectName = getProjectName();
3800
3828
  const worktrees = getProjectWorktrees();
3801
3829
  logger.info(`list \u547D\u4EE4\u6267\u884C\uFF0C\u9879\u76EE: ${projectName}\uFF0C\u5171 ${worktrees.length} \u4E2A worktree`);
@@ -3845,9 +3873,10 @@ function registerCreateCommand(program2) {
3845
3873
  });
3846
3874
  }
3847
3875
  async function handleCreate(options) {
3848
- validateMainWorktree();
3876
+ runPreChecks({ mainWorktree: true, headExists: true });
3849
3877
  await guardMainWorkBranch();
3850
3878
  await ensureOnMainWorkBranch();
3879
+ validateWorkingDirClean();
3851
3880
  const count = Number(options.number);
3852
3881
  if (!Number.isInteger(count) || count <= 0) {
3853
3882
  throw new ClawtError(
@@ -3881,8 +3910,7 @@ function registerRemoveCommand(program2) {
3881
3910
  });
3882
3911
  }
3883
3912
  async function handleRemove(options) {
3884
- validateMainWorktree();
3885
- requireProjectConfig();
3913
+ runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
3886
3914
  const projectName = getProjectName();
3887
3915
  logger.info(`remove \u547D\u4EE4\u6267\u884C\uFF0C\u9879\u76EE: ${projectName}`);
3888
3916
  const allWorktrees = getProjectWorktrees();
@@ -3988,10 +4016,11 @@ function handleDryRunFromFile(options) {
3988
4016
  printDryRunPreview(branchNames, tasks, concurrency);
3989
4017
  }
3990
4018
  async function handleRun(options) {
3991
- validateMainWorktree();
4019
+ runPreChecks({ mainWorktree: true, headExists: true });
3992
4020
  if (!options.dryRun) {
3993
4021
  await guardMainWorkBranch();
3994
4022
  await ensureOnMainWorkBranch();
4023
+ validateWorkingDirClean();
3995
4024
  }
3996
4025
  if (options.file && options.tasks) {
3997
4026
  throw new ClawtError(MESSAGES.FILE_AND_TASKS_CONFLICT);
@@ -4053,7 +4082,7 @@ function registerResumeCommand(program2) {
4053
4082
  });
4054
4083
  }
4055
4084
  async function handleResume(options) {
4056
- validateMainWorktree();
4085
+ runPreChecks({ mainWorktree: true, headExists: true });
4057
4086
  validateClaudeCodeInstalled();
4058
4087
  logger.info(`resume \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F\u8FC7\u6EE4: ${options.branch ?? "(\u65E0)"}`);
4059
4088
  const worktrees = getProjectWorktrees();
@@ -4154,8 +4183,7 @@ async function executeSyncForBranch(targetWorktreePath, branch) {
4154
4183
  return { success: true, hasConflict: false };
4155
4184
  }
4156
4185
  async function handleSync(options) {
4157
- validateMainWorktree();
4158
- requireProjectConfig();
4186
+ runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
4159
4187
  await guardMainWorkBranch();
4160
4188
  logger.info(`sync \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch ?? "(\u672A\u6307\u5B9A)"}`);
4161
4189
  const worktrees = getProjectWorktrees();
@@ -4189,8 +4217,7 @@ async function handlePatchApplyFailure(targetWorktreePath, branchName) {
4189
4217
  const syncResult = await executeSyncForBranch(targetWorktreePath, branchName);
4190
4218
  }
4191
4219
  async function handleValidateClean(options) {
4192
- validateMainWorktree();
4193
- requireProjectConfig();
4220
+ runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
4194
4221
  const projectName = getProjectName();
4195
4222
  const mainWorktreePath = getGitTopLevel();
4196
4223
  const worktrees = getProjectWorktrees();
@@ -4275,8 +4302,7 @@ async function handleValidate(options) {
4275
4302
  await handleValidateClean(options);
4276
4303
  return;
4277
4304
  }
4278
- validateMainWorktree();
4279
- requireProjectConfig();
4305
+ runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
4280
4306
  const projectName = getProjectName();
4281
4307
  const mainWorktreePath = getGitTopLevel();
4282
4308
  const worktrees = getProjectWorktrees();
@@ -4341,8 +4367,7 @@ function computeIncrementalPatch(snapshotTreeHash, mainWorktreePath) {
4341
4367
  return { patch, currentTreeHash };
4342
4368
  }
4343
4369
  async function handleCoverValidate() {
4344
- validateMainWorktree();
4345
- requireProjectConfig();
4370
+ runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
4346
4371
  const projectName = getProjectName();
4347
4372
  const mainWorktreePath = getGitTopLevel();
4348
4373
  const currentBranch = getCurrentBranch(mainWorktreePath);
@@ -4421,7 +4446,8 @@ function cleanupWorktreeAndBranch(worktreePath, branchName) {
4421
4446
  printSuccess(MESSAGES.WORKTREE_CLEANED(branchName));
4422
4447
  }
4423
4448
  async function handleMerge(options) {
4424
- validateMainWorktree();
4449
+ runPreChecks({ mainWorktree: true, headExists: true });
4450
+ await guardMainWorkBranch();
4425
4451
  await guardMainWorkBranch();
4426
4452
  const mainWorktreePath = getGitTopLevel();
4427
4453
  await ensureOnMainWorkBranch(mainWorktreePath);
@@ -4583,8 +4609,7 @@ function registerResetCommand(program2) {
4583
4609
  });
4584
4610
  }
4585
4611
  async function handleReset() {
4586
- validateMainWorktree();
4587
- requireProjectConfig();
4612
+ runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
4588
4613
  const mainWorktreePath = getGitTopLevel();
4589
4614
  logger.info("reset \u547D\u4EE4\u6267\u884C");
4590
4615
  if (!isWorkingDirClean(mainWorktreePath)) {
@@ -4614,7 +4639,7 @@ function registerStatusCommand(program2) {
4614
4639
  });
4615
4640
  }
4616
4641
  async function handleStatus(options) {
4617
- validateMainWorktree();
4642
+ runPreChecks({ mainWorktree: true, headExists: true });
4618
4643
  if (options.interactive) {
4619
4644
  const panel = new InteractivePanel(collectStatus);
4620
4645
  await panel.start();
@@ -5294,7 +5319,7 @@ function registerInitCommand(program2) {
5294
5319
  );
5295
5320
  }
5296
5321
  async function handleInitShow() {
5297
- validateMainWorktree();
5322
+ runPreChecks({ mainWorktree: true, projectConfig: true });
5298
5323
  const config2 = requireProjectConfig();
5299
5324
  logger.info("init show \u547D\u4EE4\u6267\u884C\uFF0C\u8FDB\u5165\u4EA4\u4E92\u5F0F\u9879\u76EE\u914D\u7F6E");
5300
5325
  const { key, newValue } = await interactiveConfigEditor(
@@ -5307,8 +5332,11 @@ async function handleInitShow() {
5307
5332
  printSuccess(MESSAGES.INIT_SET_SUCCESS(key, String(newValue)));
5308
5333
  }
5309
5334
  async function handleInit(options) {
5310
- validateMainWorktree();
5335
+ runPreChecks({ mainWorktree: true });
5311
5336
  const existingConfig = loadProjectConfig();
5337
+ if (!options.branch) {
5338
+ validateHeadExists();
5339
+ }
5312
5340
  const branchName = options.branch || getCurrentBranch();
5313
5341
  logger.info(`init \u547D\u4EE4\u6267\u884C\uFF0C\u4E3B\u5DE5\u4F5C\u5206\u652F: ${branchName}`);
5314
5342
  saveProjectConfig({ clawtMainWorkBranch: branchName });
@@ -5326,9 +5354,7 @@ function registerHomeCommand(program2) {
5326
5354
  });
5327
5355
  }
5328
5356
  async function handleHome() {
5329
- validateMainWorktree();
5330
- requireProjectConfig();
5331
- guardMainWorkBranchExists();
5357
+ runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true, branchExists: true });
5332
5358
  const mainBranch = getMainWorkBranch();
5333
5359
  const currentBranch = getCurrentBranch();
5334
5360
  if (currentBranch === mainBranch) {
@@ -21,6 +21,8 @@ var COMMON_MESSAGES = {
21
21
  GIT_NOT_INSTALLED: "Git \u672A\u5B89\u88C5\u6216\u4E0D\u5728 PATH \u4E2D\uFF0C\u8BF7\u5148\u5B89\u88C5 Git",
22
22
  /** Claude Code CLI 未安装 */
23
23
  CLAUDE_NOT_INSTALLED: "Claude Code CLI \u672A\u5B89\u88C5\uFF0C\u8BF7\u5148\u5B89\u88C5\uFF1Anpm install -g @anthropic-ai/claude-code",
24
+ /** HEAD 不存在(仓库无任何 commit) */
25
+ HEAD_NOT_FOUND: "\u5F53\u524D\u4ED3\u5E93\u5C1A\u672A\u521B\u5EFA\u4EFB\u4F55\u63D0\u4EA4\uFF0C\u8BF7\u5148\u6267\u884C git commit \u521B\u5EFA\u9996\u6B21\u63D0\u4EA4\u540E\u518D\u4F7F\u7528 clawt",
24
26
  /** 分支已存在 */
25
27
  BRANCH_EXISTS: (name) => `\u5206\u652F ${name} \u5DF2\u5B58\u5728\uFF0C\u65E0\u6CD5\u521B\u5EFA`,
26
28
  /** 分支名清理后为空 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "3.4.2",
3
+ "version": "3.4.4",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3,8 +3,7 @@ import { logger } from '../logger/index.js';
3
3
  import { ClawtError } from '../errors/index.js';
4
4
  import { MESSAGES, VALIDATE_BRANCH_PREFIX } from '../constants/index.js';
5
5
  import {
6
- validateMainWorktree,
7
- requireProjectConfig,
6
+ runPreChecks,
8
7
  getProjectName,
9
8
  getGitTopLevel,
10
9
  getCurrentBranch,
@@ -98,8 +97,7 @@ export function computeIncrementalPatch(snapshotTreeHash: string, mainWorktreePa
98
97
  */
99
98
  async function handleCoverValidate(): Promise<void> {
100
99
  // 步骤 1:前置校验
101
- validateMainWorktree();
102
- requireProjectConfig();
100
+ runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
103
101
  const projectName = getProjectName();
104
102
  const mainWorktreePath = getGitTopLevel();
105
103
  const currentBranch = getCurrentBranch(mainWorktreePath);
@@ -4,9 +4,10 @@ import { ClawtError } from '../errors/index.js';
4
4
  import { logger } from '../logger/index.js';
5
5
  import type { CreateOptions } from '../types/index.js';
6
6
  import {
7
- validateMainWorktree,
7
+ runPreChecks,
8
8
  createWorktrees,
9
9
  ensureOnMainWorkBranch,
10
+ validateWorkingDirClean,
10
11
  getValidateBranchName,
11
12
  printSuccess,
12
13
  printInfo,
@@ -34,12 +35,14 @@ export function registerCreateCommand(program: Command): void {
34
35
  * @param {CreateOptions} options - 命令选项
35
36
  */
36
37
  async function handleCreate(options: CreateOptions): Promise<void> {
37
- validateMainWorktree();
38
+ runPreChecks({ mainWorktree: true, headExists: true });
38
39
 
39
40
  await guardMainWorkBranch();
40
41
 
41
42
  await ensureOnMainWorkBranch();
42
43
 
44
+ validateWorkingDirClean();
45
+
43
46
  const count = Number(options.number);
44
47
 
45
48
  // 校验创建数量必须为正整数
@@ -1,8 +1,7 @@
1
1
  import type { Command } from 'commander';
2
2
  import { MESSAGES } from '../constants/index.js';
3
3
  import {
4
- validateMainWorktree,
5
- requireProjectConfig,
4
+ runPreChecks,
6
5
  ensureOnMainWorkBranch,
7
6
  getCurrentBranch,
8
7
  getMainWorkBranch,
@@ -28,9 +27,7 @@ export function registerHomeCommand(program: Command): void {
28
27
  * 执行 home 命令:切换回主工作分支
29
28
  */
30
29
  async function handleHome(): Promise<void> {
31
- validateMainWorktree();
32
- requireProjectConfig();
33
- guardMainWorkBranchExists();
30
+ runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true, branchExists: true });
34
31
 
35
32
  const mainBranch = getMainWorkBranch();
36
33
  const currentBranch = getCurrentBranch();
@@ -4,7 +4,8 @@ import { logger } from '../logger/index.js';
4
4
  import { MESSAGES, PROJECT_CONFIG_DEFINITIONS } from '../constants/index.js';
5
5
  import type { InitOptions, ProjectConfig } from '../types/index.js';
6
6
  import {
7
- validateMainWorktree,
7
+ runPreChecks,
8
+ validateHeadExists,
8
9
  getCurrentBranch,
9
10
  loadProjectConfig,
10
11
  saveProjectConfig,
@@ -40,7 +41,7 @@ export function registerInitCommand(program: Command): void {
40
41
  * 处理 init show 子命令:交互式面板展示和修改项目配置
41
42
  */
42
43
  async function handleInitShow(): Promise<void> {
43
- validateMainWorktree();
44
+ runPreChecks({ mainWorktree: true, projectConfig: true });
44
45
  const config = requireProjectConfig();
45
46
 
46
47
  logger.info('init show 命令执行,进入交互式项目配置');
@@ -66,11 +67,14 @@ async function handleInitShow(): Promise<void> {
66
67
  * @param {InitOptions} options - 命令选项
67
68
  */
68
69
  async function handleInit(options: InitOptions): Promise<void> {
69
- validateMainWorktree();
70
+ runPreChecks({ mainWorktree: true });
70
71
 
71
72
  const existingConfig = loadProjectConfig();
72
73
 
73
74
  // 确定分支名:优先使用 -b 参数,否则使用当前分支
75
+ if (!options.branch) {
76
+ validateHeadExists();
77
+ }
74
78
  const branchName = options.branch || getCurrentBranch();
75
79
 
76
80
  logger.info(`init 命令执行,主工作分支: ${branchName}`);
@@ -4,7 +4,7 @@ import { MESSAGES } from '../constants/index.js';
4
4
  import { logger } from '../logger/index.js';
5
5
  import type { ListOptions } from '../types/index.js';
6
6
  import {
7
- validateMainWorktree,
7
+ runPreChecks,
8
8
  getProjectName,
9
9
  getProjectWorktrees,
10
10
  getWorktreeStatus,
@@ -33,7 +33,7 @@ export function registerListCommand(program: Command): void {
33
33
  * @param {ListOptions} options - 命令选项
34
34
  */
35
35
  function handleList(options: ListOptions): void {
36
- validateMainWorktree();
36
+ runPreChecks({ mainWorktree: true });
37
37
 
38
38
  const projectName = getProjectName();
39
39
  const worktrees = getProjectWorktrees();
@@ -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 { MergeOptions } from '../types/index.js';
6
6
  import {
7
- validateMainWorktree,
7
+ runPreChecks,
8
8
  getProjectName,
9
9
  getGitTopLevel,
10
10
  getProjectWorktrees,
@@ -136,7 +136,9 @@ function cleanupWorktreeAndBranch(worktreePath: string, branchName: string): voi
136
136
  * @param {MergeOptions} options - 命令选项
137
137
  */
138
138
  async function handleMerge(options: MergeOptions): Promise<void> {
139
- validateMainWorktree();
139
+ runPreChecks({ mainWorktree: true, headExists: true });
140
+
141
+ await guardMainWorkBranch();
140
142
 
141
143
  await guardMainWorkBranch();
142
144
 
@@ -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
+ validateWorkingDirClean,
23
24
  guardMainWorkBranch,
24
25
  } from '../utils/index.js';
25
26
 
@@ -120,12 +121,13 @@ function handleDryRunFromFile(options: RunOptions): void {
120
121
  * @param {RunOptions} options - 命令选项
121
122
  */
122
123
  async function handleRun(options: RunOptions): Promise<void> {
123
- validateMainWorktree();
124
+ runPreChecks({ mainWorktree: true, headExists: true });
124
125
 
125
126
  // dry-run 模式跳过项目配置前置校验
126
127
  if (!options.dryRun) {
127
128
  await guardMainWorkBranch();
128
129
  await ensureOnMainWorkBranch();
130
+ validateWorkingDirClean();
129
131
  }
130
132
 
131
133
  // 互斥校验:--file 和 --tasks 不能同时使用
@@ -4,7 +4,7 @@ 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,
@@ -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
- validateMainWorktree();
49
+ runPreChecks({ mainWorktree: true, headExists: true });
50
50
 
51
51
  // 交互式面板模式
52
52
  if (options.interactive) {
@@ -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,7 +16,6 @@ import {
16
16
  printInfo,
17
17
  printWarning,
18
18
  resolveTargetWorktree,
19
- requireProjectConfig,
20
19
  getMainWorkBranch,
21
20
  rebuildValidateBranch,
22
21
  getValidateBranchName,
@@ -128,8 +127,7 @@ export async function executeSyncForBranch(targetWorktreePath: string, branch: s
128
127
  * @param {SyncOptions} options - 命令选项
129
128
  */
130
129
  async function handleSync(options: SyncOptions): Promise<void> {
131
- validateMainWorktree();
132
- requireProjectConfig();
130
+ runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
133
131
  await guardMainWorkBranch();
134
132
 
135
133
  logger.info(`sync 命令执行,分支: ${options.branch ?? '(未指定)'}`);
@@ -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();
@@ -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
  /** 分支名清理后为空 */
@@ -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, validateWorkingDirClean, 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';
@@ -1,7 +1,20 @@
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
+ 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,51 @@ 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
+ * 当存在未提交的更改时抛出错误,防止基于脏状态创建 worktree
79
+ * @throws {ClawtError} 工作区或暂存区不干净时抛出
80
+ */
81
+ export function validateWorkingDirClean(): void {
82
+ if (!isWorkingDirClean()) {
83
+ throw new ClawtError(MESSAGES.MAIN_WORKTREE_DIRTY);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * 统一前置校验入口,按需执行各项校验
89
+ * @param {PreCheckOptions} options - 校验选项
90
+ * @param {boolean} [options.mainWorktree] - 校验是否在主 worktree 根目录
91
+ * @param {boolean} [options.headExists] - 校验 HEAD 是否存在
92
+ * @param {boolean} [options.projectConfig] - 校验项目是否已初始化
93
+ * @param {boolean} [options.branchExists] - 校验配置的主工作分支是否存在
94
+ * @throws {ClawtError} 任一校验未通过时抛出
95
+ */
96
+ export function runPreChecks(options: PreCheckOptions): void {
97
+ if (options.mainWorktree) {
98
+ validateMainWorktree();
99
+ }
100
+ if (options.headExists) {
101
+ validateHeadExists();
102
+ }
103
+ if (options.projectConfig) {
104
+ requireProjectConfig();
105
+ }
106
+ if (options.branchExists) {
107
+ guardMainWorkBranchExists();
108
+ }
109
+ }
@@ -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'),
@@ -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' }),
@@ -38,14 +38,14 @@ vi.mock('../../../src/utils/index.js', () => ({
38
38
  }));
39
39
 
40
40
  import { registerCreateCommand } from '../../../src/commands/create.js';
41
- import { validateMainWorktree, createWorktrees, printSuccess } from '../../../src/utils/index.js';
41
+ import { runPreChecks, createWorktrees, printSuccess } from '../../../src/utils/index.js';
42
42
 
43
- const mockedValidateMainWorktree = vi.mocked(validateMainWorktree);
43
+ const mockedRunPreChecks = vi.mocked(runPreChecks);
44
44
  const mockedCreateWorktrees = vi.mocked(createWorktrees);
45
45
  const mockedPrintSuccess = vi.mocked(printSuccess);
46
46
 
47
47
  beforeEach(() => {
48
- mockedValidateMainWorktree.mockReset();
48
+ mockedRunPreChecks.mockReset();
49
49
  mockedCreateWorktrees.mockReset();
50
50
  mockedPrintSuccess.mockReset();
51
51
  });
@@ -70,7 +70,7 @@ describe('handleCreate', () => {
70
70
  registerCreateCommand(program);
71
71
  await program.parseAsync(['create', '-b', 'feature'], { from: 'user' });
72
72
 
73
- expect(mockedValidateMainWorktree).toHaveBeenCalled();
73
+ expect(mockedRunPreChecks).toHaveBeenCalled();
74
74
  expect(mockedCreateWorktrees).toHaveBeenCalledWith('feature', 1);
75
75
  expect(mockedPrintSuccess).toHaveBeenCalled();
76
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(),
@@ -13,7 +13,7 @@ vi.mock('../../../src/constants/index.js', () => ({
13
13
  }));
14
14
 
15
15
  vi.mock('../../../src/utils/index.js', () => ({
16
- validateMainWorktree: vi.fn(),
16
+ runPreChecks: vi.fn(),
17
17
  getProjectName: vi.fn(),
18
18
  getProjectWorktrees: vi.fn(),
19
19
  getWorktreeStatus: vi.fn(),
@@ -23,16 +23,16 @@ vi.mock('../../../src/utils/index.js', () => ({
23
23
  }));
24
24
 
25
25
  import { registerListCommand } from '../../../src/commands/list.js';
26
- import { validateMainWorktree, getProjectName, getProjectWorktrees, getWorktreeStatus, printInfo } from '../../../src/utils/index.js';
26
+ import { runPreChecks, getProjectName, getProjectWorktrees, getWorktreeStatus, printInfo } from '../../../src/utils/index.js';
27
27
 
28
- const mockedValidateMainWorktree = vi.mocked(validateMainWorktree);
28
+ const mockedRunPreChecks = vi.mocked(runPreChecks);
29
29
  const mockedGetProjectName = vi.mocked(getProjectName);
30
30
  const mockedGetProjectWorktrees = vi.mocked(getProjectWorktrees);
31
31
  const mockedGetWorktreeStatus = vi.mocked(getWorktreeStatus);
32
32
  const mockedPrintInfo = vi.mocked(printInfo);
33
33
 
34
34
  beforeEach(() => {
35
- mockedValidateMainWorktree.mockReset();
35
+ mockedRunPreChecks.mockReset();
36
36
  mockedGetProjectName.mockReset();
37
37
  mockedGetProjectWorktrees.mockReset();
38
38
  mockedGetWorktreeStatus.mockReset();
@@ -58,7 +58,7 @@ describe('handleList', () => {
58
58
  registerListCommand(program);
59
59
  program.parse(['list'], { from: 'user' });
60
60
 
61
- expect(mockedValidateMainWorktree).toHaveBeenCalled();
61
+ expect(mockedRunPreChecks).toHaveBeenCalled();
62
62
  expect(mockedPrintInfo).toHaveBeenCalled();
63
63
  });
64
64
 
@@ -40,7 +40,7 @@ vi.mock('../../../src/constants/index.js', () => ({
40
40
  }));
41
41
 
42
42
  vi.mock('../../../src/utils/index.js', () => ({
43
- validateMainWorktree: vi.fn(),
43
+ runPreChecks: vi.fn(),
44
44
  getProjectName: vi.fn(),
45
45
  getGitTopLevel: vi.fn(),
46
46
  getProjectWorktrees: vi.fn(),
@@ -33,7 +33,7 @@ vi.mock('../../../src/constants/index.js', () => ({
33
33
  }));
34
34
 
35
35
  vi.mock('../../../src/utils/index.js', () => ({
36
- validateMainWorktree: vi.fn(),
36
+ runPreChecks: vi.fn(),
37
37
  getProjectName: vi.fn(),
38
38
  getProjectWorktreeDir: vi.fn(),
39
39
  getProjectWorktrees: vi.fn(),
@@ -60,7 +60,7 @@ vi.mock('../../../src/utils/index.js', () => ({
60
60
 
61
61
  import { registerRemoveCommand } from '../../../src/commands/remove.js';
62
62
  import {
63
- validateMainWorktree,
63
+ runPreChecks,
64
64
  getProjectName,
65
65
  getProjectWorktrees,
66
66
  removeWorktreeByPath,
@@ -91,7 +91,7 @@ const mockedResolveTargetWorktrees = vi.mocked(resolveTargetWorktrees);
91
91
  const mockedGetCurrentBranch = vi.mocked(getCurrentBranch);
92
92
 
93
93
  beforeEach(() => {
94
- vi.mocked(validateMainWorktree).mockReset();
94
+ vi.mocked(runPreChecks).mockReset();
95
95
  mockedGetProjectName.mockReturnValue('test-project');
96
96
  mockedGetProjectWorktrees.mockReset();
97
97
  mockedRemoveWorktreeByPath.mockReset();
@@ -14,7 +14,7 @@ vi.mock('../../../src/constants/index.js', () => ({
14
14
  }));
15
15
 
16
16
  vi.mock('../../../src/utils/index.js', () => ({
17
- validateMainWorktree: vi.fn(),
17
+ runPreChecks: vi.fn(),
18
18
  getGitTopLevel: vi.fn(),
19
19
  getConfigValue: vi.fn(),
20
20
  isWorkingDirClean: vi.fn(),
@@ -17,7 +17,7 @@ vi.mock('../../../src/constants/index.js', () => ({
17
17
  }));
18
18
 
19
19
  vi.mock('../../../src/utils/index.js', () => ({
20
- validateMainWorktree: vi.fn(),
20
+ runPreChecks: vi.fn(),
21
21
  validateClaudeCodeInstalled: vi.fn(),
22
22
  getProjectWorktrees: vi.fn(),
23
23
  launchInteractiveClaude: vi.fn(),
@@ -33,7 +33,7 @@ vi.mock('../../../src/utils/index.js', () => ({
33
33
 
34
34
  import { registerResumeCommand } from '../../../src/commands/resume.js';
35
35
  import {
36
- validateMainWorktree,
36
+ runPreChecks,
37
37
  validateClaudeCodeInstalled,
38
38
  getProjectWorktrees,
39
39
  launchInteractiveClaude,
@@ -45,7 +45,7 @@ import {
45
45
  getConfigValue,
46
46
  } from '../../../src/utils/index.js';
47
47
 
48
- const mockedValidateMainWorktree = vi.mocked(validateMainWorktree);
48
+ const mockedRunPreChecks = vi.mocked(runPreChecks);
49
49
  const mockedValidateClaudeCodeInstalled = vi.mocked(validateClaudeCodeInstalled);
50
50
  const mockedGetProjectWorktrees = vi.mocked(getProjectWorktrees);
51
51
  const mockedLaunchInteractiveClaude = vi.mocked(launchInteractiveClaude);
@@ -57,7 +57,7 @@ const mockedConfirmAction = vi.mocked(confirmAction);
57
57
  const mockedGetConfigValue = vi.mocked(getConfigValue);
58
58
 
59
59
  beforeEach(() => {
60
- mockedValidateMainWorktree.mockReset();
60
+ mockedRunPreChecks.mockReset();
61
61
  mockedValidateClaudeCodeInstalled.mockReset();
62
62
  mockedGetProjectWorktrees.mockReset();
63
63
  mockedLaunchInteractiveClaude.mockReset();
@@ -90,7 +90,7 @@ describe('handleResume', () => {
90
90
  registerResumeCommand(program);
91
91
  await program.parseAsync(['resume', '-b', 'feature'], { from: 'user' });
92
92
 
93
- expect(mockedValidateMainWorktree).toHaveBeenCalled();
93
+ expect(mockedRunPreChecks).toHaveBeenCalled();
94
94
  expect(mockedValidateClaudeCodeInstalled).toHaveBeenCalled();
95
95
  expect(mockedResolveTargetWorktrees).toHaveBeenCalled();
96
96
  expect(mockedPromptGroupedMultiSelectBranches).not.toHaveBeenCalled();
@@ -50,7 +50,7 @@ vi.mock('../../../src/utils/index.js', async (importOriginal) => {
50
50
  const actual = await importOriginal<typeof import('../../../src/utils/index.js')>();
51
51
  return {
52
52
  ...actual,
53
- validateMainWorktree: vi.fn(),
53
+ runPreChecks: vi.fn(),
54
54
  validateClaudeCodeInstalled: vi.fn(),
55
55
  createWorktrees: vi.fn(),
56
56
  sanitizeBranchName: vi.fn(),
@@ -30,7 +30,7 @@ vi.mock('../../../src/constants/index.js', () => ({
30
30
  }));
31
31
 
32
32
  vi.mock('../../../src/utils/index.js', () => ({
33
- validateMainWorktree: vi.fn(),
33
+ runPreChecks: vi.fn(),
34
34
  getProjectName: vi.fn(),
35
35
  getCurrentBranch: vi.fn(),
36
36
  isWorkingDirClean: vi.fn(),
@@ -31,7 +31,7 @@ vi.mock('../../../src/constants/index.js', () => ({
31
31
  }));
32
32
 
33
33
  vi.mock('../../../src/utils/index.js', () => ({
34
- validateMainWorktree: vi.fn(),
34
+ runPreChecks: vi.fn(),
35
35
  getGitTopLevel: vi.fn(),
36
36
  getProjectWorktrees: vi.fn(),
37
37
  isWorkingDirClean: vi.fn(),
@@ -57,7 +57,7 @@ vi.mock('enquirer', () => ({
57
57
  }));
58
58
 
59
59
  vi.mock('../../../src/utils/index.js', () => ({
60
- validateMainWorktree: vi.fn(),
60
+ runPreChecks: vi.fn(),
61
61
  getProjectName: vi.fn(),
62
62
  getGitTopLevel: vi.fn(),
63
63
  getProjectWorktrees: vi.fn(),
@@ -10,6 +10,12 @@ vi.mock('../../../src/utils/git.js', () => ({
10
10
  getGitCommonDir: vi.fn(),
11
11
  }));
12
12
 
13
+ // mock project-config(runPreChecks 依赖 requireProjectConfig 和 guardMainWorkBranchExists)
14
+ vi.mock('../../../src/utils/project-config.js', () => ({
15
+ requireProjectConfig: vi.fn(),
16
+ guardMainWorkBranchExists: vi.fn(),
17
+ }));
18
+
13
19
  // mock logger
14
20
  vi.mock('../../../src/logger/index.js', () => ({
15
21
  logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
@@ -17,11 +23,14 @@ vi.mock('../../../src/logger/index.js', () => ({
17
23
 
18
24
  import { execCommand } from '../../../src/utils/shell.js';
19
25
  import { getGitCommonDir } from '../../../src/utils/git.js';
20
- import { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from '../../../src/utils/validation.js';
26
+ import { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled, validateHeadExists, runPreChecks } from '../../../src/utils/validation.js';
27
+ import { requireProjectConfig, guardMainWorkBranchExists } from '../../../src/utils/project-config.js';
21
28
  import { ClawtError } from '../../../src/errors/index.js';
22
29
 
23
30
  const mockedExecCommand = vi.mocked(execCommand);
24
31
  const mockedGetGitCommonDir = vi.mocked(getGitCommonDir);
32
+ const mockedRequireProjectConfig = vi.mocked(requireProjectConfig);
33
+ const mockedGuardMainWorkBranchExists = vi.mocked(guardMainWorkBranchExists);
25
34
 
26
35
  describe('validateMainWorktree', () => {
27
36
  it('.git 返回时正常通过', () => {
@@ -63,3 +72,36 @@ describe('validateClaudeCodeInstalled', () => {
63
72
  expect(() => validateClaudeCodeInstalled()).toThrow(ClawtError);
64
73
  });
65
74
  });
75
+
76
+ describe('validateHeadExists', () => {
77
+ it('HEAD 存在时正常通过', () => {
78
+ mockedExecCommand.mockReturnValue('abc1234');
79
+ expect(() => validateHeadExists()).not.toThrow();
80
+ });
81
+
82
+ it('HEAD 不存在时抛出 ClawtError', () => {
83
+ mockedExecCommand.mockImplementation(() => { throw new Error('fatal: ambiguous argument HEAD'); });
84
+ expect(() => validateHeadExists()).toThrow(ClawtError);
85
+ });
86
+ });
87
+
88
+ describe('runPreChecks', () => {
89
+ it('按选项组合调用对应校验', () => {
90
+ mockedGetGitCommonDir.mockReturnValue('.git');
91
+ mockedExecCommand.mockReturnValue('abc1234');
92
+ mockedRequireProjectConfig.mockReturnValue({ clawtMainWorkBranch: 'main' });
93
+ mockedGuardMainWorkBranchExists.mockReturnValue(undefined);
94
+
95
+ expect(() => runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true, branchExists: true })).not.toThrow();
96
+ });
97
+
98
+ it('未设置的选项不触发校验', () => {
99
+ // 不设置任何选项,不应该调用任何校验函数
100
+ mockedGetGitCommonDir.mockImplementation(() => { throw new Error('should not be called'); });
101
+ mockedExecCommand.mockImplementation(() => { throw new Error('should not be called'); });
102
+ mockedRequireProjectConfig.mockImplementation(() => { throw new Error('should not be called'); });
103
+ mockedGuardMainWorkBranchExists.mockImplementation(() => { throw new Error('should not be called'); });
104
+
105
+ expect(() => runPreChecks({})).not.toThrow();
106
+ });
107
+ });