diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a95cb05 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 100, + "tabWidth": 2 +} diff --git a/package.json b/package.json index cdfbb30..8c13e80 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,10 @@ "scripts": { "build": "tsc", "start": "node dist/index.js", - "dev": "tsx src/index.ts" + "dev": "tsx src/index.ts", + "typecheck": "tsc --noEmit", + "format": "prettier --write 'src/**/*.ts'", + "format:check": "prettier --check 'src/**/*.ts'" }, "keywords": [ "browser", @@ -28,6 +31,7 @@ }, "devDependencies": { "@types/node": "^20.10.0", + "prettier": "^3.7.4", "tsx": "^4.6.0", "typescript": "^5.3.0" } diff --git a/src/actions.ts b/src/actions.ts index 883d99c..f34aed1 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -113,10 +113,7 @@ interface SnapshotData { /** * Execute a command and return a response */ -export async function executeCommand( - command: Command, - browser: BrowserManager -): Promise { +export async function executeCommand(command: Command, browser: BrowserManager): Promise { try { switch (command.action) { case 'launch': @@ -375,56 +372,47 @@ async function handleNavigate( await page.goto(command.url, { waitUntil: command.waitUntil ?? 'load', }); - + return successResponse(command.id, { url: page.url(), title: await page.title(), }); } -async function handleClick( - command: ClickCommand, - browser: BrowserManager -): Promise { +async function handleClick(command: ClickCommand, browser: BrowserManager): Promise { const page = browser.getPage(); await page.click(command.selector, { button: command.button, clickCount: command.clickCount, delay: command.delay, }); - + return successResponse(command.id, { clicked: true }); } -async function handleType( - command: TypeCommand, - browser: BrowserManager -): Promise { +async function handleType(command: TypeCommand, browser: BrowserManager): Promise { const page = browser.getPage(); - + if (command.clear) { await page.fill(command.selector, ''); } - + await page.type(command.selector, command.text, { delay: command.delay, }); - + return successResponse(command.id, { typed: true }); } -async function handlePress( - command: PressCommand, - browser: BrowserManager -): Promise { +async function handlePress(command: PressCommand, browser: BrowserManager): Promise { const page = browser.getPage(); - + if (command.selector) { await page.press(command.selector, command.key); } else { await page.keyboard.press(command.key); } - + return successResponse(command.id, { pressed: true }); } @@ -433,21 +421,21 @@ async function handleScreenshot( browser: BrowserManager ): Promise> { const page = browser.getPage(); - + const options: Parameters[0] = { fullPage: command.fullPage, type: command.format ?? 'png', }; - + if (command.format === 'jpeg' && command.quality !== undefined) { options.quality = command.quality; } - + let target: Page | ReturnType = page; if (command.selector) { target = page.locator(command.selector); } - + if (command.path) { await target.screenshot({ ...options, path: command.path }); return successResponse(command.id, { path: command.path }); @@ -464,7 +452,7 @@ async function handleSnapshot( const page = browser.getPage(); // Use ariaSnapshot which returns a string representation of the accessibility tree const snapshot = await page.locator(':root').ariaSnapshot(); - + return successResponse(command.id, { snapshot: snapshot ?? 'Empty page', }); @@ -475,19 +463,16 @@ async function handleEvaluate( browser: BrowserManager ): Promise> { const page = browser.getPage(); - + // Evaluate the script directly as a string expression const result = await page.evaluate(command.script); - + return successResponse(command.id, { result }); } -async function handleWait( - command: WaitCommand, - browser: BrowserManager -): Promise { +async function handleWait(command: WaitCommand, browser: BrowserManager): Promise { const page = browser.getPage(); - + if (command.selector) { await page.waitForSelector(command.selector, { state: command.state ?? 'visible', @@ -499,30 +484,30 @@ async function handleWait( // Default: wait for load state await page.waitForLoadState('load'); } - + return successResponse(command.id, { waited: true }); } -async function handleScroll( - command: ScrollCommand, - browser: BrowserManager -): Promise { +async function handleScroll(command: ScrollCommand, browser: BrowserManager): Promise { const page = browser.getPage(); - + if (command.selector) { const element = page.locator(command.selector); await element.scrollIntoViewIfNeeded(); - + if (command.x !== undefined || command.y !== undefined) { - await element.evaluate((el, { x, y }) => { - el.scrollBy(x ?? 0, y ?? 0); - }, { x: command.x, y: command.y }); + await element.evaluate( + (el, { x, y }) => { + el.scrollBy(x ?? 0, y ?? 0); + }, + { x: command.x, y: command.y } + ); } } else { // Scroll the page let deltaX = command.x ?? 0; let deltaY = command.y ?? 0; - + if (command.direction) { const amount = command.amount ?? 100; switch (command.direction) { @@ -540,32 +525,26 @@ async function handleScroll( break; } } - + await page.evaluate(`window.scrollBy(${deltaX}, ${deltaY})`); } - + return successResponse(command.id, { scrolled: true }); } -async function handleSelect( - command: SelectCommand, - browser: BrowserManager -): Promise { +async function handleSelect(command: SelectCommand, browser: BrowserManager): Promise { const page = browser.getPage(); const values = Array.isArray(command.values) ? command.values : [command.values]; - + await page.selectOption(command.selector, values); - + return successResponse(command.id, { selected: values }); } -async function handleHover( - command: HoverCommand, - browser: BrowserManager -): Promise { +async function handleHover(command: HoverCommand, browser: BrowserManager): Promise { const page = browser.getPage(); await page.hover(command.selector); - + return successResponse(command.id, { hovered: true }); } @@ -574,14 +553,14 @@ async function handleContent( browser: BrowserManager ): Promise> { const page = browser.getPage(); - + let html: string; if (command.selector) { html = await page.locator(command.selector).innerHTML(); } else { html = await page.content(); } - + return successResponse(command.id, { html }); } @@ -642,37 +621,25 @@ async function handleWindowNew( // New handlers for enhanced Playwright parity -async function handleFill( - command: FillCommand, - browser: BrowserManager -): Promise { +async function handleFill(command: FillCommand, browser: BrowserManager): Promise { const frame = browser.getFrame(); await frame.fill(command.selector, command.value); return successResponse(command.id, { filled: true }); } -async function handleCheck( - command: CheckCommand, - browser: BrowserManager -): Promise { +async function handleCheck(command: CheckCommand, browser: BrowserManager): Promise { const frame = browser.getFrame(); await frame.check(command.selector); return successResponse(command.id, { checked: true }); } -async function handleUncheck( - command: UncheckCommand, - browser: BrowserManager -): Promise { +async function handleUncheck(command: UncheckCommand, browser: BrowserManager): Promise { const frame = browser.getFrame(); await frame.uncheck(command.selector); return successResponse(command.id, { unchecked: true }); } -async function handleUpload( - command: UploadCommand, - browser: BrowserManager -): Promise { +async function handleUpload(command: UploadCommand, browser: BrowserManager): Promise { const frame = browser.getFrame(); const files = Array.isArray(command.files) ? command.files : [command.files]; await frame.setInputFiles(command.selector, files); @@ -688,28 +655,19 @@ async function handleDoubleClick( return successResponse(command.id, { clicked: true }); } -async function handleFocus( - command: FocusCommand, - browser: BrowserManager -): Promise { +async function handleFocus(command: FocusCommand, browser: BrowserManager): Promise { const frame = browser.getFrame(); await frame.focus(command.selector); return successResponse(command.id, { focused: true }); } -async function handleDrag( - command: DragCommand, - browser: BrowserManager -): Promise { +async function handleDrag(command: DragCommand, browser: BrowserManager): Promise { const frame = browser.getFrame(); await frame.dragAndDrop(command.source, command.target); return successResponse(command.id, { dragged: true }); } -async function handleFrame( - command: FrameCommand, - browser: BrowserManager -): Promise { +async function handleFrame(command: FrameCommand, browser: BrowserManager): Promise { await browser.switchToFrame({ selector: command.selector, name: command.name, @@ -732,7 +690,7 @@ async function handleGetByRole( ): Promise { const page = browser.getPage(); const locator = page.getByRole(command.role as any, { name: command.name }); - + switch (command.subaction) { case 'click': await locator.click(); @@ -755,7 +713,7 @@ async function handleGetByText( ): Promise { const page = browser.getPage(); const locator = page.getByText(command.text, { exact: command.exact }); - + switch (command.subaction) { case 'click': await locator.click(); @@ -772,7 +730,7 @@ async function handleGetByLabel( ): Promise { const page = browser.getPage(); const locator = page.getByLabel(command.label); - + switch (command.subaction) { case 'click': await locator.click(); @@ -792,7 +750,7 @@ async function handleGetByPlaceholder( ): Promise { const page = browser.getPage(); const locator = page.getByPlaceholder(command.placeholder); - + switch (command.subaction) { case 'click': await locator.click(); @@ -839,11 +797,9 @@ async function handleStorageGet( ): Promise { const page = browser.getPage(); const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage'; - + if (command.key) { - const value = await page.evaluate( - `${storageType}.getItem(${JSON.stringify(command.key)})` - ); + const value = await page.evaluate(`${storageType}.getItem(${JSON.stringify(command.key)})`); return successResponse(command.id, { key: command.key, value }); } else { const data = await page.evaluate(` @@ -867,7 +823,7 @@ async function handleStorageSet( ): Promise { const page = browser.getPage(); const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage'; - + await page.evaluate( `${storageType}.setItem(${JSON.stringify(command.key)}, ${JSON.stringify(command.value)})` ); @@ -880,23 +836,17 @@ async function handleStorageClear( ): Promise { const page = browser.getPage(); const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage'; - + await page.evaluate(`${storageType}.clear()`); return successResponse(command.id, { cleared: true }); } -async function handleDialog( - command: DialogCommand, - browser: BrowserManager -): Promise { +async function handleDialog(command: DialogCommand, browser: BrowserManager): Promise { browser.setDialogHandler(command.response, command.promptText); return successResponse(command.id, { handler: 'set', response: command.response }); } -async function handlePdf( - command: PdfCommand, - browser: BrowserManager -): Promise { +async function handlePdf(command: PdfCommand, browser: BrowserManager): Promise { const page = browser.getPage(); await page.pdf({ path: command.path, @@ -907,10 +857,7 @@ async function handlePdf( // Network & Request handlers -async function handleRoute( - command: RouteCommand, - browser: BrowserManager -): Promise { +async function handleRoute(command: RouteCommand, browser: BrowserManager): Promise { await browser.addRoute(command.url, { response: command.response, abort: command.abort, @@ -934,10 +881,10 @@ async function handleRequests( browser.clearRequests(); return successResponse(command.id, { cleared: true }); } - + // Start tracking if not already browser.startRequestTracking(); - + const requests = browser.getRequests(command.filter); return successResponse(command.id, { requests }); } @@ -947,14 +894,14 @@ async function handleDownload( browser: BrowserManager ): Promise { const page = browser.getPage(); - + const [download] = await Promise.all([ page.waitForEvent('download'), page.click(command.selector), ]); - + await download.saveAs(command.path); - return successResponse(command.id, { + return successResponse(command.id, { path: command.path, suggestedFilename: download.suggestedFilename(), }); @@ -965,7 +912,7 @@ async function handleGeolocation( browser: BrowserManager ): Promise { await browser.setGeolocation(command.latitude, command.longitude, command.accuracy); - return successResponse(command.id, { + return successResponse(command.id, { latitude: command.latitude, longitude: command.longitude, }); @@ -976,7 +923,7 @@ async function handlePermissions( browser: BrowserManager ): Promise { await browser.setPermissions(command.permissions, command.grant); - return successResponse(command.id, { + return successResponse(command.id, { permissions: command.permissions, granted: command.grant, }); @@ -987,7 +934,7 @@ async function handleViewport( browser: BrowserManager ): Promise { await browser.setViewport(command.width, command.height); - return successResponse(command.id, { + return successResponse(command.id, { width: command.width, height: command.height, }); @@ -1000,25 +947,22 @@ async function handleUserAgent( const page = browser.getPage(); const context = page.context(); // Note: Can't change user agent after context is created, but we can for new pages - return successResponse(command.id, { + return successResponse(command.id, { note: 'User agent can only be set at launch time. Use device command instead.', }); } -async function handleDevice( - command: DeviceCommand, - browser: BrowserManager -): Promise { +async function handleDevice(command: DeviceCommand, browser: BrowserManager): Promise { const device = browser.getDevice(command.device); if (!device) { const available = browser.listDevices().slice(0, 10).join(', '); throw new Error(`Unknown device: ${command.device}. Available: ${available}...`); } - + // Apply device viewport await browser.setViewport(device.viewport.width, device.viewport.height); - - return successResponse(command.id, { + + return successResponse(command.id, { device: command.device, viewport: device.viewport, userAgent: device.userAgent, @@ -1078,10 +1022,7 @@ async function handleGetAttribute( return successResponse(command.id, { attribute: command.attribute, value }); } -async function handleGetText( - command: GetTextCommand, - browser: BrowserManager -): Promise { +async function handleGetText(command: GetTextCommand, browser: BrowserManager): Promise { const page = browser.getPage(); const text = await page.textContent(command.selector); return successResponse(command.id, { text }); @@ -1114,10 +1055,7 @@ async function handleIsChecked( return successResponse(command.id, { checked }); } -async function handleCount( - command: CountCommand, - browser: BrowserManager -): Promise { +async function handleCount(command: CountCommand, browser: BrowserManager): Promise { const page = browser.getPage(); const count = await page.locator(command.selector).count(); return successResponse(command.id, { count }); @@ -1140,7 +1078,7 @@ async function handleVideoStart( ): Promise { // Video recording requires context-level setup at launch // For now, return a note about this limitation - return successResponse(command.id, { + return successResponse(command.id, { note: 'Video recording must be enabled at browser launch. Use --video flag when starting.', path: command.path, }); @@ -1187,14 +1125,11 @@ async function handleHarStart( return successResponse(command.id, { started: true }); } -async function handleHarStop( - command: HarStopCommand, - browser: BrowserManager -): Promise { +async function handleHarStop(command: HarStopCommand, browser: BrowserManager): Promise { // HAR recording is handled at context level // For now, we save tracked requests as a simplified HAR-like format const requests = browser.getRequests(); - return successResponse(command.id, { + return successResponse(command.id, { path: command.path, requestCount: requests.length, }); @@ -1213,35 +1148,29 @@ async function handleStateLoad( browser: BrowserManager ): Promise { // Storage state is loaded at context creation - return successResponse(command.id, { + return successResponse(command.id, { note: 'Storage state must be loaded at browser launch. Use --state flag.', path: command.path, }); } -async function handleConsole( - command: ConsoleCommand, - browser: BrowserManager -): Promise { +async function handleConsole(command: ConsoleCommand, browser: BrowserManager): Promise { if (command.clear) { browser.clearConsoleMessages(); return successResponse(command.id, { cleared: true }); } - + browser.startConsoleTracking(); const messages = browser.getConsoleMessages(); return successResponse(command.id, { messages }); } -async function handleErrors( - command: ErrorsCommand, - browser: BrowserManager -): Promise { +async function handleErrors(command: ErrorsCommand, browser: BrowserManager): Promise { if (command.clear) { browser.clearPageErrors(); return successResponse(command.id, { cleared: true }); } - + browser.startErrorTracking(); const errors = browser.getPageErrors(); return successResponse(command.id, { errors }); @@ -1256,25 +1185,19 @@ async function handleKeyboard( return successResponse(command.id, { pressed: command.keys }); } -async function handleWheel( - command: WheelCommand, - browser: BrowserManager -): Promise { +async function handleWheel(command: WheelCommand, browser: BrowserManager): Promise { const page = browser.getPage(); - + if (command.selector) { const element = page.locator(command.selector); await element.hover(); } - + await page.mouse.wheel(command.deltaX ?? 0, command.deltaY ?? 0); return successResponse(command.id, { scrolled: true }); } -async function handleTap( - command: TapCommand, - browser: BrowserManager -): Promise { +async function handleTap(command: TapCommand, browser: BrowserManager): Promise { const page = browser.getPage(); await page.tap(command.selector); return successResponse(command.id, { tapped: true }); @@ -1285,7 +1208,7 @@ async function handleClipboard( browser: BrowserManager ): Promise { const page = browser.getPage(); - + switch (command.operation) { case 'copy': await page.keyboard.press('Control+c'); @@ -1310,10 +1233,7 @@ async function handleHighlight( return successResponse(command.id, { highlighted: true }); } -async function handleClear( - command: ClearCommand, - browser: BrowserManager -): Promise { +async function handleClear(command: ClearCommand, browser: BrowserManager): Promise { const page = browser.getPage(); await page.locator(command.selector).clear(); return successResponse(command.id, { cleared: true }); @@ -1400,13 +1320,13 @@ async function handleAddScript( browser: BrowserManager ): Promise { const page = browser.getPage(); - + if (command.content) { await page.addScriptTag({ content: command.content }); } else if (command.url) { await page.addScriptTag({ url: command.url }); } - + return successResponse(command.id, { added: true }); } @@ -1415,13 +1335,13 @@ async function handleAddStyle( browser: BrowserManager ): Promise { const page = browser.getPage(); - + if (command.content) { await page.addStyleTag({ content: command.content }); } else if (command.url) { await page.addStyleTag({ url: command.url }); } - + return successResponse(command.id, { added: true }); } @@ -1439,18 +1359,12 @@ async function handleEmulateMedia( return successResponse(command.id, { emulated: true }); } -async function handleOffline( - command: OfflineCommand, - browser: BrowserManager -): Promise { +async function handleOffline(command: OfflineCommand, browser: BrowserManager): Promise { await browser.setOffline(command.offline); return successResponse(command.id, { offline: command.offline }); } -async function handleHeaders( - command: HeadersCommand, - browser: BrowserManager -): Promise { +async function handleHeaders(command: HeadersCommand, browser: BrowserManager): Promise { await browser.setExtraHeaders(command.headers); return successResponse(command.id, { set: true }); } @@ -1470,7 +1384,7 @@ async function handleGetByAltText( ): Promise { const page = browser.getPage(); const locator = page.getByAltText(command.text, { exact: command.exact }); - + switch (command.subaction) { case 'click': await locator.click(); @@ -1487,7 +1401,7 @@ async function handleGetByTitle( ): Promise { const page = browser.getPage(); const locator = page.getByTitle(command.text, { exact: command.exact }); - + switch (command.subaction) { case 'click': await locator.click(); @@ -1504,7 +1418,7 @@ async function handleGetByTestId( ): Promise { const page = browser.getPage(); const locator = page.getByTestId(command.testId); - + switch (command.subaction) { case 'click': await locator.click(); @@ -1521,14 +1435,11 @@ async function handleGetByTestId( } } -async function handleNth( - command: NthCommand, - browser: BrowserManager -): Promise { +async function handleNth(command: NthCommand, browser: BrowserManager): Promise { const page = browser.getPage(); const base = page.locator(command.selector); const locator = command.index === -1 ? base.last() : base.nth(command.index); - + switch (command.subaction) { case 'click': await locator.click(); @@ -1583,18 +1494,15 @@ async function handleTimezone( // This is a limitation - it sets for the current context const page = browser.getPage(); await page.context().setGeolocation({ latitude: 0, longitude: 0 }); // Trigger context awareness - return successResponse(command.id, { + return successResponse(command.id, { note: 'Timezone must be set at browser launch. Use --timezone flag.', timezone: command.timezone, }); } -async function handleLocale( - command: LocaleCommand, - browser: BrowserManager -): Promise { +async function handleLocale(command: LocaleCommand, browser: BrowserManager): Promise { // Locale must be set at context creation - return successResponse(command.id, { + return successResponse(command.id, { note: 'Locale must be set at browser launch. Use --locale flag.', locale: command.locale, }); @@ -1630,10 +1538,7 @@ async function handleMouseDown( return successResponse(command.id, { down: true }); } -async function handleMouseUp( - command: MouseUpCommand, - browser: BrowserManager -): Promise { +async function handleMouseUp(command: MouseUpCommand, browser: BrowserManager): Promise { const page = browser.getPage(); await page.mouse.up({ button: command.button ?? 'left' }); return successResponse(command.id, { up: true }); @@ -1675,19 +1580,13 @@ async function handleAddInitScript( return successResponse(command.id, { added: true }); } -async function handleKeyDown( - command: KeyDownCommand, - browser: BrowserManager -): Promise { +async function handleKeyDown(command: KeyDownCommand, browser: BrowserManager): Promise { const page = browser.getPage(); await page.keyboard.down(command.key); return successResponse(command.id, { down: true, key: command.key }); } -async function handleKeyUp( - command: KeyUpCommand, - browser: BrowserManager -): Promise { +async function handleKeyUp(command: KeyUpCommand, browser: BrowserManager): Promise { const page = browser.getPage(); await page.keyboard.up(command.key); return successResponse(command.id, { up: true, key: command.key }); @@ -1717,16 +1616,16 @@ async function handleWaitForDownload( ): Promise { const page = browser.getPage(); const download = await page.waitForEvent('download', { timeout: command.timeout }); - + let filePath: string; if (command.path) { filePath = command.path; await download.saveAs(filePath); } else { - filePath = await download.path() || download.suggestedFilename(); + filePath = (await download.path()) || download.suggestedFilename(); } - - return successResponse(command.id, { + + return successResponse(command.id, { path: filePath, filename: download.suggestedFilename(), url: download.url(), @@ -1738,20 +1637,19 @@ async function handleResponseBody( browser: BrowserManager ): Promise { const page = browser.getPage(); - const response = await page.waitForResponse( - resp => resp.url().includes(command.url), - { timeout: command.timeout } - ); - + const response = await page.waitForResponse((resp) => resp.url().includes(command.url), { + timeout: command.timeout, + }); + const body = await response.text(); let parsed: unknown = body; - + try { parsed = JSON.parse(body); } catch { // Keep as string if not JSON } - + return successResponse(command.id, { url: response.url(), status: response.status(), diff --git a/src/browser.ts b/src/browser.ts index 6e3f547..a9c2f90 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1,4 +1,16 @@ -import { chromium, firefox, webkit, devices, type Browser, type BrowserContext, type Page, type Frame, type Dialog, type Request, type Route } from 'playwright'; +import { + chromium, + firefox, + webkit, + devices, + type Browser, + type BrowserContext, + type Page, + type Frame, + type Dialog, + type Request, + type Route, +} from 'playwright'; import type { LaunchCommand } from './types.js'; interface TrackedRequest { @@ -68,7 +80,7 @@ export class BrowserManager { */ async switchToFrame(options: { selector?: string; name?: string; url?: string }): Promise { const page = this.getPage(); - + if (options.selector) { const frameElement = await page.$(options.selector); if (!frameElement) { @@ -106,12 +118,12 @@ export class BrowserManager { */ setDialogHandler(response: 'accept' | 'dismiss', promptText?: string): void { const page = this.getPage(); - + // Remove existing handler if any if (this.dialogHandler) { page.removeListener('dialog', this.dialogHandler); } - + this.dialogHandler = async (dialog: Dialog) => { if (response === 'accept') { await dialog.accept(promptText); @@ -119,7 +131,7 @@ export class BrowserManager { await dialog.dismiss(); } }; - + page.on('dialog', this.dialogHandler); } @@ -155,7 +167,7 @@ export class BrowserManager { */ getRequests(filter?: string): TrackedRequest[] { if (filter) { - return this.trackedRequests.filter(r => r.url.includes(filter)); + return this.trackedRequests.filter((r) => r.url.includes(filter)); } return this.trackedRequests; } @@ -173,12 +185,17 @@ export class BrowserManager { async addRoute( url: string, options: { - response?: { status?: number; body?: string; contentType?: string; headers?: Record }; + response?: { + status?: number; + body?: string; + contentType?: string; + headers?: Record; + }; abort?: boolean; } ): Promise { const page = this.getPage(); - + const handler = async (route: Route) => { if (options.abort) { await route.abort(); @@ -193,7 +210,7 @@ export class BrowserManager { await route.continue(); } }; - + this.routes.set(url, handler); await page.route(url, handler); } @@ -203,7 +220,7 @@ export class BrowserManager { */ async removeRoute(url?: string): Promise { const page = this.getPage(); - + if (url) { const handler = this.routes.get(url); if (handler) { @@ -254,7 +271,7 @@ export class BrowserManager { /** * Get device descriptor */ - getDevice(deviceName: string): typeof devices[keyof typeof devices] | undefined { + getDevice(deviceName: string): (typeof devices)[keyof typeof devices] | undefined { return devices[deviceName as keyof typeof devices]; } @@ -420,11 +437,8 @@ export class BrowserManager { // Select browser type const browserType = options.browser ?? 'chromium'; - const launcher = browserType === 'firefox' - ? firefox - : browserType === 'webkit' - ? webkit - : chromium; + const launcher = + browserType === 'firefox' ? firefox : browserType === 'webkit' ? webkit : chromium; // Launch browser this.browser = await launcher.launch({ @@ -435,10 +449,10 @@ export class BrowserManager { const context = await this.browser.newContext({ viewport: options.viewport ?? { width: 1280, height: 720 }, }); - + // Set default timeout to 10 seconds (Playwright default is 30s) context.setDefaultTimeout(10000); - + this.contexts.push(context); // Create initial page @@ -466,7 +480,10 @@ export class BrowserManager { /** * Create a new window (new context) */ - async newWindow(viewport?: { width: number; height: number }): Promise<{ index: number; total: number }> { + async newWindow(viewport?: { + width: number; + height: number; + }): Promise<{ index: number; total: number }> { if (!this.browser) { throw new Error('Browser not launched'); } diff --git a/src/client.ts b/src/client.ts index 4d7dc67..36fd218 100644 --- a/src/client.ts +++ b/src/client.ts @@ -33,7 +33,7 @@ async function waitForSocket(maxAttempts = 30): Promise { debug('Socket found after', i * 100, 'ms'); return true; } - await new Promise(r => setTimeout(r, 100)); + await new Promise((r) => setTimeout(r, 100)); } debug('Socket not found after', maxAttempts * 100, 'ms'); return false; @@ -49,7 +49,7 @@ export async function ensureDaemon(): Promise { debug('Daemon already running'); return; } - + debug('Starting daemon...'); const daemonPath = path.join(__dirname, 'daemon.js'); const child = spawn(process.execPath, [daemonPath], { @@ -58,13 +58,13 @@ export async function ensureDaemon(): Promise { env: { ...process.env, VEB_DAEMON: '1', VEB_SESSION: session }, }); child.unref(); - + // Wait for socket to be created const ready = await waitForSocket(); if (!ready) { throw new Error('Failed to start daemon'); } - + debug(`Daemon started for session "${session}"`); } @@ -74,23 +74,23 @@ export async function ensureDaemon(): Promise { export async function sendCommand(command: Record): Promise { const socketPath = getSocketPath(); debug('Sending command:', JSON.stringify(command)); - + return new Promise((resolve, reject) => { let resolved = false; let buffer = ''; const startTime = Date.now(); - + const socket = net.createConnection(socketPath); - + socket.on('connect', () => { debug('Connected to daemon, sending command...'); socket.write(JSON.stringify(command) + '\n'); }); - + socket.on('data', (data) => { buffer += data.toString(); debug('Received data:', buffer.length, 'bytes'); - + // Try to parse complete JSON from buffer const newlineIdx = buffer.indexOf('\n'); if (newlineIdx !== -1) { @@ -106,14 +106,14 @@ export async function sendCommand(command: Record): Promise { debug('Socket error:', err.message); if (!resolved) { reject(new Error(`Connection error: ${err.message}`)); } }); - + socket.on('close', () => { debug('Socket closed, resolved:', resolved, 'buffer:', buffer.length); if (!resolved && buffer.trim()) { @@ -127,7 +127,7 @@ export async function sendCommand(command: Record): Promise { if (!resolved) { diff --git a/src/daemon.ts b/src/daemon.ts index d68fb75..3564355 100644 --- a/src/daemon.ts +++ b/src/daemon.ts @@ -45,7 +45,7 @@ export function getPidFile(session?: string): string { export function isDaemonRunning(session?: string): boolean { const pidFile = getPidFile(session); if (!fs.existsSync(pidFile)) return false; - + try { const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10); // Check if process exists @@ -78,43 +78,47 @@ export function cleanupSocket(session?: string): void { export async function startDaemon(): Promise { // Clean up any stale socket cleanupSocket(); - + const browser = new BrowserManager(); let shuttingDown = false; - + const server = net.createServer((socket) => { let buffer = ''; - + socket.on('data', async (data) => { buffer += data.toString(); - + // Process complete lines while (buffer.includes('\n')) { const newlineIdx = buffer.indexOf('\n'); const line = buffer.substring(0, newlineIdx); buffer = buffer.substring(newlineIdx + 1); - + if (!line.trim()) continue; - + try { const parseResult = parseCommand(line); - + if (!parseResult.success) { const resp = errorResponse(parseResult.id ?? 'unknown', parseResult.error); socket.write(serializeResponse(resp) + '\n'); continue; } - + // Auto-launch browser if not already launched and this isn't a launch command - if (!browser.isLaunched() && parseResult.command.action !== 'launch' && parseResult.command.action !== 'close') { + if ( + !browser.isLaunched() && + parseResult.command.action !== 'launch' && + parseResult.command.action !== 'close' + ) { await browser.launch({ id: 'auto', action: 'launch', headless: true }); } - + // Handle close command specially if (parseResult.command.action === 'close') { const response = await executeCommand(parseResult.command, browser); socket.write(serializeResponse(response) + '\n'); - + if (!shuttingDown) { shuttingDown = true; setTimeout(() => { @@ -125,7 +129,7 @@ export async function startDaemon(): Promise { } return; } - + const response = await executeCommand(parseResult.command, browser); socket.write(serializeResponse(response) + '\n'); } catch (err) { @@ -134,28 +138,28 @@ export async function startDaemon(): Promise { } } }); - + socket.on('error', () => { // Client disconnected, ignore }); }); - + const socketPath = getSocketPath(); const pidFile = getPidFile(); - + // Write PID file before listening fs.writeFileSync(pidFile, process.pid.toString()); - + server.listen(socketPath, () => { // Daemon is ready }); - + server.on('error', (err) => { console.error('Server error:', err); cleanupSocket(); process.exit(1); }); - + // Handle shutdown signals const shutdown = async () => { if (shuttingDown) return; @@ -165,10 +169,10 @@ export async function startDaemon(): Promise { cleanupSocket(); process.exit(0); }; - + process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); - + // Keep process alive process.stdin.resume(); } diff --git a/src/index.ts b/src/index.ts index 1d6d5a5..360d468 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,7 +22,9 @@ function listSessions(): string[] { const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10); process.kill(pid, 0); sessions.push(match[1]); - } catch { /* Process not running */ } + } catch { + /* Process not running */ + } } } return sessions; @@ -134,14 +136,14 @@ function printResponse(response: Response, jsonMode: boolean): void { console.log(JSON.stringify(response)); return; } - + if (!response.success) { console.error(c('red', '✗ Error:'), response.error); process.exit(1); } - + const data = response.data as Record; - + if (data.url && data.title) { console.log(c('green', '✓'), c('bold', data.title as string)); console.log(c('dim', ` ${data.url}`)); @@ -178,10 +180,10 @@ function printResponse(response: Response, jsonMode: boolean): void { } else if (data.cookies) { const cookies = data.cookies as Array<{ name: string; value: string }>; if (cookies.length === 0) console.log(c('dim', 'No cookies')); - else cookies.forEach(ck => console.log(`${c('cyan', ck.name)}: ${ck.value}`)); + else cookies.forEach((ck) => console.log(`${c('cyan', ck.name)}: ${ck.value}`)); } else if (data.tabs) { const tabs = data.tabs as Array<{ index: number; url: string; title: string; active: boolean }>; - tabs.forEach(t => { + tabs.forEach((t) => { const marker = t.active ? c('green', '→') : ' '; console.log(`${marker} [${t.index}] ${t.title || c('dim', '(untitled)')}`); if (t.url) console.log(c('dim', ` ${t.url}`)); @@ -191,18 +193,19 @@ function printResponse(response: Response, jsonMode: boolean): void { } else if (data.messages) { const msgs = data.messages as Array<{ type: string; text: string }>; if (msgs.length === 0) console.log(c('dim', 'No messages')); - else msgs.forEach(m => { - const col = m.type === 'error' ? 'red' : m.type === 'warning' ? 'yellow' : 'dim'; - console.log(`${c(col, `[${m.type}]`)} ${m.text}`); - }); + else + msgs.forEach((m) => { + const col = m.type === 'error' ? 'red' : m.type === 'warning' ? 'yellow' : 'dim'; + console.log(`${c(col, `[${m.type}]`)} ${m.text}`); + }); } else if (data.errors) { const errs = data.errors as Array<{ message: string }>; if (errs.length === 0) console.log(c('dim', 'No errors')); - else errs.forEach(e => console.log(c('red', '✗'), e.message)); + else errs.forEach((e) => console.log(c('red', '✗'), e.message)); } else if (data.requests) { const reqs = data.requests as Array<{ method: string; url: string }>; if (reqs.length === 0) console.log(c('dim', 'No requests')); - else reqs.forEach(r => console.log(`${c('cyan', r.method)} ${r.url}`)); + else reqs.forEach((r) => console.log(`${c('cyan', r.method)} ${r.url}`)); } else if (data.moved) { console.log(c('green', '✓'), `Moved to (${data.x}, ${data.y})`); } else if (data.body !== undefined && data.status !== undefined) { @@ -225,7 +228,28 @@ function printResponse(response: Response, jsonMode: boolean): void { console.log(c('green', '✓'), 'Browser launched'); } else if (data.state) { console.log(c('green', '✓'), `Load state: ${data.state}`); - } else if (Object.keys(data).some(k => ['clicked', 'typed', 'filled', 'pressed', 'hovered', 'scrolled', 'selected', 'waited', 'checked', 'unchecked', 'focused', 'set', 'cleared', 'started', 'down', 'up'].includes(k))) { + } else if ( + Object.keys(data).some((k) => + [ + 'clicked', + 'typed', + 'filled', + 'pressed', + 'hovered', + 'scrolled', + 'selected', + 'waited', + 'checked', + 'unchecked', + 'focused', + 'set', + 'cleared', + 'started', + 'down', + 'up', + ].includes(k) + ) + ) { console.log(c('green', '✓'), 'Done'); } else { console.log(c('green', '✓'), JSON.stringify(data)); @@ -239,7 +263,7 @@ function printResponse(response: Response, jsonMode: boolean): void { async function handleGet(args: string[], id: string): Promise> { const what = args[0]; const selector = args[1]; - + switch (what) { case 'text': if (!selector) err('Selector required: veb get text '); @@ -271,9 +295,9 @@ async function handleGet(args: string[], id: string): Promise> { const what = args[0]; const selector = args[1]; - + if (!selector) err(`Selector required: veb is ${what} `); - + switch (what) { case 'visible': return { id, action: 'isvisible', selector }; @@ -286,17 +310,21 @@ async function handleIs(args: string[], id: string): Promise> { +async function handleFind( + args: string[], + id: string, + flags: Flags +): Promise> { const locator = args[0]; const value = args[1]; const subaction = args[2] || 'click'; const fillValue = args[3]; - + if (!value) err(`Value required: veb find ${locator} `); - + const exact = flags.exact; const name = flags.name; - + switch (locator) { case 'role': return { id, action: 'getbyrole', role: value, subaction, value: fillValue, name, exact }; @@ -305,7 +333,14 @@ async function handleFind(args: string[], id: string, flags: Flags): Promise> { const action = args[0]; - + switch (action) { case 'move': { const x = parseInt(args[1], 10); @@ -355,7 +392,7 @@ async function handleMouse(args: string[], id: string): Promise> { const setting = args[0]; - + switch (setting) { case 'viewport': { const w = parseInt(args[1], 10); @@ -379,26 +416,42 @@ async function handleSet(args: string[], id: string): Promise'); try { return { id, action: 'headers', headers: JSON.parse(args[1]) }; - } catch { err('Invalid JSON for headers'); } + } catch { + err('Invalid JSON for headers'); + } break; case 'credentials': case 'auth': if (!args[1] || !args[2]) err('Usage: veb set credentials '); return { id, action: 'credentials', username: args[1], password: args[2] }; case 'media': { - const colorScheme = args.includes('dark') ? 'dark' : args.includes('light') ? 'light' : undefined; - const media = args.includes('print') ? 'print' : args.includes('screen') ? 'screen' : undefined; + const colorScheme = args.includes('dark') + ? 'dark' + : args.includes('light') + ? 'light' + : undefined; + const media = args.includes('print') + ? 'print' + : args.includes('screen') + ? 'screen' + : undefined; return { id, action: 'emulatemedia', colorScheme, media }; } default: - err(`Unknown: veb set ${setting}. Options: viewport, device, geo, offline, headers, credentials, media`); + err( + `Unknown: veb set ${setting}. Options: viewport, device, geo, offline, headers, credentials, media` + ); } return {}; } -async function handleNetwork(args: string[], id: string, allArgs: string[]): Promise> { +async function handleNetwork( + args: string[], + id: string, + allArgs: string[] +): Promise> { const action = args[0]; - + switch (action) { case 'route': { const url = args[1]; @@ -406,7 +459,13 @@ async function handleNetwork(args: string[], id: string, allArgs: string[]): Pro const abort = allArgs.includes('--abort'); const bodyIdx = allArgs.indexOf('--body'); const body = bodyIdx !== -1 ? allArgs[bodyIdx + 1] : undefined; - return { id, action: 'route', url, abort, response: body ? { body, contentType: 'application/json' } : undefined }; + return { + id, + action: 'route', + url, + abort, + response: body ? { body, contentType: 'application/json' } : undefined, + }; } case 'unroute': return { id, action: 'unroute', url: args[1] }; @@ -425,11 +484,11 @@ async function handleNetwork(args: string[], id: string, allArgs: string[]): Pro async function handleStorage(args: string[], id: string): Promise> { const type = args[0] as 'local' | 'session'; const sub = args[1]; - + if (type !== 'local' && type !== 'session') { err('Usage: veb storage [get|set|clear] [key] [value]'); } - + if (sub === 'set') { if (!args[2] || !args[3]) err(`Usage: veb storage ${type} set `); return { id, action: 'storage_set', type, key: args[2], value: args[3] }; @@ -443,12 +502,14 @@ async function handleStorage(args: string[], id: string): Promise> { const sub = args[0]; - + if (sub === 'set') { if (!args[1]) err('Usage: veb cookies set '); try { return { id, action: 'cookies_set', cookies: JSON.parse(args[1]) }; - } catch { err('Invalid JSON for cookies'); } + } catch { + err('Invalid JSON for cookies'); + } } else if (sub === 'clear') { return { id, action: 'cookies_clear' }; } else { @@ -459,7 +520,7 @@ async function handleCookies(args: string[], id: string): Promise> { const sub = args[0]; - + if (sub === 'new') { return { id, action: 'tab_new' }; } else if (sub === 'list' || sub === 'ls' || !sub) { @@ -476,7 +537,7 @@ async function handleTab(args: string[], id: string): Promise> { const sub = args[0]; - + if (sub === 'start') { return { id, action: 'trace_start', screenshots: true, snapshots: true }; } else if (sub === 'stop') { @@ -491,7 +552,7 @@ async function handleTrace(args: string[], id: string): Promise> { const sub = args[0]; const path = args[1]; - + if (sub === 'save') { if (!path) err('Usage: veb state save '); return { id, action: 'state_save', path }; @@ -531,13 +592,13 @@ function parseFlags(args: string[]): { flags: Flags; cleanArgs: string[] } { session: process.env.VEB_SESSION || 'default', exact: false, }; - + const cleanArgs: string[] = []; let i = 0; - + while (i < args.length) { const arg = args[i]; - + if (arg === '--json') { flags.json = true; } else if (arg === '--full' || arg === '-f') { @@ -565,7 +626,7 @@ function parseFlags(args: string[]): { flags: Flags; cleanArgs: string[] } { } i++; } - + return { flags, cleanArgs }; } @@ -576,21 +637,21 @@ function parseFlags(args: string[]): { flags: Flags; cleanArgs: string[] } { async function main(): Promise { const rawArgs = process.argv.slice(2); const { flags, cleanArgs } = parseFlags(rawArgs); - + if (flags.debug) setDebug(true); setSession(flags.session); - + if (cleanArgs.length === 0 || rawArgs.includes('--help') || rawArgs.includes('-h')) { printHelp(); process.exit(0); } - + const command = cleanArgs[0]; const args = cleanArgs.slice(1); const id = genId(); - + let cmd: Record; - + switch (command) { // === Core Commands === case 'open': @@ -601,85 +662,85 @@ async function main(): Promise { cmd = { id, action: 'navigate', url }; break; } - + case 'click': if (!args[0]) err('Selector required'); cmd = { id, action: 'click', selector: args[0] }; break; - + case 'dblclick': if (!args[0]) err('Selector required'); cmd = { id, action: 'dblclick', selector: args[0] }; break; - + case 'type': if (!args[0] || !args[1]) err('Usage: veb type '); cmd = { id, action: 'type', selector: args[0], text: args.slice(1).join(' ') }; break; - + case 'fill': if (!args[0] || !args[1]) err('Usage: veb fill '); cmd = { id, action: 'fill', selector: args[0], value: args.slice(1).join(' ') }; break; - + case 'press': case 'key': if (!args[0]) err('Key required'); cmd = { id, action: 'press', key: args[0] }; break; - + case 'keydown': if (!args[0]) err('Key required'); cmd = { id, action: 'keydown', key: args[0] }; break; - + case 'keyup': if (!args[0]) err('Key required'); cmd = { id, action: 'keyup', key: args[0] }; break; - + case 'hover': if (!args[0]) err('Selector required'); cmd = { id, action: 'hover', selector: args[0] }; break; - + case 'focus': if (!args[0]) err('Selector required'); cmd = { id, action: 'focus', selector: args[0] }; break; - + case 'check': if (!args[0]) err('Selector required'); cmd = { id, action: 'check', selector: args[0] }; break; - + case 'uncheck': if (!args[0]) err('Selector required'); cmd = { id, action: 'uncheck', selector: args[0] }; break; - + case 'select': if (!args[0] || !args[1]) err('Usage: veb select '); cmd = { id, action: 'select', selector: args[0], value: args[1] }; break; - + case 'drag': if (!args[0] || !args[1]) err('Usage: veb drag '); cmd = { id, action: 'drag', source: args[0], target: args[1] }; break; - + case 'upload': if (!args[0] || !args[1]) err('Usage: veb upload '); cmd = { id, action: 'upload', selector: args[0], files: args.slice(1) }; break; - + case 'scroll': { const dir = args[0] || 'down'; const amount = parseInt(args[1], 10) || 300; cmd = { id, action: 'scroll', direction: dir, amount, selector: flags.selector }; break; } - + case 'wait': { const target = args[0]; // Check for flags @@ -701,83 +762,83 @@ async function main(): Promise { } break; } - + case 'screenshot': { const path = args[0]; cmd = { id, action: 'screenshot', path, fullPage: flags.full, selector: flags.selector }; break; } - + case 'pdf': if (!args[0]) err('Path required'); cmd = { id, action: 'pdf', path: args[0] }; break; - + case 'snapshot': cmd = { id, action: 'snapshot' }; break; - + case 'eval': if (!args[0]) err('Script required'); cmd = { id, action: 'evaluate', script: args.join(' ') }; break; - + case 'close': case 'quit': case 'exit': cmd = { id, action: 'close' }; break; - + // === Navigation === case 'back': cmd = { id, action: 'back' }; break; - + case 'forward': cmd = { id, action: 'forward' }; break; - + case 'reload': cmd = { id, action: 'reload' }; break; - + // === Grouped Commands === case 'get': cmd = await handleGet(args, id); break; - + case 'is': cmd = await handleIs(args, id); break; - + case 'find': cmd = await handleFind(args, id, flags); break; - + case 'mouse': cmd = await handleMouse(args, id); break; - + case 'set': cmd = await handleSet(args, id); break; - + case 'network': cmd = await handleNetwork(args, id, rawArgs); break; - + case 'storage': cmd = await handleStorage(args, id); break; - + case 'cookies': cmd = await handleCookies(args, id); break; - + case 'tab': cmd = await handleTab(args, id); break; - + case 'window': if (args[0] === 'new') { cmd = { id, action: 'window_new' }; @@ -785,7 +846,7 @@ async function main(): Promise { err('Usage: veb window new'); } break; - + case 'frame': if (!args[0]) err('Selector required'); if (args[0] === 'main') { @@ -794,7 +855,7 @@ async function main(): Promise { cmd = { id, action: 'frame', selector: args[0] }; } break; - + case 'dialog': if (args[0] === 'accept') { cmd = { id, action: 'dialog', response: 'accept', promptText: args[1] }; @@ -804,59 +865,60 @@ async function main(): Promise { err('Usage: veb dialog accept|dismiss'); } break; - + case 'trace': cmd = await handleTrace(args, id); break; - + case 'state': cmd = await handleState(args, id); break; - + case 'console': cmd = { id, action: 'console', clear: rawArgs.includes('--clear') }; break; - + case 'errors': cmd = { id, action: 'errors', clear: rawArgs.includes('--clear') }; break; - + case 'highlight': if (!args[0]) err('Selector required'); cmd = { id, action: 'highlight', selector: args[0] }; break; - + case 'scrollintoview': case 'scrollinto': if (!args[0]) err('Selector required'); cmd = { id, action: 'scrollintoview', selector: args[0] }; break; - + case 'initscript': if (!args[0]) err('Script required'); cmd = { id, action: 'addinitscript', script: args.join(' ') }; break; - + case 'inserttext': case 'insert': if (!args[0]) err('Text required'); cmd = { id, action: 'inserttext', text: args.join(' ') }; break; - + case 'multiselect': - if (!args[0] || args.length < 2) err('Usage: veb multiselect [value2...]'); + if (!args[0] || args.length < 2) + err('Usage: veb multiselect [value2...]'); cmd = { id, action: 'multiselect', selector: args[0], values: args.slice(1) }; break; - + case 'download': cmd = { id, action: 'waitfordownload', path: args[0] }; break; - + case 'response': if (!args[0]) err('URL pattern required'); cmd = { id, action: 'responsebody', url: args[0] }; break; - + case 'session': if (args[0] === 'list' || args[0] === 'ls') { const sessions = listSessions(); @@ -864,7 +926,7 @@ async function main(): Promise { if (sessions.length === 0) { console.log(c('dim', 'No active sessions')); } else { - sessions.forEach(s => { + sessions.forEach((s) => { const marker = s === current ? c('green', '→') : ' '; console.log(`${marker} ${c('cyan', s)}`); }); @@ -874,7 +936,7 @@ async function main(): Promise { console.log(c('cyan', getSession())); process.exit(0); } - + // === Legacy aliases for backwards compatibility === case 'url': cmd = { id, action: 'url' }; @@ -888,13 +950,13 @@ async function main(): Promise { case 'extract': cmd = { id, action: 'content', selector: args[0] }; break; - + default: console.error(c('red', 'Unknown command:'), command); console.error(c('dim', 'Run: veb --help')); process.exit(1); } - + try { const response = await send(cmd); printResponse(response, flags.json); diff --git a/src/protocol.ts b/src/protocol.ts index 43c3531..5ee6323 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -11,10 +11,12 @@ const baseCommandSchema = z.object({ const launchSchema = baseCommandSchema.extend({ action: z.literal('launch'), headless: z.boolean().optional(), - viewport: z.object({ - width: z.number().positive(), - height: z.number().positive(), - }).optional(), + viewport: z + .object({ + width: z.number().positive(), + height: z.number().positive(), + }) + .optional(), browser: z.enum(['chromium', 'firefox', 'webkit']).optional(), }); @@ -125,17 +127,19 @@ const cookiesGetSchema = baseCommandSchema.extend({ const cookiesSetSchema = baseCommandSchema.extend({ action: z.literal('cookies_set'), - cookies: z.array(z.object({ - name: z.string(), - value: z.string(), - url: z.string().optional(), - domain: z.string().optional(), - path: z.string().optional(), - expires: z.number().optional(), - httpOnly: z.boolean().optional(), - secure: z.boolean().optional(), - sameSite: z.enum(['Strict', 'Lax', 'None']).optional(), - })), + cookies: z.array( + z.object({ + name: z.string(), + value: z.string(), + url: z.string().optional(), + domain: z.string().optional(), + path: z.string().optional(), + expires: z.number().optional(), + httpOnly: z.boolean().optional(), + secure: z.boolean().optional(), + sameSite: z.enum(['Strict', 'Lax', 'None']).optional(), + }) + ), }); const cookiesClearSchema = baseCommandSchema.extend({ @@ -169,18 +173,22 @@ const dialogSchema = baseCommandSchema.extend({ const pdfSchema = baseCommandSchema.extend({ action: z.literal('pdf'), path: z.string().min(1), - format: z.enum(['Letter', 'Legal', 'Tabloid', 'Ledger', 'A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6']).optional(), + format: z + .enum(['Letter', 'Legal', 'Tabloid', 'Ledger', 'A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6']) + .optional(), }); const routeSchema = baseCommandSchema.extend({ action: z.literal('route'), url: z.string().min(1), - response: z.object({ - status: z.number().optional(), - body: z.string().optional(), - contentType: z.string().optional(), - headers: z.record(z.string()).optional(), - }).optional(), + response: z + .object({ + status: z.number().optional(), + body: z.string().optional(), + contentType: z.string().optional(), + headers: z.record(z.string()).optional(), + }) + .optional(), abort: z.boolean().optional(), }); @@ -658,10 +666,12 @@ const tabCloseSchema = baseCommandSchema.extend({ const windowNewSchema = baseCommandSchema.extend({ action: z.literal('window_new'), - viewport: z.object({ - width: z.number().positive(), - height: z.number().positive(), - }).optional(), + viewport: z + .object({ + width: z.number().positive(), + height: z.number().positive(), + }) + .optional(), }); // Union schema for all commands @@ -783,7 +793,7 @@ const commandSchema = z.discriminatedUnion('action', [ ]); // Parse result type -export type ParseResult = +export type ParseResult = | { success: true; command: Command } | { success: false; error: string; id?: string }; @@ -800,17 +810,16 @@ export function parseCommand(input: string): ParseResult { } // Extract id for error responses if possible - const id = typeof json === 'object' && json !== null && 'id' in json - ? String((json as { id: unknown }).id) - : undefined; + const id = + typeof json === 'object' && json !== null && 'id' in json + ? String((json as { id: unknown }).id) + : undefined; // Validate against schema const result = commandSchema.safeParse(json); - + if (!result.success) { - const errors = result.error.errors - .map(e => `${e.path.join('.')}: ${e.message}`) - .join(', '); + const errors = result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '); return { success: false, error: `Validation error: ${errors}`, id }; } diff --git a/src/types.ts b/src/types.ts index a58f259..8ef443d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -165,7 +165,18 @@ export interface DialogCommand extends BaseCommand { export interface PdfCommand extends BaseCommand { action: 'pdf'; path: string; - format?: 'Letter' | 'Legal' | 'Tabloid' | 'Ledger' | 'A0' | 'A1' | 'A2' | 'A3' | 'A4' | 'A5' | 'A6'; + format?: + | 'Letter' + | 'Legal' + | 'Tabloid' + | 'Ledger' + | 'A0' + | 'A1' + | 'A2' + | 'A3' + | 'A4' + | 'A5' + | 'A6'; } // Network interception