1// Opening a URL in the user's browser, from the proxy. 2// 3// Under WSL the browser is Windows': `rundll32.exe url.dll,FileProtocolHandler 4// <url>` through interop hands the URL to Windows' default browser whole. 5// Measured 2026-09-26 on nyx: it opened Chrome with every query parameter 6// intact, and Chrome's request to http://127.0.0.1:<port>/ reached a listener 7// bound to 127.0.0.1 in the guest (WSL's localhost forwarding). `explorer.exe 8// <url>` exited 1 and opened nothing; `cmd.exe /c start` would split the URL 9// at its first `&`. `windows-exec` (SSH) cannot show a window at all: its 10// logon is session 0. 11// 12// Elsewhere, `xdg-open` or macOS's `open`. Whatever happens, the caller also 13// hands the URL to the user, so a launch that fails costs one click. 14// JEVSTRUDEL_MCP_OPEN_BROWSER=0 opens nothing: for tests, whose own headless 15// browser takes the URL instead. 16import { spawn } from 'node:child_process'; 17import { existsSync, readFileSync } from 'node:fs'; 18 19const RUNDLL32 = '/mnt/c/Windows/System32/rundll32.exe'; 20 21function isWsl() { 22 try { 23 return /microsoft/i.test(readFileSync('/proc/sys/kernel/osrelease', 'utf8')); 24 } catch { 25 return false; 26 } 27} 28 29function launcher(url) { 30 if (isWsl() && existsSync(RUNDLL32)) return [RUNDLL32, ['url.dll,FileProtocolHandler', url]]; 31 if (process.platform === 'darwin') return ['open', [url]]; 32 return ['xdg-open', [url]]; 33} 34 35// Resolves to how it was opened, or rejects with why not. 36export function openBrowser(url) { 37 if (process.env.JEVSTRUDEL_MCP_OPEN_BROWSER === '0') return Promise.reject(new Error('JEVSTRUDEL_MCP_OPEN_BROWSER=0')); 38 const [command, args] = launcher(url); 39 return new Promise((resolve, reject) => { 40 const child = spawn(command, args, { stdio: 'ignore', detached: true }); 41 child.on('error', reject); 42 child.on('exit', (code) => (code === 0 ? resolve(command) : reject(new Error(`${command} exited ${code}`)))); 43 child.unref(); 44 }); 45}