clawt 3.9.1 → 3.9.2
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 +12 -11
- 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/interactive-panel-render.ts +17 -2
- package/tests/unit/commands/status.test.ts +37 -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";
|
|
@@ -3761,7 +3754,7 @@ function buildPanelFrame(statusResult, selectedIndex, scrollOffset, rows, cols,
|
|
|
3761
3754
|
const lines = [];
|
|
3762
3755
|
lines.push(PANEL_TITLE(statusResult.main.projectName));
|
|
3763
3756
|
lines.push(renderConfiguredBranchLine(statusResult.main));
|
|
3764
|
-
lines.push(
|
|
3757
|
+
lines.push(renderMainBranchDiff(statusResult.main));
|
|
3765
3758
|
const visibleRows = calculateVisibleRows(rows);
|
|
3766
3759
|
if (statusResult.worktrees.length === 0) {
|
|
3767
3760
|
lines.push(buildSeparatorWithHint(cols, ""));
|
|
@@ -3896,8 +3889,13 @@ function renderConfiguredBranchLine(main2) {
|
|
|
3896
3889
|
}
|
|
3897
3890
|
return PANEL_CONFIGURED_BRANCH(main2.configuredMainBranch);
|
|
3898
3891
|
}
|
|
3899
|
-
function
|
|
3900
|
-
|
|
3892
|
+
function renderMainBranchDiff(main2) {
|
|
3893
|
+
if (main2.insertions === 0 && main2.deletions === 0) {
|
|
3894
|
+
return `\u5DE5\u4F5C\u533A: ${chalk9.green(MESSAGES.STATUS_CHANGE_CLEAN)}`;
|
|
3895
|
+
}
|
|
3896
|
+
const insertText = chalk9.green(`+${main2.insertions}`);
|
|
3897
|
+
const deleteText = chalk9.red(`-${main2.deletions}`);
|
|
3898
|
+
return `\u5DE5\u4F5C\u533A: ${insertText} ${deleteText}`;
|
|
3901
3899
|
}
|
|
3902
3900
|
function renderFooter(countdown) {
|
|
3903
3901
|
return `${PANEL_FOOTER_SHORTCUTS} ${PANEL_FOOTER_COUNTDOWN(countdown)}`;
|
|
@@ -5479,12 +5477,15 @@ function collectStatus() {
|
|
|
5479
5477
|
const projectConfig = loadProjectConfig();
|
|
5480
5478
|
const configuredMainBranch = projectConfig?.clawtMainWorkBranch || null;
|
|
5481
5479
|
const configuredBranchExists = configuredMainBranch ? checkBranchExists(configuredMainBranch) : null;
|
|
5480
|
+
const { insertions, deletions } = countDiffStat(process.cwd());
|
|
5482
5481
|
const main2 = {
|
|
5483
5482
|
branch: currentBranch,
|
|
5484
5483
|
isClean,
|
|
5485
5484
|
projectName,
|
|
5486
5485
|
configuredMainBranch,
|
|
5487
|
-
configuredBranchExists
|
|
5486
|
+
configuredBranchExists,
|
|
5487
|
+
insertions,
|
|
5488
|
+
deletions
|
|
5488
5489
|
};
|
|
5489
5490
|
const worktrees = getProjectWorktrees();
|
|
5490
5491
|
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 快照信息 */
|
|
@@ -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
|
});
|