fix(macos): scrub parent-session env, survive node upgrades and reboots, store lanes by real path (#22)
What broke (code audit, untested on a Mac): - Without systemd the server is always spawned from inside a Claude Code session, so it inherited CLAUDECODE, CLAUDE_CODE_SESSION_ID, CLAUDE_PLUGIN_ROOT, ... and tmux handed them to every lane pane (a codex pane even resolved as `claude` in `agenthub`'s harness check). - The ~/.local/bin/agenthub launcher pinned a versioned node path that `brew upgrade`/nvm deletes, leaving the CLI dead until the next bootstrap. - A discovery entry left over from before a reboot could name a recycled pid, so the bootstrap's fast path believed a dashboard was live and stopDashboard could signal an unrelated process. - Lanes were stored under the typed cwd; macOS reports /private/tmp and the on-disk case, so sessions never bound to a /tmp or differently-cased lane. What changed: - server/index.js deletes a DENYLIST of parent-session variables at startup; user config (CLAUDE_CONFIG_DIR, CLAUDE_CODE_USE_BEDROCK, ANTHROPIC_*) stays. - linkCli's launcher falls back to `node` on PATH when the pinned one is gone. - liveServers ignores entries whose startedAt predates this boot (60 s margin). - createLane and POST /api/lanes/ensure store/look up fs.realpathSync.native of an existing cwd; the liveness reaper also tries the realpath. Existing rows are untouched. - session-liveness gains an exec seam so the darwin ps+lsof branch is tested. - plugin-doctor names ~/.zshrc on darwin; INSTALL.md gains a macOS section with the Mac verification checklist, mirrored in both READMEs and PLUGINS.md. No launchd, no macOptionIsMeta, engines stays >=20.
This commit is contained in:
@@ -359,13 +359,15 @@ function cliDir() {
|
||||
*
|
||||
* @returns {"written"|"exists"|"foreign"|"failed"}
|
||||
*/
|
||||
function linkCli(dir = cliDir(), root = PLUGIN_ROOT) {
|
||||
function linkCli(dir = cliDir(), root = PLUGIN_ROOT, nodePath = process.execPath) {
|
||||
const marker = "# agenthub-plugin-launcher";
|
||||
const target = path.join(dir, process.platform === "win32" ? "agenthub.cmd" : "agenthub");
|
||||
const body =
|
||||
process.platform === "win32"
|
||||
? `@rem agenthub-plugin-launcher\r\n@node "${path.join(root, "bin", "agenthub.js")}" %*\r\n`
|
||||
: `#!/bin/sh\n${marker}\nexec "${process.execPath}" "${path.join(root, "bin", "agenthub.js")}" "$@"\n`;
|
||||
: // The pinned node is a versioned path (Homebrew Cellar, nvm) that an
|
||||
// upgrade deletes; fall back to whatever `node` PATH has then.
|
||||
`#!/bin/sh\n${marker}\nN="${nodePath}"; [ -x "$N" ] || N=node\nexec "$N" "${path.join(root, "bin", "agenthub.js")}" "$@"\n`;
|
||||
try {
|
||||
if (fs.existsSync(target)) {
|
||||
const current = fs.readFileSync(target, "utf8");
|
||||
@@ -419,7 +421,11 @@ function liveServers() {
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(getServerInfoPath(), "utf8"));
|
||||
const servers = Array.isArray(parsed.servers) ? parsed.servers : [parsed];
|
||||
return servers.filter((s) => s && isPidAlive(s.pid));
|
||||
// An entry older than this boot is a leftover whose pid may now belong to
|
||||
// an unrelated process (macOS has no systemd to clean up after a reboot).
|
||||
// 60 s margin for clock skew; an unparseable startedAt is kept.
|
||||
const bootedAt = Date.now() - os.uptime() * 1000 - 60_000;
|
||||
return servers.filter((s) => s && isPidAlive(s.pid) && !(Date.parse(s.startedAt) < bootedAt));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -316,7 +316,9 @@ function cliStatus() {
|
||||
: [
|
||||
"warn",
|
||||
"agenthub CLI",
|
||||
`${file} exists but ${dir} is not on PATH — add: export PATH="${dir}:$PATH"`,
|
||||
`${file} exists but ${dir} is not on PATH — add${
|
||||
process.platform === "darwin" ? " to ~/.zshrc" : ""
|
||||
}: export PATH="${dir}:$PATH"`,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user