fix(global): stop `update -g` from downgrading, and pnpm from rolling back
`pnpm update -g --latest` resolves the `latest` dist-tag, which points at
an older release than the one installed whenever that came from another
tag (`pnpm add -g <pkg>@next`) or from a major that has not been promoted
to `latest` yet. The group was reinstalled at whatever `latest` named, so
an update moved packages backwards. Both stacks now compare the resolved
versions against the ones already installed and, when any went backwards,
reinstall the group holding those at the version that is there.
The pnpm CLI's own group is exempt from `update -g` regardless: that
install is `self-update`'s, and reinstalling it here would skip the engine
signature check, the broken-release refusal and the "does it run" probe,
then relink the pnpm home's bins to the result. Naming pnpm explicitly
fails with ERR_PNPM_GLOBAL_PNPM_INSTALL, the rule `add -g` already
applied, and an `npm:` alias no longer slips past either. Two engine
groups side by side also stopped dead-ending every later `up -g` on
ERR_PNPM_GLOBAL_BIN_CONFLICT.
The reporter had a second defect the same command exposed: the differ
redraws by moving the cursor up from the end of its frame, so a frame
taller than the terminal walks past the top of the screen and overwrites
the scrollback above it. The overflowed lines are now handed to the
scrollback, and a frame the differ can no longer reach — outgrown,
shrunk under, or reflowed by a resize — is redrawn afresh below rather
than revised. The Rust height probe works on Windows too.
Closespnpm/pnpm#14270
## Summary
The v11 CLI and the Rust engine, matching #14113 (merged) plus its follow-up #14116.
Two places told users to update pnpm with Corepack:
- The **update notification** printed `corepack use pnpm@<version>` when pnpm ran under Corepack. It now prints pnpm's [standalone install script](https://pnpm.io/installation) — `curl -fsSL https://get.pnpm.io/install.sh | sh -`, or the `Invoke-WebRequest` form on Windows.
- **`pnpm self-update`** refused under Corepack with *"You should update pnpm with corepack"*. It now says it cannot update itself under Corepack, and names the standalone install script as the hint.
The notification also stopped naming `pnpm add -g pnpm`, which `add` refuses outright (`ERR_PNPM_GLOBAL_PNPM_INSTALL` in the TypeScript CLI, `GlobalError::GlobalPnpmInstall` in the Rust one). `pnpm self-update` is named only where it can replace the executable in use — an install another package manager owns is resolved from that manager's bin directory, so a self-update would land beside it and report success while the old pnpm keeps announcing the update:
| `PNPM_HOME` set | under Corepack | suggestion |
| --- | --- | --- |
| yes | no | `pnpm self-update` |
| yes | yes | standalone install script |
| no | either | standalone install script |
The command has one definition per stack — `standaloneInstallCommand` in `@pnpm/cli.meta`, `standalone_install_command` in `pnpm-config` (beside `PNPM_VERSION`) — shared by the notification and the self-update refusal so the two cannot drift.
Keep settings-only workspace manifests scoped to the root package when checking dependency status, filtering projects, and completing --filter values.
These call sites previously passed an undefined package pattern to the projects reader, activating its recursive default even though config had already defined settings-only workspaces as root-only. Preserve the projects reader default for other callers and apply the root-only fallback only at the three manifest-aware boundaries.
Fixespnpm/pnpm#14047.
Both utilities lived in the pnpm/components Bit workspace, where their
component names collide with same-named components elsewhere in the Bit
registry. Move the code here and publish it under new names:
`@pnpm/util.lex-comparator` -> `@pnpm/text.ordinal-comparator`
`@pnpm/config.nerf-dart` -> `@pnpm/config.registry-auth-key`
The new names describe what the utilities do: the comparator is ordinal
rather than locale-aware, and the mapped URL is the key that registry
settings are stored under in `.npmrc`. The exported functions keep their
names, so consumers only change their import specifiers.
Implementations and tests are carried over unchanged. The rationale from
the components' docs pages moves into doc comments: why `localeCompare`
cannot be used for values compared across machines, and where `nerfDart`
originates.
No pacquet counterpart is needed. The Rust stack has its own
implementations of both and no user-visible behavior changes.
Every import was type-only, so the declared dependency shipped the
whole config-reader tree to reporter consumers for nothing — Bit's
mirror-constrained installs were pulling config.env-replace@4.x through
it. The reporter now declares ReporterPnpmConfig, the structural slice
of the config it actually reads; the pnpm CLI's full Config satisfies
it unchanged.
fix(install-summary): suppress '(X is available)' when latest is held back by minimumReleaseAge
When minimumReleaseAge (default: 24h) left the registry's
dist-tags.latest immature, the install summary still printed
'(X is available)' for it — advertising the exact version the policy
had just refused to install.
The hint only ever names the actual latest tag: the resolvers now
surface dist-tags.latest on the resolve result only when the active
policy would allow installing it (latest_allowed_by_policy /
latestAllowedByPolicy — an O(1) check of the tag's publish timestamp
against the cutoff, honoring publishedByExclude full-name and exact-
version entries). An immature latest suppresses the hint instead of
being rewritten to an older mature version, so the hint never names a
non-latest version as latest. Suppression requires positive evidence
of immaturity: a missing or unparsable timestamp keeps the raw tag,
matching the pick itself, which only enforces the policy when the
packument's time map is usable.
Also fixes a pre-existing divergence in the pacquet reporter: it gated
the hint on 'latest != version' rather than 'latest > version', so it
would suggest downgrading when the installed version was newer than
the latest tag. Now uses node-semver comparison, matching the
TypeScript reporter's semver.lt check.
Closespnpm/pnpm#11698.
Close the four CLI-surface differences left over from pnpm/pnpm#13315
after pnpm/pnpm#13359 and pnpm/pnpm#13375 landed the other two.
Three land in the Rust engine. The `Scope:` line arrives via a new
`pnpm:scope` channel and a port of `reportScope.ts`; pnpm gates it on the
command being one that reports scope *and* the run being workspace-wide,
and since the engine's recursive-by-default set is narrower than pnpm's,
that conjunction is computed at dispatch and seeded into the reporter.
The event is emitted where the counts already exist — the `--filter`
resolution and the installer's workspace walk — so no install pays for a
second directory walk. A `sharedWorkspaceLockfile: false` plan reports
once for the whole selection rather than once per child install, which
would otherwise overwrite the filtered count with the workspace total.
The second is the `allowBuilds` placeholder an install writes for a build
it blocked, which pnpm/pnpm#13375 taught the engine to read but not yet to
write. The third is the elapsed time on a cache-satisfied lockfile
verdict.
The fourth goes the other way. The store block a first install prints is
unreachable in the TypeScript CLI: `reportContext` needs
`pnpm:package-import-method`, which only `@pnpm/fs.indexed-pkg-importer`
emits, and that runs inside a `@pnpm/worker` thread whose logger never
reaches the reporter. The main process already receives the import method
back from the worker, so it is reported from there instead.
Reporter globals are now seeded before the pre-command checks. They are
`OnceLock`s and the pre-command checks emit, so whatever fired first was
locking in unset values.
Closespnpm/pnpm#13315
Remove the legacy repository changelog files now that release changelog storage defaults to the registry. The publish path composes and injects CHANGELOG.md into release tarballs, so keeping historical copies in source control duplicates generated release data.
Update adm-zip to the patched 0.6 release and override vulnerable transitive versions after the dependency audit began rejecting versions below 0.6.0.
The strict minimumReleaseAge approval prompt is shown mid-install while
background resolution keeps emitting progress. The default reporter redrew
each frame in place and erased below it, moving the cursor into the prompt's
lines and wiping the question, so the install hung on input the user could
not see.
Bracket the prompt with pnpm:prompt start/end events and hold every frame
redraw while it is open, recreating the differ on resume so the next frame
renders in full below the answered prompt. The erase-below cleanup for
external prompts (e.g. an SSH passphrase) stays intact for the normal case.
Ref: pnpm/pnpm#13019
Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
The team command communicates with the registry through the standard npm team API endpoints. The scope:team format is parsed to separate the organization scope from the team name. For mutation subcommands (create, destroy, add, rm) the registry URL is resolved per scope from the registries map with an optional --registry override, the auth header is resolved from the configured credentials honoring scoped credentials, and the request is sent with retry support and bounded response reads. When an OTP is in play, the Rust side restricts redirects to the configured registry origins so the npm-otp header cannot leak to another host; the TypeScript fetch layer already strips it on cross-host redirects. The ls subcommand dispatches to listing teams within an org when given @scope and to listing members of a specific team when given @scope:team. Output supports three modes, the default human-readable listing, --parseable which emits newline-delimited names, and --json which emits structured arrays.
On the TypeScript side the command is registered in pnpm/src/cmd/index.ts and removed from the notImplemented list. On the Rust side it is added as a CliCommand variant, routed in dispatch, and dispatched in dispatch_query. Both sides include comprehensive tests covering all subcommands, error paths for 401 403 404 and 409 responses, empty results, and the three output formats.
pnpr serves the npm team API from each hosted registry's config-declared teams: GET /-/org/{scope}/team and GET /-/team/{scope}/{team}/user list teams and members, gated by the registry-level access with denials masked as not-found, while team mutations answer an explicit 403 since pnpr teams are config-managed.
@pnpm/cli.parse-cli-args no longer stops option parsing at an escape word (create, exec, test) that appears as another command's parameter, which previously made pnpm team create drop a trailing --registry option.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Pure directory move plus path fixups: the Rust port ships as pnpm v12,
so the source tree now lives at pnpm/ (alongside pnpm11/, the frozen
TypeScript line). No identifiers change in this pass — crate names
(pacquet-*), the pacquet bin, PACQUET_VERSION, the @pacquet/* npm
package names v11's runPacquet spawns, the .pacquet virtual-store dir,
the benchmark harness's clone dir, and the pacquet-*.yml workflow
filenames (npm trusted publishing is bound to them) all stay for a
follow-up.
Also removes the root /pnpm/ .gitignore entry (build detritus in the
pre-pnpm11 package location): pnpm/ is real source now and must not be
ignored. Developers with a stale generated pnpm/ dir should delete it
before checking out this change.
Register the pn short alias in generated shell completion scripts.
pnpm exposes pn as a binary alias, but completion generation only registered
pnpm with the supported shells. This meant zsh completions generated by
pnpm completion zsh registered pnpm only, so pn did not receive the same
completion function.
Post-process the tabtab-generated scripts to register pn alongside pnpm for
bash, fish, pwsh, and zsh, and cover each shell in the completion generator
tests.
Fixespnpm/pnpm#11955.
---------
Co-authored-by: ychampion <ychampion@users.noreply.github.com>
Co-authored-by: Zoltan Kochan <zoltankochan@gmail.com>
* fix(default-reporter): restore ansi-diff to stop duplicated output lines
PR pnpm/pnpm#12351 replaced ansi-diff with manual full-frame reprint,
which re-writes unchanged sticky blocks on every progress tick. This
caused cached lockfile verdicts and deprecation warnings to appear
dozens of times in terminals that record each write (Warp, script,
CI logs). The manual countRows also produced unbounded cursor-up
values when the frame was taller than the terminal, causing an
infinite repaint loop on pnpm run --parallel.
TypeScript CLI: restore the `ansi-diff` npm package for differential
rendering, wrapping its output with \r (column reset) + \x1b[0J
(erase below frame) to preserve the external-process-output fix
from pnpm/pnpm#12351.
Rust pacquet port: implement a zero-dependency differential renderer
(`diff.rs`) with the same algorithm, replacing the full-frame reprint
in `Sink::write_output`.
Closespnpm/pnpm#12634.
* fix(default-reporter): pass dylint, add trailing newline, harden reporter test
Builds on the restored ansi-diff differential renderer:
- pacquet: guarantee a trailing newline before diffing a frame, matching the
TypeScript reporter. This keeps an interactive prompt on a fresh line below
the frame instead of joined onto the last progress line, and leaves the
differ's tracked cursor column at 0 so it stays in sync with the \r
prepended on the next update (otherwise the inline diff computes relative
moves from a stale column and corrupts in-place rendering).
- pacquet: move the differ's inline test module into src/diff/tests.rs with
explicit imports and drop a stray trailing comma, so cargo dylint passes.
- TypeScript: locate the sticky lockfile verdict's first render with findIndex
instead of assuming writes[0], and wrap each reporter test body in
try/finally so the subscription is always stopped.
* fix(default-reporter): align rust diff with ansi-diff edge cases
- Add col !== width guard before inline diff, matching the JS
ansi-diff edgecase check that prevents mispositioning at exact-width
wrap boundaries.
- Fix clearDown to start from the cursor's current row (self.row)
instead of new_last_row, matching the JS _clearDown pattern. The
previous code could clear frame content when trailing lines were
unchanged and the frame shrank.
- Document the visible_width vs wcwidth simplification.
* test(default-reporter): expand diff coverage to 14 tests
Add 8 new tests covering:
- inline diff skipped for ANSI-containing lines
- inline diff threshold edge case
- full line rewrite with clear
- clear_down starting from cursor row (not new_last_row)
- soft-wrapped line height tracking
- empty frame clears all previous content
- multiple progress ticks (simulated install sequence)
- frame without trailing newline
* fix(default-reporter): tighten test assertions and avoid hot-path allocation
- clear_down_from_cursor_row: replace weak || assertion with direct
checks that Line A is absent, Line B changed is present, and clear
sequence is emitted.
- TS reporterRenderer: capture writes.length before the 'fetched'
event and wait for an increase, so the test can't pass early.
- pacquet Sink::write_output: write \r, diff output, and erase codes
as separate write_all calls instead of format!() which allocated a
new String on every frame.
* fix(default-reporter): make frame redraw a single write and fix diff test
Compose the \r + diff + erase sequence into a reusable per-sink buffer and
emit it with one write_all, keeping each frame update atomic against
interleaved writers without allocating on the hot progress path.
Rework inline_diff_skipped_for_small_change (which actually exercised the
inline-diff happy path already covered by inline_diff_writes_only_changed_chars)
into inline_diff_skipped_for_short_common_affix, covering the
left + right <= 4 full-rewrite branch its name promised.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
* fix: avoid passing infinite width to cli-truncate when streaming lifecycle output
cli-truncate 6.1.0 rejects a non-finite width, which crashed the reporter
when streaming lifecycle script output (streamLifecycleOutput passes Infinity
to mean "do not truncate").
* chore: drop changeset
The broken build this fixes was never released, so no release note is needed.
The TypeScript pnpm CLI freezes at v11; pnpm 12 will be the Rust pacquet
port. To make that split legible, all TypeScript source, test, and build
directories move under a new top-level pnpm11/ directory. The name states
the version boundary rather than implying a behavioral fork, since the two
stacks are meant to behave identically.
Scope is source-only: the shared workspace root stays at the repo root.
pnpm-workspace.yaml, package.json, pnpm-lock.yaml, .pnpmfile.cjs,
.meta-updater, __patches__, .changeset, .husky, and the lint/spell configs
remain in place, so one pnpm workspace and one Cargo workspace still span
all three products. pnpr/client and pacquet/tasks/registry-mock stay as
cross-product workspace members.
Rewiring the move required:
- pnpm-workspace.yaml globs prefixed with pnpm11/
- root package.json script paths, eslint.config.mjs, tsconfig.lint.json,
.gitignore, and CODEOWNERS updated
- .meta-updater/src/index.ts literals repointed (pnpm11/pnpm/package.json,
pnpm11/__utils__, pnpm11/__typings__, and the main package directory)
- regenerated every moved package's repository/homepage URL via meta-updater
- pnpm11/pnpm/bundle-deps.ts and __utils__/scripts/src/typecheck-only.ts
climb one more level to reach the repo root
.meta-updater stays at the repo root because @pnpm/meta-updater resolves
its config at <cwd>/.meta-updater/main.mjs.
TS CI (.github/workflows/ci.yml) now only runs when pnpm11/-relevant paths
change, via a dorny/paths-filter changes job plus a TS CI / Success
aggregate gate; branch protection should require only that gate.