From 9848b3145a138d92b2461799f125f9558168ece4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 28 Apr 2026 10:03:00 +0200 Subject: [PATCH] fix(coding-agent): fix Windows self-update Fixes #3857 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/config.ts | 35 ++++++++++++++----- .../coding-agent/src/package-manager-cli.ts | 20 +++++++++-- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 512092c7..9f8305f2 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Fixed Cloudflare Workers AI attribution headers to honor the install telemetry setting. +- Fixed `pi update --self` detection and execution for Windows package-manager shim installs, including symlinked global package roots, and print the manual fallback command when self-update fails. ([#3857](https://github.com/badlogic/pi-mono/issues/3857)) ## [0.70.5] - 2026-04-27 diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index 436e3930..7a8a477e 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -1,5 +1,5 @@ import { spawnSync } from "child_process"; -import { existsSync, readFileSync } from "fs"; +import { existsSync, readFileSync, realpathSync } from "fs"; import { homedir } from "os"; import { dirname, join, resolve, sep } from "path"; import { fileURLToPath } from "url"; @@ -94,6 +94,9 @@ function readCommandOutput(command: string, args: string[]): string | undefined encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 2000, + // Windows package managers are commonly .cmd shims. Use the shell so Node can execute them; + // command and args are fixed literals from getGlobalPackageRoots(), not user input. + shell: process.platform === "win32", }); if (result.status !== 0) return undefined; const stdout = result.stdout.trim(); @@ -128,18 +131,32 @@ function getGlobalPackageRoots(method: InstallMethod): string[] { } } -function isManagedByGlobalPackageManager(method: InstallMethod): boolean { - let packageDir = resolve(getPackageDir()); +function normalizeExistingPathForComparison(path: string): string | undefined { + const resolvedPath = resolve(path); + if (!existsSync(resolvedPath)) { + return undefined; + } + let normalizedPath: string; + try { + normalizedPath = realpathSync(resolvedPath); + } catch { + return undefined; + } if (process.platform === "win32") { - packageDir = packageDir.toLowerCase(); + normalizedPath = normalizedPath.toLowerCase(); + } + return normalizedPath; +} + +function isManagedByGlobalPackageManager(method: InstallMethod): boolean { + const packageDir = normalizeExistingPathForComparison(getPackageDir()); + if (!packageDir) { + return false; } return getGlobalPackageRoots(method).some((root) => { - let normalizedRoot = resolve(root); - if (process.platform === "win32") { - normalizedRoot = normalizedRoot.toLowerCase(); - } + const normalizedRoot = normalizeExistingPathForComparison(root); return ( - existsSync(normalizedRoot) && + normalizedRoot !== undefined && (packageDir === normalizedRoot || packageDir.startsWith(normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`)) ); diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index cc935fa0..8ef940f4 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -271,6 +271,12 @@ function printSelfUpdateUnavailable(): void { } } +function printSelfUpdateFallback(): void { + const command = getSelfUpdateCommand(PACKAGE_NAME); + if (!command) return; + console.error(chalk.dim(`If this keeps failing, run this command yourself: ${command.display}`)); +} + async function runSelfUpdate(): Promise { const command = getSelfUpdateCommand(PACKAGE_NAME); if (!command) { @@ -281,7 +287,9 @@ async function runSelfUpdate(): Promise { console.log(chalk.dim(`Updating ${APP_NAME} with ${command.display}...`)); await new Promise((resolve, reject) => { - const child = spawn(command.command, command.args, { stdio: "inherit" }); + // Windows package managers are commonly .cmd shims. Use the shell so Node can execute them; + // command and args come from getSelfUpdateCommandForMethod(), not user input. + const child = spawn(command.command, command.args, { stdio: "inherit", shell: process.platform === "win32" }); child.on("error", (error) => { reject(error); }); @@ -451,7 +459,15 @@ export async function handlePackageCommand(args: string[]): Promise { } if (updateTargetIncludesSelf(target)) { if (canSelfUpdate()) { - await runSelfUpdate(); + try { + await runSelfUpdate(); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : "Unknown package command error"; + console.error(chalk.red(`Error: ${message}`)); + printSelfUpdateFallback(); + process.exitCode = 1; + return true; + } console.log(chalk.green(`Updated ${APP_NAME}`)); } else { printSelfUpdateUnavailable();