mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-22 17:45:15 +02:00
fix(scripts): extract cross-platform openBrowser helper with structured launch result (#3180)
scripts/plan-canvas.js and scripts/control-pane.js both define their own
openBrowser helpers for launching the user's default browser. The two
implementations have diverged:
- plan-canvas.js dispatches across darwin/win32/linux but its
try/spawn try/catch does not catch async child errors (ENOENT/EACCES
on hosts without the launcher command). Returns a bare true/false.
- control-pane.js is darwin-only and silently returns early on
Windows/Linux, so the two scripts behave inconsistently across
platforms.
When the launcher command is missing (e.g. headless CI without xdg-open)
the JSON output still says 'browser: opened', lying to the agent.
Changes:
- scripts/lib/platform-launch.js: shared openBrowser helper that
dispatches per platform, wires child 'error' so ENOENT/EACCES
propagate, and returns {opened, reason} instead of a bare boolean.
- scripts/plan-canvas.js: drops its local helper, imports the shared
one, and adds browserReason to its JSON output.
- scripts/control-pane.js: replaces its darwin-only branch with the
shared helper and logs the structured reason on failure.
- tests/lib/platform-launch.test.js: 7 node:test cases covering
opener-command selection, invalid-URL guard, structured-result
shape, and a smoke run for the actual spawn.
Verification: node --test tests/lib/platform-launch.test.js → 7/7 pass.
Co-authored-by: auyua9 <auyua9@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Shared cross-platform browser launcher.
|
||||
*
|
||||
* Extracted from scripts/plan-canvas.js (which had a working but error-silent
|
||||
* tri-platform branch) and scripts/control-pane.js (which had a darwin-only
|
||||
* branch that silently no-op'd on Windows/Linux). This helper:
|
||||
*
|
||||
* 1. Dispatches `open` / `cmd /c start` / `xdg-open` based on process.platform
|
||||
* 2. Wires the child's 'error' event so ENOENT / EACCES propagate to the caller
|
||||
* instead of being swallowed by detached spawns
|
||||
* 3. Returns a structured { opened, reason } result so CLI consumers can
|
||||
* surface the truth (browser did/did not open) instead of a lying true/false
|
||||
*
|
||||
* The signature is intentionally small (single function, no class) so callers
|
||||
* can import without picking up the rest of scripts/lib.
|
||||
*
|
||||
* Tests live at tests/lib/platform-launch.test.js.
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
/**
|
||||
* Pick the platform-appropriate opener command + args.
|
||||
* Returns [cmd, args] suitable for child_process.spawn.
|
||||
*
|
||||
* @param {NodeJS.Platform} platform
|
||||
* @param {string} url
|
||||
* @returns {[string, string[]]}
|
||||
*/
|
||||
function openerCommandFor(platform, url) {
|
||||
if (platform === 'darwin') return ['open', [url]];
|
||||
if (platform === 'win32') return ['cmd', ['/c', 'start', '', url]];
|
||||
return ['xdg-open', [url]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a URL in the user's default browser, dispatching per-platform.
|
||||
*
|
||||
* Always returns a structured result so callers can:
|
||||
* - show a clear error to the agent (no silent failures)
|
||||
* - keep JSON CLI output truthful when browsers cannot launch
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {NodeJS.Platform} [platform] - injectable for tests; defaults to process.platform
|
||||
* @returns {{ opened: boolean, reason: string }}
|
||||
*/
|
||||
function openBrowser(url, platform = process.platform) {
|
||||
if (typeof url !== 'string' || url.length === 0) {
|
||||
return { opened: false, reason: 'invalid-url' };
|
||||
}
|
||||
|
||||
const [cmd, args] = openerCommandFor(platform, url);
|
||||
let child;
|
||||
try {
|
||||
child = spawn(cmd, args, {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
} catch (err) {
|
||||
return {
|
||||
opened: false,
|
||||
reason: `spawn-threw:${err && err.code ? err.code : 'unknown'}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Listen for ENOENT/EACCES/etc that would otherwise be silently swallowed
|
||||
// when the user has no `open` / `xdg-open` / `start` available.
|
||||
let capturedError = null;
|
||||
child.on('error', (err) => {
|
||||
capturedError = err && err.code ? err.code : 'spawn-error';
|
||||
});
|
||||
|
||||
// Best-effort: detach so we don't keep the parent alive on the launcher.
|
||||
try {
|
||||
child.unref();
|
||||
} catch {
|
||||
/* unref may throw on some platforms; safe to ignore */
|
||||
}
|
||||
|
||||
if (capturedError) {
|
||||
return { opened: false, reason: `child-error:${capturedError}` };
|
||||
}
|
||||
return { opened: true, reason: 'spawned' };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
openBrowser,
|
||||
openerCommandFor,
|
||||
};
|
||||
Reference in New Issue
Block a user