// Forensic front-end capture of our own WorkClaw app, for reassembling the // styled interface locally and getting ground-truth styles to match Clawmates. // // Auth never touches this script: you log in by hand once, it persists the // session, headless runs reuse it. // // node capture.mjs live headed; you log in by hand (incl. the emailed // code), press Enter, and it captures everything in // that same session — best for code/OTP logins // node capture.mjs login headed; you log in manually; saves out/auth.json // (only persists if the token lives in a cookie or // localStorage — not sessionStorage) // node capture.mjs browse headed + saved session; mirrors assets as you // click around; close the window to stop // node capture.mjs run headless + saved session; walks ROUTES, saving // each screen's HTML, screenshot, computed styles // // Config via env: WC_BASE_URL (default below). Outputs land in ./out (gitignored). import { chromium } from "playwright"; import { mkdir, writeFile } from "node:fs/promises"; import { existsSync } from "node:fs"; import { fileURLToPath } from "node:url"; import path from "node:path"; import readline from "node:readline"; const BASE = process.env.WC_BASE_URL ?? "https://www.workclaw.com"; const LOGIN = process.env.WC_LOGIN_URL ?? new URL("/sign-in", BASE).href; const HERE = path.dirname(fileURLToPath(import.meta.url)); const OUT = path.join(HERE, "out"); const AUTH = path.join(OUT, "auth.json"); const HOST = new URL(BASE).host; const mode = process.argv[2] ?? "run"; // Screens to capture in `run` mode. `actions(page)` runs after navigation — // e.g. enter a claw and open the Computer panel. Selectors mirror Clawmates' // (role-based); adjust if the live WorkClaw UI differs. const ROUTES = [ { name: "workspace-home", url: "/" }, { name: "computer-home", url: "/", actions: async (page) => { await page.getByRole("link", { name: /claw|chat|scout/i }).first().click(); await page.waitForURL(/chat/i, { timeout: 8000 }).catch(() => {}); await page.getByRole("button", { name: /computer/i }).click(); await page.waitForTimeout(900); }, }, ]; // Elements we want exact computed styles for (the parity unknowns). const SELECTORS = [ '[aria-label="Dock"]', '[aria-label="Dock"] button', '[role="complementary"]', '[role="complementary"] .grid, [aria-label$="apps"]', ]; const CAPTURE_TYPES = new Set([ "stylesheet", "script", "font", "image", "document", "manifest", ]); async function saveResponse(resp) { try { const url = new URL(resp.url()); if (url.host !== HOST) return; // same-origin only if (!CAPTURE_TYPES.has(resp.request().resourceType())) return; const body = await resp.body().catch(() => null); if (!body) return; let rel = url.pathname; if (rel.endsWith("/")) rel += "index.html"; const file = path.join(OUT, "assets", url.host, rel); await mkdir(path.dirname(file), { recursive: true }); await writeFile(file, body); } catch { /* ignore individual asset failures */ } } function waitForEnter(prompt) { return new Promise((resolve) => { const rl = readline.createInterface({ input: process.stdin }); rl.question(prompt, () => { rl.close(); resolve(); }); }); } async function login() { const browser = await chromium.launch({ headless: false }); const ctx = await browser.newContext(); await ctx.newPage().then((p) => p.goto(LOGIN)); await waitForEnter( "\n>>> Log in by hand in the browser window. When the app is fully loaded, press Enter here.\n", ); await mkdir(OUT, { recursive: true }); await ctx.storageState({ path: AUTH }); console.log(`Saved session → ${AUTH}`); await browser.close(); } function requireAuth() { if (!existsSync(AUTH)) { console.error("No session yet — run `node capture.mjs login` first."); process.exit(1); } } async function browse() { requireAuth(); const browser = await chromium.launch({ headless: false }); const ctx = await browser.newContext({ storageState: AUTH }); const page = await ctx.newPage(); page.on("response", saveResponse); await page.goto(BASE); console.log("Mirroring assets as you browse. Close the window to finish."); await new Promise((r) => browser.on("disconnected", r)); console.log(`Assets saved under ${path.join(OUT, "assets")}`); } async function captureScreens(page) { for (const dir of ["pages", "shots", "styles"]) { await mkdir(path.join(OUT, dir), { recursive: true }); } for (const route of ROUTES) { try { await page.goto(new URL(route.url, BASE).href, { waitUntil: "networkidle", }); if (route.actions) await route.actions(page); await page.waitForTimeout(500); await writeFile( path.join(OUT, "pages", `${route.name}.html`), await page.content(), ); await page.screenshot({ path: path.join(OUT, "shots", `${route.name}.png`), }); const styles = await page.evaluate((sels) => { const props = [ "width", "height", "margin", "padding", "gap", "justifyContent", "alignItems", "borderRadius", "boxShadow", "backgroundColor", "backdropFilter", "fontFamily", "color", ]; const dump = {}; for (const sel of sels) { dump[sel] = [...document.querySelectorAll(sel)] .slice(0, 4) .map((el) => { const cs = getComputedStyle(el); const r = el.getBoundingClientRect(); const o = { rect: { w: r.width, h: r.height, x: r.x, y: r.y } }; for (const p of props) o[p] = cs[p]; return o; }); } return dump; }, SELECTORS); await writeFile( path.join(OUT, "styles", `${route.name}.json`), JSON.stringify(styles, null, 2), ); console.log(`captured ${route.name}`); } catch (e) { console.warn(`! ${route.name}: ${e.message}`); } } console.log(`\nDone. See ${OUT}/{assets,pages,shots,styles}`); } async function run() { requireAuth(); const browser = await chromium.launch({ headless: true }); const ctx = await browser.newContext({ storageState: AUTH }); const page = await ctx.newPage(); page.on("response", saveResponse); await captureScreens(page); await browser.close(); } // Combined login + capture in one live session — robust to OTP logins and // sessionStorage tokens that wouldn't survive a save/reload. async function live() { const browser = await chromium.launch({ headless: false }); const ctx = await browser.newContext(); const page = await ctx.newPage(); page.on("response", saveResponse); // mirror assets from the first paint await page.goto(LOGIN); await waitForEnter( "\n>>> Log in by hand (enter the emailed code). When the app is fully loaded, press Enter here.\n", ); await captureScreens(page); await browser.close(); } await { live, login, browse, run }[mode]?.() ?? console.error(`Unknown mode "${mode}" — use live | login | browse | run`);