Skip to content

feat(pacquet): implement bugs command#12687

Merged
zkochan merged 21 commits into
pnpm:mainfrom
kairosci:paquet-bugs
Jul 1, 2026
Merged

feat(pacquet): implement bugs command#12687
zkochan merged 21 commits into
pnpm:mainfrom
kairosci:paquet-bugs

Conversation

@kairosci

@kairosci kairosci commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Port the pnpm bugs command from the TypeScript CLI to pacquet (Rust). The command opens the bug tracker URL of a package in the default browser. When called with no arguments, it reads the current project's package.json and derives the URL from the bugs or repository field. When called with package names, it fetches each package's latest version from the registry and derives the URL from the published manifest.

The implementation follows the same structure as the pnpm handler: pickBugsUrl prefers bugs (string or {url}) then falls back to repository with repositoryToIssuesUrl normalization. Hosted git shorthands (GitHub, GitLab, Bitbucket), git+https:///SCP-style SSH, and self-hosted git server URLs are all handled. The URL is printed to stdout and the browser is launched via xdg-open/open/start with output suppressed.

Related to #11633.

This change is pacquet-only; the TypeScript implementation already exists in pnpm11/deps/inspection/commands/src/bugs/.

Port the `pnpm bugs` command to pacquet, following the structure of the TypeScript handler at `pnpm11/deps/inspection/commands/src/bugs/index.ts` and matching its error codes (`ERR_PNPM_NO_BUGS_URL`, `ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND`). The command supports local manifest lookup and registry lookup by package name, with repository URL normalization for GitHub/GitLab/Bitbucket shorthand, hosted git URLs, and self-hosted git servers. Unit tests cover all URL derivation branches (36 tests). Integration tests cover the CLI entry points with local manifests and a mocked registry (9 tests).

Related to pnpm/pnpm#11633.
  • 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.
  • Updated the documentation if needed.

Summary by CodeRabbit

  • New Features

    • Added a new bugs CLI command to open project or package issue pages in your browser.
    • Improved package name handling for registry requests, including scoped packages and encoded paths.
    • Expanded support for more package tag formats, including non-version tags.
  • Bug Fixes

    • Standardized URL encoding across the app for more reliable link and registry handling.
    • Better issue-link detection from package metadata and repository information.
  • Tests

    • Added coverage for bugs URL resolution, registry lookups, package spec parsing, and URL encoding behavior.

@kairosci
kairosci requested a review from zkochan as a code owner June 26, 2026 14:38
@coderabbitai

coderabbitai Bot commented Jun 26, 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 bugs CLI subcommand that resolves and opens bug tracker URLs from local package.json or registry metadata. Extracts shared percent-encoding helpers (encode_package_name, encode_uri_component) into pacquet_network, removing duplicated local implementations. Extends PackageTag with an arbitrary Tag(String) variant.

Changes

Bugs command and supporting infrastructure

Layer / File(s) Summary
Shared URL encoding module
pacquet/crates/network/src/url_encoding.rs, pacquet/crates/network/src/lib.rs, pacquet/crates/cli/src/cli_args/audit/signatures.rs, pacquet/crates/cli/src/cli_args/dist_tag.rs, pacquet/crates/cli/src/cli_args/self_update/verify_engine.rs, pacquet/crates/resolving-git-resolver/src/hosted_git.rs, pacquet/crates/registry/src/package.rs, pacquet/crates/registry/src/package_version.rs
Adds encode_package_name and encode_uri_component to a new url_encoding module in pacquet_network; removes duplicated local implementations from audit/signatures, dist_tag, self_update/verify_engine, and hosted_git; updates package.rs and package_version.rs to encode package names before building registry URLs.
PackageTag arbitrary tag variant
pacquet/crates/registry/src/package_tag.rs
Adds a Tag(String) variant to PackageTag and changes FromStr to use Infallible, falling back to Tag instead of propagating semver parse errors.
BugsArgs, error types, and URL resolution
pacquet/crates/cli/src/cli_args.rs, pacquet/crates/cli/src/cli_args/bugs.rs
Defines BugsError and BugsArgs; implements project-level and per-package bug URL derivation (reading package.json or fetching registry /latest), repository URL normalization across many formats, hosted-git shorthand parsing, package-spec parsing, and platform-specific browser opening.
CLI wiring and dispatch
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
Adds Bugs(BugsArgs) variant to CliCommand, a dispatch route arm, and the async bugs handler in dispatch_query.
URL helper unit tests
pacquet/crates/cli/src/cli_args/bugs/tests.rs
Tests pick_bugs_url, repository_to_issues_url, is_http_url, try_hosted_git_shorthand, and parse_package_spec across object/string manifest forms, multiple repository specifier syntaxes, and edge cases.
CLI integration tests
pacquet/crates/cli/tests/bugs.rs
Exercises pacquet bugs against local manifests and mockito-served registry responses, covering success paths, ERR_PNPM_NO_BUGS_URL, ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND, and scoped package request encoding.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • pnpm/pnpm#11779: Overlaps with changes to hosted_git.rs — both touch encode_uri_component usage there.
  • pnpm/pnpm#12663: Both PRs modify audit/signatures.rs; the retrieved PR implements the feature while this PR refactors its percent-encoding to use pacquet_network.

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

Add pacquet bugs command to open package issue tracker URLs
✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

Description

• Add pacquet bugs subcommand to open a package’s bug tracker URL.
• Resolve URL from local package.json or registry latest manifest, matching pnpm behavior.
• Add unit + integration tests for URL derivation, normalization, and pnpm error codes.
Diagram

graph TD
A["pacquet CLI"] --> B["bugs args/run"] --> C["Local manifest"] --> D[("package.json")] --> E["URL selection"] --> F["print + open"]
B --> G["Registry fetch"] --> H[("npm registry")] --> E --> F
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use a hosted-git parsing crate for repository normalization
  • ➕ Reduces custom parsing logic
  • ➕ Potentially broader coverage of git URL edge cases
  • ➖ Adds dependency surface area
  • ➖ May diverge from pnpm’s exact normalization semantics targeted by this port
2. Use a cross-platform browser-open crate (e.g., `webbrowser`)
  • ➕ Simplifies OS-specific command handling
  • ➕ May behave more consistently across desktop environments
  • ➖ Extra dependency for a small feature
  • ➖ Less direct control over output suppression and parity with pnpm’s approach

Recommendation: The current implementation is well-aligned with the goal of parity with pnpm: it keeps normalization logic local and explicitly modeled after pnpm’s handler, and it is backed by focused unit/integration tests. Consider a browser-open crate only if future commands need the same functionality repeatedly.

Files changed (4) +918 / -0

Enhancement (2) +368 / -0
cli_args.rsRegister 'bugs' subcommand and dispatch via read-only config path +16/-0

Register 'bugs' subcommand and dispatch via read-only config path

• Adds the 'bugs' module, introduces 'CliCommand::Bugs', and wires dispatch to call 'BugsArgs::run' using 'config()' (no lockfile/install pipeline).

pacquet/crates/cli/src/cli_args.rs

bugs.rsImplement 'pacquet bugs' URL derivation + browser launching +352/-0

Implement 'pacquet bugs' URL derivation + browser launching

• Implements local-manifest and registry-backed URL resolution, matching pnpm’s preference order ('bugs' then 'repository' → issues URL). Handles hosted shorthand (GitHub/GitLab/Bitbucket), git+https, SCP-style SSH, and self-hosted git URLs; prints URL then attempts platform-specific browser launch; emits pnpm-compatible error codes.

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

Tests (2) +550 / -0
tests.rsUnit test coverage for URL selection and normalization helpers +262/-0

Unit test coverage for URL selection and normalization helpers

• Adds tests for 'pick_bugs_url', 'repository_to_issues_url', hosted shorthand recognition, HTTP URL validation, and package spec parsing.

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

bugs.rsIntegration tests for CLI behavior with local manifests and mocked registry +288/-0

Integration tests for CLI behavior with local manifests and mocked registry

• Adds end-to-end tests that run 'pacquet bugs' against temporary workspaces and a 'mockito' registry, validating stdout output and pnpm error code behavior for success and failure cases.

pacquet/crates/cli/tests/bugs.rs

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

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

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (19) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used
✅ Tickets: 🎫 Rust Roadmap

Grey Divider


Action required

1. Hardcoded hook PATH 🐞 Bug ⚙ Maintainability
Description
The commit-msg hook now prepends a contributor-specific absolute NVM path to PATH, leaking a local
username and making repo hooks depend on a machine-specific Node install layout. This is unrelated
to the feature and can break or mis-route hook execution for other contributors.
Code

.husky/commit-msg[R1-3]

+PATH="/home/alessio.attilio/.nvm/versions/node/v22.22.3/bin:$PATH" node .husky/reject-bare-issue-refs.mjs "$1"
+PATH="/home/alessio.attilio/.nvm/versions/node/v22.22.3/bin:$PATH" node .husky/reject-bare-mentions.mjs "$1"
+PATH="/home/alessio.attilio/.nvm/versions/node/v22.22.3/bin:$PATH" pnpm commitlint --edit --config=commitlint.config.cjs
Evidence
The hook hardcodes /home/alessio.attilio/... into PATH, which is machine/user-specific, and
contrasts with other hooks that do not embed developer home paths.

.husky/commit-msg[1-3]
.husky/pre-push[1-31]

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

## Issue description
`.husky/commit-msg` prepends a developer-specific absolute Node/NVM path to `PATH` for each hook command. This leaks a local username and makes hooks non-portable.
## Issue Context
Other husky hooks in the repo rely on the user’s environment (no hardcoded home directories), so this change is inconsistent and likely accidental.
## Fix Focus Areas
- .husky/commit-msg[1-3]
## Proposed fix
- Revert these commands to the prior form without the `PATH="/home/..."` prefix.
- If the intent was to ensure `node`/`pnpm` are available, use a repo-portable mechanism (e.g., rely on `corepack`/tooling documented for the project) rather than embedding a contributor-specific path.

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


2. SystemRoot binary hijack 🐞 Bug ⛨ Security
Description
On Windows, open_url_in_browser() builds the rundll32.exe path from the SystemRoot environment
variable and executes it. In a repo-controlled environment (e.g. via direnv), an attacker can
redirect SystemRoot to a directory containing a fake System32\rundll32.exe, leading to arbitrary
code execution when pacquet bugs is run.
Code

pacquet/crates/cli/src/cli_args/bugs.rs[R351-355]

+fn open_url_in_browser(url: &str) -> std::io::Result<()> {
+    let system_root = std::env::var("SystemRoot").unwrap_or_else(|_| "C:\\Windows".into());
+    let rundll32 = std::path::Path::new(&system_root).join("System32").join("rundll32.exe");
+    std::process::Command::new(rundll32)
+        .args(["url.dll,FileProtocolHandler", url])
Evidence
The Windows implementation explicitly reads SystemRoot from the process environment to construct
an executable path and then spawns it; the repo review guide treats environment variables and
executable resolution as attacker-controlled surfaces.

pacquet/crates/cli/src/cli_args/bugs.rs[350-360]
REVIEW_GUIDE.md[30-56]

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_in_browser()` on Windows trusts the `SystemRoot` environment variable to locate `rundll32.exe`. Because environment variables are attacker-controlled in the repo threat model, this can be abused to execute an attacker-provided binary.
## Issue Context
The command being executed is derived from `SystemRoot` and invoked via `std::process::Command::new(...)`.
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/bugs.rs[350-360]
Suggested direction: resolve the real Windows system directory via a Windows API call (e.g., `GetSystemDirectoryW`) and build `rundll32.exe` from that, rather than using an environment variable.

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


3. Permit dropped too early 🐞 Bug ☼ Reliability
Description
PackageVersion::fetch_from_registry builds the request from a temporary ThrottledClientGuard, so
the permit is dropped before .send().await/body consumption completes. This defeats the
semaphore-based socket bound and can lead to FD exhaustion (EMFILE) under fan-out (e.g. `pacquet
bugs` with many args).
Code

pacquet/crates/registry/src/package_version.rs[R262-266]

+        let encoded_name = pacquet_network::encode_package_name(name);
+        let url = format!("{registry}{encoded_name}/{tag}");
let network_error = |error| NetworkError { error, url: url.clone() };
let mut request = http_client.acquire_for_url(&url).await.get(&url).header(
Evidence
The network layer documents that dropping the guard before body consumption stops the semaphore from
bounding real concurrent socket count and can cause EMFILE under join fan-out.
PackageVersion::fetch_from_registry constructs the request directly from a temporary guard, so the
permit is released before .send().await/.json().await, matching the documented failure mode.

pacquet/crates/network/src/lib.rs[135-149]
pacquet/crates/registry/src/package_version.rs[251-283]
pacquet/crates/cli/src/cli_args/bugs.rs[52-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
`PackageVersion::fetch_from_registry()` currently acquires a `ThrottledClientGuard` as a temporary (`http_client.acquire_for_url(&url).await.get(...)`). Because the guard isn't bound to a variable, it is dropped as soon as the `RequestBuilder` is created, releasing the semaphore permit before `.send().await` and `.json().await` consume the response body.
This breaks the intended invariant of `ThrottledClient`: concurrency is supposed to be bounded for the entire request lifetime (including body streaming). Under fan-out, this can exceed FD limits and fail with `EMFILE`.
## Issue Context
`ThrottledClientGuard` is explicitly documented as needing to live across the body await to keep the semaphore bounding real concurrent sockets.
## Fix Focus Areas
- pacquet/crates/registry/src/package_version.rs[251-283]
- pacquet/crates/registry/src/package.rs[109-134]
- pacquet/crates/resolving-npm-resolver/src/fetch_attestation_published_at.rs[1-80]
### Implementation sketch
- Refactor the fetch helpers to:
- `let client = http_client.acquire_for_url(&url).await;`
- build the request using `client.get(&url)`
- await `send()` and body parsing (`json()`) *while `client` remains in scope*.
- Consider adding a small internal helper in `pacquet_network` (e.g. `get_json_with_throttle(...)`) to prevent future call sites from accidentally dropping the guard early.

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


View more (11)
4. Range specs misparsed 🐞 Bug ≡ Correctness
Description
pacquet bugs splits the CLI spec on the last @ and treats the suffix as a registry tag/version,
so semver ranges like react@^18 are queried as literal tags (e.g. GET /react/^18) instead of
being resolved to a matching version like pnpm does. This causes lookups to 404 or derive the bugs
URL from the wrong manifest for common pnpm-style inputs.
Code

pacquet/crates/cli/src/cli_args/bugs.rs[R104-115]

+    let (package_name, tag) = parse_package_spec(spec);
+    let package_tag = match tag {
+        None => PackageTag::Latest,
+        Some(tag_str) => tag_str.parse::<PackageTag>().unwrap_or(PackageTag::Latest),
+    };
+    let package_version = PackageVersion::fetch_from_registry(
+        package_name,
+        package_tag,
+        http_client,
+        registry_url,
+        auth_headers,
+    )
Evidence
The Rust implementation derives package_name and tag via a last-@ split and then calls
PackageVersion::fetch_from_registry, which formats the request as
{registry}{encoded_name}/{tag}—this works for latest, exact versions, and dist-tags, but not
semver ranges. In pnpm’s reference implementation, bugs delegates to fetchPackageInfo, which
uses npm-package-arg parsing and resolves tag | version | range by selecting a matching version
from metadata before returning manifest fields.

pacquet/crates/cli/src/cli_args/bugs.rs[98-135]
pacquet/crates/cli/src/cli_args/bugs.rs[368-384]
pacquet/crates/registry/src/package_version.rs[251-283]
pnpm11/deps/inspection/commands/src/bugs/index.ts[60-70]
pnpm11/deps/inspection/commands/src/fetchPackageInfo.ts[26-87]

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 bugs` currently interprets the part after the last `@` as a dist-tag/version and calls `PackageVersion::fetch_from_registry(name, selector)`. This cannot implement pnpm’s behavior for semver *ranges* (and other npm-package-arg selectors), which require fetching metadata and selecting a version.
### Issue Context
Upstream pnpm’s `bugs` command calls `fetchPackageInfo(opts, packageSpec)`, which parses `packageSpec` using `@pnpm/npm-package-arg` and resolves `tag | version | range` by selecting the matching version from metadata.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/bugs.rs[98-135]
- pacquet/crates/cli/src/cli_args/bugs.rs[368-384]
### What to change
- Replace `parse_package_spec()` + direct `PackageVersion::fetch_from_registry()` with pnpm-equivalent parsing/resolution:
- Parse the spec using the existing Rust parsing stack (e.g. `pacquet_resolving_parse_wanted_dependency::parse_wanted_dependency`, similar to `docs`), and determine whether the selector is:
- tag (including custom tags)
- exact version
- semver range
- For semver ranges, fetch the packument/metadata (`Package::fetch_from_registry` or an equivalent full-metadata fetch already used elsewhere) and pick the matching version, then derive `bugs/repository` from that selected version’s manifest.
- If alias/unsupported spec types are provided, fail fast with a clear diagnostic (mirroring pnpm’s `INVALID_PACKAGE_NAME` behavior) rather than issuing a malformed registry request.

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


5. Unencoded dist-tag in URL 🐞 Bug ⛨ Security
Description
PackageTag now accepts arbitrary custom tags, but PackageVersion::fetch_from_registry
interpolates {tag} directly into the request URL, so tags containing /, ?, or # can change
the effective request path/query and misroute authenticated registry requests. This can cause
incorrect lookups and expands the attack surface for request-target manipulation against the
configured registry origin.
Code

pacquet/crates/registry/src/package_version.rs[R262-266]

+        let encoded_name = pacquet_network::encode_package_name(name);
+        let url = format!("{registry}{encoded_name}/{tag}");
let network_error = |error| NetworkError { error, url: url.clone() };
let mut request = http_client.acquire_for_url(&url).await.get(&url).header(
Evidence
The PR makes arbitrary tags possible and then uses the tag string directly in the registry URL
format, which means reserved characters in tags can alter the request target.

pacquet/crates/registry/src/package_tag.rs[5-30]
pacquet/crates/registry/src/package_version.rs[251-275]
pacquet/crates/cli/src/cli_args/bugs.rs[96-113]

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

## Issue description
`PackageTag` can now represent arbitrary dist-tags (e.g. `beta`, `next`). That tag value is formatted into the registry URL path as `{registry}{encoded_name}/{tag}` without percent-encoding the tag. Tags containing reserved characters (notably `/`, `?`, `#`) can mutate the request target (extra path segments, query injection), and since auth headers are attached to the URL, this can result in authenticated requests being sent to unintended endpoints on the same registry origin.
### Issue Context
- `PackageTag` parsing now never fails and will treat non-semver strings as a tag.
- Registry fetch builds a URL by string formatting and uses it both for the request and auth-header lookup.
### Fix Focus Areas
- pacquet/crates/registry/src/package_version.rs[262-274]
- pacquet/crates/registry/src/package_tag.rs[6-27]
- pacquet/crates/cli/src/cli_args/bugs.rs[102-113]
### Suggested fix
- Encode the tag as a single URL path segment before formatting the URL, e.g.:
- `let encoded_tag = pacquet_network::encode_uri_component(&tag.to_string());`
- `let url = format!("{registry}{encoded_name}/{encoded_tag}");`
- Add a regression test covering a tag containing `/` (should become `%2F`) and `?` (should become `%3F`) so the request path remains a single segment.

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


6. Registry error leaks creds 🐞 Bug ⛨ Security
Description
get_bugs_url_from_registry converts PackageVersion::fetch_from_registry failures into
diagnostics without redacting the request URL, so a credential-bearing --registry/config value
(e.g. https://user:pass@host/) can be printed to terminal/CI logs on network/JSON errors. This
expands secret-exposure surface area via the new bugs command’s error paths.
Code

pacquet/crates/cli/src/cli_args/bugs.rs[R80-96]

+    let (package_name, _tag) = parse_package_spec(spec);
+    let encoded_name = encode_package_name(package_name);
+    let package_version = PackageVersion::fetch_from_registry(
+        &encoded_name,
+        PackageTag::Latest,
+        http_client,
+        registry_url,
+        auth_headers,
+    )
+    .await
+    .into_diagnostic()
+    .wrap_err_with(|| format!("fetch package info for \"{package_name}\" from the registry"))?;
+
+    let manifest = package_manifest_from_version(&package_version);
+    pick_bugs_url(&manifest)
+        .ok_or_else(|| BugsError::NoBugsUrlForPackage { package: package_name.to_string() }.into())
+}
Evidence
The new bugs command calls PackageVersion::fetch_from_registry(...).await.into_diagnostic()?, so
any RegistryError::Network will be displayed. PackageVersion::fetch_from_registry builds `url =
format!("{registry}{name}/{tag}") and wraps reqwest failures as NetworkError { url }`;
NetworkError’s Display is Failed to request {url}: {error}, which will include any embedded
credentials in registry. Existing CLI code (e.g. audit signatures) demonstrates the intended
mitigation pattern by redacting URLs/reasons before surfacing them.

pacquet/crates/cli/src/cli_args/bugs.rs[74-96]
pacquet/crates/registry/src/package_version.rs[252-282]
pacquet/crates/registry/src/lib.rs[18-24]
pacquet/crates/cli/src/cli_args/audit/signatures.rs[400-425]

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 bugs <pkg>` propagates `pacquet_registry::RegistryError` through `IntoDiagnostic`, and the underlying `NetworkError` `Display` includes the full request URL. If the registry base URL contains embedded credentials, those secrets can be printed in the diagnostic chain.
### Issue Context
Other CLI paths (e.g. `audit signatures`) explicitly call `redact_url_credentials(...)` before including URLs/reasons in user-visible errors; the new `bugs` command should do the same.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/bugs.rs[74-96]
- pacquet/crates/registry/src/lib.rs[18-24]
- pacquet/crates/registry/src/package_version.rs[252-282]
- pacquet/crates/cli/src/cli_args/audit/signatures.rs[400-425]
### Suggested fix approach
1. In `get_bugs_url_from_registry`, replace the `.into_diagnostic()?` propagation with explicit error mapping that **does not** print the raw `RegistryError` as a source if it contains URLs.
2. Pattern-match `RegistryError::Network(NetworkError { url, error })` and build a new diagnostic using:
- `redact_url_credentials(&url)` for the displayed URL
- `redact_url_credentials(&error.to_string())` for the displayed reason
- carry the original error only if you’re sure it won’t re-emit the raw URL; otherwise store it as a non-source string (like `audit/signatures.rs` does).
3. Ensure the resulting diagnostic still has an appropriate error code (existing commands often use `ERR_PNPM_REGISTRY_ERROR`-style codes for registry failures).

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


7. Bugs URL credential leak 🐞 Bug ⛨ Security
Description
open_url() prints the derived bugs URL verbatim, so inline user:pass@ credentials coming from
package.json or registry metadata can be leaked into terminal output/CI logs. The URL is then also
passed through to the browser launcher without redaction/stripping.
Code

pacquet/crates/cli/src/cli_args/bugs.rs[R282-287]

+fn open_url(url: &str) {
+    println!("{url}");
+    let result = open_url_in_browser(url);
+    if let Err(err) = result {
+        tracing::debug!(target: "pacquet_cli", %err, "could not open browser");
+    }
Evidence
The derived URL can come directly from manifest bugs (returned as-is when it parses as http(s))
and is printed verbatim by open_url(). The repo already treats user:pass@ in URLs as sensitive
and provides a redaction helper used by ping before printing.

pacquet/crates/cli/src/cli_args/bugs.rs[141-150]
pacquet/crates/cli/src/cli_args/bugs.rs[282-287]
pacquet/crates/cli/src/cli_args/ping.rs[70-78]
pacquet/crates/network/src/auth.rs[460-479]

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 bugs` prints the bugs URL to stdout via `println!("{url}")`. If the URL contains embedded basic-auth (`https://user:pass@host/...`), those credentials are leaked into terminal output and CI logs.
### Issue Context
The repo already has a dedicated credential redaction utility (`pacquet_network::redact_url_credentials`) and uses it before printing untrusted URLs in other commands (e.g. `ping`).
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/bugs.rs[282-287]
- pacquet/crates/cli/src/cli_args/bugs.rs[141-165]
### Expected fix
- Redact credentials before printing (at minimum).
- Prefer also stripping userinfo before launching the browser (parse with `url::Url`, clear username/password, and open the cleaned URL), since bug tracker URLs should not require embedded credentials.
- Reuse the existing redaction helper (`pacquet_network::redact_url_credentials`) to match established repo behavior.

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


8. Unsanitized URL stdout 🐞 Bug ⛨ Security
Description
open_url() prints a bugs URL derived from attacker-controlled registry/package.json data without
stripping control characters, enabling terminal escape/control-sequence injection when users run
pacquet bugs . The repo already has a sanitize() helper specifically to prevent raw escape
sequences reaching the terminal, but it isn’t used here.
Code

pacquet/crates/cli/src/cli_args/bugs.rs[R282-287]

+fn open_url(url: &str) {
+    println!("{url}");
+    let result = open_url_in_browser(url);
+    if let Err(err) = result {
+        tracing::debug!(target: "pacquet_cli", %err, "could not open browser");
+    }
Evidence
The new command prints a URL directly to stdout, and that URL originates from untrusted package
metadata. The repository already includes a dedicated sanitizer to strip control characters
specifically to prevent terminal escape sequences from untrusted metadata, and it’s used in other
registry-facing code paths but not in bugs.

pacquet/crates/cli/src/cli_args/bugs.rs[275-288]
pacquet/crates/cli/src/cli_args/sanitize.rs[3-17]
pacquet/crates/cli/src/cli_args/dist_tag.rs[424-440]

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 bugs` prints a URL to stdout via `println!("{url}")` without sanitizing control characters. Since the URL is derived from attacker-controlled manifest/registry metadata, this allows terminal escape injection.
### Issue Context
The CLI already has `cli_args::sanitize::sanitize()` with explicit intent to prevent stored/registry-derived metadata from emitting raw escape sequences.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/bugs.rs[282-287]
- pacquet/crates/cli/src/cli_args/sanitize.rs[3-17]
### Suggested fix
- Use the existing sanitizer when printing, e.g.:
- `use crate::cli_args::sanitize::sanitize;`
- `println!("{}", sanitize(url));`
- Consider also sanitizing the string passed to the browser launcher (or at least reject/strip control chars) to keep behavior consistent across stdout and side effects.

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


9. Windows exe search hijack 🐞 Bug ⛨ Security
Description
On Windows, open_url_in_browser() runs Command::new("rundll32.exe"), which relies on Windows
executable search order and can be hijacked by a malicious rundll32.exe placed in the working
directory of an untrusted project. This enables code execution when a user runs pacquet bugs
inside that directory.
Code

pacquet/crates/cli/src/cli_args/bugs.rs[R311-320]

+#[cfg(target_os = "windows")]
+fn open_url_in_browser(url: &str) -> std::io::Result<()> {
+    // Pass the URL as a direct argument to rundll32; it is never fed through
+    // cmd.exe, so shell metacharacters in the URL (e.g. `&`, `|`, `^`) cannot
+    // be re-parsed as command separators.
+    std::process::Command::new("rundll32.exe")
+        .args(["url.dll,FileProtocolHandler", url])
+        .stdout(std::process::Stdio::null())
+        .stderr(std::process::Stdio::null())
+        .spawn()?;
Evidence
The code explicitly spawns rundll32.exe by name, not by absolute path, which is sufficient to
establish reliance on Windows search-path resolution for executable lookup.

pacquet/crates/cli/src/cli_args/bugs.rs[311-321]

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

## Issue description
The Windows URL opener uses `Command::new("rundll32.exe")` with a bare executable name. On Windows this can resolve via the standard search order (including the current working directory), enabling binary planting/hijacking.
### Issue Context
This is a new process-execution surface introduced by the `bugs` command. The URL argument is not the problem here; the executable resolution is.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/bugs.rs[311-321]
### Suggested fix
- Resolve and invoke the absolute path to the system `rundll32.exe` instead of relying on search order.
- Commonly: `%SystemRoot%\\System32\\rundll32.exe` (obtain via `std::env::var("SystemRoot")`/`WINDIR`).
- If the environment variable is missing, fall back to `C:\\Windows\\System32\\rundll32.exe` or return an error.
- Consider also setting `.stdin(Stdio::null())` for consistency with stdout/stderr suppression.

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


10. Scoped auth header breaks 🐞 Bug ⛨ Security
Description
get_bugs_url_from_registry percent-encodes the package name and passes the encoded value into
PackageVersion::fetch_from_registry, which also uses that name for scope-aware auth header
lookup. This breaks scope detection for @scope/pkg (now @scope%2Fpkg), causing missing or wrong
credentials for private scoped registries.
Code

pacquet/crates/cli/src/cli_args/bugs.rs[R104-112]

+    let (package_name, _tag) = parse_package_spec(spec);
+    let encoded_name = encode_package_name(package_name);
+    let package_version = PackageVersion::fetch_from_registry(
+        &encoded_name,
+        PackageTag::Latest,
+        http_client,
+        registry_url,
+        auth_headers,
+    )
Evidence
The bugs command encodes package_name and passes it as name into the registry fetch; the
registry fetch uses name both to construct the URL and to select an auth header. Auth scope
detection requires a literal / in @scope/pkg, so encoding to @scope%2Fpkg prevents scoped
credentials from matching.

pacquet/crates/cli/src/cli_args/bugs.rs[98-120]
pacquet/crates/registry/src/package_version.rs[252-273]
pacquet/crates/network/src/auth.rs[197-237]
pacquet/crates/network/src/auth.rs[311-321]

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 bugs` encodes scoped names for the URL path, but the encoded string is also used as the package name for auth selection, which relies on the *raw* `@scope/pkg` form (with a literal `/`). This prevents package-scope credential matching and can break private scoped package lookups.
### Issue Context
- `encode_package_name("@scope/pkg")` returns `"@scope%2Fpkg"`.
- `AuthHeaders::for_url_with_package(..., Some(pkg_name))` uses `pkg_name.split_once('/')` to infer a scope; the encoded name no longer contains `/`.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/bugs.rs[104-115]
- pacquet/crates/registry/src/package_version.rs[252-273]
- pacquet/crates/network/src/auth.rs[197-237]
### Proposed fix
- Pass the **raw** `package_name` for auth scope detection, while still using the **encoded** name in the request URL path.
- Prefer fixing this at the registry client layer so all callers can safely fetch scoped packages:
- Change `PackageVersion::fetch_from_registry` to accept the raw name and internally percent-encode only for the URL path, but pass the raw `name` to `auth_headers.for_url_with_package(&url, Some(raw_name))`.
- Update any existing callers that might already pass encoded names to pass raw names instead (and add a regression test for scoped auth selection during a scoped package version fetch).

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


11. Scoped registries ignored 🐞 Bug ≡ Correctness
Description
BugsArgs::run computes one registry_url from config.registry (or --registry) and uses it for
all package lookups, ignoring per-scope registry routing. This breaks multi-registry setups (e.g.
GitHub Packages for @org/*) and diverges from upstream behavior which picks the registry per
package name.
Code

pacquet/crates/cli/src/cli_args/bugs.rs[R57-68]

+            let registry_url =
+                normalize_registry_url(self.registry.as_deref().unwrap_or(&config.registry));
+            let http_client = build_registry_client(config)
+                .wrap_err("build the network client for registry requests")?;
+
+            for spec in &self.packages {
+                let url = get_bugs_url_from_registry(
+                    spec,
+                    &registry_url,
+                    &http_client,
+                    &config.auth_headers,
+                )
Evidence
The new bugs command hardcodes a single registry URL for all lookups. The codebase already
provides a scope-aware pick_registry_for_package helper and uses it elsewhere, and the upstream
TypeScript implementation picks the registry per package.

pacquet/crates/cli/src/cli_args/bugs.rs[51-73]
pacquet/crates/lockfile/src/resolution.rs[360-389]
pacquet/crates/cli/src/cli_args/outdated.rs[124-138]
pnpm11/deps/inspection/commands/src/fetchPackageInfo.ts[47-56]

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 bugs` uses only the default registry (or a single CLI override) for all package specs, rather than routing by scope. This causes scoped packages that are configured to use a different registry to be queried against the wrong endpoint.
### Issue Context
The repo already has scope-aware registry routing logic (`pick_registry_for_package`) and other commands use it (e.g. `outdated`). Upstream pnpm uses `pickRegistryForPackage(opts.registries, packageName)` inside `fetchPackageInfo`.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/bugs.rs[52-73]
- pacquet/crates/lockfile/src/resolution.rs[360-389]
### Proposed fix
- If `--registry` is provided, keep current behavior (force all lookups to that registry).
- Otherwise, for each `spec`:
1. Parse out `package_name`.
2. Compute `registries: HashMap<String, String> = config.resolved_registries().into_iter().collect()`.
3. Select the registry via `pick_registry_for_package(&registries, package_name, Some(spec))`.
4. Normalize the selected registry URL and use it for that package’s fetch.
- Add an integration test that sets `@scope:registry=...` and verifies the request goes to the scoped registry host.

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


12. Shorthand fragment not stripped 🐞 Bug ≡ Correctness
Description
repository_to_issues_url routes hosted shorthands through try_hosted_git_shorthand, but
split_user_project only strips a trailing .git and leaves #/? intact. Inputs like
github:owner/repo#main or owner/repo.git#main therefore produce malformed URLs (e.g.
https://github.com/owner/repo#main/issues), so pacquet bugs opens the wrong page or a broken
URL.
Code

pacquet/crates/cli/src/cli_args/bugs.rs[R228-262]

+fn try_hosted_git_shorthand(s: &str) -> Option<String> {
+    if let Some(rest) = s.strip_prefix("github:") {
+        let (user, project) = split_user_project(rest)?;
+        return Some(format!("https://github.com/{user}/{project}/issues"));
+    }
+    if let Some(rest) = s.strip_prefix("gitlab:") {
+        let (user, project) = split_user_project(rest)?;
+        return Some(format!("https://gitlab.com/{user}/{project}/issues"));
+    }
+    if let Some(rest) = s.strip_prefix("bitbucket:") {
+        let (user, project) = split_user_project(rest)?;
+        return Some(format!("https://bitbucket.org/{user}/{project}/issues"));
+    }
+
+    // `owner/repo` shorthand — only when no ':', '//', or '@' is present,
+    // to avoid matching git+https://, git@, etc.
+    if !s.contains(':') && !s.contains("//") && !s.contains('@') {
+        let (user, project) = split_user_project(s)?;
+        return Some(format!("https://github.com/{user}/{project}/issues"));
+    }
+
+    None
+}
+
+/// Split a `user/project` string, stripping a trailing `.git` suffix.
+fn split_user_project(s: &str) -> Option<(&str, &str)> {
+    let s = s.trim_end_matches(".git");
+    let slash_pos = s.find('/')?;
+    let user = &s[..slash_pos];
+    let project = &s[slash_pos + 1..];
+    if user.is_empty() || project.is_empty() {
+        return None;
+    }
+    Some((user, project))
+}
Evidence
The new shorthand path runs before URL parsing, so fragments/queries are never stripped for
shorthand-derived URLs; split_user_project only trims .git. Pacquet’s own hosted-git parser
explicitly separates a #committish fragment, indicating these suffixes are expected in shorthand
inputs, and pnpm’s upstream implementation delegates these cases to hosted-git-info.

pacquet/crates/cli/src/cli_args/bugs.rs[167-262]
pacquet/crates/resolving-git-resolver/src/hosted_git.rs[148-206]
pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier/tests.rs[244-254]
pnpm11/deps/inspection/commands/src/bugs/index.ts[86-106]

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_git_shorthand()` builds issues URLs directly from the raw shorthand string, but `split_user_project()` only removes a trailing `.git`. When the repository string includes a fragment/query (commonly `#main`), that suffix becomes part of the generated path (e.g. `.../repo#main/issues`).
## Issue Context
- This affects hosted shorthands (`github:owner/repo#main`, bare `owner/repo#main`) and can also affect the SCP-style SSH branch (`[email protected]:owner/repo.git#main`).
- Pacquet already has hosted-git parsing logic that treats `#committish` as a separate component.
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/bugs.rs[173-262]
## Recommended fix
1. Before calling `split_user_project()` (and before `.trim_end_matches(".git")`), strip any `#...` fragment and `?...` query from the shorthand input.
- For example:
- `let base = s.split_once('#').map(|(a, _)| a).unwrap_or(s);`
- then similarly strip `?`.
2. Apply the same stripping to the SCP-style SSH path branch (`git@host:path.git#ref`).
3. Add unit tests in `pacquet/crates/cli/src/cli_args/bugs/tests.rs` covering at least:
- `repository_to_issues_url("github:owner/repo#main") == Some("https://github.com/owner/repo/issues")`
- `repository_to_issues_url("owner/repo.git#main") == Some("https://github.com/owner/repo/issues")`
- `repository_to_issues_url("[email protected]:owner/repo.git#main") == Some("https://github.com/owner/repo/issues")`
(Alternative: reuse `HostedGit::from_url()` from `pacquet/crates/resolving-git-resolver` and ignore committish when constructing the issues URL.)

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


13. Scoped registry lookup 404s 🐞 Bug ≡ Correctness
Description
get_bugs_url_from_registry passes scoped package names (e.g. @scope/pkg) unescaped into
PackageVersion::fetch_from_registry, which constructs {registry}{name}/{tag} and yields
/@scope/pkg/latest. Registries/CDNs that require scoped names to be a single encoded path segment
(@scope%2Fpkg) will 404, breaking pacquet bugs @scope/pkg.
Code

pacquet/crates/cli/src/cli_args/bugs.rs[R103-114]

+    let (package_name, _tag) = parse_package_spec(spec);
+    let package_version = PackageVersion::fetch_from_registry(
+        package_name,
+        PackageTag::Latest,
+        http_client,
+        registry_url,
+        auth_headers,
+    )
+    .await
+    .into_diagnostic()
+    .wrap_err_with(|| format!("fetch package info for \"{package_name}\" from the registry"))?;
+
Evidence
The new bugs command uses PackageVersion::fetch_from_registry with the raw package name; that
registry fetcher formats the request URL by concatenation without encoding. Elsewhere in the repo,
registry URL construction explicitly requires encoding scoped names (to avoid 404) and tests assert
requests are made to /@scope%2Fpkg.

pacquet/crates/cli/src/cli_args/bugs.rs[97-118]
pacquet/crates/registry/src/package_version.rs[251-283]
pacquet/crates/resolving-npm-resolver/src/registry_url.rs[1-12]
pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata/tests.rs[95-147]

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 bugs` registry lookups build a URL with an unencoded scoped package name (contains `/`). This produces request paths like `/@scope/pkg/latest` instead of `/@scope%2Fpkg/latest`, which can fail against registries/CDNs that canonicalize paths.
### Issue Context
The repo already documents and tests that scoped package names must be percent-encoded for registry URL paths.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/bugs.rs[97-118]
### Suggested fix
- Encode the package name for the URL path before the request (preserve the *raw* name for auth scope selection).
- Option A (localized): build the request URL using `pacquet_resolving_npm_resolver::registry_url::encode_pkg_name_path` / `to_registry_url`, then append `/latest`.
- Option B (more general): update `pacquet_registry::PackageVersion::fetch_from_registry` to accept raw names and internally percent-encode the name when composing the URL, while still passing the raw name into `AuthHeaders::for_url_with_package`.

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


14. Windows cmd URL injection 🐞 Bug ⛨ Security
Description
open_url_in_browser on Windows runs cmd /c start  with a URL derived from package.json or
registry metadata; URLs containing & (valid in query strings) will be interpreted by cmd.exe as
command separators, enabling arbitrary command execution when a user runs pacquet bugs on Windows.
This introduces an RCE path from attacker-controlled package metadata to shell execution.
Code

pacquet/crates/cli/src/cli_args/bugs.rs[R309-316]

+#[cfg(target_os = "windows")]
+fn open_url_in_browser(url: &str) -> std::io::Result<()> {
+    std::process::Command::new("cmd")
+        .args(["/c", "start", url])
+        .stdout(std::process::Stdio::null())
+        .stderr(std::process::Stdio::null())
+        .spawn()?;
+    Ok(())
Evidence
The Windows implementation passes the URL to cmd /c start (shell parsing), and the URL is derived
from manifest/registry fields. The repo’s executor code explicitly calls out that shell-wrapped
Windows invocations are unsafe due to argument escaping/CVE history, supporting that this is a
recognized dangerous pattern.

pacquet/crates/cli/src/cli_args/bugs.rs[51-72]
pacquet/crates/cli/src/cli_args/bugs.rs[139-160]
pacquet/crates/cli/src/cli_args/bugs.rs[309-316]
pacquet/crates/executor/src/shell.rs[14-17]

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

## Issue description
On Windows, the URL opener uses `cmd /c start <url>` with an unquoted URL that originates from untrusted manifest/registry data. In `cmd.exe`, metacharacters like `&` are parsed as command separators unless safely quoted/escaped, which can lead to command injection.
## Issue Context
- The URL comes from `pick_bugs_url()` reading `bugs`/`repository` fields (registry metadata and local manifests are attacker-controlled per repo security model).
- The repo already documents that shell-wrapped Windows invocations are unsafe due to argument re-escaping risks (see executor shell selection comments).
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/bugs.rs[280-317]
### Suggested fix direction
Prefer a non-shell Windows launcher (e.g. `explorer.exe <url>` or Windows API `ShellExecuteW`) so the URL is not interpreted by `cmd.exe`. If you must keep `cmd start`, pass a *single* `/c` command string that safely quotes the URL and includes an empty title, e.g. `cmd /d /s /c start "" "{url}"`, ensuring the URL is inside quotes.

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



Remediation recommended

15. Unbounded registry fan-out 🐞 Bug ➹ Performance ⭐ New
Description
BugsArgs::run spawns one async lookup per CLI arg and awaits them with join_all, buffering all
results before doing any processing; for large arg lists this causes avoidable memory/task pressure
and a bursty workload. ThrottledClient bounds socket concurrency, but it does not prevent
creating/polling N futures and allocating Vec<N> results.
Code

pacquet/crates/cli/src/cli_args/bugs.rs[R52-79]

+            let futures = self.packages.iter().map(|spec| {
+                let (package_name, _tag) = parse_package_spec(spec);
+                let target_registry = if let Some(ref override_registry) = self.registry {
+                    normalize_registry_url(override_registry)
+                } else {
+                    let picked = pacquet_resolving_npm_resolver::pick_registry_for_package(
+                        &registries,
+                        package_name,
+                        Some(spec),
+                    );
+                    normalize_registry_url(&picked)
+                };
+
+                let http_client = &http_client;
+                let auth_headers = &config.auth_headers;
+                async move {
+                    get_bugs_url_from_registry(spec, &target_registry, http_client, auth_headers)
+                        .await
+                        .wrap_err_with(|| format!(r#"look up bugs URL for "{spec}""#))
+                }
+            });
+
+            let results: Vec<miette::Result<String>> =
+                futures_util::future::join_all(futures).await;
+            for res in results {
+                let url = res?;
+                open_url(&url);
+            }
Evidence
The implementation uses join_all over self.packages and stores all results in a Vec before
iterating them; the network client’s semaphore bounds sockets but not the number of created/polled
futures or buffered results.

pacquet/crates/cli/src/cli_args/bugs.rs[41-80]
pacquet/crates/network/src/lib.rs[92-167]

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 bugs` currently fires all registry lookups at once via `join_all`, which creates/polls N futures and buffers N results even though network concurrency is limited elsewhere. This is unnecessary overhead and can spike memory and scheduler load when users pass many packages.

### Issue Context
The network layer already has a semaphore (`ThrottledClient`) to bound concurrent sockets, but `join_all` still allocates and drives all futures concurrently, and collects all results before proceeding.

### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/bugs.rs[41-80]

### Suggested fix
Replace `join_all` with a streaming approach that bounds in-flight work, e.g.:
- `futures_util::stream::iter(self.packages.iter())`
- `.map(|spec| async move { ... })`
- `.buffer_unordered(CONCURRENCY)` (pick from config if available, otherwise a conservative constant)
- process each result as it arrives (open URL / print) rather than collecting into a `Vec` first.

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


16. PATH opener resolution risk 🐞 Bug ⛨ Security ⭐ New
Description
On Linux/macOS, open_url_in_browser invokes xdg-open/open by bare name, meaning executable
resolution depends on PATH. If a user runs pacquet bugs in an environment where PATH is
influenced (e.g., wrappers/direnv), this can run an unintended binary instead of the system opener.
Code

pacquet/crates/cli/src/cli_args/bugs.rs[R328-347]

+#[cfg(target_os = "linux")]
+fn open_url_in_browser(url: &str) -> std::io::Result<()> {
+    std::process::Command::new("xdg-open")
+        .arg(url)
+        .stdin(std::process::Stdio::null())
+        .stdout(std::process::Stdio::null())
+        .stderr(std::process::Stdio::null())
+        .spawn()?;
+    Ok(())
+}
+
+#[cfg(target_os = "macos")]
+fn open_url_in_browser(url: &str) -> std::io::Result<()> {
+    std::process::Command::new("open")
+        .arg(url)
+        .stdin(std::process::Stdio::null())
+        .stdout(std::process::Stdio::null())
+        .stderr(std::process::Stdio::null())
+        .spawn()?;
+    Ok(())
Evidence
The Linux/macOS implementations directly construct Command with the program name only
(xdg-open/open), which is resolved via the process environment PATH.

pacquet/crates/cli/src/cli_args/bugs.rs[328-347]

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

### Issue description
The Linux/macOS browser opener uses `Command::new("xdg-open")` / `Command::new("open")`, which relies on `PATH` for executable resolution. In environments where `PATH` can be influenced, this can execute an unintended program.

### Issue Context
This is a local-environment hardening issue (not a remote exploit). It becomes relevant when `pacquet` is run under tooling that mutates `PATH` (shell wrappers, direnv, CI dev shells, etc.).

### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/bugs.rs[328-347]

### Suggested fix
Prefer one of:
1) Use a platform API / well-vetted crate to open URLs without relying on `PATH` resolution.
2) Resolve the opener to an absolute path up-front (e.g., via a `which`-style lookup) and reject unsafe resolutions (like current-working-directory), then execute the absolute path.

Keep passing the URL as an argument (no shell), and preserve the current stdio suppression behavior.

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


17. Empty dist-tag accepted 🐞 Bug ≡ Correctness
Description
parse_package_spec() returns an empty tag for inputs like foo@, and PackageTag::from_str("")
becomes Tag(""), producing a registry request ending in /foo/ rather than /foo/latest. This
causes confusing registry 404s/incorrect behavior for malformed bugs arguments.
Code

pacquet/crates/cli/src/cli_args/bugs.rs[R390-394]

+    } else if let Some(at_pos) = spec.rfind('@')
+        && at_pos > 0
+    {
+        (&spec[..at_pos], Some(&spec[at_pos + 1..]))
+    } else {
Evidence
parse_package_spec returns a tag slice even when it is empty (trailing @). That empty string is
then parsed into PackageTag and used to build the request URL path segment, yielding an empty last
segment (trailing slash).

pacquet/crates/cli/src/cli_args/bugs.rs[98-108]
pacquet/crates/cli/src/cli_args/bugs.rs[380-396]
pacquet/crates/registry/src/package_tag.rs[34-50]
pacquet/crates/registry/src/package_version.rs[242-255]

Agent prompt
The issue below was found duri...

Comment thread pacquet/crates/cli/src/cli_args/bugs.rs Outdated

@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/src/cli_args/bugs.rs`:
- Around line 309-317: The Windows-specific URL launcher in open_url_in_browser
is vulnerable because cmd /c start re-parses attacker-controlled URLs and can
interpret shell metacharacters. Replace this shell-based launch path with a safe
launcher approach that does not invoke cmd.exe on Windows, or add strict
validation to reject URLs containing shell metacharacters before opening them.
Update the bugs/repository URL flow around pick_bugs_url and open_url_in_browser
so the launch path is safe for attacker-controlled manifest data.
🪄 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: 434d4a82-08c0-442e-b200-6fbb09a75ae9

📥 Commits

Reviewing files that changed from the base of the PR and between 103b99b and 3821e4b.

📒 Files selected for processing (4)
  • pacquet/crates/cli/src/cli_args.rs
  • pacquet/crates/cli/src/cli_args/bugs.rs
  • pacquet/crates/cli/src/cli_args/bugs/tests.rs
  • pacquet/crates/cli/tests/bugs.rs

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 3821e4b

@codecov-commenter

codecov-commenter commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.19529% with 41 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.27%. Comparing base (f57f5af) to head (4ce8a6b).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/cli/src/cli_args/bugs.rs 88.47% 28 Missing ⚠️
...ates/cli/src/cli_args/self_update/verify_engine.rs 0.00% 11 Missing ⚠️
pacquet/crates/registry/src/package_tag.rs 80.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12687      +/-   ##
==========================================
+ Coverage   85.24%   85.27%   +0.03%     
==========================================
  Files         407      409       +2     
  Lines       61864    62079     +215     
==========================================
+ Hits        52734    52940     +206     
- Misses       9130     9139       +9     

☔ 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 26, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Commit: 2613f5aa27ea

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 3.814 ± 0.164 3.605 4.069 1.78 ± 0.12
pacquet@main 3.781 ± 0.117 3.616 3.944 1.77 ± 0.11
pnpr@HEAD 2.163 ± 0.140 2.010 2.372 1.01 ± 0.08
pnpr@main 2.139 ± 0.110 2.021 2.352 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.81350806086,
      "stddev": 0.16380215586094632,
      "median": 3.73844986806,
      "user": 3.5699700799999987,
      "system": 3.3819558,
      "min": 3.60512047256,
      "max": 4.06881038656,
      "times": [
        3.76045224656,
        3.7164474895599997,
        3.7157980085599998,
        4.06881038656,
        3.9619634485599997,
        3.88642570756,
        3.71604831756,
        4.0407848585599995,
        3.60512047256,
        3.66322967256
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.78059420516,
      "stddev": 0.11699261389367892,
      "median": 3.80623871756,
      "user": 3.6188499800000002,
      "system": 3.3910711,
      "min": 3.6164225645599997,
      "max": 3.94366819656,
      "times": [
        3.6248205885599996,
        3.84206772956,
        3.68010905856,
        3.70727433256,
        3.89932634956,
        3.78534713256,
        3.94366819656,
        3.8797757965599997,
        3.6164225645599997,
        3.8271303025599996
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.16281494996,
      "stddev": 0.14004088485357083,
      "median": 2.10048199656,
      "user": 2.65190278,
      "system": 3.0421213000000003,
      "min": 2.01005050656,
      "max": 2.37151868456,
      "times": [
        2.3538790495599997,
        2.27787883156,
        2.26799844456,
        2.01005050656,
        2.0295854045599997,
        2.0354890485599997,
        2.08747333456,
        2.11349065856,
        2.0807855365599996,
        2.37151868456
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.13933156326,
      "stddev": 0.10961432000078361,
      "median": 2.11598733606,
      "user": 2.6640449799999995,
      "system": 3.0230268,
      "min": 2.02083420556,
      "max": 2.3516355185599997,
      "times": [
        2.2110584365599997,
        2.27297496056,
        2.1262707765599997,
        2.1057038955599996,
        2.09283832156,
        2.14549708856,
        2.02083420556,
        2.03094069956,
        2.3516355185599997,
        2.03556172956
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 640.2 ± 16.0 619.4 676.3 1.00
pacquet@main 657.7 ± 54.4 617.2 808.1 1.03 ± 0.09
pnpr@HEAD 684.3 ± 18.1 662.6 718.2 1.07 ± 0.04
pnpr@main 708.0 ± 15.2 686.7 736.3 1.11 ± 0.04
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6402141846,
      "stddev": 0.015967133098974677,
      "median": 0.6400935205,
      "user": 0.3722578999999999,
      "system": 1.3172068399999997,
      "min": 0.6193850575,
      "max": 0.6763016015,
      "times": [
        0.6346770245,
        0.6193850575,
        0.6462890365,
        0.6241854565,
        0.6288798995,
        0.6763016015,
        0.6441932244999999,
        0.6471373845,
        0.6450993445,
        0.6359938165
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6577384018,
      "stddev": 0.05436424615023598,
      "median": 0.642605436,
      "user": 0.37803829999999994,
      "system": 1.3218886399999998,
      "min": 0.6172070335,
      "max": 0.8081182705,
      "times": [
        0.6400653685,
        0.6509001605,
        0.6172070335,
        0.6302607765,
        0.6331569355,
        0.6474248244999999,
        0.6408326805,
        0.6443781915,
        0.8081182705,
        0.6650397765
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6842714945,
      "stddev": 0.018073770945984884,
      "median": 0.682817982,
      "user": 0.3855723,
      "system": 1.34986464,
      "min": 0.6626243695,
      "max": 0.7181832164999999,
      "times": [
        0.6877076995,
        0.6958635925,
        0.6658521945,
        0.6695640905,
        0.6917850395,
        0.6706029065,
        0.6626243695,
        0.6779282645,
        0.7181832164999999,
        0.7026035715
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7079979106,
      "stddev": 0.015167146466108101,
      "median": 0.7043126555,
      "user": 0.40685529999999986,
      "system": 1.3576191399999997,
      "min": 0.6867400225,
      "max": 0.7362584605,
      "times": [
        0.7139788415,
        0.7362584605,
        0.7288694314999999,
        0.7030169475,
        0.7021496205,
        0.7089475135,
        0.6867400225,
        0.6922133335,
        0.7021965715,
        0.7056083635
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.319 ± 0.090 4.236 4.537 1.96 ± 0.12
pacquet@main 4.351 ± 0.040 4.271 4.401 1.97 ± 0.12
pnpr@HEAD 2.206 ± 0.113 2.071 2.378 1.00 ± 0.08
pnpr@main 2.204 ± 0.131 2.001 2.459 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.319302100879999,
      "stddev": 0.09013012036849323,
      "median": 4.30235567828,
      "user": 3.8102215999999993,
      "system": 3.3944811199999996,
      "min": 4.23649976728,
      "max": 4.537222701279999,
      "times": [
        4.537222701279999,
        4.28822928228,
        4.2660305982799995,
        4.32089747428,
        4.24150932328,
        4.23649976728,
        4.25780623728,
        4.31648207428,
        4.387532310279999,
        4.34081124028
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.35067842588,
      "stddev": 0.04001742771238971,
      "median": 4.35123037978,
      "user": 3.8423939999999996,
      "system": 3.43118852,
      "min": 4.27148645628,
      "max": 4.40122125028,
      "times": [
        4.33496805028,
        4.3334170172799995,
        4.35990594028,
        4.342554819279999,
        4.36492665828,
        4.40122125028,
        4.27148645628,
        4.392466241279999,
        4.391203506279999,
        4.31463431928
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.2062672702800006,
      "stddev": 0.11338319565302123,
      "median": 2.1881260197800003,
      "user": 2.4922007,
      "system": 2.9325475199999995,
      "min": 2.07080202628,
      "max": 2.3778372022800003,
      "times": [
        2.35479707628,
        2.12368376428,
        2.3778372022800003,
        2.23478496428,
        2.11909621128,
        2.07080202628,
        2.27425623828,
        2.28474529928,
        2.08120284528,
        2.14146707528
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.20359535028,
      "stddev": 0.1309031283226149,
      "median": 2.2108884812800005,
      "user": 2.5005597,
      "system": 2.8974872199999995,
      "min": 2.0006100242800002,
      "max": 2.45893850528,
      "times": [
        2.2107909282800002,
        2.23137701928,
        2.33292223028,
        2.2109860342800003,
        2.0006100242800002,
        2.06321765328,
        2.1147357652800003,
        2.24223254728,
        2.17014279528,
        2.45893850528
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.332 ± 0.013 1.314 1.354 2.01 ± 0.05
pacquet@main 1.345 ± 0.018 1.320 1.377 2.03 ± 0.06
pnpr@HEAD 0.663 ± 0.016 0.641 0.701 1.00
pnpr@main 0.664 ± 0.007 0.651 0.673 1.00 ± 0.03
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.3321333901799999,
      "stddev": 0.012813470319940599,
      "median": 1.32977847678,
      "user": 1.3126988799999997,
      "system": 1.67261822,
      "min": 1.31393610128,
      "max": 1.35395563028,
      "times": [
        1.35395563028,
        1.33176017928,
        1.31393610128,
        1.32160576928,
        1.33568874628,
        1.34928463428,
        1.31999187328,
        1.3400473962800001,
        1.32726679728,
        1.3277967742799999
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.3448455895799998,
      "stddev": 0.01849315646046127,
      "median": 1.3417195562800002,
      "user": 1.3093647800000001,
      "system": 1.7019837199999999,
      "min": 1.32003940428,
      "max": 1.37691452628,
      "times": [
        1.32629450728,
        1.35720671928,
        1.35353850628,
        1.3380027272800001,
        1.32003940428,
        1.37691452628,
        1.3366612362799999,
        1.34543638528,
        1.32798933128,
        1.36637255228
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6631252705799999,
      "stddev": 0.016012520487786674,
      "median": 0.66247784978,
      "user": 0.33393628,
      "system": 1.2782676199999998,
      "min": 0.64136919728,
      "max": 0.7011127742800001,
      "times": [
        0.67211229328,
        0.66185380328,
        0.65201965428,
        0.7011127742800001,
        0.6560496892800001,
        0.66488692628,
        0.66659088328,
        0.64136919728,
        0.65215558828,
        0.66310189628
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6638804689800001,
      "stddev": 0.006823029653986641,
      "median": 0.6643770842800001,
      "user": 0.34051408,
      "system": 1.2796596199999999,
      "min": 0.65130784528,
      "max": 0.67304866628,
      "times": [
        0.66015855128,
        0.65130784528,
        0.65646157928,
        0.66213273328,
        0.67304866628,
        0.67292010628,
        0.6681541212800001,
        0.66388143428,
        0.66487273428,
        0.66586691828
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.023 ± 0.027 2.988 3.082 4.51 ± 0.09
pacquet@main 3.051 ± 0.047 3.003 3.168 4.55 ± 0.11
pnpr@HEAD 0.670 ± 0.012 0.650 0.694 1.00
pnpr@main 0.691 ± 0.047 0.666 0.825 1.03 ± 0.07
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.02317627436,
      "stddev": 0.02677740062711613,
      "median": 3.01978199706,
      "user": 1.7854871000000003,
      "system": 1.9497360999999995,
      "min": 2.98812697006,
      "max": 3.08176152706,
      "times": [
        3.01185618006,
        3.01863876406,
        3.02092523006,
        3.03795495306,
        2.99934879506,
        3.08176152706,
        3.00364506106,
        3.02467478806,
        2.98812697006,
        3.04483047506
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.05116537126,
      "stddev": 0.047333562437288666,
      "median": 3.0386896115599997,
      "user": 1.8143661999999998,
      "system": 1.9751954,
      "min": 3.00345705406,
      "max": 3.16808712206,
      "times": [
        3.05463203506,
        3.0327630550599998,
        3.00345705406,
        3.03314010406,
        3.0442391190599998,
        3.00792260206,
        3.02576794706,
        3.16808712206,
        3.06067071706,
        3.08097395706
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6702514885599999,
      "stddev": 0.012409238512558367,
      "median": 0.66904578756,
      "user": 0.34744169999999996,
      "system": 1.2952583,
      "min": 0.64962153706,
      "max": 0.6939376700600001,
      "times": [
        0.67191029106,
        0.6939376700600001,
        0.6697500940600001,
        0.66471220606,
        0.68682794006,
        0.66500462106,
        0.66987015206,
        0.66253889306,
        0.64962153706,
        0.66834148106
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.69083325406,
      "stddev": 0.04742514800084136,
      "median": 0.67539160356,
      "user": 0.3438021,
      "system": 1.2768428,
      "min": 0.6664340590600001,
      "max": 0.8246905950600001,
      "times": [
        0.6853382570600001,
        0.6664340590600001,
        0.68043845906,
        0.6721094010600001,
        0.68471696606,
        0.6775635170600001,
        0.67321969006,
        0.67241719006,
        0.8246905950600001,
        0.6714044060600001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 7.193 ± 0.184 6.992 7.605 1.28 ± 0.04
pacquet@main 7.648 ± 1.379 7.146 11.569 1.36 ± 0.25
pnpr@HEAD 5.605 ± 0.109 5.455 5.784 1.00
pnpr@main 5.628 ± 0.084 5.538 5.781 1.00 ± 0.02
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 7.19276034978,
      "stddev": 0.18387492344659243,
      "median": 7.18753150838,
      "user": 3.8366562,
      "system": 3.68135374,
      "min": 6.9920549458800005,
      "max": 7.60462500488,
      "times": [
        7.60462500488,
        6.99874689688,
        7.04286941188,
        7.14440006388,
        6.9920549458800005,
        7.09299669388,
        7.23066295288,
        7.26650932288,
        7.29714176188,
        7.257596442880001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 7.648297078480001,
      "stddev": 1.3789775894670975,
      "median": 7.21830719188,
      "user": 3.997076,
      "system": 3.9085173399999995,
      "min": 7.146354365880001,
      "max": 11.56911746288,
      "times": [
        7.146354365880001,
        7.181204864880001,
        7.1480342428800006,
        7.31862717288,
        7.29864704688,
        11.56911746288,
        7.238013399880001,
        7.21842669088,
        7.21818769288,
        7.14635784488
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 5.604801475980001,
      "stddev": 0.10912320303667473,
      "median": 5.595071171880001,
      "user": 2.7712118,
      "system": 3.23614604,
      "min": 5.45466044788,
      "max": 5.78407632888,
      "times": [
        5.60323311788,
        5.55276051888,
        5.58690922588,
        5.639202789880001,
        5.554887267880001,
        5.7672304078800005,
        5.46905578588,
        5.78407632888,
        5.45466044788,
        5.635998868880001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 5.62800770348,
      "stddev": 0.08420500193727867,
      "median": 5.59950935038,
      "user": 2.8018721999999996,
      "system": 3.26697294,
      "min": 5.537848593880001,
      "max": 5.780682785880001,
      "times": [
        5.62422918388,
        5.69006261988,
        5.5595296778800005,
        5.56776330088,
        5.780682785880001,
        5.57478951688,
        5.71647569188,
        5.537848593880001,
        5.68202475888,
        5.54667090488
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12687
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,319.30 ms
(-6.95%)Baseline: 4,641.77 ms
5,570.13 ms
(77.54%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
3,023.18 ms
(-1.10%)Baseline: 3,056.74 ms
3,668.08 ms
(82.42%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,332.13 ms
(-1.92%)Baseline: 1,358.15 ms
1,629.78 ms
(81.74%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
3,813.51 ms
(-16.91%)Baseline: 4,589.37 ms
5,507.24 ms
(69.25%)
isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr📈 view plot
🚷 view threshold
7,192.76 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
640.21 ms
(+1.65%)Baseline: 629.83 ms
755.80 ms
(84.71%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12687
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,206.27 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
670.25 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
663.13 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,162.81 ms
isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr📈 view plot
⚠️ NO THRESHOLD
5,604.80 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
684.27 ms
🐰 View full continuous benchmarking report in Bencher

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

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

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

Move this use to the top with the other imports.

The new use crate::cli_args::package_name::encode_package_name; is placed at the bottom of the file, detached from the rest of the import block. rustfmt won't relocate it, so it stays an outlier. Merge it into the top crate-local imports for consistency.

As per path instructions (pacquet/CODE_STYLE_GUIDE.md: "keep imports merged by crate").

🤖 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/audit/signatures.rs` at line 519, The new
crate-local import for encode_package_name is isolated at the bottom of
signatures.rs instead of being merged with the existing import block. Move it
into the top grouped imports near the other crate imports in the
audit/signatures module so the import layout stays consistent and rustfmt won’t
leave it as an outlier.
🤖 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/patch_remove/tests.rs`:
- Around line 86-88: The patch-remove cancellation test is currently skipping
itself under a TTY, which removes the PatchRemoveError::Canceled assertion
instead of enforcing the required non-interactive stdin setup. Update the test
in tests.rs to keep the cancellation behavior verified by either making
non-interactive stdin a hard precondition or using a test harness/helper that
explicitly supplies non-interactive input, so the test still asserts the
canceled error path rather than returning early.

In `@pacquet/crates/cli/src/cli_args/patch/tests.rs`:
- Around line 70-71: The stdin TTY guard in the patch cancellation tests is
causing them to return early and stop verifying the PatchError::Canceled
regression locally. Remove the silent skip in these tests, or refactor the
stdin-dependent behavior behind a seam/harness so the dialoguer path can be
forced even when stdin is interactive. Keep the assertions in the existing test
cases in tests.rs focused on the cancel contract rather than bypassing the test
body.

---

Nitpick comments:
In `@pacquet/crates/cli/src/cli_args/audit/signatures.rs`:
- Line 519: The new crate-local import for encode_package_name is isolated at
the bottom of signatures.rs instead of being merged with the existing import
block. Move it into the top grouped imports near the other crate imports in the
audit/signatures module so the import layout stays consistent and rustfmt won’t
leave it as an outlier.
🪄 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: 1ea5ec12-bb8a-4ca5-a85d-791cb9e856c9

📥 Commits

Reviewing files that changed from the base of the PR and between 3821e4b and b8d08c4.

📒 Files selected for processing (8)
  • pacquet/crates/cli/src/cli_args.rs
  • pacquet/crates/cli/src/cli_args/audit/signatures.rs
  • pacquet/crates/cli/src/cli_args/bugs.rs
  • pacquet/crates/cli/src/cli_args/bugs/tests.rs
  • pacquet/crates/cli/src/cli_args/package_name.rs
  • pacquet/crates/cli/src/cli_args/patch/tests.rs
  • pacquet/crates/cli/src/cli_args/patch_remove/tests.rs
  • pacquet/crates/cli/tests/bugs.rs
✅ Files skipped from review due to trivial changes (1)
  • pacquet/crates/cli/src/cli_args/package_name.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • pacquet/crates/cli/src/cli_args.rs
  • pacquet/crates/cli/tests/bugs.rs
  • pacquet/crates/cli/src/cli_args/bugs.rs
  • pacquet/crates/cli/src/cli_args/bugs/tests.rs

Comment thread pacquet/crates/cli/src/cli_args/patch_remove/tests.rs Outdated
Comment thread pacquet/crates/cli/src/cli_args/patch/tests.rs Outdated
Comment thread pacquet/crates/cli/src/cli_args/bugs.rs Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

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

Copy link
Copy Markdown

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

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 7e0edd5

Comment on lines +282 to +287
fn open_url(url: &str) {
println!("{url}");
let result = open_url_in_browser(url);
if let Err(err) = result {
tracing::debug!(target: "pacquet_cli", %err, "could not open browser");
}

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. Bugs url credential leak 🐞 Bug ⛨ Security

open_url() prints the derived bugs URL verbatim, so inline user:pass@ credentials coming from
package.json or registry metadata can be leaked into terminal output/CI logs. The URL is then also
passed through to the browser launcher without redaction/stripping.
Agent Prompt
### Issue description
`pacquet bugs` prints the bugs URL to stdout via `println!("{url}")`. If the URL contains embedded basic-auth (`https://user:pass@host/...`), those credentials are leaked into terminal output and CI logs.

### Issue Context
The repo already has a dedicated credential redaction utility (`pacquet_network::redact_url_credentials`) and uses it before printing untrusted URLs in other commands (e.g. `ping`).

### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/bugs.rs[282-287]
- pacquet/crates/cli/src/cli_args/bugs.rs[141-165]

### Expected fix
- Redact credentials before printing (at minimum).
- Prefer also stripping userinfo before launching the browser (parse with `url::Url`, clear username/password, and open the cleaned URL), since bug tracker URLs should not require embedded credentials.
- Reuse the existing redaction helper (`pacquet_network::redact_url_credentials`) to match established repo behavior.

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

kairosci added a commit to kairosci/pnpm that referenced this pull request Jun 27, 2026
Port the `pnpm bugs` command from TypeScript to Rust, opening the
package's bug report URL in the default browser (or printing it).

Extracts `encode_package_name` / `encode_uri_component` into a
shared `package_name` module, replacing private copies in
`audit/signatures` and `self_update/verify_engine`.

Review feedback applied:
- dylint import-granularity (merged two `use crate::cli_args::...` lines)
- dylint bare-url (wrapped bare https:// in comment with backticks)
- CodeRabbitAI security: `rundll32.exe` uses absolute path from
  `%SystemRoot%` instead of bare executable name

Ref pnpm#12687
Comment thread pacquet/crates/cli/src/cli_args/bugs.rs Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

@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/src/cli_args/self_update/verify_engine.rs`:
- Around line 467-480: The packument fetch in verify_engine currently uses
response.text().await, which buffers the entire body before the size check and
can still exhaust memory. Update the packument reading logic to stream the
response incrementally inside the same trust-critical path, enforcing
MAX_PACKUMENT_BYTES as chunks are read rather than after full buffering. Keep
the existing oversized-response handling and error reporting around display_url
and redact_url_credentials, but move the cap enforcement into the streaming read
flow.
🪄 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: 59d7283f-d7cb-4271-b0d8-99365a97f16a

📥 Commits

Reviewing files that changed from the base of the PR and between 7e0edd5 and cd59e46.

📒 Files selected for processing (10)
  • pacquet/crates/cli/src/cli_args.rs
  • pacquet/crates/cli/src/cli_args/audit/signatures.rs
  • pacquet/crates/cli/src/cli_args/bugs.rs
  • pacquet/crates/cli/src/cli_args/bugs/tests.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/package_name.rs
  • pacquet/crates/cli/src/cli_args/self_update/verify_engine.rs
  • pacquet/crates/cli/tests/bugs.rs
✅ Files skipped from review due to trivial changes (1)
  • pacquet/crates/cli/src/cli_args/bugs/tests.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • pacquet/crates/cli/src/cli_args/package_name.rs
  • pacquet/crates/cli/tests/bugs.rs
  • pacquet/crates/cli/src/cli_args/audit/signatures.rs
  • pacquet/crates/cli/src/cli_args/bugs.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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

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/src/cli_args/self_update/verify_engine.rs`:
- Around line 467-480: The packument fetch in verify_engine currently uses
response.text().await, which buffers the entire body before the size check and
can still exhaust memory. Update the packument reading logic to stream the
response incrementally inside the same trust-critical path, enforcing
MAX_PACKUMENT_BYTES as chunks are read rather than after full buffering. Keep
the existing oversized-response handling and error reporting around display_url
and redact_url_credentials, but move the cap enforcement into the streaming read
flow.
🪄 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: 59d7283f-d7cb-4271-b0d8-99365a97f16a

📥 Commits

Reviewing files that changed from the base of the PR and between 7e0edd5 and cd59e46.

📒 Files selected for processing (10)
  • pacquet/crates/cli/src/cli_args.rs
  • pacquet/crates/cli/src/cli_args/audit/signatures.rs
  • pacquet/crates/cli/src/cli_args/bugs.rs
  • pacquet/crates/cli/src/cli_args/bugs/tests.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/package_name.rs
  • pacquet/crates/cli/src/cli_args/self_update/verify_engine.rs
  • pacquet/crates/cli/tests/bugs.rs
✅ Files skipped from review due to trivial changes (1)
  • pacquet/crates/cli/src/cli_args/bugs/tests.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • pacquet/crates/cli/src/cli_args/package_name.rs
  • pacquet/crates/cli/tests/bugs.rs
  • pacquet/crates/cli/src/cli_args/audit/signatures.rs
  • pacquet/crates/cli/src/cli_args/bugs.rs
🛑 Comments failed to post (1)
pacquet/crates/cli/src/cli_args/self_update/verify_engine.rs (1)

467-480: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Read the packument incrementally before applying the size cap. response.text().await buffers the entire response first, so a chunked or incorrect Content-Length can still exhaust memory on this trust-critical path.

🤖 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/self_update/verify_engine.rs` around lines
467 - 480, The packument fetch in verify_engine currently uses
response.text().await, which buffers the entire body before the size check and
can still exhaust memory. Update the packument reading logic to stream the
response incrementally inside the same trust-critical path, enforcing
MAX_PACKUMENT_BYTES as chunks are read rather than after full buffering. Keep
the existing oversized-response handling and error reporting around display_url
and redact_url_credentials, but move the cap enforcement into the streaming read
flow.

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

Copy link
Copy Markdown

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds the pacquet bugs CLI command (parity feature with pnpm bugs) and supporting URL/registry plumbing so pacquet can derive and open a package’s bug tracker URL from either the local package.json or the registry.

Changes:

  • Introduces pacquet bugs (alias issues) with URL derivation from bugs / repository fields and browser launch, plus unit + integration tests.
  • Centralizes JS-compatible URL encoding (encodeURIComponent + pnpm-style scoped package encoding) in pacquet_network and updates registry callers to use it (including holding throttling permits across body consumption).
  • Updates pnpr server routing to correctly serve /<encoded-scoped-name>/<tag> requests where %2F is decoded into /.

Reviewed changes

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

Show a summary per file
File Description
pnpr/crates/pnpr/src/server.rs Route fix for encoded scoped package name + tag paths.
pacquet/crates/resolving-git-resolver/src/hosted_git.rs Switch to shared URL-encoding helper.
pacquet/crates/registry/src/package.rs Encode package name in registry URLs; hold throttling guard across request+body.
pacquet/crates/registry/src/package_version.rs Encode package name + tag segment in version-manifest URLs; hold throttling guard.
pacquet/crates/registry/src/package_tag.rs Add tag support + registry path-segment helper for tags/versions.
pacquet/crates/package-manager/src/add/tests.rs Update mocks for encoded scoped package paths.
pacquet/crates/network/src/url_encoding.rs New shared URL encoding helpers (encode_uri_component, encode_package_name).
pacquet/crates/network/src/lib.rs Re-export URL encoding helpers and wire in new module.
pacquet/crates/cli/tests/bugs.rs New integration tests for pacquet bugs behavior (local + registry).
pacquet/crates/cli/src/cli_args/self_update/verify_engine.rs Use shared package-name encoding; stream response with size cap.
pacquet/crates/cli/src/cli_args/dist_tag.rs Use shared URI-component encoding (dedupe local implementation).
pacquet/crates/cli/src/cli_args/dispatch.rs Wire Bugs command into dispatch.
pacquet/crates/cli/src/cli_args/dispatch_query.rs Add bugs dispatcher entrypoint.
pacquet/crates/cli/src/cli_args/cli_command.rs Define bugs/issues CLI command and help text.
pacquet/crates/cli/src/cli_args/bugs/tests.rs New unit tests for URL derivation + spec parsing.
pacquet/crates/cli/src/cli_args/bugs.rs New bugs command implementation (manifest + registry lookup + open).
pacquet/crates/cli/src/cli_args/audit/signatures/tests.rs Update imports to shared package-name encoding.
pacquet/crates/cli/src/cli_args/audit/signatures.rs Use shared package-name encoding (dedupe local implementation).
pacquet/crates/cli/src/cli_args.rs Export the new bugs module.

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

Comment thread pacquet/crates/cli/src/cli_args/bugs.rs
Comment thread pacquet/crates/registry/src/package_tag.rs
Copilot AI review requested due to automatic review settings July 1, 2026 12:22
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 84cc28d

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 7dfa6d7

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 84cc28d

Drop logout's private encode_uri_component (and its hex_upper helper) in
favor of pacquet_network::encode_uri_component. The two produce identical
output (uppercase two-digit hex, same unreserved set), so this removes a
duplicate.

Left dist_tag's escaped_package_name as-is: its lowercase %2f matches
npm-package-arg's escapedName for the dist-tags endpoint, which is
intentionally distinct from encode_package_name's uppercase %2F toUri
encoding for packument URLs.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

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

Copy link
Copy Markdown

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

Copilot AI review requested due to automatic review settings July 1, 2026 14:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 2613f5a

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

Copy link
Copy Markdown

Qodo is busy working

Check back in a few minutes. Qodo's code review agents are on it.

Grey Divider

@kairosci

kairosci commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

@zkochan I don't know if it's on purpose or not, but you forgot to enable auto-merge

registry_path_segment claimed to return a URL-encoded segment but the
Version branch returned the raw semver string. Route it through
encode_uri_component too so build metadata (the '+' in e.g. 1.2.3+build)
cannot corrupt the request path. Add a unit test covering all variants.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

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

Copy link
Copy Markdown

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

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.

4 participants