From ef1fcfcec25973b478fec9dbc3068c908068f9d4 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 17 Apr 2026 01:21:31 +0200 Subject: [PATCH] fix(coding-agent): omit devDependencies when installing packages (fixes #3009) --- packages/coding-agent/README.md | 2 +- packages/coding-agent/docs/extensions.md | 2 + .../coding-agent/src/core/package-manager.ts | 4 +- .../coding-agent/test/package-manager.test.ts | 45 +++++++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 974dd99c..07f6dd4c 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -376,7 +376,7 @@ pi update # skips pinned packages pi config # enable/disable extensions, skills, prompts, themes ``` -Packages install to `~/.pi/agent/git/` (git) or global npm. Use `-l` for project-local installs (`.pi/git/`, `.pi/npm/`). If you use a Node version manager and want package installs to reuse a stable npm context, set `npmCommand` in `settings.json`, for example `["mise", "exec", "node@20", "--", "npm"]`. +Packages install to `~/.pi/agent/git/` (git) or global npm. Use `-l` for project-local installs (`.pi/git/`, `.pi/npm/`). Git packages install dependencies with `npm install --omit=dev`, so runtime deps must be listed under `dependencies`. If you use a Node version manager and want package installs to reuse a stable npm context, set `npmCommand` in `settings.json`, for example `["mise", "exec", "node@20", "--", "npm"]`. Create a package by adding a `pi` key to `package.json`: diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 7175bab4..57411d3b 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -145,6 +145,8 @@ To share extensions via npm or git as pi packages, see [packages.md](packages.md npm dependencies work too. Add a `package.json` next to your extension (or in a parent directory), run `npm install`, and imports from `node_modules/` are resolved automatically. +For distributed pi packages installed with `pi install` (npm or git), runtime deps must be in `dependencies`. Package installation uses production installs (`npm install --omit=dev`), so `devDependencies` are not available at runtime. + Node.js built-ins (`node:fs`, `node:path`, etc.) are also available. ## Writing an Extension diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index a9ea8799..32f147b6 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -1597,7 +1597,7 @@ export class DefaultPackageManager implements PackageManager { } const packageJsonPath = join(targetDir, "package.json"); if (existsSync(packageJsonPath)) { - await this.runNpmCommand(["install"], { cwd: targetDir }); + await this.runNpmCommand(["install", "--omit=dev"], { cwd: targetDir }); } } @@ -1632,7 +1632,7 @@ export class DefaultPackageManager implements PackageManager { const packageJsonPath = join(targetDir, "package.json"); if (existsSync(packageJsonPath)) { - await this.runNpmCommand(["install"], { cwd: targetDir }); + await this.runNpmCommand(["install", "--omit=dev"], { cwd: targetDir }); } } diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index 62d50fd2..0cda883c 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -441,6 +441,51 @@ Content`, ); }); + it("should install git package dependencies with --omit=dev", async () => { + const source = "git:github.com/user/repo"; + const targetDir = join(agentDir, "git", "github.com", "user", "repo"); + const runCommandSpy = vi + .spyOn(packageManager as any, "runCommand") + .mockImplementation(async (...callArgs: unknown[]) => { + const [command, args] = callArgs as [string, string[]]; + if (command === "git" && args[0] === "clone") { + mkdirSync(targetDir, { recursive: true }); + writeFileSync(join(targetDir, "package.json"), JSON.stringify({ name: "repo", version: "1.0.0" })); + } + }); + + await packageManager.install(source); + + expect(runCommandSpy).toHaveBeenCalledWith("npm", ["install", "--omit=dev"], { cwd: targetDir }); + }); + + it("should update git package dependencies with --omit=dev", async () => { + const source = "git:github.com/user/repo"; + const targetDir = join(tempDir, ".pi", "git", "github.com", "user", "repo"); + mkdirSync(targetDir, { recursive: true }); + writeFileSync(join(targetDir, "package.json"), JSON.stringify({ name: "repo", version: "1.0.0" })); + settingsManager.setProjectPackages([source]); + + vi.spyOn(packageManager as any, "runCommandCapture").mockImplementation(async (...callArgs: unknown[]) => { + const [_command, args] = callArgs as [string, string[]]; + if (args[0] === "rev-parse" && args[1] === "--abbrev-ref" && args[2] === "@{upstream}") { + return "origin/main"; + } + if (args[0] === "rev-parse" && args[1] === "@{upstream}") { + return "remote-head"; + } + if (args[0] === "rev-parse" && args[1] === "HEAD") { + return "local-head"; + } + throw new Error(`Unexpected runCommandCapture args: ${args.join(" ")}`); + }); + const runCommandSpy = vi.spyOn(packageManager as any, "runCommand").mockResolvedValue(undefined); + + await packageManager.update(source); + + expect(runCommandSpy).toHaveBeenCalledWith("npm", ["install", "--omit=dev"], { cwd: targetDir }); + }); + it("should use npmCommand argv for npm root lookup and invalidate cached root when npmCommand changes", () => { settingsManager = SettingsManager.inMemory({ npmCommand: ["mise", "exec", "node@20", "--", "npm"],