From 4a9c962b594053361d937d0f9a2b029f99a85278 Mon Sep 17 00:00:00 2001 From: Raj <42045620+rajp152k@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:28:35 +0530 Subject: [PATCH] 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. --- .../coding-agent/src/package-manager-cli.ts | 8 +++ .../test/package-command-paths.test.ts | 55 ++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index 32980851..09349a3b 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -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; diff --git a/packages/coding-agent/test/package-command-paths.test.ts b/packages/coding-agent/test/package-command-paths.test.ts index 6df65ded..d209ce50 100644 --- a/packages/coding-agent/test/package-command-paths.test.ts +++ b/packages/coding-agent/test/package-command-paths.test.ts @@ -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");