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 +79 -53
- package/dist/postinstall.js +2 -0
- 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 +2 -5
- package/src/commands/init.ts +7 -3
- package/src/commands/list.ts +2 -2
- package/src/commands/merge.ts +4 -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 +2 -2
- package/src/commands/sync.ts +2 -4
- package/src/commands/validate.ts +3 -7
- package/src/constants/messages/common.ts +2 -0
- package/src/utils/index.ts +1 -1
- package/src/utils/validation.ts +62 -1
- package/tests/unit/commands/cover-validate.test.ts +1 -1
- package/tests/unit/commands/create.test.ts +5 -5
- package/tests/unit/commands/init.test.ts +2 -1
- package/tests/unit/commands/list.test.ts +5 -5
- package/tests/unit/commands/merge.test.ts +1 -1
- package/tests/unit/commands/remove.test.ts +3 -3
- package/tests/unit/commands/reset.test.ts +1 -1
- package/tests/unit/commands/resume.test.ts +5 -5
- package/tests/unit/commands/run.test.ts +1 -1
- package/tests/unit/commands/status.test.ts +1 -1
- package/tests/unit/commands/sync.test.ts +1 -1
- package/tests/unit/commands/validate.test.ts +1 -1
- package/tests/unit/utils/validation.test.ts +43 -1
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/
|
|
1233
|
-
|
|
1234
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/postinstall.js
CHANGED
|
@@ -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
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|
package/src/commands/create.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
// 校验创建数量必须为正整数
|
package/src/commands/home.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { MESSAGES } from '../constants/index.js';
|
|
3
3
|
import {
|
|
4
|
-
|
|
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
|
-
|
|
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();
|
package/src/commands/init.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}`);
|
package/src/commands/list.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
36
|
+
runPreChecks({ mainWorktree: true });
|
|
37
37
|
|
|
38
38
|
const projectName = getProjectName();
|
|
39
39
|
const worktrees = getProjectWorktrees();
|
package/src/commands/merge.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 { MergeOptions } from '../types/index.js';
|
|
6
6
|
import {
|
|
7
|
-
|
|
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
|
-
|
|
139
|
+
runPreChecks({ mainWorktree: true, headExists: true });
|
|
140
|
+
|
|
141
|
+
await guardMainWorkBranch();
|
|
140
142
|
|
|
141
143
|
await guardMainWorkBranch();
|
|
142
144
|
|
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
|
+
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
|
-
|
|
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 不能同时使用
|
package/src/commands/status.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
49
|
+
runPreChecks({ mainWorktree: true, headExists: true });
|
|
50
50
|
|
|
51
51
|
// 交互式面板模式
|
|
52
52
|
if (options.interactive) {
|
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,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
|
-
|
|
132
|
-
requireProjectConfig();
|
|
130
|
+
runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
|
|
133
131
|
await guardMainWorkBranch();
|
|
134
132
|
|
|
135
133
|
logger.info(`sync 命令执行,分支: ${options.branch ?? '(未指定)'}`);
|
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();
|
|
@@ -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
|
/** 分支名清理后为空 */
|
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, 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';
|
package/src/utils/validation.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
|
41
|
+
import { runPreChecks, createWorktrees, printSuccess } from '../../../src/utils/index.js';
|
|
42
42
|
|
|
43
|
-
const
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
26
|
+
import { runPreChecks, getProjectName, getProjectWorktrees, getWorktreeStatus, printInfo } from '../../../src/utils/index.js';
|
|
27
27
|
|
|
28
|
-
const
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
+
runPreChecks: vi.fn(),
|
|
35
35
|
getGitTopLevel: vi.fn(),
|
|
36
36
|
getProjectWorktrees: vi.fn(),
|
|
37
37
|
isWorkingDirClean: 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
|
+
});
|