From 35bbc2ba878ca12b690242e07f21e1f305e2dfd2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 12 Jan 2026 17:31:59 +0000 Subject: [PATCH] feat(cli): expand browser commands --- src/cli/browser-cli-actions-input.ts | 27 +- src/cli/browser-cli-debug.ts | 182 ++++++++++ src/cli/browser-cli-inspect.ts | 2 + src/cli/browser-cli-manage.ts | 125 ++++++- src/cli/browser-cli-state.ts | 514 +++++++++++++++++++++++++++ src/cli/browser-cli.ts | 4 + 6 files changed, 851 insertions(+), 3 deletions(-) create mode 100644 src/cli/browser-cli-debug.ts create mode 100644 src/cli/browser-cli-state.ts diff --git a/src/cli/browser-cli-actions-input.ts b/src/cli/browser-cli-actions-input.ts index 4ad4b22ff..22882fd40 100644 --- a/src/cli/browser-cli-actions-input.ts +++ b/src/cli/browser-cli-actions-input.ts @@ -454,16 +454,32 @@ export function registerBrowserActionInputCommands( browser .command("wait") - .description("Wait for time or text conditions") + .description("Wait for time, selector, URL, load state, or JS conditions") + .argument("[selector]", "CSS selector to wait for (visible)") .option("--time ", "Wait for N milliseconds", (v: string) => Number(v)) .option("--text ", "Wait for text to appear") .option("--text-gone ", "Wait for text to disappear") + .option("--url ", "Wait for URL (supports globs like **/dash)") + .option("--load ", "Wait for load state") + .option("--fn ", "Wait for JS condition (passed to waitForFunction)") + .option( + "--timeout-ms ", + "How long to wait for each condition (default: 20000)", + (v: string) => Number(v), + ) .option("--target-id ", "CDP target id (or unique prefix)") - .action(async (opts, cmd) => { + .action(async (selector: string | undefined, opts, cmd) => { const parent = parentOpts(cmd); const baseUrl = resolveBrowserControlUrl(parent?.url); const profile = parent?.browserProfile; try { + const sel = selector?.trim() || undefined; + const load = + opts.load === "load" || + opts.load === "domcontentloaded" || + opts.load === "networkidle" + ? (opts.load as "load" | "domcontentloaded" | "networkidle") + : undefined; const result = await browserAct( baseUrl, { @@ -471,7 +487,14 @@ export function registerBrowserActionInputCommands( timeMs: Number.isFinite(opts.time) ? opts.time : undefined, text: opts.text?.trim() || undefined, textGone: opts.textGone?.trim() || undefined, + selector: sel, + url: opts.url?.trim() || undefined, + loadState: load, + fn: opts.fn?.trim() || undefined, targetId: opts.targetId?.trim() || undefined, + timeoutMs: Number.isFinite(opts.timeoutMs) + ? opts.timeoutMs + : undefined, }, { profile }, ); diff --git a/src/cli/browser-cli-debug.ts b/src/cli/browser-cli-debug.ts new file mode 100644 index 000000000..4015517aa --- /dev/null +++ b/src/cli/browser-cli-debug.ts @@ -0,0 +1,182 @@ +import type { Command } from "commander"; + +import { resolveBrowserControlUrl } from "../browser/client.js"; +import { + browserHighlight, + browserPageErrors, + browserRequests, + browserTraceStart, + browserTraceStop, +} from "../browser/client-actions.js"; +import { danger } from "../globals.js"; +import { defaultRuntime } from "../runtime.js"; +import type { BrowserParentOpts } from "./browser-cli-shared.js"; + +export function registerBrowserDebugCommands( + browser: Command, + parentOpts: (cmd: Command) => BrowserParentOpts, +) { + browser + .command("highlight") + .description("Highlight an element by ref") + .argument("", "Ref id from snapshot") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (ref: string, opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + try { + const result = await browserHighlight(baseUrl, { + ref: ref.trim(), + targetId: opts.targetId?.trim() || undefined, + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`highlighted ${ref.trim()}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("errors") + .description("Get recent page errors") + .option("--clear", "Clear stored errors after reading", false) + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + try { + const result = await browserPageErrors(baseUrl, { + targetId: opts.targetId?.trim() || undefined, + clear: Boolean(opts.clear), + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + if (!result.errors.length) { + defaultRuntime.log("No page errors."); + return; + } + defaultRuntime.log( + result.errors + .map( + (e) => + `${e.timestamp} ${e.name ? `${e.name}: ` : ""}${e.message}`, + ) + .join("\n"), + ); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + browser + .command("requests") + .description("Get recent network requests (best-effort)") + .option("--filter ", "Only show URLs that contain this substring") + .option("--clear", "Clear stored requests after reading", false) + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + try { + const result = await browserRequests(baseUrl, { + targetId: opts.targetId?.trim() || undefined, + filter: opts.filter?.trim() || undefined, + clear: Boolean(opts.clear), + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + if (!result.requests.length) { + defaultRuntime.log("No requests recorded."); + return; + } + defaultRuntime.log( + result.requests + .map((r) => { + const status = typeof r.status === "number" ? ` ${r.status}` : ""; + const ok = r.ok === true ? " ok" : r.ok === false ? " fail" : ""; + const fail = r.failureText ? ` (${r.failureText})` : ""; + return `${r.timestamp} ${r.method}${status}${ok} ${r.url}${fail}`; + }) + .join("\n"), + ); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + const trace = browser + .command("trace") + .description("Record a Playwright trace"); + + trace + .command("start") + .description("Start trace recording") + .option("--target-id ", "CDP target id (or unique prefix)") + .option("--no-screenshots", "Disable screenshots") + .option("--no-snapshots", "Disable snapshots") + .option("--sources", "Include sources (bigger traces)", false) + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + try { + const result = await browserTraceStart(baseUrl, { + targetId: opts.targetId?.trim() || undefined, + screenshots: Boolean(opts.screenshots), + snapshots: Boolean(opts.snapshots), + sources: Boolean(opts.sources), + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log("trace started"); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + trace + .command("stop") + .description("Stop trace recording and write a .zip") + .option("--out ", "Output path for the trace zip") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + try { + const result = await browserTraceStop(baseUrl, { + targetId: opts.targetId?.trim() || undefined, + path: opts.out?.trim() || undefined, + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`TRACE:${result.path}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); +} diff --git a/src/cli/browser-cli-inspect.ts b/src/cli/browser-cli-inspect.ts index dd110b225..0dda53b8f 100644 --- a/src/cli/browser-cli-inspect.ts +++ b/src/cli/browser-cli-inspect.ts @@ -59,6 +59,7 @@ export function registerBrowserInspectCommands( .option("--compact", "Role snapshot: compact output", false) .option("--depth ", "Role snapshot: max depth", (v: string) => Number(v)) .option("--selector ", "Role snapshot: scope to CSS selector") + .option("--frame ", "Role snapshot: scope to an iframe selector") .option("--out ", "Write snapshot to a file") .action(async (opts, cmd) => { const parent = parentOpts(cmd); @@ -74,6 +75,7 @@ export function registerBrowserInspectCommands( compact: Boolean(opts.compact) || undefined, depth: Number.isFinite(opts.depth) ? opts.depth : undefined, selector: opts.selector?.trim() || undefined, + frame: opts.frame?.trim() || undefined, profile, }); diff --git a/src/cli/browser-cli-manage.ts b/src/cli/browser-cli-manage.ts index 6164c8c73..24016b65f 100644 --- a/src/cli/browser-cli-manage.ts +++ b/src/cli/browser-cli-manage.ts @@ -1,5 +1,5 @@ import type { Command } from "commander"; - +import type { BrowserTab } from "../browser/client.js"; import { browserCloseTab, browserCreateProfile, @@ -11,6 +11,7 @@ import { browserStart, browserStatus, browserStop, + browserTabAction, browserTabs, resolveBrowserControlUrl, } from "../browser/client.js"; @@ -159,6 +160,128 @@ export function registerBrowserManageCommands( } }); + const tab = browser + .command("tab") + .description("Tab shortcuts (index-based)") + .action(async (_opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + try { + const result = (await browserTabAction(baseUrl, { + action: "list", + profile, + })) as { ok: true; tabs: BrowserTab[] }; + const tabs = result.tabs ?? []; + if (parent?.json) { + defaultRuntime.log(JSON.stringify({ tabs }, null, 2)); + return; + } + if (tabs.length === 0) { + defaultRuntime.log("No tabs (browser closed or no targets)."); + return; + } + defaultRuntime.log( + tabs + .map( + (t, i) => + `${i + 1}. ${t.title || "(untitled)"}\n ${t.url}\n id: ${t.targetId}`, + ) + .join("\n"), + ); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + tab + .command("new") + .description("Open a new tab (about:blank)") + .action(async (_opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + try { + const result = await browserTabAction(baseUrl, { + action: "new", + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log("opened new tab"); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + tab + .command("select") + .description("Focus tab by index (1-based)") + .argument("", "Tab index (1-based)", (v: string) => Number(v)) + .action(async (index: number, _opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + if (!Number.isFinite(index) || index < 1) { + defaultRuntime.error(danger("index must be a positive number")); + defaultRuntime.exit(1); + return; + } + try { + const result = await browserTabAction(baseUrl, { + action: "select", + index: Math.floor(index) - 1, + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`selected tab ${Math.floor(index)}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + tab + .command("close") + .description("Close tab by index (1-based); default: first tab") + .argument("[index]", "Tab index (1-based)", (v: string) => Number(v)) + .action(async (index: number | undefined, _opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + const idx = + typeof index === "number" && Number.isFinite(index) + ? Math.floor(index) - 1 + : undefined; + if (typeof idx === "number" && idx < 0) { + defaultRuntime.error(danger("index must be >= 1")); + defaultRuntime.exit(1); + return; + } + try { + const result = await browserTabAction(baseUrl, { + action: "close", + index: idx, + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log("closed tab"); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + browser .command("open") .description("Open a URL in a new tab") diff --git a/src/cli/browser-cli-state.ts b/src/cli/browser-cli-state.ts new file mode 100644 index 000000000..0629d3c94 --- /dev/null +++ b/src/cli/browser-cli-state.ts @@ -0,0 +1,514 @@ +import type { Command } from "commander"; + +import { resolveBrowserControlUrl } from "../browser/client.js"; +import { + browserCookies, + browserCookiesClear, + browserCookiesSet, + browserSetDevice, + browserSetGeolocation, + browserSetHeaders, + browserSetHttpCredentials, + browserSetLocale, + browserSetMedia, + browserSetOffline, + browserSetTimezone, + browserStorageClear, + browserStorageGet, + browserStorageSet, +} from "../browser/client-actions.js"; +import { browserAct } from "../browser/client-actions-core.js"; +import { danger } from "../globals.js"; +import { defaultRuntime } from "../runtime.js"; +import type { BrowserParentOpts } from "./browser-cli-shared.js"; + +function parseOnOff(raw: string): boolean | null { + const v = raw.trim().toLowerCase(); + if (v === "on" || v === "true" || v === "1") return true; + if (v === "off" || v === "false" || v === "0") return false; + return null; +} + +export function registerBrowserStateCommands( + browser: Command, + parentOpts: (cmd: Command) => BrowserParentOpts, +) { + const cookies = browser.command("cookies").description("Read/write cookies"); + + cookies + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + try { + const result = await browserCookies(baseUrl, { + targetId: opts.targetId?.trim() || undefined, + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(JSON.stringify(result.cookies ?? [], null, 2)); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + cookies + .command("set") + .description("Set a cookie (requires --url or domain+path)") + .argument("", "Cookie name") + .argument("", "Cookie value") + .requiredOption("--url ", "Cookie URL scope (recommended)") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (name: string, value: string, opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + try { + const result = await browserCookiesSet(baseUrl, { + targetId: opts.targetId?.trim() || undefined, + cookie: { name, value, url: opts.url }, + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`cookie set: ${name}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + cookies + .command("clear") + .description("Clear all cookies") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + try { + const result = await browserCookiesClear(baseUrl, { + targetId: opts.targetId?.trim() || undefined, + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log("cookies cleared"); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + const storage = browser + .command("storage") + .description("Read/write localStorage/sessionStorage"); + + function registerStorageKind(kind: "local" | "session") { + const cmd = storage.command(kind).description(`${kind}Storage commands`); + + cmd + .command("get") + .description(`Get ${kind}Storage (all keys or one key)`) + .argument("[key]", "Key (optional)") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (key: string | undefined, opts, cmd2) => { + const parent = parentOpts(cmd2); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + try { + const result = await browserStorageGet(baseUrl, { + kind, + key: key?.trim() || undefined, + targetId: opts.targetId?.trim() || undefined, + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(JSON.stringify(result.values ?? {}, null, 2)); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + cmd + .command("set") + .description(`Set a ${kind}Storage key`) + .argument("", "Key") + .argument("", "Value") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (key: string, value: string, opts, cmd2) => { + const parent = parentOpts(cmd2); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + try { + const result = await browserStorageSet(baseUrl, { + kind, + key, + value, + targetId: opts.targetId?.trim() || undefined, + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`${kind}Storage set: ${key}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + cmd + .command("clear") + .description(`Clear all ${kind}Storage keys`) + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd2) => { + const parent = parentOpts(cmd2); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + try { + const result = await browserStorageClear(baseUrl, { + kind, + targetId: opts.targetId?.trim() || undefined, + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`${kind}Storage cleared`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + } + + registerStorageKind("local"); + registerStorageKind("session"); + + const set = browser + .command("set") + .description("Browser environment settings"); + + set + .command("viewport") + .description("Set viewport size (alias for resize)") + .argument("", "Viewport width", (v: string) => Number(v)) + .argument("", "Viewport height", (v: string) => Number(v)) + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (width: number, height: number, opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + if (!Number.isFinite(width) || !Number.isFinite(height)) { + defaultRuntime.error(danger("width and height must be numbers")); + defaultRuntime.exit(1); + return; + } + try { + const result = await browserAct( + baseUrl, + { + kind: "resize", + width, + height, + targetId: opts.targetId?.trim() || undefined, + }, + { profile }, + ); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`viewport set: ${width}x${height}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + set + .command("offline") + .description("Toggle offline mode") + .argument("", "on/off") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (value: string, opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + const offline = parseOnOff(value); + if (offline === null) { + defaultRuntime.error(danger("Expected on|off")); + defaultRuntime.exit(1); + return; + } + try { + const result = await browserSetOffline(baseUrl, { + offline, + targetId: opts.targetId?.trim() || undefined, + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`offline: ${offline}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + set + .command("headers") + .description("Set extra HTTP headers (JSON object)") + .requiredOption("--json ", "JSON object of headers") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + try { + const parsed = JSON.parse(String(opts.json)) as unknown; + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("headers json must be an object"); + } + const headers: Record = {}; + for (const [k, v] of Object.entries( + parsed as Record, + )) { + if (typeof v === "string") headers[k] = v; + } + const result = await browserSetHeaders(baseUrl, { + headers, + targetId: opts.targetId?.trim() || undefined, + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log("headers set"); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + set + .command("credentials") + .description("Set HTTP basic auth credentials") + .option("--clear", "Clear credentials", false) + .argument("[username]", "Username") + .argument("[password]", "Password") + .option("--target-id ", "CDP target id (or unique prefix)") + .action( + async ( + username: string | undefined, + password: string | undefined, + opts, + cmd, + ) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + try { + const result = await browserSetHttpCredentials(baseUrl, { + username: username?.trim() || undefined, + password, + clear: Boolean(opts.clear), + targetId: opts.targetId?.trim() || undefined, + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log( + opts.clear ? "credentials cleared" : "credentials set", + ); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }, + ); + + set + .command("geo") + .description("Set geolocation (and grant permission)") + .option("--clear", "Clear geolocation + permissions", false) + .argument("[latitude]", "Latitude", (v: string) => Number(v)) + .argument("[longitude]", "Longitude", (v: string) => Number(v)) + .option("--accuracy ", "Accuracy in meters", (v: string) => Number(v)) + .option("--origin ", "Origin to grant permissions for") + .option("--target-id ", "CDP target id (or unique prefix)") + .action( + async ( + latitude: number | undefined, + longitude: number | undefined, + opts, + cmd, + ) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + try { + const result = await browserSetGeolocation(baseUrl, { + latitude: Number.isFinite(latitude) ? latitude : undefined, + longitude: Number.isFinite(longitude) ? longitude : undefined, + accuracy: Number.isFinite(opts.accuracy) + ? opts.accuracy + : undefined, + origin: opts.origin?.trim() || undefined, + clear: Boolean(opts.clear), + targetId: opts.targetId?.trim() || undefined, + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log( + opts.clear ? "geolocation cleared" : "geolocation set", + ); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }, + ); + + set + .command("media") + .description("Emulate prefers-color-scheme") + .argument("", "dark/light/none") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (value: string, opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + const v = value.trim().toLowerCase(); + const colorScheme = + v === "dark" + ? "dark" + : v === "light" + ? "light" + : v === "none" + ? "none" + : null; + if (!colorScheme) { + defaultRuntime.error(danger("Expected dark|light|none")); + defaultRuntime.exit(1); + return; + } + try { + const result = await browserSetMedia(baseUrl, { + colorScheme, + targetId: opts.targetId?.trim() || undefined, + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`media colorScheme: ${colorScheme}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + set + .command("timezone") + .description("Override timezone (CDP)") + .argument("", "Timezone ID (e.g. America/New_York)") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (timezoneId: string, opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + try { + const result = await browserSetTimezone(baseUrl, { + timezoneId, + targetId: opts.targetId?.trim() || undefined, + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`timezone: ${timezoneId}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + set + .command("locale") + .description("Override locale (CDP)") + .argument("", "Locale (e.g. en-US)") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (locale: string, opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + try { + const result = await browserSetLocale(baseUrl, { + locale, + targetId: opts.targetId?.trim() || undefined, + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`locale: ${locale}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); + + set + .command("device") + .description('Apply a Playwright device descriptor (e.g. "iPhone 14")') + .argument("", "Device name (Playwright devices)") + .option("--target-id ", "CDP target id (or unique prefix)") + .action(async (name: string, opts, cmd) => { + const parent = parentOpts(cmd); + const baseUrl = resolveBrowserControlUrl(parent?.url); + const profile = parent?.browserProfile; + try { + const result = await browserSetDevice(baseUrl, { + name, + targetId: opts.targetId?.trim() || undefined, + profile, + }); + if (parent?.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + defaultRuntime.log(`device: ${name}`); + } catch (err) { + defaultRuntime.error(danger(String(err))); + defaultRuntime.exit(1); + } + }); +} diff --git a/src/cli/browser-cli.ts b/src/cli/browser-cli.ts index df692c6da..9d3c51ec6 100644 --- a/src/cli/browser-cli.ts +++ b/src/cli/browser-cli.ts @@ -6,6 +6,7 @@ import { formatDocsLink } from "../terminal/links.js"; import { theme } from "../terminal/theme.js"; import { registerBrowserActionInputCommands } from "./browser-cli-actions-input.js"; import { registerBrowserActionObserveCommands } from "./browser-cli-actions-observe.js"; +import { registerBrowserDebugCommands } from "./browser-cli-debug.js"; import { browserActionExamples, browserCoreExamples, @@ -13,6 +14,7 @@ import { import { registerBrowserInspectCommands } from "./browser-cli-inspect.js"; import { registerBrowserManageCommands } from "./browser-cli-manage.js"; import type { BrowserParentOpts } from "./browser-cli-shared.js"; +import { registerBrowserStateCommands } from "./browser-cli-state.js"; export function registerBrowserCli(program: Command) { const browser = program @@ -50,4 +52,6 @@ export function registerBrowserCli(program: Command) { registerBrowserInspectCommands(browser, parentOpts); registerBrowserActionInputCommands(browser, parentOpts); registerBrowserActionObserveCommands(browser, parentOpts); + registerBrowserDebugCommands(browser, parentOpts); + registerBrowserStateCommands(browser, parentOpts); }