mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(coding-agent): add PI_SELF_UPDATE_COMMAND and selfUpdateCommand for pi update (#11629)
Refs #7756, #5607. Problem: when a wrapper or another package manager provides Pi (mise, a `PNPM_HOME` wrapper such as nub, Nix), `pi update` cannot update it and prints "pi cannot self-update this installation." Users with several machines have to remember which tool updates Pi on which machine. Example: ```sh export PI_SELF_UPDATE_COMMAND="mise use -g npm:{packageName}@{version}" pi update # Updating pi with mise use -g npm:@earendil-works/pi-coding-agent@1.4.0... # Updated pi from 1.3.0 to 1.4.0 ``` or in `~/.pi/agent/settings.json`: ```json { "selfUpdateCommand": ["nix", "profile", "upgrade", "pi"] } ``` Behavior: - `pi update` still checks pi.dev first. The command runs only when a newer release exists, or with `--force`, and the release note prints first. - Placeholders come from that release: `{packageName}`, `{version}`, `{installSpec}` (`<packageName>@<version>`). A package rename announced by pi.dev flows into them. - `PI_SELF_UPDATE_COMMAND` takes precedence and is split on whitespace (no quoting). The setting is argv, for arguments with spaces, and is read only from global settings, so a project's `.pi/settings.json` cannot inject a command. - The configured command replaces Pi's own update logic for every install method, including installer-managed installs: it is explicit user intent. - Exit code 0 does not prove an update. Pi reports "Updated pi from X to Y" when its own `package.json` now shows Y (in-place updates) or when `pi --version` from PATH reports Y (wrappers such as mise or Nix that install each version into a new directory). Otherwise it warns: ``Ran <cmd>, but `pi --version` reports X, not Y.`` - "cannot self-update this installation" now mentions the env var and setting. Docs: `docs/cli.md` (new "Custom update command" section), `docs/settings.md`, `docs/environment-variables.md`, `--help` env list, regenerated `schemas/settings.schema.json`. Tests: unit tests for argv resolution and placeholders in `test/config.test.ts`; CLI tests in `test/package-command-paths.test.ts` for env precedence over the setting and the managed install, global-only setting gated by the version check, and command failure. `npm run check` passes. Manual end-to-end check with real mise 2026.10.23 in a sandboxed HOME, running this branch as the mise-installed 1.3.0 (`PI_PACKAGE_DIR` pointing at mise's install): | Scenario | Result | |---|---| | `mise use -g npm:{packageName}@{version}` | mise installs 1.4.0; `Updated pi from 1.3.0 to 1.4.0` | | Already 1.4.0 | `pi is already up to date (v1.4.0)`, mise not run | | In-place `npm install -g --prefix <dir> {installSpec}` via the setting | `Updated pi from 1.3.0 to 1.4.0` | | Command `true` (exit 0, installs nothing) | ``Ran true, but `pi --version` reports 1.3.0, not 1.4.0.`` | This PR is AI-generated.
This commit is contained in:
parent
9e4868a605
commit
8bc94b19ff
11 changed files with 299 additions and 1 deletions
|
|
@ -7,6 +7,7 @@
|
|||
- Extension packages can now show their changelog after an update. Pi reads `pi.changelogPath` from the package's `package.json`, or `CHANGELOG.md` in the package root, and on the next fresh launch shows entries newer than the last seen version under "Extension Updates". The `collapseChangelog` setting condenses this to one line per package ([#5958](https://github.com/earendil-works/pi/issues/5958), [#8790](https://github.com/earendil-works/pi/pull/8790))
|
||||
- `pi.exec()` accepts a `maxBuffer` option that limits stdout and stderr combined, and its result has a `truncated` field that is true when output exceeded the limit ([#11360](https://github.com/earendil-works/pi/issues/11360))
|
||||
- Device-code login (for example GitHub Copilot) now opens the verification page and copies the code when you press Enter. Providers can turn either action off with `openBrowserOnConfirm: false` or `copyCodeOnConfirm: false` ([#9282](https://github.com/earendil-works/pi/issues/9282), [#9301](https://github.com/earendil-works/pi/pull/9301))
|
||||
- `pi update` can update Pi through a wrapper or another package manager. Set `PI_SELF_UPDATE_COMMAND` (for example `mise use -g npm:{packageName}@{version}`) or the global `selfUpdateCommand` setting; `pi update` runs it when pi.dev reports a newer release and fills `{packageName}`, `{version}`, and `{installSpec}` from that release. See [Custom update command](docs/cli.md#custom-update-command) ([#7756](https://github.com/earendil-works/pi/issues/7756), [#5607](https://github.com/earendil-works/pi/issues/5607))
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
|||
|
|
@ -282,6 +282,16 @@ Add `--force` to reinstall Pi when the selected update includes Pi.
|
|||
|
||||
`pi update` cannot update Pi when another package manager provides it, such as Nix. Update Pi with that package manager, for example `nix profile upgrade pi`. Package and model catalog updates still work.
|
||||
|
||||
#### Custom update command
|
||||
|
||||
If a wrapper or another package manager provides Pi, set `PI_SELF_UPDATE_COMMAND` so `pi update` runs its update command:
|
||||
|
||||
```sh
|
||||
export PI_SELF_UPDATE_COMMAND="mise use -g npm:{packageName}@{version}"
|
||||
```
|
||||
|
||||
`pi update` runs it only when pi.dev has a newer release, replacing `{packageName}`, `{version}`, and `{installSpec}` (`{packageName}@{version}`) with that release. The value is split on spaces. For arguments with spaces, use the `selfUpdateCommand` setting in `~/.pi/agent/settings.json` instead, an argv array such as `["nix", "profile", "upgrade", "pi"]`.
|
||||
|
||||
### Aliases and command options
|
||||
|
||||
- `pi uninstall <source>` is an alias for `pi remove <source>`.
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ These variables are read by Pi itself:
|
|||
| `PI_PACKAGE_DIR` | Override the package directory, useful for Nix/Guix store paths |
|
||||
| `PI_OFFLINE` | Disable automatic network activity, including model catalog refreshes |
|
||||
| `PI_SKIP_VERSION_CHECK` | Disable the `pi.dev` latest-version request |
|
||||
| `PI_SELF_UPDATE_COMMAND` | Command `pi update` runs to update Pi itself, split on whitespace, with `{packageName}`, `{version}`, and `{installSpec}` placeholders. Takes precedence over the `selfUpdateCommand` setting. See [Custom update command](cli.md#custom-update-command) |
|
||||
| `PI_TELEMETRY` | Override install/update telemetry and provider attribution headers: `1`/`true`/`yes` or `0`/`false`/`no` |
|
||||
| `PI_CODEMODE_MAX_WORKERS` | Maximum codemode workers alive at once across all sessions in the process; default `2`. Each worker's script may use up to 256 MB. Raise it when one process runs several sessions, such as in-process subagents. Further scripts wait for a free worker. See [Codemode](codemode.md) |
|
||||
| `PI_CACHE_RETENTION` | Set to `long` for extended provider prompt caching where supported |
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ Keep `retry.provider.maxRetries` at `0` unless provider-level retries are requir
|
|||
| `shellPath` | string | Platform default | Custom shell executable path. Supports a leading `~`. |
|
||||
| `shellCommandPrefix` | string | None | Prefix prepended to every shell command. |
|
||||
| `npmCommand` | `string[]` | `npm` | Command and arguments used for npm package lookup and installation. |
|
||||
| `selfUpdateCommand` | `string[]` | None | Command and arguments `pi update` runs to update Pi itself, with `{packageName}`, `{version}`, and `{installSpec}` placeholders. `PI_SELF_UPDATE_COMMAND` takes precedence. **Can only be set in agent-directory settings.** See [Custom update command](cli.md#custom-update-command). |
|
||||
|
||||
See [Shell aliases](shell-aliases.md) for shell setup and [Pi Packages](packages.md) for package-manager behavior.
|
||||
|
||||
|
|
|
|||
|
|
@ -267,6 +267,14 @@
|
|||
},
|
||||
"description": "Command used for npm package lookup and installation, in argv form such as [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]."
|
||||
},
|
||||
"selfUpdateCommand": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1,
|
||||
"description": "Command `pi update` runs to update Pi itself, in argv form such as [\"mise\", \"use\", \"-g\", \"npm:{packageName}@{version}\"]. Placeholders: {packageName}, {version}, {installSpec}. PI_SELF_UPDATE_COMMAND takes precedence. Global setting only."
|
||||
},
|
||||
"collapseChangelog": {
|
||||
"type": "boolean",
|
||||
"description": "Show the condensed changelog after update; use /changelog for the full changelog.",
|
||||
|
|
|
|||
|
|
@ -463,6 +463,7 @@ ${chalk.bold("Environment Variables:")}
|
|||
${ENV_SESSION_DIR.padEnd(32)} - Session storage directory (overridden by --session-dir)
|
||||
PI_PACKAGE_DIR - Override package directory (for Nix/Guix store paths)
|
||||
PI_OFFLINE - Disable startup network operations when set to 1/true/yes
|
||||
PI_SELF_UPDATE_COMMAND - Command pi update runs to update pi itself ({packageName}, {version}, {installSpec})
|
||||
PI_TELEMETRY - Override install telemetry when set to 1/true/yes or 0/false/no
|
||||
PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://pi.dev/session/)
|
||||
|
||||
|
|
|
|||
|
|
@ -334,6 +334,32 @@ export function getSelfUpdateCommand(
|
|||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* The self-update command configured by the user: `PI_SELF_UPDATE_COMMAND` (whitespace-separated
|
||||
* argv, no quoting), else the global `selfUpdateCommand` setting. Overrides install-method detection.
|
||||
*/
|
||||
export function getConfiguredSelfUpdateArgv(setting: string[] | undefined): string[] | undefined {
|
||||
const fromEnv = process.env.PI_SELF_UPDATE_COMMAND?.trim().split(/\s+/).filter(Boolean);
|
||||
if (fromEnv?.length) return fromEnv;
|
||||
return setting?.length ? setting : undefined;
|
||||
}
|
||||
|
||||
/** Fill the `{packageName}`, `{version}`, and `{installSpec}` placeholders of a configured self-update command. */
|
||||
export function makeConfiguredSelfUpdateCommand(
|
||||
argv: string[],
|
||||
release: { packageName: string; version: string },
|
||||
): SelfUpdateCommand {
|
||||
const values: Record<string, string> = {
|
||||
packageName: release.packageName,
|
||||
version: release.version,
|
||||
installSpec: `${release.packageName}@${release.version}`,
|
||||
};
|
||||
const [command = "", ...args] = argv.map((arg) =>
|
||||
arg.replace(/\{(packageName|version|installSpec)\}/g, (_, name: string) => values[name]),
|
||||
);
|
||||
return makeSelfUpdateCommandStep(command, args);
|
||||
}
|
||||
|
||||
export function getSelfUpdateUnavailableInstruction(
|
||||
packageName: string,
|
||||
npmCommand?: string[],
|
||||
|
|
|
|||
|
|
@ -283,6 +283,13 @@ export const SettingsSchema = Type.Object(
|
|||
'Command used for npm package lookup and installation, in argv form such as ["mise", "exec", "node@20", "--", "npm"].',
|
||||
}),
|
||||
),
|
||||
selfUpdateCommand: Type.Optional(
|
||||
Type.Array(Type.String(), {
|
||||
minItems: 1,
|
||||
description:
|
||||
'Command `pi update` runs to update Pi itself, in argv form such as ["mise", "use", "-g", "npm:{packageName}@{version}"]. Placeholders: {packageName}, {version}, {installSpec}. PI_SELF_UPDATE_COMMAND takes precedence. Global setting only.',
|
||||
}),
|
||||
),
|
||||
collapseChangelog: Type.Optional(
|
||||
Type.Boolean({
|
||||
description: "Show the condensed changelog after update; use /changelog for the full changelog.",
|
||||
|
|
|
|||
|
|
@ -17,11 +17,14 @@ import { createProjectTrustContext } from "./cli/project-trust.ts";
|
|||
import {
|
||||
APP_NAME,
|
||||
CONFIG_DIR_NAME,
|
||||
detectInstallChange,
|
||||
detectInstallMethod,
|
||||
getAgentDir,
|
||||
getConfiguredSelfUpdateArgv,
|
||||
getPackageDir,
|
||||
getSelfUpdateCommand,
|
||||
getSelfUpdateUnavailableInstruction,
|
||||
makeConfiguredSelfUpdateCommand,
|
||||
PACKAGE_NAME,
|
||||
type SelfUpdateCommand,
|
||||
type SelfUpdatePackageTarget,
|
||||
|
|
@ -638,6 +641,9 @@ function printSelfUpdateUnavailable(
|
|||
): void {
|
||||
console.error(`error: ${APP_NAME} cannot self-update this installation.`);
|
||||
console.error(getSelfUpdateUnavailableInstruction(PACKAGE_NAME, npmCommand, updatePackageTarget));
|
||||
console.error(
|
||||
`To make \`${APP_NAME} update\` run your own update command, set PI_SELF_UPDATE_COMMAND or the selfUpdateCommand setting.`,
|
||||
);
|
||||
|
||||
const entrypoint = process.argv[1];
|
||||
if (entrypoint) {
|
||||
|
|
@ -735,6 +741,48 @@ async function runSelfUpdate(command: SelfUpdateCommand): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
/** Run the user-configured self-update command, then report the version installed on disk. */
|
||||
async function runConfiguredSelfUpdate(argv: string[], plan: SelfUpdatePlan): Promise<void> {
|
||||
const command = makeConfiguredSelfUpdateCommand(argv, plan);
|
||||
if (plan.note) {
|
||||
printSelfUpdateNote(plan.note);
|
||||
}
|
||||
try {
|
||||
await runSelfUpdate(command);
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "Unknown self-update command error";
|
||||
console.error(chalk.red(`Error: ${message}`));
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
// Exit code 0 does not prove an update. Wrappers such as mise or Nix install each version into a
|
||||
// new directory and leave this installation untouched, so fall back to the pi on PATH.
|
||||
const change = detectInstallChange();
|
||||
const installedVersion =
|
||||
change?.kind === "updated" && change.version === plan.version ? change.version : readVersionOnPath();
|
||||
if (installedVersion === plan.version) {
|
||||
console.log(chalk.green(`Updated ${APP_NAME} from ${VERSION} to ${plan.version}`));
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`Ran ${command.display}, but \`${APP_NAME} --version\` reports ${installedVersion ?? "no version"}, not ${plan.version}.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** Version printed by `pi --version` as resolved from PATH, the pi the user runs next. */
|
||||
function readVersionOnPath(): string | undefined {
|
||||
// PI_PACKAGE_DIR points at this installation and would make the new pi report the old version.
|
||||
const { PI_PACKAGE_DIR: _, ...env } = process.env;
|
||||
const result = spawnProcessSync(APP_NAME, ["--version"], {
|
||||
encoding: "utf-8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env,
|
||||
});
|
||||
return result.status === 0 ? result.stdout.trim() || undefined : undefined;
|
||||
}
|
||||
|
||||
function prepareWindowsNpmSelfUpdate(): void {
|
||||
if (process.platform !== "win32") {
|
||||
return;
|
||||
|
|
@ -1058,7 +1106,11 @@ export async function handlePackageCommand(
|
|||
}
|
||||
}
|
||||
if (updateTargetIncludesSelf(target)) {
|
||||
const managedInstallRoot = getActiveManagedInstallRoot();
|
||||
const configuredSelfUpdateArgv = getConfiguredSelfUpdateArgv(
|
||||
settingsManager.getGlobalSettings().selfUpdateCommand,
|
||||
);
|
||||
// A configured command is explicit user intent, so it replaces the managed install path too.
|
||||
const managedInstallRoot = configuredSelfUpdateArgv ? undefined : getActiveManagedInstallRoot();
|
||||
if (managedInstallRoot && options.force) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
|
|
@ -1072,6 +1124,10 @@ export async function handlePackageCommand(
|
|||
if (!selfUpdatePlan.shouldRun) {
|
||||
return true;
|
||||
}
|
||||
if (configuredSelfUpdateArgv) {
|
||||
await runConfiguredSelfUpdate(configuredSelfUpdateArgv, selfUpdatePlan);
|
||||
return true;
|
||||
}
|
||||
if (managedInstallRoot) {
|
||||
if (selfUpdatePlan.note) {
|
||||
printSelfUpdateNote(selfUpdatePlan.note);
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ import {
|
|||
detectInstallChange,
|
||||
detectInstallMethod,
|
||||
findNodePackageDir,
|
||||
getConfiguredSelfUpdateArgv,
|
||||
getSelfUpdateCommand,
|
||||
getSelfUpdateUnavailableInstruction,
|
||||
getUpdateInstruction,
|
||||
makeConfiguredSelfUpdateCommand,
|
||||
} from "../src/config.ts";
|
||||
|
||||
const execPathDescriptor = Object.getOwnPropertyDescriptor(process, "execPath");
|
||||
|
|
@ -489,3 +491,54 @@ describe("detectInstallChange", () => {
|
|||
expect(detectInstallChange(join(installDir, "package.json"))).toEqual({ kind: "removed" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("configured self-update command", () => {
|
||||
const originalSelfUpdateCommand = process.env.PI_SELF_UPDATE_COMMAND;
|
||||
|
||||
afterEach(() => {
|
||||
if (originalSelfUpdateCommand === undefined) {
|
||||
delete process.env.PI_SELF_UPDATE_COMMAND;
|
||||
} else {
|
||||
process.env.PI_SELF_UPDATE_COMMAND = originalSelfUpdateCommand;
|
||||
}
|
||||
});
|
||||
|
||||
test("prefers PI_SELF_UPDATE_COMMAND over the setting and splits it on whitespace", () => {
|
||||
process.env.PI_SELF_UPDATE_COMMAND = " nub upgrade {installSpec} ";
|
||||
|
||||
expect(getConfiguredSelfUpdateArgv(["mise", "upgrade"])).toEqual(["nub", "upgrade", "{installSpec}"]);
|
||||
});
|
||||
|
||||
test("uses the setting when PI_SELF_UPDATE_COMMAND is unset or blank", () => {
|
||||
process.env.PI_SELF_UPDATE_COMMAND = " ";
|
||||
|
||||
expect(getConfiguredSelfUpdateArgv(["mise", "upgrade", "npm:{packageName}"])).toEqual([
|
||||
"mise",
|
||||
"upgrade",
|
||||
"npm:{packageName}",
|
||||
]);
|
||||
expect(getConfiguredSelfUpdateArgv([])).toBeUndefined();
|
||||
expect(getConfiguredSelfUpdateArgv(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("fills placeholders in every argument", () => {
|
||||
const command = makeConfiguredSelfUpdateCommand(
|
||||
["wrapper", "install", "{installSpec}", "--name={packageName}", "--version", "{version}", "{unknown}"],
|
||||
{ packageName: "@earendil-works/pi-coding-agent", version: "1.2.3" },
|
||||
);
|
||||
|
||||
expect(command).toEqual({
|
||||
command: "wrapper",
|
||||
args: [
|
||||
"install",
|
||||
"@earendil-works/pi-coding-agent@1.2.3",
|
||||
"--name=@earendil-works/pi-coding-agent",
|
||||
"--version",
|
||||
"1.2.3",
|
||||
"{unknown}",
|
||||
],
|
||||
display:
|
||||
"wrapper install @earendil-works/pi-coding-agent@1.2.3 --name=@earendil-works/pi-coding-agent --version 1.2.3 {unknown}",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -774,6 +774,140 @@ if (process.platform !== "win32") fs.chmodSync(piPath, 0o755);
|
|||
expect(process.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
function writeNodeShim(binDir: string, name: string, scriptPath: string): void {
|
||||
const shimPath = join(binDir, process.platform === "win32" ? `${name}.cmd` : name);
|
||||
writeFileSync(
|
||||
shimPath,
|
||||
process.platform === "win32"
|
||||
? `@echo off\r\n"${originalExecPath}" "${scriptPath}" %*\r\n`
|
||||
: `#!/bin/sh\nexec "${originalExecPath}" "${scriptPath}" "$@"\n`,
|
||||
);
|
||||
chmodSync(shimPath, 0o755);
|
||||
}
|
||||
|
||||
/** Put a `pi-wrapper` update command and a `pi` that prints `piVersion` first on PATH. */
|
||||
function prepareSelfUpdateCommand(piVersion: string, exitCode = 0): { scriptPath: string; recordPath: string } {
|
||||
const binDir = join(tempDir, "self-update-bin");
|
||||
const scriptPath = join(tempDir, "self-update.cjs");
|
||||
const piScriptPath = join(tempDir, "fake-pi.cjs");
|
||||
const recordPath = join(tempDir, "self-update-record.json");
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
writeFileSync(
|
||||
scriptPath,
|
||||
`require("node:fs").writeFileSync(${JSON.stringify(recordPath)}, JSON.stringify(process.argv.slice(2)));
|
||||
process.exit(${exitCode});
|
||||
`,
|
||||
);
|
||||
writeFileSync(piScriptPath, `console.log(${JSON.stringify(piVersion)});\n`);
|
||||
writeNodeShim(binDir, "pi-wrapper", scriptPath);
|
||||
writeNodeShim(binDir, "pi", piScriptPath);
|
||||
process.env.PATH = `${binDir}${delimiter}${originalPath ?? ""}`;
|
||||
return { scriptPath, recordPath };
|
||||
}
|
||||
|
||||
it("runs PI_SELF_UPDATE_COMMAND instead of the managed update and the selfUpdateCommand setting", async () => {
|
||||
const targetVersion = getNewerPatchVersion();
|
||||
const { npmRecordPath } = prepareManagedInstall(targetVersion);
|
||||
const { scriptPath, recordPath } = prepareSelfUpdateCommand(targetVersion);
|
||||
const settingRecordPath = join(tempDir, "setting-record.json");
|
||||
writeFileSync(
|
||||
join(agentDir, "settings.json"),
|
||||
JSON.stringify({ selfUpdateCommand: [originalExecPath, scriptPath, settingRecordPath] }),
|
||||
);
|
||||
vi.stubEnv("PI_SELF_UPDATE_COMMAND", "pi-wrapper upgrade {installSpec} {version}");
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => Response.json({ packageName: "@new-scope/pi", version: targetVersion, note: "Read me" })),
|
||||
);
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
await expect(runPackageCommandDirectly(["update", "--self"])).resolves.toBeUndefined();
|
||||
|
||||
expect(JSON.parse(readFileSync(recordPath, "utf8")) as string[]).toEqual([
|
||||
"upgrade",
|
||||
`@new-scope/pi@${targetVersion}`,
|
||||
targetVersion,
|
||||
]);
|
||||
expect(existsSync(npmRecordPath)).toBe(false);
|
||||
const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n");
|
||||
expect(stdout).toContain("Read me");
|
||||
// Like mise, the command leaves this installation untouched; the pi on PATH reports the new version.
|
||||
expect(stdout).toContain(`Updated pi from ${VERSION} to ${targetVersion}`);
|
||||
expect(errorSpy).not.toHaveBeenCalled();
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it("runs the global selfUpdateCommand setting only when a newer release exists", async () => {
|
||||
const targetVersion = getNewerPatchVersion();
|
||||
const { scriptPath, recordPath } = prepareSelfUpdateCommand(targetVersion);
|
||||
const projectRecordPath = join(tempDir, "project-record.json");
|
||||
writeFileSync(
|
||||
join(agentDir, "settings.json"),
|
||||
JSON.stringify({ selfUpdateCommand: [originalExecPath, scriptPath, "{packageName}"] }),
|
||||
);
|
||||
mkdirSync(join(projectDir, ".pi"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(projectDir, ".pi", "settings.json"),
|
||||
JSON.stringify({ selfUpdateCommand: [originalExecPath, scriptPath, projectRecordPath] }),
|
||||
);
|
||||
vi.stubEnv("PI_SELF_UPDATE_COMMAND", "");
|
||||
const fetchMock = vi.fn(async () => Response.json({ version: VERSION }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
await expect(runPackageCommandDirectly(["update", "--self"])).resolves.toBeUndefined();
|
||||
expect(existsSync(recordPath)).toBe(false);
|
||||
expect(logSpy.mock.calls.map(([message]) => String(message)).join("\n")).toContain(
|
||||
`pi is already up to date (v${VERSION})`,
|
||||
);
|
||||
|
||||
fetchMock.mockImplementation(async () => Response.json({ version: targetVersion }));
|
||||
await expect(runPackageCommandDirectly(["update", "--self"])).resolves.toBeUndefined();
|
||||
expect(JSON.parse(readFileSync(recordPath, "utf8")) as string[]).toEqual([PACKAGE_NAME]);
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it("warns when the pi on PATH does not report the new version", async () => {
|
||||
const targetVersion = getNewerPatchVersion();
|
||||
prepareSelfUpdateCommand(VERSION);
|
||||
vi.stubEnv("PI_SELF_UPDATE_COMMAND", "pi-wrapper upgrade");
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => Response.json({ version: targetVersion })),
|
||||
);
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
|
||||
await expect(runPackageCommandDirectly(["update", "--self"])).resolves.toBeUndefined();
|
||||
|
||||
const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n");
|
||||
expect(stdout).toContain(
|
||||
`Ran pi-wrapper upgrade, but \`pi --version\` reports ${VERSION}, not ${targetVersion}.`,
|
||||
);
|
||||
expect(stdout).not.toContain("Updated pi from");
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it("fails when the configured self-update command fails", async () => {
|
||||
prepareSelfUpdateCommand(VERSION, 23);
|
||||
vi.stubEnv("PI_SELF_UPDATE_COMMAND", "pi-wrapper upgrade");
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => Response.json({ version: getNewerPatchVersion() })),
|
||||
);
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
await expect(runPackageCommandDirectly(["update", "--self"])).resolves.toBeUndefined();
|
||||
|
||||
expect(errorSpy.mock.calls.map(([message]) => String(message)).join("\n")).toContain(
|
||||
"pi-wrapper upgrade exited with code 23",
|
||||
);
|
||||
expect(logSpy.mock.calls.map(([message]) => String(message)).join("\n")).not.toContain("Ran pi-wrapper");
|
||||
expect(process.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps the managed release active when its update fails", async () => {
|
||||
const targetVersion = getNewerPatchVersion();
|
||||
const { managedRoot } = prepareManagedInstall(targetVersion, 23);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue