Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
29 changes: 26 additions & 3 deletions packages/coding-agent/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;
}

export interface SelfUpdateCommand extends SelfUpdateCommandStep {
Expand All @@ -73,11 +75,21 @@ function makeSelfUpdateCommand(
};
}

function makeSelfUpdateCommandStep(command: string, args: string[]): SelfUpdateCommandStep {
function makeSelfUpdateCommandStep(
command: string,
args: string[],
env?: Record<string, string>,
): 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(" "),
};
}

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/src/package-manager-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,7 @@ async function runSelfUpdate(command: SelfUpdateCommand): Promise<void> {
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);
Expand Down
9 changes: 6 additions & 3 deletions packages/coding-agent/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
});
});

Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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" });
});
});