import { spawn as nodeSpawn, type SpawnOptions } from "node:child_process"; import { platform as nodePlatform } from "node:os"; type SpawnedProcess = { once( event: "spawn" | "error", listener: (error?: Error) => void, ): SpawnedProcess; unref(): void; }; export type SpawnLike = ( command: string, args: string[], options: SpawnOptions, ) => SpawnedProcess; export type OpenUrlResult = | { status: "opened"; command: string; args: string[] } | { status: "skipped"; reason: string } | { status: "failed"; command: string; args: string[]; error: string }; type OpenUrlOptions = { noOpen?: boolean; json?: boolean; env?: Record; platform?: NodeJS.Platform; spawn?: SpawnLike; }; function truthyEnv(value: string | undefined) { if (!value) return false; return !["0", "false", "no"].includes(value.trim().toLowerCase()); } function disabledReason(options: OpenUrlOptions) { if (options.noOpen) return "--no-open"; if (options.json) return "--json"; if (truthyEnv(options.env?.DENCH_NO_OPEN)) return "DENCH_NO_OPEN"; if (truthyEnv(options.env?.CI)) return "CI"; if ( ["none", "false"].includes(options.env?.BROWSER?.trim().toLowerCase() ?? "") ) { return "BROWSER"; } return undefined; } function openerCommand(url: string, currentPlatform: NodeJS.Platform) { if (currentPlatform === "darwin") { return { command: "open", args: [url] }; } if (currentPlatform === "win32") { // Invoke the URL handler directly; cmd interprets metacharacters in URLs. return { command: "rundll32.exe", args: ["url.dll,FileProtocolHandler", url], }; } return { command: "xdg-open", args: [url] }; } export async function openUrl( url: string, options: OpenUrlOptions = {}, ): Promise { const reason = disabledReason(options); if (reason) return { status: "skipped", reason }; let target: URL; try { target = new URL(url); } catch { return { status: "skipped", reason: "invalid-url" }; } if ( (target.protocol !== "https:" && target.protocol !== "http:") || target.username || target.password ) { return { status: "skipped", reason: "unsafe-url" }; } const { command, args } = openerCommand( target.href, options.platform ?? nodePlatform(), ); const spawn = options.spawn ?? (nodeSpawn as SpawnLike); return new Promise((resolve) => { let settled = false; const settle = (result: OpenUrlResult) => { if (!settled) { settled = true; resolve(result); } }; try { const child = spawn(command, args, { detached: true, stdio: "ignore", }); child.once("error", (error) => settle({ status: "failed", command, args, error: error instanceof Error ? error.message : String(error), }), ); child.once("spawn", () => { child.unref(); settle({ status: "opened", command, args }); }); } catch (error) { settle({ status: "failed", command, args, error: error instanceof Error ? error.message : String(error), }); } }); } export function formatApprovalOpenMessage(url: string, result: OpenUrlResult) { if (result.status === "opened") { return `Attempted to open Dench approval link in your browser. If it did not open, use this link: ${url}`; } if (result.status === "failed") { return `Could not open browser automatically. Open this Dench approval link: ${url}`; } if (result.reason === "--no-open") { return `Open this Dench approval link: ${url}`; } return `Automatic browser open is disabled (${result.reason}). Open this Dench approval link: ${url}`; }