Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/core/harness/sandbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ export async function startContainerRuntime({
return { runtime, started: false, ready: false, message: `${runtime} engine is not running and RStack cannot auto-start it on ${platform} — start it manually` };
}
try {
const child = spawnImpl(startCmd.cmd, startCmd.args, { stdio: 'ignore' });
// windowsHide suppresses the console window a freshly-spawned console
// subsystem process (cmd.exe on win32's docker path, podman.exe) would
// otherwise briefly flash — harmless elsewhere, since only win32 honors it.
const child = spawnImpl(startCmd.cmd, startCmd.args, { stdio: 'ignore', windowsHide: true });
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
child?.on?.('error', () => {});
child?.unref?.();
} catch (err) {
Expand Down
19 changes: 15 additions & 4 deletions src/hooks/auto-launch.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,21 @@ export async function autoLaunchBusinessHub(projectRoot, opts = {}) {
if (!opts.noBrowser && process.env.RSTACK_NO_BROWSER !== '1') openBrowser(url);
}

function openBrowser(url) {
const cmd = process.platform === 'win32' ? 'start'
: process.platform === 'darwin' ? 'open' : 'xdg-open';
// platform/spawnImpl are injectable (mirrors sandbox.js's spawnImpl convention)
// so #470's windowsHide behavior is testable per-platform without actually
// spawning a browser.
export function openBrowser(url, { platform = process.platform, spawnImpl = spawn } = {}) {
const cmd = platform === 'win32' ? 'start'
: platform === 'darwin' ? 'open' : 'xdg-open';
try {
spawn(cmd, [url], { stdio: 'ignore', detached: true, shell: process.platform === 'win32' }).unref();
// On win32 this runs through `cmd.exe /c start <url>` (shell: true); without
// windowsHide, that intermediate cmd.exe briefly flashes a visible console
// window before handing off to the actual browser process.
spawnImpl(cmd, [url], {
stdio: 'ignore',
detached: true,
shell: platform === 'win32',
windowsHide: true,
}).unref();
Comment on lines +59 to +67

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Duplicated openbrowser/openurl spawn logic 📜 Skill insight ⚙ Maintainability

The PR repeats near-identical spawn(..., { shell: win32, windowsHide: true, ... }) browser-launch
logic (and explanatory comments) across multiple files, increasing maintenance overhead and drift
risk. This violates the DRY compliance requirement to refactor duplicated logic into a shared
helper/module.
Agent Prompt
## Issue description
The same `openBrowser`/`openUrl` spawn configuration (including `shell: process.platform === 'win32'` and `windowsHide: true`) is duplicated across multiple files, which makes future changes error-prone and inconsistent.

## Issue Context
This PR already touches all copies of the helper, which is a good opportunity to consolidate into a single shared utility (e.g., `src/utils/open-browser.{js,ts}`) and import it where needed.

## Fix Focus Areas
- src/hooks/auto-launch.js[56-64]
- src/observability/dashboard/server.js[128-135]
- src/integrations/pi/rstack-sdlc.ts[102-109]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — exported openBrowser (auto-launch.js) / openUrl (rstack-sdlc.ts) with an injectable platform/spawnImpl (mirrors sandbox.js's existing spawnImpl convention), and added tests/windows-console-flash-470.test.js pinning windowsHide:true + per-platform shell selection for both, plus sandbox.js's startContainerRuntime. dashboard/server.js's identical copy is covered by code-review parity rather than a direct import test — that module calls server.listen() unguarded at the top level (pre-existing, out of scope here), so importing it for the export would start a real HTTP server as a side effect.

} catch { /* best-effort */ }
}
26 changes: 21 additions & 5 deletions src/integrations/pi/rstack-sdlc.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { StringEnum } from "@earendil-works/pi-ai";
import { Type } from "typebox";
import { spawn } from "node:child_process";
import { spawn, type ChildProcess } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import { request as httpRequest } from "node:http";
import { createConnection } from "node:net";
Expand Down Expand Up @@ -94,12 +94,28 @@ function safeOpen(filePath: string): void {
}
}

function openUrl(url: string): void {
// platform/spawnImpl are injectable (mirrors sandbox.js's spawnImpl convention)
// so #470's windowsHide behavior is testable per-platform without actually
// spawning a browser.
export function openUrl(
url: string,
opts: { platform?: NodeJS.Platform; spawnImpl?: typeof spawn } = {},
): void {
if (process.env.CI) return;
const cmd = process.platform === "win32" ? "start"
: process.platform === "darwin" ? "open" : "xdg-open";
const platform = opts.platform ?? process.platform;
const spawnImpl = opts.spawnImpl ?? spawn;
const cmd = platform === "win32" ? "start"
: platform === "darwin" ? "open" : "xdg-open";
try {
const cp = spawn(cmd, [url], { stdio: "ignore", detached: true, shell: process.platform === "win32" });
// On win32 this runs through `cmd.exe /c start <url>` (shell: true); without
// windowsHide, that intermediate cmd.exe briefly flashes a visible console
// window before handing off to the actual browser process.
const cp: ChildProcess = spawnImpl(cmd, [url], {
stdio: "ignore",
detached: true,
shell: platform === "win32",
windowsHide: true,
});
cp.on("error", () => {});
cp.unref();
} catch { /* best-effort */ }
Expand Down
18 changes: 14 additions & 4 deletions src/observability/dashboard/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,21 @@ function safeJson(str) {
try { return JSON.parse(str); } catch { return null; }
}

function openBrowser(url) {
const cmd = process.platform === 'win32' ? 'start'
: process.platform === 'darwin' ? 'open' : 'xdg-open';
// platform/spawnImpl are injectable (mirrors sandbox.js's spawnImpl convention)
// so #470's windowsHide behavior is testable per-platform without actually
// spawning a browser.
export function openBrowser(url, { platform = process.platform, spawnImpl = spawn } = {}) {
const cmd = platform === 'win32' ? 'start'
: platform === 'darwin' ? 'open' : 'xdg-open';
try {
spawn(cmd, [url], { stdio: 'ignore', detached: true, shell: process.platform === 'win32' }).unref();
// windowsHide suppresses the cmd.exe console window that `shell: true`
// would otherwise briefly flash on win32 before handing off to the browser.
spawnImpl(cmd, [url], {
stdio: 'ignore',
detached: true,
shell: platform === 'win32',
windowsHide: true,
}).unref();
} catch {
// best effort only
}
Expand Down
109 changes: 109 additions & 0 deletions tests/windows-console-flash-470.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* #470: on win32, spawning a browser/runtime process via `cmd.exe` (shell:
* true) or directly (cmd.exe/podman.exe) briefly flashes a visible console
* window unless `windowsHide: true` is passed — Node's spawn() defaults it
* to false. Pins that every one of the four affected spawn call sites
* passes windowsHide on win32, and that non-win32 platforms are unaffected
* (the option is harmless there, but shell must stay false).
*
* owner: RStack developed by Richardson Gunde
*/

// src/observability/dashboard/server.js has an identical openBrowser copy,
// but that module calls server.listen() unguarded at the top level (a
// pre-existing design predating this PR, out of scope to refactor here) —
// importing it for its openBrowser export would start a real HTTP server as
// a side effect, so it's covered by code-review parity with the two
// call sites below instead of a direct import.
import test from 'node:test';
import assert from 'node:assert/strict';
import { openBrowser as openBrowserAutoLaunch } from '../src/hooks/auto-launch.js';
import { openUrl } from '../src/integrations/pi/rstack-sdlc.ts';
import { startContainerRuntime } from '../src/core/harness/sandbox.js';

function fakeChild() {
return { on() {}, unref() {} };
}

function recordingSpawn(calls) {
return (cmd, args, options) => {
calls.push({ cmd, args, options });
return fakeChild();
};
}

test('auto-launch.js openBrowser: windowsHide:true + shell:true on win32', () => {
const calls = [];
openBrowserAutoLaunch('http://localhost:3008', { platform: 'win32', spawnImpl: recordingSpawn(calls) });
assert.equal(calls.length, 1);
assert.equal(calls[0].cmd, 'start');
assert.equal(calls[0].options.shell, true);
assert.equal(calls[0].options.windowsHide, true);
});

test('auto-launch.js openBrowser: shell:false on macOS/Linux, windowsHide harmless', () => {
const calls = [];
openBrowserAutoLaunch('http://localhost:3008', { platform: 'darwin', spawnImpl: recordingSpawn(calls) });
assert.equal(calls[0].cmd, 'open');
assert.equal(calls[0].options.shell, false);
});

// openUrl early-returns when process.env.CI is set (intentional — avoids
// popping a browser during a CI run), which GitHub Actions always sets. Clear
// it for the duration of these two tests so they exercise the real spawn
// path there too, restoring it afterward so the guard itself stays covered.
function withoutCiEnv(fn) {
const had = Object.prototype.hasOwnProperty.call(process.env, 'CI');
const prior = process.env.CI;
delete process.env.CI;
try {
fn();
} finally {
if (had) process.env.CI = prior;
}
}

test('pi/rstack-sdlc.ts openUrl: windowsHide:true + shell:true on win32', () => {
withoutCiEnv(() => {
const calls = [];
openUrl('http://localhost:3008', { platform: 'win32', spawnImpl: recordingSpawn(calls) });
assert.equal(calls[0].cmd, 'start');
assert.equal(calls[0].options.shell, true);
assert.equal(calls[0].options.windowsHide, true);
});
});

test('pi/rstack-sdlc.ts openUrl: linux uses xdg-open, shell:false', () => {
withoutCiEnv(() => {
const calls = [];
openUrl('http://localhost:3008', { platform: 'linux', spawnImpl: recordingSpawn(calls) });
assert.equal(calls[0].cmd, 'xdg-open');
assert.equal(calls[0].options.shell, false);
});
});

test('pi/rstack-sdlc.ts openUrl: no-ops under CI (never pops a browser during automated runs)', () => {
const calls = [];
const original = process.env.CI;
process.env.CI = '1';
try {
openUrl('http://localhost:3008', { platform: 'win32', spawnImpl: recordingSpawn(calls) });
} finally {
if (original === undefined) delete process.env.CI; else process.env.CI = original;
}
assert.equal(calls.length, 0, 'openUrl must not spawn anything when CI is set');
});

test('sandbox.js startContainerRuntime: windowsHide:true on the engine auto-start spawn', async () => {
const calls = [];
const result = await startContainerRuntime({
installedProbe: () => 'docker',
readyProbe: () => false,
spawnImpl: (cmd, args, options) => { calls.push({ cmd, args, options }); return fakeChild(); },
platform: 'win32',
timeoutMs: 0,
});
assert.equal(calls.length, 1);
assert.equal(calls[0].options.windowsHide, true);
assert.equal(result.started, true);
});
Loading