diff --git a/docs/NPM_DISTRIBUTION.md b/docs/NPM_DISTRIBUTION.md index 0cdc162..53c54a8 100644 --- a/docs/NPM_DISTRIBUTION.md +++ b/docs/NPM_DISTRIBUTION.md @@ -54,8 +54,9 @@ its own npm 12, which is how 0.3.1 broke there (issue #41). The binary goes in the package's `bin/`, or, when that is not writable (a root-owned `sudo npm install -g` whose postinstall npm 12 skipped), in a -per-user cache: `$XDG_CACHE_HOME`, else `~/.cache` (`~/Library/Caches` on -macOS), under `//`. The launcher looks in both. A +per-user cache: `$XDG_CACHE_HOME` (if absolute), else `~/.cache` +(`~/Library/Caches` on macOS), under `//-/`. +The launcher looks in both. A termination signal during that first-run download stops the installer too, and it removes its partial file. diff --git a/scripts/idx.js b/scripts/idx.js index ea9dde0..b743544 100755 --- a/scripts/idx.js +++ b/scripts/idx.js @@ -20,6 +20,7 @@ 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; +let installing = false; // Pass termination signals from a supervisor (kill, docker stop) to the child. for (const signal of ["SIGTERM", "SIGHUP"]) { @@ -32,8 +33,15 @@ for (const signal of ["SIGTERM", "SIGHUP"]) { } }); } -// Ctrl+C already reaches the child through the terminal's process group. -process.on("SIGINT", () => {}); +// Ctrl+C already reaches the binary through the terminal's process group, so +// the launcher ignores SIGINT then. During the first-run install, forward it +// too: a supervisor may signal only the launcher's PID. +process.on("SIGINT", () => { + if (installing) { + stopping = "SIGINT"; + child.kill("SIGINT"); + } +}); function exitLike(code, signal) { if (signal) { @@ -61,6 +69,7 @@ if (installed) { // 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`); + installing = true; child = spawn(process.execPath, [installer], { stdio: ["ignore", 2, 2] }); child.once("error", (error) => { console.error(`${packageName}: failed to run the installer: ${error.message}`); @@ -68,6 +77,7 @@ if (installed) { }); child.once("exit", (code, signal) => { child = null; + installing = false; if (signal || stopping) { exitLike(code, signal || stopping); return; diff --git a/scripts/install.js b/scripts/install.js index b19c604..126e985 100755 --- a/scripts/install.js +++ b/scripts/install.js @@ -23,13 +23,19 @@ const binaryName = process.platform === "win32" ? `${nativeBinary}.exe` : native // (a root-owned global install whose postinstall npm 12 skipped), in a // per-user cache. scripts/idx.js looks in both, in this order. const packageBinaryPath = path.join(packageRoot, "bin", binaryName); +// A relative XDG_CACHE_HOME is invalid per the XDG spec, and would make the +// launcher run a binary from the current directory: ignore it. +const xdgCacheHome = process.env.XDG_CACHE_HOME; const cacheBinaryPath = path.join( - process.env.XDG_CACHE_HOME || - (process.platform === "darwin" + xdgCacheHome && path.isAbsolute(xdgCacheHome) + ? xdgCacheHome + : process.platform === "darwin" ? path.join(os.homedir(), "Library", "Caches") - : path.join(os.homedir(), ".cache")), + : path.join(os.homedir(), ".cache"), packageName, packageManifest.version, + // A home directory shared across machines must not mix architectures. + `${process.platform}-${process.arch}`, binaryName, ); const binaryLocations = [packageBinaryPath, cacheBinaryPath]; diff --git a/scripts/npm-smoke.sh b/scripts/npm-smoke.sh index 81c82db..69c5161 100755 --- a/scripts/npm-smoke.sh +++ b/scripts/npm-smoke.sh @@ -88,9 +88,12 @@ echo "npm smoke: installing idx-mcp with --ignore-scripts" no_scripts_dir="${smoke_dir}/idx-mcp-no-scripts" npm_config_cache="$npm_cache" npm install --prefix "$no_scripts_dir" "${smoke_dir}"/idx-mcp/idx-mcp-*.tgz \ --no-save --no-package-lock --no-audit --no-fund --ignore-scripts -IDX_BINARY_URL="${repo_root}/target/release/idx-mcp" \ +# An empty user cache, so a binary cached by an earlier run cannot stand in +# for the first-run install. +XDG_CACHE_HOME="${smoke_dir}/no-scripts-cache" IDX_BINARY_URL="${repo_root}/target/release/idx-mcp" \ "${no_scripts_dir}/node_modules/.bin/idx-mcp" --help > "${smoke_dir}/no-scripts-help.txt" -if ! head -n 1 "${smoke_dir}/no-scripts-help.txt" | grep -q "MCP server"; then +if ! head -n 1 "${smoke_dir}/no-scripts-help.txt" | grep -q "MCP server" \ + || [[ ! -x "${no_scripts_dir}/node_modules/idx-mcp/bin/idx-mcp" ]]; then echo "npm smoke: idx-mcp without postinstall did not start cleanly:" >&2 cat "${smoke_dir}/no-scripts-help.txt" >&2 exit 1 @@ -104,7 +107,7 @@ chmod -R a-w "${read_only_dir}/node_modules/idx-mcp" XDG_CACHE_HOME="${smoke_dir}/user-cache" IDX_BINARY_URL="${repo_root}/target/release/idx-mcp" \ "${read_only_dir}/node_modules/.bin/idx-mcp" --help > "${smoke_dir}/read-only-help.txt" chmod -R u+w "${read_only_dir}/node_modules/idx-mcp" -cached=("${smoke_dir}"/user-cache/idx-mcp/*/idx-mcp) +cached=("${smoke_dir}"/user-cache/idx-mcp/*/*/idx-mcp) if ! head -n 1 "${smoke_dir}/read-only-help.txt" | grep -q "MCP server" || [[ ! -x "${cached[0]}" ]]; then echo "npm smoke: idx-mcp with a read-only package did not install to the user cache:" >&2 cat "${smoke_dir}/read-only-help.txt" >&2