- Keep the pre-run check read-only: pnpm's run path never writes
pnpm-lock.yaml (only the install command restores it from the current
lockfile), so the gate now uses a read-only stand-in check instead of
regenerate_wanted_lockfile_if_missing, and a missing lockfile without
a stand-in reports "Cannot find a lockfile" like pnpm instead of
passing silently.
- Match pnpm's env-var semantics for unrecognized values: pnpm assigns
pnpm_config_verify_deps_before_run verbatim without validation, so a
present-but-invalid value is truthy there (check runs, no action
matches). Parse failures now map to VerifyDepsBeforeRun::True instead
of being dropped, which also keeps a present env var overriding the
CLI consistently.
- Report "cannot check" straight from the missing workspace state,
before the workspace-projects walk, sparing fresh projects the
discovery cost on every run/exec.
- Share the recursion-guard env-var name through a constant, cover the
exec-path stamp with a test, and drop a test doc comment that only
restated the assertions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Assert the manifest side-effect too (an install in a manifest-less
directory scaffolds a package.json before doing anything else), log
stderr before the assertions, describe the current contract in the doc
comment without the historical narrative, and tighten the changeset
wording.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port pnpm's verify-deps-before-run from the TypeScript CLI (the setting
was in the NOT_PORTED parity list):
- VerifyDepsBeforeRun config setting with pnpm's 'install' default,
read from pnpm-workspace.yaml, the global config.yaml, the
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN / pnpm_config_verify_deps_before_run
env vars, and --config.verify-deps-before-run; the parity test now
asserts the default against pnpm's source literal.
- check_deps_status_before_run in pacquet-package-manager: a second
consumer of the deps-status machinery behind the install fast path,
with the run gate's differences - it runs regardless of
optimisticRepeatInstall, never treats local file dependencies as
outdated, ignores dev/optional/production drift (scripts always run
with the default groups), compares configuration dependencies, and
reports drift with pnpm's user-facing issue wording. When the target
directory has no manifest and no workspace it skips the check
entirely, mirroring the checkDepsStatus guard fixed on the TypeScript
side in this PR, so a mistyped command outside a project never spawns
an install that can only crash with NO_PKG_MANIFEST.
- Gate wiring in run, exec, their recursive paths, and the top-level
fallback, with the install / prompt / error / warn actions
(ERR_PNPM_VERIFY_DEPS_BEFORE_RUN, the non-interactive prompt error,
and --production/--dev/--no-optional install args derived from the
recorded workspace state).
- Recursion guard: every spawned script gets
pnpm_config_verify_deps_before_run=false, and that env var outranks
every other config source for this key, matching pnpm's config-reader
priority (pnpm/pnpm#10060).
Known divergences, noted for follow-up: deep lockfile-content
mismatches reuse pacquet's existing diagnostic strings instead of
pnpm's exact issue wording, and a conflicted lockfile surfaces as a
parse failure instead of the dedicated merge-conflict message. Both
affect only the text shown by the warn/error actions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
State::init unconditionally ran PackageManifest::create_if_needed on
<dir>/package.json, so an install in a project whose manifest is
package.yaml wrote a default package.json next to it — which then
shadowed the real manifest for every later read (script lookup,
importer checks). Load an existing alternate manifest base name first
and scaffold package.json only when no manifest exists at all, matching
pnpm's manifest resolution.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pacquet has not ported the verify-deps-before-run gate, so it never had
the TypeScript bug fixed in the previous commit — but per the parity
convention the scenario gets a matching Rust test: a mistyped top-level
command falling back to run in a directory without a manifest must fail
with its own missing-command error and never attempt an install, even
with verify-deps-before-run=install set.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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", 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, letting the command fail with its own
clear error.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
The 'Release pnpm (Rust)' workflow failed on the musl legs with
"cannot produce cdylib for pacquet-napi ... as the target
x86_64-unknown-linux-musl does not support these crate types". musl
targets link the CRT statically by default (crt-static), which is
incompatible with the cdylib crate type used by the .node addon.
Pass '-C target-feature=-crt-static' when building the addon, but only
for musl targets, so it links dynamically against musl libc at load
time - matching how napi-rs builds its musl prebuilds. The CLI binary
is built in a separate step and keeps crt-static, so it stays fully
static for portability across musl distros.
Add `pacquet-napi`, a napi-rs cdylib crate, plus its `@pnpm/napi`
npm wrapper, exposing pacquet's programmatic engine surface to Node.js so
programmatic pnpm consumers can drive the Rust engine instead of the
TypeScript pnpm packages. Bit is the reference consumer.
Exports: install (in-memory importers, single and multi-importer workspaces,
a synchronous readPackage hook per resolved dependency manifest, build-script
approval, and depsRequiringBuild), rebuild, resolveDependency, pack,
parseBareSpecifier, engineVersion, and auth via authHeaderByUri.
The install runs on a dedicated 32 MiB-stack worker thread with its own
tokio runtime; the napi async fn awaits the result over a oneshot channel,
so pacquet's borrowed State never crosses the FFI boundary. A reporter
bridge forwards the engine's wire-compatible log events to a JS callback,
and errors carry pnpm's ERR_PNPM_* code and hint through a structured envelope.
Engine-core changes are minimal and inert for existing callers:
PackageManifest::from_value, and two Option fields on Install
(pnpmfile_hook_override and workspace_projects_override) defaulted to None
everywhere. A new napi-release profile sets panic = "unwind" since the
workspace release profile uses panic = "abort".
getPeerDependencyIssues is stubbed pending pacquet's own peer-issue renderer.
Distribution follows the @pnpm/exe.* model via scripts/generate-packages.mjs.
Runtime archives (Node.js / Bun / Deno) ship no package.json, so pacquet
synthesizes one carrying name, version, and bin. That synthesized manifest was
only added to the in-memory cas_paths in fetch_binary_resolution_to_cas, after
the download had already queued the store-index row, so the persisted
PackageFilesIndex recorded no package.json and no bundled manifest.
A cold fetch masks this — the current install's slot still gets the manifest
from cas_paths. But a warm install materializes the runtime straight from the
store-index row (via the warm-batch prefetch, which never calls
fetch_binary_resolution_to_cas), so the slot lands without a package.json and
the warm-batch bin linker finds no bin. That is the CI failure on
pnpm/pnpm#12811: the root install cold-fetches node@runtime:26.4.0 and the
later pnpm dlx node@runtime:26.4.0 warm-reads the manifest-less row and dies in
getBinName with dlx_read_manifest.
Thread the synthesized manifest into the two binary download paths as
append_manifest, mirroring pnpm's appendManifest: fold it into the extracted
output's cas_paths, the row's files map, and its bundled manifest before the
row is persisted, so warm and cold installs land the same slot. This brings
pacquet in line with the TypeScript stack, which already bakes appendManifest
into the files index via addManifestToCafs.
Add the `publish` command to pacquet — the Rust port's first `releasing`
command — bringing it to parity with `pnpm publish`.
It implements single-package and tarball publish (pack -> build the npm publish
document -> PUT /:pkg, with private / unscoped-restricted validation and semver
cleaning); recursive / --filter workspace publish (dependency-ordered, skipping
private / unnamed / already-published packages via concurrent registry probes,
with --report-summary and --json, and the filter-without--r recursive
promotion); OIDC trusted publishing with keyless sigstore SLSA provenance
(Fulcio + Rekor); OTP / web-auth; git pre-publish checks (git-checks config and
--no-git-checks); publish-lifecycle scripts; and the --access / --tag /
--dry-run / --force / --ignore-scripts / --skip-manifest-obfuscation /
--publish-branch flags. --batch is accepted for surface parity but errors, as it
is not yet ported.
The external-service side effects — OIDC HTTP, the clock, CI-provider
detection, subprocess spawns, and the sigstore signing step — are each behind a
self-less capability trait on a Host provider, threaded as a Sys generic, so the
flows are unit-testable offline with fakes. The web-auth OTP fakes live in a
dedicated pacquet-network-web-auth-testing crate and are expanded per test by a
web_auth_fake!() macro. The `pacquet publish` binary is additionally covered by
integration tests that drive it against a mockito registry.
Related to pnpm/pnpm#11633. A live end-to-end publish harness (a pnpr instance
implementing OIDC / OTP / web-auth / provenance) is tracked by pnpm/pnpm#12738,
and real sigstore signing by pnpm/pnpm#12739.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Filter pacquet's default reporter summary inputs by the active prefix for non-global installs, matching the TypeScript reporter behavior. Pacquet was folding every importer into the root summary, so workspace installs printed child importer dependencies in the root summary.
Keep summaries unfiltered for commands whose install events intentionally come from another prefix: global add/remove/update/runtime and the cache-backed dlx/create flows.
Preserve package-manifest diff summaries by tracking manifest snapshots per prefix, emitting the pre-mutation manifest for add/remove/update, and rendering a later non-empty manifest diff when lockfile-only flows emit updated manifests after the install summary marker. Keep the public reporter stream to one initial package-manifest event and one install-closing `pnpm:summary` event, and normalize equivalent prefix paths lexically so relative or trailing-separator variants are not dropped.
Add focused reporter tests plus CLI regressions for workspace root summaries, dlx/create append-only summaries, workspace subdirectory add --lockfile-only, single-initial/single-summary NDJSON output, and normalized prefix matching.
`@pnpm/cli.default-reporter`'s `reportSkippedOptionalDependencies` only
surfaces a skipped optional dependency on the console when it is a direct
dependency of the workspace root that failed to resolve — its filter
requires `parents.length === 0` and `prefix === cwd`, and only the
resolver's `resolution_failure` emit carries a `parents` array. The
installability re-check (unsupported engine/platform) and build-failure
emits omit `parents`, so upstream keeps the console silent for them.
Pacquet was rendering a `Skipping optional dependency <pkg>` warning for
every `pnpm:skipped-optional-dependency` event, which upstream never
prints. Since pacquet's install path is lockfile-driven with no resolver,
every event it emits on this channel is one upstream filters out. Consume
the event in the default reporter without rendering so the console output
matches the TypeScript CLI. The wire event still fires, so `--reporter=ndjson`
output is unchanged, and a build failure skipped as optional still surfaces
through the lifecycle channel's "(skipped as optional)" note.
Run pacquet's package-manager version switch before printing --version or dispatching commands, so projects that declare pnpm via packageManager or devEngines.packageManager get the selected pnpm version even for version-only invocations.
Plan switching synchronously before entering Tokio, so ordinary no-switch invocations and the up-to-date install fast path avoid the runtime/thread startup cost.
Prefer the package-manager env lockfile for persistent package-manager declarations when it already pins a satisfying pnpm version and has a complete, integrity-backed packageManagerDependencies graph, matching the TypeScript CLI's lockfile-first behavior while rejecting stale or non-registry lockfile entries. Validate the package-manager dependency graph iteratively rather than recursively.
Share the existing pnpm engine installer with the switch path, but make it version-aware: pnpm versions before v12 execute through the legacy @pnpm/exe wrapper while v12 and newer execute through the unscoped pnpm wrapper. Cache hits relink missing pnpm bins before returning.
Run auto-switch engine cache lookups and installs from a trusted package-manager engine store derived from the pnpm home instead of the project storeDir, so repo-controlled workspace settings cannot redirect executable cache hits. Reject symlinked env lockfiles before reading or writing them, and write env lockfile updates atomically.
The switch is skipped for commands that manage package-manager state directly, under Corepack, and when manage-package-manager-versions is explicitly disabled.
Pacquet previously installed the unscoped pnpm package for every `pn with <version>` target. That works for v12+, where pnpm itself carries the native engine layout, but pnpm versions from 6.17.1 through v11 need the scoped `@pnpm/exe` wrapper package. Installing the unscoped package for those versions leaves the wrapper without the host native binary and fails at runtime with "no @pnpm/exe.<platform> native binary was found for this host".
Select the engine package from the resolved target version. Versions before `@pnpm/exe` was published use the JS `pnpm` package and skip native relinking. Versions from 6.17.1 through v11 use `@pnpm/exe` and link legacy platform artifacts such as `@pnpm/macos-arm64`. v12 and newer use unscoped `pnpm` and link the newer `@pnpm/exe.<platform>` artifacts.
Share that selection between self-update and `with`, use it when computing and resolving global-virtual-store slots, and derive scoped package slots by walking the full package path. Relink native platform binaries on cache hits as well as fresh installs so a previously incomplete slot is repaired before it is reused.