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.
@@ -171,6 +171,8 @@ var VALIDATE_MESSAGES = {
171
171
  \u6682\u5B58\u533A = \u4E0A\u6B21\u5FEB\u7167\uFF0C\u5DE5\u4F5C\u76EE\u5F55 = \u6700\u65B0\u53D8\u66F4`,
172
172
  /** 增量 validate 降级为全量模式提示 */
173
173
  INCREMENTAL_VALIDATE_FALLBACK: "\u589E\u91CF\u5BF9\u6BD4\u5931\u8D25\uFF0C\u5DF2\u964D\u7EA7\u4E3A\u5168\u91CF\u6A21\u5F0F",
174
+ /** 增量 validate 检测到目标 worktree 无新变更 */
175
+ INCREMENTAL_VALIDATE_NO_CHANGES: (branch) => `\u5206\u652F ${branch} \u81EA\u4E0A\u6B21 validate \u4EE5\u6765\u6CA1\u6709\u65B0\u7684\u53D8\u66F4\uFF0C\u5DF2\u6062\u590D\u5230\u4E0A\u6B21\u9A8C\u8BC1\u72B6\u6001`,
174
176
  /** validate 状态已清理 */
175
177
  VALIDATE_CLEANED: (branch) => `\u2713 \u5206\u652F ${branch} \u7684 validate \u72B6\u6001\u5DF2\u6E05\u7406`,
176
178
  /** validate patch apply 失败,提示用户同步主分支 */
@@ -437,7 +439,11 @@ var INIT_MESSAGES = {
437
439
  /** 项目未初始化(requireProjectConfig 使用) */
438
440
  PROJECT_NOT_INITIALIZED: "\u9879\u76EE\u5C1A\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u6267\u884C clawt init \u8BBE\u7F6E\u4E3B\u5DE5\u4F5C\u5206\u652F",
439
441
  /** 项目配置缺少 clawtMainWorkBranch 字段 */
440
- PROJECT_CONFIG_MISSING_BRANCH: "\u9879\u76EE\u914D\u7F6E\u7F3A\u5C11\u4E3B\u5DE5\u4F5C\u5206\u652F\u4FE1\u606F\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C clawt init \u8BBE\u7F6E\u4E3B\u5DE5\u4F5C\u5206\u652F"
442
+ PROJECT_CONFIG_MISSING_BRANCH: "\u9879\u76EE\u914D\u7F6E\u7F3A\u5C11\u4E3B\u5DE5\u4F5C\u5206\u652F\u4FE1\u606F\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C clawt init \u8BBE\u7F6E\u4E3B\u5DE5\u4F5C\u5206\u652F",
443
+ /** init show 交互式面板选择配置项提示 */
444
+ INIT_SELECT_PROMPT: "\u9009\u62E9\u8981\u4FEE\u6539\u7684\u9879\u76EE\u914D\u7F6E\u9879",
445
+ /** init show 交互式面板配置项修改成功 */
446
+ INIT_SET_SUCCESS: (key, value) => `\u2713 \u9879\u76EE\u914D\u7F6E ${key} \u5DF2\u8BBE\u7F6E\u4E3A ${value}`
441
447
  };
442
448
 
443
449
  // src/constants/messages/interactive-panel.ts
@@ -553,6 +559,32 @@ function deriveConfigDescriptions(definitions) {
553
559
  var DEFAULT_CONFIG = deriveDefaultConfig(CONFIG_DEFINITIONS);
554
560
  var CONFIG_DESCRIPTIONS = deriveConfigDescriptions(CONFIG_DEFINITIONS);
555
561
 
562
+ // src/constants/project-config.ts
563
+ var PROJECT_CONFIG_DEFINITIONS = {
564
+ clawtMainWorkBranch: {
565
+ defaultValue: "",
566
+ description: "\u4E3B worktree \u7684\u5DE5\u4F5C\u5206\u652F\u540D"
567
+ },
568
+ validateRunCommand: {
569
+ defaultValue: void 0,
570
+ description: "validate \u6210\u529F\u540E\u81EA\u52A8\u6267\u884C\u7684\u547D\u4EE4\uFF08-r \u7684\u9ED8\u8BA4\u503C\uFF09"
571
+ }
572
+ };
573
+ function deriveDefaultConfig2(definitions) {
574
+ const entries = Object.entries(definitions).map(
575
+ ([key, def]) => [key, def.defaultValue]
576
+ );
577
+ return Object.fromEntries(entries);
578
+ }
579
+ function deriveConfigDescriptions2(definitions) {
580
+ const entries = Object.entries(definitions).map(
581
+ ([key, def]) => [key, def.description]
582
+ );
583
+ return Object.fromEntries(entries);
584
+ }
585
+ var PROJECT_DEFAULT_CONFIG = deriveDefaultConfig2(PROJECT_CONFIG_DEFINITIONS);
586
+ var PROJECT_CONFIG_DESCRIPTIONS = deriveConfigDescriptions2(PROJECT_CONFIG_DEFINITIONS);
587
+
556
588
  // src/constants/update.ts
557
589
  var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
558
590
 
package/docs/config.md CHANGED
@@ -85,7 +85,8 @@ clawt config reset
85
85
 
86
86
  - 配置项类型定义:`ConfigItemDefinition` 新增可选字段 `allowedValues`(`readonly string[]`),仅对 string 类型有效,用于枚举值校验和交互式 Select 提示
87
87
  - 值解析与提示策略:`src/utils/config-strategy.ts` 中的 `parseConfigValue()`(CLI 字符串解析)和 `promptConfigValue()`(交互式提示),基于类型和 `allowedValues` 自动分发
88
+ - 交互式配置编辑:`handleInteractiveConfigSet` 调用通用的 `interactiveConfigEditor`(`src/utils/config-strategy.ts`),传入 `CONFIG_DEFINITIONS` 和 `disabledKeys`(对象类型配置项禁用映射),不再在 config 命令中直接构建选择列表和调用 `promptConfigValue`
88
89
  - `saveConfig(config)`:`src/utils/config.ts` 中新增的通用配置写入函数,将完整配置对象持久化到文件
89
- - `formatConfigValue(value)`:支持 boolean(绿色/黄色)和 string/number(青色)的格式化显示。对象类型配置项(如 `aliases`)在交互式列表中通过 `JSON.stringify` 以暗淡色显示
90
+ - `formatConfigValue(value)`:支持 boolean(绿色/黄色)和 string/number(青色)的格式化显示。`undefined` / `null` 值显示为暗淡色的 `(未设置)`。对象类型配置项(如 `aliases`)在交互式列表中通过 `JSON.stringify` 以暗淡色显示
90
91
 
91
92
  ---
package/docs/init.md CHANGED
@@ -18,11 +18,11 @@ clawt init show
18
18
  | 参数/子命令 | 必填 | 说明 |
19
19
  | --- | --- | --- |
20
20
  | `-b` | 否 | 指定主工作分支名。不传则使用当前分支 |
21
- | `show` | 否 | 查看当前项目的 init 配置 |
21
+ | `show` | 否 | 交互式查看和修改项目配置 |
22
22
 
23
23
  **功能说明:**
24
24
 
25
- 初始化项目级配置,将指定分支记录为该项目的主工作分支(`clawtMainWorkBranch`)。该配置用于 `create` / `run` 时检测当前分支是否为主工作分支,并在偏离时提醒用户。详见 [2.6 项目级配置](#26-项目级配置)。
25
+ 初始化项目级配置,将指定分支记录为该项目的主工作分支(`clawtMainWorkBranch`)。该配置用于 `create` / `run` 时检测当前分支是否为主工作分支,并在偏离时提醒用户。`init show` 子命令提供交互式面板,可查看和修改所有项目配置项(如 `validateRunCommand`)。项目级配置的完整说明见 [project-config.md](./project-config.md)。
26
26
 
27
27
  **运行流程(设置模式):**
28
28
 
@@ -40,7 +40,12 @@ clawt init show
40
40
  1. **主 worktree 校验** (2.1)
41
41
  2. **读取项目级配置**:读取 `~/.clawt/projects/<projectName>/config.json`
42
42
  - 配置不存在 → 抛出错误 `项目尚未初始化,请先执行 clawt init 设置主工作分支`
43
- - 配置存在 → 以 JSON 格式输出配置内容
43
+ - 配置存在 → 进入交互式面板
44
+ 3. **交互式配置编辑**:调用 `interactiveConfigEditor`(`src/utils/config-strategy.ts`),基于 `PROJECT_CONFIG_DEFINITIONS` 构建配置项列表(详见 [project-config.md](./project-config.md))
45
+ - 列出所有项目配置项,显示名称、当前值和描述
46
+ - 用户选择配置项后,根据值类型自动选择输入方式(与全局配置的交互式编辑逻辑一致)
47
+ 4. **持久化修改**:将修改后的值合并到当前配置并写入配置文件
48
+ 5. **输出成功提示**:`✓ 项目配置 <key> 已设置为 <value>`
44
49
 
45
50
  **输出格式:**
46
51
 
@@ -51,10 +56,8 @@ clawt init show
51
56
  # 更新已有配置
52
57
  ✓ 已将主工作分支从 develop 更新为 main
53
58
 
54
- # show 查看配置(JSON 格式输出)
55
- {
56
- "clawtMainWorkBranch": "main"
57
- }
59
+ # show 交互式修改成功
60
+ ✓ 项目配置 validateRunCommand 已设置为 npm test
58
61
 
59
62
  # show 未初始化(抛出错误)
60
63
  项目尚未初始化,请先执行 clawt init 设置主工作分支
@@ -62,4 +65,10 @@ clawt init show
62
65
 
63
66
  **重复执行:** 支持重复执行,后一次覆盖前一次的配置。
64
67
 
68
+ **实现要点:**
69
+
70
+ - `init show` 子命令从 JSON 展示改为交互式面板,调用 `interactiveConfigEditor`(`src/utils/config-strategy.ts`)实现通用交互式配置编辑
71
+ - 配置项定义来自 `PROJECT_CONFIG_DEFINITIONS`(`src/constants/project-config.ts`),详见 [项目级配置文档](./project-config.md)
72
+ - 消息常量:`MESSAGES.INIT_SELECT_PROMPT`(选择配置项提示语)、`MESSAGES.INIT_SET_SUCCESS`(修改成功提示),定义在 `src/constants/messages/init.ts`
73
+
65
74
  ---
@@ -0,0 +1,132 @@
1
+ ### 项目级配置
2
+
3
+ #### 概述
4
+
5
+ 项目级配置是每个 Git 项目独立的 clawt 配置,用于记录该项目特有的设置(如主工作分支名、validate 自动运行命令等)。与全局配置(`~/.clawt/config.json`,见 [config-file.md](./config-file.md))不同,项目级配置按项目隔离存储。
6
+
7
+ #### 存放路径
8
+
9
+ ```
10
+ ~/.clawt/projects/<projectName>/config.json
11
+ ```
12
+
13
+ 其中 `<projectName>` 为项目根目录的目录名(通过 `git rev-parse --show-toplevel` 获取后取 `basename`)。
14
+
15
+ #### 配置项列表
16
+
17
+ ```json
18
+ {
19
+ "clawtMainWorkBranch": "main",
20
+ "validateRunCommand": "npm test"
21
+ }
22
+ ```
23
+
24
+ | 配置项 | 类型 | 必填 | 默认值 | 说明 |
25
+ | --- | --- | --- | --- | --- |
26
+ | `clawtMainWorkBranch` | `string` | 是 | `""` | 项目的主工作分支名,用于 create 时检测当前分支是否为主分支,以及 sync、merge 等命令获取主分支名 |
27
+ | `validateRunCommand` | `string` | 否 | `undefined` | validate 成功后自动执行的命令(作为 `-r` 选项的默认值)。不传 `-r` 时,validate 命令会自动从此项读取 |
28
+
29
+ #### 配置项定义数据源
30
+
31
+ 项目级配置项的完整定义集中在 `src/constants/project-config.ts` 中的 `PROJECT_CONFIG_DEFINITIONS` 常量,作为**单一数据源**(Single Source of Truth)。新增项目配置项只需在此处维护,`PROJECT_DEFAULT_CONFIG` 和 `PROJECT_CONFIG_DESCRIPTIONS` 会自动从中派生:
32
+
33
+ ```typescript
34
+ // src/constants/project-config.ts
35
+
36
+ export const PROJECT_CONFIG_DEFINITIONS: ProjectConfigDefinitions = {
37
+ clawtMainWorkBranch: {
38
+ defaultValue: '',
39
+ description: '主 worktree 的工作分支名',
40
+ },
41
+ validateRunCommand: {
42
+ defaultValue: undefined as unknown as string | undefined,
43
+ description: 'validate 成功后自动执行的命令(-r 的默认值)',
44
+ },
45
+ };
46
+
47
+ /** 项目默认配置(从 PROJECT_CONFIG_DEFINITIONS 自动派生) */
48
+ export const PROJECT_DEFAULT_CONFIG: Required<ProjectConfig> = deriveDefaultConfig(PROJECT_CONFIG_DEFINITIONS);
49
+
50
+ /** 项目配置项描述映射(从 PROJECT_CONFIG_DEFINITIONS 自动派生) */
51
+ export const PROJECT_CONFIG_DESCRIPTIONS: Record<keyof Required<ProjectConfig>, string> = deriveConfigDescriptions(PROJECT_CONFIG_DEFINITIONS);
52
+ ```
53
+
54
+ #### 相关类型定义
55
+
56
+ 类型定义位于 `src/types/projectConfig.ts`:
57
+
58
+ | 类型 | 说明 |
59
+ | --- | --- |
60
+ | `ProjectConfig` | 项目级配置接口,包含 `clawtMainWorkBranch`(必填)和 `validateRunCommand`(可选) |
61
+ | `ProjectConfigItemDefinition<T>` | 单个配置项定义,含 `defaultValue`(默认值)、`description`(描述)、可选 `allowedValues`(枚举值列表,仅对 string 类型有效) |
62
+ | `ProjectConfigDefinitions` | 所有配置项的完整定义映射,键为 `ProjectConfig` 的所有属性名,值为对应的 `ProjectConfigItemDefinition` |
63
+
64
+ ```typescript
65
+ // src/types/projectConfig.ts
66
+
67
+ export interface ProjectConfig {
68
+ clawtMainWorkBranch: string;
69
+ validateRunCommand?: string;
70
+ }
71
+
72
+ export interface ProjectConfigItemDefinition<T> {
73
+ defaultValue: T;
74
+ description: string;
75
+ allowedValues?: T extends string ? readonly string[] : never;
76
+ }
77
+
78
+ export type ProjectConfigDefinitions = {
79
+ [K in keyof Required<ProjectConfig>]: ProjectConfigItemDefinition<ProjectConfig[K]>;
80
+ };
81
+ ```
82
+
83
+ #### 工具函数
84
+
85
+ 工具函数位于 `src/utils/project-config.ts`:
86
+
87
+ | 函数 | 签名 | 说明 |
88
+ | --- | --- | --- |
89
+ | `getProjectConfigPath` | `(projectName: string) => string` | 获取项目配置文件的完整路径 |
90
+ | `loadProjectConfig` | `() => ProjectConfig \| null` | 加载当前项目的配置,配置文件不存在或解析失败时返回 `null` |
91
+ | `saveProjectConfig` | `(config: ProjectConfig) => void` | 保存项目配置到文件,自动创建项目子目录 |
92
+ | `requireProjectConfig` | `() => ProjectConfig` | 获取当前项目配置,不存在或缺少 `clawtMainWorkBranch` 时抛出 `ClawtError` |
93
+ | `getMainWorkBranch` | `() => string` | 从项目配置中获取主工作分支名(内部调用 `requireProjectConfig`) |
94
+ | `getValidateRunCommand` | `() => string \| undefined` | 从项目配置中获取 validate 自动执行命令,未配置时返回 `undefined` |
95
+
96
+ #### 设置方式
97
+
98
+ 通过 `clawt init` 命令设置(详见 [init.md](./init.md)):
99
+
100
+ ```bash
101
+ # 以当前分支作为主工作分支初始化
102
+ clawt init
103
+
104
+ # 指定主工作分支名
105
+ clawt init -b <branchName>
106
+
107
+ # 交互式查看和修改所有项目配置项
108
+ clawt init show
109
+ ```
110
+
111
+ `init show` 子命令调用通用的 `interactiveConfigEditor`(`src/utils/config-strategy.ts`),基于 `PROJECT_CONFIG_DEFINITIONS` 构建配置项列表,提供交互式面板供用户查看和修改所有项目配置项。
112
+
113
+ #### 前置校验
114
+
115
+ 除 `clawt init` 以外的所有核心命令(create、run、validate、sync、remove、merge、reset),执行时都会校验项目级配置是否存在。如果未执行过 `clawt init`,命令会直接报错并提示用户先初始化:
116
+
117
+ ```
118
+ ✗ 该项目尚未初始化,请先执行 clawt init -b<branchName>设置主工作分支
119
+ ```
120
+
121
+ > **实现细节**:`ensureOnMainWorkBranch()` 内部已通过 `getMainWorkBranch()` -> `requireProjectConfig()` 完成了项目配置校验,因此调用了 `ensureOnMainWorkBranch` 的命令(create、run、validate、sync、remove、merge)无需再显式调用 `requireProjectConfig()`,避免重复校验。仅 reset 命令因不调用 `ensureOnMainWorkBranch`,需要自行调用 `requireProjectConfig()`。
122
+
123
+ #### 路径常量
124
+
125
+ 在 `src/constants/paths.ts` 中定义:
126
+
127
+ ```typescript
128
+ /** 项目级配置目录 ~/.clawt/projects/ */
129
+ export const PROJECTS_CONFIG_DIR = join(CLAWT_HOME, 'projects');
130
+ ```
131
+
132
+ ---
package/docs/spec.md CHANGED
@@ -10,7 +10,8 @@
10
10
  - [1. 技术栈](#1-技术栈)
11
11
  - [2. 核心概念](#2-核心概念)
12
12
  - [2.5 验证分支](#25-验证分支)
13
- - [2.6 项目级配置](#26-项目级配置)
13
+ - [2.6 项目级配置](#26-项目级配置)(详见 [project-config.md](./project-config.md))
14
+ - [2.7 通用交互式配置编辑器](#27-通用交互式配置编辑器)
14
15
  - [3. 全局目录结构](#3-全局目录结构)
15
16
  - [4. 命令总览](#4-命令总览)
16
17
  - [5. 需求场景详细设计](#5-需求场景详细设计)
@@ -197,38 +198,46 @@ export const VALIDATE_BRANCH_PREFIX = 'clawt-validate-';
197
198
 
198
199
  ### 2.6 项目级配置
199
200
 
200
- #### 存放位置
201
+ 每个 Git 项目独立的 clawt 配置,存放在 `~/.clawt/projects/<projectName>/config.json`。包含项目的主工作分支名(`clawtMainWorkBranch`)、validate 自动运行命令(`validateRunCommand`)等配置项。通过 `clawt init` 命令设置,核心命令执行前会校验该配置是否存在。
201
202
 
202
- ```
203
- ~/.clawt/projects/<projectName>/config.json
204
- ```
203
+ 详细的配置项列表、类型定义、工具函数和设置方式见 [项目级配置文档](./project-config.md)。
205
204
 
206
- #### 配置内容
205
+ ### 2.7 通用交互式配置编辑器
207
206
 
208
- ```json
209
- {
210
- "clawtMainWorkBranch": "main"
211
- }
207
+ `interactiveConfigEditor`(`src/utils/config-strategy.ts`)是一个通用的交互式配置编辑函数,供全局配置(`config` 命令)和项目级配置(`init show` 子命令)复用。
208
+
209
+ **函数签名:**
210
+
211
+ ```typescript
212
+ async function interactiveConfigEditor<T extends object>(
213
+ config: T,
214
+ definitions: Record<string, { description: string; allowedValues?: readonly string[] }>,
215
+ options?: { selectPrompt?: string; disabledKeys?: Record<string, string> },
216
+ ): Promise<{ key: keyof T; newValue: unknown }>
212
217
  ```
213
218
 
214
- | 配置项 | 类型 | 说明 |
215
- | --- | --- | --- |
216
- | `clawtMainWorkBranch` | `string` | 项目的主工作分支名,用于 create 时检测当前分支是否为主分支 |
219
+ **参数说明:**
217
220
 
218
- #### 设置方式
221
+ | 参数 | 类型 | 说明 |
222
+ | --- | --- | --- |
223
+ | `config` | `T` | 当前配置对象 |
224
+ | `definitions` | `Record<string, { description; allowedValues? }>` | 配置项定义映射(含描述和可选枚举值) |
225
+ | `options.selectPrompt` | `string` | 可选,选择配置项的提示语(默认使用全局配置的提示语) |
226
+ | `options.disabledKeys` | `Record<string, string>` | 可选,不可编辑的键及其禁用提示文本 |
219
227
 
220
- 通过 `clawt init` 命令设置(见 [5.19 初始化项目级配置](#519-初始化项目级配置))。
228
+ **行为:**
221
229
 
222
- `clawt init` 以外的所有核心命令(create、run、validate、sync、remove、merge、reset),执行时都会校验项目级配置是否存在。如果未执行过 `clawt init`,命令会直接报错并提示用户先初始化。
230
+ 1. 根据 `definitions` 构建选择列表,显示配置项名称、当前值(通过 `formatConfigValue` 格式化)和描述
231
+ 2. `disabledKeys` 中的配置项标灰不可选,显示禁用提示
232
+ 3. 用户选择配置项后,根据值类型自动选择输入方式(布尔 → Select、数字 → Input、字符串+枚举 → Select、字符串 → Input)
233
+ 4. 返回用户修改的 key 和新值,由调用方负责持久化
223
234
 
224
- #### 路径常量
235
+ **调用场景:**
225
236
 
226
- `src/constants/paths.ts` 中新增:
237
+ - `config` 命令:传入 `loadConfig()` + `CONFIG_DEFINITIONS` + `disabledKeys`(对象类型配置项禁用)
238
+ - `init show`:传入 `requireProjectConfig()` + `PROJECT_CONFIG_DEFINITIONS` + `selectPrompt`
227
239
 
228
- ```typescript
229
- /** 项目级配置目录 ~/.clawt/projects/ */
230
- export const PROJECTS_CONFIG_DIR = join(CLAWT_HOME, 'projects');
231
- ```
240
+ 同时,`promptConfigValue` 和 `formatConfigValue` 的类型签名已从 `ClawtConfig` 专用类型放宽为通用类型(`string` / `unknown`),以支持不同配置体系复用。`formatConfigValue` 新增了 `undefined` / `null` 值的处理,显示为暗淡色的 `(未设置)`。
232
241
 
233
242
  ---
234
243
 
@@ -245,6 +254,7 @@ export const PROJECTS_CONFIG_DIR = join(CLAWT_HOME, 'projects');
245
254
  │ └── <project-name>/ # 以项目名分组
246
255
  │ ├── <branchName>.tree # 每个分支一个 tree hash 快照文件(存储 git tree 对象的 hash)
247
256
  │ ├── <branchName>.head # 每个分支一个 HEAD commit hash 快照文件(存储快照时验证分支的 HEAD commit hash)
257
+ │ ├── <branchName>.staged # 每个分支一个 staged tree hash 快照文件(存储 validate 结束时暂存区对应的 tree hash,用于无变更时恢复)
248
258
  │ └── ...
249
259
  ├── projects/<project-name>/ # 项目级配置目录
250
260
  │ └── config.json # 项目级配置(含 clawtMainWorkBranch)
package/docs/validate.md CHANGED
@@ -16,7 +16,7 @@ clawt validate [--clean] [-r <command>]
16
16
  | ------------- | ---- | ------------------------------------------------------------------------ |
17
17
  | `-b` | 否 | 要验证的 worktree 分支名(支持模糊匹配,不传则列出所有分支供选择) |
18
18
  | `--clean` | 否 | 清理 validate 状态(重置主 worktree 并删除快照) |
19
- | `-r, --run` | 否 | validate 成功后在主 worktree 中执行的命令(如测试、构建等) |
19
+ | `-r, --run` | 否 | validate 成功后在主 worktree 中执行的命令(如测试、构建等)。不传时自动从项目配置的 `validateRunCommand` 读取 |
20
20
 
21
21
  > **限制:** 单次只能验证一个分支,不支持批量验证。
22
22
 
@@ -30,7 +30,7 @@ validate 不再在主工作分支上直接 apply patch,而是先切换到目
30
30
 
31
31
  **快照机制:**
32
32
 
33
- validate 命令引入了**快照(snapshot)机制**来支持增量对比。每次 validate 执行成功后,会将当前全量变更通过 `git write-tree` 保存为 git tree 对象,并将 tree hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.tree`),同时将验证分支的 HEAD commit hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.head`),用于增量 validate 时对齐基准。当再次执行 validate 时,如果验证分支 HEAD 未变化(正常情况),通过 `git read-tree` 将上次快照的 tree 对象载入暂存区;如果验证分支 HEAD 已变化(sync 后重建了验证分支),则将旧变更 patch(旧 tree 相对于旧 HEAD 的差异)重放到当前 HEAD 暂存区上,避免新旧 tree 基准不同导致 diff 混入 HEAD 变化的内容。最终用户可通过 `git diff` 查看两次 validate 之间的增量差异。
33
+ validate 命令引入了**快照(snapshot)机制**来支持增量对比。每次 validate 执行成功后,会将当前全量变更通过 `git write-tree` 保存为 git tree 对象,并将 tree hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.tree`),同时将验证分支的 HEAD commit hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.head`),以及 validate 结束时暂存区对应的 tree hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.staged`),用于增量 validate 时对齐基准和无变更恢复。当再次执行 validate 时,先计算当前变更的 tree hash 与旧快照对比:如果没有新变更(tree hash 和 HEAD 均未变化),直接通过 `git read-tree` 恢复上次 validate 结束时的暂存区状态,跳过后续步骤;如果有新变更,则继续执行暂存区载入流程——如果验证分支 HEAD 未变化(正常情况),通过 `git read-tree` 将上次快照的 tree 对象载入暂存区;如果验证分支 HEAD 已变化(sync 后重建了验证分支),则将旧变更 patch(旧 tree 相对于旧 HEAD 的差异)重放到当前 HEAD 暂存区上,避免新旧 tree 基准不同导致 diff 混入 HEAD 变化的内容。最终用户可通过 `git diff` 查看两次 validate 之间的增量差异。
34
34
 
35
35
  **运行流程:**
36
36
 
@@ -181,7 +181,7 @@ git restore --staged .
181
181
 
182
182
  ##### 步骤 7:执行 `--run` 命令(可选)
183
183
 
184
- 如果用户传入了 `-r, --run` 选项,在 validate 成功后自动在主 worktree 中执行指定命令:
184
+ 如果用户传入了 `-r, --run` 选项,在 validate 成功后自动在主 worktree 中执行指定命令。**如果未传 `-r`,则自动从项目配置的 `validateRunCommand` 字段读取默认命令**(通过 `resolveRunCommand` 解析优先级:`-r` 参数 > 项目配置 > 不执行)。
185
185
 
186
186
  ```bash
187
187
  # 示例:单命令
@@ -281,6 +281,7 @@ clawt validate -b feature-scheme-1 -r "pnpm test & pnpm build"
281
281
  - 并行执行:`runParallelCommands()`(`src/utils/shell.ts`),返回 `ParallelCommandResult[]`
282
282
  - 结果汇总:`reportParallelResults()`(`src/commands/validate.ts`)
283
283
  - 消息常量:`MESSAGES.VALIDATE_PARALLEL_*` 系列(`src/constants/messages/validate.ts`)
284
+ - 命令解析优先级:`resolveRunCommand()`(`src/commands/validate.ts`)负责解析最终要执行的命令,优先使用 `-r` 参数,否则从项目配置读取 `validateRunCommand`(通过 `getValidateRunCommand()`,`src/utils/project-config.ts`)
284
285
 
285
286
  #### 增量 validate(存在历史快照)
286
287
 
@@ -288,7 +289,7 @@ clawt validate -b feature-scheme-1 -r "pnpm test & pnpm build"
288
289
 
289
290
  ##### 步骤 1:读取旧快照
290
291
 
291
- 在清空主 worktree 之前,读取上次保存的快照 tree hash 及当时的 HEAD commit hash
292
+ 在清空主 worktree 之前,读取上次保存的快照 tree hash、当时的 HEAD commit hash 和暂存区 tree hash(`stagedTreeHash`)。
292
293
 
293
294
  ##### 步骤 2:确保主 worktree 干净
294
295
 
@@ -306,9 +307,27 @@ git checkout clawt-validate-<branchName>
306
307
 
307
308
  通过 patch 方式从目标分支获取最新全量变更(流程同首次 validate 的步骤 4)。如果 patch apply 失败,同样进入自动 sync 交互流程(见首次 validate 的 [patch apply 失败后的自动 sync 流程](#patch-apply-失败后的自动-sync-流程)),validate 流程提前结束。
308
309
 
309
- ##### 步骤 5:保存最新快照为 git tree 对象
310
+ ##### 步骤 5:检测是否有新变更
310
311
 
311
- 将最新全量变更保存为新的 tree 对象(覆盖旧快照),同时记录验证分支的 HEAD commit hash(流程同首次 validate 的步骤 5)。
312
+ 计算当前工作目录变更的 tree hash(`git add . → git write-tree → git restore --staged .`),并与旧快照的 tree hash 及 HEAD commit hash 对比:
313
+
314
+ ```bash
315
+ # 计算当前变更的 tree hash
316
+ git add .
317
+ git write-tree # → newTreeHash
318
+ git restore --staged .
319
+
320
+ # 获取当前 HEAD
321
+ git rev-parse HEAD # → currentHeadCommitHash
322
+
323
+ # 判断是否有新变更
324
+ hasNewChanges = (newTreeHash !== oldTreeHash) || (oldHeadCommitHash !== currentHeadCommitHash)
325
+ ```
326
+
327
+ - **无新变更**(tree hash 和 HEAD 均未变化)→ 不更新快照,直接通过 `git read-tree <oldStagedTreeHash>` 恢复上次 validate 结束时的暂存区状态,输出提示后返回
328
+ - **有新变更** → 继续步骤 6
329
+
330
+ > 无变更检测避免了重复 validate 时不必要的快照更新和暂存区重载操作。恢复上次暂存区状态后,用户看到的 `git diff` 结果与上次 validate 结束时完全一致。
312
331
 
313
332
  ##### 步骤 6:将旧变更状态载入暂存区
314
333
 
@@ -322,8 +341,8 @@ git checkout clawt-validate-<branchName>
322
341
  git read-tree <旧 tree hash>
323
342
  ```
324
343
 
325
- - **读取成功** → 结果:暂存区=上次快照,工作目录=最新全量变更(用户可通过 `git diff` 查看增量差异)
326
- - **读取失败**(tree 对象可能被 git gc 回收)→ 降级为全量模式,暂存区保持为空,等同于首次 validate 的结果
344
+ - **读取成功** → 记录 `newStagedTreeHash = oldTreeHash`,结果:暂存区=上次快照,工作目录=最新全量变更(用户可通过 `git diff` 查看增量差异)
345
+ - **读取失败**(tree 对象可能被 git gc 回收)→ 降级为全量模式,写入快照(`stagedTreeHash` 为空)后返回,暂存区保持为空,等同于首次 validate 的结果
327
346
 
328
347
  > 这是最常见的路径。相比重构前,正常情况不再需要处理 HEAD 变化的复杂逻辑,代码路径更简单、更可靠。
329
348
 
@@ -343,26 +362,49 @@ git apply --cached --check < patch
343
362
 
344
363
  # 无冲突:apply --cached 到当前 HEAD 暂存区
345
364
  git apply --cached < patch
365
+
366
+ # 记录暂存区的 tree hash
367
+ git write-tree # → newStagedTreeHash
346
368
  ```
347
369
 
348
370
  - **patch 为空**(旧变更为空)→ 暂存区保持干净
349
- - **无冲突** → apply --cached 到当前 HEAD 暂存区,结果与正常情况一致
350
- - **有冲突** → 降级为全量模式(暂存区保持为空),等同于首次 validate 的结果
371
+ - **无冲突** → apply --cached 到当前 HEAD 暂存区,通过 `git write-tree` 记录 `newStagedTreeHash`,结果与正常情况一致
372
+ - **有冲突** → 降级为全量模式(暂存区保持为空),写入快照(`stagedTreeHash` 为空)后返回
351
373
 
352
- ##### 步骤 7:输出成功提示
374
+ ##### 步骤 7:写入新快照
375
+
376
+ 将步骤 5 计算的 `newTreeHash`、当前 HEAD commit hash 和步骤 6 记录的 `newStagedTreeHash` 写入快照文件,供下次 validate 使用:
377
+
378
+ ```bash
379
+ # 写入 ~/.clawt/validate-snapshots/<project>/<branchName>.tree
380
+ echo <newTreeHash>
381
+
382
+ # 写入 ~/.clawt/validate-snapshots/<project>/<branchName>.head
383
+ echo <currentHeadCommitHash>
384
+
385
+ # 写入 ~/.clawt/validate-snapshots/<project>/<branchName>.staged
386
+ echo <newStagedTreeHash>
387
+ ```
388
+
389
+ > `stagedTreeHash` 记录了 validate 结束时暂存区的完整状态。下次 validate 如果检测到无新变更,可直接通过此值恢复暂存区,避免重复执行 read-tree 或 apply --cached 流程。
390
+
391
+ ##### 步骤 8:输出成功提示
353
392
 
354
393
  ```
355
394
  # 增量模式成功
356
395
  ✓ 已将分支 feature-scheme-1 的最新变更应用到主 worktree(增量模式)
357
396
  暂存区 = 上次快照,工作目录 = 最新变更
358
397
 
398
+ # 增量无变更
399
+ 分支 feature-scheme-1 自上次 validate 以来没有新的变更,已恢复到上次验证状态
400
+
359
401
  # 增量降级为全量
360
402
  ✓ 已切换到验证分支 clawt-validate-feature-scheme-1 并应用分支 feature-scheme-1 的变更
361
403
  可以开始验证了
362
404
  ```
363
405
 
364
- ##### 步骤 8:执行 `--run` 命令(可选)
406
+ ##### 步骤 9:执行 `--run` 命令(可选)
365
407
 
366
- 与首次 validate 的步骤 7 相同,增量 validate 成功后也会执行 `-r, --run` 指定的命令。
408
+ 与首次 validate 的步骤 7 相同,增量 validate 成功后也会执行 `-r, --run` 指定的命令(或从项目配置 `validateRunCommand` 读取的默认命令)。
367
409
 
368
410
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "3.1.2",
3
+ "version": "3.2.0",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,7 +1,5 @@
1
1
  import type { Command } from 'commander';
2
- import chalk from 'chalk';
3
- import Enquirer from 'enquirer';
4
- import { DEFAULT_CONFIG, CONFIG_DESCRIPTIONS, MESSAGES, CONFIG_ALIAS_DISABLED_HINT } from '../constants/index.js';
2
+ import { DEFAULT_CONFIG, CONFIG_DEFINITIONS, MESSAGES, CONFIG_ALIAS_DISABLED_HINT } from '../constants/index.js';
5
3
  import { logger } from '../logger/index.js';
6
4
  import {
7
5
  loadConfig,
@@ -14,10 +12,8 @@ import {
14
12
  isValidConfigKey,
15
13
  getValidConfigKeys,
16
14
  parseConfigValue,
17
- promptConfigValue,
18
- formatConfigValue,
15
+ interactiveConfigEditor,
19
16
  } from '../utils/index.js';
20
- import type { ClawtConfig } from '../types/index.js';
21
17
 
22
18
  /**
23
19
  * 注册 config 命令组:查看和管理全局配置
@@ -118,36 +114,26 @@ async function handleConfigSet(key?: string, value?: string): Promise<void> {
118
114
  */
119
115
  async function handleInteractiveConfigSet(): Promise<void> {
120
116
  const config = loadConfig();
121
- const keys = Object.keys(DEFAULT_CONFIG) as Array<keyof ClawtConfig>;
122
117
 
123
118
  logger.info('config set 命令执行,进入交互式配置');
124
119
 
125
- // 构建选择列表,显示配置项名称、当前值和描述
126
- // 对象类型配置项(如 aliases)标灰不可选,提示用户通过专用命令管理
127
- const choices = keys.map((k) => {
128
- const isObject = typeof DEFAULT_CONFIG[k] === 'object';
129
- return {
130
- name: k,
131
- message: `${k}: ${isObject ? chalk.dim(JSON.stringify(config[k])) : formatConfigValue(config[k])} ${chalk.dim(`— ${CONFIG_DESCRIPTIONS[k]}`)}`,
132
- ...(isObject && { disabled: CONFIG_ALIAS_DISABLED_HINT }),
133
- };
134
- });
135
-
136
- // @ts-expect-error enquirer 类型声明未导出 Select 类,但运行时存在
137
- const selectedKey: keyof ClawtConfig = await new Enquirer.Select({
138
- message: MESSAGES.CONFIG_SELECT_PROMPT,
139
- choices,
140
- }).run();
120
+ // 构建对象类型配置项的禁用映射(如 aliases 需通过专用命令管理)
121
+ const disabledKeys: Record<string, string> = {};
122
+ for (const k of Object.keys(DEFAULT_CONFIG)) {
123
+ if (typeof DEFAULT_CONFIG[k as keyof typeof DEFAULT_CONFIG] === 'object') {
124
+ disabledKeys[k] = CONFIG_ALIAS_DISABLED_HINT;
125
+ }
126
+ }
141
127
 
142
- // 根据类型和 allowedValues 自动选择提示策略
143
- const currentValue = config[selectedKey];
144
- const newValue = await promptConfigValue(selectedKey, currentValue);
128
+ const { key, newValue } = await interactiveConfigEditor(config, CONFIG_DEFINITIONS, {
129
+ disabledKeys,
130
+ });
145
131
 
146
132
  // 持久化并提示成功
147
- (config as unknown as Record<string, unknown>)[selectedKey] = newValue;
133
+ (config as unknown as Record<string, unknown>)[key as string] = newValue;
148
134
  saveConfig(config);
149
135
 
150
- printSuccess(MESSAGES.CONFIG_SET_SUCCESS(selectedKey, String(newValue)));
136
+ printSuccess(MESSAGES.CONFIG_SET_SUCCESS(key as string, String(newValue)));
151
137
  }
152
138
 
153
139
  /**
@@ -1,8 +1,8 @@
1
1
  import type { Command } from 'commander';
2
2
  import { Command as Cmd } from 'commander';
3
3
  import { logger } from '../logger/index.js';
4
- import { MESSAGES } from '../constants/index.js';
5
- import type { InitOptions } from '../types/index.js';
4
+ import { MESSAGES, PROJECT_CONFIG_DEFINITIONS } from '../constants/index.js';
5
+ import type { InitOptions, ProjectConfig } from '../types/index.js';
6
6
  import {
7
7
  validateMainWorktree,
8
8
  getCurrentBranch,
@@ -10,8 +10,7 @@ import {
10
10
  saveProjectConfig,
11
11
  requireProjectConfig,
12
12
  printSuccess,
13
- printInfo,
14
- safeStringify,
13
+ interactiveConfigEditor,
15
14
  } from '../utils/index.js';
16
15
 
17
16
  /**
@@ -27,24 +26,36 @@ export function registerInitCommand(program: Command): void {
27
26
  await handleInit(options);
28
27
  });
29
28
 
30
- // 注册 show 子命令:展示当前项目配置
29
+ // 注册 show 子命令:交互式查看和修改项目配置
31
30
  initCmd.addCommand(
32
31
  new Cmd('show')
33
- .description('展示当前项目的 init 配置')
34
- .action(() => {
35
- handleInitShow();
32
+ .description('交互式查看和修改项目配置')
33
+ .action(async () => {
34
+ await handleInitShow();
36
35
  }),
37
36
  );
38
37
  }
39
38
 
40
39
  /**
41
- * 处理 init show 子命令:以 JSON 格式展示当前项目完整配置
40
+ * 处理 init show 子命令:交互式面板展示和修改项目配置
42
41
  */
43
- function handleInitShow(): void {
42
+ async function handleInitShow(): Promise<void> {
44
43
  validateMainWorktree();
45
44
  const config = requireProjectConfig();
46
- const configJson = safeStringify(config);
47
- printInfo(MESSAGES.INIT_SHOW(configJson));
45
+
46
+ logger.info('init show 命令执行,进入交互式项目配置');
47
+
48
+ const { key, newValue } = await interactiveConfigEditor(
49
+ config as Required<ProjectConfig>,
50
+ PROJECT_CONFIG_DEFINITIONS,
51
+ { selectPrompt: MESSAGES.INIT_SELECT_PROMPT },
52
+ );
53
+
54
+ // 合并修改后的值并持久化
55
+ const updatedConfig: ProjectConfig = { ...config, [key]: newValue };
56
+ saveProjectConfig(updatedConfig);
57
+
58
+ printSuccess(MESSAGES.INIT_SET_SUCCESS(key as string, String(newValue)));
48
59
  }
49
60
 
50
61
  /**