feat(coding-agent): update managed installations in place

This commit is contained in:
Armin Ronacher 2026-08-24 09:34:34 +02:00
parent a470b121bf
commit 4af9d21d3b
5 changed files with 449 additions and 61 deletions

View file

@ -38,7 +38,7 @@ pi update npm:@foo/bar # update one package
pi update --extension npm:@foo/bar
```
These commands manage pi packages and `pi update` can update the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall).
These commands manage pi packages and `pi update` can update the pi CLI installation. For experimental installer-managed installations, `pi update` installs the exact checked version into a staged, lockfile-backed release and activates it only after verification, leaving the current release intact if the update fails. Managed installations do not support `--force`; rerun the installer to repair one. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall).
By default, `install` and `remove` write to user settings (`~/.pi/agent/settings.json`). Use `-l` to write to project settings (`.pi/settings.json`) instead. Project settings can be shared with your team, and pi installs any missing packages automatically on startup after the project is trusted.

View file

@ -64,7 +64,7 @@ import { builtInExtensions } from "./extensions/index.ts";
import { runMigrations, showDeprecationWarnings } from "./migrations.ts";
import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.ts";
import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.ts";
import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.ts";
import { cleanupManagedInstall, handleConfigCommand, handlePackageCommand } from "./package-manager-cli.ts";
import { isLocalPath, normalizePath, resolvePath } from "./utils/paths.ts";
import { cleanupWindowsSelfUpdateQuarantine } from "./utils/windows-self-update.ts";
@ -573,6 +573,7 @@ export async function main(args: string[], options?: MainOptions) {
if (process.platform === "win32") {
cleanupWindowsSelfUpdateQuarantine(getPackageDir());
}
cleanupManagedInstall();
const cwd = process.cwd();
const agentDir = getAgentDir();

View file

@ -1,6 +1,17 @@
import { join } from "node:path";
import {
existsSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
renameSync,
rmSync,
writeFileSync,
} from "node:fs";
import { join, resolve } from "node:path";
import { Markdown, type MarkdownTheme } from "@earendil-works/pi-tui";
import chalk from "chalk";
import lockfile from "proper-lockfile";
import { selectConfig } from "./cli/config-selector.ts";
import { createProjectTrustContext } from "./cli/project-trust.ts";
import {
@ -23,7 +34,9 @@ import { type AppMode, resolveProjectTrusted } from "./core/project-trust.ts";
import { DefaultResourceLoader } from "./core/resource-loader.ts";
import { SettingsManager } from "./core/settings-manager.ts";
import { hasTrustRequiringProjectResources, ProjectTrustStore } from "./core/trust-manager.ts";
import { spawnProcess } from "./utils/child-process.ts";
import { spawnProcess, spawnProcessSync, waitForChildProcess } from "./utils/child-process.ts";
import { canonicalizePath, getCwdRelativePath } from "./utils/paths.ts";
import { getPiUserAgent } from "./utils/pi-user-agent.ts";
import { formatVersionCheckError, getLatestPiRelease, isNewerPackageVersion } from "./utils/version-check.ts";
import {
cleanupWindowsSelfUpdateQuarantine,
@ -34,6 +47,179 @@ export type PackageCommand = "install" | "remove" | "update" | "list";
type UpdateTarget = { type: "all" } | { type: "self" } | { type: "extensions"; source?: string } | { type: "models" };
const DEFAULT_INSTALLER_API_BASE = "https://pi.dev/api/installer/releases";
const MANAGED_INSTALL_MARKER = "managed-install.json";
const MANAGED_RELEASE_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
function getActiveManagedInstallRoot(): string | undefined {
const configuredRoot = process.env.PI_MANAGED_INSTALL_ROOT?.trim();
if (!configuredRoot) return undefined;
const managedRoot = resolve(configuredRoot);
const releasesDir = canonicalizePath(join(managedRoot, "releases"));
// The launcher environment is inherited by child processes. Do not classify a
// source checkout or another Pi installation launched from managed Pi as managed.
if (getCwdRelativePath(canonicalizePath(getPackageDir()), releasesDir) === undefined) return undefined;
const markerPath = join(managedRoot, MANAGED_INSTALL_MARKER);
try {
const marker = JSON.parse(readFileSync(markerPath, "utf8")) as {
kind?: unknown;
layout?: unknown;
schemaVersion?: unknown;
};
if (marker.kind !== "pi-managed-install" || marker.schemaVersion !== 1 || marker.layout !== "releases-v1") {
throw new Error();
}
} catch {
throw new Error(`Managed install marker is missing or invalid: ${markerPath}`);
}
return managedRoot;
}
async function fetchInstallerArtifact(url: string, label: string): Promise<string> {
const response = await fetch(url, { headers: { "User-Agent": getPiUserAgent(VERSION) } });
if (!response.ok) {
throw new Error(`Could not download managed installer ${label} from ${url}: HTTP ${response.status}`);
}
return await response.text();
}
async function runManagedNpmCi(stageDir: string): Promise<void> {
const args = [
"ci",
"--ignore-scripts",
"--min-release-age=0",
"--omit=dev",
"--include=optional",
"--no-fund",
"--no-audit",
"--loglevel=error",
"--progress=false",
];
const code = await waitForChildProcess(spawnProcess("npm", args, { cwd: stageDir, stdio: "inherit" }));
if (code !== 0) throw new Error(`npm ${args.join(" ")} exited with code ${code ?? "unknown"}`);
}
function verifyManagedRelease(releaseDir: string, expectedVersion: string): void {
const binPath = join(
releaseDir,
"node_modules",
".bin",
process.platform === "win32" ? `${APP_NAME}.cmd` : APP_NAME,
);
const result = spawnProcessSync(binPath, ["--version"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
if (result.error || result.status !== 0) {
const reason = result.error?.message || result.stderr.trim() || `exit code ${result.status ?? "unknown"}`;
throw new Error(`Could not verify managed Pi ${expectedVersion}: ${reason}`);
}
const installedVersion = result.stdout.trim();
if (installedVersion !== expectedVersion) {
throw new Error(`Managed Pi smoke test returned version ${installedVersion}; expected ${expectedVersion}.`);
}
}
function activateManagedRelease(managedRoot: string, version: string): void {
const currentPath = join(managedRoot, "current-version");
const temporaryPath = join(managedRoot, `current-version.tmp.${process.pid}-${Date.now()}`);
try {
writeFileSync(temporaryPath, `${version}\n`);
renameSync(temporaryPath, currentPath);
} finally {
rmSync(temporaryPath, { force: true });
}
}
function cleanupManagedStaging(managedRoot: string): void {
const stagingRoot = join(managedRoot, "staging");
try {
for (const entry of readdirSync(stagingRoot)) {
if (entry.startsWith("update-")) {
rmSync(join(stagingRoot, entry), { force: true, recursive: true });
}
}
} catch {
// The staging directory does not exist yet or is not writable.
}
}
export function cleanupManagedInstall(): void {
let managedRoot: string | undefined;
try {
managedRoot = getActiveManagedInstallRoot();
} catch {
return;
}
if (!managedRoot) return;
try {
const releaseLock = lockfile.lockSync(join(managedRoot, "update"), { realpath: false });
try {
cleanupManagedStaging(managedRoot);
} finally {
releaseLock();
}
} catch {
// A live update owns the staging directory, or cleanup is unavailable.
}
}
async function runManagedSelfUpdate(managedRoot: string, version: string): Promise<void> {
if (!MANAGED_RELEASE_VERSION_RE.test(version)) {
throw new Error(`Invalid managed release version: ${version}`);
}
let releaseLock: () => Promise<void>;
try {
releaseLock = await lockfile.lock(join(managedRoot, "update"), { realpath: false });
} catch (error: unknown) {
if (error instanceof Error && "code" in error && error.code === "ELOCKED") {
throw new Error("Another managed Pi update is already running.");
}
throw error;
}
let stageDir: string | undefined;
try {
cleanupManagedStaging(managedRoot);
const installerApiBase = (process.env.PI_INSTALLER_API_BASE?.trim() || DEFAULT_INSTALLER_API_BASE).replace(
/\/+$/,
"",
);
const releaseUrl = `${installerApiBase}/${encodeURIComponent(version)}`;
const stagingRoot = join(managedRoot, "staging");
const releasesRoot = join(managedRoot, "releases");
mkdirSync(releasesRoot, { recursive: true });
const releaseDir = join(releasesRoot, version);
if (existsSync(releaseDir)) {
verifyManagedRelease(releaseDir, version);
activateManagedRelease(managedRoot, version);
return;
}
mkdirSync(stagingRoot, { recursive: true });
stageDir = mkdtempSync(join(stagingRoot, "update-"));
const [packageJsonContent, packageLockContent] = await Promise.all([
fetchInstallerArtifact(`${releaseUrl}/package.json`, "package.json"),
fetchInstallerArtifact(`${releaseUrl}/package-lock.json`, "package-lock.json"),
]);
writeFileSync(join(stageDir, "package.json"), packageJsonContent);
writeFileSync(join(stageDir, "package-lock.json"), packageLockContent);
await runManagedNpmCi(stageDir);
verifyManagedRelease(stageDir, version);
renameSync(stageDir, releaseDir);
activateManagedRelease(managedRoot, version);
} finally {
if (stageDir) rmSync(stageDir, { force: true, recursive: true });
await releaseLock();
}
}
const SELF_UPDATE_NOTE_MARKDOWN_THEME: MarkdownTheme = {
heading: (text) => chalk.bold(chalk.yellow(text)),
link: (text) => chalk.cyan(text),
@ -834,10 +1020,37 @@ export async function handlePackageCommand(
}
}
if (updateTargetIncludesSelf(target)) {
const managedInstallRoot = getActiveManagedInstallRoot();
if (managedInstallRoot && options.force) {
console.error(
chalk.red(
`Managed ${APP_NAME} installations do not support --force; rerun the installer to repair this installation.`,
),
);
process.exitCode = 1;
return true;
}
const selfUpdatePlan = await getSelfUpdatePlan(options.force);
if (!selfUpdatePlan.shouldRun) {
return true;
}
if (managedInstallRoot) {
if (selfUpdatePlan.note) {
printSelfUpdateNote(selfUpdatePlan.note);
}
try {
console.log(chalk.dim(`Updating managed ${APP_NAME} installation...`));
await runManagedSelfUpdate(managedInstallRoot, selfUpdatePlan.version);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown managed update error";
console.error(chalk.red(`Error: ${message}`));
process.exitCode = 1;
return true;
}
console.log(chalk.green(`Updated ${APP_NAME} from ${VERSION} to ${selfUpdatePlan.version}`));
return true;
}
const installMethod = detectInstallMethod();
if (process.platform === "win32" && installMethod !== "npm" && installMethod !== "pnpm") {
console.error(

View file

@ -1,6 +1,17 @@
import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import {
chmodSync,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
realpathSync,
rmSync,
utimesSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { delimiter, join } from "node:path";
import lockfile from "proper-lockfile";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ENV_AGENT_DIR, PACKAGE_NAME, VERSION } from "../src/config.ts";
import { ModelRuntime } from "../src/core/model-runtime.ts";
@ -29,6 +40,77 @@ describe("package commands", () => {
return `${major}.${minor}.${Number.parseInt(patch, 10) + 1}`;
}
function prepareManagedInstall(
targetVersion: string,
npmExitCode = 0,
): { managedRoot: string; npmRecordPath: string } {
const managedRoot = join(agentDir, "install");
const activeRelease = join(managedRoot, "releases", VERSION);
const selfPackageDir = join(activeRelease, "node_modules", ...PACKAGE_NAME.split("/"));
mkdirSync(selfPackageDir, { recursive: true });
writeFileSync(join(activeRelease, "active.txt"), "active");
writeFileSync(join(managedRoot, "current-version"), `${VERSION}\n`);
writeFileSync(
join(managedRoot, "managed-install.json"),
`${JSON.stringify({ kind: "pi-managed-install", schemaVersion: 1, layout: "releases-v1" })}\n`,
);
const binDir = join(tempDir, "managed-bin");
const fakeNpmPath = join(tempDir, "managed-npm.cjs");
const npmRecordPath = join(tempDir, "managed-npm-record.json");
mkdirSync(binDir, { recursive: true });
writeFileSync(
fakeNpmPath,
`const fs = require("node:fs");
const path = require("node:path");
const args = process.argv.slice(2);
fs.writeFileSync(${JSON.stringify(npmRecordPath)}, JSON.stringify(args));
if (${npmExitCode} !== 0) process.exit(${npmExitCode});
const binDir = path.join(process.cwd(), "node_modules", ".bin");
fs.mkdirSync(binDir, { recursive: true });
const piPath = path.join(binDir, process.platform === "win32" ? "pi.cmd" : "pi");
fs.writeFileSync(
piPath,
process.platform === "win32"
? "@echo off\\r\\necho ${targetVersion}\\r\\n"
: "#!/bin/sh\\nprintf '%s\\n' ${targetVersion}\\n",
);
if (process.platform !== "win32") fs.chmodSync(piPath, 0o755);
`,
);
const npmPath = join(binDir, process.platform === "win32" ? "npm.cmd" : "npm");
writeFileSync(
npmPath,
process.platform === "win32"
? `@echo off\r\n"${originalExecPath}" "${fakeNpmPath}" %*\r\n`
: `#!/bin/sh\nexec "${originalExecPath}" "${fakeNpmPath}" "$@"\n`,
);
chmodSync(npmPath, 0o755);
vi.stubEnv("PI_INSTALLER_API_BASE", "https://example.test/api/installer/releases");
vi.stubEnv("PI_MANAGED_INSTALL_ROOT", managedRoot);
process.env.PI_PACKAGE_DIR = selfPackageDir;
process.env.PATH = `${binDir}${delimiter}${originalPath ?? ""}`;
return { managedRoot, npmRecordPath };
}
function mockManagedUpdate(targetVersion: string): void {
vi.stubGlobal(
"fetch",
vi.fn(async (input: string | URL | Request) => {
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
if (url === "https://pi.dev/api/latest-version") {
return Response.json({ packageName: PACKAGE_NAME, version: targetVersion });
}
const releaseUrl = `https://example.test/api/installer/releases/${targetVersion}`;
if (url === `${releaseUrl}/package.json` || url === `${releaseUrl}/package-lock.json`) {
return Response.json({});
}
throw new Error(`Unexpected fetch: ${url}`);
}),
);
}
async function runPackageCommandDirectly(args: string[]): Promise<void> {
expect(await handlePackageCommand(args)).toBe(true);
}
@ -82,6 +164,7 @@ describe("package commands", () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.unstubAllEnvs();
vi.restoreAllMocks();
process.chdir(originalCwd);
process.exitCode = originalExitCode;
@ -523,10 +606,103 @@ describe("package commands", () => {
}
});
it("uses the update check version for forced self updates even when current", async () => {
it("updates installer-managed Pi through a staged immutable release", async () => {
const targetVersion = getNewerPatchVersion();
const { managedRoot, npmRecordPath } = prepareManagedInstall(targetVersion);
const abandonedStage = join(managedRoot, "staging", "update-abandoned");
mkdirSync(abandonedStage, { recursive: true });
writeFileSync(join(abandonedStage, "partial"), "partial");
const abandonedLock = join(managedRoot, "update.lock");
mkdirSync(abandonedLock);
utimesSync(abandonedLock, new Date(0), new Date(0));
mockManagedUpdate(targetVersion);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
await expect(runPackageCommandDirectly(["update", "--self"])).resolves.toBeUndefined();
expect(readFileSync(join(managedRoot, "current-version"), "utf8")).toBe(`${targetVersion}\n`);
expect(existsSync(join(managedRoot, "releases", targetVersion))).toBe(true);
expect(existsSync(join(managedRoot, "releases", VERSION, "active.txt"))).toBe(true);
expect(readdirSync(join(managedRoot, "staging"))).toEqual([]);
expect(JSON.parse(readFileSync(npmRecordPath, "utf8")) as string[]).toEqual(
expect.arrayContaining(["ci", "--ignore-scripts"]),
);
expect(logSpy.mock.calls.map(([message]) => String(message)).join("\n")).toContain(
`Updated pi from ${VERSION} to ${targetVersion}`,
);
expect(errorSpy).not.toHaveBeenCalled();
expect(process.exitCode).toBeUndefined();
});
it("rejects a concurrent managed update", async () => {
const targetVersion = getNewerPatchVersion();
const { managedRoot, npmRecordPath } = prepareManagedInstall(targetVersion);
const releaseLock = await lockfile.lock(join(managedRoot, "update"), { realpath: false });
mockManagedUpdate(targetVersion);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
await expect(runPackageCommandDirectly(["update", "--self"])).resolves.toBeUndefined();
} finally {
await releaseLock();
}
expect(readFileSync(join(managedRoot, "current-version"), "utf8")).toBe(`${VERSION}\n`);
expect(existsSync(npmRecordPath)).toBe(false);
expect(logSpy.mock.calls.map(([message]) => String(message)).join("\n")).not.toContain("Updated pi from");
expect(errorSpy.mock.calls.map(([message]) => String(message)).join("\n")).toContain(
"Another managed Pi update is already running.",
);
expect(process.exitCode).toBe(1);
});
it("rejects forced managed reinstalls", async () => {
const targetVersion = getNewerPatchVersion();
const { npmRecordPath } = prepareManagedInstall(targetVersion);
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
await expect(runPackageCommandDirectly(["update", "--self", "--force"])).resolves.toBeUndefined();
expect(fetchMock).not.toHaveBeenCalled();
expect(existsSync(npmRecordPath)).toBe(false);
expect(errorSpy.mock.calls.map(([message]) => String(message)).join("\n")).toContain(
"Managed pi installations do not support --force",
);
expect(process.exitCode).toBe(1);
});
it("keeps the managed release active when its update fails", async () => {
const targetVersion = getNewerPatchVersion();
const { managedRoot } = prepareManagedInstall(targetVersion, 23);
mockManagedUpdate(targetVersion);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
await expect(runPackageCommandDirectly(["update", "--self"])).resolves.toBeUndefined();
expect(readFileSync(join(managedRoot, "current-version"), "utf8")).toBe(`${VERSION}\n`);
expect(existsSync(join(managedRoot, "releases", targetVersion))).toBe(false);
expect(readdirSync(join(managedRoot, "staging"))).toEqual([]);
expect(logSpy.mock.calls.map(([message]) => String(message)).join("\n")).not.toContain("Updated pi from");
expect(errorSpy.mock.calls.map(([message]) => String(message)).join("\n")).toContain("exited with code 23");
expect(process.exitCode).toBe(1);
});
it("keeps npm self-updates non-managed when the managed environment is inherited", 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");
const inheritedManagedRoot = join(tempDir, "inherited-managed-install");
mkdirSync(join(inheritedManagedRoot, "releases"), { recursive: true });
writeFileSync(
join(inheritedManagedRoot, "managed-install.json"),
JSON.stringify({ kind: "pi-managed-install", schemaVersion: 1, layout: "releases-v1" }),
);
vi.stubEnv("PI_MANAGED_INSTALL_ROOT", inheritedManagedRoot);
const fakeNpmPath = join(tempDir, "fake-npm.cjs");
const recordPath = join(tempDir, "self-update.json");
mkdirSync(selfPackageDir, { recursive: true });

View file

@ -57,8 +57,8 @@ function parseArgs(args) {
if (!options.version || !STABLE_SEMVER_RE.test(options.version)) {
throw new Error("--version must be a stable semver version");
}
if (Boolean(options.installerPackageJson) !== Boolean(options.installerPackageLock)) {
throw new Error("--installer-package-json and --installer-package-lock must be provided together");
if (!options.installerPackageJson || !options.installerPackageLock) {
throw new Error("--installer-package-json and --installer-package-lock are required");
}
return options;
}
@ -302,6 +302,57 @@ async function main() {
}
writeFileSync(latestPath, `${JSON.stringify(release, null, "\t")}\n`);
validateInstallerArtifacts(options.installerPackageJson, options.installerPackageLock, options.version);
const installerReleasePrefix = `${INSTALLER_PREFIX}/releases/${options.version}`;
putObject(
options.bucket,
options.endpoint,
options.installerPackageJson,
`${installerReleasePrefix}/package.json`,
"public, max-age=31536000, immutable",
{ missing: true },
);
putObject(
options.bucket,
options.endpoint,
options.installerPackageLock,
`${installerReleasePrefix}/package-lock.json`,
"public, max-age=31536000, immutable",
{ missing: true },
);
putJson(
options.bucket,
options.endpoint,
releasePath,
`${installerReleasePrefix}/metadata.json`,
"public, max-age=31536000, immutable",
{ missing: true },
);
const installerLatest = await advanceLatestRelease(
options.version,
() =>
readLatestRelease(
options.bucket,
options.endpoint,
`${INSTALLER_PREFIX}/latest.json`,
join(temporaryDirectory, "installer-latest-current.json"),
),
(condition) =>
putJson(
options.bucket,
options.endpoint,
latestPath,
`${INSTALLER_PREFIX}/latest.json`,
"no-store",
condition,
),
);
console.log(
installerLatest.advanced
? `Published installer artifacts for Pi ${options.version} through s3://${options.bucket}/${INSTALLER_PREFIX}/latest.json`
: `Pi ${installerLatest.version} is already the latest installer release.`,
);
const result = await advanceLatestRelease(
options.version,
() =>
@ -326,59 +377,6 @@ async function main() {
? `Announced Pi ${options.version} through s3://${options.bucket}/${RELEASES_PREFIX}/latest.json`
: `Pi ${result.version} is already the latest announced release.`,
);
if (options.installerPackageJson) {
validateInstallerArtifacts(options.installerPackageJson, options.installerPackageLock, options.version);
const installerReleasePrefix = `${INSTALLER_PREFIX}/releases/${options.version}`;
putObject(
options.bucket,
options.endpoint,
options.installerPackageJson,
`${installerReleasePrefix}/package.json`,
"public, max-age=31536000, immutable",
{ missing: true },
);
putObject(
options.bucket,
options.endpoint,
options.installerPackageLock,
`${installerReleasePrefix}/package-lock.json`,
"public, max-age=31536000, immutable",
{ missing: true },
);
putJson(
options.bucket,
options.endpoint,
releasePath,
`${installerReleasePrefix}/metadata.json`,
"public, max-age=31536000, immutable",
{ missing: true },
);
const installerLatest = await advanceLatestRelease(
options.version,
() =>
readLatestRelease(
options.bucket,
options.endpoint,
`${INSTALLER_PREFIX}/latest.json`,
join(temporaryDirectory, "installer-latest-current.json"),
),
(condition) =>
putJson(
options.bucket,
options.endpoint,
latestPath,
`${INSTALLER_PREFIX}/latest.json`,
"no-store",
condition,
),
);
console.log(
installerLatest.advanced
? `Published installer artifacts for Pi ${options.version} through s3://${options.bucket}/${INSTALLER_PREFIX}/latest.json`
: `Pi ${installerLatest.version} is already the latest installer release.`,
);
}
} finally {
rmSync(temporaryDirectory, { force: true, recursive: true });
}