xtrm-cli 0.5.0 → 0.5.27
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/.pi/structured-returns/0e4a7405-1ac3-4ae1-8dbc-d31507b2e2e4.combined.log +17 -0
- package/.pi/structured-returns/0e4a7405-1ac3-4ae1-8dbc-d31507b2e2e4.stderr.log +0 -0
- package/.pi/structured-returns/0e4a7405-1ac3-4ae1-8dbc-d31507b2e2e4.stdout.log +17 -0
- package/dist/index.cjs +969 -1059
- package/dist/index.cjs.map +1 -1
- package/package.json +1 -1
- package/src/commands/clean.ts +7 -6
- package/src/commands/debug.ts +255 -0
- package/src/commands/docs.ts +180 -0
- package/src/commands/help.ts +92 -171
- package/src/commands/init.ts +9 -32
- package/src/commands/install-pi.ts +9 -16
- package/src/commands/install.ts +150 -2
- package/src/commands/pi-install.ts +10 -44
- package/src/core/context.ts +4 -52
- package/src/core/diff.ts +3 -16
- package/src/core/preflight.ts +0 -1
- package/src/index.ts +7 -4
- package/src/types/config.ts +0 -2
- package/src/utils/config-injector.ts +3 -3
- package/src/utils/pi-extensions.ts +41 -0
- package/src/utils/worktree-session.ts +86 -50
- package/test/extensions/beads-claim-lifecycle.test.ts +93 -0
- package/test/extensions/beads-parity.test.ts +94 -0
- package/test/extensions/extension-harness.ts +5 -5
- package/test/extensions/quality-gates-parity.test.ts +89 -0
- package/test/extensions/session-flow.test.ts +91 -0
- package/test/extensions/xtrm-loader.test.ts +38 -20
- package/test/install-pi.test.ts +22 -11
- package/test/pi-extensions.test.ts +50 -0
- package/test/session-launcher.test.ts +28 -38
- package/extensions/beads.ts +0 -109
- package/extensions/core/adapter.ts +0 -45
- package/extensions/core/lib.ts +0 -3
- package/extensions/core/logger.ts +0 -45
- package/extensions/core/runner.ts +0 -71
- package/extensions/custom-footer.ts +0 -160
- package/extensions/main-guard-post-push.ts +0 -44
- package/extensions/main-guard.ts +0 -126
- package/extensions/minimal-mode.ts +0 -201
- package/extensions/quality-gates.ts +0 -67
- package/extensions/service-skills.ts +0 -150
- package/extensions/xtrm-loader.ts +0 -89
- package/hooks/gitnexus-impact-reminder.py +0 -13
- package/src/commands/finish.ts +0 -25
- package/src/core/session-state.ts +0 -139
- package/src/core/xtrm-finish.ts +0 -267
- package/src/tests/session-flow-parity.test.ts +0 -118
- package/src/tests/session-state.test.ts +0 -124
- package/src/tests/xtrm-finish.test.ts +0 -148
package/package.json
CHANGED
package/src/commands/clean.ts
CHANGED
|
@@ -8,10 +8,7 @@ import { findRepoRoot } from '../utils/repo-root.js';
|
|
|
8
8
|
|
|
9
9
|
// Canonical hooks (files in ~/.claude/hooks/)
|
|
10
10
|
const CANONICAL_HOOKS = new Set([
|
|
11
|
-
'
|
|
12
|
-
'serena-workflow-reminder.py',
|
|
13
|
-
'main-guard.mjs',
|
|
14
|
-
'main-guard-post-push.mjs',
|
|
11
|
+
'using-xtrm-reminder.mjs',
|
|
15
12
|
'beads-gate-core.mjs',
|
|
16
13
|
'beads-gate-utils.mjs',
|
|
17
14
|
'beads-gate-messages.mjs',
|
|
@@ -22,11 +19,15 @@ const CANONICAL_HOOKS = new Set([
|
|
|
22
19
|
'beads-claim-sync.mjs',
|
|
23
20
|
'beads-compact-save.mjs',
|
|
24
21
|
'beads-compact-restore.mjs',
|
|
25
|
-
'
|
|
22
|
+
'worktree-boundary.mjs',
|
|
23
|
+
'statusline.mjs',
|
|
26
24
|
'quality-check.cjs',
|
|
25
|
+
'quality-check-env.mjs',
|
|
27
26
|
'quality-check.py',
|
|
27
|
+
'xtrm-logger.mjs',
|
|
28
|
+
'xtrm-tool-logger.mjs',
|
|
29
|
+
'xtrm-session-logger.mjs',
|
|
28
30
|
'gitnexus', // directory
|
|
29
|
-
'statusline-starship.sh',
|
|
30
31
|
'README.md',
|
|
31
32
|
]);
|
|
32
33
|
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import kleur from 'kleur';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { join, basename } from 'node:path';
|
|
6
|
+
|
|
7
|
+
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
interface XtrmEvent {
|
|
10
|
+
id: number;
|
|
11
|
+
ts: number;
|
|
12
|
+
session_id: string;
|
|
13
|
+
runtime: string;
|
|
14
|
+
worktree: string | null;
|
|
15
|
+
kind: string;
|
|
16
|
+
tool_name: string | null;
|
|
17
|
+
outcome: string | null;
|
|
18
|
+
issue_id: string | null;
|
|
19
|
+
duration_ms: number | null;
|
|
20
|
+
data: string | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface DebugOptions {
|
|
24
|
+
all: boolean;
|
|
25
|
+
follow: boolean;
|
|
26
|
+
session: string | undefined;
|
|
27
|
+
type: string | undefined;
|
|
28
|
+
json: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Kind labels ───────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
type ColorFn = (s: string) => string;
|
|
34
|
+
|
|
35
|
+
// Gate and lifecycle events: fixed 5-char label + fixed color
|
|
36
|
+
const KIND_LABELS: Record<string, { label: string; color: ColorFn }> = {
|
|
37
|
+
'session.start': { label: 'SESS+', color: kleur.green },
|
|
38
|
+
'session.end': { label: 'SESS-', color: kleur.white },
|
|
39
|
+
'gate.edit.allow': { label: 'EDIT+', color: kleur.green },
|
|
40
|
+
'gate.edit.block': { label: 'EDIT-', color: kleur.red },
|
|
41
|
+
'gate.commit.allow': { label: 'CMIT+', color: kleur.green },
|
|
42
|
+
'gate.commit.block': { label: 'CMIT-', color: kleur.red },
|
|
43
|
+
'gate.stop.block': { label: 'STOP-', color: kleur.red },
|
|
44
|
+
'gate.memory.triggered': { label: 'MEMO-', color: kleur.yellow },
|
|
45
|
+
'gate.memory.acked': { label: 'MEMO+', color: kleur.green },
|
|
46
|
+
'gate.worktree.block': { label: 'WTRE-', color: kleur.red },
|
|
47
|
+
'bd.claimed': { label: 'CLMD ', color: kleur.cyan },
|
|
48
|
+
'bd.closed': { label: 'CLSD ', color: kleur.green },
|
|
49
|
+
'bd.committed': { label: outcome => outcome === 'error' ? 'ACMT-' : 'ACMT+',
|
|
50
|
+
color: outcome => outcome === 'error' ? kleur.red : kleur.cyan } as any,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Tool call events: derive 5-char abbrev from tool_name
|
|
54
|
+
const TOOL_ABBREVS: Record<string, string> = {
|
|
55
|
+
Bash: 'BASH', bash: 'BASH', execute_shell_command: 'BASH',
|
|
56
|
+
Read: 'READ', Write: 'WRIT', Edit: 'EDIT', MultiEdit: 'EDIT', NotebookEdit: 'NTED',
|
|
57
|
+
Glob: 'GLOB', Grep: 'GREP',
|
|
58
|
+
WebFetch: 'WBFT', WebSearch: 'WSRC',
|
|
59
|
+
Agent: 'AGNT', Task: 'TASK',
|
|
60
|
+
LSP: 'LSP ',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function toolAbbrev(toolName: string): string {
|
|
64
|
+
if (TOOL_ABBREVS[toolName]) return TOOL_ABBREVS[toolName];
|
|
65
|
+
if (toolName.startsWith('mcp__serena__')) return 'SRNA';
|
|
66
|
+
if (toolName.startsWith('mcp__gitnexus__')) return 'GTNX';
|
|
67
|
+
if (toolName.startsWith('mcp__deepwiki__')) return 'WIKI';
|
|
68
|
+
if (toolName.startsWith('mcp__')) return 'MCP ';
|
|
69
|
+
return toolName.slice(0, 4).toUpperCase();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getLabel(event: XtrmEvent): string {
|
|
73
|
+
if (event.kind === 'tool.call') {
|
|
74
|
+
const abbrev = toolAbbrev(event.tool_name ?? '').padEnd(5);
|
|
75
|
+
return event.outcome === 'error' ? kleur.red(abbrev) : kleur.dim(abbrev);
|
|
76
|
+
}
|
|
77
|
+
const def = KIND_LABELS[event.kind];
|
|
78
|
+
if (!def) {
|
|
79
|
+
// Unknown kind: derive from last segment + outcome marker
|
|
80
|
+
const seg = (event.kind.split('.').pop() ?? 'UNKN').slice(0, 4).toUpperCase();
|
|
81
|
+
const label = `${seg}${event.outcome === 'block' ? '-' : '+'}`.padEnd(5);
|
|
82
|
+
return event.outcome === 'block' ? kleur.red(label) : kleur.dim(label);
|
|
83
|
+
}
|
|
84
|
+
// bd.committed has dynamic label/color
|
|
85
|
+
if (event.kind === 'bd.committed') {
|
|
86
|
+
const label = event.outcome === 'error' ? 'ACMT-' : 'ACMT+';
|
|
87
|
+
return event.outcome === 'error' ? kleur.red(label) : kleur.cyan(label);
|
|
88
|
+
}
|
|
89
|
+
return (def as { label: string; color: ColorFn }).color(
|
|
90
|
+
(def as { label: string; color: ColorFn }).label
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Session color map ─────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
const SESSION_COLORS: ColorFn[] = [
|
|
97
|
+
kleur.blue, kleur.green, kleur.yellow, kleur.cyan, kleur.magenta,
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
function buildColorMap(events: XtrmEvent[]): Map<string, ColorFn> {
|
|
101
|
+
const map = new Map<string, ColorFn>();
|
|
102
|
+
for (const ev of events) {
|
|
103
|
+
if (!map.has(ev.session_id)) {
|
|
104
|
+
map.set(ev.session_id, SESSION_COLORS[map.size % SESSION_COLORS.length]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return map;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function extendColorMap(map: Map<string, ColorFn>, events: XtrmEvent[]): void {
|
|
111
|
+
for (const ev of events) {
|
|
112
|
+
if (!map.has(ev.session_id)) {
|
|
113
|
+
map.set(ev.session_id, SESSION_COLORS[map.size % SESSION_COLORS.length]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Formatting ────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
function fmtTime(ts: number): string {
|
|
121
|
+
return new Date(ts).toLocaleTimeString('en-GB', { hour12: false });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildDetail(event: XtrmEvent): string {
|
|
125
|
+
const parts: string[] = [];
|
|
126
|
+
|
|
127
|
+
// Parse data JSON if present
|
|
128
|
+
let d: Record<string, string> | null = null;
|
|
129
|
+
if (event.data) {
|
|
130
|
+
try { d = JSON.parse(event.data); } catch { /* ignore */ }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (event.kind === 'tool.call') {
|
|
134
|
+
if (d?.cmd) parts.push(kleur.dim(d.cmd.slice(0, 72)));
|
|
135
|
+
if (d?.file) parts.push(kleur.dim(basename(d.file)));
|
|
136
|
+
if (d?.pattern) parts.push(kleur.dim(`/${d.pattern}/`));
|
|
137
|
+
if (d?.url) parts.push(kleur.dim(d.url.slice(0, 72)));
|
|
138
|
+
if (d?.query) parts.push(kleur.dim(d.query.slice(0, 72)));
|
|
139
|
+
if (d?.prompt) parts.push(kleur.dim(d.prompt.slice(0, 72)));
|
|
140
|
+
} else {
|
|
141
|
+
if (event.issue_id) parts.push(kleur.yellow(event.issue_id));
|
|
142
|
+
if (d?.file) parts.push(kleur.dim(basename(d.file)));
|
|
143
|
+
if (d?.reason_code) parts.push(kleur.dim(`[${d.reason_code}]`));
|
|
144
|
+
if (event.worktree) parts.push(kleur.dim(`wt:${event.worktree}`));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return parts.join(' ') || kleur.dim('—');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function formatLine(event: XtrmEvent, colorMap: Map<string, ColorFn>): string {
|
|
151
|
+
const time = kleur.dim(fmtTime(event.ts));
|
|
152
|
+
const colorFn = colorMap.get(event.session_id) ?? kleur.white;
|
|
153
|
+
const session = colorFn(event.session_id.slice(0, 8));
|
|
154
|
+
const label = getLabel(event);
|
|
155
|
+
const detail = buildDetail(event);
|
|
156
|
+
return `${time} ${label} ${session} ${detail}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── SQLite queries ────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
function findDbPath(cwd: string): string | null {
|
|
162
|
+
let dir = cwd;
|
|
163
|
+
for (let i = 0; i < 10; i++) {
|
|
164
|
+
if (existsSync(join(dir, '.beads'))) return join(dir, '.xtrm', 'debug.db');
|
|
165
|
+
const parent = join(dir, '..');
|
|
166
|
+
if (parent === dir) break;
|
|
167
|
+
dir = parent;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function buildWhere(opts: DebugOptions, base: string): string {
|
|
173
|
+
const clauses: string[] = [];
|
|
174
|
+
if (base) clauses.push(base);
|
|
175
|
+
if (opts.session) {
|
|
176
|
+
const s = opts.session.replace(/'/g, "''");
|
|
177
|
+
clauses.push(`session_id LIKE '${s}%'`);
|
|
178
|
+
}
|
|
179
|
+
if (opts.type) {
|
|
180
|
+
const t = opts.type.replace(/'/g, "''");
|
|
181
|
+
clauses.push(`kind LIKE '${t}.%' OR kind = '${t}'`);
|
|
182
|
+
}
|
|
183
|
+
return clauses.length ? clauses.join(' AND ') : '';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function queryEvents(dbPath: string, where: string, limit: number): XtrmEvent[] {
|
|
187
|
+
const sql = `SELECT id,ts,session_id,runtime,worktree,kind,tool_name,outcome,issue_id,duration_ms,data FROM events${where ? ` WHERE ${where}` : ''} ORDER BY id ASC LIMIT ${limit}`;
|
|
188
|
+
|
|
189
|
+
const result = spawnSync('sqlite3', [dbPath, '-json', sql], {
|
|
190
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
191
|
+
encoding: 'utf8',
|
|
192
|
+
timeout: 5000,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (result.status !== 0 || !result.stdout.trim()) return [];
|
|
196
|
+
try { return JSON.parse(result.stdout); } catch { return []; }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── Follow mode ───────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
function follow(dbPath: string, opts: DebugOptions): void {
|
|
202
|
+
const sinceTs = Date.now() - 5 * 60 * 1000;
|
|
203
|
+
const initial = queryEvents(dbPath, buildWhere(opts, `ts >= ${sinceTs}`), 200);
|
|
204
|
+
|
|
205
|
+
const colorMap = buildColorMap(initial);
|
|
206
|
+
let lastId = 0;
|
|
207
|
+
|
|
208
|
+
for (const ev of initial) {
|
|
209
|
+
if (ev.id > lastId) lastId = ev.id;
|
|
210
|
+
opts.json ? console.log(JSON.stringify(ev)) : console.log(formatLine(ev, colorMap));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Poll every 2s — clean integer comparison, no datetime overlap needed
|
|
214
|
+
const interval = setInterval(() => {
|
|
215
|
+
const events = queryEvents(dbPath, buildWhere(opts, `id > ${lastId}`), 50);
|
|
216
|
+
if (events.length > 0) {
|
|
217
|
+
extendColorMap(colorMap, events);
|
|
218
|
+
for (const ev of events) {
|
|
219
|
+
if (ev.id > lastId) lastId = ev.id;
|
|
220
|
+
opts.json ? console.log(JSON.stringify(ev)) : console.log(formatLine(ev, colorMap));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}, 2000);
|
|
224
|
+
|
|
225
|
+
process.on('SIGINT', () => { clearInterval(interval); process.exit(0); });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── Command ───────────────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
export function createDebugCommand(): Command {
|
|
231
|
+
return new Command('debug')
|
|
232
|
+
.description('Watch xtrm events: tool calls, gate decisions, bd lifecycle')
|
|
233
|
+
.option('-f, --follow', 'Follow new events (default)', false)
|
|
234
|
+
.option('--all', 'Show full history and exit', false)
|
|
235
|
+
.option('--session <id>', 'Filter by session ID (prefix match)')
|
|
236
|
+
.option('--type <domain>', 'Filter by domain: tool | gate | bd | session')
|
|
237
|
+
.option('--json', 'Output raw JSON lines', false)
|
|
238
|
+
.action((opts: DebugOptions) => {
|
|
239
|
+
const cwd = process.cwd();
|
|
240
|
+
const dbPath = findDbPath(cwd);
|
|
241
|
+
|
|
242
|
+
if (!dbPath || !existsSync(dbPath)) return;
|
|
243
|
+
|
|
244
|
+
if (opts.all) {
|
|
245
|
+
const events = queryEvents(dbPath, buildWhere(opts, ''), 1000);
|
|
246
|
+
const colorMap = buildColorMap(events);
|
|
247
|
+
for (const ev of events) {
|
|
248
|
+
opts.json ? console.log(JSON.stringify(ev)) : console.log(formatLine(ev, colorMap));
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
follow(dbPath, opts);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import kleur from 'kleur';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { findRepoRoot } from '../utils/repo-root.js';
|
|
6
|
+
import { t, sym } from '../utils/theme.js';
|
|
7
|
+
|
|
8
|
+
interface Frontmatter {
|
|
9
|
+
[key: string]: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface DocEntry {
|
|
13
|
+
filePath: string;
|
|
14
|
+
relativePath: string;
|
|
15
|
+
frontmatter: Frontmatter | null;
|
|
16
|
+
sizeBytes: number;
|
|
17
|
+
lastModified: Date;
|
|
18
|
+
parseError?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const REQUIRED_FIELDS = new Set(['title', 'type', 'status', 'updated_at', 'version']);
|
|
22
|
+
|
|
23
|
+
/** Parse YAML frontmatter from a markdown file (--- delimited block). */
|
|
24
|
+
function parseFrontmatter(content: string): Frontmatter | null {
|
|
25
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
26
|
+
if (!match) return null;
|
|
27
|
+
|
|
28
|
+
const fm: Frontmatter = {};
|
|
29
|
+
for (const line of match[1].split('\n')) {
|
|
30
|
+
const colon = line.indexOf(':');
|
|
31
|
+
if (colon === -1) continue;
|
|
32
|
+
const key = line.slice(0, colon).trim();
|
|
33
|
+
const value = line.slice(colon + 1).trim().replace(/^["']|["']$/g, '');
|
|
34
|
+
if (key) fm[key] = value;
|
|
35
|
+
}
|
|
36
|
+
return fm;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Collect all target doc files in a repo. */
|
|
40
|
+
async function collectDocFiles(repoRoot: string, filterPattern?: string): Promise<DocEntry[]> {
|
|
41
|
+
const candidates: string[] = [];
|
|
42
|
+
|
|
43
|
+
// Fixed candidates
|
|
44
|
+
for (const name of ['README.md', 'CHANGELOG.md']) {
|
|
45
|
+
const p = path.join(repoRoot, name);
|
|
46
|
+
if (await fs.pathExists(p)) candidates.push(p);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// docs/ directory
|
|
50
|
+
const docsDir = path.join(repoRoot, 'docs');
|
|
51
|
+
if (await fs.pathExists(docsDir)) {
|
|
52
|
+
const entries = await fs.readdir(docsDir);
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (entry.endsWith('.md')) candidates.push(path.join(docsDir, entry));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const results: DocEntry[] = [];
|
|
59
|
+
for (const filePath of candidates) {
|
|
60
|
+
const rel = path.relative(repoRoot, filePath);
|
|
61
|
+
|
|
62
|
+
// Apply filter if provided
|
|
63
|
+
if (filterPattern && !rel.includes(filterPattern) && !path.basename(filePath).includes(filterPattern)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let entry: DocEntry;
|
|
68
|
+
try {
|
|
69
|
+
const stat = await fs.stat(filePath);
|
|
70
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
71
|
+
const frontmatter = parseFrontmatter(content);
|
|
72
|
+
entry = {
|
|
73
|
+
filePath,
|
|
74
|
+
relativePath: rel,
|
|
75
|
+
frontmatter,
|
|
76
|
+
sizeBytes: stat.size,
|
|
77
|
+
lastModified: stat.mtime,
|
|
78
|
+
};
|
|
79
|
+
} catch (err: any) {
|
|
80
|
+
entry = {
|
|
81
|
+
filePath,
|
|
82
|
+
relativePath: rel,
|
|
83
|
+
frontmatter: null,
|
|
84
|
+
sizeBytes: 0,
|
|
85
|
+
lastModified: new Date(0),
|
|
86
|
+
parseError: err.message,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
results.push(entry);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return results;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatSize(bytes: number): string {
|
|
96
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
97
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatDate(d: Date): string {
|
|
101
|
+
return d.toISOString().slice(0, 10);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function printEntry(entry: DocEntry, raw: boolean): void {
|
|
105
|
+
const header = kleur.bold().white(entry.relativePath);
|
|
106
|
+
const meta = kleur.gray(` ${formatSize(entry.sizeBytes)} modified ${formatDate(entry.lastModified)}`);
|
|
107
|
+
console.log(`\n${header}${meta}`);
|
|
108
|
+
|
|
109
|
+
if (entry.parseError) {
|
|
110
|
+
console.log(kleur.red(` ✗ Error reading file: ${entry.parseError}`));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!entry.frontmatter || Object.keys(entry.frontmatter).length === 0) {
|
|
115
|
+
console.log(kleur.gray(' (no frontmatter)'));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (raw) {
|
|
120
|
+
console.log(kleur.gray(' ---'));
|
|
121
|
+
for (const [k, v] of Object.entries(entry.frontmatter)) {
|
|
122
|
+
console.log(` ${k}: ${v}`);
|
|
123
|
+
}
|
|
124
|
+
console.log(kleur.gray(' ---'));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const [k, v] of Object.entries(entry.frontmatter)) {
|
|
129
|
+
const keyStr = REQUIRED_FIELDS.has(k)
|
|
130
|
+
? kleur.cyan(k.padEnd(14))
|
|
131
|
+
: kleur.gray(k.padEnd(14));
|
|
132
|
+
const valStr = v ?? '';
|
|
133
|
+
console.log(` ${keyStr} ${valStr}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function createDocsCommand(): Command {
|
|
138
|
+
const docs = new Command('docs')
|
|
139
|
+
.description('Documentation management commands');
|
|
140
|
+
|
|
141
|
+
docs
|
|
142
|
+
.command('show [filter]')
|
|
143
|
+
.description('Display frontmatters for README, CHANGELOG, and docs/ files')
|
|
144
|
+
.option('--raw', 'Output raw YAML frontmatter', false)
|
|
145
|
+
.option('--json', 'Output JSON', false)
|
|
146
|
+
.action(async (filter: string | undefined, opts: { raw: boolean; json: boolean }) => {
|
|
147
|
+
const repoRoot = await findRepoRoot();
|
|
148
|
+
const entries = await collectDocFiles(repoRoot, filter);
|
|
149
|
+
|
|
150
|
+
if (entries.length === 0) {
|
|
151
|
+
console.log(kleur.yellow('\n No documentation files found.\n'));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (opts.json) {
|
|
156
|
+
const output = entries.map(e => ({
|
|
157
|
+
path: e.relativePath,
|
|
158
|
+
sizeBytes: e.sizeBytes,
|
|
159
|
+
lastModified: e.lastModified.toISOString(),
|
|
160
|
+
frontmatter: e.frontmatter,
|
|
161
|
+
parseError: e.parseError ?? null,
|
|
162
|
+
}));
|
|
163
|
+
console.log(JSON.stringify(output, null, 2));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
printEntry(entry, opts.raw);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const without = entries.filter(e => !e.frontmatter || Object.keys(e.frontmatter).length === 0).length;
|
|
172
|
+
console.log(
|
|
173
|
+
`\n ${sym.ok} ${entries.length} file${entries.length !== 1 ? 's' : ''}` +
|
|
174
|
+
(without > 0 ? kleur.gray(` (${without} without frontmatter)`) : '') +
|
|
175
|
+
'\n'
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return docs;
|
|
180
|
+
}
|