From 56f73a9aeaaba0f6fdb231c11f64b567d366e974 Mon Sep 17 00:00:00 2001 From: Eric Fitzgerald Date: Thu, 20 Aug 2026 01:12:24 -0400 Subject: [PATCH] fix(bump): node apply drops pnpm -w outside a workspace (v2.0.6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _install_cmd read an empty workspaceName as "workspace root" and fell back to `pnpm add -w`, but workspaceName is empty for the root manifest of ANY repo — including a plain single-package one, where pnpm rejects -w with "--workspace-root may only be used inside a workspace". Since in-range bumps never reach this path, every range-widening (i.e. major) bump in a non-workspace pnpm repo failed. Gate -w on pnpm-workspace.yaml actually existing — pnpm ignores package.json's `workspaces` field, so that file's presence is the definition of a pnpm workspace. A real workspace root keeps -w (plain `pnpm add` there fails with ERR_PNPM_ADDING_TO_ROOT); a plain repo gets no flag, matching the npm branch's existing behavior. Fixes #40. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016FKf6Gee6Zzd4xF6Ldho54 --- deps/.claude-plugin/plugin.json | 2 +- deps/.codex-plugin/plugin.json | 2 +- deps/scripts/bumplib/ecosystems/node.py | 12 +++++++++--- tests/test_bump_eco_node.py | 14 ++++++++++++++ 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/deps/.claude-plugin/plugin.json b/deps/.claude-plugin/plugin.json index 3d82ee7..d2e5824 100644 --- a/deps/.claude-plugin/plugin.json +++ b/deps/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "deps", - "version": "2.0.5", + "version": "2.0.6", "description": "Update dependencies safely across Go, Python, and Node ecosystems. Use when the user asks to bump, update, or upgrade dependencies, packages, or deps; run a dependency bump/upgrade; fix Dependabot or security advisories; or refresh outdated packages. Triggers on phrasings like 'bump the deps', 'bump dependencies on ', 'update packages on main', or 'run a dep upgrade'. Auto-detects ecosystems (Go/Python/Node), applies safe patch and minor updates with build, test, and lint validation, bisects failures to isolate bad packages, and surfaces a prioritized plan for major or held packages that need manual review.", "author": { "name": "efitz" } } diff --git a/deps/.codex-plugin/plugin.json b/deps/.codex-plugin/plugin.json index dcc3824..33b1b12 100644 --- a/deps/.codex-plugin/plugin.json +++ b/deps/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "deps", - "version": "2.0.5", + "version": "2.0.6", "description": "Update dependencies safely across Go, Python, and Node ecosystems. Use when the user asks to bump, update, or upgrade dependencies, packages, or deps; run a dependency bump/upgrade; fix Dependabot or security advisories; or refresh outdated packages. Triggers on phrasings like 'bump the deps', 'bump dependencies on ', 'update packages on main', or 'run a dep upgrade'. Auto-detects ecosystems (Go/Python/Node), applies safe patch and minor updates with build, test, and lint validation, bisects failures to isolate bad packages, and surfaces a prioritized plan for major or held packages that need manual review.", "author": { "name": "efitz" diff --git a/deps/scripts/bumplib/ecosystems/node.py b/deps/scripts/bumplib/ecosystems/node.py index d5feba1..3f82f08 100644 --- a/deps/scripts/bumplib/ecosystems/node.py +++ b/deps/scripts/bumplib/ecosystems/node.py @@ -223,17 +223,23 @@ def _widen(rng: str, version: str) -> str: return f"{op}{version}" -def _install_cmd(mgr: str, name: str, version: str, decl: dict) -> list: +def _install_cmd(mgr: str, name: str, version: str, decl: dict, root: Path) -> list: """Command that moves a declared dependency PAST its current range, rewriting the manifest. npm targets a workspace by directory (`-w `); pnpm targets one by package name (`--filter `) because pnpm's own `-w` means 'the workspace root', not 'a workspace'. + An empty workspaceName only means 'root manifest' -- whether that root IS a workspace + root is decided by pnpm-workspace.yaml (pnpm ignores package.json's `workspaces`), and + `-w` outside a workspace is an error, not a no-op. """ spec = f"{name}@{_widen(decl.get('range', ''), version)}" flag = {"dev": "-D", "optional": "-O", "peer": "--save-peer"}.get(decl.get("type", "")) if mgr == "pnpm": cmd = ["pnpm", "add"] - cmd += ["--filter", decl["workspaceName"]] if decl.get("workspaceName") else ["-w"] + if decl.get("workspaceName"): + cmd += ["--filter", decl["workspaceName"]] + elif (root / "pnpm-workspace.yaml").exists(): + cmd += ["-w"] else: cmd = ["npm", "install"] if decl.get("workspaceDir"): @@ -298,7 +304,7 @@ def _checked(cmd): # packages and unrecognized range forms all stay on `update`, which touches # only the lockfile. if version and decl and satisfies(version, decl.get("range", "")) is False: - err = _checked(_install_cmd(mgr, name, version, decl)) + err = _checked(_install_cmd(mgr, name, version, decl, root)) if err: return err else: diff --git a/tests/test_bump_eco_node.py b/tests/test_bump_eco_node.py index 9a00d26..c3973fc 100644 --- a/tests/test_bump_eco_node.py +++ b/tests/test_bump_eco_node.py @@ -240,6 +240,20 @@ def test_pnpm_targets_workspace_by_name(self): self.assertIn(["pnpm", "add", "--filter", "@mono/viewer", "three@^0.185.1"], self.calls) self.assertIn("pnpm-lock.yaml", res["filesModified"]) + def test_pnpm_plain_repo_omits_workspace_flag(self): + """A single-package pnpm repo has no workspace, so `-w` is rejected by pnpm (#40).""" + (self.root / "pnpm-lock.yaml").write_text("") + node.handle("apply", ["eslint@11.0.0"]) + self.assertIn(["pnpm", "add", "-D", "eslint@^11.0.0"], self.calls) + self.assertFalse(any("-w" in cmd for cmd in self.calls)) + + def test_pnpm_workspace_root_keeps_w_flag(self): + """At a real workspace root, plain `pnpm add` fails with ERR_PNPM_ADDING_TO_ROOT.""" + (self.root / "pnpm-lock.yaml").write_text("") + (self.root / "pnpm-workspace.yaml").write_text("packages:\n - packages/*\n") + node.handle("apply", ["eslint@11.0.0"]) + self.assertIn(["pnpm", "add", "-w", "-D", "eslint@^11.0.0"], self.calls) + class TestApplyReportsFailure(unittest.TestCase): """apply must surface a failed npm/pnpm command, never report success (#32)."""