## Summary
Fixes intermittent failures in the `parallel dlx calls of the same package` test, especially on Windows CI. Multiple race conditions were discovered when concurrent `pnpm dlx` processes share the same Global Virtual Store (GVS):
- **Content-verified skip in GVS mode**: When `safeToSkip` is true and a rename fails because the target already exists (ENOTEMPTY/EEXIST/EPERM), verify all files match (inode or content comparison) before skipping. Falls through to `renameOverwriteSync` if content doesn't match.
- **Tolerate EPERM during bin creation on Windows**: `cmd-shim`'s `chmod` can fail with EPERM when another process holds the `.bin` file. Warn instead of crashing.
- **Handle EPERM in DLX cache symlink**: Added EPERM to the list of tolerated errors when creating the DLX cache symlink, as Windows can throw this when another process has the symlink open.
## Test plan
- [x] `parallel dlx calls of the same package` test passes on Windows CI
- [x] Full test suite passes on both Ubuntu and Windows
Adds a `pnpm worktree:new <branch>` script that creates a git worktree
as a sibling directory of the repo root, with branch slashes replaced by
dashes (e.g. `feat/foo` → `../feat-foo`). Shell helpers in `shell/wt.fish`
and `shell/wt.sh` wrap the script to also `cd` into the new worktree.
Documents setup and usage in CONTRIBUTING.md.
- Adds two Husky hooks to prevent Claude Code from bad git practices:
- \`prepare-commit-msg\`: blocks \`git commit --amend\` (detected via the \`commit\` source argument)
- \`pre-commit\`: blocks committing directly to \`main\`
- Both hooks detect Claude Code sessions via the \`CLAUDECODE\` environment variable, so regular users are unaffected
- Error messages explicitly tell Claude what to do instead
## Summary
- Support specifying the pnpm version via `devEngines.packageManager` in `package.json`, as an alternative to the `packageManager` field
- Unlike `packageManager`, `devEngines.packageManager` supports semver ranges — the resolved version is stored in `pnpm-lock.env.yaml` and reused if it still satisfies the range
- The `onFail` field determines behavior: `download` (auto-download), `error` (default), `warn`, or `ignore`
- `devEngines.packageManager` takes precedence over `packageManager` when both are present (with a warning)
- For array notation, default `onFail` is `ignore` for non-last elements and `error` for the last
- For the legacy `packageManager` field, `onFail` is derived from existing config settings (`managePackageManagerVersions`, `packageManagerStrict`, `packageManagerStrictVersion`), so `main.ts` uses `onFail` as the single source of truth
- Reuses `EngineDependency` type from `@pnpm/types` instead of a custom `WantedPackageManager` type
## Test plan
- [x] 10 tests in `switchingVersions.test.ts` — version switching with `packageManager` field, `devEngines.packageManager` with `onFail=download` (exact + range), env lockfile reuse, corrupt binary
- [x] 15 tests in `packageManagerCheck.test.ts` — version checks with `engines.pnpm`, `packageManager` field, `devEngines.packageManager` with all `onFail` values, array notation, range matching, precedence
close https://github.com/pnpm/pnpm/issues/8153
The `pnpm audit` command now also audits dependencies from
`pnpm-lock.env.yaml`, including `configDependencies` and
`packageManagerDependencies` along with their transitive dependencies.
They appear as separate groups in the audit tree so that vulnerability
paths clearly indicate their origin (e.g. configDependencies>pkg>...).
Update all packages from zkochan/packages to their latest major versions
and exclude them from minimumReleaseAge requirement. This includes
updating catalog entries, adapting to breaking API changes (default
exports replaced with named exports, sync functions renamed with Sync
suffix), and updating type declarations.
## Summary
Store config dependency and package manager integrity info in a separate `pnpm-lock.env.yaml` lockfile instead of inlining it in `pnpm-workspace.yaml`. The workspace manifest now contains only clean version specifiers for `configDependencies`, while the resolved versions, integrity hashes, and tarball URLs are recorded in the new env lockfile.
### Key changes
- **New `pnpm-lock.env.yaml` lockfile**: Uses the standard lockfile format (`importers`, `packages`, `snapshots`) to store resolved config dependencies and package manager dependencies with integrity hashes and tarball URLs.
- **Automatic migration**: Projects using the old inline-hash format in `pnpm-workspace.yaml` are automatically migrated on install.
- **Global Virtual Store (GVS) for version switching**: When switching pnpm versions via the `packageManager` field, pnpm is installed to the global virtual store (`$STORE_DIR/links/`) instead of `globalPkgDir`, reusing the content-addressable store for deduplication.
- **Self-update uses headless install**: `pnpm self-update` performs frozen headless installs using integrity hashes from the env lockfile, then links bins to `PNPM_HOME`.
- **`packageManagerDependencies`**: The env lockfile also stores resolved `packageManagerDependencies` during version switching and self-update.
- **`@pnpm/exe` support**: Replicates `@pnpm/exe`'s postinstall script (linking platform-specific binaries) since install scripts are disabled.
- **`pnpm setup` refactored**: Uses `pnpm add -g` instead of copying the CLI binary directly.
- **Extracted `toLockfileResolution`** to `@pnpm/lockfile.utils` and **deduplicated `iteratePkgMeta`** into `@pnpm/calc-dep-state`.
- **Removed unused `@pnpm/tools.path` package**.
* refactor: rename rebuildSelectedPkgs/rebuildProjects to buildSelectedPkgs/buildProjects
The "rebuild" prefix is redundant now that these functions live in
@pnpm/building.after-install.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: rename Rebuild option types to Build (RebuildOptions → BuildOptions, etc.)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: rename plugin-commands-rebuild and exec.build-commands to building domain
- @pnpm/plugin-commands-rebuild → @pnpm/building.build-commands
- @pnpm/exec.build-commands → @pnpm/building.policy-commands
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move build-modules and pkg-requires-build to building domain
- @pnpm/build-modules → @pnpm/building.during-install
- @pnpm/exec.pkg-requires-build → @pnpm/building.pkg-requires-build
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: alphabetically sort imports after package renames
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add changeset
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract rebuild implementation into @pnpm/building.after-install
Move the rebuild implementation (rebuildProjects, rebuildSelectedPkgs)
from @pnpm/plugin-commands-rebuild into a new @pnpm/building.after-install
package. This breaks the circular dependency chain:
config/deps-installer → core → plugin-commands-rebuild → cli-utils → config/deps-installer
The CLI command layer stays in plugin-commands-rebuild, while the core
rebuild logic now lives in building/after-install with no dependency
on cli-utils.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add README for @pnpm/building.after-install
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: rename @pnpm/builder.policy to @pnpm/building.policy
Move builder/policy to building/policy, consolidating all build-related
packages under the building/ domain. Update all consumers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add changeset
* refactor: remove old implementation files from plugin-commands-rebuild
These files were copied to @pnpm/building.after-install but not removed
from the original location.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: remove unused deps from plugin-commands-rebuild
Dependencies that were only needed by the implementation (now in
@pnpm/building.after-install) are removed. Deps used only in tests
are moved to devDependencies.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: simplify patchedDependencies lockfile format to map selectors to hashes
Remove the `path` field from patchedDependencies in the lockfile, changing the
format from `Record<string, { path: string, hash: string }>` to
`Record<string, string>` (selector → hash). The path was never consumed from
the lockfile — patch file paths come from user config, not the lockfile.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: migrate old patchedDependencies format when reading lockfile
When reading a lockfile with the old `{ path, hash }` format for
patchedDependencies, extract just the hash string. This ensures
backwards compatibility with existing lockfiles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: carry patchFilePath through patch groups for runtime patch application
The previous commit removed `path` from the lockfile format but also
accidentally dropped it from the runtime PatchInfo type. This broke
patch application since `applyPatchToDir` needs the file path.
- Add optional `patchFilePath` to `PatchInfo` for runtime use
- Build patch groups with resolved file paths in install
- Fix `build-modules` to use `patchFilePath` instead of `file.path`
- Fix `calcPatchHashes` call site in `checkDepsStatus` (extra arg)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update remaining references to old PatchFile type
- Update getPatchInfo tests to use { hash, key } instead of { file, key }
- Fix createDeployFiles to handle patchedDependencies as hash strings
- Fix configurationalDependencies test assertion
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: throw when patch exists but patchFilePath is missing
Also guard against undefined patchedDependencies entry when
ignorePackageManifest is true.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: don't join lockfileDir with already-absolute patch file paths
opts.patchedDependencies values are already absolute paths, so
path.join(opts.lockfileDir, absolutePath) created invalid doubled
paths like /project/home/runner/work/pnpm/...
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use path.resolve for patch file paths and address Copilot review
- Use path.resolve instead of path.join to correctly handle both
relative and absolute patch file paths
- Use PnpmError instead of plain Error for missing patch file path
- Only copy patchedDependencies to deploy output when manifest
provides the patch file paths
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: pass rootProjectManifest in deploy patchedDependencies test
The test was missing rootProjectManifest, so createDeployFiles could
not find the manifest's patchedDependencies to propagate to the
deploy output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: install config dependencies into the global virtual store
Config dependencies are now imported into {storeDir}/links/ following the
same path convention as regular packages (@scope/name/version/hash), then
symlinked into node_modules/.pnpm-config/. When the package already exists
in the GVS, the fetch and import are skipped entirely.
* refactor: extract shared GVS path computation into @pnpm/calc-dep-state
Move the leaf node hash computation from config deps-installer into
calcLeafGlobalVirtualStorePath in @pnpm/calc-dep-state to avoid
duplicating the hash logic.
* test: strip ANSI before assertions in outdated and formatError tests
Same brittleness as in list tests: exact comparison with chalk output
fails when chalk is disabled or TTY differs. Strip control characters
on both sides before toBe/toContain so tests pass in any environment.
- reviewing/plugin-commands-outdated: renderLatest.test.ts — assert
on '(deprecated)' after stripVTControlCharacters(output).
- pnpm/test: formatError.test.ts — strip both actual and expected
before toBe().
* fix: use stripAnsi alias and simplify test assertions
Address review comments: use `stripVTControlCharacters as stripAnsi`
alias for codebase consistency, and simplify formatError tests by
comparing against plain text expected values instead of stripping
both sides.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When running `pnpm add -g .`, the linked-from path equals the cwd,
so path.relative() returns an empty string which is falsy, causing
the fallback to '???'. Use the absolute path as fallback instead.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: avoid static type import that triggers fuse-native resolution in test
The static `import type` from createFuseHandlers.ts caused Jest to resolve
the entire module graph, including fuse-native, before the mock was set up.
Derive the type from the function's return type instead.
* fix: fix mount-modules test when fuse-native is not installed
The static `import type` from createFuseHandlers.ts caused Jest to resolve
the entire module graph, including fuse-native, before the mock was set up.
Derive the type from the function's return type instead.
Also add a moduleNameMapper and mock file for fuse-native so Jest's resolver
can find the module even when the optional dependency is not installed.
* fix: preserve existing jest config in meta-updater
The meta-updater was overwriting the entire jest config with just the
preset, dropping any additional config like moduleNameMapper.
Hide the ugly temporary directory path prefix from progress output
for global installs (same approach as dlx). Show a proper install
summary with "global:" heading instead of the temp directory path.
The store index was migrated from msgpack files to SQLite in #10827,
but this test still referenced the removed getIndexFilePathInCafs function
and readMsgpackFileSync, causing a compilation error.
* feat(calc-dep-state): use allowBuilds to compute engine-agnostic GVS hashes
Use the allowBuilds config to determine which packages need ENGINE_NAME
in their GVS hash. Packages that are not allowed to build (and don't
transitively depend on packages that are) now get engine-agnostic hashes,
so they survive Node.js upgrades and architecture changes without
re-import.
Closes#10837
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(modules-yaml): persist allowBuilds and re-link GVS on change
Store the allowBuilds config in modules.yaml so that when it changes
between installs, the headless installer detects the difference and
re-processes all packages with updated GVS hashes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: deduplicate computeBuiltDepPaths into iterateHashedGraphNodes
Move builtDepPaths computation inside iterateHashedGraphNodes, which now
accepts an AllowBuild function instead of a precomputed Set. This
eliminates duplicate logic in iteratePkgsForVirtualStore and
resolve-dependencies.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(gvs): recover from failed or interrupted builds using .pnpm-needs-build marker
When a GVS package needs building, a .pnpm-needs-build marker file is added
to the filesMap before import. The import pipeline treats it as a regular file,
so it's atomically included in the staged directory and renamed with the
package. On the next install, GVS fast paths detect the marker and force a
re-fetch/re-import/re-build.
On build success, the marker is removed. On build failure, the entire hash
directory is removed so the next install starts fresh.
The marker is only checked for packages that are allowed to build (via
allowBuild), minimizing filesystem operations. It is also skipped when cached
side effects will be applied, since the package is already built.
Closes#10837
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: remove .pnpm-needs-build marker before uploading side effects
Move the marker removal before the side effects upload so the marker
file is not included in the side effects diff. Add a test assertion
that verifies the marker does not appear in the cached side effects.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(calc-dep-state): return undefined from computeBuiltDepPaths when allowBuild is not configured
Previously, computeBuiltDepPaths returned an empty Set when allowBuild
was undefined, causing all GVS hashes to become engine-agnostic even
without allowBuilds configured. Now the function is only called when
allowBuild is provided, and iterateHashedGraphNodes avoids materializing
the iterator when it's not needed.
Also restore upfront filtering in extendGraph so non-GVS installs only
hash runtime dep paths, and only pass allowBuild when GVS is on.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: use JSON for npm registry metadata cache instead of msgpack
Switch the on-disk package metadata cache from msgpack (.mpk) to JSON (.json).
When metadata is not filtered, the raw JSON response from the registry is written
directly to disk with cachedAt injected, avoiding a parse-then-serialize round-trip.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update lockfileOnly test to use .json metadata extension
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update resolving/npm-resolver/src/pickPackage.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
## Summary
Replace individual `.mpk` (MessagePack) files under `$STORE/index/` with a single SQLite database at `$STORE/index.db` using Node.js 22's built-in `node:sqlite` module. This reduces filesystem syscall overhead and improves space efficiency for small metadata entries.
Closes#10826
## Design
### New package: `@pnpm/store.index`
A new `StoreIndex` class wraps a SQLite database with a simple key-value API (`get`, `set`, `delete`, `has`, `entries`). Data is serialized with msgpackr and stored as BLOBs. The table uses `WITHOUT ROWID` for compact storage.
Key design decisions:
- **WAL mode** enables concurrent reads from workers while the main process writes.
- **`busy_timeout=5000`** plus a retry loop with `Atomics.wait`-based `sleepSync` handles `SQLITE_BUSY` errors from concurrent access.
- **Performance PRAGMAs**: `synchronous=NORMAL`, `mmap_size=512MB`, `cache_size=32MB`, `temp_store=MEMORY`, `wal_autocheckpoint=10000`.
- **Write batching**: `queueWrites()` batches pre-packed entries from tarball extraction and flushes them in a single transaction on `process.nextTick`. `setRawMany()` writes immediate batches (e.g. from `addFilesFromDir`).
- **Lifecycle**: `close()` auto-flushes pending writes, runs `PRAGMA optimize`, and closes the DB. A `process.on('exit')` handler ensures cleanup even on unexpected exits.
- **`VACUUM` after `deleteMany`** (used by `pnpm store prune`) to reclaim disk space.
### Key format
Keys are `integrity\tpkgId` (tab-separated). Git-hosted packages use `pkgId\tbuilt` or `pkgId\tnot-built`.
### Shared StoreIndex instance
A single `StoreIndex` instance is threaded through the entire install lifecycle — from `createNewStoreController` through the fetcher chain, package requester, license scanner, SBOM collector, and dependencies hierarchy. This replaces the previous pattern of each component creating its own file-based index access.
### Worker architecture
Index writes are performed in the main process, not in worker threads. Workers send pre-packed `{ key, buffer }` pairs back to the main process via `postMessage`, where they are batched and flushed to SQLite. This avoids SQLite write contention between threads.
### SQLite ExperimentalWarning suppression
`node:sqlite` emits an `ExperimentalWarning` on first load. This is suppressed via a `process.emitWarning` override injected through esbuild's `banner` option, which runs on line 1 of both `dist/pnpm.mjs` and `dist/worker.js` — before any module that loads `node:sqlite`.
### No migration from `.mpk` files
Old `.mpk` index files are not migrated. Packages missing from the new SQLite index are re-fetched on demand (the same behavior as a fresh store).
## Changed packages
121 files changed across these areas:
- **`store/index/`** — New `@pnpm/store.index` package
- **`worker/`** — Write batching moved from worker module into `StoreIndex` class; workers send pre-packed buffers to main process
- **`store/package-store/`** — StoreIndex creation and lifecycle management
- **`store/cafs/`** — Removed `getFilePathInCafs` index-file utilities (no longer needed)
- **`store/pkg-finder/`** — Reads from StoreIndex instead of `.mpk` files
- **`store/plugin-commands-store/`** — `store status` uses StoreIndex
- **`store/plugin-commands-store-inspecting/`** — `cat-index` and `find-hash` use StoreIndex
- **`fetching/tarball-fetcher/`** — Threads StoreIndex through fetchers; git-hosted fetcher flushes before reading
- **`fetching/git-fetcher/`, `binary-fetcher/`, `pick-fetcher/`** — Accept StoreIndex parameter
- **`pkg-manager/`** — `client`, `core`, `headless`, `package-requester` thread StoreIndex
- **`reviewing/`** — `license-scanner`, `sbom`, `dependencies-hierarchy` accept StoreIndex
- **`cache/api/`** — Cache view uses StoreIndex
- **`pnpm/bundle.ts`** — esbuild banner for ExperimentalWarning suppression
## Test plan
- [x] `pnpm --filter @pnpm/store.index test` — Unit tests for StoreIndex CRUD and batching
- [x] `pnpm --filter @pnpm/package-store test` — Store controller lifecycle
- [x] `pnpm --filter @pnpm/package-requester test` — Package requester reads from SQLite index
- [x] `pnpm --filter @pnpm/tarball-fetcher test` — Tarball and git-hosted fetcher writes
- [x] `pnpm --filter @pnpm/headless test` — Headless install
- [x] `pnpm --filter @pnpm/core test` — Core install, side effects, patching
- [x] `pnpm --filter @pnpm/plugin-commands-rebuild test` — Rebuild reads from index
- [x] `pnpm --filter @pnpm/license-scanner test` — License scanning
- [x] e2e tests pass
🤖 Generated with [Claude Code](https://claude.com/claude-code)
* build: enable TypeScript `verbatimModuleSyntax`
* fix: use `import type` to elide import errors during bundling
```
> pnpm dlx node@runtime:24.6.0 bundle.ts
Error: R] Could not resolve "@npm/types"
../pkg-manifest/exportable-manifest/lib/transform/index.js:1:15:
1 │ import {} from '@npm/types';
╵ ~~~~~~~~~~~~
You can mark the path "@npm/types" as external to exclude it from the bundle, which will remove this error and leave the unresolved path in the bundle.
Error: Build failed with 1 error:
../pkg-manifest/exportable-manifest/lib/transform/index.js:1:15: ERROR: Could not resolve "@npm/types"
at failureErrorWithLog (/home/runner/work/pnpm/pnpm/node_modules/.pnpm/esbuild@0.25.12/node_modules/esbuild/lib/main.js:1467:15)
```
* fix: use `import type` to elide import errors in tests
* fix: resolve relative path params in global add against CWD
When running `pnpm -g add .`, the "." was resolved relative to the
temporary install directory instead of the user's working directory.
This happened because handleGlobalAdd switches opts.dir to a fresh
temp directory before the dependency selectors are resolved.
Now relative path params (., ./foo, ../bar, file:./foo, link:../bar)
are resolved to absolute paths before the directory is switched.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve relative local selectors against opts.dir instead of process.cwd()
This fixes `pnpm -C <dir> -g add .` where the relative selector would
incorrectly resolve against process.cwd() instead of the user's intended
directory. Also adds test coverage for file: and link: prefixed selectors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(config): stop overwriting dir to globalPkgDir for global commands
Previously, `pnpmConfig.dir` was set to `globalPkgDir` when `--global`
was used. This caused `opts.dir` to point to the global packages
directory instead of the user's CWD, breaking `pnpm -g add .` because
relative paths resolved against the wrong directory.
Now `pnpmConfig.dir` is always set to the user's CWD. Global command
handlers already use `opts.globalPkgDir` where they need the global
packages directory.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use globalPkgDir in pnpm root -g handler
The root command handler was using opts.dir which no longer points to
the global packages directory. Use opts.globalPkgDir instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: allow global install to override bins owned by the new package
When a package name matches the bin name (e.g., `npm` package providing
`npm` bin), the new package gets priority and can override an existing
bin from another global package. The `npx` bin is also treated as owned
by the `npm` package via a hardcoded override map.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* ci: run benchmarks on node.js 25
* feat: skip linking bins owned by existing global packages instead of failing
When installing a package globally (e.g. node) whose bins conflict with
an already-installed package that owns those bins (e.g. npm owns npm/npx),
the install now succeeds and skips linking the conflicting bins rather
than aborting with GLOBAL_BIN_CONFLICT.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: improve global bin conflict checks for aliased packages and multi-provider bins
Track all new bin providers (not just the last one) so ownership is
correctly resolved when multiple new packages provide the same bin.
Use manifest.name instead of the dependency alias when checking
existing package ownership, fixing incorrect decisions for aliased
installs. Use symlink-dir in tests for Windows compatibility and
remove a non-null assertion in link-bins.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use Set for conflicting bins lookup and fix error hint to use alias
Convert the conflicting bins collection from an array to a Set for O(1)
lookups. Use the dependency alias in the `pnpm remove -g` hint since
that is what the user originally installed with, showing both alias and
manifest name when they differ.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: remove changeset
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace all sinon.spy/match/withArgs usage with jest.fn, toHaveBeenCalledWith,
mockClear, and mock.calls filtering. Remove sinon and @types/sinon from all
package.json files and the workspace catalog.
* test(fetch): verify pnpm fetch populates global virtual store links/
pnpm fetch flows through headlessInstall → lockfileToDepGraph →
iteratePkgsForVirtualStore, which respects enableGlobalVirtualStore.
When enabled, packages are written to the store's links/ directory
instead of only the CAS. This behavior is relied upon by tools that
pre-build stores (Docker multistage builds, Nix, CI caching) but was
not covered by any test.
Also adds enableGlobalVirtualStore to FetchCommandOptions type to
match the runtime behavior where the option flows through via the
opts spread.
* test(fetch): use lockfileOnly instead of full install in GVS test
The test only needs a lockfile for the fetch step, so a full install
followed by rimraf of node_modules is unnecessary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Previously, `ci: true` (auto-detected or configured) unconditionally
set `enableGlobalVirtualStore` to `false`, even when the user had
explicitly enabled it in `pnpm-workspace.yaml` or via CLI. This forced
users to add `ci: false` as a workaround in their workspace config
whenever they wanted GVS in CI-like environments (Nix builds, CI systems
with persistent caches, Docker multistage builds).
Now, the CI override only applies when `enableGlobalVirtualStore` was
not explicitly set (i.e., is `null` or `undefined`). This preserves the
default behavior for ephemeral CI while respecting explicit user
configuration.
Also removes the `ci: false` workarounds from existing tests that
were documenting this limitation.
Co-authored-by: Zoltan Kochan <z@kochan.io>