mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-10 01:48:38 +02:00
feat: 40+ browser commands (read, write, meta)
Read: text, html, links, forms, accessibility, js, eval, css, attrs, console, network, cookies, storage, perf Write: goto, back, forward, reload, click, fill, select, hover, type, press, scroll, wait, viewport, cookie, header, useragent Meta: tabs, tab, newtab, closetab, status, url, stop, restart, screenshot, pdf, responsive, chain, diff Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,198 @@
|
|||||||
|
/**
|
||||||
|
* Meta commands — tabs, server control, screenshots, chain, diff
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { BrowserManager } from './browser-manager';
|
||||||
|
import * as Diff from 'diff';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
export async function handleMetaCommand(
|
||||||
|
command: string,
|
||||||
|
args: string[],
|
||||||
|
bm: BrowserManager,
|
||||||
|
shutdown: () => Promise<void> | void
|
||||||
|
): Promise<string> {
|
||||||
|
switch (command) {
|
||||||
|
// ─── Tabs ──────────────────────────────────────────
|
||||||
|
case 'tabs': {
|
||||||
|
const tabs = await bm.getTabListWithTitles();
|
||||||
|
return tabs.map(t =>
|
||||||
|
`${t.active ? '→ ' : ' '}[${t.id}] ${t.title || '(untitled)'} — ${t.url}`
|
||||||
|
).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'tab': {
|
||||||
|
const id = parseInt(args[0], 10);
|
||||||
|
if (isNaN(id)) throw new Error('Usage: browse tab <id>');
|
||||||
|
bm.switchTab(id);
|
||||||
|
return `Switched to tab ${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'newtab': {
|
||||||
|
const url = args[0];
|
||||||
|
const id = await bm.newTab(url);
|
||||||
|
return `Opened tab ${id}${url ? ` → ${url}` : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'closetab': {
|
||||||
|
const id = args[0] ? parseInt(args[0], 10) : undefined;
|
||||||
|
await bm.closeTab(id);
|
||||||
|
return `Closed tab${id ? ` ${id}` : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Server Control ────────────────────────────────
|
||||||
|
case 'status': {
|
||||||
|
const page = bm.getPage();
|
||||||
|
const tabs = bm.getTabCount();
|
||||||
|
return [
|
||||||
|
`Status: healthy`,
|
||||||
|
`URL: ${page.url()}`,
|
||||||
|
`Tabs: ${tabs}`,
|
||||||
|
`PID: ${process.pid}`,
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'url': {
|
||||||
|
return bm.getCurrentUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'stop': {
|
||||||
|
await shutdown();
|
||||||
|
return 'Server stopped';
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'restart': {
|
||||||
|
// Signal that we want a restart — the CLI will detect exit and restart
|
||||||
|
console.log('[browse] Restart requested. Exiting for CLI to restart.');
|
||||||
|
await shutdown();
|
||||||
|
return 'Restarting...';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Visual ────────────────────────────────────────
|
||||||
|
case 'screenshot': {
|
||||||
|
const page = bm.getPage();
|
||||||
|
const screenshotPath = args[0] || '/tmp/browse-screenshot.png';
|
||||||
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||||
|
return `Screenshot saved: ${screenshotPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'pdf': {
|
||||||
|
const page = bm.getPage();
|
||||||
|
const pdfPath = args[0] || '/tmp/browse-page.pdf';
|
||||||
|
await page.pdf({ path: pdfPath, format: 'A4' });
|
||||||
|
return `PDF saved: ${pdfPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'responsive': {
|
||||||
|
const page = bm.getPage();
|
||||||
|
const prefix = args[0] || '/tmp/browse-responsive';
|
||||||
|
const viewports = [
|
||||||
|
{ name: 'mobile', width: 375, height: 812 },
|
||||||
|
{ name: 'tablet', width: 768, height: 1024 },
|
||||||
|
{ name: 'desktop', width: 1280, height: 720 },
|
||||||
|
];
|
||||||
|
const originalViewport = page.viewportSize();
|
||||||
|
const results: string[] = [];
|
||||||
|
|
||||||
|
for (const vp of viewports) {
|
||||||
|
await page.setViewportSize({ width: vp.width, height: vp.height });
|
||||||
|
const path = `${prefix}-${vp.name}.png`;
|
||||||
|
await page.screenshot({ path, fullPage: true });
|
||||||
|
results.push(`${vp.name} (${vp.width}x${vp.height}): ${path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore original viewport
|
||||||
|
if (originalViewport) {
|
||||||
|
await page.setViewportSize(originalViewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Chain ─────────────────────────────────────────
|
||||||
|
case 'chain': {
|
||||||
|
// Read JSON array from args[0] (if provided) or expect it was passed as body
|
||||||
|
const jsonStr = args[0];
|
||||||
|
if (!jsonStr) throw new Error('Usage: echo \'[["goto","url"],["text"]]\' | browse chain');
|
||||||
|
|
||||||
|
let commands: string[][];
|
||||||
|
try {
|
||||||
|
commands = JSON.parse(jsonStr);
|
||||||
|
} catch {
|
||||||
|
throw new Error('Invalid JSON. Expected: [["command", "arg1", "arg2"], ...]');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(commands)) throw new Error('Expected JSON array of commands');
|
||||||
|
|
||||||
|
const results: string[] = [];
|
||||||
|
// Import handlers dynamically to avoid circular deps
|
||||||
|
const { handleReadCommand } = await import('./read-commands');
|
||||||
|
const { handleWriteCommand } = await import('./write-commands');
|
||||||
|
|
||||||
|
for (const cmd of commands) {
|
||||||
|
const [name, ...cmdArgs] = cmd;
|
||||||
|
try {
|
||||||
|
// Try each command type
|
||||||
|
let result: string;
|
||||||
|
try {
|
||||||
|
result = await handleWriteCommand(name, cmdArgs, bm);
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
result = await handleReadCommand(name, cmdArgs, bm);
|
||||||
|
} catch {
|
||||||
|
result = await handleMetaCommand(name, cmdArgs, bm, shutdown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results.push(`[${name}] ${result}`);
|
||||||
|
} catch (err: any) {
|
||||||
|
results.push(`[${name}] ERROR: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Diff ──────────────────────────────────────────
|
||||||
|
case 'diff': {
|
||||||
|
const [url1, url2] = args;
|
||||||
|
if (!url1 || !url2) throw new Error('Usage: browse diff <url1> <url2>');
|
||||||
|
|
||||||
|
// Get text from URL1
|
||||||
|
const page = bm.getPage();
|
||||||
|
await page.goto(url1, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
||||||
|
const text1 = await page.evaluate(() => {
|
||||||
|
const body = document.body;
|
||||||
|
if (!body) return '';
|
||||||
|
const clone = body.cloneNode(true) as HTMLElement;
|
||||||
|
clone.querySelectorAll('script, style, noscript, svg').forEach(el => el.remove());
|
||||||
|
return clone.innerText.split('\n').map(l => l.trim()).filter(l => l).join('\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get text from URL2
|
||||||
|
await page.goto(url2, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
||||||
|
const text2 = await page.evaluate(() => {
|
||||||
|
const body = document.body;
|
||||||
|
if (!body) return '';
|
||||||
|
const clone = body.cloneNode(true) as HTMLElement;
|
||||||
|
clone.querySelectorAll('script, style, noscript, svg').forEach(el => el.remove());
|
||||||
|
return clone.innerText.split('\n').map(l => l.trim()).filter(l => l).join('\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
const changes = Diff.diffLines(text1, text2);
|
||||||
|
const output: string[] = [`--- ${url1}`, `+++ ${url2}`, ''];
|
||||||
|
|
||||||
|
for (const part of changes) {
|
||||||
|
const prefix = part.added ? '+' : part.removed ? '-' : ' ';
|
||||||
|
const lines = part.value.split('\n').filter(l => l.length > 0);
|
||||||
|
for (const line of lines) {
|
||||||
|
output.push(`${prefix} ${line}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown meta command: ${command}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
/**
|
||||||
|
* Read commands — extract data from pages without side effects
|
||||||
|
*
|
||||||
|
* text, html, links, forms, accessibility, js, eval, css, attrs,
|
||||||
|
* console, network, cookies, storage, perf
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { BrowserManager } from './browser-manager';
|
||||||
|
import { consoleBuffer, networkBuffer } from './buffers';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
export async function handleReadCommand(
|
||||||
|
command: string,
|
||||||
|
args: string[],
|
||||||
|
bm: BrowserManager
|
||||||
|
): Promise<string> {
|
||||||
|
const page = bm.getPage();
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case 'text': {
|
||||||
|
return await page.evaluate(() => {
|
||||||
|
const body = document.body;
|
||||||
|
if (!body) return '';
|
||||||
|
const clone = body.cloneNode(true) as HTMLElement;
|
||||||
|
clone.querySelectorAll('script, style, noscript, svg').forEach(el => el.remove());
|
||||||
|
return clone.innerText
|
||||||
|
.split('\n')
|
||||||
|
.map(line => line.trim())
|
||||||
|
.filter(line => line.length > 0)
|
||||||
|
.join('\n');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'html': {
|
||||||
|
const selector = args[0];
|
||||||
|
if (selector) {
|
||||||
|
return await page.innerHTML(selector);
|
||||||
|
}
|
||||||
|
return await page.content();
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'links': {
|
||||||
|
const links = await page.evaluate(() =>
|
||||||
|
[...document.querySelectorAll('a[href]')].map(a => ({
|
||||||
|
text: a.textContent?.trim().slice(0, 120) || '',
|
||||||
|
href: (a as HTMLAnchorElement).href,
|
||||||
|
})).filter(l => l.text && l.href)
|
||||||
|
);
|
||||||
|
return links.map(l => `${l.text} → ${l.href}`).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'forms': {
|
||||||
|
const forms = await page.evaluate(() => {
|
||||||
|
return [...document.querySelectorAll('form')].map((form, i) => {
|
||||||
|
const fields = [...form.querySelectorAll('input, select, textarea')].map(el => {
|
||||||
|
const input = el as HTMLInputElement;
|
||||||
|
return {
|
||||||
|
tag: el.tagName.toLowerCase(),
|
||||||
|
type: input.type || undefined,
|
||||||
|
name: input.name || undefined,
|
||||||
|
id: input.id || undefined,
|
||||||
|
placeholder: input.placeholder || undefined,
|
||||||
|
required: input.required || undefined,
|
||||||
|
value: input.value || undefined,
|
||||||
|
options: el.tagName === 'SELECT'
|
||||||
|
? [...(el as HTMLSelectElement).options].map(o => ({ value: o.value, text: o.text }))
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
index: i,
|
||||||
|
action: form.action || undefined,
|
||||||
|
method: form.method || 'get',
|
||||||
|
id: form.id || undefined,
|
||||||
|
fields,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return JSON.stringify(forms, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'accessibility': {
|
||||||
|
const snapshot = await page.locator("body").ariaSnapshot();
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'js': {
|
||||||
|
const expr = args[0];
|
||||||
|
if (!expr) throw new Error('Usage: browse js <expression>');
|
||||||
|
const result = await page.evaluate(expr);
|
||||||
|
return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'eval': {
|
||||||
|
const filePath = args[0];
|
||||||
|
if (!filePath) throw new Error('Usage: browse eval <js-file>');
|
||||||
|
if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
||||||
|
const code = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
const result = await page.evaluate(code);
|
||||||
|
return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'css': {
|
||||||
|
const [selector, property] = args;
|
||||||
|
if (!selector || !property) throw new Error('Usage: browse css <selector> <property>');
|
||||||
|
const value = await page.evaluate(
|
||||||
|
([sel, prop]) => {
|
||||||
|
const el = document.querySelector(sel);
|
||||||
|
if (!el) return `Element not found: ${sel}`;
|
||||||
|
return getComputedStyle(el).getPropertyValue(prop);
|
||||||
|
},
|
||||||
|
[selector, property]
|
||||||
|
);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'attrs': {
|
||||||
|
const selector = args[0];
|
||||||
|
if (!selector) throw new Error('Usage: browse attrs <selector>');
|
||||||
|
const attrs = await page.evaluate((sel) => {
|
||||||
|
const el = document.querySelector(sel);
|
||||||
|
if (!el) return `Element not found: ${sel}`;
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
for (const attr of el.attributes) {
|
||||||
|
result[attr.name] = attr.value;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, selector);
|
||||||
|
return typeof attrs === 'string' ? attrs : JSON.stringify(attrs, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'console': {
|
||||||
|
if (args[0] === '--clear') {
|
||||||
|
consoleBuffer.length = 0;
|
||||||
|
return 'Console buffer cleared.';
|
||||||
|
}
|
||||||
|
if (consoleBuffer.length === 0) return '(no console messages)';
|
||||||
|
return consoleBuffer.map(e =>
|
||||||
|
`[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
|
||||||
|
).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'network': {
|
||||||
|
if (args[0] === '--clear') {
|
||||||
|
networkBuffer.length = 0;
|
||||||
|
return 'Network buffer cleared.';
|
||||||
|
}
|
||||||
|
if (networkBuffer.length === 0) return '(no network requests)';
|
||||||
|
return networkBuffer.map(e =>
|
||||||
|
`${e.method} ${e.url} → ${e.status || 'pending'} (${e.duration || '?'}ms, ${e.size || '?'}B)`
|
||||||
|
).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'cookies': {
|
||||||
|
const cookies = await page.context().cookies();
|
||||||
|
return JSON.stringify(cookies, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'storage': {
|
||||||
|
if (args[0] === 'set' && args[1]) {
|
||||||
|
const key = args[1];
|
||||||
|
const value = args[2] || '';
|
||||||
|
await page.evaluate(([k, v]) => localStorage.setItem(k, v), [key, value]);
|
||||||
|
return `Set localStorage["${key}"] = "${value}"`;
|
||||||
|
}
|
||||||
|
const storage = await page.evaluate(() => ({
|
||||||
|
localStorage: { ...localStorage },
|
||||||
|
sessionStorage: { ...sessionStorage },
|
||||||
|
}));
|
||||||
|
return JSON.stringify(storage, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'perf': {
|
||||||
|
const timings = await page.evaluate(() => {
|
||||||
|
const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
||||||
|
if (!nav) return 'No navigation timing data available.';
|
||||||
|
return {
|
||||||
|
dns: Math.round(nav.domainLookupEnd - nav.domainLookupStart),
|
||||||
|
tcp: Math.round(nav.connectEnd - nav.connectStart),
|
||||||
|
ssl: Math.round(nav.secureConnectionStart > 0 ? nav.connectEnd - nav.secureConnectionStart : 0),
|
||||||
|
ttfb: Math.round(nav.responseStart - nav.requestStart),
|
||||||
|
download: Math.round(nav.responseEnd - nav.responseStart),
|
||||||
|
domParse: Math.round(nav.domInteractive - nav.responseEnd),
|
||||||
|
domReady: Math.round(nav.domContentLoadedEventEnd - nav.startTime),
|
||||||
|
load: Math.round(nav.loadEventEnd - nav.startTime),
|
||||||
|
total: Math.round(nav.loadEventEnd - nav.startTime),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (typeof timings === 'string') return timings;
|
||||||
|
return Object.entries(timings)
|
||||||
|
.map(([k, v]) => `${k.padEnd(12)} ${v}ms`)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown read command: ${command}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* Write commands — navigate and interact with pages (side effects)
|
||||||
|
*
|
||||||
|
* goto, back, forward, reload, click, fill, select, hover, type,
|
||||||
|
* press, scroll, wait, viewport, cookie, header, useragent
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { BrowserManager } from './browser-manager';
|
||||||
|
|
||||||
|
export async function handleWriteCommand(
|
||||||
|
command: string,
|
||||||
|
args: string[],
|
||||||
|
bm: BrowserManager
|
||||||
|
): Promise<string> {
|
||||||
|
const page = bm.getPage();
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case 'goto': {
|
||||||
|
const url = args[0];
|
||||||
|
if (!url) throw new Error('Usage: browse goto <url>');
|
||||||
|
const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
||||||
|
const status = response?.status() || 'unknown';
|
||||||
|
return `Navigated to ${url} (${status})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'back': {
|
||||||
|
await page.goBack({ waitUntil: 'domcontentloaded', timeout: 15000 });
|
||||||
|
return `Back → ${page.url()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'forward': {
|
||||||
|
await page.goForward({ waitUntil: 'domcontentloaded', timeout: 15000 });
|
||||||
|
return `Forward → ${page.url()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'reload': {
|
||||||
|
await page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 });
|
||||||
|
return `Reloaded ${page.url()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'click': {
|
||||||
|
const selector = args[0];
|
||||||
|
if (!selector) throw new Error('Usage: browse click <selector>');
|
||||||
|
await page.click(selector, { timeout: 5000 });
|
||||||
|
// Wait briefly for any navigation/DOM update
|
||||||
|
await page.waitForLoadState('domcontentloaded').catch(() => {});
|
||||||
|
return `Clicked ${selector} → now at ${page.url()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'fill': {
|
||||||
|
const [selector, ...valueParts] = args;
|
||||||
|
const value = valueParts.join(' ');
|
||||||
|
if (!selector || !value) throw new Error('Usage: browse fill <selector> <value>');
|
||||||
|
await page.fill(selector, value, { timeout: 5000 });
|
||||||
|
return `Filled ${selector}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'select': {
|
||||||
|
const [selector, ...valueParts] = args;
|
||||||
|
const value = valueParts.join(' ');
|
||||||
|
if (!selector || !value) throw new Error('Usage: browse select <selector> <value>');
|
||||||
|
await page.selectOption(selector, value, { timeout: 5000 });
|
||||||
|
return `Selected "${value}" in ${selector}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'hover': {
|
||||||
|
const selector = args[0];
|
||||||
|
if (!selector) throw new Error('Usage: browse hover <selector>');
|
||||||
|
await page.hover(selector, { timeout: 5000 });
|
||||||
|
return `Hovered ${selector}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'type': {
|
||||||
|
const text = args.join(' ');
|
||||||
|
if (!text) throw new Error('Usage: browse type <text>');
|
||||||
|
await page.keyboard.type(text);
|
||||||
|
return `Typed "${text}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'press': {
|
||||||
|
const key = args[0];
|
||||||
|
if (!key) throw new Error('Usage: browse press <key> (e.g., Enter, Tab, Escape)');
|
||||||
|
await page.keyboard.press(key);
|
||||||
|
return `Pressed ${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'scroll': {
|
||||||
|
const selector = args[0];
|
||||||
|
if (selector) {
|
||||||
|
await page.locator(selector).scrollIntoViewIfNeeded({ timeout: 5000 });
|
||||||
|
return `Scrolled ${selector} into view`;
|
||||||
|
}
|
||||||
|
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||||
|
return 'Scrolled to bottom';
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'wait': {
|
||||||
|
const selector = args[0];
|
||||||
|
if (!selector) throw new Error('Usage: browse wait <selector>');
|
||||||
|
const timeout = args[1] ? parseInt(args[1], 10) : 15000;
|
||||||
|
await page.waitForSelector(selector, { timeout });
|
||||||
|
return `Element ${selector} appeared`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'viewport': {
|
||||||
|
const size = args[0];
|
||||||
|
if (!size || !size.includes('x')) throw new Error('Usage: browse viewport <WxH> (e.g., 375x812)');
|
||||||
|
const [w, h] = size.split('x').map(Number);
|
||||||
|
await bm.setViewport(w, h);
|
||||||
|
return `Viewport set to ${w}x${h}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'cookie': {
|
||||||
|
const cookieStr = args[0];
|
||||||
|
if (!cookieStr || !cookieStr.includes('=')) throw new Error('Usage: browse cookie <name>=<value>');
|
||||||
|
const eq = cookieStr.indexOf('=');
|
||||||
|
const name = cookieStr.slice(0, eq);
|
||||||
|
const value = cookieStr.slice(eq + 1);
|
||||||
|
const url = new URL(page.url());
|
||||||
|
await page.context().addCookies([{
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
domain: url.hostname,
|
||||||
|
path: '/',
|
||||||
|
}]);
|
||||||
|
return `Cookie set: ${name}=${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'header': {
|
||||||
|
const headerStr = args[0];
|
||||||
|
if (!headerStr || !headerStr.includes(':')) throw new Error('Usage: browse header <name>:<value>');
|
||||||
|
const sep = headerStr.indexOf(':');
|
||||||
|
const name = headerStr.slice(0, sep).trim();
|
||||||
|
const value = headerStr.slice(sep + 1).trim();
|
||||||
|
await bm.setExtraHeader(name, value);
|
||||||
|
return `Header set: ${name}: ${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'useragent': {
|
||||||
|
const ua = args.join(' ');
|
||||||
|
if (!ua) throw new Error('Usage: browse useragent <string>');
|
||||||
|
bm.setUserAgent(ua);
|
||||||
|
return `User agent set (applies on next restart): ${ua}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown write command: ${command}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user