From 97ea63920bb6c0e64a0a06c19e3d8b8a898e570c Mon Sep 17 00:00:00 2001 From: can1357 Date: Sun, 26 Jul 2026 15:11:25 +0200 Subject: [PATCH] fix(coding-agent): keep bun/npm routing for regular-file entries on Windows Bun's global bin entry on Windows is a regular-file .exe shim, not a symlink, so the standalone-binary override would have rerouted a legitimate bun-managed install to in-place binary replacement and clobbered the shim. Gate the override on POSIX, where package-manager bin entries are always symlinks; add a regression test. --- packages/coding-agent/src/cli/update-cli.ts | 10 +++++++--- packages/coding-agent/test/update-cli.test.ts | 20 +++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/src/cli/update-cli.ts b/packages/coding-agent/src/cli/update-cli.ts index 82796901e..51ad57149 100644 --- a/packages/coding-agent/src/cli/update-cli.ts +++ b/packages/coding-agent/src/cli/update-cli.ts @@ -239,12 +239,16 @@ function resolveUpdateMethod( if (miseDataDir && isPathInDirectory(ompPath, path.join(miseDataDir, "shims"))) return "mise"; // A plain executable file in a package-manager bin dir is the standalone // binary the installer placed there, not an npm/bun-managed install (those - // symlink into node_modules). When the global bin dir overlaps the + // symlink into node_modules on POSIX). When the global bin dir overlaps the // installer's default (~/.local/bin), classifying by directory alone routes // a binary install through npm/bun, whose reinstall then collides with the // existing file (npm EEXIST). Fall through to binary replacement instead. - if (bunBinDir && isPathInDirectory(ompPath, bunBinDir) && !ompIsRegularFile) return "bun"; - if ((npmBinDir && isPathInDirectory(ompPath, npmBinDir) && !ompIsRegularFile) || isWindowsScriptLauncher) + // Windows is excluded: there package managers write regular-file shims + // (bun's .exe launcher, npm's .cmd/.ps1), so a regular file is NOT evidence + // of a standalone install and the override would hijack managed installs. + const isStandaloneRegularFile = ompIsRegularFile && process.platform !== "win32"; + if (bunBinDir && isPathInDirectory(ompPath, bunBinDir) && !isStandaloneRegularFile) return "bun"; + if ((npmBinDir && isPathInDirectory(ompPath, npmBinDir) && !isStandaloneRegularFile) || isWindowsScriptLauncher) return "npm"; return "binary"; } diff --git a/packages/coding-agent/test/update-cli.test.ts b/packages/coding-agent/test/update-cli.test.ts index 7d25ad0ac..cc3663389 100644 --- a/packages/coding-agent/test/update-cli.test.ts +++ b/packages/coding-agent/test/update-cli.test.ts @@ -111,6 +111,26 @@ describe("update-cli install target detection", () => { expect(method).toBe("binary"); }); + it("keeps bun update for regular-file entries in the bun global bin dir on Windows, where bun writes .exe shims", () => { + // On Windows a bun-managed global install is a regular-file .exe + // launcher, not a symlink, so the standalone-binary override must not + // apply there — it would clobber the shim with a raw binary. Paths use + // forward slashes so the lexical containment check works on the POSIX + // host running this suite; the platform gate is what is under test. + const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform"); + if (!platformDescriptor) throw new Error("process.platform descriptor missing"); + Object.defineProperty(process, "platform", { ...platformDescriptor, value: "win32" }); + try { + const method = resolveUpdateMethodForTest("C:/Users/test/.bun/bin/omp.exe", "C:/Users/test/.bun/bin", { + ompIsRegularFile: true, + }); + + expect(method).toBe("bun"); + } finally { + Object.defineProperty(process, "platform", platformDescriptor); + } + }); + it("still uses npm update when the npm global bin entry is a package-manager symlink, not a plain file", () => { const method = resolveUpdateMethodForTest("/home/u/.local/bin/omp", undefined, { npmBinDir: "/home/u/.local/bin",