Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion website-to-components/jobs/00-sitemap.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import { spawnSync, execSync } from "child_process";
import { writeFileSync, readFileSync, existsSync } from "fs";
import { sitePaths, siteSlug, pageSlug, ensureDir } from "../lib/paths.js";
import { browserOpen } from "../lib/browser.js";

function browserEval(js) {
const result = spawnSync("agent-browser", ["eval", "--stdin"], {
Expand Down Expand Up @@ -142,7 +143,7 @@ export async function run(url) {
console.log(`Detecting main menu for ${host} …`);

execSync("agent-browser set viewport 1440 900", { stdio: "inherit" });
execSync(`agent-browser open "${url}"`, { stdio: "inherit" });
browserOpen(url);
execSync("agent-browser wait --load networkidle", { stdio: "inherit" });

// First pass: ask agent-browser for the accessibility snapshot so we can pick
Expand Down
3 changes: 2 additions & 1 deletion website-to-components/jobs/01-screenshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { writeFileSync } from "fs";
import { sitePaths, ensureDir } from "../lib/paths.js";
import { paintVideoIframes } from "../lib/video-iframes.js";
import { measureSections } from "./01c-measure-sections.js";
import { browserOpen } from "../lib/browser.js";

function browserEval(js) {
const result = spawnSync("agent-browser", ["eval", "--stdin"], {
Expand All @@ -19,7 +20,7 @@ export async function run(url) {

console.log(`Opening ${url}...`);
execSync("agent-browser set viewport 1440 900", { stdio: "inherit" });
execSync(`agent-browser open "${url}"`, { stdio: "inherit" });
browserOpen(url);
execSync("agent-browser wait --load networkidle", { stdio: "inherit" });

// Dismiss cookie banner using AI vision on the accessibility tree.
Expand Down
5 changes: 3 additions & 2 deletions website-to-components/jobs/01b-screenshot-mobile.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { writeFileSync, readFileSync } from "fs";
import { imageSize } from "../lib/image.js";
import { sitePaths, ensureDir } from "../lib/paths.js";
import { paintVideoIframes } from "../lib/video-iframes.js";
import { browserOpen } from "../lib/browser.js";

const MOBILE_WIDTH = 390;
const MOBILE_HEIGHT = 844;
Expand Down Expand Up @@ -53,7 +54,7 @@ export async function run(url) {
// at 390px — responsive sites that branch on the initial width (JS, not just
// CSS media queries) need the reload, not just a post-load resize.
console.log(`Opening ${url} to establish page context...`);
execSync(`agent-browser open "${url}"`, { stdio: "inherit" });
browserOpen(url);
execSync("agent-browser wait --load networkidle", { stdio: "inherit" });

console.log(`Setting mobile viewport ${MOBILE_WIDTH}x${MOBILE_HEIGHT} @${MOBILE_SCALE}x...`);
Expand All @@ -63,7 +64,7 @@ export async function run(url) {
// not a 390 @2x render.
execSync(`agent-browser set viewport ${MOBILE_WIDTH} ${MOBILE_HEIGHT} ${MOBILE_SCALE}`, { stdio: "inherit" });
console.log("Reloading at mobile viewport...");
execSync(`agent-browser open "${url}"`, { stdio: "inherit" });
browserOpen(url);
execSync("agent-browser wait --load networkidle", { stdio: "inherit" });
execSync("agent-browser wait 500", { stdio: "pipe" });

Expand Down
3 changes: 2 additions & 1 deletion website-to-components/jobs/01c-measure-sections.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import { spawnSync, execSync } from "child_process";
import { writeFileSync } from "fs";
import { sitePaths, ensureDir } from "../lib/paths.js";
import { browserOpen } from "../lib/browser.js";

function browserEval(js) {
const result = spawnSync("agent-browser", ["eval", "--stdin"], {
Expand Down Expand Up @@ -216,7 +217,7 @@ export function measureSections(url) {
// Standalone entry: requires an already-open session. Opens the URL if asked.
export async function run(url, { open = false } = {}) {
if (open) {
execSync(`agent-browser open "${url}"`, { stdio: "inherit" });
browserOpen(url);
execSync("agent-browser wait --load networkidle", { stdio: "inherit" });
}
return measureSections(url);
Expand Down
3 changes: 2 additions & 1 deletion website-to-components/jobs/03b-extract-assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { execSync } from 'child_process';
import { writeFileSync, mkdirSync } from 'fs';
import { join } from 'path';
import { sitePaths } from '../lib/paths.js';
import { browserOpen } from "../lib/browser.js";

export async function run(url) {
const { outputDir: outDir } = sitePaths(url);
Expand Down Expand Up @@ -237,7 +238,7 @@ JSON.stringify(r, null, 2);
// this scroll-and-wait step, the extractor will report 0 videos on any
// page that lazy-loads its players.
try {
execSync(`agent-browser open ${url}`, { encoding: 'utf8', timeout: 30000 });
browserOpen(url, { stdio: 'pipe', encoding: 'utf8', timeout: 30000 });
} catch (err) {
// Ignore — page may already be open
}
Expand Down
3 changes: 2 additions & 1 deletion website-to-components/jobs/03b-scan-dom-images.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { execSync } from 'child_process';
import { readFileSync, writeFileSync, mkdirSync, existsSync, createWriteStream } from 'fs';
import { join, basename, extname } from 'path';
import { sitePaths, siteSlug, ROOT } from '../lib/paths.js';
import { browserOpen } from "../lib/browser.js";

// How long to wait (ms) after page load for JS widgets to inject their DOM
const SETTLE_MS = 4000;
Expand All @@ -39,7 +40,7 @@ export async function run(url) {

console.log(` Opening: ${url}`);
try {
execSync(`agent-browser open ${JSON.stringify(url)}`, { encoding: 'utf8', timeout: 30000 });
browserOpen(url, { stdio: 'pipe', encoding: 'utf8', timeout: 30000 });
} catch (err) {
// Page may already be open — continue
}
Expand Down
3 changes: 2 additions & 1 deletion website-to-components/jobs/05-sitemap.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { spawnSync, execSync } from "child_process";
import { writeFileSync, readFileSync, existsSync } from "fs";
import { sitePaths, siteSlug, ensureDir } from "../lib/paths.js";
import { browserOpen } from "../lib/browser.js";

function browserEval(js) {
const result = spawnSync("agent-browser", ["eval", "--stdin"], {
Expand All @@ -21,7 +22,7 @@ export async function run(url) {
console.log(`Discovering sitemap for ${slug} (same-domain only)...`);

execSync("agent-browser set viewport 1440 900", { stdio: "inherit" });
execSync(`agent-browser open "${url}"`, { stdio: "inherit" });
browserOpen(url);
execSync("agent-browser wait --load networkidle", { stdio: "inherit" });

// Extract all anchor hrefs from the page, keep only same-domain links
Expand Down
3 changes: 2 additions & 1 deletion website-to-components/jobs/06-mobile-check.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { writeFileSync, readFileSync, existsSync, readdirSync } from "fs";
import { join } from "path";
import { imageSize, cropPng } from "../lib/image.js";
import { sitePaths, ensureDir } from "../lib/paths.js";
import { browserOpen } from "../lib/browser.js";

const MOBILE_WIDTH = 390;
const MOBILE_HEIGHT = 844;
Expand All @@ -51,7 +52,7 @@ function openStory(componentName, storyName = "Default") {
const componentId = componentName.toLowerCase().replace(/_/g, "-");
const storyId = storyName.toLowerCase().replace(/\s+/g, "-");
const storybookUrl = `http://localhost:${STORYBOOK_PORT}/iframe.html?id=components-${componentId}--${storyId}&viewMode=story`;
execSync(`agent-browser open "${storybookUrl}"`, { stdio: "pipe" });
browserOpen(storybookUrl, { stdio: "pipe" });
execSync("agent-browser wait --load networkidle", { stdio: "pipe" });
execSync("agent-browser wait 1000", { stdio: "pipe" });
}
Expand Down
52 changes: 52 additions & 0 deletions website-to-components/lib/browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Shared agent-browser process helpers.
*
* Why this exists: several jobs end with `agent-browser close`, and the next
* job in the pipeline starts with `agent-browser open`. Closing tears down the
* daemon's unix socket, and an `open` issued immediately after can arrive
* before the daemon has been re-created:
*
* ✗ Could not configure browser: Failed to connect: No such file or
* directory (os error 2)
*
* Measured at roughly a 50% failure rate on a bare close→open sequence, and 0%
* once a short settle delay is inserted. Rather than sprinkle sleeps after every
* close, retry the open — that also covers the daemon dying for any other
* reason, not just the teardown race.
*/

import { execFileSync } from "child_process";

/** Block the event loop for `ms` — these jobs are strictly sequential. */
function sleepSync(ms) {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
}

/**
* Run `agent-browser open <url>`, retrying on failure with a linear backoff.
*
* @param {string} url URL to open.
* @param {object} [opts] execFileSync options, plus `attempts` (default 4).
* @returns {string|Buffer} Whatever execFileSync returns (honours `encoding`).
*/
export function browserOpen(url, opts = {}) {
const { attempts = 4, stdio = "inherit", ...execOpts } = opts;
let lastError;

for (let attempt = 1; attempt <= attempts; attempt++) {
try {
// argv form, no shell: a URL can never be interpreted as shell syntax.
return execFileSync("agent-browser", ["open", url], { stdio, ...execOpts });
}
catch (error) {
lastError = error;
if (attempt === attempts) break;
// Linear backoff: 300ms, 600ms, 900ms. The observed race clears well
// inside the first step; the later ones cover a slower daemon restart.
console.log(` agent-browser open failed (attempt ${attempt}/${attempts}); retrying...`);
sleepSync(300 * attempt);
}
}

throw lastError;
}