fix(coding-agent): preserved linked update ownership

Detected npm and Bun ownership from the bin link immediate target within each precise global node_modules root. Foreign aliases now update their resolved standalone binary without replacing the alias.

Fixes #8468
This commit is contained in:
can1357
2026-08-14 00:18:43 +02:00
parent 15981329d6
commit 642d6c0b31
3 changed files with 221 additions and 30 deletions
+99 -5
View File
@@ -26,6 +26,7 @@ import {
resolveReleaseDist,
resolveReleaseRename,
resolveUpdateMethodForTest,
resolveUpdateTargetFromPath,
shouldForceBinaryUpdate,
sweepStaleUpdateArtifacts,
updateViaBinaryAt,
@@ -187,6 +188,89 @@ describe("update-cli install target detection", () => {
expect(method).toBe("npm");
});
it("updates the standalone binary behind a foreign npm-bin alias without replacing the alias", async () => {
const dir = await makeTempDir();
const npmBinDir = path.join(dir, ".npm-global", "bin");
const standalonePath = path.join(dir, ".local", "bin", "omp");
const aliasPath = path.join(npmBinDir, "omp");
await fs.mkdir(npmBinDir, { recursive: true });
await Bun.write(standalonePath, "binary");
await fs.symlink(standalonePath, aliasPath);
const target = resolveUpdateTargetFromPath(aliasPath, undefined, {
allowPackageManagers: true,
npmBinDir,
});
expect(target).toEqual({ method: "binary", path: standalonePath, replacesSymlink: false });
expect(await fs.readlink(aliasPath)).toBe(standalonePath);
});
it("keeps an npm-linked checkout under npm management instead of overwriting its resolved script", async () => {
const dir = await makeTempDir();
const npmPrefix = path.join(dir, ".npm-global");
const npmBinDir = path.join(npmPrefix, "bin");
const packagePath = path.join(npmPrefix, "lib", "node_modules", "@oh-my-pi", "pi-coding-agent");
const checkoutPath = path.join(dir, "checkout");
const checkoutCli = path.join(checkoutPath, "dist", "cli.js");
const aliasPath = path.join(npmBinDir, "omp");
await fs.mkdir(npmBinDir, { recursive: true });
await fs.mkdir(path.dirname(packagePath), { recursive: true });
await Bun.write(checkoutCli, "linked checkout");
await fs.symlink(checkoutPath, packagePath, "junction");
await fs.symlink(path.relative(npmBinDir, path.join(packagePath, "dist", "cli.js")), aliasPath);
const target = resolveUpdateTargetFromPath(aliasPath, undefined, {
allowPackageManagers: true,
npmBinDir,
});
expect(await fs.realpath(aliasPath)).toBe(checkoutCli);
expect(target).toEqual({ method: "npm", path: aliasPath });
expect(await Bun.file(checkoutCli).text()).toBe("linked checkout");
});
it("treats a Bun-bin alias into ~/.bun/custom as foreign", async () => {
const dir = await makeTempDir();
const bunDir = path.join(dir, ".bun");
const bunBinDir = path.join(bunDir, "bin");
const standalonePath = path.join(bunDir, "custom", "omp");
const aliasPath = path.join(bunBinDir, "omp");
await fs.mkdir(bunBinDir, { recursive: true });
await Bun.write(standalonePath, "binary");
await fs.symlink(path.relative(bunBinDir, standalonePath), aliasPath);
const target = resolveUpdateTargetFromPath(aliasPath, bunBinDir, {
allowPackageManagers: true,
});
expect(target).toEqual({ method: "binary", path: standalonePath, replacesSymlink: false });
});
it("keeps a split-root Bun-linked checkout under Bun management instead of overwriting its script", async () => {
const dir = await makeTempDir();
const bunBinDir = path.join(dir, "bun-bin");
const bunGlobalDir = path.join(dir, "bun-global");
const packagePath = path.join(bunGlobalDir, "node_modules", "@oh-my-pi", "pi-coding-agent");
const checkoutPath = path.join(dir, "checkout");
const checkoutCli = path.join(checkoutPath, "dist", "cli.js");
const aliasPath = path.join(bunBinDir, "omp");
await fs.mkdir(bunBinDir, { recursive: true });
await fs.mkdir(path.dirname(packagePath), { recursive: true });
await Bun.write(checkoutCli, "linked checkout");
await fs.symlink(checkoutPath, packagePath, "junction");
await fs.symlink(path.relative(bunBinDir, path.join(packagePath, "dist", "cli.js")), aliasPath);
const target = resolveUpdateTargetFromPath(aliasPath, bunBinDir, {
allowPackageManagers: true,
bunGlobalDir,
});
expect(await fs.realpath(aliasPath)).toBe(checkoutCli);
expect(target).toEqual({ method: "bun", path: aliasPath });
expect(await Bun.file(checkoutCli).text()).toBe("linked checkout");
});
it("uses binary update when prioritized omp is outside bun global bin", () => {
const method = resolveUpdateMethodForTest("/Users/test/.local/bin/omp", "/Users/test/.bun/bin");
@@ -437,13 +521,23 @@ describe("update-cli bun install command", () => {
expect(args.some(arg => arg.startsWith("@oh-my-pi/pi-natives-"))).toBe(false);
});
it("derives global node_modules from supported bun global locations", () => {
expect(resolveBunGlobalNodeModulesDirFromLocations(path.join("home", ".bun", "bin"), undefined)).toBe(
path.join("home", ".bun", "install", "global", "node_modules"),
);
it("derives global node_modules from supported Bun locations with the explicit global directory taking precedence", () => {
expect(
resolveBunGlobalNodeModulesDirFromLocations(undefined, path.join("home", ".bun", "install", "cache")),
resolveBunGlobalNodeModulesDirFromLocations({
globalBinDir: path.join("home", ".bun", "bin"),
}),
).toBe(path.join("home", ".bun", "install", "global", "node_modules"));
expect(
resolveBunGlobalNodeModulesDirFromLocations({
cacheDir: path.join("home", ".bun", "install", "cache"),
}),
).toBe(path.join("home", ".bun", "install", "global", "node_modules"));
expect(
resolveBunGlobalNodeModulesDirFromLocations({
globalDir: path.join("root", "bun-global"),
globalBinDir: path.join("root", "bun-bin"),
}),
).toBe(path.join("root", "bun-global", "node_modules"));
});
});