#!/usr/bin/env node // Regenerates docs/assets/headroom-demo.svg: an animated terminal showing the // flow an agent follows (ask `can`, get NO, ask the next class, get YES, run a // job under a reservation). It seeds a synthetic Headroom home (see // scripts/lib/demo-home.mjs), runs the real CLI against it and draws the // output verbatim, so the card cannot drift from what `headroom` prints. // Run `npm run build` first. // // The animation is plain CSS inside the SVG. A viewer that does not run it, or // that prefers reduced motion, shows the finished terminal: that is the // static fallback, there is no separate file. import { spawnSync } from "node:child_process"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { seedDemoHome } from "./lib/demo-home.mjs"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const home = mkdtempSync(join(tmpdir(), "headroom-demo-")); const STEPS = [ ["An agent asks before it starts a Codex build", ["can", "codex-build", "--owner", "builder"]], ["Codex is ahead of its weekly pace, so try Claude", ["can", "claude-heavy", "--owner", "builder"]], ["Reserve the capacity, run the job, release it", ["run", "--meter", "claude-main:all", "--need", "wk:10", "--owner", "builder", "--", "echo", "ok"]], ]; try { await seedDemoHome(home); const env = { ...process.env, HEADROOM_HOME: home, TZ: "UTC" }; const blocks = STEPS.map(([comment, args]) => { const run = spawnSync(process.execPath, [join(root, "bin", "headroom.js"), ...args], { env, encoding: "utf8", timeout: 20_000 }); const lines = run.stdout.split("\n"); // `can` prints the verdict first and repeats it per meter; keep the first line. const out = args[0] === "can" ? lines.find((text) => /^(YES|NO) /.test(text)) : lines.find((text) => text.trim()); if (!out) throw new Error(`no output from: headroom ${args.join(" ")}\n${run.stdout}${run.stderr}`); return { comment, command: `headroom ${args.join(" ")}`, out }; }); const esc = (text) => text.replace(/&/g, "&").replace(//g, ">"); const LH = 22, X = 28, WIDTH = 720, TOP = 146; const TOTAL = 17; // seconds per loop const pct = (seconds) => Math.round((seconds / TOTAL) * 10000) / 100; let y = TOP, t = 2.6; const body = [], css = []; let n = 0; const reveal = (id, start) => { css.push(`@keyframes ${id}{0%,${pct(start)}%{opacity:0}${pct(start + 0.15)}%,94%{opacity:1}98%,100%{opacity:0}}#${id}{animation:${id} ${TOTAL}s infinite}`); }; const type = (id, start, chars) => { const dur = Math.max(0.5, chars * 0.035); css.push(`@keyframes ${id}{0%,${pct(start)}%{width:0}${pct(start + dur)}%,94%{width:${WIDTH}px}98%,100%{width:0}}#${id}-clip rect{animation:${id} ${TOTAL}s infinite steps(${chars})}`); return start + dur; }; // Fuel gauge strip: each bar drains from full to what is left of the week, as // read from the real `can` output above (wk N% is the share used). const GAUGE_X = 236, GAUGE_W = 300; const fuel = [["codex-main:main", blocks[0].out, "#f0883e"], ["claude-main:all", blocks[1].out, "#3fb950"]].map(([name, line, color], i) => { const used = Number(/wk (\d+)%/.exec(line)?.[1]); const state = /\b(HARVEST|NORMAL|CONSERVE|FREEZE|UNKNOWN)\b/.exec(line)?.[1]; if (!Number.isFinite(used) || !state) throw new Error(`cannot read the week from: ${line}`); return { name, left: 100 - used, state, color, y: 66 + i * 26 }; }); fuel.forEach((row, i) => { const id = `f${i}`, lid = `fl${i}`, w = Math.round((GAUGE_W * row.left) / 100); css.push(`@keyframes ${id}{0%,2%{width:${GAUGE_W}px}10%,94%{width:${w}px}98%,100%{width:${GAUGE_W}px}}#${id}{animation:${id} ${TOTAL}s infinite cubic-bezier(.2,.7,.2,1)}`); reveal(lid, 1.8); body.push(`${row.name} wk`); body.push(``); body.push(``); body.push(`${row.left}% left ${row.state}`); }); for (const block of blocks) { const cid = `c${n++}`, kid = `k${n++}`, oid = `o${n++}`; reveal(cid, t); body.push(`# ${esc(block.comment)}`); y += LH; t += 0.5; const cmd = `$ ${block.command}`; const typed = type(kid, t, cmd.length); body.push(``); body.push(`$${esc(cmd.slice(1))}`); y += LH; t = typed + 0.35; const verdict = block.out.split(" ")[0]; const color = verdict === "YES" ? "#3fb950" : verdict === "NO" ? "#f85149" : "#e6edf3"; const rest = color === "#e6edf3" ? esc(block.out) : esc(block.out.slice(verdict.length)); reveal(oid, t); body.push(`${color === "#e6edf3" ? rest : `${verdict}${rest}`}`); y += LH + 14; t += 1.6; } if (t > TOTAL - 1) throw new Error(`timeline ${t}s does not fit the ${TOTAL}s loop`); const height = y + 4; // The clip rect needs its own animated width; give every clipPath rect the right selector. const svg = ` headroom can: NO for the meter that is ahead of pace, YES for the one with room headroom (synthetic data) ${body.join("\n ").replace(/ id=""/g, "")} `; const out = join(root, "docs", "assets", "headroom-demo.svg"); writeFileSync(out, svg); console.log(out); } finally { rmSync(home, { recursive: true, force: true }); }