diff --git a/bin/ocx.mjs b/bin/ocx.mjs index 880cbeec09..bdda8bd90a 100755 --- a/bin/ocx.mjs +++ b/bin/ocx.mjs @@ -481,6 +481,10 @@ const launchContext = JSON.stringify({ }); const child = spawn(bun, [cliPath, `${NODE_LAUNCH_PROOF_PREFIX}${launchProof}`, ...process.argv.slice(2)], { stdio: "inherit", + // A headless Windows parent (Task Scheduler, dashboard restart, shortcut) has no + // console to inherit. Without this flag Windows allocates a visible console for + // the long-running Bun child, and closing that window kills the proxy (#1236). + windowsHide: true, env: { ...process.env, [NODE_LAUNCH_CONTEXT_ENV]: launchContext, diff --git a/tests/ocx-launcher-source.test.ts b/tests/ocx-launcher-source.test.ts index f634bf19bf..2644915f23 100644 --- a/tests/ocx-launcher-source.test.ts +++ b/tests/ocx-launcher-source.test.ts @@ -36,6 +36,17 @@ describe("ocx.mjs npm launcher (source invariants)", () => { expect(runtimeSource).toContain('export const BUN_RUNTIME_SOURCE_ENV = "OCX_BUN_RUNTIME_SOURCE";'); }); + test("the long-running Bun child stays hidden under a headless Windows launcher (#1236)", () => { + const spawnStart = source.indexOf("const child = spawn(bun, [cliPath"); + expect(spawnStart).toBeGreaterThanOrEqual(0); + const spawnCall = source.slice(spawnStart, source.indexOf("});", spawnStart)); + + // Scope this to the final Node-to-Bun launch. Other helper spawns already hide + // their windows, but they do not cover the child that owns the proxy lifetime. + expect(spawnCall).toContain('stdio: "inherit"'); + expect(spawnCall).toContain("windowsHide: true"); + }); + test("Windows npm spawns use the trusted absolute invocation without shell lookup", () => { expect(source).toContain("const latestInvocation = npmInvocation("); expect(source).toContain("const installInvocation = npmInvocation(");