copperhead 0.6.0 → 0.8.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.
- package/README.md +36 -4
- package/dist/agent/animate.js +76 -0
- package/dist/agent/animate.js.map +1 -0
- package/dist/agent/box.js +89 -0
- package/dist/agent/box.js.map +1 -0
- package/dist/agent/dock-renderer.js +173 -0
- package/dist/agent/dock-renderer.js.map +1 -0
- package/dist/agent/logo.js +21 -0
- package/dist/agent/logo.js.map +1 -0
- package/dist/agent/loop.js +130 -18
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/prompts.js +2 -1
- package/dist/agent/prompts.js.map +1 -1
- package/dist/agent/providers/claude-code.js +85 -116
- package/dist/agent/providers/claude-code.js.map +1 -1
- package/dist/agent/providers/cursor.js +317 -0
- package/dist/agent/providers/cursor.js.map +1 -0
- package/dist/agent/providers/tool-protocol.js +205 -0
- package/dist/agent/providers/tool-protocol.js.map +1 -0
- package/dist/agent/recovery.js +148 -0
- package/dist/agent/recovery.js.map +1 -0
- package/dist/agent/render.js +44 -12
- package/dist/agent/render.js.map +1 -1
- package/dist/agent/response-cache.js +81 -0
- package/dist/agent/response-cache.js.map +1 -0
- package/dist/agent/runmeta.js +4 -5
- package/dist/agent/runmeta.js.map +1 -1
- package/dist/agent/theme.js +84 -0
- package/dist/agent/theme.js.map +1 -0
- package/dist/agent/tools.js +61 -4
- package/dist/agent/tools.js.map +1 -1
- package/dist/agent/transcript.js.map +1 -1
- package/dist/cli.js +134 -13
- package/dist/cli.js.map +1 -1
- package/dist/commands/create.js +482 -38
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/demo.js +146 -0
- package/dist/commands/demo.js.map +1 -0
- package/dist/commands/doctor.js +240 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/repl-inspect.js +342 -0
- package/dist/commands/repl-inspect.js.map +1 -0
- package/dist/commands/repl.js +618 -0
- package/dist/commands/repl.js.map +1 -0
- package/dist/config.js +24 -2
- package/dist/config.js.map +1 -1
- package/dist/kicad/bootstrap.js +166 -0
- package/dist/kicad/bootstrap.js.map +1 -0
- package/dist/kicad/cli.js +126 -6
- package/dist/kicad/cli.js.map +1 -1
- package/dist/kicad/spice.js +306 -0
- package/dist/kicad/spice.js.map +1 -0
- package/dist/kicad/symlib.js +228 -0
- package/dist/kicad/symlib.js.map +1 -0
- package/dist/memory/bom-table.js +193 -22
- package/dist/memory/bom-table.js.map +1 -1
- package/dist/memory/drift.js +33 -11
- package/dist/memory/drift.js.map +1 -1
- package/dist/util/cli-args.js +35 -0
- package/dist/util/cli-args.js.map +1 -0
- package/dist/util/dock.js +155 -0
- package/dist/util/dock.js.map +1 -0
- package/dist/util/git.js +165 -4
- package/dist/util/git.js.map +1 -1
- package/dist/util/live-prompt.js +542 -0
- package/dist/util/live-prompt.js.map +1 -0
- package/dist/util/paths.js +9 -0
- package/dist/util/paths.js.map +1 -1
- package/dist/util/preflight.js +37 -0
- package/dist/util/preflight.js.map +1 -1
- package/dist/util/retry.js +23 -0
- package/dist/util/retry.js.map +1 -1
- package/dist/util/select.js +172 -0
- package/dist/util/select.js.map +1 -0
- package/dist/util/tmp.js +119 -0
- package/dist/util/tmp.js.map +1 -0
- package/package.json +3 -2
- package/src/agent/animate.ts +90 -0
- package/src/agent/box.ts +99 -0
- package/src/agent/dock-renderer.ts +181 -0
- package/src/agent/logo.ts +23 -0
- package/src/agent/loop.ts +148 -18
- package/src/agent/prompts.ts +2 -1
- package/src/agent/providers/claude-code.ts +91 -122
- package/src/agent/providers/cursor.ts +364 -0
- package/src/agent/providers/tool-protocol.ts +212 -0
- package/src/agent/recovery.ts +162 -0
- package/src/agent/render.ts +56 -12
- package/src/agent/response-cache.ts +80 -0
- package/src/agent/runmeta.ts +6 -7
- package/src/agent/theme.ts +91 -0
- package/src/agent/tools.ts +62 -4
- package/src/agent/transcript.ts +1 -0
- package/src/agent/types.ts +17 -0
- package/src/cli.ts +139 -15
- package/src/commands/create.ts +581 -40
- package/src/commands/demo.ts +184 -0
- package/src/commands/doctor.ts +289 -0
- package/src/commands/repl-inspect.ts +353 -0
- package/src/commands/repl.ts +685 -0
- package/src/config.ts +40 -3
- package/src/kicad/bootstrap.ts +181 -0
- package/src/kicad/cli.ts +132 -7
- package/src/kicad/spice.ts +399 -0
- package/src/kicad/symlib.ts +248 -0
- package/src/layout/claude-ui-layout.md +72 -0
- package/src/layout/repl-ui-layout.md +139 -0
- package/src/memory/bom-table.ts +191 -20
- package/src/memory/drift.ts +42 -11
- package/src/util/cli-args.ts +42 -0
- package/src/util/dock.ts +161 -0
- package/src/util/git.ts +176 -4
- package/src/util/live-prompt.ts +595 -0
- package/src/util/paths.ts +10 -0
- package/src/util/preflight.ts +44 -0
- package/src/util/retry.ts +29 -0
- package/src/util/select.ts +192 -0
- package/src/util/tmp.ts +113 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only inspect helpers for REPL slash commands (/sync, /config, /runs…).
|
|
3
|
+
* All deterministic, no LLM.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
9
|
+
import { execa } from 'execa';
|
|
10
|
+
import { loadConfig } from '../config.js';
|
|
11
|
+
import { checkDrift } from '../memory/drift.js';
|
|
12
|
+
import { loadConstraints } from '../memory/constraints.js';
|
|
13
|
+
import { parseBomTable } from '../memory/bom-table.js';
|
|
14
|
+
import { syncVerify, formatSyncReport } from './sync.js';
|
|
15
|
+
import { listSymbols, listNets, pinNets } from '../kicad/sexp.js';
|
|
16
|
+
import { openspecValidate } from '../openspec/cli.js';
|
|
17
|
+
import { copper, dim, ok, warn, err } from '../agent/theme.js';
|
|
18
|
+
import { traceRule } from '../agent/animate.js';
|
|
19
|
+
import { shortPath } from '../util/paths.js';
|
|
20
|
+
import { branchName, headCommit, isDirty, isGitRepo, uncommittedCount } from '../util/git.js';
|
|
21
|
+
|
|
22
|
+
function meta(label: string, value: string): string {
|
|
23
|
+
return ` ${dim(label.padEnd(12))} ${value}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function sortedRunIds(repoRoot: string): Promise<string[]> {
|
|
27
|
+
const runsDir = path.join(repoRoot, '.copperhead', 'runs');
|
|
28
|
+
if (!existsSync(runsDir)) return [];
|
|
29
|
+
return (await readdir(runsDir, { withFileTypes: true }))
|
|
30
|
+
.filter((d) => d.isDirectory())
|
|
31
|
+
.map((d) => d.name)
|
|
32
|
+
.sort()
|
|
33
|
+
.reverse();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function resolveSchematic(
|
|
37
|
+
repoRoot: string,
|
|
38
|
+
): Promise<{ schPath: string } | { message: string }> {
|
|
39
|
+
const config = await loadConfig(repoRoot);
|
|
40
|
+
if (!config.schematic) {
|
|
41
|
+
return { message: 'no schematic configured (run copperhead init)' };
|
|
42
|
+
}
|
|
43
|
+
const schPath = path.join(repoRoot, config.schematic);
|
|
44
|
+
if (!existsSync(schPath)) {
|
|
45
|
+
return { message: `schematic missing: ${config.schematic}` };
|
|
46
|
+
}
|
|
47
|
+
return { schPath };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** `/sync` — verify-only inconsistency report (never auto-resolves). */
|
|
51
|
+
export async function formatSyncInspect(repoRoot: string): Promise<string> {
|
|
52
|
+
const report = await syncVerify(repoRoot);
|
|
53
|
+
const body = formatSyncReport(report);
|
|
54
|
+
const headline =
|
|
55
|
+
!report.resolvable.length && !report.violations.length
|
|
56
|
+
? ok(' sync: clean')
|
|
57
|
+
: report.violations.length
|
|
58
|
+
? err(` sync: ${report.violations.length} requirement violation(s), ${report.resolvable.length} resolvable`)
|
|
59
|
+
: warn(` sync: ${report.resolvable.length} resolvable inconsistency(ies)`);
|
|
60
|
+
return ['', copper(' Sync'), traceRule(12), '', headline, '', ...body.split('\n').map((l) => ` ${l}`), ''].join(
|
|
61
|
+
'\n',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `/config` — full resolved config view. */
|
|
66
|
+
export async function formatConfigInspect(repoRoot: string): Promise<string> {
|
|
67
|
+
const c = await loadConfig(repoRoot);
|
|
68
|
+
const lines = [
|
|
69
|
+
'',
|
|
70
|
+
copper(' Config'),
|
|
71
|
+
traceRule(14),
|
|
72
|
+
'',
|
|
73
|
+
meta('schematic', c.schematic ?? dim('null')),
|
|
74
|
+
meta('board', c.board ?? dim('null')),
|
|
75
|
+
meta('docs', c.docs),
|
|
76
|
+
meta('model', c.model ?? dim('null (use --model / env)')),
|
|
77
|
+
meta('maxTurns', String(c.maxTurns)),
|
|
78
|
+
meta('repair', String(c.maxRepairCycles)),
|
|
79
|
+
meta('origin', c.origin ?? dim('unset')),
|
|
80
|
+
meta('llmCache', c.llmCache ? 'on' : 'off'),
|
|
81
|
+
meta('budgets', Object.keys(c.budgets).length ? JSON.stringify(c.budgets) : dim('{}')),
|
|
82
|
+
];
|
|
83
|
+
if (c.stageMaxTurns && Object.keys(c.stageMaxTurns).length) {
|
|
84
|
+
lines.push(meta('stageTurns', JSON.stringify(c.stageMaxTurns)));
|
|
85
|
+
}
|
|
86
|
+
lines.push('');
|
|
87
|
+
lines.push(dim(` file ${shortPath(path.join(repoRoot, '.copperhead/config.json'))}`));
|
|
88
|
+
lines.push('');
|
|
89
|
+
return lines.join('\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** `/git` — porcelain status + branch tip. */
|
|
93
|
+
export async function formatGitInspect(repoRoot: string): Promise<string> {
|
|
94
|
+
if (!(await isGitRepo(repoRoot))) {
|
|
95
|
+
return ['', warn(' git: not a repository'), ''].join('\n');
|
|
96
|
+
}
|
|
97
|
+
const [branch, commit, dirty, n, porcelain] = await Promise.all([
|
|
98
|
+
branchName(repoRoot).catch(() => 'unknown'),
|
|
99
|
+
headCommit(repoRoot).catch(() => 'unknown'),
|
|
100
|
+
isDirty(repoRoot).catch(() => null),
|
|
101
|
+
uncommittedCount(repoRoot).catch(() => null),
|
|
102
|
+
execa('git', ['status', '--porcelain'], { cwd: repoRoot }).then((r) =>
|
|
103
|
+
r.stdout ? r.stdout.split('\n').filter(Boolean) : [],
|
|
104
|
+
),
|
|
105
|
+
]);
|
|
106
|
+
const state =
|
|
107
|
+
dirty === null ? 'unknown' : dirty ? warn(`dirty (${n ?? '?'} file(s))`) : ok('clean');
|
|
108
|
+
const lines = [
|
|
109
|
+
'',
|
|
110
|
+
copper(' Git'),
|
|
111
|
+
traceRule(10),
|
|
112
|
+
'',
|
|
113
|
+
meta('branch', `${branch}@${commit.slice(0, 7)}`),
|
|
114
|
+
meta('state', state),
|
|
115
|
+
'',
|
|
116
|
+
];
|
|
117
|
+
if (porcelain.length) {
|
|
118
|
+
lines.push(dim(' changes:'));
|
|
119
|
+
for (const row of porcelain.slice(0, 40)) lines.push(` ${row}`);
|
|
120
|
+
if (porcelain.length > 40) lines.push(dim(` … and ${porcelain.length - 40} more`));
|
|
121
|
+
lines.push('');
|
|
122
|
+
}
|
|
123
|
+
return lines.join('\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** `/drift` — doc↔schematic only (no ERC/DRC). */
|
|
127
|
+
export async function formatDriftInspect(repoRoot: string): Promise<string> {
|
|
128
|
+
const config = await loadConfig(repoRoot);
|
|
129
|
+
if (!config.schematic) {
|
|
130
|
+
return ['', warn(' drift: no schematic configured (run copperhead init)'), ''].join('\n');
|
|
131
|
+
}
|
|
132
|
+
const mismatches = await checkDrift(repoRoot, config.docs, config.schematic);
|
|
133
|
+
if (!mismatches.length) {
|
|
134
|
+
return ['', ok(' drift: docs match schematic'), ''].join('\n');
|
|
135
|
+
}
|
|
136
|
+
const lines = [
|
|
137
|
+
'',
|
|
138
|
+
copper(' Drift'),
|
|
139
|
+
traceRule(12),
|
|
140
|
+
'',
|
|
141
|
+
warn(` ${mismatches.length} mismatch(es):`),
|
|
142
|
+
'',
|
|
143
|
+
];
|
|
144
|
+
for (const m of mismatches.slice(0, 30)) {
|
|
145
|
+
lines.push(` ${copper('▸')} ${m.doc}`);
|
|
146
|
+
lines.push(dim(` claim ${m.claim}`));
|
|
147
|
+
lines.push(dim(` actual ${m.actual}`));
|
|
148
|
+
}
|
|
149
|
+
if (mismatches.length > 30) lines.push(dim(` … and ${mismatches.length - 30} more`));
|
|
150
|
+
lines.push('');
|
|
151
|
+
return lines.join('\n');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** `/constraints` — registry summary. */
|
|
155
|
+
export async function formatConstraintsInspect(repoRoot: string): Promise<string> {
|
|
156
|
+
const registry = await loadConstraints(repoRoot);
|
|
157
|
+
const keys = Object.keys(registry).sort();
|
|
158
|
+
if (!keys.length) {
|
|
159
|
+
return ['', dim(' constraints: none recorded yet'), ''].join('\n');
|
|
160
|
+
}
|
|
161
|
+
const lines = [
|
|
162
|
+
'',
|
|
163
|
+
copper(' Constraints'),
|
|
164
|
+
traceRule(20),
|
|
165
|
+
'',
|
|
166
|
+
dim(` ${keys.length} open constraint(s)`),
|
|
167
|
+
'',
|
|
168
|
+
];
|
|
169
|
+
for (const key of keys.slice(0, 40)) {
|
|
170
|
+
const c = registry[key]!;
|
|
171
|
+
const bound =
|
|
172
|
+
c.value !== undefined
|
|
173
|
+
? String(c.value)
|
|
174
|
+
: [c.min !== undefined ? `min ${c.min}` : '', c.max !== undefined ? `max ${c.max}` : '']
|
|
175
|
+
.filter(Boolean)
|
|
176
|
+
.join(', ') || (c.forbidden?.length ? `forbidden ${c.forbidden.join(',')}` : '—');
|
|
177
|
+
lines.push(` ${copper(key)} ${bound}`);
|
|
178
|
+
lines.push(dim(` source ${c.source} · affects ${c.affects.join(', ') || '—'}`));
|
|
179
|
+
}
|
|
180
|
+
if (keys.length > 40) lines.push(dim(` … and ${keys.length - 40} more`));
|
|
181
|
+
lines.push('');
|
|
182
|
+
return lines.join('\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** `/runs` — recent `.copperhead/runs/` summaries. */
|
|
186
|
+
export async function formatRunsInspect(repoRoot: string): Promise<string> {
|
|
187
|
+
const runsDir = path.join(repoRoot, '.copperhead', 'runs');
|
|
188
|
+
const entries = await sortedRunIds(repoRoot);
|
|
189
|
+
if (!entries.length) {
|
|
190
|
+
return ['', dim(' runs: none yet'), ''].join('\n');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const lines = ['', copper(' Recent runs'), traceRule(18), ''];
|
|
194
|
+
for (const id of entries.slice(0, 12)) {
|
|
195
|
+
const summaryPath = path.join(runsDir, id, 'summary.md');
|
|
196
|
+
const reportPath = path.join(runsDir, id, 'REPORT.md');
|
|
197
|
+
let outcome = dim('unknown');
|
|
198
|
+
if (existsSync(summaryPath)) {
|
|
199
|
+
const text = await readFile(summaryPath, 'utf8');
|
|
200
|
+
const m =
|
|
201
|
+
text.match(/exitPath[:\s*`]+([a-z-]+)/i) ||
|
|
202
|
+
text.match(/\*\*Exit(?:\s*path)?:\*\*\s*`?([a-z-]+)/i) ||
|
|
203
|
+
text.match(/## Run stats[\s\S]*?`([a-z-]+)`/);
|
|
204
|
+
if (m?.[1]) {
|
|
205
|
+
const pathName = m[1];
|
|
206
|
+
outcome =
|
|
207
|
+
pathName === 'done' ? ok(pathName) : /fail|error|refus|exhaust|stall/i.test(pathName) ? err(pathName) : warn(pathName);
|
|
208
|
+
} else if (/## Run stats/i.test(text)) {
|
|
209
|
+
outcome = dim('recorded');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const extras = existsSync(reportPath) ? dim(' · report') : '';
|
|
213
|
+
lines.push(` ${copper(id)} ${outcome}${extras}`);
|
|
214
|
+
}
|
|
215
|
+
if (entries.length > 12) lines.push(dim(` … and ${entries.length - 12} older`));
|
|
216
|
+
lines.push('');
|
|
217
|
+
lines.push(dim(` dir ${shortPath(runsDir)}`));
|
|
218
|
+
lines.push('');
|
|
219
|
+
return lines.join('\n');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** `/parts` — schematic symbol table (refdes / value / footprint). */
|
|
223
|
+
export async function formatPartsInspect(repoRoot: string): Promise<string> {
|
|
224
|
+
const resolved = await resolveSchematic(repoRoot);
|
|
225
|
+
if ('message' in resolved) {
|
|
226
|
+
return ['', warn(` parts: ${resolved.message}`), ''].join('\n');
|
|
227
|
+
}
|
|
228
|
+
const syms = await listSymbols(resolved.schPath);
|
|
229
|
+
if (!syms.length) {
|
|
230
|
+
return ['', dim(' parts: schematic has no components'), ''].join('\n');
|
|
231
|
+
}
|
|
232
|
+
const refW = Math.max(4, ...syms.map((s) => s.ref.length));
|
|
233
|
+
const valW = Math.min(24, Math.max(5, ...syms.map((s) => s.value.length)));
|
|
234
|
+
const lines = [
|
|
235
|
+
'',
|
|
236
|
+
copper(' Parts'),
|
|
237
|
+
traceRule(12),
|
|
238
|
+
'',
|
|
239
|
+
dim(` ${'ref'.padEnd(refW)} ${'value'.padEnd(valW)} footprint`),
|
|
240
|
+
'',
|
|
241
|
+
];
|
|
242
|
+
for (const s of syms.slice(0, 60)) {
|
|
243
|
+
const val = s.value.length > valW ? s.value.slice(0, valW - 1) + '…' : s.value;
|
|
244
|
+
lines.push(` ${copper(s.ref.padEnd(refW))} ${val.padEnd(valW)} ${dim(s.footprint || '—')}`);
|
|
245
|
+
}
|
|
246
|
+
if (syms.length > 60) lines.push(dim(` … and ${syms.length - 60} more`));
|
|
247
|
+
lines.push('');
|
|
248
|
+
return lines.join('\n');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** `/nets` — net names and pin attachments from the schematic. */
|
|
252
|
+
export async function formatNetsInspect(repoRoot: string): Promise<string> {
|
|
253
|
+
const resolved = await resolveSchematic(repoRoot);
|
|
254
|
+
if ('message' in resolved) {
|
|
255
|
+
return ['', warn(` nets: ${resolved.message}`), ''].join('\n');
|
|
256
|
+
}
|
|
257
|
+
const [names, pins] = await Promise.all([listNets(resolved.schPath), pinNets(resolved.schPath)]);
|
|
258
|
+
const byNet = new Map<string, string[]>();
|
|
259
|
+
for (const p of pins) {
|
|
260
|
+
const net = p.net ?? '(unconnected)';
|
|
261
|
+
const tag = p.pinName ? `${p.ref}.${p.pinName}` : `${p.ref}:${p.pinNumber}`;
|
|
262
|
+
const list = byNet.get(net) ?? [];
|
|
263
|
+
list.push(tag);
|
|
264
|
+
byNet.set(net, list);
|
|
265
|
+
}
|
|
266
|
+
const ordered = [...new Set([...names, ...byNet.keys()])].sort((a, b) => {
|
|
267
|
+
if (a === '(unconnected)') return 1;
|
|
268
|
+
if (b === '(unconnected)') return -1;
|
|
269
|
+
return a.localeCompare(b);
|
|
270
|
+
});
|
|
271
|
+
const lines = ['', copper(' Nets'), traceRule(10), '', dim(` ${ordered.length} net(s)`), ''];
|
|
272
|
+
for (const net of ordered.slice(0, 40)) {
|
|
273
|
+
const attached = (byNet.get(net) ?? []).sort();
|
|
274
|
+
const preview = attached.slice(0, 8).join(', ');
|
|
275
|
+
const extra = attached.length > 8 ? dim(` +${attached.length - 8}`) : '';
|
|
276
|
+
lines.push(` ${copper(net)} ${dim(preview)}${extra}`);
|
|
277
|
+
}
|
|
278
|
+
if (ordered.length > 40) lines.push(dim(` … and ${ordered.length - 40} more`));
|
|
279
|
+
lines.push('');
|
|
280
|
+
return lines.join('\n');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** `/bom` — parsed docs/BOM.md (inspect only; no export). */
|
|
284
|
+
export async function formatBomInspect(repoRoot: string): Promise<string> {
|
|
285
|
+
const config = await loadConfig(repoRoot);
|
|
286
|
+
const bomPath = path.join(repoRoot, config.docs, 'BOM.md');
|
|
287
|
+
if (!existsSync(bomPath)) {
|
|
288
|
+
return ['', warn(' bom: docs/BOM.md not found'), ''].join('\n');
|
|
289
|
+
}
|
|
290
|
+
const rows = parseBomTable(await readFile(bomPath, 'utf8'));
|
|
291
|
+
if (!rows.length) {
|
|
292
|
+
return ['', dim(' bom: no parseable rows in BOM.md'), ''].join('\n');
|
|
293
|
+
}
|
|
294
|
+
const refW = Math.max(5, ...rows.map((r) => r.refdes.length));
|
|
295
|
+
const lines = ['', copper(' BOM'), traceRule(8), '', dim(` ${rows.length} row(s) from ${shortPath(bomPath)}`), ''];
|
|
296
|
+
for (const r of rows.slice(0, 50)) {
|
|
297
|
+
const flags = r.flags.length ? dim(` [${r.flags.join(',')}]`) : '';
|
|
298
|
+
lines.push(
|
|
299
|
+
` ${copper(r.refdes.padEnd(refW))} ${r.value ?? '—'} ${dim(r.footprint ?? '')}${flags}`,
|
|
300
|
+
);
|
|
301
|
+
if (r.mpn) lines.push(dim(` mpn ${r.mpn}`));
|
|
302
|
+
}
|
|
303
|
+
if (rows.length > 50) lines.push(dim(` … and ${rows.length - 50} more`));
|
|
304
|
+
lines.push('');
|
|
305
|
+
return lines.join('\n');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** `/openspec` — `openspec validate` summary (same gate as check). */
|
|
309
|
+
export async function formatOpenSpecInspect(repoRoot: string): Promise<string> {
|
|
310
|
+
const cfgPath = path.join(repoRoot, 'openspec', 'config.yaml');
|
|
311
|
+
if (!existsSync(cfgPath)) {
|
|
312
|
+
return ['', dim(' openspec: no openspec/config.yaml in this repo'), ''].join('\n');
|
|
313
|
+
}
|
|
314
|
+
const res = await openspecValidate(repoRoot);
|
|
315
|
+
const headline = res.ok ? ok(' openspec: valid') : err(' openspec: validation failed');
|
|
316
|
+
const body = res.output
|
|
317
|
+
.trim()
|
|
318
|
+
.split('\n')
|
|
319
|
+
.slice(0, 30)
|
|
320
|
+
.map((l) => ` ${l}`)
|
|
321
|
+
.join('\n');
|
|
322
|
+
const lines = ['', copper(' OpenSpec'), traceRule(16), '', headline, ''];
|
|
323
|
+
if (body) lines.push(body, '');
|
|
324
|
+
return lines.join('\n');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** `/last` — preview the newest run summary.md. */
|
|
328
|
+
export async function formatLastInspect(repoRoot: string): Promise<string> {
|
|
329
|
+
const entries = await sortedRunIds(repoRoot);
|
|
330
|
+
if (!entries.length) {
|
|
331
|
+
return ['', dim(' last: no runs yet'), ''].join('\n');
|
|
332
|
+
}
|
|
333
|
+
const id = entries[0]!;
|
|
334
|
+
const runsDir = path.join(repoRoot, '.copperhead', 'runs');
|
|
335
|
+
const summaryPath = path.join(runsDir, id, 'summary.md');
|
|
336
|
+
const transcriptPath = path.join(runsDir, id, 'transcript.jsonl');
|
|
337
|
+
const lines = ['', copper(' Last run'), traceRule(14), '', meta('id', id), ''];
|
|
338
|
+
if (!existsSync(summaryPath)) {
|
|
339
|
+
lines.push(dim(' summary.md missing'), '');
|
|
340
|
+
return lines.join('\n');
|
|
341
|
+
}
|
|
342
|
+
const text = await readFile(summaryPath, 'utf8');
|
|
343
|
+
const preview = text.split('\n').slice(0, 40);
|
|
344
|
+
lines.push(dim(` ${shortPath(summaryPath)}`));
|
|
345
|
+
if (existsSync(transcriptPath)) {
|
|
346
|
+
lines.push(dim(` ${shortPath(transcriptPath)}`));
|
|
347
|
+
}
|
|
348
|
+
lines.push('');
|
|
349
|
+
for (const line of preview) lines.push(` ${line}`);
|
|
350
|
+
if (text.split('\n').length > 40) lines.push(dim(' … truncated'));
|
|
351
|
+
lines.push('');
|
|
352
|
+
return lines.join('\n');
|
|
353
|
+
}
|