From a37b3f6b02715f6e14d2f6269226143de20c7f17 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Tue, 4 Aug 2026 14:09:59 -0700 Subject: [PATCH] Fix Windows question browser opening (#510) AI assistance: Codex reproduced the issue, implemented the fix, and ran the validation described in the pull request. --- skill/scripts/lib/open-system-browser.mjs | 26 +++++++++++++++++++++++ skill/scripts/serve-question.mjs | 4 ++-- tests/serve-question.test.mjs | 21 ++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 skill/scripts/lib/open-system-browser.mjs diff --git a/skill/scripts/lib/open-system-browser.mjs b/skill/scripts/lib/open-system-browser.mjs new file mode 100644 index 000000000..c44cd847a --- /dev/null +++ b/skill/scripts/lib/open-system-browser.mjs @@ -0,0 +1,26 @@ +import { spawn } from 'node:child_process'; + +export function browserOpenCommand(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', +} = {}) { + if (platform === 'darwin') return { command: 'open', args: [url] }; + if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] }; + return { command: 'xdg-open', args: [url] }; +} + +export function openSystemBrowser(url, { + platform = process.platform, + comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe', + spawnImpl = spawn, +} = {}) { + const { command, args } = browserOpenCommand(url, { platform, comspec }); + try { + const child = spawnImpl(command, args, { stdio: 'ignore', detached: true }); + child.on('error', () => {}); + child.unref(); + return true; + } catch { + return false; + } +} diff --git a/skill/scripts/serve-question.mjs b/skill/scripts/serve-question.mjs index 25beaa759..2d8c24a6a 100644 --- a/skill/scripts/serve-question.mjs +++ b/skill/scripts/serve-question.mjs @@ -79,6 +79,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { openSystemBrowser } from './lib/open-system-browser.mjs'; function arg(name, fallback = null) { const i = process.argv.indexOf(`--${name}`); @@ -976,8 +977,7 @@ server.listen(portArg, '127.0.0.1', () => { console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...'); } if (!hasFlag('no-open')) { - const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; - try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ } + openSystemBrowser(url); } if (timeoutSec > 0) { setTimeout(() => { diff --git a/tests/serve-question.test.mjs b/tests/serve-question.test.mjs index 0371be4a1..cfee001b0 100644 --- a/tests/serve-question.test.mjs +++ b/tests/serve-question.test.mjs @@ -5,6 +5,9 @@ import { writeFileSync, mkdtempSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { EventEmitter } from 'node:events'; + +import { browserOpenCommand, openSystemBrowser } from '../skill/scripts/lib/open-system-browser.mjs'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const SCRIPT = path.join(ROOT, 'skill', 'scripts', 'serve-question.mjs'); @@ -39,6 +42,24 @@ const PAYLOAD = { }; describe('serve-question', () => { + it('opens Windows URLs through cmd.exe and reserves the start title argument', () => { + assert.deepEqual( + browserOpenCommand('http://127.0.0.1:1234/', { platform: 'win32', comspec: 'cmd.exe' }), + { command: 'cmd.exe', args: ['/c', 'start', '', 'http://127.0.0.1:1234/'] }, + ); + }); + + it('absorbs asynchronous system-opener failures after printing the URL', () => { + const child = new EventEmitter(); + child.unref = () => {}; + assert.equal(openSystemBrowser('http://127.0.0.1:1234/', { + platform: 'linux', + spawnImpl: () => child, + }), true); + assert.equal(child.listenerCount('error'), 1); + assert.doesNotThrow(() => child.emit('error', Object.assign(new Error('missing opener'), { code: 'ENOENT' }))); + }); + it('serves the page, records the answer, prints ANSWER, exits 0', async () => { const { child, url, read } = await startServer(PAYLOAD); const html = await (await fetch(url)).text();