feat(pacquet-cli): implement pnpm repo command#12702
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a Changespacquet repo subcommand
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested labels
Suggested reviewers
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
PR Summary by Qodopacquet-cli: add Description
Diagram
High-Level Assessment
Files changed (5)
|
Code Review by Qodo
1. Hardcoded master branch
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
pacquet/crates/cli/src/cli_args/repo.rs (3)
377-379: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove unit tests to an external
testssubmodule and avoid the star import.pacquet/CODE_STYLE_GUIDE.md requires unit tests in an external
testssubmodule (e.g.,src/cli_args/repo/tests.rs) rather than inline, and disallows star imports in module bodies including tests. This inlinemod tests { use super::*; }violates both.As per path instructions: "Unit test layout: place unit tests in an external
testssubmodule (e.g.,src/foo/tests.rs), not inline in the parent file" and "no star imports inside module bodies (keep explicit imports in normal modules/tests)."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pacquet/crates/cli/src/cli_args/repo.rs` around lines 377 - 379, Move the unit tests out of the inline tests module in repo.rs into an external tests submodule, following the repo module’s test layout convention, and replace the star import in that test module with explicit imports. Update the existing tests for the repo-related functionality so they live under a dedicated tests.rs (or equivalent) and only import the specific items they use instead of use super::*.Source: Path instructions
342-352: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd a
// SAFETY:justification for theShellExecuteWFFI call.The
unsafeblock has no comment explaining why the call is sound (valid null-terminated wide pointers, null operation/params). Per the repo conventions,unsafeshould carry a clear justification; pacquet/AGENTS.md additionally asks in-code upstream references to pin a commit SHA.As per coding guidelines: "Do not introduce
unsafewithout a clear justification and review."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pacquet/crates/cli/src/cli_args/repo.rs` around lines 342 - 352, Add a clear // SAFETY: justification to the unsafe ShellExecuteW call in the repo URL opening logic, explaining that the wide string pointer is valid and null-terminated, and that the null operation/params are intentional and acceptable for this Windows FFI call. Update the unsafe block around ShellExecuteW in the URL-launching path to follow the repo’s unsafe conventions, and include the required upstream reference with a pinned commit SHA if you are documenting the external API behavior.Source: Coding guidelines
66-77: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoffA new
ThrottledClientis built for every package on the registry path.
get_repo_url_from_registryis called once per package inrun(lines 25-28), and each call constructs a fresh throttled HTTP client (connection pool). For multi-package invocations this rebuilds the client repeatedly. Consider constructing the client once inrunand passing it in.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pacquet/crates/cli/src/cli_args/repo.rs` around lines 66 - 77, get_repo_url_from_registry is rebuilding a new ThrottledClient on every package lookup, which is wasteful for multi-package runs. Move the ThrottledClient::for_installs construction out of get_repo_url_from_registry and into run so the client is created once and reused, then pass the shared client into get_repo_url_from_registry (and any helpers it calls) instead of recreating the connection pool each time.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pacquet/crates/cli/src/cli_args/repo.rs`:
- Around line 54-57: The repository URL lookup in repo.rs is collapsing all
PackageManifest::from_path failures into RepoError::NoRepoUrlLocal, which hides
missing-file, parse, and IO diagnostics. Update the logic around
PackageManifest::from_path and pick_repo_url so manifest read/parse errors are
preserved and surfaced separately from the missing repository field case, using
RepoError variants that distinguish manifest-loading failures from “no
repository URL” in the same flow.
- Around line 60-64: Use the parsed version range when resolving the repository
manifest in get_repo_url_from_registry, because it currently resolves only by
name and bare specifier and ignores the requested version. Update the logic that
calls PackageManifest::resolve_registry_dependency so it incorporates the parsed
range from parse_wanted_dependency, and then use the resulting resolved package
info when looking up the repo URL so repo foo@1.2.3 opens the matching version
instead of the latest.
- Around line 151-159: The browse URL construction in the repo command currently
overwrites the directory-based path when try_extract_fragment(raw_url) returns a
fragment, causing directory information to be lost. Update the final_url logic
in the repository URL builder so the fragment fallback preserves any existing
directory path from directory rather than replacing it entirely, and adjust the
formatting in the repo URL assembly path to keep both the base_url and directory
context when a fragment is present.
---
Nitpick comments:
In `@pacquet/crates/cli/src/cli_args/repo.rs`:
- Around line 377-379: Move the unit tests out of the inline tests module in
repo.rs into an external tests submodule, following the repo module’s test
layout convention, and replace the star import in that test module with explicit
imports. Update the existing tests for the repo-related functionality so they
live under a dedicated tests.rs (or equivalent) and only import the specific
items they use instead of use super::*.
- Around line 342-352: Add a clear // SAFETY: justification to the unsafe
ShellExecuteW call in the repo URL opening logic, explaining that the wide
string pointer is valid and null-terminated, and that the null operation/params
are intentional and acceptable for this Windows FFI call. Update the unsafe
block around ShellExecuteW in the URL-launching path to follow the repo’s unsafe
conventions, and include the required upstream reference with a pinned commit
SHA if you are documenting the external API behavior.
- Around line 66-77: get_repo_url_from_registry is rebuilding a new
ThrottledClient on every package lookup, which is wasteful for multi-package
runs. Move the ThrottledClient::for_installs construction out of
get_repo_url_from_registry and into run so the client is created once and
reused, then pass the shared client into get_repo_url_from_registry (and any
helpers it calls) instead of recreating the connection pool each time.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 247eb84c-200c-4665-92d6-0eec20843340
📒 Files selected for processing (5)
pacquet/crates/cli/src/cli_args.rspacquet/crates/cli/src/cli_args/cli_command.rspacquet/crates/cli/src/cli_args/dispatch.rspacquet/crates/cli/src/cli_args/dispatch_query.rspacquet/crates/cli/src/cli_args/repo.rs
|
Code review by qodo was updated up to the latest commit 774d123 |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #12702 +/- ##
==========================================
- Coverage 85.25% 85.09% -0.17%
==========================================
Files 396 398 +2
Lines 60060 61561 +1501
==========================================
+ Hits 51207 52387 +1180
- Misses 8853 9174 +321 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Integrated-Benchmark Report (Linux)Commit: Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD. Scenario: Isolated linker: fresh restore, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.433837114720001,
"stddev": 0.20930403329673944,
"median": 4.41370911912,
"user": 4.05332546,
"system": 2.1661827800000006,
"min": 4.17392106862,
"max": 4.86141149262,
"times": [
4.25476705362,
4.52065670162,
4.3993881236200005,
4.30442826162,
4.86141149262,
4.676355697620001,
4.45059239662,
4.17392106862,
4.42803011462,
4.268820236620001
]
},
{
"command": "pacquet@main",
"mean": 4.34799034272,
"stddev": 0.17066200336056894,
"median": 4.32839994862,
"user": 4.00662966,
"system": 2.16897428,
"min": 4.16315777262,
"max": 4.625899860620001,
"times": [
4.364937240620001,
4.189562001620001,
4.393061414620001,
4.619822095620001,
4.625899860620001,
4.4135782506200005,
4.16315777262,
4.24987478262,
4.29186265662,
4.16814735162
]
},
{
"command": "pnpr@HEAD",
"mean": 2.6422446187200004,
"stddev": 0.08138507579652692,
"median": 2.6525964006200002,
"user": 2.9877334599999994,
"system": 1.8524739799999999,
"min": 2.53358285562,
"max": 2.76615685162,
"times": [
2.57961811562,
2.67913824662,
2.70548860962,
2.76615685162,
2.68402718562,
2.58128499362,
2.62605455462,
2.53358285562,
2.72653091562,
2.54056385862
]
},
{
"command": "pnpr@main",
"mean": 2.78453706672,
"stddev": 0.1874481404898183,
"median": 2.81918574562,
"user": 2.9571713599999994,
"system": 1.84234868,
"min": 2.53619006962,
"max": 3.00775168162,
"times": [
2.98407627362,
2.76397489862,
2.90133332762,
2.62923218762,
2.53619006962,
2.87439659262,
2.97515713662,
2.63126572362,
2.5419927756200003,
3.00775168162
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.48248755704000007,
"stddev": 0.01882911951046065,
"median": 0.47600150654000006,
"user": 0.38450598,
"system": 0.7907888000000001,
"min": 0.46435035754000004,
"max": 0.52075916354,
"times": [
0.52075916354,
0.49783724354000003,
0.47401991054000003,
0.50422158354,
0.47773743054000006,
0.46558850354000003,
0.47868301354000004,
0.47426558254,
0.46435035754000004,
0.46741278154000004
]
},
{
"command": "pacquet@main",
"mean": 0.48062496503999996,
"stddev": 0.011315773974443884,
"median": 0.47724199604000006,
"user": 0.38906838,
"system": 0.7859508,
"min": 0.46912468354000003,
"max": 0.50780882154,
"times": [
0.49047636454000004,
0.47738546254,
0.47129176054000005,
0.46912468354000003,
0.47629452454000004,
0.47709852954000004,
0.47836555354000004,
0.50780882154,
0.47439542854000005,
0.48400852154
]
},
{
"command": "pnpr@HEAD",
"mean": 0.56467989994,
"stddev": 0.09254383793316508,
"median": 0.5145622275399999,
"user": 0.39104037999999997,
"system": 0.8213991,
"min": 0.49297145354000005,
"max": 0.74816349354,
"times": [
0.50430018754,
0.5202284175399999,
0.49297145354000005,
0.50077403054,
0.49839175554000004,
0.74816349354,
0.68721740754,
0.64008742454,
0.54576879154,
0.50889603754
]
},
{
"command": "pnpr@main",
"mean": 0.5200231691400001,
"stddev": 0.01955773050112286,
"median": 0.5177117390399999,
"user": 0.39444747999999996,
"system": 0.8123631999999998,
"min": 0.49971625854000007,
"max": 0.56210066254,
"times": [
0.56210066254,
0.52678886754,
0.50453177854,
0.50044763054,
0.52258796954,
0.50566752654,
0.53348986654,
0.53206562254,
0.5128355085399999,
0.49971625854000007
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.597548531680001,
"stddev": 0.05984670527226594,
"median": 4.58521920688,
"user": 4.171333199999999,
"system": 2.12097828,
"min": 4.5357153638800005,
"max": 4.72697472888,
"times": [
4.57823836888,
4.64385301088,
4.5854754378800004,
4.59657504788,
4.58496297588,
4.5357153638800005,
4.64410064388,
4.72697472888,
4.539869690880001,
4.53972004788
]
},
{
"command": "pacquet@main",
"mean": 4.61349409968,
"stddev": 0.04493876801625252,
"median": 4.60119291288,
"user": 4.134300299999999,
"system": 2.13837848,
"min": 4.54115756088,
"max": 4.68380440388,
"times": [
4.66366920288,
4.60498930588,
4.58404671088,
4.68380440388,
4.64022816988,
4.58987485788,
4.65366024588,
4.54115756088,
4.59739651988,
4.57611401888
]
},
{
"command": "pnpr@HEAD",
"mean": 2.6529518042799998,
"stddev": 0.11818916167587372,
"median": 2.60551963438,
"user": 2.8227588,
"system": 1.8136042800000003,
"min": 2.51239425488,
"max": 2.88487477888,
"times": [
2.56194057888,
2.80016198488,
2.59320493288,
2.5979977068799998,
2.75138018388,
2.88487477888,
2.51239425488,
2.61657826188,
2.61304156188,
2.5979437978799997
]
},
{
"command": "pnpr@main",
"mean": 2.56827112648,
"stddev": 0.08686611807468356,
"median": 2.5492706938799996,
"user": 2.8106025000000003,
"system": 1.78531618,
"min": 2.44434561388,
"max": 2.71845584088,
"times": [
2.59967487188,
2.70174112188,
2.58383280888,
2.44434561388,
2.50421390988,
2.71845584088,
2.56476566088,
2.5337757268799996,
2.52533286788,
2.5065728418799997
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.1461372877399998,
"stddev": 0.006771784830264861,
"median": 1.14453097534,
"user": 1.3079899400000001,
"system": 1.01221564,
"min": 1.1369686618400001,
"max": 1.15835862284,
"times": [
1.14042029084,
1.14348995484,
1.14536068184,
1.1369686618400001,
1.14764477784,
1.15171029984,
1.13983470284,
1.1437012688400001,
1.15835862284,
1.15388361584
]
},
{
"command": "pacquet@main",
"mean": 1.23762315304,
"stddev": 0.1456238624164348,
"median": 1.1591980313399999,
"user": 1.36485364,
"system": 1.08676374,
"min": 1.12207180584,
"max": 1.5317467748400002,
"times": [
1.5317467748400002,
1.4300730268400001,
1.14512243484,
1.12207180584,
1.18204935184,
1.14878123884,
1.14444685784,
1.3535439768400002,
1.15984467984,
1.15855138284
]
},
{
"command": "pnpr@HEAD",
"mean": 0.49146234734000005,
"stddev": 0.005866105683589101,
"median": 0.4912456203400001,
"user": 0.33201004,
"system": 0.74892884,
"min": 0.47951921084000004,
"max": 0.4996506958400001,
"times": [
0.47951921084000004,
0.49238176384000004,
0.48653777284000005,
0.49067347384000004,
0.4913174658400001,
0.48901878684000005,
0.4911737748400001,
0.4996506958400001,
0.49838852984000004,
0.49596199884000003
]
},
{
"command": "pnpr@main",
"mean": 0.49646039904000006,
"stddev": 0.007963619886198833,
"median": 0.49704245834000005,
"user": 0.33581364,
"system": 0.7535568400000001,
"min": 0.48172666884000004,
"max": 0.5132057228400001,
"times": [
0.49425900784000004,
0.49377807784000005,
0.48172666884000004,
0.49835559084000003,
0.5132057228400001,
0.49591462984000007,
0.49948226384000005,
0.49933635684000005,
0.49037538484000004,
0.49817028684000003
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.8537572791800003,
"stddev": 0.05115658378873497,
"median": 2.8402476616800003,
"user": 1.8129241999999994,
"system": 1.20650736,
"min": 2.8148536706800003,
"max": 2.9899576966800003,
"times": [
2.8235046506800003,
2.8533254346800003,
2.84104101668,
2.82462836468,
2.8148536706800003,
2.86853084968,
2.8394543066800004,
2.8208651486800003,
2.9899576966800003,
2.86141165268
]
},
{
"command": "pacquet@main",
"mean": 2.86689964478,
"stddev": 0.05558179425867631,
"median": 2.8607931351800002,
"user": 1.7932012,
"system": 1.22589526,
"min": 2.81821879168,
"max": 3.01407336968,
"times": [
2.86270799068,
2.83269366768,
2.83388184068,
2.85940156968,
2.87450073768,
2.87920111668,
2.81821879168,
2.8621847006800003,
3.01407336968,
2.8321326626800003
]
},
{
"command": "pnpr@HEAD",
"mean": 0.5104732758800001,
"stddev": 0.0026244087773728227,
"median": 0.5111204801800001,
"user": 0.3399696,
"system": 0.7827311600000001,
"min": 0.5066447416800001,
"max": 0.5136454246800001,
"times": [
0.5099037566800001,
0.5120543916800001,
0.50727857868,
0.5072351836800001,
0.51291955168,
0.5066447416800001,
0.5123615196800001,
0.5136454246800001,
0.5101865686800001,
0.51250304168
]
},
{
"command": "pnpr@main",
"mean": 0.49346316848000005,
"stddev": 0.00528490700367657,
"median": 0.49378568868000006,
"user": 0.3386639,
"system": 0.7441559600000001,
"min": 0.48692545868000003,
"max": 0.5043466886800001,
"times": [
0.49540816368,
0.48920729668,
0.5043466886800001,
0.49575648468,
0.48692545868000003,
0.49422460268,
0.49769273168,
0.48990959068,
0.49334677468000004,
0.48781389268
]
}
]
} |
|
| Branch | pr/12702 |
| Testbed | pacquet |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result milliseconds (ms) (Result Δ%) | Upper Boundary milliseconds (ms) (Limit %) |
|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 4,597.55 ms(-3.19%)Baseline: 4,749.26 ms | 5,699.11 ms (80.67%) |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot 🚷 view threshold | 2,853.76 ms(-6.27%)Baseline: 3,044.82 ms | 3,653.78 ms (78.10%) |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 1,146.14 ms(-15.42%)Baseline: 1,355.02 ms | 1,626.02 ms (70.49%) |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 4,433.84 ms(-8.35%)Baseline: 4,837.58 ms | 5,805.09 ms (76.38%) |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 482.49 ms(-25.62%)Baseline: 648.68 ms | 778.42 ms (61.98%) |
|
| Branch | pr/12702 |
| Testbed | pnpr |
⚠️ 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-thresholdsflag.
Click to view all benchmark results
| Benchmark | Latency | milliseconds (ms) |
|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot | 2,652.95 ms |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot | 510.47 ms |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot | 491.46 ms |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot | 2,642.24 ms |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot | 564.68 ms |
|
Code review by qodo was updated up to the latest commit b676fa6 |
|
Code review by qodo was updated up to the latest commit b676fa6 |
|
Code review by qodo was updated up to the latest commit d94c0ba |
1 similar comment
|
Code review by qodo was updated up to the latest commit d94c0ba |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pacquet/crates/cli/tests/repo.rs`:
- Around line 25-30: The repo CLI integration tests are only checking a broad
stderr substring, so they can miss regressions in the pnpm error contract.
Update the assertions in the repo tests to match the user-visible output from
RepoError::NoRepoUrlLocal in cli_args/repo.rs by verifying the specific error
code ERR_PNPM_NO_REPO_URL and the local-project guidance text, rather than just
“repository URL”. Use the existing repo test cases to pin the exact stderr
contract so behavior stays aligned with pnpm.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 3b28e54c-5277-4ff9-9dca-cf16ce758820
📒 Files selected for processing (1)
pacquet/crates/cli/tests/repo.rs
|
Code review by qodo was updated up to the latest commit f4e4dc3 |
1 similar comment
|
Code review by qodo was updated up to the latest commit f4e4dc3 |
…ion tests Verify both the error code and the guidance text in stderr assertions to prevent regressions in the user-visible error contract.
|
Code review by qodo was updated up to the latest commit a27a91c |
|
Code review by qodo was updated up to the latest commit a27a91c |
| fn parse_allowed_versions( | ||
| allowed: &HashMap<String, String>, | ||
| ) -> (AllowAllMatcher, AllowByParentMatcher) { | ||
| let mut match_all: HashMap<String, Vec<String>> = HashMap::new(); | ||
| let mut by_parent: HashMap<String, HashMap<String, Vec<String>>> = HashMap::new(); | ||
|
|
||
| for (selector, spec) in allowed { | ||
| if let Some((parent, target)) = selector.split_once('>') { | ||
| let parent_name = parent.trim().to_string(); | ||
| let target_name = target.trim().to_string(); | ||
| let ranges: Vec<String> = spec.split("||").map(|s| s.trim().to_string()).collect(); | ||
| by_parent | ||
| .entry(parent_name) | ||
| .or_default() | ||
| .entry(target_name) | ||
| .or_default() | ||
| .extend(ranges); | ||
| } else { | ||
| let target_name = if let Some((name, _version)) = selector.split_once('@') { | ||
| name.to_string() | ||
| } else { | ||
| selector.clone() | ||
| }; | ||
| let ranges: Vec<String> = spec.split("||").map(|s| s.trim().to_string()).collect(); | ||
| match_all.entry(target_name).or_default().extend(ranges); | ||
| } |
There was a problem hiding this comment.
1. Allowedversions selector misparsed 🐞 Bug ≡ Correctness
parse_allowed_versions() misparses peerDependencyRules.allowedVersions: it splits on the first @ (breaking scoped names like @scope/pkg) and it stores parent@range literally even though filtering later matches only by declaring_parent.name, making documented parent@range>peer rules unreachable and causing false peer-issue failures/exit(1).
Agent Prompt
### Issue description
`peerDependencyRules.allowedVersions` selectors are parsed incorrectly:
- Global selectors with scoped package names (e.g. `@types/node`) are split at the leading `@` and become an empty key.
- Override selectors of the documented form `parent@range>peer` are stored with `parent@range` as the key, but matching later uses only the declaring parent **name**, so these rules can never apply.
This makes configured peer-dependency suppressions ineffective and can cause `pacquet peers` to exit non-zero unexpectedly.
### Issue Context
The config schema explicitly documents `allowedVersions` selectors supporting `parent>name` and `parent@range>name`.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/peers.rs[430-498]
- pacquet/crates/config/src/workspace_yaml.rs[458-466]
- pacquet/crates/cli/src/cli_args/peers/tests.rs[197-233]
### What to change
1. Implement selector parsing that:
- Preserves scoped package names (do not split the leading `@`).
- Supports `name@range` only when there is an `@` **after** the name (e.g. `react@^18`, `@scope/pkg@^2`).
- For `parent@range>peer`, either:
- strip `@range` from the parent key and additionally store the range so matching can check `satisfies(declaring_parent.version, range)`, or
- model a structured key (parent name + optional range) in the matcher map.
2. Update the matching logic in `filter_peer_issues()` accordingly (it currently uses `allow_by_parent.get(&declaring_parent.name)`).
3. Add unit tests:
- `allowedVersions: { "@types/node": "^20" }` should apply.
- `allowedVersions: { "foo@^1.0.0>react": "^18" }` should apply only when declaring parent version satisfies `^1.0.0`.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| let importer_ids: Vec<String> = if recursive || config_has_workspace(lockfile_dir, dir) { | ||
| lockfile.importers.keys().cloned().collect() | ||
| } else { | ||
| vec![resolve_importer_id(lockfile_dir, dir)] | ||
| }; | ||
|
|
||
| let mut result: IssuesByProjects = HashMap::new(); | ||
|
|
||
| for importer_id in &importer_ids { | ||
| let Some(importer) = lockfile.importers.get(importer_id.as_str()) else { continue }; | ||
|
|
||
| let mut issues = PeerIssues { | ||
| bad: HashMap::new(), | ||
| missing: HashMap::new(), | ||
| conflicts: Vec::new(), | ||
| intersections: HashMap::new(), | ||
| }; | ||
|
|
||
| let mut visited = HashSet::new(); | ||
|
|
||
| let groups = [ | ||
| (&importer.dependencies, false), | ||
| (&importer.dev_dependencies, false), | ||
| (&importer.optional_dependencies, true), | ||
| ]; | ||
|
|
||
| for (dep_map, _is_optional) in &groups { | ||
| let Some(dep_map) = dep_map else { continue }; | ||
| for (alias, spec) in dep_map { | ||
| if let Some(key) = spec.version.resolved_key(alias) { | ||
| walk_snapshot(&key, snapshots, packages, &[], &mut issues, &mut visited); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| let merged = merge_missing_peers(&issues.missing); | ||
| issues.conflicts = merged.conflicts; | ||
| issues.intersections = merged.intersections; | ||
|
|
||
| result.insert(importer_id.clone(), issues); | ||
| } | ||
|
|
||
| result | ||
| } | ||
|
|
||
| fn config_has_workspace(lockfile_dir: &std::path::Path, dir: &std::path::Path) -> bool { | ||
| lockfile_dir != dir || lockfile_dir.parent().is_none() | ||
| } |
There was a problem hiding this comment.
2. Peers scans all importers 🐞 Bug ➹ Performance
check_peer_dependencies_from_lockfile() scans every lockfile importer whenever `lockfile_dir != dir (workspace), even when recursive` is false, which can report issues from unrelated projects and causes large workspaces to do full-graph work unexpectedly.
Agent Prompt
### Issue description
In `check_peer_dependencies_from_lockfile()`, importer selection expands to **all** importers when `config_has_workspace(lockfile_dir, dir)` is true. Since `config_has_workspace()` returns true whenever `lockfile_dir != dir`, running `pacquet peers` from a workspace subdirectory (without `--recursive`) still scans and reports across the entire workspace.
This is both a performance regression (full-graph traversal on big workspaces) and a correctness/UX issue (failures caused by unrelated workspace projects).
### Issue Context
Other CLI commands (e.g. `list`) compute a single importer id from `dir` vs `lockfile_dir` and operate on that importer unless recursion is explicitly requested.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/peers.rs[112-182]
- pacquet/crates/cli/src/cli_args/list.rs[104-136]
### What to change
1. Remove the implicit workspace fan-out from `check_peer_dependencies_from_lockfile()`.
- Use `lockfile.importers.keys()` only when `recursive` is true.
- Otherwise, operate only on `resolve_importer_id(lockfile_dir, dir)` (optionally falling back to root importer if missing, similar to `list`).
2. (Optional perf improvement) If recursive mode still needs to scan all importers, consider de-duplicating traversal work across importers (e.g., cache per-snapshot results) to avoid repeating the same graph walk N times.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Code review by qodo was updated up to the latest commit c32753a |
| fn try_hosted_shorthand(raw_url: &str, directory: Option<&str>) -> Option<String> { | ||
| let cleaned = | ||
| raw_url.strip_prefix("git+").unwrap_or(raw_url).strip_prefix("git://").unwrap_or(raw_url); | ||
|
|
There was a problem hiding this comment.
1. Git+ shorthand not stripped 🐞 Bug ≡ Correctness
try_hosted_shorthand() and try_hosted_url() attempt to strip git+ then git://, but the second unwrap_or(raw_url) can revert to the original string and undo the git+ strip. This breaks valid inputs like git+github:owner/repo by preventing hosted-shorthand detection and can make pacquet repo fail to resolve a repository URL.
Agent Prompt
### Issue description
`try_hosted_shorthand()` and `try_hosted_url()` compute `cleaned` by chaining `strip_prefix()` calls but fall back to `raw_url` on the second `unwrap_or(...)`, which can reintroduce `git+` and break hosted-shorthand parsing (e.g. `git+github:owner/repo`).
### Issue Context
This affects repository URL normalization for `pacquet repo` when a `repository.url` uses a hosted shorthand with a `git+` prefix.
### Fix
Preserve the already-stripped intermediate value when the `git://` prefix is not present, e.g.:
- `let cleaned = raw_url.strip_prefix("git+").unwrap_or(raw_url);`
- `let cleaned = cleaned.strip_prefix("git://").unwrap_or(cleaned);`
Add a unit test for at least:
- `repository_to_web_url("git+github:owner/repo", None)`
(and the analogous `git+gitlab:` / `git+bitbucket:` forms if supported).
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[169-172]
- pacquet/crates/cli/src/cli_args/repo.rs[257-260]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Code review by qodo was updated up to the latest commit c32753a |
| for url in urls { | ||
| match open_url(&url) { | ||
| Ok(()) => {} | ||
| Err(e) => { | ||
| let redacted = redact_url(&url); | ||
| R::emit(&LogEvent::Pnpm(PnpmLog { | ||
| level: LogLevel::Warn, | ||
| message: format!("Could not open browser: {e}"), | ||
| prefix: prefix.clone(), | ||
| })); | ||
| println!("{redacted}"); | ||
| } |
There was a problem hiding this comment.
1. repo prints url with ndjsonreporter 📎 Requirement gap ◔ Observability
RepoArgs::run writes the repository URL to stdout via println!, even when dispatch_query::repo selects NdjsonReporter/SilentReporter. This can corrupt NDJSON output (and breaks log-event-only expectations), undermining the logging parity goal.
Agent Prompt
## Issue description
`pacquet repo` prints `redacted` URLs using `println!`, which bypasses the reporter system and can break NDJSON output when `ReporterType::Ndjson` is selected.
## Issue Context
`dispatch_query::repo` explicitly dispatches `RepoArgs::run` with `NdjsonReporter`/`SilentReporter`, so user-visible output should be emitted via `R::emit(...)` (or otherwise gated) rather than unconditional stdout printing.
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[68-79]
- pacquet/crates/cli/src/cli_args/dispatch_query.rs[204-213]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| let ranges: Vec<&str> = issues.iter().map(|i| i.wanted_range.as_str()).collect(); | ||
| let unique: HashSet<&&str> = ranges.iter().collect(); | ||
| if unique.len() == 1 { | ||
| intersections.insert(peer_name.clone(), issues[0].wanted_range.clone()); | ||
| continue; | ||
| } | ||
| let range_owned: Vec<String> = issues.iter().map(|i| i.wanted_range.clone()).collect(); | ||
| if have_common_version(&range_owned) { | ||
| intersections.insert(peer_name.clone(), issues[0].wanted_range.clone()); | ||
| } else { |
There was a problem hiding this comment.
2. Bad peer range intersection 🐞 Bug ≡ Correctness
merge_missing_peers() records an intersections[peer] entry but, when multiple non-equal ranges exist, it stores issues[0].wanted_range instead of the actual semver intersection of all required ranges. This breaks pnpm parity and makes --json output (and conflict/intersection classification) incorrect for missing peers that have multiple wanted ranges.
Agent Prompt
## Issue description
`merge_missing_peers()` currently sets `intersections[peer]` to `issues[0].wanted_range` when multiple wanted ranges exist and are deemed compatible via `have_common_version()`. This does not compute the actual range intersection (often narrower than the first range) and diverges from upstream pnpm which uses semver-range intersection.
## Issue Context
Upstream implementation (`pnpm11/.../checkPeerDependencies.ts`) computes `intersection = safeIntersect(ranges)` and stores that exact intersection string.
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/peers.rs[314-341]
## Suggested fix approach
- Replace the boolean `have_common_version(...)` pathway with a function that returns `Option<String>` representing the actual intersection string.
- Implement real semver-range intersection behavior (port the minimal logic used by `semver-range-intersect`, or add an internal helper/crate) and set `intersections[peer]` to that computed intersection.
- If intersection cannot be computed or is empty, classify as conflict (push to `conflicts`).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| for url in urls { | ||
| match open_url(&url) { | ||
| Ok(()) => {} | ||
| Err(e) => { | ||
| let redacted = redact_url(&url); | ||
| R::emit(&LogEvent::Pnpm(PnpmLog { | ||
| level: LogLevel::Warn, | ||
| message: format!("Could not open browser: {e}"), | ||
| prefix: prefix.clone(), | ||
| })); | ||
| println!("{redacted}"); | ||
| } |
There was a problem hiding this comment.
3. Repo url terminal injection 🐞 Bug ⛨ Security
On browser-open failure, pacquet repo prints the (attacker-controlled) repository URL directly to stdout without stripping control characters. A crafted repository value from package.json or registry metadata can inject terminal escape/control sequences into user output.
Agent Prompt
## Issue description
`pacquet repo` prints `redacted` directly via `println!("{redacted}")` on browser-open failure. Repository URLs come from attacker-controlled sources (local manifests / registry metadata), and may include control characters in cases where URL parsing fails and the code falls back to the raw string.
## Issue Context
The CLI already has `cli_args::sanitize::sanitize()` specifically to prevent stored/remote metadata from emitting raw escape sequences to the user's terminal.
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[68-79]
## Suggested fix approach
- Apply `sanitize()` before printing the fallback URL, e.g. `println!("{}", sanitize(&redacted));`.
- Consider also sanitizing the URL in any other direct-to-terminal output paths added by this command (stdout/stderr).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Code review by qodo was updated up to the latest commit 9d526e9 |
| fn select_package_version( | ||
| package: &pacquet_registry::Package, | ||
| range: &str, | ||
| ) -> Option<std::sync::Arc<pacquet_registry::PackageVersion>> { | ||
| if range.is_empty() || range == "latest" { | ||
| return package.latest(); | ||
| } | ||
| if let Some(tag_version) = package.dist_tag(range) { | ||
| return package.versions.get(tag_version); | ||
| } | ||
| package.pinned_version(range) | ||
| } |
There was a problem hiding this comment.
1. Repo range parse panic 🐞 Bug ☼ Reliability
select_package_version() forwards the user/manifest selector into Package::pinned_version(), which unwrap()s Range::parse; non-semver selectors (e.g. workspace:*, git URLs) can crash pacquet repo via a panic.
Agent Prompt
### Issue description
`pacquet repo` can panic when the CLI arg (or derived `range`) is not a valid semver range because `pacquet_registry::Package::pinned_version()` calls `version_range.parse().unwrap()`. `select_package_version()` calls `pinned_version()` for any selector that is not empty/`latest`/a dist-tag, so inputs like `foo@workspace:*` or other non-semver selectors can crash the process.
### Issue Context
`get_repo_url_from_registry()` uses `parse_wanted_dependency()` + `PackageManifest::resolve_registry_dependency()` to produce `(resolved_name, range)`, then `select_package_version(package, range)` decides which version to read `repository` from.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[155-166]
- pacquet/crates/registry/src/package.rs[139-156]
- pacquet/crates/package-manifest/src/lib.rs[221-235]
- pacquet/crates/resolving-parse-wanted-dependency/src/lib.rs[38-60]
### Implementation notes
- Make `Package::pinned_version()` non-panicking (e.g. `let range = version_range.parse().ok()?;`). This is the safest central fix.
- Optionally add input validation in `select_package_version()` to avoid calling `pinned_version()` for selectors that are clearly not semver ranges, returning `None` (and thus a structured `RepoError`) instead of crashing.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Code review by qodo was updated up to the latest commit 9d526e9 |
- SCP-style SSH URL parsing (git@github.com:owner/repo.git) - Preserve PackageManifestError details for non-missing-manifest errors - Combine directory and fragment in browse URLs - Use Config-based RetryOpts instead of Default - Check browser exit status via .status() instead of .spawn() - Strip .git suffix in hosted shorthand and build_hosted_browse_url - Use tempfile::TempDir in tests to avoid collisions - Strip fragment/query from base_url before appending path - Redact query and fragment in URL fallback output - Select version matching user spec (dist-tag / semver range) - Normalize git:// to https:// for web URL construction - Thread Reporter through RepoArgs::run for structured log events - Hoist ThrottledClient and registries map outside per-package loop
|
Code review by qodo was updated up to the latest commit 8e4b183 |
|
Code review by qodo was updated up to the latest commit 8e4b183 |
|
Code review by qodo was updated up to the latest commit 0b7d2dc |
| let mut parsed = url::Url::parse(&cleaned).ok()?; | ||
| if parsed.scheme() != "http" && parsed.scheme() != "https" { | ||
| return None; | ||
| } | ||
|
|
||
| let fragment = try_extract_fragment(raw_url); | ||
| parsed.set_fragment(None); | ||
| parsed.set_query(None); | ||
|
|
||
| let mut url = parsed.to_string(); |
There was a problem hiding this comment.
1. Repo url leaks credentials 🐞 Bug ⛨ Security
repository_to_web_url() strips query/fragment but preserves URL userinfo (username/password), so repository values like https://token@github.com/org/repo.git will be passed to xdg-open/open/ShellExecuteW with the secret intact. This can leak credentials via process arguments and cause unintended credential transmission/storage outside the intended auth flow.
Agent Prompt
### Issue description
`repository_to_web_url()` constructs the final browser URL from a parsed `url::Url`, but never clears `username`/`password`. Any credentials embedded in `repository.url` (from local `package.json` or registry metadata) will survive into the URL that is opened.
### Issue Context
Other parts of this repo treat URL userinfo as sensitive and explicitly strip/redact it.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[198-207]
### Suggested fix
After parsing and scheme validation (and alongside the existing `set_query(None)` / `set_fragment(None)`), clear userinfo before calling `to_string()`:
- `let _ = parsed.set_username("");`
- `let _ = parsed.set_password(None);`
This ensures the success path never forwards embedded credentials to the OS/browser opener.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Code review by qodo was updated up to the latest commit 0b7d2dc |
| let (hosted, path) = if let Some(rest) = cleaned.strip_prefix("github:") { | ||
| (HostedRepo { base_url: "https://github.com".to_string(), default_branch: "master" }, rest) | ||
| } else if let Some(rest) = cleaned.strip_prefix("gitlab:") { | ||
| (HostedRepo { base_url: "https://gitlab.com".to_string(), default_branch: "master" }, rest) | ||
| } else if let Some(rest) = cleaned.strip_prefix("bitbucket:") { | ||
| ( | ||
| HostedRepo { base_url: "https://bitbucket.org".to_string(), default_branch: "master" }, | ||
| rest, | ||
| ) |
There was a problem hiding this comment.
1. Hardcoded master branch 🐞 Bug ≡ Correctness
try_hosted_shorthand()/try_hosted_url() default to master when building browse URLs for monorepo repository.directory, so many repos with default branch main (or any non-master) will open a broken/wrong URL. This diverges from pnpm’s reference implementation, which uses HEAD to follow the host’s default branch when appending a directory.
Agent Prompt
### Issue description
`pacquet repo` builds hosted browse URLs with `/tree/master/...` when `repository.directory` is present and no fragment/branch is specified. This is wrong for repositories whose default branch is not `master` (very common: `main`) and breaks parity with pnpm, which uses `HEAD` to track the default branch.
### Issue Context
The Rust implementation already uses `HEAD` in the generic URL path when `directory` is set, but the hosted-shorthand/hosted-url paths hardcode `master`, creating inconsistent behavior depending on which parser path matches.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/repo.rs[215-222]
- pacquet/crates/cli/src/cli_args/repo.rs[234-266]
- pacquet/crates/cli/src/cli_args/repo.rs[306-316]
- pacquet/crates/cli/src/cli_args/repo.rs[341-360]
### Suggested change
- Replace the hosted default branch fallback from `master` to `HEAD` anywhere it’s only used when `directory` is present and no explicit fragment/branch was provided.
- Keep behavior consistent across:
- `try_hosted_shorthand()`
- `try_user_repo_shorthand()`
- `try_hosted_url()`
- Update/add unit tests to assert `/tree/HEAD/...` for directory cases in hosted shorthand and hosted URL cases (matching pnpm).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Code review by qodo was updated up to the latest commit 666fcc6 |
1 similar comment
|
Code review by qodo was updated up to the latest commit 666fcc6 |
Brings in the filter->recursive promotion (#12726), the `bin` (#12703) and `repo` (#12702) commands, and a pnpr cold-store perf fix (#12709). Resolved one conflict in cli_args/dispatch_query.rs: the publish and bin handlers and their imports were both added at the same spot, so both were kept. Adapted recursive publish to #12726's new shared-function signatures: `discover_workspace_projects` now returns `(projects, patterns)` and `select_recursive_projects` takes an `AutoExcludeRoot`. Publish passes `AutoExcludeRoot::Disabled` because it is not in pnpm's run/exec/add/test root-auto-exclusion set; its private/unnamed eligibility check drops the workspace root instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtBQzmLLDU3RcGzzCMopPB
Summary
Implements the
pnpm repocommand in the Rust pacquet port, matching the existing TypeScript CLI behavior. The command opens the repository URL of a package in the browser, supporting lookup from the local package manifest or the npm registry.The implementation supports looking up the repository of the current project from its
package.jsonmanifest, or looking up any package from the npm registry by name, optionally with a version specifier or dist-tag. Repository URLs can be specified as a string, an object withurland optionaldirectoryfields, or a shorthand likeowner/repofor GitHub/GitLab/Bitbucket.URL formats handled include HTTPS, SSH (
git@host:pathSCP-style),git://, andgit+prefixed URLs. The.gitsuffix is stripped and shorthand references are expanded to proper web URLs. When the registry is queried, version selection respects dist-tags and semver ranges. The browser is launched via the OSopen/xdg-opencommand, and errors are reported through the structuredReportertrait as warnings rather than failing the command.Related to: #11633
Squash Commit Body
Checklist
Written by an agent (Claude Code, claude-opus-4-7).