clawt 3.1.2 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/index.js +291 -229
- package/dist/postinstall.js +33 -1
- package/docs/config.md +2 -1
- package/docs/init.md +16 -7
- package/docs/project-config.md +132 -0
- package/docs/spec.md +32 -22
- package/docs/validate.md +55 -13
- package/package.json +1 -1
- package/src/commands/config.ts +14 -28
- package/src/commands/init.ts +23 -12
- package/src/commands/validate.ts +50 -228
- package/src/constants/index.ts +1 -0
- package/src/constants/messages/init.ts +4 -0
- package/src/constants/messages/validate.ts +3 -0
- package/src/constants/project-config.ts +46 -0
- package/src/types/index.ts +1 -1
- package/src/types/projectConfig.ts +17 -0
- package/src/utils/config-strategy.ts +68 -20
- package/src/utils/index.ts +4 -2
- package/src/utils/project-config.ts +9 -0
- package/src/utils/validate-core.ts +174 -0
- package/src/utils/validate-runner.ts +105 -0
- package/src/utils/validate-snapshot.ts +29 -9
- package/tests/unit/commands/config.test.ts +1 -0
- package/tests/unit/commands/init.test.ts +41 -6
- package/tests/unit/commands/validate.test.ts +96 -243
- package/tests/unit/utils/config-strategy.test.ts +77 -1
- package/tests/unit/utils/project-config.test.ts +32 -0
- package/tests/unit/utils/validate-snapshot.test.ts +8 -3
package/src/commands/validate.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { logger } from '../logger/index.js';
|
|
3
|
-
import { ClawtError } from '../errors/index.js';
|
|
4
3
|
import { MESSAGES } from '../constants/index.js';
|
|
5
4
|
import type { ValidateOptions } from '../types/index.js';
|
|
6
5
|
import { executeSyncForBranch } from './sync.js';
|
|
@@ -11,21 +10,10 @@ import {
|
|
|
11
10
|
getProjectWorktrees,
|
|
12
11
|
getConfigValue,
|
|
13
12
|
isWorkingDirClean,
|
|
14
|
-
gitAddAll,
|
|
15
|
-
gitCommit,
|
|
16
|
-
gitRestoreStaged,
|
|
17
13
|
gitResetHard,
|
|
18
14
|
gitCleanForce,
|
|
19
|
-
gitDiffBinaryAgainstBranch,
|
|
20
|
-
gitApplyFromStdin,
|
|
21
|
-
gitApplyCachedFromStdin,
|
|
22
|
-
gitResetSoft,
|
|
23
|
-
gitWriteTree,
|
|
24
15
|
gitReadTree,
|
|
25
16
|
getHeadCommitHash,
|
|
26
|
-
getCommitTreeHash,
|
|
27
|
-
gitDiffTree,
|
|
28
|
-
gitApplyCachedCheck,
|
|
29
17
|
hasLocalCommits,
|
|
30
18
|
hasSnapshot,
|
|
31
19
|
readSnapshot,
|
|
@@ -34,23 +22,21 @@ import {
|
|
|
34
22
|
confirmDestructiveAction,
|
|
35
23
|
confirmAction,
|
|
36
24
|
printSuccess,
|
|
37
|
-
printError,
|
|
38
25
|
printWarning,
|
|
39
26
|
printInfo,
|
|
40
|
-
printSeparator,
|
|
41
27
|
resolveTargetWorktree,
|
|
42
|
-
runCommandInherited,
|
|
43
|
-
parseParallelCommands,
|
|
44
|
-
runParallelCommands,
|
|
45
28
|
requireProjectConfig,
|
|
46
|
-
getValidateBranchName,
|
|
47
|
-
gitCheckout,
|
|
48
29
|
ensureOnMainWorkBranch,
|
|
49
|
-
checkBranchExists,
|
|
50
|
-
getCurrentBranch,
|
|
51
30
|
handleDirtyWorkingDir,
|
|
31
|
+
getValidateRunCommand,
|
|
32
|
+
executeRunCommand,
|
|
33
|
+
migrateChangesViaPatch,
|
|
34
|
+
computeCurrentTreeHash,
|
|
35
|
+
saveCurrentSnapshotTree,
|
|
36
|
+
loadOldSnapshotToStage,
|
|
37
|
+
switchToValidateBranch,
|
|
52
38
|
} from '../utils/index.js';
|
|
53
|
-
import type { WorktreeResolveMessages
|
|
39
|
+
import type { WorktreeResolveMessages } from '../utils/index.js';
|
|
54
40
|
|
|
55
41
|
/** validate 命令的分支解析消息配置 */
|
|
56
42
|
const VALIDATE_RESOLVE_MESSAGES: WorktreeResolveMessages = {
|
|
@@ -85,59 +71,6 @@ async function handleDirtyMainWorktree(mainWorktreePath: string): Promise<void>
|
|
|
85
71
|
await handleDirtyWorkingDir(mainWorktreePath);
|
|
86
72
|
}
|
|
87
73
|
|
|
88
|
-
/**
|
|
89
|
-
* 通过 patch 将目标分支的全量变更(已提交 + 未提交)迁移到主 worktree
|
|
90
|
-
* 使用 git diff HEAD...branch --binary 获取变更,避免 stash 方式无法检测已提交 commit 的问题
|
|
91
|
-
* @param {string} targetWorktreePath - 目标 worktree 路径
|
|
92
|
-
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
93
|
-
* @param {string} branchName - 分支名
|
|
94
|
-
* @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
|
|
95
|
-
* @returns {{ success: boolean }} patch 迁移结果
|
|
96
|
-
*/
|
|
97
|
-
function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: string, branchName: string, hasUncommitted: boolean): { success: boolean } {
|
|
98
|
-
let didTempCommit = false;
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
// 如果有未提交修改,先做临时 commit 以便 diff 能捕获全部变更
|
|
102
|
-
if (hasUncommitted) {
|
|
103
|
-
gitAddAll(targetWorktreePath);
|
|
104
|
-
gitCommit('clawt:temp-commit-for-validate', targetWorktreePath);
|
|
105
|
-
didTempCommit = true;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// 在主 worktree 执行三点 diff,获取目标分支自分叉点以来的全量变更
|
|
109
|
-
const patch = gitDiffBinaryAgainstBranch(branchName, mainWorktreePath);
|
|
110
|
-
|
|
111
|
-
// 应用 patch 到主 worktree 工作目录
|
|
112
|
-
if (patch.length > 0) {
|
|
113
|
-
try {
|
|
114
|
-
gitApplyFromStdin(patch, mainWorktreePath);
|
|
115
|
-
} catch (error) {
|
|
116
|
-
logger.warn(`patch apply 失败: ${error}`);
|
|
117
|
-
printWarning(MESSAGES.VALIDATE_PATCH_APPLY_FAILED(branchName));
|
|
118
|
-
return { success: false };
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return { success: true };
|
|
123
|
-
} finally {
|
|
124
|
-
// 确保临时 commit 一定会被撤销,恢复目标 worktree 原状
|
|
125
|
-
// 每个操作独立 try-catch,避免前一个失败导致后续操作不执行
|
|
126
|
-
if (didTempCommit) {
|
|
127
|
-
try {
|
|
128
|
-
gitResetSoft(1, targetWorktreePath);
|
|
129
|
-
} catch (error) {
|
|
130
|
-
logger.error(`撤销临时 commit 失败: ${error}`);
|
|
131
|
-
}
|
|
132
|
-
try {
|
|
133
|
-
gitRestoreStaged(targetWorktreePath);
|
|
134
|
-
} catch (error) {
|
|
135
|
-
logger.error(`恢复暂存区失败: ${error}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
74
|
/**
|
|
142
75
|
* patch apply 失败后的交互处理:询问用户是否自动执行 sync
|
|
143
76
|
* @param {string} targetWorktreePath - 目标 worktree 路径
|
|
@@ -160,24 +93,6 @@ async function handlePatchApplyFailure(targetWorktreePath: string, branchName: s
|
|
|
160
93
|
// sync 冲突提示已在 executeSyncForBranch 内部输出(SYNC_CONFLICT),此处无需重复提示
|
|
161
94
|
}
|
|
162
95
|
|
|
163
|
-
/**
|
|
164
|
-
* 保存当前主 worktree 工作目录变更为 git tree 对象快照
|
|
165
|
-
* 操作序列:git add . → git write-tree → git restore --staged .
|
|
166
|
-
* 同时保存当前 HEAD commit hash,用于增量 validate 时对齐基准
|
|
167
|
-
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
168
|
-
* @param {string} projectName - 项目名
|
|
169
|
-
* @param {string} branchName - 分支名
|
|
170
|
-
* @returns {string} 生成的 tree hash
|
|
171
|
-
*/
|
|
172
|
-
function saveCurrentSnapshotTree(mainWorktreePath: string, projectName: string, branchName: string): string {
|
|
173
|
-
gitAddAll(mainWorktreePath);
|
|
174
|
-
const treeHash = gitWriteTree(mainWorktreePath);
|
|
175
|
-
gitRestoreStaged(mainWorktreePath);
|
|
176
|
-
const headCommitHash = getHeadCommitHash(mainWorktreePath);
|
|
177
|
-
writeSnapshot(projectName, branchName, treeHash, headCommitHash);
|
|
178
|
-
return treeHash;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
96
|
/**
|
|
182
97
|
* 处理 --clean 选项:清理 validate 状态
|
|
183
98
|
* @param {ValidateOptions} options - 命令选项
|
|
@@ -234,11 +149,7 @@ async function handleValidateClean(options: ValidateOptions): Promise<void> {
|
|
|
234
149
|
*/
|
|
235
150
|
async function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): Promise<void> {
|
|
236
151
|
// 切换主 worktree 到验证分支
|
|
237
|
-
const validateBranchName =
|
|
238
|
-
if (!checkBranchExists(validateBranchName)) {
|
|
239
|
-
throw new ClawtError(MESSAGES.VALIDATE_BRANCH_NOT_FOUND(validateBranchName, branchName));
|
|
240
|
-
}
|
|
241
|
-
gitCheckout(validateBranchName, mainWorktreePath);
|
|
152
|
+
const validateBranchName = switchToValidateBranch(branchName, mainWorktreePath);
|
|
242
153
|
|
|
243
154
|
// 通过 patch 迁移目标分支全量变更到主 worktree
|
|
244
155
|
const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
@@ -266,8 +177,8 @@ async function handleFirstValidate(targetWorktreePath: string, mainWorktreePath:
|
|
|
266
177
|
* @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
|
|
267
178
|
*/
|
|
268
179
|
async function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): Promise<void> {
|
|
269
|
-
// 步骤 1:读取旧快照(tree hash + 当时的 HEAD commit hash)
|
|
270
|
-
const { treeHash: oldTreeHash, headCommitHash: oldHeadCommitHash } = readSnapshot(projectName, branchName);
|
|
180
|
+
// 步骤 1:读取旧快照(tree hash + 当时的 HEAD commit hash + 暂存区 tree hash)
|
|
181
|
+
const { treeHash: oldTreeHash, headCommitHash: oldHeadCommitHash, stagedTreeHash: oldStagedTreeHash } = readSnapshot(projectName, branchName);
|
|
271
182
|
|
|
272
183
|
// 步骤 2:确保主 worktree 干净(调用方已通过 handleDirtyMainWorktree 处理)
|
|
273
184
|
// 这里做兜底清理,防止 handleDirtyMainWorktree 之后仍有残留
|
|
@@ -277,14 +188,7 @@ async function handleIncrementalValidate(targetWorktreePath: string, mainWorktre
|
|
|
277
188
|
}
|
|
278
189
|
|
|
279
190
|
// 步骤 3:切换到验证分支(如果已在该分支上则跳过)
|
|
280
|
-
const validateBranchName =
|
|
281
|
-
if (!checkBranchExists(validateBranchName)) {
|
|
282
|
-
throw new ClawtError(MESSAGES.VALIDATE_BRANCH_NOT_FOUND(validateBranchName, branchName));
|
|
283
|
-
}
|
|
284
|
-
const currentBranch = getCurrentBranch(mainWorktreePath);
|
|
285
|
-
if (currentBranch !== validateBranchName) {
|
|
286
|
-
gitCheckout(validateBranchName, mainWorktreePath);
|
|
287
|
-
}
|
|
191
|
+
const validateBranchName = switchToValidateBranch(branchName, mainWorktreePath);
|
|
288
192
|
|
|
289
193
|
// 步骤 4:通过 patch 从目标分支获取最新全量变更
|
|
290
194
|
const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
@@ -296,140 +200,57 @@ async function handleIncrementalValidate(targetWorktreePath: string, mainWorktre
|
|
|
296
200
|
return;
|
|
297
201
|
}
|
|
298
202
|
|
|
299
|
-
// 步骤 5
|
|
300
|
-
|
|
203
|
+
// 步骤 5:计算当前变更的 tree hash,检测是否有新变更
|
|
204
|
+
const newTreeHash = computeCurrentTreeHash(mainWorktreePath);
|
|
205
|
+
const currentHeadCommitHash = getHeadCommitHash(mainWorktreePath);
|
|
301
206
|
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if (oldChangePatch.length > 0 && gitApplyCachedCheck(oldChangePatch, mainWorktreePath)) {
|
|
314
|
-
// 无冲突:apply --cached 到当前 HEAD 暂存区
|
|
315
|
-
gitApplyCachedFromStdin(oldChangePatch, mainWorktreePath);
|
|
316
|
-
} else if (oldChangePatch.length > 0) {
|
|
317
|
-
// 有冲突:降级为全量模式(暂存区保持为空)
|
|
318
|
-
logger.warn('旧变更 patch 与当前 HEAD 冲突,降级为全量模式');
|
|
319
|
-
printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
|
|
320
|
-
printSuccess(MESSAGES.VALIDATE_SUCCESS_WITH_BRANCH(branchName, validateBranchName));
|
|
321
|
-
return;
|
|
207
|
+
// 检测目标 worktree 自上次 validate 以来是否有新变更
|
|
208
|
+
const hasNewChanges = newTreeHash !== oldTreeHash
|
|
209
|
+
|| (oldHeadCommitHash && oldHeadCommitHash !== currentHeadCommitHash);
|
|
210
|
+
|
|
211
|
+
if (!hasNewChanges) {
|
|
212
|
+
// 无新变更:不更新快照,恢复到上次 validate 结束时的完整状态
|
|
213
|
+
if (oldStagedTreeHash) {
|
|
214
|
+
try {
|
|
215
|
+
gitReadTree(oldStagedTreeHash, mainWorktreePath);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
logger.warn(`恢复暂存区失败: ${error}`);
|
|
322
218
|
}
|
|
323
|
-
// oldChangePatch 为空表示旧变更为空,暂存区保持干净即可
|
|
324
|
-
} else {
|
|
325
|
-
// HEAD 未变化(或旧版快照无 HEAD 信息):直接 read-tree 旧快照
|
|
326
|
-
gitReadTree(oldTreeHash, mainWorktreePath);
|
|
327
219
|
}
|
|
328
|
-
|
|
329
|
-
// 旧 tree 对象无法读取(可能被 git gc 回收),降级为全量模式
|
|
330
|
-
logger.warn(`增量 read-tree 失败: ${error}`);
|
|
331
|
-
printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
|
|
332
|
-
// 降级后暂存区保持为空,工作目录为最新全量变更,与首次 validate 一致
|
|
220
|
+
printInfo(MESSAGES.INCREMENTAL_VALIDATE_NO_CHANGES(branchName));
|
|
333
221
|
printSuccess(MESSAGES.VALIDATE_SUCCESS_WITH_BRANCH(branchName, validateBranchName));
|
|
334
222
|
return;
|
|
335
223
|
}
|
|
336
224
|
|
|
337
|
-
//
|
|
338
|
-
printSuccess(MESSAGES.INCREMENTAL_VALIDATE_SUCCESS(branchName));
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* 执行单个命令(同步方式,保持原有行为不变)
|
|
343
|
-
* @param {string} command - 要执行的命令字符串
|
|
344
|
-
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
345
|
-
*/
|
|
346
|
-
function executeSingleCommand(command: string, mainWorktreePath: string): void {
|
|
347
|
-
printInfo(MESSAGES.VALIDATE_RUN_START(command));
|
|
348
|
-
printSeparator();
|
|
349
|
-
|
|
350
|
-
const result = runCommandInherited(command, { cwd: mainWorktreePath });
|
|
351
|
-
|
|
352
|
-
printSeparator();
|
|
225
|
+
// 有新变更:执行暂存区载入并记录 stagedTreeHash
|
|
353
226
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
227
|
+
// 步骤 6:将旧变更状态载入暂存区
|
|
228
|
+
const stageResult = loadOldSnapshotToStage(oldTreeHash, oldHeadCommitHash, currentHeadCommitHash, mainWorktreePath);
|
|
229
|
+
if (!stageResult.success) {
|
|
230
|
+
printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
|
|
231
|
+
// 降级后暂存区保持为空,工作目录为最新全量变更,与首次 validate 一致
|
|
232
|
+
writeSnapshot(projectName, branchName, newTreeHash, currentHeadCommitHash, '');
|
|
233
|
+
printSuccess(MESSAGES.VALIDATE_SUCCESS_WITH_BRANCH(branchName, validateBranchName));
|
|
357
234
|
return;
|
|
358
235
|
}
|
|
359
236
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
printSuccess(MESSAGES.VALIDATE_RUN_SUCCESS(command));
|
|
363
|
-
} else {
|
|
364
|
-
printError(MESSAGES.VALIDATE_RUN_FAILED(command, exitCode));
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* 汇总输出并行命令的执行结果
|
|
370
|
-
* @param {ParallelCommandResult[]} results - 各命令的执行结果数组
|
|
371
|
-
*/
|
|
372
|
-
function reportParallelResults(results: ParallelCommandResult[]): void {
|
|
373
|
-
printSeparator();
|
|
374
|
-
|
|
375
|
-
const successCount = results.filter((r) => r.exitCode === 0 && !r.error).length;
|
|
376
|
-
const failedCount = results.length - successCount;
|
|
377
|
-
|
|
378
|
-
for (const result of results) {
|
|
379
|
-
if (result.error) {
|
|
380
|
-
printError(MESSAGES.VALIDATE_PARALLEL_CMD_ERROR(result.command, result.error));
|
|
381
|
-
} else if (result.exitCode === 0) {
|
|
382
|
-
printSuccess(MESSAGES.VALIDATE_PARALLEL_CMD_SUCCESS(result.command));
|
|
383
|
-
} else {
|
|
384
|
-
printError(MESSAGES.VALIDATE_PARALLEL_CMD_FAILED(result.command, result.exitCode));
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (failedCount === 0) {
|
|
389
|
-
printSuccess(MESSAGES.VALIDATE_PARALLEL_RUN_ALL_SUCCESS(results.length));
|
|
390
|
-
} else {
|
|
391
|
-
printError(MESSAGES.VALIDATE_PARALLEL_RUN_SUMMARY(successCount, failedCount));
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* 并行执行多个命令并汇总结果
|
|
397
|
-
* @param {string[]} commands - 要并行执行的命令数组
|
|
398
|
-
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
399
|
-
*/
|
|
400
|
-
async function executeParallelCommands(commands: string[], mainWorktreePath: string): Promise<void> {
|
|
401
|
-
printInfo(MESSAGES.VALIDATE_PARALLEL_RUN_START(commands.length));
|
|
402
|
-
|
|
403
|
-
for (let i = 0; i < commands.length; i++) {
|
|
404
|
-
printInfo(MESSAGES.VALIDATE_PARALLEL_CMD_START(i + 1, commands.length, commands[i]));
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
printSeparator();
|
|
237
|
+
// 步骤 7:写入新快照(包含 stagedTreeHash 供下次无变更时恢复用)
|
|
238
|
+
writeSnapshot(projectName, branchName, newTreeHash, currentHeadCommitHash, stageResult.stagedTreeHash);
|
|
408
239
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
reportParallelResults(results);
|
|
240
|
+
// 结果:暂存区=上次快照,工作目录=最新全量变更
|
|
241
|
+
printSuccess(MESSAGES.INCREMENTAL_VALIDATE_SUCCESS(branchName));
|
|
412
242
|
}
|
|
413
243
|
|
|
414
244
|
/**
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
* @param {string} command - 要执行的命令字符串
|
|
419
|
-
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
245
|
+
* 解析最终要执行的 run 命令:优先使用 -r 参数,否则从项目配置读取
|
|
246
|
+
* @param {string} [optionRun] - 用户通过 -r 传入的命令
|
|
247
|
+
* @returns {string | undefined} 最终要执行的命令,无配置时返回 undefined
|
|
420
248
|
*/
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const commands = parseParallelCommands(command);
|
|
425
|
-
|
|
426
|
-
if (commands.length <= 1) {
|
|
427
|
-
// 单命令(包括含 && 的串行命令),走原有同步路径
|
|
428
|
-
executeSingleCommand(commands[0] || command, mainWorktreePath);
|
|
429
|
-
} else {
|
|
430
|
-
// 多命令,并行执行
|
|
431
|
-
await executeParallelCommands(commands, mainWorktreePath);
|
|
249
|
+
function resolveRunCommand(optionRun?: string): string | undefined {
|
|
250
|
+
if (optionRun) {
|
|
251
|
+
return optionRun;
|
|
432
252
|
}
|
|
253
|
+
return getValidateRunCommand();
|
|
433
254
|
}
|
|
434
255
|
|
|
435
256
|
/**
|
|
@@ -484,8 +305,9 @@ async function handleValidate(options: ValidateOptions): Promise<void> {
|
|
|
484
305
|
await handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
485
306
|
}
|
|
486
307
|
|
|
487
|
-
// validate
|
|
488
|
-
|
|
489
|
-
|
|
308
|
+
// validate 成功后执行用户指定的命令(优先 -r 参数,否则从项目配置读取)
|
|
309
|
+
const runCommand = resolveRunCommand(options.run);
|
|
310
|
+
if (runCommand) {
|
|
311
|
+
await executeRunCommand(runCommand, mainWorktreePath);
|
|
490
312
|
}
|
|
491
313
|
}
|
package/src/constants/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { UPDATE_MESSAGES, UPDATE_COMMANDS } from './messages/update.js';
|
|
|
6
6
|
export { EXIT_CODES } from './exitCodes.js';
|
|
7
7
|
export { ENABLE_BRACKETED_PASTE, DISABLE_BRACKETED_PASTE, PASTE_THRESHOLD_MS, VALID_TERMINAL_APPS, ITERM2_APP_PATH } from './terminal.js';
|
|
8
8
|
export { DEFAULT_CONFIG, CONFIG_DESCRIPTIONS, CONFIG_DEFINITIONS, APPEND_SYSTEM_PROMPT } from './config.js';
|
|
9
|
+
export { PROJECT_CONFIG_DEFINITIONS, PROJECT_DEFAULT_CONFIG, PROJECT_CONFIG_DESCRIPTIONS } from './project-config.js';
|
|
9
10
|
export { AUTO_SAVE_COMMIT_MESSAGE } from './git.js';
|
|
10
11
|
export { DEBUG_LOG_PREFIX, DEBUG_TIMESTAMP_FORMAT } from './logger.js';
|
|
11
12
|
export { UPDATE_CHECK_INTERVAL_MS, NPM_REGISTRY_URL, NPM_REGISTRY_TIMEOUT_MS, PACKAGE_NAME } from './update.js';
|
|
@@ -15,4 +15,8 @@ export const INIT_MESSAGES = {
|
|
|
15
15
|
PROJECT_NOT_INITIALIZED: '项目尚未初始化,请先执行 clawt init 设置主工作分支',
|
|
16
16
|
/** 项目配置缺少 clawtMainWorkBranch 字段 */
|
|
17
17
|
PROJECT_CONFIG_MISSING_BRANCH: '项目配置缺少主工作分支信息,请重新执行 clawt init 设置主工作分支',
|
|
18
|
+
/** init show 交互式面板选择配置项提示 */
|
|
19
|
+
INIT_SELECT_PROMPT: '选择要修改的项目配置项',
|
|
20
|
+
/** init show 交互式面板配置项修改成功 */
|
|
21
|
+
INIT_SET_SUCCESS: (key: string, value: string) => `✓ 项目配置 ${key} 已设置为 ${value}`,
|
|
18
22
|
} as const;
|
|
@@ -8,6 +8,9 @@ export const VALIDATE_MESSAGES = {
|
|
|
8
8
|
`✓ 已将分支 ${branch} 的最新变更应用到主 worktree(增量模式)\n 暂存区 = 上次快照,工作目录 = 最新变更`,
|
|
9
9
|
/** 增量 validate 降级为全量模式提示 */
|
|
10
10
|
INCREMENTAL_VALIDATE_FALLBACK: '增量对比失败,已降级为全量模式',
|
|
11
|
+
/** 增量 validate 检测到目标 worktree 无新变更 */
|
|
12
|
+
INCREMENTAL_VALIDATE_NO_CHANGES: (branch: string) =>
|
|
13
|
+
`分支 ${branch} 自上次 validate 以来没有新的变更,已恢复到上次验证状态`,
|
|
11
14
|
/** validate 状态已清理 */
|
|
12
15
|
VALIDATE_CLEANED: (branch: string) => `✓ 分支 ${branch} 的 validate 状态已清理`,
|
|
13
16
|
/** validate patch apply 失败,提示用户同步主分支 */
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ProjectConfig, ProjectConfigDefinitions } from '../types/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 项目级配置项完整定义(单一数据源)
|
|
5
|
+
* 新增项目配置项时只需在此处维护,PROJECT_DEFAULT_CONFIG 和 PROJECT_CONFIG_DESCRIPTIONS 会自动同步
|
|
6
|
+
*/
|
|
7
|
+
export const PROJECT_CONFIG_DEFINITIONS: ProjectConfigDefinitions = {
|
|
8
|
+
clawtMainWorkBranch: {
|
|
9
|
+
defaultValue: '',
|
|
10
|
+
description: '主 worktree 的工作分支名',
|
|
11
|
+
},
|
|
12
|
+
validateRunCommand: {
|
|
13
|
+
defaultValue: undefined as unknown as string | undefined,
|
|
14
|
+
description: 'validate 成功后自动执行的命令(-r 的默认值)',
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 从 PROJECT_CONFIG_DEFINITIONS 派生默认配置
|
|
20
|
+
* @param {ProjectConfigDefinitions} definitions - 项目配置项完整定义
|
|
21
|
+
* @returns {Required<ProjectConfig>} 默认项目配置对象
|
|
22
|
+
*/
|
|
23
|
+
function deriveDefaultConfig(definitions: ProjectConfigDefinitions): Required<ProjectConfig> {
|
|
24
|
+
const entries = Object.entries(definitions).map(
|
|
25
|
+
([key, def]) => [key, def.defaultValue],
|
|
26
|
+
);
|
|
27
|
+
return Object.fromEntries(entries) as Required<ProjectConfig>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 从 PROJECT_CONFIG_DEFINITIONS 派生配置项描述映射
|
|
32
|
+
* @param {ProjectConfigDefinitions} definitions - 项目配置项完整定义
|
|
33
|
+
* @returns {Record<keyof Required<ProjectConfig>, string>} 配置项描述映射
|
|
34
|
+
*/
|
|
35
|
+
function deriveConfigDescriptions(definitions: ProjectConfigDefinitions): Record<keyof Required<ProjectConfig>, string> {
|
|
36
|
+
const entries = Object.entries(definitions).map(
|
|
37
|
+
([key, def]) => [key, def.description],
|
|
38
|
+
);
|
|
39
|
+
return Object.fromEntries(entries) as Record<keyof Required<ProjectConfig>, string>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 项目默认配置 */
|
|
43
|
+
export const PROJECT_DEFAULT_CONFIG: Required<ProjectConfig> = deriveDefaultConfig(PROJECT_CONFIG_DEFINITIONS);
|
|
44
|
+
|
|
45
|
+
/** 项目配置项描述映射 */
|
|
46
|
+
export const PROJECT_CONFIG_DESCRIPTIONS: Record<keyof Required<ProjectConfig>, string> = deriveConfigDescriptions(PROJECT_CONFIG_DEFINITIONS);
|
package/src/types/index.ts
CHANGED
|
@@ -6,4 +6,4 @@ export type { TaskResult, TaskSummary } from './taskResult.js';
|
|
|
6
6
|
export type { WorktreeDetailedStatus, MainWorktreeStatus, SnapshotInfo, SnapshotSummary, StatusResult } from './status.js';
|
|
7
7
|
export type { TaskFileEntry, ParseTaskFileOptions } from './taskFile.js';
|
|
8
8
|
export type { ProjectOverview, ProjectWorktreeDetail, ProjectDetailResult, ProjectsOverviewResult } from './project.js';
|
|
9
|
-
export type { ProjectConfig } from './projectConfig.js';
|
|
9
|
+
export type { ProjectConfig, ProjectConfigItemDefinition, ProjectConfigDefinitions } from './projectConfig.js';
|
|
@@ -2,4 +2,21 @@
|
|
|
2
2
|
export interface ProjectConfig {
|
|
3
3
|
/** 主 worktree 的工作分支名 */
|
|
4
4
|
clawtMainWorkBranch: string;
|
|
5
|
+
/** validate 成功后自动执行的命令(-r 的默认值) */
|
|
6
|
+
validateRunCommand?: string;
|
|
5
7
|
}
|
|
8
|
+
|
|
9
|
+
/** 单个项目配置项的完整定义(默认值 + 描述) */
|
|
10
|
+
export interface ProjectConfigItemDefinition<T> {
|
|
11
|
+
/** 默认值 */
|
|
12
|
+
defaultValue: T;
|
|
13
|
+
/** 配置项描述,用于交互式面板展示 */
|
|
14
|
+
description: string;
|
|
15
|
+
/** 可选:允许的枚举值列表,仅对 string 类型有效 */
|
|
16
|
+
allowedValues?: T extends string ? readonly string[] : never;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 所有项目配置项的完整定义映射 */
|
|
20
|
+
export type ProjectConfigDefinitions = {
|
|
21
|
+
[K in keyof Required<ProjectConfig>]: ProjectConfigItemDefinition<ProjectConfig[K]>;
|
|
22
|
+
};
|
|
@@ -73,14 +73,16 @@ export function parseConfigValue(
|
|
|
73
73
|
* - string + 有 allowedValues → Select(枚举列表)
|
|
74
74
|
* - string + 无 allowedValues → Input(自由输入)
|
|
75
75
|
*
|
|
76
|
-
* @param {
|
|
77
|
-
* @param {
|
|
78
|
-
* @
|
|
76
|
+
* @param {string} key - 配置项名称
|
|
77
|
+
* @param {unknown} currentValue - 当前值
|
|
78
|
+
* @param {readonly string[]} [allowedValues] - 可选的枚举值列表
|
|
79
|
+
* @returns {Promise<unknown>} 用户输入/选择的新值
|
|
79
80
|
*/
|
|
80
81
|
export async function promptConfigValue(
|
|
81
|
-
key:
|
|
82
|
-
currentValue:
|
|
83
|
-
|
|
82
|
+
key: string,
|
|
83
|
+
currentValue: unknown,
|
|
84
|
+
allowedValues?: readonly string[],
|
|
85
|
+
): Promise<unknown> {
|
|
84
86
|
const expectedType = typeof currentValue;
|
|
85
87
|
|
|
86
88
|
// 布尔类型策略
|
|
@@ -94,9 +96,8 @@ export async function promptConfigValue(
|
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
// 字符串类型:根据 allowedValues 自动选择提示策略
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return promptEnumValue(key, currentValue as string, definition.allowedValues);
|
|
99
|
+
if (allowedValues) {
|
|
100
|
+
return promptEnumValue(key, currentValue as string, allowedValues);
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
return promptStringValue(key, currentValue as string);
|
|
@@ -104,23 +105,70 @@ export async function promptConfigValue(
|
|
|
104
105
|
|
|
105
106
|
/**
|
|
106
107
|
* 格式化配置值的显示样式
|
|
107
|
-
* @param {
|
|
108
|
+
* @param {unknown} value - 配置值
|
|
108
109
|
* @returns {string} 格式化后的字符串
|
|
109
110
|
*/
|
|
110
|
-
export function formatConfigValue(value:
|
|
111
|
+
export function formatConfigValue(value: unknown): string {
|
|
112
|
+
if (value === undefined || value === null) {
|
|
113
|
+
return chalk.dim('(未设置)');
|
|
114
|
+
}
|
|
111
115
|
if (typeof value === 'boolean') {
|
|
112
116
|
return value ? chalk.green('true') : chalk.yellow('false');
|
|
113
117
|
}
|
|
114
118
|
return chalk.cyan(String(value));
|
|
115
119
|
}
|
|
116
120
|
|
|
121
|
+
/**
|
|
122
|
+
* 通用交互式配置编辑器
|
|
123
|
+
* @param {T} config - 当前配置对象
|
|
124
|
+
* @param {Record<string, { description: string; allowedValues?: readonly string[] }>} definitions - 配置项定义(含 description、allowedValues)
|
|
125
|
+
* @param {object} [options] - 可选配置
|
|
126
|
+
* @param {string} [options.selectPrompt] - 选择配置项的提示语
|
|
127
|
+
* @param {Record<string, string>} [options.disabledKeys] - 不可编辑的键及其禁用提示
|
|
128
|
+
* @returns {Promise<{ key: string; newValue: unknown }>} 修改后的 key 和 newValue
|
|
129
|
+
*/
|
|
130
|
+
export async function interactiveConfigEditor<T extends object>(
|
|
131
|
+
config: T,
|
|
132
|
+
definitions: Record<string, { description: string; allowedValues?: readonly string[] }>,
|
|
133
|
+
options?: { selectPrompt?: string; disabledKeys?: Record<string, string> },
|
|
134
|
+
): Promise<{ key: keyof T; newValue: unknown }> {
|
|
135
|
+
const keys = Object.keys(definitions);
|
|
136
|
+
const disabledKeys = options?.disabledKeys ?? {};
|
|
137
|
+
const configRecord = config as Record<string, unknown>;
|
|
138
|
+
|
|
139
|
+
// 构建选择列表,显示配置项名称、当前值和描述
|
|
140
|
+
const choices = keys.map((k) => {
|
|
141
|
+
const isDisabled = k in disabledKeys;
|
|
142
|
+
const value = configRecord[k];
|
|
143
|
+
const isObject = typeof value === 'object' && value !== null;
|
|
144
|
+
return {
|
|
145
|
+
name: k,
|
|
146
|
+
message: `${k}: ${isObject || isDisabled ? chalk.dim(isObject ? JSON.stringify(value) : String(value ?? '')) : formatConfigValue(value)} ${chalk.dim(`— ${definitions[k].description}`)}`,
|
|
147
|
+
...(isDisabled && { disabled: disabledKeys[k] }),
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// @ts-expect-error enquirer 类型声明未导出 Select 类,但运行时存在
|
|
152
|
+
const selectedKey: string = await new Enquirer.Select({
|
|
153
|
+
message: options?.selectPrompt ?? MESSAGES.CONFIG_SELECT_PROMPT,
|
|
154
|
+
choices,
|
|
155
|
+
}).run();
|
|
156
|
+
|
|
157
|
+
// 根据类型和 allowedValues 自动选择提示策略
|
|
158
|
+
const currentValue = configRecord[selectedKey];
|
|
159
|
+
const definition = definitions[selectedKey];
|
|
160
|
+
const newValue = await promptConfigValue(selectedKey, currentValue, definition.allowedValues);
|
|
161
|
+
|
|
162
|
+
return { key: selectedKey as keyof T, newValue };
|
|
163
|
+
}
|
|
164
|
+
|
|
117
165
|
/**
|
|
118
166
|
* 交互式布尔值选择(内部辅助函数)
|
|
119
|
-
* @param {
|
|
167
|
+
* @param {string} key - 配置项名称
|
|
120
168
|
* @param {boolean} currentValue - 当前值
|
|
121
169
|
* @returns {Promise<boolean>} 用户选择的布尔值
|
|
122
170
|
*/
|
|
123
|
-
async function promptBooleanValue(key:
|
|
171
|
+
async function promptBooleanValue(key: string, currentValue: boolean): Promise<boolean> {
|
|
124
172
|
const choices = [
|
|
125
173
|
{ name: 'true', message: 'true' },
|
|
126
174
|
{ name: 'false', message: 'false' },
|
|
@@ -138,11 +186,11 @@ async function promptBooleanValue(key: keyof ClawtConfig, currentValue: boolean)
|
|
|
138
186
|
|
|
139
187
|
/**
|
|
140
188
|
* 交互式数字输入(内部辅助函数)
|
|
141
|
-
* @param {
|
|
189
|
+
* @param {string} key - 配置项名称
|
|
142
190
|
* @param {number} currentValue - 当前值
|
|
143
191
|
* @returns {Promise<number>} 用户输入的数字值
|
|
144
192
|
*/
|
|
145
|
-
async function promptNumberValue(key:
|
|
193
|
+
async function promptNumberValue(key: string, currentValue: number): Promise<number> {
|
|
146
194
|
// @ts-expect-error enquirer 类型声明未导出 Input 类,但运行时存在
|
|
147
195
|
const input: string = await new Enquirer.Input({
|
|
148
196
|
message: MESSAGES.CONFIG_INPUT_PROMPT(key),
|
|
@@ -158,13 +206,13 @@ async function promptNumberValue(key: keyof ClawtConfig, currentValue: number):
|
|
|
158
206
|
|
|
159
207
|
/**
|
|
160
208
|
* 交互式枚举值选择(内部辅助函数,用于有 allowedValues 的 string 配置项)
|
|
161
|
-
* @param {
|
|
209
|
+
* @param {string} key - 配置项名称
|
|
162
210
|
* @param {string} currentValue - 当前值
|
|
163
211
|
* @param {readonly string[]} allowedValues - 允许的枚举值列表
|
|
164
212
|
* @returns {Promise<string>} 用户选择的枚举值
|
|
165
213
|
*/
|
|
166
214
|
async function promptEnumValue(
|
|
167
|
-
key:
|
|
215
|
+
key: string,
|
|
168
216
|
currentValue: string,
|
|
169
217
|
allowedValues: readonly string[],
|
|
170
218
|
): Promise<string> {
|
|
@@ -183,14 +231,14 @@ async function promptEnumValue(
|
|
|
183
231
|
|
|
184
232
|
/**
|
|
185
233
|
* 交互式字符串自由输入(内部辅助函数,用于无 allowedValues 的 string 配置项)
|
|
186
|
-
* @param {
|
|
234
|
+
* @param {string} key - 配置项名称
|
|
187
235
|
* @param {string} currentValue - 当前值
|
|
188
236
|
* @returns {Promise<string>} 用户输入的字符串值
|
|
189
237
|
*/
|
|
190
|
-
async function promptStringValue(key:
|
|
238
|
+
async function promptStringValue(key: string, currentValue: string): Promise<string> {
|
|
191
239
|
// @ts-expect-error enquirer 类型声明未导出 Input 类,但运行时存在
|
|
192
240
|
return await new Enquirer.Input({
|
|
193
241
|
message: MESSAGES.CONFIG_INPUT_PROMPT(key),
|
|
194
|
-
initial: currentValue,
|
|
242
|
+
initial: currentValue || '',
|
|
195
243
|
}).run();
|
|
196
244
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -68,11 +68,13 @@ export type { ParsedActivity, StreamEvent, LineBuffer } from './stream-parser.js
|
|
|
68
68
|
export { detectTerminalApp, openCommandInNewTerminalTab } from './terminal.js';
|
|
69
69
|
export { truncateTaskDesc, printDryRunPreview } from './dry-run.js';
|
|
70
70
|
export { applyAliases } from './alias.js';
|
|
71
|
-
export { isValidConfigKey, getValidConfigKeys, parseConfigValue, promptConfigValue, formatConfigValue } from './config-strategy.js';
|
|
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 } from './project-config.js';
|
|
73
|
+
export { getProjectConfigPath, loadProjectConfig, saveProjectConfig, requireProjectConfig, getMainWorkBranch, 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
|
+
export { executeRunCommand } from './validate-runner.js';
|
|
77
|
+
export { migrateChangesViaPatch, computeCurrentTreeHash, saveCurrentSnapshotTree, loadOldSnapshotToStage, switchToValidateBranch } from './validate-core.js';
|
|
76
78
|
export { InteractivePanel } from './interactive-panel.js';
|
|
77
79
|
export { buildPanelFrame, buildGroupedWorktreeLines, buildDisplayOrder, renderDateSeparator, renderWorktreeBlock, renderSnapshotSummary, renderFooter, calculateVisibleRows } from './interactive-panel-render.js';
|
|
78
80
|
export type { PanelLine } from './interactive-panel-render.js';
|
|
@@ -75,3 +75,12 @@ export function getMainWorkBranch(): string {
|
|
|
75
75
|
const config = requireProjectConfig();
|
|
76
76
|
return config.clawtMainWorkBranch;
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 从项目配置中获取 validate 成功后自动执行的命令
|
|
81
|
+
* @returns {string | undefined} 配置的命令字符串,未配置时返回 undefined
|
|
82
|
+
*/
|
|
83
|
+
export function getValidateRunCommand(): string | undefined {
|
|
84
|
+
const config = loadProjectConfig();
|
|
85
|
+
return config?.validateRunCommand || undefined;
|
|
86
|
+
}
|