fix: WSL auto-open crash + Windows-interop handoff

spawn()'s ENOENT for a missing binary surfaces via the async 'error'
event, not a synchronous throw — the prior try/catch never caught it,
crashing the whole process when xdg-open isn't installed (the normal
case on WSL, which has no desktop environment).

WSL also now hands off to explorer.exe via WSL interop (wslpath -w to
get a Windows-style path) instead of trying xdg-open at all. Verified
against a real WSL2 box: the prior code crashed on `-- open`, this one
opens the file in the user's actual Windows browser.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
M T 2026-06-23 11:57:39 +10:00
parent cd518288f9
commit 8d396294a9
1 changed files with 37 additions and 5 deletions

View File

@ -18,7 +18,7 @@
*/
import { existsSync, readFileSync, writeFileSync, statSync, readdirSync } from "fs";
import { join, basename } from "path";
import { join, basename, resolve } from "path";
import { execFileSync, spawn } from "child_process";
import { homedir } from "os";
@ -773,26 +773,58 @@ function renderPage(slug: string, branch: string, sections: Record<string, strin
// ─── Open helper ─────────────────────────────────────────────────────────
function isWsl(): boolean {
if (process.platform !== "linux") return false;
try {
return /microsoft/i.test(readFileSync("/proc/version", "utf-8"));
} catch {
return false;
}
}
function openInBrowser(path: string) {
const absPath = resolve(path);
let cmd: string;
let args: string[];
if (process.platform === "darwin") {
cmd = "open";
args = [path];
args = [absPath];
} else if (process.platform === "win32") {
// explorer.exe, not `cmd /c start` — start mishandles paths with
// special characters and pops a console window on some configs.
cmd = "explorer.exe";
args = [path];
args = [absPath];
} else if (isWsl()) {
// WSL has no desktop environment / MIME handlers on the Linux side —
// xdg-open is typically not even installed. Hand off to the Windows
// side via WSL interop instead. explorer.exe needs a Windows-style
// path (C:\...), not the WSL /mnt/c/... or native Linux path.
cmd = "explorer.exe";
try {
const winPath = execFileSync("wslpath", ["-w", absPath], {
encoding: "utf-8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
args = [winPath];
} catch {
args = [absPath];
}
} else {
cmd = "xdg-open";
args = [path];
args = [absPath];
}
try {
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
// ENOENT (missing binary) surfaces asynchronously via the 'error' event,
// not as a synchronous throw — without this handler it's an unhandled
// exception that crashes the whole process (observed: xdg-open missing
// on a WSL box with no desktop environment).
child.on("error", () => {
console.error(`Could not auto-open ${absPath} automatically — open it manually.`);
});
child.unref();
} catch {
console.error(`Could not auto-open ${path} — open it manually.`);
console.error(`Could not auto-open ${absPath} automatically — open it manually.`);
}
}