Commit Graph

11458 Commits

Author SHA1 Message Date
Zoltan Kochan
b4f8f47ac2 fix: update @zkochan/cmd-shim to 9.0.3 (#11595)
Picks up the MSYS path-translation fix from pnpm/cmd-shim#55: the sh shim
written for `.cmd` / `.bat` targets now escapes the `/C` switch as `//C`
so Git Bash passes it through to cmd.exe unchanged. Without this, a bare
`/C` was rewritten to `C:\` before reaching cmd.exe — cmd started
interactively and the calling script saw cmd's banner instead of the
wrapped command's output. Affects any cmd-shim-wrapped batch script
invoked from Git Bash / MSYS / Cygwin on Windows.
2026-05-12 08:24:05 +02:00
Zoltan Kochan
b8223ec6fc fix(ci): pass tag_name to softprops/action-gh-release
v3 of the action requires an explicit tag_name when GITHUB_REF is not
a tag ref. The release workflow runs on pushes to main, so the API
rejected the create-release call with "Missing tag_name parameter"
once dependabot bumped the action from v1 to v3 in 70eb8538.
2026-05-12 02:29:50 +02:00
Zoltan Kochan
fdbdc09121 chore(release): 0.2.2-6 2026-05-12 02:16:36 +02:00
Zoltan Kochan
9edcb40c66 docs(CHANGELOG): fixed link to named registries PR 2026-05-12 02:09:40 +02:00
Khải
bbfa18f12b feat: bin (#333)
Resolves #330.

Mirrors pnpm v11's `@pnpm/bins.resolver`, `@pnpm/bins.linker`, and `@zkochan/cmd-shim` (resolved at pnpm `4750fd370c` and cmd-shim `0d79ca9534`):

- New `pacquet-cmd-shim` crate parses the `bin` field of a package manifest into commands. Both `bin: string | object` and the `directories.bin` glob fallback are supported, with the same path-traversal and URL-safe-name guards as upstream. Conflict resolution follows `pkgOwnsBin` + lexical compare.
- Shim generator writes all three flavors per bin: Unix `/bin/sh` (byte-compatible with pnpm's `generateShShim`), Windows `.cmd` (CRLF, `%~dp0` paths), and PowerShell `.ps1` (`$basedir`/`$exe` header with Windows/POSIX-pwsh detection). Mirrors `generateCmdShim` and `generatePwshShim` minus the unused `nodePath`/`prependToPath`/`progArgs` branches.
- All filesystem IO behind per-capability DI traits — `FsReadHead`, `FsReadFile`, `FsReadString`, `FsReadDir`, `FsWalkFiles`, `FsCreateDirAll`, `FsWrite`, `FsSetExecutable`, `FsEnsureExecutableBits` — following the pattern documented in [#332](https://github.com/pnpm/pacquet/pull/332#issuecomment-4345054524). `FsWrite` is a thin shim over `std::fs::write` and is **not atomic**; a future atomic-write capability can build on it without renaming. Production callers turbofish the unit-struct provider `::<RealApi>`; tests inject unit-struct fakes to cover IO error paths real fs can't trigger portably. Generic param is named `Api` (not `Fs`) so the same provider can grow non-fs capabilities without a rename.
- New `LinkVirtualStoreBins` step walks each virtual-store slot and links its child packages' bins into the slot's own `node_modules/.bin`, matching `linkBinsOfDependencies` in pnpm's `building/during-install`. Slot iteration, per-slot child reads, and shim writes parallelised on rayon.
- `SymlinkDirectDependencies` and `InstallWithoutLockfile` call the bin linker after the symlink layout is in place. Direct-dep linking and the symlink loop are exposed as free `link_direct_dep_bins` / `symlink_direct_deps_into_node_modules` functions so they're unit-testable without the mock-registry scaffold.

Tests port the relevant `bins/resolver` and `bins/linker` cases from pnpm (`directories.bin` discovery, scoped bin names, dangerous locations, idempotent shim skip, conflict resolution) and add fakes-injected coverage for the IO error variants.

**Not in this PR**: hoisted-bin precedence and lifecycle-script-created bins. Both depend on subsystems pacquet hasn't built yet — hoisting (`hoistPattern` / `publicHoistPattern` resolution + the lift step) and lifecycle scripts (`exec/lifecycle/` runner + `allowBuilds` plumbing). The bin-linking side already does the right thing for both (shims are paths, so they pick up files generated post-extract; the precedence rule is a small addition once hoisting tags candidates as hoisted). Tracked separately in  with full porting coordinates.



## Performance

When the bin-linking step landed it added ~7% to the warm-cache Frozen Lockfile install ([benchmark](https://github.com/pnpm/pacquet/pull/333#issuecomment-4344568317)). Closing that gap took a sequence of changes that mirror how pnpm v11 avoids the same costs in its own `linkBinsOfDependencies` ([4750fd370c](https://github.com/pnpm/pnpm/blob/4750fd370c/building/during-install/src/index.ts#L258-L309)):

- **Bundled manifests in `index.db`.** `extract_tarball_entries` captures the parsed `package.json`, picks the subset pnpm caches via a port of `normalizeBundledManifest` (`name`, `version`, `bin`, `directories`, `dependencies`, lifecycle `scripts`, …), and stuffs it into `PackageFilesIndex.manifest`. `prefetch_cas_paths` surfaces it back as a `HashMap<cache_key, Arc<Value>>` so the bin linker doesn't have to re-read `package.json` per child off disk.
- **msgpackr-records encoder learns `serde_json::Value`.** The encoder previously errored with `ManifestNotSupported` if anything ever set `manifest`; now it records-encodes nested JSON objects (slot-allocated per distinct key set) so `useRecords: true` readers see them as JS `Object`s rather than `Map`s — necessary for pnpm's `manifest.bin` / `manifest.directories?.bin` property accesses to resolve.
- **`Arc<Value>` for `PackageBinSource.manifest`.** The lockfile-driven bin-link path produces `slots × children` clones of the parsed manifest (~6k on the integrated-benchmark fixture). Sharing via `Arc` turns each push into a refcount bump.
- **`hasBin` filter via lockfile.** `LinkVirtualStoreBins` now takes the lockfile `packages:` section, pre-computes an `Option<HashSet<PackageKey>>` of `hasBin: true` keys at install start, and skips snapshot children that aren't in the set *before* any path-building or manifest lookup. ~95% of a real lockfile's packages don't declare a bin, so this short-circuits the bulk of the per-slot work. `None` is reserved for the pathological case where the lockfile lacks a `packages:` section (fall back to processing every child); `Some(empty)` is authoritative — lockfile says no package has a bin, every slot short-circuits. Mirrors pnpm's filter at `building/during-install/src/index.ts:283`.
- **Lockfile-driven slot iteration.** `run_lockfile_driven` walks `snapshots` directly and builds slot paths lexically. The previous `run_with_readdir` path enumerated every slot via `Api::read_dir(virtual_store_dir)` and probed each slot's own package directory with a wasted `read_dir` (~1267 wasted `open(O_DIRECTORY) + close` per warm install). The readdir-driven path survives as a fallback for `install_without_lockfile` and retains the existence probe there (small N, no virtual-store invariant from `create_virtual_dir_by_snapshot` to lean on).
- **Parallel prefetch decode.** `StoreIndex::get_many_raw` returns undecoded row bytes; the SQLite mutex releases before `prefetch_cas_paths` fans the msgpackr decode + integrity check across rayon.

Latest CI benchmark on this branch ([job 75462998251](https://github.com/pnpm/pacquet/actions/runs/25701679172/job/75462998251)):

| Scenario | pacquet@HEAD | pacquet@main | Relative |
|---|---|---|---|
| Frozen Lockfile | 2.074s ± 102ms | 2.026s ± 70ms | +2.4% (within noise) |
| Frozen Lockfile (Hot Cache) | **479.6ms ± 38ms** | **535.8ms ± 37ms** | **~10% faster than main** |

The follow-up #417 splits manifests into a dedicated `package_manifests` SQLite table so the unfiltered `package_index` prefetch payload stays at its original size and the manifest fetch becomes a targeted SELECT against just the `hasBin: true` subset.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-05-12 02:07:14 +02:00
Zoltan Kochan
eb2ae4d32b docs(LICENSE): sync with pnpm/pnpm repo 2026-05-12 01:54:09 +02:00
Zoltan Kochan
02e9cf5b67 fix(deps.status): skip engine check when scanning workspace projects (#11592)
* fix(deps.status): skip engine check when scanning workspace projects

checkDepsStatus (run by verifyDepsBeforeRun) called findWorkspaceProjects
without a nodeVersion, so the engine check fell back to the system Node from
PATH and emitted spurious "Unsupported engine" warnings before scripts ran.
The actual install still does engine validation; a status check shouldn't.

* test(pnpm): cover engine warning regression for verifyDepsBeforeRun

* docs(changeset): clarify scope of the skipped validation

* test(pnpm): also check stderr for unsupported engine warning
2026-05-12 00:22:57 +02:00
Khải
a8e8dd3091 ci: setup dylint with perfectionist (#416)
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-12 02:11:19 +07:00
Zoltan Kochan
732312f49e chore(release): 11.1.0 v11.1.0 2026-05-11 19:56:10 +02:00
Zoltan Kochan
a575dd2461 fix: allow pnpm runtime set in workspace root (#11584)
* fix: allow `pnpm runtime set` in workspace root

`pnpm runtime set <name> <version>` previously failed in the root of a
multi-package workspace with the `ADDING_TO_ROOT` error. Since installing
a runtime is workspace-wide configuration, pass `--workspace-root` to the
underlying `pnpm add` call when not running globally.

* fix: use --ignore-workspace-root-check instead of --workspace-root

The previous fix forced every non-global runtime install to land in
the workspace root, which broke running the command from a workspace
subproject. Switch to --ignore-workspace-root-check, which only
suppresses the safety warning and leaves the install target as the
current directory.
2026-05-11 19:53:44 +02:00
Zoltan Kochan
4b25a3dfa8 fix: install each global package in its own isolated directory by default (#11588)
* fix: install each global package in its own isolated directory by default (#11587)

`pnpm add -g foo bar` now installs `foo` and `bar` as separate isolated
globals — removing one no longer wipes out the other. Packages can still
be bundled into a single isolated install with a comma-separated list:
`pnpm add -g foo,bar qar` keeps foo+bar together and qar separate.

* chore: downgrade changeset to patch

* fix: do not split commas inside local paths or URL selectors

`splitCommaSeparated` now detects path-like params (`./`, `/`, `~`,
`file:`, `link:`, Windows drive paths) and URLs (anything containing
`://`), and skips splitting when the param as a whole resolves to an
existing local path. Plain package specs like `foo,bar` are still
split as before. Adds an e2e regression test using a local package
whose directory contains commas.

Also reword the changeset bullet so the example sentence doesn't end
abruptly at the issue link.

* fix: consolidate global add summary so every installed package is listed

`pnpm add -g foo bar` runs each space-separated arg as its own isolated
install, but the default-reporter's summary pipeline takes the first
`summary` log event and unsubscribes — so only the first group's
"global: + X" block was printed and later groups disappeared from the
summary even though they had been installed correctly.

Adds an `omitSummaryLog` install option that suppresses the per-install
summary log inside `mutateModules`. `handleGlobalAdd` enables it for
each group and emits a single consolidated summary log at the very end,
so the reporter prints one "global:" block listing every package that
was added across all groups.

* chore: update tsconfig refs after adding @pnpm/core-loggers dep

* fix: show per-prefix stats and progress when global add installs multiple groups

When `pnpm add -g` is given more than one CLI param (and so installs
several isolated groups), force the reporter to use its prefixed
progress/stats output. Without that, the single-prefix stats pipeline
limits emissions to one install via `take(2)`, so only the first
group's "Packages: +N" line is printed and later groups' stats are
silently dropped. Each group now shows its own progress and stats line
labelled with the install dir, and the consolidated "global:" summary
still prints once at the end.

Single-package `pnpm add -g foo` output is unchanged.

* chore: bump @pnpm/installing.deps-installer in changeset

The new omitSummaryLog install option is consumed by global.commands,
so deps-installer needs a version bump alongside it.
2026-05-11 19:53:22 +02:00
Zoltan Kochan
20e7affbe2 fix: publish web-auth honors proxy when polling doneUrl (#11581)
- `pnpm publish` failed to complete the web-based authentication flow when an HTTP/HTTPS proxy was configured. `libnpmpublish` (used for the initial publish request) routes through the proxy, but the subsequent `doneUrl` polling went through `@pnpm/network.fetch` without forwarding any proxy/TLS settings. The registry rejected the poll with `403` because the source IP differed from the initial request, so publish hung on the QR-code prompt forever.
- Adds `createDispatchedFetch(opts)` to `@pnpm/network.fetch` — a curried `fetchWithDispatcher` that pre-binds proxy / TLS / local-address / `configByUri`-derived client certificates. `publishPackedPkg` uses it to build an `OtpContext` whose `fetch` honors the same network configuration as the publish request.
- `extractTlsConfigs` is now performed automatically inside `createDispatchedFetch` (and hoisted out of the per-request loop in `createFetchFromRegistry`), so callers only have to pass `configByUri` once.

Fixes #11561.
2026-05-11 16:22:25 +02:00
Zoltan Kochan
91b0e64048 fix: terminate worker pool on short-circuit returns from pnpm/main (#11571)
`pnpm` could hang for the lifetime of the worker pool after a command logically finished whenever the code path returned through `main` without `process.exit(...)`.

`main.ts` only ran `finishWorkers()` inside the command-handler `finally` block. Short-circuit returns that came earlier — `--version`, `--help`, fatal config errors that surface before a command runs — bypassed it and left the integrity-resolution worker pool active.

The CLI entry (`pnpm/src/pnpm.ts`) now runs `finishWorkers()` in its own `finally`, so every exit path tears down the pool. Cleanup rejections are swallowed so the `finally` never masks the real command result.

## Reproducer

```bash
echo '{"devEngines":{"packageManager":{"name":"pnpm","version":"11.0.9","onFail":"download"}}}' > package.json
time pnpm --version
# before (Linux): prints 11.0.9, then hangs for the worker-pool lifetime
# after:          prints 11.0.9, exits in ~1-3 s
```

`switchCliVersion` resolves the integrity (spawning workers), finds nothing to swap (range/version already matches running pnpm), and returns through main's `--version` short-circuit. The unterminated workers used to keep the event loop alive.

Surfaced in pnpm/action-setup#254 — bumping the action's bootstrap to 11.0.9 made every CI job using `devEngines.packageManager` with `onFail: "download"` hang.
2026-05-11 14:10:06 +02:00
dependabot[bot]
2f95d840c9 chore(cargo): bump tokio from 1.52.1 to 1.52.3 (#413)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.52.1 to 1.52.3.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.52.1...tokio-1.52.3)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.52.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-05-11 12:43:33 +02:00
dependabot[bot]
55b18a488c chore(cargo): bump serde-saphyr from 0.0.25 to 0.0.26 (#415)
Bumps [serde-saphyr](https://github.com/bourumir-wyngs/serde-saphyr) from 0.0.25 to 0.0.26.
- [Release notes](https://github.com/bourumir-wyngs/serde-saphyr/releases)
- [Commits](https://github.com/bourumir-wyngs/serde-saphyr/compare/0.0.25...0.0.26)

---
updated-dependencies:
- dependency-name: serde-saphyr
  dependency-version: 0.0.26
  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-05-11 12:42:53 +02:00
btea
8543b89fb2 feat: view command display published time (#11532)
* **New Features**
  * `pnpm view` now shows publish age (e.g., "published 2 hours ago") and, when available, publisher attribution ("by …"); invalid or future timestamps fall back to "just now".
  * Console styling for package metadata (name/version, license, counts, links, dependencies, maintainers) was improved for readability.

* **Tests**
  * Added tests verifying the published timestamp and publisher attribution appear in `pnpm view` output.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-11 12:28:04 +02:00
dependabot[bot]
5c5b333e17 chore(github-actions): bump codecov/codecov-action from 3 to 6 (#410)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 6.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v3...v6)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 12:24:08 +02:00
dependabot[bot]
1d67d0ec67 chore(cargo): bump sysinfo from 0.38.4 to 0.39.1 (#414)
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.38.4 to 0.39.1.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/main/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/compare/v0.38.4...v0.39.1)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-version: 0.39.1
  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-05-11 12:23:39 +02:00
dependabot[bot]
246485c3d5 chore(github-actions): bump EndBug/version-check from 2 to 3 (#412)
Bumps [EndBug/version-check](https://github.com/endbug/version-check) from 2 to 3.
- [Release notes](https://github.com/endbug/version-check/releases)
- [Commits](https://github.com/endbug/version-check/compare/v2...v3)

---
updated-dependencies:
- dependency-name: EndBug/version-check
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 12:22:06 +02:00
dependabot[bot]
89d53ba6b3 chore(github-actions): bump crate-ci/typos from 1.16.22 to 1.46.1 (#411)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.16.22 to 1.46.1.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.16.22...v1.46.1)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.46.1
  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-05-11 12:21:42 +02:00
Allan Kimmer Jensen
d685005de9 feat: lifecycle script execution and allowBuilds policy (#391)
## Summary

- Add lifecycle script runner to `pacquet-executor`: preinstall, install, postinstall in correct order, `binding.gyp` auto-detection, `INIT_CWD`/`PNPM_SCRIPT_SRC_DIR`/PATH environment setup
- Add `allowBuilds` build policy matching pnpm 11's default-deny behavior, with `dangerouslyAllowAllBuilds` escape hatch
- Wire build step into the frozen-lockfile install path
- Port `buildSequence` + `graphSequencer` so packages with `requires_build` run children-first
- Add `pnpm:lifecycle` and `pnpm:ignored-scripts` reporter channels — script start/stdio/exit events flow through the same NDJSON pipeline `@pnpm/cli.default-reporter` consumes from upstream pnpm
- Port 11 upstream lifecycle script tests as `known_failures`

Upstream refs:
- [`runPostinstallHooks`](https://github.com/pnpm/pnpm/blob/7e91e4b35f/exec/lifecycle/src/index.ts)
- [`runLifecycleHook`](https://github.com/pnpm/pnpm/blob/80037699fb/exec/lifecycle/src/runLifecycleHook.ts)
- [`createAllowBuildFunction`](https://github.com/pnpm/pnpm/blob/7e91e4b35f/building/policy/src/index.ts)
- [`buildSequence`](https://github.com/pnpm/pnpm/blob/80037699fb/building/during-install/src/buildSequence.ts)
- [`graphSequencer`](https://github.com/pnpm/pnpm/blob/80037699fb/deps/graph-sequencer/src/index.ts)
- [`lifecycleLogger`](https://github.com/pnpm/pnpm/blob/80037699fb/core/core-loggers/src/lifecycleLogger.ts)
- [`ignoredScriptsLogger`](https://github.com/pnpm/pnpm/blob/80037699fb/core/core-loggers/src/ignoredScriptsLogger.ts)

## What's implemented

- `run_postinstall_hooks` in `pacquet-executor` faithfully ports pnpm's hook runner. The hook is now generic over `R: Reporter` and emits `pnpm:lifecycle` events through the standard pacquet reporter pipeline: `Script` before each spawn, one `Stdio` event per stdout/stderr line read through `Stdio::piped()` byline buffering, and `Exit` after the script returns
- `AllowBuildPolicy` reads `pnpm.allowBuilds` and `pnpm.dangerouslyAllowAllBuilds` from `package.json` with correct tri-state semantics (allow/deny/ignored)
- Default-deny matches pnpm 11: packages must be explicitly listed in `allowBuilds` to run their scripts
- `BuildModules::run::<R>` runs lifecycle scripts for `requires_build: true` packages in the frozen-lockfile path and returns the sorted (peer-stripped) list of packages whose scripts were skipped because they weren't in `allowBuilds`. `InstallFrozenLockfile::run` emits one `pnpm:ignored-scripts` event with that list (always, even when empty), mirroring upstream
- `build_sequence` walks the dep graph from each importer, keeps only nodes whose subtree contains a build node, and feeds that subgraph into `graph_sequencer` to produce topologically ordered chunks. `BuildModules` iterates the chunks in order so children build before parents
- `AllowBuildPolicy` and `BuildModules.lockfile_dir` use the existing `requester` field (derived from `manifest.path().parent()`) instead of `std::env::current_dir()`
- New `LogEvent::Lifecycle` and `LogEvent::IgnoredScripts` variants with wire-shape tests
- Recording-fake reporter tests for `run_postinstall_hooks` (Script→Stdio→Exit ordering, non-zero exit propagation) and for `BuildModules::run` (sorted ignored-builds, explicit-deny exclusion). Existing `install_emits_pnpm_event_sequence` updated to expect the new `IgnoredScripts` event between `Stats` and `ImportingDone`
- Unit tests for the build policy, key parsing, `graph_sequencer`, and `build_sequence`

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-05-11 12:20:33 +02:00
Pig Fang
6bd335e65c fix: change message for case that downgrading pnpm (#11560) 2026-05-11 12:13:52 +02:00
Zoltan Kochan
27499b90eb chore: added codeowner 2026-05-11 02:33:19 +02:00
Zoltan Kochan
c64fbe4d1c docs: gate PR scope to stage 1 of the roadmap (#409)
* docs: gate PR scope to stage 1 of the roadmap

Add a "Scope and Roadmap" section to `CONTRIBUTING.md` and a
`.github/pull_request_template.md` so contributors learn the scope rule
before they write code:

- pacquet only takes work that is listed under stage 1 of the roadmap
  (#299). Stage 2+ items, and any new top-level commands, are out of
  scope and will be closed.
- Stage 1 covers a single command, `install`. Configuration arrives
  through pnpm settings (`.npmrc`, `pnpm-workspace.yaml`), not new
  command-line flags.
- Opening an issue first is optional when the change is in stage 1 and
  exactly mirrors the pnpm CLI; otherwise required.
- Deviating from pnpm's behavior is not an option. Behavior changes
  must land in `pnpm/pnpm` first and be ported afterwards.

* docs: address Copilot review feedback on #409

- Reword the "single command" line: pacquet's CLI today exposes other
  top-level commands. Stage 1 is the focus, not the only thing that
  exists; new top-level commands remain out of scope.
- Drop the mid-sentence em dash from the parity-rule sentence so the
  new text obeys the Writing Style guidance present in the same file.
2026-05-11 01:49:29 +02:00
Khải
e50f912f6e feat(package-manager): write .modules.yaml after install (#407)
* feat(package-manager): write `.modules.yaml` after install

Mirror upstream pnpm's `writeModulesManifest` call at the end of a
successful install (deps-installer/src/install/index.ts L1608-L1630
on commit 086c5e91e8) so `node_modules/.modules.yaml` is persisted
with the resolved layout: `layoutVersion`, `nodeLinker`, the
`included` set derived from the dispatched dependency groups, the
store and virtual-store directories, the `default` registry, and
the `pacquet@<version>` package-manager identifier. This is the
contract a follow-up install (or another tool) keys off to detect
a layout change and prune accordingly.

Adds a focused test that runs an empty-snapshot frozen install and
asserts the on-disk manifest fields, mirroring upstream's
`installing/modules-yaml/test/index.ts` style.

* test(package-manager): use `pipe_as_ref` for modules-yaml read

Address KSXGitHub's review on #407: thread the `modules_dir` path
through `pipe_as_ref` instead of the bare function call, matching
the pipe-trait style used in `crates/modules-yaml/tests/index.rs`.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-09 23:32:55 +02:00
Khải
ef0fe2c01b docs(cli): fix long and wrong help messages (#405)
* docs(cli): trim verbose --reporter doc comments

The doc comments on `CliArgs.reporter` and `ReporterType` had grown into
multi-paragraph explanations with markdown backticks, internal-attribute
notes (`global = true`), and GitHub issue references that all leaked
straight into `pacquet --help` output. Match pnpm's terse style: a single
short line on the field, no enum-level prose. Per-variant docs already
describe what `ndjson` and `silent` do.

* docs: re-add docs with improvement

* docs: more specific message

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-09 22:16:42 +02:00
Khải
0df762b02c feat: propagate pnpm-workspace.yaml load errors (#404)
Resolves <https://github.com/pnpm/pacquet/issues/402>.

## What this PR does

Pacquet already reads `pnpm-workspace.yaml` (`crates/npmrc/src/workspace_yaml.rs` defines `WorkspaceSettings` and `Npmrc::current` walks up from cwd to layer it on top of `.npmrc`). The remaining divergence from pnpm 11 was on the error path. Upstream's [`workspace-manifest-reader`](https://github.com/pnpm/pnpm/blob/8eb1be4988/workspace/workspace-manifest-reader/src/index.ts) silently treats `ENOENT` as "no manifest" and propagates every other failure (read errors, parse errors, `EISDIR`, permission denied, …). Pacquet swallowed all errors via `if let Ok(Some(...))`, and its `find_workspace_manifest` helper used an `is_file()` filter that also masked non-regular entries (e.g. a directory named `pnpm-workspace.yaml`).

This PR closes both gaps.

## Changes

- `crates/npmrc/src/workspace_yaml.rs`
  - Replace the bare `Debug`-only `LoadWorkspaceYamlError` with a `Display + Error + Diagnostic` enum that stores `path` + `source` directly on each struct variant, mirroring `pacquet_modules_yaml::ReadModulesError`. Only `serde_saphyr::Error` is boxed (`io::Error` fits the variant inline). The single-place `path` formatting keeps the miette `Caused by:` chain free of duplication.
  - Rewrite `find_and_load` as a single read-and-catch loop over `start_dir.ancestors()`: at each level read `pnpm-workspace.yaml`, treat only `ErrorKind::NotFound` as "no manifest here, walk up", and propagate every other error through `LoadWorkspaceYamlError`. This matches pnpm's `readManifestRaw` semantics, closes the previous TOCTOU window between the old `is_file()` check and the read, and surfaces non-regular entries (directory, EISDIR, permission denied) as hard errors instead of silently walking up.
- `crates/npmrc/src/lib.rs`
  - `Npmrc::current` is now `Result<Self, LoadWorkspaceYamlError>`. The `.npmrc` parsing stays best-effort; the yaml parsing fails loud.
  - Two regression tests added in this round: `invalid_workspace_yaml_propagates_error` (parse error) and `find_propagates_when_manifest_path_is_a_directory` (`EISDIR`-class error). Existing tests adopt the fallible signature with `.expect(...)`.
- `crates/cli/src/cli_args.rs`
  - The `npmrc` closure now returns `miette::Result<&'static mut Npmrc>` and is wrapped with `wrap_err("load configuration")`. The `state` closure threads `?`.
- `crates/cli/src/cli_args/store.rs`
  - `StoreCommand::run` accepts `impl FnOnce() -> miette::Result<&Npmrc>` so `Prune` and `Path` get the new error path; `Store` and `Add` still panic before loading config.
- `crates/npmrc/Cargo.toml`
  - Add `derive_more` and `miette` deps for the new error type.
2026-05-09 22:11:43 +02:00
Zoltan Kochan
e1e29c1520 feat: add --no-runtime to skip installing runtime entries (#11557)
Adds a `--no-runtime` flag (config: `runtime: boolean`, default `true`) that suppresses install of runtime entries declared via `devEngines.runtime` (the `runtime:` protocol) **without modifying the lockfile**.

The lockfile keeps the runtime entry, so frozen-lockfile validation still passes; only the runtime fetch and `.bin` linking are skipped. Useful in CI matrices where the runtime is provisioned externally (e.g. via `pnpm runtime -g set node <version>`) before `pnpm install` runs.

The existing `--runtime-on-fail=ignore` is unsuitable for this case: it mutates the manifest and regenerates the lockfile to drop the runtime entry, which trips frozen-lockfile validation. The two flags are orthogonal and serve different purposes.

### Implementation

The hook lives in the lockfile filter stage:

- `lockfile/filtering/src/filterImporter.ts` — strips `runtime:` refs from the importer's deps maps when `skipRuntimes` is set.
- `lockfile/filtering/src/filterLockfileByImportersAndEngine.ts` — new `skipRuntimes?: boolean` option; runtime-protocol direct deps are dropped before they enter `pickedPackages`, so they never reach the dep graph or bin-linker. Applies to all runtimes (`node`, `deno`, `bun`) since they share the `runtime:` protocol prefix.

The option is plumbed through `installing/deps-restorer`, `installing/deps-installer`, and `installing/commands` to the user-facing `pnpm install --no-runtime` flag.

### Example

```json
// package.json
{
  "devEngines": {
    "runtime": {
      "name": "node",
      "version": "22.13.0",
      "onFail": "download"
    }
  }
}
```

Local dev: `pnpm install` — installs node 22.13.0 as before.

CI matrix entry:
```yaml
- run: pn runtime -g set node ${{ matrix.node }}
- run: pn install --no-runtime
```

The lockfile is unchanged; the matrix's externally-provisioned node is used.
2026-05-09 17:16:27 +00:00
Zoltan Kochan
f2b28f85ff chore(release): 11.0.9 2026-05-09 02:06:35 +02:00
Zoltan Kochan
38bdfefdf9 fix: upgrade @pnpm/semver-diff and @pnpm/colorize-semver-diff to v2 (#11555)
- Upgrade `@pnpm/semver-diff` and `@pnpm/colorize-semver-diff` to v2, which expose the helpers as named exports.
- Update the call sites in `@pnpm/deps.inspection.commands` and `@pnpm/installing.commands` from `semverDiff.default(...)` / `colorizeSemverDiff.default(...)` to plain `semverDiff(...)` / `colorizeSemverDiff(...)`.
- Refactor `buildPkgChoice` in `getUpdateChoices.ts` to build the row as a `string[]`. Previously the row was an object whose values relied on `nextVersion` being inferred as `any` (a side effect of the broken `.default` access poisoning the type) — that masked `outdatedPkg.current` and `outdatedPkg.workspace` being `string | undefined`. With the v2 named imports the types tighten up, and `Object.values(lineParts)` would no longer assign cleanly to `string[]`.

The previous v1 packages exported their helpers as `module.exports.default = fn`, so `.default(...)` only worked through the legacy CJS interop — and it broke under Node.js ESM (which is what the Jest runner uses with `--experimental-vm-modules`). Most of the `deps/inspection/commands` outdated tests had been silently failing on `main` with `TypeError: semverDiff.default is not a function`; this change brings them back.
2026-05-09 01:30:12 +02:00
Zoltan Kochan
39e42665b3 fix(git-resolver): avoid encoded slash in GitLab tarball URL (#11551)
* fix(git-resolver): avoid encoded slash in GitLab tarball URL

hosted-git-info's default GitLab tarball URL routes through
`/api/v4/projects/<user>%2F<project>/...`. The `%2F` survives into the
virtual store directory name (depPathToFilename only escapes raw `/`,
not `%`), and Node refuses to import any module whose path contains an
encoded slash. The same URL is also intermittently rejected by GitLab
with a 406.

Override the GitLab tarballtemplate to the `/-/archive/` URL, which works
for both public and private repos and contains no encoded slashes.

Closes #11533

* test: avoid cspell-flagged words

* test: keep existing gitlab assertions, only add new ones

Restore the skipped tests' original API-URL assertions; they document the
old expected shape and weren't running anyway. Add the new `/-/archive/`
URL to the pick-fetcher fixture as an additional case so both shapes are
exercised.
2026-05-08 22:29:27 +02:00
Zoltan Kochan
219854fab8 fix(pack): bundle dependencies declared in bundleDependencies (#11524)
- Fixes [#11519](https://github.com/pnpm/pnpm/issues/11519): `pnpm pack` in pnpm 11 silently dropped every package listed in `bundleDependencies` / `bundledDependencies`, producing tarballs that no longer contained the bundled `node_modules/<dep>` files that v10 produced.
- Root cause: the npm-packlist v10 upgrade ([#10658](https://github.com/pnpm/pnpm/pull/10658)) changed its API to require the caller to pre-populate the dependency tree's `edgesOut` Map. The wrapper in `fs/packlist` passed an empty Map, so npm-packlist's `gatherBundles()` looked up each declared name, found nothing, and skipped them all.
- Fix: `fs/packlist` now reads each bundled dep's `package.json` (walking up parent `node_modules` to support hoisted layouts), recursively populates `edgesOut` for transitive deps of bundled packages, and normalizes `bundleDependencies: true` to an explicit array (npm-packlist iterates the field directly).
2026-05-08 22:23:06 +02:00
Zoltan Kochan
e5664f2539 chore: update pnpm-lock.yaml (#11484) 2026-05-08 22:08:55 +02:00
Zoltan Kochan
164221c5b2 fix: honor --prefix when resolving workspace dir (#11549) 2026-05-08 20:12:24 +02:00
Zoltan Kochan
2b267a71b6 fix: run Node.js version check before loading bundle (#11546) (#11547)
The static `import` of `dist/pnpm.mjs` in `bin/pnpm.mjs` was hoisted by
the ES module loader and parsed before the version check below it,
causing pnpm to crash with `SyntaxError: Invalid regular expression
flags` on Node.js versions older than the bundle's syntax target instead
of printing a clear "requires Node.js v22.13" error. Switching to a
dynamic `await import()` lets the version check run first.
2026-05-08 17:07:30 +02:00
Zoltan Kochan
6925be3b90 fix(config): honor NPM_CONFIG_USERCONFIG as a low-priority fallback (#11545)
* fix(config): honor NPM_CONFIG_USERCONFIG as a low-priority fallback

Restores compatibility with environments that point npm at a custom
.npmrc via NPM_CONFIG_USERCONFIG (e.g. actions/setup-node writing to
${runner.temp}/.npmrc), which silently broke after the v11 env var
prefix change. PNPM-prefixed env vars and npmrcAuthFile from the
global config.yaml continue to take precedence.

Closes #11539

* fix(config): treat empty NPM_CONFIG_USERCONFIG as unset

`??` accepts an empty string as a defined value, so an exported but
unset NPM_CONFIG_USERCONFIG would short-circuit the fallback chain and
make normalizePath('') resolve to process.cwd(). Mirror readEnvVar's
empty-string-to-undefined coercion via a readNpmEnvVar helper so the
fallback to ~/.npmrc works as expected.
2026-05-08 16:00:38 +02:00
Khải
837a920022 refactor(npmrc): collapse WorkspaceSettings::apply_to with macro (#406)
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-08 13:16:46 +02:00
Zoltan Kochan
6836547017 feat(default-reporter): add pnpm-render bin to render NDJSON from stdin (#11530)
- Adds a `pnpm-render` bin to `@pnpm/cli.default-reporter` that reads pnpm-shaped NDJSON from stdin and pipes it through the default reporter, so external tools that emit `pnpm:*` log records can reuse pnpm's renderer.
- Optional first positional arg sets the command name (defaults to `install`), e.g. `pnpm-render add` for piping output from `pacquet add`.

## Motivation

[Pacquet](https://github.com/pnpm/pacquet) emits pnpm-shaped `--reporter=ndjson` output for forward-compatibility with pnpm's renderer, but there was no way to actually render it. With this bin:

```sh
pacquet install --reporter=ndjson 2>&1 >/dev/null | pnpm-render
```

(pacquet writes NDJSON to stderr, so the redirect is needed.)
2026-05-08 01:56:54 +02:00
Zoltan Kochan
bf1937bebf fix: render NDJSON output cleanly through @pnpm/default-reporter (#403)
* fix(cli): canonicalize `--dir` so reporter `prefix` matches subprocess `process.cwd()`

`pacquet --reporter=ndjson` piped into `@pnpm/cli.default-reporter`
emitted every progress / stats line with a `.   |   ` path prefix
because the bunyan-envelope `prefix` field was the literal `"."`
(derived from the manifest's parent of `./package.json`). The reporter
compares each event's `prefix` to its own `cwd` (an absolute,
canonicalized path from `process.cwd()`), and the mismatch routed every
log through `zoomOut`.

Canonicalize `--dir` once at the top of `CliArgs::run`, mirroring pnpm's
`config/reader/src/index.ts:270` where
`cwd = fs.realpathSync(betterPathResolve(cliOptions.dir ...))` becomes
`pnpmConfig.dir` and is threaded into every event's `prefix` as
`lockfileDir`. Ports the same realpath behavior to pacquet so a
default-reporter subprocess and pacquet agree on the project root.

Side effect: `pacquet init` now derives the package `name` from the
working-directory basename (matching pnpm) instead of leaving it as
`""` — the `.` parent had no `file_name`. Snapshot updated.

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix(tarball): emit `pnpm:fetching-progress started` with one-indexed `attempt`

The default reporter's `reportBigTarballsProgress` filters started
events with `log.attempt === 1` so a transient retry doesn't reset the
"Downloading <pkg>: …/<size>" progress line. Pacquet's loop counter is
zero-indexed and was being passed straight through, so every started
event was emitted with `attempt: 0` and the reporter silently dropped
the entire channel — no big-tarball progress lines ever rendered.

Pnpm's underlying `node-retry` `op.attempt(cb)` callback hands `cb` a
1-indexed counter, which `packageRequester` forwards verbatim into the
log. Match that wire shape by emitting `attempt + 1` at the
fetching-progress site, mirroring the same `+ 1` adjustment already
present at the neighbouring `pnpm:request-retry` emit.

Updates the recording-fake test to assert `[1, 2]` (one-indexed) and
fixes a stray `attempt: 0` in the reporter serialization test.

* test(cli): add NDJSON `prefix` regression test, refine review nits

Addresses three review comments on #403:

* Add an integration test (`install_emits_canonical_prefix_in_ndjson_events`)
  that runs `pacquet --reporter=ndjson install` and asserts every
  event's `prefix` is the canonicalized workspace path — never `"."`.
  Pins the user-visible regression: the original failure mode was the
  default-reporter subprocess prepending `<prefix> | ` to every progress
  / stats line because the emitted `"."` never matched its own
  `process.cwd()`.
* Reword the canonicalize-failure context to reference the `--dir`
  argument explicitly so a stat / permission failure points the user at
  the flag they passed.
* Restore the `assertion_line` field on
  `init__should_create_package_json.snap` so its metadata matches the
  surrounding insta snapshots.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-08 01:41:35 +02:00
Khải
ad467d7fda fix(integrated-benchmark): mirror settings in pnpm-workspace.yaml (#401)
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-08 06:27:45 +07:00
Khải
aff5978e23 fix: optimize Claude Code Web session (#385)
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-08 03:58:17 +07:00
Khải
2af4cf89a4 feat: .modules.yaml (#332)
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-07 20:38:27 +00:00
Zoltan Kochan
27e3e16810 chore: update pnpm 2026-05-07 22:30:00 +02:00
Khải
8cf3a861ac docs(ai): clarify adding deps rule (#400)
* docs(agents): clarify when AI agents may add dependencies

Replace the vague "do not add dependencies casually" rule with concrete
guidance: agents may freely pull a workspace-declared dependency into a
crate, but must not add a dependency that is not already declared in
`[workspace.dependencies]` without explicit human approval.

* docs(agents): reword dep-adding rule to match writing-style guide

Remove the em dash, the inline parenthetical, and the italicized 'not'
so the bullet matches the formal-tone, complete-sentence convention
documented in CONTRIBUTING.md.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-07 21:56:23 +02:00
Zoltan Kochan
ad0d61e212 ci(integrated-benchmark): list open PRs to find fork-PR head SHA (#399)
* ci(integrated-benchmark): list open PRs to find fork-PR head SHA

The fallback `gh api repos/$REPO/commits/$HEAD_SHA/pulls` lookup
only returns PRs whose head commit lives on the base repo — for fork
PRs the head SHA exists on the fork (e.g. `Saturate/pacquet`), not
the base, so that endpoint returns `[]`. The Stage-2 comment workflow
on PR #391 failed at this step:

> ##[error]no open PR matches head_sha=b965c5e... in pnpm/pacquet

Switch to listing open PRs in the base repo and filtering by
`head.sha`. The PRs API records each PR's head SHA on the base-repo
side regardless of where the head branch lives, so this works for
both same-repo PRs and fork PRs through one code path.

Same-repo PRs still bypass the API call entirely via the trusted
`workflow_run.pull_requests[0].number` shortcut. The fallback only
runs when that array is empty (the fork-PR case).

* ci(integrated-benchmark): fix paginated jq aggregation

`gh api ... --paginate --jq` runs the jq filter per page, so a query
that returns multiple pages emits one JSON array per page on stdout,
not a single combined array. Reproduced locally with per_page=2:

  $ gh api '.../pulls?state=open&per_page=2' --paginate \
      --jq '[.[] | .number]'
  [399,391]
  [385,337]
  [333,332]
  ...

Today pacquet has under 100 open PRs so a single page suffices and
the bug is dormant; past that threshold the downstream
`jq 'length'` would read each line as a separate input and produce
multiple values like `2\n2\n2\n1`, which fails both the `count == 0`
and `count == 1` checks and bottoms out in the "ambiguous" error.

Fix: emit one PR per line with `--paginate --jq '.[]'` so the stream
spans pages cleanly, then `jq -s` slurps the whole thing back into a
single array. `--arg sha` keeps the head SHA out of the jq program
text. Verified with per_page=2 against PR #391's head SHA: returns
`[391]` as expected.

Reported by Copilot on #399.
2026-05-07 20:04:43 +02:00
Zoltan Kochan
c5cf5d1fec ci(integrated-benchmark): post the comment from a workflow_run job (#398)
* ci(integrated-benchmark): post the comment from a workflow_run job

The benchmark comment never landed on fork PRs (e.g. #391). The original
workflow ran on `pull_request`, which means GitHub gives it a read-only
`GITHUB_TOKEN` when the PR comes from a fork — `peter-evans/create-or-update-comment`
would 403 trying to write back. The workflow guarded the write with
`if: github.event.pull_request.head.repo.full_name == github.repository`,
so it silently skipped instead of failing.

Switch to the standard two-stage pattern:

1. `integrated-benchmark.yml` runs the benchmark and uploads
   `SUMMARY.md`, the PR number, and the runner OS as a single artifact.
   It no longer needs `issues` / `pull-requests` write permissions.

2. `integrated-benchmark-comment.yml` (new) is triggered on
   `workflow_run` of `Integrated-Benchmark` and runs in the base
   repository's privilege context, where it can post the comment back
   to a fork PR. It downloads the artifact via the GitHub API
   (`actions/download-artifact@v8` with `run-id` + `github-token`),
   sanitises both untrusted values (the artifact comes from a
   fork-controlled run), then runs the same find/create-or-update
   sequence the original workflow used.

Same gating as before: only success-path runs of `pull_request` events
post a comment. Comments from same-repo PRs continue to work; fork PRs
now also get them.

* ci(integrated-benchmark): resolve PR number from trusted workflow_run trigger

Address CodeRabbit's PR-redirection concern on #398: a fork-controlled
artifact must not be the source of truth for which PR receives the
comment. Move the PR-number resolution into the comment workflow,
sourced from `workflow_run` event metadata that GitHub itself fills in:

- Same-repo PRs: read directly from
  `github.event.workflow_run.pull_requests[0].number`.
- Fork PRs (where that array is empty): query
  `gh api repos/$REPO/commits/$HEAD_SHA/pulls` against the BASE repo,
  using `workflow_run.head_sha` from the same trusted payload.

CodeRabbit's suggested fix used `pull_requests[0].number` unconditionally,
which would have broken the fork-PR case this workflow exists to fix —
GitHub's docs explicitly state that field is empty for fork-triggered
runs.

Drop `pr-number.txt` from the uploaded artifact entirely. The runner OS
is also no longer carried; the matrix only runs on Linux today, and
the comment-matching string is hard-coded to `Integrated-Benchmark
Report (Linux)`. If the matrix expands later, both halves need updating
together.
2026-05-07 18:27:44 +02:00
Zoltan Kochan
8eb1be4988 fix(publish): strip semver build metadata before packing (#11525)
When a published version contained a `+<build>` segment (e.g.
`1.0.0-canary.0+abc1234`), `pnpm publish --provenance` was rejected by
the registry with a 422 verifying the sigstore provenance bundle.

`libnpmpublish.publish()` runs `semver.clean()` on `manifest.version`,
which strips build metadata, before computing the provenance subject.
pnpm was packing the tarball with the original version, so the version
embedded in the packed `package.json` no longer matched the version in
the metadata payload and the bundle's subject — causing the registry to
reject the publish.

Strip build metadata from the published version after creating the
publish manifest, then derive both the tarball filename and the
manifest packed inside the tarball from that cleaned version.

Closes #11518.
2026-05-07 17:55:59 +02:00
Zoltan Kochan
91812dc47b feat(reporter): emit pnpm:package-manifest, pnpm:root, pnpm:stats, and pnpm:request-retry (#375)
## Summary

Wires the four remaining portable channels from #347 into pacquet's reporter pipeline. Each follows the convention documented in `CODE_STYLE_GUIDE.md` § "Reporter / log events":

- a `LogEvent` variant + payload struct in `pacquet-reporter`
- a wire-shape unit test in `crates/reporter/src/tests.rs`
- the emit at the matching upstream pnpm site, with a pinned permalink at SHA `086c5e91e8`
- a recording-fake test where the surrounding code makes one reachable

### Channels

- **`pnpm:package-manifest`** — presence-tagged on `initial` / `updated`. `Initial` fires from `Install::run` next to the existing `pnpm:context` emit so consumers have the on-disk manifest before the install header renders. `Updated` fires from `Add::run` after `manifest.save()` so the audit pipeline can diff initial vs updated.
- **`pnpm:root`** — `Added` per direct dependency from `SymlinkDirectDependencies::run`. Iterates per `DependencyGroup` so each emit can label its `dependencyType` (`prod` / `dev` / `optional`); peer deps aren't materialised here so they're skipped, matching pnpm.
- **`pnpm:stats`** — `Added` (snapshot count) and `Removed` (placeholder `0` until pruning lands) from `CreateVirtualStore::run`. The code comment notes the upstream-divergence: pnpm reports the current/wanted diff (`newDepPathsSet.size`), pacquet has no current-lockfile tracking yet so every install looks like a fresh install and `added` ends up as the total snapshot count.
- **`pnpm:request-retry`** — emitted from `fetch_and_extract_with_retry` before each backoff sleep, with `attempt` one-indexed (the failed attempt) and `timeout` mirroring `RetryOpts::delay_for` ms. A new `tarball_error_to_request_retry` helper maps `TarballError` onto pnpm's JS-shaped error object: `httpStatusCode` for HTTP failures, a placeholder `code` derived from the variant name for everything else, so the JS reporter's `?? `-chain dispatch (`httpStatusCode ?? status ?? errno ?? code`) picks up the right field.

### Tests

- `crates/reporter/src/tests.rs` — wire-shape tests for all four new channels, asserting the JSON shape `@pnpm/cli.default-reporter` consumes (camelCase field names, presence-tagged dispatch, optional fields skipping rather than rendering as `null`).
- `crates/package-manager/src/install/tests.rs` — extends `install_emits_pnpm_event_sequence` to pin the new `[PackageManifest, Context, ImportingStarted, Stats(Added), Stats(Removed), ImportingDone, Summary]` order.
- `crates/package-manager/src/symlink_direct_dependencies/tests.rs` — new unit tests proving `pnpm:root added` fires once per direct dep with the right `dependencyType` per group, and pinning the existing `MissingRootImporter` error path.
- `crates/tarball/src/tests.rs` — `request_retry_event_fires_per_retried_attempt` proves the retry-loop emits exactly once per failed-and-being-retried attempt with `httpStatusCode: "503"` (no placeholder `code` when the JS-side dispatch field is populated).

Pure additive — no existing wire-shape changed.
2026-05-07 16:19:55 +02:00
Zoltan Kochan
24f3669c4a fix: add missing changeset 2026-05-07 15:19:03 +02:00
Zoltan Kochan
80037699fb ci: update dist-tag in update-latest.yml 2026-05-07 12:42:52 +02:00