fix(coding-agent): install checked pi update version
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `pi update` to install the exact version returned by the Pi update check, make `--force` reinstall that checked version, fail instead of falling back to an unversioned reinstall when no version is available, and report both the old and updated versions.
|
||||
|
||||
## [0.79.9] - 2026-06-20
|
||||
|
||||
### New Features
|
||||
|
||||
@@ -38,6 +38,18 @@ export interface SelfUpdateCommand extends SelfUpdateCommandStep {
|
||||
steps?: SelfUpdateCommandStep[];
|
||||
}
|
||||
|
||||
export type SelfUpdatePackageTarget = string | { packageName: string; installSpec?: string };
|
||||
|
||||
function normalizeSelfUpdatePackageTarget(target: SelfUpdatePackageTarget): {
|
||||
packageName: string;
|
||||
installSpec: string;
|
||||
} {
|
||||
if (typeof target === "string") {
|
||||
return { packageName: target, installSpec: target };
|
||||
}
|
||||
return { packageName: target.packageName, installSpec: target.installSpec ?? target.packageName };
|
||||
}
|
||||
|
||||
function makeSelfUpdateCommand(
|
||||
installStep: SelfUpdateCommandStep,
|
||||
uninstallStep?: SelfUpdateCommandStep,
|
||||
@@ -103,9 +115,10 @@ function getInferredNpmInstall(): { root: string; prefix: string } | undefined {
|
||||
function getSelfUpdateCommandForMethod(
|
||||
method: InstallMethod,
|
||||
installedPackageName: string,
|
||||
updatePackageName = installedPackageName,
|
||||
updatePackageTarget: SelfUpdatePackageTarget = installedPackageName,
|
||||
npmCommand?: string[],
|
||||
): SelfUpdateCommand | undefined {
|
||||
const target = normalizeSelfUpdatePackageTarget(updatePackageTarget);
|
||||
switch (method) {
|
||||
case "bun-binary":
|
||||
return undefined;
|
||||
@@ -123,17 +136,17 @@ function getSelfUpdateCommandForMethod(
|
||||
"--ignore-scripts",
|
||||
"--config.minimumReleaseAge=0",
|
||||
...binDirArgs,
|
||||
updatePackageName,
|
||||
target.installSpec,
|
||||
]),
|
||||
updatePackageName === installedPackageName
|
||||
target.packageName === installedPackageName
|
||||
? undefined
|
||||
: makeSelfUpdateCommandStep("pnpm", ["remove", "-g", ...binDirArgs, installedPackageName]),
|
||||
);
|
||||
}
|
||||
case "yarn":
|
||||
return makeSelfUpdateCommand(
|
||||
makeSelfUpdateCommandStep("yarn", ["global", "add", "--ignore-scripts", updatePackageName]),
|
||||
updatePackageName === installedPackageName
|
||||
makeSelfUpdateCommandStep("yarn", ["global", "add", "--ignore-scripts", target.installSpec]),
|
||||
target.packageName === installedPackageName
|
||||
? undefined
|
||||
: makeSelfUpdateCommandStep("yarn", ["global", "remove", installedPackageName]),
|
||||
);
|
||||
@@ -144,9 +157,9 @@ function getSelfUpdateCommandForMethod(
|
||||
"-g",
|
||||
"--ignore-scripts",
|
||||
"--minimum-release-age=0",
|
||||
updatePackageName,
|
||||
target.installSpec,
|
||||
]),
|
||||
updatePackageName === installedPackageName
|
||||
target.packageName === installedPackageName
|
||||
? undefined
|
||||
: makeSelfUpdateCommandStep("bun", ["uninstall", "-g", installedPackageName]),
|
||||
);
|
||||
@@ -160,10 +173,10 @@ function getSelfUpdateCommandForMethod(
|
||||
"-g",
|
||||
"--ignore-scripts",
|
||||
"--min-release-age=0",
|
||||
updatePackageName,
|
||||
target.installSpec,
|
||||
]);
|
||||
const uninstallStep =
|
||||
updatePackageName === installedPackageName
|
||||
target.packageName === installedPackageName
|
||||
? undefined
|
||||
: makeSelfUpdateCommandStep(command, [...prefixArgs, "uninstall", "-g", installedPackageName]);
|
||||
return makeSelfUpdateCommand(installStep, uninstallStep);
|
||||
@@ -302,10 +315,10 @@ function isManagedByGlobalPackageManager(method: InstallMethod, packageName: str
|
||||
export function getSelfUpdateCommand(
|
||||
packageName: string,
|
||||
npmCommand?: string[],
|
||||
updatePackageName = packageName,
|
||||
updatePackageTarget: SelfUpdatePackageTarget = packageName,
|
||||
): SelfUpdateCommand | undefined {
|
||||
const method = detectInstallMethod();
|
||||
const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageName, npmCommand);
|
||||
const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageTarget, npmCommand);
|
||||
if (!command || !isManagedByGlobalPackageManager(method, packageName, npmCommand) || !isSelfUpdatePathWritable()) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -315,20 +328,21 @@ export function getSelfUpdateCommand(
|
||||
export function getSelfUpdateUnavailableInstruction(
|
||||
packageName: string,
|
||||
npmCommand?: string[],
|
||||
updatePackageName = packageName,
|
||||
updatePackageTarget: SelfUpdatePackageTarget = packageName,
|
||||
): string {
|
||||
const method = detectInstallMethod();
|
||||
const target = normalizeSelfUpdatePackageTarget(updatePackageTarget);
|
||||
if (method === "bun-binary") {
|
||||
return `Download from: https://github.com/earendil-works/pi-mono/releases/latest`;
|
||||
}
|
||||
const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageName, npmCommand);
|
||||
const command = getSelfUpdateCommandForMethod(method, packageName, target, npmCommand);
|
||||
if (command) {
|
||||
if (isManagedByGlobalPackageManager(method, packageName, npmCommand) && !isSelfUpdatePathWritable()) {
|
||||
return `This installation is managed by a global ${method} install, but the install path is not writable. Update it yourself with: ${command.display}`;
|
||||
}
|
||||
return `This installation is not managed by a global ${method} install. Update it with the package manager, wrapper, or source checkout that provides it.`;
|
||||
}
|
||||
return `Update ${updatePackageName} using the package manager, wrapper, or source checkout that provides this installation.`;
|
||||
return `Update ${target.installSpec} using the package manager, wrapper, or source checkout that provides this installation.`;
|
||||
}
|
||||
|
||||
export function getUpdateInstruction(packageName: string): string {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getSelfUpdateUnavailableInstruction,
|
||||
PACKAGE_NAME,
|
||||
type SelfUpdateCommand,
|
||||
type SelfUpdatePackageTarget,
|
||||
VERSION,
|
||||
} from "./config.ts";
|
||||
import type { ExtensionFactory } from "./core/extensions/types.ts";
|
||||
@@ -353,9 +354,12 @@ function updateTargetIncludesExtensions(target: UpdateTarget): boolean {
|
||||
return target.type === "all" || target.type === "extensions";
|
||||
}
|
||||
|
||||
function printSelfUpdateUnavailable(npmCommand?: string[], updatePackageName = PACKAGE_NAME): void {
|
||||
function printSelfUpdateUnavailable(
|
||||
npmCommand?: string[],
|
||||
updatePackageTarget: SelfUpdatePackageTarget = PACKAGE_NAME,
|
||||
): void {
|
||||
console.error(`error: ${APP_NAME} cannot self-update this installation.`);
|
||||
console.error(getSelfUpdateUnavailableInstruction(PACKAGE_NAME, npmCommand, updatePackageName));
|
||||
console.error(getSelfUpdateUnavailableInstruction(PACKAGE_NAME, npmCommand, updatePackageTarget));
|
||||
|
||||
const entrypoint = process.argv[1];
|
||||
if (entrypoint) {
|
||||
@@ -390,27 +394,38 @@ function printSelfUpdateNote(note: string): void {
|
||||
|
||||
interface SelfUpdatePlan {
|
||||
packageName: string;
|
||||
installSpec: string;
|
||||
version: string;
|
||||
shouldRun: boolean;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
async function getSelfUpdatePlan(force: boolean): Promise<SelfUpdatePlan> {
|
||||
if (force) {
|
||||
return { packageName: PACKAGE_NAME, shouldRun: true };
|
||||
let latestRelease: Awaited<ReturnType<typeof getLatestPiRelease>>;
|
||||
try {
|
||||
latestRelease = await getLatestPiRelease(VERSION);
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Could not determine latest ${APP_NAME} version: ${message}`);
|
||||
}
|
||||
if (!latestRelease) {
|
||||
throw new Error(`Could not determine latest ${APP_NAME} version.`);
|
||||
}
|
||||
|
||||
try {
|
||||
const latestRelease = await getLatestPiRelease(VERSION);
|
||||
const packageName = latestRelease?.packageName ?? PACKAGE_NAME;
|
||||
if (!latestRelease || packageName !== PACKAGE_NAME || isNewerPackageVersion(latestRelease.version, VERSION)) {
|
||||
return { packageName, shouldRun: true, ...(latestRelease?.note ? { note: latestRelease.note } : {}) };
|
||||
}
|
||||
} catch {
|
||||
return { packageName: PACKAGE_NAME, shouldRun: true };
|
||||
const packageName = latestRelease.packageName ?? PACKAGE_NAME;
|
||||
const installSpec = `${packageName}@${latestRelease.version}`;
|
||||
if (force || packageName !== PACKAGE_NAME || isNewerPackageVersion(latestRelease.version, VERSION)) {
|
||||
return {
|
||||
packageName,
|
||||
installSpec,
|
||||
version: latestRelease.version,
|
||||
...(latestRelease.note ? { note: latestRelease.note } : {}),
|
||||
shouldRun: true,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(chalk.green(`${APP_NAME} is already up to date (v${VERSION})`));
|
||||
return { packageName: PACKAGE_NAME, shouldRun: false };
|
||||
return { packageName, installSpec, version: latestRelease.version, shouldRun: false };
|
||||
}
|
||||
|
||||
async function runSelfUpdate(command: SelfUpdateCommand): Promise<void> {
|
||||
@@ -717,13 +732,13 @@ export async function handlePackageCommand(
|
||||
process.exitCode = 1;
|
||||
return true;
|
||||
}
|
||||
const selfUpdateCommand = getSelfUpdateCommand(
|
||||
PACKAGE_NAME,
|
||||
selfUpdateNpmCommand,
|
||||
selfUpdatePlan.packageName,
|
||||
);
|
||||
const selfUpdateTarget = {
|
||||
packageName: selfUpdatePlan.packageName,
|
||||
installSpec: selfUpdatePlan.installSpec,
|
||||
};
|
||||
const selfUpdateCommand = getSelfUpdateCommand(PACKAGE_NAME, selfUpdateNpmCommand, selfUpdateTarget);
|
||||
if (!selfUpdateCommand) {
|
||||
printSelfUpdateUnavailable(selfUpdateNpmCommand, selfUpdatePlan.packageName);
|
||||
printSelfUpdateUnavailable(selfUpdateNpmCommand, selfUpdateTarget);
|
||||
process.exitCode = 1;
|
||||
return true;
|
||||
}
|
||||
@@ -742,7 +757,7 @@ export async function handlePackageCommand(
|
||||
process.exitCode = 1;
|
||||
return true;
|
||||
}
|
||||
console.log(chalk.green(`Updated ${APP_NAME}`));
|
||||
console.log(chalk.green(`Updated ${APP_NAME} from ${VERSION} to ${selfUpdatePlan.version}`));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -188,6 +188,29 @@ describe("detectInstallMethod", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("self-updates exact npm versions without uninstalling the current package", () => {
|
||||
const { prefix } = createNpmPrefixInstall();
|
||||
|
||||
const command = getSelfUpdateCommand("@earendil-works/pi-coding-agent", undefined, {
|
||||
packageName: "@earendil-works/pi-coding-agent",
|
||||
installSpec: "@earendil-works/pi-coding-agent@1.2.3",
|
||||
});
|
||||
|
||||
expect(command).toEqual({
|
||||
command: "npm",
|
||||
args: [
|
||||
"--prefix",
|
||||
prefix,
|
||||
"install",
|
||||
"-g",
|
||||
"--ignore-scripts",
|
||||
"--min-release-age=0",
|
||||
"@earendil-works/pi-coding-agent@1.2.3",
|
||||
],
|
||||
display: `npm --prefix ${prefix} install -g --ignore-scripts --min-release-age=0 @earendil-works/pi-coding-agent@1.2.3`,
|
||||
});
|
||||
});
|
||||
|
||||
test("self-updates renamed packages from the current install prefix", () => {
|
||||
const { prefix } = createNpmPrefixInstall();
|
||||
|
||||
|
||||
@@ -374,7 +374,7 @@ describe("package commands", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("uses global npmCommand and current package name for forced self updates without checking the api", async () => {
|
||||
it("uses the update check version for forced self updates even when current", async () => {
|
||||
const globalPrefix = join(tempDir, "global-prefix");
|
||||
const projectPrefix = join(tempDir, "project-prefix");
|
||||
const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@earendil-works", "pi-coding-agent");
|
||||
@@ -402,7 +402,7 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args));
|
||||
value: join(selfPackageDir, "dist", "cli.js"),
|
||||
configurable: true,
|
||||
});
|
||||
const fetchMock = vi.fn();
|
||||
const fetchMock = vi.fn(async () => Response.json({ version: VERSION }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
@@ -413,11 +413,14 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args));
|
||||
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
expect(errorSpy).not.toHaveBeenCalled();
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n");
|
||||
const recordedArgs = JSON.parse(readFileSync(recordPath, "utf-8")) as string[];
|
||||
expect(recordedArgs).toContain(globalPrefix);
|
||||
expect(recordedArgs).toContain(PACKAGE_NAME);
|
||||
expect(recordedArgs).toContain(`${PACKAGE_NAME}@${VERSION}`);
|
||||
expect(recordedArgs).not.toContain(PACKAGE_NAME);
|
||||
expect(recordedArgs).not.toContain(projectPrefix);
|
||||
expect(stdout).toContain(`Updated pi from ${VERSION} to ${VERSION}`);
|
||||
} finally {
|
||||
logSpy.mockRestore();
|
||||
errorSpy.mockRestore();
|
||||
@@ -446,7 +449,8 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args));
|
||||
value: join(selfPackageDir, "dist", "cli.js"),
|
||||
configurable: true,
|
||||
});
|
||||
const fetchMock = vi.fn(async () => Response.json({ version: getNewerPatchVersion() }));
|
||||
const targetVersion = getNewerPatchVersion();
|
||||
const fetchMock = vi.fn(async () => Response.json({ version: targetVersion }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
@@ -458,8 +462,11 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args));
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
expect(errorSpy).not.toHaveBeenCalled();
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n");
|
||||
const recordedArgs = JSON.parse(readFileSync(recordPath, "utf-8")) as string[];
|
||||
expect(recordedArgs).toContain(PACKAGE_NAME);
|
||||
expect(recordedArgs).toContain(`${PACKAGE_NAME}@${targetVersion}`);
|
||||
expect(recordedArgs).not.toContain(PACKAGE_NAME);
|
||||
expect(stdout).toContain(`Updated pi from ${VERSION} to ${targetVersion}`);
|
||||
} finally {
|
||||
logSpy.mockRestore();
|
||||
errorSpy.mockRestore();
|
||||
@@ -509,7 +516,7 @@ else {
|
||||
const recordedCalls = JSON.parse(readFileSync(recordPath, "utf-8")) as string[][];
|
||||
expect(recordedCalls).toEqual([
|
||||
expect.arrayContaining(["uninstall", "-g", PACKAGE_NAME]),
|
||||
expect.arrayContaining(["install", "-g", activePackageName]),
|
||||
expect.arrayContaining(["install", "-g", `${activePackageName}@0.73.0`]),
|
||||
]);
|
||||
} finally {
|
||||
logSpy.mockRestore();
|
||||
@@ -565,7 +572,7 @@ if(args.includes("install")) process.exit(23);
|
||||
const recordedCalls = JSON.parse(readFileSync(recordPath, "utf-8")) as string[][];
|
||||
expect(recordedCalls).toEqual([
|
||||
expect.arrayContaining(["uninstall", "-g", PACKAGE_NAME]),
|
||||
expect.arrayContaining(["install", "-g", activePackageName]),
|
||||
expect.arrayContaining(["install", "-g", `${activePackageName}@0.73.0`]),
|
||||
]);
|
||||
} finally {
|
||||
logSpy.mockRestore();
|
||||
|
||||
Reference in New Issue
Block a user