From bcfe52efa5d23eeb5bede2df2d4fa27a0e389ee1 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Wed, 26 Aug 2026 16:20:54 -0500 Subject: [PATCH] fix(ui): launch a CODEGRAPH_BROWSER override through cmd on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CreateProcess — which node's spawn uses without a shell — only launches a real .exe, so a `.cmd`/`.bat` browser shim (how most Windows wrappers are written) silently launched nothing. Routing the override through `cmd /c`, the way the default `start` opener already goes, makes .exe, .cmd and .bat all work and keeps node's per-argument quoting so a path with spaces survives. Caught on the Windows VM. Co-Authored-By: Claude Opus 5 --- __tests__/ui-server.test.ts | 6 ++++++ src/ui-server/open-browser.ts | 8 ++++++++ 2 files changed, 14 insertions(+) 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] };