Skip to content

fix(pacquet): support recursive project listing#12848

Merged
zkochan merged 5 commits into
mainfrom
fix-pq12-list-depth
Jul 7, 2026
Merged

fix(pacquet): support recursive project listing#12848
zkochan merged 5 commits into
mainfrom
fix-pq12-list-depth

Conversation

@zkochan

@zkochan zkochan commented Jul 7, 2026

Copy link
Copy Markdown
Member

Summary

Pacquet now handles the recursive list shape used by the pnpm repo TS CI chunker: pn -r list --depth -1 --json.

The fix has four parts:

  • list --depth -1 accepts -1 as a separate value, matching TypeScript pnpm.
  • list/ll honor recursive dispatch and enumerate the selected workspace projects instead of listing only the current directory.
  • Tree-mode root labels sanitize package names before printing dependency-free roots. This is pacquet-side hardening to match pacquet's existing terminal-output sanitizer; the TypeScript nameAtVersion() helper still prints raw names and can be aligned separately if maintainers want that broader parity change.
  • The new pkg command from current main is included in the version-switch command-name mapping, fixing the PR merge-commit Dylint failure.

This fixes the pnpm v12 CI path in #12811 where switching the script to --depth=-1 avoided the parser error but made Windows chunks select 0 of 0 tests.

Squash Commit Body

Support the recursive project-listing path used by the pnpm repo CI test chunker.

Pacquet's `list` depth parser already treated `-1` as project-only mode, but clap rejected `--depth -1` before the parser saw it. In addition, `dispatch_query::list` ignored the recursive flag, so `pn -r list --depth=-1 --json` returned only the workspace root. The chunker then found no `.test` scripts and skipped every Windows chunk.

Allow hyphen-prefixed depth values, pass the recursive flag into `list` and `ll`, and render selected workspace projects for recursive project-only listings. This matches TypeScript pnpm for `pn -r list --depth -1 --json` and keeps filtered recursive selection on the shared workspace filtering path.

Sanitize root package names in tree-mode labels before printing dependency-free roots, matching pacquet's existing terminal-output sanitizer for other list labels. The TypeScript list helper still prints raw names and can be aligned in a separate TS changeset if maintainers want exact parity for that hardening.

Also map the current `pkg` command in the version-switch command-name helper so the merge commit remains exhaustive under Dylint.

Adds parser, integration, and renderer regressions for the CI command shape, package-param project-only parity, and root-label sanitization.

Related to pnpm/pnpm#12811.

Checklist

  • The change is implemented in both the TypeScript CLI and the Rust
    pacquet/ port, or the description notes what still needs porting.
  • Added or updated tests.

Written by an agent (Codex, GPT-5).

Summary by CodeRabbit

  • Bug Fixes
    • Fixed pacquet list/ll recursive mode so it’s properly applied to listing runs.
    • Fixed --depth parsing for negative values (including --depth -1), including with --json.
    • Corrected command-name mapping for pkg when applying switch-skipping logic.
  • New Features
    • Improved recursive workspace listing with consistent results across JSON and non-JSON output.
    • Updated local JSON rendering for listing roots and sanitized displayed names to avoid ANSI/control characters.
    • Expanded --depth parsing to accept hyphenated values.
  • Tests
    • Added unit and integration coverage for recursive --json, negative depth behavior, and ANSI-safe rendering.

Copilot AI review requested due to automatic review settings July 7, 2026 21:39

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.

@coderabbitai

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

The list CLI now supports recursive workspace listing, forwards the recursive flag through dispatch, and updates local output rendering and parsing behavior. A separate switch-version mapping now recognizes the pkg command name.

Changes

Recursive list command flow

Layer / File(s) Summary
Parse and dispatch recursive list
pacquet/crates/cli/src/cli_args/list.rs, pacquet/crates/cli/src/cli_args/dispatch_query.rs, pacquet/crates/cli/src/cli_args/tests.rs
The depth flag accepts hyphen-prefixed values, dispatch forwards the recursive flag into list execution, and parsing is covered by a test for --depth -1 with --json.
Recursive and local root selection
pacquet/crates/cli/src/cli_args/list.rs
ListArgs::run branches into recursive workspace listing or local listing, and local empty-result cases are returned as enum variants instead of being printed directly.
Local output rendering and sanitization
pacquet/crates/cli/src/cli_args/list.rs, pacquet/crates/cli/src/cli_args/list/tests.rs
Local root names are sanitized before rendering, and JSON output now serializes one root object at a time through a shared array serializer.
Workspace list integration test
pacquet/crates/cli/tests/list.rs
The CLI integration test builds a temporary workspace, runs recursive JSON listing with --depth -1, and checks the expected project names in the output.
Pkg command name mapping
pacquet/crates/cli/src/cli_args/switch_cli_version.rs
CliCommand::Pkg(_) now maps to "pkg" in the switch-version command name lookup.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • pnpm/pnpm#11959: Adds the global recursive flag plumbing that is forwarded into list execution here.
  • pnpm/pnpm#12664: Changes the shared list command dispatch signature in a similar way by threading extra arguments into ListArgs::run.
  • pnpm/pnpm#12726: Touches the same recursive-mode plumbing that this PR relies on for recursive list execution.

Suggested labels: product: pacquet

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-pq12-list-depth

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-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Pacquet CLI: allow --depth -1 for list to match pnpm/TS behavior

🐞 Bug fix 🧪 Tests 🕐 Less than 10 minutes

Grey Divider

AI Description

• Allow list --depth to accept hyphen-prefixed values like -1
• Fix pnpm v12 CI invocation pn -r list --depth -1 --json parsing in pacquet
• Add regression test ensuring --depth -1 maps to projects-only recursion
Diagram

graph TD
  A["CI/user CLI invocation"] --> B["clap arg parser"] --> C["ListArgs (--depth)"] --> D["parse_depth"] --> E["RecursionLimit"] --> F["list command execution"]
  T["cli_args/tests.rs"] --> B
Loading
High-Level Assessment

The chosen approach (setting clap allow_hyphen_values = true on --depth) is the most direct fix: it restores access to the existing parse_depth semantics without changing depth interpretation, and it aligns behavior with pnpm's TypeScript CLI. Alternatives like requiring --depth=-1 or forcing users to use -- would be less compatible with real-world CI invocations.

Files changed (2) +14 / -1

Bug fix (1) +1 / -1
list.rsAllow '--depth' to consume hyphen-prefixed values (e.g., '-1') +1/-1

Allow '--depth' to consume hyphen-prefixed values (e.g., '-1')

• Updates the clap attribute on 'ListArgs.depth' to accept hyphen-prefixed tokens as option values. This prevents clap from rejecting '--depth -1' before 'parse_depth' can map '-1' to the existing projects-only recursion mode.

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

Tests (1) +13 / -0
tests.rsAdd regression test for '-r list --depth -1 --json' parsing +13/-0

Add regression test for '-r list --depth -1 --json' parsing

• Adds a unit test asserting that recursive 'list' accepts '--depth -1' when provided as a separate token. Verifies the command parses as 'List', sets 'recursive = true', and maps depth to 'RecursionLimit::ProjectsOnly'.

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

@qodo-code-review

qodo-code-review Bot commented Jul 7, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (1) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 51 rules

Grey Divider


Action required

1. name_at_version sanitizes package names ✗ Dismissed 📘 Rule violation ≡ Correctness ⭐ New
Description
name_at_version() now runs sanitize() on the package name before rendering, which changes
user-visible list output compared to the pnpm TypeScript implementation that prints the raw name.
This introduces a pacquet-only behavior change (and a new test codifies it), risking pnpm/pacquet
parity drift without an explicit alignment plan.
Code

pacquet/crates/cli/src/cli_args/list.rs[R709-711]

+    let name = sanitize(name);
    if version.is_empty() {
-        name.to_string()
+        name.into_owned()
Evidence
The changed Rust implementation now sanitizes name before rendering (changing observable stdout),
and the newly added test asserts that ESC is removed. The pnpm TypeScript equivalent
nameAtVersion() returns the raw name when there is no version, demonstrating a behavior mismatch
between implementations.

Rule 713429: Pacquet must not introduce new user-visible behavior beyond pnpm
Rule 713428: Keep pnpm TypeScript and pacquet Rust user-visible behavior changes in sync
pacquet/crates/cli/src/cli_args/list.rs[708-714]
pacquet/crates/cli/src/cli_args/list/tests.rs[229-243]
pnpm11/deps/inspection/list/src/peerVariants.ts[3-7]

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` now sanitizes package names in `name_at_version()`, altering `list` output relative to pnpm’s TypeScript `nameAtVersion()` (which returns the raw name). This is a user-visible behavior change that must either be implemented in pnpm as well (preferred for parity) or reverted in pacquet and replaced with an agreed cross-implementation approach.

## Issue Context
A new Rust unit test asserts removal of the ESC character from the root name, which locks in this divergence.

## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/list.rs[708-714]
- pacquet/crates/cli/src/cli_args/list/tests.rs[229-243]
- pnpm11/deps/inspection/list/src/peerVariants.ts[3-7]

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


2. Recursive list ignores package filter ✗ Dismissed 🐞 Bug ≡ Correctness
Description
ListArgs::run_recursive’s --depth -1 (ProjectsOnly) branch builds roots directly from workspace
projects and never applies the positional packages filter, so pacquet -r list --depth -1 <pkg>
can emit projects even though the local path would produce “No matching packages found”. This is a
behavior regression introduced by the new recursive dispatch for list.
Code

pacquet/crates/cli/src/cli_args/list.rs[R110-122]

+    fn run_recursive(&self, config: &Config, dir: &Path) -> miette::Result<()> {
+        let workspace_root = config.workspace_dir.as_deref().unwrap_or(dir);
+        let (projects, _) = discover_workspace_projects(workspace_root)?;
+        let selection =
+            select_recursive_projects(&projects, config, dir, AutoExcludeRoot::Disabled)?;
+
+        let roots = if self.depth == RecursionLimit::ProjectsOnly {
+            selection
+                .selected
+                .values()
+                .map(|node| project_only_root(node.package.project))
+                .collect::<Vec<_>>()
+        } else {
Evidence
The new recursive ProjectsOnly branch constructs roots without consulting self.packages, while the
local path applies self.packages filtering and returns NoMatchingPackages when the dependency
tree is empty (which it always is for ProjectsOnly). That makes recursive ProjectsOnly ignore
package filters in a way local list does not.

pacquet/crates/cli/src/cli_args/list.rs[110-123]
pacquet/crates/cli/src/cli_args/list.rs[205-219]
pacquet/crates/cli/src/cli_args/list.rs[325-339]

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 -r list --depth -1 ...` currently bypasses the `packages` filtering semantics because `run_recursive`’s `ProjectsOnly` branch does not go through `local_root()` (where `self.packages` filtering and the `NoMatchingPackages` behavior are implemented).

This creates an inconsistent and misleading result vs non-recursive `list`, where `--depth -1` produces an empty dependency tree and thus yields `No matching packages found` when `packages` is non-empty.

## Issue Context
- `build_local_tree()` intentionally returns an empty tree for `RecursionLimit::ProjectsOnly`, so the local filtering logic deterministically treats any non-empty `packages` filter as “no matches”.
- The recursive ProjectsOnly path should preserve this behavior (or explicitly define a different one and test it), but today it silently lists all selected workspace projects.

## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/list.rs[110-134]
- pacquet/crates/cli/src/cli_args/list.rs[205-219]
- pacquet/crates/cli/src/cli_args/list.rs[325-339]

### Suggested implementation direction
- In `run_recursive`, if `self.depth == ProjectsOnly` and `!self.packages.is_empty()`, produce the same observable behavior as `run_local` (e.g., print `No matching packages found` and return), OR apply an equivalent filtering rule and add a regression test.
- Consider also handling the “no matches across all selected projects” case in recursive mode to avoid silent empty output when `packages` is set.

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


3. Unsanitized root label output 🐞 Bug ⛨ Security
Description
project_only_root() copies name, version, and path directly from workspace manifests/paths,
and tree-mode output prints these root fields without applying sanitize(), enabling terminal
escape/control-sequence injection when listing a malicious workspace.
This is newly reachable via recursive projects-only listing (-r list --depth -1) which prints
multiple project roots in terminal mode.
Code

pacquet/crates/cli/src/cli_args/list.rs[R297-304]

+fn project_only_root(project: &pacquet_workspace::Project) -> LocalTreeRoot {
+    let manifest = project.manifest.value();
+    LocalTreeRoot {
+        name: manifest.get("name").and_then(Value::as_str).map(str::to_string),
+        version: manifest.get("version").and_then(Value::as_str).map(str::to_string),
+        private: manifest.get("private").and_then(Value::as_bool),
+        path: project.root_dir.to_string_lossy().into_owned(),
+        dependencies: Vec::new(),
Evidence
The repo already has a sanitize() helper specifically to prevent control characters/escape
sequences from reaching the terminal. Dependencies are sanitized, but root labels in tree rendering
use raw root.name and root.path; the new project_only_root() provides additional untrusted
root values from workspace manifests and paths.

pacquet/crates/cli/src/cli_args/sanitize.rs[1-17]
pacquet/crates/cli/src/cli_args/list.rs[297-304]
pacquet/crates/cli/src/cli_args/list.rs[603-615]
pacquet/crates/cli/src/cli_args/list.rs[695-706]

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

### Issue description
Tree-mode `list` output renders root `name`/`version`/`path` without calling `sanitize()`. Because these values come from filesystem paths and `package.json`, a malicious repository can include escape/control characters that will be emitted to the user's terminal.

### Issue Context
This PR adds a new recursive projects-only path (`project_only_root`) and multi-root printing, increasing the surface area of root labels printed in tree output.

### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/list.rs[297-309]
- pacquet/crates/cli/src/cli_args/list.rs[253-259]

### Suggested approach
- Apply `sanitize()` to root label components in the tree output path (e.g., sanitize `root.name`, `root.version`, and `root.path` before passing them to `name_at_version`, `dim`, and `bold`).
- Ensure the recursive projects-only path does not bypass this sanitation (either sanitize at render time or when constructing `LocalTreeRoot` for projects-only roots).

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



Remediation recommended

4. run_local prints raw errors 📘 Rule violation ◔ Observability
Description
When list can’t find a lockfile/packages, the new run_local() path prints user-facing failure
messages via println! and still returns Ok(()), bypassing the centralized
miette/pacquet-diagnostics reporting path. This can lead to inconsistent error rendering and makes
it harder to standardize UX/error codes.
Code

pacquet/crates/cli/src/cli_args/list.rs[R136-154]

    fn run_local(&self, config: &Config, dir: &Path) -> miette::Result<()> {
+        match self.local_root(config, dir)? {
+            LocalRootResult::Root(root) => self.print_roots(&[root]),
+            LocalRootResult::NoLockfile { lockfile_dir } => {
+                if self.packages.is_empty() {
+                    println!("No lockfile found in {}", lockfile_dir.display());
+                } else {
+                    println!("No matching packages found");
+                }
+            }
+            LocalRootResult::NoPackages => {
+                if self.packages.is_empty() {
+                    println!("No packages found");
+                } else {
+                    println!("No matching packages found");
+                }
+            }
+            LocalRootResult::NoMatchingPackages => println!("No matching packages found"),
+        }
Evidence
The checklist requires user-facing errors to be routed through miette via pacquet-diagnostics. The
changed run_local() match arms print raw messages with println! for failure-like conditions
instead of returning a diagnostic error.

Rule 713453: Route user-facing errors through miette via pacquet-diagnostics
pacquet/crates/cli/src/cli_args/list.rs[136-154]

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

## Issue description
`ListArgs::run_local()` prints failure messages (`No lockfile found...`, `No packages found`, `No matching packages found`) using `println!` and then returns success. For user-facing failures, the repo standard is to route errors through miette via pacquet-diagnostics.

## Issue Context
This path is newly/explicitly structured in this PR (moved from early returns inside `local_root()` to `run_local()`), so it’s a good time to ensure the user-facing failure mode uses the centralized diagnostics mechanism.

## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/list.rs[136-155]

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


5. Recursive list reloads lockfile 🐞 Bug ➹ Performance
Description
In run_recursive, the non-ProjectsOnly path calls local_root() once per selected project, and
local_root() reloads/parses the lockfile each time, causing O(projects) redundant disk IO and
parsing for -r list.
This can significantly slow pacquet -r list on large workspaces compared to existing recursive
commands that load the lockfile once and reuse it.
Code

pacquet/crates/cli/src/cli_args/list.rs[R123-129]

+            let mut roots = Vec::new();
+            for project_dir in selection.selected.keys() {
+                if let LocalRootResult::Root(root) = self.local_root(config, project_dir)? {
+                    roots.push(root);
+                }
+            }
+            roots
Evidence
The new recursive path loops over each selected project and calls local_root(), which loads the
lockfile from disk; this repeats lockfile parsing per project. peers demonstrates the preferred
pattern: load the lockfile once, then process all projects under the recursive flag.

pacquet/crates/cli/src/cli_args/list.rs[110-130]
pacquet/crates/cli/src/cli_args/list.rs[158-171]
pacquet/crates/cli/src/cli_args/peers.rs[75-103]

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

### Issue description
`ListArgs::run_recursive` currently iterates selected projects and calls `local_root()` for each, but `local_root()` loads and parses the lockfile on every call. This creates redundant IO/CPU work proportional to the number of projects.

### Issue Context
Other recursive commands (e.g., `peers`) load the lockfile once and then iterate importers/projects, avoiding repeated lockfile parsing. `list -r` should follow the same pattern for depths other than `ProjectsOnly`.

### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/list.rs[110-135]
- pacquet/crates/cli/src/cli_args/list.rs[158-171]

### Suggested approach
- In `run_recursive`, load the lockfile once (wanted/current based on flags) and pass a reference into a helper that builds `LocalTreeRoot` for each project/importer.
- Alternatively, compute importer IDs once and build trees from the single loaded lockfile, similar to how `peers` handles `recursive`.

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


6. Depth consumes other flags 🐞 Bug ☼ Reliability
Description
ListArgs.depth now uses allow_hyphen_values = true, which makes hyphen-prefixed tokens
(including valid flags like --json or -g) eligible to be consumed as the --depth value. This
can turn inputs like list --depth --json into an invalid depth parse error instead of parsing
--json, which is a surprising CLI parsing regression for wrappers/automation that build argv
dynamically.
Code

pacquet/crates/cli/src/cli_args/list.rs[R54-55]

+    #[clap(long, default_value = "0", value_parser = parse_depth, allow_hyphen_values = true)]
    pub depth: RecursionLimit,
Evidence
The change explicitly enables hyphen-prefixed values for --depth, while the same command defines
other hyphen-prefixed flags (--json, -g). In this codebase, allow_hyphen_values is otherwise
used with trailing_var_arg passthrough args, which avoids stealing subsequent flags; --depth is
a normal option and thus is more prone to flag/value ambiguity.

pacquet/crates/cli/src/cli_args/list.rs[38-71]
pacquet/crates/cli/src/cli_args/list.rs[24-36]
pacquet/crates/cli/src/cli_args/completion.rs[8-20]
pacquet/crates/cli/src/cli_args/create.rs[14-19]

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

## Issue description
`--depth` is meant to accept `-1` as a numeric value, but `allow_hyphen_values = true` is overly permissive: it allows any hyphen-prefixed token to be treated as the value for `--depth`, including other flags like `--json` and `-g`. This can cause confusing parse failures when flags are accidentally ordered after `--depth` without a value.

## Issue Context
This PR needs `--depth -1` to work (separate token). The current implementation achieves that, but also expands what `--depth` can consume.

## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/list.rs[24-36]
- pacquet/crates/cli/src/cli_args/list.rs[38-71]

### Suggested approach
- Prefer a more targeted clap setting (if supported by the repo's clap version), such as allowing *negative numbers* specifically (rather than all hyphen-prefixed strings), so `-1` works but `--json`/`-g` are not consumed as depth values.
- Add a small parser regression test to ensure `--depth --json` does **not** silently consume `--json` as a depth value (i.e., it should still be treated as the `--json` flag and `--depth` should be reported as missing its value, if that's the desired behavior).

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



Informational

7. ANSI assertion environment-sensitive 🐞 Bug ☼ Reliability ⭐ New
Description
render_local_tree_sanitizes_root_name_without_deps asserts the entire output contains no ESC
(\u{1b}), but render_local_tree may legitimately include ANSI escapes when color output is
enabled, making the test dependent on the runtime color-detection/env. This can cause false failures
unrelated to the sanitization behavior it intends to verify.
Code

pacquet/crates/cli/src/cli_args/list/tests.rs[R240-242]

+    let output = render_local_tree(&root, false);
+    assert!(!output.contains('\u{1b}'));
+    assert!(output.contains("bad[31m-name"));
Evidence
The test bans any ESC in the rendered output, but the renderer intentionally uses
owo_colors::if_supports_color wrappers (bold/dim/gray) that can introduce ANSI escapes when
color is enabled; this makes the test assertion broader than the behavior under test (sanitizing
user-controlled name text).

pacquet/crates/cli/src/cli_args/list/tests.rs[229-243]
pacquet/crates/cli/src/cli_args/list.rs[603-651]
pacquet/crates/cli/src/cli_args/list.rs[903-917]

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 unit test `render_local_tree_sanitizes_root_name_without_deps` fails if *any* ESC (`\u{1b}`) appears in output. However, `render_local_tree` uses color helpers that may emit ANSI escapes depending on environment/color support detection, so the test can fail even if the injected ESC in the *package name* was correctly stripped.

### Issue Context
`render_local_tree` formats the label using `bold()/dim()/gray()` which call `if_supports_color(Stream::Stdout, ...)` and may add ANSI sequences when color is enabled.

### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/list/tests.rs[229-244]

### Suggested change
Update the test to assert sanitization of the injected string specifically, without globally banning `\u{1b}` in the whole output. For example:
- Assert the exact malicious substring (e.g. `"bad\u{1b}[31m-name"`) does **not** appear.
- Keep asserting the sanitized expected text (e.g. `"bad[31m-name"`) **does** appear.
- Remove (or relax) `assert!(!output.contains('\u{1b}'))` so legitimate ANSI coloring doesn’t break the test.

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


8. Recursive output double-buffers 🐞 Bug ➹ Performance
Description
print_roots builds a Vec<String> of per-root renders and then joins into another String,
increasing peak memory and allocation work for recursive outputs with many projects. Streaming
per-root output to a locked stdout (or writing incrementally) would reduce peak memory without
changing output bytes.
Code

pacquet/crates/cli/src/cli_args/list.rs[R243-259]

+            let output = roots
+                .iter()
+                .map(|root| render_local_parseable(root, self.long))
+                .filter(|output| !output.is_empty())
+                .collect::<Vec<_>>()
+                .join("\n");
+            if !output.is_empty() {
+                println!("{output}");
+            }
        } else {
-            let output = render_local_tree(&root, self.long);
+            let joiner = if self.depth == RecursionLimit::ProjectsOnly { "\n" } else { "\n\n" };
+            let output = roots
+                .iter()
+                .map(|root| render_local_tree(root, self.long))
+                .filter(|output| !output.is_empty())
+                .collect::<Vec<_>>()
+                .join(joiner);
Evidence
The new multi-root rendering path explicitly collects all per-root output strings and then joins
them, which inherently buffers the entire output at least twice (per-root strings + joined string).

pacquet/crates/cli/src/cli_args/list.rs[235-263]

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

## Issue description
Recursive `list/ll` can now print many roots. `print_roots` currently renders all roots into intermediate `String`s, collects them, then joins into one more `String` before printing. This double-buffers output and increases peak memory.

## Issue Context
This is most noticeable for large workspaces and non-JSON output modes where each root may include a sizable dependency tree.

## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/list.rs[235-263]

### Suggested implementation direction
- Replace `collect::<Vec<_>>().join(...)` with incremental writes:
 - Acquire a `std::io::StdoutLock` and `write!` each rendered root with the correct separators.
 - Alternatively, `println!` per-root with separator logic, avoiding materializing the full combined string.
- Keep existing behavior regarding:
 - Not printing anything when each root render is empty
 - The joiner choice (`\n` vs `\n\n`) for ProjectsOnly vs other depths.

ⓘ 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 bcfff0a

Results up to commit 20d922b


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


Remediation recommended
1. Depth consumes other flags 🐞 Bug ☼ Reliability
Description
ListArgs.depth now uses allow_hyphen_values = true, which makes hyphen-prefixed tokens
(including valid flags like --json or -g) eligible to be consumed as the --depth value. This
can turn inputs like list --depth --json into an invalid depth parse error instead of parsing
--json, which is a surprising CLI parsing regression for wrappers/automation that build argv
dynamically.
Code

pacquet/crates/cli/src/cli_args/list.rs[R54-55]

+    #[clap(long, default_value = "0", value_parser = parse_depth, allow_hyphen_values = true)]
    pub depth: RecursionLimit,
Evidence
The change explicitly enables hyphen-prefixed values for --depth, while the same command defines
other hyphen-prefixed flags (--json, -g). In this codebase, allow_hyphen_values is otherwise
used with trailing_var_arg passthrough args, which avoids stealing subsequent flags; --depth is
a normal option and thus is more prone to flag/value ambiguity.

pacquet/crates/cli/src/cli_args/list.rs[38-71]
pacquet/crates/cli/src/cli_args/list.rs[24-36]
pacquet/crates/cli/src/cli_args/completion.rs[8-20]
pacquet/crates/cli/src/cli_args/create.rs[14-19]

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

## Issue description
`--depth` is meant to accept `-1` as a numeric value, but `allow_hyphen_values = true` is overly permissive: it allows any hyphen-prefixed token to be treated as the value for `--depth`, including other flags like `--json` and `-g`. This can cause confusing parse failures when flags are accidentally ordered after `--depth` without a value.

## Issue Context
This PR needs `--depth -1` to work (separate token). The current implementation achieves that, but also expands what `--depth` can consume.

## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/list.rs[24-36]
- pacquet/crates/cli/src/cli_args/list.rs[38-71]

### Suggested approach
- Prefer a more targeted clap setting (if supported by the repo's clap version), such as allowing *negative numbers* specifically (rather than all hyphen-prefixed strings), so `-1` works but `--json`/`-g` are not consumed as depth values.
- Add a small parser regression test to ensure `--depth --json` does **not** silently consume `--json` as a depth value (i.e., it should still be treated as the `--json` flag and `--depth` should be reported as missing its value, if that's the desired behavior).

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


Results up to commit 9e7bff2


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


Action required
1. Unsanitized root label output 🐞 Bug ⛨ Security
Description
project_only_root() copies name, version, and path directly from workspace manifests/paths,
and tree-mode output prints these root fields without applying sanitize(), enabling terminal
escape/control-sequence injection when listing a malicious workspace.
This is newly reachable via recursive projects-only listing (-r list --depth -1) which prints
multiple project roots in terminal mode.
Code

pacquet/crates/cli/src/cli_args/list.rs[R297-304]

+fn project_only_root(project: &pacquet_workspace::Project) -> LocalTreeRoot {
+    let manifest = project.manifest.value();
+    LocalTreeRoot {
+        name: manifest.get("name").and_then(Value::as_str).map(str::to_string),
+        version: manifest.get("version").and_then(Value::as_str).map(str::to_string),
+        private: manifest.get("private").and_then(Value::as_bool),
+        path: project.root_dir.to_string_lossy().into_owned(),
+        dependencies: Vec::new(),
Evidence
The repo already has a sanitize() helper specifically to prevent control characters/escape
sequences from reaching the terminal. Dependencies are sanitized, but root labels in tree rendering
use raw root.name and root.path; the new project_only_root() provides additional untrusted
root values from workspace manifests and paths.

pacquet/crates/cli/src/cli_args/sanitize.rs[1-17]
pacquet/crates/cli/src/cli_args/list.rs[297-304]
pacquet/crates/cli/src/cli_args/list.rs[603-615]
pacquet/crates/cli/src/cli_args/list.rs[695-706]

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

### Issue description
Tree-mode `list` output renders root `name`/`version`/`path` without calling `sanitize()`. Because these values come from filesystem paths and `package.json`, a malicious repository can include escape/control characters that will be emitted to the user's terminal.

### Issue Context
This PR adds a new recursive projects-only path (`project_only_root`) and multi-root printing, increasing the surface area of root labels printed in tree output.

### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/list.rs[297-309]
- pacquet/crates/cli/src/cli_args/list.rs[253-259]

### Suggested approach
- Apply `sanitize()` to root label components in the tree output path (e.g., sanitize `root.name`, `root.version`, and `root.path` before passing them to `name_at_version`, `dim`, and `bold`).
- Ensure the recursive projects-only path does not bypass this sanitation (either sanitize at render time or when constructing `LocalTreeRoot` for projects-only roots).

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



Remediation recommended
2. Recursive list reloads lockfile 🐞 Bug ➹ Performance
Description
In run_recursive, the non-ProjectsOnly path calls local_root() once per selected project, and
local_root() reloads/parses the lockfile each time, causing O(projects) redundant disk IO and
parsing for -r list.
This can significantly slow pacquet -r list on large workspaces compared to existing recursive
commands that load the lockfile once and reuse it.
Code

pacquet/crates/cli/src/cli_args/list.rs[R123-129]

+            let mut roots = Vec::new();
+            for project_dir in selection.selected.keys() {
+                if let LocalRootResult::Root(root) = self.local_root(config, project_dir)? {
+                    roots.push(root);
+                }
+            }
+            roots
Evidence
The new recursive path loops over each selected project and calls local_root(), which loads the
lockfile from disk; this repeats lockfile parsing per project. peers demonstrates the preferred
pattern: load the lockfile once, then process all projects under the recursive flag.

pacquet/crates/cli/src/cli_args/list.rs[110-130]
pacquet/crates/cli/src/cli_args/list.rs[158-171]
pacquet/crates/cli/src/cli_args/peers.rs[75-103]

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

### Issue description
`ListArgs::run_recursive` currently iterates selected projects and calls `local_root()` for each, but `local_root()` loads and parses the lockfile on every call. This creates redundant IO/CPU work proportional to the number of projects.

### Issue Context
Other recursive commands (e.g., `peers`) load the lockfile once and then iterate importers/projects, avoiding repeated lockfile parsing. `list -r` should follow the same pattern for depths other than `ProjectsOnly`.

### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/list.rs[110-135]
- pacquet/crates/cli/src/cli_args/list.rs[158-171]

### Suggested approach
- In `run_recursive`, load the lockfile once (wanted/current based on flags) and pass a reference into a helper that builds `LocalTreeRoot` for each project/importer.
- Alternatively, compute importer IDs once and build trees from the single loaded lockfile, similar to how `peers` handles `recursive`.

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


Results up to commit abbcf29


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


Action required
1. Recursive list ignores package filter ✗ Dismissed 🐞 Bug ≡ Correctness
Description
ListArgs::run_recursive’s --depth -1 (ProjectsOnly) branch builds roots directly from workspace
projects and never applies the positional packages filter, so pacquet -r list --depth -1 <pkg>
can emit projects even though the local path would produce “No matching packages found”. This is a
behavior regression introduced by the new recursive dispatch for list.
Code

pacquet/crates/cli/src/cli_args/list.rs[R110-122]

+    fn run_recursive(&self, config: &Config, dir: &Path) -> miette::Result<()> {
+        let workspace_root = config.workspace_dir.as_deref().unwrap_or(dir);
+        let (projects, _) = discover_workspace_projects(workspace_root)?;
+        let selection =
+            select_recursive_projects(&projects, config, dir, AutoExcludeRoot::Disabled)?;
+
+        let roots = if self.depth == RecursionLimit::ProjectsOnly {
+            selection
+                .selected
+                .values()
+                .map(|node| project_only_root(node.package.project))
+                .collect::<Vec<_>>()
+        } else {
Evidence
The new recursive ProjectsOnly branch constructs roots without consulting self.packages, while the
local path applies self.packages filtering and returns NoMatchingPackages when the dependency
tree is empty (which it always is for ProjectsOnly). That makes recursive ProjectsOnly ignore
package filters in a way local list does not.

pacquet/crates/cli/src/cli_args/list.rs[110-123]
pacquet/crates/cli/src/cli_args/list.rs[205-219]
pacquet/crates/cli/src/cli_args/list.rs[325-339]

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 -r list --depth -1 ...` currently bypasses the `packages` filtering semantics because `run_recursive`’s `ProjectsOnly` branch does not go through `local_root()` (where `self.packages` filtering and the `NoMatchingPackages` behavior are implemented).

This creates an inconsistent and misleading result vs non-recursive `list`, where `--depth -1` produces an empty dependency tree and thus yields `No matching packages found` when `packages` is non-empty.

## Issue Context
- `build_local_tree()` intentionally returns an empty tree for `RecursionLimit::ProjectsOnly`, so the local filtering logic deterministically treats any non-empty `packages` filter as “no matches”.
- The recursive ProjectsOnly path should preserve this behavior (or explicitly define a different one and test it), but today it silently lists all selected workspace projects.

## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/list.rs[110-134]
- pacquet/crates/cli/src/cli_args/list.rs[205-219]
- pacquet/crates/cli/src/cli_args/list.rs[325-339]

### Suggested implementation direction
- In `run_recursive`, if `self.depth == ProjectsOnly` and `!self.packages.is_empty()`, produce the same observable behavior as `run_local` (e.g., print `No matching packages found` and return), OR apply an equivalent filtering rule and add a regression test.
- Consider also handling the “no matches across all selected projects” case in recursive mode to avoid silent empty output when `packages` is set.

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



Remediation recommended
2. run_local prints raw errors 📘 Rule violation ◔ Observability
Description
When list can’t find a lockfile/packages, the new run_local() path prints user-facing failure
messages via println! and still returns Ok(()), bypassing the centralized
miette/pacquet-diagnostics reporting path. This can lead to inconsistent error rendering and makes
it harder to standardize UX/error codes.
Code

pacquet/crates/cli/src/cli_args/list.rs[R136-154]

    fn run_local(&self, config: &Config, dir: &Path) -> miette::Result<()> {
+        match self.local_root(config, dir)? {
+            LocalRootResult::Root(root) => self.print_roots(&[root]),
+            LocalRootResult::NoLockfile { lockfile_dir } => {
+                if self.packages.is_empty() {
+                    println!("No lockfile found in {}", lockfile_dir.display());
+                } else {
+                    println!("No matching packages found");
+                }
+            }
+            LocalRootResult::NoPackages => {
+                if self.packages.is_empty() {
+                    println!("No packages found");
+                } else {
+                    println!("No matching packages found");
+                }
+            }
+            LocalRootResult::NoMatchingPackages => println!("No matching packages found"),
+        }
Evidence
The checklist requires user-facing errors to be routed through miette via pacquet-diagnostics. The
changed run_local() match arms print raw messages with println! for failure-like conditions
instead of returning a diagnostic error.

Rule 713453: Route user-facing errors through miette via pacquet-diagnostics
pacquet/crates/cli/src/cli_args/list.rs[136-154]

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

## Issue description
`ListArgs::run_local()` prints failure messages (`No lockfile found...`, `No packages found`, `No matching packages found`) using `println!` and then returns success. For user-facing failures, the repo standard is to route errors through miette via pacquet-diagnostics.

## Issue Context
This path is newly/explicitly structured in this PR (moved from early returns inside `local_root()` to `run_local()`), so it’s a good time to ensure the user-facing failure mode uses the centralized diagnostics mechanism.

## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/list.rs[136-155]

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



Informational
3. Recursive output double-buffers 🐞 Bug ➹ Performance
Description
print_roots builds a Vec<String> of per-root renders and then joins into another String,
increasing peak memory and allocation work for recursive outputs with many projects. Streaming
per-root output to a locked stdout (or writing incrementally) would reduce peak memory without
changing output bytes.
Code

pacquet/crates/cli/src/cli_args/list.rs[R243-259]

+            let output = roots
+                .iter()
+                .map(|root| render_local_parseable(root, self.long))
+                .filter(|output| !output.is_empty())
+                .collect::<Vec<_>>()
+                .join("\n");
+            if !output.is_empty() {
+                println!("{output}");
+            }
        } else {
-            let output = render_local_tree(&root, self.long);
+            let joiner = if self.depth == RecursionLimit::ProjectsOnly { "\n" } else { "\n\n" };
+            let output = roots
+                .iter()
+                .map(|root| render_local_tree(root, self.long))
+                .filter(|output| !output.is_empty())
+                .collect::<Vec<_>>()
+                .join(joiner);
Evidence
The new multi-root rendering path explicitly collects all per-root output strings and then joins
them, which inherently buffers the entire output at least twice (per-root strings + joined string).

pacquet/crates/cli/src/cli_args/list.rs[235-263]

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

## Issue description
Recursive `list/ll` can now print many roots. `print_roots` currently renders all roots into intermediate `String`s, collects them, then joins into one more `String` before printing. This double-buffers output and increases peak memory.

## Issue Context
This is most noticeable for large workspaces and non-JSON output modes where each root may include a sizable dependency tree.

## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/list.rs[235-263]

### Suggested implementation direction
- Replace `collect::<Vec<_>>().join(...)` with incremental writes:
 - Acquire a `std::io::StdoutLock` and `write!` each rendered root with the correct separators.
 - Alternatively, `println!` per-root with separator logic, avoiding materializing the full combined string.
- Keep existing behavior regarding:
 - Not printing anything when each root render is empty
 - The joiner choice (`\n` vs `\n\n`) for ProjectsOnly vs other depths.

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


Qodo Logo

@github-actions github-actions Bot added the reviewed: coderabbit CodeRabbit submitted an approving review label Jul 7, 2026
@qodo-code-review

qodo-code-review Bot commented Jul 7, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

Latest suggestions up to bcfff0a

Warning

/improve is deprecated. Use /agentic_review instead (removal date not yet scheduled).

CategorySuggestion                                                                                                                                    Impact
Security
Sanitize rendered package versions

Sanitize the version variable in name_at_version to prevent ANSI escape sequence
injection. This ensures both package name and version are safe for terminal
output.

pacquet/crates/cli/src/cli_args/list.rs [708-715]

 pub(crate) fn name_at_version(name: &str, version: &str) -> String {
     let name = sanitize(name);
     if version.is_empty() {
         name.into_owned()
     } else {
+        let version = sanitize(version);
         format!("{}{}", name, gray(&format!("@{version}")))
     }
 }
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: Sanitizing the version string prevents potential ANSI escape sequence injection in terminal output, completing the security improvement initiated for the package name.

Medium
  • More

Previous suggestions

Suggestions up to commit bb40324
CategorySuggestion                                                                                                                                    Impact
General
Stabilize recursive output ordering

Iterate over the ordered projects list instead of the HashMap keys to ensure
deterministic output.

pacquet/crates/cli/src/cli_args/list.rs [116-130]

 let roots = if self.depth == RecursionLimit::ProjectsOnly {
-    selection
-        .selected
-        .values()
-        .map(|node| project_only_root(node.package.project))
+    projects
+        .iter()
+        .filter(|project| selection.selected.contains_key(&project.root_dir))
+        .map(project_only_root)
         .collect::<Vec<_>>()
 } else {
     let mut roots = Vec::new();
-    for project_dir in selection.selected.keys() {
-        if let LocalRootResult::Root(root) = self.local_root(config, project_dir)? {
+    for project in &projects {
+        if !selection.selected.contains_key(&project.root_dir) {
+            continue;
+        }
+        if let LocalRootResult::Root(root) = self.local_root(config, &project.root_dir)? {
             roots.push(root);
         }
     }
     roots
 };
Suggestion importance[1-10]: 8

__

Why: Iterating over HashMap keys produces non-deterministic order, so using the ordered projects list stabilizes the output across runs.

Medium
Suggestions
CategorySuggestion                                                                                                                                    Impact
Possible issue
Prevent incorrect dependency fallback

Only use lockfile.root_project() as a fallback when the requested project is the
root ".".

pacquet/crates/cli/src/cli_args/list.rs [182-186]

-let Some(importer) =
+let importer = if importer_id == "." {
     lockfile.importers.get(&importer_id).or_else(|| lockfile.root_project())
-else {
+} else {
+    lockfile.importers.get(&importer_id)
+};
+let Some(importer) = importer else {
     return Ok(LocalRootResult::NoPackages);
 };
Suggestion importance[1-10]: 8

__

Why: Falling back to the root_project for workspace sub-projects incorrectly lists the root's dependencies for them instead of returning an empty state.

Medium
General
Stabilize recursive output order

Sort the selected projects alphabetically before rendering to ensure a
deterministic output order.

pacquet/crates/cli/src/cli_args/list.rs [116-130]

+let mut selected_project_dirs = selection.selected.keys().collect::<Vec<_>>();
+selected_project_dirs.sort();
+
 let roots = if self.depth == RecursionLimit::ProjectsOnly {
-    selection
-        .selected
-        .values()
+    selected_project_dirs
+        .into_iter()
+        .filter_map(|project_dir| selection.selected.get(project_dir))
         .map(|node| project_only_root(node.package.project))
         .collect::<Vec<_>>()
 } else {
     let mut roots = Vec::new();
-    for project_dir in selection.selected.keys() {
+    for project_dir in selected_project_dirs {
         if let LocalRootResult::Root(root) = self.local_root(config, project_dir)? {
             roots.push(root);
         }
     }
     roots
 };
Suggestion importance[1-10]: 3

__

Why: If selection.selected is an IndexMap (common for representing resolved project graphs), its iteration is already deterministic, and sorting alphabetically would unnecessarily destroy the intended topological ordering.

Low

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      6.7±0.06ms   652.0 KB/sec    1.01      6.7±0.47ms   648.6 KB/sec

@codecov-commenter

codecov-commenter commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 47.25275% with 48 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.62%. Comparing base (6fbbebf) to head (bcfff0a).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/cli/src/cli_args/list.rs 47.72% 46 Missing ⚠️
pacquet/crates/cli/src/cli_args/dispatch_query.rs 50.00% 1 Missing ⚠️
...quet/crates/cli/src/cli_args/switch_cli_version.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12848      +/-   ##
==========================================
- Coverage   85.84%   85.62%   -0.22%     
==========================================
  Files         448      449       +1     
  Lines       69558    69986     +428     
==========================================
+ Hits        59709    59925     +216     
- Misses       9849    10061     +212     

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

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Commit: 20d922b51c9e

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.153 ± 0.134 3.949 4.330 2.02 ± 0.10
pacquet@main 4.077 ± 0.133 3.942 4.265 1.98 ± 0.09
pnpr@HEAD 2.056 ± 0.071 1.944 2.139 1.00
pnpr@main 2.086 ± 0.137 1.941 2.345 1.01 ± 0.08
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.152881940039999,
      "stddev": 0.13421702250105774,
      "median": 4.19242159164,
      "user": 3.81019826,
      "system": 3.4823563199999996,
      "min": 3.94854923364,
      "max": 4.33028517864,
      "times": [
        4.23157881764,
        4.007818954639999,
        4.33028517864,
        4.17178343064,
        3.94854923364,
        4.28139507264,
        4.06074956164,
        4.2130597526399995,
        4.26915624964,
        4.01444314864
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.07744507964,
      "stddev": 0.1332408875761204,
      "median": 4.01595162964,
      "user": 3.8104067600000002,
      "system": 3.44589752,
      "min": 3.9416688046400004,
      "max": 4.26450676164,
      "times": [
        3.97348734964,
        3.97643180264,
        3.97377213764,
        4.055471456639999,
        4.26450676164,
        3.9416688046400004,
        4.1244356696399995,
        4.2437778676399995,
        3.96543941364,
        4.25545953264
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.05567175164,
      "stddev": 0.07075487630152924,
      "median": 2.05987321364,
      "user": 2.64070026,
      "system": 2.99751602,
      "min": 1.94386012164,
      "max": 2.13900419664,
      "times": [
        2.13634876464,
        1.94386012164,
        1.98899685964,
        2.02031614164,
        2.13900419664,
        2.00447454664,
        2.0029037676400003,
        2.09943028564,
        2.11463125664,
        2.10675157564
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.0861243814400003,
      "stddev": 0.13702878160160592,
      "median": 2.01759180064,
      "user": 2.6472023600000005,
      "system": 2.96351172,
      "min": 1.94091217064,
      "max": 2.34539882764,
      "times": [
        2.34539882764,
        1.99202856964,
        2.22930558464,
        2.16614550264,
        2.0030648396400004,
        1.9786796226399999,
        1.94091217064,
        2.03211876164,
        2.1953378846400002,
        1.9782520506399999
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 640.0 ± 11.8 619.1 656.9 1.00
pacquet@main 653.7 ± 78.1 613.6 874.4 1.02 ± 0.12
pnpr@HEAD 697.1 ± 13.9 677.5 720.1 1.09 ± 0.03
pnpr@main 689.8 ± 26.3 661.8 754.9 1.08 ± 0.05
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.64001491106,
      "stddev": 0.011812198994178538,
      "median": 0.63951689976,
      "user": 0.37351614,
      "system": 1.321026,
      "min": 0.61913096376,
      "max": 0.65691702976,
      "times": [
        0.61913096376,
        0.64412978876,
        0.63278749276,
        0.6409922837600001,
        0.65604717376,
        0.63556951876,
        0.63804151576,
        0.64767428976,
        0.6288590537600001,
        0.65691702976
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.65372895486,
      "stddev": 0.07806841294697547,
      "median": 0.63101982826,
      "user": 0.36972594000000003,
      "system": 1.3231692,
      "min": 0.6136461507600001,
      "max": 0.87442577276,
      "times": [
        0.64610656576,
        0.6315359137600001,
        0.63238467676,
        0.63445272176,
        0.61654292976,
        0.63050374276,
        0.62892962976,
        0.6287614447600001,
        0.6136461507600001,
        0.87442577276
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6971113189600001,
      "stddev": 0.01394880198707752,
      "median": 0.69537546276,
      "user": 0.39836224,
      "system": 1.3530887999999999,
      "min": 0.6774956117600001,
      "max": 0.72008328676,
      "times": [
        0.69171484876,
        0.67920663376,
        0.69395407876,
        0.7047858837600001,
        0.6968929997600001,
        0.69679684676,
        0.69299698576,
        0.71718601376,
        0.72008328676,
        0.6774956117600001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.68977328656,
      "stddev": 0.026310953004549782,
      "median": 0.68915545126,
      "user": 0.39202803999999997,
      "system": 1.3410170999999997,
      "min": 0.66180918776,
      "max": 0.75487018576,
      "times": [
        0.66731069876,
        0.6986604657600001,
        0.6994531537600001,
        0.69011537376,
        0.68972904276,
        0.67148551676,
        0.68858185976,
        0.75487018576,
        0.66180918776,
        0.67571738076
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.224 ± 0.027 4.191 4.272 1.98 ± 0.07
pacquet@main 4.226 ± 0.041 4.170 4.287 1.98 ± 0.07
pnpr@HEAD 2.183 ± 0.097 2.078 2.345 1.02 ± 0.06
pnpr@main 2.138 ± 0.074 2.033 2.276 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.22419483874,
      "stddev": 0.02727807993099961,
      "median": 4.220838247340001,
      "user": 3.7242448999999995,
      "system": 3.3378434599999993,
      "min": 4.190685745840001,
      "max": 4.2715248828400005,
      "times": [
        4.190685745840001,
        4.192019627840001,
        4.22223560084,
        4.219440893840001,
        4.20158647384,
        4.21580839784,
        4.25818411784,
        4.223152043840001,
        4.247310602840001,
        4.2715248828400005
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.226148386740001,
      "stddev": 0.04130619056456617,
      "median": 4.220863896340001,
      "user": 3.7181679,
      "system": 3.3447957599999993,
      "min": 4.17002972384,
      "max": 4.28725437984,
      "times": [
        4.28725437984,
        4.2447901988400005,
        4.232114198840001,
        4.20961359384,
        4.18794215784,
        4.26667536184,
        4.17002972384,
        4.182229196840001,
        4.20593713784,
        4.274897917840001
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.1829362899399998,
      "stddev": 0.09681637851647654,
      "median": 2.16709389784,
      "user": 2.4835495,
      "system": 2.89103956,
      "min": 2.07789108684,
      "max": 2.34546071184,
      "times": [
        2.14303711184,
        2.20235666184,
        2.34546071184,
        2.09033907084,
        2.1403007018399998,
        2.07789108684,
        2.09423249484,
        2.19115068384,
        2.34525027084,
        2.1993441048399998
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.13815998514,
      "stddev": 0.07417790524325311,
      "median": 2.14911346534,
      "user": 2.4737620999999996,
      "system": 2.8836227599999993,
      "min": 2.03297788684,
      "max": 2.27557740084,
      "times": [
        2.15310771084,
        2.05673649384,
        2.14928301884,
        2.14894391184,
        2.16557599784,
        2.03297788684,
        2.27557740084,
        2.06399130984,
        2.12035866484,
        2.2150474558399997
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.355 ± 0.016 1.335 1.387 2.08 ± 0.04
pacquet@main 1.378 ± 0.037 1.353 1.479 2.12 ± 0.06
pnpr@HEAD 0.662 ± 0.031 0.639 0.747 1.02 ± 0.05
pnpr@main 0.651 ± 0.008 0.633 0.661 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.35484943534,
      "stddev": 0.015942483654712254,
      "median": 1.35098547614,
      "user": 1.30789244,
      "system": 1.69271914,
      "min": 1.33496924814,
      "max": 1.38663572114,
      "times": [
        1.35909195414,
        1.38663572114,
        1.3650181861400001,
        1.37120454814,
        1.34320541814,
        1.34305726214,
        1.34334106314,
        1.35627894414,
        1.33496924814,
        1.3456920081400001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.37763532644,
      "stddev": 0.03712632468680655,
      "median": 1.36629350864,
      "user": 1.31765104,
      "system": 1.6949237400000001,
      "min": 1.35318146014,
      "max": 1.4787352711400001,
      "times": [
        1.35318146014,
        1.3639066181400001,
        1.36868039914,
        1.4787352711400001,
        1.37266569014,
        1.39207604114,
        1.35937896814,
        1.3575517241400001,
        1.36913185514,
        1.3610452371400001
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6616755592400001,
      "stddev": 0.030920394918131772,
      "median": 0.6555687611400001,
      "user": 0.34152883999999994,
      "system": 1.2832577400000003,
      "min": 0.6388008101400001,
      "max": 0.74678448214,
      "times": [
        0.65254199014,
        0.6464679321400001,
        0.64381471814,
        0.74678448214,
        0.6585955321400001,
        0.65936798214,
        0.6602476211400001,
        0.6621369571400001,
        0.6388008101400001,
        0.6479975671400001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.65132565894,
      "stddev": 0.008423488567772818,
      "median": 0.6515928091400001,
      "user": 0.34105394,
      "system": 1.27172874,
      "min": 0.6334627691400001,
      "max": 0.6613157771400001,
      "times": [
        0.6608442471400001,
        0.65214282314,
        0.6613157771400001,
        0.6334627691400001,
        0.65959068214,
        0.6510427951400001,
        0.6530304691400001,
        0.64497796514,
        0.64833353914,
        0.6485155221400001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.021 ± 0.023 2.994 3.066 4.48 ± 0.07
pacquet@main 3.036 ± 0.075 2.994 3.246 4.50 ± 0.13
pnpr@HEAD 0.675 ± 0.009 0.655 0.687 1.00
pnpr@main 0.678 ± 0.069 0.650 0.872 1.00 ± 0.10
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.02112379162,
      "stddev": 0.023020787216307608,
      "median": 3.0138283869199998,
      "user": 1.77585302,
      "system": 1.94397652,
      "min": 2.99360767942,
      "max": 3.0662644064199998,
      "times": [
        3.03405283042,
        3.00640395742,
        3.01032499042,
        3.0108149744199997,
        3.02037564942,
        2.99360767942,
        3.01684179942,
        3.0662644064199998,
        3.00081901742,
        3.05173261142
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.03618421752,
      "stddev": 0.07512892373559935,
      "median": 3.0181541669199996,
      "user": 1.7687727199999999,
      "system": 1.9571121200000001,
      "min": 2.99358411442,
      "max": 3.24562400842,
      "times": [
        3.01900398642,
        3.0173043474199996,
        3.00362143942,
        2.99358411442,
        3.04423805242,
        3.02128463742,
        2.99365652042,
        3.24562400842,
        3.00404393642,
        3.0194811324199997
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.67504738432,
      "stddev": 0.008853246670759844,
      "median": 0.67631966042,
      "user": 0.34524772,
      "system": 1.30926452,
      "min": 0.65462059842,
      "max": 0.68745472642,
      "times": [
        0.68280728642,
        0.68745472642,
        0.65462059842,
        0.67323269042,
        0.67125230142,
        0.67569352942,
        0.67993804742,
        0.67794860242,
        0.67058026942,
        0.67694579142
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.67752480262,
      "stddev": 0.06854580979831519,
      "median": 0.6571892429199999,
      "user": 0.34385101999999995,
      "system": 1.2796234199999998,
      "min": 0.64978875742,
      "max": 0.87224882942,
      "times": [
        0.65944320042,
        0.64978875742,
        0.66261958542,
        0.65057942542,
        0.65222736142,
        0.65712438042,
        0.65436055742,
        0.65960182342,
        0.65725410542,
        0.87224882942
      ]
    }
  ]
}

Scenario: Isolated linker: fresh resolve, hot cache, offline

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 654.3 ± 13.6 638.1 674.3 6.06 ± 0.13
pacquet@main 646.9 ± 11.8 627.2 674.0 6.00 ± 0.12
pnpr@HEAD 107.9 ± 0.8 104.9 109.2 1.00
pnpr@main 108.0 ± 0.4 107.0 108.9 1.00 ± 0.01
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.65425210524,
      "stddev": 0.013572890268959527,
      "median": 0.65674990454,
      "user": 0.62016344,
      "system": 0.15208536,
      "min": 0.63812289254,
      "max": 0.6743359935400001,
      "times": [
        0.65812371454,
        0.64235511854,
        0.65537609454,
        0.63950893454,
        0.63812289254,
        0.63944220054,
        0.66335304854,
        0.67061149454,
        0.6743359935400001,
        0.66129156054
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.64692366854,
      "stddev": 0.011812110630754458,
      "median": 0.64406136404,
      "user": 0.6104749399999999,
      "system": 0.15317245999999998,
      "min": 0.62715140454,
      "max": 0.67401603454,
      "times": [
        0.64041653454,
        0.65160894054,
        0.62715140454,
        0.64331091754,
        0.65136236254,
        0.64433802754,
        0.67401603454,
        0.64954175354,
        0.64370600954,
        0.64378470054
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.10787726670000003,
      "stddev": 0.0007511433544329648,
      "median": 0.10802762554,
      "user": 0.028440600000000003,
      "system": 0.017433960000000002,
      "min": 0.10494263554000001,
      "max": 0.10915797654,
      "times": [
        0.10737653654000001,
        0.10814809454,
        0.10818401154000001,
        0.10787919354,
        0.10814132754000001,
        0.10774764454,
        0.10825412454000001,
        0.10766771354,
        0.10819933854000001,
        0.10769969354,
        0.10494263554000001,
        0.10792821954000001,
        0.10789610954000001,
        0.10827689954000001,
        0.10810425654000001,
        0.10832466754,
        0.10744014954,
        0.10802762554,
        0.10851208654000001,
        0.10732845554,
        0.10915797654,
        0.10714227854000001,
        0.10776021554000001,
        0.10853456954,
        0.10825784354000001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.10801660694000001,
      "stddev": 0.00041878026300983017,
      "median": 0.10802907754,
      "user": 0.02655584,
      "system": 0.019212439999999997,
      "min": 0.10701094954000001,
      "max": 0.10889877754,
      "times": [
        0.10735641854000001,
        0.10822735554,
        0.10701094954000001,
        0.10837454154000001,
        0.10785135154,
        0.10851945554,
        0.10803469254,
        0.10806855554,
        0.10767735254000001,
        0.10833549754,
        0.10802289554000001,
        0.10802907754,
        0.10754739954,
        0.10784600654000001,
        0.10867038154000001,
        0.10752022954000001,
        0.10889877754,
        0.10783754154000001,
        0.10802023054000001,
        0.10806520354,
        0.10793095554000001,
        0.10816849654,
        0.10775112954,
        0.10814942054000001,
        0.10850125754
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 6.858 ± 0.144 6.712 7.136 1.40 ± 0.03
pacquet@main 6.858 ± 0.131 6.696 7.115 1.40 ± 0.03
pnpr@HEAD 4.906 ± 0.054 4.812 4.981 1.00
pnpr@main 4.966 ± 0.137 4.791 5.235 1.01 ± 0.03
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 6.85774151936,
      "stddev": 0.14399848890315398,
      "median": 6.81025513226,
      "user": 4.017508059999999,
      "system": 3.7526394000000005,
      "min": 6.71210739626,
      "max": 7.13567015826,
      "times": [
        7.13567015826,
        6.78171504326,
        6.79621291426,
        7.0522639402600005,
        6.82429735026,
        6.95670565426,
        6.71210739626,
        6.73456137026,
        6.84678787526,
        6.7370934912600005
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 6.858461168260002,
      "stddev": 0.13130365002339717,
      "median": 6.84339541576,
      "user": 4.02402766,
      "system": 3.7795646999999994,
      "min": 6.695562922260001,
      "max": 7.11478670626,
      "times": [
        6.88972754026,
        7.02720848926,
        7.11478670626,
        6.81844408226,
        6.695562922260001,
        6.8683467492600005,
        6.79493761326,
        6.76627276426,
        6.72264922826,
        6.88667558726
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 4.90645633906,
      "stddev": 0.05414392359835954,
      "median": 4.91565017326,
      "user": 2.73993386,
      "system": 3.2245492,
      "min": 4.81166332026,
      "max": 4.98095403726,
      "times": [
        4.97872079726,
        4.87131082826,
        4.92348760726,
        4.92512263526,
        4.9201140292600005,
        4.98095403726,
        4.91118631726,
        4.8391454752600005,
        4.81166332026,
        4.90285834326
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 4.965768923360001,
      "stddev": 0.13657339564812823,
      "median": 4.97319805876,
      "user": 2.7552669599999997,
      "system": 3.2158420999999997,
      "min": 4.79084252826,
      "max": 5.23495814126,
      "times": [
        5.03367784426,
        5.01285234226,
        4.79084252826,
        5.07569490626,
        5.23495814126,
        4.87648101626,
        4.81684285426,
        5.02361319226,
        4.93354377526,
        4.85918263326
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12848
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,224.19 ms
(-0.39%)Baseline: 4,240.68 ms
5,088.82 ms
(83.01%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
3,021.12 ms
(-0.25%)Baseline: 3,028.72 ms
3,634.47 ms
(83.12%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,354.85 ms
(+0.46%)Baseline: 1,348.63 ms
1,618.35 ms
(83.72%)
isolated-linker.fresh-resolve.hot-cache.offline📈 view plot
🚷 view threshold
654.25 ms
(+2.57%)Baseline: 637.87 ms
765.44 ms
(85.47%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
4,152.88 ms
(+3.77%)Baseline: 4,002.13 ms
4,802.56 ms
(86.47%)
isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr📈 view plot
🚷 view threshold
6,857.74 ms
(+0.39%)Baseline: 6,831.16 ms
8,197.39 ms
(83.66%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
640.01 ms
(+0.05%)Baseline: 639.68 ms
767.62 ms
(83.38%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12848
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,182.94 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
675.05 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
661.68 ms
isolated-linker.fresh-resolve.hot-cache.offline📈 view plot
⚠️ NO THRESHOLD
107.88 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,055.67 ms
isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr📈 view plot
⚠️ NO THRESHOLD
4,906.46 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
697.11 ms
🐰 View full continuous benchmarking report in Bencher

@zkochan zkochan changed the title fix(pacquet): accept negative list depth values fix(pacquet): support recursive project listing Jul 7, 2026
Comment thread pacquet/crates/cli/src/cli_args/list.rs
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 9e7bff2

@qodo-code-review

qodo-code-review Bot commented Jul 7, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

Warning

/improve is deprecated. Use /agentic_review instead (removal date not yet scheduled).

CategorySuggestion                                                                                                                                    Impact
Possible issue
Prevent incorrect dependency fallback

Only use lockfile.root_project() as a fallback when the requested project is the
root ".".

pacquet/crates/cli/src/cli_args/list.rs [182-186]

-let Some(importer) =
+let importer = if importer_id == "." {
     lockfile.importers.get(&importer_id).or_else(|| lockfile.root_project())
-else {
+} else {
+    lockfile.importers.get(&importer_id)
+};
+let Some(importer) = importer else {
     return Ok(LocalRootResult::NoPackages);
 };
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: Falling back to the root_project for workspace sub-projects incorrectly lists the root's dependencies for them instead of returning an empty state.

Medium
General
Stabilize recursive output order

Sort the selected projects alphabetically before rendering to ensure a
deterministic output order.

pacquet/crates/cli/src/cli_args/list.rs [116-130]

+let mut selected_project_dirs = selection.selected.keys().collect::<Vec<_>>();
+selected_project_dirs.sort();
+
 let roots = if self.depth == RecursionLimit::ProjectsOnly {
-    selection
-        .selected
-        .values()
+    selected_project_dirs
+        .into_iter()
+        .filter_map(|project_dir| selection.selected.get(project_dir))
         .map(|node| project_only_root(node.package.project))
         .collect::<Vec<_>>()
 } else {
     let mut roots = Vec::new();
-    for project_dir in selection.selected.keys() {
+    for project_dir in selected_project_dirs {
         if let LocalRootResult::Root(root) = self.local_root(config, project_dir)? {
             roots.push(root);
         }
     }
     roots
 };
  • Apply / Chat
Suggestion importance[1-10]: 3

__

Why: If selection.selected is an IndexMap (common for representing resolved project graphs), its iteration is already deterministic, and sorting alphabetically would unnecessarily destroy the intended topological ordering.

Low
  • More

@zkochan
zkochan force-pushed the fix-pq12-list-depth branch from 9e7bff2 to abbcf29 Compare July 7, 2026 22:32
Copilot AI review requested due to automatic review settings July 7, 2026 22:32

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.

Comment thread pacquet/crates/cli/src/cli_args/list.rs
@qodo-code-review

Copy link
Copy Markdown

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

@qodo-code-review

Copy link
Copy Markdown

PR Code Suggestions ✨

Warning

/improve is deprecated. Use /agentic_review instead (removal date not yet scheduled).

CategorySuggestion                                                                                                                                    Impact
Possible issue
Prevent incorrect dependency fallback

Only use lockfile.root_project() as a fallback when the requested project is the
root ".".

pacquet/crates/cli/src/cli_args/list.rs [182-186]

-let Some(importer) =
+let importer = if importer_id == "." {
     lockfile.importers.get(&importer_id).or_else(|| lockfile.root_project())
-else {
+} else {
+    lockfile.importers.get(&importer_id)
+};
+let Some(importer) = importer else {
     return Ok(LocalRootResult::NoPackages);
 };
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: Falling back to the root_project for workspace sub-projects incorrectly lists the root's dependencies for them instead of returning an empty state.

Medium
General
Stabilize recursive output order

Sort the selected projects alphabetically before rendering to ensure a
deterministic output order.

pacquet/crates/cli/src/cli_args/list.rs [116-130]

+let mut selected_project_dirs = selection.selected.keys().collect::<Vec<_>>();
+selected_project_dirs.sort();
+
 let roots = if self.depth == RecursionLimit::ProjectsOnly {
-    selection
-        .selected
-        .values()
+    selected_project_dirs
+        .into_iter()
+        .filter_map(|project_dir| selection.selected.get(project_dir))
         .map(|node| project_only_root(node.package.project))
         .collect::<Vec<_>>()
 } else {
     let mut roots = Vec::new();
-    for project_dir in selection.selected.keys() {
+    for project_dir in selected_project_dirs {
         if let LocalRootResult::Root(root) = self.local_root(config, project_dir)? {
             roots.push(root);
         }
     }
     roots
 };
  • Apply / Chat
Suggestion importance[1-10]: 3

__

Why: If selection.selected is an IndexMap (common for representing resolved project graphs), its iteration is already deterministic, and sorting alphabetically would unnecessarily destroy the intended topological ordering.

Low
  • More

Comment thread pacquet/crates/cli/src/cli_args/list.rs
@qodo-code-review

Copy link
Copy Markdown

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

Copilot AI review requested due to automatic review settings July 7, 2026 22:41

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-code-review

Copy link
Copy Markdown

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

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

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

42-44: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Set-based comparison can hide duplicate entries.

Collecting into a BTreeSet<String> de-duplicates names, so a regression that emits the same project twice (e.g. root counted alongside a workspace package) would not be caught by the current assertions. Consider also asserting packages.len() matches the expected count (or returning a Vec and checking both length and set membership) to make the regression test more meaningful.

♻️ Suggested strengthening
-fn recursive_project_names(pacquet: Command, extra_args: &[&str]) -> BTreeSet<String> {
+fn recursive_project_names(pacquet: Command, extra_args: &[&str]) -> (BTreeSet<String>, usize) {
     let output = pacquet
         .with_arg("-r")
         .with_arg("list")
         .with_args(extra_args)
         .with_arg("--depth")
         .with_arg("-1")
         .with_arg("--json")
         .output()
         .expect("spawn pacquet list");

     assert!(
         output.status.success(),
         "recursive list should succeed:\n{}",
         String::from_utf8_lossy(&output.stderr),
     );
     let packages: Vec<Value> = serde_json::from_slice(&output.stdout).expect("parse list JSON");
-    packages.iter().map(|pkg| pkg["name"].as_str().expect("package name").to_string()).collect()
+    let names: BTreeSet<String> =
+        packages.iter().map(|pkg| pkg["name"].as_str().expect("package name").to_string()).collect();
+    (names, packages.len())
 }

Then assert names.len() == packages.len() at each call site alongside the set equality check.

As per path instructions, "add regression tests that fail without the fix ... with meaningful assertions on returned project sets and JSON shape; don't accept vacuous assertions."

🤖 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/list.rs` around lines 42 - 44, The list tests in the
`packages` helper are only checking set membership, which can mask duplicate
project entries. Update the `packages` parsing/assertion flow in `list.rs` so
the helper returns a `Vec<String>` (or otherwise preserves duplicates) and add
explicit length checks at each call site, alongside the existing `BTreeSet`
equality assertions. Use the existing `packages`/`names` test helpers and JSON
parsing logic to verify both the expected project set and the exact count, so
duplicate emissions fail the regression tests.

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.

Nitpick comments:
In `@pacquet/crates/cli/tests/list.rs`:
- Around line 42-44: The list tests in the `packages` helper are only checking
set membership, which can mask duplicate project entries. Update the `packages`
parsing/assertion flow in `list.rs` so the helper returns a `Vec<String>` (or
otherwise preserves duplicates) and add explicit length checks at each call
site, alongside the existing `BTreeSet` equality assertions. Use the existing
`packages`/`names` test helpers and JSON parsing logic to verify both the
expected project set and the exact count, so duplicate emissions fail the
regression tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: da0d0dfc-2180-45cc-96bc-e9008f7e9933

📥 Commits

Reviewing files that changed from the base of the PR and between bb40324 and bcfff0a.

📒 Files selected for processing (1)
  • pacquet/crates/cli/tests/list.rs

@zkochan
zkochan merged commit 692c449 into main Jul 7, 2026
33 of 35 checks passed
@zkochan
zkochan deleted the fix-pq12-list-depth branch July 7, 2026 23:15
KSXGitHub pushed a commit that referenced this pull request Jul 8, 2026
Brings in the pkg command (#12795), recursive project listing
(#12848), the Pkg command_name match fix (#12850), and
the github-actions dependency bump (#12785).

No conflicts. main's changes are the pkg/list CLI commands and CI
workflows, disjoint from the login/auth-commands work; the shared
command-registry files (cli_args, cli_command, dispatch, dispatch_query,
switch_cli_version) merged cleanly with both the login and the pkg/list
entries.
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants