diff --git a/__tests__/ui-server.test.ts b/__tests__/ui-server.test.ts index e48d6d4..8f07592 100644 --- a/__tests__/ui-server.test.ts +++ b/__tests__/ui-server.test.ts @@ -523,6 +523,12 @@ describe('browserOpenCommand', () => { command: 'firefox', args: ['http://x'], }); + // Windows routes the override through cmd so a `.cmd`/`.bat` shim — which + // CreateProcess cannot launch directly — still works. + expect(browserOpenCommand('http://x', 'win32', 'C:\\tools\\open.cmd')).toEqual({ + command: 'cmd', + args: ['/c', 'C:\\tools\\open.cmd', 'http://x'], + }); for (const off of ['none', 'NONE', '0', 'false', 'off', '', ' ']) { expect(browserOpenCommand('http://x', 'darwin', off), off).toBeNull(); } diff --git a/src/ui-server/open-browser.ts b/src/ui-server/open-browser.ts index b8be7f5..ede1d3e 100644 --- a/src/ui-server/open-browser.ts +++ b/src/ui-server/open-browser.ts @@ -32,6 +32,14 @@ export function browserOpenCommand( if (override !== undefined) { const trimmed = override.trim(); if (SUPPRESS_VALUES.has(trimmed.toLowerCase())) return null; + // Windows: go through `cmd /c` rather than spawning the override directly. + // `spawn` there is CreateProcess, which only ever launches a real .exe — a + // `.cmd`/`.bat` browser shim (how most Windows wrappers are written) fails + // outright, and an extension-less name only resolves because CreateProcess + // appends `.exe`. Routing through cmd makes .exe, .cmd and .bat all work, + // and node quotes each argument, so a path with spaces survives. Caught on + // the Windows VM, where the direct spawn silently launched nothing. + if (platform === 'win32') return { command: 'cmd', args: ['/c', trimmed, url] }; return { command: trimmed, args: [url] }; } if (platform === 'darwin') return { command: 'open', args: [url] };