feat(cli): expand browser commands
This commit is contained in:
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user