copperhead 0.7.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 +15 -5
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/providers/claude-code.js +1 -212
- 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/render.js +32 -15
- package/dist/agent/render.js.map +1 -1
- 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/cli.js +134 -13
- package/dist/cli.js.map +1 -1
- package/dist/commands/create.js +41 -32
- 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 +5 -2
- package/dist/config.js.map +1 -1
- package/dist/kicad/cli.js +126 -6
- package/dist/kicad/cli.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 +129 -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/select.js +172 -0
- package/dist/util/select.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 +15 -5
- package/src/agent/providers/claude-code.ts +2 -216
- package/src/agent/providers/cursor.ts +364 -0
- package/src/agent/providers/tool-protocol.ts +212 -0
- package/src/agent/render.ts +33 -16
- package/src/agent/runmeta.ts +6 -7
- package/src/agent/theme.ts +91 -0
- package/src/cli.ts +139 -15
- package/src/commands/create.ts +81 -30
- 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 +6 -3
- package/src/kicad/cli.ts +132 -7
- package/src/layout/claude-ui-layout.md +72 -0
- package/src/layout/repl-ui-layout.md +139 -0
- package/src/util/cli-args.ts +42 -0
- package/src/util/dock.ts +161 -0
- package/src/util/git.ts +140 -4
- package/src/util/live-prompt.ts +595 -0
- package/src/util/paths.ts +10 -0
- package/src/util/select.ts +192 -0
package/src/commands/create.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { diagnoseStageFailure, transcriptExcerpt, withTimeout, type StageDiagnos
|
|
|
14
14
|
import type { Provider } from '../agent/types.js';
|
|
15
15
|
import type { RunMetaInput } from '../agent/runmeta.js';
|
|
16
16
|
import { fmtDuration, fmtTokens, type ProgressRenderer } from '../agent/render.js';
|
|
17
|
+
import { copper, dim, ok, stageLine, warn } from '../agent/theme.js';
|
|
17
18
|
import { openspecInit } from '../openspec/cli.js';
|
|
18
19
|
import { sweepStaleTempDirs, pruneHistoryDir } from '../util/tmp.js';
|
|
19
20
|
import { assertDiskSpace, DEFAULT_MIN_FREE_BYTES } from '../util/preflight.js';
|
|
@@ -161,7 +162,7 @@ export interface CreateOptions {
|
|
|
161
162
|
async function emitJlcpcbAfterOutputs(stageName: string, opts: CreateOptions): Promise<void> {
|
|
162
163
|
if (stageName !== 'outputs') return;
|
|
163
164
|
const out = await emitCreateJlcpcbBom(opts.repoRoot);
|
|
164
|
-
if (out) opts.log(
|
|
165
|
+
if (out) opts.log(stageLine('outputs', `emitted ${out} (JLCPCB assembly BOM)`, 'ok'));
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
/** Stages whose output is a KiCad file worth rendering to an image (5.4). */
|
|
@@ -203,16 +204,26 @@ async function commitResumedStage(opts: CreateOptions, config: CopperheadConfig,
|
|
|
203
204
|
const foreign = dirty.filter((f) => !isManagedPath(f, config));
|
|
204
205
|
if (foreign.length) {
|
|
205
206
|
opts.log(
|
|
206
|
-
|
|
207
|
-
|
|
207
|
+
stageLine(
|
|
208
|
+
stageName,
|
|
209
|
+
`already-complete work is uncommitted, but the tree also has non-copperhead changes ` +
|
|
210
|
+
`(${foreign.slice(0, 3).join(', ')}${foreign.length > 3 ? ', …' : ''}); leaving it uncommitted so nothing of yours is swept up`,
|
|
211
|
+
'warn',
|
|
212
|
+
),
|
|
208
213
|
);
|
|
209
214
|
return;
|
|
210
215
|
}
|
|
211
216
|
try {
|
|
212
217
|
const sha = await commitAll(opts.repoRoot, `copperhead: resume — commit completed stage ${stageName}`);
|
|
213
|
-
opts.log(
|
|
214
|
-
|
|
215
|
-
|
|
218
|
+
opts.log(
|
|
219
|
+
stageLine(
|
|
220
|
+
stageName,
|
|
221
|
+
`committed already-complete work ${sha.slice(0, 10)} so a later rollback cannot wipe it (2.4)`,
|
|
222
|
+
'ok',
|
|
223
|
+
),
|
|
224
|
+
);
|
|
225
|
+
} catch (e) {
|
|
226
|
+
opts.log(stageLine(stageName, `could not commit resumed work (${(e as Error).message})`, 'err'));
|
|
216
227
|
}
|
|
217
228
|
}
|
|
218
229
|
|
|
@@ -243,12 +254,18 @@ async function renderStageArtifacts(opts: CreateOptions, stageName: string, tran
|
|
|
243
254
|
try {
|
|
244
255
|
await exportSvg(kind, path.join(opts.repoRoot, file), artifactsDir);
|
|
245
256
|
rendered++;
|
|
246
|
-
} catch (
|
|
247
|
-
opts.log(
|
|
257
|
+
} catch (e) {
|
|
258
|
+
opts.log(stageLine(stageName, `could not render ${kind} SVG (${(e as Error).message})`, 'warn'));
|
|
248
259
|
}
|
|
249
260
|
}
|
|
250
261
|
if (rendered) {
|
|
251
|
-
opts.log(
|
|
262
|
+
opts.log(
|
|
263
|
+
stageLine(
|
|
264
|
+
stageName,
|
|
265
|
+
`rendered ${rendered} SVG artifact(s) into ${path.relative(opts.repoRoot, artifactsDir)}/`,
|
|
266
|
+
'ok',
|
|
267
|
+
),
|
|
268
|
+
);
|
|
252
269
|
}
|
|
253
270
|
}
|
|
254
271
|
|
|
@@ -331,10 +348,14 @@ function resumeCommand(opts: CreateOptions): string {
|
|
|
331
348
|
*/
|
|
332
349
|
function logResumePoint(opts: CreateOptions, stage: Stage, index: number): void {
|
|
333
350
|
opts.log('');
|
|
334
|
-
opts.log(`⏸ stopped at stage ${index + 1}/${STAGES.length} (${stage.name}). To resume from here, run:`);
|
|
335
|
-
opts.log(` ${resumeCommand(opts)}`);
|
|
336
351
|
opts.log(
|
|
337
|
-
|
|
352
|
+
warn(`⏸ stopped at stage ${index + 1}/${STAGES.length} (${stage.name}). To resume from here, run:`),
|
|
353
|
+
);
|
|
354
|
+
opts.log(copper(` ${resumeCommand(opts)}`));
|
|
355
|
+
opts.log(
|
|
356
|
+
dim(
|
|
357
|
+
` (${index} stage(s) already complete are detected from repo state and skipped; it resumes at ${stage.name}.)`,
|
|
358
|
+
),
|
|
338
359
|
);
|
|
339
360
|
}
|
|
340
361
|
|
|
@@ -380,16 +401,21 @@ function printCostTable(opts: CreateOptions, costs: StageCost[]): void {
|
|
|
380
401
|
` ${r.stage.padEnd(w.stage)} ${r.wall.padStart(w.wall)} ${r.turns.padStart(w.turns)} ${r.out.padStart(w.out)} ${r.cache.padStart(w.cache)}`;
|
|
381
402
|
const rule = ` ${'-'.repeat(w.stage)} ${'-'.repeat(w.wall)} ${'-'.repeat(w.turns)} ${'-'.repeat(w.out)} ${'-'.repeat(w.cache)}`;
|
|
382
403
|
opts.log('');
|
|
383
|
-
opts.log('Per-stage cost summary
|
|
384
|
-
opts.log(line(header));
|
|
385
|
-
opts.log(rule);
|
|
404
|
+
opts.log(copper('Per-stage cost summary'));
|
|
405
|
+
opts.log(dim(line(header)));
|
|
406
|
+
opts.log(dim(rule));
|
|
386
407
|
for (const r of rows) opts.log(line(r));
|
|
387
408
|
if (total) {
|
|
388
|
-
opts.log(rule);
|
|
389
|
-
opts.log(line(total));
|
|
409
|
+
opts.log(dim(rule));
|
|
410
|
+
opts.log(boldTotal(line(total)));
|
|
390
411
|
}
|
|
391
412
|
}
|
|
392
413
|
|
|
414
|
+
function boldTotal(s: string): string {
|
|
415
|
+
// TOTAL row: keep digits readable, accent only the label when color is on.
|
|
416
|
+
return s.replace(/^(\s*)TOTAL/, (_, sp: string) => `${sp}${copper('TOTAL')}`);
|
|
417
|
+
}
|
|
418
|
+
|
|
393
419
|
/** Sum the cost of the stages that actually ran this invocation (resumed stages
|
|
394
420
|
* cost nothing). Shared by the cumulative line and the end-of-run report (5.6). */
|
|
395
421
|
function ranTotals(stageCosts: StageCost[]): {
|
|
@@ -422,8 +448,10 @@ function logCumulative(opts: CreateOptions, stageCosts: StageCost[]): void {
|
|
|
422
448
|
const t = ranTotals(stageCosts);
|
|
423
449
|
if (!t.turns && !t.wallMs) return; // nothing has actually run yet (all resumed)
|
|
424
450
|
opts.log(
|
|
425
|
-
|
|
426
|
-
|
|
451
|
+
dim(
|
|
452
|
+
`pipeline so far: ${stageCosts.length}/${STAGES.length} stages · ${fmtDuration(t.wallMs)} · ` +
|
|
453
|
+
`${fmtTokens(t.tokensOut)} out tokens · ${cachePct(t.cacheHits, t.turns)}% cache hits`,
|
|
454
|
+
),
|
|
427
455
|
);
|
|
428
456
|
}
|
|
429
457
|
|
|
@@ -533,11 +561,11 @@ export async function runCreate(opts: CreateOptions): Promise<{ ok: boolean; com
|
|
|
533
561
|
// so a concurrent run's fresh dirs are never touched; best-effort, so it never
|
|
534
562
|
// blocks a run (I8).
|
|
535
563
|
const swept = await sweepStaleTempDirs(Date.now());
|
|
536
|
-
if (swept.length) opts.log(`startup: reclaimed ${swept.length} stale temp dir(s) from earlier runs`);
|
|
564
|
+
if (swept.length) opts.log(dim(`startup: reclaimed ${swept.length} stale temp dir(s) from earlier runs`));
|
|
537
565
|
// Cap the gitignored .history/ so KiCad local history cannot grow unbounded
|
|
538
566
|
// across a long run and fill the disk (4.1, I8). Best-effort; keeps the newest.
|
|
539
567
|
const pruned = await pruneHistoryDir(opts.repoRoot);
|
|
540
|
-
if (pruned) opts.log(`startup: pruned ${pruned} old .history/ entrie(s) to cap local-history growth`);
|
|
568
|
+
if (pruned) opts.log(dim(`startup: pruned ${pruned} old .history/ entrie(s) to cap local-history growth`));
|
|
541
569
|
await openspecInit(opts.repoRoot);
|
|
542
570
|
const completed: string[] = [];
|
|
543
571
|
const stageCosts: StageCost[] = [];
|
|
@@ -550,10 +578,12 @@ export async function runCreate(opts: CreateOptions): Promise<{ ok: boolean; com
|
|
|
550
578
|
// contract can eventually be met. No-op once a project exists.
|
|
551
579
|
if (stage.name === 'schematic') {
|
|
552
580
|
const created = await bootstrapKicadProject(opts.repoRoot, brief);
|
|
553
|
-
if (created)
|
|
581
|
+
if (created) {
|
|
582
|
+
opts.log(stageLine('schematic', `scaffolded empty KiCad project (${created} + board + project), wired into config`));
|
|
583
|
+
}
|
|
554
584
|
}
|
|
555
585
|
if (await stage.isComplete(opts.repoRoot, config.docs)) {
|
|
556
|
-
opts.log(
|
|
586
|
+
opts.log(stageLine(stage.name, 'already complete (resuming past it)', 'ok'));
|
|
557
587
|
await commitResumedStage(opts, config, stage.name);
|
|
558
588
|
completed.push(stage.name);
|
|
559
589
|
stageCosts.push({ name: stage.name, resumed: true, wallMs: 0, turns: 0, tokensIn: 0, tokensOut: 0, cacheHits: 0 });
|
|
@@ -585,9 +615,16 @@ export async function runCreate(opts: CreateOptions): Promise<{ ok: boolean; com
|
|
|
585
615
|
// whenever the project already exists.
|
|
586
616
|
if (stage.name === 'schematic') {
|
|
587
617
|
const rescaffolded = await bootstrapKicadProject(opts.repoRoot, brief);
|
|
588
|
-
if (rescaffolded && attempt > 1)
|
|
618
|
+
if (rescaffolded && attempt > 1) {
|
|
619
|
+
opts.log(stageLine('schematic', 're-scaffolded empty KiCad project after rollback, wired into config'));
|
|
620
|
+
}
|
|
589
621
|
}
|
|
590
|
-
opts.log(
|
|
622
|
+
opts.log(
|
|
623
|
+
stageLine(
|
|
624
|
+
stage.name,
|
|
625
|
+
`running${attempt > 1 ? ` (attempt ${attempt}/${config.maxStageRetries + 1})` : ''}`,
|
|
626
|
+
),
|
|
627
|
+
);
|
|
591
628
|
const res = await runAgentLoop({
|
|
592
629
|
repoRoot: opts.repoRoot,
|
|
593
630
|
model: opts.model,
|
|
@@ -635,12 +672,16 @@ export async function runCreate(opts: CreateOptions): Promise<{ ok: boolean; com
|
|
|
635
672
|
|
|
636
673
|
if (attempt > config.maxStageRetries) {
|
|
637
674
|
opts.log(
|
|
638
|
-
|
|
675
|
+
stageLine(
|
|
676
|
+
stage.name,
|
|
677
|
+
`${failure}; exhausted ${config.maxStageRetries} auto-retry(ies). Stopping for a human.`,
|
|
678
|
+
'err',
|
|
679
|
+
),
|
|
639
680
|
);
|
|
640
681
|
break;
|
|
641
682
|
}
|
|
642
683
|
|
|
643
|
-
opts.log(
|
|
684
|
+
opts.log(stageLine(stage.name, `${failure}; asking the model whether to retry…`, 'warn'));
|
|
644
685
|
const diagnosis = await diagnose({
|
|
645
686
|
model: opts.model,
|
|
646
687
|
timeoutMs: config.turnTimeoutMs,
|
|
@@ -656,9 +697,15 @@ export async function runCreate(opts: CreateOptions): Promise<{ ok: boolean; com
|
|
|
656
697
|
// not under-report by omitting it.
|
|
657
698
|
cost.tokensIn += diagnosis.usage?.inputTokens ?? 0;
|
|
658
699
|
cost.tokensOut += diagnosis.usage?.outputTokens ?? 0;
|
|
659
|
-
opts.log(
|
|
700
|
+
opts.log(
|
|
701
|
+
stageLine(
|
|
702
|
+
stage.name,
|
|
703
|
+
`diagnosis → ${diagnosis.verdict} — ${diagnosis.reason}`,
|
|
704
|
+
diagnosis.verdict === 'abort' ? 'err' : 'warn',
|
|
705
|
+
),
|
|
706
|
+
);
|
|
660
707
|
if (diagnosis.verdict === 'abort') {
|
|
661
|
-
opts.log(
|
|
708
|
+
opts.log(stageLine(stage.name, 'recovery supervisor recommends stopping for a human.', 'err'));
|
|
662
709
|
break;
|
|
663
710
|
}
|
|
664
711
|
guidance = diagnosis.guidance ?? `The previous attempt failed: ${failure}. ${diagnosis.reason}`;
|
|
@@ -680,7 +727,11 @@ export async function runCreate(opts: CreateOptions): Promise<{ ok: boolean; com
|
|
|
680
727
|
}
|
|
681
728
|
|
|
682
729
|
const check = await runCheck(opts.repoRoot, opts.log);
|
|
683
|
-
opts.log(
|
|
730
|
+
opts.log(
|
|
731
|
+
check.ok
|
|
732
|
+
? ok('create pipeline complete; all checks green')
|
|
733
|
+
: warn('create pipeline complete with check failures'),
|
|
734
|
+
);
|
|
684
735
|
printCostTable(opts, stageCosts);
|
|
685
736
|
await writeRunReport(opts, stageCosts);
|
|
686
737
|
return { ok: check.ok, completed };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end demo: scaffold a tiny git repo and run `create` against the
|
|
3
|
+
* USB-C power breakout brief (same path as `npm run demo:simple`).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { mkdir, writeFile, readFile, readdir, appendFile } from 'node:fs/promises';
|
|
10
|
+
import { execa } from 'execa';
|
|
11
|
+
import type { ProgressRenderer } from '../agent/render.js';
|
|
12
|
+
import type { RunMetaInput } from '../agent/runmeta.js';
|
|
13
|
+
import type { BudgetExhaustedStats } from '../agent/loop.js';
|
|
14
|
+
import { copper, dim, ok } from '../agent/theme.js';
|
|
15
|
+
import { traceRule } from '../agent/animate.js';
|
|
16
|
+
import { shortPath } from '../util/paths.js';
|
|
17
|
+
import { runCreate } from './create.js';
|
|
18
|
+
|
|
19
|
+
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
20
|
+
const DEFAULT_BRIEF = path.join(PKG_ROOT, 'examples/simple/usb-c-breakout.md');
|
|
21
|
+
const DEFAULT_DEMO_DIR = path.join(PKG_ROOT, 'demo-runs/usb-c-breakout');
|
|
22
|
+
|
|
23
|
+
export interface DemoOptions {
|
|
24
|
+
model: string;
|
|
25
|
+
modelSource: RunMetaInput['modelSource'];
|
|
26
|
+
version: string;
|
|
27
|
+
kicadCliVersion: string;
|
|
28
|
+
interactive?: boolean;
|
|
29
|
+
/** Override demo repo (defaults to demo-runs/usb-c-breakout or COPPERHEAD_DEMO_DIR). */
|
|
30
|
+
demoDir?: string;
|
|
31
|
+
briefPath?: string;
|
|
32
|
+
log?: (line: string) => void;
|
|
33
|
+
renderer: ProgressRenderer;
|
|
34
|
+
onBudgetExhausted?: (stats: BudgetExhaustedStats) => Promise<number>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Resolve the packaged USB-C brief; throws a clear error if the install is incomplete. */
|
|
38
|
+
export function defaultBriefPath(): string {
|
|
39
|
+
if (!existsSync(DEFAULT_BRIEF)) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`demo brief missing at ${DEFAULT_BRIEF}; reinstall copperhead or run from a full checkout`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return DEFAULT_BRIEF;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function defaultDemoDir(): string {
|
|
48
|
+
return process.env.COPPERHEAD_DEMO_DIR ?? DEFAULT_DEMO_DIR;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Marker identifying a directory scaffolded by `copperhead demo`. */
|
|
52
|
+
const DEMO_MARKER = path.join('.copperhead', 'demo-repo');
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Prepare a clean-enough git repo for the create pipeline (git init, ignore
|
|
56
|
+
* runs/, baseline config + commit). Mirrors scripts/demo-simple.sh.
|
|
57
|
+
*/
|
|
58
|
+
export async function scaffoldDemoRepo(demoDir: string): Promise<void> {
|
|
59
|
+
await mkdir(demoDir, { recursive: true });
|
|
60
|
+
|
|
61
|
+
// Fail closed before touching git: only scaffold into an empty directory
|
|
62
|
+
// or one this function created earlier (identified by the marker file).
|
|
63
|
+
// Anything else risks git-initializing and committing into a directory
|
|
64
|
+
// the user cares about.
|
|
65
|
+
const entries = await readdir(demoDir);
|
|
66
|
+
if (entries.length > 0 && !existsSync(path.join(demoDir, DEMO_MARKER))) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`refusing to scaffold the demo repo in ${demoDir}: the directory is not empty and was not created by copperhead demo; use an empty directory (or point COPPERHEAD_DEMO_DIR elsewhere)`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const git = async (...args: string[]) => execa('git', args, { cwd: demoDir });
|
|
73
|
+
|
|
74
|
+
if (!existsSync(path.join(demoDir, '.git'))) {
|
|
75
|
+
await git('init', '-q');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const hasName = await execa('git', ['config', 'user.name'], { cwd: demoDir, reject: false });
|
|
79
|
+
if (hasName.exitCode !== 0) await git('config', 'user.name', 'copperhead demo');
|
|
80
|
+
const hasEmail = await execa('git', ['config', 'user.email'], { cwd: demoDir, reject: false });
|
|
81
|
+
if (hasEmail.exitCode !== 0) await git('config', 'user.email', 'demo@copperhead.local');
|
|
82
|
+
|
|
83
|
+
const gi = path.join(demoDir, '.gitignore');
|
|
84
|
+
if (!existsSync(gi)) await writeFile(gi, '', 'utf8');
|
|
85
|
+
const giText = await readFile(gi, 'utf8');
|
|
86
|
+
if (!giText.split('\n').includes('.copperhead/runs/')) {
|
|
87
|
+
await appendFile(gi, (giText.endsWith('\n') || giText === '' ? '' : '\n') + '.copperhead/runs/\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const cfgDir = path.join(demoDir, '.copperhead');
|
|
91
|
+
await mkdir(cfgDir, { recursive: true });
|
|
92
|
+
const marker = path.join(demoDir, DEMO_MARKER);
|
|
93
|
+
if (!existsSync(marker)) await writeFile(marker, 'created by copperhead demo\n', 'utf8');
|
|
94
|
+
const cfg = path.join(cfgDir, 'config.json');
|
|
95
|
+
if (!existsSync(cfg)) {
|
|
96
|
+
// Create stages need a larger turn budget than a single `do` edit.
|
|
97
|
+
await writeFile(
|
|
98
|
+
cfg,
|
|
99
|
+
`${JSON.stringify({ docs: 'docs/', maxTurns: 100, maxRepairCycles: 5 }, null, 2)}\n`,
|
|
100
|
+
'utf8',
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await git('add', '.gitignore', '.copperhead/config.json');
|
|
105
|
+
const head = await execa('git', ['rev-parse', '--verify', 'HEAD'], { cwd: demoDir, reject: false });
|
|
106
|
+
if (head.exitCode !== 0) {
|
|
107
|
+
await git('commit', '-q', '-m', 'demo: initialize repository');
|
|
108
|
+
} else {
|
|
109
|
+
const staged = await execa('git', ['diff', '--cached', '--quiet'], { cwd: demoDir, reject: false });
|
|
110
|
+
if (staged.exitCode !== 0) {
|
|
111
|
+
await git('commit', '-q', '-m', 'demo: update demo scaffolding');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** What copperhead is — printed by `/demo` and `copperhead demo --tour`. */
|
|
117
|
+
export function demoTourText(): string {
|
|
118
|
+
return [
|
|
119
|
+
'',
|
|
120
|
+
copper(' What copperhead does'),
|
|
121
|
+
traceRule(28),
|
|
122
|
+
'',
|
|
123
|
+
dim(' Cursor for circuit boards. You describe a change; the agent edits'),
|
|
124
|
+
dim(' real KiCad files, keeps design docs in sync, and verifies with'),
|
|
125
|
+
dim(' kicad-cli ERC/DRC before anything is committed.'),
|
|
126
|
+
'',
|
|
127
|
+
copper(' The loop'),
|
|
128
|
+
traceRule(16),
|
|
129
|
+
'',
|
|
130
|
+
` ${copper('1.')} ${dim('Propose')} write an OpenSpec change (edit tools stay locked until it validates)`,
|
|
131
|
+
` ${copper('2.')} ${dim('Edit')} anchored edits to .kicad_sch / .kicad_pcb + docs`,
|
|
132
|
+
` ${copper('3.')} ${dim('Verify')} run ERC (and DRC if the board changed); repair or roll back`,
|
|
133
|
+
` ${copper('4.')} ${dim('Remember')} DECISIONS.md + CHANGELOG + a run summary next to the transcript`,
|
|
134
|
+
'',
|
|
135
|
+
copper(' Try it'),
|
|
136
|
+
traceRule(14),
|
|
137
|
+
'',
|
|
138
|
+
` ${copper('copperhead demo')} ${dim('full create pipeline (USB-C breakout)')}`,
|
|
139
|
+
` ${copper('copperhead')} ${dim('interactive shell — type a change request')}`,
|
|
140
|
+
` ${copper('copperhead do "add an LED"')} ${dim('one-shot edit on the current repo')}`,
|
|
141
|
+
'',
|
|
142
|
+
copper(' Example prompts'),
|
|
143
|
+
traceRule(22),
|
|
144
|
+
'',
|
|
145
|
+
dim(' • add reverse-polarity protection on VIN'),
|
|
146
|
+
dim(' • rename net KEY_DAH to KEY_DASH'),
|
|
147
|
+
dim(' • move the key input to a different RTC-capable pin'),
|
|
148
|
+
'',
|
|
149
|
+
].join('\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function runDemo(opts: DemoOptions): Promise<{ ok: boolean; demoDir: string }> {
|
|
153
|
+
const log = opts.log ?? ((l: string) => console.log(l));
|
|
154
|
+
const demoDir = path.resolve(opts.demoDir ?? defaultDemoDir());
|
|
155
|
+
const briefPath = path.resolve(opts.briefPath ?? defaultBriefPath());
|
|
156
|
+
|
|
157
|
+
log('');
|
|
158
|
+
log(` ${copper('copperhead demo')} ${dim('USB-C power breakout')}`);
|
|
159
|
+
log(` ${dim('repo')} ${shortPath(demoDir)}`);
|
|
160
|
+
log(` ${dim('brief')} ${shortPath(briefPath)}`);
|
|
161
|
+
log('');
|
|
162
|
+
|
|
163
|
+
await scaffoldDemoRepo(demoDir);
|
|
164
|
+
log(ok(' scaffold ready'));
|
|
165
|
+
log('');
|
|
166
|
+
|
|
167
|
+
const res = await runCreate({
|
|
168
|
+
repoRoot: demoDir,
|
|
169
|
+
briefPath,
|
|
170
|
+
model: opts.model,
|
|
171
|
+
interactive: opts.interactive ?? false,
|
|
172
|
+
...(opts.onBudgetExhausted ? { onBudgetExhausted: opts.onBudgetExhausted } : {}),
|
|
173
|
+
log,
|
|
174
|
+
renderer: opts.renderer,
|
|
175
|
+
meta: {
|
|
176
|
+
command: 'create',
|
|
177
|
+
modelSource: opts.modelSource,
|
|
178
|
+
version: opts.version,
|
|
179
|
+
kicadCliVersion: opts.kicadCliVersion,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return { ok: res.ok, demoDir };
|
|
184
|
+
}
|