fix(cli): accept npm 12 array output when resolving published version (#14000)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The CLI `install` and `update` commands resolve the published
version with `npm view paperclipai@<spec> version --json`
> - npm 12 changed the output of this command. It wraps a single value
in an array: `["2026.916.1"]` instead of `"2026.916.1"`
> - `parseResolvedVersion()` accepts only a bare JSON string, so it
throws "npm returned an unexpected version response"
> - Node 26 ships with npm 12, so a new install with the official
`install.sh` fails for these users
> - This pull request makes the parser accept a one-element string array
> - The benefit is that install and update work with npm 12 and with
older npm versions

## Linked Issues or Issue Description

Fixes #12226

## What Changed

- `cli/src/commands/install.ts`: `parseResolvedVersion()` returns the
value when npm returns a one-element array of strings. Other shapes
still throw the same error.
- `cli/src/__tests__/install-command.test.ts`: add a test for npm 11
output, npm 12 output, and plain-text output. The test also checks that
a multi-element array still throws.

## Verification

- Before the fix: on Node 26.8.2 with npm 12.0.2, `bash install.sh`
fails with `npm returned an unexpected version response: [ "2026.916.1"
]`.
- `npm view paperclipai@latest version --json` gives `["2026.916.1"]`
with npm 12.0.2 and `"2026.916.1"` with npm 11.20.0.
- `cd cli && npx vitest run src/__tests__/install-command.test.ts
src/__tests__/update-command.test.ts src/__tests__/channels.test.ts`: 40
tests pass.
- Without the source change, the new test fails with the same error as
the bug report.

## Risks

- Low risk. The change adds one accepted input shape. Existing accepted
shapes do not change.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use
(shell and file editing). It helped to find the cause, write the change
and the test, and run the tests.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Phorel authored and GitHub committed 2026-10-15 16:08:11 -07:00
1 parent fa062b202d
commit e15435b651
2 files changed
+12

No files matched your search

+10
View File
@@ -10,6 +10,7 @@ import {
resolveGitInstallRequest,
resolveGitInstallWorkspacePackages,
resolveNpmInstallRequest,
resolvePublishedVersion,
runCommandWithDiagnostics,
} from "../commands/install.js";
import { uninstallCommand } from "../commands/uninstall.js";
@@ -62,6 +63,15 @@ describe("managed install commands", () => {
expect(() => resolveNpmInstallRequest({ version: "latest" })).toThrow();
});
it("parses npm view version output from npm 11 and npm 12", async () => {
for (const stdout of ['"2026.916.1"\n', '[\n "2026.916.1"\n]\n', "2026.916.1\n"]) {
const runCommand = vi.fn(async () => ({ stdout, stderr: "" }));
await expect(resolvePublishedVersion("latest", runCommand)).resolves.toBe("2026.916.1");
}
const multiple = vi.fn(async () => ({ stdout: '["2026.916.0","2026.916.1"]', stderr: "" }));
await expect(resolvePublishedVersion("latest", multiple)).rejects.toThrow("unexpected version response");
});
it("resolves branch, tag, full SHA, and short SHA refs through GitHub", async () => {
const sha = "a".repeat(40);
const runCommand = vi.fn(async (_file: string, _args: string[]) => ({ stdout: JSON.stringify({ sha }), stderr: "" }));
+2
View File
@@ -111,6 +111,8 @@ function parseResolvedVersion(stdout: string): string {
try {
const parsed = JSON.parse(trimmed) as unknown;
if (typeof parsed === "string") return parsed;
// npm 12 wraps single-value `npm view --json` output in an array.
if (Array.isArray(parsed) && parsed.length === 1 && typeof parsed[0] === "string") return parsed[0];
} catch {
if (EXACT_VERSION_PATTERN.test(trimmed)) return trimmed;
}