Skip to content

feat(pacquet): --resume-from, --report-summary#12093

Merged
zkochan merged 5 commits into
mainfrom
claude/great-ride-aeKak
Jun 1, 2026
Merged

feat(pacquet): --resume-from, --report-summary#12093
zkochan merged 5 commits into
mainfrom
claude/great-ride-aeKak

Conversation

@KSXGitHub

@KSXGitHub KSXGitHub commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Ports pnpm's recursive run (pnpm run -r) to pacquet, including the --resume-from and --report-summary flags — which previously existed only in the TypeScript CLI.

What's added

  • pacquet -r run <script> — runs the script in every workspace project in topological order. Mirrors runRecursive: discover projects → build the inter-project graph → sort into chunks (graph_sequencer, the port of sortProjects) → execute sequentially.
  • --resume-from <pkg> — drops every chunk before the one containing <pkg> (port of getResumedPackageChunks). Unknown package → ERR_PNPM_RESUME_FROM_NOT_FOUND.
  • --report-summary — writes pnpm-exec-summary.json ({ "executionStatus": { <rootDir>: { status, duration, … } } }), port of writeRecursiveSummary. Written on bail-exit and at the end.
  • --no-bail — keeps going after a failure (recursive runs bail by default). Failures surface ERR_PNPM_RECURSIVE_FAIL (or ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL when bailing); a run matching no script → ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT.

A non-breaking executor helper, execute_shell_with_status, returns the child's exit status so per-package pass/fail can be recorded; execute_shell is unchanged.

Not yet ported (noted in the module)

--no-sort, --reverse, --workspace-concurrency parallelism, --filter narrowing of the selected set, and the RegExp script selector. The selected set is currently every workspace project, matching pacquet's currently-unfiltered install.

Tests

Integration tests port the upstream resume-from and report-summary cases from exec/commands/test/runRecursive.ts.


Written by an agent (Claude Code).

Summary by CodeRabbit

  • New Features
    • Run scripts recursively across all workspace projects with the -r/--recursive support.
    • Resume recursive execution from a specific package using --resume-from.
    • Generate/write execution summaries (pnpm-exec-summary.json) with --report-summary, showing per-project status and durations.
    • Continue execution across projects after failures using --no-bail.
    • --if-present silently skips missing scripts; missing-script errors reported otherwise.

Port pnpm's `pnpm run -r` (recursive run) to pacquet, including the
`--resume-from` and `--report-summary` flags, which previously existed
only in the TypeScript CLI.

- `pacquet -r run <script>` now runs the script in every workspace
  project in topological order, mirroring pnpm's runRecursive: discover
  projects, build the inter-project dependency graph, sort it into
  chunks via graph_sequencer (the port of sortProjects), and execute.
- `--resume-from <pkg>` drops every chunk before the one containing
  <pkg>, mirroring getResumedPackageChunks; an unknown package fails
  with ERR_PNPM_RESUME_FROM_NOT_FOUND.
- `--report-summary` writes pnpm-exec-summary.json with the per-package
  status (queued/running/passed/skipped/failure) and duration, nested
  under an executionStatus key, mirroring writeRecursiveSummary.
- `--no-bail` keeps running after a failure (recursive runs bail by
  default). Failures surface ERR_PNPM_RECURSIVE_FAIL, or
  ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL when bailing; a run that matches no
  script fails with ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT.

A new executor helper, execute_shell_with_status, returns the child's
exit status so per-package pass/fail can be recorded; execute_shell is
unchanged.

Not yet ported (noted in the module): --no-sort, --reverse,
--workspace-concurrency parallelism, --filter narrowing of the selected
set, and the RegExp script selector. The selected set is every workspace
project, matching pacquet's currently-unfiltered install.

Integration tests port the upstream resume-from and report-summary
cases from exec/commands/test/runRecursive.ts.

https://claude.ai/code/session_01QUdrDcP9iU3DwxR2TATobQ
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e6c4e58a-078a-4244-a433-1621ac360567

📥 Commits

Reviewing files that changed from the base of the PR and between b7f05fa and 63c1486.

📒 Files selected for processing (3)
  • pacquet/crates/cli/src/cli_args/run/recursive.rs
  • pacquet/crates/cli/tests/run_recursive.rs
  • pacquet/crates/executor/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • pacquet/crates/cli/src/cli_args/run/recursive.rs
  • pacquet/crates/cli/tests/run_recursive.rs
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Dylint
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plain String or &str in Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too via TryFrom<String> and/or FromStr and do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallible From<String> constructor
If upstream TypeScript occasionally constructs a branded string without validation, expose from_str_unchecked in Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization
Derive simple conversions for branded strings using #[derive(derive_more::From)] and #[derive(derive_more::Into)] instead of handwriting impl blocks; use manual impl only when conversion needs custom logic
Model TypeScript string literal unions (like 'auto' | 'always' | 'never') as Rust enums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like `${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide in CODE_STYLE_GUIDE.md — imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Choose owned vs. borrowed parameters to minimize copies; widen to t...

Files:

  • pacquet/crates/executor/src/lib.rs
🧠 Learnings (3)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.

Applied to files:

  • pacquet/crates/executor/src/lib.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.

Applied to files:

  • pacquet/crates/executor/src/lib.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.

Applied to files:

  • pacquet/crates/executor/src/lib.rs
🔇 Additional comments (1)
pacquet/crates/executor/src/lib.rs (1)

16-19: LGTM!

Also applies to: 33-35, 37-66


📝 Walkthrough

Walkthrough

This PR implements recursive workspace script execution for pacquet run -r <script>, enabling scripts to run across workspace projects in dependency-aware topological order with support for resume, reporting, and early-bail control flow.

Changes

Recursive Workspace Script Execution

Layer / File(s) Summary
Shell Execution Status API
pacquet/crates/executor/src/lib.rs
New execute_shell_with_status returns process ExitStatus alongside spawn/wait errors, enabling callers to capture non-zero exit codes as data rather than errors.
CLI Arguments & Dependencies
pacquet/crates/cli/Cargo.toml, pacquet/crates/cli/src/cli_args/run.rs
Three new CLI flags (--resume-from, --report-summary, --no-bail) are added to RunArgs; workspace crate dependencies and indexmap/serde are added to support recursive logic; run_recursive entry point method delegates to the recursive module.
Recursive Execution Core Logic
pacquet/crates/cli/src/cli_args/run/recursive.rs
Main recursive run implementation: builds workspace project graph, topologically sorts projects into execution chunks, optionally trims chunks from a resume-from package, executes the script per project in chunk order while tracking per-project status (queued/running/passed/skipped/failure), optionally writes pnpm-exec-summary.json, and fails overall if any project failed or if bail-on-first-failure is enabled.
CLI Command Dispatch
pacquet/crates/cli/src/cli_args.rs
CliCommand::Run now branches on the global --recursive flag: invokes run_recursive(config()?, &dir) when recursive is set; otherwise runs the single-project run(manifest_path()) path.
Integration Tests
pacquet/crates/cli/tests/run_recursive.rs
Comprehensive test coverage for recursive execution: basic script execution across all projects, --resume-from chunk skipping, error handling for unknown resume packages and missing scripts, --report-summary output with --no-bail to run all packages despite failures, default bail-on-first-failure with and without summary output, and --if-present conditional execution.

Sequence Diagram

sequenceDiagram
  participant CLI as CLI Dispatch
  participant Graph as Workspace Graph
  participant Sorter as Topological Sorter
  participant Executor as Script Executor
  participant Summary as Summary Writer
  CLI->>Graph: find workspace projects
  Graph->>Sorter: build projects graph
  Sorter->>Sorter: sort projects into chunks
  alt resume_from set
    Sorter->>Sorter: trim to resume point
  end
  Sorter->>Executor: execute script per project in chunk order
  Executor->>Executor: track status (queued/running/passed/skipped/failure)
  alt report_summary enabled
    Executor->>Summary: write pnpm-exec-summary.json
  end
  alt no_bail and failures exist
    Summary->>CLI: fail with RecursiveFail count
  end
  alt no_bail disabled and first failure
    Executor->>CLI: bail with RecursiveRunFirstFail
  end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

  • pnpm/pnpm#11959: Adds the global -r/--recursive flag and Config.recursive wiring that this PR builds upon to implement the actual recursive run behavior.

Suggested Reviewers

  • zkochan

Poem

I’m a rabbit in the workspace maze,
Hopping chunks in tidy arrays,
Scripts run in order, not astray,
Summaries saved at end of day,
Cheers to runs that clear the way! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly references the two main feature flags being added (--resume-from and --report-summary) as part of implementing recursive run support for pacquet, accurately capturing a core aspect of this changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/great-ride-aeKak

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 and usage tips.

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.01      5.1±0.32ms   850.9 KB/sec    1.00      5.1±0.11ms   856.1 KB/sec

…mmas

Fix two CI failures on the recursive-run integration tests:

- Dylint (`perfectionist::macro_trailing_comma`): add the trailing comma
  to the four multi-line `assert!` invocations.
- Lint and Test (windows-latest): the shared helpers are used only by
  the Unix-gated tests, so on Windows they tripped `dead_code` under
  `-D warnings`. Gate the whole file with `#![cfg(unix)]` (the build
  scripts run through pacquet's `sh -c` executor anyway), matching the
  single-package `run` tests.

https://claude.ai/code/session_01QUdrDcP9iU3DwxR2TATobQ
@codecov-commenter

codecov-commenter commented Jun 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.30%. Comparing base (54d2b57) to head (63c1486).

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12093      +/-   ##
==========================================
+ Coverage   87.22%   87.30%   +0.07%     
==========================================
  Files         248      249       +1     
  Lines       27605    27769     +164     
==========================================
+ Hits        24079    24243     +164     
  Misses       3526     3526              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.074 ± 0.078 2.006 2.251 1.00 ± 0.05
pacquet@main 2.072 ± 0.060 2.015 2.185 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.07416274696,
      "stddev": 0.07815654428114473,
      "median": 2.04439023316,
      "user": 2.6297493000000003,
      "system": 3.3593942199999995,
      "min": 2.0064088951600003,
      "max": 2.25118169916,
      "times": [
        2.05305552016,
        2.25118169916,
        2.01698324916,
        2.0113056081600003,
        2.0064088951600003,
        2.02930408816,
        2.1098961011600004,
        2.07228113716,
        2.03572494616,
        2.15548622516
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.07214217726,
      "stddev": 0.06047464612910224,
      "median": 2.04982742316,
      "user": 2.6977301000000002,
      "system": 3.37255482,
      "min": 2.01486815216,
      "max": 2.18461878616,
      "times": [
        2.08668058816,
        2.04672434816,
        2.04761089616,
        2.0520439501600003,
        2.01585065516,
        2.05265608916,
        2.01486815216,
        2.18461878616,
        2.0443154851600003,
        2.17605282216
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 653.3 ± 52.3 621.0 799.1 1.02 ± 0.09
pacquet@main 643.3 ± 16.6 625.5 676.1 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.65334615132,
      "stddev": 0.052314935739467695,
      "median": 0.63531286342,
      "user": 0.3555385,
      "system": 1.3350335999999998,
      "min": 0.62095724292,
      "max": 0.79905940992,
      "times": [
        0.79905940992,
        0.6600021849200001,
        0.64791840092,
        0.6352284299200001,
        0.63539729692,
        0.64062614892,
        0.63094751092,
        0.6349968889200001,
        0.62095724292,
        0.62832799892
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.64326231892,
      "stddev": 0.016582656764899405,
      "median": 0.63803208992,
      "user": 0.3542862,
      "system": 1.3333996,
      "min": 0.6254718689200001,
      "max": 0.67606360192,
      "times": [
        0.67606360192,
        0.63010966392,
        0.63935389792,
        0.66924525892,
        0.64473687492,
        0.63671028192,
        0.63313375892,
        0.64297860592,
        0.63481937592,
        0.6254718689200001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.383 ± 0.054 2.308 2.457 1.00 ± 0.03
pacquet@main 2.382 ± 0.045 2.279 2.425 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.3827069179600002,
      "stddev": 0.054234773127909086,
      "median": 2.38215944286,
      "user": 3.807846799999999,
      "system": 3.1957575,
      "min": 2.30765953186,
      "max": 2.45747474686,
      "times": [
        2.41851152686,
        2.38240048786,
        2.30765953186,
        2.3819183978600003,
        2.32011169686,
        2.33936648186,
        2.34208514386,
        2.42793732886,
        2.45747474686,
        2.44960383686
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.38166182396,
      "stddev": 0.04498563565952798,
      "median": 2.39739775336,
      "user": 3.8640014000000003,
      "system": 3.1986265000000005,
      "min": 2.27886948886,
      "max": 2.42465810386,
      "times": [
        2.4024332948600002,
        2.4091242518600002,
        2.41201902386,
        2.39236221186,
        2.36523202886,
        2.36826174086,
        2.42137123586,
        2.42465810386,
        2.27886948886,
        2.34228685886
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.552 ± 0.024 1.515 1.592 1.00 ± 0.03
pacquet@main 1.551 ± 0.043 1.474 1.617 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.5519432103000002,
      "stddev": 0.023753161319417383,
      "median": 1.5504571761000001,
      "user": 1.78003954,
      "system": 1.8688306799999999,
      "min": 1.5152500041,
      "max": 1.5923332221000002,
      "times": [
        1.5300457011000002,
        1.5152500041,
        1.5581972891,
        1.5923332221000002,
        1.5427170631000002,
        1.5663835511000002,
        1.5377939751000003,
        1.5363387501,
        1.5796359091,
        1.5607366381
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.5508677894000003,
      "stddev": 0.04343723320614904,
      "median": 1.5365895641000002,
      "user": 1.7464895400000002,
      "system": 1.8694905800000001,
      "min": 1.4744028401,
      "max": 1.6174974541000002,
      "times": [
        1.5220032991,
        1.5818595311,
        1.5973877211000003,
        1.5341448471,
        1.4744028401,
        1.5860280001,
        1.5390342811000002,
        1.6174974541000002,
        1.5331483121,
        1.5231716081000002
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12093
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
2,382.71 ms
(-0.32%)Baseline: 2,390.30 ms
2,868.36 ms
(83.07%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,551.94 ms
(+2.19%)Baseline: 1,518.62 ms
1,822.35 ms
(85.16%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
2,074.16 ms
(-2.12%)Baseline: 2,118.99 ms
2,542.79 ms
(81.57%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
653.35 ms
(-3.30%)Baseline: 675.63 ms
810.76 ms
(80.58%)
🐰 View full continuous benchmarking report in Bencher

claude added 2 commits June 1, 2026 07:55
Fill the two coverage holes in the recursive-run handler:

- bail + report-summary: the first failing script writes the summary,
  then aborts with ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL; a package that
  sorts after the failure stays `queued`.
- no-script: a recursive run for a script no package defines fails with
  ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT, and `--if-present` turns that into a
  clean no-op.

https://claude.ai/code/session_01QUdrDcP9iU3DwxR2TATobQ
The bail tests always passed --report-summary, leaving the
report-summary-off side of the bail block (recursive.rs:136) uncovered.
Add a test for a failing script with bail on and no --report-summary:
it still fails with ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL and writes no
summary file. Verified with cargo llvm-cov that recursive.rs now has no
missing lines.

https://claude.ai/code/session_01QUdrDcP9iU3DwxR2TATobQ
@KSXGitHub KSXGitHub marked this pull request as ready for review June 1, 2026 08:44
@KSXGitHub KSXGitHub requested a review from zkochan as a code owner June 1, 2026 08:44
Copilot AI review requested due to automatic review settings June 1, 2026 08:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Ports pnpm’s recursive script runner (pnpm run -r) into pacquet, adding workspace-topological execution plus --resume-from and --report-summary support so recursive runs can be resumed and their outcomes recorded similarly to the TypeScript CLI.

Changes:

  • Add recursive pacquet -r run <script> implementation with --resume-from, --report-summary, and --no-bail semantics.
  • Extend the executor with execute_shell_with_status so callers can record per-package exit status without treating non-zero exits as spawn/wait errors.
  • Add Unix-gated integration tests covering resume-from, report-summary, bail/no-bail, and no-script behavior.

Reviewed changes

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

Show a summary per file
File Description
pacquet/crates/executor/src/lib.rs Adds an executor helper that returns ExitStatus for shell execution.
pacquet/crates/cli/src/cli_args/run/recursive.rs Implements recursive run across workspace projects, including resume-from and summary writing.
pacquet/crates/cli/src/cli_args/run.rs Adds new CLI flags and dispatches to recursive execution when -r/--recursive is set.
pacquet/crates/cli/src/cli_args.rs Routes pacquet run to recursive implementation when global recursive mode is enabled.
pacquet/crates/cli/tests/run_recursive.rs Adds integration coverage for recursive run flags and error modes.
pacquet/crates/cli/Cargo.toml Adds dependencies needed by the recursive runner (indexmap, serde, workspace graph crates).
Cargo.lock Updates lockfile for the new dependencies.

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

Comment thread pacquet/crates/cli/src/cli_args/run/recursive.rs Outdated
Comment thread pacquet/crates/cli/tests/run_recursive.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@pacquet/crates/cli/src/cli_args/run/recursive.rs`:
- Around line 290-315: The serialized summary currently includes
ExecutionStatus's prefix/message fields, but pnpm expects only status+duration;
add a new serializable type (e.g., ExecutionStatusInSummary with only status:
Status and optional duration: Option<f64>) and use that for the
ExecSummaryFile's execution_status map or convert ExecutionStatus ->
ExecutionStatusInSummary inside write_recursive_summary before serializing; keep
the existing ExecutionStatus struct for runtime use (and its queued()
constructor) but stop serializing its prefix/message (remove their writes in the
failure branch and any serde derives that cause them to be emitted), and ensure
write_recursive_summary serializes IndexMap<String, ExecutionStatusInSummary> so
output matches pnpm's format.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 04169c6a-21be-4e9b-8e90-db032df8fcf8

📥 Commits

Reviewing files that changed from the base of the PR and between 54d2b57 and b7f05fa.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • pacquet/crates/cli/Cargo.toml
  • pacquet/crates/cli/src/cli_args.rs
  • pacquet/crates/cli/src/cli_args/run.rs
  • pacquet/crates/cli/src/cli_args/run/recursive.rs
  • pacquet/crates/cli/tests/run_recursive.rs
  • pacquet/crates/executor/src/lib.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Agent
🧰 Additional context used
📓 Path-based instructions (3)
pacquet/**/Cargo.toml

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/Cargo.toml: Check whether the workspace already depends on something suitable in [workspace.dependencies] in the root Cargo.toml before adding a new dependency
Keep dependencies at the right level — add a new dependency to the specific crate that needs it, not to the workspace root or shared crate unless multiple crates depend on it

Files:

  • pacquet/crates/cli/Cargo.toml
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plain String or &str in Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too via TryFrom<String> and/or FromStr and do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallible From<String> constructor
If upstream TypeScript occasionally constructs a branded string without validation, expose from_str_unchecked in Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization
Derive simple conversions for branded strings using #[derive(derive_more::From)] and #[derive(derive_more::Into)] instead of handwriting impl blocks; use manual impl only when conversion needs custom logic
Model TypeScript string literal unions (like 'auto' | 'always' | 'never') as Rust enums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like `${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide in CODE_STYLE_GUIDE.md — imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Choose owned vs. borrowed parameters to minimize copies; widen to t...

Files:

  • pacquet/crates/executor/src/lib.rs
  • pacquet/crates/cli/src/cli_args.rs
  • pacquet/crates/cli/src/cli_args/run.rs
  • pacquet/crates/cli/tests/run_recursive.rs
  • pacquet/crates/cli/src/cli_args/run/recursive.rs
pacquet/**/tests/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/tests/**/*.rs: Prefer real fixtures over dependency-injection seams — use tempfile::TempDir, the mocked registry, or integration tests spawning the actual binary for happy paths and most error paths; use the DI seam only for filesystem error kinds, deterministic time, shared process-global state, or external-service happy paths
Follow test-logging guidance — log before non-assert_eq! assertions, dbg! complex structures, skip logging for simple scalar assert_eq!
Tests must not be tolerant of missing build/runtime environment by silently returning early when a tool isn't found — let the test panic when a required tool is absent
Prefer #[cfg_attr(target_os = "windows", ignore = "...")] (or matching #[cfg(unix)] gates) over runtime probe-and-skip helpers for platform-locked tools
Port relevant pnpm tests to Rust tests whenever they translate when porting behavior from pnpm
Use snapshot tests with insta and carefully review diffs when intentional changes alter snapshots; accept with cargo insta review only after careful review
Tests that need the mocked registry should start pnpr through pacquet-testing-utils; cargo test / cargo nextest run should not require a separate just registry-mock launch step

Files:

  • pacquet/crates/cli/tests/run_recursive.rs
🧠 Learnings (3)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.

Applied to files:

  • pacquet/crates/executor/src/lib.rs
  • pacquet/crates/cli/src/cli_args.rs
  • pacquet/crates/cli/src/cli_args/run.rs
  • pacquet/crates/cli/tests/run_recursive.rs
  • pacquet/crates/cli/src/cli_args/run/recursive.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.

Applied to files:

  • pacquet/crates/executor/src/lib.rs
  • pacquet/crates/cli/src/cli_args.rs
  • pacquet/crates/cli/src/cli_args/run.rs
  • pacquet/crates/cli/tests/run_recursive.rs
  • pacquet/crates/cli/src/cli_args/run/recursive.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.

Applied to files:

  • pacquet/crates/executor/src/lib.rs
  • pacquet/crates/cli/src/cli_args.rs
  • pacquet/crates/cli/src/cli_args/run.rs
  • pacquet/crates/cli/tests/run_recursive.rs
  • pacquet/crates/cli/src/cli_args/run/recursive.rs
🪛 OpenGrep (1.22.0)
pacquet/crates/executor/src/lib.rs

[ERROR] 44-44: Dynamic command passed to Command::new with a shell invocation. Pass arguments directly to Command::new without a shell wrapper.

(coderabbit.command-injection.rust-shell-command)

🔇 Additional comments (7)
pacquet/crates/executor/src/lib.rs (1)

30-47: LGTM!

pacquet/crates/cli/Cargo.toml (1)

18-44: LGTM!

pacquet/crates/cli/src/cli_args/run.rs (1)

23-70: LGTM!

pacquet/crates/cli/src/cli_args/run/recursive.rs (2)

105-157: LGTM!


167-288: LGTM!

pacquet/crates/cli/src/cli_args.rs (1)

236-242: LGTM!

pacquet/crates/cli/tests/run_recursive.rs (1)

1-319: LGTM!

Comment thread pacquet/crates/cli/src/cli_args/run/recursive.rs
Recursive run spawned every package's script via `sh -c` without
setting the working directory, so scripts ran in pacquet's process CWD
(the workspace root) instead of their own package root. That breaks
scripts relying on relative paths and diverges from pnpm, whose
`runLifecycleHook` runs with `pkgRoot` as the working directory.

- Give `execute_shell_with_status` a `current_dir` argument (factored
  through a private `spawn_shell` helper); `execute_shell` keeps its
  inherited-CWD behavior, so its callers are unchanged.
- Pass each package's root as the script's working directory.
- Make the marker-based recursive-run tests cwd-sensitive: scripts now
  write a relative `ran.txt`, and the tests assert it lands under each
  package root (and not at the workspace root), so a wrong-CWD
  regression fails the suite.

https://claude.ai/code/session_01QUdrDcP9iU3DwxR2TATobQ
@zkochan zkochan merged commit 577a90f into main Jun 1, 2026
28 checks passed
@zkochan zkochan deleted the claude/great-ride-aeKak branch June 1, 2026 10:48
zkochan pushed a commit that referenced this pull request Jun 2, 2026
…11938)

> [!WARNING]
> **Scope note.** Per [`pacquet/CONTRIBUTING.md`](https://github.com/pnpm/pnpm/blob/d4a2b0364c/pacquet/CONTRIBUTING.md), pacquet's current focus is Stage 1 (the headless installer); `exec` and `dlx` are new top-level commands, so this PR sits outside Stage 1 and is opened for review/discussion ([roadmap pnpm/pacquet#299](https://github.com/pnpm/pacquet/issues/299)).

## Summary

Ports of `run`, `exec`, and `dlx` from the TypeScript pnpm CLI.

- **`run`**: runs scripts through a new foreground `run_script` in `pacquet-executor` (sets up `node_modules/.bin` on `PATH` + the `npm_*` env). Handles `pre`/`post` scripts under `enablePrePostScripts`, arg shell-quoting (with the Windows `cmd /d /s /c` verbatim `raw_arg` path), script listing, hidden (`.`-prefixed) scripts, `--if-present`, the `start`→`server.js` fallback (with the NO_SCRIPT_OR_SERVER guard, including empty-string `start`), the `[ELIFECYCLE]` failure line (with the `test`-stage and signal-killed variants), and exit-code propagation. The recursive runner's scaffolding (`--resume-from` / `--report-summary`) landed separately on `main` via [#12093](#12093); this PR dispatches to it when `-r` is set and hardens it to match pnpm — per-project `pre`/`post`, the `PNPM_SCRIPT_SRC_DIR` recursion guard, pnpm's per-stage no-op guards, hidden-script handling, and `--no-bail`.
- **`exec`**: runs a command with `node_modules/.bin` + `extraBinPaths` on `PATH` (resolved via `which`), stamps `npm_config_user_agent` / `PNPM_PACKAGE_NAME` / `NODE_OPTIONS`, supports `--shell-mode` (the joined command goes through the shared `push_script_arg` helper, so the Windows `cmd /d /s /c` verbatim path uses `raw_arg` and embedded quoting survives), rejects delimiter-containing dirs (`ERR_PNPM_BAD_PATH_DIR`). The **recursive variant** (`-r`) runs the command in every workspace project, topologically sorted and sequential, with `--resume-from` / `--report-summary` / `--no-bail` and pnpm's error codes (`ERR_PNPM_RECURSIVE_EXEC_NO_PACKAGE` / `ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL` / `ERR_PNPM_RECURSIVE_FAIL`). The workspace-graph / summary machinery is shared with recursive `run` through a new `cli_args::recursive` module.
- **`dlx`**: installs the package(s) into a TTL cache dir (reusing the install pipeline, anchored at the cache dir, with a *fresh* per-install build-script allow-list — caller's `allow_builds`/`dangerouslyAllowAllBuilds` don't leak in), then runs the resolved bin in the process cwd. Supports `--package`, `--allow-build`, `--shell-mode` (same `push_script_arg` verbatim path as exec), `--cpu`/`--os`/`--libc` architecture overrides (folded into the per-axis `supportedArchitectures` of the dlx install **and** into the cache key, so different overrides don't share a cached install), `dlxCacheMaxAge`; same PATH guard as exec.

New `Config` settings: `enablePrePostScripts`, `scriptShell`, `nodeOptions`, `dlxCacheMaxAge` (wired into `pnpm-workspace.yaml` + the `PNPM_CONFIG_*` overlay). Their defaults match pnpm and are asserted by the `pnpm_default_parity` contract test — this PR moves `enablePrePostScripts` (which pnpm defaults to `true`, a breaking change in [#7634](#7634) shipped in v9) and `dlxCacheMaxAge` into its mapped rows. `extraBinPaths` is kept as a computed field (empty until workspace support lands), matching pnpm — it is not a user-settable key.

## Deferred (documented in code)

- **`--filter` and `--workspace-concurrency`.** Recursive `run` and `exec` run every workspace project sequentially; the `--filter` package-selector subsystem and `--workspace-concurrency` parallelism are not ported yet (the global `--filter` / `--recursive` flags are accepted via clap but `--filter` is not consumed). `dlx` stays single-package by design (matches pnpm).
- `run`: the `/regexp/` script selector and the fuzzy "did you mean" hint are not ported (no regex/levenshtein dep); `scriptsPrependNodePath: always` can't prepend the node dir (pacquet resolves no node execpath anywhere yet).
- `dlx`: the cache key uses raw specs (not resolved ids); no `approve-builds` prompt.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants