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:
@@ -372,6 +372,11 @@ function printSelfUpdateFallback(command: SelfUpdateCommand): void {
|
|||||||
console.error(chalk.dim(`If this keeps failing, run this command yourself: ${command.display}`));
|
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 {
|
function printSelfUpdateNote(note: string): void {
|
||||||
const trimmedNote = note.trim();
|
const trimmedNote = note.trim();
|
||||||
if (!trimmedNote) {
|
if (!trimmedNote) {
|
||||||
@@ -753,6 +758,9 @@ export async function handlePackageCommand(
|
|||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : "Unknown package command error";
|
const message = error instanceof Error ? error.message : "Unknown package command error";
|
||||||
console.error(chalk.red(`Error: ${message}`));
|
console.error(chalk.red(`Error: ${message}`));
|
||||||
|
if (installMethod === "pnpm") {
|
||||||
|
printPnpmSelfUpdateMetadataHint();
|
||||||
|
}
|
||||||
printSelfUpdateFallback(selfUpdateCommand);
|
printSelfUpdateFallback(selfUpdateCommand);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -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 { 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 { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { ENV_AGENT_DIR, PACKAGE_NAME, VERSION } from "../src/config.ts";
|
import { ENV_AGENT_DIR, PACKAGE_NAME, VERSION } from "../src/config.ts";
|
||||||
import { ProjectTrustStore } from "../src/core/trust-manager.ts";
|
import { ProjectTrustStore } from "../src/core/trust-manager.ts";
|
||||||
@@ -15,6 +15,7 @@ describe("package commands", () => {
|
|||||||
let originalCwd: string;
|
let originalCwd: string;
|
||||||
let originalAgentDir: string | undefined;
|
let originalAgentDir: string | undefined;
|
||||||
let originalPiPackageDir: string | undefined;
|
let originalPiPackageDir: string | undefined;
|
||||||
|
let originalPath: string | undefined;
|
||||||
let originalExitCode: typeof process.exitCode;
|
let originalExitCode: typeof process.exitCode;
|
||||||
let originalExecPath: string;
|
let originalExecPath: string;
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ describe("package commands", () => {
|
|||||||
originalCwd = process.cwd();
|
originalCwd = process.cwd();
|
||||||
originalAgentDir = process.env[ENV_AGENT_DIR];
|
originalAgentDir = process.env[ENV_AGENT_DIR];
|
||||||
originalPiPackageDir = process.env.PI_PACKAGE_DIR;
|
originalPiPackageDir = process.env.PI_PACKAGE_DIR;
|
||||||
|
originalPath = process.env.PATH;
|
||||||
originalExitCode = process.exitCode;
|
originalExitCode = process.exitCode;
|
||||||
originalExecPath = process.execPath;
|
originalExecPath = process.execPath;
|
||||||
process.exitCode = undefined;
|
process.exitCode = undefined;
|
||||||
@@ -69,6 +71,11 @@ describe("package commands", () => {
|
|||||||
} else {
|
} else {
|
||||||
process.env.PI_PACKAGE_DIR = originalPiPackageDir;
|
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 });
|
Object.defineProperty(process, "execPath", { value: originalExecPath, configurable: true });
|
||||||
rmSync(tempDir, { recursive: true, force: 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 () => {
|
it("fails self-update when renamed npm package installation fails", async () => {
|
||||||
const globalPrefix = join(tempDir, "global-prefix");
|
const globalPrefix = join(tempDir, "global-prefix");
|
||||||
const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@mariozechner", "pi-coding-agent");
|
const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@mariozechner", "pi-coding-agent");
|
||||||
|
|||||||
Reference in New Issue
Block a user