clawt 1.0.5 → 1.1.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.
@@ -4,9 +4,10 @@
4
4
 
5
5
  ### docs/spec.md
6
6
  - 完整的软件规格说明,包含 7 大章节
7
- - 命令流程在 `5. 需求场景详细设计` 下,每个命令一个子章节(5.1-5.8
7
+ - 命令流程在 `5. 需求场景详细设计` 下,每个命令一个子章节(5.1-5.10
8
8
  - run 命令对应 `5.2 批量创建 Worktree + 执行 Claude Code 任务`,流程按步骤编号描述
9
9
  - merge 命令对应 `5.6 合并验证过的分支`,流程按步骤编号描述
10
+ - config 命令对应 `5.10 查看全局配置`,只读展示配置
10
11
  - 配置项说明在 `5.7 默认配置文件` 章节的表格中
11
12
  - 更新模式:新增步骤时追加编号,配置项影响范围变化时更新说明列
12
13
 
@@ -34,15 +35,26 @@
34
35
  ## 配置项同步检查点
35
36
 
36
37
  配置项变更时需在以下 5 处保持一致:
37
- 1. `src/constants/config.ts` — DEFAULT_CONFIG 对象
38
+ 1. `src/constants/config.ts` — CONFIG_DEFINITIONS 对象(单一数据源,包含 defaultValue + description)
38
39
  2. `src/types/config.ts` — ClawtConfig 接口
39
40
  3. `docs/spec.md` — 5.7 默认配置文件章节(JSON 示例 + 配置项表格)
40
41
  4. `CLAUDE.md` — 关键约定段落中的配置描述
41
42
  5. `README.md` — 配置文件章节(JSON 示例 + 配置项表格)
42
43
 
44
+ ## 配置架构
45
+
46
+ - `CONFIG_DEFINITIONS` 是配置项的单一数据源,定义在 `src/constants/config.ts`
47
+ - `DEFAULT_CONFIG` 和 `CONFIG_DESCRIPTIONS` 均从 `CONFIG_DEFINITIONS` 派生
48
+ - 新增配置项时只需在 `CONFIG_DEFINITIONS` 中维护,派生值自动同步
49
+ - 类型:`ConfigItemDefinition<T>` 和 `ConfigDefinitions` 定义在 `src/types/config.ts`
50
+
43
51
  ## run 命令双模式
44
52
 
45
53
  run 命令有两种模式(自 claudeCodeCommand 特性后):
46
54
  - 不传 `--tasks`:交互式界面模式(单 worktree + `launchInteractiveClaude` + spawnSync)
47
55
  - 传 `--tasks`:并行任务模式(多 worktree + `executeClaudeTask` + spawnProcess)
48
56
  - CLAUDE.md 中的核心流程按模式分段描述
57
+
58
+ ## 命令清单(7 个)
59
+
60
+ `create`、`run`、`list`、`remove`、`validate`、`merge`、`config`
package/CLAUDE.md CHANGED
@@ -24,7 +24,7 @@ npm i -g . # 本地全局安装进行测试
24
24
 
25
25
  每个命令为独立文件 `src/commands/<name>.ts`,导出 `registerXxxCommand(program)` 函数,在 `src/index.ts` 中统一注册到 Commander。命令内部逻辑封装在对应的 `handleXxx` 函数中。
26
26
 
27
- 六个命令:`create`、`run`、`list`、`remove`、`validate`、`merge`。
27
+ 七个命令:`create`、`run`、`list`、`remove`、`validate`、`merge`、`config`。
28
28
 
29
29
  ### 核心流程(run 命令)
30
30
 
@@ -65,7 +65,7 @@ run 命令有两种模式:
65
65
 
66
66
  - 所有命令执行前都会调用 `validateMainWorktree()` 确保在主 worktree 根目录(`git rev-parse --git-common-dir === ".git"`)
67
67
  - Worktree 统一存放在 `~/.clawt/worktrees/<projectName>/` 下
68
- - 全局配置文件 `~/.clawt/config.json`,postinstall 时自动创建/合并,包含 `autoDeleteBranch`(是否自动删除分支)和 `claudeCodeCommand`(Claude Code CLI 启动指令)两个配置项
68
+ - 全局配置文件 `~/.clawt/config.json`,postinstall 时自动创建/合并,包含 `autoDeleteBranch`(是否自动删除分支)、`claudeCodeCommand`(Claude Code CLI 启动指令)、`autoPullPush`(merge 后是否自动 pull/push)三个配置项。配置项以 `CONFIG_DEFINITIONS` 为单一数据源,`DEFAULT_CONFIG` 和 `CONFIG_DESCRIPTIONS` 均从中派生
69
69
  - shell 命令执行有同步(`execCommand` → `execSync`)和异步(`spawnProcess` → `spawn`)两种方式
70
70
  - 项目为纯 ESM(`"type": "module"`),模块导入需带 `.js` 后缀
71
71
  - 分支名特殊字符会被 `sanitizeBranchName()` 自动清理
package/README.md CHANGED
@@ -139,6 +139,14 @@ clawt list
139
139
 
140
140
  列出当前项目在 `~/.clawt/worktrees/` 下的所有 worktree 及对应分支。
141
141
 
142
+ ### `clawt config` — 查看全局配置
143
+
144
+ ```bash
145
+ clawt config
146
+ ```
147
+
148
+ 读取并展示全局配置文件 `~/.clawt/config.json` 中的所有配置项,包括每项的当前值和描述说明。编辑配置需直接修改配置文件。
149
+
142
150
  ## 配置文件
143
151
 
144
152
  安装后会自动在 `~/.clawt/config.json` 生成全局配置文件:
@@ -146,7 +154,8 @@ clawt list
146
154
  ```json
147
155
  {
148
156
  "autoDeleteBranch": false,
149
- "claudeCodeCommand": "claude"
157
+ "claudeCodeCommand": "claude",
158
+ "autoPullPush": false
150
159
  }
151
160
  ```
152
161
 
@@ -154,6 +163,7 @@ clawt list
154
163
  | ------ | ---- | ------ | ---- |
155
164
  | `autoDeleteBranch` | `boolean` | `false` | 移除 worktree 时自动删除对应本地分支;merge 成功后自动清理 worktree 和分支;run 中断后自动清理本次创建的 worktree 和分支 |
156
165
  | `claudeCodeCommand` | `string` | `"claude"` | Claude Code CLI 启动指令,用于 `clawt run` 不传 `--tasks` 时在 worktree 中打开交互式界面 |
166
+ | `autoPullPush` | `boolean` | `false` | merge 成功后是否自动执行 git pull 和 git push |
157
167
 
158
168
  ## 分支名规则
159
169
 
package/dist/index.js CHANGED
@@ -79,7 +79,9 @@ var MESSAGES = {
79
79
  /** 粗分隔线 */
80
80
  DOUBLE_SEPARATOR: "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
81
81
  /** 创建数量参数无效 */
82
- INVALID_COUNT: (value) => `\u65E0\u6548\u7684\u521B\u5EFA\u6570\u91CF: "${value}"\uFF0C\u8BF7\u8F93\u5165\u6B63\u6574\u6570`
82
+ INVALID_COUNT: (value) => `\u65E0\u6548\u7684\u521B\u5EFA\u6570\u91CF: "${value}"\uFF0C\u8BF7\u8F93\u5165\u6B63\u6574\u6570`,
83
+ /** worktree 状态获取失败 */
84
+ WORKTREE_STATUS_UNAVAILABLE: "(\u72B6\u6001\u4E0D\u53EF\u7528)"
83
85
  };
84
86
 
85
87
  // src/constants/exitCodes.ts
@@ -94,13 +96,34 @@ var EXIT_CODES = {
94
96
 
95
97
  // src/constants/config.ts
96
98
  var APPEND_SYSTEM_PROMPT = "After the code execution is completed, it is prohibited to build the project for verification.";
97
- var DEFAULT_CONFIG = {
98
- autoDeleteBranch: false,
99
- /** 默认 Claude Code CLI 启动指令,用于不传 --tasks 时在 worktree 中打开交互式界面 */
100
- claudeCodeCommand: "claude",
101
- /** 默认不自动执行 git pull 和 git push,需用户手动操作 */
102
- autoPullPush: false
99
+ var CONFIG_DEFINITIONS = {
100
+ autoDeleteBranch: {
101
+ defaultValue: false,
102
+ description: "\u79FB\u9664 worktree \u65F6\u662F\u5426\u81EA\u52A8\u5220\u9664\u5BF9\u5E94\u672C\u5730\u5206\u652F"
103
+ },
104
+ claudeCodeCommand: {
105
+ defaultValue: "claude",
106
+ description: "Claude Code CLI \u542F\u52A8\u6307\u4EE4"
107
+ },
108
+ autoPullPush: {
109
+ defaultValue: false,
110
+ description: "merge \u6210\u529F\u540E\u662F\u5426\u81EA\u52A8\u6267\u884C git pull \u548C git push"
111
+ }
103
112
  };
113
+ function deriveDefaultConfig(definitions) {
114
+ const entries = Object.entries(definitions).map(
115
+ ([key, def]) => [key, def.defaultValue]
116
+ );
117
+ return Object.fromEntries(entries);
118
+ }
119
+ function deriveConfigDescriptions(definitions) {
120
+ const entries = Object.entries(definitions).map(
121
+ ([key, def]) => [key, def.description]
122
+ );
123
+ return Object.fromEntries(entries);
124
+ }
125
+ var DEFAULT_CONFIG = deriveDefaultConfig(CONFIG_DEFINITIONS);
126
+ var CONFIG_DESCRIPTIONS = deriveConfigDescriptions(CONFIG_DEFINITIONS);
104
127
 
105
128
  // src/errors/index.ts
106
129
  var ClawtError = class extends Error {
@@ -266,6 +289,33 @@ function hasLocalCommits(branchName, cwd) {
266
289
  return false;
267
290
  }
268
291
  }
292
+ function getCommitCountAhead(branchName, cwd) {
293
+ const output = execCommand(`git rev-list --count HEAD..${branchName}`, { cwd });
294
+ return parseInt(output, 10) || 0;
295
+ }
296
+ function parseShortStat(output) {
297
+ let insertions = 0;
298
+ let deletions = 0;
299
+ const insertMatch = output.match(/(\d+)\s+insertion/);
300
+ if (insertMatch) {
301
+ insertions = parseInt(insertMatch[1], 10);
302
+ }
303
+ const deleteMatch = output.match(/(\d+)\s+deletion/);
304
+ if (deleteMatch) {
305
+ deletions = parseInt(deleteMatch[1], 10);
306
+ }
307
+ return { insertions, deletions };
308
+ }
309
+ function getDiffStat(branchName, worktreePath, cwd) {
310
+ const committedOutput = execCommand(`git diff --shortstat HEAD...${branchName}`, { cwd });
311
+ const committed = parseShortStat(committedOutput);
312
+ const uncommittedOutput = execCommand("git diff --shortstat HEAD", { cwd: worktreePath });
313
+ const uncommitted = parseShortStat(uncommittedOutput);
314
+ return {
315
+ insertions: committed.insertions + uncommitted.insertions,
316
+ deletions: committed.deletions + uncommitted.deletions
317
+ };
318
+ }
269
319
 
270
320
  // src/utils/formatter.ts
271
321
  import chalk from "chalk";
@@ -300,6 +350,22 @@ function confirmAction(question) {
300
350
  });
301
351
  });
302
352
  }
353
+ function formatWorktreeStatus(status) {
354
+ const parts = [];
355
+ parts.push(chalk.yellow(`${status.commitCount} \u4E2A\u63D0\u4EA4`));
356
+ if (status.insertions === 0 && status.deletions === 0) {
357
+ parts.push("\u65E0\u53D8\u66F4");
358
+ } else {
359
+ const diffParts = [];
360
+ diffParts.push(chalk.green(`+${status.insertions}`));
361
+ diffParts.push(chalk.red(`-${status.deletions}`));
362
+ parts.push(diffParts.join(" "));
363
+ }
364
+ if (status.hasDirtyFiles) {
365
+ parts.push(chalk.gray("(\u672A\u63D0\u4EA4\u4FEE\u6539)"));
366
+ }
367
+ return parts.join(" ");
368
+ }
303
369
 
304
370
  // src/utils/branch.ts
305
371
  function sanitizeBranchName(branchName) {
@@ -428,6 +494,17 @@ function cleanupWorktrees(worktrees) {
428
494
  const projectDir = getProjectWorktreeDir();
429
495
  removeEmptyDir(projectDir);
430
496
  }
497
+ function getWorktreeStatus(worktree) {
498
+ try {
499
+ const commitCount = getCommitCountAhead(worktree.branch);
500
+ const { insertions, deletions } = getDiffStat(worktree.branch, worktree.path);
501
+ const hasDirtyFiles = !isWorkingDirClean(worktree.path);
502
+ return { commitCount, insertions, deletions, hasDirtyFiles };
503
+ } catch (error) {
504
+ logger.error(`\u83B7\u53D6 worktree \u72B6\u6001\u5931\u8D25: ${worktree.path} - ${error}`);
505
+ return null;
506
+ }
507
+ }
431
508
 
432
509
  // src/utils/config.ts
433
510
  import { existsSync as existsSync4, readFileSync, writeFileSync } from "fs";
@@ -461,6 +538,7 @@ function ensureClawtDirs() {
461
538
  import Enquirer from "enquirer";
462
539
 
463
540
  // src/commands/list.ts
541
+ import chalk2 from "chalk";
464
542
  function registerListCommand(program2) {
465
543
  program2.command("list").description("\u5217\u51FA\u5F53\u524D\u9879\u76EE\u6240\u6709 worktree").action(() => {
466
544
  handleList();
@@ -478,9 +556,15 @@ function handleList() {
478
556
  } else {
479
557
  for (const wt of worktrees) {
480
558
  printInfo(` ${wt.path} [${wt.branch}]`);
559
+ const status = getWorktreeStatus(wt);
560
+ if (status) {
561
+ printInfo(` ${formatWorktreeStatus(status)}`);
562
+ } else {
563
+ printInfo(` ${chalk2.yellow(MESSAGES.WORKTREE_STATUS_UNAVAILABLE)}`);
564
+ }
565
+ printInfo("");
481
566
  }
482
- printInfo(`
483
- \u5171 ${worktrees.length} \u4E2A worktree`);
567
+ printInfo(`\u5171 ${worktrees.length} \u4E2A worktree`);
484
568
  }
485
569
  }
486
570
 
@@ -918,6 +1002,38 @@ async function handleMerge(options) {
918
1002
  }
919
1003
  }
920
1004
 
1005
+ // src/commands/config.ts
1006
+ import chalk3 from "chalk";
1007
+ function registerConfigCommand(program2) {
1008
+ program2.command("config").description("\u67E5\u770B\u5168\u5C40\u914D\u7F6E").action(() => {
1009
+ handleConfig();
1010
+ });
1011
+ }
1012
+ function handleConfig() {
1013
+ const config = loadConfig();
1014
+ logger.info("config \u547D\u4EE4\u6267\u884C\uFF0C\u5C55\u793A\u5168\u5C40\u914D\u7F6E");
1015
+ printInfo(`
1016
+ ${chalk3.dim("\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84:")} ${CONFIG_PATH}
1017
+ `);
1018
+ printSeparator();
1019
+ const keys = Object.keys(DEFAULT_CONFIG);
1020
+ for (const key of keys) {
1021
+ const value = config[key];
1022
+ const description = CONFIG_DESCRIPTIONS[key];
1023
+ const formattedValue = formatConfigValue(value);
1024
+ printInfo(` ${chalk3.bold(key)}: ${formattedValue}`);
1025
+ printInfo(` ${chalk3.dim(description)}
1026
+ `);
1027
+ }
1028
+ printSeparator();
1029
+ }
1030
+ function formatConfigValue(value) {
1031
+ if (typeof value === "boolean") {
1032
+ return value ? chalk3.green("true") : chalk3.yellow("false");
1033
+ }
1034
+ return chalk3.cyan(String(value));
1035
+ }
1036
+
921
1037
  // src/index.ts
922
1038
  var require2 = createRequire(import.meta.url);
923
1039
  var { version } = require2("../package.json");
@@ -930,6 +1046,7 @@ registerRemoveCommand(program);
930
1046
  registerRunCommand(program);
931
1047
  registerValidateCommand(program);
932
1048
  registerMergeCommand(program);
1049
+ registerConfigCommand(program);
933
1050
  process.on("uncaughtException", (error) => {
934
1051
  if (error instanceof ClawtError) {
935
1052
  printError(error.message);
@@ -12,13 +12,34 @@ var LOGS_DIR = join(CLAWT_HOME, "logs");
12
12
  var WORKTREES_DIR = join(CLAWT_HOME, "worktrees");
13
13
 
14
14
  // src/constants/config.ts
15
- var DEFAULT_CONFIG = {
16
- autoDeleteBranch: false,
17
- /** 默认 Claude Code CLI 启动指令,用于不传 --tasks 时在 worktree 中打开交互式界面 */
18
- claudeCodeCommand: "claude",
19
- /** 默认不自动执行 git pull 和 git push,需用户手动操作 */
20
- autoPullPush: false
15
+ var CONFIG_DEFINITIONS = {
16
+ autoDeleteBranch: {
17
+ defaultValue: false,
18
+ description: "\u79FB\u9664 worktree \u65F6\u662F\u5426\u81EA\u52A8\u5220\u9664\u5BF9\u5E94\u672C\u5730\u5206\u652F"
19
+ },
20
+ claudeCodeCommand: {
21
+ defaultValue: "claude",
22
+ description: "Claude Code CLI \u542F\u52A8\u6307\u4EE4"
23
+ },
24
+ autoPullPush: {
25
+ defaultValue: false,
26
+ description: "merge \u6210\u529F\u540E\u662F\u5426\u81EA\u52A8\u6267\u884C git pull \u548C git push"
27
+ }
21
28
  };
29
+ function deriveDefaultConfig(definitions) {
30
+ const entries = Object.entries(definitions).map(
31
+ ([key, def]) => [key, def.defaultValue]
32
+ );
33
+ return Object.fromEntries(entries);
34
+ }
35
+ function deriveConfigDescriptions(definitions) {
36
+ const entries = Object.entries(definitions).map(
37
+ ([key, def]) => [key, def.description]
38
+ );
39
+ return Object.fromEntries(entries);
40
+ }
41
+ var DEFAULT_CONFIG = deriveDefaultConfig(CONFIG_DEFINITIONS);
42
+ var CONFIG_DESCRIPTIONS = deriveConfigDescriptions(CONFIG_DEFINITIONS);
22
43
 
23
44
  // scripts/postinstall.ts
24
45
  var CLAWT_HOME2 = join2(homedir2(), ".clawt");
package/docs/spec.md CHANGED
@@ -21,6 +21,7 @@
21
21
  - [5.7 默认配置文件](#57-默认配置文件)
22
22
  - [5.8 获取当前项目所有 Worktree](#58-获取当前项目所有-worktree)
23
23
  - [5.9 日志系统](#59-日志系统)
24
+ - [5.10 查看全局配置](#510-查看全局配置)
24
25
  - [6. 错误处理规范](#6-错误处理规范)
25
26
  - [7. 非功能性需求](#7-非功能性需求)
26
27
 
@@ -157,6 +158,7 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
157
158
  | `clawt merge` | 合并某个已验证的 worktree 分支到主 worktree | 5.6 |
158
159
  | `clawt remove` | 移除 worktree(支持单个/批量/全部) | 5.5 |
159
160
  | `clawt list` | 列出当前项目所有 worktree | 5.8 |
161
+ | `clawt config` | 查看全局配置 | 5.10 |
160
162
 
161
163
  所有命令执行前,都必须先执行**主 worktree 校验**(见 [2.1](#21-主-worktree-的定义与定位规则))。
162
164
 
@@ -580,7 +582,8 @@ clawt merge -b <branchName> [-m <commitMessage>]
580
582
  ```json
581
583
  {
582
584
  "autoDeleteBranch": false,
583
- "claudeCodeCommand": "claude"
585
+ "claudeCodeCommand": "claude",
586
+ "autoPullPush": false
584
587
  }
585
588
  ```
586
589
 
@@ -590,6 +593,7 @@ clawt merge -b <branchName> [-m <commitMessage>]
590
593
  | ------------------ | --------- | --------- | -------------------------------------------------- |
591
594
  | `autoDeleteBranch` | `boolean` | `false` | 移除 worktree 时是否自动删除对应本地分支(无需每次确认);merge 成功后是否自动清理 worktree 和分支;run 任务被中断(Ctrl+C)后是否自动清理本次创建的 worktree 和分支 |
592
595
  | `claudeCodeCommand` | `string` | `"claude"` | Claude Code CLI 启动指令,用于 `clawt run` 不传 `--tasks` 时在 worktree 中打开交互式界面 |
596
+ | `autoPullPush` | `boolean` | `false` | merge 成功后是否自动执行 git pull 和 git push |
593
597
 
594
598
  ---
595
599
 
@@ -665,6 +669,43 @@ clawt list
665
669
 
666
670
  ---
667
671
 
672
+ ### 5.10 查看全局配置
673
+
674
+ **命令:**
675
+
676
+ ```bash
677
+ clawt config
678
+ ```
679
+
680
+ **运行流程:**
681
+
682
+ 1. 读取全局配置文件 `~/.clawt/config.json`
683
+ 2. 遍历所有配置项(以 `CONFIG_DEFINITIONS` 为单一数据源),逐项展示:
684
+ - 配置项名称(粗体)
685
+ - 当前值(布尔值绿色/黄色,字符串青色)
686
+ - 配置项描述(灰色)
687
+ 3. 输出配置文件路径,提示用户可直接编辑
688
+
689
+ **输出格式:**
690
+
691
+ ```
692
+ 配置文件路径: ~/.clawt/config.json
693
+ ────────────────────────────────────────
694
+ autoDeleteBranch: false
695
+ 移除 worktree 时是否自动删除对应本地分支
696
+
697
+ claudeCodeCommand: claude
698
+ Claude Code CLI 启动指令
699
+
700
+ autoPullPush: false
701
+ merge 成功后是否自动执行 git pull 和 git push
702
+
703
+ ────────────────────────────────────────
704
+
705
+ ```
706
+
707
+ ---
708
+
668
709
  ## 6. 错误处理规范
669
710
 
670
711
  ### 6.1 通用错误处理
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,56 @@
1
+ import type { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { CONFIG_PATH, DEFAULT_CONFIG, CONFIG_DESCRIPTIONS } from '../constants/index.js';
4
+ import { logger } from '../logger/index.js';
5
+ import { loadConfig, printInfo, printSeparator } from '../utils/index.js';
6
+ import type { ClawtConfig } from '../types/index.js';
7
+
8
+ /**
9
+ * 注册 config 命令:查看全局配置
10
+ * @param {Command} program - Commander 实例
11
+ */
12
+ export function registerConfigCommand(program: Command): void {
13
+ program
14
+ .command('config')
15
+ .description('查看全局配置')
16
+ .action(() => {
17
+ handleConfig();
18
+ });
19
+ }
20
+
21
+ /**
22
+ * 执行 config 命令的核心逻辑,读取并展示配置列表
23
+ */
24
+ function handleConfig(): void {
25
+ const config = loadConfig();
26
+
27
+ logger.info('config 命令执行,展示全局配置');
28
+
29
+ printInfo(`\n${chalk.dim('配置文件路径:')} ${CONFIG_PATH}\n`);
30
+ printSeparator();
31
+
32
+ const keys = Object.keys(DEFAULT_CONFIG) as Array<keyof ClawtConfig>;
33
+
34
+ for (const key of keys) {
35
+ const value = config[key];
36
+ const description = CONFIG_DESCRIPTIONS[key];
37
+ const formattedValue = formatConfigValue(value);
38
+
39
+ printInfo(` ${chalk.bold(key)}: ${formattedValue}`);
40
+ printInfo(` ${chalk.dim(description)}\n`);
41
+ }
42
+
43
+ printSeparator();
44
+ }
45
+
46
+ /**
47
+ * 格式化配置值的显示样式
48
+ * @param {ClawtConfig[keyof ClawtConfig]} value - 配置值
49
+ * @returns {string} 格式化后的字符串
50
+ */
51
+ function formatConfigValue(value: ClawtConfig[keyof ClawtConfig]): string {
52
+ if (typeof value === 'boolean') {
53
+ return value ? chalk.green('true') : chalk.yellow('false');
54
+ }
55
+ return chalk.cyan(String(value));
56
+ }
@@ -1,10 +1,13 @@
1
1
  import type { Command } from 'commander';
2
+ import chalk from 'chalk';
2
3
  import { MESSAGES } from '../constants/index.js';
3
4
  import { logger } from '../logger/index.js';
4
5
  import {
5
6
  validateMainWorktree,
6
7
  getProjectName,
7
8
  getProjectWorktrees,
9
+ getWorktreeStatus,
10
+ formatWorktreeStatus,
8
11
  printInfo,
9
12
  } from '../utils/index.js';
10
13
 
@@ -39,7 +42,17 @@ function handleList(): void {
39
42
  } else {
40
43
  for (const wt of worktrees) {
41
44
  printInfo(` ${wt.path} [${wt.branch}]`);
45
+
46
+ // 获取并展示 worktree 变更状态
47
+ const status = getWorktreeStatus(wt);
48
+ if (status) {
49
+ printInfo(` ${formatWorktreeStatus(status)}`);
50
+ } else {
51
+ printInfo(` ${chalk.yellow(MESSAGES.WORKTREE_STATUS_UNAVAILABLE)}`);
52
+ }
53
+
54
+ printInfo('');
42
55
  }
43
- printInfo(`\n共 ${worktrees.length} 个 worktree`);
56
+ printInfo(`共 ${worktrees.length} 个 worktree`);
44
57
  }
45
58
  }
@@ -1,14 +1,54 @@
1
- import type { ClawtConfig } from '../types/index.js';
1
+ import type { ClawtConfig, ConfigDefinitions } from '../types/index.js';
2
2
 
3
3
  /** Claude Code 系统约束提示,禁止代码执行完成后构建项目验证 */
4
4
  export const APPEND_SYSTEM_PROMPT =
5
5
  'After the code execution is completed, it is prohibited to build the project for verification.';
6
6
 
7
- /** 默认配置 */
8
- export const DEFAULT_CONFIG: ClawtConfig = {
9
- autoDeleteBranch: false,
10
- /** 默认 Claude Code CLI 启动指令,用于不传 --tasks 时在 worktree 中打开交互式界面 */
11
- claudeCodeCommand: 'claude',
12
- /** 默认不自动执行 git pull 和 git push,需用户手动操作 */
13
- autoPullPush: false,
7
+ /**
8
+ * 配置项完整定义(单一数据源)
9
+ * 新增配置项时只需在此处维护,DEFAULT_CONFIG 和 CONFIG_DESCRIPTIONS 会自动同步
10
+ */
11
+ export const CONFIG_DEFINITIONS: ConfigDefinitions = {
12
+ autoDeleteBranch: {
13
+ defaultValue: false,
14
+ description: '移除 worktree 时是否自动删除对应本地分支',
15
+ },
16
+ claudeCodeCommand: {
17
+ defaultValue: 'claude',
18
+ description: 'Claude Code CLI 启动指令',
19
+ },
20
+ autoPullPush: {
21
+ defaultValue: false,
22
+ description: 'merge 成功后是否自动执行 git pull 和 git push',
23
+ },
14
24
  };
25
+
26
+ /**
27
+ * 从 CONFIG_DEFINITIONS 派生默认配置
28
+ * @param {ConfigDefinitions} definitions - 配置项完整定义
29
+ * @returns {ClawtConfig} 默认配置对象
30
+ */
31
+ function deriveDefaultConfig(definitions: ConfigDefinitions): ClawtConfig {
32
+ const entries = Object.entries(definitions).map(
33
+ ([key, def]) => [key, def.defaultValue],
34
+ );
35
+ return Object.fromEntries(entries) as ClawtConfig;
36
+ }
37
+
38
+ /**
39
+ * 从 CONFIG_DEFINITIONS 派生配置项描述映射
40
+ * @param {ConfigDefinitions} definitions - 配置项完整定义
41
+ * @returns {Record<keyof ClawtConfig, string>} 配置项描述映射
42
+ */
43
+ function deriveConfigDescriptions(definitions: ConfigDefinitions): Record<keyof ClawtConfig, string> {
44
+ const entries = Object.entries(definitions).map(
45
+ ([key, def]) => [key, def.description],
46
+ );
47
+ return Object.fromEntries(entries) as Record<keyof ClawtConfig, string>;
48
+ }
49
+
50
+ /** 默认配置 */
51
+ export const DEFAULT_CONFIG: ClawtConfig = deriveDefaultConfig(CONFIG_DEFINITIONS);
52
+
53
+ /** 配置项描述映射 */
54
+ export const CONFIG_DESCRIPTIONS: Record<keyof ClawtConfig, string> = deriveConfigDescriptions(CONFIG_DEFINITIONS);
@@ -3,4 +3,4 @@ export { INVALID_BRANCH_CHARS } from './branch.js';
3
3
  export { MESSAGES } from './messages.js';
4
4
  export { EXIT_CODES } from './exitCodes.js';
5
5
  export { ENABLE_BRACKETED_PASTE, DISABLE_BRACKETED_PASTE, PASTE_THRESHOLD_MS } from './terminal.js';
6
- export { DEFAULT_CONFIG, APPEND_SYSTEM_PROMPT } from './config.js';
6
+ export { DEFAULT_CONFIG, CONFIG_DESCRIPTIONS, APPEND_SYSTEM_PROMPT } from './config.js';
@@ -62,4 +62,6 @@ export const MESSAGES = {
62
62
  DOUBLE_SEPARATOR: '════════════════════════════════════════',
63
63
  /** 创建数量参数无效 */
64
64
  INVALID_COUNT: (value: string) => `无效的创建数量: "${value}",请输入正整数`,
65
+ /** worktree 状态获取失败 */
66
+ WORKTREE_STATUS_UNAVAILABLE: '(状态不可用)',
65
67
  } as const;
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ import { registerRemoveCommand } from './commands/remove.js';
10
10
  import { registerRunCommand } from './commands/run.js';
11
11
  import { registerValidateCommand } from './commands/validate.js';
12
12
  import { registerMergeCommand } from './commands/merge.js';
13
+ import { registerConfigCommand } from './commands/config.js';
13
14
 
14
15
  // 从 package.json 读取版本号,避免硬编码
15
16
  const require = createRequire(import.meta.url);
@@ -32,6 +33,7 @@ registerRemoveCommand(program);
32
33
  registerRunCommand(program);
33
34
  registerValidateCommand(program);
34
35
  registerMergeCommand(program);
36
+ registerConfigCommand(program);
35
37
 
36
38
  // 全局未捕获异常处理
37
39
  process.on('uncaughtException', (error) => {
@@ -7,3 +7,16 @@ export interface ClawtConfig {
7
7
  /** merge 成功后是否自动执行 git pull 和 git push */
8
8
  autoPullPush: boolean;
9
9
  }
10
+
11
+ /** 单个配置项的完整定义(默认值 + 描述) */
12
+ export interface ConfigItemDefinition<T> {
13
+ /** 默认值 */
14
+ defaultValue: T;
15
+ /** 配置项描述,用于 config 命令展示 */
16
+ description: string;
17
+ }
18
+
19
+ /** 所有配置项的完整定义映射 */
20
+ export type ConfigDefinitions = {
21
+ [K in keyof ClawtConfig]: ConfigItemDefinition<ClawtConfig[K]>;
22
+ };
@@ -1,5 +1,5 @@
1
- export type { ClawtConfig } from './config.js';
1
+ export type { ClawtConfig, ConfigItemDefinition, ConfigDefinitions } from './config.js';
2
2
  export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions } from './command.js';
3
- export type { WorktreeInfo } from './worktree.js';
3
+ export type { WorktreeInfo, WorktreeStatus } from './worktree.js';
4
4
  export type { ClaudeCodeResult } from './claudeCode.js';
5
5
  export type { TaskResult, TaskSummary } from './taskResult.js';
@@ -5,3 +5,15 @@ export interface WorktreeInfo {
5
5
  /** 分支名 */
6
6
  branch: string;
7
7
  }
8
+
9
+ /** worktree 变更统计信息 */
10
+ export interface WorktreeStatus {
11
+ /** 相对于主分支的新增提交数 */
12
+ commitCount: number;
13
+ /** 新增行数 */
14
+ insertions: number;
15
+ /** 删除行数 */
16
+ deletions: number;
17
+ /** 工作区是否有未提交修改 */
18
+ hasDirtyFiles: boolean;
19
+ }
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { MESSAGES } from '../constants/index.js';
3
3
  import { createInterface } from 'node:readline';
4
+ import type { WorktreeStatus } from '../types/index.js';
4
5
 
5
6
  /**
6
7
  * 输出成功信息
@@ -65,3 +66,32 @@ export function confirmAction(question: string): Promise<boolean> {
65
66
  });
66
67
  });
67
68
  }
69
+
70
+ /**
71
+ * 将 WorktreeStatus 格式化为带颜色的字符串
72
+ * @param {WorktreeStatus} status - worktree 变更统计信息
73
+ * @returns {string} 格式化后的状态字符串
74
+ */
75
+ export function formatWorktreeStatus(status: WorktreeStatus): string {
76
+ const parts: string[] = [];
77
+
78
+ // 提交数(黄色)
79
+ parts.push(chalk.yellow(`${status.commitCount} 个提交`));
80
+
81
+ // 变更统计
82
+ if (status.insertions === 0 && status.deletions === 0) {
83
+ parts.push('无变更');
84
+ } else {
85
+ const diffParts: string[] = [];
86
+ diffParts.push(chalk.green(`+${status.insertions}`));
87
+ diffParts.push(chalk.red(`-${status.deletions}`));
88
+ parts.push(diffParts.join(' '));
89
+ }
90
+
91
+ // 未提交修改提示(灰色)
92
+ if (status.hasDirtyFiles) {
93
+ parts.push(chalk.gray('(未提交修改)'));
94
+ }
95
+
96
+ return parts.join(' ');
97
+ }
package/src/utils/git.ts CHANGED
@@ -241,3 +241,58 @@ export function hasLocalCommits(branchName: string, cwd?: string): boolean {
241
241
  return false;
242
242
  }
243
243
  }
244
+
245
+ /**
246
+ * 获取目标分支相对于当前分支的新增提交数
247
+ * @param {string} branchName - 目标分支名
248
+ * @param {string} [cwd] - 工作目录
249
+ * @returns {number} 新增提交数
250
+ */
251
+ export function getCommitCountAhead(branchName: string, cwd?: string): number {
252
+ const output = execCommand(`git rev-list --count HEAD..${branchName}`, { cwd });
253
+ return parseInt(output, 10) || 0;
254
+ }
255
+
256
+ /**
257
+ * 解析 git diff --shortstat 输出,提取新增行数和删除行数
258
+ * @param {string} output - shortstat 输出字符串
259
+ * @returns {{ insertions: number; deletions: number }} 新增和删除行数
260
+ */
261
+ function parseShortStat(output: string): { insertions: number; deletions: number } {
262
+ let insertions = 0;
263
+ let deletions = 0;
264
+
265
+ const insertMatch = output.match(/(\d+)\s+insertion/);
266
+ if (insertMatch) {
267
+ insertions = parseInt(insertMatch[1], 10);
268
+ }
269
+
270
+ const deleteMatch = output.match(/(\d+)\s+deletion/);
271
+ if (deleteMatch) {
272
+ deletions = parseInt(deleteMatch[1], 10);
273
+ }
274
+
275
+ return { insertions, deletions };
276
+ }
277
+
278
+ /**
279
+ * 获取目标分支的变更统计(已提交 + 未提交)
280
+ * @param {string} branchName - 目标分支名
281
+ * @param {string} worktreePath - worktree 目录路径
282
+ * @param {string} [cwd] - 执行 git diff HEAD...branch 的工作目录
283
+ * @returns {{ insertions: number; deletions: number }} 聚合后的新增和删除行数
284
+ */
285
+ export function getDiffStat(branchName: string, worktreePath: string, cwd?: string): { insertions: number; deletions: number } {
286
+ // 已提交的变更(当前分支与目标分支的差异)
287
+ const committedOutput = execCommand(`git diff --shortstat HEAD...${branchName}`, { cwd });
288
+ const committed = parseShortStat(committedOutput);
289
+
290
+ // 未提交的变更(在 worktree 内执行)
291
+ const uncommittedOutput = execCommand('git diff --shortstat HEAD', { cwd: worktreePath });
292
+ const uncommitted = parseShortStat(uncommittedOutput);
293
+
294
+ return {
295
+ insertions: committed.insertions + uncommitted.insertions,
296
+ deletions: committed.deletions + uncommitted.deletions,
297
+ };
298
+ }
@@ -25,11 +25,13 @@ export {
25
25
  gitWorktreeList,
26
26
  gitWorktreePrune,
27
27
  hasLocalCommits,
28
+ getCommitCountAhead,
29
+ getDiffStat,
28
30
  } from './git.js';
29
31
  export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
30
32
  export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
31
- export { createWorktrees, getProjectWorktrees, getProjectWorktreeDir, cleanupWorktrees } from './worktree.js';
33
+ export { createWorktrees, getProjectWorktrees, getProjectWorktreeDir, cleanupWorktrees, getWorktreeStatus } from './worktree.js';
32
34
  export { loadConfig, getConfigValue, ensureClawtDirs } from './config.js';
33
- export { printSuccess, printError, printWarning, printInfo, printSeparator, printDoubleSeparator, confirmAction } from './formatter.js';
35
+ export { printSuccess, printError, printWarning, printInfo, printSeparator, printDoubleSeparator, confirmAction, formatWorktreeStatus } from './formatter.js';
34
36
  export { ensureDir, removeEmptyDir } from './fs.js';
35
37
  export { multilineInput } from './prompt.js';
@@ -2,10 +2,10 @@ import { join } from 'node:path';
2
2
  import { existsSync, readdirSync } from 'node:fs';
3
3
  import { WORKTREES_DIR } from '../constants/index.js';
4
4
  import { logger } from '../logger/index.js';
5
- import { createWorktree as gitCreateWorktree, getProjectName, gitWorktreeList, removeWorktreeByPath, deleteBranch, gitWorktreePrune } from './git.js';
5
+ import { createWorktree as gitCreateWorktree, getProjectName, gitWorktreeList, removeWorktreeByPath, deleteBranch, gitWorktreePrune, getCommitCountAhead, getDiffStat, isWorkingDirClean } from './git.js';
6
6
  import { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
7
7
  import { ensureDir, removeEmptyDir } from './fs.js';
8
- import type { WorktreeInfo } from '../types/index.js';
8
+ import type { WorktreeInfo, WorktreeStatus } from '../types/index.js';
9
9
 
10
10
  /**
11
11
  * 获取当前项目的 worktree 存放目录
@@ -105,3 +105,22 @@ export function cleanupWorktrees(worktrees: WorktreeInfo[]): void {
105
105
  const projectDir = getProjectWorktreeDir();
106
106
  removeEmptyDir(projectDir);
107
107
  }
108
+
109
+ /**
110
+ * 获取 worktree 的变更统计信息
111
+ * 聚合提交数、变更行数、未提交修改状态
112
+ * @param {WorktreeInfo} worktree - worktree 信息
113
+ * @returns {WorktreeStatus | null} 变更统计信息,获取失败时返回 null
114
+ */
115
+ export function getWorktreeStatus(worktree: WorktreeInfo): WorktreeStatus | null {
116
+ try {
117
+ const commitCount = getCommitCountAhead(worktree.branch);
118
+ const { insertions, deletions } = getDiffStat(worktree.branch, worktree.path);
119
+ const hasDirtyFiles = !isWorkingDirClean(worktree.path);
120
+
121
+ return { commitCount, insertions, deletions, hasDirtyFiles };
122
+ } catch (error) {
123
+ logger.error(`获取 worktree 状态失败: ${worktree.path} - ${error}`);
124
+ return null;
125
+ }
126
+ }