Merge PR #6527: fix(coding-agent): self-update binary install when its dir overlaps npm/bun bin dir (@am423)

This commit is contained in:
can1357
2026-07-26 15:41:29 +02:00
3 changed files with 66 additions and 4 deletions
+3
View File
@@ -40,6 +40,9 @@
### Fixed
- Fixed advisor retry amplification after transient Codex SSE socket closures by limiting each advisor-level try to one provider transport attempt.
### Fixed
- Fixed `omp update` aborting with `npm error EEXIST` on standalone binary installs whose directory coincides with the global npm/bun bin dir (for example `npm prefix -g` set to `~/.local`, which the installer also targets). The install-target resolver classified the binary as npm/bun-managed from directory containment alone, so `npm install -g` tried to replace a regular file its symlink step would clobber; it now treats a plain executable (not a symlink) in a package-manager bin dir as the standalone binary and self-updates it in place ([#6527](https://github.com/can1357/oh-my-pi/pull/6527) by [@am423](https://github.com/am423)).
## [17.1.2] - 2026-07-24
+33 -4
View File
@@ -209,6 +209,13 @@ interface UpdateMethodResolutionOptions {
miseBinDirs?: readonly string[];
miseDataDir?: string;
npmBinDir?: string;
/**
* Whether the resolved omp path is a plain file (the standalone binary)
* rather than a package-manager symlink. Stops a binary install from being
* misrouted to npm/bun when the global bin dir overlaps the installer's
* target directory.
*/
ompIsRegularFile?: boolean;
}
type UpdateTarget =
@@ -223,15 +230,22 @@ function resolveUpdateMethod(
bunBinDir: string | undefined,
options: UpdateMethodResolutionOptions = {},
): UpdateMethod {
const { homebrewPrefix, miseBinDirs = [], miseDataDir, npmBinDir } = options;
const { homebrewPrefix, miseBinDirs = [], miseDataDir, npmBinDir, ompIsRegularFile = false } = options;
const launcherExtension = path.extname(ompPath).toLowerCase();
const isWindowsScriptLauncher =
launcherExtension === ".cmd" || launcherExtension === ".ps1" || launcherExtension === ".bat";
if (homebrewPrefix && isPathInDirectory(ompPath, path.join(homebrewPrefix, "bin"))) return "brew";
if (miseBinDirs.some(dir => isPathInDirectory(ompPath, dir))) return "mise";
if (miseDataDir && isPathInDirectory(ompPath, path.join(miseDataDir, "shims"))) return "mise";
if (bunBinDir && isPathInDirectory(ompPath, bunBinDir)) return "bun";
if ((npmBinDir && isPathInDirectory(ompPath, npmBinDir)) || isWindowsScriptLauncher) return "npm";
// 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
// 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)
return "npm";
return "binary";
}
@@ -252,7 +266,22 @@ async function resolveUpdateTarget(): Promise<UpdateTarget> {
const ompPath = resolveOmpPath();
if (ompPath) {
const method = resolveUpdateMethod(ompPath, bunBinDir, { homebrewPrefix, miseBinDirs, miseDataDir, npmBinDir });
// Package-manager installs symlink the bin entry into node_modules; the
// standalone installer writes a plain executable. When the global bin dir
// overlaps the installer's default (~/.local/bin), that file type — not
// directory containment — distinguishes a binary install from npm/bun.
let ompIsRegularFile = false;
try {
const stat = fs.lstatSync(ompPath);
ompIsRegularFile = stat.isFile() && !stat.isSymbolicLink();
} catch {}
const method = resolveUpdateMethod(ompPath, bunBinDir, {
homebrewPrefix,
miseBinDirs,
miseDataDir,
npmBinDir,
ompIsRegularFile,
});
if (method === "binary") return { method, path: ompPath };
return { method };
}
@@ -90,6 +90,36 @@ describe("update-cli install target detection", () => {
expect(method).toBe("npm");
});
it("uses binary update when a plain file in the npm global bin dir is the standalone binary, not an npm symlink", () => {
// Regression: with `npm prefix -g` pointed at the installer's default
// (~/.local), directory containment alone misclassified the standalone
// binary as npm-managed, so `npm install -g` failed with EEXIST refusing
// to overwrite the existing executable.
const method = resolveUpdateMethodForTest("/home/u/.local/bin/omp", undefined, {
npmBinDir: "/home/u/.local/bin",
ompIsRegularFile: true,
});
expect(method).toBe("binary");
});
it("uses binary update when a plain file in the bun global bin dir is the standalone binary", () => {
const method = resolveUpdateMethodForTest("/home/u/.local/bin/omp", "/home/u/.local/bin", {
ompIsRegularFile: true,
});
expect(method).toBe("binary");
});
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",
ompIsRegularFile: false,
});
expect(method).toBe("npm");
});
it("uses binary update when prioritized omp is outside bun global bin", () => {
const method = resolveUpdateMethodForTest("/Users/test/.local/bin/omp", "/Users/test/.bun/bin");