"use strict"; /** * Binary discovery + child environment for GUI-context launches. * * macOS (Finder/launchd) and Windows (Explorer/scheduled-task) both start GUI * apps with a minimal PATH that cannot resolve the `#!/usr/bin/env node` * shebangs of dsh/npm. All spawned children therefore run through childEnv(), * which prepends the directories that actually hold dsh, npm, and node. */ const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const IS_WIN = process.platform === "win32"; function firstExisting(candidates) { for (const candidate of candidates) { try { if (candidate && fs.existsSync(candidate)) return candidate; } catch { /* next */ } } return null; } function pathDirs() { const sep = IS_WIN ? ";" : ":"; return (process.env.PATH || "").split(sep).filter(Boolean); } /** The executable extension for node/npm shims (.cmd on Windows, else none). */ function exe(name) { return IS_WIN ? `${name}.cmd` : name; } /** * The dsh binary: explicit override, common user install spots, then PATH. * On Windows, `npm i -g @deepseek-ai/dsh` places dsh.cmd in the npm prefix * (%APPDATA%\npm); on macOS, ~/.local/bin/dsh (a symlink to the package). */ function findDsh() { const override = process.env.DSH_DESKTOP_DSH_BIN; if (override) return override; const home = os.homedir(); const appdata = process.env.APPDATA || path.join(home, "AppData", "Roaming"); const candidates = IS_WIN ? [ path.join(appdata, "npm", "dsh.cmd"), path.join(home, ".local", "bin", "dsh.cmd"), path.join(home, "AppData", "Roaming", "npm", "dsh.cmd"), ...pathDirs().map((dir) => path.join(dir, "dsh.cmd")), ] : [ path.join(home, ".local/bin/dsh"), "/opt/homebrew/bin/dsh", "/usr/local/bin/dsh", ...pathDirs().map((dir) => path.join(dir, "dsh")), ]; return firstExisting(candidates) || "dsh"; } /** The npm binary: the same discovery strategy. */ function findNpm() { const home = os.homedir(); const appdata = process.env.APPDATA || path.join(home, "AppData", "Roaming"); const candidates = IS_WIN ? [ path.join(appdata, "npm", "npm.cmd"), path.join(home, ".local", "bin", "npm.cmd"), path.join(home, "AppData", "Roaming", "npm", "npm.cmd"), ...pathDirs().map((dir) => path.join(dir, "npm.cmd")), ] : [ path.join(home, ".local/bin/npm"), "/opt/homebrew/bin/npm", "/usr/local/bin/npm", ...pathDirs().map((dir) => path.join(dir, "npm")), ]; return firstExisting(candidates) || "npm"; } const DSH_BIN = findDsh(); const NPM_BIN = findNpm(); /** * Re-resolve the dsh binary right before every spawn. `npm install -g` * replaces the bin symlink/cmd during upgrades; a path captured at shell * startup would then hit ENOENT — exactly the upgrade-race failure in the wild. */ function resolveDsh() { return findDsh(); } /** Environment (PATH augmented) for every child process we spawn. */ function childEnv() { const sep = IS_WIN ? ";" : ":"; const dirs = []; for (const bin of [DSH_BIN, NPM_BIN]) { if (path.isAbsolute(bin)) dirs.push(path.dirname(bin)); try { dirs.push(path.dirname(fs.realpathSync(bin))); } catch { /* not a file */ } } if (IS_WIN) { const appdata = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"); dirs.push(path.join(appdata, "npm")); dirs.push(path.join(os.homedir(), ".local", "bin")); } else { dirs.push( path.join(os.homedir(), ".local/bin"), "/opt/homebrew/bin", "/usr/local/bin", ); } const unique = [...new Set(dirs.filter(Boolean))]; const augmented = unique.join(sep); const existing = process.env.PATH || ""; return { ...process.env, PATH: existing ? `${augmented}${sep}${existing}` : augmented }; } module.exports = { DSH_BIN, NPM_BIN, findDsh, findNpm, resolveDsh, childEnv, IS_WIN };