Same failure mode as create-release-pr: the full install wires the husky
hooks and the push step runs the pre-push TS compile/lint on every daily
run. It only skips the Rust sweep when the remote chore/update-lockfile
branch already exists, because the shallow clone lacks the remote tip and
the hook's git log fails silently; on a fresh branch it does the full
cargo clippy/doc pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pre-push hook ran the full TS compile/lint and the Rust clippy/doc
sweep inside the workflow's "Commit and push" step, because the shallow
clone and URL-based push remote make the hook's change detection always
conclude that Rust sources changed. Setting HUSKY=0 on the job keeps
husky from wiring any hooks on the runner.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When deduplicating a missing peer onto the workspace's preferred
versions, hoistPeers matched candidates against '*' unless the wanted
range was an exact version. In a multi-importer workspace,
allPreferredVersions aggregates versions from every importer, so a peer
declared as ^1.0.0 inside one importer's closure could be auto-installed
as a 2.x resolved for another importer, silently producing a peer graph
that mixes incompatible majors. This survived pnpm/pnpm#12847 (which
fixed the single-importer case in resolvePeers) and is independent of
the pnpm/pnpm#12921 deadlock fix: the wrong version is chosen upstream
of peer resolution, during peer hoisting.
Both stacks now dedupe onto the highest preferred version that
satisfies the wanted range and fall back to resolving the range from
the registry when no candidate satisfies it. Non-semver specifiers
(workspace:, npm: aliases, dist-tags) keep the dedupe-to-highest
behavior.
Fixing this exposed a pacquet-only divergence the old behavior masked:
resolve_peers indexed a node's children into ParentRefs only by their
install alias, so an npm-alias child could never provide a peer under
its real package name below the importer level, and the binding fell
through to the importer-level hoisted entry. Descent-time children are
now indexed by both alias and real name, matching resolvePeers.ts.
Measured on the multi-importer reproduction: 46 poisoned snapshots on
pnpm 11.11.0, 0 with this change, in both the TypeScript CLI and
pacquet.
Additionally, hoistPeers now binds the same peer on a re-resolve with
an existing lockfile as on a fresh install of the same manifest: root
dependencies reused from the lockfile skip full resolution and were
invisible to the workspace-root branch of the hoist, so the picked
version depended on whether a lockfile was present. The root-dep table
falls back to the importer's wanted specifiers, and its entry type is
narrowed to HoistableRootDep — the fields hoistPeers reads, matching
the Rust port's WorkspaceRootDep.
Related to pnpm/pnpm#12847 and pnpm/pnpm#12921.
---------
Signed-off-by: C. Spencer Beggs <spencer@beggs.codes>
Co-authored-by: Zoltan Kochan <z@kochan.io>
The global virtual store lays slots out as
links/<scope>/<name>/<version>/<hash>, with a literal @ placeholder as the
scope segment of unscoped packages (see format_global_virtual_store_path).
global_virtual_store_root_from_slot walked the slot path upward without
that placeholder, so for the unscoped pnpm wrapper it landed on @ where it
expected links, gave up, and native_source_trust_root fell back to the
wrapper's own slot. The wrapper's @pnpm/exe.<target> platform package is a
symlink to a sibling slot under links, so linking the native binary then
failed with "the native pnpm binary ... resolves outside ...". This is
exactly the packageManager-delegation path: any delegation to a different
v12 version aborted, which broke the Update Lockfile workflow after its
updater bumped the pin mid-job.
Derive the slot's links-relative path with format_global_virtual_store_path
- the formatter that laid the slot out - instead of a hand-rolled walk, so
the reader and writer of the layout cannot drift again. Scoped wrappers
resolved to the links root before and still do, and a platform-package
symlink escaping links entirely is still rejected.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The dep path of a package with peer dependencies is computed after the
dep paths of its peers, and peer cycles are broken by collapsing the
suffix to name@version. That cycle detection only sees the children of
a single resolvePeersOfChildren call, and since hoisted peer providers
stopped being walked in the root context (commit fecfe8334b), a peer
cycle between a project's own dependency and a hoisted provider - or a
provider resolved at its true tree position - spans two calls, so the
dep path calculations awaited each other's promise forever and
pnpm install hung, e.g. with electron-builder@26.15.3.
Record which dep path promise each calculation awaits (borrowing the
cache owner's edges for peers-cache hits) and, after the traversal,
resolve every promise on a remaining await cycle to name@version - the
same collapse in-call cycle detection applies. Installs that resolved
before are untouched: the breaker only fires on await graphs that could
never settle.
pacquet's synchronous walker already detects these cycles globally and
needs no code change; mirrored tests in both stacks pin the same dep
paths, and the electron-builder repro now produces byte-identical
lockfiles across the two implementations.
Fixes https://github.com/pnpm/pnpm/issues/12921
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(cli): stop pacquet bugs tests from opening the developer's browser
The bugs happy-path integration tests spawned the real binary, whose
open_url launched xdg-open unconditionally, so every test run opened
https://github.com/test/pkg/issues (and friends) in the developer's
browser.
Thread the existing pacquet_network_web_auth::OpenUrl capability
through BugsArgs::run, replacing the hand-rolled per-OS launcher with
the shared Host provider (the open crate, as the web-auth flow already
uses). Port the happy-path coverage to unit tests driving run through a
recording OpenUrl fake — mirroring the mocked open module in the
TypeScript tests — and keep only the error paths, which never reach the
browser, as binary-level integration tests.
* style(cli): appease clippy pedantic in bugs tests
* test(cli): drain the recorded browser URLs between bugs tests
Tests that libtest runs on the same thread (--test-threads=1) would
otherwise observe URLs recorded by earlier tests through the shared
thread-local.
* **Bug Fixes**
* Improved lockfile generation for injected workspace dependencies: when the dependency alias matches the package name, lockfile entries now use a plain `file:<pkg>` reference instead of an alias-prefixed form.
* Preserved the previous alias-based lockfile behavior when the alias differs from the package name.
* **Tests**
* Added unit test coverage for both the matching-alias and differing-alias scenarios.
* Updated end-to-end assertions to reflect the new expected lockfile output for injected workspace dependencies.
Accept --if-present ahead of the script name, the way pnpm's option
parser does. The repo's own test-pkgs-branch script uses that spelling,
so every PR TS CI job died at argument parsing under the pacquet-based
pnpm v12. Declared top-level like the other run-scoped options but not
clap-global (run/stop/restart declare their own --if-present), merged
with the subcommand flags, validated like --resume-from/--no-bail.
Rejected for exec like pnpm, where dispatch would drop it silently.
Never scaffold a missing manifest inside a workspace. Gate State::init's
no-scaffold path on workspace membership (config.workspace_dir) rather
than a pnpm-workspace.yaml beside the missing manifest, so installs in
workspace member directories can't scaffold a package.json either —
pnpm never creates one anywhere in a workspace. Keep an empty object as
the in-memory stand-in instead of the init template: pnpm's add at a
rootless workspace root persists a manifest holding only the added
dependencies, and the template's failing test script would otherwise
ride along on any explicit save.
Together with pnpm/pnpm#12914 this unblocks the ci:test-branch job once
a pnpm v12 alpha carrying both is released and packageManager is bumped.
* **New Features**
* The CLI build and distributed archives are now `pnpm`, with updated dev commands and release artifacts.
* Shell completions are consistently branded as `pnpm` and generate `pnpm completion-server` wiring.
* **Bug Fixes**
* Version output/user-agent reporting now aligns with `pnpm`.
* Diagnostics and retry-related error codes are updated to `ERR_PNPM_*`.
* **Documentation**
* Updated E2E and integrated-benchmark guides to reference `pnpm` build outputs.
* SBOM metadata now identifies the tool as `pnpm`.
* **Chores**
* Updated CI E2E/benchmark workflows and test suites to run the `pnpm` binary.
Port `pnpm stage` (publish, list, view, approve, reject, download) to the
Rust CLI, mirroring the TypeScript command's flags, error codes, endpoints,
and output. `stage publish` reuses the publish pipeline (PublishArgs is
split into a reusable PublishFlags, and the pipeline returns summaries
instead of printing); approve/reject drive the shared OTP / web-auth flow;
download summarizes the tarball with the same traversal protections as
the TypeScript implementation. Also fixes the staged-publish route in
pacquet-publish to POST -/stage/package/:pkg (libnpmpublish's stage route)
instead of the regular packument PUT — previously unreachable because the
CLI hardcoded stage: false.
pnpr grows the server half: POST /-/stage/package/:pkg validates and
authorizes like a direct publish and holds the document under a UUID;
list/view/tarball inspect held records; approve replays the document
through the regular validate/stage/commit flow; reject deletes it. Records
persist under a reserved .staged/ namespace on the fs and S3 backends,
with stage-id validation ahead of any path or object key.
Shared plumbing: the capped response-body reader moves from the dist-tag
command into pacquet-network, and pacquet-pack exports its en-locale path
sort for the tarball summary.
Also fixes a broken test on main (semantic conflict between
pnpm/pnpm#12910 and pnpm/pnpm#12914): State::init no longer persists a
scaffolded root package.json when a pnpm-workspace.yaml sits next to the
missing manifest, so a verify-deps-before-run install can't turn the
workspace root into a selectable project with the init template's failing
test script.
The Release pnpm (Rust) workflow renamed the built binary to `pnpm` at the
repo root before tarring it. Since the pacquet/ directory was renamed to
pnpm/, that `mv` moved the binary *into* the existing `pnpm/` source
directory instead, so the non-Windows archives contained the whole Rust
source tree. The publish job then extracted a directory where it expected a
binary and failed with EISDIR in generate-packages.mjs
(https://github.com/pnpm/pnpm/actions/runs/29113833296).
Stage the binary in a scratch directory and tar from there, keeping the
archive layout (a plain `pnpm` binary at the root) unchanged.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Spelling was the only Rust CI gate with no local counterpart in the
pre-push hook, so typos surfaced for the first time in CI. Add a typos
block mirroring the "Rust CI / Spell Check" job (same dirs), with the
same skip-if-not-installed behavior as the dylint and taplo checks —
`just init` already installs typos-cli.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
pnpm (TypeScript): when pnpm was executed in a directory without a
package.json (most commonly through a mistyped command falling back to
`pnpm run`), checkDepsStatus found no workspace state and reported the
dependencies as outdated. With verify-deps-before-run set to "install"
(the default), that spawned a `pnpm install` which could only fail with
NO_PKG_MANIFEST, and the parent process crashed with a raw execa stack
trace. Report "unknown" (upToDate: undefined) instead when there is no
root manifest, no workspace, and no project list — runDepsStatusCheck
already skips the check in that case.
pacquet: port the verify-deps-before-run gate (config setting with the
'install' default, deps-status check for the run/exec paths, the
install/prompt/error/warn actions, and the script-env recursion guard),
including the same no-project skip. Also stop `pacquet install` from
scaffolding a package.json over an existing package.yaml manifest,
which the gate's default install action exposed.
Port git-diff project selection from @pnpm/workspace.projects-filter to
pacquet, so a --filter "...[origin/main]" selector selects the workspace
projects whose files changed since the given git ref instead of failing
with unsupported_diff_selector. This unblocks the ci:test-branch job,
whose "pn --filter=...[origin/main] -r list --depth=-1 --json" discovery
step fails on every PR with the pacquet-based pnpm exe.
The port mirrors upstream getChangedProjects:
- "git diff --name-only <since> -- <workspace_dir>" runs in the
selector's {dir} (else the workspace root); the repository root the
diff paths are relative to is the parent of the nearest .git
directory, falling back to the nearest .git file (worktrees).
- Each changed file maps to the nearest enclosing workspace project.
Files matching changedFilesIgnorePattern are ignored entirely;
projects whose changes all match testPattern are selected without
their dependents. Both pattern sets are matched with wax, the glob
crate pacquet already uses where upstream uses micromatch/fast-glob.
- A failing git diff surfaces git's stderr under pnpm's
ERR_PNPM_FILTER_CHANGED error code.
FilterWorkspaceProjectsOptions / FilterProjectsOptions gain
workspace_dir, test_pattern, and changed_files_ignore_pattern, threaded
from Config by the recursive commands and deploy. The testPattern /
changedFilesIgnorePattern settings are read from pnpm-workspace.yaml
and PNPM_CONFIG_* env vars, overridable by the global --test-pattern /
--changed-files-ignore-pattern flags, and excluded from the global
config.yaml - all matching the TypeScript CLI.
All upstream tests around the feature are ported: the three
projects-filter unit tests (from their known_failures stubs into
filter::tests::changed_packages), the two monorepo e2e tests
(testPattern respected by the test script; changedFilesIgnorePattern
respected incl. the empty CLI override), and the config-reader tests
for both settings. The recursive run/exec integration tests now assert
the selector scopes execution to the changed project. Verified against the
TypeScript CLI on this repository: both stacks select the identical
project set for ...[origin/main], with and without the two pattern
flags.
Pure directory move plus path fixups: the Rust port ships as pnpm v12,
so the source tree now lives at pnpm/ (alongside pnpm11/, the frozen
TypeScript line). No identifiers change in this pass — crate names
(pacquet-*), the pacquet bin, PACQUET_VERSION, the @pacquet/* npm
package names v11's runPacquet spawns, the .pacquet virtual-store dir,
the benchmark harness's clone dir, and the pacquet-*.yml workflow
filenames (npm trusted publishing is bound to them) all stay for a
follow-up.
Also removes the root /pnpm/ .gitignore entry (build detritus in the
pre-pnpm11 package location): pnpm/ is real source now and must not be
ignored. Developers with a stale generated pnpm/ dir should delete it
before checking out this change.
Fixespnpm/pnpm#12877.
The self-update command previously skipped work whenever the resolved target
version matched the currently running pnpm version. That breaks recovery from a
removed global install when a local project pnpm is used to reinstall the same
version globally.
Check the global self-update directory before taking the active-version no-op
path, and add coverage for both cases: already installed globally and missing
globally. Apply the same fix to pacquet's self-update, and deduplicate the
TypeScript global-install check into a shared findGlobalPnpmInstallDir helper.
Ports pnpm's login / adduser command (with 2FA) to the Rust pacquet
stack, reaching parity with the TypeScript @pnpm/auth.commands login.
The command probes web-based login (POST -/v1/login) first and falls
back to classic username/password/email login (PUT
-/user/org.couchdb.user:{name}) on HTTP 404/405. Either path satisfies
a two-factor (OTP) challenge through pacquet-network-web-auth — a
browser round-trip for web auth, or a prompted one-time password
otherwise — and the granted token is written to auth.ini, keyed to the
scope when --scope is given.
TypeScript's function-closure DI is reworked into pacquet's trait seam:
the OTP / web-auth effects reuse pacquet-network-web-auth's eight
self-less capability traits (composed on one Sys parameter), credential
prompts sit behind the new PromptInput / PromptPassword capabilities (the
raw dialoguer reads; the spawn_blocking handoff and error classification
stay in the prompt_line wrapper, outside the DI seam), auth.ini I/O
reuses logout's FsReadToString / FsWrite, and globalInfo maps to the
Reporter seam on the pnpm:global channel. The two registry requests go
over the shared ThrottledClient, tested against a mockito server; only
the effects a fixture can't stage portably sit behind the Sys seam.
Hardening beyond a literal port (both no-ops for normal input): the
success line redacts registry credentials / escape sequences (matching
logout and ping), and auth.ini keys and values are JSON-quoted exactly
as the ini package does, so a newline-bearing token cannot inject extra
auth entries, a registry path containing = still keys its token, and
every entry round-trips.
All 17 upstream login.test.ts cases are ported as unit tests
(TEST_PORTING.md updated).
The update-lockfile workflow still bumped the `@pnpm/pacquet` config
dependency, which has since been removed from the repo, so the daily run
would have re-added it. It also ran `pnpm self-update latest`, but the
repo is now on the pnpm v12 prereleases published under the `next-12`
dist-tag, so `latest` would have downgraded the `packageManager` and
`devEngines.packageManager` pins back to v11.
Drop the config-dependency bump and self-update from `next-12` instead,
updating the step names, commit message, and PR text accordingly.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The Tag workflow hardcoded the latest-11 dist-tag (each release branch
carried its own hardcoded major), so dispatching it from the wrong branch
applied the wrong line's tag: tagging 10.34.5 from main pointed latest-11
at a v10 release (https://github.com/pnpm/pnpm/issues/12906).
Derive latest-<major> from the version input instead, and fail fast when
the requested tag names a different major than the version, or when a
plain 'latest' tag would move backwards to an older major.
* ci(pacquet): draft a GitHub release with install-script assets
The Rust pnpm releases only published to npm, so get.pnpm.io's
install.sh / install.ps1 — which download pnpm-<platform>-<arch>[-musl]
archives from GitHub releases — could not install v12 at all.
Add a github-release job that repackages the build artifacts under the
v11 asset naming scheme (binary renamed from pacquet-<target> to pnpm /
pnpm.exe at the archive root), attests them, and drafts a GitHub
release. Draft keeps publication a deliberate maintainer action, same
as the staged npm wrappers; make_latest: false keeps a v12 prerelease
from taking the "Latest" badge from the stable v11 line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci(pacquet): ship the release archives under their final pnpm names
Rename the build-job archives from pacquet-<target> to pnpm-<target>
with a plain pnpm / pnpm.exe binary at the archive root — the layout
the v11 releases use and the install scripts expect. The github-release
job now uploads the build artifacts byte-for-byte as attested, instead
of repackaging (and re-attesting) them.
Since every archive now holds an identically-named binary, the publish
job extracts each into a scratch dir and moves the binary out under its
target-qualified name (this also drops the third-party unzip action).
The generated native package dirs keep the pacquet- prefix so they
stay out of the pacquet/npm/pnpm* wrapper glob.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Accept global and dotted store directory overrides with pnpm-compatible path and config precedence.
Expose overrides to updateConfig hooks and keep global virtual store derivation in sync. Preserve the raw empty CLI value for hooks while restoring the platform's volume-aware default store for the effective configuration.
Cover workspace-relative paths, quoted home paths, frozen installs, global virtual stores, hook replacement, and empty overrides.
Related to pnpm/pnpm#11633
`pnpm outdated` now skips dependencies whose lockfile ref is local (`link:`, `file:`, or `workspace:`), even when the manifest specifier is a plain semver range.
This covers workspace-linked packages such as:
```yaml
devDependencies:
private-workspace-pkg:
specifier: ^1.0.0
version: link:../private-workspace-pkg
```
In that shape the dependency is already resolved locally, so asking the registry for a latest version can fail for private workspace packages that are not published. Refs pnpm/pnpm#12827.
Follow-up commits after review:
- The local-ref early return runs before any other per-dependency work, and the `isLocalRef` helper documents why local refs have no registry "latest".
- Added a test locking in the other side of the behavior: a dependency whose *current* ref is local but whose *wanted* ref is a registry version is still checked and reported (addresses CodeRabbit's suggestion).
- **pacquet parity:** pacquet's `outdated` already skips local refs — `ImporterDepVersion::ver_peer()` returns `None` for `link:`/`file:`, so `collect_outdated` drops those deps before any registry fetch. This PR adds a Rust test (`current_versions_omit_local_refs`) proving that behavior, so both stacks now cover the same scenario. No pacquet behavior change was needed; the TypeScript fix brings pnpm in line with pacquet.
---------
Co-authored-by: Zoltan Kochan <zoltankochan@gmail.com>
Support custom fetchers from pnpmfiles in pacquet, with delegate-envelope parity in the TypeScript CLI (related to pnpm/pnpm#11685)
Pacquet already supported custom resolvers via its Node.js worker IPC but
lacked the fetcher counterpart. This extends the same protocol pattern:
- Define a CustomFetcher trait and get_custom_fetchers() on PnpmfileHooks
- Add fetchers/fetcher target dispatch to the worker's NDJSON protocol
and JS runner, mirroring the existing resolver path; the hook is
invoked with the TS-parity args fetch(cafs, resolution, opts, fetchers)
(cafs/fetchers are null over IPC)
- Implement NodeJsCustomFetcher bridging the trait to the worker
- Create CustomFetcherPicker for consulting fetchers in declared order
- Consult custom fetchers before the built-in dispatch on both the
frozen-lockfile and fresh-lockfile install paths; hook-load failures
abort the install (PNPMFILE_FAIL)
- Support delegation: { delegate: <resolution> } rewrites the resolution
for the standard tarball/git path; non-delegate responses and
custom-typed delegates fail the install
- lockfile: add LockfileResolution::Custom preserving custom-typed
resolution objects verbatim, so custom resolvers/fetchers can round-trip
them; unclaimed custom-typed resolutions fail with the TS-parity
UNSUPPORTED_RESOLUTION_TYPE error
- TypeScript: pickFetcher now accepts the { delegate: <resolution> }
envelope from custom fetchers (the portable delegation form), and
hooks.types exports CustomFetcherDelegation
Full CAS fetch in pacquet (where a fetcher produces file content directly
rather than delegating) requires a streaming protocol extension
(separate follow-up).
* **New Features**
* Installations now generate and preserve `file:` injected workspace dependency mappings and carry them through `.modules.yaml`.
* Manifest `link:` dependencies are now materialized as symlinks in project `modulesDir` (supports absolute, relative, and `link:.`, plus safe `modulesDir` validation).
* Workspace components missing `package.json` now link to all other sibling root members.
* **Bug Fixes**
* Empty/whitespace “no range” specifiers no longer break dependency resolution.
* Peer-suffixed metadata is now resolved via peer-stripped lookups when needed.
* Hoisted workspace importers are retained even when workspace-hoisting is disabled.
* Trusted importer IDs prevent incorrect unsafe-path rejection during isolated symlinking.
pnpm adds the workspace root's node_modules/.bin to PATH for scripts and
exec in every workspace project (extraBinPaths in the config reader), so
root-level dev tools like tsgo are callable from member packages. pacquet
had the extra_bin_paths plumbing through run/exec/dlx/pack/publish, but
nothing ever populated it, so member scripts failed with 'command not
found' for workspace-root binaries.
Populate extra_bin_paths in Config::current from the discovered workspace
dir, mirroring pnpm's config reader.
The gap survived because the behavior-defining upstream tests were never
ported: pnpm/test/recursive/run.ts 'pnpm recursive run finds bins from
the root of the workspace' and the config reader's 'extraBinPaths' test.
Port them — regression tests for root-bin resolution in plain and
filtered-recursive run, the bin-priority rule (a project's own
node_modules/.bin outranks the workspace root's), and the config-level
population — and record them in plans/TEST_PORTING.md.
Also scope the typos invocation in the just ready recipe to pacquet and
pnpr, matching CI's spell-check job; the bare invocation failed on
generated files under pnpm11 that cspell covers instead.
The git-resolver unit tests hit live github.com by default: the mocks for
fetchWithDispatcher and graceful-git existed, but beforeEach restored the
real implementations. When GitHub throttles the shared CI runner IPs, the
HEAD probe in isRepoPublic() fails (it has zero retries and treats any
error as "private"), and resolution silently degrades from the hosted
tarball to a git clone, changing the resolved id and failing the
assertions. This broke the main branch build at
https://github.com/pnpm/pnpm/actions/runs/29026897310/job/86153736091
The mocks are now the default: fetch reports every repository as public
and graceful-git serves ls-remote output from a fixture table captured
from the real repositories, with the same commit hashes the assertions
already expected. The private-repo-over-HTTPS test now calls
mockFetchAsPrivate() explicitly instead of relying on a real 404 for the
nonexistent github.com/foo/bar. The one live-network case in
parsePref.test.ts got the same treatment. The suite drops from ~40s to
under half a second and runs offline.
The dlx e2e test stays a genuine end-to-end test against GitHub, but its
allowBuild list now approves both resolution shapes of the same commit
(codeload tarball and git+https clone), so the resolver's
rate-limit-induced fallback no longer trips the
GIT_DEP_PREPARE_NOT_ALLOWED gate, as seen in
https://github.com/pnpm/pnpm/actions/runs/29029971938/job/86170695840
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
With autoInstallPeers, peers resolved inside a dependency subtree are
attached to the root importer's direct dependencies so other subtrees
can reuse them. Those entries alias tree nodes that already have a
position deep in the graph, and the peer-resolution pass walked them a
second time as root children. The two walks raced on the shared
per-node dep-path state, so a package inside a self-contained closure
could get its peers bound to the root project's incompatible version
of a peer instead of the provider next to it in the tree, producing a
lockfile that mixes both versions and a peer mismatch at run time.
The peer-resolution pass now keeps the attached providers visible as
root-level peer providers but resolves their own peers only at their
true tree position, falling back to the root context only when that
position was pruned by the peers cache. All pruned providers are
resolved in a single fallback pass, because a pass only detects peer
cycles among its own children and mutually peer-depending providers
would otherwise await each other's dep path forever. The fix lands in both stacks:
the TypeScript `@pnpm/installing.deps-resolver` and pacquet's
`resolving-deps-resolver` crate.
Fixes https://github.com/pnpm/pnpm/issues/4993
---------
Signed-off-by: C. Spencer Beggs <spencer@beggs.codes>
Co-authored-by: Zoltan Kochan <zoltankochan@gmail.com>
Register the pn short alias in generated shell completion scripts.
pnpm exposes pn as a binary alias, but completion generation only registered
pnpm with the supported shells. This meant zsh completions generated by
pnpm completion zsh registered pnpm only, so pn did not receive the same
completion function.
Post-process the tabtab-generated scripts to register pn alongside pnpm for
bash, fish, pwsh, and zsh, and cover each shell in the completion generator
tests.
Fixespnpm/pnpm#11955.
---------
Co-authored-by: ychampion <ychampion@users.noreply.github.com>
Co-authored-by: Zoltan Kochan <zoltankochan@gmail.com>
When re-resolution of an optional dependency fails (for example, a
registry mirror whose packument has not synced the pinned version yet),
the resolver silently skipped the dependency. The parent's snapshot was
then rewritten without the edge and the pruner erased the locked
entries, so identical inputs produced different lockfiles depending on
which machine ran the install, and a frozen install on another host had
no entry to link for the affected package.
Rethrow the resolution error instead of skipping when the wanted
lockfile already holds an entry that satisfies the wanted range. An
optional dependency that never resolved keeps the skip-on-failure
behavior.
pacquet previously failed loudly on every optional-dependency
resolution failure. It now skips never-locked optional deps like pnpm,
emitting the same skipped-optional-dependency log (with the parents
chain and the same top-level reporter output), and keeps the loud
failure — with the same hint — when the lockfile holds a satisfying
entry, so the two stacks agree on both cases.
Closespnpm/pnpm#12853
---------
Co-authored-by: Zoltan Kochan <zoltankochan@gmail.com>
The npm-resolver memoizes its metadata fetch for the whole resolution phase
and clears it only once, via clearResolutionCache(), after resolution ends.
Each memoized FetchMetadataResult carried jsonText, the raw registry response
body kept only to mirror the response to disk without re-serializing meta.
With an unbounded memo cache living for the entire phase, every raw body
stayed resident until the end. On large cold-cache graphs that fetch full
metadata (minimumReleaseAge / trustPolicy) these bodies reached hundreds of
MB and OOM-killed 3GB CI runners. v10 did not keep the raw text on the
memoized result, so this was a v10-to-v11 regression.
Enforce the invariant at the cache boundary: the memoized fetch
(memoizeFetchMetadata, replacing p-memoize, which has no hook between compute
and store) caches a body-less shallow clone of each result, so jsonText
reaches only the caller that initiated the fetch — the one that writes the
disk mirror — and dies with that call. The cache is structurally incapable of
pinning a body regardless of who consumes the fetch. Peak RSS drops by ~30%
(back to the v10 level) with a byte-identical lockfile. Cache-hit callers see
jsonText undefined and fall back to JSON.stringify(meta) in
prepareJsonForDisk, which is functionally equivalent because loadMeta
re-derives etag from the headers line on read. The replacement preserves
p-memoize's semantics: in-flight dedup, eviction of rejections, and cache
clearing. Follows the projection applied to the verifier caches in
pnpm/pnpm#11878.
Closespnpm/pnpm#12868.
Allow git-hosted build approvals to match the package/repository portion of a git depPath before the resolved commit hash.
This lets trusted git repositories continue running build scripts after branch updates without adding a new hash-qualified `allowBuilds` entry for every resolved commit. Exact depPath rules still work, disallow rules still win first, and package-name-only allow rules remain limited to trusted registry-style depPaths so git artifacts are not approved by name alone.
Fixespnpm/pnpm#12367.
---------
Co-authored-by: ychampion <ychampion@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Implement the search / s / se / find command in pacquet. The command resolves the registry URL from config or CLI overrides, executes a GET request against the registry's search endpoint with the search query and limit, and pretty-prints the formatted output. Includes support for `--json` format matching the TypeScript output. Also includes a comprehensive integration test suite.
Related to pnpm/pnpm#11633
Allow global config.yaml to carry registries and namedRegistries.
pnpm already parses those fields from YAML settings, but the global config
allowlist filtered them out and pnpm config set --global rejected them. That
leaves global installs unable to resolve named registry aliases.
Add registries/named-registries to the global YAML key allowlist, preserve
global YAML registries through the registry merge path (below _auth and CLI),
and make pnpm config set --global accept them. Mirror the same fix in pacquet
so both stacks stay behaviorally identical.
Add config set, reader, precedence, and security-regression coverage — the
latter asserting a global registry alias cannot rebind an _auth token's host.
Fixespnpm/pnpm#12858.
Contain crafted-lockfile and malicious-manifest path traversal across both
stacks. Three advisories:
- GHSA-c59q-g84q-2gj5 (TS): lockfile depPath name -> isolated linker + PnP.
- GHSA-vq4v-j7r6-jq4m (TS + pacquet): dependency manifest `name` from the
fetched tarball (e.g. @x/../../../path) -> resolver/linker.
- GHSA-2rx9-3g3h-c2jv (pacquet): lockfile alias / snapshot key name, and the
global-virtual-store version segment; also bypassable under trustLockfile.
TS: apply safeJoinModulesDir at lockfileToDepGraph and the PnP
packageLocation; validate manifest names in deps-resolver / resolvePeers /
env-installer; contain the global-virtual-store slot dir against its store
root in iteratePkgsForVirtualStore so a traversal version segment is rejected.
pacquet: add verify_lockfile_dependency_names (offline check over importer
aliases, snapshot package names, and snapshot dependency aliases) run
unconditionally in InstallFrozenLockfile::run so trustLockfile cannot bypass
it; add validate_virtual_store_slot_containment for the GVS version escape;
guard the sink joins (create_symlink_layout, create_virtual_dir_by_snapshot,
hoist, install_package_from_registry) and the resolver with
safe_join_modules_dir.
All rejections surface ERR_PNPM_INVALID_DEPENDENCY_NAME.
* docs(contributing): document Rust toolchain and git-hook tooling
Rust is now the primary language in this repository, but the root
CONTRIBUTING.md only covered the TypeScript setup. Add a "Rust toolchain
and git hooks" section under "Setting Up the Environment" that covers
rustup and the pinned toolchain, just, the just init tools, and the
dylint tools.
Two things that are easy to get wrong and cost real debugging time:
- cargo-dylint and dylint-link must be installed from source, not with
cargo binstall. The prebuilt binaries reference the dylint_driver
crate at the path where they were built, so building the per-toolchain
driver fails locally with an error pointing at a nonexistent
.../dylint/driver directory.
- ~/.cargo/bin must be on PATH (ahead of any system Rust in /usr/bin),
because the pre-push hook locates its tools through PATH and silently
skips a Rust check when the tool is missing rather than failing, so a
push that looks clean locally can still fail format, doc, or dylint in
CI.
Point pacquet/CONTRIBUTING.md at the new root section instead of
repeating the tool list, and correct its cargo binstall advice for the
dylint tools.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: correct dylint install hint in justfile and pre-push hook
The `just dylint` recipe comment and the pre-push hook's skip message
both told contributors to install cargo-dylint via `cargo binstall`,
which produces a prebuilt binary that fails to build the per-toolchain
driver locally. Point both at `cargo install` from source, matching the
CONTRIBUTING.md guidance.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Project-level pnpm-workspace.yaml is repository-controlled and therefore
untrusted, so getOptionsFromPnpmSettings refuses to expand ${...}
environment-variable placeholders in request-destination fields. The guarded
set only covered registry, namedRegistries, and pnprServer; the httpProxy,
httpsProxy, and noProxy fields were missing, so a malicious repository could
route requests through an attacker-controlled proxy whose URL embedded a
secret and exfiltrate install-time secrets before any lifecycle script runs.
Add httpProxy, httpsProxy, and noProxy to REQUEST_DESTINATION_SCALAR_KEYS so
they receive the same protection already applied to the registry channel, and
extend the workspace-yaml request-destination test to cover them.
Reported by YESHYUNGSEOK as flaw F01 of GHSA-vx52-2968-3vc6.
* fix(pacquet): resolve non-npm specifiers in the napi `resolveDependency`
`resolveDependency` in the `@pnpm/napi` binding built a bare `NpmResolver`,
so any non-npm specifier (a folder path, git URL, `file:` / `link:` /
`workspace:` spec, `http(s)` tarball URL, or `<alias>:` named-registry
spec) errored with "the specifier was not claimed by the npm resolver".
Bit's `resolveRemoteVersion` passes exactly these shapes and expects a
manifest back.
Mirror the install path's `DefaultResolver` chain
(`install_with_fresh_lockfile.rs`): npm -> git -> tarball -> localScheme
-> node -> deno -> bun -> namedRegistry -> localPath. The single-resolve
path runs the tarball resolver without a fetch context (there is no
install store to extract into), and omits the pnpmfile custom resolvers
(an install-time concern); everything else matches the install chain, so
a folder path resolves to its manifest, a `file:` / `link:` spec resolves
to its directory, a git URL pins a commit, and a normal npm spec still
resolves as before. An unclaimed spec now surfaces the chain's
`SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER` diagnostic.
Adds offline unit tests covering the `file:` / `link:` directory cases and
the no-resolver-claimed error.
* feat(pacquet): support root-component member reachability for hoistingLimits: workspaces
Bit's root-components install writes one importer per root under
node_modules/.bit_roots/<id> whose manifest injects the root's sibling
components as workspace deps and carries
installConfig: { hoistingLimits: "workspaces" }. Under nodeLinker:
isolated those injected members are materialized as file: deps, each in
its own global-virtual-store slot whose node_modules only holds that
member's own deps. A realpath-based resolution (Node default) that lands
in a member's slot never reaches a sibling, so require of a sibling
component throws MODULE_NOT_FOUND even though peer resolution is correct.
Parse installConfig.hoistingLimits from the manifest and, after the
isolated symlink pass, cross-link every one of a root's injected members
into every other member's slot node_modules so the upward walk from any
member finds its siblings. The pass is gated on hoistingLimits:
"workspaces" (a shape no non-Bit install produces) and is purely
additive, so ordinary installs are untouched. Because each root's peer
context is folded into its members' slot hashes, two roots get distinct
member slots and cross-linking one root can never leak a wrong sibling
into another. Wired into both the fresh and frozen isolated paths.
* fix(pacquet): honor offline in deno/bun resolvers and dedupe ArcResolver
Addresses review feedback on the napi resolver-chain PR.
- The resolve chain wired NodeResolver's offline flag but left the deno
and bun runtime resolvers fetching GitHub release metadata
unconditionally, so an offline resolve could still reach the network
for their `runtime:` specifiers. Add an `offline` field (with matching
NO_OFFLINE_DENO_RESOLUTION / NO_OFFLINE_BUN_RESOLUTION errors) to both
resolvers, short-circuit before any fetch, and set it in both the napi
and fresh-install paths so the node/deno/bun trio all fail fast offline.
- Replace the two duplicated ArcResolver adapters (napi and
package-manager) with a single `impl Resolver for Arc<dyn Resolver>` in
resolving-resolver-base.
- Add offline unit tests for the deno/bun runtime specifiers and the
pre-assertion logging the style guide requires.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pacquet): treat non-reparse-point paths as non-junctions on Windows
`is_symlink_or_junction` propagated `junction::exists`'s
ERROR_NOT_A_REPARSE_POINT (4390) for a plain directory instead of
returning `false`, so the `injected_members_are_cross_linked` test
panicked on Windows where it asserts a member's own package dir is not
a symlink. Check `Path::is_symlink` first, then map the
not-a-reparse-point error from `junction::exists` back to `false`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: reject offline resolution in the deno and bun runtime resolvers
The deno and bun runtime resolvers accepted an `offline` flag but
ignored it, reaching out to GitHub for release assets even when offline.
Fail fast with NO_OFFLINE_DENO_RESOLUTION / NO_OFFLINE_BUN_RESOLUTION
instead, matching the Node.js runtime resolver and the pacquet engine.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: drop deno/bun runtime-resolver offline gating
Reverts the deno/bun offline fail-fast added earlier in this PR. It
diverged from the tested behavior: the e2e tests
test/install/denoRuntime.ts and test/install/bunRuntime.ts assert that
an offline runtime install fails downstream (the "in package mirror"
error), and the runtime resolvers intentionally do not short-circuit on
`offline` — only the node resolver does, because it reaches the mirror
at resolve time. The ArcResolver dedup and test-logging changes are
kept.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(pacquet): scope root-component linking to declared siblings
Replace the all-to-all cross-link of a root component's injected members
with pnpm's per-package child linking: each member's slot `node_modules/`
gains symlinks only to the siblings it declares in its own manifest, so a
transitive sibling resolves through the chain (comp3 -> comp2 -> comp1)
instead of a clique. This mirrors how pnpm's isolated linker wires an
injected package's own children and drops the "broader than the declared
dependency set" caveat.
The member's manifest is the source of truth — it survives Bit's
`excludeLinksFromLockfile` / `dedupeInjectedDeps`, where the lockfile
edges do not — so the function signature and both install call sites are
unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(pacquet): add trailing commas to multi-line asserts
Satisfies the `perfectionist::macro-trailing-comma` dylint lint on the
three multi-line `assert!` invocations in the root-component tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement the pnpm access command in both the TypeScript CLI (pnpm11/registry-access/commands/src/access.ts) and the Rust pacquet port (pacquet/crates/cli/src/cli_args/access.rs). The command follows npm's npm access paradigm and supports: list packages (users, orgs, teams, and per-package collaborators), get status (package public/restricted access status), set status (change package visibility with restricted/private mapped to restricted), set mfa (configure two-factor authentication requirements per package with none/publish/automation levels), grant (grant read-only or read-write team access to scoped packages), revoke (revoke team access from scoped packages). Both implementations include auth header and OTP support, JSON output mode, proper error handling with pnpm error codes, and comprehensive unit tests. The TypeScript tests use mock agent fixtures and the Rust tests cover parameter validation and error paths.
The pkg command (pnpm/pnpm#12795) added a CliCommand::Pkg variant but
did not extend the exhaustive match in command_name, breaking every Rust
CI job with a non-exhaustive-patterns compile error.
Support the recursive project-listing path used by the pnpm repo CI test chunker.
Pacquet's `list` depth parser already treated `-1` as project-only mode, but clap rejected `--depth -1` before the parser saw it. In addition, `dispatch_query::list` ignored the recursive flag, so `pn -r list --depth=-1 --json` returned only the workspace root. The chunker then found no `.test` scripts and skipped every Windows chunk.
Allow hyphen-prefixed depth values, pass the recursive flag into `list` and `ll`, and render selected workspace projects for recursive project-only listings. This matches TypeScript pnpm for `pn -r list --depth -1 --json` and keeps filtered recursive selection on the shared workspace filtering path.
Sanitize root package names in tree-mode labels before printing dependency-free roots, matching pacquet's existing terminal-output sanitizer for other list labels. The TypeScript list helper still prints raw names and can be aligned in a separate TS changeset if maintainers want exact parity for that hardening.
Also map the current `pkg` command in the version-switch command-name helper so the merge commit remains exhaustive under Dylint.
Adds parser, integration, and renderer regressions for the CI command shape, package-param project-only parity, and root-label sanitization.
Related to pnpm/pnpm#12811.
Port the pkg command from the TypeScript CLI to the Rust pacquet CLI.
The `pnpm pkg get` subcommand retrieves values from package.json using
property-path syntax (dot and bracket notation). With no keys it returns the
full manifest; with one key it returns the raw string value or a JSON-encoded
value; with multiple keys it returns a JSON object of the selected keys.
The `pnpm pkg set` subcommand writes values to package.json using
key=value syntax. The --json flag parses the value as JSON. Intermediate
objects are created automatically for dot-separated paths. Unsafe keys
(__proto__, constructor, prototype) are rejected.
The `pnpm pkg delete` subcommand removes keys from package.json using
property-path syntax. Array indices splice the element rather than leaving
a hole.
The `pnpm pkg fix` subcommand auto-corrects common errors in package.json:
it removes name/version fields with non-string types, dependency/script fields
with non-object types, and bin fields that are neither a string nor an object.
Pacquet's script dispatch still assumed a project manifest was always package.json. The pnpm/pnpm v12 CI update runs pnpm --dir=pnpm11/__fixtures__ prepareFixtures, and that fixture declares its scripts in package.yaml. The TypeScript CLI reads package.yaml through the project manifest reader, so pacquet fell through to exec and reported the command missing.
Add read-only package.yaml support to pacquet's shared project-manifest reader, teach workspace project discovery to match both package.json and package.yaml while keeping package.json precedence, and route script execution through that reader. This makes shorthand script fallback, run, test, start, and PNPM_PACKAGE_NAME stamping behave consistently for YAML manifests.
The next CI step also invokes the shorthand recursive script form as pn --no-sort --workspace-concurrency=1 -r --report-summary .test. Parse these recursive script options at the top level so fallback commands receive them, apply workspace-concurrency through the shared config, and implement --no-sort ordering for recursive run and exec by preserving workspace order. Validate the shared parser so unrelated commands still reject script-only flags, while test/start/stop accept the run-compatible recursive flags and publish keeps --report-summary for filtered publish summaries.
Add `linkWorkspacePackages` (`true` / `false` / `"deep"`) to the `@pnpm/napi`
install options, mapping it onto `Config::link_workspace_packages` (reusing the
config crate's `Deserialize`). Programmatic consumers need this to opt
bare-semver dependencies — including an auto-installed peer that names a sibling
workspace package — into workspace-package resolution, matching pnpm's
`link-workspace-packages` setting. The default stays `false`, so existing
behavior is unchanged.
Treat pnpm and `@pnpm/exe` as equivalent replacement aliases during isolated global installs.
The standalone installer for pnpm v11 installs the downloaded binary as a local `@pnpm/exe` package, while pnpm v12 records the global CLI as `pnpm`. Before this change, the global bin conflict check saw both packages as distinct owners of the `pnpm` bin and failed before the existing install could be removed.
Expand the replacement alias set only for the pnpm-managed package pair before conflict checking and removal. The install still hashes and records the actual resolved aliases, and unrelated bin conflicts continue to fail.
Mirror the same behavior in pacquet.
Two pnpm self-update fixes:
1. Honor trustPolicy=no-downgrade when resolving the pnpm engine. Both
stacks now resolve the engine version like a regular install — deriving
the metadata mode from config (full metadata under time-based /
no-downgrade) and passing the trust policy into the resolve — instead of
a self-update-specific abbreviated-metadata path. Fixes pacquet's "trust
check failed: missing time" crash and the TypeScript CLI silently not
enforcing the policy. pacquet centralizes the metadata decision in
Config::requires_full_metadata_for_resolution, shared with PickPolicy.
2. (pacquet) Reinstall pnpm when a cached engine slot's wrapper resolves
outside its slot (older global-virtual-store layout). The cache-hit
native-binary relink is now best-effort: on the containment guard's
refusal, fall through to a fresh signature-verified install rather than
aborting self-update. The guard itself is unchanged.