feat(cli): expand browser commands
This commit is contained in:
@@ -454,16 +454,32 @@ export function registerBrowserActionInputCommands(
|
|||||||
|
|
||||||
browser
|
browser
|
||||||
.command("wait")
|
.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 <ms>", "Wait for N milliseconds", (v: string) => Number(v))
|
.option("--time <ms>", "Wait for N milliseconds", (v: string) => Number(v))
|
||||||
.option("--text <value>", "Wait for text to appear")
|
.option("--text <value>", "Wait for text to appear")
|
||||||
.option("--text-gone <value>", "Wait for text to disappear")
|
.option("--text-gone <value>", "Wait for text to disappear")
|
||||||
|
.option("--url <pattern>", "Wait for URL (supports globs like **/dash)")
|
||||||
|
.option("--load <load|domcontentloaded|networkidle>", "Wait for load state")
|
||||||
|
.option("--fn <js>", "Wait for JS condition (passed to waitForFunction)")
|
||||||
|
.option(
|
||||||
|
"--timeout-ms <ms>",
|
||||||
|
"How long to wait for each condition (default: 20000)",
|
||||||
|
(v: string) => Number(v),
|
||||||
|
)
|
||||||
.option("--target-id <id>", "CDP target id (or unique prefix)")
|
.option("--target-id <id>", "CDP target id (or unique prefix)")
|
||||||
.action(async (opts, cmd) => {
|
.action(async (selector: string | undefined, opts, cmd) => {
|
||||||
const parent = parentOpts(cmd);
|
const parent = parentOpts(cmd);
|
||||||
const baseUrl = resolveBrowserControlUrl(parent?.url);
|
const baseUrl = resolveBrowserControlUrl(parent?.url);
|
||||||
const profile = parent?.browserProfile;
|
const profile = parent?.browserProfile;
|
||||||
try {
|
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(
|
const result = await browserAct(
|
||||||
baseUrl,
|
baseUrl,
|
||||||
{
|
{
|
||||||
@@ -471,7 +487,14 @@ export function registerBrowserActionInputCommands(
|
|||||||
timeMs: Number.isFinite(opts.time) ? opts.time : undefined,
|
timeMs: Number.isFinite(opts.time) ? opts.time : undefined,
|
||||||
text: opts.text?.trim() || undefined,
|
text: opts.text?.trim() || undefined,
|
||||||
textGone: opts.textGone?.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,
|
targetId: opts.targetId?.trim() || undefined,
|
||||||
|
timeoutMs: Number.isFinite(opts.timeoutMs)
|
||||||
|
? opts.timeoutMs
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{ profile },
|
{ profile },
|
||||||
);
|
);
|
||||||
|
|||||||
182
src/cli/browser-cli-debug.ts
Normal file
182
src/cli/browser-cli-debug.ts
Normal file
@@ -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>", "Ref id from snapshot")
|
||||||
|
.option("--target-id <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 <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 <text>", "Only show URLs that contain this substring")
|
||||||
|
.option("--clear", "Clear stored requests after reading", false)
|
||||||
|
.option("--target-id <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 <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 <path>", "Output path for the trace zip")
|
||||||
|
.option("--target-id <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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -59,6 +59,7 @@ export function registerBrowserInspectCommands(
|
|||||||
.option("--compact", "Role snapshot: compact output", false)
|
.option("--compact", "Role snapshot: compact output", false)
|
||||||
.option("--depth <n>", "Role snapshot: max depth", (v: string) => Number(v))
|
.option("--depth <n>", "Role snapshot: max depth", (v: string) => Number(v))
|
||||||
.option("--selector <sel>", "Role snapshot: scope to CSS selector")
|
.option("--selector <sel>", "Role snapshot: scope to CSS selector")
|
||||||
|
.option("--frame <sel>", "Role snapshot: scope to an iframe selector")
|
||||||
.option("--out <path>", "Write snapshot to a file")
|
.option("--out <path>", "Write snapshot to a file")
|
||||||
.action(async (opts, cmd) => {
|
.action(async (opts, cmd) => {
|
||||||
const parent = parentOpts(cmd);
|
const parent = parentOpts(cmd);
|
||||||
@@ -74,6 +75,7 @@ export function registerBrowserInspectCommands(
|
|||||||
compact: Boolean(opts.compact) || undefined,
|
compact: Boolean(opts.compact) || undefined,
|
||||||
depth: Number.isFinite(opts.depth) ? opts.depth : undefined,
|
depth: Number.isFinite(opts.depth) ? opts.depth : undefined,
|
||||||
selector: opts.selector?.trim() || undefined,
|
selector: opts.selector?.trim() || undefined,
|
||||||
|
frame: opts.frame?.trim() || undefined,
|
||||||
profile,
|
profile,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
|
import type { BrowserTab } from "../browser/client.js";
|
||||||
import {
|
import {
|
||||||
browserCloseTab,
|
browserCloseTab,
|
||||||
browserCreateProfile,
|
browserCreateProfile,
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
browserStart,
|
browserStart,
|
||||||
browserStatus,
|
browserStatus,
|
||||||
browserStop,
|
browserStop,
|
||||||
|
browserTabAction,
|
||||||
browserTabs,
|
browserTabs,
|
||||||
resolveBrowserControlUrl,
|
resolveBrowserControlUrl,
|
||||||
} from "../browser/client.js";
|
} 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("<index>", "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
|
browser
|
||||||
.command("open")
|
.command("open")
|
||||||
.description("Open a URL in a new tab")
|
.description("Open a URL in a new tab")
|
||||||
|
|||||||
514
src/cli/browser-cli-state.ts
Normal file
514
src/cli/browser-cli-state.ts
Normal file
@@ -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 <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("<name>", "Cookie name")
|
||||||
|
.argument("<value>", "Cookie value")
|
||||||
|
.requiredOption("--url <url>", "Cookie URL scope (recommended)")
|
||||||
|
.option("--target-id <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 <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 <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>", "Key")
|
||||||
|
.argument("<value>", "Value")
|
||||||
|
.option("--target-id <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 <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("<width>", "Viewport width", (v: string) => Number(v))
|
||||||
|
.argument("<height>", "Viewport height", (v: string) => Number(v))
|
||||||
|
.option("--target-id <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>", "on/off")
|
||||||
|
.option("--target-id <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>", "JSON object of headers")
|
||||||
|
.option("--target-id <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<string, string> = {};
|
||||||
|
for (const [k, v] of Object.entries(
|
||||||
|
parsed as Record<string, unknown>,
|
||||||
|
)) {
|
||||||
|
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 <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 <m>", "Accuracy in meters", (v: string) => Number(v))
|
||||||
|
.option("--origin <origin>", "Origin to grant permissions for")
|
||||||
|
.option("--target-id <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>", "dark/light/none")
|
||||||
|
.option("--target-id <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("<timezoneId>", "Timezone ID (e.g. America/New_York)")
|
||||||
|
.option("--target-id <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>", "Locale (e.g. en-US)")
|
||||||
|
.option("--target-id <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("<name>", "Device name (Playwright devices)")
|
||||||
|
.option("--target-id <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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { formatDocsLink } from "../terminal/links.js";
|
|||||||
import { theme } from "../terminal/theme.js";
|
import { theme } from "../terminal/theme.js";
|
||||||
import { registerBrowserActionInputCommands } from "./browser-cli-actions-input.js";
|
import { registerBrowserActionInputCommands } from "./browser-cli-actions-input.js";
|
||||||
import { registerBrowserActionObserveCommands } from "./browser-cli-actions-observe.js";
|
import { registerBrowserActionObserveCommands } from "./browser-cli-actions-observe.js";
|
||||||
|
import { registerBrowserDebugCommands } from "./browser-cli-debug.js";
|
||||||
import {
|
import {
|
||||||
browserActionExamples,
|
browserActionExamples,
|
||||||
browserCoreExamples,
|
browserCoreExamples,
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
import { registerBrowserInspectCommands } from "./browser-cli-inspect.js";
|
import { registerBrowserInspectCommands } from "./browser-cli-inspect.js";
|
||||||
import { registerBrowserManageCommands } from "./browser-cli-manage.js";
|
import { registerBrowserManageCommands } from "./browser-cli-manage.js";
|
||||||
import type { BrowserParentOpts } from "./browser-cli-shared.js";
|
import type { BrowserParentOpts } from "./browser-cli-shared.js";
|
||||||
|
import { registerBrowserStateCommands } from "./browser-cli-state.js";
|
||||||
|
|
||||||
export function registerBrowserCli(program: Command) {
|
export function registerBrowserCli(program: Command) {
|
||||||
const browser = program
|
const browser = program
|
||||||
@@ -50,4 +52,6 @@ export function registerBrowserCli(program: Command) {
|
|||||||
registerBrowserInspectCommands(browser, parentOpts);
|
registerBrowserInspectCommands(browser, parentOpts);
|
||||||
registerBrowserActionInputCommands(browser, parentOpts);
|
registerBrowserActionInputCommands(browser, parentOpts);
|
||||||
registerBrowserActionObserveCommands(browser, parentOpts);
|
registerBrowserActionObserveCommands(browser, parentOpts);
|
||||||
|
registerBrowserDebugCommands(browser, parentOpts);
|
||||||
|
registerBrowserStateCommands(browser, parentOpts);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user