clawt 3.9.1 → 3.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -649,13 +649,6 @@ var PANEL_FOOTER_SHORTCUTS = Object.entries(SHORTCUT_LABELS).map(([key, label])
649
649
  var PANEL_FOOTER_COUNTDOWN = (seconds) => chalk.gray(`(${seconds}s \u540E\u5237\u65B0)`);
650
650
  var PANEL_OVERFLOW_DOWN_HINT = chalk.gray("\u2193 \u66F4\u591A worktree...");
651
651
  var PANEL_OVERFLOW_UP_HINT = chalk.gray("\u2191 \u66F4\u591A worktree...");
652
- var PANEL_SNAPSHOT_SUMMARY = (total, orphaned) => {
653
- const base = `\u5FEB\u7167: ${total} \u4E2A`;
654
- if (orphaned > 0) {
655
- return `${base}\uFF08${chalk.yellow(`${orphaned} \u4E2A\u5B64\u7ACB`)}\uFF09`;
656
- }
657
- return base;
658
- };
659
652
  var PANEL_NO_WORKTREES = "(\u65E0\u6D3B\u8DC3 worktree)";
660
653
  var PANEL_PRESS_ENTER_TO_RETURN = chalk.gray("\n\u6309 Enter \u8FD4\u56DE\u9762\u677F...");
661
654
  var PANEL_NOT_TTY = "\u4EA4\u4E92\u5F0F\u9762\u677F\u9700\u8981 TTY \u7EC8\u7AEF\u73AF\u5883\uFF0C\u8BF7\u76F4\u63A5\u5728\u7EC8\u7AEF\u4E2D\u8FD0\u884C clawt status -i";
@@ -1006,9 +999,14 @@ import { execSync as execSync2, execFileSync, spawn, spawnSync } from "child_pro
1006
999
  import { join as join2, isAbsolute } from "path";
1007
1000
  import { execSync } from "child_process";
1008
1001
  var INDEX_LOCK_ERROR_PATTERNS = [
1002
+ // 英文错误消息
1009
1003
  /Unable to write.*index/i,
1010
1004
  /index\.lock/i,
1011
- /Unable to create.*index/i
1005
+ /Unable to create.*index/i,
1006
+ // 中文错误消息(Git 本地化)
1007
+ /不能写入索引/,
1008
+ /无法写入.*索引/,
1009
+ /无法创建.*index/i
1012
1010
  ];
1013
1011
  var INDEX_LOCK_PATH_EXTRACT_PATTERN = /'([^']*index\.lock)'/;
1014
1012
  function isGitIndexLockError(errorMessage) {
@@ -3761,7 +3759,7 @@ function buildPanelFrame(statusResult, selectedIndex, scrollOffset, rows, cols,
3761
3759
  const lines = [];
3762
3760
  lines.push(PANEL_TITLE(statusResult.main.projectName));
3763
3761
  lines.push(renderConfiguredBranchLine(statusResult.main));
3764
- lines.push(renderSnapshotSummary(statusResult.snapshots.total, statusResult.snapshots.orphaned));
3762
+ lines.push(renderMainBranchDiff(statusResult.main));
3765
3763
  const visibleRows = calculateVisibleRows(rows);
3766
3764
  if (statusResult.worktrees.length === 0) {
3767
3765
  lines.push(buildSeparatorWithHint(cols, ""));
@@ -3896,8 +3894,13 @@ function renderConfiguredBranchLine(main2) {
3896
3894
  }
3897
3895
  return PANEL_CONFIGURED_BRANCH(main2.configuredMainBranch);
3898
3896
  }
3899
- function renderSnapshotSummary(total, orphaned) {
3900
- return PANEL_SNAPSHOT_SUMMARY(total, orphaned);
3897
+ function renderMainBranchDiff(main2) {
3898
+ if (main2.insertions === 0 && main2.deletions === 0) {
3899
+ return `\u5DE5\u4F5C\u533A: ${chalk9.green(MESSAGES.STATUS_CHANGE_CLEAN)}`;
3900
+ }
3901
+ const insertText = chalk9.green(`+${main2.insertions}`);
3902
+ const deleteText = chalk9.red(`-${main2.deletions}`);
3903
+ return `\u5DE5\u4F5C\u533A: ${insertText} ${deleteText}`;
3901
3904
  }
3902
3905
  function renderFooter(countdown) {
3903
3906
  return `${PANEL_FOOTER_SHORTCUTS} ${PANEL_FOOTER_COUNTDOWN(countdown)}`;
@@ -5479,12 +5482,15 @@ function collectStatus() {
5479
5482
  const projectConfig = loadProjectConfig();
5480
5483
  const configuredMainBranch = projectConfig?.clawtMainWorkBranch || null;
5481
5484
  const configuredBranchExists = configuredMainBranch ? checkBranchExists(configuredMainBranch) : null;
5485
+ const { insertions, deletions } = countDiffStat(process.cwd());
5482
5486
  const main2 = {
5483
5487
  branch: currentBranch,
5484
5488
  isClean,
5485
5489
  projectName,
5486
5490
  configuredMainBranch,
5487
- configuredBranchExists
5491
+ configuredBranchExists,
5492
+ insertions,
5493
+ deletions
5488
5494
  };
5489
5495
  const worktrees = getProjectWorktrees();
5490
5496
  const worktreeStatuses = worktrees.map((wt) => collectWorktreeDetailedStatus(wt, projectName));
package/docs/status.md CHANGED
@@ -131,7 +131,9 @@ clawt status [--json] [-i | --interactive]
131
131
  "isClean": true,
132
132
  "projectName": "main-project",
133
133
  "configuredMainBranch": "main",
134
- "configuredBranchExists": true
134
+ "configuredBranchExists": true,
135
+ "insertions": 185,
136
+ "deletions": 42
135
137
  },
136
138
  "worktrees": [
137
139
  {
@@ -163,10 +165,12 @@ clawt status [--json] [-i | --interactive]
163
165
  | `projectName` | `string` | 项目名 |
164
166
  | `configuredMainBranch` | `string \| null` | 配置的主工作分支名(项目未初始化时为 null) |
165
167
  | `configuredBranchExists`| `boolean \| null` | 配置的主工作分支是否存在(项目未初始化时为 null)|
168
+ | `insertions` | `number` | 工作区和暂存区的新增行数 |
169
+ | `deletions` | `number` | 工作区和暂存区的删除行数 |
166
170
 
167
171
  **实现要点:**
168
172
 
169
- - 类型定义在 `src/types/status.ts`:`WorktreeDetailedStatus`(`snapshotTime: string | null`、`createdAt: string | null`)、`MainWorktreeStatus`(包含 `configuredMainBranch` 和 `configuredBranchExists`)、`SnapshotInfo`、`SnapshotSummary`(包含 `total` 和 `orphaned`)、`StatusResult`(`snapshots` 为 `SnapshotSummary` 类型)
173
+ - 类型定义在 `src/types/status.ts`:`WorktreeDetailedStatus`(`snapshotTime: string | null`、`createdAt: string | null`)、`MainWorktreeStatus`(包含 `configuredMainBranch`、`configuredBranchExists`、`insertions`、`deletions`)、`SnapshotInfo`、`SnapshotSummary`(包含 `total` 和 `orphaned`)、`StatusResult`(`snapshots` 为 `SnapshotSummary` 类型)
170
174
  - 消息常量在 `MESSAGES.STATUS_*` 系列:
171
175
  - `STATUS_TITLE(projectName)`:标题文本
172
176
  - `STATUS_MAIN_SECTION`:主 worktree 区块标题
@@ -202,7 +206,7 @@ clawt status [--json] [-i | --interactive]
202
206
  ```
203
207
  项目状态总览: my-project
204
208
  主工作分支: main
205
- 快照: 3 个(1 个孤立)
209
+ 工作区: +185 -42
206
210
  ──────── ↑ 更多 worktree... ────────
207
211
  ════ 2026-03-01(2 天前) ════
208
212
 
@@ -239,7 +243,7 @@ clawt status [--json] [-i | --interactive]
239
243
  - 分支已删除(红色):`✗ 主工作分支: <branchName>(已不存在)`
240
244
  - 分支不一致(黄色):`⚠ 主工作分支: <branchName>(不一致)`
241
245
  - 未初始化(灰色):`未初始化(执行 clawt init 设置主工作分支)`
242
- 3. **快照摘要行**:显示快照总数和孤立快照数
246
+ 3. **工作区 diff 信息行**:显示主工作分支的工作区 diff 统计,有变更时格式为 `工作区: +N -M`(新增行数绿色,删除行数红色),无变更时显示 `工作区: 无变更`(绿色)
243
247
  4. **顶部分隔线**:当存在向上溢出时,分隔线中间嵌入 `↑ 更多 worktree...` 提示
244
248
  5. **Worktree 滚动区域**:按日期分组显示 worktree 列表,支持上下滚动
245
249
  6. **底部分隔线**:当存在向下溢出时,分隔线中间嵌入 `↓ 更多 worktree...` 提示
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "3.9.1",
3
+ "version": "3.9.3",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -81,6 +81,9 @@ export function collectStatus(): StatusResult {
81
81
  const configuredMainBranch = projectConfig?.clawtMainWorkBranch || null;
82
82
  const configuredBranchExists = configuredMainBranch ? checkBranchExists(configuredMainBranch) : null;
83
83
 
84
+ // 主 worktree 的 diff 统计
85
+ const { insertions, deletions } = countDiffStat(process.cwd());
86
+
84
87
  // 主 worktree 状态
85
88
  const main: MainWorktreeStatus = {
86
89
  branch: currentBranch,
@@ -88,6 +91,8 @@ export function collectStatus(): StatusResult {
88
91
  projectName,
89
92
  configuredMainBranch,
90
93
  configuredBranchExists,
94
+ insertions,
95
+ deletions,
91
96
  };
92
97
 
93
98
  // 各 worktree 详细状态
@@ -32,6 +32,10 @@ export interface MainWorktreeStatus {
32
32
  configuredMainBranch: string | null;
33
33
  /** 配置的主工作分支是否存在(项目未初始化时为 null) */
34
34
  configuredBranchExists: boolean | null;
35
+ /** 工作区和暂存区的新增行数 */
36
+ insertions: number;
37
+ /** 工作区和暂存区的删除行数 */
38
+ deletions: number;
35
39
  }
36
40
 
37
41
  /** validate 快照信息 */
@@ -7,11 +7,17 @@ import { MESSAGES } from '../constants/index.js';
7
7
  /**
8
8
  * index.lock 错误的关键词匹配模式
9
9
  * 每个模式同时要求包含 index 关键词,避免 "Unable to write" 单独匹配导致误报
10
+ * 同时支持英文和中文(Git 本地化)错误消息
10
11
  */
11
12
  const INDEX_LOCK_ERROR_PATTERNS = [
13
+ // 英文错误消息
12
14
  /Unable to write.*index/i,
13
15
  /index\.lock/i,
14
16
  /Unable to create.*index/i,
17
+ // 中文错误消息(Git 本地化)
18
+ /不能写入索引/,
19
+ /无法写入.*索引/,
20
+ /无法创建.*index/i,
15
21
  ];
16
22
 
17
23
  /** 从 Git 错误消息中提取 index.lock 文件路径的正则(路径被 ASCII 单引号包裹) */
@@ -87,8 +87,8 @@ export function buildPanelFrame(
87
87
  // 配置分支信息行
88
88
  lines.push(renderConfiguredBranchLine(statusResult.main));
89
89
 
90
- // 快照摘要行
91
- lines.push(renderSnapshotSummary(statusResult.snapshots.total, statusResult.snapshots.orphaned));
90
+ // 主工作分支 diff 信息行
91
+ lines.push(renderMainBranchDiff(statusResult.main));
92
92
 
93
93
  // 计算可用的 worktree 显示区域行数
94
94
  const visibleRows = calculateVisibleRows(rows);
@@ -315,6 +315,21 @@ function renderConfiguredBranchLine(main: MainWorktreeStatus): string {
315
315
  return PANEL_CONFIGURED_BRANCH(main.configuredMainBranch);
316
316
  }
317
317
 
318
+ /**
319
+ * 渲染主工作分支的 diff 信息行
320
+ * 格式:工作区: +N -M(绿色新增,红色删除)或 工作区: 无变更(绿色)
321
+ * @param {MainWorktreeStatus} main - 主 worktree 状态
322
+ * @returns {string} 格式化的 diff 信息
323
+ */
324
+ function renderMainBranchDiff(main: MainWorktreeStatus): string {
325
+ if (main.insertions === 0 && main.deletions === 0) {
326
+ return `工作区: ${chalk.green(MESSAGES.STATUS_CHANGE_CLEAN)}`;
327
+ }
328
+ const insertText = chalk.green(`+${main.insertions}`);
329
+ const deleteText = chalk.red(`-${main.deletions}`);
330
+ return `工作区: ${insertText} ${deleteText}`;
331
+ }
332
+
318
333
  /**
319
334
  * 渲染快照摘要行
320
335
  * @param {number} total - 快照总数
@@ -340,4 +340,41 @@ describe('handleStatus', () => {
340
340
  const unverifiedLine = printedLines.find((line) => line.includes('未验证'));
341
341
  expect(unverifiedLine).toBeUndefined();
342
342
  });
343
+
344
+ it('主 worktree diff 统计包含在 JSON 输出中', async () => {
345
+ // 模拟主 worktree 有变更
346
+ mockedGetDiffStat.mockReturnValue({ insertions: 185, deletions: 42 });
347
+
348
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
349
+
350
+ const program = new Command();
351
+ program.exitOverride();
352
+ registerStatusCommand(program);
353
+ await program.parseAsync(['status', '--json'], { from: 'user' });
354
+
355
+ const jsonCall = consoleSpy.mock.calls.find((call) => {
356
+ try { JSON.parse(call[0]); return true; } catch { return false; }
357
+ });
358
+ const parsed = JSON.parse(jsonCall![0]);
359
+ expect(parsed.main.insertions).toBe(185);
360
+ expect(parsed.main.deletions).toBe(42);
361
+ });
362
+
363
+ it('主 worktree 无变更时 diff 统计为 0', async () => {
364
+ mockedGetDiffStat.mockReturnValue({ insertions: 0, deletions: 0 });
365
+
366
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
367
+
368
+ const program = new Command();
369
+ program.exitOverride();
370
+ registerStatusCommand(program);
371
+ await program.parseAsync(['status', '--json'], { from: 'user' });
372
+
373
+ const jsonCall = consoleSpy.mock.calls.find((call) => {
374
+ try { JSON.parse(call[0]); return true; } catch { return false; }
375
+ });
376
+ const parsed = JSON.parse(jsonCall![0]);
377
+ expect(parsed.main.insertions).toBe(0);
378
+ expect(parsed.main.deletions).toBe(0);
379
+ });
343
380
  });
@@ -76,6 +76,18 @@ describe('isGitIndexLockError', () => {
76
76
  it('大小写不敏感匹配 "unable to write"', () => {
77
77
  expect(isGitIndexLockError('FATAL: UNABLE TO WRITE INDEX.')).toBe(true);
78
78
  });
79
+
80
+ it('检测中文 "不能写入索引" 错误', () => {
81
+ expect(isGitIndexLockError('致命错误:不能写入索引。')).toBe(true);
82
+ });
83
+
84
+ it('检测中文 "无法写入索引" 错误', () => {
85
+ expect(isGitIndexLockError('致命错误:无法写入新索引文件')).toBe(true);
86
+ });
87
+
88
+ it('检测中文 "无法创建 index.lock" 错误', () => {
89
+ expect(isGitIndexLockError("致命错误:无法创建 '/repo/.git/index.lock':文件已存在")).toBe(true);
90
+ });
79
91
  });
80
92
 
81
93
  describe('findGitIndexLockPath', () => {