Spaces:
Configuration error
Configuration error
| import { execFileSync } from "node:child_process"; | |
| import fs from "node:fs"; | |
| import os from "node:os"; | |
| import path from "node:path"; | |
| import type { ResolvedBrowserConfig } from "./config.js"; | |
| export type BrowserExecutable = { | |
| kind: "brave" | "canary" | "chromium" | "chrome" | "custom" | "edge"; | |
| path: string; | |
| }; | |
| const CHROMIUM_BUNDLE_IDS = new Set([ | |
| "com.google.Chrome", | |
| "com.google.Chrome.beta", | |
| "com.google.Chrome.canary", | |
| "com.google.Chrome.dev", | |
| "com.brave.Browser", | |
| "com.brave.Browser.beta", | |
| "com.brave.Browser.nightly", | |
| "com.microsoft.Edge", | |
| "com.microsoft.EdgeBeta", | |
| "com.microsoft.EdgeDev", | |
| "com.microsoft.EdgeCanary", | |
| "org.chromium.Chromium", | |
| "com.vivaldi.Vivaldi", | |
| "com.operasoftware.Opera", | |
| "com.operasoftware.OperaGX", | |
| "com.yandex.desktop.yandex-browser", | |
| "company.thebrowser.Browser", // Arc | |
| ]); | |
| const CHROMIUM_DESKTOP_IDS = new Set([ | |
| "google-chrome.desktop", | |
| "google-chrome-beta.desktop", | |
| "google-chrome-unstable.desktop", | |
| "brave-browser.desktop", | |
| "microsoft-edge.desktop", | |
| "microsoft-edge-beta.desktop", | |
| "microsoft-edge-dev.desktop", | |
| "microsoft-edge-canary.desktop", | |
| "chromium.desktop", | |
| "chromium-browser.desktop", | |
| "vivaldi.desktop", | |
| "vivaldi-stable.desktop", | |
| "opera.desktop", | |
| "opera-gx.desktop", | |
| "yandex-browser.desktop", | |
| "org.chromium.Chromium.desktop", | |
| ]); | |
| const CHROMIUM_EXE_NAMES = new Set([ | |
| "chrome.exe", | |
| "msedge.exe", | |
| "brave.exe", | |
| "brave-browser.exe", | |
| "chromium.exe", | |
| "vivaldi.exe", | |
| "opera.exe", | |
| "launcher.exe", | |
| "yandex.exe", | |
| "yandexbrowser.exe", | |
| // mac/linux names | |
| "google chrome", | |
| "google chrome canary", | |
| "brave browser", | |
| "microsoft edge", | |
| "chromium", | |
| "chrome", | |
| "brave", | |
| "msedge", | |
| "brave-browser", | |
| "google-chrome", | |
| "google-chrome-stable", | |
| "google-chrome-beta", | |
| "google-chrome-unstable", | |
| "microsoft-edge", | |
| "microsoft-edge-beta", | |
| "microsoft-edge-dev", | |
| "microsoft-edge-canary", | |
| "chromium-browser", | |
| "vivaldi", | |
| "vivaldi-stable", | |
| "opera", | |
| "opera-stable", | |
| "opera-gx", | |
| "yandex-browser", | |
| ]); | |
| function exists(filePath: string) { | |
| try { | |
| return fs.existsSync(filePath); | |
| } catch { | |
| return false; | |
| } | |
| } | |
| function execText( | |
| command: string, | |
| args: string[], | |
| timeoutMs = 1200, | |
| maxBuffer = 1024 * 1024, | |
| ): string | null { | |
| try { | |
| const output = execFileSync(command, args, { | |
| timeout: timeoutMs, | |
| encoding: "utf8", | |
| maxBuffer, | |
| }); | |
| return String(output ?? "").trim() || null; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| function inferKindFromIdentifier(identifier: string): BrowserExecutable["kind"] { | |
| const id = identifier.toLowerCase(); | |
| if (id.includes("brave")) return "brave"; | |
| if (id.includes("edge")) return "edge"; | |
| if (id.includes("chromium")) return "chromium"; | |
| if (id.includes("canary")) return "canary"; | |
| if ( | |
| id.includes("opera") || | |
| id.includes("vivaldi") || | |
| id.includes("yandex") || | |
| id.includes("thebrowser") | |
| ) { | |
| return "chromium"; | |
| } | |
| return "chrome"; | |
| } | |
| function inferKindFromExecutableName(name: string): BrowserExecutable["kind"] { | |
| const lower = name.toLowerCase(); | |
| if (lower.includes("brave")) return "brave"; | |
| if (lower.includes("edge") || lower.includes("msedge")) return "edge"; | |
| if (lower.includes("chromium")) return "chromium"; | |
| if (lower.includes("canary") || lower.includes("sxs")) return "canary"; | |
| if (lower.includes("opera") || lower.includes("vivaldi") || lower.includes("yandex")) | |
| return "chromium"; | |
| return "chrome"; | |
| } | |
| function detectDefaultChromiumExecutable(platform: NodeJS.Platform): BrowserExecutable | null { | |
| if (platform === "darwin") return detectDefaultChromiumExecutableMac(); | |
| if (platform === "linux") return detectDefaultChromiumExecutableLinux(); | |
| if (platform === "win32") return detectDefaultChromiumExecutableWindows(); | |
| return null; | |
| } | |
| function detectDefaultChromiumExecutableMac(): BrowserExecutable | null { | |
| const bundleId = detectDefaultBrowserBundleIdMac(); | |
| if (!bundleId || !CHROMIUM_BUNDLE_IDS.has(bundleId)) return null; | |
| const appPathRaw = execText("/usr/bin/osascript", [ | |
| "-e", | |
| `POSIX path of (path to application id "${bundleId}")`, | |
| ]); | |
| if (!appPathRaw) return null; | |
| const appPath = appPathRaw.trim().replace(/\/$/, ""); | |
| const exeName = execText("/usr/bin/defaults", [ | |
| "read", | |
| path.join(appPath, "Contents", "Info"), | |
| "CFBundleExecutable", | |
| ]); | |
| if (!exeName) return null; | |
| const exePath = path.join(appPath, "Contents", "MacOS", exeName.trim()); | |
| if (!exists(exePath)) return null; | |
| return { kind: inferKindFromIdentifier(bundleId), path: exePath }; | |
| } | |
| function detectDefaultBrowserBundleIdMac(): string | null { | |
| const plistPath = path.join( | |
| os.homedir(), | |
| "Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist", | |
| ); | |
| if (!exists(plistPath)) return null; | |
| const handlersRaw = execText( | |
| "/usr/bin/plutil", | |
| ["-extract", "LSHandlers", "json", "-o", "-", "--", plistPath], | |
| 2000, | |
| 5 * 1024 * 1024, | |
| ); | |
| if (!handlersRaw) return null; | |
| let handlers: unknown; | |
| try { | |
| handlers = JSON.parse(handlersRaw); | |
| } catch { | |
| return null; | |
| } | |
| if (!Array.isArray(handlers)) return null; | |
| const resolveScheme = (scheme: string) => { | |
| let candidate: string | null = null; | |
| for (const entry of handlers) { | |
| if (!entry || typeof entry !== "object") continue; | |
| const record = entry as Record<string, unknown>; | |
| if (record.LSHandlerURLScheme !== scheme) continue; | |
| const role = | |
| (typeof record.LSHandlerRoleAll === "string" && record.LSHandlerRoleAll) || | |
| (typeof record.LSHandlerRoleViewer === "string" && record.LSHandlerRoleViewer) || | |
| null; | |
| if (role) candidate = role; | |
| } | |
| return candidate; | |
| }; | |
| return resolveScheme("http") ?? resolveScheme("https"); | |
| } | |
| function detectDefaultChromiumExecutableLinux(): BrowserExecutable | null { | |
| const desktopId = | |
| execText("xdg-settings", ["get", "default-web-browser"]) || | |
| execText("xdg-mime", ["query", "default", "x-scheme-handler/http"]); | |
| if (!desktopId) return null; | |
| const trimmed = desktopId.trim(); | |
| if (!CHROMIUM_DESKTOP_IDS.has(trimmed)) return null; | |
| const desktopPath = findDesktopFilePath(trimmed); | |
| if (!desktopPath) return null; | |
| const execLine = readDesktopExecLine(desktopPath); | |
| if (!execLine) return null; | |
| const command = extractExecutableFromExecLine(execLine); | |
| if (!command) return null; | |
| const resolved = resolveLinuxExecutablePath(command); | |
| if (!resolved) return null; | |
| const exeName = path.posix.basename(resolved).toLowerCase(); | |
| if (!CHROMIUM_EXE_NAMES.has(exeName)) return null; | |
| return { kind: inferKindFromExecutableName(exeName), path: resolved }; | |
| } | |
| function detectDefaultChromiumExecutableWindows(): BrowserExecutable | null { | |
| const progId = readWindowsProgId(); | |
| const command = | |
| (progId ? readWindowsCommandForProgId(progId) : null) || readWindowsCommandForProgId("http"); | |
| if (!command) return null; | |
| const expanded = expandWindowsEnvVars(command); | |
| const exePath = extractWindowsExecutablePath(expanded); | |
| if (!exePath) return null; | |
| if (!exists(exePath)) return null; | |
| const exeName = path.win32.basename(exePath).toLowerCase(); | |
| if (!CHROMIUM_EXE_NAMES.has(exeName)) return null; | |
| return { kind: inferKindFromExecutableName(exeName), path: exePath }; | |
| } | |
| function findDesktopFilePath(desktopId: string): string | null { | |
| const candidates = [ | |
| path.join(os.homedir(), ".local", "share", "applications", desktopId), | |
| path.join("/usr/local/share/applications", desktopId), | |
| path.join("/usr/share/applications", desktopId), | |
| path.join("/var/lib/snapd/desktop/applications", desktopId), | |
| ]; | |
| for (const candidate of candidates) { | |
| if (exists(candidate)) return candidate; | |
| } | |
| return null; | |
| } | |
| function readDesktopExecLine(desktopPath: string): string | null { | |
| try { | |
| const raw = fs.readFileSync(desktopPath, "utf8"); | |
| const lines = raw.split(/\r?\n/); | |
| for (const line of lines) { | |
| if (line.startsWith("Exec=")) { | |
| return line.slice("Exec=".length).trim(); | |
| } | |
| } | |
| } catch { | |
| // ignore | |
| } | |
| return null; | |
| } | |
| function extractExecutableFromExecLine(execLine: string): string | null { | |
| const tokens = splitExecLine(execLine); | |
| for (const token of tokens) { | |
| if (!token) continue; | |
| if (token === "env") continue; | |
| if (token.includes("=") && !token.startsWith("/") && !token.includes("\\")) continue; | |
| return token.replace(/^["']|["']$/g, ""); | |
| } | |
| return null; | |
| } | |
| function splitExecLine(line: string): string[] { | |
| const tokens: string[] = []; | |
| let current = ""; | |
| let inQuotes = false; | |
| let quoteChar = ""; | |
| for (let i = 0; i < line.length; i += 1) { | |
| const ch = line[i]; | |
| if ((ch === '"' || ch === "'") && (!inQuotes || ch === quoteChar)) { | |
| if (inQuotes) { | |
| inQuotes = false; | |
| quoteChar = ""; | |
| } else { | |
| inQuotes = true; | |
| quoteChar = ch; | |
| } | |
| continue; | |
| } | |
| if (!inQuotes && /\s/.test(ch)) { | |
| if (current) { | |
| tokens.push(current); | |
| current = ""; | |
| } | |
| continue; | |
| } | |
| current += ch; | |
| } | |
| if (current) tokens.push(current); | |
| return tokens; | |
| } | |
| function resolveLinuxExecutablePath(command: string): string | null { | |
| const cleaned = command.trim().replace(/%[a-zA-Z]/g, ""); | |
| if (!cleaned) return null; | |
| if (cleaned.startsWith("/")) return cleaned; | |
| const resolved = execText("which", [cleaned], 800); | |
| return resolved ? resolved.trim() : null; | |
| } | |
| function readWindowsProgId(): string | null { | |
| const output = execText("reg", [ | |
| "query", | |
| "HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice", | |
| "/v", | |
| "ProgId", | |
| ]); | |
| if (!output) return null; | |
| const match = output.match(/ProgId\s+REG_\w+\s+(.+)$/im); | |
| return match?.[1]?.trim() || null; | |
| } | |
| function readWindowsCommandForProgId(progId: string): string | null { | |
| const key = | |
| progId === "http" | |
| ? "HKCR\\http\\shell\\open\\command" | |
| : `HKCR\\${progId}\\shell\\open\\command`; | |
| const output = execText("reg", ["query", key, "/ve"]); | |
| if (!output) return null; | |
| const match = output.match(/REG_\w+\s+(.+)$/im); | |
| return match?.[1]?.trim() || null; | |
| } | |
| function expandWindowsEnvVars(value: string): string { | |
| return value.replace(/%([^%]+)%/g, (_match, name) => { | |
| const key = String(name ?? "").trim(); | |
| return key ? (process.env[key] ?? `%${key}%`) : _match; | |
| }); | |
| } | |
| function extractWindowsExecutablePath(command: string): string | null { | |
| const quoted = command.match(/"([^"]+\\.exe)"/i); | |
| if (quoted?.[1]) return quoted[1]; | |
| const unquoted = command.match(/([^\\s]+\\.exe)/i); | |
| if (unquoted?.[1]) return unquoted[1]; | |
| return null; | |
| } | |
| function findFirstExecutable(candidates: Array<BrowserExecutable>): BrowserExecutable | null { | |
| for (const candidate of candidates) { | |
| if (exists(candidate.path)) return candidate; | |
| } | |
| return null; | |
| } | |
| export function findChromeExecutableMac(): BrowserExecutable | null { | |
| const candidates: Array<BrowserExecutable> = [ | |
| { | |
| kind: "chrome", | |
| path: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", | |
| }, | |
| { | |
| kind: "chrome", | |
| path: path.join(os.homedir(), "Applications/Google Chrome.app/Contents/MacOS/Google Chrome"), | |
| }, | |
| { | |
| kind: "brave", | |
| path: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser", | |
| }, | |
| { | |
| kind: "brave", | |
| path: path.join(os.homedir(), "Applications/Brave Browser.app/Contents/MacOS/Brave Browser"), | |
| }, | |
| { | |
| kind: "edge", | |
| path: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", | |
| }, | |
| { | |
| kind: "edge", | |
| path: path.join( | |
| os.homedir(), | |
| "Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", | |
| ), | |
| }, | |
| { | |
| kind: "chromium", | |
| path: "/Applications/Chromium.app/Contents/MacOS/Chromium", | |
| }, | |
| { | |
| kind: "chromium", | |
| path: path.join(os.homedir(), "Applications/Chromium.app/Contents/MacOS/Chromium"), | |
| }, | |
| { | |
| kind: "canary", | |
| path: "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", | |
| }, | |
| { | |
| kind: "canary", | |
| path: path.join( | |
| os.homedir(), | |
| "Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", | |
| ), | |
| }, | |
| ]; | |
| return findFirstExecutable(candidates); | |
| } | |
| export function findChromeExecutableLinux(): BrowserExecutable | null { | |
| const candidates: Array<BrowserExecutable> = [ | |
| { kind: "chrome", path: "/usr/bin/google-chrome" }, | |
| { kind: "chrome", path: "/usr/bin/google-chrome-stable" }, | |
| { kind: "chrome", path: "/usr/bin/chrome" }, | |
| { kind: "brave", path: "/usr/bin/brave-browser" }, | |
| { kind: "brave", path: "/usr/bin/brave-browser-stable" }, | |
| { kind: "brave", path: "/usr/bin/brave" }, | |
| { kind: "brave", path: "/snap/bin/brave" }, | |
| { kind: "edge", path: "/usr/bin/microsoft-edge" }, | |
| { kind: "edge", path: "/usr/bin/microsoft-edge-stable" }, | |
| { kind: "chromium", path: "/usr/bin/chromium" }, | |
| { kind: "chromium", path: "/usr/bin/chromium-browser" }, | |
| { kind: "chromium", path: "/snap/bin/chromium" }, | |
| ]; | |
| return findFirstExecutable(candidates); | |
| } | |
| export function findChromeExecutableWindows(): BrowserExecutable | null { | |
| const localAppData = process.env.LOCALAPPDATA ?? ""; | |
| const programFiles = process.env.ProgramFiles ?? "C:\\Program Files"; | |
| // Must use bracket notation: variable name contains parentheses | |
| const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)"; | |
| const joinWin = path.win32.join; | |
| const candidates: Array<BrowserExecutable> = []; | |
| if (localAppData) { | |
| // Chrome (user install) | |
| candidates.push({ | |
| kind: "chrome", | |
| path: joinWin(localAppData, "Google", "Chrome", "Application", "chrome.exe"), | |
| }); | |
| // Brave (user install) | |
| candidates.push({ | |
| kind: "brave", | |
| path: joinWin(localAppData, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"), | |
| }); | |
| // Edge (user install) | |
| candidates.push({ | |
| kind: "edge", | |
| path: joinWin(localAppData, "Microsoft", "Edge", "Application", "msedge.exe"), | |
| }); | |
| // Chromium (user install) | |
| candidates.push({ | |
| kind: "chromium", | |
| path: joinWin(localAppData, "Chromium", "Application", "chrome.exe"), | |
| }); | |
| // Chrome Canary (user install) | |
| candidates.push({ | |
| kind: "canary", | |
| path: joinWin(localAppData, "Google", "Chrome SxS", "Application", "chrome.exe"), | |
| }); | |
| } | |
| // Chrome (system install, 64-bit) | |
| candidates.push({ | |
| kind: "chrome", | |
| path: joinWin(programFiles, "Google", "Chrome", "Application", "chrome.exe"), | |
| }); | |
| // Chrome (system install, 32-bit on 64-bit Windows) | |
| candidates.push({ | |
| kind: "chrome", | |
| path: joinWin(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"), | |
| }); | |
| // Brave (system install, 64-bit) | |
| candidates.push({ | |
| kind: "brave", | |
| path: joinWin(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"), | |
| }); | |
| // Brave (system install, 32-bit on 64-bit Windows) | |
| candidates.push({ | |
| kind: "brave", | |
| path: joinWin(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"), | |
| }); | |
| // Edge (system install, 64-bit) | |
| candidates.push({ | |
| kind: "edge", | |
| path: joinWin(programFiles, "Microsoft", "Edge", "Application", "msedge.exe"), | |
| }); | |
| // Edge (system install, 32-bit on 64-bit Windows) | |
| candidates.push({ | |
| kind: "edge", | |
| path: joinWin(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe"), | |
| }); | |
| return findFirstExecutable(candidates); | |
| } | |
| export function resolveBrowserExecutableForPlatform( | |
| resolved: ResolvedBrowserConfig, | |
| platform: NodeJS.Platform, | |
| ): BrowserExecutable | null { | |
| if (resolved.executablePath) { | |
| if (!exists(resolved.executablePath)) { | |
| throw new Error(`browser.executablePath not found: ${resolved.executablePath}`); | |
| } | |
| return { kind: "custom", path: resolved.executablePath }; | |
| } | |
| const detected = detectDefaultChromiumExecutable(platform); | |
| if (detected) return detected; | |
| if (platform === "darwin") return findChromeExecutableMac(); | |
| if (platform === "linux") return findChromeExecutableLinux(); | |
| if (platform === "win32") return findChromeExecutableWindows(); | |
| return null; | |
| } | |