Skip to content

feat(install): add --dry-run option#12270

Closed
samuelfullerthomas wants to merge 2 commits into
pnpm:mainfrom
samuelfullerthomas:feat/dry-run-install
Closed

feat(install): add --dry-run option#12270
samuelfullerthomas wants to merge 2 commits into
pnpm:mainfrom
samuelfullerthomas:feat/dry-run-install

Conversation

@samuelfullerthomas

@samuelfullerthomas samuelfullerthomas commented Jun 8, 2026

Copy link
Copy Markdown

PR Summary by Qodo

feat(install): add --dry-run option to pnpm install
✨ Enhancement 🧪 Tests ⚙️ Configuration changes 📝 Documentation 🕐 20-40 Minutes

Grey Divider

Walkthroughs

User Description

Description

Adds a --dry-run flag to the install command that runs dependency resolution and validates the lockfile without writing any changes to disk.

This enables use in pre-commit hooks to catch outdated lockfiles and other common install errors.

resolves #7340

AI Description
• Adds pnpm install --dry-run to validate lockfile freshness without installing packages.
• Implements dry-run as --frozen-lockfile + --lockfile-only, with conflict checks.
• Adds TypeScript + Rust/pacquet test coverage for fresh and stale lockfile scenarios.
Diagram
graph TD
  CLI[/"pnpm CLI\n--dry-run"/] --> Install["install.ts\nhandler()"] --> InstallDeps["installDeps.ts"] --> DepsInstaller["deps-installer\ninstall()"] --> Headless["deps-restorer\nheadlessInstall()"] --> Lockfile[("pnpm-lock.yaml\nvalidation")]
  Pacquet[/"pacquet CLI (Rust)\n--dry-run"/] --> DepsInstaller
  Config["Config.ts\ndryRun?: boolean"] -.-> Install

  subgraph Legend
    direction LR
    _cli[/"CLI"/] ~~~ _mod["Module"] ~~~ _db[("Lockfile")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Dedicated verify-lockfile command
  • ➕ Clearer intent than an install flag
  • ➕ Could offer richer output (diff/suggestions)
  • ➖ Adds a new command surface to maintain
  • ➖ Doesn’t match the requested install --dry-run workflow
2. New resolver-only 'read-only' mode (no disk IO)
  • ➕ Could guarantee absolutely no writes even in edge cases
  • ➖ Significantly larger change to ensure all write paths are gated
  • ➖ Current approach already reuses proven frozen-lockfile safeguards

Recommendation: The chosen implementation (translate --dry-run to --frozen-lockfile --lockfile-only) is the best tradeoff: minimal new logic, maximum reuse of existing correctness checks, and easy to mirror in pacquet. Alternatives either expand surface area (new command) or require much broader refactors (true IO-free mode) for limited additional value.

Grey Divider

File Changes

Enhancement (3)
install.ts Add '--dry-run' flag and map it to frozen lockfile validation +14/-0

Add '--dry-run' flag and map it to frozen lockfile validation

• Registers 'dry-run' in rc/CLI help and includes 'dryRun' in option typing. When enabled, sets 'frozenLockfile=true' and 'lockfileOnly=true', and rejects use with '--resolution-only'.

installing/commands/src/install.ts


index.ts Thread 'dryRun' through headless install and pruning +3/-1

Thread 'dryRun' through headless install and pruning

• Adds 'dryRun?: boolean' to 'HeadlessOptions' and forwards it into 'scriptsOpts' and 'prune()' so headless flows can honor dry-run semantics.

installing/deps-restorer/src/index.ts


install.rs Add pacquet 'install --dry-run' and map to frozen+lockfile-only +12/-0

Add pacquet 'install --dry-run' and map to frozen+lockfile-only

• Adds a '--dry-run' clap flag and translates it into '(frozen_lockfile=true, lockfile_only=true)' before handing off to the install pipeline.

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


Refactor (1)
installDeps.ts Allow forwarding 'frozenLockfile' override through InstallDepsOptions +1/-1

Allow forwarding 'frozenLockfile' override through InstallDepsOptions

• Expands 'InstallDepsOptions' to include 'frozenLockfile' in its partial config pick so the install command can set it for dry-run.

installing/commands/src/installDeps.ts


Tests (4)
install.ts Add install-command test for stale lockfile in dry-run +26/-0

Add install-command test for stale lockfile in dry-run

• Adds a test that installs once, drifts 'package.json', and verifies 'dryRun: true' fails with a frozen-lockfile error.

installing/commands/test/install.ts


dryRun.ts Add deps-installer dry-run behavior tests +107/-0

Add deps-installer dry-run behavior tests

• Introduces a new test suite covering success on fresh lockfile, failure on stale lockfile, and ensuring no lockfile or 'node_modules' writes occur during dry-run scenarios.

installing/deps-installer/test/install/dryRun.ts


tests.rs Test parsing of pacquet '--dry-run' flag +10/-0

Test parsing of pacquet '--dry-run' flag

• Adds a unit test ensuring 'dry_run' defaults to false and becomes true when the flag is passed.

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


dry_run.rs Add pacquet end-to-end dry-run tests +84/-0

Add pacquet end-to-end dry-run tests

• Adds integration tests verifying '--dry-run' does not create 'node_modules' when lockfile is fresh and exits non-zero with an outdated-lockfile diagnostic when stale.

pacquet/crates/cli/tests/dry_run.rs


Documentation (1)
dry-run-install.md Document '--dry-run' and bump minor versions +6/-0

Document '--dry-run' and bump minor versions

• Adds a changeset announcing 'pnpm install --dry-run', describing lockfile validation behavior and non-zero exit on stale lockfiles.

.changeset/dry-run-install.md


Other (2)
Config.ts Make 'dryRun' a supported config field +1/-1

Make 'dryRun' a supported config field

• Removes the deprecated comment on 'dryRun?: boolean', making it a normal supported config option.

config/reader/src/Config.ts


pnpm-workspace.yaml Add dev workspace override for 'pd' +2/-0

Add dev workspace override for 'pd'

• Adds a dev-only workspace override linking 'pd' via 'link:' for local development wiring.

pnpm/dev/pnpm-workspace.yaml


Grey Divider

Qodo Logo

Summary by CodeRabbit

  • New Features

    • Added a --dry-run install option that validates the lockfile without installing and exits non-zero if outdated (CLI flag supported).
  • Bug Fixes

    • Prevents lockfile writes during frozen/lockfile-only checks and ensures dry runs do not create or modify node_modules or prune packages.
  • Tests

    • Added unit and integration tests covering dry-run success/failure scenarios and no-install behavior.
  • Documentation

    • Added changeset entry and updated config/help text describing dry-run semantics.

Copilot AI review requested due to automatic review settings June 8, 2026 15:33
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Grey Divider


Action required

1. pnpr breaks dry-run 🐞 Bug ≡ Correctness
Description
install.handler() implements --dry-run by forcing frozenLockfile=true and lockfileOnly=true,
but if pnprServer is configured the deps-installer takes the pnpr server path which writes
pnpm-lock.yaml and inherently writes to the store. This violates the dry-run contract of “validate
without writing changes to disk” and can dirty workspaces in pre-commit/CI environments that have
pnpr enabled.
Code

installing/commands/src/install.ts[R425-432]

+  if (opts.dryRun) {
+    if (opts.resolutionOnly) {
+      throw new PnpmError('CONFIG_CONFLICT_DRY_RUN_WITH_RESOLUTION_ONLY',
+        'Cannot use --dry-run with --resolution-only')
+    }
+    installDepsOptions.frozenLockfile = true
+    installDepsOptions.lockfileOnly = true
+  }
Evidence
The new --dry-run mapping forces frozenLockfile and lockfileOnly in the install command
handler, but the deps-installer has an unconditional early dispatch to installViaPnprServer()
whenever opts.pnprServer is set; that pnpr path writes the lockfile via
writeWantedLockfileAndRecordVerified() and explicitly documents that it inherently writes to the
store. Therefore, --dry-run is not side-effect-free under pnprServer configuration.

installing/commands/src/install.ts[398-433]
installing/deps-installer/src/install/index.ts[174-185]
installing/deps-installer/src/install/index.ts[2487-2594]

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 validate lockfile freshness without writing to disk. However, when `pnprServer` is configured, the deps-installer routes the install to `installViaPnprServer()`, which persists a lockfile and writes to the store, breaking dry-run’s side-effect-free contract.
## Issue Context
`--dry-run` is desugared to `frozenLockfile + lockfileOnly` in the install command handler. That desugaring does not prevent the pnpr-server dispatch, which happens before the normal flow and includes unconditional lockfile persistence.
## Fix Focus Areas
- installing/commands/src/install.ts[425-432]
## Proposed fix
- In the `if (opts.dryRun)` block, add a config conflict guard when `opts.pnprServer` (or equivalent config-derived value) is set.
- Throw a `PnpmError` with a new code like `CONFIG_CONFLICT_DRY_RUN_WITH_PNPR_SERVER` and a clear message (e.g. “Cannot use --dry-run with --pnpr-server / pnprServer because pnpr installs write lockfile/store state”).
- Alternatively (less strict), explicitly unset `installDepsOptions.pnprServer` when `dryRun` is set so the normal frozen validation path is used.

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


2. Pacquet dry-run writes lockfile 🐞 Bug ≡ Correctness
Description
pacquet’s new --dry-run sets lockfile_only = true, and the lockfile_only && take_frozen_path
branch persists pnpm-lock.yaml via save_to_path, so a successful dry run can still rewrite/touch
the lockfile on disk. This violates the intended dry-run semantics (validate without changing
workspace files) and can dirty/normalize lockfiles in pre-commit/CI workflows.
Code

pacquet/crates/cli/src/cli_args/install.rs[R289-292]

+        // --dry-run: validate lockfile is in sync without installing.
+        let frozen_lockfile = frozen_lockfile || dry_run;
+        let lockfile_only = lockfile_only || dry_run;
+
Evidence
The new CLI mapping makes --dry-run imply lockfile_only, and the package-manager’s lockfile-only
frozen path explicitly writes the wanted lockfile to disk, so the new flag can still cause workspace
writes.

pacquet/crates/cli/src/cli_args/install.rs[289-292]
pacquet/crates/package-manager/src/install.rs[775-803]

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 implemented by OR-ing `dry_run` into `frozen_lockfile` and `lockfile_only`. However, pacquet’s install pipeline writes `pnpm-lock.yaml` in the `lockfile_only && take_frozen_path` branch, so `--dry-run` can still perform on-disk writes.
### Issue Context
- The CLI mapping is in `pacquet/crates/cli/src/cli_args/install.rs`.
- The actual write occurs in `pacquet/crates/package-manager/src/install.rs` inside the `if lockfile_only && take_frozen_path { ... save_to_path(...) ... }` branch.
- Goal: `--dry-run` should validate freshness and exit status without modifying workspace files.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/install.rs[289-292]
- pacquet/crates/package-manager/src/install.rs[783-803]
### Implementation sketch
- Plumb a dedicated `dry_run` boolean into the package-manager `Install` options (not just derived `lockfile_only`).
- When `dry_run` is true, skip any persistence of `pnpm-lock.yaml` (and any other workspace files) on success; keep the freshness checks and non-zero exits when stale.
- Add/extend a test to assert `pnpm-lock.yaml` content (and ideally mtime) is unchanged after `pacquet install --dry-run` when the lockfile is already fresh.

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


3. Dry-run writes pnpm-lock.yaml 🐞 Bug ≡ Correctness
Description
install.handler() implements --dry-run by forcing frozenLockfile=true and lockfileOnly=true,
but the frozen+lockfileOnly path unconditionally calls writeWantedLockfile, so a successful dry
run still writes pnpm-lock.yaml to disk (at least updating mtime and potentially normalizing
content). This breaks the documented/expected “validate without writing changes to disk” behavior
for pre-commit/CI usage.
Code

installing/commands/src/install.ts[R419-426]

+  if (opts.dryRun) {
+    if (opts.resolutionOnly) {
+      throw new PnpmError('CONFIG_CONFLICT_DRY_RUN_WITH_RESOLUTION_ONLY',
+        'Cannot use --dry-run with --resolution-only')
+    }
+    installDepsOptions.frozenLockfile = true
+    installDepsOptions.lockfileOnly = true
+  }
Evidence
The PR makes --dry-run set lockfileOnly=true. In the installer, lockfileOnly triggers an
unconditional call to writeWantedLockfile, and writeWantedLockfile always writes via
writeFileAtomic, so dry-run will still write the lockfile even when up-to-date.

installing/commands/src/install.ts[392-427]
installing/deps-installer/src/install/index.ts[948-992]
lockfile/fs/src/write.ts[34-90]

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 sets `lockfileOnly=true`, which routes the install into a code path that **always writes** `pnpm-lock.yaml` even when the lockfile is already up-to-date. This violates the intended semantics of dry-run (no disk writes) and can dirty working trees or trigger filesystem watchers.
## Issue Context
The installer’s frozen+lockfileOnly early-return branch calls `writeWantedLockfile(...)`, and `writeWantedLockfile` always uses `writeFileAtomic(...)` (no content-diff guard), so it will replace/touch the file.
## Fix Focus Areas
- installing/commands/src/install.ts[419-426]
### Suggested approach
1. Introduce/propagate a dedicated `dryRun`/`noWrite` flag into the deps-installer layer (or reuse an existing “check-only” mechanism) so you can:
- keep `frozenLockfile=true` (to validate lockfile freshness)
- avoid `node_modules` materialization
- **avoid calling any lockfile write functions** when `--dry-run` is set.
2. Concretely, gate the `writeWantedLockfile(...)` call in the frozen+lockfileOnly branch on a new option (e.g. `saveLockfile !== false` / `dryRun !== true`). Then set that option from the CLI when `--dry-run` is used.
3. Add a test assertion that `pnpm-lock.yaml` mtime/content does not change on a successful `--dry-run` run.

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



Remediation recommended

4. Dry-run creates store directory 🐞 Bug ≡ Correctness ⭐ New
Description
pnpm install --dry-run still calls installDeps(), which unconditionally initializes the store
controller and can create the configured store directory via fs.mkdir(..., { recursive: true }).
This breaks the dry-run “no writes” expectation and can dirty fresh CI/pre-commit environments by
creating storeDir even though no install artifacts are produced.
Code

installing/commands/src/install.ts[R425-436]

+  if (opts.dryRun) {
+    if (opts.resolutionOnly) {
+      throw new PnpmError('CONFIG_CONFLICT_DRY_RUN_WITH_RESOLUTION_ONLY',
+        'Cannot use --dry-run with --resolution-only')
+    }
+    if (opts.pnprServer) {
+      throw new PnpmError('CONFIG_CONFLICT_DRY_RUN_WITH_PNPR_SERVER',
+        'Cannot use --dry-run with a configured pnpr server because pnpr installs resolve through the server and write the lockfile and store')
+    }
+    installDepsOptions.frozenLockfile = true
+    installDepsOptions.lockfileOnly = true
+  }
Evidence
The new --dry-run path in install.ts delegates into installDeps(). installDeps() always
initializes the store controller, and store controller creation explicitly creates the store
directory on disk when frozenStore is not enabled—so dry-run can still write to disk even though
it sets lockfileOnly/frozenLockfile.

installing/commands/src/install.ts[398-437]
installing/commands/src/installDeps.ts[201-214]
store/connection-manager/src/createNewStoreController.ts[67-79]

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 validate an up-to-date lockfile without writing to disk, but the current implementation still goes through `installDeps()`, which eagerly creates the store controller and may create the store directory (`fs.mkdir(storeDir, { recursive: true })`). This means `--dry-run` can still have observable filesystem side effects in new/clean environments.

## Issue Context
- `--dry-run` is implemented in the install command handler by forcing `frozenLockfile=true` and `lockfileOnly=true`.
- `installDeps()` always initializes the store controller early.
- Store controller creation (`createNewStoreController`) creates `opts.storeDir` unless `frozenStore` is enabled.

## Fix Focus Areas
- installing/commands/src/install.ts[425-436]
- installing/commands/src/installDeps.ts[201-214]
- store/connection-manager/src/createNewStoreController.ts[67-79]

## Suggested fix
1. In `installing/commands/src/install.ts`, when `opts.dryRun` is true, also set `installDepsOptions.frozenStore = true` (and/or introduce a dedicated flag to skip store initialization) so that `createNewStoreController()` will not create the store directory.
2. If setting `frozenStore` for dry-run is not acceptable globally, refactor `installDeps()` to defer `createStoreController(opts)` until it is actually needed (i.e., after any early-return validation paths), so the dry-run validation path can exit without initializing store machinery.
3. Add a targeted test that runs `install.handler({ dryRun: true, ... })` with a fresh `storeDir` (temp dir) and asserts the directory is not created.

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


5. Misleading headless dryRun 🐞 Bug ⚙ Maintainability
Description
HeadlessOptions.dryRun is documented as “skip writing to disk (lockfile and node_modules)”, but
headlessInstall still unconditionally writes .modules.yaml and lockfiles without checking
opts.dryRun. This documentation/behavior mismatch can cause callers to rely on dryRun for
side-effect-free execution when it is not actually enforced.
Code

installing/deps-restorer/src/index.ts[R119-120]

+  /** When true, skip writing to disk (lockfile and node_modules). */
+  dryRun?: boolean
Evidence
The interface comment promises no disk writes, but the implementation still performs unconditional
writes to modules/lockfile state without checking opts.dryRun anywhere in that write path.

installing/deps-restorer/src/index.ts[109-120]
installing/deps-restorer/src/index.ts[664-699]

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

## Issue description
`HeadlessOptions.dryRun` is documented as preventing writes to disk, but `headlessInstall` still writes `.modules.yaml` and lockfiles regardless. This creates a misleading contract and risks future misuse.
### Issue Context
In `headlessInstall`, `dryRun` is currently only threaded into pruning and script execution options, while the core write operations (`writeModulesManifest`, `writeLockfiles`/`writeCurrentLockfile`) are not gated.
### Fix Focus Areas
- installing/deps-restorer/src/index.ts[119-120]
- installing/deps-restorer/src/index.ts[664-699]
### Implementation options
- **Preferred (if dryRun is meant to be real):** Gate the write calls (`writeModulesManifest`, `writeLockfiles`, `writeCurrentLockfile`, and any other workspace writes) behind `if (!opts.dryRun)`.
- **Alternative (if dryRun is only meant to skip pruning):** Update the doc comment to reflect actual behavior (e.g., “skip pruning orphan packages / destructive operations”) and consider renaming the flag to avoid implying a full no-write dry run.

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


6. Nested workspace root breakage ✓ Resolved 🐞 Bug ☼ Reliability
Description
The new pnpm/dev/pnpm-workspace.yaml causes findWorkspaceDir() to treat pnpm/dev as the
workspace root when invoked from that subtree. pnpm/dev/pd.js relies on discovering the repo
workspace from pnpm/dev, so this changes its behavior (and may break it) by reading a manifest
without packages and scanning the wrong scope.
Code

pnpm/dev/pnpm-workspace.yaml[R1-2]

+overrides:
+  pd: 'link:'
Evidence
findWorkspaceDir() resolves the nearest pnpm-workspace.yaml. Since the PR adds one inside
pnpm/dev, the pd script (which searches from pnpm/dev) will resolve the nested workspace root
and read a manifest that lacks packages, changing or breaking its workspace package discovery.

pnpm/dev/pnpm-workspace.yaml[1-2]
workspace/root-finder/src/index.ts[19-28]
pnpm/dev/pd.js[13-17]

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 new `pnpm/dev/pnpm-workspace.yaml` changes workspace-root discovery: tooling executed from `pnpm/dev` will now stop at this nested manifest rather than the repo root. The `pd` dev script (`pnpm/dev/pd.js`) calls `findWorkspaceDir(import.meta.dirname)` and then reads `workspaceManifest.packages`, so this nested manifest can cause incorrect package discovery or failures.
## Issue Context
`findWorkspaceDir()` returns the *nearest* `pnpm-workspace.yaml` found by walking up the directory tree. The new file contains only `overrides` (no `packages`), and it is placed exactly where the `pd` script starts searching.
## Fix Focus Areas
- pnpm/dev/pnpm-workspace.yaml[1-2]
### Suggested approach
- Prefer removing this file entirely unless it is strictly required.
- If an override for `pd` is needed, move it to the repo-root `pnpm-workspace.yaml` (workspace-level overrides belong there).
- If you truly need a separate workspace under `pnpm/dev`, ensure the file contains a valid `packages:` list and that `pd.js` explicitly targets the repo root workspace (instead of “nearest manifest”).

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


Grey Divider

Previous review results

Review updated until commit 1de255d

Results up to commit N/A


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


Action required
1. pnpr breaks dry-run 🐞 Bug ≡ Correctness
Description
install.handler() implements --dry-run by forcing frozenLockfile=true and lockfileOnly=true,
but if pnprServer is configured the deps-installer takes the pnpr server path which writes
pnpm-lock.yaml and inherently writes to the store. This violates the dry-run contract of “validate
without writing changes to disk” and can dirty workspaces in pre-commit/CI environments that have
pnpr enabled.
Code

installing/commands/src/install.ts[R425-432]

+  if (opts.dryRun) {
+    if (opts.resolutionOnly) {
+      throw new PnpmError('CONFIG_CONFLICT_DRY_RUN_WITH_RESOLUTION_ONLY',
+        'Cannot use --dry-run with --resolution-only')
+    }
+    installDepsOptions.frozenLockfile = true
+    installDepsOptions.lockfileOnly = true
+  }
Evidence
The new --dry-run mapping forces frozenLockfile and lockfileOnly in the install command
handler, but the deps-installer has an unconditional early dispatch to installViaPnprServer()
whenever opts.pnprServer is set; that pnpr path writes the lockfile via
writeWantedLockfileAndRecordVerified() and explicitly documents that it inherently writes to the
store. Therefore, --dry-run is not side-effect-free under pnprServer configuration.

installing/commands/src/install.ts[398-433]
installing/deps-installer/src/install/index.ts[174-185]
installing/deps-installer/src/install/index.ts[2487-2594]

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 validate lockfile freshness without writing to disk. However, when `pnprServer` is configured, the deps-installer routes the install to `installViaPnprServer()`, which persists a lockfile and writes to the store, breaking dry-run’s side-effect-free contract.
## Issue Context
`--dry-run` is desugared to `frozenLockfile + lockfileOnly` in the install command handler. That desugaring does not prevent the pnpr-server dispatch, which happens before the normal flow and includes unconditional lockfile persistence.
## Fix Focus Areas
- installing/commands/src/install.ts[425-432]
## Proposed fix
- In the `if (opts.dryRun)` block, add a config conflict guard when `opts.pnprServer` (or equivalent config-derived value) is set.
- Throw a `PnpmError` with a new code like `CONFIG_CONFLICT_DRY_RUN_WITH_PNPR_SERVER` and a clear message (e.g. “Cannot use --dry-run with --pnpr-server / pnprServer because pnpr installs write lockfile/store state”).
- Alternatively (less strict), explicitly unset `installDepsOptions.pnprServer` when `dryRun` is set so the normal frozen validation path is used.

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


2. Pacquet dry-run writes lockfile 🐞 Bug ≡ Correctness
Description
pacquet’s new --dry-run sets lockfile_only = true, and the lockfile_only && take_frozen_path
branch persists pnpm-lock.yaml via save_to_path, so a successful dry run can still rewrite/touch
the lockfile on disk. This violates the intended dry-run semantics (validate without changing
workspace files) and can dirty/normalize lockfiles in pre-commit/CI workflows.
Code

pacquet/crates/cli/src/cli_args/install.rs[R289-292]

+        // --dry-run: validate lockfile is in sync without installing.
+        let frozen_lockfile = frozen_lockfile || dry_run;
+        let lockfile_only = lockfile_only || dry_run;
+
Evidence
The new CLI mapping makes --dry-run imply lockfile_only, and the package-manager’s lockfile-only
frozen path explicitly writes the wanted lockfile to disk, so the new flag can still cause workspace
writes.

pacquet/crates/cli/src/cli_args/install.rs[289-292]
pacquet/crates/package-manager/src/install.rs[775-803]

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 implemented by OR-ing `dry_run` into `frozen_lockfile` and `lockfile_only`. However, pacquet’s install pipeline writes `pnpm-lock.yaml` in the `lockfile_only && take_frozen_path` branch, so `--dry-run` can still perform on-disk writes.
### Issue Context
- The CLI mapping is in `pacquet/crates/cli/src/cli_args/install.rs`.
- The actual write occurs in `pacquet/crates/package-manager/src/install.rs` inside the `if lockfile_only && take_frozen_path { ... save_to_path(...) ... }` branch.
- Goal: `--dry-run` should validate freshness and exit status without modifying workspace files.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/install.rs[289-292]
- pacquet/crates/package-manager/src/install.rs[783-803]
### Implementation sketch
- Plumb a dedicated `dry_run` boolean into the package-manager `Install` options (not just derived `lockfile_only`).
- When `dry_run` is true, skip any persistence of `pnpm-lock.yaml` (and any other workspace files) on success; keep the freshness checks and non-zero exits when stale.
- Add/extend a test to assert `pnpm-lock.yaml` content (and ideally mtime) is unchanged after `pacquet install --dry-run` when the lockfile is already fresh.

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


3. Dry-run writes pnpm-lock.yaml 🐞 Bug ≡ Correctness
Description
install.handler() implements --dry-run by forcing frozenLockfile=true and lockfileOnly=true,
but the frozen+lockfileOnly path unconditionally calls writeWantedLockfile, so a successful dry
run still writes pnpm-lock.yaml to disk (at least updating mtime and potentially normalizing
content). This breaks the documented/expected “validate without writing changes to disk” behavior
for pre-commit/CI usage.
Code

installing/commands/src/install.ts[R419-426]

+  if (opts.dryRun) {
+    if (opts.resolutionOnly) {
+      throw new PnpmError('CONFIG_CONFLICT_DRY_RUN_WITH_RESOLUTION_ONLY',
+        'Cannot use --dry-run with --resolution-only')
+    }
+    installDepsOptions.frozenLockfile = true
+    installDepsOptions.lockfileOnly = true
+  }
Evidence
The PR makes --dry-run set lockfileOnly=true. In the installer, lockfileOnly triggers an
unconditional call to writeWantedLockfile, and writeWantedLockfile always writes via
writeFileAtomic, so dry-run will still write the lockfile even when up-to-date.

installing/commands/src/install.ts[392-427]
installing/deps-installer/src/install/index.ts[948-992]
lockfile/fs/src/write.ts[34-90]

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 sets `lockfileOnly=true`, which routes the install into a code path that **always writes** `pnpm-lock.yaml` even when the lockfile is already up-to-date. This violates the intended semantics of dry-run (no disk writes) and can dirty working trees or trigger filesystem watchers.
## Issue Context
The installer’s frozen+lockfileOnly early-return branch calls `writeWantedLockfile(...)`, and `writeWantedLockfile` always uses `writeFileAtomic(...)` (no content-diff guard), so it will replace/touch the file.
## Fix Focus Areas
- installing/commands/src/install.ts[419-426]
### Suggested approach
1. Introduce/propagate a dedicated `dryRun`/`noWrite` flag into the deps-installer layer (or reuse an existing “check-only” mechanism) so you can:
- keep `frozenLockfile=true` (to validate lockfile freshness)
- avoid `node_modules` materialization
- **avoid calling any lockfile write functions** when `--dry-run` is set.
2. Concretely, gate the `writeWantedLockfile(...)` call in the frozen+lockfileOnly branch on a new option (e.g. `saveLockfile !== false` / `dryRun !== true`). Then set that option from the CLI when `--dry-run` is used.
3. Add a test assertion that `pnpm-lock.yaml` mtime/content does not change on a successful `--dry-run` run.

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



Remediation recommended
4. Misleading headless dryRun 🐞 Bug ⚙ Maintainability
Description
HeadlessOptions.dryRun is documented as “skip writing to disk (lockfile and node_modules)”, but
headlessInstall still unconditionally writes .modules.yaml and lockfiles without checking
opts.dryRun. This documentation/behavior mismatch can cause callers to rely on dryRun for
side-effect-free execution when it is not actually enforced.
Code

installing/deps-restorer/src/index.ts[R119-120]

+  /** When true, skip writing to disk (lockfile and node_modules). */
+  dryRun?: boolean
Evidence
The interface comment promises no disk writes, but the implementation still performs unconditional
writes to modules/lockfile state without checking opts.dryRun anywhere in that write path.

installing/deps-restorer/src/index.ts[109-120]
installing/deps-restorer/src/index.ts[664-699]

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

## Issue description
`HeadlessOptions.dryRun` is documented as preventing writes to disk, but `headlessInstall` still writes `.modules.yaml` and lockfiles regardless. This creates a misleading contract and risks future misuse.
### Issue Context
In `headlessInstall`, `dryRun` is currently only threaded into pruning and script execution options, while the core write operations (`writeModulesManifest`, `writeLockfiles`/`writeCurrentLockfile`) are not gated.
### Fix Focus Areas
- installing/deps-restorer/src/index.ts[119-120]
- installing/deps-restorer/src/index.ts[664-699]
### Implementation options
- **Preferred (if dryRun is meant to be real):** Gate the write calls (`writeModulesManifest`, `writeLockfiles`, `writeCurrentLockfile`, and any other workspace writes) behind `if (!opts.dryRun)`.
- **Alternative (if dryRun is only meant to skip pruning):** Update the doc comment to reflect actual behavior (e.g., “skip pruning orphan packages / destructive operations”) and consider renaming the flag to avoid implying a full no-write dry run.

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


5. Nested workspace root breakage ✓ Resolved 🐞 Bug ☼ Reliability
Description
The new pnpm/dev/pnpm-workspace.yaml causes findWorkspaceDir() to treat pnpm/dev as the
workspace root when invoked from that subtree. pnpm/dev/pd.js relies on discovering the repo
workspace from pnpm/dev, so this changes its behavior (and may break it) by reading a manifest
without packages and scanning the wrong scope.
Code

pnpm/dev/pnpm-workspace.yaml[R1-2]

+overrides:
+  pd: 'link:'
Evidence
findWorkspaceDir() resolves the nearest pnpm-workspace.yaml. Since the PR adds one inside
pnpm/dev, the pd script (which searches from pnpm/dev) will resolve the nested workspace root
and read a manifest that lacks packages, changing or breaking its workspace package discovery.

pnpm/dev/pnpm-workspace.yaml[1-2]
workspace/root-finder/src/index.ts[19-28]
pnpm/dev/pd.js[13-17]

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 new `pnpm/dev/pnpm-workspace.yaml` changes workspace-root discovery: tooling executed from `pnpm/dev` will now stop at this nested manifest rather than the repo root. The `pd` dev script (`pnpm/dev/pd.js`) calls `findWorkspaceDir(import.meta.dirname)` and then reads `workspaceManifest.packages`, so this nested manifest can cause incorrect package discovery or failures.
## Issue Context
`findWorkspaceDir()` returns the *nearest* `pnpm-workspace.yaml` found by walking up the directory tree. The new file contains only `overrides` (no `packages`), and it is placed exactly where the `pd` script starts searching.
## Fix Focus Areas
- pnpm/dev/pnpm-workspace.yaml[1-2]
### Suggested approach
- Prefer removing this file entirely unless it is strictly required.
- If an override for `pd` is needed, move it to the repo-root `pnpm-workspace.yaml` (workspace-level overrides belong there).
- If you truly need a separate workspace under `pnpm/dev`, ensure the file contains a valid `packages:` list and that `pd.js` explicitly targets the repo root workspace (instead of “nearest manifest”).

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


Qodo Logo

@coderabbitai

coderabbitai Bot commented Jun 8, 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

Adds a --dry-run option to pnpm and pacquet install commands that validates lockfile freshness without installing or writing changes by forcing frozenLockfile + lockfileOnly; propagates a dryRun flag through install types, updates frozen-path behavior, and adds unit/integration tests and a changeset.

Changes

Dry-run feature for install command

Layer / File(s) Summary
Type contracts and configuration
config/reader/src/Config.ts, installing/commands/src/installDeps.ts, installing/deps-restorer/src/index.ts
Config keeps dryRun?: boolean; InstallDepsOptions includes frozenLockfile; HeadlessOptions adds dryRun?: boolean.
pnpm install CLI and handler
installing/commands/src/install.ts, installing/commands/test/install.ts
Registers --dry-run and help text; extends InstallCommandOptions; handler errors on --resolution-only+--dry-run, and forces installDepsOptions.frozenLockfile = true and lockfileOnly = true when dry-run is enabled; adds a command-level test for stale lockfile rejection.
Frozen-install write behavior
installing/deps-installer/src/install/index.ts
When taking the frozen-install compatibility path with lockfileOnly, skip writing the wanted lockfile if frozenLockfile is enabled.
Installer dry-run tests
installing/deps-installer/test/install/dryRun.ts
Adds tests for dry-run: success when lockfile is fresh; rejection when dependency changes would update the lockfile; rejection on fresh project; lockfile preserved after failure; no node_modules population.
Headless/script prune forwarding
installing/deps-restorer/src/index.ts, installing/deps-restorer/test/index.ts
HeadlessOptions gains dryRun?: boolean; scriptsOpts and prune(...) now receive dryRun; regression test ensures dry runs do not prune orphan packages.
pacquet CLI implementation & tests
pacquet/crates/cli/src/cli_args/install.rs, pacquet/crates/cli/src/cli_args/install/tests.rs, pacquet/crates/cli/tests/dry_run.rs
Adds --dry-run to InstallArgs, forces frozen_lockfile+lockfile_only when enabled, unit test for parsing, and integration tests for fresh vs stale lockfile behavior and absence of node_modules.
Release metadata
.changeset/dry-run-install.md
Changeset documenting pnpm install --dry-run behavior and marking @pnpm/installing.commands and pnpm as minor.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • zkochan

Poem

🐇 I nibble lockfiles, gentle and spry,
I check without fetching, then give a small sigh.
Fresh — I hop forward; stale — I thump once and stay,
No installs, no writes, just a tidy relay.
🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 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' accurately summarizes the primary change of adding a --dry-run flag to the install command.
Linked Issues check ✅ Passed The PR successfully implements all coding requirements from #7340: adds --dry-run option, validates lockfile without disk writes, returns meaningful exit codes, and includes comprehensive tests.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to implementing the --dry-run feature. Changes include option registration, conflict validation, lockfile-only enforcement, and supporting tests across pnpm and pacquet.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds --dry-run support for pnpm install and pacquet install, validating lockfile freshness without installing packages.

Changes:

  • Introduces --dry-run CLI flag (Rust pacquet + TS pnpm) and wires it to frozen-lockfile + lockfile-only behavior.
  • Threads a dryRun option through headless install plumbing.
  • Adds unit/integration tests covering parsing and lockfile-staleness behavior.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
pnpm/dev/pnpm-workspace.yaml Adds a workspace override intended to link a dependency locally.
pacquet/crates/cli/tests/dry_run.rs New integration tests for pacquet install --dry-run success/failure behavior.
pacquet/crates/cli/src/cli_args/install/tests.rs Adds parsing test for the new --dry-run flag.
pacquet/crates/cli/src/cli_args/install.rs Adds dry_run flag and maps it to --frozen-lockfile --lockfile-only.
installing/deps-restorer/src/index.ts Adds dryRun option to HeadlessOptions and passes through to installs.
installing/deps-installer/test/install/dryRun.ts Adds tests asserting “dry-run-like” behavior (via frozen+lockfile-only).
installing/commands/test/install.ts Adds pnpm install --dry-run test for stale lockfile failure.
installing/commands/src/installDeps.ts Extends InstallDepsOptions typing to include frozenLockfile.
installing/commands/src/install.ts Adds --dry-run option, config support, and conflict check with --resolution-only.
config/reader/src/Config.ts Promotes dryRun to a supported config option (removes “might not be supported” comment).
.changeset/dry-run-install.md Changeset documenting the new --dry-run behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pnpm/dev/pnpm-workspace.yaml Outdated
Comment thread installing/deps-restorer/src/index.ts Outdated
Comment thread installing/deps-restorer/src/index.ts Outdated
Comment thread installing/deps-installer/test/install/dryRun.ts Outdated
Comment thread pacquet/crates/cli/src/cli_args/install.rs Outdated
Comment thread installing/commands/src/install.ts
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

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

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

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

Copilot AI review requested due to automatic review settings June 8, 2026 16:02
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 66d4a1b

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (2)

installing/deps-restorer/src/index.ts:1

  • A normalized boolean headlessOpts.dryRun is computed here, but later code forwards opts.dryRun directly. To keep behavior consistent and avoid accidental undefined propagation, prefer using the normalized value (or Boolean(opts.dryRun)) everywhere you pass dryRun downstream.
import { promises as fs } from 'node:fs'

installing/deps-restorer/src/index.ts:1

  • This changes dryRun from an explicit boolean (false) to potentially undefined. If downstream logic distinguishes undefined from false (or uses strict boolean checks), this can cause unintended behavior. Pass a definite boolean (e.g. dryRun: opts.dryRun === true or reuse the earlier normalized value) to preserve the prior semantics.
import { promises as fs } from 'node:fs'

Comment thread installing/deps-restorer/src/index.ts Outdated
Comment thread pacquet/crates/cli/tests/dry_run.rs Outdated
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

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

Comment thread pacquet/crates/cli/src/cli_args/install.rs
Copilot AI review requested due to automatic review settings June 8, 2026 16:29
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 640b3b0

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

installing/deps-restorer/src/index.ts:1

  • opts.dryRun is optional, but this call now forwards it as-is. If prune expects a strict boolean (the previous code passed false), this can become a type error or change runtime behavior. Prefer normalizing to a boolean here (e.g., Boolean(opts.dryRun)) to keep the contract consistent.
import { promises as fs } from 'node:fs'

Comment thread installing/deps-restorer/src/index.ts Outdated
Comment thread installing/deps-installer/src/install/index.ts
Comment thread installing/deps-installer/src/install/index.ts
@github-actions

github-actions Bot commented Jun 8, 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 9.945 ± 0.117 9.811 10.181 1.86 ± 0.03
pacquet@main 9.865 ± 0.051 9.800 9.957 1.85 ± 0.02
pnpr@HEAD 5.411 ± 0.121 5.305 5.698 1.01 ± 0.02
pnpr@main 5.345 ± 0.049 5.280 5.465 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 9.945341445919999,
      "stddev": 0.11741516209588951,
      "median": 9.913862849520001,
      "user": 3.1136955800000004,
      "system": 3.37122912,
      "min": 9.81106969902,
      "max": 10.18063951202,
      "times": [
        9.94507818902,
        10.03599916002,
        9.88763056202,
        9.87890624902,
        9.93796182502,
        9.88976387402,
        10.06881526002,
        9.81755012902,
        9.81106969902,
        10.18063951202
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 9.86520509862,
      "stddev": 0.050838178806918946,
      "median": 9.853005370520002,
      "user": 2.9709813800000004,
      "system": 3.3080621199999998,
      "min": 9.79970765502,
      "max": 9.95676016202,
      "times": [
        9.83200978402,
        9.85443987802,
        9.84532888202,
        9.95676016202,
        9.82150184602,
        9.87957272402,
        9.851570863020001,
        9.79970765502,
        9.94688976902,
        9.86426942302
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 5.410972537520001,
      "stddev": 0.12114923368395995,
      "median": 5.36463355902,
      "user": 2.47690148,
      "system": 2.978618419999999,
      "min": 5.30487737202,
      "max": 5.6982544840200005,
      "times": [
        5.33554118602,
        5.42076472002,
        5.6982544840200005,
        5.54292100502,
        5.36269410302,
        5.34832399702,
        5.30487737202,
        5.40119460802,
        5.36657301502,
        5.32858088502
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 5.345286166519999,
      "stddev": 0.04883508237456016,
      "median": 5.344445561020001,
      "user": 2.45264788,
      "system": 2.9708365199999998,
      "min": 5.27953799802,
      "max": 5.46527800102,
      "times": [
        5.31638297902,
        5.31680785502,
        5.27953799802,
        5.34925765002,
        5.36750107302,
        5.3414353210200005,
        5.46527800102,
        5.34762985202,
        5.34745580102,
        5.32157513502
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 641.9 ± 17.4 618.8 675.7 1.00
pacquet@main 688.5 ± 103.7 636.4 979.3 1.07 ± 0.16
pnpr@HEAD 786.6 ± 57.5 727.8 921.5 1.23 ± 0.10
pnpr@main 758.3 ± 47.6 720.8 885.7 1.18 ± 0.08
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6419319654800001,
      "stddev": 0.01739310893059641,
      "median": 0.63967202508,
      "user": 0.36381927999999997,
      "system": 1.3260347999999997,
      "min": 0.6187823315800001,
      "max": 0.6757170515800001,
      "times": [
        0.6757170515800001,
        0.62807579958,
        0.64210065058,
        0.62942566858,
        0.63011981258,
        0.6450421375800001,
        0.66371098658,
        0.6187823315800001,
        0.64910181658,
        0.63724339958
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6884931805800001,
      "stddev": 0.10373359859476472,
      "median": 0.64816161508,
      "user": 0.3653211799999999,
      "system": 1.3467700999999999,
      "min": 0.6364152655800001,
      "max": 0.9792931715800001,
      "times": [
        0.66538649258,
        0.6364152655800001,
        0.64704085658,
        0.6468855625800001,
        0.64201925058,
        0.9792931715800001,
        0.67689954358,
        0.64842671158,
        0.6478965185800001,
        0.69466843258
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.78657758218,
      "stddev": 0.057485964018396674,
      "median": 0.7665493160800001,
      "user": 0.38990718,
      "system": 1.3392351999999998,
      "min": 0.72781495858,
      "max": 0.9215407935800001,
      "times": [
        0.7661770545800001,
        0.8032284335800001,
        0.7874759625800001,
        0.76557107558,
        0.75535868358,
        0.9215407935800001,
        0.83864535458,
        0.7330419275800001,
        0.72781495858,
        0.76692157758
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7582633080800001,
      "stddev": 0.047599493936848446,
      "median": 0.74171553958,
      "user": 0.38762198000000003,
      "system": 1.3279283,
      "min": 0.7208355725800001,
      "max": 0.88566175558,
      "times": [
        0.74142858858,
        0.74200249058,
        0.74469011858,
        0.73198412158,
        0.7322611705800001,
        0.88566175558,
        0.74115164158,
        0.7750682625800001,
        0.7208355725800001,
        0.7675493585800001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 9.204 ± 0.038 9.149 9.271 1.78 ± 0.04
pacquet@main 9.190 ± 0.035 9.140 9.256 1.78 ± 0.04
pnpr@HEAD 5.232 ± 0.207 5.036 5.571 1.01 ± 0.05
pnpr@main 5.167 ± 0.121 5.063 5.465 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 9.20415763596,
      "stddev": 0.037766784126559136,
      "median": 9.204446440759998,
      "user": 3.5303544799999997,
      "system": 3.28898342,
      "min": 9.14876748626,
      "max": 9.27083942726,
      "times": [
        9.18472146126,
        9.153134120259999,
        9.24548242326,
        9.21175093226,
        9.14876748626,
        9.21255618726,
        9.19415492326,
        9.27083942726,
        9.197141949259999,
        9.22302744926
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 9.18962239736,
      "stddev": 0.03456067828380239,
      "median": 9.17662721526,
      "user": 3.51406978,
      "system": 3.28609462,
      "min": 9.13960809326,
      "max": 9.25609583526,
      "times": [
        9.17172158426,
        9.17437157726,
        9.17888285326,
        9.216207643259999,
        9.25609583526,
        9.13960809326,
        9.21227021526,
        9.21675466726,
        9.16311138326,
        9.16720012126
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 5.231956451159999,
      "stddev": 0.2070765134911823,
      "median": 5.15669674576,
      "user": 2.3122893799999997,
      "system": 2.8544574199999997,
      "min": 5.03632365226,
      "max": 5.57137615126,
      "times": [
        5.21498150126,
        5.49621880926,
        5.47588039226,
        5.57137615126,
        5.06480151426,
        5.23440696226,
        5.09841199026,
        5.05043609926,
        5.03632365226,
        5.07672743926
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 5.16660529186,
      "stddev": 0.1209047645051234,
      "median": 5.124124655259999,
      "user": 2.28854868,
      "system": 2.86555462,
      "min": 5.063190350259999,
      "max": 5.46523642726,
      "times": [
        5.46523642726,
        5.10109964426,
        5.088263641259999,
        5.094539794259999,
        5.23913551326,
        5.14714966626,
        5.08734399726,
        5.063190350259999,
        5.151033583259999,
        5.22906030126
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.403 ± 0.024 1.370 1.445 2.05 ± 0.06
pacquet@main 1.399 ± 0.033 1.362 1.474 2.05 ± 0.07
pnpr@HEAD 0.684 ± 0.016 0.671 0.722 1.00
pnpr@main 0.685 ± 0.021 0.653 0.720 1.00 ± 0.04
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.4032059216000001,
      "stddev": 0.024002108173536655,
      "median": 1.4012061261,
      "user": 1.5673464200000002,
      "system": 1.78311664,
      "min": 1.3702187206,
      "max": 1.4448521996,
      "times": [
        1.3741414026,
        1.4009046646,
        1.3995860146,
        1.4037738306,
        1.4054467756,
        1.3702187206,
        1.3915958246,
        1.4448521996,
        1.4015075876,
        1.4400321956000002
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.3993016033999999,
      "stddev": 0.033035516997678364,
      "median": 1.3993141636000002,
      "user": 1.54927902,
      "system": 1.8146001400000003,
      "min": 1.3616879066,
      "max": 1.4738539806,
      "times": [
        1.3916789616,
        1.3616879066,
        1.3667364246,
        1.3663378516,
        1.4030066266,
        1.4218609946,
        1.4738539806,
        1.3956217006,
        1.4088539916,
        1.4033775956
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6841485704,
      "stddev": 0.01598001613839885,
      "median": 0.6782918246,
      "user": 0.34513541999999997,
      "system": 1.2969448399999999,
      "min": 0.6706247286000001,
      "max": 0.7224148196000001,
      "times": [
        0.7224148196000001,
        0.6833339536,
        0.6764907886,
        0.6972709456,
        0.6912033236,
        0.6800928606000001,
        0.6718807196000001,
        0.6706247286000001,
        0.6745042196000001,
        0.6736693446
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6853422984,
      "stddev": 0.021340555555859196,
      "median": 0.6815400271000001,
      "user": 0.33500571999999995,
      "system": 1.28146904,
      "min": 0.6525011116,
      "max": 0.7197533746000001,
      "times": [
        0.6950652246000001,
        0.6843178316,
        0.6667558166,
        0.7149720956000001,
        0.6787622226000001,
        0.6764339866000001,
        0.6525011116,
        0.7197533746000001,
        0.6961698446000001,
        0.6686914756000001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.980 ± 0.034 4.941 5.036 7.39 ± 0.25
pacquet@main 4.977 ± 0.038 4.918 5.047 7.39 ± 0.25
pnpr@HEAD 0.674 ± 0.023 0.657 0.734 1.00
pnpr@main 0.718 ± 0.077 0.672 0.928 1.07 ± 0.12
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.979749742659999,
      "stddev": 0.03390081018633877,
      "median": 4.9636405106599994,
      "user": 1.7278894399999998,
      "system": 1.93752754,
      "min": 4.940607721659999,
      "max": 5.03561122766,
      "times": [
        4.940607721659999,
        4.96103010766,
        4.9554962506599995,
        4.96625091366,
        4.960428032659999,
        5.00792859366,
        4.94968760866,
        5.02812947066,
        5.03561122766,
        4.99232749966
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.97688632666,
      "stddev": 0.038478475824973525,
      "median": 4.97453969566,
      "user": 1.72373904,
      "system": 1.9265377399999999,
      "min": 4.91818239666,
      "max": 5.04659271266,
      "times": [
        5.04659271266,
        4.9687435966599995,
        4.91818239666,
        4.9241789236599995,
        4.97175394866,
        4.99812378566,
        4.98412812566,
        4.964361161659999,
        4.97732544266,
        5.01547317266
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6736641353599999,
      "stddev": 0.02260708072467761,
      "median": 0.66708160566,
      "user": 0.32219993999999996,
      "system": 1.2864787399999997,
      "min": 0.65690852666,
      "max": 0.73360288566,
      "times": [
        0.73360288566,
        0.66658003666,
        0.6677342026599999,
        0.65690852666,
        0.68648111266,
        0.66365989166,
        0.66758317466,
        0.66351046466,
        0.65835155766,
        0.67222950066
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7183136438600001,
      "stddev": 0.07713986928706175,
      "median": 0.68968651066,
      "user": 0.33201844,
      "system": 1.3020250399999997,
      "min": 0.67215782266,
      "max": 0.92793518166,
      "times": [
        0.75300126266,
        0.67917044166,
        0.92793518166,
        0.71038567566,
        0.68303440266,
        0.69543094466,
        0.67215782266,
        0.69140162566,
        0.68797139566,
        0.68264768566
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12270
Testbedpacquet

🚨 2 Alerts

BenchmarkMeasure
Units
ViewBenchmark Result
(Result Δ%)
Upper Boundary
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-storeLatency
seconds (s)
📈 plot
🚷 threshold
🚨 alert (🔔)
9.20 s
(+101.92%)Baseline: 4.56 s
5.47 s
(168.27%)

isolated-linker.fresh-restore.cold-cache.cold-storeLatency
seconds (s)
📈 plot
🚷 threshold
🚨 alert (🔔)
9.95 s
(+36.57%)Baseline: 7.28 s
8.74 s
(113.81%)

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
🚨 view alert (🔔)
9,204.16 ms
(+101.92%)Baseline: 4,558.35 ms
5,470.02 ms
(168.27%)

isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
4,979.75 ms
(-0.44%)Baseline: 5,001.90 ms
6,002.27 ms
(82.96%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,403.21 ms
(+3.78%)Baseline: 1,352.09 ms
1,622.50 ms
(86.48%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
🚨 view alert (🔔)
9,945.34 ms
(+36.57%)Baseline: 7,282.35 ms
8,738.82 ms
(113.81%)

isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
641.93 ms
(-2.45%)Baseline: 658.02 ms
789.63 ms
(81.30%)
🐰 View full continuous benchmarking report in Bencher

Add a --dry-run flag to the install command that runs dependency
resolution and validates the lockfile without writing any changes to
disk. This enables use in pre-commit hooks to catch outdated lockfiles
and other common install errors.
@zkochan zkochan force-pushed the feat/dry-run-install branch from 640b3b0 to d836086 Compare June 16, 2026 08:26
@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 d836086

Comment thread installing/commands/src/install.ts
@codecov-commenter

codecov-commenter commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.01%. Comparing base (d36b6f8) to head (1de255d).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #12270   +/-   ##
=======================================
  Coverage   88.00%   88.01%           
=======================================
  Files         308      308           
  Lines       41395    41401    +6     
=======================================
+ Hits        36431    36438    +7     
+ Misses       4964     4963    -1     

☔ 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.

… setups

Address review feedback on the --dry-run option:

- pacquet: skip persisting pnpm-lock.yaml in the frozen + lockfile-only
  branch (gate save_to_path on !frozen_lockfile), mirroring pnpm's
  !frozenLockfile gate, so a fresh dry run no longer touches the lockfile.
- pacquet: bypass the optimistic up-to-date fast path for --dry-run so it
  takes the same full validation path as --frozen-lockfile --lockfile-only.
- Reject --dry-run together with a configured pnpr server on both stacks:
  the pnpr path resolves through the server and writes the lockfile and
  store, which can't honor dry-run's no-write contract.
- pnpm: remove the unused, misleading HeadlessOptions.dryRun plumbing
  (headlessInstall is never reached by --dry-run and still wrote to disk).
- Document the lockfile-only frozen write suppression and add no-rewrite
  and conflict tests on both stacks.

---
Written by an agent (Claude Code, claude-opus-4-8).
Copilot AI review requested due to automatic review settings June 16, 2026 08:43
@zkochan zkochan force-pushed the feat/dry-run-install branch from d836086 to 1de255d Compare June 16, 2026 08:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@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 1de255d

Comment thread installing/commands/src/install.ts
@zkochan

zkochan commented Jun 16, 2026

Copy link
Copy Markdown
Member

No, this is not correct behavior for pnpm install --dry-run. pnpm install --dry-run should be just like regular install without changing node_modules and the lockfile.

@zkochan zkochan closed this Jun 16, 2026
zkochan added a commit that referenced this pull request Jun 16, 2026
## 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.
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

4 participants