diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 08322c0f1..441f3f301 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ - Restored bare `prime-agent --resume` opening the agents view and the `/resume [id|path]` slash command; bare commands open the agents view and an argument resumes that session in place. - Fixed URLs not opening on click in fullscreen mode on terminals such as Ghostty; clicking a link in the transcript, dock, or overlays now opens it in the browser. - Fixed ctrl+p ("Toggle agent message expansion") only toggling received agent messages; it now expands and collapses sent agent messages together with received ones. +- Fixed `prime-agent update` failing with `EALLOWREMOTE` on npm 12 when the release is installed from its tarball URL ([#1272](https://github.com/PrimeIntellect-ai/prime-agent/pull/1272) by [@smwbev](https://github.com/smwbev)). ## [0.7.2] - 2026-08-11 diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index b709ab10e..73cd9353d 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -47,6 +47,8 @@ interface SelfUpdateCommandStep { command: string; args: string[]; display: string; + /** Extra environment for this step only; merged over the current environment when it runs. */ + env?: Record; } export interface SelfUpdateCommand extends SelfUpdateCommandStep { @@ -73,11 +75,21 @@ function makeSelfUpdateCommand( }; } -function makeSelfUpdateCommandStep(command: string, args: string[]): SelfUpdateCommandStep { +function makeSelfUpdateCommandStep( + command: string, + args: string[], + env?: Record, +): SelfUpdateCommandStep { + const quote = (value: string) => (/\s/.test(value) ? `"${value}"` : value); + // Render the environment into the display so the update log and the + // copy-paste fallback describe what actually runs, rather than a command + // that fails without the assignment. + const envPrefix = Object.entries(env ?? {}).map(([name, value]) => `${name}=${quote(value)}`); return { command, args, - display: [command, ...args].map((arg) => (/\s/.test(arg) ? `"${arg}"` : arg)).join(" "), + ...(env ? { env } : {}), + display: [...envPrefix, ...[command, ...args].map(quote)].join(" "), }; } @@ -189,7 +201,18 @@ function getSelfUpdateCommandForMethod( const [command = "npm", ...npmArgs] = npmCommand ?? []; const inferred = npmCommand?.length ? undefined : getInferredNpmInstall(); const prefixArgs = [...npmArgs, ...(inferred ? ["--prefix", inferred.prefix] : [])]; - const installStep = makeSelfUpdateCommandStep(command, [...prefixArgs, "install", "-g", updateSpec]); + // npm >= 12 defaults allow-remote to none, so installing a release + // artifact by URL fails with EALLOWREMOTE. Grant it for this install + // only — never through the user's npmrc — and only when the spec is + // such an artifact; registry specs must keep the stricter default. + // "root" is not enough: the released tarball itself depends on + // further tarball URLs, which npm treats as non-root. + const installEnv = isDirectPackageArtifactSpec(updateSpec) ? { npm_config_allow_remote: "all" } : undefined; + const installStep = makeSelfUpdateCommandStep( + command, + [...prefixArgs, "install", "-g", updateSpec], + installEnv, + ); const uninstallStep = updatePackageName === installedPackageName ? undefined diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index a9f4bd6cb..13c5c1fbc 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -466,6 +466,7 @@ async function runSelfUpdate(command: SelfUpdateCommand): Promise { const child = spawn(step.command, step.args, { stdio: "inherit", shell: shouldUseWindowsShell(step.command), + env: step.env ? { ...process.env, ...step.env } : undefined, }); child.on("error", (error) => { reject(error); diff --git a/packages/coding-agent/test/config.test.ts b/packages/coding-agent/test/config.test.ts index b21443c56..438c242fa 100644 --- a/packages/coding-agent/test/config.test.ts +++ b/packages/coding-agent/test/config.test.ts @@ -242,7 +242,8 @@ describe("detectInstallMethod", () => { expect(command).toEqual({ command: "npm", args: ["--prefix", prefix, "install", "-g", tarballUrl], - display: `npm --prefix ${prefix} install -g ${tarballUrl}`, + env: { npm_config_allow_remote: "all" }, + display: `npm_config_allow_remote=all npm --prefix ${prefix} install -g ${tarballUrl}`, }); }); @@ -255,12 +256,14 @@ describe("detectInstallMethod", () => { expect(command).toEqual({ command: "npm", args: ["--prefix", prefix, "install", "-g", tarballUrl], - display: `npm --prefix ${prefix} install -g ${tarballUrl} && npm --prefix ${prefix} uninstall -g @earendil-works/pi-coding-agent`, + env: { npm_config_allow_remote: "all" }, + display: `npm_config_allow_remote=all npm --prefix ${prefix} install -g ${tarballUrl} && npm --prefix ${prefix} uninstall -g @earendil-works/pi-coding-agent`, steps: [ { command: "npm", args: ["--prefix", prefix, "install", "-g", tarballUrl], - display: `npm --prefix ${prefix} install -g ${tarballUrl}`, + env: { npm_config_allow_remote: "all" }, + display: `npm_config_allow_remote=all npm --prefix ${prefix} install -g ${tarballUrl}`, }, { command: "npm", diff --git a/packages/coding-agent/test/suite/regressions/741-npm-self-update-allow-remote.test.ts b/packages/coding-agent/test/suite/regressions/741-npm-self-update-allow-remote.test.ts new file mode 100644 index 000000000..6eff4b505 --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/741-npm-self-update-allow-remote.test.ts @@ -0,0 +1,75 @@ +import { mkdirSync, mkdtempSync, rmSync } from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; +import { afterEach, describe, expect, it } from "vitest"; +import { getSelfUpdateCommand } from "../../../src/config.js"; + +const TARBALL_URL = "https://downloads.example.test/releases/v0.7.2/prime-agent-0.7.2.tgz"; +const PACKAGE_NAME = "@earendil-works/pi-coding-agent"; + +const execPathDescriptor = Object.getOwnPropertyDescriptor(process, "execPath"); +const originalPiPackageDir = process.env.PI_PACKAGE_DIR; +let tempDir: string | undefined; + +// Fake a global npm install under a custom prefix so self-update resolves to npm. +function createNpmPrefixInstall(): string { + const prefix = mkdtempSync(join(tmpdir(), "pi-741-")); + const packageDir = join(prefix, "lib", "node_modules", "@earendil-works", "pi-coding-agent"); + mkdirSync(packageDir, { recursive: true }); + tempDir = prefix; + process.env.PI_PACKAGE_DIR = packageDir; + Object.defineProperty(process, "execPath", { value: join(packageDir, "dist", "cli.js"), configurable: true }); + return prefix; +} + +afterEach(() => { + if (execPathDescriptor) { + Object.defineProperty(process, "execPath", execPathDescriptor); + } + if (originalPiPackageDir === undefined) { + delete process.env.PI_PACKAGE_DIR; + } else { + process.env.PI_PACKAGE_DIR = originalPiPackageDir; + } + if (tempDir) { + rmSync(tempDir, { recursive: true, force: true }); + tempDir = undefined; + } +}); + +describe("issue #741: npm 12 refuses the self-update release tarball", () => { + it("grants allow-remote to the install step when updating from a release artifact", () => { + const prefix = createNpmPrefixInstall(); + + const command = getSelfUpdateCommand(PACKAGE_NAME, undefined, TARBALL_URL); + + expect(command?.env).toEqual({ npm_config_allow_remote: "all" }); + expect(command?.args).toEqual(["--prefix", prefix, "install", "-g", TARBALL_URL]); + expect(command?.display).toBe(`npm_config_allow_remote=all npm --prefix ${prefix} install -g ${TARBALL_URL}`); + }); + + it("keeps registry updates on the npm 12 default", () => { + createNpmPrefixInstall(); + + const command = getSelfUpdateCommand(PACKAGE_NAME); + + expect(command?.env).toBeUndefined(); + expect(command?.display).not.toContain("allow_remote"); + }); + + it("grants allow-remote to the install step only, not the follow-up uninstall", () => { + createNpmPrefixInstall(); + + const command = getSelfUpdateCommand(PACKAGE_NAME, undefined, TARBALL_URL, "prime-agent"); + + expect(command?.steps?.map((step) => step.env)).toEqual([{ npm_config_allow_remote: "all" }, undefined]); + }); + + it("grants allow-remote to local artifacts too, whose transitive dependencies are remote", () => { + createNpmPrefixInstall(); + + const command = getSelfUpdateCommand(PACKAGE_NAME, undefined, "file:/tmp/prime-agent-0.7.2.tgz"); + + expect(command?.env).toEqual({ npm_config_allow_remote: "all" }); + }); +});