Something went wrong. Try again.
more up to date at codeberg https://codeberg.org/cowie/tau.git
Something went wrong. Try again.
14 kB · 359 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360import { mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path";import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";import { ENV_AGENT_DIR, PACKAGE_NAME, VERSION } from "../src/config.ts";import { main } from "../src/main.ts";
describe("package commands", () => { let tempDir: string; let agentDir: string; let projectDir: string; let packageDir: string; let originalCwd: string; let originalAgentDir: string | undefined; let originalPiPackageDir: string | undefined; let originalExitCode: typeof process.exitCode; let originalExecPath: string;
function getNewerPatchVersion(): string { const [major = "0", minor = "0", patch = "0"] = VERSION.split("."); return `${major}.${minor}.${Number.parseInt(patch, 10) + 1}`; }
beforeEach(() => { tempDir = join(tmpdir(), `pi-package-commands-${Date.now()}-${Math.random().toString(36).slice(2)}`); agentDir = join(tempDir, "agent"); projectDir = join(tempDir, "project"); packageDir = join(tempDir, "local-package"); mkdirSync(agentDir, { recursive: true }); mkdirSync(projectDir, { recursive: true }); mkdirSync(packageDir, { recursive: true });
originalCwd = process.cwd(); originalAgentDir = process.env[ENV_AGENT_DIR]; originalPiPackageDir = process.env.PI_PACKAGE_DIR; originalExitCode = process.exitCode; originalExecPath = process.execPath; process.exitCode = undefined; process.env[ENV_AGENT_DIR] = agentDir; process.chdir(projectDir); });
afterEach(() => { vi.unstubAllGlobals(); process.chdir(originalCwd); process.exitCode = originalExitCode; if (originalAgentDir === undefined) { delete process.env[ENV_AGENT_DIR]; } else { process.env[ENV_AGENT_DIR] = originalAgentDir; } if (originalPiPackageDir === undefined) { delete process.env.PI_PACKAGE_DIR; } else { process.env.PI_PACKAGE_DIR = originalPiPackageDir; } Object.defineProperty(process, "execPath", { value: originalExecPath, configurable: true }); rmSync(tempDir, { recursive: true, force: true }); });
it("should persist global relative local package paths relative to settings.json", async () => { const relativePkgDir = join(projectDir, "packages", "local-package"); mkdirSync(relativePkgDir, { recursive: true });
await main(["install", "./packages/local-package"]);
const settingsPath = join(agentDir, "settings.json"); const settings = JSON.parse(readFileSync(settingsPath, "utf-8")) as { packages?: string[] }; expect(settings.packages?.length).toBe(1); const stored = settings.packages?.[0] ?? ""; const resolvedFromSettings = realpathSync(join(agentDir, stored)); expect(resolvedFromSettings).toBe(realpathSync(relativePkgDir)); });
it("should remove local packages using a path with a trailing slash", async () => { await main(["install", `${packageDir}/`]);
const settingsPath = join(agentDir, "settings.json"); const installedSettings = JSON.parse(readFileSync(settingsPath, "utf-8")) as { packages?: string[] }; expect(installedSettings.packages?.length).toBe(1);
await main(["remove", `${packageDir}/`]);
const removedSettings = JSON.parse(readFileSync(settingsPath, "utf-8")) as { packages?: string[] }; expect(removedSettings.packages ?? []).toHaveLength(0); });
it("shows install subcommand help", async () => { const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try { await expect(main(["install", "--help"])).resolves.toBeUndefined();
const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n"); expect(stdout).toContain("Usage:"); expect(stdout).toContain("pi install <source> [-l]"); expect(errorSpy).not.toHaveBeenCalled(); expect(process.exitCode).toBeUndefined(); } finally { logSpy.mockRestore(); errorSpy.mockRestore(); } });
it("shows a friendly error for unknown install options", async () => { const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try { await expect(main(["install", "--unknown"])).resolves.toBeUndefined();
const stderr = errorSpy.mock.calls.map(([message]) => String(message)).join("\n"); expect(stderr).toContain('Unknown option --unknown for "install".'); expect(stderr).toContain('Use "pi --help" or "pi install <source> [-l]".'); expect(process.exitCode).toBe(1); } finally { errorSpy.mockRestore(); } });
it("shows a friendly error for missing install source", async () => { const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try { await expect(main(["install"])).resolves.toBeUndefined();
const stderr = errorSpy.mock.calls.map(([message]) => String(message)).join("\n"); expect(stderr).toContain("Missing install source."); expect(stderr).toContain("Usage: pi install <source> [-l]"); expect(stderr).not.toContain("at "); expect(process.exitCode).toBe(1); } finally { errorSpy.mockRestore(); } });
it("uses global npmCommand and current package name for forced self updates without checking the api", async () => { const globalPrefix = join(tempDir, "global-prefix"); const projectPrefix = join(tempDir, "project-prefix"); const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@earendil-works", "pi-coding-agent"); const fakeNpmPath = join(tempDir, "fake-npm.cjs"); const recordPath = join(tempDir, "self-update.json"); mkdirSync(selfPackageDir, { recursive: true }); mkdirSync(join(projectDir, ".pi"), { recursive: true }); writeFileSync( fakeNpmPath, `const fs=require("node:fs"),path=require("node:path"),args=process.argv.slice(2),prefix=args[args.indexOf("--prefix")+1];if(args.includes("root")) console.log(path.join(prefix,"lib","node_modules"));else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args));`, ); writeFileSync( join(agentDir, "settings.json"), JSON.stringify({ npmCommand: [originalExecPath, fakeNpmPath, "--prefix", globalPrefix] }, null, 2), ); writeFileSync( join(projectDir, ".pi", "settings.json"), JSON.stringify({ npmCommand: [originalExecPath, fakeNpmPath, "--prefix", projectPrefix] }, null, 2), ); process.env.PI_PACKAGE_DIR = selfPackageDir; Object.defineProperty(process, "execPath", { value: join(selfPackageDir, "dist", "cli.js"), configurable: true, }); const fetchMock = vi.fn(); vi.stubGlobal("fetch", fetchMock);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try { await expect(main(["update", "--self", "--force"])).resolves.toBeUndefined();
expect(process.exitCode).toBeUndefined(); expect(errorSpy).not.toHaveBeenCalled(); expect(fetchMock).not.toHaveBeenCalled(); const recordedArgs = JSON.parse(readFileSync(recordPath, "utf-8")) as string[]; expect(recordedArgs).toContain(globalPrefix); expect(recordedArgs).toContain(PACKAGE_NAME); expect(recordedArgs).not.toContain(projectPrefix); } finally { logSpy.mockRestore(); errorSpy.mockRestore(); } });
it("uses the current package name when the update check omits packageName", async () => { const globalPrefix = join(tempDir, "global-prefix"); const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@mariozechner", "pi-coding-agent"); const fakeNpmPath = join(tempDir, "fake-npm.cjs"); const recordPath = join(tempDir, "self-update.json"); mkdirSync(selfPackageDir, { recursive: true }); writeFileSync( fakeNpmPath, `const fs=require("node:fs"),path=require("node:path"),args=process.argv.slice(2),prefix=args[args.indexOf("--prefix")+1];if(args.includes("root")) console.log(path.join(prefix,"lib","node_modules"));else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args));`, ); writeFileSync( join(agentDir, "settings.json"), JSON.stringify({ npmCommand: [originalExecPath, fakeNpmPath, "--prefix", globalPrefix] }, null, 2), ); process.env.PI_PACKAGE_DIR = selfPackageDir; Object.defineProperty(process, "execPath", { value: join(selfPackageDir, "dist", "cli.js"), configurable: true, }); const fetchMock = vi.fn(async () => Response.json({ version: getNewerPatchVersion() })); vi.stubGlobal("fetch", fetchMock);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try { await expect(main(["update", "--self"])).resolves.toBeUndefined();
expect(process.exitCode).toBeUndefined(); expect(errorSpy).not.toHaveBeenCalled(); expect(fetchMock).toHaveBeenCalledOnce(); const recordedArgs = JSON.parse(readFileSync(recordPath, "utf-8")) as string[]; expect(recordedArgs).toContain(PACKAGE_NAME); } finally { logSpy.mockRestore(); errorSpy.mockRestore(); } });
it("installs the active package name from the update check during self-update", async () => { const globalPrefix = join(tempDir, "global-prefix"); const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@mariozechner", "pi-coding-agent"); const fakeNpmPath = join(tempDir, "fake-npm.cjs"); const recordPath = join(tempDir, "self-update.json"); mkdirSync(selfPackageDir, { recursive: true }); writeFileSync( fakeNpmPath, `const fs=require("node:fs"),path=require("node:path"),args=process.argv.slice(2),prefix=args[args.indexOf("--prefix")+1];if(args.includes("root")) console.log(path.join(prefix,"lib","node_modules"));else { const records=fs.existsSync(${JSON.stringify(recordPath)})?JSON.parse(fs.readFileSync(${JSON.stringify(recordPath)},"utf-8")):[]; records.push(args); fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(records));}`, ); writeFileSync( join(agentDir, "settings.json"), JSON.stringify({ npmCommand: [originalExecPath, fakeNpmPath, "--prefix", globalPrefix] }, null, 2), ); process.env.PI_PACKAGE_DIR = selfPackageDir; Object.defineProperty(process, "execPath", { value: join(selfPackageDir, "dist", "cli.js"), configurable: true, }); const activePackageName = PACKAGE_NAME === "@new-scope/pi" ? "@newer-scope/pi" : "@new-scope/pi"; vi.stubGlobal( "fetch", vi.fn(async () => Response.json({ packageName: activePackageName, version: "0.73.0" })), );
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try { await expect(main(["update", "--self"])).resolves.toBeUndefined();
expect(process.exitCode).toBeUndefined(); expect(errorSpy).not.toHaveBeenCalled(); const recordedCalls = JSON.parse(readFileSync(recordPath, "utf-8")) as string[][]; expect(recordedCalls).toEqual([ expect.arrayContaining(["uninstall", "-g", PACKAGE_NAME]), expect.arrayContaining(["install", "-g", activePackageName]), ]); } finally { logSpy.mockRestore(); errorSpy.mockRestore(); } });
it("fails self-update when renamed npm package installation fails", async () => { const globalPrefix = join(tempDir, "global-prefix"); const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@mariozechner", "pi-coding-agent"); const fakeNpmPath = join(tempDir, "fake-npm-fail.cjs"); const recordPath = join(tempDir, "self-update-fail.json"); mkdirSync(selfPackageDir, { recursive: true }); writeFileSync( fakeNpmPath, `const fs=require("node:fs"),path=require("node:path"),args=process.argv.slice(2),prefix=args[args.indexOf("--prefix")+1];if(args.includes("root")) { console.log(path.join(prefix,"lib","node_modules")); process.exit(0);}const records=fs.existsSync(${JSON.stringify(recordPath)})?JSON.parse(fs.readFileSync(${JSON.stringify(recordPath)},"utf-8")):[];records.push(args);fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(records));if(args.includes("install")) process.exit(23);`, ); writeFileSync( join(agentDir, "settings.json"), JSON.stringify({ npmCommand: [originalExecPath, fakeNpmPath, "--prefix", globalPrefix] }, null, 2), ); process.env.PI_PACKAGE_DIR = selfPackageDir; Object.defineProperty(process, "execPath", { value: join(selfPackageDir, "dist", "cli.js"), configurable: true, }); const activePackageName = PACKAGE_NAME === "@new-scope/pi" ? "@newer-scope/pi" : "@new-scope/pi"; vi.stubGlobal( "fetch", vi.fn(async () => Response.json({ packageName: activePackageName, version: "0.73.0" })), );
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try { await expect(main(["update", "--self"])).resolves.toBeUndefined();
expect(process.exitCode).toBe(1); const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n"); const stderr = errorSpy.mock.calls.map(([message]) => String(message)).join("\n"); expect(stdout).not.toContain(`Updated pi`); expect(stderr).toContain("exited with code 23"); const recordedCalls = JSON.parse(readFileSync(recordPath, "utf-8")) as string[][]; expect(recordedCalls).toEqual([ expect.arrayContaining(["uninstall", "-g", PACKAGE_NAME]), expect.arrayContaining(["install", "-g", activePackageName]), ]); } finally { logSpy.mockRestore(); errorSpy.mockRestore(); } });
it("suggests the configured source when update input omits the npm prefix", async () => { const settingsPath = join(agentDir, "settings.json"); writeFileSync(settingsPath, JSON.stringify({ packages: ["npm:pi-formatter"] }, null, 2));
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
try { await expect(main(["update", "pi-formatter"])).resolves.toBeUndefined();
const stderr = errorSpy.mock.calls.map(([message]) => String(message)).join("\n"); const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n"); expect(stderr).toContain("Did you mean npm:pi-formatter?"); expect(stdout).not.toContain("Updated pi-formatter"); expect(process.exitCode).toBe(1);
const settings = JSON.parse(readFileSync(settingsPath, "utf-8")) as { packages?: string[] }; expect(settings.packages).toContain("npm:pi-formatter"); } finally { errorSpy.mockRestore(); logSpy.mockRestore(); } });});