#!/usr/bin/env npx tsx /** * Browser launcher using Camoufox with persistent profile * * Usage: * npx tsx browse.ts --url "https://example.com" * npx tsx browse.ts --url "https://example.com" --screenshot --output page.png * npx tsx browse.ts --url "https://example.com" --headless false --wait 5000 */ import { Camoufox } from 'camoufox-js'; import { homedir } from 'os'; import { join } from 'path'; import { existsSync, mkdirSync } from 'fs'; import parseArgs from 'minimist'; import type { Page, BrowserContext } from 'playwright-core'; // Types interface BrowseOptions { url: string; headless?: boolean; screenshot?: boolean; output?: string; wait?: number; timeout?: number; interactive?: boolean; } interface BrowseResult { title: string; url: string; screenshotPath?: string; } // Get profile directory const getProfilePath = (): string => { const customPath = process.env.CAMOUFOX_PROFILE_PATH; if (customPath) return customPath; const profileDir = join(homedir(), '.camoufox-profile'); if (!existsSync(profileDir)) { mkdirSync(profileDir, { recursive: true }); } return profileDir; }; // Launch browser with persistent profile export async function launchBrowser(options: { headless?: boolean; }): Promise { const profilePath = getProfilePath(); const headless = options.headless ?? (process.env.CAMOUFOX_HEADLESS ? process.env.CAMOUFOX_HEADLESS === 'true' : true); console.log(`Using profile: ${profilePath}`); console.log(`Headless mode: ${headless}`); const browser = await Camoufox({ user_data_dir: profilePath, headless, }); return browser; } // Browse to URL and optionally take screenshot export async function browse(options: BrowseOptions): Promise { const browser = await launchBrowser({ headless: options.headless }); const page = await browser.newPage(); try { // Navigate to URL console.log(`Navigating to: ${options.url}`); await page.goto(options.url, { timeout: options.timeout ?? 60000, waitUntil: 'domcontentloaded', }); // Wait if specified if (options.wait) { console.log(`Waiting ${options.wait}ms...`); await page.waitForTimeout(options.wait); } const result: BrowseResult = { title: await page.title(), url: page.url(), }; console.log(`Page title: ${result.title}`); console.log(`Final URL: ${result.url}`); // Take screenshot if requested if (options.screenshot) { const outputPath = options.output ?? 'screenshot.png'; await page.screenshot({ path: outputPath, fullPage: true }); result.screenshotPath = outputPath; console.log(`Screenshot saved: ${outputPath}`); } // If interactive mode, keep browser open if (options.interactive) { console.log('\nInteractive mode - browser will stay open.'); console.log('Press Ctrl+C to close.'); await new Promise(() => {}); // Keep running } return result; } finally { if (!options.interactive) { await browser.close(); } } } // Export page for use in other scripts export async function getPage(options?: { headless?: boolean; }): Promise<{ page: Page; browser: BrowserContext }> { const browser = await launchBrowser({ headless: options?.headless }); const page = await browser.newPage(); return { page, browser }; } // CLI entry point async function main() { const args = parseArgs(process.argv.slice(2), { string: ['url', 'output'], boolean: ['screenshot', 'headless', 'interactive', 'help'], default: { headless: true, screenshot: false, interactive: false, }, alias: { u: 'url', o: 'output', s: 'screenshot', h: 'help', i: 'interactive', }, }); if (args.help || !args.url) { console.log(` Web Browser with Camoufox Usage: npx tsx browse.ts --url [options] Options: -u, --url URL to navigate to (required) -s, --screenshot Take a screenshot of the page -o, --output Output path for screenshot (default: screenshot.png) --headless Run in headless mode (default: true) --wait Wait time after page load in milliseconds --timeout Navigation timeout (default: 60000) -i, --interactive Keep browser open for manual interaction -h, --help Show this help message Examples: npx tsx browse.ts --url "https://example.com" npx tsx browse.ts --url "https://example.com" --screenshot --output page.png npx tsx browse.ts --url "https://example.com" --headless false --interactive Environment Variables: CAMOUFOX_PROFILE_PATH Custom profile directory (default: ~/.camoufox-profile/) CAMOUFOX_HEADLESS Default headless mode (true/false) `); process.exit(args.help ? 0 : 1); } try { await browse({ url: args.url, headless: args.headless, screenshot: args.screenshot, output: args.output, wait: args.wait ? parseInt(args.wait, 10) : undefined, timeout: args.timeout ? parseInt(args.timeout, 10) : undefined, interactive: args.interactive, }); } catch (error) { console.error('Error:', error instanceof Error ? error.message : error); process.exit(1); } } // Run if executed directly const isMainModule = process.argv[1]?.includes('browse.ts'); if (isMainModule) { main(); }