/** * @file Merge the user's login-shell PATH into `process.env.PATH`. A systemd * unit or LaunchAgent snapshots PATH when it is installed, so a CLI installed * later into a new directory (nvm, ~/.local/bin, a vendor installer) is * invisible to AI Tools detection and to every lane pane. `$SHELL -ilc` runs * the rc files where those installers add to PATH; delimiters discard * whatever the rc files print, stdin is closed so a prompt gets EOF, and a * timeout bounds a hang. Off with `AGENTHUB_SHELL_PATH=0`. The shell's own * PATH is also kept in ITS order (`shellPath()`): the merge appends, so an * install job that must use the shell's npm puts this first. Adapted from Orca's * `hydrate-shell-path.ts` (MIT). * @author Nguyễn Ngọc Trí Vĩ */ const { execFile } = require("node:child_process"); const path = require("node:path"); const MARK = "__AGENTHUB_PATH__"; let lastShellPath = null; /** The PATH the login shell last printed, in its order; null before/without one. */ const shellPath = () => lastShellPath; /** * @returns {Promise} whether a shell PATH was read and merged. */ function hydrateShellPath({ spawn = execFile, shell = process.env.SHELL || "/bin/bash", timeoutMs = 5000, } = {}) { if (/^(0|false|off)$/i.test(process.env.AGENTHUB_SHELL_PATH || "")) return Promise.resolve(false); return new Promise((resolve) => { let child; try { child = spawn( shell, ["-ilc", `printf '${MARK}%s${MARK}' "$PATH"`], { timeout: timeoutMs, encoding: "utf8" }, (err, stdout) => { const m = String(stdout || "").match(new RegExp(`${MARK}(.*?)${MARK}`, "s")); if (!m) return resolve(false); // timeout, spawn error, no output lastShellPath = m[1]; const have = (process.env.PATH || "").split(path.delimiter).filter(Boolean); const add = m[1].split(path.delimiter).filter((d) => d && !have.includes(d)); process.env.PATH = [...have, ...new Set(add)].join(path.delimiter); resolve(true); } ); } catch { return resolve(false); } // An rc file that reads stdin gets EOF instead of hanging until the timeout. child?.stdin?.end(); }); } module.exports = { hydrateShellPath, shellPath };