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.
@@ -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, ParallelCommandResult } from '../utils/index.js';
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 = getValidateBranchName(branchName);
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 = getValidateBranchName(branchName);
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:保存最新快照为 git tree 对象(同时记录当前 HEAD)
300
- saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
203
+ // 步骤 5:计算当前变更的 tree hash,检测是否有新变更
204
+ const newTreeHash = computeCurrentTreeHash(mainWorktreePath);
205
+ const currentHeadCommitHash = getHeadCommitHash(mainWorktreePath);
301
206
 
302
- // 步骤 6:将旧变更状态载入暂存区
303
- try {
304
- const currentHeadCommitHash = getHeadCommitHash(mainWorktreePath);
305
-
306
- if (oldHeadCommitHash && oldHeadCommitHash !== currentHeadCommitHash) {
307
- // HEAD 发生了变化:
308
- // 将旧变更 patch(旧 tree 相对于旧 HEAD 的差异)重放到当前 HEAD 暂存区上,
309
- // 避免新旧 tree 基准不同导致 diff 混入 HEAD 变化的内容
310
- const oldHeadTreeHash = getCommitTreeHash(oldHeadCommitHash, mainWorktreePath);
311
- const oldChangePatch = gitDiffTree(oldHeadTreeHash, oldTreeHash, mainWorktreePath);
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
- } catch (error) {
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
- if (result.error) {
355
- // 进程启动失败(如命令不存在)
356
- printError(MESSAGES.VALIDATE_RUN_ERROR(command, result.error.message));
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
- const exitCode = result.status ?? 1;
361
- if (exitCode === 0) {
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
- const results = await runParallelCommands(commands, { cwd: mainWorktreePath });
410
-
411
- reportParallelResults(results);
240
+ // 结果:暂存区=上次快照,工作目录=最新全量变更
241
+ printSuccess(MESSAGES.INCREMENTAL_VALIDATE_SUCCESS(branchName));
412
242
  }
413
243
 
414
244
  /**
415
- * 在主 worktree 中执行用户指定的命令
416
- * 根据命令字符串中的 & 分隔符决定是单命令执行还是并行执行
417
- * 命令执行失败不影响 validate 本身的结果,仅输出提示
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
- async function executeRunCommand(command: string, mainWorktreePath: string): Promise<void> {
422
- printInfo('');
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
- if (options.run) {
489
- await executeRunCommand(options.run, mainWorktreePath);
308
+ // validate 成功后执行用户指定的命令(优先 -r 参数,否则从项目配置读取)
309
+ const runCommand = resolveRunCommand(options.run);
310
+ if (runCommand) {
311
+ await executeRunCommand(runCommand, mainWorktreePath);
490
312
  }
491
313
  }
@@ -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);
@@ -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 {keyof ClawtConfig} key - 配置项名称
77
- * @param {ClawtConfig[keyof ClawtConfig]} currentValue - 当前值
78
- * @returns {Promise<ClawtConfig[keyof ClawtConfig]>} 用户输入/选择的新值
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: keyof ClawtConfig,
82
- currentValue: ClawtConfig[keyof ClawtConfig],
83
- ): Promise<ClawtConfig[keyof ClawtConfig]> {
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
- const definition = CONFIG_DEFINITIONS[key];
98
- if (definition.allowedValues) {
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 {ClawtConfig[keyof ClawtConfig]} value - 配置值
108
+ * @param {unknown} value - 配置值
108
109
  * @returns {string} 格式化后的字符串
109
110
  */
110
- export function formatConfigValue(value: ClawtConfig[keyof ClawtConfig]): string {
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 {keyof ClawtConfig} key - 配置项名称
167
+ * @param {string} key - 配置项名称
120
168
  * @param {boolean} currentValue - 当前值
121
169
  * @returns {Promise<boolean>} 用户选择的布尔值
122
170
  */
123
- async function promptBooleanValue(key: keyof ClawtConfig, currentValue: boolean): Promise<boolean> {
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 {keyof ClawtConfig} key - 配置项名称
189
+ * @param {string} key - 配置项名称
142
190
  * @param {number} currentValue - 当前值
143
191
  * @returns {Promise<number>} 用户输入的数字值
144
192
  */
145
- async function promptNumberValue(key: keyof ClawtConfig, currentValue: number): Promise<number> {
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 {keyof ClawtConfig} key - 配置项名称
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: keyof ClawtConfig,
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 {keyof ClawtConfig} key - 配置项名称
234
+ * @param {string} key - 配置项名称
187
235
  * @param {string} currentValue - 当前值
188
236
  * @returns {Promise<string>} 用户输入的字符串值
189
237
  */
190
- async function promptStringValue(key: keyof ClawtConfig, currentValue: string): Promise<string> {
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
  }
@@ -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
+ }