pi-web.dev
Fallback screenshot for ${escapeHtml(url)}.
#!/usr/bin/env node import { spawn, spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { existsSync } from "node:fs"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { createServer } from "node:net"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(SCRIPT_DIR, ".."); const DEFAULT_OUTPUT_DIR = join(REPO_ROOT, "docs", "assets"); const SESSION_ID = "019ef4c0-0000-7000-8000-000000000001"; const DEMO_FILE = "docs/assets/pi-web-dev-screenshot.png"; const DEFAULT_SITE_URL = "https://pi-web.dev/"; const VIEWPORTS = { desktop: { width: 1440, height: 900, mobile: false }, tablet: { width: 1024, height: 768, mobile: false }, mobile: { width: 390, height: 844, mobile: true }, }; const args = parseArgs(process.argv.slice(2)); const outputDir = resolve(args.outputDir ?? DEFAULT_OUTPUT_DIR); const keepTemp = args.keepTemp === true; const siteUrl = args.siteUrl ?? DEFAULT_SITE_URL; const chromeBin = args.chromeBin ?? process.env["CHROME_BIN"] ?? findChrome(); if (chromeBin === undefined) { fail("Chromium was not found. Install chromium-browser/chromium or set CHROME_BIN=/path/to/chrome."); } const tempRoot = await mkdtemp(join(tmpdir(), "pi-web-screenshots-")); const children = new Set(); let cleanedUp = false; process.once("SIGINT", () => { void cleanup().finally(() => process.exit(130)); }); process.once("SIGTERM", () => { void cleanup().finally(() => process.exit(143)); }); async function main() { const logsDir = join(tempRoot, "logs"); const dataDir = join(tempRoot, "pi-web-data"); const configPath = join(tempRoot, "config.json"); const sessionDir = join(tempRoot, "sessions"); const agentDir = join(tempRoot, "pi-agent"); const demoProject = join(tempRoot, "pi-web"); const projectsFile = join(dataDir, "projects.json"); const socketPath = join(dataDir, "sessiond.sock"); await Promise.all([ mkdir(logsDir, { recursive: true }), mkdir(sessionDir, { recursive: true }), mkdir(agentDir, { recursive: true }), mkdir(dataDir, { recursive: true }), mkdir(outputDir, { recursive: true }), ]); console.log(`Temporary workspace: ${tempRoot}`); await cloneDemoProject(demoProject); await removeLegacyDemoMedia(demoProject); const projectId = "pi-web-demo"; const workspaceId = createWorkspaceId(projectId, demoProject); await writeJson(projectsFile, { projects: [{ id: projectId, name: "pi-web", path: demoProject, createdAt: new Date().toISOString() }], }); await writeJson(configPath, { host: "127.0.0.1", allowedHosts: true }); await writeDemoSession(sessionDir, demoProject); const apiPort = await getFreePort(); const clientPort = await getFreePort(); const debugPort = await getFreePort(); const env = { ...process.env, PI_WEB_DATA_DIR: dataDir, PI_WEB_CONFIG: configPath, PI_WEB_PROJECTS_FILE: projectsFile, PI_WEB_SESSIOND_SOCKET: socketPath, PI_WEB_HOST: "127.0.0.1", PI_WEB_PORT: String(apiPort), PI_WEB_ALLOWED_HOSTS: "true", PI_CODING_AGENT_DIR: agentDir, PI_CODING_AGENT_SESSION_DIR: sessionDir, PI_OFFLINE: "1", NO_COLOR: "1", }; const tsxBin = join(REPO_ROOT, "node_modules", ".bin", process.platform === "win32" ? "tsx.cmd" : "tsx"); const viteBin = join(REPO_ROOT, "node_modules", ".bin", process.platform === "win32" ? "vite.cmd" : "vite"); assertExecutable(tsxBin, "Run npm install before capturing screenshots."); assertExecutable(viteBin, "Run npm install before capturing screenshots."); console.log("Starting isolated PI WEB session daemon, API server, and Vite client…"); startChild("sessiond", tsxBin, ["src/server/sessiond.ts"], { env, cwd: REPO_ROOT, logsDir }); await waitForFile(socketPath, 10_000); startChild("api", tsxBin, ["src/server/index.ts"], { env, cwd: REPO_ROOT, logsDir }); await waitForHttp(`http://127.0.0.1:${apiPort}/api/projects`, 15_000); startChild("vite", viteBin, ["--host", "127.0.0.1", "--port", String(clientPort), "--strictPort", "true"], { env, cwd: REPO_ROOT, logsDir }); await waitForHttp(`http://127.0.0.1:${clientPort}/`, 30_000); console.log("Starting Chromium and capturing screenshots…"); const chrome = startChild("chromium", chromeBin, chromeArgs(debugPort, join(tempRoot, "chrome-profile")), { env, cwd: REPO_ROOT, logsDir }); await waitForHttp(`http://127.0.0.1:${debugPort}/json/version`, 15_000); const cdp = await openPage(debugPort); try { await cdp.send("Page.enable"); await cdp.send("Runtime.enable"); await captureWebsiteScreenshot(cdp, join(demoProject, DEMO_FILE), siteUrl); const appUrl = new URL(`http://127.0.0.1:${clientPort}/`); appUrl.searchParams.set("project", projectId); appUrl.searchParams.set("workspace", workspaceId); appUrl.searchParams.set("session", SESSION_ID); appUrl.searchParams.set("view", "chat"); await captureDesktop(cdp, appUrl, join(outputDir, "pi-web-desktop.png")); await captureDefaultApp(cdp, appUrl, VIEWPORTS.tablet, join(outputDir, "pi-web-tablet.png")); await captureDefaultApp(cdp, appUrl, VIEWPORTS.mobile, join(outputDir, "pi-web-mobile.png")); } finally { cdp.close(); chrome.kill("SIGTERM"); } console.log(`Wrote ${join(outputDir, "pi-web-desktop.png")}`); console.log(`Wrote ${join(outputDir, "pi-web-tablet.png")}`); console.log(`Wrote ${join(outputDir, "pi-web-mobile.png")}`); if (keepTemp) console.log(`Kept temporary workspace: ${tempRoot}`); } async function captureWebsiteScreenshot(cdp, outputPath, url) { await mkdir(dirname(outputPath), { recursive: true }); await setViewport(cdp, { width: 1280, height: 720, mobile: false }); try { await navigate(cdp, url, 20_000); await waitForDocumentFonts(cdp); await sleep(3500); await capturePng(cdp, outputPath); } catch (error) { console.warn(`Unable to capture ${url}; using a local fallback image. ${error instanceof Error ? error.message : String(error)}`); const fallback = `data:text/html,${encodeURIComponent(fallbackWebsiteHtml(url))}`; await navigate(cdp, fallback, 10_000); await sleep(300); await capturePng(cdp, outputPath); } } async function captureDesktop(cdp, appUrl, outputPath) { await setViewport(cdp, VIEWPORTS.desktop); await navigate(cdp, appUrl.href, 15_000); await waitForApp(cdp); await selectPreviewImage(cdp); await sleep(500); await capturePng(cdp, outputPath); } async function captureDefaultApp(cdp, appUrl, viewport, outputPath) { await setViewport(cdp, viewport); await navigate(cdp, appUrl.href, 15_000); await waitForApp(cdp); await sleep(700); await capturePng(cdp, outputPath); } async function selectPreviewImage(cdp) { await evaluate(cdp, `(async () => { const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const app = document.querySelector("pi-web-app"); if (!app) throw new Error("pi-web-app not found"); if (typeof app.openWorkspaceTool !== "function" || app.files === undefined) { throw new Error("PI WEB app internals needed for deterministic screenshot setup were not available"); } app.openWorkspaceTool("core:workspace.files"); await app.updateComplete; await sleep(900); await app.files.refreshFiles(); await app.updateComplete; if (app.state?.expandedDirs?.["docs"] === undefined) await app.files.expandDir("docs"); await app.updateComplete; if (app.state?.expandedDirs?.["docs/assets"] === undefined) await app.files.expandDir("docs/assets"); await app.updateComplete; await app.files.selectFile(${JSON.stringify(DEMO_FILE)}); await app.updateComplete; const panel = app.shadowRoot?.querySelector("workspace-panel"); const root = panel?.shadowRoot; await panel?.updateComplete; const start = Date.now(); while (Date.now() - start < 8000) { const image = root?.querySelector(".image-preview img"); if (root?.textContent.includes(${JSON.stringify(DEMO_FILE)}) && image instanceof HTMLImageElement && image.complete) return true; await sleep(100); } throw new Error("Timed out waiting for image preview"); })()`); } async function waitForApp(cdp) { await evaluate(cdp, `new Promise((resolve, reject) => { const start = Date.now(); const visibleText = () => { const app = document.querySelector("pi-web-app"); const appRoot = app?.shadowRoot; const chatRoot = appRoot?.querySelector("chat-view")?.shadowRoot; return [appRoot?.textContent ?? "", chatRoot?.textContent ?? ""].join("\\n"); }; const check = () => { const text = visibleText(); if (text.includes("Showing messages") && text.includes("assistant")) { resolve(true); return; } if (Date.now() - start > 15000) { reject(new Error("PI WEB app did not restore the seeded session in time. Visible text: " + visibleText())); return; } setTimeout(check, 100); }; check(); })`); } async function waitForDocumentFonts(cdp) { try { await evaluate(cdp, `document.fonts?.ready?.then(() => true) ?? true`); } catch { // Font loading is best-effort; screenshots still work with fallback fonts. } } async function navigate(cdp, url, timeoutMs) { const loaded = cdp.waitForEvent("Page.loadEventFired", timeoutMs).catch(() => undefined); await cdp.send("Page.navigate", { url }); await loaded; } async function setViewport(cdp, viewport) { await cdp.send("Emulation.setDeviceMetricsOverride", { width: viewport.width, height: viewport.height, deviceScaleFactor: 1, mobile: viewport.mobile, }); } async function capturePng(cdp, outputPath) { await mkdir(dirname(outputPath), { recursive: true }); const { data } = await cdp.send("Page.captureScreenshot", { format: "png", fromSurface: true, captureBeyondViewport: false }); await writeFile(outputPath, Buffer.from(data, "base64")); } async function cloneDemoProject(target) { const result = spawnSync("git", ["clone", "--quiet", "--local", "--no-hardlinks", REPO_ROOT, target], { cwd: REPO_ROOT, encoding: "utf8", }); if (result.status !== 0) throw new Error(`git clone failed:\n${result.stderr || result.stdout}`); } async function removeLegacyDemoMedia(projectRoot) { await Promise.all([ rm(join(projectRoot, "docs", "assets", "pi-web-demo.gif"), { force: true }), rm(join(projectRoot, "docs", "assets", "pi-web-demo.webm"), { force: true }), rm(join(projectRoot, "docs", "assets", "pi-web-demo-flow.gif"), { force: true }), ]); } async function writeDemoSession(sessionDir, cwd) { const now = new Date(); const timestamp = now.toISOString(); const file = join(sessionDir, `${timestamp.replaceAll(":", "-")}_${SESSION_ID}.jsonl`); const ms = now.getTime(); const entries = [ { type: "session", version: 3, id: SESSION_ID, timestamp, cwd }, { type: "model_change", id: "10000001", parentId: null, timestamp: iso(ms + 100), provider: "openai-codex", modelId: "gpt-5.5" }, { type: "thinking_level_change", id: "10000002", parentId: "10000001", timestamp: iso(ms + 200), thinkingLevel: "off" }, { type: "message", id: "10000003", parentId: "10000002", timestamp: iso(ms + 1000), message: { role: "user", content: [{ type: "text", text: "Take a screenshot of https://pi-web.dev, save it under docs/assets, and tell me where I can preview it." }], timestamp: ms + 1000, }, }, { type: "session_info", id: "10000004", parentId: "10000003", timestamp: iso(ms + 1100), name: "Screenshot pi-web.dev" }, { type: "message", id: "10000005", parentId: "10000004", timestamp: iso(ms + 2000), message: { role: "assistant", content: [{ type: "toolCall", id: "call_demo_screenshot", name: "bash", arguments: { command: `capture-browser-screenshot https://pi-web.dev ${DEMO_FILE}` }, }], api: "openai-codex-responses", provider: "openai-codex", model: "gpt-5.5", usage: zeroUsage(), stopReason: "toolUse", timestamp: ms + 2000, }, }, { type: "message", id: "10000006", parentId: "10000005", timestamp: iso(ms + 3000), message: { role: "toolResult", toolCallId: "call_demo_screenshot", toolName: "bash", content: [{ type: "text", text: `Saved screenshot to ${DEMO_FILE}` }], isError: false, timestamp: ms + 3000, }, }, { type: "message", id: "10000007", parentId: "10000006", timestamp: iso(ms + 4000), message: { role: "assistant", content: [{ type: "text", text: `Done — I saved the screenshot at \`${DEMO_FILE}\`. Open the Files panel to preview it.` }], api: "openai-codex-responses", provider: "openai-codex", model: "gpt-5.5", usage: zeroUsage(), stopReason: "stop", timestamp: ms + 4000, }, }, ]; await writeFile(file, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf8"); } function fallbackWebsiteHtml(url) { return `
Fallback screenshot for ${escapeHtml(url)}.