fix(coding-agent): omit devDependencies when installing packages (fixes #3009)

This commit is contained in:
Mario Zechner
2026-04-17 01:21:31 +02:00
parent 36bffc1d13
commit ef1fcfcec2
4 changed files with 50 additions and 3 deletions

View File

@@ -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`:

View File

@@ -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

View File

@@ -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 });
}
}

View File

@@ -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"],