import { mkdirSync, readFileSync, realpathSync, rmSync } 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 } from "../src/config.js"; import { main } from "../src/main.js"; describe("package commands", () => { let tempDir: string; let agentDir: string; let projectDir: string; let packageDir: string; let originalCwd: string; let originalAgentDir: string | undefined; let originalExitCode: typeof process.exitCode; 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]; originalExitCode = process.exitCode; process.exitCode = undefined; process.env[ENV_AGENT_DIR] = agentDir; process.chdir(projectDir); }); afterEach(() => { process.chdir(originalCwd); process.exitCode = originalExitCode; if (originalAgentDir === undefined) { delete process.env[ENV_AGENT_DIR]; } else { process.env[ENV_AGENT_DIR] = originalAgentDir; } 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 [-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 [-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 [-l]"); expect(stderr).not.toContain("at "); expect(process.exitCode).toBe(1); } finally { errorSpy.mockRestore(); } }); });