Skip to content

feat(cli): port the bin command to pacquet#12703

Merged
zkochan merged 4 commits into
pnpm:mainfrom
exidniy:feat/pacquet-bin
Jun 29, 2026
Merged

feat(cli): port the bin command to pacquet#12703
zkochan merged 4 commits into
pnpm:mainfrom
exidniy:feat/pacquet-bin

Conversation

@exidniy

@exidniy exidniy commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports the bin command from the TypeScript pnpm CLI to the Rust pacquet port, mirroring pnpm11/pnpm/src/cmd/bin.ts and the bin-dir resolution in pnpm11/config/reader/src/index.ts.

pacquet bin prints the directory where executables are installed:

  • locally, <dir>/node_modules/.bin — the node_modules/.bin leaf is hardcoded (a configured modules-dir is ignored) and the path is anchored on the already-canonicalized --dir, matching pnpm's config.dir (the cwd, not the workspace root). Includes a differential test against the real pnpm from a workspace subdirectory.
  • with --global / -g, the resolved global bin directory (global-bin-dir ?? <pnpm-home>/bin). Mirroring pnpm's config reader, it creates the directory and validates it with checkGlobalBinDir (PATH membership + writability, since globalDirShouldAllowWrite is true for every command except root) before printing, erroring with ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH / ERR_PNPM_NO_PATH_ENV / ERR_PNPM_PNPM_DIR_NOT_WRITABLE, or ERR_PNPM_NO_GLOBAL_BIN_DIR when no pnpm home resolves.

Related to #11633.

Squash Commit Body

Port pnpm's `bin` command, which prints the directory where executables are
installed.

Locally it prints `<dir>/node_modules/.bin`: the `node_modules/.bin` leaf is
hardcoded, so a configured `modules-dir` is ignored, and the path is anchored
on the already-canonicalized `--dir`, mirroring pnpm's config reader.

`--global` resolves `global-bin-dir ?? <pnpm-home>/bin`, creates it, and runs
`check_global_bin_dir` (PATH membership + writability, since
`globalDirShouldAllowWrite` is true for every command except `root`) before
printing — the same `mkdir` + `checkGlobalBinDir` pnpm's config reader performs
for every `--global` command. Errors mirror pnpm:
`ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH`, `ERR_PNPM_NO_PATH_ENV`,
`ERR_PNPM_PNPM_DIR_NOT_WRITABLE`, and `ERR_PNPM_NO_GLOBAL_BIN_DIR`.

Ported from pnpm's bin.ts and config reader.

Related to pnpm/pnpm#11633.

Checklist

  • The change is implemented in both the TypeScript CLI and the Rust pacquet/ port, or the description notes what still needs porting.
    • pnpm already has bin; this ports it to pacquet. No TypeScript changes needed.
  • Added a changeset (pnpm changeset) if this PR changes any published package. Keep it short and written for pnpm users — it becomes a release note.
    • Not needed; pacquet is not published to npm.
  • Added or updated tests.
    • 5 integration tests: local node_modules/.bin, ignores a custom modules-dir, -g prints the validated global bin dir, -g errors when not on PATH, and a differential test against the real pnpm from a workspace subdir. The two -g tests are Unix-gated (like global.rs) because the PATH validation is platform-specific.
  • Updated the documentation if needed.
    • None (consistent with sibling command ports).

Summary by CodeRabbit

  • New Features
    • Added a bin CLI subcommand that prints the resolved node_modules/.bin (local) or global executables directory.
    • Added --global / -g to output the global executables directory and create it if needed.
  • Bug Fixes
    • Improved resolution and validation of the global executables directory against the current PATH, with clearer failure behavior when not reachable.
  • Tests
    • Added integration tests for local, workspace, and global modes (including exact stdout matching, directory creation, and parity with the external pnpm bin).

Port pnpm's `bin` command, which prints the directory where executables
are installed. The local invocation prints `<dir>/node_modules/.bin`: the
`node_modules/.bin` leaf is hardcoded, so a configured `modules-dir` is
ignored, and the path is anchored on the already-canonicalized `--dir`,
mirroring pnpm's config reader. `--global` prints the resolved global bin
directory (`global-bin-dir ?? <pnpm-home>/bin`) and reuses the existing
`ERR_PNPM_NO_GLOBAL_BIN_DIR` diagnostic when it cannot be resolved.

Unlike pnpm, the read-only `--global` path does not run `checkGlobalBinDir`
(its PATH/writability validation) or create the directory. pacquet limits
that to its mutating global handlers, matching the sibling `outdated
--global`; porting the validation to the config layer for all read-only
`-g` commands is left as a follow-up.

Ported from pnpm's bin.ts and config reader.
@exidniy exidniy requested a review from zkochan as a code owner June 28, 2026 12:01
@welcome

welcome Bot commented Jun 28, 2026

Copy link
Copy Markdown

💖 Thanks for opening this pull request! 💖
Please be patient and we will get back to you as soon as we can.

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 41ab4b54-6ed6-4c98-92f5-d690c3e4492e

📥 Commits

Reviewing files that changed from the base of the PR and between dd3bb4c and 681d93b.

📒 Files selected for processing (2)
  • pacquet/crates/cli/src/cli_args/bin.rs
  • pacquet/crates/cli/tests/bin.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • pacquet/crates/cli/src/cli_args/bin.rs
  • pacquet/crates/cli/tests/bin.rs

📝 Walkthrough

Walkthrough

Adds a new pacquet bin CLI subcommand that prints the local node_modules/.bin directory or, with --global/-g, the configured global bin directory. The change includes command registration, dispatch wiring, implementation, and integration tests.

Changes

pacquet bin subcommand

Layer / File(s) Summary
BinArgs and CLI wiring
pacquet/crates/cli/src/cli_args/bin.rs, pacquet/crates/cli/src/cli_args.rs, pacquet/crates/cli/src/cli_args/cli_command.rs, pacquet/crates/cli/src/cli_args/dispatch_query.rs, pacquet/crates/cli/src/cli_args/dispatch.rs
BinArgs with --global is defined, Bin(BinArgs) is added to CliCommand, the module is exported, and route dispatches CliCommand::Bin through dispatch_query::bin to BinArgs::run.
Integration tests for bin output
pacquet/crates/cli/tests/bin.rs
Tests cover local node_modules/.bin output, modulesDir override behavior, global bin resolution and PATH validation, directory creation, and parity with pnpm bin from a workspace subdirectory.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested labels

product: pacquet

Suggested reviewers

  • zkochan
✨ 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.

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

Copy link
Copy Markdown

PR Summary by Qodo

feat(cli): add pacquet bin command with pnpm-compatible bin-dir output
✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

Description

• Add pacquet bin to print /node_modules/.bin anchored on canonicalized --dir.
• Support --global/-g by printing config.global_bin and reusing ERR_PNPM_NO_GLOBAL_BIN_DIR.
• Add integration tests, including a differential parity test against real pnpm in a workspace
 subdir.
Diagram

graph TD
  U(["User"]) --> CLI["pacquet CLI"] --> CMD["bin handler"] --> DEC{"--global?"}
  DEC -->|"no"| LOCAL["<dir>/node_modules/.bin"] --> OUT["stdout"]
  DEC -->|"yes"| GBIN["Config.global_bin"] --> OUT
  GBIN -->|"None"| ERR["ERR_PNPM_NO_GLOBAL_BIN_DIR"]

  subgraph Legend
    direction LR
    _ext(["External"]) ~~~ _cmd["Command"] ~~~ _dec{"Decision"}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Centralize local bin-dir derivation as a shared helper
  • ➕ Avoids duplicating pnpm-parity rules across commands (root and bin both hardcode node_modules leaves)
  • ➕ Easier to keep workspace/cwd anchoring behavior consistent over time
  • ➖ Introduces an abstraction that may not pay off until more commands need it
  • ➖ Risk of over-generalizing command-specific parity quirks (e.g., intentionally ignoring modules-dir)
2. Validate global bin dir for read-only `bin -g` via `check_global_bin_dir`
  • ➕ Closer behavioral parity with pnpm (PATH membership and writability validation)
  • ➕ Earlier, clearer feedback when global config is broken
  • ➖ Changes current pacquet policy of validating/creating global bin dir only in mutating global commands
  • ➖ May be undesirable for a pure query command that should not enforce PATH invariants

Recommendation: The chosen approach is solid for a read-only query command and matches existing pacquet behavior for other read-only global queries (e.g., outdated --global). Consider a follow-up to move global-bin validation into a shared config/bootstrap step if strict pnpm parity for all -g commands is desired, and optionally factor a small helper for node_modules-leaf path construction if more pnpm query ports are added.

Files changed (6) +208 / -0

Enhancement (5) +64 / -0
cli_args.rsRegister new 'bin' args module +1/-0

Register new 'bin' args module

• Adds 'pub mod bin;' so the new 'bin' subcommand args can be compiled and referenced by the CLI.

pacquet/crates/cli/src/cli_args.rs

bin.rsImplement 'pacquet bin' command (local and '--global') +53/-0

Implement 'pacquet bin' command (local and '--global')

• Introduces 'BinArgs' with '--global/-g'. Local mode prints '<canonicalized --dir>/node_modules/.bin' (deliberately ignoring 'modules-dir'); global mode prints 'config.global_bin' and errors with 'ERR_PNPM_NO_GLOBAL_BIN_DIR' when missing.

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

cli_command.rsExpose 'bin' as a top-level CLI subcommand +3/-0

Expose 'bin' as a top-level CLI subcommand

• Adds 'CliCommand::Bin(BinArgs)' and help text so 'pacquet bin' is available via clap parsing.

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

dispatch.rsRoute 'bin' command through main dispatcher +1/-0

Route 'bin' command through main dispatcher

• Extends the router match to dispatch 'CliCommand::Bin' to the query dispatch module.

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

dispatch_query.rsAdd query dispatch entry for 'bin' +6/-0

Add query dispatch entry for 'bin'

• Adds 'dispatch_query::bin()' which loads config and invokes 'BinArgs::run', returning a ready future like other synchronous query commands.

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

Tests (1) +144 / -0
bin.rsAdd integration tests for 'pacquet bin' (local, global, pnpm parity) +144/-0

Add integration tests for 'pacquet bin' (local, global, pnpm parity)

• Adds tests verifying local output (with canonicalization), that 'modulesDir' does not change output, deterministic 'bin -g' via pinned env vars, and a differential test ensuring byte-identical output to real 'pnpm bin' from a workspace subdirectory (ignored on Windows due to 'pnpm.cmd' resolution).

pacquet/crates/cli/tests/bin.rs

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

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

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (8) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Non-hermetic bin tests 🐞 Bug ☼ Reliability
Description
The new local pacquet bin integration tests spawn pacquet bin without pinning HOME/XDG env
vars, but bin currently loads full configuration, so the tests can become environment-dependent or
fail on machines with unexpected user/global pnpm config state. This makes CI/local runs potentially
flaky for reasons unrelated to the test’s intent.
Code

pacquet/crates/cli/tests/bin.rs[R19-24]

+fn bin_prints_the_local_node_modules_bin_dir() {
+    let CommandTempCwd { pacquet, root, workspace, .. } = CommandTempCwd::init();
+
+    let output = pacquet.with_args(["bin"]).output().expect("run pacquet bin");
+    dbg!(&output);
+    assert!(output.status.success(), "pacquet bin should succeed");
Evidence
The tests run pacquet bin with no environment isolation, while the bin dispatch path forces a
configuration load that reads from user/environment-derived locations, making the test outcome
depend on the runner’s HOME/XDG state.

pacquet/crates/cli/tests/bin.rs[18-24]
pacquet/crates/testing-utils/src/bin.rs[23-36]
pacquet/crates/cli/src/cli_args/dispatch_query.rs[169-172]
pacquet/crates/cli/src/cli_args/dispatch.rs[158-181]

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 local `pacquet bin` tests run the CLI without isolating user-level config locations (HOME/XDG). Because `pacquet bin` currently routes through `ctx.config()` even in local mode, these tests can depend on the developer/CI machine’s global config files and environment.
### Issue Context
- `dispatch_query::bin` calls `args.run(ctx.dir, (ctx.config)()?)`, which forces a config load.
- The config loader consults user/environment-derived config locations.
- `CommandTempCwd::init()` only sets the working directory; it doesn’t pin HOME/XDG env vars for the spawned command.
### Fix Focus Areas
- pacquet/crates/cli/tests/bin.rs[19-49]
### Suggested fix
In `bin_prints_the_local_node_modules_bin_dir` and `bin_ignores_a_custom_modules_dir`, set the same env vars you already pin in the `-g` tests (at least `HOME` and `XDG_CONFIG_HOME`; optionally also `XDG_DATA_HOME` if config lookup uses it elsewhere) to directories under `root.path()` before calling `.output()`. This makes the tests hermetic even while `pacquet bin` continues to load config.

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


2. Non-UTF8 PATH misread 🐞 Bug ≡ Correctness
Description
BinArgs::run reads PATH using std::env::var("PATH"), which returns an error for non-UTF-8
values on Unix; the code converts that to None and check_global_bin_dir then reports
ERR_PNPM_NO_PATH_ENV even though PATH is set. This can cause pacquet bin -g to fail incorrectly
(and with a misleading error) in the rare case where PATH contains non-Unicode bytes.
Code

pacquet/crates/cli/src/cli_args/bin.rs[R53-54]

+            check_global_bin_dir(&bin, std::env::var("PATH").ok().as_deref(), true)
+                .map_err(miette::Report::new)?;
Evidence
The new bin -g implementation uses std::env::var("PATH").ok() (Unicode-only), while
check_global_bin_dir treats a None/empty path_env as ERR_PNPM_NO_PATH_ENV. Elsewhere in the
CLI, PATH is handled via var_os, indicating the codebase expects to support OS-string PATH values.

pacquet/crates/cli/src/cli_args/bin.rs[49-55]
pacquet/crates/config/src/global_bin_check.rs[53-60]
pacquet/crates/cli/src/cli_args/exec.rs[242-249]
pacquet/crates/cli/src/cli_args/dlx.rs[557-564]

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 bin -g` passes `std::env::var("PATH").ok().as_deref()` into `check_global_bin_dir`. On Unix, `std::env::var` fails if PATH contains non-UTF-8 bytes, so `.ok()` turns that into `None`, and `check_global_bin_dir` emits `ERR_PNPM_NO_PATH_ENV` (misleading) and the command fails.
### Issue Context
Other CLI code paths treat PATH as an OS string (`var_os`) to avoid Unicode-only assumptions.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/bin.rs[53-54]
- pacquet/crates/config/src/global_bin_check.rs[53-60]
### Suggested fix
Prefer making `check_global_bin_dir` accept `Option<&std::ffi::OsStr>` (so it can call `std::env::split_paths` directly without lossy conversion), and update call sites (`bin`, and any other `check_global_bin_dir` callers) to use `std::env::var_os("PATH")`.
If you want a smaller/local change, you can use `std::env::var_os("PATH")` and convert with `to_string_lossy()` before passing it in, but that’s lossy; the `OsStr` signature is the more correct long-term shape.

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


3. Relative global-bin-dir writes 🐞 Bug ☼ Reliability
Description
pacquet bin -g calls create_dir_all() on config.global_bin without ensuring it is an absolute
path, so a relative global-bin-dir value will be created relative to the process CWD. Because
Config::current takes global-bin-dir verbatim from env/config into a PathBuf with no
normalization, this can lead to confusing/incorrect filesystem mutations and output when users (or
CI) provide a relative setting.
Code

pacquet/crates/cli/src/cli_args/bin.rs[R44-54]

+            let bin = config.global_bin.clone().ok_or(GlobalError::NoGlobalBinDir)?;
+            // pnpm's config reader `mkdir`s the global bin dir and runs
+            // `checkGlobalBinDir` for every `--global` command before the handler
+            // prints; `globalDirShouldAllowWrite` is true for all but `root`, so
+            // `bin` validates writability too.
+            std::fs::create_dir_all(&bin).map_err(|error| {
+                let bin_dir = bin.display();
+                miette::miette!("failed to create the global bin directory {bin_dir}: {error}")
+            })?;
+            check_global_bin_dir(&bin, std::env::var("PATH").ok().as_deref(), true)
+                .map_err(miette::Report::new)?;
Evidence
The bin -g handler unconditionally creates config.global_bin on disk, and Config::current
assigns global_bin from global_bin_dir sourced directly from env/config via PathBuf::from
without making it absolute; therefore a relative configured value can be used and will be created
relative to the current directory.

pacquet/crates/cli/src/cli_args/bin.rs[42-55]
pacquet/crates/config/src/lib.rs[2269-2289]

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 bin -g` creates the resolved global bin directory (`config.global_bin`) via `std::fs::create_dir_all(&bin)` even if `bin` is a relative path. Since `Config::current` may populate `global_bin_dir` (and thus `global_bin`) directly from `PNPM_CONFIG_GLOBAL_BIN_DIR`/`pnpm_config_global_bin_dir` without converting it to an absolute path, a relative value will create directories relative to the current working directory.
### Issue Context
This is triggered only for `--global`, but it is a user-facing command and should avoid creating directories in unexpected locations.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/bin.rs[42-55]
- pacquet/crates/config/src/lib.rs[2269-2289]
### Suggested fix
- Before `create_dir_all`, validate `bin.is_absolute()` and return a diagnostic error if not.
- Alternatively (if matching pnpm requires it), explicitly resolve relative `global-bin-dir` against a well-defined anchor (e.g., pnpm home dir) during `Config::current`, so all global commands share the same invariant.
- Add/adjust a test that sets `PNPM_CONFIG_GLOBAL_BIN_DIR` to a relative value and asserts the command errors (or resolves deterministically) rather than creating a directory under the CWD.

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


View more (3)
4. Global bin query writes 🐞 Bug ⛨ Security
Description
BinArgs::run for pacquet bin -g creates the global bin directory and then performs a writability
probe, which writes and deletes a temporary file. This makes a read-only “print a path” command
mutate the filesystem and introduces extra latency/failure modes (e.g., slow FS,
permission-restricted dirs) before printing.
Code

pacquet/crates/cli/src/cli_args/bin.rs[R43-56]

+        let bin = if self.global {
+            let bin = config.global_bin.clone().ok_or(GlobalError::NoGlobalBinDir)?;
+            // pnpm's config reader `mkdir`s the global bin dir and runs
+            // `checkGlobalBinDir` for every `--global` command before the handler
+            // prints; `globalDirShouldAllowWrite` is true for all but `root`, so
+            // `bin` validates writability too.
+            std::fs::create_dir_all(&bin).map_err(|error| {
+                miette::miette!(
+                    "failed to create the global bin directory {}: {error}",
+                    bin.display(),
+                )
+            })?;
+            check_global_bin_dir(&bin, std::env::var("PATH").ok().as_deref(), true)
+                .map_err(miette::Report::new)?;
Evidence
The new bin -g handler explicitly creates the directory and calls the global-bin validation with
should_allow_write=true, which triggers an on-disk write-probe (create+delete) inside the target
directory.

pacquet/crates/cli/src/cli_args/bin.rs[43-56]
pacquet/crates/config/src/global_bin_check.rs[112-135]

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 bin -g` currently performs filesystem writes (directory creation + writability probing) before printing the global bin directory. This is a behavior/semantics mismatch for a command that appears to be a pure query, and it can add latency or unexpected side effects.
## Issue Context
The implementation calls `std::fs::create_dir_all` and then `check_global_bin_dir(..., should_allow_write=true)`. The latter’s “writable” check uses an on-disk probe file, so even successful runs write to disk.
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/bin.rs[43-60]
- pacquet/crates/config/src/global_bin_check.rs[112-135]
## Suggested fix
- For `bin -g`, avoid mutating the filesystem:
- Remove `create_dir_all(&bin)`.
- Call `check_global_bin_dir` with `should_allow_write=false` (or skip validation entirely) if you still want to keep a PATH-membership check.
- Keep directory creation and writability validation in mutating global handlers (e.g., global add/remove/update/setup), not in a path-printing query.
- Update/adjust the integration tests accordingly (the ones currently asserting directory creation / PATH validation for `bin -g`).

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


5. Terminal escape injection 🐞 Bug ⛨ Security
Description
BinArgs::run prints a user-influenced filesystem path directly to stdout, so a path containing
control characters/ANSI escapes (from --dir/cwd or global-bin-dir/PNPM_HOME) can manipulate
terminal/CI log output. This is inconsistent with the existing sanitize helper used elsewhere to
prevent raw escape sequences reaching the terminal.
Code

pacquet/crates/cli/src/cli_args/bin.rs[R44-50]

+    pub fn run(self, dir: &Path, config: &Config) -> miette::Result<()> {
+        let bin = if self.global {
+            config.global_bin.clone().ok_or(GlobalError::NoGlobalBinDir)?
+        } else {
+            dir.join("node_modules").join(".bin")
+        };
+        println!("{}", bin.display());
Evidence
The new command prints the path verbatim, while the codebase already includes and uses a
sanitization helper to prevent control characters/escape sequences from reaching the terminal.

pacquet/crates/cli/src/cli_args/bin.rs[44-51]
pacquet/crates/cli/src/cli_args/sanitize.rs[3-17]
pacquet/crates/cli/src/cli_args/dispatch_query.rs[107-113]

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 bin` prints a path built from attacker-influenced inputs (`--dir`/cwd, env/config-derived `global-bin-dir`/`PNPM_HOME`) using `println!("{}", bin.display())`. If that path contains control characters (notably ANSI escape sequences), terminals and CI log viewers may interpret them, enabling output/log injection.
### Issue Context
The repo already has a `sanitize` helper intended to strip control characters before printing untrusted text to the terminal, and it is used for other read-only query outputs (e.g., `whoami`). `bin` should apply similar protection to its printed path.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/bin.rs[44-51]
### Suggested fix approach
- Convert the path to a string (`bin.display().to_string()`).
- Strip control characters before printing. You can:
- reuse `super::sanitize::sanitize(...)` and additionally remove `\n`/`\r` (paths should be single-line output), or
- introduce a stricter helper for path output (recommended) that removes *all* ASCII control chars including newlines/tabs, and use it here.
- Print the sanitized string.

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


6. Unneeded config load 🐞 Bug ➹ Performance
Description
dispatch_query::bin always calls (ctx.config)()?, so pacquet bin (local) unnecessarily
reads/parses repo/user config and can fail on config-load errors even though the local path only
needs ctx.dir. This adds avoidable overhead and expands the untrusted-input surface for a
read-only path query.
Code

pacquet/crates/cli/src/cli_args/dispatch_query.rs[R169-171]

+pub(super) fn bin<'a>(ctx: &RunCtx<'a>, args: BinArgs) -> miette::Result<CommandFuture<'a>> {
+    args.run(ctx.dir, (ctx.config)()?)?;
+    Ok(Box::pin(std::future::ready(Ok(()))))
Evidence
The dispatch layer unconditionally loads config, while the command implementation only consumes
config on the --global branch; the repo already documents/implements lazy config loading and
conditional config use for similar commands.

pacquet/crates/cli/src/cli_args/dispatch_query.rs[169-172]
pacquet/crates/cli/src/cli_args/bin.rs[44-49]
pacquet/crates/cli/src/cli_args/dispatch.rs[18-23]
pacquet/crates/cli/src/cli_args/dispatch_query.rs[35-52]

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 bin` currently loads configuration unconditionally via `(ctx.config)()?` in `dispatch_query::bin`, even when `--global` is not set. This defeats the intended lazy-config design and can cause `pacquet bin` to fail due to unrelated config parsing/loading errors.
### Issue Context
`BinArgs::run` only needs `Config` when `self.global` is true; otherwise it only joins `ctx.dir/node_modules/.bin`. Other commands (e.g. `outdated`) already conditionally load config only for their `--global` paths.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/dispatch_query.rs[35-52]
- pacquet/crates/cli/src/cli_args/dispatch_query.rs[169-172]
- pacquet/crates/cli/src/cli_args/bin.rs[44-52]
- pacquet/crates/cli/src/cli_args/dispatch.rs[18-23]
### Implementation sketch
- In `dispatch_query::bin`, branch on `args.global`:
- if global: load config once and print `config.global_bin`.
- else: print `ctx.dir.join("node_modules").join(".bin")` without calling `ctx.config`.
- Alternatively, split `BinArgs::run` into `run_local(dir)` and `run_global(config)` to keep logic inside `bin.rs` while preserving lazy loading.

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



Informational

7. Lost create_dir error source 🐞 Bug ◔ Observability
Description
BinArgs::run converts create_dir_all failures into a new miette::Report string, dropping the
original io::Error as a chained source and reducing diagnostic fidelity. This is inconsistent with
existing CLI code paths that preserve the underlying error via IntoDiagnostic + wrap_err, making
failures harder to debug.
Code

pacquet/crates/cli/src/cli_args/bin.rs[R30-33]

+            std::fs::create_dir_all(&bin).map_err(|error| {
+                let bin_dir = bin.display();
+                miette::miette!("failed to create the global bin directory {bin_dir}: {error}")
+            })?;
Evidence
The new bin command drops the underlying io::Error when wrapping create_dir_all, while
existing CLI code preserves sources via into_diagnostic().wrap_err(...) for similar filesystem
operations.

pacquet/crates/cli/src/cli_args/bin.rs[24-35]
pacquet/crates/cli/src/cli_args/global.rs[96-101]

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 bin -g` wraps `std::fs::create_dir_all` errors using `map_err` + `miette::miette!(...)`, which produces a new report and loses the original `io::Error` as a source in the diagnostic chain.
### Issue Context
Other CLI code (e.g. global command setup) uses `IntoDiagnostic` and `wrap_err` to keep the underlying error and add context. Matching that pattern here improves debugging without changing behavior.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/bin.rs[24-36]
### Suggested change
Replace the `map_err(|error| miette::miette!(...))` block with something that preserves the source, e.g.:

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


8. dbg! in tests 🐞 Bug ⚙ Maintainability
Description
The new integration tests unconditionally call dbg!(&output), which spams stderr on successful
runs and adds noise to CI logs.
Code

pacquet/crates/cli/tests/bin.rs[R23-25]

+    let output = pacquet.with_args(["bin"]).output().expect("run pacquet bin");
+    dbg!(&output);
+    assert!(output.status.success(), "pacquet bin should succeed");
Evidence
The new test file contains multiple unconditional dbg!(&output) calls in normal test flow.

pacquet/crates/cli/tests/bin.rs[23-26]
pacquet/crates/cli/tests/bin.rs[45-47]
pacquet/crates/cli/tests/bin.rs[68-81]

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

## Issue description
`dbg!(&output)` is left in the committed integration tests and prints on every successful test run.
### Issue Context
This is pure test-log noise; the assertions already provide failure context.
### Fix Focus Areas
- pacquet/crates/cli/tests/bin.rs[23-26]
- pacquet/crates/cli/tests/bin.rs[45-47]
- pacquet/crates/cli/tests/bin.rs[68-81]
### Fix
Remove the `dbg!` calls (or gate them behind an env var if occasional debugging is needed).

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


Grey Divider

Qodo Logo

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 0e98d96

@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

🧹 Nitpick comments (1)
pacquet/crates/cli/tests/bin.rs (1)

56-88: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the unresolved-global-bin regression.

The suite locks in the happy path for bin -g, but not the ERR_PNPM_NO_GLOBAL_BIN_DIR branch on Line 46 of pacquet/crates/cli/src/cli_args/bin.rs. One more CLI integration test that removes the home/global-bin inputs would keep that user-visible error contract from drifting. As per path instructions, “add regression/e2e coverage that proves the changed behavior.”

🤖 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/tests/bin.rs` around lines 56 - 88, Add a CLI integration
regression for the unresolved global bin case handled by BinArgs::run in the bin
command, covering the ERR_PNPM_NO_GLOBAL_BIN_DIR branch. Extend the tests in
bin.rs with a scenario that clears the global-bin inputs (for example unset
PNPM_HOME and the global-bin-dir config env vars, while keeping
HOME/XDG_CONFIG_HOME pinned) and invoke pacquet bin -g. Assert the command fails
with the expected user-visible error so the error contract stays locked in
alongside bin_global_prints_the_configured_global_bin_dir.

Source: Path instructions

🤖 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/cli/src/cli_args/bin.rs`:
- Around line 35-46: `bin -g` is bypassing pnpm’s shared global-bin resolution
behavior and returning `config.global_bin` directly, which can change the
visible error/output for a newly ported command. Update `bin::run` to use the
same global-bin lookup/validation path as the mutating global handlers, so
`check_global_bin_dir`-equivalent logic runs before `bin -g` prints a path. Keep
the fix centered around `run` and the
`config.global_bin`/`GlobalError::NoGlobalBinDir` flow so the command matches
pnpm’s `checkGlobalBinDir` behavior.

---

Nitpick comments:
In `@pacquet/crates/cli/tests/bin.rs`:
- Around line 56-88: Add a CLI integration regression for the unresolved global
bin case handled by BinArgs::run in the bin command, covering the
ERR_PNPM_NO_GLOBAL_BIN_DIR branch. Extend the tests in bin.rs with a scenario
that clears the global-bin inputs (for example unset PNPM_HOME and the
global-bin-dir config env vars, while keeping HOME/XDG_CONFIG_HOME pinned) and
invoke pacquet bin -g. Assert the command fails with the expected user-visible
error so the error contract stays locked in alongside
bin_global_prints_the_configured_global_bin_dir.
🪄 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: d43df582-a930-4378-8c0e-8bde0102544c

📥 Commits

Reviewing files that changed from the base of the PR and between c00400d and 0e98d96.

📒 Files selected for processing (6)
  • pacquet/crates/cli/src/cli_args.rs
  • pacquet/crates/cli/src/cli_args/bin.rs
  • pacquet/crates/cli/src/cli_args/cli_command.rs
  • pacquet/crates/cli/src/cli_args/dispatch.rs
  • pacquet/crates/cli/src/cli_args/dispatch_query.rs
  • pacquet/crates/cli/tests/bin.rs

Comment thread pacquet/crates/cli/src/cli_args/bin.rs Outdated
@codecov-commenter

codecov-commenter commented Jun 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.21053% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.32%. Comparing base (c00400d) to head (681d93b).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/cli/src/cli_args/bin.rs 78.57% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12703      +/-   ##
==========================================
+ Coverage   85.25%   85.32%   +0.06%     
==========================================
  Files         396      405       +9     
  Lines       60060    61623    +1563     
==========================================
+ Hits        51207    52577    +1370     
- Misses       8853     9046     +193     

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

Match pnpm's config reader for `--global`: create the global bin directory
and run `check_global_bin_dir` (PATH membership + writability, since
`globalDirShouldAllowWrite` is true for every command except `root`) before
printing, instead of printing `config.global_bin` unvalidated. `bin -g` now
errors with `ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH` / `ERR_PNPM_NO_PATH_ENV` /
`ERR_PNPM_PNPM_DIR_NOT_WRITABLE` exactly as pnpm does, which also removes the
read-only `--global` divergence from the original port.

Add Unix-gated tests for the on-PATH success and not-in-PATH failure paths.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

@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

🧹 Nitpick comments (1)
pacquet/crates/cli/src/cli_args/bin.rs (1)

55-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Thread PATH in; don't read it directly here.

This env-sensitive branch reaches into std::env instead of taking PATH from the Sys/Host seam, which is the pacquet convention for process-state dependent logic. Passing it in keeps this command testable the same way as the rest of the CLI plumbing. As per path instructions, "library code should generally not call std::env directly—thread it through parameters or the DI seam (important given bin -g depends on env like PATH/home/pnpm-home)."

🤖 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/bin.rs` at line 55, The `bin` CLI path
currently reads `PATH` directly via `std::env` inside the `check_global_bin_dir`
call, which bypasses the `Sys`/`Host` seam. Update the `bin` command wiring in
`cli_args/bin.rs` to accept `PATH` from the existing process-state dependency
injection layer and pass that value into `check_global_bin_dir` instead of
calling `std::env::var` there. Keep the fix localized to the `bin` argument
handling and the `check_global_bin_dir` invocation so the command remains
testable and consistent with the rest of the CLI plumbing.

Source: Path instructions

🤖 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/cli/src/cli_args/bin.rs`:
- Around line 49-54: The `create_dir_all` failure path in `bin.rs` is emitting a
generic `miette!` message instead of pnpm’s stable coded diagnostic. Update the
error mapping in the `bin -g` setup path to return the same pnpm-style error
code/message for unwritable home/bin directories, using the existing
`create_dir_all` branch and `bin` handling as the entry point. Ensure the
user-facing diagnostic matches pnpm exactly rather than a raw local message.

---

Nitpick comments:
In `@pacquet/crates/cli/src/cli_args/bin.rs`:
- Line 55: The `bin` CLI path currently reads `PATH` directly via `std::env`
inside the `check_global_bin_dir` call, which bypasses the `Sys`/`Host` seam.
Update the `bin` command wiring in `cli_args/bin.rs` to accept `PATH` from the
existing process-state dependency injection layer and pass that value into
`check_global_bin_dir` instead of calling `std::env::var` there. Keep the fix
localized to the `bin` argument handling and the `check_global_bin_dir`
invocation so the command remains testable and consistent with the rest of the
CLI plumbing.
🪄 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: 66876a4d-5fbb-4f18-a379-fd9ec7616888

📥 Commits

Reviewing files that changed from the base of the PR and between 0e98d96 and e5d6222.

📒 Files selected for processing (2)
  • pacquet/crates/cli/src/cli_args/bin.rs
  • pacquet/crates/cli/tests/bin.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • pacquet/crates/cli/tests/bin.rs

Comment thread pacquet/crates/cli/src/cli_args/bin.rs
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Commit: 681d93bffbb0

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.624 ± 0.163 4.457 4.959 1.51 ± 0.08
pacquet@main 4.632 ± 0.127 4.456 4.866 1.52 ± 0.07
pnpr@HEAD 3.116 ± 0.171 2.929 3.377 1.02 ± 0.07
pnpr@main 3.054 ± 0.118 2.873 3.236 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.6244192260000005,
      "stddev": 0.1632418997900026,
      "median": 4.5817925399,
      "user": 3.8496015999999997,
      "system": 3.37614758,
      "min": 4.457340080900001,
      "max": 4.9589745579,
      "times": [
        4.9589745579,
        4.4598500619,
        4.684537710900001,
        4.775089745900001,
        4.565753043900001,
        4.597832035900001,
        4.5349610569,
        4.457340080900001,
        4.475160955900001,
        4.734693009900001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.6324662349,
      "stddev": 0.12668596816913794,
      "median": 4.5798370059,
      "user": 3.9063811999999993,
      "system": 3.4183351799999997,
      "min": 4.455753035900001,
      "max": 4.866166138900001,
      "times": [
        4.803392107900001,
        4.866166138900001,
        4.5580820709,
        4.642798267900001,
        4.5731351979,
        4.455753035900001,
        4.5726219709,
        4.586538813900001,
        4.5478412739000005,
        4.7183334709
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 3.1159693474999997,
      "stddev": 0.1711368244495741,
      "median": 3.0786588644,
      "user": 2.8331536999999996,
      "system": 3.02630778,
      "min": 2.9293841909,
      "max": 3.3766722709,
      "times": [
        3.2475598669,
        3.2864188819,
        2.9389345529,
        3.2815632349,
        2.9609959859,
        3.0057907769,
        3.3766722709,
        3.1515269518999998,
        2.9808467619,
        2.9293841909
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 3.0542685628,
      "stddev": 0.11812687325157778,
      "median": 3.0837257819,
      "user": 2.8162016999999997,
      "system": 3.04804668,
      "min": 2.8733198879,
      "max": 3.2359434489,
      "times": [
        3.1086142289,
        3.1430870549,
        3.0302131419,
        2.8733198879,
        3.1474924799,
        3.0823682199,
        3.0850833439,
        2.9406651419,
        2.8958986799,
        3.2359434489
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 659.5 ± 9.3 643.5 673.4 1.00
pacquet@main 706.1 ± 49.4 669.7 840.3 1.07 ± 0.08
pnpr@HEAD 716.0 ± 34.2 686.8 805.7 1.09 ± 0.05
pnpr@main 699.5 ± 30.1 666.3 745.0 1.06 ± 0.05
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6594830742000001,
      "stddev": 0.009348632548609247,
      "median": 0.6599387217000001,
      "user": 0.41397456000000005,
      "system": 1.3339206,
      "min": 0.6434793372000001,
      "max": 0.6734277422,
      "times": [
        0.6734277422,
        0.6700343622,
        0.6571484082000001,
        0.6646401712000001,
        0.6583100592000001,
        0.6642442552000001,
        0.6541480422,
        0.6478309802000001,
        0.6615673842,
        0.6434793372000001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.7061202181,
      "stddev": 0.049375509358668114,
      "median": 0.6982585162000001,
      "user": 0.41113685999999994,
      "system": 1.3594964999999999,
      "min": 0.6696645012000001,
      "max": 0.8402836932000001,
      "times": [
        0.6775783282000001,
        0.7039033602000001,
        0.6717811672,
        0.6984842332000001,
        0.7001027202000001,
        0.7147372642,
        0.8402836932000001,
        0.6866341142000001,
        0.6980327992,
        0.6696645012000001
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.7160159576,
      "stddev": 0.03420449789973863,
      "median": 0.7129914132000001,
      "user": 0.41961116,
      "system": 1.3508265000000002,
      "min": 0.6867644012,
      "max": 0.8057103912000001,
      "times": [
        0.7201119182000001,
        0.6877873922000001,
        0.7117217332000001,
        0.6867644012,
        0.7194624092,
        0.7175821742,
        0.7072441892000001,
        0.6895138742000001,
        0.8057103912000001,
        0.7142610932000001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6994623231,
      "stddev": 0.030095808083333807,
      "median": 0.6944825142000001,
      "user": 0.39621686,
      "system": 1.3376103,
      "min": 0.6662602802,
      "max": 0.7450293142000001,
      "times": [
        0.7282141432000001,
        0.6698606872,
        0.7101974762000001,
        0.6662602802,
        0.6920487002000001,
        0.6744277862000001,
        0.7450293142000001,
        0.6969163282,
        0.7403708332000001,
        0.6712976822000001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.868 ± 0.039 4.801 4.931 1.73 ± 0.06
pacquet@main 4.812 ± 0.040 4.751 4.864 1.71 ± 0.06
pnpr@HEAD 2.820 ± 0.094 2.674 3.030 1.00
pnpr@main 2.860 ± 0.141 2.676 3.046 1.01 ± 0.06
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.8678361186800005,
      "stddev": 0.038981599158947874,
      "median": 4.86980388848,
      "user": 3.991254,
      "system": 3.3749446,
      "min": 4.801036309480001,
      "max": 4.9307410034800006,
      "times": [
        4.9307410034800006,
        4.87748042848,
        4.882888883480001,
        4.86431572648,
        4.87088360448,
        4.801036309480001,
        4.9128102374800005,
        4.816012676480001,
        4.853468144480001,
        4.86872417248
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.81237314128,
      "stddev": 0.040051240960100945,
      "median": 4.815659327480001,
      "user": 3.9598172,
      "system": 3.3442836999999996,
      "min": 4.75137314048,
      "max": 4.863751496480001,
      "times": [
        4.7763516074800005,
        4.807301739480001,
        4.863751496480001,
        4.756806222480001,
        4.860245513480001,
        4.75137314048,
        4.83813912348,
        4.83844391448,
        4.813622657480001,
        4.81769599748
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.82020939848,
      "stddev": 0.09372181899393389,
      "median": 2.80760268698,
      "user": 2.6320497000000005,
      "system": 2.8592299999999997,
      "min": 2.67416878548,
      "max": 3.0295678054799997,
      "times": [
        2.82060684948,
        2.77766458448,
        2.80992044948,
        2.77001555448,
        2.8280400814799997,
        3.0295678054799997,
        2.9070476944799997,
        2.67416878548,
        2.80528492448,
        2.77977725548
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.8596629254799995,
      "stddev": 0.14087319625459097,
      "median": 2.81528348948,
      "user": 2.6345053999999997,
      "system": 2.8003747999999997,
      "min": 2.67586839848,
      "max": 3.04558626148,
      "times": [
        2.83248030448,
        3.04558626148,
        3.03723277248,
        2.79808667448,
        2.7172693994799997,
        2.73011181248,
        2.79364098948,
        3.0168542124799997,
        2.9494984294799997,
        2.67586839848
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.389 ± 0.007 1.379 1.400 2.09 ± 0.03
pacquet@main 1.396 ± 0.024 1.374 1.452 2.10 ± 0.05
pnpr@HEAD 0.665 ± 0.009 0.652 0.684 1.00
pnpr@main 0.670 ± 0.011 0.651 0.688 1.01 ± 0.02
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.3887261919200002,
      "stddev": 0.007084986580673581,
      "median": 1.39066138152,
      "user": 1.36956104,
      "system": 1.7033499200000002,
      "min": 1.3792077705199999,
      "max": 1.39952779152,
      "times": [
        1.3792077705199999,
        1.38009634852,
        1.39269514152,
        1.39043038252,
        1.38186647452,
        1.38400278652,
        1.39708296852,
        1.39145987452,
        1.39089238052,
        1.39952779152
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.3962445322199997,
      "stddev": 0.024145397454521243,
      "median": 1.3845634705199998,
      "user": 1.34771024,
      "system": 1.72771592,
      "min": 1.37370536252,
      "max": 1.4522097385200001,
      "times": [
        1.4522097385200001,
        1.41341626052,
        1.37869423952,
        1.40314741652,
        1.41031463752,
        1.38402923552,
        1.37818107152,
        1.37370536252,
        1.38509770552,
        1.38364965452
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.66471595662,
      "stddev": 0.008513519402470441,
      "median": 0.6644030785199999,
      "user": 0.3604703399999999,
      "system": 1.28936132,
      "min": 0.65172278152,
      "max": 0.68438663752,
      "times": [
        0.66044195752,
        0.66452237052,
        0.65172278152,
        0.65850576252,
        0.66493201252,
        0.66651461452,
        0.66428378652,
        0.68438663752,
        0.66184441452,
        0.67000522852
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6698545571200001,
      "stddev": 0.010918064631357033,
      "median": 0.6686461720200001,
      "user": 0.33782063999999995,
      "system": 1.29533582,
      "min": 0.65087596852,
      "max": 0.68844141252,
      "times": [
        0.68449339252,
        0.67121396352,
        0.65087596852,
        0.66305293252,
        0.66722316952,
        0.66844252552,
        0.68844141252,
        0.66884981852,
        0.66135087952,
        0.67460150852
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.055 ± 0.039 3.016 3.144 4.50 ± 0.15
pacquet@main 3.101 ± 0.065 3.017 3.211 4.57 ± 0.17
pnpr@HEAD 0.688 ± 0.009 0.675 0.705 1.01 ± 0.03
pnpr@main 0.679 ± 0.021 0.662 0.720 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.05491679962,
      "stddev": 0.03852659336867342,
      "median": 3.04574349082,
      "user": 1.81932262,
      "system": 1.9788687599999997,
      "min": 3.01587210332,
      "max": 3.14447448632,
      "times": [
        3.03449473632,
        3.08112945032,
        3.02677149432,
        3.01807079732,
        3.01587210332,
        3.0719620293200003,
        3.04502007032,
        3.14447448632,
        3.06490591732,
        3.04646691132
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.10090176712,
      "stddev": 0.06518632868884316,
      "median": 3.08782682782,
      "user": 1.8125300199999999,
      "system": 2.0212715599999997,
      "min": 3.01717129832,
      "max": 3.21051245032,
      "times": [
        3.01717129832,
        3.06456165632,
        3.0245637863200003,
        3.08165181932,
        3.06628593832,
        3.09400183632,
        3.15126210432,
        3.10954648632,
        3.21051245032,
        3.18946029532
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6878605028200001,
      "stddev": 0.00891981197060841,
      "median": 0.6874942508200002,
      "user": 0.37372492,
      "system": 1.32468086,
      "min": 0.6754186283200001,
      "max": 0.7045826313200001,
      "times": [
        0.6971793073200001,
        0.7045826313200001,
        0.6817226483200001,
        0.6877953993200001,
        0.6754186283200001,
        0.69421252032,
        0.6846371453200001,
        0.6882868493200001,
        0.6871931023200001,
        0.6775767963200001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.67851414112,
      "stddev": 0.02117015289466461,
      "median": 0.6703563698200001,
      "user": 0.35646601999999994,
      "system": 1.29689896,
      "min": 0.66156945432,
      "max": 0.71966284832,
      "times": [
        0.66521117732,
        0.6648998183200001,
        0.66402101532,
        0.66156945432,
        0.6748348073200001,
        0.66587793232,
        0.7146254763200001,
        0.6768464643200001,
        0.71966284832,
        0.6775924173200001
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12703
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,867.84 ms
(+2.22%)Baseline: 4,762.30 ms
5,714.75 ms
(85.18%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
3,054.92 ms
(-0.06%)Baseline: 3,056.85 ms
3,668.22 ms
(83.28%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,388.73 ms
(+2.26%)Baseline: 1,358.09 ms
1,629.71 ms
(85.21%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
4,624.42 ms
(-4.53%)Baseline: 4,843.80 ms
5,812.56 ms
(79.56%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
659.48 ms
(+2.20%)Baseline: 645.27 ms
774.33 ms
(85.17%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12703
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,820.21 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
687.86 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
664.72 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
3,115.97 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
716.02 ms
🐰 View full continuous benchmarking report in Bencher

Bind the path display to a `let` before the `create_dir_all` failure
`miette!` instead of passing `bin.display()` directly. perfectionist's
`macro_argument_binding` Dylint rejects an impure expression passed straight
to a third-party macro (it exempts std `format!` / `println!`), matching the
existing pattern in `link.rs`. Dylint runs only in CI (the pre-push hook skips
it when `cargo-dylint` is absent), so this was missed locally.
@github-actions github-actions Bot added the reviewed: coderabbit CodeRabbit submitted an approving review label Jun 28, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

1 similar comment
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

@zkochan zkochan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I see too many comments in the code. I doubt this adheres our styleguides.

Cut the duplicated and what-restating comments in the `bin` command and its
tests. The local/global resolution rationale and the modules-dir-ignored note
were repeated across the struct doc, the `run` doc, and an inline comment;
keep the non-obvious why once on the struct, and one short note on why
`should_allow_write` is true. In the tests, drop comments that restate the
asserts (trailing newline, dir creation) and condense the doc comments to the
non-obvious why (modules-dir hardcoding, the Unix gate, the Windows skip).
@exidniy

exidniy commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, good call. I trimmed the comments down to the non-obvious why and removed the ones that restated the code or the test assertions, so the same rationale is no longer repeated across the struct doc, the run doc, and the inline comment. Updated in 681d93b.

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 681d93b

1 similar comment
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 681d93b

@zkochan zkochan merged commit cc04eb1 into pnpm:main Jun 29, 2026
31 checks passed
KSXGitHub pushed a commit that referenced this pull request Jun 29, 2026
Brings in the filter->recursive promotion (#12726), the `bin`
(#12703) and `repo` (#12702) commands, and a pnpr
cold-store perf fix (#12709).

Resolved one conflict in cli_args/dispatch_query.rs: the publish and bin
handlers and their imports were both added at the same spot, so both were
kept. Adapted recursive publish to #12726's new shared-function
signatures: `discover_workspace_projects` now returns `(projects,
patterns)` and `select_recursive_projects` takes an `AutoExcludeRoot`.
Publish passes `AutoExcludeRoot::Disabled` because it is not in pnpm's
run/exec/add/test root-auto-exclusion set; its private/unnamed
eligibility check drops the workspace root instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KtBQzmLLDU3RcGzzCMopPB
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

product: pacquet reviewed: coderabbit CodeRabbit submitted an approving review state: automerge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants