clawt 3.9.6 → 3.9.8

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.
@@ -551,8 +551,10 @@ var TASKS_CMD_MESSAGES = {
551
551
  TASK_INIT_FILE_EXISTS: (path) => `\u6587\u4EF6\u5DF2\u5B58\u5728: ${path}\uFF0C\u5982\u9700\u8986\u76D6\u8BF7\u5148\u5220\u9664`,
552
552
  /** 任务模板生成成功 */
553
553
  TASK_INIT_SUCCESS: (path) => `\u2713 \u4EFB\u52A1\u6A21\u677F\u5DF2\u751F\u6210: ${path}`,
554
- /** 任务模板使用提示 */
555
- TASK_INIT_HINT: (path) => `\u4F7F\u7528 clawt run -f ${path} \u6267\u884C\u4EFB\u52A1`
554
+ /** 任务模板使用提示(分行列出 run 和 resume 两种用法) */
555
+ TASK_INIT_HINT: (path) => `\u6267\u884C\u4EFB\u52A1:
556
+ clawt run -f ${path} # \u521B\u5EFA worktree \u5E76\u6267\u884C\uFF08\u5206\u652F\u540D\u9700\u4E0D\u5B58\u5728\uFF09
557
+ clawt resume -f ${path} # \u5728\u5DF2\u6709 worktree \u4E2D\u8FFD\u95EE\uFF08\u5206\u652F\u540D\u9700\u5DF2\u5B58\u5728\uFF09`
556
558
  };
557
559
 
558
560
  // src/constants/messages/post-create.ts
@@ -722,6 +724,10 @@ var PROJECT_CONFIG_DEFINITIONS = {
722
724
  postCreate: {
723
725
  defaultValue: void 0,
724
726
  description: "worktree \u521B\u5EFA\u540E\u81EA\u52A8\u6267\u884C\u7684\u547D\u4EE4\uFF0C\u7528\u4E8E\u5B89\u88C5\u4F9D\u8D56\u7B49\u521D\u59CB\u5316\u64CD\u4F5C"
727
+ },
728
+ claudeCodeCommand: {
729
+ defaultValue: void 0,
730
+ description: "Claude Code CLI \u542F\u52A8\u6307\u4EE4\uFF08\u672A\u8BBE\u7F6E\u65F6\u56DE\u9000\u5230\u5168\u5C40\u914D\u7F6E\uFF09"
725
731
  }
726
732
  };
727
733
  function deriveDefaultConfig2(definitions) {
@@ -35,7 +35,7 @@
35
35
  | 配置项 | 类型 | 默认值 | 说明 |
36
36
  | ------------------ | --------- | --------- | -------------------------------------------------- |
37
37
  | `autoDeleteBranch` | `boolean` | `false` | 移除 worktree 时是否自动删除对应本地分支(无需每次确认);merge 成功后是否自动清理 worktree 和分支;run 任务被中断(Ctrl+C)后是否自动清理本次创建的 worktree 和分支 |
38
- | `claudeCodeCommand` | `string` | `"claude"` | Claude Code CLI 启动指令,用于 `clawt run` 不传 `--tasks` 时和 `clawt resume` 在 worktree 中打开交互式界面 |
38
+ | `claudeCodeCommand` | `string` | `"claude"` | Claude Code CLI 启动指令,用于 `clawt run` 不传 `--tasks` 时和 `clawt resume` 在 worktree 中打开交互式界面。可被项目级配置 `claudeCodeCommand` 覆盖(优先级:项目级 > 全局级,详见 [project-config.md](./project-config.md)) |
39
39
  | `autoPullPush` | `boolean` | `false` | merge 成功后是否自动执行 git pull 和 git push |
40
40
  | `confirmDestructiveOps` | `boolean` | `true` | 执行破坏性操作(reset、validate --clean)前是否提示确认 |
41
41
  | `maxConcurrency` | `number` | `0` | run 命令默认最大并发数,`0` 表示不限制 |
package/docs/init.md CHANGED
@@ -26,7 +26,7 @@ clawt init show --json
26
26
 
27
27
  **功能说明:**
28
28
 
29
- 初始化项目级配置,将指定分支记录为该项目的主工作分支(`clawtMainWorkBranch`)。该配置用于 `create` / `run` 时检测当前分支是否为主工作分支,并在偏离时提醒用户。`init show` 子命令提供交互式面板,可查看和修改所有项目配置项(如 `validateRunCommand`、`postCreate`)。项目级配置的完整说明见 [project-config.md](./project-config.md)。
29
+ 初始化项目级配置,将指定分支记录为该项目的主工作分支(`clawtMainWorkBranch`)。该配置用于 `create` / `run` 时检测当前分支是否为主工作分支,并在偏离时提醒用户。`init show` 子命令提供交互式面板,可查看和修改所有项目配置项(如 `validateRunCommand`、`postCreate`、`claudeCodeCommand`)。项目级配置的完整说明见 [project-config.md](./project-config.md)。
30
30
 
31
31
  **运行流程(设置模式):**
32
32
 
@@ -53,7 +53,7 @@ clawt init show --json
53
53
  3. **交互式配置编辑**:调用 `interactiveConfigEditor`(`src/utils/config-strategy.ts`),基于 `PROJECT_CONFIG_DEFINITIONS` 构建配置项列表(详见 [project-config.md](./project-config.md))
54
54
  - 列出所有项目配置项,显示名称、当前值和描述
55
55
  - 用户选择配置项后,根据值类型自动选择输入方式(与全局配置的交互式编辑逻辑一致)
56
- 4. **持久化修改**:将修改后的值合并到当前配置并写入配置文件
56
+ 4. **持久化修改**:将修改后的值合并到当前配置,经 `normalizeProjectConfig` 归一化处理后写入配置文件(可选字段的空字符串会被移除,等同于未设置)
57
57
  5. **输出成功提示**:`✓ 项目配置 <key> 已设置为 <value>`
58
58
 
59
59
  **输出格式:**
@@ -85,5 +85,6 @@ clawt init show --json
85
85
  - `init show` 子命令从 JSON 展示改为交互式面板,调用 `interactiveConfigEditor`(`src/utils/config-strategy.ts`)实现通用交互式配置编辑
86
86
  - 配置项定义来自 `PROJECT_CONFIG_DEFINITIONS`(`src/constants/project-config.ts`),详见 [项目级配置文档](./project-config.md)
87
87
  - 消息常量:`MESSAGES.INIT_SELECT_PROMPT`(选择配置项提示语)、`MESSAGES.INIT_SET_SUCCESS`(修改成功提示),定义在 `src/constants/messages/init.ts`
88
+ - `handleInitShow` 使用 `normalizeProjectConfig` 对修改后的配置进行归一化处理:可选字段(如 `validateRunCommand`、`postCreate`、`claudeCodeCommand`)设为空字符串时自动移除该键,避免 JSON 文件中出现冗余的 `"field": ""` 条目
88
89
 
89
90
  ---
@@ -18,7 +18,8 @@
18
18
  {
19
19
  "clawtMainWorkBranch": "main",
20
20
  "validateRunCommand": "npm test",
21
- "postCreate": "npm install"
21
+ "postCreate": "npm install",
22
+ "claudeCodeCommand": "claude --model opus"
22
23
  }
23
24
  ```
24
25
 
@@ -27,6 +28,7 @@
27
28
  | `clawtMainWorkBranch` | `string` | 是 | `""` | 项目的主工作分支名,用于 create 时检测当前分支是否为主分支,以及 sync、merge 等命令获取主分支名 |
28
29
  | `validateRunCommand` | `string` | 否 | `undefined` | validate 成功后自动执行的命令(作为 `-r` 选项的默认值)。不传 `-r` 时,validate 命令会自动从此项读取 |
29
30
  | `postCreate` | `string` | 否 | `undefined` | worktree 创建后自动执行的初始化命令(如安装依赖、生成配置文件、编译资源等)。详见 [post-create-hook.md](./post-create-hook.md) |
31
+ | `claudeCodeCommand` | `string` | 否 | `undefined` | Claude Code CLI 启动指令,项目级覆盖全局配置。未设置时回退到全局配置 `claudeCodeCommand`(详见 [config-file.md](./config-file.md))。优先级:项目级 > 全局级 |
30
32
 
31
33
  #### 配置项定义数据源
32
34
 
@@ -48,6 +50,10 @@ export const PROJECT_CONFIG_DEFINITIONS: ProjectConfigDefinitions = {
48
50
  defaultValue: undefined as unknown as string | undefined,
49
51
  description: 'worktree 创建后自动执行的命令,用于安装依赖等初始化操作',
50
52
  },
53
+ claudeCodeCommand: {
54
+ defaultValue: undefined as unknown as string | undefined,
55
+ description: 'Claude Code CLI 启动指令(未设置时回退到全局配置)',
56
+ },
51
57
  };
52
58
 
53
59
  /** 项目默认配置(从 PROJECT_CONFIG_DEFINITIONS 自动派生) */
@@ -63,7 +69,7 @@ export const PROJECT_CONFIG_DESCRIPTIONS: Record<keyof Required<ProjectConfig>,
63
69
 
64
70
  | 类型 | 说明 |
65
71
  | --- | --- |
66
- | `ProjectConfig` | 项目级配置接口,包含 `clawtMainWorkBranch`(必填)、`validateRunCommand`(可选)和 `postCreate`(可选) |
72
+ | `ProjectConfig` | 项目级配置接口,包含 `clawtMainWorkBranch`(必填)、`validateRunCommand`(可选)、`postCreate`(可选)和 `claudeCodeCommand`(可选) |
67
73
  | `ProjectConfigItemDefinition<T>` | 单个配置项定义,含 `defaultValue`(默认值)、`description`(描述)、可选 `allowedValues`(枚举值列表,仅对 string 类型有效) |
68
74
  | `ProjectConfigDefinitions` | 所有配置项的完整定义映射,键为 `ProjectConfig` 的所有属性名,值为对应的 `ProjectConfigItemDefinition` |
69
75
 
@@ -74,6 +80,7 @@ export interface ProjectConfig {
74
80
  clawtMainWorkBranch: string;
75
81
  validateRunCommand?: string;
76
82
  postCreate?: string;
83
+ claudeCodeCommand?: string;
77
84
  }
78
85
 
79
86
  export interface ProjectConfigItemDefinition<T> {
@@ -99,6 +106,8 @@ export type ProjectConfigDefinitions = {
99
106
  | `requireProjectConfig` | `() => ProjectConfig` | 获取当前项目配置,不存在或缺少 `clawtMainWorkBranch` 时抛出 `ClawtError` |
100
107
  | `getMainWorkBranch` | `() => string` | 从项目配置中获取主工作分支名(内部调用 `requireProjectConfig`) |
101
108
  | `getValidateRunCommand` | `() => string \| undefined` | 从项目配置中获取 validate 自动执行命令,未配置时返回 `undefined` |
109
+ | `resolveClaudeCodeCommand` | `() => string` | 解析当前项目生效的 Claude Code 启动指令,优先级:项目级配置 > 全局配置 |
110
+ | `normalizeProjectConfig` | `(config: ProjectConfig, key: string, value: unknown) => ProjectConfig` | 归一化项目配置:可选字段的空字符串等同于未设置,从对象中删除该键以保持 JSON 文件整洁 |
102
111
 
103
112
  #### 设置方式
104
113
 
package/docs/tasks.md CHANGED
@@ -46,7 +46,7 @@ clawt tasks init [path]
46
46
  # 格式说明: 标签外的文本会被忽略,每个任务用 START/END 标签包裹
47
47
  #
48
48
  # 规则:
49
- # 1. 每个任务块用 <!-- CLAWT-TASKS:START --> <!-- CLAWT-TASKS:END --> 包裹
49
+ # 1. 每个任务块用 <START><END> 标签包裹(实际标签见下方示例)
50
50
  # 2. 块内 # branch: <分支名> 声明分支名(使用 -b 参数时可省略)
51
51
  # 3. 块内其余行为任务描述(支持多行)
52
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "3.9.6",
3
+ "version": "3.9.8",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -13,6 +13,7 @@ import {
13
13
  printSuccess,
14
14
  interactiveConfigEditor,
15
15
  safeStringify,
16
+ normalizeProjectConfig,
16
17
  } from '../utils/index.js';
17
18
 
18
19
  /**
@@ -62,7 +63,8 @@ async function handleInitShow(options: InitShowOptions): Promise<void> {
62
63
  );
63
64
 
64
65
  // 合并修改后的值并持久化
65
- const updatedConfig: ProjectConfig = { ...config, [key]: newValue };
66
+ const mergedConfig: ProjectConfig = { ...config, [key]: newValue };
67
+ const updatedConfig = normalizeProjectConfig(mergedConfig, key as string, newValue);
66
68
  saveProjectConfig(updatedConfig);
67
69
 
68
70
  printSuccess(MESSAGES.INIT_SET_SUCCESS(key as string, String(newValue)));
@@ -9,11 +9,10 @@ import {
9
9
  getCurrentBranch,
10
10
  isWorkingDirClean,
11
11
  getProjectWorktrees,
12
- getCommitCountAhead,
13
- getCommitCountBehind,
12
+ getCommitDivergenceAsync,
13
+ getDiffStatAsync,
14
+ getStatusPorcelainAsync,
14
15
  getDiffStat,
15
- hasMergeConflict,
16
- hasLocalCommits,
17
16
  getSnapshotModifiedTime,
18
17
  getProjectSnapshotBranches,
19
18
  getWorktreeCreatedTime,
@@ -55,7 +54,7 @@ async function handleStatus(options: StatusOptions): Promise<void> {
55
54
  return;
56
55
  }
57
56
 
58
- const statusResult = collectStatus();
57
+ const statusResult = await collectStatus();
59
58
 
60
59
  logger.info(`status 命令执行,项目: ${statusResult.main.projectName},共 ${statusResult.totalWorktrees} 个 worktree`);
61
60
 
@@ -69,9 +68,10 @@ async function handleStatus(options: StatusOptions): Promise<void> {
69
68
 
70
69
  /**
71
70
  * 收集项目全局状态信息
72
- * @returns {StatusResult} 完整的状态数据
71
+ * worktree 的数据通过 Promise.all 并行收集,避免串行阻塞
72
+ * @returns {Promise<StatusResult>} 完整的状态数据
73
73
  */
74
- export function collectStatus(): StatusResult {
74
+ export async function collectStatus(): Promise<StatusResult> {
75
75
  const projectName = getProjectName();
76
76
  const currentBranch = getCurrentBranch();
77
77
  const isClean = isWorkingDirClean();
@@ -95,9 +95,11 @@ export function collectStatus(): StatusResult {
95
95
  deletions,
96
96
  };
97
97
 
98
- // 各 worktree 详细状态
98
+ // 各 worktree 详细状态(异步并行收集)
99
99
  const worktrees = getProjectWorktrees();
100
- const worktreeStatuses = worktrees.map((wt) => collectWorktreeDetailedStatus(wt, projectName));
100
+ const worktreeStatuses = await Promise.all(
101
+ worktrees.map((wt) => collectWorktreeDetailedStatusAsync(wt, projectName)),
102
+ );
101
103
 
102
104
  // 未清理的 validate 快照
103
105
  const snapshots = collectSnapshots(projectName, worktrees);
@@ -111,70 +113,100 @@ export function collectStatus(): StatusResult {
111
113
  }
112
114
 
113
115
  /**
114
- * 收集单个 worktree 的详细状态
115
- * 变更状态判断优先级:冲突 > 未提交 > 已提交 > 干净
116
+ * 异步收集单个 worktree 的详细状态
117
+ * 内部 3 git 命令通过 Promise.all 并行执行,每个 worktree 内部也是并行的
116
118
  * @param {WorktreeInfo} worktree - worktree 信息
117
119
  * @param {string} projectName - 项目名
118
- * @returns {WorktreeDetailedStatus} 详细状态
120
+ * @returns {Promise<WorktreeDetailedStatus>} 详细状态
119
121
  */
120
- function collectWorktreeDetailedStatus(worktree: WorktreeInfo, projectName: string): WorktreeDetailedStatus {
121
- const changeStatus = detectChangeStatus(worktree);
122
- const { commitsAhead, commitsBehind } = countCommitDivergence(worktree.branch);
123
- const { insertions, deletions } = countDiffStat(worktree.path);
122
+ async function collectWorktreeDetailedStatusAsync(worktree: WorktreeInfo, projectName: string): Promise<WorktreeDetailedStatus> {
123
+ // 3 git 命令并行执行:提交差异、工作区状态、diff 统计
124
+ const [divergence, porcelain, diffStat] = await Promise.all([
125
+ countCommitDivergenceAsync(worktree.branch),
126
+ detectStatusPorcelainAsync(worktree.path),
127
+ countDiffStatAsync(worktree.path),
128
+ ]);
129
+
130
+ const changeStatus = detectChangeStatusFromPorcelain(porcelain, divergence.commitsAhead);
124
131
  const createdAt = getWorktreeCreatedTime(worktree.path);
125
132
 
126
133
  return {
127
134
  path: worktree.path,
128
135
  branch: worktree.branch,
129
136
  changeStatus,
130
- commitsAhead,
131
- commitsBehind,
137
+ commitsAhead: divergence.commitsAhead,
138
+ commitsBehind: divergence.commitsBehind,
132
139
  snapshotTime: resolveSnapshotTime(projectName, worktree.branch),
133
- insertions,
134
- deletions,
140
+ insertions: diffStat.insertions,
141
+ deletions: diffStat.deletions,
135
142
  createdAt,
136
143
  };
137
144
  }
138
145
 
139
146
  /**
140
- * 检测 worktree 的变更状态
147
+ * porcelain 输出判断变更状态
141
148
  * 优先级:冲突 > 未提交 > 已提交 > 干净
142
- * @param {WorktreeInfo} worktree - worktree 信息
149
+ * @param {string} porcelain - git status --porcelain 输出
150
+ * @param {number} commitsAhead - 领先提交数
143
151
  * @returns {WorktreeDetailedStatus['changeStatus']} 变更状态
144
152
  */
145
- function detectChangeStatus(worktree: WorktreeInfo): WorktreeDetailedStatus['changeStatus'] {
153
+ function detectChangeStatusFromPorcelain(porcelain: string, commitsAhead: number): WorktreeDetailedStatus['changeStatus'] {
154
+ // 检测合并冲突(UU/AA/DD/DU/UD/AU/UA 开头的行)
155
+ const hasConflict = porcelain.split('\n').some((line) => /^(UU|AA|DD|DU|UD|AU|UA)/.test(line));
156
+ if (hasConflict) {
157
+ return 'conflict';
158
+ }
159
+ // 检测未提交修改(porcelain 非空即有未提交变更)
160
+ if (porcelain !== '') {
161
+ return 'uncommitted';
162
+ }
163
+ // 用 commitsAhead > 0 判断是否有本地提交
164
+ if (commitsAhead > 0) {
165
+ return 'committed';
166
+ }
167
+ return 'clean';
168
+ }
169
+
170
+ /**
171
+ * 异步获取工作区 porcelain 状态,出错时返回空字符串
172
+ * @param {string} worktreePath - worktree 目录路径
173
+ * @returns {Promise<string>} porcelain 格式输出
174
+ */
175
+ async function detectStatusPorcelainAsync(worktreePath: string): Promise<string> {
146
176
  try {
147
- if (hasMergeConflict(worktree.path)) {
148
- return 'conflict';
149
- }
150
- if (!isWorkingDirClean(worktree.path)) {
151
- return 'uncommitted';
152
- }
153
- if (hasLocalCommits(worktree.branch)) {
154
- return 'committed';
155
- }
156
- return 'clean';
177
+ return await getStatusPorcelainAsync(worktreePath);
157
178
  } catch {
158
- return 'clean';
179
+ return '';
159
180
  }
160
181
  }
161
182
 
162
183
  /**
163
- * 统计分支与主分支的提交差异(领先/落后数)
184
+ * 异步统计分支与主分支的提交差异(领先/落后数)
164
185
  * @param {string} branchName - 分支名
165
- * @returns {{ commitsAhead: number; commitsBehind: number }} 领先和落后的提交数
186
+ * @returns {Promise<{ commitsAhead: number; commitsBehind: number }>} 领先和落后的提交数
166
187
  */
167
- function countCommitDivergence(branchName: string): { commitsAhead: number; commitsBehind: number } {
188
+ async function countCommitDivergenceAsync(branchName: string): Promise<{ commitsAhead: number; commitsBehind: number }> {
168
189
  try {
169
- return {
170
- commitsAhead: getCommitCountAhead(branchName),
171
- commitsBehind: getCommitCountBehind(branchName),
172
- };
190
+ const { ahead, behind } = await getCommitDivergenceAsync(branchName);
191
+ return { commitsAhead: ahead, commitsBehind: behind };
173
192
  } catch {
174
193
  return { commitsAhead: 0, commitsBehind: 0 };
175
194
  }
176
195
  }
177
196
 
197
+ /**
198
+ * 异步统计 worktree 的差异行数
199
+ * @param {string} worktreePath - worktree 目录路径
200
+ * @returns {Promise<{ insertions: number; deletions: number }>} 新增和删除行数
201
+ */
202
+ async function countDiffStatAsync(worktreePath: string): Promise<{ insertions: number; deletions: number }> {
203
+ try {
204
+ return await getDiffStatAsync(worktreePath);
205
+ } catch {
206
+ return { insertions: 0, deletions: 0 };
207
+ }
208
+ }
209
+
178
210
  /**
179
211
  * 统计 worktree 的差异行数
180
212
  * @param {string} worktreePath - worktree 路径
@@ -4,6 +4,7 @@ export const TASKS_CMD_MESSAGES = {
4
4
  TASK_INIT_FILE_EXISTS: (path: string) => `文件已存在: ${path},如需覆盖请先删除`,
5
5
  /** 任务模板生成成功 */
6
6
  TASK_INIT_SUCCESS: (path: string) => `✓ 任务模板已生成: ${path}`,
7
- /** 任务模板使用提示 */
8
- TASK_INIT_HINT: (path: string) => `使用 clawt run -f ${path} 执行任务`,
7
+ /** 任务模板使用提示(分行列出 run 和 resume 两种用法) */
8
+ TASK_INIT_HINT: (path: string) =>
9
+ `执行任务:\n clawt run -f ${path} # 创建 worktree 并执行(分支名需不存在)\n clawt resume -f ${path} # 在已有 worktree 中追问(分支名需已存在)`,
9
10
  } as const;
@@ -17,6 +17,10 @@ export const PROJECT_CONFIG_DEFINITIONS: ProjectConfigDefinitions = {
17
17
  defaultValue: undefined as unknown as string | undefined,
18
18
  description: 'worktree 创建后自动执行的命令,用于安装依赖等初始化操作',
19
19
  },
20
+ claudeCodeCommand: {
21
+ defaultValue: undefined as unknown as string | undefined,
22
+ description: 'Claude Code CLI 启动指令(未设置时回退到全局配置)',
23
+ },
20
24
  };
21
25
 
22
26
  /**
@@ -11,7 +11,7 @@ export const TASK_TEMPLATE_CONTENT = `# Clawt 任务文件
11
11
  # 格式说明: 标签外的文本会被忽略,每个任务用 START/END 标签包裹
12
12
  #
13
13
  # 规则:
14
- # 1. 每个任务块用 <!-- CLAWT-TASKS:START --> <!-- CLAWT-TASKS:END --> 包裹
14
+ # 1. 每个任务块用 <START><END> 标签包裹(实际标签见下方示例)
15
15
  # 2. 块内 # branch: <分支名> 声明分支名(使用 -b 参数时可省略)
16
16
  # 3. 块内其余行为任务描述(支持多行)
17
17
 
@@ -6,6 +6,8 @@ export interface ProjectConfig {
6
6
  validateRunCommand?: string;
7
7
  /** worktree 创建后自动执行的命令,用于安装依赖等初始化操作 */
8
8
  postCreate?: string;
9
+ /** Claude Code CLI 启动指令(项目级覆盖全局配置,未设置时回退到全局配置) */
10
+ claudeCodeCommand?: string;
9
11
  }
10
12
 
11
13
  /** 单个项目配置项的完整定义(默认值 + 描述) */
@@ -3,7 +3,7 @@ import { existsSync, readdirSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { ClawtError } from '../errors/index.js';
5
5
  import { APPEND_SYSTEM_PROMPT, CLAUDE_PROJECTS_DIR } from '../constants/index.js';
6
- import { getConfigValue } from './config.js';
6
+ import { resolveClaudeCodeCommand } from './project-config.js';
7
7
  import { printInfo, printWarning } from './formatter.js';
8
8
  import { openCommandInNewTerminalTab } from './terminal.js';
9
9
  import type { WorktreeInfo } from '../types/index.js';
@@ -51,7 +51,7 @@ interface LaunchClaudeOptions {
51
51
  }
52
52
 
53
53
  export function launchInteractiveClaude(worktree: WorktreeInfo, options: LaunchClaudeOptions = {}): void {
54
- const commandStr = getConfigValue('claudeCodeCommand');
54
+ const commandStr = resolveClaudeCodeCommand();
55
55
  const parts = commandStr.split(/\s+/).filter(Boolean);
56
56
  const cmd = parts[0];
57
57
  const args = [
@@ -107,7 +107,7 @@ function escapeShellSingleQuote(str: string): string {
107
107
  * @returns {string} 完整的 shell 命令字符串
108
108
  */
109
109
  export function buildClaudeCommand(worktree: WorktreeInfo, hasPreviousSession: boolean): string {
110
- const commandStr = getConfigValue('claudeCodeCommand');
110
+ const commandStr = resolveClaudeCodeCommand();
111
111
  const systemPrompt = APPEND_SYSTEM_PROMPT;
112
112
 
113
113
  const escapedPath = escapeShellSingleQuote(worktree.path);
@@ -1,4 +1,4 @@
1
- import { execCommand } from './shell.js';
1
+ import { execCommand, execCommandAsync } from './shell.js';
2
2
  import { logger } from '../logger/index.js';
3
3
 
4
4
  /**
@@ -68,6 +68,51 @@ export function getCommitCountBehind(branchName: string, cwd?: string): number {
68
68
  }
69
69
  }
70
70
 
71
+ /**
72
+ * 解析 git rev-list --left-right --count 的输出
73
+ * 输出格式:<left_count>\t<right_count>
74
+ * left = HEAD 侧独有提交数(即 behind),right = branch 侧独有提交数(即 ahead)
75
+ * @param {string} output - rev-list 命令输出
76
+ * @returns {{ ahead: number; behind: number }} 领先和落后的提交数
77
+ */
78
+ function parseDivergenceOutput(output: string): { ahead: number; behind: number } {
79
+ const [leftStr, rightStr] = output.trim().split(/\s+/);
80
+ return { ahead: parseInt(rightStr, 10) || 0, behind: parseInt(leftStr, 10) || 0 };
81
+ }
82
+
83
+ /**
84
+ * 获取当前分支与目标分支的双向提交差异
85
+ * 使用单条 git rev-list --left-right --count 命令同时获取领先和落后提交数,
86
+ * 替代分别调用 getCommitCountAhead 和 getCommitCountBehind 两条命令
87
+ * @param {string} branchName - 目标分支名
88
+ * @param {string} [cwd] - 工作目录
89
+ * @returns {{ ahead: number; behind: number }} 领先和落后的提交数
90
+ */
91
+ export function getCommitDivergence(branchName: string, cwd?: string): { ahead: number; behind: number } {
92
+ try {
93
+ const output = execCommand(`git rev-list --left-right --count HEAD...${branchName}`, { cwd });
94
+ return parseDivergenceOutput(output);
95
+ } catch {
96
+ return { ahead: 0, behind: 0 };
97
+ }
98
+ }
99
+
100
+ /**
101
+ * 异步获取当前分支与目标分支的双向提交差异
102
+ * 用于并行收集多个 worktree 的提交差异时避免串行阻塞
103
+ * @param {string} branchName - 目标分支名
104
+ * @param {string} [cwd] - 工作目录
105
+ * @returns {Promise<{ ahead: number; behind: number }>} 领先和落后的提交数
106
+ */
107
+ export async function getCommitDivergenceAsync(branchName: string, cwd?: string): Promise<{ ahead: number; behind: number }> {
108
+ try {
109
+ const output = await execCommandAsync(`git rev-list --left-right --count HEAD...${branchName}`, { cwd });
110
+ return parseDivergenceOutput(output);
111
+ } catch {
112
+ return { ahead: 0, behind: 0 };
113
+ }
114
+ }
115
+
71
116
  /**
72
117
  * 获取当前分支名
73
118
  * @param {string} [cwd] - 工作目录
@@ -1,6 +1,6 @@
1
1
  import { basename } from 'node:path';
2
2
  import { execSync, execFileSync } from 'node:child_process';
3
- import { execCommand, execCommandWithInput } from './shell.js';
3
+ import { execCommand, execCommandAsync, execCommandWithInput } from './shell.js';
4
4
  import { logger } from '../logger/index.js';
5
5
  import { EXEC_MAX_BUFFER, AUTO_SAVE_COMMIT_MESSAGE_PREFIX } from '../constants/git.js';
6
6
 
@@ -81,6 +81,16 @@ export function getStatusPorcelain(cwd?: string): string {
81
81
  return execCommand('git status --porcelain', { cwd });
82
82
  }
83
83
 
84
+ /**
85
+ * 异步获取工作区状态(git status --porcelain)
86
+ * 用于并行收集多个 worktree 状态时避免串行阻塞
87
+ * @param {string} cwd - 工作目录
88
+ * @returns {Promise<string>} porcelain 格式输出,为空表示干净
89
+ */
90
+ export async function getStatusPorcelainAsync(cwd?: string): Promise<string> {
91
+ return execCommandAsync('git status --porcelain', { cwd });
92
+ }
93
+
84
94
  /**
85
95
  * 判断工作区是否干净
86
96
  * @param {string} cwd - 工作目录
@@ -256,6 +266,17 @@ export function getDiffStat(worktreePath: string): { insertions: number; deletio
256
266
  return parseShortStat(output);
257
267
  }
258
268
 
269
+ /**
270
+ * 异步获取 worktree 中工作区和暂存区的变更统计
271
+ * 用于并行收集多个 worktree 的 diff 统计时避免串行阻塞
272
+ * @param {string} worktreePath - worktree 目录路径
273
+ * @returns {Promise<{ insertions: number; deletions: number }>} 新增和删除行数
274
+ */
275
+ export async function getDiffStatAsync(worktreePath: string): Promise<{ insertions: number; deletions: number }> {
276
+ const output = await execCommandAsync('git diff --shortstat HEAD', { cwd: worktreePath });
277
+ return parseShortStat(output);
278
+ }
279
+
259
280
  /**
260
281
  * 获取暂存区相对于 HEAD 的完整 diff(含二进制文件)
261
282
  * 注意:返回原始输出不做 trim,保留 patch 格式完整性
@@ -1,4 +1,4 @@
1
- export { execCommand, spawnProcess, killAllChildProcesses, execCommandWithInput, runCommandInherited, parseParallelCommands, runParallelCommands, runCommandWithStderrCapture, runParallelCommandsWithStderrCapture } from './shell.js';
1
+ export { execCommand, execCommandAsync, spawnProcess, killAllChildProcesses, execCommandWithInput, runCommandInherited, parseParallelCommands, runParallelCommands, runCommandWithStderrCapture, runParallelCommandsWithStderrCapture } from './shell.js';
2
2
  export type { ParallelCommandResult, CommandResultWithStderr, ParallelCommandResultWithStderr } from './shell.js';
3
3
  export { copyToClipboard } from './clipboard.js';
4
4
  export {
@@ -13,6 +13,7 @@ export {
13
13
  removeWorktreeByPath,
14
14
  deleteBranch,
15
15
  getStatusPorcelain,
16
+ getStatusPorcelainAsync,
16
17
  isWorkingDirClean,
17
18
  gitAddAll,
18
19
  gitCommit,
@@ -34,7 +35,10 @@ export {
34
35
  hasLocalCommits,
35
36
  getCommitCountAhead,
36
37
  getCommitCountBehind,
38
+ getCommitDivergence,
39
+ getCommitDivergenceAsync,
37
40
  getDiffStat,
41
+ getDiffStatAsync,
38
42
  gitDiffCachedBinary,
39
43
  gitApplyCachedFromStdin,
40
44
  getCurrentBranch,
@@ -82,7 +86,7 @@ export { truncateTaskDesc, printDryRunPreview } from './dry-run.js';
82
86
  export { applyAliases } from './alias.js';
83
87
  export { isValidConfigKey, getValidConfigKeys, parseConfigValue, promptConfigValue, formatConfigValue, interactiveConfigEditor } from './config-strategy.js';
84
88
  export { checkForUpdates } from './update-checker.js';
85
- export { getProjectConfigPath, loadProjectConfig, saveProjectConfig, requireProjectConfig, getMainWorkBranch, guardMainWorkBranchExists, getValidateRunCommand } from './project-config.js';
89
+ export { getProjectConfigPath, loadProjectConfig, saveProjectConfig, requireProjectConfig, getMainWorkBranch, guardMainWorkBranchExists, getValidateRunCommand, resolveClaudeCodeCommand, normalizeProjectConfig } from './project-config.js';
86
90
  export { getValidateBranchName, createValidateBranch, deleteValidateBranch, rebuildValidateBranch, ensureOnMainWorkBranch, handleDirtyWorkingDir } from './validate-branch.js';
87
91
  export { safeStringify } from './json.js';
88
92
  export { isNonInteractive, setNonInteractive } from './interactive.js';
@@ -69,6 +69,7 @@ function buildSeparatorWithHint(cols: number, hint: string): string {
69
69
  * @param {number} rows - 终端行数
70
70
  * @param {number} cols - 终端列数
71
71
  * @param {number} countdown - 刷新倒计时秒数
72
+ * @param {PanelLine[]} [cachedPanelLines] - 缓存的面板行列表(传入时复用,不传则重新构建)
72
73
  * @returns {string[]} 帧内容行数组
73
74
  */
74
75
  export function buildPanelFrame(
@@ -78,6 +79,7 @@ export function buildPanelFrame(
78
79
  rows: number,
79
80
  cols: number,
80
81
  countdown: number,
82
+ cachedPanelLines?: PanelLine[],
81
83
  ): string[] {
82
84
  const lines: string[] = [];
83
85
 
@@ -100,8 +102,8 @@ export function buildPanelFrame(
100
102
  // 底部分隔线(无溢出提示)
101
103
  lines.push(buildSeparatorWithHint(cols, ''));
102
104
  } else {
103
- // 构建分组的 worktree 行列表
104
- const panelLines = buildGroupedWorktreeLines(statusResult.worktrees, selectedIndex);
105
+ // 构建分组的 worktree 行列表(优先使用缓存)
106
+ const panelLines = cachedPanelLines ?? buildGroupedWorktreeLines(statusResult.worktrees, selectedIndex);
105
107
 
106
108
  // 判断溢出状态
107
109
  const hasOverflowUp = scrollOffset > 0;