import fs from "node:fs"; import path from "node:path"; type SpawnTarget = { command: string; argv: string[]; windowsHide?: boolean; }; function isFilePath(value: string): boolean { try { const stat = fs.statSync(value); return stat.isFile(); } catch { return false; } } function resolveWindowsExecutablePath(execPath: string, env: NodeJS.ProcessEnv): string { if (execPath.includes("/") || execPath.includes("\\") || path.isAbsolute(execPath)) { return execPath; } const pathValue = env.PATH ?? env.Path ?? process.env.PATH ?? process.env.Path ?? ""; const pathEntries = pathValue .split(";") .map((entry) => entry.trim()) .filter(Boolean); const hasExtension = path.extname(execPath).length > 0; const pathExtRaw = env.PATHEXT ?? env.Pathext ?? process.env.PATHEXT ?? process.env.Pathext ?? ".EXE;.CMD;.BAT;.COM"; const pathExt = hasExtension ? [""] : pathExtRaw .split(";") .map((ext) => ext.trim()) .filter(Boolean) .map((ext) => (ext.startsWith(".") ? ext : `.${ext}`)); for (const dir of pathEntries) { for (const ext of pathExt) { for (const candidateExt of [ext, ext.toLowerCase(), ext.toUpperCase()]) { const candidate = path.join(dir, `${execPath}${candidateExt}`); if (isFilePath(candidate)) { return candidate; } } } } return execPath; } function resolveBinEntry(binField: string | Record | undefined): string | null { if (typeof binField === "string") { const trimmed = binField.trim(); return trimmed || null; } if (!binField || typeof binField !== "object") { return null; } const preferred = binField.lobster; if (typeof preferred === "string" && preferred.trim()) { return preferred.trim(); } for (const value of Object.values(binField)) { if (typeof value === "string" && value.trim()) { return value.trim(); } } return null; } function resolveLobsterScriptFromPackageJson(wrapperPath: string): string | null { const wrapperDir = path.dirname(wrapperPath); const packageDirs = [ // Local install: /node_modules/.bin/lobster.cmd -> ../lobster path.resolve(wrapperDir, "..", "lobster"), // Global npm install: /lobster.cmd -> ./node_modules/lobster path.resolve(wrapperDir, "node_modules", "lobster"), ]; for (const packageDir of packageDirs) { const packageJsonPath = path.join(packageDir, "package.json"); if (!isFilePath(packageJsonPath)) { continue; } try { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as { bin?: string | Record; }; const scriptRel = resolveBinEntry(packageJson.bin); if (!scriptRel) { continue; } const scriptPath = path.resolve(packageDir, scriptRel); if (isFilePath(scriptPath)) { return scriptPath; } } catch { // Ignore malformed package metadata; caller will throw a guided error. } } return null; } function resolveLobsterScriptFromCmdShim(wrapperPath: string): string | null { if (!isFilePath(wrapperPath)) { return null; } try { const content = fs.readFileSync(wrapperPath, "utf8"); const candidates: string[] = []; const extractRelativeFromToken = (token: string): string | null => { const match = token.match(/%~?dp0%\s*[\\/]*(.*)$/i); if (!match) { return null; } const relative = match[1]; if (!relative) { return null; } return relative; }; const matches = content.matchAll(/"([^"\r\n]*)"/g); for (const match of matches) { const token = match[1] ?? ""; const relative = extractRelativeFromToken(token); if (!relative) { continue; } const normalizedRelative = relative .trim() .replace(/[\\/]+/g, path.sep) .replace(/^[\\/]+/, ""); const candidate = path.resolve(path.dirname(wrapperPath), normalizedRelative); if (isFilePath(candidate)) { candidates.push(candidate); } } const nonNode = candidates.find((candidate) => { const base = path.basename(candidate).toLowerCase(); return base !== "node.exe" && base !== "node"; }); if (nonNode) { return nonNode; } } catch { // Ignore unreadable shims; caller will throw a guided error. } return null; } export function resolveWindowsLobsterSpawn( execPath: string, argv: string[], env: NodeJS.ProcessEnv, ): SpawnTarget { const resolvedExecPath = resolveWindowsExecutablePath(execPath, env); const ext = path.extname(resolvedExecPath).toLowerCase(); if (ext !== ".cmd" && ext !== ".bat") { return { command: resolvedExecPath, argv }; } const scriptPath = resolveLobsterScriptFromCmdShim(resolvedExecPath) ?? resolveLobsterScriptFromPackageJson(resolvedExecPath); if (!scriptPath) { throw new Error( `${path.basename(resolvedExecPath)} wrapper resolved, but no Node entrypoint could be resolved without shell execution. Ensure Lobster is installed and runnable on PATH (prefer lobster.exe).`, ); } const entryExt = path.extname(scriptPath).toLowerCase(); if (entryExt === ".exe") { return { command: scriptPath, argv, windowsHide: true }; } return { command: process.execPath, argv: [scriptPath, ...argv], windowsHide: true }; }