From 92d987e600ad3600346be9d11e9c521a0c00d433 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 3 Jun 2026 23:04:22 -0700 Subject: [PATCH] fix: suppress Windows console flashes from browse cold-start child spawns --- browse/src/cli.ts | 19 ++++++--- browse/src/config.ts | 28 +++++++++++++ browse/src/file-permissions.ts | 4 +- browse/test/cli-windows-hide.test.ts | 63 ++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 browse/test/cli-windows-hide.test.ts diff --git a/browse/src/cli.ts b/browse/src/cli.ts index 59327b792..9803c2c3b 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -11,7 +11,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { spawn as nodeSpawn } from 'child_process'; +import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from 'child_process'; import { safeUnlink, safeUnlinkQuiet, safeKill, isProcessAlive } from './error-handling'; import { writeSecureFile, mkdirSecure } from './file-permissions'; import { resolveConfig, ensureStateDir, readVersionHash } from './config'; @@ -141,10 +141,13 @@ async function killServer(pid: number): Promise { if (IS_WINDOWS) { // taskkill /T /F kills the process tree (Node + Chromium) try { - Bun.spawnSync( - ['taskkill', '/PID', String(pid), '/T', '/F'], - { stdout: 'pipe', stderr: 'pipe', timeout: 5000 } - ); + const proc = nodeSpawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { + stdout: 'pipe', + stderr: 'pipe', + timeout: 5000, + windowsHide: true, + }); + if (proc.error) throw proc.error; } catch (err: any) { if (err?.code !== 'ENOENT') throw err; } @@ -325,7 +328,11 @@ async function startServer(extraEnv?: Record): Promise { + test('Windows detach launcher uses hidden Node spawnSync', () => { + const body = read(CLI); + expect(body).toMatch(/spawnSync as nodeSpawnSync/); + expect(body).toMatch( + /nodeSpawnSync\(\s*['"]node['"],\s*\[\s*['"]-e['"],\s*launcherCode\s*\][\s\S]{0,250}windowsHide:\s*true/, + ); + expect(body).not.toMatch(/Bun\.spawnSync\(\s*\[\s*['"]node['"]/); + }); + + test('Windows taskkill helper uses hidden Node spawnSync', () => { + const body = read(CLI); + expect(body).toMatch( + /nodeSpawnSync\(\s*['"]taskkill['"][\s\S]{0,250}windowsHide:\s*true/, + ); + expect(body).not.toMatch(/Bun\.spawnSync\(\s*\[\s*['"]taskkill['"]/); + }); + + test('git probes use hidden Node spawnSync on Windows and keep Bun on POSIX', () => { + const body = read(CONFIG); + const hiddenGitSpawns = body.match( + /nodeSpawnSync\(\s*['"]git['"][\s\S]{0,250}windowsHide:\s*true/g, + ) || []; + expect(body).toContain("process.platform === 'win32'"); + expect(hiddenGitSpawns).toHaveLength(2); + expect(body).toMatch( + /Bun\.spawnSync\(\s*\[\s*['"]git['"],\s*['"]rev-parse['"],\s*['"]--show-toplevel['"]/, + ); + expect(body).toMatch( + /Bun\.spawnSync\(\s*\[\s*['"]git['"],\s*['"]remote['"],\s*['"]get-url['"],\s*['"]origin['"]/, + ); + }); + + test('icacls ACL helpers pass windowsHide to execFileSync', () => { + const body = read(FILE_PERMISSIONS); + const hiddenIcaclsCalls = body.match( + /execFileSync\(\s*['"]icacls['"][\s\S]{0,250}windowsHide:\s*true/g, + ) || []; + expect(hiddenIcaclsCalls).toHaveLength(2); + }); +});