From 809ff494dca8c7fb14a1ceb8a119903acc5241ce Mon Sep 17 00:00:00 2001 From: Ayush Nangia Date: Thu, 6 Aug 2026 23:32:59 +0530 Subject: [PATCH 1/2] fix: npm 10+ compatibility across the update and install surfaces fixes #738, fixes #749 Defect 1 (#738): the standalone update command hard-rejects package targets ('Package updates moved to the package command'), but the TUI kept spawning 'update --extensions' for the advertised /update --extensions flow, so it always exited 1. Non-self updates now spawn 'package update' with legacy flags translated (--extensions -> bare, --extension -> positional, daemon-socket plumbing dropped). Defect 2 (#738): npm >= 11 emits a JSON array from 'npm view version --json'; comparing ['2.87.2'] !== '2.87.2' made the startup update notice a permanent false positive that funneled users into defect 1. normalizeNpmViewVersion handles both forms and fails loudly on malformed output. #749: install.sh's PATH-recovery guidance printed 'npm bin -g', removed in npm 10; it now prints the npm-prefix form. --- install.sh | 3 +- .../coding-agent/src/core/package-manager.ts | 18 +++++++ .../src/modes/interactive/interactive-mode.ts | 38 ++++++++++++- .../regressions/738-npm-update-compat.test.ts | 54 +++++++++++++++++++ 4 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 packages/coding-agent/test/suite/regressions/738-npm-update-compat.test.ts diff --git a/install.sh b/install.sh index 0e76ea5437..7c2bcda81c 100755 --- a/install.sh +++ b/install.sh @@ -135,9 +135,10 @@ main() { The $prime_agent_cmd command was installed, but it is not on your PATH yet. Check npm's global bin directory with: - npm bin -g + echo "\$(npm prefix -g)/bin" Then add that directory to your shell PATH. +(npm 10 removed the old "npm bin -g" command.) EOF fi } diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index fd818595f3..e5cab76faa 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -36,6 +36,24 @@ import { isStdoutTakenOver } from "./output-guard.js"; import type { PackageSource, SettingsManager } from "./settings-manager.js"; const NETWORK_TIMEOUT_MS = 10000; + +/** + * Parse `npm view version --json` output. npm 10 emits a bare JSON + * string; npm >= 11 emits a JSON array (e.g. `["2.87.2"]`). Comparing the + * un-normalized array against the installed version string made every + * startup report "Package updates available" as a false positive (#738). + * Exported for testing. + */ +export function normalizeNpmViewVersion(stdout: string): string { + const raw = stdout.trim(); + if (!raw) throw new Error("Empty response from npm view"); + const parsed: unknown = JSON.parse(raw); + const version = Array.isArray(parsed) ? parsed[parsed.length - 1] : parsed; + if (typeof version !== "string" || version.length === 0) { + throw new Error(`Unexpected npm view version output: ${raw}`); + } + return version; +} const UPDATE_CHECK_CONCURRENCY = 4; const GIT_UPDATE_CONCURRENCY = 4; diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index ccfef6a4d5..03192a6b08 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -671,6 +671,37 @@ function getPayloadWorkingIndicatorOptions( }; } +/** + * Translate legacy `/update` package arguments into the `package update` + * CLI surface. The standalone `update` command hard-rejects package targets + * ("Package updates moved to the package command"), but the TUI kept + * spawning `update --extensions`, so the advertised flow always exited 1 + * (#738). `--extensions` means "all packages" (the default), `--extension + * ` and positional sources name one package, and daemon-socket + * plumbing belongs only to self-updates. + */ +export function translatePackageUpdateArgs(args: readonly string[]): string[] { + const translated: string[] = []; + for (let index = 0; index < args.length; index++) { + const arg = args[index]; + if (arg === "--extensions" || arg === "--self") { + continue; + } + if (arg === "--extension" || arg === "--daemon-socket") { + const value = args[index + 1]; + index++; + if (arg === "--extension" && value !== undefined) { + translated.push(value); + } + continue; + } + if (arg !== undefined) { + translated.push(arg); + } + } + return translated; +} + export function updateArgsIncludeSelf(args: readonly string[]): boolean { let selfFlag = false; let extensionsOnlyFlag = false; @@ -8388,7 +8419,10 @@ export class InteractiveMode { updateArgs, resolveDaemonUpdateRestartSocketPath(this.options.daemonSocketPath), ); - const updateChildArgs = includesSelf ? buildUpdateChildArgs(updateArgs, daemonSocketPath) : updateArgs; + const updateChildArgs = includesSelf + ? buildUpdateChildArgs(updateArgs, daemonSocketPath) + : translatePackageUpdateArgs(updateArgs); + const updateChildCommand = includesSelf ? ["update"] : ["package", "update"]; this.stopWorkingLoader(); await this.ui.terminal.drainInput(1000).catch(() => undefined); this.ui.stop(); @@ -8396,7 +8430,7 @@ export class InteractiveMode { const updateEnv = includesSelf ? { ...process.env, [SELF_UPDATE_INTERACTIVE_CHILD_ENV]: "1" } : process.env; const updateResult = spawnSync( process.execPath, - [...process.execArgv, entrypoint, "update", ...updateChildArgs], + [...process.execArgv, entrypoint, ...updateChildCommand, ...updateChildArgs], { stdio: "inherit", cwd: updateCwd, diff --git a/packages/coding-agent/test/suite/regressions/738-npm-update-compat.test.ts b/packages/coding-agent/test/suite/regressions/738-npm-update-compat.test.ts new file mode 100644 index 0000000000..6ae56235a6 --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/738-npm-update-compat.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import { normalizeNpmViewVersion } from "../../../src/core/package-manager.js"; +import { translatePackageUpdateArgs, updateArgsIncludeSelf } from "../../../src/modes/interactive/interactive-mode.js"; + +describe("issue #738 defect 1: /update --extensions must spawn the package command", () => { + it("translates --extensions to the bare package-update form", () => { + expect(translatePackageUpdateArgs(["--extensions"])).toEqual([]); + }); + + it("translates --extension to a positional source", () => { + expect(translatePackageUpdateArgs(["--extension", "npm:bigpowers"])).toEqual(["npm:bigpowers"]); + }); + + it("keeps positional sources and drops daemon-socket plumbing", () => { + expect(translatePackageUpdateArgs(["npm:bigpowers", "--daemon-socket", "/tmp/x.sock"])).toEqual([ + "npm:bigpowers", + ]); + }); + + it("agrees with updateArgsIncludeSelf about what is a package update", () => { + // Every arg shape the TUI routes to the package path must translate + // into something `package update` accepts (no legacy flags). + for (const args of [["--extensions"], ["--extension", "npm:x"], ["npm:x"]]) { + expect(updateArgsIncludeSelf(args)).toBe(false); + const translated = translatePackageUpdateArgs(args); + expect(translated.some((a) => a === "--extensions" || a === "--extension" || a === "--self")).toBe(false); + } + }); +}); + +describe("issue #738 defect 2: npm >= 11 emits a JSON array from npm view", () => { + it("normalizes the npm 11+ array form", () => { + expect(normalizeNpmViewVersion('["2.87.2"]\n')).toBe("2.87.2"); + }); + + it("keeps the npm 10 bare-string form", () => { + expect(normalizeNpmViewVersion('"2.87.2"\n')).toBe("2.87.2"); + }); + + it("takes the newest entry when several versions are listed", () => { + expect(normalizeNpmViewVersion('["2.87.1","2.87.2"]')).toBe("2.87.2"); + }); + + it("rejects empty and malformed output loudly", () => { + expect(() => normalizeNpmViewVersion("")).toThrow("Empty response"); + expect(() => normalizeNpmViewVersion("[]")).toThrow("Unexpected npm view version output"); + expect(() => normalizeNpmViewVersion("42")).toThrow("Unexpected npm view version output"); + }); + + it("equality against the installed version works after normalization", () => { + // The false-positive mechanism: ["2.87.2"] !== "2.87.2" was always true. + expect(normalizeNpmViewVersion('["2.87.2"]')).toBe("2.87.2"); + }); +}); From 00d8d19e187143493bb73300146aa5ea7672238a Mon Sep 17 00:00:00 2001 From: Ayush Nangia Date: Fri, 7 Aug 2026 12:33:58 +0530 Subject: [PATCH 2/2] fix: pass allow-remote=all to npm >= 12 during install fixes #741 npm 12 defaults allow-remote=none and the published tarball has transitive URL dependencies (R2-hosted runtime packages), so the installer's 'npm install -g' exits with EALLOWREMOTE. 'root' is insufficient because the blocked tarballs are transitive. Scope allow-remote=all to the install invocation via env (no ~/.npmrc changes), gated to npm >= 12 so older versions see no unknown-config warnings. Mechanism and override verified on npm 12.0.2 by @d4not. --- install.sh | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 7c2bcda81c..fe3eb5b558 100755 --- a/install.sh +++ b/install.sh @@ -143,6 +143,24 @@ EOF fi } +# npm 12 defaults allow-remote=none, and the published prime-agent tarball +# carries transitive URL dependencies (R2-hosted @earendil-works tarballs), +# so a plain "npm install -g" exits with EALLOWREMOTE. "root" is not enough — +# the blocked tarballs are transitive — so the install invocation is scoped +# to allow-remote=all via env, without touching the user's ~/.npmrc. (#741, +# verified by @d4not on npm 12.0.2.) Gated to npm >= 12 so older npm versions +# see no unknown-config warnings. +npm_allow_remote_env() { + case "$(npm --version 2>/dev/null)" in + 1[2-9].* | [2-9][0-9].*) + printf 'npm_config_allow_remote=all' + ;; + *) + printf '' + ;; + esac +} + create_temp_dir() { if command -v mktemp >/dev/null 2>&1; then if tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/prime-agent-install.XXXXXX" 2>/dev/null); then @@ -1603,7 +1621,7 @@ Finalizing npm install." "Installing Prime Agent" \ "Installing Prime Agent" \ "$npm_install_details" \ - env PRIME_AGENT_BOOTSTRAP_TOOLS_ON_INSTALL=1 PRIME_AGENT_BOOTSTRAP_KERNEL_ON_INSTALL=1 PRIME_AGENT_INSTALL_UV=1 npm install -g --no-fund --no-audit --loglevel=error --progress=false "$tarball_path" + env $(npm_allow_remote_env) PRIME_AGENT_BOOTSTRAP_TOOLS_ON_INSTALL=1 PRIME_AGENT_BOOTSTRAP_KERNEL_ON_INSTALL=1 PRIME_AGENT_INSTALL_UV=1 npm install -g --no-fund --no-audit --loglevel=error --progress=false "$tarball_path" else npm_install_details="Preparing global install. Linking command binaries. @@ -1614,7 +1632,7 @@ Finalizing npm install." "Installing Prime Agent" \ "Installing Prime Agent" \ "$npm_install_details" \ - env PRIME_AGENT_BOOTSTRAP_TOOLS_ON_INSTALL=1 npm install -g --no-fund --no-audit --loglevel=error --progress=false "$tarball_path" + env $(npm_allow_remote_env) PRIME_AGENT_BOOTSTRAP_TOOLS_ON_INSTALL=1 npm install -g --no-fund --no-audit --loglevel=error --progress=false "$tarball_path" fi }