import { type ChildProcess, spawn } from 'node:child_process' import { existsSync, mkdirSync } from 'node:fs' import { homedir, platform } from 'node:os' import { join } from 'node:path' import { chromePath, localAppData } from '../consts.ts' import { reclaimHomeDirPath } from '../paths.ts' export interface LaunchOptions { /** Port for --remote-debugging-port. */ port: number /** Path to user-data-dir. Created if missing. */ profileDir: string /** If true, launch without a visible window (--headless=new). Useful * for tests and CI; user-driven flows want it false. */ headless?: boolean } /** * Build the platform-specific list of Chromium-family binaries to probe. * Order matters: Chrome first, then Chromium, then Edge (Edge is * Chromium-based, supports CDP, and is preinstalled on Win 10/11 — a * useful fallback when Chrome itself isn't installed). * * On Windows we also probe the per-user Chrome install under * %LOCALAPPDATA%, which is what you get when Chrome was installed * without admin rights ("Install for me only"). Skipping this was the * #1 first-run failure observed in the field. */ function browserCandidates(): string[] { switch (platform()) { case 'darwin': return [ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', join( homedir(), 'Applications/Google Chrome.app/Contents/MacOS/Google Chrome', ), '/Applications/Chromium.app/Contents/MacOS/Chromium', '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', ] case 'win32': { const appData = localAppData() ?? join(homedir(), 'AppData', 'Local') return [ // System-wide Chrome 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', // Per-user Chrome ("Install for me only") join(appData, 'Google', 'Chrome', 'Application', 'chrome.exe'), // Edge — Chromium-based, preinstalled on Win 10/11 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe', 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', ] } case 'linux': return [ '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium', '/usr/bin/microsoft-edge', ] default: return [] } } export function defaultProfileDir(): string { return join(reclaimHomeDirPath(), 'chrome-profile') } export function findChromeBinary(): string { const candidates = browserCandidates() for(const c of candidates) { if(existsSync(c)) { return c } } throw new Error( `No Chromium-family browser found on ${platform()}. Tried:\n ` + candidates.join('\n ') + '\nSet RECLAIM_AGENT_CHROME_PATH to point at ' + 'your chrome.exe / msedge.exe / chromium binary.', ) } export function launchChrome(opts: LaunchOptions): ChildProcess { const binary = chromePath() ?? findChromeBinary() if(!existsSync(opts.profileDir)) { mkdirSync(opts.profileDir, { recursive: true }) } const args = [ `--remote-debugging-port=${opts.port}`, `--user-data-dir=${opts.profileDir}`, '--no-first-run', '--no-default-browser-check', ] if(opts.headless) { args.push('--headless=new', '--disable-gpu') } const child = spawn(binary, args, { stdio: 'ignore', detached: false, }) return child } export async function waitForCdpReady( port: number, timeoutMs = 10_000, ) { const deadline = Date.now() + timeoutMs while(Date.now() < deadline) { try { const res = await fetch(`http://127.0.0.1:${port}/json/version`) if(res.ok) { return } } catch{ // retry } await new Promise((r) => setTimeout(r, 150)) } throw new Error( `Chrome CDP did not become ready on port ${port} within ${timeoutMs}ms`, ) }