Skip to content

feat(install): add --dry-run option (npm-style preview)#12449

Merged
zkochan merged 10 commits into
mainfrom
feat/dry-run-install-v2
Jun 16, 2026
Merged

feat(install): add --dry-run option (npm-style preview)#12449
zkochan merged 10 commits into
mainfrom
feat/dry-run-install-v2

Conversation

@zkochan

@zkochan zkochan commented Jun 16, 2026

Copy link
Copy Markdown
Member

Description

Adds a --dry-run option to pnpm install with npm-style preview semantics: it runs a full dependency resolution and reports what a real install would add/remove/update, but writes nothing to disk (no lockfile, no node_modules, no .modules.yaml, no workspace-state file) and always exits 0.

$ pnpm install --dry-run
Dry run complete. A real install would make the following changes (nothing was written to disk):

Importers
.
  + is-negative 1.0.0

Packages
+ is-negative@1.0.0

When the lockfile is already up to date it prints Dry run complete. pnpm-lock.yaml is up to date; a real install would make no changes.

Resolves #7340.

Why this shape

An earlier attempt (#12270, now closed) implemented --dry-run as --frozen-lockfile --lockfile-only — i.e. a fail-on-drift lockfile validator. That collides with the well-established meaning of --dry-run across npm/yarn ("preview, never fail") and duplicated existing behaviour (pnpm install --frozen-lockfile --lockfile-only already does that). This PR implements the intuitive preview meaning instead.

How it works (pnpm)

  • Reuses the existing lockfileCheck callback (resolve fully, skip the lockfile write, hand back the before/after wanted lockfile) plus lockfileOnly (skip node_modules, the workspace-state file, and metadata-cache writes).
  • The frozen/headless fast path is disabled whenever lockfileCheck is set, so a check-only install always resolves and never materialises anything.
  • The before/after lockfiles are diffed (reusing the dedupe diff engine, now exported as calcDedupeCheckIssues) and rendered into the report.
  • --dry-run with a configured pnpr server is rejected (that path resolves/links through the server).

Pacquet

Ported in the second commit — pacquet install --dry-run forces the fresh-resolve path, skips every write (a new dry_run flag on InstallWithFreshLockfile skips the pnpm-lock.yaml save), and a new dry_run module diffs the existing lockfile against the freshly-resolved one and prints the same report.

Tests

  • pnpm: deps-installer + install-command tests for the no-write guarantee, the would-change report, the up-to-date case, and the pnpr-server conflict; verified end-to-end against the bundled CLI.
  • pacquet: clap flag-parse test, diff/render unit tests, and three integration tests (fresh project, stale-against-existing-lockfile, up-to-date).

Written by an agent (Claude Code, claude-opus-4-8).

Summary by CodeRabbit

Release Notes

  • New Features
    • Added --dry-run to install to resolve dependencies and report what would change without writing pnpm-lock.yaml or creating node_modules (no lockfile updates; exits successfully).
    • Dry-run reports include dedupe-related changes when applicable.
  • Bug Fixes
    • Made check-only dry-run handling consistent across supported install flows, including correct “up to date” output.
  • Tests
    • Added/expanded integration tests for reported changes, lockfile immutability, and up-to-date cases.
  • Documentation
    • Clarified dryRun help/inline documentation to match the preview behavior.

zkochan added 2 commits June 16, 2026 13:23
…ting

Implements npm-style `pnpm install --dry-run`: it runs a full dependency
resolution and reports what a real install would add/remove/update, but
writes nothing to disk (no lockfile, no node_modules, no workspace-state
file, no metadata cache) and always exits 0.

Mechanism:
- The install command sets the existing `lockfileCheck` callback plus
  `lockfileOnly`, which together make the installer resolve fully while
  suppressing every write, and hand back the wanted lockfile before and
  after resolution for comparison.
- The frozen/headless fast path is disabled whenever `lockfileCheck` is set
  so a check-only install always resolves and never materializes anything.
- The before/after lockfiles are diffed (reusing the dedupe diff engine,
  now exported as `calcDedupeCheckIssues`) and rendered into a human
  report.

`--dry-run` with a configured pnpr server is rejected, since that path
resolves and links through the server.

Resolves #7340

---
Written by an agent (Claude Code, claude-opus-4-8).
Mirrors the pnpm-side `install --dry-run`: resolve fully but write
nothing, then report what a real install would change, and exit 0.

- `--dry-run` forces the fresh-resolve path (so the would-be lockfile is
  always computed) and reuses the lockfile-only plumbing to skip
  node_modules / .modules.yaml / workspace-state, while additionally
  skipping the pnpm-lock.yaml write in InstallWithFreshLockfile.
- New `dry_run` module diffs the existing on-disk lockfile against the
  freshly-resolved one (importer direct deps + packages) and renders a
  report printed to stdout.

Other Install constructions (add/remove/update, pnpr resolve) pass
dry_run: false.

---
Written by an agent (Claude Code, claude-opus-4-8).
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (0) 📎 Requirement gaps (1) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0)

Grey Divider


Action required

1. pnpr path forces dry_run: false ✓ Resolved 📎 Requirement gap ≡ Correctness
Description
When a pnprServer is configured, pacquet install --dry-run takes the pnpr fast path and
constructs an Install with dry_run: false, so it can perform a real, mutating install instead of
a non-writing preview. This violates the dry-run requirement to be non-mutating.
Code

pacquet/crates/cli/src/cli_args/install.rs[R694-699]

         supported_architectures: link.supported_architectures,
         node_linker: link.node_linker,
         lockfile_only: false,
+            dry_run: false,
         update_seed_policy: UpdateSeedPolicy::KeepAll,
         auth_override: None,
Evidence
The --dry-run flag is parsed into dry_run, but if config.pnpr_server is set, execution returns
early into install_via_pnpr(...) without using dry_run. Inside install_via_pnpr, an Install
is created with lockfile_only: false and dry_run: false, which can write to disk (e.g.
create/link node_modules).

Implement a dry-run option for pnpm install
pacquet/crates/cli/src/cli_args/install.rs[332-432]
pacquet/crates/cli/src/cli_args/install.rs[677-702]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`pacquet install --dry-run` is not enforced when `pnprServer` is configured: the code takes the pnpr path and proceeds with a real install by setting `dry_run: false`.
## Issue Context
pnpm’s implementation explicitly rejects `--dry-run` with a configured pnpr server because that path resolves/links through the server. pacquet should match that contract to preserve the “writes nothing to disk” guarantee.
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/install.rs[409-432]
- pacquet/crates/cli/src/cli_args/install.rs[677-702]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Dry-run can exit nonzero ✓ Resolved 🐞 Bug ☼ Reliability
Description
pnpm install --dry-run throws PnpmError('DRY_RUN_UNSUPPORTED') when the installer cannot surface
before/after lockfiles, which will produce a non-zero exit code. This contradicts the PR’s release
note claiming dry-run “always exits with code 0” and can break automation expecting non-failing
preview behavior.
Code

installing/commands/src/install.ts[R453-461]

+  const dryRunResult = await installDeps(installDepsOptions, [])
+  if (dryRunResult == null) {
+    // Fail closed rather than render "up to date": a missing comparison means
+    // this install configuration's resolve path doesn't yet surface the
+    // dry-run lockfiles (e.g. a workspace without a shared lockfile), so we
+    // can't tell whether the lockfile would change.
+    throw new PnpmError('DRY_RUN_UNSUPPORTED',
+      '--dry-run is not supported for this install configuration (no shared lockfile to compare)')
+  }
Evidence
The changeset explicitly promises exit code 0, but the new dry-run path throws DRY_RUN_UNSUPPORTED
when no lockfile comparison is available, which propagates as a command failure.

.changeset/dry-run-install.md[8-8]
installing/commands/src/install.ts[441-462]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`dryRunInstall()` throws `DRY_RUN_UNSUPPORTED` when `installDeps()` returns no lockfile comparison, causing `pnpm install --dry-run` to exit non-zero. The PR’s changeset text says dry-run always exits 0, so this is a contract mismatch.
### Issue Context
The thrown error happens when `dryRunResult == null` (comment cites e.g. workspaces without a shared lockfile), so users can hit it depending on install topology.
### Fix Focus Areas
- Make `--dry-run` non-failing per stated contract by returning an informational string instead of throwing (or, if the intended behavior is to fail, update the changeset/help text to remove the “always exits 0” claim).
- installing/commands/src/install.ts[453-461]
- .changeset/dry-run-install.md[8-8]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Rustfmt check will fail 🐞 Bug ⚙ Maintainability
Description
pacquet/crates/package-manager/src/install/tests.rs has a struct initializer line where the new
dry_run field is jammed onto the same line with update_seed_policy and inconsistent spacing,
which will be rewritten by rustfmt. Pacquet’s contributor docs state CI enforces cargo fmt, so
this is likely to fail format checks.
Code

pacquet/crates/package-manager/src/install/tests.rs[R4051-4053]

     lockfile_only: false,
-        update_seed_policy: crate::UpdateSeedPolicy::KeepAll,
+        dry_run: false,        update_seed_policy: crate::UpdateSeedPolicy::KeepAll,
     auth_override: None,
Evidence
The misformatted initializer is present in the test file, and pacquet’s documented workflow/CI runs
cargo fmt, meaning formatting diffs are merge-blocking.

pacquet/crates/package-manager/src/install/tests.rs[4049-4053]
pacquet/CONTRIBUTING.md[45-49]
pacquet/CONTRIBUTING.md[91-100]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A Rust test struct initializer contains two fields on one line with inconsistent spacing (`dry_run: false,        update_seed_policy: ...`). This is not rustfmt-normalized formatting and is likely to fail `cargo fmt --check`.
### Issue Context
Pacquet’s CONTRIBUTING explicitly states formatting is enforced by `cargo fmt` and CI runs it.
### Fix Focus Areas
- Split the fields onto separate lines (or just run `cargo fmt --all`) so the file matches rustfmt output.
- pacquet/crates/package-manager/src/install/tests.rs[4049-4053]
- pacquet/CONTRIBUTING.md[45-49]
- pacquet/CONTRIBUTING.md[91-100]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (5)
4. --dry-run skips fetch/link/build 📎 Requirement gap ☼ Reliability
Description
The new --dry-run path forces lockfileOnly, which causes the resolver to set skipFetch and the
installer to skip the link/build phases. As a result, pnpm install --dry-run can exit successfully
even when a real install would fail during tarball fetch, linking, or lifecycle/build steps, so it
does not reliably simulate install validity for automation.
Code

installing/commands/src/install.ts[R440-453]

+async function dryRunInstall (installDepsOptions: InstallDepsOptions, opts: InstallCommandOptions): Promise<string> {
+  if (opts.pnprServer) {
+    throw new PnpmError('CONFIG_CONFLICT_DRY_RUN_WITH_PNPR_SERVER',
+      'Cannot use --dry-run with a configured pnpr server because the pnpr install path resolves and links through the server')
+  }
+  // `dryRun` makes the installer resolve fully and return the before/after
+  // wanted lockfile without writing anything. `lockfileOnly` keeps it from
+  // materializing `node_modules` and skips the metadata cache (resolution
+  // skips fetching). The optimistic fast path is disabled so resolution
+  // always runs.
+  installDepsOptions.optimisticRepeatInstall = false
+  installDepsOptions.lockfileOnly = true
+  installDepsOptions.dryRun = true
+  const dryRunResult = await installDeps(installDepsOptions, [])
Evidence
dryRunInstall() explicitly sets installDepsOptions.lockfileOnly = true, which flows into the
deps-installer and resolver. The resolver uses skipFetch: ctx.dryRun, and deps-installer sets
dryRun: opts.lockfileOnly in resolution and gates the link/build pipeline on !opts.lockfileOnly,
so dry-run omits phases where real installs can fail.

pnpm install supports a dry-run mode
Dry-run mode yields meaningful success and failure exit codes
installing/commands/src/install.ts[440-453]
installing/deps-installer/src/install/index.ts[1534-1562]
installing/deps-installer/src/install/index.ts[1671-1775]
installing/deps-resolver/src/resolveDependencies.ts[1689-1723]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`pnpm install --dry-run` currently forces `lockfileOnly`, which sets `skipFetch` in dependency requests and skips the link/build phases. This means dry-run can report success even when a real install would fail later (fetch/link/scripts), so it is not a reliable install-validity check for automation.
## Issue Context
The compliance requirement expects dry-run to perform the same validations needed to determine whether a real install would succeed, while avoiding side effects.
## Fix Focus Areas
- installing/commands/src/install.ts[440-453]
- installing/deps-installer/src/install/index.ts[1560-1560]
- installing/deps-installer/src/install/index.ts[1671-1775]
- installing/deps-resolver/src/resolveDependencies.ts[1689-1723]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Dry-run writes workspace manifest ✓ Resolved 🐞 Bug ≡ Correctness
Description
dryRunInstall() does not disable save, so installDeps() can still persist policy-driven
updates (e.g. minimumReleaseAgeExclude) to pnpm-workspace.yaml, violating the “writes nothing to
disk” dry-run guarantee.
Code

installing/commands/src/install.ts[R440-453]

+async function dryRunInstall (installDepsOptions: InstallDepsOptions, opts: InstallCommandOptions): Promise<string> {
+  if (opts.pnprServer) {
+    throw new PnpmError('CONFIG_CONFLICT_DRY_RUN_WITH_PNPR_SERVER',
+      'Cannot use --dry-run with a configured pnpr server because the pnpr install path resolves and links through the server')
+  }
+  // `dryRun` makes the installer resolve fully and return the before/after
+  // wanted lockfile without writing anything. `lockfileOnly` keeps it from
+  // materializing `node_modules` and skips the metadata cache (resolution
+  // skips fetching). The optimistic fast path is disabled so resolution
+  // always runs.
+  installDepsOptions.optimisticRepeatInstall = false
+  installDepsOptions.lockfileOnly = true
+  installDepsOptions.dryRun = true
+  const dryRunResult = await installDeps(installDepsOptions, [])
Evidence
The dry-run wrapper enables check-only mode but doesn’t disable save, while installDeps() will
still write pnpm-workspace.yaml when policy updates are produced; minimumReleaseAge policy
explicitly generates such persisted updates.

installing/commands/src/install.ts[440-453]
installing/commands/src/installDeps.ts[447-469]
installing/commands/src/policyHandlers.ts[161-196]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`pnpm install --dry-run` is intended to write nothing to disk, but the current dry-run wrapper only sets `lockfileOnly` and `dryRun`. This still allows `installDeps()` to perform workspace-manifest writes (and potentially other persistence guarded by `opts.save !== false`) when policy handlers produce updates.
## Issue Context
- `dryRunInstall()` sets `lockfileOnly`/`dryRun` but does not set `save: false`.
- `installDeps()` may call `updateWorkspaceManifest()` even for a plain install when `policyUpdates != null`.
## Fix Focus Areas
- installing/commands/src/install.ts[440-453]
- installing/commands/src/installDeps.ts[447-469]
- installing/commands/src/policyHandlers.ts[161-196]
## Expected change
- In `dryRunInstall()`, explicitly disable persistence by setting `installDepsOptions.save = false` (and consider also `installDepsOptions.saveLockfile = false` for belt-and-suspenders).
- Add (or consider adding) a defensive guard in `installDeps()` to skip `writeProjectManifest()` / `updateWorkspaceManifest()` when `opts.dryRun === true`, so future dry-run entrypoints can’t accidentally persist.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Dry-run ignores lockfile specifiers ✓ Resolved 📎 Requirement gap ≡ Correctness
Description
install --dry-run determines “up to date” status using a dedupe-oriented diff that only checks
importer dependency fields and package dependency wiring, not other lockfile changes like importer
specifiers. This can incorrectly report pnpm-lock.yaml is up to date when a real install would
still rewrite the lockfile, undermining its use for detecting outdated lockfiles in hook automation.
Code

installing/commands/src/install.ts[R461-467]

+function renderDryRunReport (changes: Array<ReturnType<typeof calcDedupeCheckIssues>>): string {
+  const reports = changes
+    .filter((issues) => countDedupeCheckIssues(issues) > 0)
+    .map(renderDedupeCheckIssues)
+  if (reports.length === 0) {
+    return `Dry run complete. ${WANTED_LOCKFILE} is up to date; a real install would make no changes.`
+  }
Evidence
The compliance objective requires dry-run to detect outdated lockfiles without performing a real
installation. The current dry-run “up to date” message is driven by countDedupeCheckIssues(), but
the underlying diff (calcDedupeCheckIssues) only compares importer dependency fields and limited
package wiring fields, so lockfile changes outside those fields (notably importer specifiers) will
not be detected and can yield an incorrect “up to date” result.

Add a dry-run option for the install command
installing/commands/src/install.ts[461-467]
installing/dedupe/check/src/dedupeDiffCheck.ts[11-23]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`pnpm install --dry-run` may print `pnpm-lock.yaml is up to date` even when a real install would rewrite the lockfile, because the diff logic used for the report does not account for importer `specifiers` (and potentially other non-dependency-wiring lockfile changes).
## Issue Context
The dry-run report is generated from `calcDedupeCheckIssues()` / `countDedupeCheckIssues()`, which only diffs importer `dependencies`/`devDependencies`/`optionalDependencies` and package snapshot `dependencies`/`optionalDependencies`. Importer `specifiers` are not included, so specifier-only manifest drift (or other lockfile-only rewrites) can be missed while still reporting “up to date”.
## Fix Focus Areas
- installing/commands/src/install.ts[461-467]
- installing/dedupe/check/src/dedupeDiffCheck.ts[11-23]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Dry-run ignores snapshots ✓ Resolved 🐞 Bug ≡ Correctness
Description
In pacquet, diff_lockfiles() only diffs lockfile.packages keys and never inspects
lockfile.snapshots, even though v9 lockfiles store dependency wiring and peer-variant state in
snapshots. As a result, pacquet install --dry-run can incorrectly report “pnpm-lock.yaml is up
to date” while a real install would change the lockfile (e.g., snapshot/peer-variant rewrites).
Code

pacquet/crates/package-manager/src/dry_run.rs[R75-88]

+    let new_keys = package_keys(Some(new));
+    let old_keys = package_keys(old);
+    diff.added_packages = new_keys.difference(&old_keys).cloned().collect();
+    diff.removed_packages = old_keys.difference(&new_keys).cloned().collect();
+
+    diff
+}
+
+fn package_keys(lockfile: Option<&Lockfile>) -> BTreeSet<String> {
+    lockfile
+        .and_then(|lockfile| lockfile.packages.as_ref())
+        .map(|packages| packages.keys().map(ToString::to_string).collect())
+        .unwrap_or_default()
+}
Evidence
The dry-run diff only collects key sets from lockfile.packages, but pacquet’s v9 lockfile schema
has a separate snapshots map that contains dependency wiring
(dependencies/optional_dependencies). Since peer-specific wiring lives in snapshots (not
packages), snapshot-only changes are invisible to the current diff and can produce a false empty
diff.

pacquet/crates/package-manager/src/dry_run.rs[53-88]
pacquet/crates/lockfile/src/lib.rs[188-206]
pacquet/crates/lockfile/src/package_metadata.rs[5-8]
pacquet/crates/lockfile/src/snapshot_entry.rs[17-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`pacquet install --dry-run` determines whether the lockfile would change by comparing importer direct-deps and the set of keys in `lockfile.packages`. However, in pacquet’s v9 lockfile model, dependency wiring and peer-variant information is stored under `lockfile.snapshots` (`SnapshotEntry`). Because the dry-run diff never reads `snapshots`, it can miss lockfile changes that are confined to snapshots and incorrectly print the “up to date” message.
### Issue Context
- `Lockfile` has both `packages` (per-version metadata) and `snapshots` (per-peer wiring).
- `SnapshotEntry` contains dependency maps that can change without the `packages` set changing.
### Fix Focus Areas
- pacquet/crates/package-manager/src/dry_run.rs[53-88]
### Suggested fix approach
1. Extend `LockfileDiff` to account for `snapshots` changes:
- At minimum, include added/removed snapshot keys (`snapshots` key set diff).
- Preferably also detect updated snapshot entries for keys present in both old and new by diffing `SnapshotEntry.dependencies` and `SnapshotEntry.optional_dependencies` (mirroring pnpm’s dedupe diff behavior).
2. Update `LockfileDiff::is_empty()` to consider snapshot changes.
3. Update `render_dry_run_report()` to render snapshot-related changes (can be under the existing “Packages” section or a new “Snapshots” section), ensuring the report is non-empty whenever `pnpm-lock.yaml` would be rewritten.
4. Add/extend a unit/integration test that creates a snapshot-only change (e.g., peer-variant change) and asserts `--dry-run` does not report “up to date”.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. --dry-run always exits 0 ✓ Resolved 📎 Requirement gap ≡ Correctness
Description
The new --dry-run implementation is explicitly designed to “exit successfully regardless of
whether changes were found”, so a stale/out-of-date lockfile cannot be detected via exit code. This
violates the requirement that dry-run exit codes distinguish a successful would-be install from a
failing one (including outdated lockfile cases).
Code

installing/commands/src/install.ts[R434-458]

+/**
+ * Runs a full resolution but writes nothing to disk (no lockfile, no
+ * `node_modules`), then reports what a real install would change. Exits
+ * successfully regardless of whether changes were found — mirroring the
+ * preview semantics of `npm install --dry-run`.
+ */
+async function dryRunInstall (installDepsOptions: InstallDepsOptions, opts: InstallCommandOptions): Promise<string> {
+  if (opts.pnprServer) {
+    throw new PnpmError('CONFIG_CONFLICT_DRY_RUN_WITH_PNPR_SERVER',
+      'Cannot use --dry-run with a configured pnpr server because the pnpr install path resolves and links through the server')
+  }
+  // `lockfileCheck` makes the installer resolve fully but skip the lockfile
+  // write and hand back the wanted lockfile before and after resolution.
+  // `lockfileOnly` suppresses every other write — node_modules linking, the
+  // workspace-state file, and even the metadata cache (resolution skips
+  // fetching). Together they make the run side-effect-free. The optimistic
+  // fast path is disabled so the comparison always runs.
+  installDepsOptions.optimisticRepeatInstall = false
+  installDepsOptions.lockfileOnly = true
+  const changes: ReturnType<typeof calcDedupeCheckIssues>[] = []
+  installDepsOptions.lockfileCheck = (prev: LockfileObject, next: LockfileObject) => {
+    changes.push(calcDedupeCheckIssues(prev, next))
+  }
+  await installDeps(installDepsOptions, [])
+  return renderDryRunReport(changes)
Evidence
The checklist requires dry-run to return a failure exit code when the would-be install fails
(including outdated lockfile). The PR explicitly documents and implements --dry-run as always
succeeding when changes are found, so exit codes do not distinguish lockfile drift from a clean
state.

Add dry-run option to pnpm install with meaningful exit codes
installing/commands/src/install.ts[434-458]
.changeset/dry-run-install.md[8-8]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`pnpm install --dry-run` currently always exits successfully even when it detects that a real install would change the lockfile (i.e. the lockfile is stale). Compliance requires meaningful exit codes: dry-run must return non-zero when the would-be install would fail (including outdated lockfile cases).
## Issue Context
The PR documentation and code comment state that `--dry-run` “always exits with code 0” / “exits successfully regardless of whether changes were found”, which prevents using it in pre-commit/pre-push hooks to block lockfile drift.
## Fix Focus Areas
- installing/commands/src/install.ts[434-473]
- .changeset/dry-run-install.md[1-8]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

9. dryRun affects non-install ✓ Resolved 🐞 Bug ≡ Correctness
Description
dryRun is now threaded through InstallDepsOptions from general Config, and deps-installer
treats opts.dryRun===true as “check-only” (suppresses writes and returns a dryRunResult).
Because non-install commands (e.g. add) forward ...opts into installDeps() without forcing
dryRun=false and ignore the returned dryRunResult, a config-level dryRun=true can make those
commands silently do a resolution-only no-op.
Code

installing/commands/src/installDeps.ts[R176-184]

* subcommand — see `runPacquet.ts`'s `noRuntime` opt.
*/
isInstallCommand?: boolean
-} & Partial<Pick<Config, 'pnpmHomeDir' | 'strictDepBuilds' | 'useLockfile' | 'useGitBranchLockfile'>>
+} & Partial<Pick<Config, 'dryRun' | 'pnpmHomeDir' | 'strictDepBuilds' | 'useLockfile' | 'useGitBranchLockfile'>>
export async function installDeps (
opts: InstallDepsOptions,
params: string[]
-): Promise<void> {
+): Promise<DryRunInstallResult | undefined> {
Evidence
dryRun is part of the global config shape and is passed into installDeps(), which uses it to
skip persistence and return dryRunResult. deps-installer also treats opts.dryRun as check-only
(suppressing lockfile writes). add forwards ...opts and ignores the returned dryRunResult, so
a config-level dryRun=true changes behavior outside pnpm install without any user-visible
report.

config/reader/src/Config.ts[89-92]
installing/commands/src/installDeps.ts[179-184]
installing/commands/src/installDeps.ts[411-439]
installing/commands/src/add.ts[308-316]
installing/deps-installer/src/install/index.ts[1409-1417]
installing/deps-installer/src/install/index.ts[1919-1927]
installing/deps-installer/src/install/index.ts[1996-1998]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`dryRun` is a general config option and is included in `InstallDepsOptions`. If a user sets `dryRun=true` in rc/config, commands like `pnpm add` can inherit it, triggering deps-installer’s check-only behavior (no writes) while the command handler neither prints a dry-run report nor performs the intended mutation.
### Issue Context
- `Config.dryRun` is documented as an `install` behavior, but it is not inherently command-scoped.
- `installDeps()` gates persistence on `!opts.dryRun` and returns `dryRunResult`, but callers like `add` drop the return value.
### Fix Focus Areas
- Ensure non-`install` command handlers explicitly override `dryRun: false` when calling `installDeps()` (similar to pacquet’s add/remove/update explicitly setting `dry_run: false`).
- Alternatively (or additionally), gate `installDeps()`’s dry-run-specific behavior on `opts.isInstallCommand === true` so rc-level `dryRun` doesn’t affect unrelated commands.
- Consider emitting/returning a report (or a clear warning) if `opts.dryRun` is detected in non-install flows.
- config/reader/src/Config.ts[89-92]
- installing/commands/src/installDeps.ts[179-184]
- installing/commands/src/installDeps.ts[411-439]
- installing/commands/src/add.ts[308-316]
- installing/commands/src/dedupe.ts[58-71]
- installing/commands/src/update/index.ts[316-323]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Settings-only lockfile diffs missed 🐞 Bug ≡ Correctness
Description
renderDryRunReport() decides “up to date” by diffing only importer and package snapshots, but the
installer can rewrite top-level lockfile fields like settings/overrides/checksums, so dry-run
can incorrectly claim no lockfile changes.
Code

installing/commands/src/install.ts[R465-468]

+function renderDryRunReport (dryRunResult: DryRunInstallResult): string {
+  const issues = calcDedupeCheckIssues(dryRunResult.originalLockfile, dryRunResult.wantedLockfile, { includeImporterSpecifiers: true })
+  if (countDedupeCheckIssues(issues) === 0) {
+    return `Dry run complete. ${WANTED_LOCKFILE} is up to date; a real install would make no changes.`
Evidence
Deps-installer mutates lockfile settings (and sometimes other top-level fields) based on config
and real installs persist newLockfile, but the dry-run diff logic only checks importers/packages;
thus metadata-only changes won’t be detected and can be reported as ‘up to date’.

installing/commands/src/install.ts[465-469]
installing/deps-installer/src/install/index.ts[661-711]
installing/deps-installer/src/install/index.ts[1919-1926]
installing/dedupe/check/src/dedupeDiffCheck.ts[35-45]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The dry-run report uses `calcDedupeCheckIssues()` which only diffs importer snapshots and package snapshots. However, the installer mutates top-level lockfile fields (e.g. `settings`, and when full resolution is needed also `overrides`, `packageExtensionsChecksum`, etc.), and a real install will persist those changes.
This can cause a false “pnpm-lock.yaml is up to date; a real install would make no changes” message even when a real install would rewrite `pnpm-lock.yaml` due to metadata changes.
## Issue Context
- Lockfile settings/metadata are updated based on current config.
- Lockfile persistence writes the whole `newLockfile`.
- The dry-run diff currently ignores these top-level fields.
## Fix Focus Areas
- installing/commands/src/install.ts[465-469]
- installing/dedupe/check/src/dedupeDiffCheck.ts[35-45]
- installing/deps-installer/src/install/index.ts[661-711]
- installing/deps-installer/src/install/index.ts[1919-1926]
## Expected change
- Extend the dry-run comparison to include relevant top-level lockfile fields that can change during install (at minimum `settings`; consider also `overrides`, `ignoredOptionalDependencies`, `packageExtensionsChecksum`, `pnpmfileChecksum`, `patchedDependencies`, etc.).
- Ensure `renderDryRunReport()` treats those metadata changes as “would make changes”, and ideally renders a small “Lockfile” section or an explicit note when only metadata differs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Dry-run skips workspace expansion 🐞 Bug ≡ Correctness
Description
In installInContext(), the workspace-partial install branch that expands the project set to all
importers under the shared lockfile is now gated by !isCheckOnlyInstall(opts). Because `pnpm
install --dry-run sets opts.dryRun=true`, dry-run can resolve only the selected project(s) and
produce an incomplete/incorrect wanted-lockfile diff versus a real install in that workspace
scenario.
Code

installing/deps-installer/src/install/index.ts[R2075-2081]

if (!opts.frozenLockfile && opts.useLockfile) {
const allProjectsLocatedInsideWorkspace = Object.values(ctx.projects)
 .filter((project) => isPathInsideWorkspace(project.rootDirRealPath ?? project.rootDir))
-      if (allProjectsLocatedInsideWorkspace.length > projects.length && opts.lockfileCheck == null && opts.enableModulesDir) {
+      if (allProjectsLocatedInsideWorkspace.length > projects.length && !isCheckOnlyInstall(opts) && opts.enableModulesDir) {
 const newProjects = [...projects]
 const getWantedDepsOpts = {
   autoInstallPeers: opts.autoInstallPeers,
Evidence
pnpm install --dry-run explicitly sets installDepsOptions.dryRun = true, which makes
isCheckOnlyInstall(opts) true. The workspace-partial branch that expands the project set only runs
when !isCheckOnlyInstall(opts), so dry-run skips the same expansion a real install uses to resolve
against the full shared workspace lockfile when projects is a subset.

installing/commands/src/install.ts[428-453]
installing/deps-installer/src/install/index.ts[2072-2128]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`pnpm install --dry-run` sets `dryRun: true` (and `lockfileOnly: true`) so it can diff the before/after wanted lockfile. However, in `installInContext()` the workspace-partial branch that expands the project list to all importers inside the workspace is now disabled for all “check-only” installs via `!isCheckOnlyInstall(opts)`.
That means in a workspace where `projects` is a subset of all workspace projects under the shared lockfile (e.g. install invoked from a package subdir), dry-run will skip the expansion that a real install would perform and may under-report or misreport lockfile changes.
### Issue Context
- `--dry-run` is implemented by setting `installDepsOptions.lockfileOnly = true` and `installDepsOptions.dryRun = true`.
- `isCheckOnlyInstall(opts)` returns true for `dryRun`, so the workspace expansion branch is skipped.
### Fix Focus Areas
Update `installInContext()` so that **dry-run still performs the workspace expansion resolution step** when `allProjectsLocatedInsideWorkspace.length > projects.length`, but **does not materialize** anything.
One approach:
- Allow the “workspace partial” expansion logic to run when `opts.dryRun === true`.
- After computing `result = await installInContext(newProjects, ctx, { ...opts, lockfileOnly: true })`, if `opts.dryRun === true` (or `opts.lockfileOnly === true`), return `result` immediately without calling `materializeOrDelegate(...)`.
- Keep the current behavior for non-dry-run installs.
Fix locations:
- installing/deps-installer/src/install/index.ts[2072-2128]
- installing/commands/src/install.ts[428-453]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (4)
12. Dry-run misses group moves ✓ Resolved 🐞 Bug ≡ Correctness
Description
pnpm install --dry-run diffs importer changes using only the lockfile specifiers map, so moving
a dependency between dependencies/devDependencies/optionalDependencies without changing its
specifier can be reported as “up to date” even though a real install would rewrite the lockfile
importer sections.
Code

installing/dedupe/check/src/dedupeDiffCheck.ts[R13-43]

+// A direct dependency's manifest `specifier` is the reliable signal of a
+// would-be importer change: it always reflects the current `package.json`,
+// whereas the resolved-version fields are cleared in memory for any dep whose
+// specifier no longer matches the lockfile (they're about to be re-resolved).
+// For a direct dependency the resolved version only changes when the
+// specifier does, so comparing specifiers captures every importer-level
+// change a real install would persist.
+const IMPORTER_DRY_RUN_FIELDS = ['specifiers'] as const
+
+/**
+ * Compute the changes between two lockfiles, as added/removed/updated
+ * importer and package snapshots. Unlike {@link dedupeDiffCheck} this never
+ * throws — callers that only want to report the diff (e.g. `install
+ * --dry-run`) consume the result directly.
+ *
+ * `includeImporterSpecifiers` diffs each importer by its direct dependencies'
+ * `specifier` instead of their resolved versions. `pnpm install --dry-run`
+ * sets it so a specifier-only manifest edit (which a real install would
+ * persist to the lockfile) is reported; `dedupe --check` leaves it off
+ * because a specifier change is irrelevant to deduplication.
+ */
+export function calcDedupeCheckIssues (
+  prev: LockfileObject,
+  next: LockfileObject,
+  opts?: { includeImporterSpecifiers?: boolean }
+): DedupeCheckIssues {
+  const importerFields = opts?.includeImporterSpecifiers ? IMPORTER_DRY_RUN_FIELDS : DEPENDENCIES_FIELDS
+  return {
+    importerIssuesByImporterId: diffSnapshots(prev.importers, next.importers, importerFields),
packageIssuesByDepPath: diffSnapshots(prev.packages ?? {}, next.packages ?? {}, PACKAGE_SNAPSHOT_DEP_FIELDS),
}
Evidence
The dry-run report explicitly enables includeImporterSpecifiers, which makes
calcDedupeCheckIssues compare only importers[*].specifiers (IMPORTER_DRY_RUN_FIELDS). Since
dependency-group moves change importers[*].dependencies/devDependencies/optionalDependencies (the
fields enumerated by DEPENDENCIES_FIELDS) but do not necessarily change specifiers, the current
logic can miss real lockfile rewrites and incorrectly return an “up to date” message.

installing/commands/src/install.ts[457-463]
installing/dedupe/check/src/dedupeDiffCheck.ts[13-43]
core/types/src/misc.ts[1-10]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`calcDedupeCheckIssues(..., { includeImporterSpecifiers: true })` switches importer diffing to `['specifiers']` only. This detects specifier-only edits, but it *misses* dependency-group moves (e.g. `devDependencies` → `dependencies`) where the specifier stays the same; those moves still change `importers.{id}.dependencies/devDependencies/...` and will be persisted by a real install.
### Issue Context
`install --dry-run` calls `calcDedupeCheckIssues` with `includeImporterSpecifiers: true`, so the specifiers-only importer diff is used for the dry-run report.
### Fix Focus Areas
- installing/dedupe/check/src/dedupeDiffCheck.ts[13-43]
- installing/commands/src/install.ts[457-463]
### Suggested fix
When `includeImporterSpecifiers` is enabled, diff importers using **both**:
- dependency-group fields (`DEPENDENCIES_FIELDS`: dependencies/devDependencies/optionalDependencies)
- and `specifiers`
This can be done by combining the field sets (or by diffing twice and merging) so dry-run reports both specifier-only edits and group moves.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. Dry-run output injection 🐞 Bug ⛨ Security
Description
pnpm install --dry-run renders importer IDs and package depPaths directly into terminal output via
renderDedupeCheckIssues without stripping control characters, allowing terminal
escape/output-spoofing if a repo contains a crafted lockfile. This is a new user-facing printing
path for untrusted lockfile strings and can be abused to mislead users (e.g., hidden text, forged
lines, clickable links).
Code

installing/commands/src/install.ts[R461-465]

+function renderDryRunReport (changes: Array<ReturnType<typeof calcDedupeCheckIssues>>): string {
+  const reports = changes
+    .filter((issues) => countDedupeCheckIssues(issues) > 0)
+    .map(renderDedupeCheckIssues)
+  if (reports.length === 0) {
Evidence
The dry-run handler returns a report composed from renderDedupeCheckIssues, whose implementation
inserts snapshot IDs and aliases directly into output strings; these identifiers originate from
lockfile maps keyed by string-like project IDs/depPaths.

installing/commands/src/install.ts[461-472]
installing/dedupe/issues-renderer/src/index.ts[32-37]
installing/dedupe/issues-renderer/src/index.ts[40-55]
lockfile/types/src/index.ts[29-32]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`pnpm install --dry-run` returns a human report built from lockfile keys (importer IDs / depPaths) and prints them as-is. If those keys contain VT/ANSI control characters (possible via a crafted `pnpm-lock.yaml`), the terminal output can be spoofed/injected.
### Issue Context
The dry-run report is created by mapping `renderDedupeCheckIssues` over lockfile diffs. The renderer interpolates snapshot IDs/aliases directly into output strings.
### Fix Focus Areas
- installing/commands/src/install.ts[461-465]
- installing/dedupe/issues-renderer/src/index.ts[32-55]
- lockfile/types/src/index.ts[29-32]
### Suggested fix
Sanitize *untrusted* labels before rendering/printing:
- Strip VT control chars from importer IDs, depPaths, and alias strings (e.g., `node:util.stripVTControlCharacters`) before concatenating into output.
- Do this inside `renderDedupeCheckIssues` (preferred, centralizes for all callers) or immediately before printing the dry-run report, but avoid stripping chalk formatting globally (sanitize only the interpolated labels).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


14. Pacquet output injection 🐞 Bug ⛨ Security
Description
pacquet install --dry-run prints importer IDs and package keys from the parsed lockfile directly
to stdout without escaping control characters, enabling terminal escape/output-spoofing from a
crafted pnpm-lock.yaml. This can mislead users reading the dry-run report (e.g., hiding/remapping
lines or creating deceptive hyperlinks).
Code

pacquet/crates/package-manager/src/dry_run.rs[R194-224]

+    if !diff.importers.is_empty() {
+        lines.push("Importers".to_string());
+        for importer in &diff.importers {
+            lines.push(importer.id.clone());
+            for (alias, version) in &importer.added {
+                lines.push(format!("  + {alias} {version}"));
+            }
+            for (alias, version) in &importer.removed {
+                lines.push(format!("  - {alias} {version}"));
+            }
+            for (alias, old, new) in &importer.updated {
+                lines.push(format!("  {alias} {old} -> {new}"));
+            }
+        }
+        lines.push(String::new());
+    }
+
+    if !diff.added_packages.is_empty()
+        || !diff.removed_packages.is_empty()
+        || !diff.updated_packages.is_empty()
+    {
+        lines.push("Packages".to_string());
+        for key in &diff.added_packages {
+            lines.push(format!("+ {key}"));
+        }
+        for key in &diff.removed_packages {
+            lines.push(format!("- {key}"));
+        }
+        for key in &diff.updated_packages {
+            lines.push(format!("~ {key}"));
+        }
Evidence
The dry-run report code pushes importer IDs and package snapshot keys straight into output lines,
and the lockfile model stores importer keys as arbitrary strings parsed from YAML.

pacquet/crates/package-manager/src/dry_run.rs[182-228]
pacquet/crates/lockfile/src/lib.rs[188-205]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new `pacquet install --dry-run` report renders lockfile-derived strings (importer IDs and package snapshot keys) directly into stdout. A crafted lockfile can include terminal control sequences in these strings, causing output injection/spoofing.
### Issue Context
`Lockfile.importers` is keyed by `String` (parsed from YAML). The dry-run reporter prints `importer.id` and snapshot keys with `format!("{key}")` without sanitization.
### Fix Focus Areas
- pacquet/crates/package-manager/src/dry_run.rs[194-224]
- pacquet/crates/lockfile/src/lib.rs[188-205]
### Suggested fix
Before pushing any lockfile-derived identifier into `lines`, sanitize it:
- Implement a small helper like `fn sanitize_label(s: &str) -> Cow<str>` that removes VT control characters (e.g., filter out `c.is_control()` except `\n`/`\t`, and specifically remove ESC `\x1b`).
- Apply it to `importer.id`, dependency aliases, versions (defensive), and snapshot keys.
- Keep the rest of the report formatting unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


15. Dry-run misses specifier changes ✓ Resolved 🐞 Bug ≡ Correctness
Description
In pacquet, diff_importer only compares ResolvedDependencySpec.version and collapses
prod/dev/optional dependency maps, so pacquet install --dry-run can report “up to date” even when
a real install would rewrite pnpm-lock.yaml due to specifier changes or dependency group moves.
Code

pacquet/crates/package-manager/src/dry_run.rs[R118-135]

+/// Flatten an importer's prod / dev / optional dependency maps into a
+/// single `alias -> version` map.
+fn collect_direct_deps(snapshot: Option<&ProjectSnapshot>) -> BTreeMap<String, String> {
+    let mut map = BTreeMap::new();
+    let Some(snapshot) = snapshot else {
+        return map;
+    };
+    for deps in
+        [&snapshot.dependencies, &snapshot.dev_dependencies, &snapshot.optional_dependencies]
+            .into_iter()
+            .flatten()
+    {
+        for (name, spec) in deps {
+            map.insert(name.to_string(), spec.version.to_string());
+        }
+    }
+    map
+}
Evidence
collect_direct_deps only inserts spec.version into the comparison map, so specifier-only changes
and dependency-group moves are invisible to the diff. This is incorrect because the lockfile’s
importer dependency entries include a specifier field in addition to version.

pacquet/crates/package-manager/src/dry_run.rs[118-135]
pacquet/crates/lockfile/src/resolved_dependency.rs[16-22]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`pacquet install --dry-run` builds and diffs lockfiles, but the importer diff currently only tracks `version` (and merges prod/dev/optional into one map). This can hide lockfile changes where the resolved version is unchanged but the lockfile entry’s `specifier` changes, or when a dependency moves between dependency groups.
### Issue Context
In the lockfile model, importer dependencies are `ResolvedDependencySpec { specifier, version }`. The dry-run report should treat specifier/group changes as meaningful because a real install would persist them into `pnpm-lock.yaml`.
### Fix Focus Areas
- pacquet/crates/package-manager/src/dry_run.rs[90-135]
### What to implement
- Change the importer diff collection to preserve **(group, specifier, version)** per alias (or diff each dependency group map separately).
- Mark entries as updated if either `specifier` or `version` changes.
- Ensure the rendered report reflects these changes (either by showing specifier changes explicitly or by treating them as updates so the diff is non-empty).
- Add/extend unit tests to cover a specifier-only change and a dependency moving between groups (dev→prod) with identical version resolution.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Implements --dry-run for both pnpm install (TypeScript) and pacquet install (Rust). Both CLIs add a --dry-run flag that performs full dependency resolution, diffs the resolved lockfile against the existing one, prints a change report, and exits with code 0 without writing lockfile or node_modules.

Changes

pnpm install --dry-run (TypeScript)

Layer / File(s) Summary
Dedupe diff helpers
installing/dedupe/check/src/dedupeDiffCheck.ts, installing/dedupe/check/src/index.ts
Extracts calcDedupeCheckIssues and countDedupeCheckIssues as standalone exported functions; refactors dedupeDiffCheck to use them; re-exports both from the package index.
install command --dry-run handler
.changeset/dry-run-install.md, config/reader/src/Config.ts, installing/commands/package.json, installing/commands/src/install.ts, installing/commands/tsconfig.json
Adds --dry-run CLI flag and dryRun to InstallCommandOptions; routes to dryRunInstall which configures lockfileOnly, runs installDeps to get a DryRunInstallResult, computes dedupe issues, and renders either "up to date" or a list of dedupe-related changes. Updates Config.ts JSDoc and adds changeset release notes documenting the feature.
installDeps and deps-installer integration
installing/commands/src/installDeps.ts, installing/deps-installer/src/install/index.ts, installing/deps-installer/src/install/extendInstallOptions.ts
Changes installDeps return type from Promise<void> to Promise<DryRunInstallResult | undefined>; adds dryRun to StrictInstallOptions and InstallDepsOptions; wires dryRunResult (containing before/after lockfiles) through install, mutateModules, and mutateModulesInSingleProject return objects. Introduces isCheckOnlyInstall helper to gate frozen/pacquet-delegation paths away from check-only runs.
Install caller await fixes
installing/commands/src/prune.ts, installing/commands/src/add.ts, installing/commands/src/dedupe.ts, installing/commands/src/update/index.ts, patching/commands/src/patchCommit.ts, patching/commands/src/patchRemove.ts
Callers of install.handler and installDeps are updated to await instead of return the promise, required because the handler now returns Promise<void | string> instead of Promise<void>.
TypeScript integration tests
installing/commands/test/install.ts
Three Jest integration tests: one verifies dry-run reports the would-be dependency update while leaving lockfile and node_modules unchanged; one verifies "up to date" when the project is current; one verifies specifier-only changes are reported as changes.

pacquet install --dry-run (Rust)

Layer / File(s) Summary
LockfileDiff model and renderer
pacquet/crates/package-manager/src/lib.rs, pacquet/crates/package-manager/src/dry_run.rs, pacquet/crates/package-manager/src/dry_run/tests.rs
New dry_run module with LockfileDiff and ImporterDiff structs, diff_lockfiles, diff_snapshots, diff_importer, and render_dry_run_report functions; unit tests validate empty-diff "up to date" messages, non-empty diff rendering, snapshot wiring detection, group moves, and specifier-only change handling.
Install and InstallWithFreshLockfile wiring
pacquet/crates/package-manager/src/install.rs, pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
Adds dry_run: bool to both Install and InstallWithFreshLockfile structs; introduces resolve_only = lockfile_only || dry_run gate to force full resolution and skip frozen path; captures existing_wanted_lockfile before synthesis fallback; bypasses skip_runtimes validation under resolve_only; after resolution, diffs and prints report then returns without materializing.
CLI flag, call-site plumbing, and test updates
pacquet/crates/cli/src/cli_args/install.rs, pacquet/crates/cli/src/cli_args/install/tests.rs, pacquet/crates/package-manager/src/add.rs, pacquet/crates/package-manager/src/remove.rs, pacquet/crates/package-manager/src/update.rs, pacquet/crates/package-manager/src/install/tests.rs, pnpr/crates/pnpr/src/resolver/resolve.rs
Adds --dry-run clap flag to InstallArgs, threads dry_run into the local install path; sets dry_run: false on pnpr verification/linking and on add/remove/update follow-up installs. Updates all Install { ... } literals throughout the test suite to add dry_run: false.
Rust integration tests
pacquet/crates/cli/tests/dry_run.rs
Three integration tests: fresh project verifies no pnpm-lock.yaml or node_modules are created and stdout includes the to-be-added dependency; drifted package.json verifies the lockfile remains byte-for-byte unchanged and stdout includes the would-be dependency; up-to-date project verifies exit code 0 and "up to date" message.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant pnpmCLI as pnpm install CLI
  participant handler as install.handler
  participant dryRunInstall
  participant installDeps
  participant calcDedupeCheckIssues

  rect rgba(100, 149, 237, 0.5)
    Note over User,calcDedupeCheckIssues: TypeScript --dry-run path
    User->>pnpmCLI: pnpm install --dry-run
    pnpmCLI->>handler: opts.dryRun = true
    handler->>dryRunInstall: route check-only flow
    dryRunInstall->>dryRunInstall: lockfileOnly=true, disable optimistic repeat
    dryRunInstall->>installDeps: run with dryRun options
    installDeps-->>dryRunInstall: DryRunInstallResult{original, wanted}
    dryRunInstall->>calcDedupeCheckIssues: diff lockfiles
    calcDedupeCheckIssues-->>dryRunInstall: DedupeCheckIssues (or empty)
    dryRunInstall-->>handler: formatted report string
    handler-->>User: print report, exit 0 (no disk writes)
  end
Loading
sequenceDiagram
  participant User
  participant pacquetCLI as pacquet install CLI
  participant Install
  participant InstallWithFreshLockfile
  participant diff_lockfiles
  participant render

  rect rgba(144, 238, 144, 0.5)
    Note over User,render: Rust --dry-run path
    User->>pacquetCLI: pacquet install --dry-run
    pacquetCLI->>Install: dry_run=true
    Install->>Install: resolve_only=true, frozen_path=false
    Install->>Install: capture existing_wanted_lockfile
    Install->>InstallWithFreshLockfile: lockfile_only=true, dry_run=true
    InstallWithFreshLockfile->>InstallWithFreshLockfile: resolve in-memory
    InstallWithFreshLockfile->>InstallWithFreshLockfile: skip lockfile verification record
    InstallWithFreshLockfile-->>Install: wanted_lockfile (memory only)
    Install->>diff_lockfiles: old=existing, new=resolved
    diff_lockfiles-->>Install: LockfileDiff
    Install->>render: format diff
    render-->>Install: report string
    Install-->>User: print report, exit 0 (skip materialization)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • pnpm/pnpm#12155: Modifies the same pnprServer option wiring that the dry-run handler checks for incompatibility.
  • pnpm/pnpm#12046: Threads a "no materialization" lockfile_only mode through the same Install::run/InstallWithFreshLockfile::run paths that dry_run now also uses.
  • pnpm/pnpm#12210: Modifies the frozen-install eligibility predicates and pacquet-delegation gating in installing/deps-installer/src/install/index.ts at the same locations this PR introduces the isCheckOnlyInstall guard.

Suggested labels

area: cli/dlx, area: lockfile

🐇 Hop hop, no files to touch,
just resolution — nothing much!
The lockfile stays, the modules hide,
a dry run peeks what waits inside.
--dry-run tells you what would change,
before you commit to the full arrange! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(install): add --dry-run option (npm-style preview)' directly and clearly summarizes the main feature being added—a --dry-run option for the install command with npm-compatible semantics.
Linked Issues check ✅ Passed The PR fully addresses issue #7340's requirement for a dry-run option that performs dependency resolution, reports changes, writes nothing to disk, and enables pre-commit hook validation with proper exit codes.
Out of Scope Changes check ✅ Passed All changes are directly aligned with implementing the --dry-run feature: core install logic, CLI wiring, test coverage, and supporting infrastructure (dedupe diff helpers, pacquet parity implementation). No extraneous changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dry-run-install-v2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

feat(install): add npm-style --dry-run preview to pnpm install (and pacquet)
✨ Enhancement 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

Walkthroughs

Description
• Add pnpm install --dry-run to resolve fully and preview changes without disk writes.
• Diff before/after wanted lockfiles and render an npm-style report; always exits 0.
• Port the same --dry-run behavior to pacquet with dedicated diff/report logic and tests.
Diagram
graph TD
A["pnpm install.ts"] --> B["deps-installer mutateModules"] --> C["lockfileCheck callback"] --> D["dedupe check diff"] --> E["issues renderer"]
F["pacquet install args"] --> G["package-manager install"] --> H["dry_run diff+report"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Catch-and-render DedupeCheckIssuesError
  • ➕ Avoid exporting new diff/count helpers from the dedupe check module
  • ➕ Keeps dedupe check API surface smaller (error remains the primary artifact)
  • ➖ Relies on an exception path for normal control flow (dry-run preview)
  • ➖ Harder to support 'no changes' without throwing
  • ➖ Less composable if multiple importer reports are needed
2. Introduce a dedicated lockfile-diff API shared by install/dedupe/dry-run
  • ➕ Single canonical diff model for all commands (dedupe --check, install --dry-run, future CI checks)
  • ➕ Could standardize rendering and reduce duplicated report logic
  • ➖ Bigger upfront refactor; more risk and churn than this PR’s targeted exports
  • ➖ May delay shipping the user-facing flag
3. Implement dry-run as `--frozen-lockfile --lockfile-only` validator
  • ➕ Very small implementation; mostly wiring existing options
  • ➖ Does not match npm/yarn dry-run semantics (would fail on drift)
  • ➖ Duplicates existing behavior users can already invoke explicitly
  • ➖ Doesn’t provide a 'would change' preview report

Recommendation: The PR’s approach is the best fit for the stated npm-style semantics: it forces a real resolution, guarantees no writes via existing lockfile-only plumbing, and produces a concrete diff report while always exiting 0. Exporting calcDedupeCheckIssues/countDedupeCheckIssues is a reasonable minimal surface-area change to reuse the existing diff engine without abusing exceptions for control flow.

Grey Divider

File Changes

Enhancement (5)
install.ts Implement 'pnpm install --dry-run' with lockfile diff report +54/-1

Implement 'pnpm install --dry-run' with lockfile diff report

• Adds the '--dry-run' CLI option, help text, and a dedicated dry-run handler that runs full resolution with 'lockfileOnly' + 'lockfileCheck', rejects pnpr-server usage, and returns a human report string.

installing/commands/src/install.ts


install.rs Add '--dry-run' flag and thread it through install dispatch +11/-0

Add '--dry-run' flag and thread it through install dispatch

• Introduces a clap '--dry-run' flag for 'pacquet install' and propagates it through the install config structures; pnpr install paths explicitly set 'dry_run: false'.

pacquet/crates/cli/src/cli_args/install.rs


dry_run.rs Implement pacquet lockfile diff + dry-run report renderer +183/-0

Implement pacquet lockfile diff + dry-run report renderer

• Adds a new module that diffs old vs freshly-resolved lockfiles (importer direct deps and package keys) and renders the npm-style dry-run report output.

pacquet/crates/package-manager/src/dry_run.rs


install.rs Implement 'pacquet install --dry-run' no-write preview flow +38/-4

Implement 'pacquet install --dry-run' no-write preview flow

• Adds a 'dry_run' install option that forces fresh resolution, suppresses all materialization and lockfile writes, diffs lockfiles, prints the report, and exits 0.

pacquet/crates/package-manager/src/install.rs


install_with_fresh_lockfile.rs Skip saving pnpm-lock.yaml during dry-run fresh resolve +12/-1

Skip saving pnpm-lock.yaml during dry-run fresh resolve

• Adds a 'dry_run' flag that builds and returns the would-be lockfile for diffing but bypasses persisting it to disk (while still implying lockfile-only behavior).

pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs


Bug fix (1)
index.ts Disable frozen/headless fast paths when doing lockfile checks +6/-0

Disable frozen/headless fast paths when doing lockfile checks

• Prevents the frozen/optimistic short-circuit path when 'lockfileCheck' is set so check-only runs always resolve fully and never materialize side effects.

installing/deps-installer/src/install/index.ts


Refactor (10)
prune.ts Adjust prune to ignore install handler return value +1/-1

Adjust prune to ignore install handler return value

• Switches from returning 'install.handler(...)' to 'await'ing it so prune continues to behave as void even though install may now return a report string.

installing/commands/src/prune.ts


dedupeDiffCheck.ts Export non-throwing lockfile diff computation helpers +17/-4

Export non-throwing lockfile diff computation helpers

• Factors the diff logic into 'calcDedupeCheckIssues' and 'countDedupeCheckIssues' so callers (like dry-run) can compute and render changes without throwing.

installing/dedupe/check/src/dedupeDiffCheck.ts


index.ts Re-export new dedupe diff helpers +1/-1

Re-export new dedupe diff helpers

• Exports 'calcDedupeCheckIssues' and 'countDedupeCheckIssues' from the package public API for reuse by install dry-run reporting.

installing/dedupe/check/src/index.ts


add.rs Plumb 'dry_run' field through add install config +1/-0

Plumb 'dry_run' field through add install config

• Updates add’s install invocation to pass 'dry_run: false' to the shared install engine after introducing the new field.

pacquet/crates/package-manager/src/add.rs


lib.rs Register new 'dry_run' module +1/-0

Register new 'dry_run' module

• Wires the new dry-run module into the package-manager crate module graph.

pacquet/crates/package-manager/src/lib.rs


remove.rs Plumb 'dry_run' field through remove install config +1/-0

Plumb 'dry_run' field through remove install config

• Updates remove’s install invocation to pass 'dry_run: false' after adding the new option to the install engine.

pacquet/crates/package-manager/src/remove.rs


update.rs Plumb 'dry_run' field through update install config +1/-0

Plumb 'dry_run' field through update install config

• Updates update’s install invocation to pass 'dry_run: false' after adding the new option to the install engine.

pacquet/crates/package-manager/src/update.rs


patchCommit.ts Adjust patch-commit to await install handler and return void +3/-2

Adjust patch-commit to await install handler and return void

• Updates patch-commit to 'await install.handler(...)' and explicitly return 'undefined' to accommodate install handler’s new 'void | string' return type.

patching/commands/src/patchCommit.ts


patchRemove.ts Adjust patch-remove to await install handler +1/-1

Adjust patch-remove to await install handler

• Changes patch-remove to 'await install.handler(...)' so it ignores any returned dry-run report string and preserves prior void behavior.

patching/commands/src/patchRemove.ts


resolve.rs Set 'dry_run: false' in pnpr resolver install config +1/-0

Set 'dry_run: false' in pnpr resolver install config

• Updates pnpr’s pacquet install invocation to include the new 'dry_run' field, explicitly disabled for pnpr resolve paths.

pnpr/crates/pnpr/src/resolver/resolve.rs


Tests (5)
install.ts Add pnpm dry-run behavioral tests +51/-0

Add pnpm dry-run behavioral tests

• Adds tests asserting '--dry-run' reports would-be changes, does not modify 'pnpm-lock.yaml' or link new deps, and reports no changes when up to date.

installing/commands/test/install.ts


tests.rs Add clap parsing test for '--dry-run' +10/-0

Add clap parsing test for '--dry-run'

• Adds a unit test verifying '--dry-run' parses correctly and defaults to false when absent.

pacquet/crates/cli/src/cli_args/install/tests.rs


dry_run.rs Add pacquet CLI integration tests for dry-run +129/-0

Add pacquet CLI integration tests for dry-run

• Adds integration coverage asserting dry-run exits 0, prints would-change output, and does not write 'pnpm-lock.yaml' or create 'node_modules' across fresh/stale/up-to-date cases.

pacquet/crates/cli/tests/dry_run.rs


tests.rs Unit test dry-run report rendering +26/-0

Unit test dry-run report rendering

• Adds unit tests for the dry-run report renderer for both empty and non-empty diffs.

pacquet/crates/package-manager/src/dry_run/tests.rs


tests.rs Update install tests for new 'dry_run' field +72/-1

Update install tests for new 'dry_run' field

• Mechanically updates test install invocations to include 'dry_run: false' to satisfy the updated install configuration struct.

pacquet/crates/package-manager/src/install/tests.rs


Documentation (2)
dry-run-install.md Add changeset entry for 'pnpm install --dry-run' +8/-0

Add changeset entry for 'pnpm install --dry-run'

• Introduces a changeset describing the new '--dry-run' install preview behavior and its no-write / exit-0 semantics.

.changeset/dry-run-install.md


Config.ts Document 'dryRun' config option semantics +2/-1

Document 'dryRun' config option semantics

• Updates the 'Config' interface comment for 'dryRun' to reflect the new supported behavior for install preview without writes.

config/reader/src/Config.ts


Other (3)
package.json Add issues renderer dependency for dry-run reporting +1/-0

Add issues renderer dependency for dry-run reporting

• Adds '@pnpm/installing.dedupe.issues-renderer' as a workspace dependency so the install command can render diff reports.

installing/commands/package.json


tsconfig.json Reference dedupe issues renderer project +3/-0

Reference dedupe issues renderer project

• Adds the issues-renderer TS project reference to support the new import from the install command package.

installing/commands/tsconfig.json


pnpm-lock.yaml Update lockfile for new workspace dependency +3/-0

Update lockfile for new workspace dependency

• Adds the new '@pnpm/installing.dedupe.issues-renderer' workspace link under the installing/commands importer.

pnpm-lock.yaml


Grey Divider

Qodo Logo

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      7.9±0.36ms   547.0 KB/sec    1.00      7.9±0.29ms   546.1 KB/sec

Comment thread installing/commands/src/install.ts Outdated
Comment thread pacquet/crates/package-manager/src/dry_run.rs Outdated
@codecov-commenter

codecov-commenter commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.35802% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.03%. Comparing base (d36b6f8) to head (2e974cc).
⚠️ Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/package-manager/src/dry_run.rs 89.06% 14 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12449      +/-   ##
==========================================
+ Coverage   88.00%   88.03%   +0.02%     
==========================================
  Files         308      309       +1     
  Lines       41395    41580     +185     
==========================================
+ Hits        36431    36605     +174     
- Misses       4964     4975      +11     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pacquet/crates/cli/src/cli_args/install.rs (1)

413-431: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

--dry-run is silently ignored on the pnpr path instead of being rejected

When pnpr_server is configured, InstallArgs::run delegates to install_via_pnpr without guarding dry_run, and both pnpr Install calls force dry_run: false (Line 697 and Line 825). This breaks the dry-run contract and can perform real writes even though the user explicitly asked for preview-only behavior.

Suggested fix
@@
-        if let Some(pnpr_server) = config.pnpr_server.as_deref() {
+        if dry_run && config.pnpr_server.is_some() {
+            return Err(miette::miette!(
+                "`--dry-run` cannot be used with `pnprServer`; disable pnpr server or remove `--dry-run`."
+            ));
+        }
+        if let Some(pnpr_server) = config.pnpr_server.as_deref() {
             return install_via_pnpr::<Reporter>(
                 &state,
                 pnpr_server,

As per coding guidelines, “Keep pnpm and pacquet in sync for any user-visible change … behavior … must be implemented on both sides,” and this PR’s objective explicitly requires rejecting pnpr-server configurations for --dry-run.

Also applies to: 697-698, 825-826

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pacquet/crates/cli/src/cli_args/install.rs` around lines 413 - 431, The
`--dry-run` flag is being silently ignored when pnpr_server is configured,
allowing actual writes despite the user's explicit preview-only request. Add a
guard check before the `install_via_pnpr` call in the pnpr_server branch that
rejects the operation if `dry_run` is true, raising an error to inform the user
that dry-run is not supported with pnpr configurations. This check should be
placed before delegating to `install_via_pnpr` around line 413-431. The
hardcoded `dry_run: false` values in the pnpr Install calls at lines 697-698 and
825-826 are manifestations of this issue and do not require direct changes once
the root-cause guard is implemented at the entry point.

Source: Coding guidelines

🧹 Nitpick comments (1)
pacquet/crates/package-manager/src/dry_run/tests.rs (1)

3-26: ⚡ Quick win

Add direct unit coverage for diff_lockfiles, not just renderer output.

These tests validate render_dry_run_report, but they don’t assert diff_lockfiles behavior (importer add/remove/update and package key add/remove). A focused diff test here would catch parity regressions earlier.

As per coding guidelines, behavior changes should be covered with targeted tests, and this feature’s core behavior lives in diff construction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pacquet/crates/package-manager/src/dry_run/tests.rs` around lines 3 - 26, Add
direct unit tests for the `diff_lockfiles` function to validate its core
behavior rather than relying only on `render_dry_run_report` output validation.
Create new test functions that call `diff_lockfiles` with specific lockfile
inputs and directly assert the resulting `LockfileDiff` structure for importer
add/remove/update operations and package additions/removals. This ensures the
diff construction logic is covered independently from the renderer, catching
parity regressions earlier as per coding guidelines.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pacquet/crates/package-manager/src/install.rs`:
- Around line 1318-1320: The issue is that diff_lockfiles is using the lockfile
variable which may have been previously modified to include a synthesized
fallback from virtual_store_dir/lock.yaml, causing the dry-run diff baseline to
be incorrect and potentially hiding real changes to pnpm-lock.yaml. To fix this,
ensure that the render_dry_run_report call with diff_lockfiles uses the original
on-disk lockfile state rather than the potentially modified lockfile variable.
Store the original lockfile state before any fallback synthesis occurs, and pass
that original state to diff_lockfiles so the comparison accurately reflects what
would actually be written to disk, matching pnpm's dry-run behavior exactly.

---

Outside diff comments:
In `@pacquet/crates/cli/src/cli_args/install.rs`:
- Around line 413-431: The `--dry-run` flag is being silently ignored when
pnpr_server is configured, allowing actual writes despite the user's explicit
preview-only request. Add a guard check before the `install_via_pnpr` call in
the pnpr_server branch that rejects the operation if `dry_run` is true, raising
an error to inform the user that dry-run is not supported with pnpr
configurations. This check should be placed before delegating to
`install_via_pnpr` around line 413-431. The hardcoded `dry_run: false` values in
the pnpr Install calls at lines 697-698 and 825-826 are manifestations of this
issue and do not require direct changes once the root-cause guard is implemented
at the entry point.

---

Nitpick comments:
In `@pacquet/crates/package-manager/src/dry_run/tests.rs`:
- Around line 3-26: Add direct unit tests for the `diff_lockfiles` function to
validate its core behavior rather than relying only on `render_dry_run_report`
output validation. Create new test functions that call `diff_lockfiles` with
specific lockfile inputs and directly assert the resulting `LockfileDiff`
structure for importer add/remove/update operations and package
additions/removals. This ensures the diff construction logic is covered
independently from the renderer, catching parity regressions earlier as per
coding guidelines.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d7fe4ff5-ffb7-4795-aba3-f86fcd4f55fb

📥 Commits

Reviewing files that changed from the base of the PR and between 4ca9247 and 7df096f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • .changeset/dry-run-install.md
  • config/reader/src/Config.ts
  • installing/commands/package.json
  • installing/commands/src/install.ts
  • installing/commands/src/prune.ts
  • installing/commands/test/install.ts
  • installing/commands/tsconfig.json
  • installing/dedupe/check/src/dedupeDiffCheck.ts
  • installing/dedupe/check/src/index.ts
  • installing/deps-installer/src/install/index.ts
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/cli/src/cli_args/install/tests.rs
  • pacquet/crates/cli/tests/dry_run.rs
  • pacquet/crates/package-manager/src/add.rs
  • pacquet/crates/package-manager/src/dry_run.rs
  • pacquet/crates/package-manager/src/dry_run/tests.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/install/tests.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/package-manager/src/lib.rs
  • pacquet/crates/package-manager/src/remove.rs
  • pacquet/crates/package-manager/src/update.rs
  • patching/commands/src/patchCommit.ts
  • patching/commands/src/patchRemove.ts
  • pnpr/crates/pnpr/src/resolver/resolve.rs

Comment thread pacquet/crates/package-manager/src/install.rs Outdated
Address review on the dry-run diff:

- Diff against the actual on-disk pnpm-lock.yaml (captured before the
  synthesized-from-current fallback), so a real install creating
  pnpm-lock.yaml is reported instead of hidden as "no changes".
- Diff each dependency group (prod/dev/optional) independently so a
  dependency moving between groups registers as a change, matching pnpm's
  per-field diff. Specifiers stay uncompared, matching pnpm (which keeps
  them in a separate map outside its diff fields).
- Unit tests for the group-move and specifier-only cases.

---
Written by an agent (Claude Code, claude-opus-4-8).
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit c964340

Comment thread pacquet/crates/package-manager/src/dry_run.rs Outdated
The package-level diff now walks the v9 `snapshots:` map (the peer-aware
dependency wiring a real install rewrites) instead of the `packages:`
name@version metadata keys, matching pnpm's `dedupeDiffCheck` whose
in-memory `packages` map is depPath-keyed. This catches peer-variant
re-resolutions (snapshot key changes) and snapshot wiring changes
(`dependencies`/`optionalDependencies`) that the previous key-set diff
missed. Adds an `updated_packages` group to the report and unit tests for
the wiring-change case.

---
Written by an agent (Claude Code, claude-opus-4-8).
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit b6cb9ba

Comment thread installing/commands/src/install.ts Outdated
Comment thread installing/commands/src/install.ts Outdated
Comment thread pacquet/crates/package-manager/src/dry_run.rs
Replaces the reuse of the dedupe-oriented `lockfileCheck` callback with a
proper `dryRun` install option, mirroring the pacquet implementation.

- `StrictInstallOptions.dryRun`: resolve fully, write nothing, and return
  the before/after wanted lockfiles in the install result's `dryRunResult`.
- A shared `isCheckOnlyInstall` helper folds `dryRun` and `lockfileCheck`
  into one "resolve-only, no writes" concept, so the frozen/headless fast
  paths are skipped for both (removes the ad-hoc `lockfileCheck == null`
  guards).
- `dryRunResult` is threaded up through the install result types; the
  command layer diffs it (still via the lockfile-diff engine) and renders
  the report. `installDeps` now returns the dry-run result; callers that
  returned its promise from void handlers now await it.

`lockfileCheck` stays as the mechanism behind `dedupe --check`.

---
Written by an agent (Claude Code, claude-opus-4-8).
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 4e49843

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
installing/commands/src/installDeps.ts (1)

412-428: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate manifest writes during dry-run.

opts.dryRun now flows into installDeps(), but these branches still write package.json / pnpm-workspace.yaml whenever opts.save !== false. Treat dry-run like no-save for manifest and workspace-manifest persistence so the preview path cannot modify project files. Based on PR objectives, dry-run must write nothing to disk.

🛡️ Skip manifest persistence when dry-run is active
-    if (opts.save !== false) {
+    if (opts.save !== false && opts.dryRun !== true) {
       // Only pick entries when we'll actually persist. Otherwise the
       // info log would claim we added entries the workspace manifest
       // never saw, and the next install would re-prompt or fail
-  if (opts.save !== false) {
+  if (opts.save !== false && opts.dryRun !== true) {
     const policyUpdates = policyHandlers?.pickManifestUpdates(resolutionPolicyViolations)
     if (opts.update === true) {

Also applies to: 443-470

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@installing/commands/src/installDeps.ts` around lines 412 - 428, The manifest
persistence logic in the block starting at line 412 checks `opts.save !== false`
before writing manifests but does not account for dry-run mode. Modify the
condition that gates the Promise.all block containing writeProjectManifest and
updateWorkspaceManifest to also check that opts.dryRun is not active, ensuring
manifests are only persisted when both save is enabled and dry-run is false. The
same pattern should also be applied to the similar manifest write block at lines
443-470.
installing/deps-installer/src/install/index.ts (1)

184-185: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Apply isCheckOnlyInstall() before every dry-run side-effect path.

The new helper skips several materialization paths, but dry-run can still reach pnpr delegation, verification-cache writes, merge-branch lockfile cleanup, and resolver non-dry-run behavior. This makes the exported dryRun option depend on command-layer lockfileOnly and can still modify disk under some configs. Gate these paths with isCheckOnlyInstall(opts) and pass that flag through to the resolver. Based on PR objectives, dry-run must write nothing to disk.

🛡️ Reuse the check-only flag for remaining side-effect gates
   const opts = extendOptions(maybeOpts)
+  const checkOnlyInstall = isCheckOnlyInstall(opts)

+  if (opts.pnprServer && checkOnlyInstall) {
+    throw new PnpmError('CONFIG_CONFLICT_CHECK_ONLY_WITH_PNPR_SERVER',
+      'Cannot use a check-only install with a configured pnpr server because the pnpr install path resolves and links through the server')
+  }
+
   // When a pnpr server is configured, use server-side resolution. The pnpr server
-    const cacheActive = opts.cacheDir != null && opts.resolutionVerifiers.length > 0
+    const cacheActive = !checkOnlyInstall && opts.cacheDir != null && opts.resolutionVerifiers.length > 0
     const wantedLockfilePath = cacheActive
       ? path.resolve(ctx.lockfileDir, await getWantedLockfileName({
         useGitBranchLockfile: opts.useGitBranchLockfile,
         mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
       }))
       : undefined
     verifyLockfilePromise = verifyLockfileResolutions(ctx.wantedLockfile, opts.resolutionVerifiers, {
-      cacheDir: opts.cacheDir,
+      cacheDir: checkOnlyInstall ? undefined : opts.cacheDir,
       lockfilePath: wantedLockfilePath,
     })
-  if (opts.mergeGitBranchLockfiles) {
+  if (opts.mergeGitBranchLockfiles && !checkOnlyInstall) {
     await cleanGitBranchLockfiles(ctx.lockfileDir)
   }
-      dryRun: opts.lockfileOnly,
+      dryRun: opts.lockfileOnly || isInstallationOnlyForLockfileCheck,

Also applies to: 325-327, 425-440, 502-503, 1560-1560

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@installing/deps-installer/src/install/index.ts` around lines 184 - 185, The
dry-run functionality currently allows side effects to reach several code paths
including pnpr delegation, verification-cache writes, merge-branch lockfile
cleanup, and resolver behavior. Guard all these side-effect paths with
`isCheckOnlyInstall(opts)` to ensure dry-run mode writes nothing to disk.
Specifically, wrap the pnpr delegation path (the `installViaPnprServer` call),
verification-cache write operations, merge-branch lockfile cleanup, and resolver
invocations with checks using `isCheckOnlyInstall(opts)`, and pass this flag
through to the resolver so it also respects the dry-run constraint. This ensures
the `dryRun` option properly isolates disk modifications regardless of
command-layer `lockfileOnly` configuration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@installing/commands/src/install.ts`:
- Around line 453-462: The renderDryRunReport function currently treats
undefined dryRunResult as "up to date" which masks stale workspace lockfiles.
Make the dryRunResult parameter required (non-optional) in renderDryRunReport
instead of allowing it to be undefined, and ensure the workspace recursive path
in installDeps function properly forwards and returns the DryRunInstallResult so
that all dry-run comparisons are accurately calculated and reported. This
ensures stale workspace lockfiles are detected and reported as changes rather
than being silently ignored.

---

Outside diff comments:
In `@installing/commands/src/installDeps.ts`:
- Around line 412-428: The manifest persistence logic in the block starting at
line 412 checks `opts.save !== false` before writing manifests but does not
account for dry-run mode. Modify the condition that gates the Promise.all block
containing writeProjectManifest and updateWorkspaceManifest to also check that
opts.dryRun is not active, ensuring manifests are only persisted when both save
is enabled and dry-run is false. The same pattern should also be applied to the
similar manifest write block at lines 443-470.

In `@installing/deps-installer/src/install/index.ts`:
- Around line 184-185: The dry-run functionality currently allows side effects
to reach several code paths including pnpr delegation, verification-cache
writes, merge-branch lockfile cleanup, and resolver behavior. Guard all these
side-effect paths with `isCheckOnlyInstall(opts)` to ensure dry-run mode writes
nothing to disk. Specifically, wrap the pnpr delegation path (the
`installViaPnprServer` call), verification-cache write operations, merge-branch
lockfile cleanup, and resolver invocations with checks using
`isCheckOnlyInstall(opts)`, and pass this flag through to the resolver so it
also respects the dry-run constraint. This ensures the `dryRun` option properly
isolates disk modifications regardless of command-layer `lockfileOnly`
configuration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e262d2e4-b99e-4211-a8ab-9e524e042d7a

📥 Commits

Reviewing files that changed from the base of the PR and between b6cb9ba and 4e49843.

📒 Files selected for processing (7)
  • installing/commands/src/add.ts
  • installing/commands/src/dedupe.ts
  • installing/commands/src/install.ts
  • installing/commands/src/installDeps.ts
  • installing/commands/src/update/index.ts
  • installing/deps-installer/src/install/extendInstallOptions.ts
  • installing/deps-installer/src/install/index.ts
✅ Files skipped from review due to trivial changes (1)
  • installing/commands/src/add.ts

Comment thread installing/commands/src/install.ts
@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD.

Scenario: Isolated linker: fresh restore, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.212 ± 0.160 4.024 4.490 1.95 ± 0.12
pacquet@main 4.149 ± 0.149 3.995 4.511 1.93 ± 0.11
pnpr@HEAD 2.155 ± 0.099 2.034 2.326 1.00
pnpr@main 2.160 ± 0.122 1.986 2.386 1.00 ± 0.07
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.21226762788,
      "stddev": 0.1600202559451862,
      "median": 4.16461524488,
      "user": 3.9960526,
      "system": 3.5617277799999996,
      "min": 4.02421643338,
      "max": 4.49032058638,
      "times": [
        4.2269652863800005,
        4.32395347038,
        4.02421643338,
        4.0783009773800005,
        4.49032058638,
        4.07995786738,
        4.087860028380001,
        4.383120083380001,
        4.10226520338,
        4.325716342380001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.14918284558,
      "stddev": 0.14947491424895396,
      "median": 4.096980497880001,
      "user": 3.9515578,
      "system": 3.5372433799999996,
      "min": 3.9947684203800002,
      "max": 4.5109396653800005,
      "times": [
        4.09553513138,
        4.053216622380001,
        3.9947684203800002,
        4.07559700938,
        4.18727395438,
        4.5109396653800005,
        4.28065114238,
        4.06409104238,
        4.09842586438,
        4.13132960338
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.15502554608,
      "stddev": 0.09916187754885875,
      "median": 2.13443844038,
      "user": 2.6588359999999995,
      "system": 3.04636618,
      "min": 2.03373399538,
      "max": 2.32575818538,
      "times": [
        2.2688718523799998,
        2.16127512038,
        2.0984458513799997,
        2.03373399538,
        2.14896182038,
        2.0899632113799997,
        2.11991506038,
        2.25869302938,
        2.04463733438,
        2.32575818538
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.1603548679799998,
      "stddev": 0.12237318257349977,
      "median": 2.16041859788,
      "user": 2.6015556999999996,
      "system": 3.0056674799999996,
      "min": 1.98565003838,
      "max": 2.38614739938,
      "times": [
        2.20245420638,
        2.05638622238,
        1.98565003838,
        2.25784558238,
        2.11838298938,
        2.23870257438,
        2.09800459038,
        2.03630046238,
        2.22367461438,
        2.38614739938
      ]
    }
  ]
}

Scenario: Isolated linker: fresh restore, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 622.0 ± 12.0 603.2 639.6 1.00
pacquet@main 633.2 ± 23.3 616.5 690.7 1.02 ± 0.04
pnpr@HEAD 669.1 ± 12.7 653.0 690.4 1.08 ± 0.03
pnpr@main 684.6 ± 17.8 655.7 720.7 1.10 ± 0.04
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6220306105,
      "stddev": 0.011963004710281694,
      "median": 0.6229404656,
      "user": 0.35907142,
      "system": 1.32441656,
      "min": 0.6031680961000001,
      "max": 0.6395582541000001,
      "times": [
        0.6115014831000001,
        0.6149002161000001,
        0.6241994201000001,
        0.6296264561000001,
        0.6031680961000001,
        0.6216815111,
        0.6395582541000001,
        0.6096353621,
        0.6347071321000001,
        0.6313281741000001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6332479241000001,
      "stddev": 0.023299471480440436,
      "median": 0.6245711631,
      "user": 0.37919442,
      "system": 1.31826026,
      "min": 0.6165416671,
      "max": 0.6907386261,
      "times": [
        0.6190231541000001,
        0.6234899821000001,
        0.6260234021000001,
        0.6430415211000001,
        0.6169503641,
        0.6165416671,
        0.6193488041,
        0.6516693761000001,
        0.6256523441,
        0.6907386261
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6690965217,
      "stddev": 0.012676275978345307,
      "median": 0.6692010406000001,
      "user": 0.38609492,
      "system": 1.34511296,
      "min": 0.6530029151000001,
      "max": 0.6904213531000001,
      "times": [
        0.6781803021000001,
        0.6559626641,
        0.6661716011000001,
        0.6637317251000001,
        0.6530029151000001,
        0.6722304801000001,
        0.6806436551000001,
        0.6537288071,
        0.6904213531000001,
        0.6768917141
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6846030869,
      "stddev": 0.017797612593057056,
      "median": 0.6858700841,
      "user": 0.37668492,
      "system": 1.35934376,
      "min": 0.6556859241,
      "max": 0.7207105731000001,
      "times": [
        0.7207105731000001,
        0.6833490861000001,
        0.6887964051000001,
        0.6881435641000001,
        0.6851620581000001,
        0.6815682381000001,
        0.6865781101,
        0.6610035701000001,
        0.6950333401000001,
        0.6556859241
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.247 ± 0.049 4.163 4.329 1.96 ± 0.07
pacquet@main 4.255 ± 0.037 4.177 4.295 1.96 ± 0.07
pnpr@HEAD 2.166 ± 0.072 2.093 2.305 1.00
pnpr@main 2.240 ± 0.171 2.061 2.484 1.03 ± 0.09
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.24702607264,
      "stddev": 0.04885676168108486,
      "median": 4.25686886974,
      "user": 3.8042719999999997,
      "system": 3.3904595800000004,
      "min": 4.16313962824,
      "max": 4.32916092024,
      "times": [
        4.27984116724,
        4.32916092024,
        4.24656869724,
        4.28565540424,
        4.16313962824,
        4.21070288024,
        4.19933219424,
        4.2207199522400005,
        4.26716904224,
        4.26797084024
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.25493320784,
      "stddev": 0.036976327280891334,
      "median": 4.26569125274,
      "user": 3.7550787,
      "system": 3.4214877800000005,
      "min": 4.17674198124,
      "max": 4.29544271224,
      "times": [
        4.21255936824,
        4.17674198124,
        4.26248010824,
        4.29544271224,
        4.2839656262400005,
        4.28396180624,
        4.23210756824,
        4.2706904022400005,
        4.2680935702400005,
        4.26328893524
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.16648909444,
      "stddev": 0.07168253730570763,
      "median": 2.15527133324,
      "user": 2.5640302000000004,
      "system": 2.98579118,
      "min": 2.09329547824,
      "max": 2.3051042602400003,
      "times": [
        2.10041860424,
        2.16329853724,
        2.17320344424,
        2.1321554702400003,
        2.10859676524,
        2.09329547824,
        2.14724412924,
        2.27760346724,
        2.16397078824,
        2.3051042602400003
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.2402290068399995,
      "stddev": 0.17138406304400167,
      "median": 2.15012803624,
      "user": 2.5531553000000002,
      "system": 3.0158982799999996,
      "min": 2.06104426624,
      "max": 2.48433324124,
      "times": [
        2.06831743924,
        2.4401319792400002,
        2.13582101824,
        2.36484040624,
        2.11425960124,
        2.44451019624,
        2.48433324124,
        2.12459686624,
        2.16443505424,
        2.06104426624
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, hot cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.459 ± 0.018 1.432 1.491 2.21 ± 0.05
pacquet@main 1.447 ± 0.112 1.362 1.742 2.19 ± 0.17
pnpr@HEAD 0.671 ± 0.034 0.652 0.766 1.02 ± 0.05
pnpr@main 0.661 ± 0.012 0.645 0.685 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.45872451616,
      "stddev": 0.0182214366793883,
      "median": 1.45826905786,
      "user": 1.47087904,
      "system": 1.7762791,
      "min": 1.43151340486,
      "max": 1.49145383086,
      "times": [
        1.45953043586,
        1.47355435486,
        1.4771733828600002,
        1.45700767986,
        1.49145383086,
        1.44860985086,
        1.46191407886,
        1.43151340486,
        1.44647549786,
        1.4400126448600001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.44655124736,
      "stddev": 0.112297678614344,
      "median": 1.41705880436,
      "user": 1.42010124,
      "system": 1.7644368,
      "min": 1.3618677618600001,
      "max": 1.7419938508600001,
      "times": [
        1.43885957886,
        1.7419938508600001,
        1.4743613988600002,
        1.45774530986,
        1.4655926948600002,
        1.3678289328600002,
        1.3618677618600001,
        1.39319586386,
        1.36880905186,
        1.3952580298600001
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6713675588600001,
      "stddev": 0.03398846500277617,
      "median": 0.6601346273600001,
      "user": 0.34064214,
      "system": 1.298981,
      "min": 0.6522886048600001,
      "max": 0.7663621888600001,
      "times": [
        0.6655507068600001,
        0.6699659458600001,
        0.6548412908600001,
        0.6522886048600001,
        0.7663621888600001,
        0.6710302488600001,
        0.6598275488600001,
        0.65952488086,
        0.6604417058600001,
        0.6538424668600001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.66114302806,
      "stddev": 0.01187685133313228,
      "median": 0.6619593938600001,
      "user": 0.34340033999999997,
      "system": 1.2955269,
      "min": 0.6447275868600001,
      "max": 0.6854135438600001,
      "times": [
        0.6642865288600001,
        0.6612230288600001,
        0.6626957588600001,
        0.64587237486,
        0.6447275868600001,
        0.6854135438600001,
        0.6568135758600001,
        0.6715598538600001,
        0.6553989988600001,
        0.6634390298600001
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.074 ± 0.070 3.014 3.257 4.72 ± 0.12
pacquet@main 3.071 ± 0.070 3.018 3.263 4.72 ± 0.12
pnpr@HEAD 0.651 ± 0.009 0.641 0.670 1.00
pnpr@main 0.660 ± 0.018 0.638 0.704 1.01 ± 0.03
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.07373477354,
      "stddev": 0.06964622147138776,
      "median": 3.05259201914,
      "user": 1.83069344,
      "system": 2.01602088,
      "min": 3.01350193564,
      "max": 3.25670076064,
      "times": [
        3.04062946964,
        3.05705741864,
        3.01350193564,
        3.04812661964,
        3.06685486464,
        3.10666796064,
        3.04008552464,
        3.25670076064,
        3.08094349164,
        3.02677968964
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.07129199654,
      "stddev": 0.06991823042131683,
      "median": 3.05343734764,
      "user": 1.82205814,
      "system": 2.0091297799999994,
      "min": 3.01793402864,
      "max": 3.26281408364,
      "times": [
        3.05007746164,
        3.04474985564,
        3.04031316964,
        3.06554524864,
        3.03037539764,
        3.01793402864,
        3.0567972336399998,
        3.26281408364,
        3.0869254866399998,
        3.05738799964
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.65135332844,
      "stddev": 0.008626985280428713,
      "median": 0.64942442564,
      "user": 0.33592083999999994,
      "system": 1.30336288,
      "min": 0.64078132564,
      "max": 0.6699297696400001,
      "times": [
        0.6699297696400001,
        0.6549340476400001,
        0.65531798564,
        0.64818192064,
        0.64607656964,
        0.64675745464,
        0.6425924576400001,
        0.64078132564,
        0.65066693064,
        0.65829482264
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6601961093400001,
      "stddev": 0.018319807896529934,
      "median": 0.66266749714,
      "user": 0.32824894,
      "system": 1.29826708,
      "min": 0.63751094564,
      "max": 0.70379259464,
      "times": [
        0.66424171064,
        0.64370533764,
        0.66440859364,
        0.66274093964,
        0.66399511164,
        0.64484182964,
        0.63751094564,
        0.66259405464,
        0.65412997564,
        0.70379259464
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12449
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
4,247.03 ms
(+1.84%)Baseline: 4,170.37 ms
5,004.44 ms
(84.87%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
3,073.73 ms
(+2.85%)Baseline: 2,988.48 ms
3,586.17 ms
(85.71%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,458.72 ms
(+11.10%)Baseline: 1,313.00 ms
1,575.60 ms
(92.58%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
4,212.27 ms
(+6.65%)Baseline: 3,949.75 ms
4,739.70 ms
(88.87%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
622.03 ms
(+0.20%)Baseline: 620.80 ms
744.97 ms
(83.50%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12449
Testbedpnpr

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,166.49 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
651.35 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
671.37 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,155.03 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
669.10 ms
🐰 View full continuous benchmarking report in Bencher

A direct dependency whose manifest specifier no longer matches the
lockfile is something a real install would rewrite, so --dry-run now
reports it (instead of "up to date") while still exiting 0 and writing
nothing.

The importer diff keys on each direct dependency's `specifier` rather than
its resolved version: the in-memory resolved-version fields are cleared for
specifier-mismatched deps (they're about to be re-resolved), and for a
direct dependency the resolved version only changes when the specifier
does. Both stacks updated; pnpm gates this behind a new
`includeImporterSpecifiers` option on the lockfile-diff helper that
`dedupe --check` leaves off.

---
Written by an agent (Claude Code, claude-opus-4-8).
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit b87f89a

Comment thread installing/dedupe/check/src/dedupeDiffCheck.ts Outdated
… closed

The workspace recursive install returned without forwarding the dry-run
lockfile comparison, so a workspace `install --dry-run` could mask stale
lockfiles as "up to date". Thread `dryRunResult` through the shared
workspace-lockfile path (recursive -> recursiveInstallThenUpdateWorkspaceState
-> installDeps), and make `renderDryRunReport` require a result. When the
installer can't produce one (e.g. a workspace without a shared lockfile),
fail closed with `ERR_PNPM_DRY_RUN_UNSUPPORTED` rather than reporting no
changes. Adds a workspace dry-run regression test.

---
Written by an agent (Claude Code, claude-opus-4-8).
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit aff90d1

The specifier-only importer diff missed a dependency moving between
dependency groups (e.g. dev -> prod) when its specifier was unchanged,
since the lockfile `specifiers` map is flat across groups. Diff importers by
the per-group dependency fields *and* `specifiers`: the per-group fields
catch group moves, `specifiers` (compared last so it wins the render on a
collision) catches specifier-only edits. Adds a group-move regression test.

---
Written by an agent (Claude Code, claude-opus-4-8).
Comment thread installing/deps-installer/src/install/index.ts
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 8b15544

Comment thread installing/commands/src/install.ts
Comment thread installing/commands/src/install.ts
Comment thread installing/commands/src/install.ts
A plain install persists auto-policy updates (e.g. minimumReleaseAgeExclude)
to pnpm-workspace.yaml, and --update rewrites package.json, both gated only
on `opts.save !== false`. A dry-run could therefore still write a manifest.
Gate every project/workspace manifest write on `!opts.dryRun` across the
single-project, recursive workspace, and add/update paths so --dry-run truly
writes nothing.

---
Written by an agent (Claude Code, claude-opus-4-8).
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 78b676d

Comment thread pacquet/crates/cli/src/cli_args/install.rs
Comment thread installing/commands/src/install.ts
Comment thread installing/commands/src/installDeps.ts
Comment thread pacquet/crates/package-manager/src/install/tests.rs
… add/update/remove

- pacquet: reject `install --dry-run` when a pnpr server is configured
  (ERR_PNPM_CONFIG_CONFLICT_DRY_RUN_WITH_PNPR_SERVER), matching pnpm —
  the pnpr path resolves/links through the server and can't be no-write.
- pnpm: when no lockfile comparison can be produced, print an
  informational message and exit 0 instead of throwing, honoring the
  documented "--dry-run always exits 0" contract.
- pnpm: `dry-run` is a real config key, so force `dryRun: false` in the
  add/update/remove/dedupe install paths — a config-level `dry-run` must
  not silently turn those into no-op check-only runs (matches pacquet,
  which already sets `dry_run: false` on those paths).

---
Written by an agent (Claude Code, claude-opus-4-8).
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 2e974cc

@zkochan zkochan merged commit c112b61 into main Jun 16, 2026
27 checks passed
@zkochan zkochan deleted the feat/dry-run-install-v2 branch June 16, 2026 17:12
KSXGitHub pushed a commit that referenced this pull request Jun 16, 2026
Fast-forward was not possible: both sides diverged from 7c97d9e (2 local
commits removing redundant Arc usage, 15 commits from main). The merge is
conflict-free — of main's new commits, only #12449 (install
--dry-run) touched install_with_fresh_lockfile.rs, in a region disjoint
from the compat-extender change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dry-run option for install

2 participants