mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-05-23 12:56:55 +00:00
386 lines
14 KiB
TypeScript
386 lines
14 KiB
TypeScript
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
import { tmpdir } from "os";
|
|
import { delimiter, join } from "path";
|
|
import { afterEach, describe, expect, test } from "vitest";
|
|
import {
|
|
detectInstallMethod,
|
|
getSelfUpdateCommand,
|
|
getSelfUpdateUnavailableInstruction,
|
|
getUpdateInstruction,
|
|
} from "../src/config.js";
|
|
|
|
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 {
|
|
Object.defineProperty(process, "execPath", {
|
|
value,
|
|
configurable: true,
|
|
});
|
|
}
|
|
|
|
afterEach(() => {
|
|
if (execPathDescriptor) {
|
|
Object.defineProperty(process, "execPath", execPathDescriptor);
|
|
}
|
|
if (originalPath === undefined) {
|
|
delete process.env.PATH;
|
|
} else {
|
|
process.env.PATH = originalPath;
|
|
}
|
|
if (originalPiPackageDir === undefined) {
|
|
delete process.env.PI_PACKAGE_DIR;
|
|
} 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 });
|
|
tempDir = undefined;
|
|
}
|
|
});
|
|
|
|
function createNpmPrefixInstall(template = "pi-prefix-"): { prefix: string; packageDir: string } {
|
|
const prefix = mkdtempSync(join(tmpdir(), template));
|
|
const root = join(prefix, "lib", "node_modules");
|
|
const scopeDir = join(root, "@earendil-works");
|
|
const packageDir = join(scopeDir, "pi-coding-agent");
|
|
mkdirSync(packageDir, { recursive: true });
|
|
tempDir = prefix;
|
|
process.env.PI_PACKAGE_DIR = packageDir;
|
|
setExecPath(join(packageDir, "dist", "cli.js"));
|
|
return { prefix, packageDir };
|
|
}
|
|
|
|
function createPnpmGlobalInstall(): { root: string; packageDir: string } {
|
|
const temp = mkdtempSync(join(tmpdir(), "pi-pnpm-"));
|
|
const binDir = join(temp, "bin");
|
|
const root = join(temp, "pnpm", "global", "5", "node_modules");
|
|
const packageDir = join(root, "@mariozechner", "pi-coding-agent");
|
|
mkdirSync(packageDir, { recursive: true });
|
|
mkdirSync(binDir, { recursive: true });
|
|
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 = packageDir;
|
|
setExecPath(
|
|
join(
|
|
root,
|
|
".pnpm",
|
|
"@mariozechner+pi-coding-agent@0.0.0",
|
|
"node_modules",
|
|
"@mariozechner",
|
|
"pi-coding-agent",
|
|
"dist",
|
|
"cli.js",
|
|
),
|
|
);
|
|
return { root, packageDir };
|
|
}
|
|
|
|
function createYarnGlobalInstall(): { globalDir: string; packageDir: string } {
|
|
const temp = mkdtempSync(join(tmpdir(), "pi-yarn-"));
|
|
const binDir = join(temp, "bin");
|
|
const globalDir = join(temp, "yarn", "global");
|
|
const packageDir = join(globalDir, "node_modules", "@mariozechner", "pi-coding-agent");
|
|
mkdirSync(packageDir, { recursive: true });
|
|
mkdirSync(binDir, { recursive: true });
|
|
writeFileSync(join(binDir, process.platform === "win32" ? "yarn.cmd" : "yarn"), createFakeYarnScript(globalDir));
|
|
chmodSync(join(binDir, process.platform === "win32" ? "yarn.cmd" : "yarn"), 0o755);
|
|
tempDir = temp;
|
|
process.env.PATH = `${binDir}${delimiter}${originalPath ?? ""}`;
|
|
process.env.PI_PACKAGE_DIR = packageDir;
|
|
setExecPath(join(globalDir, ".yarn", "@mariozechner", "pi-coding-agent", "dist", "cli.js"));
|
|
return { globalDir, packageDir };
|
|
}
|
|
|
|
function createBunGlobalInstall(): { packageDir: string } {
|
|
const temp = mkdtempSync(join(tmpdir(), "pi-bun-"));
|
|
const prefix = join(temp, ".bun");
|
|
const bunBin = join(prefix, "bin");
|
|
const root = join(prefix, "install", "global", "node_modules");
|
|
const scopeDir = join(root, "@earendil-works");
|
|
const packageDir = join(scopeDir, "pi-coding-agent");
|
|
mkdirSync(packageDir, { recursive: true });
|
|
mkdirSync(bunBin, { recursive: true });
|
|
writeFileSync(join(bunBin, process.platform === "win32" ? "bun.cmd" : "bun"), createFakeBunScript(bunBin));
|
|
chmodSync(join(bunBin, process.platform === "win32" ? "bun.cmd" : "bun"), 0o755);
|
|
tempDir = temp;
|
|
process.env.PATH = `${bunBin}${delimiter}${originalPath ?? ""}`;
|
|
process.env.PI_PACKAGE_DIR = packageDir;
|
|
setExecPath(join(packageDir, "dist", "cli.js"));
|
|
return { packageDir };
|
|
}
|
|
|
|
function createFakePnpmScript(root: string): string {
|
|
if (process.platform === "win32") {
|
|
return `@echo off\r\nif "%1"=="root" if "%2"=="-g" echo ${root}\r\n`;
|
|
}
|
|
const escapedRoot = root.replaceAll("'", "'\\''");
|
|
return `#!/bin/sh\nif [ "$1" = "root" ] && [ "$2" = "-g" ]; then\n\tprintf '%s\\n' '${escapedRoot}'\n\texit 0\nfi\nexit 1\n`;
|
|
}
|
|
|
|
function createFakeYarnScript(globalDir: string): string {
|
|
if (process.platform === "win32") {
|
|
return `@echo off\r\nif "%1"=="global" if "%2"=="dir" echo ${globalDir}\r\n`;
|
|
}
|
|
const escapedGlobalDir = globalDir.replaceAll("'", "'\\''");
|
|
return `#!/bin/sh\nif [ "$1" = "global" ] && [ "$2" = "dir" ]; then\n\tprintf '%s\\n' '${escapedGlobalDir}'\n\texit 0\nfi\nexit 1\n`;
|
|
}
|
|
|
|
function createFakeBunScript(bunBin: string): string {
|
|
if (process.platform === "win32") {
|
|
return `@echo off\r\nif "%1"=="pm" if "%2"=="bin" if "%3"=="-g" echo ${bunBin}\r\n`;
|
|
}
|
|
const escapedBunBin = bunBin.replaceAll("'", "'\\''");
|
|
return `#!/bin/sh\nif [ "$1" = "pm" ] && [ "$2" = "bin" ] && [ "$3" = "-g" ]; then\n\tprintf '%s\\n' '${escapedBunBin}'\n\texit 0\nfi\nexit 1\n`;
|
|
}
|
|
|
|
describe("detectInstallMethod", () => {
|
|
test("detects pnpm from Windows .pnpm install paths", () => {
|
|
setExecPath(
|
|
"C:\\Users\\Admin\\Documents\\pnpm-repository\\global\\5\\.pnpm\\@earendil-works+pi-coding-agent@0.67.68\\node_modules\\@earendil-works\\pi-coding-agent\\dist\\cli.js",
|
|
);
|
|
|
|
expect(detectInstallMethod()).toBe("pnpm");
|
|
expect(getUpdateInstruction("@earendil-works/pi-coding-agent")).toBe(
|
|
"Run: pnpm install -g @earendil-works/pi-coding-agent",
|
|
);
|
|
});
|
|
|
|
test("does not self-update unknown wrapper installs", () => {
|
|
setExecPath("/usr/local/bin/node");
|
|
|
|
expect(detectInstallMethod()).toBe("unknown");
|
|
expect(getSelfUpdateCommand("@earendil-works/pi-coding-agent")).toBeUndefined();
|
|
expect(getUpdateInstruction("@earendil-works/pi-coding-agent")).toBe(
|
|
"Update @earendil-works/pi-coding-agent using the package manager, wrapper, or source checkout that provides this installation.",
|
|
);
|
|
});
|
|
|
|
test("self-updates npm installs from custom prefixes", () => {
|
|
const { prefix } = createNpmPrefixInstall();
|
|
|
|
const command = getSelfUpdateCommand("@earendil-works/pi-coding-agent");
|
|
|
|
expect(detectInstallMethod()).toBe("npm");
|
|
expect(command).toEqual({
|
|
command: "npm",
|
|
args: ["--prefix", prefix, "install", "-g", "@earendil-works/pi-coding-agent"],
|
|
display: `npm --prefix ${prefix} install -g @earendil-works/pi-coding-agent`,
|
|
});
|
|
});
|
|
|
|
test("self-updates renamed packages from the current install prefix", () => {
|
|
const { prefix } = createNpmPrefixInstall();
|
|
|
|
const command = getSelfUpdateCommand("@mariozechner/pi-coding-agent", undefined, "@new-scope/pi");
|
|
|
|
expect(command).toEqual({
|
|
command: "npm",
|
|
args: ["--prefix", prefix, "install", "-g", "@new-scope/pi"],
|
|
display: `npm --prefix ${prefix} uninstall -g @mariozechner/pi-coding-agent && npm --prefix ${prefix} install -g @new-scope/pi`,
|
|
steps: [
|
|
{
|
|
command: "npm",
|
|
args: ["--prefix", prefix, "uninstall", "-g", "@mariozechner/pi-coding-agent"],
|
|
display: `npm --prefix ${prefix} uninstall -g @mariozechner/pi-coding-agent`,
|
|
},
|
|
{
|
|
command: "npm",
|
|
args: ["--prefix", prefix, "install", "-g", "@new-scope/pi"],
|
|
display: `npm --prefix ${prefix} install -g @new-scope/pi`,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test("self-update respects configured npmCommand", () => {
|
|
const { prefix } = createNpmPrefixInstall();
|
|
|
|
const command = getSelfUpdateCommand("@earendil-works/pi-coding-agent", ["npm", "--prefix", prefix]);
|
|
|
|
expect(command).toEqual({
|
|
command: "npm",
|
|
args: ["--prefix", prefix, "install", "-g", "@earendil-works/pi-coding-agent"],
|
|
display: `npm --prefix ${prefix} install -g @earendil-works/pi-coding-agent`,
|
|
});
|
|
});
|
|
|
|
test("self-update treats empty npmCommand as unset", () => {
|
|
const { prefix } = createNpmPrefixInstall();
|
|
|
|
const command = getSelfUpdateCommand("@earendil-works/pi-coding-agent", []);
|
|
|
|
expect(command?.args).toEqual(["--prefix", prefix, "install", "-g", "@earendil-works/pi-coding-agent"]);
|
|
});
|
|
|
|
test("quotes npm self-update display paths", () => {
|
|
const { prefix } = createNpmPrefixInstall("pi prefix ");
|
|
|
|
const command = getSelfUpdateCommand("@earendil-works/pi-coding-agent");
|
|
|
|
expect(command?.display).toBe(`npm --prefix "${prefix}" install -g @earendil-works/pi-coding-agent`);
|
|
});
|
|
|
|
test("does not infer Windows npm custom prefixes from package paths", () => {
|
|
const packageDir = "C:\\Users\\Admin\\npm prefix\\node_modules\\@earendil-works\\pi-coding-agent";
|
|
process.env.PI_PACKAGE_DIR = packageDir;
|
|
setExecPath(`${packageDir}\\dist\\cli.js`);
|
|
|
|
expect(detectInstallMethod()).toBe("npm");
|
|
expect(getUpdateInstruction("@earendil-works/pi-coding-agent")).toBe(
|
|
"Run: npm install -g @earendil-works/pi-coding-agent",
|
|
);
|
|
});
|
|
|
|
test("self-updates bun global installs from bun pm bin", () => {
|
|
createBunGlobalInstall();
|
|
|
|
const command = getSelfUpdateCommand("@earendil-works/pi-coding-agent");
|
|
|
|
expect(detectInstallMethod()).toBe("bun");
|
|
expect(command).toEqual({
|
|
command: "bun",
|
|
args: ["install", "-g", "@earendil-works/pi-coding-agent"],
|
|
display: "bun install -g @earendil-works/pi-coding-agent",
|
|
});
|
|
});
|
|
|
|
test("self-updates renamed pnpm global installs by removing the old package first", () => {
|
|
createPnpmGlobalInstall();
|
|
|
|
const command = getSelfUpdateCommand("@mariozechner/pi-coding-agent", undefined, "@new-scope/pi");
|
|
|
|
expect(detectInstallMethod()).toBe("pnpm");
|
|
expect(command).toEqual({
|
|
command: "pnpm",
|
|
args: ["install", "-g", "@new-scope/pi"],
|
|
display: "pnpm remove -g @mariozechner/pi-coding-agent && pnpm install -g @new-scope/pi",
|
|
steps: [
|
|
{
|
|
command: "pnpm",
|
|
args: ["remove", "-g", "@mariozechner/pi-coding-agent"],
|
|
display: "pnpm remove -g @mariozechner/pi-coding-agent",
|
|
},
|
|
{
|
|
command: "pnpm",
|
|
args: ["install", "-g", "@new-scope/pi"],
|
|
display: "pnpm install -g @new-scope/pi",
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
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();
|
|
|
|
const command = getSelfUpdateCommand("@mariozechner/pi-coding-agent", undefined, "@new-scope/pi");
|
|
|
|
expect(detectInstallMethod()).toBe("yarn");
|
|
expect(command).toEqual({
|
|
command: "yarn",
|
|
args: ["global", "add", "@new-scope/pi"],
|
|
display: "yarn global remove @mariozechner/pi-coding-agent && yarn global add @new-scope/pi",
|
|
steps: [
|
|
{
|
|
command: "yarn",
|
|
args: ["global", "remove", "@mariozechner/pi-coding-agent"],
|
|
display: "yarn global remove @mariozechner/pi-coding-agent",
|
|
},
|
|
{
|
|
command: "yarn",
|
|
args: ["global", "add", "@new-scope/pi"],
|
|
display: "yarn global add @new-scope/pi",
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test("self-updates renamed bun global installs by removing the old package first", () => {
|
|
createBunGlobalInstall();
|
|
|
|
const command = getSelfUpdateCommand("@mariozechner/pi-coding-agent", undefined, "@new-scope/pi");
|
|
|
|
expect(detectInstallMethod()).toBe("bun");
|
|
expect(command).toEqual({
|
|
command: "bun",
|
|
args: ["install", "-g", "@new-scope/pi"],
|
|
display: "bun uninstall -g @mariozechner/pi-coding-agent && bun install -g @new-scope/pi",
|
|
steps: [
|
|
{
|
|
command: "bun",
|
|
args: ["uninstall", "-g", "@mariozechner/pi-coding-agent"],
|
|
display: "bun uninstall -g @mariozechner/pi-coding-agent",
|
|
},
|
|
{
|
|
command: "bun",
|
|
args: ["install", "-g", "@new-scope/pi"],
|
|
display: "bun install -g @new-scope/pi",
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test("does not self-update when npm install path is not writable", () => {
|
|
const { packageDir } = createNpmPrefixInstall();
|
|
chmodSync(packageDir, 0o500);
|
|
|
|
expect(getSelfUpdateCommand("@earendil-works/pi-coding-agent")).toBeUndefined();
|
|
expect(getSelfUpdateUnavailableInstruction("@earendil-works/pi-coding-agent")).toContain(
|
|
"the install path is not writable",
|
|
);
|
|
});
|
|
});
|