diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index fd7e6c948..4cab06ab4 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -14,6 +14,7 @@ - Fixed compaction and branch summaries failing with "generation hit the token cap" when reasoning used most of the fixed summary output limit. Summaries now use the model's output limit, and `compaction.reserveTokens` only controls when compaction starts ([#9512](https://github.com/earendil-works/pi/issues/9512), [#9075](https://github.com/earendil-works/pi/issues/9075)). - Fixed `models.schema.json` missing descriptions for provider and model fields, so editors showed no hints for them ([#10790](https://github.com/earendil-works/pi/issues/10790)) +- Fixed `pi update` failing with "Managed install marker is missing or invalid" when `managed-install.json` starts with a UTF-8 BOM, which Windows PowerShell 5.1 adds when it rewrites the file. ## [1.3.0] - 2026-10-15 diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index 45a6db707..48d605620 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -37,6 +37,7 @@ import { hasTrustRequiringProjectResources, ProjectTrustStore } from "./core/tru 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 { stripBom } from "./utils/text.ts"; import { formatVersionCheckError, getLatestPiRelease, isNewerPackageVersion } from "./utils/version-check.ts"; import { cleanupWindowsSelfUpdateQuarantine, @@ -63,7 +64,7 @@ function getActiveManagedInstallRoot(): string | undefined { const markerPath = join(managedRoot, MANAGED_INSTALL_MARKER); try { - const marker = JSON.parse(readFileSync(markerPath, "utf8")) as { + const marker = JSON.parse(stripBom(readFileSync(markerPath, "utf8"))) as { kind?: unknown; layout?: unknown; schemaVersion?: unknown; diff --git a/packages/coding-agent/test/package-command-paths.test.ts b/packages/coding-agent/test/package-command-paths.test.ts index c97fbe9bf..68949f9a3 100644 --- a/packages/coding-agent/test/package-command-paths.test.ts +++ b/packages/coding-agent/test/package-command-paths.test.ts @@ -697,6 +697,24 @@ if (process.platform !== "win32") fs.chmodSync(piPath, 0o755); expect(process.exitCode).toBeUndefined(); }); + it("accepts a managed install marker with a UTF-8 BOM", async () => { + const targetVersion = getNewerPatchVersion(); + const { managedRoot } = prepareManagedInstall(targetVersion); + writeFileSync( + join(managedRoot, "managed-install.json"), + `\uFEFF${JSON.stringify({ kind: "pi-managed-install", schemaVersion: 1, layout: "releases-v1" })}\r\n`, + ); + mockManagedUpdate(targetVersion); + 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(errorSpy).not.toHaveBeenCalled(); + expect(process.exitCode).toBeUndefined(); + }); + it("removes older managed releases after an update", async () => { // https://github.com/earendil-works/pi/issues/10392 const targetVersion = getNewerPatchVersion();