// SPDX-License-Identifier: GPL-3.0-or-later /** * Render docs/screenshots/demo.gif — the real CLI report, revealed inside a * terminal window. * * Self-contained: runs `bin/doctor.js`, renders the output in a styled * terminal with Playwright's cached Chromium, captures one frame per step, and * assembles the GIF with ffmpeg. No VHS/ttyd needed. * * Usage: * node scripts/demo-gif.mjs # writes docs/screenshots/demo.gif * node scripts/demo-gif.mjs out.gif # custom output * CHROME_PATH=/path/to/chrome ... # override the browser */ import { spawnSync } from "node:child_process"; import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { chromium } from "playwright-core"; const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); const OUT = process.argv[2] || join(ROOT, "docs", "screenshots", "demo.gif"); const WIDTH = 1100; const HEIGHT = 720; const FPS = 12; const VISIBLE = 28; function findChromium() { if (process.env.CHROME_PATH) return process.env.CHROME_PATH; const base = join(process.env.HOME || "", ".cache", "ms-playwright"); if (existsSync(base)) { const dirs = readdirSync(base).filter((d) => d.startsWith("chromium-")).sort().reverse(); for (const d of dirs) { const p = join(base, d, "chrome-linux64", "chrome"); if (existsSync(p)) return p; } } throw new Error("no Chromium found — set CHROME_PATH or run `npx playwright install chromium`"); } function captureReport() { const dir = mkdtempSync(join(tmpdir(), "ld-demo-")); const history = join(dir, "history.json"); const env = { ...process.env, LINUX_DOCTOR_HISTORY: history }; const args = [join(ROOT, "bin", "doctor.js"), "--severity", "medium"]; const runOnce = () => { const r = spawnSync(process.execPath, args, { cwd: ROOT, env, encoding: "utf8", timeout: 180000 }); if (r.error) throw r.error; if (!r.stdout) throw new Error(`doctor produced no output (exit ${r.status})`); return r.stdout; }; runOnce(); const out = runOnce(); rmSync(dir, { recursive: true, force: true }); return out.replace(/\n$/, "").split("\n"); } const esc = (s) => s.replace(/&/g, "&").replace(//g, ">"); function lineHtml(l) { if (/^🩺/.test(l)) return `${esc(l)}`; if (/^=+$/.test(l)) return `${esc(l)}`; if (/^▶ START HERE/.test(l)) return `${esc(l)}`; if (/^🔴/.test(l)) return `${esc(l)}`; if (/^🟡/.test(l)) return `${esc(l)}`; if (/^\d+\. \[/.test(l)) return `${esc(l)}`; if (/^\s+Evidence:/.test(l)) return `${esc(l)}`; if (/^\s+\$ /.test(l)) return `${esc(l)}`; if (/^\s+How to fix:/.test(l)) return `${esc(l)}`; if (/^─+$/.test(l)) return `${esc(l)}`; if (/^(STATUS)/.test(l)) return esc(l) .replace(/(\d+) high/, '$1 high') .replace(/(\d+) medium/, '$1 medium') .replace(/(\d+) info/, '$1 info'); if (/^(System|Note):|^SCORE|^TREND|^SINCE LAST RUN|^Ran |^⚠|^Found |^✓/.test(l)) return `${esc(l)}`; if (/^Linux Doctor only|^Report bugs/.test(l)) return `${esc(l)}`; return esc(l); } const CMD = "linux-doctor"; const OUTPUT = captureReport(); const TYPE = CMD.length; const PAUSE = 3; const STEP = 2; const HOLD = 14; const OUT_FRAMES = Math.ceil(OUTPUT.length / STEP); const TOTAL = TYPE + PAUSE + OUT_FRAMES + HOLD; const html = `
linux-doctor — bash
`; const exe = findChromium(); const framesDir = mkdtempSync(join(tmpdir(), "ld-demo-frames-")); const browser = await chromium.launch({ executablePath: exe, headless: true, args: ["--no-sandbox"] }); try { const context = await browser.newContext({ viewport: { width: WIDTH, height: HEIGHT }, deviceScaleFactor: 2 }); const page = await context.newPage(); await page.setContent(html, { waitUntil: "load" }); const card = page.locator("#card"); for (let i = 0; i < TOTAL; i++) { await page.evaluate((n) => window.renderFrame(n), i); await card.screenshot({ path: join(framesDir, `${String(i).padStart(4, "0")}.png`) }); } } finally { await browser.close(); } console.log(`Captured ${TOTAL} frames (~${(TOTAL / FPS).toFixed(1)}s) — encoding GIF…`); const vf = `fps=${FPS},scale=950:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=96[p];[s1][p]paletteuse=dither=bayer:bayer_scale=5`; const res = spawnSync("ffmpeg", ["-y", "-loglevel", "error", "-framerate", String(FPS), "-i", join(framesDir, "%04d.png"), "-vf", vf, "-loop", "0", OUT], { stdio: "inherit" }); rmSync(framesDir, { recursive: true, force: true }); if (res.status !== 0) throw new Error("ffmpeg failed"); console.log(`Wrote ${OUT}`);