fix(coding-agent): add pnpm self-update prune hint (#6279)

Adds a pnpm-specific self-update recovery hint when pi update fails during self-update.
Fixes #6215.
This commit is contained in:
Raj 2026-07-03 16:28:35 +05:30 committed by GitHub
parent c9715af358
commit 4a9c962b59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 61 additions and 2 deletions

View file

@ -372,6 +372,11 @@ function printSelfUpdateFallback(command: SelfUpdateCommand): void {
console.error(chalk.dim(`If this keeps failing, run this command yourself: ${command.display}`));
}
function printPnpmSelfUpdateMetadataHint(): void {
console.error(chalk.yellow("If pnpm reports missing package versions, its cached registry metadata may be stale."));
console.error(chalk.yellow(`Run \`pnpm store prune\` and retry \`${APP_NAME} update --self\`.`));
}
function printSelfUpdateNote(note: string): void {
const trimmedNote = note.trim();
if (!trimmedNote) {
@ -753,6 +758,9 @@ export async function handlePackageCommand(
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown package command error";
console.error(chalk.red(`Error: ${message}`));
if (installMethod === "pnpm") {
printPnpmSelfUpdateMetadataHint();
}
printSelfUpdateFallback(selfUpdateCommand);
process.exitCode = 1;
return true;

View file

@ -1,6 +1,6 @@
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { delimiter, join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ENV_AGENT_DIR, PACKAGE_NAME, VERSION } from "../src/config.ts";
import { ProjectTrustStore } from "../src/core/trust-manager.ts";
@ -15,6 +15,7 @@ describe("package commands", () => {
let originalCwd: string;
let originalAgentDir: string | undefined;
let originalPiPackageDir: string | undefined;
let originalPath: string | undefined;
let originalExitCode: typeof process.exitCode;
let originalExecPath: string;
@ -39,6 +40,7 @@ describe("package commands", () => {
originalCwd = process.cwd();
originalAgentDir = process.env[ENV_AGENT_DIR];
originalPiPackageDir = process.env.PI_PACKAGE_DIR;
originalPath = process.env.PATH;
originalExitCode = process.exitCode;
originalExecPath = process.execPath;
process.exitCode = undefined;
@ -69,6 +71,11 @@ describe("package commands", () => {
} else {
process.env.PI_PACKAGE_DIR = originalPiPackageDir;
}
if (originalPath === undefined) {
delete process.env.PATH;
} else {
process.env.PATH = originalPath;
}
Object.defineProperty(process, "execPath", { value: originalExecPath, configurable: true });
rmSync(tempDir, { recursive: true, force: true });
});
@ -524,6 +531,50 @@ else {
}
});
it("prints a pnpm metadata hint when self-update fails", async () => {
const globalRoot = join(tempDir, "pnpm", "global", "v11");
const selfPackageDir = join(globalRoot, "node_modules", "@earendil-works", "pi-coding-agent");
const fakeBinDir = join(tempDir, "bin");
const fakePnpmPath = join(fakeBinDir, process.platform === "win32" ? "pnpm.cmd" : "pnpm");
mkdirSync(selfPackageDir, { recursive: true });
mkdirSync(fakeBinDir, { recursive: true });
writeFileSync(join(selfPackageDir, "package.json"), JSON.stringify({ name: PACKAGE_NAME, version: VERSION }));
const fakePnpmScript =
process.platform === "win32"
? `@echo off\r\nif "%1"=="root" if "%2"=="-g" (echo ${globalRoot} & exit /b 0)\r\nexit /b 23\r\n`
: `#!/bin/sh\nif [ "$1" = "root" ] && [ "$2" = "-g" ]; then\n\tprintf '%s\\n' '${globalRoot.replaceAll("'", "'\\''")}'\n\texit 0\nfi\nexit 23\n`;
writeFileSync(fakePnpmPath, fakePnpmScript);
chmodSync(fakePnpmPath, 0o755);
process.env.PATH = `${fakeBinDir}${process.env.PATH ? `${delimiter}${process.env.PATH}` : ""}`;
process.env.PI_PACKAGE_DIR = selfPackageDir;
Object.defineProperty(process, "execPath", {
value: join(tempDir, "pnpm", "bin", "node"),
configurable: true,
});
vi.stubGlobal(
"fetch",
vi.fn(async () => Response.json({ version: getNewerPatchVersion() })),
);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
await expect(runPackageCommandDirectly(["update", "--self"])).resolves.toBeUndefined();
expect(process.exitCode).toBe(1);
const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n");
const stderr = errorSpy.mock.calls.map(([message]) => String(message)).join("\n");
expect(stdout).not.toContain("Updated pi");
expect(stderr).toContain("exited with code 23");
expect(stderr).toContain("If pnpm reports missing package versions");
expect(stderr).toContain("Run `pnpm store prune` and retry `pi update --self`.");
} finally {
logSpy.mockRestore();
errorSpy.mockRestore();
}
});
it("fails self-update when renamed npm package installation fails", async () => {
const globalPrefix = join(tempDir, "global-prefix");
const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@mariozechner", "pi-coding-agent");