fix(npm): install to a user cache when the package is read-only; signal-safe first run

- When the package's bin/ is not writable (a root-owned `sudo npm i -g`
  whose postinstall npm 12 skipped), install the binary to a per-user
  cache ($XDG_CACHE_HOME, ~/.cache, ~/Library/Caches on macOS, under
  <package>/<version>/); the launcher looks in both. Previously the first
  run failed with EACCES and advice to rerun npm install.
- Run the first-run installer asynchronously with SIGTERM/SIGHUP
  forwarded, so a supervisor stopping the launcher stops the download;
  the installer removes its partial file and the launcher exits by the
  same signal.
- Verify an IDX_BINARY_URL local file's checksum on the copy, not the
  source, so the installed bytes are the checked ones.
- npm smoke covers the read-only fallback.

The last three come from a Codex review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh
This commit is contained in:
hermes 2026-10-16 08:07:41 +00:00
commit 907223a838
4 changed files with 156 additions and 51 deletions

View file

@ -3,51 +3,83 @@
const fs = require("node:fs");
const path = require("node:path");
const { spawn, spawnSync } = require("node:child_process");
const { spawn } = require("node:child_process");
const packageManifest = JSON.parse(
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"),
);
const nativeBinary = packageManifest.nativeBinary || "idx";
const packageName = packageManifest.name;
const binaryName = process.platform === "win32" ? `${nativeBinary}.exe` : nativeBinary;
const binaryPath = path.join(__dirname, "..", "bin", binaryName);
const installer = path.join(__dirname, "install.js");
// Requiring the installer only reads its paths; it installs when run directly.
const { binaryLocations } = require(installer);
// postinstall never ran (ignore-scripts) or its download failed: install now.
// Its log goes to stderr, since stdout is the MCP channel for idx-mcp.
if (!fs.existsSync(binaryPath)) {
console.error(`${packageName}: binary not installed; running the installer`);
spawnSync(process.execPath, [path.join(__dirname, "install.js")], {
stdio: ["ignore", 2, 2],
const findBinary = () => binaryLocations.find((candidate) => fs.existsSync(candidate));
// The running child: the first-run installer, then the binary.
let child = null;
// A termination signal received while installing; the installer cleans up
// and exits, and the launcher then stops the same way.
let stopping = null;
// Pass termination signals from a supervisor (kill, docker stop) to the child.
for (const signal of ["SIGTERM", "SIGHUP"]) {
process.on(signal, () => {
stopping = signal;
if (child) {
child.kill(signal);
} else {
process.exit(1);
}
});
}
if (!fs.existsSync(binaryPath)) {
console.error(`${packageName}: installed binary not found at ${binaryPath}`);
console.error(`${packageName}: rerun npm install or set IDX_BINARY_URL for a local binary`);
process.exit(1);
}
const child = spawn(binaryPath, process.argv.slice(2), { stdio: "inherit" });
// Pass termination signals from a supervisor (kill, docker stop) to the binary.
for (const signal of ["SIGTERM", "SIGHUP"]) {
process.on(signal, () => child.kill(signal));
}
// Ctrl+C already reaches the binary through the terminal's process group.
// Ctrl+C already reaches the child through the terminal's process group.
process.on("SIGINT", () => {});
child.once("error", (error) => {
console.error(`${packageName}: failed to start ${binaryPath}: ${error.message}`);
process.exit(1);
});
child.once("exit", (code, signal) => {
function exitLike(code, signal) {
if (signal) {
process.removeAllListeners(signal);
process.kill(process.pid, signal);
return;
}
process.exit(code === null ? 1 : code);
});
}
function run(binaryPath) {
child = spawn(binaryPath, process.argv.slice(2), { stdio: "inherit" });
child.once("error", (error) => {
console.error(`${packageName}: failed to start ${binaryPath}: ${error.message}`);
process.exit(1);
});
child.once("exit", exitLike);
}
const installed = findBinary();
if (installed) {
run(installed);
} else {
// postinstall never ran (npm 12 `allow-scripts`, pnpm, bun, --ignore-scripts)
// or its download failed: install now. Its log goes to stderr, since stdout
// is the MCP channel for idx-mcp.
console.error(`${packageName}: binary not installed; running the installer`);
child = spawn(process.execPath, [installer], { stdio: ["ignore", 2, 2] });
child.once("error", (error) => {
console.error(`${packageName}: failed to run the installer: ${error.message}`);
process.exit(1);
});
child.once("exit", (code, signal) => {
child = null;
if (signal || stopping) {
exitLike(code, signal || stopping);
return;
}
const binaryPath = findBinary();
if (!binaryPath) {
console.error(`${packageName}: no installed binary at ${binaryLocations.join(" or ")}`);
console.error(
`${packageName}: check access to github.com and rerun, or set IDX_BINARY_URL to a local binary`,
);
process.exit(1);
}
run(binaryPath);
});
}