diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 3aeb8606..747e83fb 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -94,7 +94,7 @@ function expandTildePath(input: string): string { function resolveLocalSourceFromInput(source: string, cwd: string): string { const expanded = expandTildePath(source); - return isAbsolute(expanded) ? expanded : resolve(cwd, expanded); + return isAbsolute(expanded) ? resolve(expanded) : resolve(cwd, expanded); } function resolveLocalSourceFromSettings(source: string, baseDir: string): string { @@ -172,18 +172,21 @@ function updatePackageSources( cwd: string, agentDir: string, action: "add" | "remove", -): void { +): boolean { const currentSettings = local ? settingsManager.getProjectSettings() : settingsManager.getGlobalSettings(); const currentPackages = currentSettings.packages ?? []; const baseDir = local ? join(cwd, CONFIG_DIR_NAME) : agentDir; const normalizedSource = normalizePackageSourceForSettings(source, baseDir, cwd); let nextPackages: PackageSource[]; + let changed = false; if (action === "add") { const exists = currentPackages.some((existing) => packageSourcesMatch(existing, source, baseDir, cwd)); nextPackages = exists ? currentPackages : [...currentPackages, normalizedSource]; + changed = !exists; } else { nextPackages = currentPackages.filter((existing) => !packageSourcesMatch(existing, source, baseDir, cwd)); + changed = nextPackages.length !== currentPackages.length; } if (local) { @@ -191,6 +194,8 @@ function updatePackageSources( } else { settingsManager.setPackages(nextPackages); } + + return changed; } async function handlePackageCommand(args: string[]): Promise { @@ -230,7 +235,11 @@ async function handlePackageCommand(args: string[]): Promise { process.exit(1); } await packageManager.remove(options.source, { local: options.local }); - updatePackageSources(settingsManager, options.source, options.local, cwd, agentDir, "remove"); + const removed = updatePackageSources(settingsManager, options.source, options.local, cwd, agentDir, "remove"); + if (!removed) { + console.error(chalk.red(`No matching package found for ${options.source}`)); + process.exit(1); + } console.log(chalk.green(`Removed ${options.source}`)); return true; } diff --git a/packages/coding-agent/test/package-command-paths.test.ts b/packages/coding-agent/test/package-command-paths.test.ts new file mode 100644 index 00000000..539bf7d6 --- /dev/null +++ b/packages/coding-agent/test/package-command-paths.test.ts @@ -0,0 +1,53 @@ +import { mkdirSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } 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; + + 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]; + process.env[ENV_AGENT_DIR] = agentDir; + process.chdir(projectDir); + }); + + afterEach(() => { + process.chdir(originalCwd); + if (originalAgentDir === undefined) { + delete process.env[ENV_AGENT_DIR]; + } else { + process.env[ENV_AGENT_DIR] = originalAgentDir; + } + rmSync(tempDir, { recursive: true, force: true }); + }); + + 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); + }); +});