@@ -5,6 +5,7 @@
|
||||
### Fixed
|
||||
|
||||
- Fixed Windows npm self-updates to move loaded native dependency packages out of the active install before reinstalling pi ([#4157](https://github.com/earendil-works/pi/issues/4157)).
|
||||
- Fixed `pi update --self` detection for pnpm v11 global installs whose package path resolves through the pnpm store ([#4647](https://github.com/earendil-works/pi/issues/4647)).
|
||||
|
||||
## [0.75.1] - 2026-05-18
|
||||
|
||||
|
||||
@@ -215,16 +215,18 @@ function getGlobalPackageRoots(method: InstallMethod, _packageName: string, npmC
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeExistingPathForComparison(path: string): string | undefined {
|
||||
function normalizeExistingPathForComparison(path: string, resolveSymlinks: boolean): string | undefined {
|
||||
const resolvedPath = resolve(path);
|
||||
if (!existsSync(resolvedPath)) {
|
||||
return undefined;
|
||||
}
|
||||
let normalizedPath: string;
|
||||
try {
|
||||
normalizedPath = realpathSync(resolvedPath);
|
||||
} catch {
|
||||
return undefined;
|
||||
let normalizedPath = resolvedPath;
|
||||
if (resolveSymlinks) {
|
||||
try {
|
||||
normalizedPath = realpathSync(resolvedPath);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (process.platform === "win32") {
|
||||
normalizedPath = normalizedPath.toLowerCase();
|
||||
@@ -232,6 +234,29 @@ function normalizeExistingPathForComparison(path: string): string | undefined {
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
function getPathComparisonCandidates(path: string): string[] {
|
||||
return Array.from(
|
||||
new Set(
|
||||
[normalizeExistingPathForComparison(path, false), normalizeExistingPathForComparison(path, true)].filter(
|
||||
(candidate): candidate is string => !!candidate,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function getEntrypointPackageDir(): string | undefined {
|
||||
const entrypoint = process.argv[1];
|
||||
if (!entrypoint) return undefined;
|
||||
let dir = dirname(entrypoint);
|
||||
while (dir !== dirname(dir)) {
|
||||
if (existsSync(join(dir, "package.json"))) {
|
||||
return dir;
|
||||
}
|
||||
dir = dirname(dir);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isSelfUpdatePathWritable(): boolean {
|
||||
const packageDir = getPackageDir();
|
||||
try {
|
||||
@@ -244,17 +269,14 @@ function isSelfUpdatePathWritable(): boolean {
|
||||
}
|
||||
|
||||
function isManagedByGlobalPackageManager(method: InstallMethod, packageName: string, npmCommand?: string[]): boolean {
|
||||
const packageDir = normalizeExistingPathForComparison(getPackageDir());
|
||||
return (
|
||||
!!packageDir &&
|
||||
getGlobalPackageRoots(method, packageName, npmCommand).some((root) => {
|
||||
const normalizedRoot = normalizeExistingPathForComparison(root);
|
||||
return (
|
||||
!!normalizedRoot &&
|
||||
packageDir.startsWith(normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`)
|
||||
);
|
||||
})
|
||||
);
|
||||
const packageDirs = [getPackageDir(), getEntrypointPackageDir()].filter((dir): dir is string => !!dir);
|
||||
const packageDirCandidates = packageDirs.flatMap((dir) => getPathComparisonCandidates(dir));
|
||||
return getGlobalPackageRoots(method, packageName, npmCommand).some((root) => {
|
||||
return getPathComparisonCandidates(root).some((normalizedRoot) => {
|
||||
const rootPrefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`;
|
||||
return packageDirCandidates.some((packageDir) => packageDir.startsWith(rootPrefix));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getSelfUpdateCommand(
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
const execPathDescriptor = Object.getOwnPropertyDescriptor(process, "execPath");
|
||||
const originalPath = process.env.PATH;
|
||||
const originalPiPackageDir = process.env.PI_PACKAGE_DIR;
|
||||
const originalArgv1 = process.argv[1];
|
||||
let tempDir: string | undefined;
|
||||
|
||||
function setExecPath(value: string): void {
|
||||
@@ -35,6 +36,11 @@ afterEach(() => {
|
||||
} else {
|
||||
process.env.PI_PACKAGE_DIR = originalPiPackageDir;
|
||||
}
|
||||
if (originalArgv1 === undefined) {
|
||||
process.argv.splice(1, 1);
|
||||
} else {
|
||||
process.argv[1] = originalArgv1;
|
||||
}
|
||||
if (tempDir) {
|
||||
chmodSync(tempDir, 0o700);
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
@@ -275,6 +281,49 @@ describe("detectInstallMethod", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("self-updates pnpm v11 global installs resolved through the store", () => {
|
||||
const temp = mkdtempSync(join(tmpdir(), "pi-pnpm11-"));
|
||||
const binDir = join(temp, "bin");
|
||||
const root = join(temp, "Library", "pnpm", "global", "v11");
|
||||
const packageName = "@earendil-works/pi-coding-agent";
|
||||
const globalPackageDir = join(root, "11e9a", "node_modules", "@earendil-works", "pi-coding-agent");
|
||||
const storePackageDir = join(
|
||||
temp,
|
||||
"Library",
|
||||
"pnpm",
|
||||
"store",
|
||||
"v11",
|
||||
"links",
|
||||
"@earendil-works",
|
||||
"pi-coding-agent",
|
||||
"0.75.0",
|
||||
"hash",
|
||||
"node_modules",
|
||||
"@earendil-works",
|
||||
"pi-coding-agent",
|
||||
);
|
||||
mkdirSync(globalPackageDir, { recursive: true });
|
||||
mkdirSync(storePackageDir, { recursive: true });
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
writeFileSync(join(globalPackageDir, "package.json"), "{}");
|
||||
writeFileSync(join(binDir, process.platform === "win32" ? "pnpm.cmd" : "pnpm"), createFakePnpmScript(root));
|
||||
chmodSync(join(binDir, process.platform === "win32" ? "pnpm.cmd" : "pnpm"), 0o755);
|
||||
tempDir = temp;
|
||||
process.env.PATH = `${binDir}${delimiter}${originalPath ?? ""}`;
|
||||
process.env.PI_PACKAGE_DIR = storePackageDir;
|
||||
process.argv[1] = join(globalPackageDir, "dist", "cli.js");
|
||||
setExecPath(join(storePackageDir, "dist", "cli.js"));
|
||||
|
||||
const command = getSelfUpdateCommand(packageName);
|
||||
|
||||
expect(detectInstallMethod()).toBe("pnpm");
|
||||
expect(command).toEqual({
|
||||
command: "pnpm",
|
||||
args: ["install", "-g", packageName],
|
||||
display: `pnpm install -g ${packageName}`,
|
||||
});
|
||||
});
|
||||
|
||||
test("self-updates renamed yarn global installs by removing the old package first", () => {
|
||||
createYarnGlobalInstall();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user