@automaze/proof 0.20260311.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/dist/index.js ADDED
@@ -0,0 +1,843 @@
1
+ // src/index.ts
2
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
3
+ import { join as join5, basename as basename2 } from "path";
4
+ import { tmpdir } from "os";
5
+ import { existsSync as existsSync3 } from "fs";
6
+
7
+ // src/detect.ts
8
+ import { existsSync } from "fs";
9
+ import { readFile } from "fs/promises";
10
+ import { join } from "path";
11
+ var BROWSER_CONFIG_PATTERNS = [
12
+ "playwright.config.ts",
13
+ "playwright.config.js",
14
+ "playwright.config.mjs"
15
+ ];
16
+ async function detectMode(projectDir) {
17
+ for (const config of BROWSER_CONFIG_PATTERNS) {
18
+ if (existsSync(join(projectDir, config))) {
19
+ return "browser";
20
+ }
21
+ }
22
+ const pkgPath = join(projectDir, "package.json");
23
+ if (existsSync(pkgPath)) {
24
+ try {
25
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
26
+ const allDeps = {
27
+ ...pkg.dependencies,
28
+ ...pkg.devDependencies,
29
+ ...pkg.peerDependencies
30
+ };
31
+ if (allDeps["@playwright/test"] || allDeps["playwright"]) {
32
+ return "browser";
33
+ }
34
+ } catch {}
35
+ }
36
+ return "terminal";
37
+ }
38
+
39
+ // src/modes/visual.ts
40
+ import { readdir, rm, copyFile, mkdir } from "fs/promises";
41
+ import { existsSync as existsSync2 } from "fs";
42
+ import { join as join2, dirname } from "path";
43
+ import { spawn as spawn2 } from "child_process";
44
+
45
+ // src/duration.ts
46
+ import { spawn } from "child_process";
47
+ async function getVideoDuration(filePath) {
48
+ try {
49
+ const output = await new Promise((resolve, reject) => {
50
+ const proc = spawn("ffprobe", [
51
+ "-v",
52
+ "quiet",
53
+ "-print_format",
54
+ "json",
55
+ "-show_format",
56
+ filePath
57
+ ], { stdio: ["ignore", "pipe", "pipe"] });
58
+ let stdout = "";
59
+ proc.stdout.on("data", (data) => {
60
+ stdout += data.toString();
61
+ });
62
+ proc.on("close", (code) => code === 0 ? resolve(stdout) : reject(new Error("ffprobe failed")));
63
+ proc.on("error", reject);
64
+ });
65
+ const parsed = JSON.parse(output);
66
+ const seconds = parseFloat(parsed.format?.duration ?? "0");
67
+ return Math.round(seconds * 1000);
68
+ } catch {
69
+ return 0;
70
+ }
71
+ }
72
+
73
+ // src/modes/visual.ts
74
+ var CURSOR_HIGHLIGHT_SCRIPT = `
75
+ (function() {
76
+ function init() {
77
+ if (document.getElementById('__proof_cursor')) return;
78
+ const cursor = document.createElement('div');
79
+ cursor.id = '__proof_cursor';
80
+ cursor.style.cssText = [
81
+ 'position: fixed',
82
+ 'width: 20px',
83
+ 'height: 20px',
84
+ 'border-radius: 50%',
85
+ 'background: rgba(255, 50, 50, 0.8)',
86
+ 'border: 2px solid rgba(255, 255, 255, 0.95)',
87
+ 'pointer-events: none',
88
+ 'z-index: 2147483647',
89
+ 'transform: translate(-50%, -50%)',
90
+ 'box-shadow: 0 0 8px rgba(255,50,50,0.4), 0 0 2px rgba(0,0,0,0.3)',
91
+ 'display: none',
92
+ 'transition: left 0.08s ease-out, top 0.08s ease-out',
93
+ ].join(';');
94
+ document.documentElement.appendChild(cursor);
95
+
96
+ function showCursor(x, y) {
97
+ cursor.style.left = x + 'px';
98
+ cursor.style.top = y + 'px';
99
+ cursor.style.display = 'block';
100
+ }
101
+
102
+ function spawnRipple(x, y) {
103
+ const ripple = document.createElement('div');
104
+ ripple.style.cssText = [
105
+ 'position: fixed',
106
+ 'pointer-events: none',
107
+ 'z-index: 2147483646',
108
+ 'border-radius: 50%',
109
+ 'border: 3px solid rgba(255, 50, 50, 0.7)',
110
+ 'width: 10px',
111
+ 'height: 10px',
112
+ 'left: ' + x + 'px',
113
+ 'top: ' + y + 'px',
114
+ 'transform: translate(-50%, -50%) scale(1)',
115
+ 'opacity: 1',
116
+ 'transition: transform 0.5s ease-out, opacity 0.5s ease-out',
117
+ ].join(';');
118
+ document.documentElement.appendChild(ripple);
119
+ requestAnimationFrame(() => {
120
+ ripple.style.transform = 'translate(-50%, -50%) scale(5)';
121
+ ripple.style.opacity = '0';
122
+ });
123
+ setTimeout(() => ripple.remove(), 600);
124
+ }
125
+
126
+ for (const evt of ['mousemove', 'pointermove']) {
127
+ document.addEventListener(evt, (e) => showCursor(e.clientX, e.clientY), true);
128
+ window.addEventListener(evt, (e) => showCursor(e.clientX, e.clientY), true);
129
+ }
130
+ for (const evt of ['mousedown', 'pointerdown', 'click']) {
131
+ document.addEventListener(evt, (e) => {
132
+ showCursor(e.clientX, e.clientY);
133
+ spawnRipple(e.clientX, e.clientY);
134
+ }, true);
135
+ window.addEventListener(evt, (e) => {
136
+ showCursor(e.clientX, e.clientY);
137
+ spawnRipple(e.clientX, e.clientY);
138
+ }, true);
139
+ }
140
+ }
141
+
142
+ if (document.readyState === 'loading') {
143
+ document.addEventListener('DOMContentLoaded', init);
144
+ } else {
145
+ init();
146
+ }
147
+ })();
148
+ `;
149
+ async function captureVisual(options, runDir, filePrefix, config) {
150
+ const label = options.label ?? "recording";
151
+ const testFile = options.testFile;
152
+ const testDir = dirname(testFile);
153
+ const configCandidates = [
154
+ join2(testDir, "playwright.config.ts"),
155
+ join2(testDir, "playwright.config.js"),
156
+ join2(testDir, "playwright.config.mjs")
157
+ ];
158
+ const existingConfig = configCandidates.find((c) => existsSync2(c));
159
+ const testResultsDir = join2(runDir, "pw-results");
160
+ await mkdir(testResultsDir, { recursive: true });
161
+ const testArgs = [
162
+ "playwright",
163
+ "test",
164
+ testFile,
165
+ "--reporter=list",
166
+ "--output",
167
+ testResultsDir
168
+ ];
169
+ if (options.testName) {
170
+ testArgs.push("-g", options.testName);
171
+ }
172
+ if (existingConfig) {
173
+ testArgs.push("--config", existingConfig);
174
+ }
175
+ const env = {
176
+ ...process.env,
177
+ PWDEBUG: "0",
178
+ PLAYWRIGHT_VIDEO: "on"
179
+ };
180
+ let exitCode = null;
181
+ let stderr = "";
182
+ await new Promise((resolve) => {
183
+ const proc = spawn2("npx", testArgs, {
184
+ stdio: "pipe",
185
+ env,
186
+ cwd: testDir
187
+ });
188
+ proc.stderr.on("data", (chunk) => {
189
+ stderr += chunk.toString();
190
+ });
191
+ proc.on("close", (code) => {
192
+ exitCode = code;
193
+ resolve();
194
+ });
195
+ proc.on("error", (err) => {
196
+ throw new Error(`Failed to run Playwright: ${err.message}. Is @playwright/test installed?`);
197
+ });
198
+ });
199
+ const videoFile = await findVideo(testResultsDir);
200
+ if (!videoFile) {
201
+ const hint = stderr.trim() ? `
202
+ Playwright output:
203
+ ${stderr.trim().split(`
204
+ `).slice(-5).join(`
205
+ `)}` : "";
206
+ throw new Error(`No video file found after Playwright test run. Check that video: 'on' is set in playwright config.${hint}`);
207
+ }
208
+ const ext = videoFile.endsWith(".webm") ? ".webm" : ".mp4";
209
+ const finalPath = join2(runDir, `${filePrefix}${ext}`);
210
+ await copyFile(videoFile, finalPath);
211
+ await rm(testResultsDir, { recursive: true, force: true });
212
+ const duration = await getVideoDuration(finalPath);
213
+ return {
214
+ path: finalPath,
215
+ mode: "browser",
216
+ duration,
217
+ label
218
+ };
219
+ }
220
+ async function findVideo(resultsDir) {
221
+ if (!existsSync2(resultsDir))
222
+ return null;
223
+ const entries = await readdir(resultsDir, { withFileTypes: true });
224
+ for (const entry of entries) {
225
+ if (!entry.isDirectory() && (entry.name.endsWith(".webm") || entry.name.endsWith(".mp4"))) {
226
+ return join2(resultsDir, entry.name);
227
+ }
228
+ }
229
+ for (const entry of entries) {
230
+ if (!entry.isDirectory())
231
+ continue;
232
+ const subDir = join2(resultsDir, entry.name);
233
+ const files = await readdir(subDir);
234
+ const video = files.find((f) => f.endsWith(".webm") || f.endsWith(".mp4"));
235
+ if (video)
236
+ return join2(subDir, video);
237
+ }
238
+ return null;
239
+ }
240
+ function getCursorHighlightScript() {
241
+ return CURSOR_HIGHLIGHT_SCRIPT;
242
+ }
243
+
244
+ // src/modes/terminal.ts
245
+ import { spawn as spawn3 } from "child_process";
246
+ import { writeFile } from "fs/promises";
247
+ import { join as join3 } from "path";
248
+ async function captureTerminal(options, runDir, filePrefix, command, config) {
249
+ const label = options.label ?? "terminal";
250
+ const castPath = join3(runDir, `${filePrefix}.cast`);
251
+ const htmlPath = join3(runDir, `${filePrefix}.html`);
252
+ const cols = config.cols ?? 120;
253
+ const rows = config.rows ?? 30;
254
+ const events = [];
255
+ const startTime = Date.now();
256
+ let exitCode = null;
257
+ await new Promise((resolve) => {
258
+ const proc = spawn3("/bin/sh", ["-c", command], {
259
+ stdio: ["ignore", "pipe", "pipe"],
260
+ env: {
261
+ ...process.env,
262
+ COLUMNS: String(cols),
263
+ LINES: String(rows),
264
+ FORCE_COLOR: "1",
265
+ TERM: "xterm-256color",
266
+ NO_COLOR: undefined
267
+ }
268
+ });
269
+ proc.stdout.on("data", (chunk) => {
270
+ events.push({
271
+ time: (Date.now() - startTime) / 1000,
272
+ data: chunk.toString()
273
+ });
274
+ });
275
+ proc.stderr.on("data", (chunk) => {
276
+ events.push({
277
+ time: (Date.now() - startTime) / 1000,
278
+ data: chunk.toString()
279
+ });
280
+ });
281
+ proc.on("close", (code) => {
282
+ exitCode = code;
283
+ resolve();
284
+ });
285
+ proc.on("error", (err) => {
286
+ events.push({
287
+ time: (Date.now() - startTime) / 1000,
288
+ data: `\x1B[31mError: ${err.message}\x1B[0m
289
+ `
290
+ });
291
+ resolve();
292
+ });
293
+ });
294
+ const duration = Date.now() - startTime;
295
+ const realDurationSec = duration / 1000;
296
+ const initialSpeed = realDurationSec < 0.2 ? 0.1 : realDurationSec < 0.5 ? 0.25 : realDurationSec < 1 ? 0.5 : realDurationSec < 2 ? 0.5 : 1;
297
+ const header = JSON.stringify({
298
+ version: 2,
299
+ width: cols,
300
+ height: rows,
301
+ timestamp: Math.floor(startTime / 1000),
302
+ env: { SHELL: "/bin/sh", TERM: "xterm-256color" }
303
+ });
304
+ const castLines = [header];
305
+ for (const evt of events) {
306
+ castLines.push(JSON.stringify([evt.time, "o", evt.data]));
307
+ }
308
+ await writeFile(castPath, castLines.join(`
309
+ `) + `
310
+ `, "utf-8");
311
+ const castDataJson = JSON.stringify(events);
312
+ const html = buildPlayerHtml(label, cols, rows, castDataJson, realDurationSec, initialSpeed);
313
+ await writeFile(htmlPath, html, "utf-8");
314
+ return {
315
+ path: htmlPath,
316
+ mode: "terminal",
317
+ duration,
318
+ label
319
+ };
320
+ }
321
+ function buildPlayerHtml(label, cols, rows, castDataJson, durationSec, initialSpeed) {
322
+ const speeds = [0.1, 0.25, 0.5, 1, 1.25, 1.5, 1.75, 2, 4];
323
+ const speedOptions = speeds.map((s) => `<option value="${s}"${s === initialSpeed ? " selected" : ""}>${s}x</option>`).join("");
324
+ return `<!DOCTYPE html>
325
+ <html>
326
+ <head>
327
+ <meta charset="utf-8">
328
+ <title>${label} — proof terminal recording</title>
329
+ <style>
330
+ * { box-sizing: border-box; margin: 0; padding: 0; }
331
+ html, body { margin: 0; height: 100%; }
332
+ body { background: transparent; -webkit-font-smoothing: antialiased; font-family: system-ui, sans-serif; }
333
+ .player {
334
+ background: #0d1117;
335
+ overflow: hidden;
336
+ width: 100%;
337
+ height: 100%;
338
+ }
339
+ #terminal {
340
+ height: calc(100% - 40px);
341
+ padding: 14px;
342
+ font-family: ui-monospace, monospace;
343
+ font-size: 12px;
344
+ line-height: 1.4;
345
+ color: #fff;
346
+ white-space: pre-wrap;
347
+ word-wrap: break-word;
348
+ overflow-y: auto;
349
+ opacity: .9;
350
+ }
351
+ .controls {
352
+ padding: 6px 10px;
353
+ border-top: 1px solid #30363d;
354
+ display: flex;
355
+ align-items: center;
356
+ gap: 10px;
357
+ height: 40px;
358
+ }
359
+ .btn {
360
+ background: none;
361
+ border: 1px solid #30363d;
362
+ color: #e6edf3;
363
+ padding: 4px 12px;
364
+ border-radius: 4px;
365
+ cursor: pointer;
366
+ font-size: 13px;
367
+ font-family: system-ui;
368
+ }
369
+ .btn:hover { background: #21262d; }
370
+ .speed-select {
371
+ background: #0d1117;
372
+ border: 1px solid #30363d;
373
+ color: #e6edf3;
374
+ padding: 4px 8px;
375
+ border-radius: 4px;
376
+ font-size: 13px;
377
+ font-family: system-ui;
378
+ cursor: pointer;
379
+ }
380
+ .speed-select:hover { background: #21262d; }
381
+ .progress {
382
+ flex: 1;
383
+ height: 4px;
384
+ background: #21262d;
385
+ border-radius: 2px;
386
+ overflow: hidden;
387
+ cursor: pointer;
388
+ }
389
+ .progress-bar {
390
+ height: 100%;
391
+ background: #58a6ff;
392
+ width: 0%;
393
+ transition: width 0.1s linear;
394
+ }
395
+ .time { color: #8b949e; font-size: 12px; font-family: monospace; min-width: 40px; text-align: right; line-height: 27px; }
396
+ </style>
397
+ </head>
398
+ <body>
399
+ <div class="player">
400
+ <div id="terminal"></div>
401
+ <div class="controls">
402
+ <button class="btn" id="playBtn" onclick="toggle()">Play</button>
403
+ <select class="speed-select" id="speedSelect" onchange="changeSpeed(this.value)">${speedOptions}</select>
404
+ <div class="progress" onclick="seek(event)"><div class="progress-bar" id="bar"></div></div>
405
+ <span class="time" id="time">0.0s / ${durationSec < 1 ? (durationSec * 1000).toFixed(0) + "ms" : durationSec.toFixed(1) + "s"}</span>
406
+ </div>
407
+ </div>
408
+ <script>
409
+ const events = ${castDataJson};
410
+ const totalDuration = ${durationSec.toFixed(3)};
411
+ const realDuration = ${durationSec.toFixed(3)};
412
+ const term = document.getElementById('terminal');
413
+ const bar = document.getElementById('bar');
414
+ const timeEl = document.getElementById('time');
415
+ const playBtn = document.getElementById('playBtn');
416
+
417
+ let playing = false;
418
+ let currentIdx = 0;
419
+ let startTs = 0;
420
+ let pausedAt = 0;
421
+ let speed = ${initialSpeed};
422
+ let raf = null;
423
+
424
+ // Convert ANSI to styled HTML spans
425
+ function ansiToHtml(str) {
426
+ const colors = ['#0d1117','#ff7b72','#3fb950','#d29922','#58a6ff','#bc8cff','#39d2e0','#e6edf3'];
427
+ const brights = ['#484f58','#ffa198','#56d364','#e3b341','#79c0ff','#d2a8ff','#56d4dd','#f0f6fc'];
428
+ let out = '';
429
+ let i = 0;
430
+ while (i < str.length) {
431
+ if (str[i] === '\\x1b' || str.charCodeAt(i) === 0x1b) {
432
+ const m = str.slice(i).match(/^(?:\\x1b|\\u001b|\\033|\\x1B)\\[([0-9;]*)m/);
433
+ if (m) {
434
+ const codes = m[1].split(';').map(Number);
435
+ let style = '';
436
+ for (const c of codes) {
437
+ if (c === 0) style += 'color:#e6edf3;font-weight:normal;font-style:normal;text-decoration:none;';
438
+ else if (c === 1) style += 'font-weight:bold;';
439
+ else if (c === 2) style += 'opacity:0.6;';
440
+ else if (c === 3) style += 'font-style:italic;';
441
+ else if (c === 4) style += 'text-decoration:underline;';
442
+ else if (c >= 30 && c <= 37) style += 'color:' + colors[c - 30] + ';';
443
+ else if (c >= 90 && c <= 97) style += 'color:' + brights[c - 90] + ';';
444
+ }
445
+ if (style) out += '</span><span style="' + style + '">';
446
+ i += m[0].length;
447
+ continue;
448
+ }
449
+ }
450
+ const ch = str[i];
451
+ if (ch === '<') out += '&lt;';
452
+ else if (ch === '>') out += '&gt;';
453
+ else if (ch === '&') out += '&amp;';
454
+ else out += ch;
455
+ i++;
456
+ }
457
+ return out;
458
+ }
459
+
460
+ function renderUpTo(elapsed) {
461
+ let html = '<span>';
462
+ for (let j = 0; j < events.length; j++) {
463
+ if (events[j].time > elapsed) { currentIdx = j; break; }
464
+ html += ansiToHtml(events[j].data);
465
+ if (j === events.length - 1) currentIdx = events.length;
466
+ }
467
+ html += '</span>';
468
+ term.innerHTML = html;
469
+ term.scrollTop = term.scrollHeight;
470
+ const pct = totalDuration > 0 ? Math.min(100, (elapsed / totalDuration) * 100) : 100;
471
+ bar.style.width = pct + '%';
472
+ timeEl.textContent = elapsed.toFixed(1) + 's';
473
+ }
474
+
475
+ function tick() {
476
+ if (!playing) return;
477
+ const elapsed = ((Date.now() - startTs) / 1000) * speed;
478
+ if (elapsed >= totalDuration) {
479
+ renderUpTo(totalDuration);
480
+ playing = false;
481
+ playBtn.textContent = 'Replay';
482
+ return;
483
+ }
484
+ renderUpTo(elapsed);
485
+ raf = requestAnimationFrame(tick);
486
+ }
487
+
488
+ function toggle() {
489
+ if (playing) {
490
+ playing = false;
491
+ pausedAt = ((Date.now() - startTs) / 1000) * speed;
492
+ playBtn.textContent = 'Play';
493
+ if (raf) cancelAnimationFrame(raf);
494
+ } else {
495
+ if (currentIdx >= events.length) {
496
+ pausedAt = 0;
497
+ term.innerHTML = '';
498
+ currentIdx = 0;
499
+ }
500
+ playing = true;
501
+ playBtn.textContent = 'Pause';
502
+ startTs = Date.now() - (pausedAt / speed) * 1000;
503
+ tick();
504
+ }
505
+ }
506
+
507
+ function changeSpeed(val) {
508
+ if (playing) pausedAt = ((Date.now() - startTs) / 1000) * speed;
509
+ speed = parseFloat(val);
510
+ if (playing) {
511
+ startTs = Date.now() - (pausedAt / speed) * 1000;
512
+ }
513
+ }
514
+
515
+ function seek(e) {
516
+ const rect = e.currentTarget.getBoundingClientRect();
517
+ const pct = (e.clientX - rect.left) / rect.width;
518
+ pausedAt = pct * totalDuration;
519
+ renderUpTo(pausedAt);
520
+ if (playing) {
521
+ startTs = Date.now() - (pausedAt / speed) * 1000;
522
+ }
523
+ }
524
+
525
+ // Render first frame
526
+ renderUpTo(0);
527
+ </script>
528
+ </body>
529
+ </html>`;
530
+ }
531
+
532
+ // src/report.ts
533
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
534
+ import { join as join4, basename } from "path";
535
+ async function generateReport(manifest, runDir, format) {
536
+ switch (format) {
537
+ case "md":
538
+ return generateMd(manifest, runDir);
539
+ case "html":
540
+ return generateHtml(manifest, runDir, false);
541
+ case "archive":
542
+ return generateHtml(manifest, runDir, true);
543
+ }
544
+ }
545
+ function formatTime(iso) {
546
+ return new Date(iso).toLocaleTimeString("en-US", { hour12: false });
547
+ }
548
+ function formatDate(iso) {
549
+ return new Date(iso).toLocaleDateString("en-US", {
550
+ year: "numeric",
551
+ month: "long",
552
+ day: "numeric"
553
+ });
554
+ }
555
+ function formatDuration(ms) {
556
+ if (ms < 1000)
557
+ return `${ms}ms`;
558
+ return `${(ms / 1000).toFixed(1)}s`;
559
+ }
560
+ function modeIcon(mode) {
561
+ return mode === "browser" ? "\uD83C\uDF10" : "\uD83D\uDDA5";
562
+ }
563
+ function generateMd(manifest, runDir) {
564
+ const lines = [];
565
+ const totalDuration = manifest.entries.reduce((sum, e) => sum + e.duration, 0);
566
+ lines.push(`# Proof Report`);
567
+ lines.push(``);
568
+ lines.push(`**App:** ${manifest.appName}`);
569
+ lines.push(`**Run:** ${manifest.run}`);
570
+ lines.push(`**Date:** ${formatDate(manifest.createdAt)}`);
571
+ lines.push(`**Entries:** ${manifest.entries.length}`);
572
+ if (manifest.description) {
573
+ lines.push(``);
574
+ lines.push(manifest.description);
575
+ }
576
+ lines.push(``);
577
+ lines.push(`| # | Label | Mode | Duration | Artifact |`);
578
+ lines.push(`|---|-------|------|----------|----------|`);
579
+ manifest.entries.forEach((entry, i) => {
580
+ const label = entry.label ?? entry.mode;
581
+ lines.push(`| ${i + 1} | ${modeIcon(entry.mode)} ${label} | ${entry.mode} | ${formatDuration(entry.duration)} | [${entry.artifact}](./${entry.artifact}) |`);
582
+ });
583
+ lines.push(``);
584
+ lines.push(`---`);
585
+ lines.push(``);
586
+ for (const entry of manifest.entries) {
587
+ lines.push(`### ${modeIcon(entry.mode)} ${entry.label ?? entry.mode}`);
588
+ lines.push(``);
589
+ lines.push(`${entry.description}`);
590
+ lines.push(``);
591
+ lines.push(`- **Time:** ${formatTime(entry.timestamp)}`);
592
+ lines.push(`- **Duration:** ${formatDuration(entry.duration)}`);
593
+ if (entry.command)
594
+ lines.push(`- **Command:** \`${entry.command}\``);
595
+ if (entry.testFile)
596
+ lines.push(`- **Test file:** \`${basename(entry.testFile)}\``);
597
+ if (entry.testName)
598
+ lines.push(`- **Test name:** ${entry.testName}`);
599
+ lines.push(`- **Artifact:** [${entry.artifact}](./${entry.artifact})`);
600
+ lines.push(``);
601
+ }
602
+ lines.push(`---`);
603
+ lines.push(`*Generated by [@automaze/proof](https://www.npmjs.com/package/@automaze/proof)*`);
604
+ const md = lines.join(`
605
+ `);
606
+ const outPath = join4(runDir, "report.md");
607
+ return writeFile2(outPath, md, "utf-8").then(() => outPath);
608
+ }
609
+ async function generateHtml(manifest, runDir, inline) {
610
+ const totalDuration = manifest.entries.reduce((sum, e) => sum + e.duration, 0);
611
+ const terminalCount = manifest.entries.filter((e) => e.mode === "terminal").length;
612
+ const browserCount = manifest.entries.filter((e) => e.mode === "browser").length;
613
+ const sections = [];
614
+ for (const entry of manifest.entries) {
615
+ const mediaHtml = await buildMediaEmbed(entry, runDir, inline);
616
+ const label = entry.label ?? entry.mode;
617
+ const isTerminal = entry.mode === "terminal";
618
+ const badgeClass = isTerminal ? "text-green-800 bg-emerald-100/80" : "text-blue-800 bg-sky-100";
619
+ const modeLabel = isTerminal ? "Terminal" : "Browser";
620
+ let sourceHtml = "";
621
+ if (entry.command) {
622
+ sourceHtml = `
623
+ <p class="font-medium text-sm mt-4 mb-1">Command:</p>
624
+ <div class="font-mono text-xs border px-1 rounded bg-neutral-50 border-neutral-200 subpixel-antialiased w-fit">${esc(entry.command)}</div>`;
625
+ } else if (entry.testFile) {
626
+ sourceHtml = `
627
+ <p class="font-medium text-sm mt-4 mb-1">Test:</p>
628
+ <div class="font-mono text-xs border px-1 rounded bg-neutral-50 border-neutral-200 subpixel-antialiased w-fit">${esc(basename(entry.testFile))}</div>`;
629
+ }
630
+ sections.push(`
631
+ <section class="mt-12 grid-cols-12 gap-x-6 border-t border-neutral-200 pt-12 md:grid">
632
+ <div class="col-span-5">
633
+ <h2 class="text-2xl mb-2">${esc(label)}</h2>
634
+ <div class="flex items-center space-x-4 subpixel-antialiased">
635
+ <span class="rounded ${badgeClass} px-2 py-0.5 text-sm">${modeLabel}</span>
636
+ <time class="font-mono text-[13px]">${formatTime(entry.timestamp)} (${formatDuration(entry.duration)})</time>
637
+ </div>
638
+ <p class="opacity-80 my-4">${esc(entry.description)}</p>
639
+ </div>
640
+ <div class="col-span-7">
641
+ ${mediaHtml}
642
+ ${sourceHtml}
643
+ </div>
644
+ </section>`);
645
+ }
646
+ const descriptionHtml = manifest.description ? `<p class="max-w-2xl leading-relaxed opacity-80">${esc(manifest.description)}</p>` : "";
647
+ const badgesHtml = [
648
+ terminalCount > 0 ? `<span class="whitespace-nowrap rounded font-medium text-green-800 bg-emerald-100/80 px-2 py-0.5 text-sm">${terminalCount} Terminal</span>` : "",
649
+ browserCount > 0 ? `<span class="whitespace-nowrap rounded font-medium text-blue-800 bg-sky-100 px-2 py-0.5 text-sm">${browserCount} Browser</span>` : ""
650
+ ].filter(Boolean).join(`
651
+ `);
652
+ const tailwindScript = inline ? `<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>` : `<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>`;
653
+ const html = `<!DOCTYPE html>
654
+ <html lang="en">
655
+ <head>
656
+ <meta charset="utf-8">
657
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
658
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover">
659
+ <meta http-equiv="cleartype" content="on">
660
+ <title>Proof Report - ${esc(manifest.run)}</title>
661
+ ${tailwindScript}
662
+ </head>
663
+ <body>
664
+
665
+ <div class="container mx-auto px-6 my-14 w-full max-w-5xl antialiased">
666
+ <header>
667
+ <div class="flex items-center space-x-4 mb-6">
668
+ <h1 class="text-4xl leading-none font-medium">Proof report</h1>
669
+ <span class="mt-2 rounded bg-neutral-100 px-2 py-1 font-mono text-sm leading-none">${esc(manifest.run)}</span>
670
+ </div>
671
+
672
+ ${descriptionHtml}
673
+
674
+ <div class="mt-6 md:flex md:items-center md:space-x-4 leading-none">
675
+ <span class="block mb-2 md:inline md:m-0">${formatDate(manifest.createdAt)} &middot; ${formatTime(manifest.createdAt)} (${formatDuration(totalDuration)})</span>
676
+ ${badgesHtml}
677
+ </div>
678
+
679
+ <p class="my-6">Generated by <a href="https://www.npmjs.com/package/@automaze/proof" class="font-medium text-blue-600">@automaze/proof</a></p>
680
+ </header>
681
+
682
+ <main>
683
+ ${sections.join(`
684
+ `)}
685
+ </main>
686
+
687
+ <footer></footer>
688
+ </div>
689
+
690
+ </body>
691
+ </html>`;
692
+ const ext = inline ? "archive.html" : "report.html";
693
+ const outPath = join4(runDir, ext);
694
+ await writeFile2(outPath, html, "utf-8");
695
+ return outPath;
696
+ }
697
+ async function buildMediaEmbed(entry, runDir, inline) {
698
+ const artifactPath = join4(runDir, entry.artifact);
699
+ if (entry.mode === "browser") {
700
+ if (inline) {
701
+ const videoData = await readFile2(artifactPath);
702
+ const ext = entry.artifact.endsWith(".mp4") ? "mp4" : "webm";
703
+ const b64 = videoData.toString("base64");
704
+ return `<video class="bg-black w-full shadow-xs rounded-md aspect-3/2" controls muted loop src="data:video/${ext};base64,${b64}"></video>`;
705
+ }
706
+ return `<video class="bg-neutral-200 w-full rounded-md aspect-3/2" controls muted loop src="./${esc(entry.artifact)}"></video>`;
707
+ }
708
+ if (entry.mode === "terminal") {
709
+ if (inline) {
710
+ const playerHtml = await readFile2(artifactPath, "utf-8");
711
+ return `<iframe class="w-full aspect-3/2 rounded-lg bg-black" allowtransparency="true" srcdoc="${escAttr(playerHtml)}"></iframe>`;
712
+ }
713
+ return `<iframe class="w-full aspect-3/2 rounded-lg bg-black" allowtransparency="true" src="./${esc(entry.artifact)}"></iframe>`;
714
+ }
715
+ return `<a href="./${esc(entry.artifact)}">${esc(entry.artifact)}</a>`;
716
+ }
717
+ function esc(s) {
718
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
719
+ }
720
+ function escAttr(s) {
721
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
722
+ }
723
+
724
+ // src/index.ts
725
+ class Proof {
726
+ config;
727
+ runDir;
728
+ runName;
729
+ initTime;
730
+ constructor(config) {
731
+ this.config = config;
732
+ this.initTime = new Date;
733
+ const root = config.proofDir ?? process.env.PROOF_DIR ?? join5(tmpdir(), "proof");
734
+ const dateStr = this.formatDate(this.initTime);
735
+ this.runName = config.run ?? this.formatTime(this.initTime);
736
+ this.runDir = join5(root, config.appName, dateStr, this.runName);
737
+ }
738
+ formatDate(d) {
739
+ const pad = (n) => String(n).padStart(2, "0");
740
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
741
+ }
742
+ formatTime(d) {
743
+ const pad = (n) => String(n).padStart(2, "0");
744
+ return `${pad(d.getHours())}${pad(d.getMinutes())}`;
745
+ }
746
+ formatTimestamp(d) {
747
+ const pad = (n) => String(n).padStart(2, "0");
748
+ return `${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
749
+ }
750
+ async ensureRunDir() {
751
+ await mkdir2(this.runDir, { recursive: true });
752
+ return this.runDir;
753
+ }
754
+ async resolveMode(optMode) {
755
+ const envMode = process.env.PROOF_MODE;
756
+ const mode = optMode ?? envMode ?? "auto";
757
+ if (mode !== "auto")
758
+ return mode;
759
+ return detectMode(process.cwd());
760
+ }
761
+ async appendToManifest(entry) {
762
+ const manifestPath = join5(this.runDir, "proof.json");
763
+ let manifest;
764
+ if (existsSync3(manifestPath)) {
765
+ const raw = await readFile3(manifestPath, "utf-8");
766
+ manifest = JSON.parse(raw);
767
+ manifest.entries.push(entry);
768
+ } else {
769
+ manifest = {
770
+ version: 1,
771
+ appName: this.config.appName,
772
+ description: this.config.description,
773
+ run: this.runName,
774
+ createdAt: this.initTime.toISOString(),
775
+ entries: [entry]
776
+ };
777
+ }
778
+ await writeFile3(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
779
+ }
780
+ async capture(options) {
781
+ const runDir = await this.ensureRunDir();
782
+ const mode = await this.resolveMode(options.mode);
783
+ const ts = this.formatTimestamp(new Date);
784
+ const label = options.label ?? mode;
785
+ const filePrefix = `${label}-${ts}`;
786
+ let recording;
787
+ switch (mode) {
788
+ case "browser": {
789
+ if (!options.testFile) {
790
+ throw new Error("browser mode requires testFile");
791
+ }
792
+ recording = await captureVisual(options, runDir, filePrefix, {
793
+ viewport: this.config.browser?.viewport
794
+ });
795
+ break;
796
+ }
797
+ case "terminal": {
798
+ if (!options.command) {
799
+ throw new Error("terminal mode requires command");
800
+ }
801
+ recording = await captureTerminal(options, runDir, filePrefix, options.command, this.config.terminal ?? {});
802
+ break;
803
+ }
804
+ }
805
+ const source = options.testFile ? basename2(options.testFile) : options.command ?? mode;
806
+ const fallbackDescriptions = {
807
+ browser: `Playwright browser test recording of ${source}${options.testName ? ` — test: "${options.testName}"` : ""}`,
808
+ terminal: `Terminal capture: ${source}`
809
+ };
810
+ const entry = {
811
+ timestamp: new Date().toISOString(),
812
+ mode,
813
+ label,
814
+ command: options.command,
815
+ testFile: options.testFile,
816
+ testName: options.testName,
817
+ duration: recording.duration,
818
+ artifact: basename2(recording.path),
819
+ description: options.description ?? fallbackDescriptions[mode]
820
+ };
821
+ await this.appendToManifest(entry);
822
+ return recording;
823
+ }
824
+ async report(options) {
825
+ const manifestPath = join5(this.runDir, "proof.json");
826
+ if (!existsSync3(manifestPath)) {
827
+ throw new Error("No proof.json found — run capture() first");
828
+ }
829
+ const manifest = JSON.parse(await readFile3(manifestPath, "utf-8"));
830
+ const formatInput = options?.format ?? "md";
831
+ const formats = Array.isArray(formatInput) ? formatInput : [formatInput];
832
+ const paths = [];
833
+ for (const format of formats) {
834
+ const path = await generateReport(manifest, this.runDir, format);
835
+ paths.push(path);
836
+ }
837
+ return Array.isArray(formatInput) ? paths : paths[0];
838
+ }
839
+ }
840
+ export {
841
+ getCursorHighlightScript,
842
+ Proof
843
+ };