Commit Graph

12095 Commits

Author SHA1 Message Date
Alessio Attilio
ddbb4899c2 feat(pacquet): implement bugs command (#12687)
Port the `pnpm bugs` command to pacquet, following the structure of the TypeScript handler at `pnpm11/deps/inspection/commands/src/bugs/index.ts` and matching its error codes (`ERR_PNPM_NO_BUGS_URL`, `ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND`). The command supports local manifest lookup and registry lookup by package name, with repository URL normalization for GitHub/GitLab/Bitbucket shorthand, hosted git URLs, and self-hosted git servers. Unit tests cover all URL derivation branches (36 tests). Integration tests cover the CLI entry points with local manifests and a mocked registry (9 tests).

Related to pnpm/pnpm#11633.
2026-07-01 18:09:02 +02:00
Zoltan Kochan
397f9783f4 fix: avoid passing infinite width to cli-truncate when streaming lifecycle output (#12750)
* fix: avoid passing infinite width to cli-truncate when streaming lifecycle output

cli-truncate 6.1.0 rejects a non-finite width, which crashed the reporter
when streaming lifecycle script output (streamLifecycleOutput passes Infinity
to mean "do not truncate").

* chore: drop changeset

The broken build this fixes was never released, so no release note is needed.
2026-07-01 09:15:29 +02:00
Zoltan Kochan
b6d29d212d chore: update lockfile, Node.js, pnpm, and pacquet versions (#12692)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-30 20:56:39 +02:00
Khải
f57f5af8bd docs(pacquet): remove TypeScript-code references from comments and document the parity paradigm (#12737)
pacquet and the TypeScript pnpm CLI are co-developed parallel
implementations at near-complete feature parity, with neither stack
downstream of the other. Remove the GitHub permalinks and the "mirrors
upstream's X" framing that pointed pacquet's comments at the pnpm
TypeScript source, and rewrite the affected prose to describe pacquet's
own behavior. This also resolves the dead permalinks the PR originally
set out to repair: the links are gone rather than re-pinned, so they
cannot rot again.

Removed every /blob/ and /tree/ permalink to the pnpm-org TypeScript
repos (over 1,700 links across ~330 files) plus the cited TS symbols and
"Mirrors/Ports/matches upstream's X" prose. Kept issue/PR/CI references,
pnpm/pacquet self-references, third-party attributions, Rust intra-doc
links, shared-contract identifiers (ERR_PNPM_* codes, the pnpm-prefixed
reporter wire format), and generic pnpm-product mentions.

Updated the contributor guides to state the new paradigm: root and
pacquet AGENTS.md (cardinal rule rewritten, permalink-citation guidance
removed), pacquet CODE_STYLE_GUIDE.md (reporter section reframed around
the shared wire contract), pacquet README, and a few plan/task docs.

The Rust changes are comment-only except for three #[expect(reason =
"...")] attribute strings that themselves cited a TypeScript symbol.
2026-06-30 20:53:14 +02:00
Khải
a6f303c2ff chore(rust/dylint): upgrade perfectionist 0.0.0-rc.22 (#12719)
* chore(dylint): upgrade perfectionist to 0.0.0-rc.21

Bump the perfectionist dylint library from 0.0.0-rc.17 to 0.0.0-rc.21 and
migrate the configuration to the rc.21 rule names and semantics.

Renamed config keys (perfectionist renamed every lint to name the
anti-pattern it flags):
- macro_argument_binding -> impure_macro_arguments
- derive_ordering        -> unordered_derives
- import_granularity     -> import_granularity_mismatch
- unit_test_file_layout  -> excessive_inline_tests

Remove three lint suppressions that upstream bug fixes made unnecessary
(each now reported an unfulfilled #[expect]):
- two bare_url suppressions on include_str!-ed fixtures (comment scanners
  now ignore include_str!-ed files)
- the import_granularity suppression in resolve_importer tests (the rule
  no longer folds a named item into self; the both-module-and-item case
  is left unflagged by default)

flat_module_pattern was removed from perfectionist in favor of
clippy::mod_module_files, which this workspace already enables; update
CODE_STYLE_GUIDE.md to match.

Tune the heavy new active-by-default rules for this change:
- needless_borrowed_parameters: disabled pending a parity-reviewed pass
  (https://github.com/pnpm/pnpm/issues/12715)
- bare_identifier_reference: narrowed to own-module, multi-word names
  (https://github.com/pnpm/pnpm/issues/12716)
- clap_help_markdown: code spans ignored; link leaks still flagged

Simplify the executor test gate now that excessive_inline_tests accepts a
compound #[cfg(all(test, unix))] on an external test module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* fix(lint): comply with new perfectionist rc.21 rules

Apply the rc.21 rule changes across the Rust workspace and tune the
heaviest new active-by-default rules in dylint.toml.

Code compliance:
- bare_identifier_reference: wrap backticked identifiers that resolve in
  the documenting item's own module as intra-doc links (~197 sites). The
  rule is narrowed to own-module, multi-word names; widening is tracked in
  https://github.com/pnpm/pnpm/issues/12716.
- avoidable_string_escapes: convert escape-only string literals to raw
  strings (74 sites).
- allow_attributes: convert #[allow] to #[expect] where the lint
  deterministically fires (8 sites). Drop three suppressions whose lint
  does not actually fire — a too_many_arguments on a 2-arg fn and two
  large_stack_frames blocks already neutralised by Box::pin (the rule
  flagged dead #[allow]s under its driver; #[expect] is unfulfilled under
  stable clippy).
- wildcard_imports: expand `use super::*` to explicit imports in six test
  modules (pacquet-cli, pnpr).

Config (dylint.toml): disable needless_borrowed_parameters
(https://github.com/pnpm/pnpm/issues/12715), unpinned_repo_ref
(https://github.com/pnpm/pnpm/issues/12717), and clap_help_markdown
(https://github.com/pnpm/pnpm/issues/12718) pending dedicated follow-up
passes.

Edits are limited to doc comments, raw-string conversions, lint
attributes, and import lists — no runtime behaviour changes. Verified
locally: cargo dylint, cargo doc, cargo clippy (all -D warnings), taplo
format --check, and the affected crates' unit tests all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* test(executor): restore the original Unix-only test gate

The rc.21 upgrade moved the gate from a plain `#[cfg(test)] mod tests;`
(with `#![cfg(unix)]` inside the test file) to `#[cfg(all(test, unix))]`
on the module declaration. No lint required that move: rc.21's
`excessive_inline_tests` recognises the external test module regardless
of where the Unix gate sits, so the original form was already clean
(verified with `cargo dylint`). Revert it and trim the now-obsolete
workaround rationale from the comment — the rc.17 `unit_test_file_layout`
compound-`cfg` misclassification it dodged is fixed in rc.21.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* chore(dylint): trim stale-prone comments in dylint.toml

Remove the verbose rationale comment above the `disable` list — its
per-rule counts and narrative would drift out of date; the disabled rules
are tracked in their follow-up issues and the commit history. Shorten the
`bare_identifier_reference` comment to the configured intent plus its
tracking issue, dropping the firing-count and default-behaviour narrative.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* chore(dylint): disable perfectionist bare_identifier_reference

Its autofix blindly rewrites every doc code span into an intra-doc link,
which silently produces false intra-links when the span names an upstream
(TypeScript) type that only shares a name with a local Rust item. The rule
itself is sound, so disable it rather than keep narrowing it until the
autofix is removed or downgraded to MaybeIncorrect.

Keep the one site corrected by hand (graph-hasher dep_state) as a reference
for the right pattern: an explicit reference link to the upstream source
instead of a self-resolving intra-doc link.

Re-enable tracked in https://github.com/pnpm/pnpm/issues/12722.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* chore(executor): drop the doc-comment churn in the test-module gate

The earlier revert rewrote the gate comment in tests.rs instead of
restoring it, leaving a documentation-only diff against the base. Restore
the original comment verbatim so this file is unchanged by the PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* docs(pacquet): link false intra-doc links to their upstream source

Several doc comments wrapped an upstream (pnpm TypeScript / yarn) type or
const name in [`Name`], which resolved to a same-named local Rust item — a
false intra-link that reads as "upstream's Name" but points at the port.

Convert each to an explicit reference link to the upstream definition
([`Name`][ts-Name] plus a commit-pinned [ts-Name]: <url> permalink),
sourcing the URL from the doc block's existing citation, the file's
module-level reference, or (for the handful with no in-repo citation) the
upstream definition located at pnpm main 6fadd7def9. SINGLE_LINE_KEYS lives
in the out-of-scope zkochan/js-yaml fork, so it becomes a plain code span
rather than a fabricated permalink.

Doc-comment-only change. Verified with cargo doc, cargo dylint, and cargo
fmt (all -D warnings / --check), plus a rendered-HTML check that every
reference link resolves.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* docs(lockfile): permalink SINGLE_LINE_KEYS to the js-yaml fork source

github.com/zkochan/js-yaml returns 404 (the fork is not a public repo),
verified against known-good controls (nodeca/js-yaml, pnpm/pnpm, and the
zkochan user page all 200). A repo permalink therefore cannot be formed,
so point the reference at the immutable, version-pinned npm CDN copy of
the fork's dumper.js, where SINGLE_LINE_KEYS (cpu/engines/os/libc) is
defined at lines 63-68.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* docs(pacquet): pin upstream permalinks to their definition line ranges

Every upstream-source permalink added in this PR now carries a line anchor
(#Ln for a one-line definition, #Ln-Lm for a multi-line one) pointing at the
exact definition it cites, resolved against each link's pinned commit.

Three links pointed at a module that only imports or uses the symbol rather
than defining it; repoint them to the defining file: DepPath to
core/types/src/misc.ts, EnvLockfile to the lockfile.types index, and
InvalidWorkspaceManifestError to its dedicated error module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* docs(pacquet): reword doc links that named non-existent upstream identifiers

Several doc comments linked a bracketed identifier that does not exist in the
upstream source. Replace each with plain English describing the code it
mirrors, keeping a permalink only where one resolves:

- render_specifier: upstream inlines the specifier string in its no-resolver
  error rather than naming a function; link the error block instead.
- HoistingLimits: yarn exposes only the lowercase `hoistingLimits` option
  (no named type); link that option's definition.
- ConfigFileChangeType / ConfigReport / EnvVariableChange: pacquet's own
  names for report shapes from pnpm's os.env.path-extender package, which
  lives outside the pnpm/pnpm monorepo (the old links 404'd). Name the
  package without a dead link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* docs: share reference

* docs(lockfile): point js-yaml fork links at pnpm/js-yaml

The `@zkochan/js-yaml` source repo is pnpm/js-yaml, not zkochan/js-yaml
(a hard 404). Repoint the module-level fork link there.

Also drop the `#L63-L68` anchor from the SINGLE_LINE_KEYS jsDelivr
reference: 0.0.11 has no tag in pnpm/js-yaml and the anchor is inert on
the raw CDN file, so it resolved to nothing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* fix(lint): resolve dylint findings surfaced by the main merge

Merging main brought two pre-existing items into the linted tree:

- pnpr storage tests imported `CachedTarballIntegrity` and
  `TARBALL_INTEGRITY_SUFFIX`, but main removed their uses; drop the now
  dead imports.
- integrated-benchmark's per-path `rm` retry command is escape-heavy as a
  normal string literal; make it a raw string (avoidable_string_escapes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* chore(rust/dylint): upgrade perfectionist to 0.0.0-rc.22 and re-enable bare_identifier_reference

rc.22 downgrades `bare_identifier_reference`'s autofix from
`MachineApplicable` to `MaybeIncorrect` and rewords its diagnostic to warn
that a doc code span may name a foreign symbol of the same name — the exact
fix that justified disabling the rule (pnpm/pnpm#12722). Re-enable it at the
previously narrowed scope (`reference_scope = "own_module"`, `min_words = 2`;
widening still tracked in pnpm/pnpm#12716).

Comply with the 8 sites it flags in the merged tree — all foreign/upstream
references, so each gets an explicit reference link to its upstream source
(or a reword), never a self-resolving intra-doc link:

- `DependencyManifest` (resolving-resolver-base): reword; the upstream type
  is already linked earlier in the same doc.
- `WebAuthFetchOptions`, `WebAuthFetchResponse`, `WebAuthTimeoutError`,
  `OtpErrorBody`, `SyntheticOtpError`, `OtpNonInteractiveError`,
  `OtpSecondChallengeError` (the ported `@pnpm/network.web-auth` crate):
  link to their definitions in the upstream TypeScript package.

No other rc.22 change affects the workspace: no rules added, removed, or
renamed; no rule config-schema change; same nightly toolchain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

* fix(lint): resolve rc.22 lint findings from the latest main merge

main's newly merged code tripped two rc.22 rules:

- The `repo` command's tests used `use super::*` (wildcard_imports);
  expand it to the four items the tests actually use.
- The integrated-benchmark `BenchmarkScenario` enum carried an
  `#[expect(clippy::enum_variant_names)]`. main's new
  `IsolatedFreshRestoreColdCacheColdStoreColdPnpr` variant ends in
  `Pnpr`, breaking the shared `...Store` suffix the lint had fired on, so
  the lint no longer fires and the expectation is unfulfilled. The
  suppression is dead (its own comment anticipated this once another
  linker bucket landed) — drop it; `#[allow]` would instead trip the
  re-enabled allow_attributes rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WTm1H87D62ECyf9nLgPZkZ

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-30 00:27:17 +02:00
Zoltan Kochan
c4acfadd86 fix(pnpr): stream proxied tarballs instead of buffering-then-verifying (and benchmark the cold pnpr cache) (#12729)
On a cache miss the proxy buffered the whole tarball into the cache, verified
its SRI, and only then streamed the finished file — a head-of-line stall on
every cold fetch: the client waited for the full upstream fetch plus a verify
pass before the first byte, with no overlap. Buffer-then-verify is the wrong
model for a streaming proxy.

Stream the upstream response to the client while teeing it into the cache and
hashing it (stream_verified_to_cache); promote the cache entry only once the
full body matches dist.integrity. A mismatched/truncated/oversize body is never
cached and the client re-verifies what it receives, so a bad upstream tarball
can't poison a later client — it just can't be turned into a BAD_GATEWAY, since
SRI is only known after the body is already streamed.

Converge the namespaced /~<uplink>/ route onto the same model (the integrity is
known from the packument either way), removing the now-dead uplink integrity
sidecar machinery. Stop forwarding the upstream Content-Length on streamed
responses (attacker-controlled, unverifiable before streaming; kept only for
the internal early oversize check). Add a cold-pnpr-cache benchmark scenario
that actually exercises the cold download/serve path (every other scenario
warms the mock); it runs frozen so resolution variance doesn't mask the
serve-path delta.

The measured speedup on that scenario is modest and within noise (this fixture
is small tarballs over a latency-bound link); the change is a correctness fix,
and the benchmark confirms no regression.

Out of scope: packuments with no usable dist.integrity (pnpm/pnpm#12727).
2026-06-29 23:42:49 +02:00
Nikita Zhukovskii
cc04eb174e feat(cli): port the bin command to pacquet (#12703)
Port pnpm's `bin` command, which prints the directory where executables are
installed.

Locally it prints `<dir>/node_modules/.bin`: the `node_modules/.bin` leaf is
hardcoded, so a configured `modules-dir` is ignored, and the path is anchored
on the already-canonicalized `--dir`, mirroring pnpm's config reader.

`--global` resolves `global-bin-dir ?? <pnpm-home>/bin`, creates it, and runs
`check_global_bin_dir` (PATH membership + writability, since
`globalDirShouldAllowWrite` is true for every command except `root`) before
printing — the same `mkdir` + `checkGlobalBinDir` pnpm's config reader performs
for every `--global` command. Errors mirror pnpm:
`ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH`, `ERR_PNPM_NO_PATH_ENV`,
`ERR_PNPM_PNPM_DIR_NOT_WRITABLE`, and `ERR_PNPM_NO_GLOBAL_BIN_DIR`.

Ported from pnpm's bin.ts and config reader.

Related to pnpm/pnpm#11633.
2026-06-29 22:53:32 +02:00
Alessio Attilio
da9f834e34 feat(pacquet-cli): implement pnpm repo command (#12702)
Implement the pnpm repo command in pacquet-cli, which opens a package's
repository URL in the browser. Supports local manifest lookup and
registry-based lookup with version selection via dist-tags and semver
ranges. Handles SSH, git://, git+https, and shorthand repository URL
formats, strips .git suffixes, and expands shorthands to proper web
URLs. Uses the structured Reporter trait for browser-open error logging.
Includes 32 unit tests and 3 integration tests covering all URL formats,
error paths, and registry interaction.
2026-06-29 22:52:42 +02:00
Khải
d22f277613 feat(pacquet/cli): promote --filter/--filter-prod to recursive mode (#12726)
* feat(pacquet/cli): promote `--filter`/`--filter-prod` to recursive mode

A bare `--filter` / `--filter-prod` (without `-r`/`--recursive`) now puts
the command into recursive mode CLI-wide, matching pnpm's parse-cli-args
promotion. This is the prerequisite for driving release.yml, which runs
`pn publish --filter=<pkg>` without `-r`.

Also ports the `!{<workspace-root>}` augmentation from pnpm's main
dispatch: for run/exec, an all-exclusion (or unfiltered) selection
auto-excludes the workspace root unless the workspace is root-only
(patterns === ['.']). The recursive selection now filters with glob
directory matching (pnpm's useGlobDirFiltering default), which is
load-bearing for that augmentation: it excludes only the project at the
workspace root, not every nested package. pack stays out of the
auto-exclusion command set, matching pnpm.

Refs https://github.com/pnpm/pnpm/issues/12723

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SBAmoo35Kr6K96ie7vG8tF

* test(pacquet/cli): cover pack root-inclusion and subdir root-exclusion

Strengthens coverage of the recursive root auto-exclusion, surfaced by review:

- `recursive_pack_includes_workspace_root` pins the
  `AutoExcludeRoot::Disabled` contract — a recursive `pack` keeps the
  workspace root. This was previously indistinguishable from `Enabled`
  because no test set up a root package.
- `recursive_run_from_subdirectory_still_excludes_root` exercises the
  non-trivial relative-path branch of the `!{<workspace-root>}`
  augmentation (running `-r run` with `--dir packages/<pkg>`), which had
  no coverage.

Also documents that pnpm's `--include-workspace-root` / `--workspace-root`
gates are not ported because pacquet surfaces neither flag yet.

Refs https://github.com/pnpm/pnpm/issues/12723

* test(pacquet/cli): cover --filter-prod all-exclusion root drop

Pins the load-bearing `follow_prod_deps_only` flag on the
`!{<workspace-root>}` augmentation, surfaced by review. A
`--filter-prod`-only all-exclusion must route the root exclusion into the
same production-only selection pass as the user's exclusion; otherwise the
union of the prod and non-prod passes re-adds both the workspace root and
the excluded package, silently running the script everywhere.

Refs https://github.com/pnpm/pnpm/issues/12723

* test(pacquet/cli): cover the select_recursive_projects error path

The `select_recursive_projects(...)?` in the recursive `run` / `exec`
runners propagates the filter engine's error, which a coverage report
flagged as uncovered. A `[<since>]` changed-packages selector is rejected
with `UnsupportedDiffSelector` (pacquet has not ported git-diff project
selection), and that error travels out through the `?`. Add integration
tests that pass `--filter [main]` to recursive `run` / `exec` and assert
the error surfaces (non-zero exit plus the explanatory message) rather
than being swallowed.

A real fixture (a `[<since>]` selector string) reaches this branch, so per
pacquet/AGENTS.md these are plain integration tests, not the dependency-
injection seam (which is reserved for branches a real fixture cannot
reach, e.g. filesystem error kinds or time).

Refs https://github.com/pnpm/pnpm/issues/12723

* test(pacquet/cli): assert recursive run/exec don't dispatch on a rejected selector

Strengthen the `[<since>]` diff-selector regression tests so they prove the
command short-circuits before dispatch, not merely that stderr names the
error. `run` asserts the build script's `ran.txt` marker is absent, and
`exec` now runs a marker-writing command (`touch ran.txt`) and asserts the
marker is absent. Per REVIEW_GUIDE.md, regression assertions must observe
the changed behavior rather than only the error message.

Refs https://github.com/pnpm/pnpm/issues/12723

* style(pacquet/cli): drop call-site comment duplicating the callee doc

The comment above `promote_recursive_for_filter()` in main restated the
function's own doc comment clause-for-clause (the recursive promotion, the
`parse-cli-args` parity, and the place-once-before-dispatch rationale all
already live on the `///`). Per the repo comment convention, a call site
must not re-explain what the callee documents; the method name carries the
intent and the doc comment carries the why.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-29 17:20:05 +00:00
Zoltan Kochan
a6f04d4ae5 perf(pnpr): restore cold-store install speed on the proxy tarball serve path (#12709)
pnpm/pnpm#12570 made every tarball serve — warm cache hits included — both
re-hash the cached bytes against dist.integrity and load+parse the packument to
bind the request to a version. The benchmark mock is pnpr and a cold-store
install pulls ~1300 tarballs through it, so both costs are paid per tarball:
together the cold-store regression from ~2.15s to ~3.15s on the Bencher pnpr
testbed (pnpm/pnpm#12700 recovered only the resolution-cache part).

Both are redundant on a cache hit. A tarball only enters the proxy cache via
download_verified_to_cache, which resolves the version and verifies the bytes
against dist.integrity as they are written, so the cache only ever holds correct
bytes for a (name, filename); the install client re-verifies whatever it receives
anyway. Serve cached bytes directly and defer the packument load to the
cache-miss download path, which still verifies freshly-fetched bytes before
caching them. The OSV screen on the filename version still runs first.

Write-time verification is preserved, so GHSA-5f9g-98vq-2jxw stays mitigated —
the bytes that enter the cache are still verified, which the pre-regression serve
path did not even do. Drops the now-dead per-serve integrity sidecar and helpers.
2026-06-29 16:33:41 +02:00
Khải
6fadd7def9 feat(pacquet): port @pnpm/network.web-auth (#12569)
* feat(network-web-auth): port web-based authentication flow from pnpm

Port the `@pnpm/network.web-auth` package to a new `pacquet-network-web-auth`
crate, in preparation for the registry-auth commands (login / publish) that
pacquet has not ported yet.

The crate provides:
- `poll_for_web_auth_token` — polls a registry "done" URL for an auth token,
  honoring `Retry-After` and an overall timeout (`ERR_PNPM_WEBAUTH_TIMEOUT`).
- `with_otp_handling` — runs an operation and, on an EOTP challenge, drives
  either the web-auth flow (QR code + browser open + poll) or a classic OTP
  prompt, then retries once. Surfaces `ERR_PNPM_OTP_NON_INTERACTIVE` and
  `ERR_PNPM_OTP_SECOND_CHALLENGE`.
- `prompt_browser_open`, `generate_qr_code`, and the `SyntheticOtpError` helpers.

Rather than porting the TypeScript context-of-closures verbatim, every side
effect is a self-less capability trait composed on a single `Sys` parameter
(`Clock`, `Sleep`, `WebAuthFetch`, `OpenUrl`, `EnterKeyListener`, `PromptOtp`,
plus the TTY probes), with the real OS behind `Host` and fn-bound unit-struct
fakes in tests. User-facing messages flow through the `Reporter` seam on a new
`pnpm:global` channel, matching pnpm's `globalInfo` / `globalWarn` and rendered
by `pacquet-default-reporter`.

New workspace dependencies: `qrcode` (no default features) and `open`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWejcxTU8n144a1KK4nRj8

* style(network-web-auth): satisfy perfectionist dylint lints

CI's dylint job flagged the new crate against the `perfectionist` lint
library (which clippy does not run). Fix every finding:

- Reorder derive lists to the configured `prefix_then_alphabetical`
  order (`Debug, Default, Clone, ...`).
- Rename the single-letter `R` reporter generic to `Reporter`, bound as
  `self::Reporter`, matching the existing pacquet convention.
- Move the inline `mod tests { ... }` blocks for `capabilities`,
  `generate_qr_code`, and `web_auth_timeout_error` into external
  `tests.rs` files (the crate's `unit_test_file_layout` is `external_only`).
- Add trailing commas to multi-line macro invocations.

No behavior change. Verified clean with the same command CI runs:
`RUSTFLAGS='-D warnings' cargo dylint --all -- --all-targets --workspace`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWejcxTU8n144a1KK4nRj8

* fix(network-web-auth): cancel the web-auth Enter listener on drop

The production `EnterKeyListener` used `spawn_blocking` + a blocking
`stdin().read_line()`, which cannot be cancelled: if the user never
pressed Enter (e.g. authentication completed via a phone QR scan), the
reader thread lingered until process exit. Replace it with a dedicated
thread that polls stdin with a 100ms timeout via `crossterm::event::poll`
and re-checks a cancel flag each tick, so dropping the handle stops the
thread within one poll interval.

`crossterm` reads in the terminal's default (cooked) mode — no raw mode —
matching pnpm's plain `readline.createInterface({ input: process.stdin })`,
which reacts to a submitted line rather than individual keypresses. In
Node, stdin is event-loop-driven so `rl.close()` needs no thread to
cancel; the poll loop is the Rust equivalent.

Adds `crossterm` to `[workspace.dependencies]`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWejcxTU8n144a1KK4nRj8

* fix(network-web-auth): don't let a dropped Enter listener steal stdin

A code review of the cancellable web-auth listener surfaced two items:

- The polling reader thread could consume one keystroke in the window
  between the handle being dropped (cancel flag set) and the thread
  noticing it: `event::poll` reports input, then `event::read` swallows a
  key meant for whatever reads stdin next. Re-check the cancel flag after
  poll and before read so a dropped handle leaves the input buffered,
  matching pnpm's clean `readline` close.
- Drop the redundant `Option<sender>`: the Enter arm returns right after
  sending, so the oneshot sender moves directly into `send()`.

Also document that the handle is meant to be raced and never resolves on a
stdin read error, so it must not be awaited on its own.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWejcxTU8n144a1KK4nRj8

* docs(network-web-auth): de-duplicate Enter-listener comments

Review follow-up: keep the polling/cancel rationale on `HostEnterHandle`
(where `Drop` lives) and trim `listen`'s doc to its unique cooked-mode
detail; drop a select-arm comment sentence that restated automatic `Drop`
and the function's own doc comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWejcxTU8n144a1KK4nRj8

* docs(network-web-auth): correct the Enter-listener cancellation rationale

Round-3 review follow-up:
- The `HostEnterHandle` doc blamed `std`'s blocking `read_line` for needing
  a poll loop, but the listener uses crossterm's `event::read()`
  (`read_line` was the pre-crossterm implementation). Name the real
  primitive.
- Drop the `listen` doc's reference to the private `HostEnterHandle` type
  (a public method's docs must not name a more-private item); point to "the
  returned handle" instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWejcxTU8n144a1KK4nRj8

* docs(network-web-auth): state the why in the clock test doc

The doc comment on host_clock_reads_a_non_zero_time restated the test name
and its assertion instead of the non-obvious reason behind it. Replace it
with the rationale: 0 is Host::now_ms's pre-epoch fallback, so a non-zero
read proves the real wall clock was queried.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-29 10:18:35 +00:00
Khải
4cd45a3e15 feat(pacquet/cli): apply --filter selection in recursive run and exec (#12712)
* feat(pacquet-cli): apply --filter selection in recursive run and exec

The recursive `run` / `exec` command path operated on every workspace
project; the `--filter` / `--filter-prod` selectors were parsed into
`Config::filter` / `Config::filter_prod` but never narrowed the set.

Add `select_recursive_projects`, a shared helper that builds the
workspace graph over all projects and restricts it to the selectors'
selection — include selectors unioned, `!`-prefixed excludes subtracted —
mirroring the `selectedProjectsGraph` pnpm hands its recursive handlers in
main.ts. With no selectors every project is selected, so the unfiltered
behavior is unchanged. The recursive `run` no-script error now
distinguishes "None of the packages" from "None of the selected packages"
the way pnpm does, keyed on whether the filter narrowed the set.

Refs https://github.com/pnpm/pnpm/issues/12711

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgEs4dECzHdR3aj19DLxiD

* fix(pacquet-cli): no-op recursive run/exec on an empty --filter selection

Address review feedback on the --filter wiring:

- When --filter narrows a non-empty workspace to no projects, recursive
  run/exec now exit 0 instead of erroring on --resume-from or writing an
  empty --report-summary summary, matching pnpm's main-dispatch exit-0 for
  an empty selectedProjectsGraph (main.ts).
- Resolve the selectors against the same graph options used for the
  topological sort, so the selected set and the run order agree on edges
  (upstream reads one linkWorkspacePackages-aware graph for both).

Empty-workspace handling (recursive run -> no-script error, exec ->
NO_PACKAGE) is left as-is; it predates --filter and is orthogonal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgEs4dECzHdR3aj19DLxiD

* feat(pacquet-cli): apply --filter to recursive pack via the shared path

Route `pack -r`'s workspace sweep through the shared
`select_recursive_projects` helper that `run -r` / `exec -r` use, so
`pack -r --filter` narrows which projects are packed instead of silently
packing every project. Keeps `--filter` a single cross-cutting selection
path across all recursive commands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgEs4dECzHdR3aj19DLxiD

* test(pacquet-cli): cover the --filter-prod selector path in recursive run

Add a recursive-run integration test that narrows the workspace with
`--filter-prod`, exercising the `follow_prod_deps_only: true` branch of
`select_recursive_projects` that the `--filter`-only tests never reached
(flagged uncovered by Codecov).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgEs4dECzHdR3aj19DLxiD

* docs(pacquet-cli): trim verbose comments in the recursive filter path

Per review, make the comments added for `--filter` selection succinct:
keep the non-obvious "why", drop restatement of self-documenting code and
narration of pnpm's behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgEs4dECzHdR3aj19DLxiD

* docs(pacquet-cli): correct stale "every project" recursive doc comments

The recursive `run` / `exec` / `pack` function- and module-level doc
comments still said the command runs over "every workspace project"; with
`--filter` wired in they run over the selected subset. Update them to
match the code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgEs4dECzHdR3aj19DLxiD

* docs(pacquet-cli): correct remaining "every project" recursive dispatcher docs

Follow-up: the `RunArgs::run_recursive` / `ExecArgs::run_recursive`
dispatcher docs and the `PackArgs::run` summary still said the recursive
command runs over "every project"; with `--filter` wired in they run over
the selected subset. The global `--recursive` flag help is left as-is — it
describes `-r`'s workspace-wide-vs-single behavior, and `--filter`
narrowing is a separate flag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgEs4dECzHdR3aj19DLxiD

* test(pacquet-cli): prove --filter-prod prod-only walk in recursive run

The flat-workspace fixture passed even if `follow_prod_deps_only` were
ignored: with no dependency edges, `--filter-prod app` and `--filter app`
select the same set, so the test could not tell them apart.

Replace it with a dev-only edge (`app` devDependencies `lib`) and the
`app...` ellipsis selector, so `--filter-prod`'s production-only
dependency walk must drop the dev edge. The test runs `app` and asserts
`lib` is skipped, failing if `--filter-prod` collapses to `--filter`.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-29 06:07:54 +00:00
Khải
4dabe4c3d1 ci(benchmark): double the precompile-revisions timeout (#12713)
Bump the "Precompile benchmark revisions" step from 25 to 50 minutes; the
superset build of pacquet + pnpr revisions has been hitting the limit.


Claude-Session: https://claude.ai/code/session_01KtBQzmLLDU3RcGzzCMopPB

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-29 06:06:34 +00:00
dependabot[bot]
517ea5d510 chore(cargo): bump getrandom from 0.4.2 to 0.4.3 (#12683)
Bumps [getrandom](https://github.com/rust-random/getrandom) from 0.4.2 to 0.4.3.
- [Changelog](https://github.com/rust-random/getrandom/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/getrandom/compare/v0.4.2...v0.4.3)

---
updated-dependencies:
- dependency-name: getrandom
  dependency-version: 0.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 23:42:44 +02:00
Zoltan Kochan
99af755b6e docs: pacquet has ported nearly all commands, so mirror every user-visible change (#12708) 2026-06-28 22:55:27 +02:00
Zoltan Kochan
1dd12bd515 feat(pnpr): make resolver cache authorization-aware (#12700)
Make the pnpr resolver cache authorization-aware and route private
dependencies through per-uplink registry endpoints.

Add route classification at the single fetch/auth-selection point: pnpr
selects its own server-owned upstream credentials instead of forwarding
client upstream auth, records a per-resolve footprint, and rejects inline
URL credentials. A uplinks: entry that declares an access: policy becomes
the private-route credential (folding in the former upstreamAliases block),
matched by registry origin and exposed as a read-only registry endpoint at
/~<uplink>/. access reuses bearer-token-backed pnpr identities, package
access policy, and static groups; a SHA-256 digest of the uplink's credential
participates in the footprint, so rotating the upstream credential
automatically moves new resolves to a fresh namespace. The credential is
attached only over a matching scheme (no token over plain http).

Gate every server-side fetch behind a default-deny allowlist (built-in npm
host, public routes, configured uplinks and their /~<uplink>/ endpoints, and
pnpr itself). A registry/namedRegistries matching none is rejected at the
request boundary, closing the resolver's SSRF surface at the source and
superseding the link-local denylist. The boundary also covers direct-URL
(http(s)/git, incl. scp-style) dependency specs, overrides, and lockfile
tarballs; a `..` path segment is rejected; and the same allowlist re-validates
every redirect hop. The official npm registry is a built-in host-level public
route (scoped names included), so the npmjsPublic toggle is gone. With no
off-allowlist route to resolve, every route is public or carries a private
descriptor: RouteClass::Unknown, the non-shareable footprint tier, and the
MetadataCacheScope::Bypass tier are removed. Transitive deps fetched during the
tree walk are a connect-time-guard follow-up (pnpm/pnpm#12705).

Route resolver tarball URLs through those endpoints. A proxied route emits
its /~<uplink>/ endpoint URL; public routes keep their upstream URL (fetched
directly from the registry/CDN); pnpr-hosted packages use pnpr-hosted URLs.
An endpoint URL is canonical for a client whose scope
points there, so the lockfile entry stays integrity-only and the host comes
from the client's registry config rather than the lockfile — a project
resolves to the same lockfile through /resolve or a direct/proxied install.
verification_lockfile reverses endpoint URLs to upstream for input-lockfile
verification, and classification recognizes pnpr's own /~<uplink>/ URLs.
The opaque per-tarball gateway scheme and its in-memory key->URL map are
removed.

Serve each access-bearing uplink as a /~<uplink>/ registry endpoint
(packument + tarball, gated by the uplink access policy) with a private
cache namespaced by an HMAC of (uplink, credential), so a private install
caches like a public one, a rotation re-keys automatically, and a private
uplink's content never lands in the shared mirror.

Store bounded candidate lists under an auth-excluded base key instead of a
single lockfile. Public candidates match every caller; private candidates
carry the footprint and descriptor HMAC and are reused only when the caller
still satisfies the stored uplink-or-hosted gates. Compute the base key for
both no-lockfile and lockfile-seeded requests, using hash_lockfile() for
input lockfiles, and evict expired/LRU private candidates before public
ones.

Record metadata fast-path routes into the footprint. The hook only fires at
the auth-selection point, which the npm resolver's metadata fast paths
(in-memory hit, offline disk read, version-spec exact match, publishedBy
mtime shortcut) bypass. AuthHeaders::record_route drives the hook without a
request, called up front in pick_package so every layer contributes to the
footprint.

Scope the npm metadata mirror by private access descriptor. A
MetadataCacheScope (Public / Private) classified per (registry, package)
fetch threads through pick_package, fetch_full_metadata_cached, the mirror
path, in-memory/fetch-lock keys, and the verifier's local-mirror read. A
private route stores its packument under v11/metadata-private/<descriptor-id>/.
Fail closed on 401/403/private-404. The CLI installs no hook, so every fetch
stays Public and the global mirror is unchanged.

Related to pnpm/pnpm#12699 and pnpm/rfcs#11.
2026-06-28 21:51:47 +02:00
Zoltan Kochan
c00400da70 feat(pacquet-cli): port the with command (#12696)
Port pnpm's `with` command to pacquet.

`pnpm with <version> <args...>` resolves the spec through the trusted
package-manager bootstrap registry, installs that pnpm engine into the
global virtual store (matching pnpm's installPnpmToStore — shared across
invocations and, unlike self-update, not registered in the global
packages directory), and spawns it with the remaining args and the
child's packageManager check disabled.

`pnpm with current <args...>` is rewritten before clap parses argv into a
direct in-process dispatch of `<args>`, forcing pmOnFail to ignore via
the pnpm_config_pm_on_fail env var.

Wire the pmOnFail setting into Config and the env overlay so the override
is honored once the packageManager check is ported. Reuse the self-update
engine installer (now global-virtual-store aware) and the engine-identity
verifier rather than duplicating them.

The end-to-end `with <version>` download path has no automated test
because the mock registry does not serve the pnpm / `@pnpm/exe` engine
packages, the same limitation self-update has.
2026-06-27 18:17:56 +02:00
Khải
66c531e382 feat(pacquet): logout (#12568)
Port `pnpm logout` to pacquet as a new pacquet-auth-commands crate and a
`pacquet logout [--registry <url>]` CLI command, mirroring the TypeScript
@pnpm/auth.commands behavior, flags, error codes, and messages.

The command revokes the registry auth token (DELETE -/user/token/<token>)
and removes it from auth.ini. It warns when the token lives in .npmrc or
another source pnpm does not own, and fails only when the registry rejects
revocation and no local token is left to remove.

Upstream wires side effects through a LogoutContext object of functions;
this port uses pacquet's capability-trait DI seam instead: FsReadToString /
FsWrite / RevokeToken on Sys (production provider Host, test fakes are unit
structs), and the global* log channels through the Reporter seam.

Config gains auth_tokens_by_uri (raw _authToken per nerf-darted registry
URI) and config_dir, since Config previously baked credentials into
Authorization headers and discarded the raw token that logout must revoke.

Tests: all 13 upstream logout.test.ts cases ported as DI unit tests, plus
real-fixture integration tests (the Host provider via TempDir + mockito, and
a binary test spawning `pacquet logout`) covering the production code paths
the DI fakes bypass.

Part of the Rust Roadmap (pnpm/pnpm#11633).

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-27 17:46:55 +02:00
Zoltan Kochan
e96c2ef229 feat(pacquet-cli): port the setup command (#12697)
Port pnpm's `setup` command to pacquet, making `pnpm` available for global
use. Mirrors pnpm11/engine/pm/commands/src/setup/setup.ts and the
`@pnpm/os.env.path-extender` package family it depends on.

The command installs the CLI into the global packages directory (spawning
`<exe> add -g --ignore-scripts file:<execDir>` with a temporary `@pnpm/exe`
package.json, as pnpm does), writes the `pn` / `pnpx` / `pnx` alias scripts
into `$PNPM_HOME/bin`, and persists `PNPM_HOME` plus `$PNPM_HOME/bin` into
the user's environment — the current shell's rc file on POSIX
(bash/zsh/ksh/dash/sh/fish/nushell) or the registry on Windows.

The path-extender is inlined as a private module under the command. The
shell rc-block editing and the Windows `reg query` parsing use plain string
operations rather than a regex dependency. Error codes mirror pnpm's
PnpmError codes, including the `--force` hints.

Unit tests port the upstream path-extender-posix spec cases and cover the
Windows registry parser/renderer and the output rendering.
2026-06-27 17:15:41 +02:00
Zoltan Kochan
f8e5a0de8c feat(pacquet-cli): port the self-update command (#12693)
Ports pnpm's self-update command to the Rust pacquet CLI. Resolves the
target version from the trusted package-manager bootstrap registry
(honoring minimumReleaseAge / trustPolicy, strict resolution failing
closed), guards against corepack, refuses to downgrade on an implicit
`latest`, and emits the major-upgrade migration hint.

When the project pins pnpm via packageManager / devEngines.packageManager,
the pin is updated in place (range-style preserved, integrities persisted).
Otherwise the engine is installed into the global packages directory:
integrities are resolved into the env lockfile, the engine's npm registry
signature is verified against npm's embedded public keys, the host's native
platform binary is hard-linked into the wrapper (the preinstall is skipped
because the engine installs with scripts disabled), and the bins are linked
into the global bin directory with a cache-keyed hash symlink.

New modules: self_update/install_pnpm (installPnpm + linkExePlatformBinary)
and self_update/verify_engine (verifyPnpmEngineIdentity). config_deps gains
resolve_pnpm_version, the policy-aware version probe.
2026-06-27 13:25:48 +02:00
Khải
4e18e44e9b refactor(pacquet/cli): split cli_args.rs (#12690)
cli_args.rs had grown to ~1500 lines mixing the CLI argument types, the
command-dispatch match, the install/deploy/dedupe/prune pipelines, reporter
wiring, and packageManager/devEngines parsing — a frequent merge-conflict
hotspot. Split it into focused submodules (cli_command, dispatch, pipelines,
package_manager, reporter), leaving cli_args.rs as a thin module index.

Then split the ~600-line dispatch match: dispatch.rs keeps a RunCtx context
and a thin route() match, and the per-command handler bodies move into three
modules grouped by behavior — dispatch_install (mutate the install graph),
dispatch_query (read-only queries), and dispatch_script (package.json
scripts).

The refactor is behavior-preserving (visibility widenings, import collapsing
for the import-granularity dylint lint, and a `+ Sync` bound on
ApproveBuildsArgs::prepare's closures so the shared config/state closures keep
the async handler futures Send). Two pre-existing pnpm-parity gaps surfaced by
review are also fixed: the `i` alias for install, and scope-aware
packageManager parsing.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-27 12:36:06 +02:00
dependabot[bot]
687e92b68a chore(cargo): bump tower-http from 0.6.11 to 0.7.0 (#12685)
Bumps [tower-http](https://github.com/tower-rs/tower-http) from 0.6.11 to 0.7.0.
- [Release notes](https://github.com/tower-rs/tower-http/releases)
- [Commits](https://github.com/tower-rs/tower-http/compare/tower-http-0.6.11...tower-http-0.7.0)

---
updated-dependencies:
- dependency-name: tower-http
  dependency-version: 0.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-27 11:22:21 +02:00
dependabot[bot]
d5fdeff3d1 chore(cargo): bump bytes from 1.11.1 to 1.12.0 (#12684)
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.11.1 to 1.12.0.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.11.1...v1.12.0)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-27 11:21:32 +02:00
YES!HYUNGSEOK
de3ee84106 fix(pnpr): cap distinct interned configs to bound resolver memory (#12674)
The resolve and verify-lockfile endpoints intern one `&'static Config`
per distinct client configuration, keyed by the canonical JSON of the
caller-supplied registry, namedRegistries, overrides, and verification
policy. Each config is leaked because pacquet's install path requires a
`&'static Config`, and the cache was unbounded.

An authenticated caller could exhaust the server's memory by sending a
request with a fresh registry/policy combination every time: each
distinct key leaks another config that can never be reclaimed.

Cap the cache at MAX_INTERNED_CONFIGS (1024, matching
MAX_RESOLUTION_CACHE_ENTRIES); past the cap, both endpoints return 503
instead of interning more. Eviction is not an option — a leaked config
can't be freed, and removing its key would just let it be re-leaked, so
a hard cap is the only real bound. Reclaiming memory instead would
require threading Arc<Config> through pacquet's install path in place of
`&'static Config`, which is out of scope.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-27 01:41:44 +02:00
Alessio Attilio
6bd7ba8adb feat(pacquet-cli): implement pnpm docs / pnpm home command (#12671)
This change adds a DocsArgs struct registered as the Docs variant on CliCommand, visible as both docs and home through clap's visible_alias. At runtime the command fetches package metadata from the configured registry, extracts the homepage field (validated as an http/https URL), and opens it via the platform browser launcher: xdg-open on Linux, open on macOS, cmd /c start on Windows. When the homepage is missing or not an http/https URL, it falls back to https://npmx.dev/package/<name>. When the browser cannot be launched (non-interactive CI, unsupported platform), the URL is printed to stdout instead.

The is_http_url helper validates URL strings and is unit-tested for valid https, http, empty, non-url, ftp, and spaced inputs. Integration tests verify that omitting the package name produces the expected clap error, that the home alias routes correctly, and that help output is generated.

The url crate was added as a dependency of pacquet-cli, already available in the workspace dependencies.

Related to #11633.
2026-06-27 01:33:19 +02:00
Allan Kimmer Jensen
8b1b537493 fix(lockfile): match pnpm's __ separator for nested peer-group boundaries (#12658)
`to_virtual_store_name` removed all `)` instead of only stripping the
trailing one and replacing inner `)` with `_`. For nested peers like
`(eslint@9.35.0(jiti@2.6.1))(typescript@6.0.3)`, the inner `)` closing
the nested group was deleted rather than becoming `_`, losing the `__`
that pnpm produces at the group boundary.

Fixes https://github.com/pnpm/pnpm/issues/12452
2026-06-27 01:32:08 +02:00
kyungseopk1m
96da7c557d fix: pack node-gyp's gyp entrypoints as executable (#12652)
gyp's make generator execs gyp/gyp_main.py and gyp/gyp directly through
their shebangs when it regenerates the Makefile during a from-source
native build. They were missing from publishConfig.executableFiles, so
they got packed at 0644 and the build failed with "Permission denied"
(make error 126).

pnpm/pnpm#11485 fixed the node-gyp bin shims but not these two. Add them
to PUBLISH_EXECUTABLE_FILES so the pnpm and `@pnpm/exe` tarballs pack
them at 0755.

Closes pnpm/pnpm#12455
2026-06-27 01:30:31 +02:00
Zoltan Kochan
a33eeec9cd feat(pnpm): publish pnpm v12 (the Rust port) as pnpm and @pnpm/exe (#12670)
Make the pacquet Rust port installable from npm as pnpm v12, published with
equal content under two names — `pnpm` and `@pnpm/exe` — on the `next-12`
dist-tag (never `latest`, which keeps serving the production TypeScript pnpm
v11). First release is 12.0.0-alpha.0.

CLI presents as pnpm: clap name/bin_name are `pnpm`; PACQUET_VERSION holds the
pnpm 12.x version; the default User-Agent is the plain `pnpm/<version>` form;
the reporter footer reads `Done in ... using pnpm v<version>`; the `pnpm link`
/ `pnpm patch` / `pnpm patch-commit` hints now name pnpm (parity fixes); and
launched as `pnpx`/`pnx`, the binary injects the `dlx` subcommand itself by
detecting its current_exe name (porting pnpm's buildArgv).

npm packaging mirrors how `@pnpm/exe` ships pnpm: both the `pnpm` and `@pnpm/exe`
wrappers ship root-level placeholder bins (pnpm/pn/pnpx/pnx) plus an install.js
preinstall that hardlinks the host's native binary over the placeholder (no Node
launcher). Native binaries publish as `@pnpm/exe.<platform>-<arch>[-musl]` (file
`pnpm` inside). `pacquet`/`@pnpm/pacquet` are no longer published.

installPnpm initializes v12 exactly like `@pnpm/exe`: linkExePlatformBinary takes
the wrapper name and resolves the native binary via require, and PNPM_ALLOW_BUILDS
includes both names so the GVS hash is per-platform. resolvePackageManagerIntegrities
already resolves both `pnpm` and `@pnpm/exe`, so publishing `@pnpm/exe@12` makes
self-update / version-switch work with no v12-specific logic. From v12 the
install always uses the `pnpm` package (even from a `@pnpm/exe` build), via
pnpmPackageNameToInstall().

The configDependencies install-engine path still installs the published
`pacquet`/`@pnpm/pacquet` releases (which ship `@pacquet/<target>` binaries) and
is left untouched.
2026-06-27 01:24:33 +02:00
Zoltan Kochan
103b99bdb4 perf(npm): ship pacquet and pnpr as placeholder bins relinked to the native binary on install (#12507)
The published pacquet/@pnpm/pacquet and @pnpm/pnpr bins were #!/usr/bin/env
node launchers that spawned the real native binary, so every invocation paid
full Node startup (~170ms) on top of the ~30ms the binary needs.

Ship the native binary as the bin instead, mirroring @pnpm/exe. bin/pacquet and
bin/pnpr are published as non-executable placeholders, and an install.js
preinstall replaces the placeholder with the platform native binary (hard link,
copy fallback across filesystems, atomic temp-file + rename). On Windows it adds
a .exe twin, overwrites the original-name file the existing shim points at, and
repoints bin at the .exe (npm doesn't re-read package.json after preinstall).
Binary selection resolves whichever @pacquet/* / @pnpm/pnpr.* package the
package manager installed; the guarded process.report glibc probe only orders
the linux glibc/musl pair for the --force both-installed case.

A placeholder (not a launcher overwritten in place) is required because on
Windows the shim is generated from the launcher's node shebang and would keep
calling node; a placeholder has no shebang so the generated shim runs the file
directly.

Trade-off: no Node-launcher fallback. With build scripts blocked
(--ignore-scripts, pnpm/Bun default) the bin stays a placeholder until
allow-listed.

The configDependencies install-engine path is unaffected: pnpm resolves and
spawns the @pacquet/<platform>-<arch> native binary directly, never bin/pacquet.
A stale comment in runPacquet.ts is corrected to match.
2026-06-26 16:08:25 +02:00
Zoltan Kochan
b98cbf8d89 chore: update lockfile, Node.js, pnpm, and pacquet versions (#12680)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-26 10:50:41 +02:00
YES!HYUNGSEOK
6beda88be8 ci: pin oz-agent-action to the exact tag for zizmor's version check (#12678) 2026-06-26 10:24:34 +02:00
spencer17x
aea6aceb68 feat(pacquet-cli): add dynamic completion server (#12661)
* feat: add pacquet completion shell parser

* test: cover completion shell precedence

* test: cover completion diagnostics

* feat: generate pacquet completion scripts

* fix: hide pacquet completion parser plumbing

* test: suggest pacquet completion shells

* feat: add dynamic pacquet completion server

* fix(pacquet-cli): handle completion server edge cases
2026-06-26 01:39:07 +00:00
Zoltan Kochan
f38e696da7 feat(pacquet): port deploy command (#12668)
Port the existing TypeScript deploy command to pacquet.

The new pacquet deploy command validates workspace selection and target directory rules, copies the selected project using the package packlist by default, generates deploy-local manifest and lockfile files for shared-lockfile deploys, and runs the pacquet install engine in deploy mode.

This also wires deploy config defaults and config overrides, keeps install root discovery configurable so generated deploy lockfiles are resolved from the deploy directory, and adds integration coverage for shared-lockfile deploys, legacy deploys, and non-empty target validation.
2026-06-26 03:22:14 +02:00
A
ec76ca3cf3 feat(pacquet): port the "ping" command (#12666)
Add `pacquet ping`, ported from pnpm's ping command.

`ping` GETs `<registry>-/ping?write=true` and prints pnpm's PING/PONG
report, plus a pretty-printed `PONG <details>` line when the registry
returns a non-empty JSON body. It resolves the registry from `--registry`
or the configured default, adds a trailing slash before joining so a
registry path prefix is preserved (matching pnpm's
`new URL('./-/ping', ...)`), sends the optional `Authorization` header when
one is configured (unlike whoami it does not require auth), and issues a
single attempt with no retries (pnpm's `retry: { retries: 0 }`). Timing
wraps the request and body read, matching pnpm's `Date.now()` measurement.

Both a transport failure and a non-success status map to the single
`ERR_PNPM_PING_ERROR` code with pnpm's "Failed to reach registry" message;
the transport error message is credential-redacted before it reaches
stderr / CI logs, the same precaution as whoami. Unlike whoami's username
(printed raw, hence sanitized), ping's JSON details are re-serialized
through serde_json, which escapes control characters, so no raw registry
bytes reach the terminal.

Extract the registry-client builder duplicated from whoami into a shared
`cli_args::registry_client::build_registry_client` and switch whoami to it.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-26 03:05:23 +02:00
Zoltan Kochan
2c7369dbab feat(pacquet-cli): port the pack-app command (#12669)
Ports pnpm's `pack-app` command into pacquet. It packs a CommonJS entry
file into a standalone executable for one or more target platforms,
embedding a Node.js binary through the Node.js Single Executable
Applications API.

Reuses pacquet's node-version resolution, network client, detect-libc,
and `node@runtime:` installs. Two intentional divergences from pnpm,
both forced by pacquet being a Rust binary rather than a Node.js script:
the SEA builder is always downloaded (pnpm reuses its running
interpreter), and the runtime install spawns the pacquet binary
(mirroring pnpm's runPnpmCli).

Ports the upstream Jest validation suite (17 tests) covering the
fail-fast branches. The download / SEA-injection / signing paths need
network, Node.js, and subprocesses and are out of unit-test scope,
matching upstream coverage.

Ported from pnpm/pnpm@9f3df6b9b4.
2026-06-26 01:49:05 +02:00
Zoltan Kochan
6312916595 feat(pacquet-cli): port the config command (#12672)
Port pnpm's `config` command (set, get, delete, list; alias `c`) from
`@pnpm/config.commands`, matching its file routing, key validation, value
casting, and output.

Supporting infrastructure ported into `pacquet-config`:
- `config_types`: the merged `types` registry plus `is_ini_config_key` /
  `is_config_file_key` predicates.
- `naming_cases`, `property_path`, `protected_settings`.
- `Config::explicit_settings` / `raw_auth_config` / `config_dir` as the
  stand-ins for pnpm's `explicitlySetKeys` / `authConfig` / `configDir`,
  populated while loading the merged config.

`pacquet-workspace-manifest-writer` gains a generic, format-preserving
`update_manifest_field` (scalar/object set + delete), and a small INI
read-modify-write helper backs `.npmrc` / `auth.ini` writes.
2026-06-26 00:23:34 +02:00
Zoltan Kochan
3fe10ac744 ci: add Oz cloud triage workflow for new issues (#12673)
* ci: add Oz cloud triage workflow for new issues

Wire Warp's Oz "cloud factory" issue-triage stage onto pnpm/pnpm as a
GitHub-triggered cloud agent (part of the pnpm x Warp OSS partnership).

- .github/workflows/triage-issues.yml runs on issues:[opened], skips
  Bot-authored issues, and invokes warpdotdev/oz-agent-action@v1 with the
  triage skill. Minimal permissions: contents:read, issues:write.
- .agents/skills/triage/SKILL.md adapts the canonical triage rubric to pnpm:
  it maps the four readiness states onto existing pnpm "state:" labels
  (accepted / needs design / needs steps to repro / blocked / rejected)
  instead of creating new ones, is aware of the TypeScript/pacquet/pnpr split
  and the install/add/update/remove parity rule, and is conservative about
  removing maintainer-applied labels.

Triage-only for now; the implementation/auto-PR stage is intentionally not
included. Activation also requires the WARP_API_KEY secret and the Oz by Warp
GitHub App with the org enabled in Warp's Admin Panel.

* ci: address CodeRabbit review on Oz triage PR

- Pin actions to commit SHAs (actions/checkout to the repo's v7.0.0 pin,
  warpdotdev/oz-agent-action to the v1 tag commit) with version comments,
  matching the repo convention and satisfying zizmor.
- Harden the triage skill against prompt injection: treat all fetched issue
  content (title, body, comments, attachments, linked docs) as untrusted data,
  never as instructions, and forbid actions beyond the defined read/label/comment
  steps.
- Make the non-bug "Needs info" path consistent across the decision rule, the
  label-application step, and the result template: apply no state label and post
  clarifying questions instead of forcing an ill-fitting label.
2026-06-26 00:21:22 +02:00
Zoltan Kochan
3d7d2d17e7 feat(pacquet): port dist-tag command (#12667)
Port the native dist-tag command to pacquet so the Rust CLI can manage package distribution tags without delegating to another client.

The new command mirrors pnpm's TypeScript behavior for subcommand dispatch, default list handling, exact semver validation on add, latest-tag removal refusal, scoped registry/auth selection, dist-tag URL encoding, OTP headers, and sorted list output.

Add spawned-CLI integration coverage against a mock registry for list/add/remove behavior, alias handling, registry override, scoped auth, empty output, versioned package specs, and error cases.
2026-06-26 00:02:00 +02:00
Alessio Attilio
af5827376b feat(pacquet-cli): add list, ls, ll, and la commands for local and global package inspection (#12664)
* feat(pacquet-cli): add list, ls, ll, and la commands for local and global package inspection

* chore(pacquet-cli): split list tests into separate test file

* fix(pacquet-cli): align list with pnpm flags, filtering, peers, and error handling

- Changed --production to --prod (matching pnpm CLI surface)
- lockfile_only now branches between current/wanted lockfile
- Selects correct workspace importer instead of always root_project()
- Filters params after full tree build, preserving transitive matches
- Walks optional_dependencies in snapshot traversal
- Skips peer DepNode push when --exclude-peers is set
- Propagates read_root_manifest errors instead of swallowing

---
Written by an agent (opencode, big-pickle).

* fix(pacquet-cli): address dylint violations and Qodo review comments

- Replace single-letter closure params with descriptive names
- Move inline tests to external file for dylint compliance
- Fix prod/dev tri-state logic to match pnpm computeInclude
- Resolve workspace importer by relative path instead of root fallback
- Normalize importer path separators for cross-platform lockfile lookup

* fix(pacquet-cli): replace remaining single-letter closures and star import in tests

* fix(pacquet-cli): propagate current lockfile load errors instead of swallowing

* fix(pacquet-cli): use standard mod tests; resolution instead of #[path]

* refactor(pacquet-cli): move list tests from src to tests/list_unit

* fix(pacquet-cli): address Qodo sanitize, tri-state, and depth reviews

- Apply sanitize() in print_label, color helpers, and parseable output
- Fix prod/dev tri-state: use has_both pattern like install.rs
- Replace i32 depth with RecursionLimit (Levels, ProjectsOnly, Unlimited)
- Move tests/list_unit/mod.rs to src/list_unit.rs (dylint compliance)

* fix(pacquet-cli): inline list unit tests instead of external module

Dylint perfrectionist::unit-test-file-layout rejects external test
modules not in the canonical nested location. Move tests inline in
list.rs as #[cfg(test)] mod tests to satisfy both dylint rules.

* fix(pacquet-cli): move list tests to list/tests.rs and fix link: DepNode.path

Dylint perfectionist::unit-test-file-layout with inline_style=external_only
requires tests in an external module at the canonical nested location:
list/tests.rs (sibling directory of list.rs in Rust 2024).

Also fix resolve_importer_dep and resolve_snapshot_children to populate
DepNode.path with link:target for linked workspace siblings instead of
leaving it as an empty string.
2026-06-25 19:27:11 +00:00
Khải
159bd27ecb feat(pacquet): pack (#12567)
* feat(pack): port the pnpm pack command to pacquet

Port `pnpm pack` to Rust: create a reproducible gzipped tarball from a
project, running the prepack / prepare / postpack lifecycle scripts,
building the publish manifest, computing the file list, and writing the
archive.

- Extract npm-packlist into a shared `pacquet-fs-packlist` crate so the
  git and directory fetchers and the new pack command share one copy,
  matching pnpm's standalone `@pnpm/fs.packlist` package.
- Extend `pacquet-exportable-manifest` with the full
  createExportableManifest: manifest obfuscation, workspace / catalog /
  jsr specifier rewriting, publishConfig hoisting, README embedding, and
  the bin / peerDependenciesMeta / repository transform.
- Add the `pacquet-pack` crate (single-project api plus JSON / text
  output) and wire `pacquet pack` into the CLI, including the recursive
  (-r) workspace sweep in topological order.

The dependency-converter composition is a straight function sequence
rather than upstream's higher-order combineConverters, and the tarball
write phase uses the capability-trait DI seam (a Host provider plus Fs*
traits) so its filesystem error branches stay testable without
process-global mutation.

Ported from
https://github.com/pnpm/pnpm/blob/cab1c11c69/releasing/commands/src/publish/pack.ts
https://github.com/pnpm/pnpm/blob/ef87f3ccff/releasing/exportable-manifest/src/index.ts

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4k3hPXaosUkKBPbnChg4q

* style(pack): rustfmt the pack test fixture

Apply `cargo fmt` to the `fixture` helper's `package.json` write, which
drifted after a manual edit. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4k3hPXaosUkKBPbnChg4q

* style(exportable-manifest): satisfy perfectionist dylint lints

- Add trailing commas to two multi-line macro invocations in the
  transform tests (perfectionist::macro_trailing_comma).
- Replace a Unicode ellipsis with ASCII "..." in a create.rs doc
  comment (perfectionist::unicode_ellipsis_in_docs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4k3hPXaosUkKBPbnChg4q

* style(pacquet): satisfy perfectionist single-letter and layout lints

- Rename the reporter generic `R` to `Reporter` (bounded `self::Reporter`)
  in pack's `api` / `run_scripts_if_present` and the CLI `pack` command,
  matching the convention already used in executor, git-fetcher, and
  package-manager (perfectionist::single_letter_generic).
- Rename `locale_compare_en`'s `a` / `b` parameters to `left` / `right`
  (perfectionist::single_letter_function_param).
- Move the inline `transform` test module into an external
  `transform/tests.rs` (perfectionist::unit_test_file_layout).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4k3hPXaosUkKBPbnChg4q

* test(pack): cover the prepack/prepare/postpack lifecycle path

Port pnpm's `pack: runs prepack, prepare, and postpack`
(releasing/commands/test/publish/pack.ts) so `run_scripts_if_present`
and `script_body` are exercised. The existing pack tests all pass
`ignore_scripts: true`, so that branch was never entered.

- runs_prepack_prepare_and_postpack (Unix-gated, like the executor's
  other script-running tests): with scripts enabled, each of prepack /
  prepare / postpack runs and leaves a marker file.
- pack_succeeds_when_scripts_enabled_but_absent: an empty `prepack`
  makes `script_body` yield `None` and `run_scripts_if_present` return
  early, so packing still succeeds without shelling out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4k3hPXaosUkKBPbnChg4q

* test(pack): port bundleDependencies, custom-directory, and peer/jsr tests

Port the in-scope pnpm pack / createExportableManifest tests that close
coverage holes in this PR's new code:

- pack: bundleDependencies (array) and bundleDependencies: true cover the
  fs-packlist bundle recursion and the node_linker: hoisted allow-path.
- pack: publishConfig.directory covers reading the manifest from the
  build-output dir while resolving workspace: deps against the project
  root's node_modules.
- createExportableManifest: a peerDependencies workspace: rewrite and a
  jsr: specifier without a version selector.

Ported from releasing/commands/test/publish/pack.ts and
releasing/exportable-manifest/test/index.test.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4k3hPXaosUkKBPbnChg4q

* docs(pack): pin pack.ts source citations to a valid commit

The `cab1c11c69` SHA the crate docs referenced does not exist upstream.
Repoint every `pack.ts` source link in `pack`'s module docs to
`54c5c0e028`, where pnpm's releasing command now lives under
`pnpm11/releasing/commands/...`, so the citations resolve.

* test(pack): port the transitive bundleDependencies pack test

Port pnpm's `pack: bundles transitive dependencies of bundled
dependencies (hoisted)` test. `top` is the only directly-bundled
package, but it depends on `nested`, which is hoisted to the root
`node_modules`; both must end up in the tarball. This exercises
`fs-packlist`'s transitive bundled-closure walk that landed via
pnpm/pnpm#12620 (resolving pnpm/pnpm#12602) and arrived on this branch
through the latest merge from `main`.

Also repoint the existing pack-test citations to the valid
`54c5c0e028` SHA, matching the docs fix in the surrounding crate.

* docs(cli): document the pacquet pack command

Add a `pacquet pack` section to the CLI README listing its flag support
(`--dry-run`, `--pack-destination`, `--json`, `--out`,
`--pack-gzip-level`, `--skip-manifest-obfuscation`, recursive mode), so
the new command ships with the same flag-status table the other
commands carry. Also pin the last `pack.ts` citation in `cli_args/pack`
to the valid `54c5c0e028` SHA, matching the rest of the crate.

* test(pack): assert the empty-prepack guard skips, not just succeeds

Mutation-testing the pack tests surfaced that
`pack_succeeds_when_scripts_enabled_but_absent` did not actually guard
the behavior its name and doc claim. It asserted only that packing
succeeds with an empty `prepack` script — but an empty lifecycle script
runs as a successful no-op, so removing `script_body`'s
`.filter(|s| !s.is_empty())` left the test green while the empty script
was now executed.

Record the emitted log events with the crate's `RecordingReporter`
idiom and assert no `pnpm:lifecycle` event fires, pinning the "returns
early without shelling out" half of the contract. With the empty-string
filter removed the empty `prepack` shells out and emits a lifecycle
event, so the test now fails — making it a real regression guard for
`script_body`.

* fix(pack): align manifest validation with pnpm and harden license injection

Address review feedback on the pack port:

- Validate `--pack-gzip-level` against 0-9 at the CLI boundary.
- Skip recursive-pack targets whose name/version are empty strings, so
  they match the pacquet_pack::api precondition.
- Stop coercing a non-object peerDependencies into an empty object in
  create_exportable_manifest; leave it in place like the other dep
  fields.
- Gate transform_required_fields on JavaScript truthiness to match
  upstream's `if (!manifest.name)` check instead of requiring a
  non-empty string.
- Gate the bundledDependencies guard on JS truthiness so an explicit
  `false` packs cleanly under the default non-hoisted linker.
- Only inject a workspace LICENSE when the entry resolves to a regular
  file, mirroring fast-glob's onlyFiles default and avoiding an "Is a
  directory" failure in the later read/size pass.

* fix(pack): skip symlinked workspace LICENSE to prevent file leakage

A symlinked workspace-root LICENSE was followed via fs::metadata, so a
symlink pointing outside the workspace could leak its target's bytes
into a sub-package's tarball. Use DirEntry::file_type (which does not
follow symlinks) so only a regular file is injected, mirroring the
symlink-skipping read_readme_file in exportable-manifest. Add a unix
regression test.

Also pin the empty-string version case in the required-field truthiness
test.

* fix(pack): write the tarball atomically to avoid symlink-redirected overwrite

Host::write used std::fs::write, which follows a symlink at the output
path and truncates in place. A repo-controlled symlink planted at the
tarball's output path could therefore redirect the write to clobber an
arbitrary file, and a crash mid-write could leave a truncated .tgz.

Write a sibling temp file, fsync it, then rename it over the target.
The rename replaces a symlink instead of following it and is atomic,
matching the write-file-atomic pattern pacquet-package-manifest already
uses for package.json. The mode is preserved on overwrite (else 0o644)
so the archive is not left owner-only. Add a unix regression test.

* fix(pack): reject path-traversal in version and main/bin packlist fields

Two manifest-controlled path-escape vectors, since manifests are
attacker-controlled per REVIEW_GUIDE:

- pacquet-pack interpolated the version into the default tarball name
  (<name>-<version>.tgz) after only a non-empty check, so a version
  containing a path separator could write the tarball outside the
  destination directory. Reject a version with a / or \.

- pacquet-fs-packlist force-included main/bin after only stripping a
  leading ./ or /, so ../secret (or a Windows drive/UNC path) escaped
  the package root and was read into the tarball/CAS — the packlist
  also feeds the git fetcher on untrusted packages. Require the
  normalized field to stay inside the package (only normal components,
  no backslash) and to be a regular file via symlink_metadata so a
  symlinked main/bin can't leak an out-of-tree target.

Add regression tests for both.

* fix(pack): reject --out with --pack-destination in recursive mode

Recursive pack resolved a shared destination before `api` ran, so the
`--out` branch silently dropped `pack_destination` and the mutually-
exclusive pair was accepted — unlike single-project pack, which rejects
it with ERR_PNPM_INVALID_OPTION. Validate the pair up front in the
recursive path so both modes behave the same.

* fix(pack): stream the tarball, honor unsafe_perm, and harden output

Address the remaining Qodo findings on the pack port:

- Stream the gzip+tar archive into the atomic temp file instead of
  buffering the whole archive in a Vec, so peak memory no longer scales
  with the packed size. Per-file reads stay in memory, matching pnpm's
  readFileSync. The FsWrite seam becomes FsAtomicWrite, taking a
  streaming write body.
- Thread Config.unsafe_perm into PackOptions instead of hardcoding
  unsafe_perm = true for prepack/prepare/postpack, so packing honors the
  same policy (and TMPDIR isolation) as pnpm.
- Strip control characters from the text pack output, so a filename with
  raw ANSI escapes can't spoof the terminal. JSON output is left as data.
- Reject an --out value that resolves to no file name (., .., "") up
  front instead of failing later with a confusing OS write error.
- Sort the contents list with one cached lowercase key per item instead
  of recomputing to_lowercase on both sides per comparison, and check
  bin executability against a HashSet instead of a linear scan.

Add regression tests for the --out and control-character cases.

* fix(pack): canonicalize main/bin containment and harden tarball mode

Two follow-ups from review:

- The main/bin force-include verified containment only on the path
  string, so an intermediate symlinked directory (subdir -> /outside)
  could still resolve a host file into the packlist. Verify the
  canonical resolved path is strictly inside the canonical package root
  instead, which also follows symlinks that stay in-package (matching
  npm-packlist) while failing closed on any escape.
- The atomic tarball write read the destination's mode with metadata
  (follows symlinks), so a symlink at the output path could donate its
  target's or a directory's mode to the archive. Use symlink_metadata
  and only inherit from an existing regular file, and set the mode
  before syncing so content and metadata flush together.

* test(package-manager): adapt patch-commit escape test to packlist filtering

Hardening the shared packlist to drop `..`-escaping main/bin fields
means patch-commit's `prepare_pkg_files_for_diff` no longer receives an
escaping entry, so the old integration test's `expect_err` no longer
fires. Unit-test patch-commit's own `safe_package_file_path` guard
directly (defense in depth), and rework the integration test to assert
the escaping `main` is filtered and never materialized into the diff
view.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-25 18:54:09 +00:00
Juan Picado
6aa5b70595 feat(pnpr): multiple-uplink fallback ordering per package (#12648)
Let a `proxy:` rule name an ordered list of uplinks
(`proxy: npmjs private`, or the YAML sequence form) and walk it as a
fallback chain, matching verdaccio's serial first-success semantics:
try uplinks in declared order, stop at the first that answers, and fall
through to the next on a 404 or an availability failure. A later private
uplink can host a package the primary 404s.

- config: PackageAccess.proxy becomes an ordered AccessSpec list;
  Config::resolve_uplinks returns the chain (skipping unknown names);
  resolve_uplink stays as a primary-only convenience.
- packument: walk the chain, sending each uplink only its own cached
  validators. The cache holds one shared body, so its validators are
  scoped to that body's origin uplink (the validator map is replaced, not
  merged, on every write) — a conditional GET only ever reaches the
  origin, so a 304 can only confirm the bytes actually on disk and can't
  revalidate another origin's body. The freshness window (maxage) comes
  from the primary uplink only.
- tarball: try each uplink in order; integrity binds the streamed bytes,
  so the serving uplink decides whether they are mirrored (caches())
  while the cache read is gated on whether any uplink caches.
- fallthrough is restricted to availability failures
  (RegistryError::allows_uplink_fallthrough): transport errors, an open
  circuit, and upstream 5xx. Any authoritative 4xx (401/403 auth, 429
  throttle, 400/410, ...) stops the walk immediately so a later uplink
  can't mask the primary's rejection of a scoped or rate-limited request.

Out of scope (left to a later change): verdaccio's full cross-uplink
metadata merge and per-version origin tracking. pnpr stays first-success
fallback with one cache entry per package.

Part of pnpm/pnpm#11973.
2026-06-25 20:53:27 +02:00
Zoltan Kochan
9f3df6b9b4 ci(pacquet): shrink test build debuginfo to avoid ENOSPC on the ubuntu runner (#12665)
The Rust CI test job's `cargo nextest` build holds a full set of
`--all-targets` debug binaries for the whole workspace (~60 crates plus
heavy C deps such as aws-lc-sys, ring, libsql-ffi, and tree-sitter) in
target/. That had grown close to the hosted ubuntu runner's free-disk
ceiling even after the fixed `Free up runner disk space` step, and a run
crossed it: the linker died with "No space left on device" while building
an unrelated crate's test binary (macOS and Windows were unaffected).

Build the test artifacts with line-tables-only debug info via job-level
CARGO_PROFILE_DEV_DEBUG / CARGO_PROFILE_TEST_DEBUG env. This drops most of
the debuginfo bytes that fill target/ while keeping panic backtraces
file:line-accurate. Setting it in the workflow rather than Cargo.toml
leaves local `just test` builds with full debug info.
2026-06-25 19:24:37 +02:00
Zoltan Kochan
c50c6aaf6b feat(pacquet-cli): port audit signatures (#12663)
Port pnpm's `pnpm audit signatures` registry-signature verification to
pacquet. For every installed package, the package's own registry is
queried for its signing keys (`/-/npm/v1/keys`) and full packument; the
package is verified as soon as one of its `dist.signatures` validates
over the message `name@version:integrity` against a trusted ECDSA-P256
key. Registries that advertise no signing keys are skipped (no trust
root); a package whose registry provides keys but whose signature is
absent is reported as missing, and one whose signature is present but
does not validate as invalid (a tamper signal). Exit code 1 when any
package is missing or invalid; `--json` emits the structured report.

Mirrors pnpm's deps.security.signatures package and the auditSignatures
command handler, including the per-package packument-error handling, the
"one valid signature wins / prefer the tamper reason" logic, the
key-expiry consistency check, encodeURIComponent-faithful packument URLs,
the report wording, the error codes, and the JSON result shape.

Adds the `p256` (RustCrypto) crate for ECDSA-P256/SHA-256 verification;
it was already present transitively in the lockfile.
2026-06-25 18:25:57 +02:00
A
b0304429b0 feat(pacquet): port whoami command (#12649)
* feat(pacquet): port whoami command

* fix(cli): redact registry URL from whoami error context

* fix(cli): sanitize whoami username before printing
2026-06-25 15:38:16 +00:00
Nikita Zhukovskii
32d4d3f1f0 feat(cli): port the root command to pacquet (#12632)
* feat(cli): port the `root` command to pacquet

Add `pacquet root`, ported from pnpm11/pnpm/src/cmd/root.ts.

Prints `<dir>/node_modules` built from the canonicalized `--dir`. It hardcodes
the `node_modules` leaf and deliberately does not read `config.modules_dir`:
pnpm's handler ignores a configured modules-dir, and pacquet re-anchors
`config.modules_dir` to the workspace root inside a workspace, whereas pnpm's
`root` uses `config.dir` (the cwd). Anchoring on `--dir` keeps parity in both
cases.

`--global` / `-g` is rejected with a "not supported yet" error: pacquet has no
global packages directory yet, deferred to a shared follow-up that also unblocks
`bin -g`.

Related to pnpm/pnpm#11633.

* fix(cli): gate `root` differential test on Windows and type its `--global` error

The `root_matches_pnpm_from_a_workspace_subdir` differential test spawns the
real `pnpm`, which on Windows is a `pnpm.cmd` shim that `std::process::Command`
cannot resolve via `PATHEXT` ("program not found"). Gate it with
`#[cfg_attr(target_os = "windows", ignore)]`, matching the `cfg(unix)` gating in
`pnpm_compatibility` and `hoist`. The other three `root` tests spawn only
`pacquet` and keep running on Windows.

Replace the bare `miette::miette!` rejection of `pacquet root --global` with a
typed `RootError::GlobalUnsupported` diagnostic (code
`pacquet_cli::root_global_unsupported`), mirroring `runtime`'s `GlobalUnsupported`,
so the refusal carries a stable error code.
2026-06-25 15:29:47 +00:00
Zoltan Kochan
47685fddab fix(pnpr): allow metadata-only re-publish so pnpm deprecate works (#12659)
pnpm/pnpm#12646 rejects any re-PUT of an already-hosted version with 409 to
keep published content immutable. But `pnpm deprecate`/`undeprecate` work by
GET-ing the packument, flipping the `deprecated` flag, and re-PUT-ting it
with no attachments and the hosted `dist` unchanged. That metadata-only
update tripped the 409 guard, breaking test/deprecate.ts across the e2e
suite (on every PR, via the merge commit).

Narrow the guard to reject only a content re-publish of a clashing version:
one that carries a new tarball (an attachment) or changes `dist.integrity`.
Integrity is the content anchor clients verify against; the `tarball` URL is
rewritten on read, so a faithful deprecate re-PUT carries a different URL but
the same integrity and must be allowed. A metadata-only clash now passes, and
merge_manifest preserves the hosted version's `dist`, so a metadata update can
never repoint a version's integrity or tarball.

Adds pnpr integration tests: a deprecate-style update is allowed and keeps the
hosted `dist`; the attachment re-publish and the no-attachment integrity
smuggle stay 409.
2026-06-25 16:15:33 +02:00
Alessio Attilio
208b6921b1 feat(pacquet-cli): add unlink command (#12590)
Ports the unlink command from pnpm to pacquet. UnlinkArgs::run builds a
list of packages to remove: the explicitly provided names or all
link:-versioned dependencies across all groups (prod, dev, optional). If
the list is empty the command is a no-op. It calls
PackageManifest::remove_dependencies, saves the manifest, and delegates
to Install which cleans up the stale symlink. No changeset because
pacquet is not a published package. Related to pnpm/pnpm#11633

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-25 16:14:27 +02:00
YES!HYUNGSEOK
dd8ea85662 fix(package-manifest): write package.json atomically via a temp file (#12642)
PackageManifest::save truncated package.json in place with File::create + write_all. A crash,
OOM, or ENOSPC between the truncate and the completed write left the file empty or partial,
destroying the user's dependency declarations with no recovery path. add/remove/update and
set-script all go through this path.

Write the serialized manifest to a sibling temp file, fsync it, then rename it over the target,
matching the write-file-atomic guarantee the TypeScript pnpm CLI relies on. The crate's sibling
workspace-manifest-writer already uses this pattern.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-25 15:38:47 +02:00
YES!HYUNGSEOK
114e4f5aac ci: gate the pacquet release publish job behind the release environment (#12641) 2026-06-25 15:36:01 +02:00
YES!HYUNGSEOK
0c44bb94e6 ci: gate the pnpr release publish and docker jobs behind the release environment (#12640)
pnpr-release-to-npm.yml can be triggered via workflow_dispatch. Its publish job uses npm OIDC
trusted publishing to publish the `@pnpm/pnpr` package, and its docker job pushes the mutable
ghcr.io/pnpm/pnpr:latest tag. Neither had an environment gate, so any collaborator with write
access could publish to npm and overwrite the mutable Docker tag without reviewer approval.

Add an environment: release gate to the publish and docker jobs (the build job produces no
externally visible artifacts and is intentionally left ungated), matching the protection used
by release.yml.

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-25 15:34:58 +02:00