/**
* Meta commands — tabs, server control, screenshots, chain, diff, snapshot
*/
import type { BrowserManager } from './browser-manager';
import { handleSnapshot } from './snapshot';
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[] = [];
const { handleReadCommand } = await import('./read-commands');
const { handleWriteCommand } = await import('./write-commands');
const WRITE_SET = new Set(['goto','back','forward','reload','click','fill','select','hover','type','press','scroll','wait','viewport','cookie','header','useragent']);
const READ_SET = new Set(['text','html','links','forms','accessibility','js','eval','css','attrs','console','network','cookies','storage','perf']);
for (const cmd of commands) {
const [name, ...cmdArgs] = cmd;
try {
let result: string;
if (WRITE_SET.has(name)) result = await handleWriteCommand(name, cmdArgs, bm);
else if (READ_SET.has(name)) result = await handleReadCommand(name, cmdArgs, bm);
else 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');
}
// ─── Snapshot ─────────────────────────────────────
case 'snapshot': {
return await handleSnapshot(args, bm);
}
default:
throw new Error(`Unknown meta command: ${command}`);
}
}