Skip to content

feat(pacquet-cli): implement pkg command#12795

Merged
zkochan merged 4 commits into
pnpm:mainfrom
kairosci:feat/pacquet-pkg
Jul 7, 2026
Merged

feat(pacquet-cli): implement pkg command#12795
zkochan merged 4 commits into
pnpm:mainfrom
kairosci:feat/pacquet-pkg

Conversation

@kairosci

@kairosci kairosci commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Implement the pnpm pkg command in Rust for the pacquet CLI, ported from the TypeScript implementation at pnpm/pkg-manifest/commands/src/pkg.ts. The command manages package.json through four subcommands: get, set, delete, and fix. This implementation provides native Rust subcommand dispatch via clap instead of the TypeScript approach of parsing positional arguments, while preserving identical observable behavior.

Related to: #11633.

Squash Commit Body

Port the pkg command from the TypeScript CLI to the Rust pacquet CLI.

The `pnpm pkg get` subcommand retrieves values from package.json using
property-path syntax (dot and bracket notation). With no keys it returns the
full manifest; with one key it returns the raw string value or a JSON-encoded
value; with multiple keys it returns a JSON object of the selected keys.

The `pnpm pkg set` subcommand writes values to package.json using
key=value syntax. The --json flag parses the value as JSON. Intermediate
objects are created automatically for dot-separated paths. Unsafe keys
(__proto__, constructor, prototype) are rejected.

The `pnpm pkg delete` subcommand removes keys from package.json using
property-path syntax. Array indices splice the element rather than leaving
a hole.

The `pnpm pkg fix` subcommand auto-corrects common errors in package.json:
it removes name/version fields with non-string types, dependency/script fields
with non-object types, and bin fields that are neither a string nor an object.

Checklist

  • The change is implemented in both the TypeScript CLI and the Rust
    pacquet/ port, or the description notes what still needs porting.
  • Added a changeset (pnpm changeset) if this PR changes any published
    package. Keep it short and written for pnpm users — it becomes a release note.
  • Added or updated tests.
  • Updated the documentation if needed.

Summary by CodeRabbit

  • New Features
    • Added a new pkg CLI command to view, update, delete, and fix package.json.
    • Supports property-path reads/writes (including nested objects/arrays) with optional pretty JSON handling.
    • Adds recursive operations across workspace projects when selected via configuration/filter options.
  • Bug Fixes
    • Strengthened property-path validation to prevent prototype-pollution-style keys and unsafe segments.
    • Improved fix normalization by removing fields that don’t match expected shapes.
  • Tests
    • Added unit tests covering get/set/delete, recursive behavior helpers, and safety/edge-case validations.

@kairosci
kairosci requested a review from zkochan as a code owner July 4, 2026 16:58
Copilot AI review requested due to automatic review settings July 4, 2026 16:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a new pkg CLI subcommand for reading, writing, deleting, and normalizing package.json fields. It wires the command into dispatch, adds recursive workspace execution, implements property-path helpers, and expands unit coverage.

Changes

pkg subcommand

Layer / File(s) Summary
CLI wiring
pacquet/crates/cli/src/cli_args.rs, pacquet/crates/cli/src/cli_args/cli_command.rs, pacquet/crates/cli/src/cli_args/dispatch.rs, pacquet/crates/cli/src/cli_args/dispatch_script.rs
Declares the pkg module, adds Pkg(PkgArgs) to CliCommand, and routes the command to dispatch_script::pkg.
pkg command handlers
pacquet/crates/cli/src/cli_args/pkg.rs
Defines PkgError, PkgArgs, PkgSubcommand, recursive execution, read/fix helpers, property-path safety checks, array-index handling, and manifest normalization.
Property-path updates and removals
pacquet/crates/cli/src/cli_args/pkg.rs
Implements setters and deleters for manifest values, including nested traversal, intermediate container creation, index validation, and array resizing or removal.
pkg helper tests
pacquet/crates/cli/src/cli_args/pkg/tests.rs
Covers get_output, fix_manifest, and property-path set/delete helpers for nested objects, arrays, missing keys, unsafe paths, and numeric-key handling.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • pnpm/pnpm#12632: Extends the same CLI command-dispatch framework with a different CliCommand variant and routing arm.
  • pnpm/pnpm#12687: Also adds a new CliCommand variant and matching dispatch path in the central CLI routing.
  • pnpm/pnpm#12728: Updates the Rust CLI plumbing by adding a new subcommand variant and dispatch match arm.

Suggested labels: product: pacquet

Suggested reviewers: zkochan, qodo-free-for-open-source-projects

✨ 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 pnpm pkg command to pacquet CLI (get/set/delete/fix)

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Add pkg top-level CLI command with get, set, delete, and fix subcommands.
• Implement property-path manifest reads/writes with --json mode and unsafe-key rejection.
• Add unit tests covering output formatting, mutations, deletions, and manifest auto-fixes.
Diagram

graph TD
  A["pacquet CLI (clap)"] --> B["CliCommand routing"] --> C["pkg command"] --> D["Property-path parser"]
  C --> E["PackageManifest"] --> F[("package.json")]
  C --> E
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Move set/delete helpers into `pacquet_config::property_path`
  • ➕ Centralizes all property-path behavior (parse/get/set/delete) in one crate for reuse by future commands.
  • ➕ Encourages consistent error handling and unsafe-key policy across the CLI.
  • ➖ Requires expanding the config crate’s scope and API surface.
  • ➖ May force a more generic mutation API than this command needs right now.
2. Adopt JSON Pointer / JSONPath library instead of pnpm property-path
  • ➕ Leverages well-tested ecosystem parsing and mutation support.
  • ➕ Potentially simpler implementation for set/delete semantics.
  • ➖ Would likely diverge from pnpm’s observable CLI behavior and accepted syntax.
  • ➖ Adds a dependency and migration complexity for parity-focused porting.

Recommendation: Current approach (pnpm-compatible property-path syntax + custom set/delete semantics) is appropriate for behavior parity. If additional commands need mutation through property paths, consider promoting the set/delete logic into pacquet_config::property_path to avoid duplication and keep semantics consistent.

Files changed (6) +600 / -0

Enhancement (5) +349 / -0
cli_args.rsExport new 'pkg' CLI args module +1/-0

Export new 'pkg' CLI args module

• Adds the 'pkg' module to the CLI args module list so it can be referenced by command parsing and dispatch.

pacquet/crates/cli/src/cli_args.rs

cli_command.rsRegister 'Pkg' as a top-level CLI command +3/-0

Register 'Pkg' as a top-level CLI command

• Introduces 'CliCommand::Pkg(PkgArgs)' and wires it into the clap command enum so 'pnpm pkg ...' is recognized.

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

dispatch.rsRoute 'Pkg' command to script-style dispatcher +1/-0

Route 'Pkg' command to script-style dispatcher

• Adds a dispatch arm mapping 'CliCommand::Pkg' to 'dispatch_script::pkg', keeping it synchronous like other manifest-only commands.

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

dispatch_script.rsAdd synchronous dispatcher for 'pkg' +6/-0

Add synchronous dispatcher for 'pkg'

• Defines 'dispatch_script::pkg' which runs 'PkgArgs::run' against the resolved 'manifest_path' without invoking install/lockfile pipelines.

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

pkg.rsImplement 'pkg get/set/delete/fix' with property-path support +338/-0

Implement 'pkg get/set/delete/fix' with property-path support

• Adds clap subcommand parsing for 'get', 'set', 'delete', and 'fix', including global '--json' behavior. Implements pnpm-style property-path reads, JSON-aware writes, array-element removal on delete, unsafe key rejection, and a 'fix' routine that removes common invalid manifest fields before saving.

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

Tests (1) +251 / -0
tests.rsUnit tests for 'pkg' output and manifest mutations +251/-0

Unit tests for 'pkg' output and manifest mutations

• Adds tests for 'get' formatting behavior, selection semantics (no keys / single key / multiple keys), 'fix' cleanup rules, and property-path set/delete behavior including nested keys and array indices.

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

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

qodo-free-for-open-source-projects Bot commented Jul 4, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Recursive pkg not supported ✓ Resolved 🐞 Bug ≡ Correctness
Description
dispatch_script::pkg always runs against ctx.manifest_path and ignores the global --recursive
execution mode, so pacquet -r pkg ... will silently operate only on the root project instead of
every selected workspace package (unlike pnpm’s implementation). This is an observable behavior
mismatch and can lead to partially-updated workspaces.
Code

pacquet/crates/cli/src/cli_args/dispatch_script.rs[R31-34]

+pub(super) fn pkg<'a>(ctx: &RunCtx<'a>, args: PkgArgs) -> miette::Result<CommandFuture<'a>> {
+    let result = args.run(ctx.manifest_path);
+    Ok(Box::pin(std::future::ready(result)))
+}
Evidence
The Rust dispatch path for pkg never checks ctx.recursive, while other commands (e.g., run)
do. The pnpm reference implementation explicitly supports recursive pkg execution via a dedicated
handler.

pacquet/crates/cli/src/cli_args/dispatch_script.rs[31-34]
pacquet/crates/cli/src/cli_args/dispatch_script.rs[45-60]
pnpm11/pkg-manifest/commands/src/pkg.ts[48-98]
pnpm11/pkg-manifest/commands/src/pkg.ts[86-93]

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

## Issue description
`Pkg` does not honor the global `-r/--recursive` mode. Unlike `run`/`exec`, `dispatch_script::pkg` unconditionally calls `args.run(ctx.manifest_path)` and never iterates workspace projects.
## Issue Context
Upstream pnpm’s `pkg` command has an explicit recursive path (`handleRecursiveCommand`) that runs `get` across selected workspace projects.
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/dispatch_script.rs[31-34]
- pacquet/crates/cli/src/cli_args/pkg.rs[92-113]
## Suggested fix
- Add a `run_recursive(...)` method for `PkgArgs` (similar to `RunArgs::run_recursive`).
- In `dispatch_script::pkg`, branch on `ctx.recursive` and call `args.run_recursive((ctx.config)()?, ctx.dir, ...)` (and/or include filter selection behavior consistent with other recursive commands).
- Ensure output format matches pnpm’s recursive `pkg get` behavior (a JSON map keyed by package name).

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


2. Pkg save mutates unrelated fields 🐞 Bug ≡ Correctness
Description
pkg set/pkg delete/pkg fix persist via PackageManifest::save(), which rewrites
dependencies/devDependencies entries of the form runtime: into engines/devEngines.runtime
and can fail saving when those dependency fields exist but are not JSON objects. This means pkg
can unexpectedly change unrelated keys (or error) even when the user edits a completely different
path, diverging from the intended “raw package.json editor” behavior.
Code

pacquet/crates/cli/src/cli_args/pkg.rs[R168-215]

+fn pkg_set(manifest_path: &Path, pairs: &[String], json: bool) -> miette::Result<()> {
+    if pairs.is_empty() {
+        return Err(PkgError::SetMissingArgs.into());
+    }
+    let mut manifest =
+        PackageManifest::from_path(manifest_path.to_path_buf()).wrap_err("reading package.json")?;
+    let value = manifest.value_mut();
+    for pair in pairs {
+        let eq_index =
+            pair.find('=').ok_or_else(|| PkgError::SetInvalidArg { arg: pair.clone() })?;
+        let key = &pair[..eq_index];
+        let raw_value = &pair[eq_index + 1..];
+        let parsed_value: Value = if json {
+            serde_json::from_str(raw_value)
+                .map_err(|_| PkgError::SetJsonParse { value: raw_value.to_string() })?
+        } else {
+            Value::String(raw_value.to_string())
+        };
+        set_object_value_by_property_path(value, key, parsed_value)?;
+    }
+    manifest.save().wrap_err("saving package.json")?;
+    Ok(())
+}
+
+fn pkg_delete(manifest_path: &Path, keys: &[String]) -> miette::Result<()> {
+    if keys.is_empty() {
+        return Err(PkgError::DeleteMissingArgs.into());
+    }
+    for key in keys {
+        check_unsafe_key_in_path(key)?;
+    }
+    let mut manifest =
+        PackageManifest::from_path(manifest_path.to_path_buf()).wrap_err("reading package.json")?;
+    let value = manifest.value_mut();
+    for key in keys {
+        delete_object_value_by_property_path(value, key)?;
+    }
+    manifest.save().wrap_err("saving package.json")?;
+    Ok(())
+}
+
+fn pkg_fix(manifest_path: &Path) -> miette::Result<()> {
+    let mut manifest =
+        PackageManifest::from_path(manifest_path.to_path_buf()).wrap_err("reading package.json")?;
+    let value = manifest.value_mut();
+    fix_manifest(value);
+    manifest.save().wrap_err("saving package.json")?;
+    Ok(())
Evidence
The new pkg command always persists via PackageManifest::save(), and save() rewrites runtime
dependency entries and can error when dependency fields are not objects, creating unexpected side
effects/failures for a command that should only apply the user’s requested key edits.

pacquet/crates/cli/src/cli_args/pkg.rs[168-215]
pacquet/crates/package-manifest/src/lib.rs[179-190]
pacquet/crates/package-manifest/src/lib.rs[415-443]

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

## Issue description
`pkg set/delete/fix` call `PackageManifest::save()`, but `save()` is not a raw serializer: it performs manifest-wide normalization (`convert_dependencies_to_engines_runtime`) and can reject manifests where `dependencies`/`devDependencies` are present but not objects. This causes `pkg` to mutate unrelated fields and/or fail for manifests that pnpm’s `pkg` command would still edit.
## Issue Context
`PackageManifest::save_and_get_written_value()` clones the JSON and applies runtime/deps folding before writing. That behavior is appropriate for pacquet’s runtime-engine contract, but `pkg` is intended to be a general package.json editor.
## Fix Focus Areas
- Implement a raw/escape-hatch save path that writes the in-memory manifest without calling `convert_dependencies_to_engines_runtime`.
- pacquet/crates/package-manifest/src/lib.rs[179-190]
- Use that raw save method from `pkg set/delete/fix` instead of `save()`.
- pacquet/crates/cli/src/cli_args/pkg.rs[168-215]
(Keep atomic write + permission preservation behavior consistent with `write_atomic`.)

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


3. Index traversal loses position 🐞 Bug ≡ Correctness
Description
In set_object_value_by_property_path, when an intermediate Segment::Index is applied while
current is neither an object nor array, the code replaces current with a container but does not
descend into the indexed slot. Subsequent segments then apply at the wrong level, producing an
incorrect JSON structure for paths that start with (or encounter) an index while the current value
is scalar.
Code

pacquet/crates/cli/src/cli_args/pkg.rs[R346-357]

+                } else {
+                    let replacement = if needs_array {
+                        let mut arr = Vec::with_capacity(index.saturating_add(1));
+                        arr.resize(index.saturating_add(1), Value::Null);
+                        Value::Array(arr)
+                    } else {
+                        let mut map = Map::new();
+                        map.insert(idx_to_string(*idx), Value::Null);
+                        Value::Object(map)
+                    };
+                    *current = replacement;
+                }
Evidence
The traversal assigns *current = replacement without updating the cursor; since
PackageManifest::from_path accepts any JSON Value (no enforcement that the root is an object),
this can occur in real runs against malformed/non-object package.json files and will misplace
subsequent writes.

pacquet/crates/cli/src/cli_args/pkg.rs[313-357]
pacquet/crates/package-manifest/src/lib.rs[122-128]
pacquet/crates/package-manifest/src/lib.rs[139-146]

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

## Issue description
In `set_object_value_by_property_path`, the `Segment::Index` traversal has a branch where `current` is neither object nor array. That branch sets `*current = replacement` but never updates `current` to the newly created `arr[index]` (array case) or `obj[key]` (object case). Any remaining segments are then applied to the wrong node.
### Issue Context
This breaks nested sets for some valid property paths when the JSON root (or an intermediate value reached via index traversal) is scalar and must be replaced to continue.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/pkg.rs[292-360]
### Suggested approach
After `*current = replacement;`:
- If `needs_array` is true: ensure the array has length `index + 1` and set `current = &mut arr[index]`.
- Else: ensure the object contains `key` and set `current = obj.get_mut(&key).unwrap()`.
Add a unit test that demonstrates the bug, e.g. starting with a scalar root and running a path like `[0].a` or similar, and verifying the nested structure is created under key/index `0` rather than at the root.

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


View more (2)
4. Unbounded index cast ✓ Resolved 🐞 Bug ⛨ Security
Description
set_object_value_by_property_path casts numeric segments (Segment::Index(f64)) to usize and
resizes arrays to index + 1 without validating finiteness/integer-ness or guarding overflow, so
extreme inputs can attempt enormous allocations or overflow and crash the CLI (DoS). This bypasses
the repo’s existing array_index() validation rules used by the read path.
Code

pacquet/crates/cli/src/cli_args/pkg.rs[R270-292]

+            Segment::Index(idx) => {
+                let index = *idx as usize;
+                let arr = current.as_array_mut().ok_or_else(|| set_path_err(path))?;
+                if index >= arr.len() {
+                    arr.resize(index + 1, Value::Null);
+                }
+                current = &mut arr[index];
+            }
+        }
+    }
+    match &segments[last_idx] {
+        Segment::Key(k) => {
+            let obj = current.as_object_mut().ok_or_else(|| set_path_err(path))?;
+            obj.insert(k.clone(), value);
+        }
+        Segment::Index(idx) => {
+            let index = *idx as usize;
+            let arr = current.as_array_mut().ok_or_else(|| set_path_err(path))?;
+            if index >= arr.len() {
+                arr.resize(index + 1, Value::Null);
+            }
+            arr[index] = value;
+        }
Evidence
The write path directly casts f64 indices to usize and resizes to index + 1 without checks. In
contrast, the shared property-path read helper explicitly rejects non-integer/NaN/inf indices via
array_index() before indexing arrays.

pacquet/crates/cli/src/cli_args/pkg.rs[251-295]
pacquet/crates/config/src/property_path.rs[16-22]
pacquet/crates/config/src/property_path.rs[319-326]

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

## Issue description
`pkg set` treats property-path numeric segments as array indices by doing `*idx as usize` and `resize(index + 1, ...)` without validating that the number is finite, non-negative, integral, and within a safe bound. With very large numeric literals (or NaN from parsing), the cast can saturate to a huge `usize`, leading to attempted giant allocation or arithmetic overflow.
## Issue Context
The property-path parser used here represents numeric literals as `f64` (`Segment::Index(f64)`), and the read helper in `pacquet_config::property_path` already has an `array_index()` gate that rejects non-integers/NaN/inf. The write path should apply equivalent validation and also protect `index + 1` with checked arithmetic.
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/pkg.rs[251-295]

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


5. Set can't build arrays ✓ Resolved 🐞 Bug ≡ Correctness
Description
pkg set always creates missing intermediate nodes as objects and never creates arrays or replaces
mismatched container types, so pnpm-valid paths like foo[0]=bar fail when the container doesn’t
already exist (or has the wrong shape). This diverges from pnpm’s setObjectValueByPropertyPath*
semantics, which create arrays/objects as needed and replace mismatched intermediates to ensure JSON
round-tripping.
Code

pacquet/crates/cli/src/cli_args/pkg.rs[R259-293]

+    if segments.is_empty() {
+        return Ok(());
+    }
+    let last_idx = segments.len() - 1;
+    let mut current = root;
+    for segment in &segments[..last_idx] {
+        match segment {
+            Segment::Key(k) => {
+                let obj = current.as_object_mut().ok_or_else(|| set_path_err(path))?;
+                current = obj.entry(k.clone()).or_insert_with(|| Value::Object(Map::new()));
+            }
+            Segment::Index(idx) => {
+                let index = *idx as usize;
+                let arr = current.as_array_mut().ok_or_else(|| set_path_err(path))?;
+                if index >= arr.len() {
+                    arr.resize(index + 1, Value::Null);
+                }
+                current = &mut arr[index];
+            }
+        }
+    }
+    match &segments[last_idx] {
+        Segment::Key(k) => {
+            let obj = current.as_object_mut().ok_or_else(|| set_path_err(path))?;
+            obj.insert(k.clone(), value);
+        }
+        Segment::Index(idx) => {
+            let index = *idx as usize;
+            let arr = current.as_array_mut().ok_or_else(|| set_path_err(path))?;
+            if index >= arr.len() {
+                arr.resize(index + 1, Value::Null);
+            }
+            arr[index] = value;
+        }
+    }
Evidence
In Rust, missing Segment::Key intermediates are always initialized as objects, and
Segment::Index requires the current node already be an array. pnpm’s implementation explicitly
creates either [] or {} depending on the next segment and replaces mismatched container shapes
so writes always succeed in a JSON-round-trippable structure.

pacquet/crates/cli/src/cli_args/pkg.rs[251-295]
pnpm11/object/property-path/src/set.ts[14-47]

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

## Issue description
`set_object_value_by_property_path` can only walk into an array if it already exists, and it always inserts `Value::Object` for missing `Segment::Key` intermediates. This prevents creating arrays when the next segment is numeric (e.g. setting `files[0]` when `files` is absent), and it errors instead of replacing scalar/mismatched shapes.
## Issue Context
pnpm’s reference implementation (`pnpm11/object/property-path/src/set.ts`) chooses whether to create an object or an array based on the *next* segment (`needsArray`) and replaces mismatched existing nodes with a fresh container.
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/pkg.rs[251-295]

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



Remediation recommended

6. Large index memory blowup 🐞 Bug ➹ Performance
Description
pkg set uses Vec::resize(index+1, Null) to materialize arrays, so a single large index (allowed
up to 1<<20) can force allocation of ~1M elements; with --recursive this can be repeated across
many workspace projects. This is attacker-controlled CLI input and can lead to severe slowdown or
OOM (practical DoS), even though the index is capped.
Code

pacquet/crates/cli/src/cli_args/pkg.rs[R362-485]

+pub(crate) const MAX_ARRAY_INDEX: usize = 1 << 20;
+
+fn validate_index(idx: f64) -> Result<usize, PkgError> {
+    if idx.fract() != 0.0 || idx.is_sign_negative() || !idx.is_finite() {
+        return Err(PkgError::SetPathError { path: idx.to_string() });
+    }
+    let index = idx as usize;
+    if index > MAX_ARRAY_INDEX {
+        return Err(PkgError::SetPathError { path: idx.to_string() });
+    }
+    Ok(index)
+}
+
+fn idx_to_string(idx: f64) -> String {
+    if idx.fract() == 0.0 && idx.is_finite() { format!("{}", idx as i64) } else { idx.to_string() }
+}
+
+fn set_object_value_by_property_path(
+    root: &mut Value,
+    path: &str,
+    value: Value,
+) -> miette::Result<()> {
+    if path.is_empty() {
+        return Err(PkgError::EmptyPath.into());
+    }
+    check_unsafe_key_in_path(path)?;
+    let segments = parse_property_path(path)
+        .map_err(|err| miette::Report::new(PkgError::InvalidPropertyPath(err)))?;
+    if segments.is_empty() {
+        return Err(PkgError::EmptyPath.into());
+    }
+    let last_idx = segments.len() - 1;
+    let mut current = root;
+    for i in 0..last_idx {
+        let needs_array = matches!(&segments[i + 1], Segment::Index(_));
+        match &segments[i] {
+            Segment::Key(k) => {
+                if !current.is_object() {
+                    *current = Value::Object(Map::new());
+                }
+                let obj = current.as_object_mut().unwrap();
+                let entry = obj.get_mut(k);
+                let is_good = entry
+                    .is_some_and(|val| if needs_array { val.is_array() } else { val.is_object() });
+                if !is_good {
+                    let replacement = if needs_array {
+                        Value::Array(Vec::new())
+                    } else {
+                        Value::Object(Map::new())
+                    };
+                    obj.insert(k.clone(), replacement);
+                }
+                current = obj.get_mut(k).unwrap();
+            }
+            Segment::Index(idx) => {
+                let index = validate_index(*idx)?;
+                if current.is_object() {
+                    let key = idx_to_string(*idx);
+                    let obj = current.as_object_mut().unwrap();
+                    let entry = obj.get_mut(&key);
+                    let is_good = entry.is_some_and(|val| {
+                        if needs_array { val.is_array() } else { val.is_object() }
+                    });
+                    if !is_good {
+                        let replacement = if needs_array {
+                            Value::Array(Vec::new())
+                        } else {
+                            Value::Object(Map::new())
+                        };
+                        obj.insert(key.clone(), replacement);
+                    }
+                    current = obj.get_mut(&key).unwrap();
+                } else if current.is_array() {
+                    let arr = current.as_array_mut().unwrap();
+                    if index >= arr.len() {
+                        arr.resize(index.saturating_add(1), Value::Null);
+                    }
+                    let entry = &mut arr[index];
+                    let is_good = if needs_array { entry.is_array() } else { entry.is_object() };
+                    if !is_good {
+                        *entry = if needs_array {
+                            Value::Array(Vec::new())
+                        } else {
+                            Value::Object(Map::new())
+                        };
+                    }
+                    current = &mut arr[index];
+                } else {
+                    let replacement = if needs_array {
+                        let mut arr = Vec::with_capacity(index.saturating_add(1));
+                        arr.resize(index.saturating_add(1), Value::Null);
+                        Value::Array(arr)
+                    } else {
+                        let mut map = Map::new();
+                        map.insert(idx_to_string(*idx), Value::Null);
+                        Value::Object(map)
+                    };
+                    *current = replacement;
+                }
+            }
+        }
+    }
+    match &segments[last_idx] {
+        Segment::Key(k) => {
+            if !current.is_object() {
+                *current = Value::Object(Map::new());
+            }
+            current.as_object_mut().unwrap().insert(k.clone(), value);
+        }
+        Segment::Index(idx) => {
+            let index = validate_index(*idx)?;
+            if current.is_object() {
+                current.as_object_mut().unwrap().insert(idx_to_string(*idx), value);
+            } else if current.is_array() {
+                let arr = current.as_array_mut().unwrap();
+                if index >= arr.len() {
+                    arr.resize(index.saturating_add(1), Value::Null);
+                }
+                arr[index] = value;
+            } else {
+                let mut arr = Vec::with_capacity(index.saturating_add(1));
+                arr.resize(index.saturating_add(1), Value::Null);
+                arr[index] = value;
+                *current = Value::Array(arr);
Evidence
The setter validates indices up to MAX_ARRAY_INDEX, then grows arrays with resize(index+1),
which creates a dense vector of serde_json::Value entries. The recursive implementation applies
set_object_value_by_property_path to each selected project, multiplying the cost.

pacquet/crates/cli/src/cli_args/pkg.rs[164-187]
pacquet/crates/cli/src/cli_args/pkg.rs[362-373]
pacquet/crates/cli/src/cli_args/pkg.rs[434-439]
pacquet/crates/cli/src/cli_args/pkg.rs[476-485]

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

## Issue description
Setting `foo[big]=...` forces dense allocation via `Vec::resize`, which is expensive and can OOM at the current cap (1<<20). This is especially problematic under `--recursive` where the same input is applied to many manifests.
## Issue Context
`serde_json::Value::Array` is dense; unlike JS sparse arrays, resizing to a large index allocates and initializes every intermediate element.
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/pkg.rs[362-373]
- pacquet/crates/cli/src/cli_args/pkg.rs[434-439]
- pacquet/crates/cli/src/cli_args/pkg.rs[476-485]
## Suggested change
- Tighten the bound and/or add an additional growth guard:
- Example: reject if `index > MAX_ARRAY_INDEX` **or** if `index.saturating_sub(arr.len()) > MAX_ARRAY_GROWTH_DELTA`.
- Consider a smaller `MAX_ARRAY_INDEX` for a CLI editor, or make it configurable.
- Ensure the error clearly indicates the offending path/index (so users can fix scripts that accidentally pass huge indices).

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


7. Symlinked manifest disclosure 🐞 Bug ⛨ Security
Description
pkg get prints data read from PackageManifest::from_path, and the manifest reader uses
fs::read_to_string, which follows symlinks. In automation/CI that runs pacquet pkg get on
untrusted checkouts, a malicious repo can symlink package.json to a local JSON file and have its
contents printed to logs (local file read/exfiltration).
Code

pacquet/crates/cli/src/cli_args/pkg.rs[R105-112]

+    pub fn run(self, manifest_path: &Path) -> miette::Result<()> {
+        match self.command {
+            PkgSubcommand::Get(args) => {
+                let output = pkg_get(manifest_path, &args.keys, self.json)?;
+                if !output.is_empty() {
+                    println!("{output}");
+                }
+            }
Evidence
The new command prints the manifest output, and the existing manifest loader reads via
fs::read_to_string, which follows symlinks by default, enabling disclosure if package.json is a
symlink to another JSON file.

pacquet/crates/cli/src/cli_args/pkg.rs[105-112]
pacquet/crates/cli/src/cli_args/pkg.rs[218-223]
pacquet/crates/package-manifest/src/lib.rs[122-127]

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

## Issue description
`pkg get` can disclose arbitrary local JSON files when `package.json` is a symlink, because the manifest reader follows symlinks and the command prints the content.
## Issue Context
This is most concerning in CI/tooling that processes untrusted repositories and logs command output.
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/pkg.rs[105-112]
- pacquet/crates/cli/src/cli_args/pkg.rs[218-223]
- pacquet/crates/package-manifest/src/lib.rs[122-127]
## Suggested change
- Add a symlink check for `package.json` before reading:
- For minimal surface-area change: in `pkg_get` (and the recursive-get loop), call `std::fs::symlink_metadata(manifest_path)` and error if `file_type().is_symlink()`.
- For a broader hardening: add an opt-in "no symlinks" read path in `PackageManifest` and use it from commands that print manifest contents.
- Ensure the error message clearly states symlinks are not allowed for this operation.

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


8. Redundant path parsing 🐞 Bug ➹ Performance
Description
pkg set/pkg delete parse each property path multiple times (unsafe-key check plus the actual
set/delete), adding avoidable CPU overhead when many keys are processed in one invocation. This is
especially wasteful in pkg delete, which can parse each key three times (pre-check loop + delete +
delete's own unsafe check).
Code

pacquet/crates/cli/src/cli_args/pkg.rs[R284-286]

+    check_unsafe_key_in_path(path)?;
+    let segments = parse_property_path(path)
+        .map_err(|err| miette::Report::new(PkgError::InvalidPropertyPath(err)))?;
Evidence
check_unsafe_key_in_path calls parse_property_path, but both set_object_value_by_property_path
and delete_object_value_by_property_path call parse_property_path again; pkg_delete also
pre-validates each key before calling delete, compounding the overhead.

pacquet/crates/cli/src/cli_args/pkg.rs[192-206]
pacquet/crates/cli/src/cli_args/pkg.rs[247-257]
pacquet/crates/cli/src/cli_args/pkg.rs[281-287]
pacquet/crates/cli/src/cli_args/pkg.rs[389-396]

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

## Issue description
`check_unsafe_key_in_path()` parses the property path, and then `set_object_value_by_property_path()` / `delete_object_value_by_property_path()` parse the same string again. For `pkg delete`, there is an additional pre-check loop in `pkg_delete`, so the same key can be parsed multiple times.
### Issue Context
This is a performance/maintainability issue on the hot path for `pkg set`/`pkg delete` with many keys.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/pkg.rs[192-206]
- pacquet/crates/cli/src/cli_args/pkg.rs[247-257]
- pacquet/crates/cli/src/cli_args/pkg.rs[276-287]
- pacquet/crates/cli/src/cli_args/pkg.rs[389-396]
### Suggested approach
- Change `check_unsafe_key_in_path` to take already-parsed `&[Segment]` (or return parsed segments after validation).
- In `set_object_value_by_property_path` and `delete_object_value_by_property_path`, parse once, validate unsafe keys by iterating the segments, then proceed.
- In `pkg_delete`, remove the pre-check loop once delete validates segments itself (or keep the pre-check but make delete accept pre-parsed segments).

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


View more (3)
9. Index semantics differ from reads ✓ Resolved 🐞 Bug ≡ Correctness
Description
set_object_value_by_property_path requires Segment::Index to always traverse an array, but the
shared read semantics treat numeric segments on objects as string keys (JS key coercion). This makes
pkg set inconsistent with pkg get/config-property-path behavior and can fail on manifests
containing object maps keyed by numeric strings accessed via bracket notation.
Code

pacquet/crates/cli/src/cli_args/pkg.rs[R264-277]

+    for segment in &segments[..last_idx] {
+        match segment {
+            Segment::Key(k) => {
+                let obj = current.as_object_mut().ok_or_else(|| set_path_err(path))?;
+                current = obj.entry(k.clone()).or_insert_with(|| Value::Object(Map::new()));
+            }
+            Segment::Index(idx) => {
+                let index = *idx as usize;
+                let arr = current.as_array_mut().ok_or_else(|| set_path_err(path))?;
+                if index >= arr.len() {
+                    arr.resize(index + 1, Value::Null);
+                }
+                current = &mut arr[index];
+            }
Evidence
The newly added setter treats Segment::Index as array-only (errors on objects). The shared getter,
used elsewhere and imported by this module, explicitly converts Segment::Index into a string key
when indexing objects, so the same property-path syntax has different meaning between read and write
paths.

pacquet/crates/cli/src/cli_args/pkg.rs[264-279]
pacquet/crates/config/src/property_path.rs[296-303]

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

## Issue description
`set_object_value_by_property_path` interprets every `Segment::Index` as an array index and errors if the current node is not an array. However, `get_object_value_by_property_path` treats `Segment::Index(n)` as a string key when the current node is an object (mirroring JS object key coercion). This mismatch makes set/get behavior inconsistent for the same path syntax.
### Issue Context
The property-path implementation used across the repo has explicit rules:
- On `Value::Object`, `Segment::Index(n)` is converted into a key string and used as an object key.
- On `Value::Array`, only numeric indices that pass validation are allowed.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/pkg.rs[251-295]
### Suggested fix
- When encountering `Segment::Index(n)` and `current` is an object, convert `n` to the same key string representation used by the read path (ideally by refactoring/exposing a shared helper in `pacquet_config::property_path`).
- Only require `as_array_mut()` when `current` is already an array; otherwise treat the segment as an object key when `current` is an object.
- Add tests demonstrating `pkg set` can write through numeric-keyed object paths consistently with the getter.

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


10. Multi-get outputs nulls ✓ Resolved 🐞 Bug ≡ Correctness
Description
For pkg get with multiple keys, select_from_manifest swallows property-path parse errors and
emits null for missing/invalid keys, whereas pnpm throws on invalid property paths and omits
missing keys (because undefined values disappear under JSON.stringify). This changes observable
output and can mask user typos.
Code

pacquet/crates/cli/src/cli_args/pkg.rs[R144-156]

+fn select_from_manifest(manifest: &Value, keys: &[String]) -> Value {
+    if keys.is_empty() {
+        return manifest.clone();
+    }
+    let mut result = Map::new();
+    for key in keys {
+        let value = parse_property_path(key)
+            .ok()
+            .and_then(|segments| get_object_value_by_property_path(manifest, &segments))
+            .map_or(Value::Null, std::borrow::ToOwned::to_owned);
+        result.insert(key.clone(), value);
+    }
+    Value::Object(result)
Evidence
Rust’s multi-key path ignores parse failures (.ok()) and forces null, while pnpm’s
implementation calls the parsing getter directly per key (so invalid syntax errors) and relies on
JSON.stringify behavior where undefined values are omitted.

pacquet/crates/cli/src/cli_args/pkg.rs[144-157]
pnpm11/pkg-manifest/commands/src/pkg.ts[100-120]

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

## Issue description
`select_from_manifest()` currently does `parse_property_path(key).ok()` and inserts `Value::Null` when parsing fails or the key is absent. This makes invalid paths look like `null` and includes keys that pnpm would omit.
## Issue Context
pnpm’s `selectFromManifest` sets `result[key] = getObjectValueByPropertyPathString(...)`; invalid paths throw during parsing, and missing values become `undefined` and are omitted from JSON.
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/pkg.rs[144-157]

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


11. Delete ignores unsafe paths ✓ Resolved 🐞 Bug ≡ Correctness
Description
delete_object_value_by_property_path silently returns false on parse failure and does not reject
unsafe keys (__proto__, constructor, prototype), while pnpm rejects unsafe keys and will throw
on invalid property-path syntax. pkg_delete ignores the boolean return, so bad inputs can appear
to succeed while doing nothing.
Code

pacquet/crates/cli/src/cli_args/pkg.rs[R297-317]

+fn delete_object_value_by_property_path(root: &mut Value, path: &str) -> bool {
+    let Ok(segments) = parse_property_path(path) else { return false };
+    if segments.is_empty() {
+        return false;
+    }
+    let last_idx = segments.len() - 1;
+    let mut current = root;
+    for segment in &segments[..last_idx] {
+        match segment {
+            Segment::Key(k) => {
+                let Some(obj) = current.as_object_mut() else { return false };
+                let Some(next) = obj.get_mut(k) else { return false };
+                current = next;
+            }
+            Segment::Index(idx) => {
+                let index = *idx as usize;
+                let Some(arr) = current.as_array_mut() else { return false };
+                let Some(next) = arr.get_mut(index) else { return false };
+                current = next;
+            }
+        }
Evidence
The Rust delete path explicitly suppresses parse errors and has no unsafe-key check, and the command
loop doesn’t act on the bool result. pnpm’s delete path rejects unsafe keys up front and will
throw on invalid property paths because parsing happens inside
deleteObjectValueByPropertyPathString.

pacquet/crates/cli/src/cli_args/pkg.rs[183-195]
pacquet/crates/cli/src/cli_args/pkg.rs[297-335]
pnpm11/object/property-path/src/delete.ts[6-46]

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 delete implementation swallows property-path parse errors (`let Ok(segments) = ... else { return false }`) and does not apply unsafe-key rejection. The caller also ignores the returned `bool`, so invalid deletes are silent.
## Issue Context
pnpm’s `deleteObjectValueByPropertyPathString` parses the path (throwing on invalid syntax) and calls `rejectUnsafeKeys(path)` before deleting.
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args/pkg.rs[183-195]
- pacquet/crates/cli/src/cli_args/pkg.rs[297-335]

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



Informational

12. Extra quote in error 🐞 Bug ⚙ Maintainability
Description
PkgError::SetJsonParse formats with an extra trailing quote, producing malformed user-facing error
output for invalid --json values.
Code

pacquet/crates/cli/src/cli_args/pkg.rs[R24-26]

+    #[display(r#"Failed to parse value as JSON: "{value}""#)]
+    #[diagnostic(code(ERR_PNPM_PKG_SET_JSON_PARSE))]
+    SetJsonParse { value: String },
Evidence
The raw string literal includes "{value}"" (two quotes at the end), which will render an unmatched
trailing quote in the formatted message.

pacquet/crates/cli/src/cli_args/pkg.rs[24-26]

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 `SetJsonParse` error message contains an extra trailing quote, leading to malformed error output.
### Issue Context
This affects user-facing diagnostics when `pnpm pkg set --json key=<value>` fails to parse the value.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/pkg.rs[24-26]
### Suggested fix
Change the display string from:
- `Failed to parse value as JSON: "{value}""`
To:
- `Failed to parse value as JSON: "{value}"`
(or similar), ensuring the rendered string has balanced quotes.

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


13. SetPathError omits path 🐞 Bug ◔ Observability
Description
PkgError::SetPathError stores a path field but the display string does not include it, reducing
diagnosability for invalid index/path failures.
Code

pacquet/crates/cli/src/cli_args/pkg.rs[R40-43]

+    #[display("Cannot set property on a non-object or non-array value at path")]
+    #[diagnostic(code(ERR_PNPM_PKG_SET_PATH_ERROR))]
+    SetPathError { path: String },
+
Evidence
The display annotation is a fixed string and does not reference {path}, so the stored context is
not shown to the user.

pacquet/crates/cli/src/cli_args/pkg.rs[40-43]
pacquet/crates/cli/src/cli_args/pkg.rs[261-269]

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

## Issue description
`SetPathError { path: String }` does not interpolate `path` into its display message, so users don’t see what segment/index caused the failure.
### Issue Context
`validate_index` constructs `SetPathError { path: idx.to_string() }` on invalid indices; without including `{path}` in the display, the error is much less actionable.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/pkg.rs[40-43]
- pacquet/crates/cli/src/cli_args/pkg.rs[261-269]
### Suggested fix
Update the display to include the captured field, e.g.:
- `Cannot set property on a non-object or non-array value at path: {path}`
(or rename the variant / field if the message is intended to cover more than index validation).

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


14. Empty path returns whole manifest ✓ Resolved 🐞 Bug ☼ Reliability
Description
An empty property path parses to zero segments, so pkg get '' returns the entire manifest (because
the getter returns the root value when given an empty segment list). This is surprising behavior for
a “single key” query and can mask bugs in scripts that accidentally pass empty keys.
Code

pacquet/crates/cli/src/cli_args/pkg.rs[R119-137]

+fn get_output(manifest: &Value, keys: &[String], json: bool) -> miette::Result<String> {
+    if keys.len() == 1 {
+        let key = &keys[0];
+        let segments = parse_property_path(key).map_err(PkgError::InvalidPropertyPath)?;
+        match get_object_value_by_property_path(manifest, &segments) {
+            None => Ok(String::new()),
+            Some(found) => {
+                if json {
+                    serde_json::to_string_pretty(found).map_err(|e| miette::miette!("{e}"))
+                } else {
+                    match found {
+                        Value::String(s) => Ok(s.clone()),
+                        other => {
+                            serde_json::to_string_pretty(other).map_err(|e| miette::miette!("{e}"))
+                        }
+                    }
+                }
+            }
+        }
Evidence
The property-path parser explicitly treats the empty string as a valid path producing zero segments.
The getter returns the current value after iterating segments; with zero segments it returns the
root manifest, so pkg get with a single empty key prints the full package.json.

pacquet/crates/cli/src/cli_args/pkg.rs[120-137]
pacquet/crates/config/src/property_path/tests.rs[10-28]
pacquet/crates/config/src/property_path.rs[286-317]

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

## Issue description
`pkg get` treats a single empty key as a valid property path and returns the entire manifest because parsing yields an empty segment list and the getter returns the root value for empty paths. This is an edge case but can cause unexpected output and hide script bugs.
### Issue Context
`parse_property_path("")` returns `Ok(vec![])`, and `get_object_value_by_property_path` returns `Some(current)` when `property_path` is empty.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/pkg.rs[119-137]
### Suggested fix
- In `get_output`, if `keys.len() == 1 && keys[0].is_empty()`, return an explicit `PkgError` (new error code) instead of treating it as "get root".
- Optionally apply the same empty-path rejection to `set`/`delete` for consistency, unless pnpm explicitly defines empty-path semantics for `pkg` (then add tests documenting it).

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


Grey Divider

Qodo Logo

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

codecov-commenter commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 47.23757% with 191 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.85%. Comparing base (11a7fdd) to head (24f7d71).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/cli/src/cli_args/pkg.rs 48.16% 184 Missing ⚠️
pacquet/crates/cli/src/cli_args/dispatch_script.rs 0.00% 6 Missing ⚠️
pacquet/crates/cli/src/cli_args/dispatch.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12795      +/-   ##
==========================================
- Coverage   85.02%   84.85%   -0.17%     
==========================================
  Files         415      416       +1     
  Lines       65438    65895     +457     
==========================================
+ Hits        55638    55916     +278     
- Misses       9800     9979     +179     

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

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 50b900f

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
pacquet/crates/cli/src/cli_args/pkg/tests.rs (1)

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

Duplicate multi-key get tests.

test_get_multiple_keys and test_get_multiple_keys_returns_json_object exercise the same manifest and the same behavior (selecting name/version) with only a trivial construction difference. Consider consolidating into one test.

🤖 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/pkg/tests.rs` around lines 61 - 97, There are
duplicate coverage cases in the pkg CLI tests: test_get_multiple_keys and
test_get_multiple_keys_returns_json_object both verify the same multi-key get
behavior on the same manifest. Consolidate them into a single test in the pkg
tests module, keeping one assertion path that exercises get_output with
name/version and validates the JSON object result.
pacquet/crates/cli/src/cli_args/pkg.rs (1)

235-262: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Property path parsed twice per set call.

check_unsafe_key_in_path parses path internally, then set_object_value_by_property_path immediately re-parses the same path a few lines later. Pass the already-parsed segments into the unsafe-key check instead of re-deriving them.

♻️ Proposed fix: parse once, validate on the parsed segments
-fn check_unsafe_key_in_path(key: &str) -> Result<(), PkgError> {
-    let segments = parse_property_path(key).map_err(PkgError::InvalidPropertyPath)?;
-    for segment in &segments {
+fn check_unsafe_key_in_segments(segments: &[Segment]) -> Result<(), PkgError> {
+    for segment in segments {
         if let Segment::Key(k) = segment
             && UNSAFE_KEYS.contains(&k.as_str())
         {
             return Err(PkgError::UnsafeKey { key: k.clone() });
         }
     }
     Ok(())
 }
@@
-    check_unsafe_key_in_path(path)?;
-    let segments = parse_property_path(path)
-        .map_err(|err| miette::Report::new(PkgError::InvalidPropertyPath(err)))?;
+    let segments = parse_property_path(path)
+        .map_err(|err| miette::Report::new(PkgError::InvalidPropertyPath(err)))?;
+    check_unsafe_key_in_segments(&segments)?;

As per path instructions (REVIEW_GUIDE.md): "Performance is secondary; avoid extra parsing/serialization beyond what's needed per command."

🤖 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/pkg.rs` around lines 235 - 262, The set path
flow in set_object_value_by_property_path is parsing the same property path
twice, once in check_unsafe_key_in_path and again immediately afterward. Change
the validation helper to accept the already-parsed segments (or move the
unsafe-key check into set_object_value_by_property_path after
parse_property_path) so the path is parsed only once per set call. Keep the
existing unsafe-key behavior by reusing the parsed Segment values instead of
re-deriving them.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pacquet/crates/cli/src/cli_args/pkg.rs`:
- Around line 264-295: The path-setting logic in the segment-walk loop always
creates missing intermediate keys as objects, so `Segment::Key` must look ahead
to the next segment and choose an object or array container accordingly; update
the container creation in the `current` traversal so documented paths like
`keywords[0]` can initialize `keywords` as an array instead of failing later in
`Segment::Index`. Also harden both `arr.resize(index + 1, Value::Null)` branches
by validating/capping `idx` before converting to `usize` and before resizing, so
a malformed `Segment::Index` cannot force an unbounded allocation or overflow.
- Around line 183-195: The pkg_delete flow currently allows unsafe property
paths that the rest of the pkg contract rejects. Update pkg_delete in the pkg.rs
CLI logic to validate each key before calling
delete_object_value_by_property_path, rejecting __proto__, constructor, and
prototype the same way pkg set and pnpm’s TS pkg delete do. Keep the validation
close to pkg_delete so malformed paths fail before PackageManifest::from_path,
manifest.value_mut, or manifest.save are used.

In `@pacquet/crates/cli/src/cli_args/pkg/tests.rs`:
- Around line 197-251: Add a regression test in test_set_* coverage for unsafe
path rejection in set_object_value_by_property_path, since it already calls
check_unsafe_key_in_path but has no assertion for blocked prototype-polluting
keys. Extend the tests around set_object_value_by_property_path to verify paths
like __proto__.polluted or constructor.prototype are rejected with an error and
do not mutate the JSON value. Use the existing test helpers and function name to
locate the behavior being validated.

---

Nitpick comments:
In `@pacquet/crates/cli/src/cli_args/pkg.rs`:
- Around line 235-262: The set path flow in set_object_value_by_property_path is
parsing the same property path twice, once in check_unsafe_key_in_path and again
immediately afterward. Change the validation helper to accept the already-parsed
segments (or move the unsafe-key check into set_object_value_by_property_path
after parse_property_path) so the path is parsed only once per set call. Keep
the existing unsafe-key behavior by reusing the parsed Segment values instead of
re-deriving them.

In `@pacquet/crates/cli/src/cli_args/pkg/tests.rs`:
- Around line 61-97: There are duplicate coverage cases in the pkg CLI tests:
test_get_multiple_keys and test_get_multiple_keys_returns_json_object both
verify the same multi-key get behavior on the same manifest. Consolidate them
into a single test in the pkg tests module, keeping one assertion path that
exercises get_output with name/version and validates the JSON object result.
🪄 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: b17bfb93-f9b9-4d74-917d-e47f22056852

📥 Commits

Reviewing files that changed from the base of the PR and between 07c9787 and 50b900f.

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

Comment thread pacquet/crates/cli/src/cli_args/pkg.rs
Comment thread pacquet/crates/cli/src/cli_args/pkg.rs Outdated
Comment thread pacquet/crates/cli/src/cli_args/pkg/tests.rs
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Commit: 24f7d7144095

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.782 ± 0.147 3.628 4.114 1.77 ± 0.13
pacquet@main 3.835 ± 0.127 3.639 3.993 1.79 ± 0.13
pnpr@HEAD 2.151 ± 0.162 1.977 2.389 1.01 ± 0.10
pnpr@main 2.139 ± 0.133 1.997 2.397 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.7822814852199995,
      "stddev": 0.1465037300023765,
      "median": 3.73859849402,
      "user": 3.61716898,
      "system": 3.3692105399999996,
      "min": 3.6283688205200004,
      "max": 4.11394171152,
      "times": [
        3.8789691815200005,
        3.7896692185200003,
        3.8962060175200004,
        3.71051575152,
        4.11394171152,
        3.6599044865200003,
        3.6680426765200003,
        3.74592039952,
        3.6283688205200004,
        3.73127658852
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.83467366922,
      "stddev": 0.12698112196217864,
      "median": 3.82583213452,
      "user": 3.5941966799999996,
      "system": 3.37493854,
      "min": 3.6391551245200002,
      "max": 3.99292523652,
      "times": [
        3.99292523652,
        3.6391551245200002,
        3.7628192455200002,
        3.95438312552,
        3.85547116252,
        3.9674158805200004,
        3.6721049815200004,
        3.79619310652,
        3.9417612035200005,
        3.7645076255200003
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.1510186833200002,
      "stddev": 0.1617546995725202,
      "median": 2.0837725555200004,
      "user": 2.65824178,
      "system": 2.9610634400000007,
      "min": 1.97681448052,
      "max": 2.38885254452,
      "times": [
        2.32639567052,
        2.38885254452,
        2.0404234865200004,
        2.1062530185200004,
        1.97681448052,
        2.03881053552,
        2.20702050552,
        1.9848563085200002,
        2.3794681905200004,
        2.06129209252
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.1391052853200003,
      "stddev": 0.1327565687008611,
      "median": 2.12565060952,
      "user": 2.6471240799999998,
      "system": 2.9606953399999996,
      "min": 1.9968216575200002,
      "max": 2.3968276525200003,
      "times": [
        2.05872698852,
        2.3968276525200003,
        2.25308706852,
        2.2114306315200003,
        2.03109800152,
        2.03957625652,
        2.0040287355200004,
        1.9968216575200002,
        2.19257423052,
        2.2068816305200003
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 632.7 ± 17.4 615.2 671.1 1.00
pacquet@main 633.5 ± 16.9 612.4 659.5 1.00 ± 0.04
pnpr@HEAD 707.9 ± 59.2 659.0 870.6 1.12 ± 0.10
pnpr@main 705.0 ± 46.8 667.5 833.0 1.11 ± 0.08
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6326642331600001,
      "stddev": 0.017424694617822766,
      "median": 0.6288632548599999,
      "user": 0.37429094,
      "system": 1.3070935599999998,
      "min": 0.61519017786,
      "max": 0.67111311986,
      "times": [
        0.67111311986,
        0.61810539086,
        0.64621837086,
        0.61519017786,
        0.6272696558599999,
        0.63090293086,
        0.61866500186,
        0.62182173286,
        0.6304568538599999,
        0.64689909686
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6335322267600001,
      "stddev": 0.016926000677317223,
      "median": 0.62725412986,
      "user": 0.36612954,
      "system": 1.32496706,
      "min": 0.61236112186,
      "max": 0.65951354286,
      "times": [
        0.6281737898599999,
        0.65143540986,
        0.65951354286,
        0.64819055386,
        0.61236112186,
        0.61400021786,
        0.62051799486,
        0.6487663648599999,
        0.62602880186,
        0.62633446986
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.7078798737600001,
      "stddev": 0.059177850478771506,
      "median": 0.69652407736,
      "user": 0.38917594,
      "system": 1.3479778599999999,
      "min": 0.65900208386,
      "max": 0.87059778386,
      "times": [
        0.69770829386,
        0.70178732486,
        0.65900208386,
        0.70973450086,
        0.69283878786,
        0.69533986086,
        0.69987278786,
        0.67930960086,
        0.67260771286,
        0.87059778386
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.70504978086,
      "stddev": 0.04677482978680625,
      "median": 0.69509456986,
      "user": 0.38641593999999996,
      "system": 1.3452670599999998,
      "min": 0.66753435986,
      "max": 0.83295132586,
      "times": [
        0.71063682186,
        0.69857645486,
        0.69121933286,
        0.70013101186,
        0.69333887886,
        0.66753435986,
        0.68797879486,
        0.69685026086,
        0.83295132586,
        0.67128056686
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.226 ± 0.052 4.164 4.360 1.97 ± 0.13
pacquet@main 4.200 ± 0.044 4.149 4.272 1.96 ± 0.12
pnpr@HEAD 2.148 ± 0.135 2.025 2.378 1.00
pnpr@main 2.208 ± 0.151 2.005 2.465 1.03 ± 0.10
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.226236957959999,
      "stddev": 0.052328648082538946,
      "median": 4.218877710360001,
      "user": 3.7108414599999997,
      "system": 3.3525622399999997,
      "min": 4.163743168860001,
      "max": 4.35978788286,
      "times": [
        4.35978788286,
        4.196814397860001,
        4.163743168860001,
        4.2196437728600005,
        4.2183541598600005,
        4.219401260860001,
        4.210946513860001,
        4.22054136786,
        4.1981187138600005,
        4.25501834086
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.200449624759999,
      "stddev": 0.043531247136894664,
      "median": 4.19126164786,
      "user": 3.7174738599999997,
      "system": 3.3462843399999995,
      "min": 4.149298685860001,
      "max": 4.271531293860001,
      "times": [
        4.271531293860001,
        4.25474260286,
        4.22331555586,
        4.15391340886,
        4.165614569860001,
        4.149298685860001,
        4.18629872286,
        4.23579601486,
        4.19622457286,
        4.167760819860001
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.14806017886,
      "stddev": 0.13479926327956876,
      "median": 2.07437102386,
      "user": 2.4929604599999995,
      "system": 2.8863801400000004,
      "min": 2.02491004786,
      "max": 2.37818828486,
      "times": [
        2.37818828486,
        2.05386565386,
        2.07615642186,
        2.03130367686,
        2.27511163186,
        2.02491004786,
        2.15393281686,
        2.34665163886,
        2.06789598986,
        2.07258562586
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.2080459401599994,
      "stddev": 0.15106285770322717,
      "median": 2.19542316036,
      "user": 2.4911481599999994,
      "system": 2.88722274,
      "min": 2.00485598886,
      "max": 2.46457664086,
      "times": [
        2.46457664086,
        2.04544341886,
        2.28328167986,
        2.36969669286,
        2.00485598886,
        2.32372371286,
        2.06855704986,
        2.12947789686,
        2.17638819786,
        2.21445812286
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.328 ± 0.013 1.308 1.353 2.04 ± 0.05
pacquet@main 1.375 ± 0.094 1.325 1.636 2.11 ± 0.15
pnpr@HEAD 0.683 ± 0.092 0.637 0.940 1.05 ± 0.14
pnpr@main 0.652 ± 0.016 0.640 0.697 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.3277827386399998,
      "stddev": 0.012846721020045873,
      "median": 1.3284332378400001,
      "user": 1.29864444,
      "system": 1.6741275599999998,
      "min": 1.30775116284,
      "max": 1.35296053384,
      "times": [
        1.32982401484,
        1.32808098184,
        1.35296053384,
        1.32878549384,
        1.30775116284,
        1.32899975084,
        1.34126562784,
        1.31619335284,
        1.31650392284,
        1.32746254484
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.3747937182399996,
      "stddev": 0.09360022243894765,
      "median": 1.33922946134,
      "user": 1.2966214399999998,
      "system": 1.7092517599999997,
      "min": 1.32521995284,
      "max": 1.63617743084,
      "times": [
        1.32993483284,
        1.63617743084,
        1.3646941718399999,
        1.33761227384,
        1.3557000638399999,
        1.33511635584,
        1.32521995284,
        1.38502317784,
        1.33901061684,
        1.33944830584
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6834283736400002,
      "stddev": 0.09173915770470631,
      "median": 0.65372793934,
      "user": 0.3328369400000001,
      "system": 1.30236746,
      "min": 0.63708253984,
      "max": 0.93999297584,
      "times": [
        0.64155840284,
        0.63745188984,
        0.63708253984,
        0.64219169784,
        0.93999297584,
        0.6819803008399999,
        0.66550562084,
        0.68106442984,
        0.66117069884,
        0.64628517984
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.65237269454,
      "stddev": 0.0163178186114579,
      "median": 0.64867194884,
      "user": 0.34458854000000005,
      "system": 1.2695949599999998,
      "min": 0.63990027984,
      "max": 0.69702958784,
      "times": [
        0.65124589684,
        0.65152255584,
        0.64807637984,
        0.64750147484,
        0.64489695784,
        0.64073077684,
        0.63990027984,
        0.69702958784,
        0.65355551784,
        0.64926751784
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.991 ± 0.038 2.964 3.087 4.63 ± 0.08
pacquet@main 3.003 ± 0.046 2.959 3.109 4.65 ± 0.09
pnpr@HEAD 0.657 ± 0.013 0.640 0.678 1.02 ± 0.02
pnpr@main 0.646 ± 0.008 0.634 0.655 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.9907900125199998,
      "stddev": 0.03829000651422907,
      "median": 2.97795824552,
      "user": 1.7541768599999998,
      "system": 1.93806812,
      "min": 2.96389283252,
      "max": 3.0869080205199997,
      "times": [
        2.98197440852,
        2.9781280845199998,
        2.96799320852,
        3.02442815752,
        2.9777884065199998,
        2.99352306052,
        2.96664233552,
        3.0869080205199997,
        2.96389283252,
        2.96662161052
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.0030864203199994,
      "stddev": 0.046053123169286676,
      "median": 2.99620000352,
      "user": 1.7453888599999998,
      "system": 1.95140442,
      "min": 2.9585215305199997,
      "max": 3.10879179952,
      "times": [
        3.00528080052,
        3.03909251252,
        2.9815383125199997,
        3.0233125835199997,
        2.9618685015199997,
        2.98883494052,
        2.9585215305199997,
        3.0035650665199998,
        3.10879179952,
        2.96005815552
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.65701131712,
      "stddev": 0.013240598837840187,
      "median": 0.65301090802,
      "user": 0.33562285999999997,
      "system": 1.2875543200000001,
      "min": 0.64013179952,
      "max": 0.67843321752,
      "times": [
        0.65641257052,
        0.65444451052,
        0.65157730552,
        0.66867067552,
        0.6487772605200001,
        0.64013179952,
        0.67687405052,
        0.67843321752,
        0.65007435552,
        0.64471742552
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6457571263199999,
      "stddev": 0.007871988511410839,
      "median": 0.6495935820200001,
      "user": 0.33609866,
      "system": 1.28012502,
      "min": 0.63395620752,
      "max": 0.65507719952,
      "times": [
        0.65507719952,
        0.6536868865200001,
        0.64978487052,
        0.63724186352,
        0.6507289765200001,
        0.63568608252,
        0.63395620752,
        0.64940229352,
        0.65049905352,
        0.64150782952
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 647.5 ± 34.6 621.3 721.7 6.29 ± 0.34
pacquet@main 630.8 ± 7.9 620.9 647.4 6.13 ± 0.08
pnpr@HEAD 104.0 ± 1.8 102.7 107.8 1.01 ± 0.02
pnpr@main 102.9 ± 0.4 102.2 103.8 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.64754421134,
      "stddev": 0.03459582219913087,
      "median": 0.63654121044,
      "user": 0.60754028,
      "system": 0.15320823999999997,
      "min": 0.62133498144,
      "max": 0.72167481544,
      "times": [
        0.72167481544,
        0.69848001744,
        0.63497844744,
        0.62209906244,
        0.62133498144,
        0.62327374144,
        0.63810397344,
        0.62596359944,
        0.64866752744,
        0.64086594744
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6308395543399999,
      "stddev": 0.007938993698063751,
      "median": 0.6292289494400001,
      "user": 0.60432448,
      "system": 0.14627593999999997,
      "min": 0.6208765754400001,
      "max": 0.6474362824400001,
      "times": [
        0.6474362824400001,
        0.6351504114400001,
        0.6248671114400001,
        0.6208765754400001,
        0.63525639944,
        0.62638972944,
        0.63206816944,
        0.6363001394400001,
        0.62515969644,
        0.62489102844
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.10400979355538462,
      "stddev": 0.0018455080076893147,
      "median": 0.10303675244,
      "user": 0.026164664615384605,
      "system": 0.018517001538461534,
      "min": 0.10268257644,
      "max": 0.10778921744,
      "times": [
        0.10620845744,
        0.10332345644,
        0.10729560344,
        0.10298878644000001,
        0.10300469644,
        0.10287146844,
        0.10311314944000001,
        0.10316735044,
        0.10268257644,
        0.10303905344,
        0.10303445144000001,
        0.10279179044,
        0.10269783444000001,
        0.10268572144,
        0.10302491844,
        0.10299729644000001,
        0.10278863644000001,
        0.10289686644,
        0.10361819944,
        0.10778921744,
        0.10760859244000001,
        0.10280060144,
        0.10344051344,
        0.10714681444,
        0.10379600044000001,
        0.10744257944
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.10289890686307693,
      "stddev": 0.00040127155251048374,
      "median": 0.10281092894,
      "user": 0.02525643384615385,
      "system": 0.01881427076923077,
      "min": 0.10223388844,
      "max": 0.10376214444000001,
      "times": [
        0.10280246344,
        0.10257540544,
        0.10335683144,
        0.10232265644,
        0.10327834244,
        0.10353436344,
        0.10257881244,
        0.10299778744,
        0.10238720844,
        0.10268656744,
        0.10306184544000001,
        0.10295813344,
        0.10263456044000001,
        0.10358093044000001,
        0.10281939444,
        0.10271848544,
        0.10318032244,
        0.10223388844,
        0.10334669044,
        0.10269612444000001,
        0.10376214444000001,
        0.10239658244000001,
        0.10301839444000001,
        0.10285648144000001,
        0.10279191044000001,
        0.10279525144
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 6.590 ± 0.059 6.487 6.692 1.33 ± 0.04
pacquet@main 6.675 ± 0.123 6.542 6.837 1.35 ± 0.05
pnpr@HEAD 4.966 ± 0.121 4.847 5.277 1.00 ± 0.04
pnpr@main 4.957 ± 0.157 4.809 5.266 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 6.58964831414,
      "stddev": 0.05855499873471422,
      "median": 6.5991376847400005,
      "user": 3.7816249400000004,
      "system": 3.6489552599999997,
      "min": 6.486919798240001,
      "max": 6.69188391824,
      "times": [
        6.62979877624,
        6.54053035424,
        6.69188391824,
        6.62797118724,
        6.54429163324,
        6.61836432624,
        6.586405042240001,
        6.558447778240001,
        6.61187032724,
        6.486919798240001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 6.67473878034,
      "stddev": 0.12285833438362853,
      "median": 6.653653737740001,
      "user": 3.8004678399999996,
      "system": 3.66689856,
      "min": 6.541597788240001,
      "max": 6.836735641240001,
      "times": [
        6.832616248240001,
        6.686250059240001,
        6.723347671240001,
        6.543075255240001,
        6.56374269624,
        6.62105741624,
        6.575595893240001,
        6.82336913424,
        6.836735641240001,
        6.541597788240001
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 4.96569435334,
      "stddev": 0.12149370176196984,
      "median": 4.94938025574,
      "user": 2.7346137400000003,
      "system": 3.20623236,
      "min": 4.8468692972400005,
      "max": 5.277461429240001,
      "times": [
        4.955007737240001,
        4.949984365240001,
        4.8720097112400005,
        4.87901123724,
        5.277461429240001,
        4.94877614624,
        4.993908534240001,
        4.8468692972400005,
        5.0104956432400005,
        4.92341943224
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 4.957016987840001,
      "stddev": 0.15717088015578462,
      "median": 4.900393241740001,
      "user": 2.75709154,
      "system": 3.1891815599999993,
      "min": 4.808712063240001,
      "max": 5.26598802324,
      "times": [
        5.26598802324,
        4.81797559924,
        5.070101575240001,
        5.1599495632400005,
        4.870838758240001,
        4.9513580602400005,
        4.892588491240001,
        4.808712063240001,
        4.908197992240001,
        4.82445975224
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12795
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,226.24 ms
(-0.09%)Baseline: 4,229.85 ms
5,075.82 ms
(83.26%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
2,990.79 ms
(-1.03%)Baseline: 3,021.99 ms
3,626.39 ms
(82.47%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,327.78 ms
(-0.85%)Baseline: 1,339.23 ms
1,607.07 ms
(82.62%)
isolated-linker.fresh-resolve.hot-cache.offline📈 view plot
🚷 view threshold
647.54 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
3,782.28 ms
(-6.51%)Baseline: 4,045.44 ms
4,854.53 ms
(77.91%)
isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr📈 view plot
🚷 view threshold
6,589.65 ms
(-5.17%)Baseline: 6,949.12 ms
8,338.94 ms
(79.02%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
632.66 ms
(+0.48%)Baseline: 629.62 ms
755.55 ms
(83.74%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12795
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,148.06 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
657.01 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
683.43 ms
isolated-linker.fresh-resolve.hot-cache.offline📈 view plot
⚠️ NO THRESHOLD
104.01 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,151.02 ms
isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr📈 view plot
⚠️ NO THRESHOLD
4,965.69 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
707.88 ms
🐰 View full continuous benchmarking report in Bencher

- Validate f64 indices against MAX_ARRAY_INDEX cap
- Implement look-ahead container creation to match TS behavior
- Correct Object vs Array handling for Index segments
- Reject empty property paths in set/get/delete
- Block unsafe keys (__proto__, constructor.prototype) in delete path
- Return Result<Option<Value>> from get_output, omit missing keys
- Use is_some_and instead of map_or(false, ...) per clippy
- Add trailing commas for dylint

Closes pnpm#12795
Comment on lines +346 to +357
} else {
let replacement = if needs_array {
let mut arr = Vec::with_capacity(index.saturating_add(1));
arr.resize(index.saturating_add(1), Value::Null);
Value::Array(arr)
} else {
let mut map = Map::new();
map.insert(idx_to_string(*idx), Value::Null);
Value::Object(map)
};
*current = replacement;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Index traversal loses position 🐞 Bug ≡ Correctness

In set_object_value_by_property_path, when an intermediate Segment::Index is applied while
current is neither an object nor array, the code replaces current with a container but does not
descend into the indexed slot. Subsequent segments then apply at the wrong level, producing an
incorrect JSON structure for paths that start with (or encounter) an index while the current value
is scalar.
Agent Prompt
### Issue description
In `set_object_value_by_property_path`, the `Segment::Index` traversal has a branch where `current` is neither object nor array. That branch sets `*current = replacement` but never updates `current` to the newly created `arr[index]` (array case) or `obj[key]` (object case). Any remaining segments are then applied to the wrong node.

### Issue Context
This breaks nested sets for some valid property paths when the JSON root (or an intermediate value reached via index traversal) is scalar and must be replaced to continue.

### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/pkg.rs[292-360]

### Suggested approach
After `*current = replacement;`:
- If `needs_array` is true: ensure the array has length `index + 1` and set `current = &mut arr[index]`.
- Else: ensure the object contains `key` and set `current = obj.get_mut(&key).unwrap()`.

Add a unit test that demonstrates the bug, e.g. starting with a scalar root and running a path like `[0].a` or similar, and verifying the nested structure is created under key/index `0` rather than at the root.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed in 24f7d71

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 007bdfd

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

272-298: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add a regression test for set with an index segment on an existing object/array.

Current set tests only cover creating containers from scratch ({}) or replacing a scalar. None assert what happens when the target key already holds a valid container of the "other" type — exactly the data-loss case flagged in pkg.rs (Lines 295-345). A test like the following would pin the intended parity behavior and fail against the current overwrite logic:

#[test]
fn test_set_index_on_existing_object_preserves_object() {
    let mut value = json!({ "scripts": { "test": "echo test" } });
    set_object_value_by_property_path(&mut value, "scripts[0]", json!("x")).unwrap();
    // Expected (consistent with get/delete): index-as-string-key on the object,
    // existing entries retained — confirm against pnpm TS.
    assert_eq!(value["scripts"]["test"], "echo test");
}
🤖 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/pkg/tests.rs` around lines 272 - 298, Add a
regression test in the `set_object_value_by_property_path` test module for the
case where an index segment is applied to an existing container of the other
type, not just `{}` or scalars. Use a test alongside
`test_set_creates_array_from_scratch`, `test_set_creates_array_intermediate`,
and `test_set_replaces_scalar_with_object/array` to verify that setting a path
like `scripts[0]` on an existing object preserves existing object data instead
of overwriting it. This should lock in the intended `set` behavior for mixed
object/array paths and catch the data-loss case in `pkg.rs`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pacquet/crates/cli/src/cli_args/pkg/tests.rs`:
- Around line 272-298: Add a regression test in the
`set_object_value_by_property_path` test module for the case where an index
segment is applied to an existing container of the other type, not just `{}` or
scalars. Use a test alongside `test_set_creates_array_from_scratch`,
`test_set_creates_array_intermediate`, and
`test_set_replaces_scalar_with_object/array` to verify that setting a path like
`scripts[0]` on an existing object preserves existing object data instead of
overwriting it. This should lock in the intended `set` behavior for mixed
object/array paths and catch the data-loss case in `pkg.rs`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3ac51955-bf47-4462-83c0-cd70aaa46e19

📥 Commits

Reviewing files that changed from the base of the PR and between 50b900f and 007bdfd.

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

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 007bdfd

@github-actions github-actions Bot added the reviewed: coderabbit CodeRabbit submitted an approving review label Jul 4, 2026
Perfectionist lint rejects trailing commas on single-line macro invocations.
Copilot AI review requested due to automatic review settings July 4, 2026 20:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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

Copy link
Copy Markdown

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

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

Copy link
Copy Markdown

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

@kairosci kairosci changed the title feat(pacquet): implement pkg command feat(pacquet-cli): implement pkg command Jul 4, 2026
Support -r/--recursive flag for pkg get/set/delete/fix, matching the TS
reference. Recursive get returns a JSON object keyed by package name.
Set/delete/fix run sequentially on each selected workspace project.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 24f7d71

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

31-38: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Inconsistent error-propagation pattern vs. sibling run() dispatcher.

Here, errors from args.run_recursive(...) / args.run(...) are captured into result and deferred into the future::ready(result) payload, so the function itself always returns Ok(future). The adjacent run() dispatcher (lines 49-56) instead uses ? to propagate errors immediately, only ever wrapping Ok(()) in the ready future. Functionally the error should still surface once the future is polled/awaited, but if any code path treats "dispatch function returned Err" differently from "future resolved to Err" (e.g., spinner/telemetry/early-exit logic before polling), this divergence could produce different observable behavior for pkg vs. other commands.

Consider aligning pkg with the established run() pattern for consistency, unless the deferred-error behavior is intentional:

♻️ Align with existing dispatcher pattern
 pub(super) fn pkg<'a>(ctx: &RunCtx<'a>, args: PkgArgs) -> miette::Result<CommandFuture<'a>> {
-    let result = if ctx.recursive {
-        args.run_recursive((ctx.config)()?, ctx.dir)
-    } else {
-        args.run(ctx.manifest_path)
-    };
-    Ok(Box::pin(std::future::ready(result)))
+    if ctx.recursive {
+        args.run_recursive((ctx.config)()?, ctx.dir)?;
+    } else {
+        args.run(ctx.manifest_path)?;
+    }
+    Ok(Box::pin(std::future::ready(Ok(()))))
 }

Could you confirm whether the caller of these dispatch_* functions handles a top-level Err the same way as a future that resolves to Err? If so, this is purely cosmetic; if not, this is a functional discrepancy.

🤖 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/dispatch_script.rs` around lines 31 - 38, The
pkg dispatcher currently wraps both args.run_recursive(...) and args.run(...)
errors inside future::ready(result), unlike the sibling run() dispatcher that
uses ? before boxing. Update pkg in dispatch_script.rs to match the run()
error-propagation pattern by propagating the dispatch-time error immediately and
only returning a ready future for the successful path, using the pkg function
and its args.run_recursive/args.run branches as the reference points.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pacquet/crates/cli/src/cli_args/dispatch_script.rs`:
- Around line 31-38: The pkg dispatcher currently wraps both
args.run_recursive(...) and args.run(...) errors inside future::ready(result),
unlike the sibling run() dispatcher that uses ? before boxing. Update pkg in
dispatch_script.rs to match the run() error-propagation pattern by propagating
the dispatch-time error immediately and only returning a ready future for the
successful path, using the pkg function and its args.run_recursive/args.run
branches as the reference points.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 470a2d3a-dece-47a4-8a58-c1c0e9d78a1f

📥 Commits

Reviewing files that changed from the base of the PR and between 3795988 and 24f7d71.

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

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 24f7d71

@zkochan
zkochan merged commit 7a27027 into pnpm:main Jul 7, 2026
34 checks passed
zkochan added a commit that referenced this pull request Jul 7, 2026
The pkg command (#12795) added a CliCommand::Pkg variant but
did not extend the exhaustive match in command_name, breaking every Rust
CI job with a non-exhaustive-patterns compile error.
@kairosci
kairosci deleted the feat/pacquet-pkg branch July 8, 2026 07:15
KSXGitHub pushed a commit that referenced this pull request Jul 8, 2026
Brings in the pkg command (#12795), recursive project listing
(#12848), the Pkg command_name match fix (#12850), and
the github-actions dependency bump (#12785).

No conflicts. main's changes are the pkg/list CLI commands and CI
workflows, disjoint from the login/auth-commands work; the shared
command-registry files (cli_args, cli_command, dispatch, dispatch_query,
switch_cli_version) merged cleanly with both the login and the pkg/list
entries.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants