Update imports and dependencies to use new package names after the
monorepo-wide rename (e.g., @pnpm/fetching-types → @pnpm/fetching.types,
@pnpm/resolver-base → @pnpm/resolving.resolver-base). Fix tsconfig
references to use new directory paths. Use PnpmError instead of plain
Error in registry.ts for consistency.
Add a new `aqua:` protocol that resolves packages using the aqua-registry,
a curated collection of 3,000+ CLI tools with cross-platform GitHub Release
asset definitions.
Usage: pnpm add aqua:owner/repo[@version]
Example: pnpm add --global aqua:junegunn/fzf@v0.57.0
The resolver fetches the tool's registry YAML from aquaproj/aqua-registry,
resolves versions via the GitHub Releases API, expands Go-style asset
templates for each supported platform, and returns a VariationsResolution
with per-platform binary download URLs.
Key advantages over the Homebrew+Scoop approach:
- Single data source covers all platforms (no merging two registries)
- Downloads from GitHub Releases (no GHCR auth or redirect dance)
- Binaries are statically linked (no native library deps or LD_LIBRARY_PATH)
- Full version history via GitHub Releases API
- Checksum verification using aqua-registry's checksum configuration
The resolver is placed before the local resolver in the resolution chain
to prevent specifiers containing "/" (e.g., aqua:owner/repo) from being
misinterpreted as local file paths.eat: aqua protocol
* fix: respect frozen-lockfile flag when migrating config dependencies
* fix: throw FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE when installing config deps with --frozen-lockfile
* fix: correct changeset package name and clean up minor issues
- Fix changeset referencing non-existent @pnpm/config.deps-installer
(should be @pnpm/installing.env-installer)
- Fix merge artifact in AGENTS.md
- Revert unnecessary Promise.all refactoring in migrateConfigDeps.ts
- Remove extra blank line in test file
* fix: move frozenLockfile check to call site and add missing tests
Move the frozenLockfile check from migrateConfigDepsToLockfile() to
normalizeForInstall() to minimize the number of check points.
Add unit tests for all frozenLockfile code paths:
- installConfigDeps: migration fails with frozenLockfile
- resolveAndInstallConfigDeps: old-format migration, new-format
resolution, and up-to-date lockfile success
- resolveConfigDeps: fails with frozenLockfile
* refactor: consolidate duplicate frozenLockfile checks in resolveAndInstallConfigDeps
Merge two identical frozenLockfile throw statements into a single check
covering both lockfileChanged and depsToResolve conditions.
* Delete respect-frozen-lockfile.md
* refactor: order fields
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
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
* feat: make clean/setup/deploy prefer user scripts over built-in commands
When a project's package.json has a script named "clean", "setup", or
"deploy", running `pnpm clean/setup/deploy` now executes the script
instead of the built-in command. This prevents surprising behavior for
users with existing scripts.
When running from a workspace subdirectory where the root package.json
has one of these scripts, an error is thrown with guidance on how to
proceed.
Added "purge" as an alias for the built-in clean command, which always
runs the built-in regardless of scripts.
Closes#6816
* feat: also make rebuild prefer user scripts over the built-in
* refactor: move scriptOverride to command definitions
Each command now declares `scriptOverride = true` instead of a
centralized list in main.ts. All command names including aliases
are overridable by same-named scripts.
* refactor: rename scriptOverride to overridableByScript
* test: add e2e tests for script override behavior in clean/purge
* fix: address review feedback
- Fix JSDoc to reflect that aliases are also overridable by scripts
- Update npm_command env var to 'run-script' when redirecting to run
- Add 'purge' alias to clean command help text
* fix: remove --workspace flag from version command, use only --recursive
The --workspace/--workspaces flags were incorrectly added as synonyms for
--recursive in the version command. In pnpm, --recursive (-r) is the
standard convention for applying commands across workspace packages.
Recursive versioning now only activates with the explicit -r flag.
* test: improve version command test coverage
Add tests for major/patch bumps, --json output, --allow-same-version,
invalid version handling, missing name/version, empty params, and
recursive mode with workspace packages including JSON output, skipping
unnamed packages, and verifying --recursive is required.
* test: use expect().rejects instead of try/catch in version tests
Avoids silent passes when the handler doesn't throw.
* 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>
The resilientCopyFileSync fix only covered hardlink and copy paths.
The clone path (COPYFILE_FICLONE_FORCE) was missed, so transient
ENOTSUP under heavy parallel I/O still caused failures.
* 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>
* fix(auth-header): decode _password from base64 for default registry auth
* fix(auth): prepend 'Bearer ' to auth token generated by tokenHelper
* test: skip flaky parallel dlx test on Node 25
* fix(auth): improve tokenHelper Bearer prefix with validation and generic scheme detection
- Throw an error when the token helper returns an empty token instead of
producing an invalid "Bearer " header
- Use a generic auth scheme regex instead of hardcoding only Bearer/Basic,
so other schemes (Token, Negotiate, etc.) are preserved as-is
- Add tests for raw token prefixing, existing scheme preservation, and
empty token error
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
* fix(importer): handle ENOTSUP error in linkOrCopy during parallel imports
On Linux CI, copy_file_range/sendfile can transiently fail with ENOTSUP
under heavy parallel I/O on the same CAS store files. Fall back to
manual read+write when copyFileSync hits this error.
* fix(importer): preserve file mode in ENOTSUP fallback and add tests
Address review feedback:
- Preserve source file permissions (mode) when falling back to
read+write on ENOTSUP
- Add tests for the ENOTSUP fallback path and for rethrow of
non-ENOTSUP errors
* fix(importer): handle ENOTSUP in all copyFileSync paths
The previous fix only handled ENOTSUP in linkOrCopy, but the error can
occur in any code path that calls copyFileSync: the copy import method,
atomicCopyFileSync, and the clone function.
Extract resilientCopyFileSync that falls back to read+write when
copy_file_range/sendfile fails with ENOTSUP, and use it in all paths:
- linkOrCopy (hardlink fallback)
- copyPkg (copy import method)
- atomicCopyFileSync (package.json completion marker)
- createCloneFunction (tolerate ENOTSUP alongside EEXIST)
* fix(importer): don't swallow ENOTSUP in clone function
ENOTSUP from COPYFILE_FICLONE_FORCE means "reflinks not supported" and
must propagate so the auto importer falls through to hardlink. Only the
regular copyFileSync path (resilientCopyFileSync) should handle ENOTSUP
as a transient copy_file_range failure.
The previous commit incorrectly tolerated ENOTSUP in the clone function,
causing it to silently skip files and produce empty directories.
## Summary
`linkBin()` unconditionally calls `cmdShim()` / `symlinkDir()` even when the target bin already points at the correct path. This causes redundant I/O on repeated installs and `EACCES` failures when the bin directory lives on a read-only filesystem (Docker layer caching, CI prewarm, NFS mounts).
This PR adds a check at the top of `linkBin()` that verifies the existing bin before skipping:
- **Symlinks**: `readlink` target is compared against `cmd.path`
- **Cmd-shim files**: checked via `isShimPointingAt()` from `@zkochan/cmd-shim` v9, which embeds a `# cmd-shim-target=<path>` marker in every generated sh shim
- Files larger than 4KB (binaries) are never skipped — they are not cmd-shims
Stale or incorrect bins (wrong target, missing marker, different provider) are always rewritten.
Follows up on feedback from #11020.
## Changes
- `bins/linker/src/index.ts` — add target verification check in `linkBin()`
- `bins/linker/test/index.ts` — tests for skip and rewrite behavior
- `pnpm-workspace.yaml` — upgrade `@zkochan/cmd-shim` to v9
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
On Windows, npm's .cmd/.ps1 bin shims reference the extensionless
`pnpm` file from the published package.json bin entry. Previously,
setup.js and linkExePlatformBinary wrote a dummy text file ("This file
intentionally left blank") at that path, causing the shim to silently
fail — PowerShell's $LASTEXITCODE stays $null, so `exit $LASTEXITCODE`
exits with code 0, making all pnpm commands appear to succeed while
doing nothing.
Fix by hardlinking the real platform binary as both `pnpm.exe` and
`pnpm` (no extension), so the shim executes the actual binary.
## Problem
The indexed package importer always creates a staging temp directory, imports files there, then renames to the final location. For cold installs where the target doesn't exist (the common case), the staging + rename is unnecessary overhead.
## Solution
- **Fast path**: callers already verify the target package is missing before calling `importIndexedDir`, so we can write directly into the final directory and skip the temp dir + rename. Falls back to the atomic staging path on EEXIST (concurrent import race) or when `keepModulesDir` is set (hoisted linker needs to merge existing `node_modules`).
- **Completion marker**: `package.json` is written last by `tryImportIndexedDir`, so `pkgExistsAtTargetDir()` (which checks for `package.json`) won't consider a partially-imported directory as complete after a crash.
- **Atomic copy**: the copy import path (non-COW filesystems) uses a temp file + `renameOverwriteSync` for the `package.json` write, since `copyFileSync` is not atomic. Hard links and reflinks are inherently atomic. This is expressed via the `Importer` interface (`importFile` + `importFileAtomic`), passed as the first argument to `importIndexedDir`.
- **Synthetic package.json**: packages that lack a `package.json` (e.g. injected Bit workspace packages) now get a synthetic empty `{}` added to the store, so the completion marker works universally.
- **DRY**: extracted `retryWithSanitizedFilenames()` to deduplicate the ENOENT handler used by both the fast path and staging path.
* feat: load default trusted deps list from @pnpm/plugin-trusted-deps
Add a new `use-default-trusted-deps` setting (default: true) that
automatically loads a curated list of known-good packages into
`allowBuilds` from @pnpm/plugin-trusted-deps. User-configured
allowBuilds entries take precedence over the defaults. Set
`use-default-trusted-deps=false` to disable.
* fix: use catalog reference for @pnpm/plugin-trusted-deps
* fix: use default import for @pnpm/plugin-trusted-deps CJS compat
The package uses Object.defineProperty for DEFAULT_ALLOW_BUILDS,
which Node.js/Jest ESM interop can't detect as a named export.
Switch to a default import to fix test failures.
* fix: use named ESM import from @pnpm/plugin-trusted-deps@0.3.0-1
The package now ships an ESM entry point with proper named exports,
so we can use a clean named import instead of the default import
workaround.
* fix: update @pnpm/plugin-trusted-deps to 0.3.0-2
Uses static JSON import attributes in ESM entry, fixing the bundle
issue where createRequire resolved paths relative to the bundle
output instead of the original package.
* refactor: rename setting to allow-builds-for-trusted-deps
* test: disable default trusted deps in approveBuilds tests
The tests assert exact allowBuilds contents, so the default trusted
list must be disabled to avoid polluting the expected values.
* fix: don't persist default trusted deps list to pnpm-workspace.yaml
Track the user's original allowBuilds separately as userAllowBuilds
before merging the default trusted list. Use userAllowBuilds when
writing back to pnpm-workspace.yaml to avoid persisting the ~370
default entries from @pnpm/plugin-trusted-deps.
* refactor: rename setting to allow-builds-of-trusted-deps
* docs: use camelCase for setting name in changeset
* fix: include userAllowBuilds in install command opts types
Without this, userAllowBuilds wasn't passed through to
handleIgnoredBuilds, causing the default trusted list to be
written to pnpm-workspace.yaml during e2e tests.
* fix: set userAllowBuilds to empty object when user has no config
When the user has no allowBuilds configured, userAllowBuilds was
undefined, causing handleIgnoredBuilds to fall back to the merged
allowBuilds (with defaults). Use empty object instead so the
fallback doesn't trigger.
* fix: read allowBuilds from workspace manifest when writing back
Instead of tracking userAllowBuilds separately (which gets stale
when other code writes to pnpm-workspace.yaml mid-install), read
the current allowBuilds directly from pnpm-workspace.yaml before
writing. This avoids persisting the default trusted list and
preserves entries written by --allow-build earlier in the flow.
Also update e2e test expectation: esbuild is now in the default
trusted list, so it builds instead of being ignored.
* chore: update tsconfig references for new dependencies
* test: disable default trusted deps in approveBuilds e2e install
The execPnpmInstall helper runs the bundled CLI which picks up
the default allowBuildsOfTrustedDeps=true. This causes extra
placeholder entries in pnpm-workspace.yaml that break assertions.
* fix: revert approveBuilds to use config-based allowBuilds
approveBuilds.handler should use opts.allowBuilds from getConfig()
(which excludes trusted deps defaults when disabled) rather than
reading the workspace manifest. The handler's job is to write
approve/deny decisions, not merge with auto-populated placeholders.
* test: add config reader tests for allowBuildsOfTrustedDeps
Cover: (1) default enabled with trusted defaults merged,
(2) user allowBuilds overrides defaults, (3) setting
allow-builds-of-trusted-deps=false disables the merge.
## Problem
Every file extracted to the CAS goes through a temp-file-plus-rename cycle: `writeFile(temp, buffer)` then `renameOverwriteSync(temp, fileDest)`. For a typical cold install with ~30k files, this adds ~30k extra rename syscalls.
## Solution
Use `writeFileExclusive()` with `{ flag: 'wx' }` (O_CREAT|O_EXCL) to write directly to the final CAS path when the file doesn't exist — skipping the temp+rename overhead. For recovery paths (corrupt/partial files, EEXIST races), fall back to the existing atomic temp+rename via `optimisticRenameOverwrite`.
### Write paths
- **File doesn't exist (common cold-install path)** → `writeFileExclusive` writes directly, no rename
- **File exists with correct integrity** → return immediately, no write
- **File exists with wrong integrity (corruption/crash)** → atomic temp+rename recovery
- **EEXIST (concurrent write)** → verify integrity; if OK return, otherwise atomic temp+rename recovery
### Concurrent safety
- `writeFileExclusive` (`O_CREAT|O_EXCL`) ensures only one process creates a given CAS file
- Recovery overwrites use the battle-tested `optimisticRenameOverwrite` + `pathTemp` for atomic replacement
- `verifyFileIntegrity` is non-destructive (no `unlinkSync` on mismatch), safe when another process may be mid-write
- A crash mid-`writeFileExclusive` can leave a partial file, recovered on next access via atomic temp+rename
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
* fix(cafs): update locker cache when file exists with correct integrity
The CAS locker cache was not updated when a file already existed on disk
with correct integrity. This caused repeated verifyFileIntegrity calls
on subsequent lookups within the same process, adding unnecessary I/O.
* fix(test): assert locker cache value not just key existence
Strengthen the test to verify locker.get() returns the correct
checkedAt timestamp, not just that the key exists.
* perf(cafs): optimize hot path string operations
Replace path.join with string concatenation in contentPathFromHex and
getFilePathByModeInCafs. These functions are called ~30k times per
install and the simpler string operations avoid path.join's argument
validation overhead.
Increase gunzipSync chunk size from default 16KB to 128KB for faster
tarball decompression with fewer zlib iterations.
* refactor: remove dead Buffer.isBuffer check in tarball path
tarballBuffer is typed as Buffer, so the isBuffer/Buffer.from
fallback was unreachable dead code.
* docs: add comments explaining path.join bypass and chunkSize choice
Address review feedback:
- Explain why string concat is used instead of path.join in CAS hot path
- Document why 128KB chunkSize was chosen (microbenchmarks, diminishing
returns at larger sizes, bounded memory cost)
* fix: cspell — use 'Benchmarks' instead of 'Microbenchmarks'
* fix(cafs): restore Buffer.isBuffer check for worker thread compatibility
The structured clone algorithm converts Buffer to Uint8Array when sent
via postMessage to worker threads. parseTarball relies on
Buffer.prototype.toString('utf8', ...) which doesn't exist on
Uint8Array — Uint8Array.toString() returns comma-separated decimal
values, causing parseOctal to misparse tar headers.
* fix: handle non-native Error throws in requirePnpmfile
When a pnpmfile throws a non-native Error value (e.g. a string),
`assert(util.types.isNativeError(err))` crashes pnpm with an
unhelpful assertion failure. Replace the assertion with a guard
that wraps non-native errors into a proper Error and reports them
via PnpmFileFailError.
* fix: improve non-native error wrapping with toError helper
* feat: add `dedupePeers` option to reduce peer dependency duplication
When enabled, this option applies two optimizations to peer dependency resolution:
1. Version-only peer suffixes: Uses name@version instead of full dep paths
(including nested peer suffixes) when building peer identity hashes.
This eliminates deeply nested suffixes like (foo@1.0.0(bar@2.0.0)).
2. Transitive peer pruning: Only directly declared peer dependencies are
included in a package's suffix. Transitive peers from children are not
propagated upward, preventing combinatorial explosion while maintaining
correct node_modules layout.
The option is scoped per-project: each workspace project defines a peer
resolution environment, and all packages within that project's tree share
that environment. Projects with different peer versions correctly produce
different instances.
Closes#11070
* fix: pass dedupePeers to getOutdatedLockfileSetting and use spread for lockfile write
The frozen install path (used by approve-builds) calls getOutdatedLockfileSetting
but was missing the dedupePeers parameter. This caused a false LOCKFILE_CONFIG_MISMATCH
error because the lockfile had the key written (as undefined/null via YAML serialization)
while the check function received undefined for the config value.
Fix: pass dedupePeers to the settings check call, and use spread syntax to only write
the dedupePeers key to lockfile settings when it's truthy (avoiding undefined keys).
* fix: write dedupePeers to lockfile like other settings
Write the value directly instead of spread syntax, and use the same
!= null guard pattern as autoInstallPeers in the settings checker.
* test: add integration test for dedupePeers in peerDependencies.ts
* fix: only write dedupePeers to lockfile when enabled
When dedupePeers is false (default), don't write it to lockfile settings.
This avoids adding a new key to every lockfile.
* test: simplify dedupePeers test assertions
* test: check exact snapshot keys in dedupePeers integration test
* test: add workspace test for dedupePeers with different peer versions
* fix: keep transitive peers in suffix with version-only IDs
Instead of pruning transitive peers entirely (which prevented per-project
differentiation), keep them but use version-only identifiers. This way:
- Packages like abc-grand-parent still get a peer suffix when different
projects provide different peer versions (correct per-project isolation)
- But the suffixes use name@version instead of full dep paths, eliminating
the nested parentheses that cause combinatorial explosion
* refactor: extract peerNodeIdToPeerId helper in resolvePeers
* refactor: simplify peerNodeIdToPeerId return
* fix: pin peer-a dist tag in dedupePeers tests for CI stability
* fix: address review comments
- Register dedupe-peers in config schema, types, and defaults so
.npmrc/pnpm-workspace.yaml settings are parsed correctly
- Use Boolean() comparison in settings checker so enabling dedupePeers
on a pre-existing lockfile triggers re-resolution
- Fix changeset text and test names: transitive peers are still
propagated, just with version-only IDs (no nested dep paths)
* perf: skip redundant GVS internal linking on warm reinstall
When GVS is enabled and the store is warm (added === 0), skip
re-creating internal symlinks, re-linking bins inside the GVS store,
and re-importing packages since they already persist outside
node_modules/. Also filter directPkgDirs by hasBin to avoid
unnecessary package.json reads when linking direct dep bins.
* fix: preserve link: deps in hasBin filter for bin linking
The hasBin filter was dropping directories not present in the dep graph
(e.g. link: dependencies), which would silently break bin linking for
linked local packages that expose binaries.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Adds a `--check-peers` flag to `pnpm list` that detects unmet and
missing peer dependency issues by reading the lockfile. This allows
users to check for peer dependency problems without triggering a
full resolution, which is especially useful in CI or after pulling
a lockfile from another developer.
Closes#7087