feat(pacquet): --resume-from, --report-summary#12093
Conversation
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
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (2)
📜 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)
🧰 Additional context used📓 Path-based instructions (1)pacquet/**/*.rs📄 CodeRabbit inference engine (pacquet/AGENTS.md)
Files:
🧠 Learnings (3)📚 Learning: 2026-05-20T19:40:55.051ZApplied to files:
📚 Learning: 2026-05-22T00:08:44.646ZApplied to files:
📚 Learning: 2026-05-20T23:07:58.444ZApplied to files:
🔇 Additional comments (1)
📝 WalkthroughWalkthroughThis PR implements recursive workspace script execution for ChangesRecursive Workspace Script Execution
Sequence DiagramsequenceDiagram
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
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly Related PRs
Suggested Reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Micro-Benchmark ResultsLinux |
…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 Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
Integrated-Benchmark Report (Linux)Scenario: Isolated linker: fresh restore, cold cache + cold store
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
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
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
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
]
}
]
} |
|
| Branch | pr/12093 |
| Testbed | pacquet |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result milliseconds (ms) (Result Δ%) | Upper Boundary milliseconds (ms) (Limit %) |
|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 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%) |
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
There was a problem hiding this comment.
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-bailsemantics. - Extend the executor with
execute_shell_with_statusso 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.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pacquet/crates/cli/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
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (6)
pacquet/crates/cli/Cargo.tomlpacquet/crates/cli/src/cli_args.rspacquet/crates/cli/src/cli_args/run.rspacquet/crates/cli/src/cli_args/run/recursive.rspacquet/crates/cli/tests/run_recursive.rspacquet/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 rootCargo.tomlbefore 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 firespnpm:<channel>events throughglobalLogger,logger.debug(...), orstreamParser.write(...), mirror the call site, payload, and ordering so@pnpm/cli.default-reporterparses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plainStringor&strin Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too viaTryFrom<String>and/orFromStrand 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 infallibleFrom<String>constructor
If upstream TypeScript occasionally constructs a branded string without validation, exposefrom_str_uncheckedin 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 handwritingimplblocks; use manualimplonly when conversion needs custom logic
Model TypeScript string literal unions (like'auto' | 'always' | 'never') as Rustenums 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 inCODE_STYLE_GUIDE.md— imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching,pipe-trait, error handling, test layout, and cloning ofArcandRc
Choose owned vs. borrowed parameters to minimize copies; widen to t...
Files:
pacquet/crates/executor/src/lib.rspacquet/crates/cli/src/cli_args.rspacquet/crates/cli/src/cli_args/run.rspacquet/crates/cli/tests/run_recursive.rspacquet/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 — usetempfile::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 scalarassert_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 withinstaand carefully review diffs when intentional changes alter snapshots; accept withcargo insta reviewonly after careful review
Tests that need the mocked registry should startpnprthroughpacquet-testing-utils;cargo test/cargo nextest runshould not require a separatejust registry-mock launchstep
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.rspacquet/crates/cli/src/cli_args.rspacquet/crates/cli/src/cli_args/run.rspacquet/crates/cli/tests/run_recursive.rspacquet/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.rspacquet/crates/cli/src/cli_args.rspacquet/crates/cli/src/cli_args/run.rspacquet/crates/cli/tests/run_recursive.rspacquet/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.rspacquet/crates/cli/src/cli_args.rspacquet/crates/cli/src/cli_args/run.rspacquet/crates/cli/tests/run_recursive.rspacquet/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!
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
…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.
Ports pnpm's recursive run (
pnpm run -r) to pacquet, including the--resume-fromand--report-summaryflags — 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. MirrorsrunRecursive: discover projects → build the inter-project graph → sort into chunks (graph_sequencer, the port ofsortProjects) → execute sequentially.--resume-from <pkg>— drops every chunk before the one containing<pkg>(port ofgetResumedPackageChunks). Unknown package →ERR_PNPM_RESUME_FROM_NOT_FOUND.--report-summary— writespnpm-exec-summary.json({ "executionStatus": { <rootDir>: { status, duration, … } } }), port ofwriteRecursiveSummary. Written on bail-exit and at the end.--no-bail— keeps going after a failure (recursive runs bail by default). Failures surfaceERR_PNPM_RECURSIVE_FAIL(orERR_PNPM_RECURSIVE_RUN_FIRST_FAILwhen 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_shellis unchanged.Not yet ported (noted in the module)
--no-sort,--reverse,--workspace-concurrencyparallelism,--filternarrowing of the selected set, and the RegExp script selector. The selected set is currently every workspace project, matching pacquet's currently-unfilteredinstall.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
-r/--recursivesupport.--resume-from.--report-summary, showing per-project status and durations.--no-bail.--if-presentsilently skips missing scripts; missing-script errors reported otherwise.