/** * 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 ): Promise { 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 '); 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 '); // 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}`); } }