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
25 changes: 22 additions & 3 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,32 @@ 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
}

# 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
Expand Down Expand Up @@ -1602,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.
Expand All @@ -1613,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
}

Expand Down
18 changes: 18 additions & 0 deletions packages/coding-agent/src/core/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <pkg> 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;

Expand Down
38 changes: 36 additions & 2 deletions packages/coding-agent/src/modes/interactive/interactive-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <source>` 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;
Expand Down Expand Up @@ -8388,15 +8419,18 @@ 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();

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