/** * 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 ): 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[] = []; // 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 '); // 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}`); } }