/* * WHERE WILL A NEW GLOBAL INSTALL LAND, AND IS IT WHERE THE FLEET LOADS FROM? * * These are two different questions and neither answers the other. The * `server-build-drift` check names which copy is RUNNING; this names where the * NEXT copy will be written. A run of `npm i -g agent-coord-mcp` that prints * "added 1 package" is not evidence it landed anywhere that runs. * * MEASURED ON THIS BOX, 2026-08-28 — the divergence is not hypothetical: * npm prefix -g -> .../node/v22.22.2 * every live fleet server-> .../node/v22.21.1/lib/node_modules/agent-coord-mcp * Three prefixes exist here (two nvm, one /opt/homebrew), `npm prefix -g` is * PATH-dependent, and nvm switches it per shell. So the install target and the * load target had silently diverged, and a successful install would have * updated a copy nothing loads. That gap cost the fleet a day. */ import path from "node:path"; /** * The global root a module path sits under, or null if it is not in one. * `/p/lib/node_modules/agent-coord-mcp/dist` -> `/p` */ export function prefixOf(modulePath: string | undefined): string | null { if (!modulePath) return null; // Split on the LAST occurrence: a global prefix can itself live under a path // containing `node_modules`, and taking the first match would name an // ancestor that installs nothing. const marker = `${path.sep}lib${path.sep}node_modules${path.sep}`; const i = modulePath.lastIndexOf(marker); if (i === -1) return null; return modulePath.slice(0, i); } export type PrefixVerdict = | { level: "ok"; detail: string } | { level: "warn"; detail: string } | { level: "error"; detail: string }; /** * `loadPrefix` is where THIS server was loaded from; `installPrefix` is what * `npm prefix -g` answered, and `npmPath` is which npm answered it — because a * prefix without the binary that reported it cannot be reproduced by anyone. */ export function prefixVerdict(loadPrefix: string | null, installPrefix: string | null, npmPath?: string): PrefixVerdict { const via = npmPath ? ` (asked: ${npmPath})` : ""; // NOT DETERMINED IS NOT MATCHING. Both unknown branches are warnings that say // what could not be established, never an "ok" over an unasked question. // NOT APPLICABLE IS NOT THE SAME AS UNCHECKED, and conflating them is how a // check earns its way into being ignored. A dev checkout has no load prefix // BY CONSTRUCTION: this process is not the copy the fleet loads, so there is // no divergence for it to have. Warning on every dev run would fire on every // test run and every local session — noise, which is what a denylist does. // // The question is still ASKED where it can be answered: a session running // from a global install has a load prefix, and that is where the fleet lives. if (!loadPrefix) return { level: "ok", detail: `not applicable: this server runs from a dev checkout, not a global install${via}, so it is not the copy the fleet loads and has no prefix to diverge from. Run \`doctor\` in an installed session to compare install target against load target.` }; if (!installPrefix) return { level: "warn", detail: `could not determine the global install prefix${via} — \`npm prefix -g\` gave no answer, so where a new copy would land is UNKNOWN. Running from ${loadPrefix}.` }; if (path.resolve(loadPrefix) === path.resolve(installPrefix)) return { level: "ok", detail: `a global install would land where this server loads from (${loadPrefix})${via}` }; return { level: "error", detail: `INSTALL PREFIX AND LOAD PREFIX DIVERGE. A global install from this shell writes to ${installPrefix}${via}, but this server is running from ${loadPrefix}. ` + `A successful "added 1 package" would update a copy nothing loads, and every check that reads a VERSION would keep reporting the old one truthfully. ` + `Install with an explicit prefix (\`npm i -g --prefix ${loadPrefix} \`) or switch node/nvm to the version owning ${loadPrefix} before installing.`, }; }