Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
17 kB · 459 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";import { homedir, tmpdir } from "os";import { delimiter, join } from "path";import { afterEach, describe, expect, test } from "vitest";import { detectInstallMethod, ENV_LEGACY_SESSION_DIR, ENV_SESSION_DIR, getDaemonLogPath, getSelfUpdateCommand, getSelfUpdateUnavailableInstruction, getSessionsDir, getUpdateInstruction,} from "../src/config.js";import { getDefaultSessionDir } from "../src/core/session-manager.js";
const execPathDescriptor = Object.getOwnPropertyDescriptor(process, "execPath");const originalPath = process.env.PATH;const originalPiPackageDir = process.env.PI_PACKAGE_DIR;const originalSessionDir = process.env[ENV_SESSION_DIR];const originalLegacySessionDir = process.env[ENV_LEGACY_SESSION_DIR];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 (originalSessionDir === undefined) { delete process.env[ENV_SESSION_DIR]; } else { process.env[ENV_SESSION_DIR] = originalSessionDir; } if (originalLegacySessionDir === undefined) { delete process.env[ENV_LEGACY_SESSION_DIR]; } else { process.env[ENV_LEGACY_SESSION_DIR] = originalLegacySessionDir; } 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 createHomebrewInstall(): { packageDir: string } { const prefix = mkdtempSync(join(tmpdir(), "pi-homebrew-")); const packageDir = join(prefix, "Cellar", "prime-agent", "0.7.0", "libexec", "lib", "node_modules", "prime-agent"); mkdirSync(packageDir, { recursive: true }); tempDir = prefix; process.env.PI_PACKAGE_DIR = packageDir; setExecPath(join(packageDir, "dist", "cli.js")); return { 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("leaves Homebrew installs under Homebrew ownership", () => { createHomebrewInstall();
expect(detectInstallMethod()).toBe("homebrew"); expect(getSelfUpdateCommand("prime-agent")).toBeUndefined(); expect(getSelfUpdateUnavailableInstruction("prime-agent")).toBe("Update with: brew upgrade prime-agent"); expect(getUpdateInstruction("prime-agent")).toBe("Update with: brew upgrade prime-agent"); });
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-updates tarball specs without uninstalling the same logical package first", () => { const { prefix } = createNpmPrefixInstall(); const tarballUrl = "https://downloads.example.test/prime-agent/prime-agent-0.73.0.tgz";
const command = getSelfUpdateCommand("@earendil-works/pi-coding-agent", undefined, tarballUrl);
expect(command).toEqual({ command: "npm", args: ["--prefix", prefix, "install", "-g", tarballUrl], display: `npm --prefix ${prefix} install -g ${tarballUrl}`, }); });
test("self-updates renamed tarball packages by uninstalling the old package after install", () => { const { prefix } = createNpmPrefixInstall(); const tarballUrl = "https://downloads.example.test/prime-agent/prime-agent-0.73.0.tgz";
const command = getSelfUpdateCommand("@earendil-works/pi-coding-agent", undefined, tarballUrl, "prime-agent");
expect(command).toEqual({ command: "npm", args: ["--prefix", prefix, "install", "-g", tarballUrl], display: `npm --prefix ${prefix} install -g ${tarballUrl} && npm --prefix ${prefix} uninstall -g @earendil-works/pi-coding-agent`, steps: [ { command: "npm", args: ["--prefix", prefix, "install", "-g", tarballUrl], display: `npm --prefix ${prefix} install -g ${tarballUrl}`, }, { command: "npm", args: ["--prefix", prefix, "uninstall", "-g", "@earendil-works/pi-coding-agent"], display: `npm --prefix ${prefix} uninstall -g @earendil-works/pi-coding-agent`, }, ], }); });
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 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", ); });});
describe("session paths", () => { test("uses the short app-prefixed session dir env var", () => { expect(ENV_SESSION_DIR).toBe("PRIME_AGENT_SESSION_DIR"); });
test("uses the session root env var when computing sessions dir", () => { const sessionRoot = join(tmpdir(), `pi-session-root-${Date.now()}`); process.env[ENV_SESSION_DIR] = sessionRoot;
expect(getSessionsDir("/agent")).toBe(sessionRoot); });
test("uses the legacy coding agent session root env var when the new env var is unset", () => { const sessionRoot = join(tmpdir(), `pi-legacy-session-root-${Date.now()}`); delete process.env[ENV_SESSION_DIR]; process.env[ENV_LEGACY_SESSION_DIR] = sessionRoot;
expect(getSessionsDir("/agent")).toBe(sessionRoot); });
test("expands tilde in the session root env var", () => { process.env[ENV_SESSION_DIR] = "~/prime-agent-sessions";
expect(getSessionsDir("/agent")).toBe(join(homedir(), "prime-agent-sessions")); });
test("uses the env session root as the default session dir", () => { tempDir = mkdtempSync(join(tmpdir(), "pi-session-root-")); const cwd = join(tempDir, "project"); const sessionRoot = join(tempDir, "sessions-root"); process.env[ENV_SESSION_DIR] = sessionRoot;
const sessionDir = getDefaultSessionDir(cwd, join(tempDir, "agent"));
expect(sessionDir).toBe(sessionRoot); });});
describe("getDaemonLogPath", () => { test("normalizes socket path spellings to one log file", () => { if (process.platform === "win32") { return; }
expect(getDaemonLogPath("/a//b.sock")).toBe(getDaemonLogPath("/a/b.sock")); });});