Skip to content

feat(pacquet-cli): implement pnpm repo command#12702

Merged
zkochan merged 6 commits into
pnpm:mainfrom
kairosci:paquet-repo
Jun 29, 2026
Merged

feat(pacquet-cli): implement pnpm repo command#12702
zkochan merged 6 commits into
pnpm:mainfrom
kairosci:paquet-repo

Conversation

@kairosci

@kairosci kairosci commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the pnpm repo command in the Rust pacquet port, matching the existing TypeScript CLI behavior. The command opens the repository URL of a package in the browser, supporting lookup from the local package manifest or the npm registry.

The implementation supports looking up the repository of the current project from its package.json manifest, or looking up any package from the npm registry by name, optionally with a version specifier or dist-tag. Repository URLs can be specified as a string, an object with url and optional directory fields, or a shorthand like owner/repo for GitHub/GitLab/Bitbucket.

URL formats handled include HTTPS, SSH (git@host:path SCP-style), git://, and git+ prefixed URLs. The .git suffix is stripped and shorthand references are expanded to proper web URLs. When the registry is queried, version selection respects dist-tags and semver ranges. The browser is launched via the OS open/xdg-open command, and errors are reported through the structured Reporter trait as warnings rather than failing the command.

Related to: #11633

Squash Commit Body

Implement the pnpm repo command in pacquet-cli, which opens a package's
repository URL in the browser. Supports local manifest lookup and
registry-based lookup with version selection via dist-tags and semver
ranges. Handles SSH, git://, git+https, and shorthand repository URL
formats, strips .git suffixes, and expands shorthands to proper web
URLs. Uses the structured Reporter trait for browser-open error logging.
Includes 32 unit tests and 3 integration tests covering all URL formats,
error paths, and registry interaction.

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 (Claude Code, claude-opus-4-7).

@kairosci kairosci requested a review from zkochan as a code owner June 27, 2026 18:09
@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a pacquet repo CLI subcommand that resolves repository URLs from local package data or registry metadata, normalizes supported repository formats, and opens the result in a browser.

Changes

pacquet repo subcommand

Layer / File(s) Summary
CLI registration and dispatch wiring
pacquet/crates/cli/src/cli_args.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
Declares pub mod repo, adds CliCommand::Repo(RepoArgs), and routes it through dispatch_query::repo.
RepoArgs URL resolution
pacquet/crates/cli/src/cli_args/repo.rs
Defines RepoArgs and RepoError, reads repository data from the current project or registry metadata, normalizes repository inputs into web URLs, and opens each resolved URL.
Repository URL normalization
pacquet/crates/cli/src/cli_args/repo.rs
Implements repository URL normalization for shorthand and hosted forms, fragment handling, monorepo directory handling, and fragment extraction.
Browser launch and tests
pacquet/crates/cli/src/cli_args/repo.rs, pacquet/crates/cli/src/cli_args/repo/tests.rs, pacquet/crates/cli/tests/repo.rs
Implements OS-specific browser launching, prints errors and redacted URLs on failure, and adds unit and integration tests for repository resolution and the new CLI subcommand.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 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

pacquet-cli: add repo command to open package repository URLs
✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

Description

• Add pacquet repo command mirroring pnpm to open repository URLs in a browser.
• Resolve repository from local package.json or registry packument, then normalize to web URLs.
• Add unit tests covering URL/shorthand normalization and missing-repository error cases.
Diagram

graph TD
  A["pacquet CLI"] --> B["Repo command"] --> C{"Args provided?"}
  C -->|"No"| D["Read package.json"] --> F["Normalize repo URL"] --> G["Open in browser"]
  C -->|"Yes"| E["Fetch registry metadata"] --> F --> G
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use a cross-platform `open`/`webbrowser` crate
  • ➕ Removes OS-specific process/ShellExecute branching and reduces platform edge cases
  • ➕ Often handles quoting/escaping and environment differences more robustly
  • ➖ Adds (or changes) a dependency surface; may conflict with existing dependency policies
  • ➖ May provide less control over redaction/fallback printing behavior
2. Adopt a dedicated hosted-git parsing crate (or small internal module)
  • ➕ Centralizes and documents the shorthand/URL normalization rules
  • ➕ Potentially reduces subtle mismatches with upstream pnpm/npm behavior over time
  • ➖ Rust ecosystem support for fully npm-compatible hosted-git-info parsing may be limited
  • ➖ Pulling in a heavy parser dependency may be undesirable for a small feature

Recommendation: The PR’s approach is reasonable for parity with pnpm: it reuses existing registry-fetch plumbing and implements targeted URL normalization without introducing a large dependency. Consider switching open_url to a small, well-maintained cross-platform opener crate if platform quirks or maintenance burden become an issue.

Files changed (5) +544 / -0

Enhancement (5) +544 / -0
cli_args.rsRegister new 'repo' CLI args module +1/-0

Register new 'repo' CLI args module

• Adds the 'repo' module to the CLI args module list so it is compiled and available for command wiring.

pacquet/crates/cli/src/cli_args.rs

cli_command.rsAdd 'Repo' subcommand to the CLI command enum +3/-0

Add 'Repo' subcommand to the CLI command enum

• Introduces a new 'Repo(RepoArgs)' variant and imports 'RepoArgs', making 'pacquet repo' available as a top-level command with help text.

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

dispatch.rsRoute 'repo' command to query dispatcher +1/-0

Route 'repo' command to query dispatcher

• Extends the main command router to dispatch 'CliCommand::Repo' to 'dispatch_query::repo'.

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

dispatch_query.rsAdd query-dispatch entrypoint for 'repo' +7/-0

Add query-dispatch entrypoint for 'repo'

• Adds 'repo()' dispatcher function that loads config/dir and calls 'RepoArgs::run', consistent with other read-only/query commands.

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

repo.rsImplement 'pacquet repo' resolution, normalization, and tests +532/-0

Implement 'pacquet repo' resolution, normalization, and tests

• Implements repository URL resolution from the local 'package.json' or via full registry metadata fetch for provided package specs, including shorthand/URL normalization to web URLs. Adds platform-specific browser launching with safe fallback to printing a redacted URL on failure, plus extensive unit tests for normalization and error paths.

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

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

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

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (18) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Hardcoded master branch 🐞 Bug ≡ Correctness
Description
try_hosted_shorthand()/try_hosted_url() default to master when building browse URLs for
monorepo repository.directory, so many repos with default branch main (or any non-master) will
open a broken/wrong URL. This diverges from pnpm’s reference implementation, which uses HEAD to
follow the host’s default branch when appending a directory.
Code

pacquet/crates/cli/src/cli_args/repo.rs[R234-242]

+    let (hosted, path) = if let Some(rest) = cleaned.strip_prefix("github:") {
+        (HostedRepo { base_url: "https://github.com".to_string(), default_branch: "master" }, rest)
+    } else if let Some(rest) = cleaned.strip_prefix("gitlab:") {
+        (HostedRepo { base_url: "https://gitlab.com".to_string(), default_branch: "master" }, rest)
+    } else if let Some(rest) = cleaned.strip_prefix("bitbucket:") {
+        (
+            HostedRepo { base_url: "https://bitbucket.org".to_string(), default_branch: "master" },
+            rest,
+        )
Evidence
pnpm’s repositoryToWebUrl appends /tree/HEAD/ for directory-based repository browsing; the Rust
implementation uses master for hosted shorthands/known hosts, but uses HEAD in its generic URL
path, so the same repository can produce different branch defaults depending on parsing path and can
break on main-default repos.

pnpm11/deps/inspection/commands/src/repo/index.ts[82-104]
pacquet/crates/cli/src/cli_args/repo.rs[215-222]
pacquet/crates/cli/src/cli_args/repo.rs[234-266]
pacquet/crates/cli/src/cli_args/repo.rs[341-360]

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 repo` builds hosted browse URLs with `/tree/master/...` when `repository.directory` is present and no fragment/branch is specified. This is wrong for repositories whose default branch is not `master` (very common: `main`) and breaks parity with pnpm, which uses `HEAD` to track the default branch.
### Issue Context
The Rust implementation already uses `HEAD` in the generic URL path when `directory` is set, but the hosted-shorthand/hosted-url paths hardcode `master`, creating inconsistent behavior depending on which parser path matches.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[215-222]
- pacquet/crates/cli/src/cli_args/repo.rs[234-266]
- pacquet/crates/cli/src/cli_args/repo.rs[306-316]
- pacquet/crates/cli/src/cli_args/repo.rs[341-360]
### Suggested change
- Replace the hosted default branch fallback from `master` to `HEAD` anywhere it’s only used when `directory` is present and no explicit fragment/branch was provided.
- Keep behavior consistent across:
- `try_hosted_shorthand()`
- `try_user_repo_shorthand()`
- `try_hosted_url()`
- Update/add unit tests to assert `/tree/HEAD/...` for directory cases in hosted shorthand and hosted URL cases (matching pnpm).

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


2. Repo URL leaks credentials 🐞 Bug ⛨ Security
Description
repository_to_web_url() strips query/fragment but preserves URL userinfo (username/password), so
repository values like https://token@github.com/org/repo.git will be passed to
xdg-open/open/ShellExecuteW with the secret intact. This can leak credentials via process
arguments and cause unintended credential transmission/storage outside the intended auth flow.
Code

pacquet/crates/cli/src/cli_args/repo.rs[R198-207]

+    let mut parsed = url::Url::parse(&cleaned).ok()?;
+    if parsed.scheme() != "http" && parsed.scheme() != "https" {
+        return None;
+    }
+
+    let fragment = try_extract_fragment(raw_url);
+    parsed.set_fragment(None);
+    parsed.set_query(None);
+
+    let mut url = parsed.to_string();
Evidence
The conversion path in repository_to_web_url() explicitly removes only fragment/query before
serializing the URL, so any username:password@ portion remains. The repo already has established
patterns to strip userinfo on URLs, indicating userinfo is considered sensitive and should not be
propagated.

pacquet/crates/cli/src/cli_args/repo.rs[198-207]
pacquet/crates/network/src/proxy.rs[119-131]
pacquet/crates/cli/src/cli_args/docs.rs[129-136]
REVIEW_GUIDE.md[50-59]

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

## Issue description
`repository_to_web_url()` constructs the final browser URL from a parsed `url::Url`, but never clears `username`/`password`. Any credentials embedded in `repository.url` (from local `package.json` or registry metadata) will survive into the URL that is opened.
### Issue Context
Other parts of this repo treat URL userinfo as sensitive and explicitly strip/redact it.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[198-207]
### Suggested fix
After parsing and scheme validation (and alongside the existing `set_query(None)` / `set_fragment(None)`), clear userinfo before calling `to_string()`:
- `let _ = parsed.set_username("");`
- `let _ = parsed.set_password(None);`
This ensures the success path never forwards embedded credentials to the OS/browser opener.

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


3. Repo range parse panic 🐞 Bug ☼ Reliability
Description
select_package_version() forwards the user/manifest selector into Package::pinned_version(),
which unwrap()s Range::parse; non-semver selectors (e.g. workspace:*, git URLs) can crash
pacquet repo via a panic.
Code

pacquet/crates/cli/src/cli_args/repo.rs[R155-166]

+fn select_package_version(
+    package: &pacquet_registry::Package,
+    range: &str,
+) -> Option<std::sync::Arc<pacquet_registry::PackageVersion>> {
+    if range.is_empty() || range == "latest" {
+        return package.latest();
+    }
+    if let Some(tag_version) = package.dist_tag(range) {
+        return package.versions.get(tag_version);
+    }
+    package.pinned_version(range)
+}
Evidence
The repo command always ends up calling package.pinned_version(range) for any selector that is
not empty/latest/a dist-tag, and pinned_version() unconditionally unwrap()s the semver range
parse, so invalid ranges crash the process.

pacquet/crates/cli/src/cli_args/repo.rs[155-166]
pacquet/crates/registry/src/package.rs[139-142]
pacquet/crates/resolving-parse-wanted-dependency/src/lib.rs[38-60]
pacquet/crates/package-manifest/src/lib.rs[221-235]

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 repo` can panic when the CLI arg (or derived `range`) is not a valid semver range because `pacquet_registry::Package::pinned_version()` calls `version_range.parse().unwrap()`. `select_package_version()` calls `pinned_version()` for any selector that is not empty/`latest`/a dist-tag, so inputs like `foo@workspace:*` or other non-semver selectors can crash the process.
### Issue Context
`get_repo_url_from_registry()` uses `parse_wanted_dependency()` + `PackageManifest::resolve_registry_dependency()` to produce `(resolved_name, range)`, then `select_package_version(package, range)` decides which version to read `repository` from.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[155-166]
- pacquet/crates/registry/src/package.rs[139-156]
- pacquet/crates/package-manifest/src/lib.rs[221-235]
- pacquet/crates/resolving-parse-wanted-dependency/src/lib.rs[38-60]
### Implementation notes
- Make `Package::pinned_version()` non-panicking (e.g. `let range = version_range.parse().ok()?;`). This is the safest central fix.
- Optionally add input validation in `select_package_version()` to avoid calling `pinned_version()` for selectors that are clearly not semver ranges, returning `None` (and thus a structured `RepoError`) instead of crashing.

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


View more (9)
4. repo prints URL with NdjsonReporter 📎 Requirement gap ◔ Observability
Description
RepoArgs::run writes the repository URL to stdout via println!, even when dispatch_query::repo
selects NdjsonReporter/SilentReporter. This can corrupt NDJSON output (and breaks log-event-only
expectations), undermining the logging parity goal.
Code

pacquet/crates/cli/src/cli_args/repo.rs[R68-79]

+        for url in urls {
+            match open_url(&url) {
+                Ok(()) => {}
+                Err(e) => {
+                    let redacted = redact_url(&url);
+                    R::emit(&LogEvent::Pnpm(PnpmLog {
+                        level: LogLevel::Warn,
+                        message: format!("Could not open browser: {e}"),
+                        prefix: prefix.clone(),
+                    }));
+                    println!("{redacted}");
+                }
Evidence
The compliance checklist requires closing logging parity gaps so output matches the intended pnpm
logger schema. The code routes repo through NdjsonReporter, but still prints to stdout
(println!), which can interleave with NDJSON and violate the log-output contract.

Backfill missing log events in already-ported code (pacquet#347)
pacquet/crates/cli/src/cli_args/dispatch_query.rs[204-213]
pacquet/crates/cli/src/cli_args/repo.rs[68-79]

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

## Issue description
`pacquet repo` prints `redacted` URLs using `println!`, which bypasses the reporter system and can break NDJSON output when `ReporterType::Ndjson` is selected.
## Issue Context
`dispatch_query::repo` explicitly dispatches `RepoArgs::run` with `NdjsonReporter`/`SilentReporter`, so user-visible output should be emitted via `R::emit(...)` (or otherwise gated) rather than unconditional stdout printing.
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[68-79]
- pacquet/crates/cli/src/cli_args/dispatch_query.rs[204-213]

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


5. Bad peer range intersection 🐞 Bug ≡ Correctness
Description
merge_missing_peers() records an intersections[peer] entry but, when multiple non-equal ranges
exist, it stores issues[0].wanted_range instead of the actual semver intersection of all required
ranges. This breaks pnpm parity and makes --json output (and conflict/intersection classification)
incorrect for missing peers that have multiple wanted ranges.
Code

pacquet/crates/cli/src/cli_args/peers.rs[R326-335]

+        let ranges: Vec<&str> = issues.iter().map(|i| i.wanted_range.as_str()).collect();
+        let unique: HashSet<&&str> = ranges.iter().collect();
+        if unique.len() == 1 {
+            intersections.insert(peer_name.clone(), issues[0].wanted_range.clone());
+            continue;
+        }
+        let range_owned: Vec<String> = issues.iter().map(|i| i.wanted_range.clone()).collect();
+        if have_common_version(&range_owned) {
+            intersections.insert(peer_name.clone(), issues[0].wanted_range.clone());
+        } else {
Evidence
The Rust code inserts the first wanted range into intersections after only a compatibility check,
while upstream pnpm computes and stores the real intersection string; this makes Rust’s
intersections data wrong whenever multiple distinct wanted ranges intersect to a narrower range.

pacquet/crates/cli/src/cli_args/peers.rs[314-340]
pnpm11/deps/inspection/peers-checker/src/checkPeerDependencies.ts[219-244]

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

## Issue description
`merge_missing_peers()` currently sets `intersections[peer]` to `issues[0].wanted_range` when multiple wanted ranges exist and are deemed compatible via `have_common_version()`. This does not compute the actual range intersection (often narrower than the first range) and diverges from upstream pnpm which uses semver-range intersection.
## Issue Context
Upstream implementation (`pnpm11/.../checkPeerDependencies.ts`) computes `intersection = safeIntersect(ranges)` and stores that exact intersection string.
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/peers.rs[314-341]
## Suggested fix approach
- Replace the boolean `have_common_version(...)` pathway with a function that returns `Option<String>` representing the actual intersection string.
- Implement real semver-range intersection behavior (port the minimal logic used by `semver-range-intersect`, or add an internal helper/crate) and set `intersections[peer]` to that computed intersection.
- If intersection cannot be computed or is empty, classify as conflict (push to `conflicts`).

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


6. Repo URL terminal injection 🐞 Bug ⛨ Security
Description
On browser-open failure, pacquet repo prints the (attacker-controlled) repository URL directly to
stdout without stripping control characters. A crafted repository value from package.json or
registry metadata can inject terminal escape/control sequences into user output.
Code

pacquet/crates/cli/src/cli_args/repo.rs[R68-79]

+        for url in urls {
+            match open_url(&url) {
+                Ok(()) => {}
+                Err(e) => {
+                    let redacted = redact_url(&url);
+                    R::emit(&LogEvent::Pnpm(PnpmLog {
+                        level: LogLevel::Warn,
+                        message: format!("Could not open browser: {e}"),
+                        prefix: prefix.clone(),
+                    }));
+                    println!("{redacted}");
+                }
Evidence
The new repo command prints a URL on failure without using the repo’s dedicated terminal
sanitization helper, even though the sanitization helper is explicitly intended to prevent raw
escape sequences from reaching the terminal.

pacquet/crates/cli/src/cli_args/repo.rs[68-79]
pacquet/crates/cli/src/cli_args/sanitize.rs[3-17]
REVIEW_GUIDE.md[30-36]

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 repo` prints `redacted` directly via `println!("{redacted}")` on browser-open failure. Repository URLs come from attacker-controlled sources (local manifests / registry metadata), and may include control characters in cases where URL parsing fails and the code falls back to the raw string.
## Issue Context
The CLI already has `cli_args::sanitize::sanitize()` specifically to prevent stored/remote metadata from emitting raw escape sequences to the user's terminal.
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[68-79]
## Suggested fix approach
- Apply `sanitize()` before printing the fallback URL, e.g. `println!("{}", sanitize(&redacted));`.
- Consider also sanitizing the URL in any other direct-to-terminal output paths added by this command (stdout/stderr).

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


7. git+ shorthand not stripped 🐞 Bug ≡ Correctness
Description
try_hosted_shorthand() and try_hosted_url() attempt to strip git+ then git://, but the
second unwrap_or(raw_url) can revert to the original string and undo the git+ strip. This breaks
valid inputs like git+github:owner/repo by preventing hosted-shorthand detection and can make
pacquet repo fail to resolve a repository URL.
Code

pacquet/crates/cli/src/cli_args/repo.rs[R169-172]

+fn try_hosted_shorthand(raw_url: &str, directory: Option<&str>) -> Option<String> {
+    let cleaned =
+        raw_url.strip_prefix("git+").unwrap_or(raw_url).strip_prefix("git://").unwrap_or(raw_url);
+
Evidence
Both helpers compute cleaned such that failing to strip git:// falls back to the original
raw_url, undoing a successful git+ strip and preventing later strip_prefix("github:")/etc
checks from matching.

pacquet/crates/cli/src/cli_args/repo.rs[169-184]
pacquet/crates/cli/src/cli_args/repo.rs[257-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
`try_hosted_shorthand()` and `try_hosted_url()` compute `cleaned` by chaining `strip_prefix()` calls but fall back to `raw_url` on the second `unwrap_or(...)`, which can reintroduce `git+` and break hosted-shorthand parsing (e.g. `git+github:owner/repo`).
### Issue Context
This affects repository URL normalization for `pacquet repo` when a `repository.url` uses a hosted shorthand with a `git+` prefix.
### Fix
Preserve the already-stripped intermediate value when the `git://` prefix is not present, e.g.:
- `let cleaned = raw_url.strip_prefix("git+").unwrap_or(raw_url);`
- `let cleaned = cleaned.strip_prefix("git://").unwrap_or(cleaned);`
Add a unit test for at least:
- `repository_to_web_url("git+github:owner/repo", None)`
(and the analogous `git+gitlab:` / `git+bitbucket:` forms if supported).
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[169-172]
- pacquet/crates/cli/src/cli_args/repo.rs[257-260]

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


8. AllowedVersions selector misparsed 🐞 Bug ≡ Correctness
Description
parse_allowed_versions() misparses peerDependencyRules.allowedVersions: it splits on the first
@ (breaking scoped names like @scope/pkg) and it stores parent@range literally even though
filtering later matches only by declaring_parent.name, making documented parent@range>peer rules
unreachable and causing false peer-issue failures/exit(1).
Code

pacquet/crates/cli/src/cli_args/peers.rs[R469-494]

+fn parse_allowed_versions(
+    allowed: &HashMap<String, String>,
+) -> (AllowAllMatcher, AllowByParentMatcher) {
+    let mut match_all: HashMap<String, Vec<String>> = HashMap::new();
+    let mut by_parent: HashMap<String, HashMap<String, Vec<String>>> = HashMap::new();
+
+    for (selector, spec) in allowed {
+        if let Some((parent, target)) = selector.split_once('>') {
+            let parent_name = parent.trim().to_string();
+            let target_name = target.trim().to_string();
+            let ranges: Vec<String> = spec.split("||").map(|s| s.trim().to_string()).collect();
+            by_parent
+                .entry(parent_name)
+                .or_default()
+                .entry(target_name)
+                .or_default()
+                .extend(ranges);
+        } else {
+            let target_name = if let Some((name, _version)) = selector.split_once('@') {
+                name.to_string()
+            } else {
+                selector.clone()
+            };
+            let ranges: Vec<String> = spec.split("||").map(|s| s.trim().to_string()).collect();
+            match_all.entry(target_name).or_default().extend(ranges);
+        }
Evidence
The config docs explicitly claim allowedVersions supports parent@range>name, but
parse_allowed_versions() stores the parent selector verbatim and later matching looks up only by
parent **name**, so parent@range can never match. Additionally, splitting a selector using
split_once('@') will turn a scoped selector like @scope/pkg into an empty name key.

pacquet/crates/config/src/workspace_yaml.rs[458-466]
pacquet/crates/cli/src/cli_args/peers.rs[411-448]
pacquet/crates/cli/src/cli_args/peers.rs[469-498]

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

## Issue description
`peerDependencyRules.allowedVersions` selectors are parsed incorrectly:
- Global selectors with scoped package names (e.g. `@types/node`) are split at the leading `@` and become an empty key.
- Override selectors of the documented form `parent@range>peer` are stored with `parent@range` as the key, but matching later uses only the declaring parent **name**, so these rules can never apply.
This makes configured peer-dependency suppressions ineffective and can cause `pacquet peers` to exit non-zero unexpectedly.
### Issue Context
The config schema explicitly documents `allowedVersions` selectors supporting `parent>name` and `parent@range>name`.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/peers.rs[430-498]
- pacquet/crates/config/src/workspace_yaml.rs[458-466]
- pacquet/crates/cli/src/cli_args/peers/tests.rs[197-233]
### What to change
1. Implement selector parsing that:
- Preserves scoped package names (do not split the leading `@`).
- Supports `name@range` only when there is an `@` **after** the name (e.g. `react@^18`, `@scope/pkg@^2`).
- For `parent@range>peer`, either:
- strip `@range` from the parent key and additionally store the range so matching can check `satisfies(declaring_parent.version, range)`, or
- model a structured key (parent name + optional range) in the matcher map.
2. Update the matching logic in `filter_peer_issues()` accordingly (it currently uses `allow_by_parent.get(&declaring_parent.name)`).
3. Add unit tests:
- `allowedVersions: { "@types/node": "^20" }` should apply.
- `allowedVersions: { "foo@^1.0.0>react": "^18" }` should apply only when declaring parent version satisfies `^1.0.0`.

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


9. Peers scans all importers 🐞 Bug ➹ Performance
Description
check_peer_dependencies_from_lockfile() scans every lockfile importer whenever `lockfile_dir !=
dir (workspace), even when recursive` is false, which can report issues from unrelated projects
and causes large workspaces to do full-graph work unexpectedly.
Code

pacquet/crates/cli/src/cli_args/peers.rs[R123-170]

+    let importer_ids: Vec<String> = if recursive || config_has_workspace(lockfile_dir, dir) {
+        lockfile.importers.keys().cloned().collect()
+    } else {
+        vec![resolve_importer_id(lockfile_dir, dir)]
+    };
+
+    let mut result: IssuesByProjects = HashMap::new();
+
+    for importer_id in &importer_ids {
+        let Some(importer) = lockfile.importers.get(importer_id.as_str()) else { continue };
+
+        let mut issues = PeerIssues {
+            bad: HashMap::new(),
+            missing: HashMap::new(),
+            conflicts: Vec::new(),
+            intersections: HashMap::new(),
+        };
+
+        let mut visited = HashSet::new();
+
+        let groups = [
+            (&importer.dependencies, false),
+            (&importer.dev_dependencies, false),
+            (&importer.optional_dependencies, true),
+        ];
+
+        for (dep_map, _is_optional) in &groups {
+            let Some(dep_map) = dep_map else { continue };
+            for (alias, spec) in dep_map {
+                if let Some(key) = spec.version.resolved_key(alias) {
+                    walk_snapshot(&key, snapshots, packages, &[], &mut issues, &mut visited);
+                }
+            }
+        }
+
+        let merged = merge_missing_peers(&issues.missing);
+        issues.conflicts = merged.conflicts;
+        issues.intersections = merged.intersections;
+
+        result.insert(importer_id.clone(), issues);
+    }
+
+    result
+}
+
+fn config_has_workspace(lockfile_dir: &std::path::Path, dir: &std::path::Path) -> bool {
+    lockfile_dir != dir || lockfile_dir.parent().is_none()
+}
Evidence
Peers’ importer selection uses recursive || config_has_workspace(lockfile_dir, dir) where
config_has_workspace is true for lockfile_dir != dir, forcing full-workspace scans by default in
subprojects. In contrast, list resolves a single importer id based on
dir.strip_prefix(lockfile_dir) even when workspace_dir is set, indicating the intended
non-recursive behavior is single-importer scope.

pacquet/crates/cli/src/cli_args/peers.rs[112-170]
pacquet/crates/cli/src/cli_args/list.rs[104-136]

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

## Issue description
In `check_peer_dependencies_from_lockfile()`, importer selection expands to **all** importers when `config_has_workspace(lockfile_dir, dir)` is true. Since `config_has_workspace()` returns true whenever `lockfile_dir != dir`, running `pacquet peers` from a workspace subdirectory (without `--recursive`) still scans and reports across the entire workspace.
This is both a performance regression (full-graph traversal on big workspaces) and a correctness/UX issue (failures caused by unrelated workspace projects).
### Issue Context
Other CLI commands (e.g. `list`) compute a single importer id from `dir` vs `lockfile_dir` and operate on that importer unless recursion is explicitly requested.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/peers.rs[112-182]
- pacquet/crates/cli/src/cli_args/list.rs[104-136]
### What to change
1. Remove the implicit workspace fan-out from `check_peer_dependencies_from_lockfile()`.
- Use `lockfile.importers.keys()` only when `recursive` is true.
- Otherwise, operate only on `resolve_importer_id(lockfile_dir, dir)` (optionally falling back to root importer if missing, similar to `list`).
2. (Optional perf improvement) If recursive mode still needs to scan all importers, consider de-duplicating traversal work across importers (e.g., cache per-snapshot results) to avoid repeating the same graph walk N times.

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


10. Opener PATH/flag injection 🐞 Bug ⛨ Security
Description
open_url() executes xdg-open/open via PATH and passes the attacker-controlled URL as the first
argument without --, allowing PATH hijacking (running a repo-provided xdg-open) and option
injection when the URL starts with -. In environments where pacquet runs with a project-influenced
PATH (e.g. pnpm run), this can become arbitrary code execution under the user account.
Code

pacquet/crates/cli/src/cli_args/repo.rs[R324-333]

+fn open_url(url: &str) -> miette::Result<()> {
+    let result = {
+        #[cfg(target_os = "linux")]
+        {
+            std::process::Command::new("xdg-open").arg(url).spawn()
+        }
+        #[cfg(target_os = "macos")]
+        {
+            std::process::Command::new("open").arg(url).spawn()
+        }
Evidence
The PR introduces a new open_url() that executes PATH-resolved programs with attacker-controlled
URL arguments; REVIEW_GUIDE flags this class as security-sensitive, and existing CLI code
demonstrates a safer executable-resolution pattern.

pacquet/crates/cli/src/cli_args/repo.rs[324-333]
REVIEW_GUIDE.md[30-59]
pacquet/crates/cli/src/cli_args/exec.rs[160-168]

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

## Issue description
`open_url()` spawns `xdg-open`/`open` by program name (PATH resolution) and passes an untrusted URL without an end-of-options marker. This permits PATH hijacking (e.g., a repo-controlled `xdg-open` earlier in PATH) and option injection when the URL begins with `-`.
## Issue Context
- REVIEW_GUIDE.md explicitly calls out executable resolution and command-injection surfaces and treats manifest/registry metadata as attacker-controlled.
- Other CLI code (e.g. `exec`) explicitly resolves executables with `which` rather than relying on `Command::new("name")`.
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[324-333]
## Suggested implementation direction
- Resolve opener to an absolute path instead of relying on PATH at spawn time:
- macOS: prefer `/usr/bin/open`.
- Linux: attempt known paths (e.g. `/usr/bin/xdg-open`) and/or use `which::which("xdg-open")` as a fallback.
- If resolution fails or resolves inside the current workspace / `node_modules/.bin`, do not execute it; print the URL instead.
- Add an end-of-options marker when supported:
- `xdg-open -- <url>` and `open -- <url>`.
- Keep Windows on `ShellExecuteW` as-is.
- Consider refactoring `docs.rs` + `repo.rs` to share a single hardened helper to avoid repeating the risky pattern.

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


11. GitLab shorthand truncates path 🐞 Bug ≡ Correctness
Description
try_hosted_shorthand()/build_hosted_browse_url() build the browse URL from parts[..2], so
gitlab:group/subgroup/repo becomes https://gitlab.com/group/subgroup (wrong repository). This
breaks nested-group GitLab repos, which are common in practice.
Code

pacquet/crates/cli/src/cli_args/repo.rs[R186-205]

+    let fragment = extract_fragment(cleaned);
+    let path_clean = path.split(&['#', '?'][..]).next().unwrap_or(path).trim_end_matches('/');
+    let parts: Vec<&str> = path_clean.split('/').collect();
+    if parts.len() < 2 {
+        return None;
+    }
+
+    let hosted_base_url = &hosted.base_url;
+    let browse_path = format!("{hosted_base_url}/{}", parts[..2].join("/"));
+
+    let browse_url = if let Some(dir) = directory {
+        let default_branch = hosted.default_branch;
+        format!("{browse_path}/tree/{default_branch}/{}", dir.trim_start_matches('/'))
+    } else if let Some(branch) = fragment {
+        format!("{browse_path}/tree/{branch}")
+    } else {
+        browse_path
+    };
+
+    Some(browse_url)
Evidence
The repo URL builder slices hosted shorthand paths to the first two segments, which necessarily
drops additional segments for nested GitLab groups. Pacquet’s existing hosted-git shortcut parsing
instead splits at the last '/', preserving the full owner/group path before the repo name.

pacquet/crates/cli/src/cli_args/repo.rs[186-205]
pacquet/crates/cli/src/cli_args/repo.rs[293-311]
pacquet/crates/resolving-git-resolver/src/hosted_git.rs[180-207]

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 repo` truncates hosted shorthand paths to the first two segments when building web URLs. For GitLab shorthands with nested groups (e.g. `gitlab:group/subgroup/repo`), this drops the actual repo name and opens the wrong page.
### Issue Context
GitLab owner paths can contain multiple `/`-separated segments. The code currently assumes `owner/repo` by taking `parts[..2]`.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[169-206]
- pacquet/crates/cli/src/cli_args/repo.rs[287-312]
### Suggested fix
- Replace the `parts[..2]` logic with a split at the **last** `/`:
- `let (owner_path, repo) = path_clean.rsplit_once('/')?;`
- `browse_path = format!("{base}/{owner_path}/{repo}")` (and trim `.git` on `repo` as needed).
- Apply the same logic in both `try_hosted_shorthand()` and `build_hosted_browse_url()` so github/gitlab/bitbucket shorthands behave consistently.
- Add a unit test for `gitlab:group/subgroup/repo` (and optionally `github:org/team/repo` if you intend to support it similarly).

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


12. SCP git@ URL unsupported 🐞 Bug ≡ Correctness
Description
try_user_repo_shorthand() routes git@... repository values to try_hosted_url(), but
try_hosted_url() uses url::Url::parse, which cannot parse SCP-style SSH URLs like
git@github.com:owner/repo.git. As a result, pacquet repo will incorrectly fail to resolve
repository URLs for a common real-world repository format.
Code

pacquet/crates/cli/src/cli_args/repo.rs[R257-263]

+fn try_hosted_url(raw_url: &str, directory: Option<&str>) -> Option<String> {
+    let cleaned =
+        raw_url.strip_prefix("git+").unwrap_or(raw_url).strip_prefix("git://").unwrap_or(raw_url);
+
+    let parsed = url::Url::parse(cleaned).ok()?;
+    let host = parsed.host_str()?;
+
Evidence
try_user_repo_shorthand() explicitly routes git@... inputs to try_hosted_url(), but
try_hosted_url() relies on url::Url::parse, which does not accept scp-like SSH syntax, so the
resolution necessarily returns None for these common repository URLs.

pacquet/crates/cli/src/cli_args/repo.rs[208-213]
pacquet/crates/cli/src/cli_args/repo.rs[257-263]
pacquet/crates/resolving-git-resolver/src/hosted_git.rs[270-287]

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 repo` intends to support SSH repository URLs, but the scp-like form (`git@github.com:owner/repo.git[#branch]`) is not a valid hierarchical URL and therefore fails `url::Url::parse(...)` in `try_hosted_url()`. This makes `repository_to_web_url()` return `None` for common repository values.
### Issue Context
The code already checks `cleaned.starts_with("git@")` and routes to `try_hosted_url()`, so the intent is to support this shape; the parser choice is what blocks it.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[208-213]
- pacquet/crates/cli/src/cli_args/repo.rs[257-285]
### Suggested fix
1. In `try_hosted_url()`, add an explicit parser for scp-like SSH URLs:
- Accept optional `git+` prefix.
- Parse `git@<host>:<owner>/<repo>(.git)?` and optional `#<branch>`.
- For supported hosts (github.com/gitlab.com/bitbucket.org), convert to `https://<host>/<owner>/<repo>` and then append `/tree/<branch>` or `/tree/<default_branch>/<directory>` as appropriate.
2. Add unit tests covering at least:
- `git@github.com:test/pkg.git` -> `https://github.com/test/pkg`
- `git@github.com:test/pkg.git#main` -> `https://github.com/test/pkg/tree/main`
- directory handling if present (if you decide to support it for this syntax).

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



Remediation recommended

13. Sequential registry lookups 🐞 Bug ➹ Performance ⭐ New
Description
When RepoArgs::packages contains multiple entries, RepoArgs::run() fetches registry metadata
sequentially by awaiting each lookup inside a loop, making runtime roughly the sum of all request
latencies. This is avoidable because the command already has a shared ThrottledClient and a
network_concurrency setting, so it can fetch in parallel with bounded concurrency and then open
URLs in input order.
Code

pacquet/crates/cli/src/cli_args/repo.rs[R54-65]

+        let urls = if self.packages.is_empty() {
+            vec![get_repo_url_from_current_project(dir)?]
+        } else {
+            let mut urls = Vec::with_capacity(self.packages.len());
+            for pkg in &self.packages {
+                urls.push(
+                    get_repo_url_from_registry(config, pkg, &http_client, &registries, &retry_opts)
+                        .await?,
+                );
+            }
+            urls
+        };
Evidence
The code awaits each registry lookup inside a for loop, preventing overlap of network requests.
The same function already wires network_concurrency into the HTTP client settings, indicating
concurrency is an intended tuning knob for network work.

pacquet/crates/cli/src/cli_args/repo.rs[31-40]
pacquet/crates/cli/src/cli_args/repo.rs[54-65]

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 repo <pkg1> <pkg2> ...` performs N registry requests strictly sequentially (`await` inside a `for` loop), which scales latency linearly with the number of packages.

### Issue Context
This PR already constructs a shared `ThrottledClient` and has `NetworkSettings.network_concurrency`, but that concurrency knob is not used for multi-package repo lookups.

### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[31-65]

### Implementation direction
- Convert the per-package `get_repo_url_from_registry(...)` calls into a bounded-concurrency stream (e.g., `futures::stream::iter(pkgs).map(...).buffered(N)` or `buffer_unordered(N)`), where `N` is derived from `config.network_concurrency` (and clamped to `packages.len()` and a reasonable minimum like 1).
- Preserve stable output/open ordering by collecting results keyed by original index (or use `buffered` instead of unordered), then iterate URLs in the original argument order when calling `open_url()`.
- Keep error behavior consistent (first error should fail the command as it does today).

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


14. Directory URL not sanitized 🐞 Bug ⛨ Security
Description
repository_to_web_url() appends the attacker-controlled repository.directory into the final
browse URL without stripping ?/# or percent-encoding. This can produce malformed URLs or alter
how the browser interprets the URL (query/fragment confusion) when opening a repo from
registry/local manifests.
Code

pacquet/crates/cli/src/cli_args/repo.rs[R217-220]

+    Some(if let Some(dir) = directory {
+        let branch = fragment.as_deref().unwrap_or("HEAD");
+        format!("{base_url}/tree/{branch}/{}", dir.trim_start_matches('/'))
+    } else if let Some(branch) = fragment {
Evidence
The directory field is read verbatim from JSON and later appended into the constructed URL with
only a leading-slash trim; this allows reserved characters to be interpreted as query/fragment
delimiters. The separate redact_url() logic also demonstrates an intent to treat query/fragment as
sensitive/noisy, but those components can be reintroduced via directory.

pacquet/crates/cli/src/cli_args/repo.rs[168-180]
pacquet/crates/cli/src/cli_args/repo.rs[217-224]
pacquet/crates/cli/src/cli_args/repo.rs[433-443]

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

## Issue description
`repository.directory` is taken from JSON and interpolated directly into the browse URL path. If it contains reserved URL characters (e.g. `?`, `#`) or other non-path-safe characters, the resulting URL can be malformed or interpreted differently by the browser.
### Issue Context
This input is attacker-controlled via:
- local `package.json` `repository.directory`
- registry metadata `repository.directory`
### Fix approach
- Strip any query/fragment delimiters from `directory` (and consider normalizing `..` / `.` segments).
- Percent-encode path segments (branch and directory) before concatenation.
- Prefer constructing the URL via `url::Url` and `path_segments_mut()` rather than `format!(...)` string concatenation.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[168-180]
- pacquet/crates/cli/src/cli_args/repo.rs[217-224]
- pacquet/crates/cli/src/cli_args/repo.rs[260-267]
- pacquet/crates/cli/src/cli_args/repo.rs[310-317]
- pacquet/crates/cli/src/cli_args/repo.rs[355-362]
- pacquet/crates/cli/src/cli_args/repo.rs[381-388]

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


15. Repo needs network for local 🐞 Bug ☼ Reliability
Description
RepoArgs::run() always constructs a ThrottledClient and related registry/retry settings before
it knows whether it will do any registry lookups. This makes pacquet repo (no args; read local
package.json) unnecessarily dependent on proxy/TLS config validity and adds avoidable startup
work.
Code

pacquet/crates/cli/src/cli_args/repo.rs[R33-67]

+        let http_client = ThrottledClient::for_installs(
+            &config.proxy,
+            &config.tls,
+            &config.tls_by_uri,
+            &NetworkSettings {
+                network_concurrency: config.network_concurrency,
+                fetch_timeout: Duration::from_millis(config.fetch_timeout),
+                user_agent: config.user_agent.clone(),
+            },
+        )
+        .into_diagnostic()
+        .wrap_err("create the network client for repo")?;
+
+        let registries: HashMap<String, String> =
+            config.resolved_registries().into_iter().collect();
+
+        let retry_opts = RetryOpts {
+            retries: config.fetch_retries,
+            factor: config.fetch_retry_factor,
+            min_timeout: Duration::from_millis(config.fetch_retry_mintimeout),
+            max_timeout: Duration::from_millis(config.fetch_retry_maxtimeout),
+        };
+
+        let urls = if self.packages.is_empty() {
+            vec![get_repo_url_from_current_project(dir)?]
+        } else {
+            let mut urls = Vec::with_capacity(self.packages.len());
+            for pkg in &self.packages {
+                urls.push(
+                    get_repo_url_from_registry(config, pkg, &http_client, &registries, &retry_opts)
+                        .await?,
+                );
+            }
+            urls
+        };
Evidence
The local-manifest path (packages.is_empty()) only calls get_repo_url_from_current_project(dir)
and does not use the HTTP client, but ThrottledClient::for_installs(...) is created before this
branch. ThrottledClient::for_installs explicitly returns errors for invalid proxy/TLS and other
settings, so the local-only flow can fail even though it does not require network access.

pacquet/crates/cli/src/cli_args/repo.rs[25-67]
pacquet/crates/network/src/lib.rs[269-288]

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 repo` should be able to open the repository URL from the *local* `package.json` without initializing the network client. Today, `RepoArgs::run()` constructs `ThrottledClient::for_installs(...)` unconditionally, so the local-only path can fail early on invalid proxy/TLS settings (and does unnecessary setup work).
### Issue Context
The `self.packages.is_empty()` branch does not perform any registry fetches, but it still pays the cost (and failure modes) of building the HTTP client.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[25-67]
### Suggested fix
Move `ThrottledClient::for_installs(...)`, `registries`, and `retry_opts` construction into the `else` branch (only when `self.packages` is non-empty), or wrap them in a lazy initializer so they’re created only when needed.

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


View more (2)
16. Browser opener blocks CLI 🐞 Bug ☼ Reliability
Description
open_url() uses Command::status() on Linux/macOS, which blocks pacquet repo until
xdg-open/open exits. This can make the command appear stuck/slow on systems where the opener
waits, and is inconsistent with the existing docs command’s non-blocking .spawn() behavior.
Code

pacquet/crates/cli/src/cli_args/repo.rs[R397-407]

+fn open_url(url: &str) -> std::io::Result<()> {
+    #[cfg(target_os = "linux")]
+    {
+        let status = std::process::Command::new("xdg-open").arg(url).status()?;
+        if status.success() { Ok(()) } else { Err(std::io::Error::other("xdg-open failed")) }
+    }
+    #[cfg(target_os = "macos")]
+    {
+        let status = std::process::Command::new("open").arg(url).status()?;
+        if status.success() { Ok(()) } else { Err(std::io::Error::other("open failed")) }
+    }
Evidence
The new repo implementation waits on the opener process via .status() on Linux/macOS, while the
existing docs command uses .spawn() (non-blocking), demonstrating an inconsistency and showing
how to avoid blocking.

pacquet/crates/cli/src/cli_args/repo.rs[397-407]
pacquet/crates/cli/src/cli_args/docs.rs[87-97]

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 repo` blocks on Linux/macOS because it uses `Command::status()` to launch the platform opener. This waits for the opener process to exit, which can delay completion significantly depending on the opener implementation.
### Issue Context
`pacquet docs` already launches the browser in a non-blocking way (`spawn()`), suggesting the intended behavior is to fire-and-forget.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[397-407]
- pacquet/crates/cli/src/cli_args/docs.rs[87-97]
### Suggested fix
- On Linux/macOS, replace `.status()?` with `.spawn()?` and treat successful spawn as success.
- Keep the existing error path that logs/prints the redacted URL when spawning fails.

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


17. Full packument fetched unnecessarily 🐞 Bug ➹ Performance
Description
get_repo_url_from_registry() forces full_metadata: true, which downloads and parses a larger
packument than needed just to read a version’s repository field. This increases latency/bandwidth
and CPU/memory usage per package lookup (especially noticeable when passing multiple packages).
Code

pacquet/crates/cli/src/cli_args/repo.rs[R126-136]

+    let outcome = fetch_full_metadata(
+        resolved_name,
+        &FetchFullMetadataOptions {
+            registry: &registry,
+            http_client,
+            auth_headers: &config.auth_headers,
+            full_metadata: true,
+            etag: None,
+            modified: None,
+            retry_opts: *retry_opts,
+        },
Evidence
The new command explicitly opts into full packument fetching. The fetcher implementation in-repo
documents that abbreviated metadata exists and that the primary differences are fields unrelated to
opening repository URLs, indicating a clear opportunity to reduce work on this code path.

pacquet/crates/cli/src/cli_args/repo.rs[126-153]
pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata.rs[1-18]

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 repo <pkg...>` always requests full registry metadata (`full_metadata: true`). For this command, we only need the selected version’s `repository` field, so fetching the abbreviated packument should be sufficient and cheaper.
### Issue Context
The resolver already supports abbreviated vs full metadata, and the abbreviated form is documented (in-repo) as primarily omitting fields like `time`, `_npmUser`, and `dist.attestations`.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[126-136]
- pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata.rs[1-18]
### Suggested fix...

Comment thread pacquet/crates/cli/src/cli_args/repo.rs

@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: 3

🧹 Nitpick comments (3)
pacquet/crates/cli/src/cli_args/repo.rs (3)

377-379: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move unit tests to an external tests submodule and avoid the star import.

pacquet/CODE_STYLE_GUIDE.md requires unit tests in an external tests submodule (e.g., src/cli_args/repo/tests.rs) rather than inline, and disallows star imports in module bodies including tests. This inline mod tests { use super::*; } violates both.

As per path instructions: "Unit test layout: place unit tests in an external tests submodule (e.g., src/foo/tests.rs), not inline in the parent file" and "no star imports inside module bodies (keep explicit imports in normal modules/tests)."

🤖 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/repo.rs` around lines 377 - 379, Move the
unit tests out of the inline tests module in repo.rs into an external tests
submodule, following the repo module’s test layout convention, and replace the
star import in that test module with explicit imports. Update the existing tests
for the repo-related functionality so they live under a dedicated tests.rs (or
equivalent) and only import the specific items they use instead of use super::*.

Source: Path instructions


342-352: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a // SAFETY: justification for the ShellExecuteW FFI call.

The unsafe block has no comment explaining why the call is sound (valid null-terminated wide pointers, null operation/params). Per the repo conventions, unsafe should carry a clear justification; pacquet/AGENTS.md additionally asks in-code upstream references to pin a commit SHA.

As per coding guidelines: "Do not introduce unsafe without a clear justification and review."

🤖 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/repo.rs` around lines 342 - 352, Add a clear
// SAFETY: justification to the unsafe ShellExecuteW call in the repo URL
opening logic, explaining that the wide string pointer is valid and
null-terminated, and that the null operation/params are intentional and
acceptable for this Windows FFI call. Update the unsafe block around
ShellExecuteW in the URL-launching path to follow the repo’s unsafe conventions,
and include the required upstream reference with a pinned commit SHA if you are
documenting the external API behavior.

Source: Coding guidelines


66-77: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

A new ThrottledClient is built for every package on the registry path.

get_repo_url_from_registry is called once per package in run (lines 25-28), and each call constructs a fresh throttled HTTP client (connection pool). For multi-package invocations this rebuilds the client repeatedly. Consider constructing the client once in run and passing it in.

🤖 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/repo.rs` around lines 66 - 77,
get_repo_url_from_registry is rebuilding a new ThrottledClient on every package
lookup, which is wasteful for multi-package runs. Move the
ThrottledClient::for_installs construction out of get_repo_url_from_registry and
into run so the client is created once and reused, then pass the shared client
into get_repo_url_from_registry (and any helpers it calls) instead of recreating
the connection pool each time.
🤖 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/repo.rs`:
- Around line 54-57: The repository URL lookup in repo.rs is collapsing all
PackageManifest::from_path failures into RepoError::NoRepoUrlLocal, which hides
missing-file, parse, and IO diagnostics. Update the logic around
PackageManifest::from_path and pick_repo_url so manifest read/parse errors are
preserved and surfaced separately from the missing repository field case, using
RepoError variants that distinguish manifest-loading failures from “no
repository URL” in the same flow.
- Around line 60-64: Use the parsed version range when resolving the repository
manifest in get_repo_url_from_registry, because it currently resolves only by
name and bare specifier and ignores the requested version. Update the logic that
calls PackageManifest::resolve_registry_dependency so it incorporates the parsed
range from parse_wanted_dependency, and then use the resulting resolved package
info when looking up the repo URL so repo foo@1.2.3 opens the matching version
instead of the latest.
- Around line 151-159: The browse URL construction in the repo command currently
overwrites the directory-based path when try_extract_fragment(raw_url) returns a
fragment, causing directory information to be lost. Update the final_url logic
in the repository URL builder so the fragment fallback preserves any existing
directory path from directory rather than replacing it entirely, and adjust the
formatting in the repo URL assembly path to keep both the base_url and directory
context when a fragment is present.

---

Nitpick comments:
In `@pacquet/crates/cli/src/cli_args/repo.rs`:
- Around line 377-379: Move the unit tests out of the inline tests module in
repo.rs into an external tests submodule, following the repo module’s test
layout convention, and replace the star import in that test module with explicit
imports. Update the existing tests for the repo-related functionality so they
live under a dedicated tests.rs (or equivalent) and only import the specific
items they use instead of use super::*.
- Around line 342-352: Add a clear // SAFETY: justification to the unsafe
ShellExecuteW call in the repo URL opening logic, explaining that the wide
string pointer is valid and null-terminated, and that the null operation/params
are intentional and acceptable for this Windows FFI call. Update the unsafe
block around ShellExecuteW in the URL-launching path to follow the repo’s unsafe
conventions, and include the required upstream reference with a pinned commit
SHA if you are documenting the external API behavior.
- Around line 66-77: get_repo_url_from_registry is rebuilding a new
ThrottledClient on every package lookup, which is wasteful for multi-package
runs. Move the ThrottledClient::for_installs construction out of
get_repo_url_from_registry and into run so the client is created once and
reused, then pass the shared client into get_repo_url_from_registry (and any
helpers it calls) instead of recreating the connection pool each time.
🪄 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: 247eb84c-200c-4665-92d6-0eec20843340

📥 Commits

Reviewing files that changed from the base of the PR and between c00400d and 774d123.

📒 Files selected for processing (5)
  • pacquet/crates/cli/src/cli_args.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/src/cli_args/repo.rs

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 774d123

@codecov-commenter

codecov-commenter commented Jun 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.43345% with 113 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.09%. Comparing base (c00400d) to head (666fcc6).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/cli/src/cli_args/repo.rs 60.91% 111 Missing ⚠️
pacquet/crates/cli/src/cli_args/dispatch_query.rs 75.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12702      +/-   ##
==========================================
- Coverage   85.25%   85.09%   -0.17%     
==========================================
  Files         396      398       +2     
  Lines       60060    61561    +1501     
==========================================
+ Hits        51207    52387    +1180     
- Misses       8853     9174     +321     

☔ 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 Jun 27, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Commit: 666fcc674c1c

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.434 ± 0.209 4.174 4.861 1.68 ± 0.09
pacquet@main 4.348 ± 0.171 4.163 4.626 1.65 ± 0.08
pnpr@HEAD 2.642 ± 0.081 2.534 2.766 1.00
pnpr@main 2.785 ± 0.187 2.536 3.008 1.05 ± 0.08
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.433837114720001,
      "stddev": 0.20930403329673944,
      "median": 4.41370911912,
      "user": 4.05332546,
      "system": 2.1661827800000006,
      "min": 4.17392106862,
      "max": 4.86141149262,
      "times": [
        4.25476705362,
        4.52065670162,
        4.3993881236200005,
        4.30442826162,
        4.86141149262,
        4.676355697620001,
        4.45059239662,
        4.17392106862,
        4.42803011462,
        4.268820236620001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.34799034272,
      "stddev": 0.17066200336056894,
      "median": 4.32839994862,
      "user": 4.00662966,
      "system": 2.16897428,
      "min": 4.16315777262,
      "max": 4.625899860620001,
      "times": [
        4.364937240620001,
        4.189562001620001,
        4.393061414620001,
        4.619822095620001,
        4.625899860620001,
        4.4135782506200005,
        4.16315777262,
        4.24987478262,
        4.29186265662,
        4.16814735162
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.6422446187200004,
      "stddev": 0.08138507579652692,
      "median": 2.6525964006200002,
      "user": 2.9877334599999994,
      "system": 1.8524739799999999,
      "min": 2.53358285562,
      "max": 2.76615685162,
      "times": [
        2.57961811562,
        2.67913824662,
        2.70548860962,
        2.76615685162,
        2.68402718562,
        2.58128499362,
        2.62605455462,
        2.53358285562,
        2.72653091562,
        2.54056385862
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.78453706672,
      "stddev": 0.1874481404898183,
      "median": 2.81918574562,
      "user": 2.9571713599999994,
      "system": 1.84234868,
      "min": 2.53619006962,
      "max": 3.00775168162,
      "times": [
        2.98407627362,
        2.76397489862,
        2.90133332762,
        2.62923218762,
        2.53619006962,
        2.87439659262,
        2.97515713662,
        2.63126572362,
        2.5419927756200003,
        3.00775168162
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 482.5 ± 18.8 464.4 520.8 1.00 ± 0.05
pacquet@main 480.6 ± 11.3 469.1 507.8 1.00
pnpr@HEAD 564.7 ± 92.5 493.0 748.2 1.17 ± 0.19
pnpr@main 520.0 ± 19.6 499.7 562.1 1.08 ± 0.05
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.48248755704000007,
      "stddev": 0.01882911951046065,
      "median": 0.47600150654000006,
      "user": 0.38450598,
      "system": 0.7907888000000001,
      "min": 0.46435035754000004,
      "max": 0.52075916354,
      "times": [
        0.52075916354,
        0.49783724354000003,
        0.47401991054000003,
        0.50422158354,
        0.47773743054000006,
        0.46558850354000003,
        0.47868301354000004,
        0.47426558254,
        0.46435035754000004,
        0.46741278154000004
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.48062496503999996,
      "stddev": 0.011315773974443884,
      "median": 0.47724199604000006,
      "user": 0.38906838,
      "system": 0.7859508,
      "min": 0.46912468354000003,
      "max": 0.50780882154,
      "times": [
        0.49047636454000004,
        0.47738546254,
        0.47129176054000005,
        0.46912468354000003,
        0.47629452454000004,
        0.47709852954000004,
        0.47836555354000004,
        0.50780882154,
        0.47439542854000005,
        0.48400852154
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.56467989994,
      "stddev": 0.09254383793316508,
      "median": 0.5145622275399999,
      "user": 0.39104037999999997,
      "system": 0.8213991,
      "min": 0.49297145354000005,
      "max": 0.74816349354,
      "times": [
        0.50430018754,
        0.5202284175399999,
        0.49297145354000005,
        0.50077403054,
        0.49839175554000004,
        0.74816349354,
        0.68721740754,
        0.64008742454,
        0.54576879154,
        0.50889603754
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.5200231691400001,
      "stddev": 0.01955773050112286,
      "median": 0.5177117390399999,
      "user": 0.39444747999999996,
      "system": 0.8123631999999998,
      "min": 0.49971625854000007,
      "max": 0.56210066254,
      "times": [
        0.56210066254,
        0.52678886754,
        0.50453177854,
        0.50044763054,
        0.52258796954,
        0.50566752654,
        0.53348986654,
        0.53206562254,
        0.5128355085399999,
        0.49971625854000007
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.598 ± 0.060 4.536 4.727 1.79 ± 0.06
pacquet@main 4.613 ± 0.045 4.541 4.684 1.80 ± 0.06
pnpr@HEAD 2.653 ± 0.118 2.512 2.885 1.03 ± 0.06
pnpr@main 2.568 ± 0.087 2.444 2.718 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.597548531680001,
      "stddev": 0.05984670527226594,
      "median": 4.58521920688,
      "user": 4.171333199999999,
      "system": 2.12097828,
      "min": 4.5357153638800005,
      "max": 4.72697472888,
      "times": [
        4.57823836888,
        4.64385301088,
        4.5854754378800004,
        4.59657504788,
        4.58496297588,
        4.5357153638800005,
        4.64410064388,
        4.72697472888,
        4.539869690880001,
        4.53972004788
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.61349409968,
      "stddev": 0.04493876801625252,
      "median": 4.60119291288,
      "user": 4.134300299999999,
      "system": 2.13837848,
      "min": 4.54115756088,
      "max": 4.68380440388,
      "times": [
        4.66366920288,
        4.60498930588,
        4.58404671088,
        4.68380440388,
        4.64022816988,
        4.58987485788,
        4.65366024588,
        4.54115756088,
        4.59739651988,
        4.57611401888
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.6529518042799998,
      "stddev": 0.11818916167587372,
      "median": 2.60551963438,
      "user": 2.8227588,
      "system": 1.8136042800000003,
      "min": 2.51239425488,
      "max": 2.88487477888,
      "times": [
        2.56194057888,
        2.80016198488,
        2.59320493288,
        2.5979977068799998,
        2.75138018388,
        2.88487477888,
        2.51239425488,
        2.61657826188,
        2.61304156188,
        2.5979437978799997
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.56827112648,
      "stddev": 0.08686611807468356,
      "median": 2.5492706938799996,
      "user": 2.8106025000000003,
      "system": 1.78531618,
      "min": 2.44434561388,
      "max": 2.71845584088,
      "times": [
        2.59967487188,
        2.70174112188,
        2.58383280888,
        2.44434561388,
        2.50421390988,
        2.71845584088,
        2.56476566088,
        2.5337757268799996,
        2.52533286788,
        2.5065728418799997
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.146 ± 0.007 1.137 1.158 2.33 ± 0.03
pacquet@main 1.238 ± 0.146 1.122 1.532 2.52 ± 0.30
pnpr@HEAD 0.491 ± 0.006 0.480 0.500 1.00
pnpr@main 0.496 ± 0.008 0.482 0.513 1.01 ± 0.02
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.1461372877399998,
      "stddev": 0.006771784830264861,
      "median": 1.14453097534,
      "user": 1.3079899400000001,
      "system": 1.01221564,
      "min": 1.1369686618400001,
      "max": 1.15835862284,
      "times": [
        1.14042029084,
        1.14348995484,
        1.14536068184,
        1.1369686618400001,
        1.14764477784,
        1.15171029984,
        1.13983470284,
        1.1437012688400001,
        1.15835862284,
        1.15388361584
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.23762315304,
      "stddev": 0.1456238624164348,
      "median": 1.1591980313399999,
      "user": 1.36485364,
      "system": 1.08676374,
      "min": 1.12207180584,
      "max": 1.5317467748400002,
      "times": [
        1.5317467748400002,
        1.4300730268400001,
        1.14512243484,
        1.12207180584,
        1.18204935184,
        1.14878123884,
        1.14444685784,
        1.3535439768400002,
        1.15984467984,
        1.15855138284
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.49146234734000005,
      "stddev": 0.005866105683589101,
      "median": 0.4912456203400001,
      "user": 0.33201004,
      "system": 0.74892884,
      "min": 0.47951921084000004,
      "max": 0.4996506958400001,
      "times": [
        0.47951921084000004,
        0.49238176384000004,
        0.48653777284000005,
        0.49067347384000004,
        0.4913174658400001,
        0.48901878684000005,
        0.4911737748400001,
        0.4996506958400001,
        0.49838852984000004,
        0.49596199884000003
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.49646039904000006,
      "stddev": 0.007963619886198833,
      "median": 0.49704245834000005,
      "user": 0.33581364,
      "system": 0.7535568400000001,
      "min": 0.48172666884000004,
      "max": 0.5132057228400001,
      "times": [
        0.49425900784000004,
        0.49377807784000005,
        0.48172666884000004,
        0.49835559084000003,
        0.5132057228400001,
        0.49591462984000007,
        0.49948226384000005,
        0.49933635684000005,
        0.49037538484000004,
        0.49817028684000003
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.854 ± 0.051 2.815 2.990 5.78 ± 0.12
pacquet@main 2.867 ± 0.056 2.818 3.014 5.81 ± 0.13
pnpr@HEAD 0.510 ± 0.003 0.507 0.514 1.03 ± 0.01
pnpr@main 0.493 ± 0.005 0.487 0.504 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.8537572791800003,
      "stddev": 0.05115658378873497,
      "median": 2.8402476616800003,
      "user": 1.8129241999999994,
      "system": 1.20650736,
      "min": 2.8148536706800003,
      "max": 2.9899576966800003,
      "times": [
        2.8235046506800003,
        2.8533254346800003,
        2.84104101668,
        2.82462836468,
        2.8148536706800003,
        2.86853084968,
        2.8394543066800004,
        2.8208651486800003,
        2.9899576966800003,
        2.86141165268
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.86689964478,
      "stddev": 0.05558179425867631,
      "median": 2.8607931351800002,
      "user": 1.7932012,
      "system": 1.22589526,
      "min": 2.81821879168,
      "max": 3.01407336968,
      "times": [
        2.86270799068,
        2.83269366768,
        2.83388184068,
        2.85940156968,
        2.87450073768,
        2.87920111668,
        2.81821879168,
        2.8621847006800003,
        3.01407336968,
        2.8321326626800003
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.5104732758800001,
      "stddev": 0.0026244087773728227,
      "median": 0.5111204801800001,
      "user": 0.3399696,
      "system": 0.7827311600000001,
      "min": 0.5066447416800001,
      "max": 0.5136454246800001,
      "times": [
        0.5099037566800001,
        0.5120543916800001,
        0.50727857868,
        0.5072351836800001,
        0.51291955168,
        0.5066447416800001,
        0.5123615196800001,
        0.5136454246800001,
        0.5101865686800001,
        0.51250304168
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.49346316848000005,
      "stddev": 0.00528490700367657,
      "median": 0.49378568868000006,
      "user": 0.3386639,
      "system": 0.7441559600000001,
      "min": 0.48692545868000003,
      "max": 0.5043466886800001,
      "times": [
        0.49540816368,
        0.48920729668,
        0.5043466886800001,
        0.49575648468,
        0.48692545868000003,
        0.49422460268,
        0.49769273168,
        0.48990959068,
        0.49334677468000004,
        0.48781389268
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12702
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,597.55 ms
(-3.19%)Baseline: 4,749.26 ms
5,699.11 ms
(80.67%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
2,853.76 ms
(-6.27%)Baseline: 3,044.82 ms
3,653.78 ms
(78.10%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,146.14 ms
(-15.42%)Baseline: 1,355.02 ms
1,626.02 ms
(70.49%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
4,433.84 ms
(-8.35%)Baseline: 4,837.58 ms
5,805.09 ms
(76.38%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
482.49 ms
(-25.62%)Baseline: 648.68 ms
778.42 ms
(61.98%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12702
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,652.95 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
510.47 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
491.46 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,642.24 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
564.68 ms
🐰 View full continuous benchmarking report in Bencher

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

Copy link
Copy Markdown

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

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

Copy link
Copy Markdown

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

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

Copy link
Copy Markdown

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

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

Copy link
Copy Markdown

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

@github-actions github-actions Bot added the reviewed: coderabbit CodeRabbit submitted an approving review label Jun 28, 2026
@kairosci kairosci changed the title feat(pacquet-cli): implement pnpm repo command feat(pacquet-cli): implement native 'pnpm repo' command Jun 28, 2026

@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

🤖 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/tests/repo.rs`:
- Around line 25-30: The repo CLI integration tests are only checking a broad
stderr substring, so they can miss regressions in the pnpm error contract.
Update the assertions in the repo tests to match the user-visible output from
RepoError::NoRepoUrlLocal in cli_args/repo.rs by verifying the specific error
code ERR_PNPM_NO_REPO_URL and the local-project guidance text, rather than just
“repository URL”. Use the existing repo test cases to pin the exact stderr
contract so behavior stays aligned with pnpm.
🪄 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: 3b28e54c-5277-4ff9-9dca-cf16ce758820

📥 Commits

Reviewing files that changed from the base of the PR and between d94c0ba and f4e4dc3.

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

Comment thread pacquet/crates/cli/tests/repo.rs
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

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

Copy link
Copy Markdown

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

…ion tests

Verify both the error code and the guidance text in stderr assertions
to prevent regressions in the user-visible error contract.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

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

Copy link
Copy Markdown

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

@kairosci kairosci changed the title feat(pacquet-cli): implement native 'pnpm repo' command feat(pacquet-cli): implement pnpm peers command Jun 28, 2026
Comment on lines +469 to +494
fn parse_allowed_versions(
allowed: &HashMap<String, String>,
) -> (AllowAllMatcher, AllowByParentMatcher) {
let mut match_all: HashMap<String, Vec<String>> = HashMap::new();
let mut by_parent: HashMap<String, HashMap<String, Vec<String>>> = HashMap::new();

for (selector, spec) in allowed {
if let Some((parent, target)) = selector.split_once('>') {
let parent_name = parent.trim().to_string();
let target_name = target.trim().to_string();
let ranges: Vec<String> = spec.split("||").map(|s| s.trim().to_string()).collect();
by_parent
.entry(parent_name)
.or_default()
.entry(target_name)
.or_default()
.extend(ranges);
} else {
let target_name = if let Some((name, _version)) = selector.split_once('@') {
name.to_string()
} else {
selector.clone()
};
let ranges: Vec<String> = spec.split("||").map(|s| s.trim().to_string()).collect();
match_all.entry(target_name).or_default().extend(ranges);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Allowedversions selector misparsed 🐞 Bug ≡ Correctness

parse_allowed_versions() misparses peerDependencyRules.allowedVersions: it splits on the first
@ (breaking scoped names like @scope/pkg) and it stores parent@range literally even though
filtering later matches only by declaring_parent.name, making documented parent@range>peer rules
unreachable and causing false peer-issue failures/exit(1).
Agent Prompt
### Issue description
`peerDependencyRules.allowedVersions` selectors are parsed incorrectly:
- Global selectors with scoped package names (e.g. `@types/node`) are split at the leading `@` and become an empty key.
- Override selectors of the documented form `parent@range>peer` are stored with `parent@range` as the key, but matching later uses only the declaring parent **name**, so these rules can never apply.

This makes configured peer-dependency suppressions ineffective and can cause `pacquet peers` to exit non-zero unexpectedly.

### Issue Context
The config schema explicitly documents `allowedVersions` selectors supporting `parent>name` and `parent@range>name`.

### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/peers.rs[430-498]
- pacquet/crates/config/src/workspace_yaml.rs[458-466]
- pacquet/crates/cli/src/cli_args/peers/tests.rs[197-233]

### What to change
1. Implement selector parsing that:
   - Preserves scoped package names (do not split the leading `@`).
   - Supports `name@range` only when there is an `@` **after** the name (e.g. `react@^18`, `@scope/pkg@^2`).
   - For `parent@range>peer`, either:
     - strip `@range` from the parent key and additionally store the range so matching can check `satisfies(declaring_parent.version, range)`, or
     - model a structured key (parent name + optional range) in the matcher map.
2. Update the matching logic in `filter_peer_issues()` accordingly (it currently uses `allow_by_parent.get(&declaring_parent.name)`).
3. Add unit tests:
   - `allowedVersions: { "@types/node": "^20" }` should apply.
   - `allowedVersions: { "foo@^1.0.0>react": "^18" }` should apply only when declaring parent version satisfies `^1.0.0`.

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

Comment on lines +123 to +170
let importer_ids: Vec<String> = if recursive || config_has_workspace(lockfile_dir, dir) {
lockfile.importers.keys().cloned().collect()
} else {
vec![resolve_importer_id(lockfile_dir, dir)]
};

let mut result: IssuesByProjects = HashMap::new();

for importer_id in &importer_ids {
let Some(importer) = lockfile.importers.get(importer_id.as_str()) else { continue };

let mut issues = PeerIssues {
bad: HashMap::new(),
missing: HashMap::new(),
conflicts: Vec::new(),
intersections: HashMap::new(),
};

let mut visited = HashSet::new();

let groups = [
(&importer.dependencies, false),
(&importer.dev_dependencies, false),
(&importer.optional_dependencies, true),
];

for (dep_map, _is_optional) in &groups {
let Some(dep_map) = dep_map else { continue };
for (alias, spec) in dep_map {
if let Some(key) = spec.version.resolved_key(alias) {
walk_snapshot(&key, snapshots, packages, &[], &mut issues, &mut visited);
}
}
}

let merged = merge_missing_peers(&issues.missing);
issues.conflicts = merged.conflicts;
issues.intersections = merged.intersections;

result.insert(importer_id.clone(), issues);
}

result
}

fn config_has_workspace(lockfile_dir: &std::path::Path, dir: &std::path::Path) -> bool {
lockfile_dir != dir || lockfile_dir.parent().is_none()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Peers scans all importers 🐞 Bug ➹ Performance

check_peer_dependencies_from_lockfile() scans every lockfile importer whenever `lockfile_dir !=
dir (workspace), even when recursive` is false, which can report issues from unrelated projects
and causes large workspaces to do full-graph work unexpectedly.
Agent Prompt
### Issue description
In `check_peer_dependencies_from_lockfile()`, importer selection expands to **all** importers when `config_has_workspace(lockfile_dir, dir)` is true. Since `config_has_workspace()` returns true whenever `lockfile_dir != dir`, running `pacquet peers` from a workspace subdirectory (without `--recursive`) still scans and reports across the entire workspace.

This is both a performance regression (full-graph traversal on big workspaces) and a correctness/UX issue (failures caused by unrelated workspace projects).

### Issue Context
Other CLI commands (e.g. `list`) compute a single importer id from `dir` vs `lockfile_dir` and operate on that importer unless recursion is explicitly requested.

### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/peers.rs[112-182]
- pacquet/crates/cli/src/cli_args/list.rs[104-136]

### What to change
1. Remove the implicit workspace fan-out from `check_peer_dependencies_from_lockfile()`.
   - Use `lockfile.importers.keys()` only when `recursive` is true.
   - Otherwise, operate only on `resolve_importer_id(lockfile_dir, dir)` (optionally falling back to root importer if missing, similar to `list`).
2. (Optional perf improvement) If recursive mode still needs to scan all importers, consider de-duplicating traversal work across importers (e.g., cache per-snapshot results) to avoid repeating the same graph walk N times.

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

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

Copy link
Copy Markdown

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

Comment on lines +169 to +172
fn try_hosted_shorthand(raw_url: &str, directory: Option<&str>) -> Option<String> {
let cleaned =
raw_url.strip_prefix("git+").unwrap_or(raw_url).strip_prefix("git://").unwrap_or(raw_url);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Git+ shorthand not stripped 🐞 Bug ≡ Correctness

try_hosted_shorthand() and try_hosted_url() attempt to strip git+ then git://, but the
second unwrap_or(raw_url) can revert to the original string and undo the git+ strip. This breaks
valid inputs like git+github:owner/repo by preventing hosted-shorthand detection and can make
pacquet repo fail to resolve a repository URL.
Agent Prompt
### Issue description
`try_hosted_shorthand()` and `try_hosted_url()` compute `cleaned` by chaining `strip_prefix()` calls but fall back to `raw_url` on the second `unwrap_or(...)`, which can reintroduce `git+` and break hosted-shorthand parsing (e.g. `git+github:owner/repo`).

### Issue Context
This affects repository URL normalization for `pacquet repo` when a `repository.url` uses a hosted shorthand with a `git+` prefix.

### Fix
Preserve the already-stripped intermediate value when the `git://` prefix is not present, e.g.:
- `let cleaned = raw_url.strip_prefix("git+").unwrap_or(raw_url);`
- `let cleaned = cleaned.strip_prefix("git://").unwrap_or(cleaned);`

Add a unit test for at least:
- `repository_to_web_url("git+github:owner/repo", None)`
(and the analogous `git+gitlab:` / `git+bitbucket:` forms if supported).

### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[169-172]
- pacquet/crates/cli/src/cli_args/repo.rs[257-260]

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

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

Copy link
Copy Markdown

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

Comment on lines +68 to +79
for url in urls {
match open_url(&url) {
Ok(()) => {}
Err(e) => {
let redacted = redact_url(&url);
R::emit(&LogEvent::Pnpm(PnpmLog {
level: LogLevel::Warn,
message: format!("Could not open browser: {e}"),
prefix: prefix.clone(),
}));
println!("{redacted}");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. repo prints url with ndjsonreporter 📎 Requirement gap ◔ Observability

RepoArgs::run writes the repository URL to stdout via println!, even when dispatch_query::repo
selects NdjsonReporter/SilentReporter. This can corrupt NDJSON output (and breaks log-event-only
expectations), undermining the logging parity goal.
Agent Prompt
## Issue description
`pacquet repo` prints `redacted` URLs using `println!`, which bypasses the reporter system and can break NDJSON output when `ReporterType::Ndjson` is selected.

## Issue Context
`dispatch_query::repo` explicitly dispatches `RepoArgs::run` with `NdjsonReporter`/`SilentReporter`, so user-visible output should be emitted via `R::emit(...)` (or otherwise gated) rather than unconditional stdout printing.

## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[68-79]
- pacquet/crates/cli/src/cli_args/dispatch_query.rs[204-213]

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

Comment on lines +326 to +335
let ranges: Vec<&str> = issues.iter().map(|i| i.wanted_range.as_str()).collect();
let unique: HashSet<&&str> = ranges.iter().collect();
if unique.len() == 1 {
intersections.insert(peer_name.clone(), issues[0].wanted_range.clone());
continue;
}
let range_owned: Vec<String> = issues.iter().map(|i| i.wanted_range.clone()).collect();
if have_common_version(&range_owned) {
intersections.insert(peer_name.clone(), issues[0].wanted_range.clone());
} else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Bad peer range intersection 🐞 Bug ≡ Correctness

merge_missing_peers() records an intersections[peer] entry but, when multiple non-equal ranges
exist, it stores issues[0].wanted_range instead of the actual semver intersection of all required
ranges. This breaks pnpm parity and makes --json output (and conflict/intersection classification)
incorrect for missing peers that have multiple wanted ranges.
Agent Prompt
## Issue description
`merge_missing_peers()` currently sets `intersections[peer]` to `issues[0].wanted_range` when multiple wanted ranges exist and are deemed compatible via `have_common_version()`. This does not compute the actual range intersection (often narrower than the first range) and diverges from upstream pnpm which uses semver-range intersection.

## Issue Context
Upstream implementation (`pnpm11/.../checkPeerDependencies.ts`) computes `intersection = safeIntersect(ranges)` and stores that exact intersection string.

## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/peers.rs[314-341]

## Suggested fix approach
- Replace the boolean `have_common_version(...)` pathway with a function that returns `Option<String>` representing the actual intersection string.
- Implement real semver-range intersection behavior (port the minimal logic used by `semver-range-intersect`, or add an internal helper/crate) and set `intersections[peer]` to that computed intersection.
- If intersection cannot be computed or is empty, classify as conflict (push to `conflicts`).

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

Comment on lines +68 to +79
for url in urls {
match open_url(&url) {
Ok(()) => {}
Err(e) => {
let redacted = redact_url(&url);
R::emit(&LogEvent::Pnpm(PnpmLog {
level: LogLevel::Warn,
message: format!("Could not open browser: {e}"),
prefix: prefix.clone(),
}));
println!("{redacted}");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Repo url terminal injection 🐞 Bug ⛨ Security

On browser-open failure, pacquet repo prints the (attacker-controlled) repository URL directly to
stdout without stripping control characters. A crafted repository value from package.json or
registry metadata can inject terminal escape/control sequences into user output.
Agent Prompt
## Issue description
`pacquet repo` prints `redacted` directly via `println!("{redacted}")` on browser-open failure. Repository URLs come from attacker-controlled sources (local manifests / registry metadata), and may include control characters in cases where URL parsing fails and the code falls back to the raw string.

## Issue Context
The CLI already has `cli_args::sanitize::sanitize()` specifically to prevent stored/remote metadata from emitting raw escape sequences to the user's terminal.

## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[68-79]

## Suggested fix approach
- Apply `sanitize()` before printing the fallback URL, e.g. `println!("{}", sanitize(&redacted));`.
- Consider also sanitizing the URL in any other direct-to-terminal output paths added by this command (stdout/stderr).

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

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

Copy link
Copy Markdown

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

Comment on lines +155 to +166
fn select_package_version(
package: &pacquet_registry::Package,
range: &str,
) -> Option<std::sync::Arc<pacquet_registry::PackageVersion>> {
if range.is_empty() || range == "latest" {
return package.latest();
}
if let Some(tag_version) = package.dist_tag(range) {
return package.versions.get(tag_version);
}
package.pinned_version(range)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Repo range parse panic 🐞 Bug ☼ Reliability

select_package_version() forwards the user/manifest selector into Package::pinned_version(),
which unwrap()s Range::parse; non-semver selectors (e.g. workspace:*, git URLs) can crash
pacquet repo via a panic.
Agent Prompt
### Issue description
`pacquet repo` can panic when the CLI arg (or derived `range`) is not a valid semver range because `pacquet_registry::Package::pinned_version()` calls `version_range.parse().unwrap()`. `select_package_version()` calls `pinned_version()` for any selector that is not empty/`latest`/a dist-tag, so inputs like `foo@workspace:*` or other non-semver selectors can crash the process.

### Issue Context
`get_repo_url_from_registry()` uses `parse_wanted_dependency()` + `PackageManifest::resolve_registry_dependency()` to produce `(resolved_name, range)`, then `select_package_version(package, range)` decides which version to read `repository` from.

### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[155-166]
- pacquet/crates/registry/src/package.rs[139-156]
- pacquet/crates/package-manifest/src/lib.rs[221-235]
- pacquet/crates/resolving-parse-wanted-dependency/src/lib.rs[38-60]

### Implementation notes
- Make `Package::pinned_version()` non-panicking (e.g. `let range = version_range.parse().ok()?;`). This is the safest central fix.
- Optionally add input validation in `select_package_version()` to avoid calling `pinned_version()` for selectors that are clearly not semver ranges, returning `None` (and thus a structured `RepoError`) instead of crashing.

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

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

Copy link
Copy Markdown

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

- SCP-style SSH URL parsing (git@github.com:owner/repo.git)
- Preserve PackageManifestError details for non-missing-manifest errors
- Combine directory and fragment in browse URLs
- Use Config-based RetryOpts instead of Default
- Check browser exit status via .status() instead of .spawn()
- Strip .git suffix in hosted shorthand and build_hosted_browse_url
- Use tempfile::TempDir in tests to avoid collisions
- Strip fragment/query from base_url before appending path
- Redact query and fragment in URL fallback output
- Select version matching user spec (dist-tag / semver range)
- Normalize git:// to https:// for web URL construction
- Thread Reporter through RepoArgs::run for structured log events
- Hoist ThrottledClient and registries map outside per-package loop
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

@kairosci kairosci changed the title feat(pacquet-cli): implement pnpm peers command fix(pacquet-cli): address review findings on pnpm repo command Jun 28, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

@kairosci kairosci changed the title fix(pacquet-cli): address review findings on pnpm repo command feat(pacquet-cli): implement pnpm repo command Jun 28, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

Comment on lines +198 to +207
let mut parsed = url::Url::parse(&cleaned).ok()?;
if parsed.scheme() != "http" && parsed.scheme() != "https" {
return None;
}

let fragment = try_extract_fragment(raw_url);
parsed.set_fragment(None);
parsed.set_query(None);

let mut url = parsed.to_string();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Repo url leaks credentials 🐞 Bug ⛨ Security

repository_to_web_url() strips query/fragment but preserves URL userinfo (username/password), so
repository values like https://token@github.com/org/repo.git will be passed to
xdg-open/open/ShellExecuteW with the secret intact. This can leak credentials via process
arguments and cause unintended credential transmission/storage outside the intended auth flow.
Agent Prompt
### Issue description
`repository_to_web_url()` constructs the final browser URL from a parsed `url::Url`, but never clears `username`/`password`. Any credentials embedded in `repository.url` (from local `package.json` or registry metadata) will survive into the URL that is opened.

### Issue Context
Other parts of this repo treat URL userinfo as sensitive and explicitly strip/redact it.

### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[198-207]

### Suggested fix
After parsing and scheme validation (and alongside the existing `set_query(None)` / `set_fragment(None)`), clear userinfo before calling `to_string()`:
- `let _ = parsed.set_username("");`
- `let _ = parsed.set_password(None);`

This ensures the success path never forwards embedded credentials to the OS/browser opener.

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

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

Copy link
Copy Markdown

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

Comment on lines +234 to +242
let (hosted, path) = if let Some(rest) = cleaned.strip_prefix("github:") {
(HostedRepo { base_url: "https://github.com".to_string(), default_branch: "master" }, rest)
} else if let Some(rest) = cleaned.strip_prefix("gitlab:") {
(HostedRepo { base_url: "https://gitlab.com".to_string(), default_branch: "master" }, rest)
} else if let Some(rest) = cleaned.strip_prefix("bitbucket:") {
(
HostedRepo { base_url: "https://bitbucket.org".to_string(), default_branch: "master" },
rest,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Hardcoded master branch 🐞 Bug ≡ Correctness

try_hosted_shorthand()/try_hosted_url() default to master when building browse URLs for
monorepo repository.directory, so many repos with default branch main (or any non-master) will
open a broken/wrong URL. This diverges from pnpm’s reference implementation, which uses HEAD to
follow the host’s default branch when appending a directory.
Agent Prompt
### Issue description
`pacquet repo` builds hosted browse URLs with `/tree/master/...` when `repository.directory` is present and no fragment/branch is specified. This is wrong for repositories whose default branch is not `master` (very common: `main`) and breaks parity with pnpm, which uses `HEAD` to track the default branch.

### Issue Context
The Rust implementation already uses `HEAD` in the generic URL path when `directory` is set, but the hosted-shorthand/hosted-url paths hardcode `master`, creating inconsistent behavior depending on which parser path matches.

### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[215-222]
- pacquet/crates/cli/src/cli_args/repo.rs[234-266]
- pacquet/crates/cli/src/cli_args/repo.rs[306-316]
- pacquet/crates/cli/src/cli_args/repo.rs[341-360]

### Suggested change
- Replace the hosted default branch fallback from `master` to `HEAD` anywhere it’s only used when `directory` is present and no explicit fragment/branch was provided.
- Keep behavior consistent across:
  - `try_hosted_shorthand()`
  - `try_user_repo_shorthand()`
  - `try_hosted_url()`
- Update/add unit tests to assert `/tree/HEAD/...` for directory cases in hosted shorthand and hosted URL cases (matching pnpm).

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

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 666fcc6

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 666fcc6

@zkochan zkochan merged commit da9f834 into pnpm:main Jun 29, 2026
32 checks passed
@kairosci kairosci deleted the paquet-repo branch June 29, 2026 20:56
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