1// Screenshot a page with phone emulation over CDP.2//3// npm run dev:shot <url> <out.png> [width] [height] [--nostr] [--eval <js>]4//5// --nostr signs in as the dev signer's key (scripts/dev/dev-signer.mjs must be6// running), so the owner's console is in the picture; --eval runs a snippet7// after the page has loaded, such as opening a fold or a tab, before the shot.8import { spawn } from "node:child_process";9import { writeFileSync } from "node:fs";10import WebSocket from "ws";11const args = process.argv.slice(2);12const flag = (name) => { const i = args.indexOf(name); if (i < 0) return null; const [, v] = args.splice(i, name === "--nostr" ? 1 : 2); return v ?? true; };13const nostr = flag("--nostr");14const evalJS = flag("--eval");15const [url, out, w = "390", h = "1600"] = args;16// The dev signer's key, never real: new Uint8Array(32).fill(7).17const DEV_PUBKEY = "989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f";18const NOSTR = `localStorage.setItem("me", "${DEV_PUBKEY}"); window.nostr = { getPublicKey: async () => (await (await fetch("http://127.0.0.1:9999/pk")).text()), signEvent: async (e) => (await fetch("http://127.0.0.1:9999/sign", { method: "POST", body: JSON.stringify(e) })).json() };`;19const bin = "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser";20const port = 9333;21const br = spawn(bin, ["--headless=new", "--disable-gpu", "--no-first-run", `--remote-debugging-port=${port}`, "--user-data-dir=/tmp/brave-cdp", "about:blank"], { stdio: "ignore" });22const sleep = (ms) => new Promise((r) => setTimeout(r, ms));23let targets;24for (let i = 0; i < 40; i++) { try { targets = await (await fetch(`http://127.0.0.1:${port}/json`)).json(); break; } catch { await sleep(250); } }25const page = targets.find((t) => t.type === "page");26const ws = new WebSocket(page.webSocketDebuggerUrl);27await new Promise((r) => ws.on("open", r));28let id = 0; const pending = new Map();29ws.on("message", (m) => { const j = JSON.parse(m); if (j.id && pending.has(j.id)) { pending.get(j.id)(j); pending.delete(j.id); } });30const call = (method, params = {}) => new Promise((res) => { const i = ++id; pending.set(i, res); ws.send(JSON.stringify({ id: i, method, params })); });31await call("Emulation.setDeviceMetricsOverride", { width: +w, height: +h, deviceScaleFactor: 2, mobile: true });32await call("Page.enable");33if (nostr) await call("Page.addScriptToEvaluateOnNewDocument", { source: NOSTR });34await call("Page.navigate", { url });35await sleep(3500);36if (evalJS) { await call("Runtime.evaluate", { expression: evalJS, awaitPromise: true }); await sleep(1500); }37const { result } = await call("Runtime.evaluate", { expression: "JSON.stringify({sw:document.documentElement.scrollWidth, cw:document.documentElement.clientWidth, sh:document.documentElement.scrollHeight})" });38console.log("layout", result.result.value);39const shot = await call("Page.captureScreenshot", { format: "png", captureBeyondViewport: false });40writeFileSync(out, Buffer.from(shot.result.data, "base64"));41ws.close(); br.kill();