* refactor: rename @pnpm/exe platform packages to @pnpm/exe.<platform>-<arch>[-musl]
Aligns pnpm's own published platform artifacts with the one naming
convention the rest of the codebase already uses (`process.platform`
values plus an explicit `-musl` libc suffix), matching what `pnpm
pack-app`, `pnpm add --os/--cpu/--libc`, `supportedArchitectures.os`,
and Node.js tarball names all already settled on.
Package renames:
- @pnpm/linux-x64 -> @pnpm/exe.linux-x64
- @pnpm/linux-arm64 -> @pnpm/exe.linux-arm64
- @pnpm/linuxstatic-x64 -> @pnpm/exe.linux-x64-musl (new dir)
- @pnpm/linuxstatic-arm64 -> @pnpm/exe.linux-arm64-musl
- @pnpm/macos-x64 -> @pnpm/exe.darwin-x64
- @pnpm/macos-arm64 -> @pnpm/exe.darwin-arm64
- @pnpm/win-x64 -> @pnpm/exe.win32-x64
- @pnpm/win-arm64 -> @pnpm/exe.win32-arm64
GitHub release asset names follow suit (`pnpm-linuxstatic-x64.tar.gz`
-> `pnpm-linux-x64-musl.tar.gz`, `pnpm-macos-*` -> `pnpm-darwin-*`,
`pnpm-win-*` -> `pnpm-win32-*`). Internal artifact directories under
`pnpm/artifacts/` renamed to match, which drops the awkward mixed
naming between target and directory.
The umbrella package `@pnpm/exe` keeps its name so that `pnpm
self-update` from v10 and any `npm i -g @pnpm/exe` scripts continue to
resolve. Platform children can be renamed freely because npm/pnpm
filter optional deps by each child's `os`/`cpu`/`libc` manifest
fields, not by package names.
Also updates:
- `@pnpm/exe`'s `setup.js` (preinstall) and the self-updater's
`linkExePlatformBinary` to look up the platform package by the new
scheme, using `detect-libc` to append `-musl` on musl Linux hosts.
- `.meta-updater` optional-dependency list for @pnpm/exe.
- `copy-artifacts.ts` target list and Windows detection prefix.
- cspell wordlist (drops `linuxstatic`; it's no longer used anywhere).
Final transition publishes of the old package names (pointing at the
new ones so direct pins keep resolving) are a release-engineering step
handled separately.
Refs #11314.
* chore: keep "linuxstatic" in cspell wordlist for changeset references
* test(pack-app rename): cover the musl branch of platform-package-name lookup
Copilot flagged that the musl -> -musl suffix logic in setup.js's preinstall
and self-updater's linkExePlatformBinary had no regression coverage. Extract
the name-computation from both into small pure helpers and unit-test all
four matrix cases (linux+musl, linux+glibc, darwin, win32) plus the
win32 ia32->x86 arch normalization:
- pnpm/artifacts/exe/platform-pkg-name.js exposes `exePlatformPkgName`
(returns `@pnpm/exe.<platform>-<arch>[-musl]`). setup.js imports it
instead of inlining the logic; the new setup.test.ts block covers the
four-case matrix without having to mock detect-libc or patch
process.platform.
- engine/pm/commands/src/self-updater/installPnpm.ts exports a new
`exePlatformPkgDirName` returning `exe.<platform>-<arch>[-musl]` (the
scope-local dir). linkExePlatformBinary calls it; the new
selfUpdate.test.ts block covers the same matrix.
Both helpers are deliberately pure so the non-musl CI host can still
exercise the musl code path.
* fix: give each runtime variant its own global virtual store entry
When a runtime package (e.g. node@runtime:X.Y.Z) uses a variations
resolution, createFullPkgId() in @pnpm/deps.graph-hasher was hashing
the whole VariationsResolution — the same hash on every host — so the
global virtual store path collided between variants. Whichever variant
installed first won, and a later `pnpm add --libc=musl node@runtime:<v>`
silently reused the cached glibc (or macOS/Windows) binary.
The fix threads supportedArchitectures down to createFullPkgId so the
selected variant's integrity is used as the package fingerprint. Two
related cleanups land with it:
- Extract the platform-variant selection logic to @pnpm/resolving.resolver-base
as selectPlatformVariant/resolvePlatformSelector. The helper's libc
match also required a fix: a variant with no libc is the "default"
build, and a request for a non-default libc (e.g. musl) must require
an exact match so the default variant doesn't silently win.
- @pnpm/installing.package-requester's findResolution now delegates to
the shared helper, and the new supportedArchitectures param is plumbed
through calcDepState / calcGraphNodeHash / iterateHashedGraphNodes /
lockfileToDepGraph and their callers in deps-resolver, deps-restorer,
deps-installer, graph-builder, and building.after-install.
* feat: add pnpm build-sea command for building Node.js SEA executables
Adds `pnpm build-sea` under @pnpm/releasing.commands. Takes a CommonJS
entry file and a set of target triplets (linux-x64, linux-x64-musl,
linux-arm64, linux-arm64-musl, macos-x64, macos-arm64, win-x64,
win-arm64) and produces a standalone executable per target under
dist-sea/<target>/.
Each target's Node.js runtime is fetched via `pnpm add node@runtime:<v>
--os=<os> --cpu=<arch> --libc=<libc>` into $PNPM_HOME/build-sea/<target>-<v>/
so binaries are hardlinked from the global content-addressable store and
`pnpm store prune` can reclaim them.
Requires Node.js v25.5+ to perform the --build-sea injection. If the
running Node is older, a v25 binary is downloaded and used as the builder
automatically. macOS outputs are ad-hoc signed with codesign (on macOS)
or ldid (when cross-compiling from Linux), which is required because SEA
injection invalidates the binary's existing signature.
* fix(build-sea): reject malformed --target, --output-name and use mkdtemp for config
Addresses Copilot review feedback on the build-sea command:
- parseTarget() previously destructured the target string, silently
accepting extra `-` segments. Inputs like `linux-x64-musl-../../outside`
would pass validation and flow into path.join. Validation is now done
with a strict anchored regex.
- --output-name was passed into path.join() without sanitization, so a
caller could escape the output directory with path separators or `..`.
validateOutputName() now rejects anything that isn't a plain basename.
- The per-target SEA config file was written to a predictable path under
os.tmpdir() (derived from the target name and Date.now()), which is
unsafe on multi-user systems. It now lives inside a fresh mkdtemp()
directory and is opened with the exclusive "wx" flag.
- New test cases cover extra-segment targets, uppercase/whitespace
variants, and the full matrix of invalid --output-name inputs.
* rename: build-sea → pack-app
`build-sea` required knowing what a SEA is. `pack-app` is self-describing,
doesn't collide with pnpm's existing `bin` concept, and parallels the
existing `pack` command.
- Command name: build-sea → pack-app
- Default output dir: dist-sea → dist-app
- Error codes: PACK_APP_* (was BUILD_SEA_*)
- Export/type: packApp / PackAppOptions (was buildSea / BuildSeaOptions)
- Install cache dir: $PNPM_HOME/pack-app (was $PNPM_HOME/build-sea)
The Node.js `--build-sea` flag name itself is unchanged — that's a
Node.js feature and outside this project's naming.
* fix(pack-app): reject directory entries, pin builder to >=25.5, refuse macOS target on Windows
Addresses Copilot review feedback on the pack-app command:
- entry validation now rejects non-file paths (directories, symlinks to
non-files) with a dedicated PACK_APP_ENTRY_NOT_FILE instead of
surfacing a less actionable error later in the SEA build.
- DEFAULT_BUILDER_SPEC was the bare major ("25"), which would satisfy
with 25.0.x if that version is still present — those point releases
predate --build-sea support. Tightened to ">=25.5.0 <26.0.0" so the
download is guaranteed to support the flag without ever crossing a
major.
- adHocSignMacBinary() silently skipped re-signing on Windows hosts.
Now throws PACK_APP_MACOS_SIGN_UNSUPPORTED_HOST with a hint to build
the target on macOS/Linux or re-sign manually.
- resolvePlatformSelector() JSDoc now matches what the code actually
does (picks the first entry when it is not "current"; later entries
are ignored).
- New test case covers the directory-as-entry rejection.
* refactor(pack-app): switch target OS names to process.platform constants
Previously `pack-app` accepted `macos-*` / `win-*` as the OS portion of a
target triplet and translated them to `darwin` / `win32` internally. The
translation layer made the CLI surface inconsistent with the values that
`pnpm add --os=…` and `supportedArchitectures.os` already use, and added
a small footgun (e.g. users setting `supportedArchitectures: { os: [darwin] }`
but typing `macos-arm64` for pack-app).
The supported target OS set is now `linux | darwin | win32`, matching
`process.platform`. Old inputs like `macos-arm64` or `win-x64` now fail
validation with a clear error pointing to the new naming. The internal
parseTarget helper drops its TARGET_OS_MAP lookup entirely.
This is a change to an unreleased command so there is no back-compat
concern. pnpm's own artifact directory names (`pnpm/artifacts/macos-*/`,
`pnpm/artifacts/win-*/`) are an internal implementation detail and are
not affected by this change.
* feat(pack-app): read defaults from pnpm.app in package.json
Every pack-app flag (--entry, --target, --node-version, --output-dir,
--output-name) can now be preconfigured in the project's package.json
under a new "pnpm.app" object:
{
"name": "my-cli",
"pnpm": {
"app": {
"entry": "dist/index.cjs",
"targets": ["linux-x64", "darwin-arm64", "win32-x64"],
"nodeVersion": "25",
"outputDir": "release",
"outputName": "my-cli"
}
}
}
CLI flags always win. --target replaces the configured list rather than
appending, so a user can narrow the default set at the command line.
The config loader is strict: unknown keys under pnpm.app and any
type-mismatched values throw PACK_APP_INVALID_CONFIG so mistakes surface
at invocation time instead of silently being ignored.
Chose pnpm.app over pnpm.packApp because it's the shorter, cleaner
namespace for anything related to the app bundle (future sibling
commands like run-app / deploy-app could share the same object without
a naming clash). Chose package.json over pnpm-workspace.yaml because
the config is inherently per-project, whereas pnpm-workspace.yaml is
workspace-root-only.
* fix(pack-app): deterministic libc selection and stricter output-name validation
Addresses Copilot review feedback:
- ensureNodeRuntime() now always passes an explicit --libc for linux
targets. Without a suffix, linux-x64 and linux-arm64 default to
--libc=glibc instead of letting the user's supportedArchitectures.libc
config or the host's detected libc decide the variant. The install
cache directory mirrors this, so glibc and musl variants are always
distinct (linux-x64-glibc vs linux-x64-musl).
- resolveBuilderBinary() now pins the host libc when downloading a
builder Node on Linux. A user whose config sets supportedArchitectures.libc
to musl no longer ends up with a musl Node that the glibc host cannot
execute.
- validateOutputName() rejects Windows-invalid filename characters
(<>:"|?* and NUL), Windows reserved device names (CON, NUL, COM1, etc.),
and names ending in a dot or space — problems surface at invocation
time rather than during writeFile(outputFile, ...) on Windows.
- lockfileToDepGraph variants tests no longer derive the "host"
variant from process.platform/process.arch; they always pass an
explicit supportedArchitectures selector so the expectations hold on
any CI host (including Alpine/musl).
* chore: add "toctou" to cspell wordlist
`TOCTOU` (time-of-check-to-time-of-use) is the standard term for the
race-condition class the pack-app SEA-config comment describes. Adding
it to the wordlist unblocks the Lint CI step.
* fix: lint
* fix(sbom): resolve licenses for git-sourced dependencies
`readPackageFileMap` did not handle `type: 'git'` resolutions, causing
`pnpm sbom` to emit NOASSERTION and `pnpm licenses` to throw for any
dependency installed from a git URL.
Closes#11260
* fix: add missing store.cafs devDep, test tsconfigs, and size field
- Add @pnpm/store.cafs devDependency and tsconfig reference to
license-scanner so CI typecheck resolves the PackageFilesIndex import
- Add test/tsconfig.json to pkg-finder so CI typechecks the new tests
- Add required `size` field to PackageFileInfo test fixtures
* fix: replace spellcheck-failing test strings
* fix: use spellcheck-safe integrity string in test
* style: fix import sort in pkg-finder test
* fix(sbom): use packageIdFromSnapshot to match store index keys
The SBOM used `snapshot.id ?? depPath` as the package ID, which
includes the package name prefix (e.g. `left-pad@git+https://...`).
The store index stores git packages under just the git URL without
the name prefix. Use `packageIdFromSnapshot` which strips the prefix,
matching how the licenses command already does it.
Also fixes test store keys to match the real installer layout so the
mismatch would have been caught by tests.
* refactor: move git resolution check after tarball check
Tarball resolutions are more common than type: 'git', so check them
first. Per review feedback from @zkochan.
## Summary
Adds an opt-in **pnpm agent** server that resolves dependencies server-side and streams only the files missing from the client's content-addressable store.
- **`@pnpm/agent.server`** — multi-process HTTP server (Node.js `cluster`) with SQLite-backed metadata and file caches
- **`@pnpm/agent.client`** — streams an NDJSON response, dispatches worker threads to fetch files while the server is still resolving
- **New config**: `agent` in `pnpm-workspace.yaml` (opt-in)
## How it works
1. Client reads integrity hashes from its local store index
2. Sends `POST /v1/install` with dependencies + store integrities
3. Server resolves the dependency tree using pnpm's `install({ lockfileOnly: true })`, with a SQLite-backed `PackageMetaCache` for fast repeat resolution
4. As each package resolves, a wrapped `storeController.requestPackage` looks up its files and immediately streams digests the client is missing (NDJSON `D` lines)
5. Client reads the stream line by line; digest batches fill up and dispatch worker threads to `POST /v1/files` — file downloads overlap with server-side resolution
6. After resolution, server sends index entries (`I` lines) and lockfile (`L` line)
7. Client writes index entries to store, then runs headless install with a wrapped `fetchPackage` that calls `readPkgFromCafs` with `verifyStoreIntegrity: false` (files are trusted from the agent)
8. `/v1/files` response is gzip-streamed (274MB → ~80MB) — server pipes through `createGzip`, worker pipes through `createGunzip`, parsing and writing files to CAFS as data arrives
## Performance
1351-package project, cold local store, warm server (localhost):
| Scenario | Time |
|----------|------|
| Vanilla pnpm install (cold OS cache) | ~48s |
| Vanilla pnpm install (warm OS cache) | ~34s |
| With pnpm agent (consistent) | **~33s** |
### Key optimizations
1. **SQLite metadata cache** — server-side resolution drops from ~3.4s to ~0.9s
2. **SQLite file store** — consistent read performance regardless of OS file cache state
3. **Streaming `/v1/install`** — file digests stream during resolution, downloads start before resolution finishes
4. **Gzip-streamed `/v1/files`** — whole-stream gzip (274MB → ~80MB), significant savings on remote servers
5. **Worker-thread streaming HTTP** — workers pipe gzip → parse → write to CAFS as data arrives, no buffering
6. **No rehashing** — server-provided digests used directly, skipping 33K SHA-512 computations
7. **No re-verification** — wrapped `fetchPackage` calls `readPkgFromCafs` with `verifyStoreIntegrity: false`
8. **Direct `writeFileSync` with `wx`** — no stat + temp + rename
9. **Pre-packed msgpack** — server sends raw store index buffers, client writes directly to SQLite
10. **WAL checkpoint** — ensures store index entries written by agent are visible to headless install's worker threads
## Usage
Start the server:
```bash
node agent/server/lib/bin.js
```
Configure in `pnpm-workspace.yaml`:
```yaml
agent: http://localhost:4873
```
- `pnpm sbom` now recognizes the deprecated `licenses` array and falls back to scanning on-disk `LICENSE` files, matching the resolution already used by `pnpm licenses`. Fixes packages like `busboy`, `streamsearch`, `limiter` being reported as `NOASSERTION`.
- Root component scans the project directory for LICENSE files too, matching transitive-dep behavior.
- Shared license resolution lives in a new `@pnpm/deps.compliance.license-resolver` package, with `parseLicenseFromManifest`, `resolveLicense` (store-file-map), and `resolveLicenseFromDir` (on-disk scan).
- LICENSE-file-detected names are filtered against `SPDX_LICENSE_IDS` before being emitted to SBOM — long-form names like "Eclipse Public License 1.0" fall through to `NOASSERTION` rather than producing non-compliant SPDX. Manifest-declared licenses pass through untouched.
- When both `license` and `licenses` are set, the modern `license` wins (previously `pnpm licenses` preferred `licenses`).
- Close#11248.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
* fix: preserve pnpm 10 peer suffix encoding for linked paths
The filenamify upgrade from v4 to v7 changed the peer-suffix "version"
token for linked dependency paths: `../packages/b` became `+packages+b`
instead of `packages+b`, causing lockfile churn for workspaces with
packages linked from outside the workspace root.
Replace the filenamify call with a small inline encoder that reproduces
v4's output for link paths, and drop the now-unused dependency.
Closes#11272.
* chore: avoid cspell-flagged word in peer suffix comment
* test: cover linkPathToPeerVersion and clarify its lossy encoding
Address Copilot review feedback on #11297:
- Correct the comment — any leading run of `.` is dropped, not just
`./` and `../` segments (so `.hidden/pkg` becomes `hidden+pkg`).
- Export the helper and add a focused test that pins the exact token
output for link paths, so future lockfile-breaking regressions get
caught by the test suite.
* refactor: extract linkPathToPeerVersion into its own file
* feat: add runtimeOnFail setting
Adds a `runtimeOnFail` config setting ('ignore' | 'warn' | 'error' |
'download') that overrides the `onFail` field on `devEngines.runtime`
and `engines.runtime` in the root project's package.json. This makes
it possible to opt into (or out of) runtime auto-download without
changing the project manifest.
* fix: skip runtime download when version is missing
Without a version, convertEnginesRuntimeToDependencies would write
`runtime:undefined` into the manifest. Warn and skip instead.
* feat: apply runtimeOnFail override during install
The config reader override only mutates the context's rootProjectManifest,
but installDeps reads the manifest fresh via tryReadProjectManifest and
findWorkspaceProjects. Apply the override there too so `runtimeOnFail`
actually affects what gets installed. Adds an e2e test covering both
download and ignore overrides through the real CLI bundle.
## Summary
- **New command `pnpm with <version|current> <args...>`** — runs pnpm at a specific version (or the currently active one) for a single invocation, bypassing the project's `packageManager` and `devEngines.packageManager` pins. Uses the same install mechanism as `pnpm self-update`, caching the downloaded pnpm in the global virtual store for reuse.
- **New config setting `pmOnFail`** — overrides the `onFail` behavior of both `packageManager` and `devEngines.packageManager`. Accepted values: `download`, `error`, `warn`, `ignore`. Readable from CLI flag, env var, `pnpm-workspace.yaml`, or `.npmrc` — useful when version management is handled by an external tool (asdf, mise, Volta, etc.) and the project wants pnpm itself to skip the check.
```
pnpm with current install # one-shot, use running pnpm
pnpm with 11.0.0-rc.1 install # one-shot, use specific version
pnpm install --pm-on-fail=ignore # direct CLI flag
pnpm install --config.pm-on-fail=ignore # equivalent via --config.* sugar
pnpm_config_pm_on_fail=ignore pnpm install # env var
# or in pnpm-workspace.yaml: pmOnFail: ignore
```
## Implementation notes
- Command handler lives in `@pnpm/engine.pm.commands` (next to `self-update` and `setup`).
- `'with'` added to `SPECIALLY_ESCAPED_CMDS` in `cli/parse-cli-args` so args after `<spec>` pass through opaquely like `dlx`/`run`.
- `pnpm with current <cmd> [args]` is rewritten in `pnpm/src/parseCliArgs.ts` to an in-process dispatch — argv is rebuilt in place so any global flags the user put before `with` (e.g. `--dir`, `--filter`) are preserved. `process.env.pnpm_config_pm_on_fail=ignore` is set so the override survives `parseCliArgsLib`'s `-v` / `--help` short-circuits (which discard other parsed options).
- `main.ts` treats `skipPackageManagerCheck: true` as bypassing both the auto-download and the warn/error check (previously only the check). Also skips when `cmd='help'` and the help target is itself a skip-check command, so `pnpm with -h` works in pinned projects without downloading the pinned version first.
- Errors reported to stderr for `with` (aligned with `dlx`/`create`/`sbom`).
- `pmOnFail` wired in `config/reader/src/index.ts`: added to `types`, `Config`, and `pnpmConfigFileKeys`; applied as an override in the `onFail` resolution block.
- The `with <version>` child process sets both `COREPACK_ROOT` (honored by every pnpm release via `isExecutedByCorepack()`) and `pnpm_config_pm_on_fail=ignore` (principled override on new releases that ship the setting). This gives graceful behavior when `pnpm with 9.3.0 install` spawns an older pnpm that predates the new setting.
- Store controller lifecycle in the handler wrapped in `try/finally` to prevent leaks on install errors. Signal-induced child exits return a non-zero exit code so interrupted runs aren't masked as success.
The legacy `/-/npm/v1/security/audits{,/quick}` endpoints have been retired by npmjs.org. This PR rewires the audit client to the replacement `/-/npm/v1/security/advisories/bulk` endpoint.
The new endpoint is not a drop-in rename — the request and response contracts are both different:
- **Request**: a flat `{ pkgName: [versions] }` map. `lockfileToAuditRequest` walks the lockfile once and builds the POST body directly; there is no more nested `AuditTree`.
- **Response**: only `id`, `url`, `title`, `severity`, `vulnerable_versions`, and `cwe` per advisory. Everything else the old endpoint returned is computed locally:
- `findings[].paths` are walked from the lockfile (skipped entirely when the response is empty; the second walk intentionally avoids `@pnpm/lockfile.walker`'s global dedup so alternate install chains to the same shared dep aren't dropped).
- `metadata.vulnerabilities` counts advisories per severity.
- `metadata.dependencies` / `devDependencies` / `optionalDependencies` / `totalDependencies` come from a classified lockfile walk; the classifier respects `--prod`/`--dev` include flags when deciding whether a subgraph is reachable non-optionally.
- `patched_versions` is inferred from the vulnerable range for common `<X.Y.Z` / `<=X.Y.Z` shapes so `audit --fix` can still produce usable overrides; left `undefined` when inference fails.
- `github_advisory_id` is parsed from the advisory URL and canonicalized to the github.com form (uppercase `GHSA-` prefix, lowercase suffix).
- `info` severity is now supported end-to-end (severity type, `--audit-level`, filters, colors).
## Breaking changes (v11)
- Private registries that do not implement `/advisories/bulk` now fail with `AuditEndpointNotExistsError`.
- CVE-based filtering is replaced with GHSA-based filtering, since the bulk endpoint does not return CVE identifiers:
- `auditConfig.ignoreCves` → `auditConfig.ignoreGhsas` (the old key is no longer recognized).
- `pnpm audit --ignore <id>` and `--ignore-unfixable` now read and write GHSAs.
- Migration: replace each `CVE-YYYY-NNNNN` in `auditConfig.ignoreCves` with the matching `GHSA-xxxx-xxxx-xxxx` (visible in the `More info` column of `pnpm audit` output) under `auditConfig.ignoreGhsas`.
- `--ignore-unfixable` now only targets advisories whose patched range couldn't be inferred — the only "no fix available" signal the bulk endpoint provides.
- `AuditReport` and `AuditAdvisory` are trimmed to just the fields the audit client actually populates:
- `AuditReport`: `advisories` + `metadata` only (`actions` and `muted` removed).
- `AuditAdvisory`: `findings`, `id`, `title`, `module_name`, `vulnerable_versions`, `patched_versions?`, `severity`, `cwe`, `github_advisory_id`, `url`. Dropped: `cves`, `created`, `updated`, `deleted`, `access`, `overview`, `recommendation`, `references`, `found_by`, `reported_by`, `metadata`.
- `AuditAction`, `AuditResolution`, `AuditActionRecommendation` removed (no consumers).
## Hardening
- Response body validated: non-object / malformed JSON / non-array package buckets all surface as `ERR_PNPM_AUDIT_BAD_RESPONSE` with a body excerpt. Advisory `id` must be a finite number and `severity` must be a known value before being indexed.
- Name-keyed records use `Object.create(null)` so a hostile/unusual package name can't trigger prototype pollution.
- GHSA ids canonicalized on both read and write so casing drift between config and registry doesn't mask ignores.
- `findings[].paths` are deduped and capped per (name, version) to keep pathologically shared graphs from blowing up memory.
## Internals
- `AuditTree` / `AuditNode` / `lockfileToAuditTree` removed. `lockfileToAuditIndex.ts` exports `lockfileToAuditRequest` (flat POST body + counts) and `buildAuditPathIndex` (only invoked when the response has advisories).
- `AuditAdvisory.findings` is now `AuditFinding[]` (was an unintended 1-tuple).
- Top-level test fixtures regenerated from real `registry.npmjs.org` responses; synthetic `update-*` fixtures converted in place to bulk shape.
---------
Co-authored-by: John van Leeuwen <john.van.leeuwen@priva.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
This PR implements the native `pnpm search` command and its aliases (`s`, `se`, `find`), removing the fallback to npm CLI. It follows the project standards by being root-cause and avoiding type duplications.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
When pnpm self-updates via the headless install path, the install
directory was not registered in the store's project registry. This
caused `pnpm store prune` to treat its global virtual store packages
as unreachable and remove them, breaking the global pnpm binary.
Register the install dir after headless install in installPnpmToGlobalDir
* feat: add pnpm docs command and home alias
* chore: update manifests
* fix: address review comments for pnpm docs command
- Remove 'docs' and 'home' from NOT_IMPLEMENTED_COMMANDS to prevent
not-implemented handlers from overriding the real implementations
- Change fallback URL from npmjs.com to npmx.dev
- Remove bugs URL as a docs fallback (it's an issue tracker, not docs)
- Fix ESM mock in tests: use jest.unstable_mockModule instead of jest.mock
* refactor: use open package for browser opening in promptBrowserOpen
Replace the hand-rolled platform-specific execFile logic with the open
npm package, which handles WSL, Docker-in-WSL, and Windows edge cases
better. This removes the execFile dependency injection from
promptBrowserOpen, OtpContext, LoginContext, and SharedContext.
* fix: address copilot review comments
- docs: use www.npmjs.com fallback (not npmx.dev) and validate homepage
URL is http(s) before opening
- promptBrowserOpen: validate authUrl protocol before passing to open(),
and guard against synchronous throws from open()
* fix: restore npmx.dev fallback for pnpm docs
* chore: expand changeset with web-auth refactor impact
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
* test: update registry-mock to 6.0.0 stable and use pnpm view in tests
Update @pnpm/registry-mock from 6.0.0-6 to 6.0.0 stable release.
Replace npm view with pnpm view in test helpers now that pnpm has
native view/dist-tag commands. Unskip the nodeRuntime test that was
blocked on the registry-mock republish.
* chore: update pnpm to beta 8
* feat: support versions, dist-tags, and time field selectors in pnpm view
The view command now exposes versions (as an array of version strings),
dist-tags, and time from registry metadata. Single-field --json output
returns the raw value instead of wrapping it in an object, matching npm
behavior. This allows tests to use pnpm view instead of npm view.
- Update `@pnpm/registry-mock` from 5.2.4 to 6.0.0-6
- Fix auth tests to use bearer token from `globalSetup` instead of hardcoding credentials
- Replace hardcoded integrity checksums with `getIntegrity()` from registry-mock in `customResolvers` tests
- Add `prepareFixtureWithIntegrity()` helper in deps-restorer tests to dynamically patch `@pnpm.e2e` integrity values in fixture lockfiles at runtime, so they don't go stale when registry-mock is updated
- Fix `workspace-external-depends-deep` fixture's current lockfile (was missing `packages/f` and `packages/g` importers)
- Remove unnecessary credentials from `gitChecks` tests (they reject before any registry interaction)
* test: ensure prerelease weighting is correct
* fix: use higher weight for package versions already in lockfile
* test: remove fundamentally incompatible test
* fix(test): use undici MockAgent instead of nock for HTTP mocking
nock only patches Node's built-in http/https modules, but pnpm uses
undici for HTTP requests. Replace nock with @pnpm/testing.mock-agent
(which wraps undici's MockAgent) so the regression test actually
intercepts registry metadata requests.
* fix(benchmarks): show errors from store populate step
The populate step redirected both stdout and stderr to /dev/null,
hiding the actual error when pnpm install fails during benchmarks.
* fix(benchmarks): replace deprecated packages in benchmark fixture
The old fixture used deprecated babel 6, gulp, and other legacy
packages whose transitive dependencies (e.g. es-abstract) are missing
the "time" field in registry metadata, causing ERR_PNPM_MISSING_TIME
with time-based resolution mode.
Replace with modern equivalents (babel 7, webpack 5, MUI, Redux
Toolkit, etc.) that maintain a similar dependency tree size (~1300
packages) while using well-maintained packages with proper registry
metadata.
* fix(benchmarks): drop eslint plugins that pull in es-abstract
eslint-plugin-react, eslint-plugin-import, and eslint-plugin-jsx-a11y
transitively depend on es-abstract, whose registry metadata lacks the
"time" field. Replace them with eslint-plugin-prettier to avoid
ERR_PNPM_MISSING_TIME with time-based resolution.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Implement dist-tag ls, add, and rm subcommands natively instead of
delegating to npm. Follows the same pattern as the recently added
deprecate and unpublish commands.
- Implements the `pnpm unpublish` command natively instead of passing through to npm
- Supports unpublishing specific versions or version ranges using semver
- Supports unpublishing entire packages with `--force` flag (with protection against accidental unpublish)
- Supports OTP authentication via `--otp` flag
- Supports custom registry via `--registry` flag
- Reuses existing data structures from the deprecate command
## Usage
```bash
# Unpublish a specific version
pnpm unpublish my-package@1.0.0
# Unpublish multiple versions matching a range
pnpm unpublish my-package@">1.0.0 <2.0.0"
# Unpublish entire package (requires --force)
pnpm unpublish my-package --force
# With custom registry
pnpm unpublish my-package --registry https://my-registry.com
```
## Changes
- Added `unpublish.ts` command in `releasing/plugin-commands-publishing/`
- Removed unpublish from npm pass-through list in `pnpm.ts`
- Added required dependencies: `@pnpm/fetch`, `semver`, `@types/semver`
Follows npm unpublish behavior and is aligned with the existing `pnpm deprecate` implementation.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
12 command test suites had near-identical ~50-field DEFAULT_OPTS objects
copy-pasted between them. Extract the common fields into a single shared
package so each suite only declares its overrides.
Replace the unmaintained @pnpm/npm-conf package with a purpose-built
module that reads only auth/registry-related settings from .npmrc files
using read-ini-file + @pnpm/config.env-replace (both already deps).
All non-registry settings (hoist-pattern, node-linker, etc.) are now
only read from pnpm-workspace.yaml, CLI options, or environment
variables. Registry-related settings (auth tokens, registry URLs,
SSL certs, proxy settings) continue to be read from .npmrc for
migration compatibility, and can also be set in pnpm-workspace.yaml.
New modules:
- loadNpmrcFiles.ts: reads .npmrc from standard locations, filters to
auth/registry keys, returns structured layers
- npmConfigTypes.ts: inlined npm config type definitions
- npmDefaults.ts: inlined npm defaults (registry, unsafe-perm, etc.)
* refactor(config): stop shelling out to npm for auth settings
Read and write auth-related settings (registry, tokens, credentials,
scoped registries) directly to INI config files instead of delegating
to `npm config`. Removes the @pnpm/exec.run-npm dependency from
@pnpm/config.commands.
* fix(config): give pnpm global rc priority over ~/.npmrc for auth settings
Auth settings from the pnpm global rc file (e.g. ~/.config/pnpm/rc) now
override ~/.npmrc in rawConfig. This ensures tokens written by `pnpm login`
are correctly picked up by `pnpm publish`, since login writes to the pnpm
global rc but ~/.npmrc previously took priority in the npm-conf chain.
* chore: remove @pnpm/exec.run-npm package
No longer used after removing npm config CLI delegation.
* chore: remove accidentally committed __typecheck__/tsconfig.json
* fix(config): narrow non-string rejection to credential keys, add priority test
Non-string value rejection now only applies to credential keys (_auth,
_authToken, _password, username), registry URLs, and scoped/registry-
prefixed keys — not to INI settings like strict-ssl, proxy, or ca that
can legitimately have boolean/null values.
Added a test verifying that auth tokens from the pnpm global rc take
priority over ~/.npmrc.
* fix(exe): create pn/pnpx/pnx binaries in linkExePlatformBinary
When pnpm auto-manages its version via the `packageManager` field,
it installs @pnpm/exe to the store with scripts disabled. The
`linkExePlatformBinary` function replicates setup.js by linking the
platform binary, but it only created the `pnpm` binary.
The published @pnpm/exe tarball has placeholder files for pn, pnpx,
and pnx (written by prepare.js). Without setup.js running, these
remain as placeholders, causing "This: not found" when invoked.
Create pn (hardlink to native binary) and pnpx/pnx (shell scripts)
in linkExePlatformBinary, matching what setup.js does.
* fix(exe): remove unnecessary placeholder writes on Windows
* test(exe): verify pn/pnpx/pnx are created by linkExePlatformBinary
* test(exe): e2e test that setup.js creates all binaries after prepare.js
Runs prepare.js (simulating publish) then setup.js (simulating install)
and verifies that pnpm and pn are hardlinks to the platform binary,
and pnpx and pnx are executable shell scripts.
Also fixes setup.js to unlink before writing shell scripts, so that
the 0o755 mode is applied even when prepare.js already created the
file with 0o644.
* fix: use node: protocol for imports
* fix(exe): use shell script aliases for pn instead of hardlinks
pn, like pnpx and pnx, is now a shell script (`exec pnpm "$@"`)
instead of a hardlink to the native binary. This avoids duplicating
the ~100MB binary.
Updated in both setup.js (registry installs) and
linkExePlatformBinary (store installs via version switching).
* fix(exe): revert pn back to hardlink, keep pnpx/pnx as shell scripts
Hardlinks have zero overhead and no disk cost (shared inode).
Shell scripts are only needed for pnpx/pnx which inject the dlx arg.
* fix(exe): only ignore ENOENT in createShellScript unlink
* fix(exe): publish pnpx/pnx with real content instead of placeholders
prepare.js now writes the actual shell scripts for pnpx and pnx
(and their .cmd/.ps1 Windows wrappers) instead of placeholder text.
This means setup.js and linkExePlatformBinary only need to handle
the native binary hardlinks (pnpm, pn) and the Windows bin rewrite.
The published tarball contains the correct pnpx/pnx scripts for all
platforms, so they work even when lifecycle scripts don't run (e.g.
store installs during auto version management).
* fix(exe): skip hardlink test when platform binary is unavailable
The platform-specific packages (@pnpm/linux-x64 etc.) are optional
dependencies only available in the @pnpm/exe package, not in CI
test environments. Split the test so prepare.js content verification
always runs, while the setup.js hardlink test skips gracefully.
* style: use single quotes in test
Replace node-fetch with native undici for HTTP requests throughout pnpm.
Key changes:
- Replace node-fetch with undici's fetch() and dispatcher system
- Replace @pnpm/network.agent with a new dispatcher module in @pnpm/network.fetch
- Cache dispatchers via LRU cache keyed by connection parameters
- Handle proxies via undici ProxyAgent instead of http/https-proxy-agent
- Convert test mocking from nock to undici MockAgent where applicable
- Add minimatch@9 override to fix ESM incompatibility with brace-expansion
Instead of rendering the full peer dependency issues tree during installation,
suggest users run "pnpm peers check" to view the issues. Remove the now-unused
@pnpm/installing.render-peer-issues package.
* feat: use yarn-like output for script execution
Print `$ command` instead of `> pkg@version stage path\n> command`.
Show project name and path only when running in a different directory.
* fix: sort chalk dependency after @pnpm packages
* refactor: remove project info line from run output
* chore: add changeset
* refactor: print script command line to stderr
The `$ command` line is metadata, not program output. Printing it to
stderr keeps stdout clean for piping, matching bun's behavior.
* chore: update changeset to major
* fix: stop setting npm_config_ env vars from pnpm config during lifecycle scripts
Update @pnpm/npm-lifecycle to 1100.0.0-0 which no longer dumps the
entire pnpm config as npm_config_* environment variables. This fixes
npm warnings about unknown config when lifecycle scripts invoke npm.
Only well-known npm_* env vars are now set, matching Yarn's behavior.
* fix: fix spellcheck in changeset
* chore: remove obsolete @pnpm/npm-lifecycle patch file
* fix: pass npm_config_user_agent via extraEnv in lifecycle scripts
The npm-lifecycle makeEnv() strips all npm_* vars from process.env,
so npm_config_user_agent must be explicitly passed via extraEnv.
* chore: mark changeset as major (breaking change)
* feat: add native view/info command
* test: add unit tests for native view command
* fix(view): support ranges, aliases, and tags
* chore: update lockfile and tsconfig
* refactor(view): reuse pickPackageFromMeta from npm-resolver
- Share version resolution logic with the npm-resolver instead of
reimplementing tag/range/version matching in the view command.
- Export pickPackageFromMeta and pickVersionByVersionRange from
@pnpm/resolving.npm-resolver.
- Remove redundant double HTTP fetch (metadata already contains all
version data).
- Remove duplicate author/repository fields from PackageInRegistry
(already inherited from BaseManifest).
- Consolidate four changesets into one.
- Revert unrelated .gitignore change.
- Drop direct semver dependency from deps.inspection.commands.
* refactor(view): reuse fetchMetadataFromFromRegistry from npm-resolver
Use the npm-resolver's fetchMetadataFromFromRegistry instead of
hand-rolled fetch logic. This fixes:
- Broken URL encoding for scoped packages (@scope/pkg)
- Missing auth header, proxy, SSL, and retry config
- Duplicated fetch + error handling code
Also pass proper Config options (rawConfig, userAgent, SSL, proxy,
retry, timeout) through to createFetchFromRegistry and
createGetAuthHeaderByURI so the view command works with private
registries and corporate proxies.
* test(view): improve test coverage for view command
Add tests for:
- non-registry spec rejection (git URLs)
- no matching version error
- version range resolution (^1.0.0)
- dist-tag resolution (latest)
- nested field selection (dist.shasum)
- field selection with --json
- text output format (header, dist section, dist-tags)
- scoped package lookup (@pnpm.e2e/pkg-with-1-dep)
- deps count / deps: none in header
- object field rendering as JSON
* revert: undo rename of @pnpm/resolving.registry.types
The rename from @pnpm/resolving.registry.types to
@pnpm/registry.types (and the move from resolving/registry/types/
to registry/types/) is a separate refactoring concern unrelated to
the view command. Revert all rename-related changes.
Keep the legitimate type additions to PackageInRegistry:
maintainers, contributors, and dist.unpackedSize.
* revert: restore pnpm-workspace.yaml (remove registry/* glob)
* fix(view): handle edge cases in formatBytes and unpackedSize
- Use explicit null check for unpackedSize so 0 B is still rendered
- Add TB/PB units and clamp index to prevent undefined output
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
* refactor: extract web auth QR code and polling into @pnpm/network.web-auth
Extract generateQrCode() and pollForWebAuthToken() from releasing/commands
into a new shared package so that both `pnpm publish` and the upcoming
`pnpm login` can reuse the web-based authentication flow with QR code
display and doneUrl polling.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* feat: implement `pnpm login` command
Add `pnpm login` (and `pnpm adduser` alias) for authenticating with npm
registries. The command:
- Tries web-based login first (POST /-/v1/login), displaying a QR code
and polling for the token using @pnpm/network.web-auth
- Falls back to classic username/password/email login (PUT /-/user/
org.couchdb.user:<username>) when web login is not supported (404/405)
- Saves the received auth token to the user's global rc file
Also fixes a tsgo build issue in releasing/commands where
OtpWebAuthFetchOptions was used as a local type alias but was only
available as a re-exported name.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* fix: resolve spellcheck issues in login test
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* fix: correct alphabetical ordering for meta-updater
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* chore: add meta-updater generated tsconfig files
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* fix: add explicit return type to prompt mock for tsgo compatibility
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* fix: use @pnpm/network.fetch instead of globalThis.fetch
Switch from globalThis.fetch to fetchWithAgent from @pnpm/network.fetch
so that pnpm login respects proxy settings (httpProxy/httpsProxy/noProxy),
custom SSL certificates (ca/cert/key), strictSsl, and retry configuration.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* refactor: improve login fetch types and use URL constructor
- Type LoginContext.fetch using WebAuthFetchOptions/WebAuthFetchResponse
from @pnpm/network.web-auth, extended with text() and wider method
- Replace regex-based URL construction with new URL() constructor
- Remove redundant LoginFetchInit type
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* refactor: match publish pattern for dependency injection
- Static DEFAULT_CONTEXT constant instead of createDefaultContext factory
- context = DEFAULT_CONTEXT default parameter instead of context?: Partial
- Destructure context in function signatures for natural calling
- Use plain fetch from @pnpm/network.fetch (like SHARED_CONTEXT in publish)
- Context contains only side-effect functions and modules, not config
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* refactor: use typeof fetch instead of custom fetch types
Remove LoginFetchOptions and LoginFetchResponse. Type LoginContext.fetch
as typeof fetch from @pnpm/network.fetch directly, eliminating all casts.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* fix: remove placeholder username from login success message
Web login doesn't return a username, so just report the registry.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* refactor: use tempDir from @pnpm/prepare instead of manual tmp dirs
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* chore: update tsconfig references for @pnpm/prepare
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* refactor: inject readSettings/writeSettings for fully pure tests
Add readSettings and writeSettings to LoginContext so tests need no
filesystem side effects. Remove @pnpm/prepare devDependency.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* refactor: remove DEFAULT_CONTEXT from tests, use pure test context
Tests now construct their own TEST_CONTEXT with all no-op mocks,
eliminating any reliance on real side-effectful functions.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* test: use distinct opts per test, assert URLs and config paths
Each test now uses a different registry and configDir to verify URL
construction, config key generation, and save path are correct for
non-default options.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* test: throw on unexpected mock calls instead of silent fallbacks
All mock functions in TEST_CONTEXT now throw on unexpected calls,
ensuring tests fail loudly if the code makes unanticipated side effects.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* test: use IANA-reserved example.com domains in test URLs
Replace custom.registry.io and private.reg.co with example.com and
example.org (RFC 2606 reserved) to prevent domain squatting risks.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* test: use deterministic Date mock instead of native Date
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* test: assert globalInfo calls, throw on unexpected ones
Default globalInfo in TEST_CONTEXT now throws. Each test overrides it
to capture messages and asserts the expected output.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* fix: use inferred type for fetch url parameter in tests
Drop explicit `string` annotation so the parameter matches the
`RequestInfo` type expected by the fetch signature.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* fix: resolve type errors in login test mock fetch
Use mockResponse helper with `as any` cast to satisfy the Response
type, and String(url) for RequestInfo-to-string conversion.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* chore: add tsconfig.lint.tsbuildinfo to .gitignore
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* refactor: replace typeof fetch with explicit LoginFetchResponse/LoginFetchOptions types
Derive the fetch signature from actual call-site usage instead of
coupling to the concrete @pnpm/network.fetch type. This lets test
mocks return plain objects without casts.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* chore: gitignore generated pn/pnpx/pnx artifacts
These files are created by setup.js during preinstall and should not
be tracked.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* refactor: remove unnecessary backwards-compat aliases from otp.ts
Remove Otp-prefixed re-exports (OtpWebAuthFetchOptions,
OtpWebAuthFetchResponse, OtpWebAuthTimeoutError) that only existed as
backwards-compatibility shims. Update the test to import directly from
@pnpm/network.web-auth. Restore the named OtpDate interface that was
unnecessarily inlined.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* test(web-auth): add comprehensive unit tests for @pnpm/network.web-auth
Add dependency-injected unit tests covering:
- WebAuthTimeoutError: properties, code, hint, message
- generateQrCode: basic output and input differentiation
- pollForWebAuthToken: happy path, fetch argument passing,
Retry-After handling (valid, non-finite, null, sub-interval,
capped to remaining timeout, timeout during retry wait),
error recovery (fetch throws, non-ok response, json parse error,
missing token, empty token, multiple consecutive errors),
custom timeout, poll interval timing
All tests use fake Date.now() and setTimeout — no real timers or
side effects.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* fix(web-auth): fix TS2339 compile errors in test assertions
Replace `.catch((e: WebAuthTimeoutError) => e)` pattern with
`rejects.toMatchObject()` to avoid `string | WebAuthTimeoutError`
union type issue when accessing `.timeout` property.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* feat(web-auth,login): extract shared OTP handling and add OTP support to login
- Create `withOtpHandling<T>()` in `@pnpm/network.web-auth` that wraps
any operation with EOTP challenge detection, web auth flow, and
classic OTP prompting.
- Refactor `publishWithOtpHandling` to delegate to the shared function.
- Add OTP handling to `pnpm login`'s classic (CouchDB) login flow:
detects 401 + `www-authenticate: otp` header and retries with the
OTP code (or web auth token) in the `npm-otp` header.
- Remove overly strict `this: this` constraints from WebAuthFetchResponse
interfaces to improve cross-package type compatibility.
- Add 13 unit tests for `withOtpHandling` (classic + webauth flows).
- Add 4 login OTP tests (classic OTP, webauth OTP, non-401, non-otp 401).
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* fix(login): use word-boundary regex for URL assertion in test
Replace `m.includes(url)` with a regex that checks the URL is
bounded by whitespace or string boundaries, addressing the CodeQL
"incomplete URL substring sanitization" finding.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* refactor(login): use toContainEqual + stringMatching for URL assertion
Replace manual `.some()` with Jest's `toContainEqual(expect.stringMatching(...))`
for better error messages on failure.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* refactor(web-auth): use expect.any(String) instead of typeof check
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* refactor(web-auth): consolidate multi-property assertions
Use toMatchObject and toEqual instead of separate per-property expects.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* docs: explain why npm-auth-type header is sent unconditionally
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* refactor: remove unused re-exports and add missing test coverage
Remove dead re-exports of OtpHandlingPromptOptions and
OtpHandlingPromptResponse from releasing/commands/src/publish/otp.ts.
Add tests for:
- LOGIN_MISSING_CREDENTIALS (empty username in classic login)
- LOGIN_NO_TOKEN (registry returns success without token)
- LOGIN_INVALID_RESPONSE (web login returns incomplete response)
- isWebLoginNotSupported with 405 status code
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* refactor(login): rename readSettings/writeSettings to safeReadIniFile/writeIniFile
Use the actual function names in the LoginContext interface instead of
abstract names, matching the implementations they wrap.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* refactor(otp): remove unnecessary re-exports from otp.ts
OtpNonInteractiveError, OtpSecondChallengeError, and OtpHandlingEnquirer
were re-exported only for the test file, which can import them directly
from @pnpm/network.web-auth.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* refactor(otp): remove unused SHARED_CONTEXT re-export
All consumers already import SHARED_CONTEXT directly from
./utils/shared-context.js, making this re-export dead code.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* refactor(login): extract LoginDate and LoginEnquirer interfaces
Extract named interfaces for the Date and enquirer members of
LoginContext instead of inlining their types.
https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S
* refactor: stop renaming
Claude Code Web didn't rename them thoroughly, so I had to do it myself
* docs: correct the lines
Why did Claude Code Web misaligned?
* refactor: strictly type `LoginFetchOptions.headers`
* docs: remove redundant comments
* refactor: inline `npm-otp`
* refactor: inline `headers`
* feat: add `WebLoginError.responseText`
* refactor: rename `statusCode` into `httpStatus`
* refactor(login): extract ClassicLoginError subclass from PnpmError
Extract the LOGIN_FAILED error into a dedicated ClassicLoginError class
with httpStatus and responseText properties, matching the WebLoginError
pattern.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* refactor: remove unnecessary import
* docs(changeset): correct a changeset
* docs(changeset): re-add `releasing.commands`
* refactor(web-auth): split monolithic test file into per-module files
Split index.test.ts into four files matching the source structure:
- WebAuthTimeoutError.test.ts
- generateQrCode.test.ts
- pollForWebAuthToken.test.ts
- withOtpHandling.test.ts
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* refactor: remove unnecessary `as const`
* refactor: remove unnecessary `as const`
* chore: undo Claude's BS
* refactor: extract `LoginEnquirerOptions`
* refactor: move types closer to their usesites
* refactor: remove simple type alias
* fix: type errors
* refactor(login): inject readIniFile instead of safeReadIniFile in context
The context object should only contain external dependencies. safeReadIniFile
is a local wrapper, not an external dependency, so inject readIniFile (from
read-ini-file) instead and pass it to safeReadIniFile as a parameter.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* test(login): add coverage for safeReadIniFile ENOENT handling
Test that login succeeds with empty settings when the config file does
not exist (ENOENT), and that non-ENOENT errors (e.g. EACCES) are
properly propagated.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* refactor: fix ugliness
* refactor: just pass context object
* refactor: destructure `context`
* refactor: pass the `context` object
* refactor: destructure `context`
* refactor: pass `context` object directly
* refactor: remove unnecessary parenthesis
* fix: remove unused import
* refactor: remove unnecessary parentheses from single-param arrows in tests
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* refactor: extract `LoginFetchResponseHeaders`
* fix(login): remove inline default from --registry option description
No other pnpm command includes "(default: ...)" in option descriptions.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* refactor(tests): enforce realistic mock response behavior
- Add createMockResponse helpers that enforce single body consumption
(calling text() or json() twice, or both, throws an error)
- Default headers.get to throwing on unexpected calls, forcing tests
to explicitly provide headers when the code under test reads them
- Replace all inline response objects with createMockResponse calls
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* fix: formatting
* refactor: reuse
* docs: clarify what the error is actually about
* docs: consistent error message
* refactor: use consistent error message convention in test mocks
Capitalize and use "Unexpected call to <thing>" pattern instead of
AI-generated "unexpected X call" messages.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* refactor: expand inline process mock objects to multi-line
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* refactor(login): extract PnpmError subclasses and use stricter test assertions
Extract LoginNonInteractiveError, LoginInvalidResponseError,
LoginMissingCredentialsError, and LoginNoTokenError subclasses instead
of throwing PnpmError directly.
Update test assertions to use the const promise pattern with
toHaveProperty checks on both code and message, matching the
convention used elsewhere in the codebase.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* refactor: undo ai's nonsensical deletion
* refactor: simplify
* refactor: rename OtpHandling* types to Otp* for brevity
OtpHandlingContext → OtpContext
OtpHandlingEnquirer → OtpEnquirer
OtpHandlingPromptOptions → OtpPromptOptions
OtpHandlingPromptResponse → OtpPromptResponse
The OtpHandling prefix was named after the function (withOtpHandling)
rather than the domain concept.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* refactor: extract `OtpDate`
* refactor: reuse
* fix: eslint
* refactor: add OtpRequiredError with body validation and globalWarn
- Add OtpRequiredError class with static fromUnknown() that validates
the EOTP error body shape and returns either a validated error or an
OtpBodyWarning when fields have unexpected types
- Add globalWarn to OtpContext so withOtpHandling can warn on bad body
shapes instead of silently dropping them
- Update throwIfOtpRequired in login.ts to pass raw body through so
validation happens in withOtpHandling via fromUnknown
- Add tests for bad body shapes (wrong types for authUrl/doneUrl)
- Add tests for OtpRequiredError.fromUnknown
- Propagate globalWarn through LoginContext, DEFAULT_CONTEXT,
SHARED_CONTEXT, and all test mocks
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* docs: remove misleading comment from throwIfOtpRequired
The comment referenced downstream machinery (OtpRequiredError.fromUnknown)
that the reader shouldn't need to know about at this call site.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* refactor: replace Object.assign hack with OtpRequiredError in throwIfOtpRequired
throwIfOtpRequired now validates the raw response body via
OtpRequiredError.fromUnknown and throws a proper OtpRequiredError
instead of monkey-patching properties onto a plain Error.
withOtpHandling skips re-validation when the caught error is already
an OtpRequiredError instance.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* chore(git): revert an imperfect fix
This reverts commit f91efc1d9e.
* chore(git): revert would-be irrelevant change
This reverts commit 646c09cc66.
* chore(git): revert an imperfect fix
This reverts commit 45ff1ca601.
* refactor: replace Object.assign hack with ArtificialOtpError
Add ArtificialOtpError class that implements OtpError and validates
unknown body shapes via fromUnknownBody static method, warning on
unexpected types instead of silently dropping them.
Add globalWarn to OtpContext and propagate through LoginContext,
DEFAULT_CONTEXT, SHARED_CONTEXT, and all test mocks.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* refactor: rename ArtificialOtpError to SyntheticOtpError
"Synthetic" better conveys that the error is programmatically
constructed from raw data, not that it's fake.
Also fix grammatical error in JSDoc ("meant to thrown" → "meant to be thrown").
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* fix: eslint
Claude Code Web got it wrong this time
(or maybe because it inherited from my sketch diff? I'm not sure)
* fix: eslint
Ah! I got it. Claude Code Web was at fault here: It renamed "artificial"
to "synthetic" without re-ordering
Dumb AI!
* fix: formatting
Once again caused by Claude Code.
Anyway,
The exact equivalent refactor should have been `void warnings.push(msg)`,
if you really want to be pedantic, that is.
TypeScript, however, allows a `void` function to return any type. Reason
being that they shall all be discarded anyway.
* refactor: remove unnecessary re-assignment
* test: remove unnecessary assertion
* refactor: make default globalInfo and globalWarn mocks throw on unexpected calls
Replace no-op defaults with throwing mocks in createOtpMockContext
and createMockContext. Tests that expect these to be called now
explicitly override them.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* refactor: use toEqual with stringContaining for array assertions
Replace toHaveLength + indexed toContain pairs with single
toEqual([expect.stringContaining(...)]) assertions.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* refactor: replace globalInfo no-ops with jest.fn() and add assertions
For error tests: remove globalInfo override entirely, letting the
default throwing mock catch unexpected calls.
For success tests: use jest.fn() and assert globalInfo was called
with the expected arguments.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* refactor: replace manual array collectors with jest.fn()
Replace infoMessages/warnings arrays and push callbacks with
jest.fn() and assertions on .mock.calls. This is more idiomatic
and eliminates the boilerplate array + push pattern.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* refactor: replace remaining globalInfo no-ops with jest.fn() in otp.test.ts
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* fix(test): throw on unexpected second call instead of returning 'never'
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* fix(test): add missing globalInfo assertion in classic OTP test
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* fix(test): add missing globalInfo assertion in otp webauth polling test
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* fix(test): add @jest/globals import for jest.fn()
jest is not a global in ESM mode (--experimental-vm-modules).
Add import { jest } from '@jest/globals' to all test files using
jest.fn(), and add @jest/globals devDependency to network/web-auth.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* chore(deps): update lockfile
* fix: eslint
* fix(test): add globalInfo mock to EACCES readIniFile test
The test triggers web login (which calls globalInfo with the QR code)
before reaching readIniFile. Without a globalInfo override, the
default throwing mock causes the test to fail at the wrong point.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* fix(test): add missing globalInfo assertion in EACCES readIniFile test
Extract inline jest.fn() to const and assert it was called with
the web login QR code URL.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* refactor: convert functions with 3+ args to params objects
Per the style guide: "Functions should have no more than two or three
arguments. If a function needs more parameters, use a single options
object instead."
- withOtpHandling(operation, context, fetchOptions) → withOtpHandling({ operation, context, fetchOptions })
- pollForWebAuthToken(doneUrl, context, fetchOptions, timeoutMs) → pollForWebAuthToken({ doneUrl, context, fetchOptions, timeoutMs })
- webLogin(registry, fetchOptions, context) → webLogin({ registry, fetchOptions, context })
- classicLogin(registry, context, fetchOptions) → classicLogin({ registry, context, fetchOptions })
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* refactor: sort params object properties alphabetically
Sort interface properties, function signature destructuring, and
call site arguments in alphabetical order to match the convention
used by publishWithOtpHandling.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* refactor: adopt otp.test.ts patterns in login and web-auth tests
- Build context and opts as separate variables, then call login/
withOtpHandling/pollForWebAuthToken on a clean line
- Add createMockContext to login.test.ts
- Convert createMockContext to arrow functions (single return
expression), keep createMockResponse as function declaration
(has local state)
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* fix: eslint
* refactor: inline the one-off function
* fix(login): avoid sending 'npm-otp: undefined' header on initial request
When otp is undefined (first attempt before OTP challenge), the header
'npm-otp': undefined could be coerced to the string "undefined" by
some HTTP implementations. Use conditional spread instead.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* docs(login): explain why npm-otp header is conditionally spread
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* docs(otp): explain why otp: undefined is safe in publishOptions spread
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* fix(test): use path.join in assertions for Windows compatibility
path.join produces backslashes on Windows, so hardcoded forward-slash
paths in assertions fail on Windows CI.
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
* fix: import order — standard library before external deps
https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ
---------
Co-authored-by: Claude <noreply@anthropic.com>