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 +18 -12
- package/docs/status.md +8 -4
- package/package.json +1 -1
- package/src/commands/status.ts +5 -0
- package/src/types/status.ts +4 -0
- package/src/utils/git-lock.ts +6 -0
- package/src/utils/interactive-panel-render.ts +17 -2
- package/tests/unit/commands/status.test.ts +37 -0
- package/tests/unit/utils/git-lock.test.ts +12 -0
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(
|
|
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
|
|
3900
|
-
|
|
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
|
|
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
|
-
|
|
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
package/src/commands/status.ts
CHANGED
|
@@ -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 详细状态
|
package/src/types/status.ts
CHANGED
|
@@ -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 快照信息 */
|
package/src/utils/git-lock.ts
CHANGED
|
@@ -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(
|
|
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', () => {
|