feat(default-reporter): render pnpm-identical visual install output in pacquet#12431
Conversation
Add a `pacquet-default-reporter` crate that renders the same terminal output as pnpm's `@pnpm/cli.default-reporter` for install/add/update/ remove — the live progress line, the packages diff, lifecycle script output, and the "Done in ..." footer — and make it pacquet's default reporter. The reporter folds the existing `pnpm:*` log events into a mutex-guarded state machine that recomputes the terminal frame, replacing pnpm's RxJS graph (which only exists because pnpm's reporter is a separate process). It renders in place on a TTY and append-only otherwise. A new `pnpm:execution-time` channel drives the "Done in" footer. The footer reads "using pacquet v<version>" rather than "using pnpm v..." so pacquet does not misreport the tool identity; everything else matches pnpm's output.
Code Review by Qodo
1. Unbounded lifecycle output buffering
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughA new ChangesDefault Reporter and Event Handling Refinement
Sequence Diagram(s)sequenceDiagram
participant main as main()
participant cli as CliArgs::run
participant install as InstallPipeline::run~Reporter~
participant reporter as DefaultReporter::emit
participant sink as global Mutex~Sink~
main->>main: set_package_version(PACQUET_VERSION)
main->>cli: run(ReporterType::Default)
cli->>reporter: set_cwd(root)
cli->>cli: started_at = now_millis()
cli->>cli: is_install_family = (cmd == Add | Remove | Install)
cli->>install: pipeline.run::<DefaultReporter>()
install->>reporter: emit(LogEvent::Progress/Stats/...)
reporter->>sink: serialize and handle
cli->>reporter: emit(LogEvent::ExecutionTime)
sink->>sink: render "Done in X ms using pacquet v…"
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related issues
Possibly related PRs
Suggested labels
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 |
PR Summary by QodoAdd pnpm-style default reporter and make it pacquet’s default output WalkthroughsDescription• Introduces a new pacquet-default-reporter crate that renders pnpm-identical install frames. • Switches the CLI default reporter from silent to default, with non-TTY append-only fallback. • Adds pnpm:execution-time log events to drive a Done in … using pacquet v… footer. Diagramgraph TD
A["pacquet CLI"] --> B["pacquet-reporter: LogEvent"] --> C["DefaultReporter (emit)"] --> D["ReporterState (fold events)"] --> E{TTY stdout?}
E -->|"yes"| F["In-place frame redraw"] --> G["stdout/terminal"]
E -->|"no"| H["Append-only lines"] --> G
High-Level AssessmentThe following are alternative approaches to this PR: 1. Make `Reporter` instance-based (no global mutex)
2. Use a terminal control library (e.g., crossterm) for redraw/size
3. Model pnpm’s separate-process reporter (spawn + stream)
Recommendation: Given the existing File ChangesEnhancement (7)
Tests (2)
Other (3)
|
Micro-Benchmark ResultsLinux |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #12431 +/- ##
==========================================
- Coverage 88.20% 87.83% -0.37%
==========================================
Files 303 307 +4
Lines 40051 41035 +984
==========================================
+ Hits 35325 36045 +720
- Misses 4726 4990 +264 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pacquet/crates/cli/src/cli_args.rs (1)
100-114:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftAdd explicit
append-onlyreporter mode to keep CLI parity with pnpm.Line 101-Line 103 documents that
append-onlyis not selectable, which makes--reporter=append-onlydiverge from pnpm’s user-visible reporter contract for install/add/update/remove flows. This is a compatibility break for users/scripts expecting pnpm-compatible flag values.As per coding guidelines, “pacquet is the port of pnpm; pnpm is source of truth, pacquet must not diverge.”
🤖 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.rs` around lines 100 - 114, Add an explicit `AppendOnly` variant to the `ReporterType` enum alongside the existing `Default`, `Ndjson`, and `Silent` variants to maintain parity with pnpm's reporter contract. Include documentation for the new `AppendOnly` variant describing its append-only output behavior. Update the `Default` variant's documentation to clarify that it automatically falls back to append-only rendering only when stdout is not a TTY, distinguishing it from an explicit `AppendOnly` mode.Source: Coding guidelines
🧹 Nitpick comments (1)
pacquet/crates/default-reporter/tests/render.rs (1)
250-255: ⚡ Quick winAvoid pinning the footer to a fixed package version in this golden frame.
Line 254 hardcodes
pacquet v0.0.1, which will fail this ordering test on routine version bumps even when rendering behavior is unchanged.Suggested test-stability adjustment
- assert_eq!( - frame, - "Packages: +1\n+\n\ndependencies:\n+ foo 1.0.0\n\n\ - Progress: resolved 1, reused 1, downloaded 0, added 1, done\n\ - Done in 1.2s using pacquet v0.0.1", - ); + assert!( + frame.starts_with( + "Packages: +1\n+\n\ndependencies:\n+ foo 1.0.0\n\n\ + Progress: resolved 1, reused 1, downloaded 0, added 1, done\n\ + Done in 1.2s using pacquet v" + ), + "got: {frame}" + );🤖 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/default-reporter/tests/render.rs` around lines 250 - 255, The assert_eq! call in the test contains a hardcoded version string `pacquet v0.0.1` in the expected frame output, which will cause the test to fail whenever the package version is bumped even if the rendering logic is unchanged. Instead of asserting an exact string match, use a regex pattern or similar flexible assertion to validate the frame structure and content while allowing the version number to vary dynamically. This decouples the test from the hardcoded version and makes it resilient to routine version updates.
🤖 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.rs`:
- Around line 72-73: A user-visible change to add a default reporter
(ReporterType::Default as the default value in the reporter field) requires its
own dedicated changeset entry. Create a new changeset file under the .changeset
directory that documents this default reporter feature change and explicitly
includes "pnpm" with the appropriate bump type, separate from the existing
catalog-update-policy.md changeset which documents an unrelated catalog version
policy fix. Follow the repository policy that requires an explicit "pnpm" entry
in the changeset for any user-visible changes affecting published packages.
In `@pacquet/crates/default-reporter/src/format.rs`:
- Around line 191-193: The contains_path function currently performs raw
substring matching on normalized paths, which incorrectly matches unrelated
paths with overlapping text and violates the path segment run contract. Replace
the simple substring matching in contains_path with proper path segment
matching: split both the normalized haystack and needle by the path separator,
then verify that the needle segments appear as a continuous sequence within the
haystack segments, returning true only when there is a proper segment-level
match rather than an arbitrary substring occurrence.
---
Outside diff comments:
In `@pacquet/crates/cli/src/cli_args.rs`:
- Around line 100-114: Add an explicit `AppendOnly` variant to the
`ReporterType` enum alongside the existing `Default`, `Ndjson`, and `Silent`
variants to maintain parity with pnpm's reporter contract. Include documentation
for the new `AppendOnly` variant describing its append-only output behavior.
Update the `Default` variant's documentation to clarify that it automatically
falls back to append-only rendering only when stdout is not a TTY,
distinguishing it from an explicit `AppendOnly` mode.
---
Nitpick comments:
In `@pacquet/crates/default-reporter/tests/render.rs`:
- Around line 250-255: The assert_eq! call in the test contains a hardcoded
version string `pacquet v0.0.1` in the expected frame output, which will cause
the test to fail whenever the package version is bumped even if the rendering
logic is unchanged. Instead of asserting an exact string match, use a regex
pattern or similar flexible assertion to validate the frame structure and
content while allowing the version number to vary dynamically. This decouples
the test from the hardcoded version and makes it resilient to routine version
updates.
🪄 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: dff2103f-1089-437e-967a-57108a740ca0
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (12)
Cargo.tomlpacquet/crates/cli/Cargo.tomlpacquet/crates/cli/src/cli_args.rspacquet/crates/cli/src/lib.rspacquet/crates/cli/tests/run.rspacquet/crates/default-reporter/Cargo.tomlpacquet/crates/default-reporter/src/colors.rspacquet/crates/default-reporter/src/format.rspacquet/crates/default-reporter/src/lib.rspacquet/crates/default-reporter/src/state.rspacquet/crates/default-reporter/tests/render.rspacquet/crates/reporter/src/lib.rs
Integrated-Benchmark Report (Linux)Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD. Scenario: Isolated linker: fresh restore, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.196649032639999,
"stddev": 0.13969080034837775,
"median": 4.25501661304,
"user": 3.8573088800000006,
"system": 3.5249276599999995,
"min": 4.01514281654,
"max": 4.37183912354,
"times": [
4.30761073154,
4.28882259054,
4.37183912354,
4.29807666554,
4.221210635539999,
4.07111433554,
4.04128634454,
4.0351383705399995,
4.316248712539999,
4.01514281654
]
},
{
"command": "pacquet@main",
"mean": 4.179492271239999,
"stddev": 0.17750074757837794,
"median": 4.161331690539999,
"user": 3.8632493800000005,
"system": 3.53073636,
"min": 3.92295635454,
"max": 4.46441690954,
"times": [
4.35405995054,
4.365831249539999,
4.232680183539999,
4.46441690954,
4.094485747539999,
4.079271675539999,
3.92295635454,
4.22817763354,
4.058926876539999,
3.9941161315400002
]
},
{
"command": "pnpr@HEAD",
"mean": 2.21690309254,
"stddev": 0.12902181780852698,
"median": 2.22182790554,
"user": 2.61974048,
"system": 2.99058596,
"min": 1.9646542245400003,
"max": 2.35772534754,
"times": [
2.35772534754,
2.17590949454,
2.34642215154,
2.2996220535400003,
2.10017548154,
1.9646542245400003,
2.1391569345400003,
2.3462567505400003,
2.17136217054,
2.2677463165400003
]
},
{
"command": "pnpr@main",
"mean": 2.1279276523400004,
"stddev": 0.0759986871572718,
"median": 2.10806214754,
"user": 2.61529788,
"system": 2.98815386,
"min": 2.05374868954,
"max": 2.29261904554,
"times": [
2.14197593454,
2.12170661254,
2.07665712354,
2.09441768254,
2.20319048754,
2.16681759454,
2.05374868954,
2.05978819054,
2.06835516254,
2.29261904554
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.6068294398999999,
"stddev": 0.011272576586188996,
"median": 0.6068501070000001,
"user": 0.35202219999999995,
"system": 1.31115758,
"min": 0.5900226455,
"max": 0.6308222345000001,
"times": [
0.6074044315,
0.6038516755000001,
0.6308222345000001,
0.6152138875000001,
0.5900226455,
0.5937172645000001,
0.6063189415,
0.6108749635,
0.6026870825,
0.6073812725000001
]
},
{
"command": "pacquet@main",
"mean": 0.6367955085,
"stddev": 0.019985156779834597,
"median": 0.6296891440000001,
"user": 0.36189119999999997,
"system": 1.3202634800000002,
"min": 0.6135616485000001,
"max": 0.6742110165,
"times": [
0.6252272295000001,
0.6303096445,
0.6280259635000001,
0.6742110165,
0.6666111275000001,
0.6345901485000001,
0.6135616485000001,
0.6290686435,
0.6186425965000001,
0.6477070665000001
]
},
{
"command": "pnpr@HEAD",
"mean": 0.6684919581,
"stddev": 0.023023083481709467,
"median": 0.6663745965000001,
"user": 0.3699013,
"system": 1.34186318,
"min": 0.6450966885,
"max": 0.7110846625,
"times": [
0.7110846625,
0.6450966885,
0.6714927055000001,
0.6470918985,
0.6999271295,
0.6798518505000001,
0.6634580235,
0.6692911695,
0.6523038945,
0.6453215585000001
]
},
{
"command": "pnpr@main",
"mean": 0.7110956536000002,
"stddev": 0.12387570822215609,
"median": 0.6637157650000001,
"user": 0.3770805,
"system": 1.34828298,
"min": 0.6484800235,
"max": 1.0563255305,
"times": [
0.6622872145,
0.7305159315,
0.6484800235,
0.6539143435,
0.6802552515,
0.6988293105000001,
0.6532996105000001,
0.6619050045,
0.6651443155000001,
1.0563255305
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.14312098616,
"stddev": 0.051074851262721266,
"median": 4.13758608866,
"user": 3.67852514,
"system": 3.3384147200000003,
"min": 4.06885850516,
"max": 4.2292421751600004,
"times": [
4.2292421751600004,
4.13366005616,
4.12434076716,
4.11192821716,
4.1770032201600005,
4.21045415816,
4.14151212116,
4.14945353516,
4.06885850516,
4.08475710616
]
},
{
"command": "pacquet@main",
"mean": 4.1636785205599995,
"stddev": 0.045479871160442935,
"median": 4.1493186606600005,
"user": 3.66262894,
"system": 3.35895952,
"min": 4.09616084916,
"max": 4.24477372416,
"times": [
4.1793535651600004,
4.19511543316,
4.15482761116,
4.14380971016,
4.09616084916,
4.13595867516,
4.12718936816,
4.24477372416,
4.14008621316,
4.21951005616
]
},
{
"command": "pnpr@HEAD",
"mean": 2.1503038594599997,
"stddev": 0.1281439775540753,
"median": 2.14171122166,
"user": 2.46847924,
"system": 2.88307542,
"min": 2.00723112916,
"max": 2.39659886316,
"times": [
2.26447518716,
2.16775532316,
2.11566712016,
2.04541219616,
2.39659886316,
2.00723112916,
2.17168056616,
2.01324070716,
2.05641058316,
2.26456691916
]
},
{
"command": "pnpr@main",
"mean": 2.1539535776600003,
"stddev": 0.11389604246834908,
"median": 2.13045562366,
"user": 2.45262594,
"system": 2.89942562,
"min": 2.01764699216,
"max": 2.35211990216,
"times": [
2.11188587016,
2.06813783316,
2.35211990216,
2.01764699216,
2.06191543116,
2.0566292121600003,
2.16251307316,
2.14902537716,
2.2841023381600003,
2.27555974716
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.3261457880200003,
"stddev": 0.017154592021769766,
"median": 1.32912233992,
"user": 1.3145486199999996,
"system": 1.7042112799999998,
"min": 1.30139666892,
"max": 1.35320046092,
"times": [
1.35320046092,
1.3345205979199999,
1.31065557792,
1.33361709492,
1.33770671692,
1.34413608292,
1.32462758492,
1.3127292509200001,
1.30886784392,
1.30139666892
]
},
{
"command": "pacquet@main",
"mean": 1.29941433622,
"stddev": 0.033537883304263924,
"median": 1.28922132892,
"user": 1.2769729199999997,
"system": 1.6840928800000001,
"min": 1.25951334992,
"max": 1.38253767492,
"times": [
1.29978657092,
1.38253767492,
1.2876998369200001,
1.25951334992,
1.2756116529200001,
1.28270176892,
1.31594875592,
1.31190109392,
1.28999001792,
1.28845263992
]
},
{
"command": "pnpr@HEAD",
"mean": 0.6688330136199999,
"stddev": 0.09554912279235006,
"median": 0.63446298092,
"user": 0.32117211999999995,
"system": 1.2801084799999998,
"min": 0.62781756392,
"max": 0.93924617692,
"times": [
0.6307734139200001,
0.6310989139200001,
0.62781756392,
0.65523359392,
0.63413688292,
0.93924617692,
0.65544645392,
0.64648521792,
0.63478907892,
0.63330283992
]
},
{
"command": "pnpr@main",
"mean": 0.6581819388200001,
"stddev": 0.033976976266682196,
"median": 0.64517786992,
"user": 0.33403762,
"system": 1.28642038,
"min": 0.6398389169200001,
"max": 0.75271298592,
"times": [
0.66542534692,
0.6445540629200001,
0.64430122292,
0.64580167692,
0.6496136599200001,
0.64433699592,
0.65235408792,
0.64288043192,
0.6398389169200001,
0.75271298592
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.9707204715600004,
"stddev": 0.028161123241876404,
"median": 2.96450905586,
"user": 1.7484544200000003,
"system": 1.9438605599999998,
"min": 2.9319246363600002,
"max": 3.03001361636,
"times": [
2.9319246363600002,
2.97949693236,
2.9912712523600002,
2.94485903536,
2.9569843823600004,
2.96111834436,
2.98994601936,
2.96789976736,
3.03001361636,
2.9536907293600003
]
},
{
"command": "pacquet@main",
"mean": 3.0157092494599995,
"stddev": 0.031797122880824924,
"median": 3.0214409033600003,
"user": 1.75973112,
"system": 1.9970821599999997,
"min": 2.95749410336,
"max": 3.05193243136,
"times": [
3.01059789836,
3.00817969936,
3.03228390836,
3.0422934763600002,
3.04121510936,
2.95749410336,
3.00017736636,
3.05193243136,
3.04015804436,
2.97276045736
]
},
{
"command": "pnpr@HEAD",
"mean": 0.6485104511600002,
"stddev": 0.013063665761500485,
"median": 0.64557134486,
"user": 0.32311612,
"system": 1.3007218599999997,
"min": 0.6328051423600001,
"max": 0.6727941453600002,
"times": [
0.6385260073600001,
0.64652425036,
0.63772199236,
0.6477807173600001,
0.6669768353600001,
0.6446184393600001,
0.6727941453600002,
0.6565146473600001,
0.6328051423600001,
0.64084233436
]
},
{
"command": "pnpr@main",
"mean": 0.6707623388600001,
"stddev": 0.03845579819583998,
"median": 0.6604325633600001,
"user": 0.33773911999999995,
"system": 1.2736264599999998,
"min": 0.6310334393600001,
"max": 0.7712253373600001,
"times": [
0.6551765593600001,
0.6615494203600001,
0.6593157063600001,
0.6704903743600001,
0.6310334393600001,
0.6676540413600001,
0.68831212636,
0.64386173036,
0.6590046533600001,
0.7712253373600001
]
}
]
} |
|
| Branch | pr/12431 |
| Testbed | pacquet |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result milliseconds (ms) (Result Δ%) | Upper Boundary milliseconds (ms) (Limit %) |
|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 4,143.12 ms(+0.73%)Baseline: 4,113.05 ms | 4,935.66 ms (83.94%) |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot 🚷 view threshold | 2,970.72 ms(+1.43%)Baseline: 2,928.79 ms | 3,514.55 ms (84.53%) |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 1,326.15 ms(+4.77%)Baseline: 1,265.79 ms | 1,518.95 ms (87.31%) |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 4,196.65 ms(+7.15%)Baseline: 3,916.65 ms | 4,699.97 ms (89.29%) |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 606.83 ms(-1.52%)Baseline: 616.20 ms | 739.44 ms (82.07%) |
|
| Branch | pr/12431 |
| Testbed | pnpr |
⚠️ WARNING: No Threshold found!Without a Threshold, no Alerts will ever be generated.
Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the--ci-only-thresholdsflag.
Click to view all benchmark results
| Benchmark | Latency | milliseconds (ms) |
|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot | 2,150.30 ms |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot | 648.51 ms |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot | 668.83 ms |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot | 2,216.90 ms |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot | 668.49 ms |
…ng, append-only flag - Emit `pnpm:execution-time` on the up-to-date fast path too, so the `Done in ...` footer shows there as well as on the full install path. - Throttle high-volume progress redraws (200ms in place, 1s append-only), mirroring pnpm's `throttleProgress`; non-progress events still render immediately and the final frame is always forced. - Add `--reporter=append-only`, forcing append-only rendering on a TTY, matching pnpm's reporter values. - Borrow in the `is_install_family` `matches!` and clarify the `contains_path` doc (it intentionally mirrors pnpm's substring `String.includes`, not segment matching).
|
Code review by qodo was updated up to the latest commit 5332dcd |
|
Addressed the review in
Declined, with rationale in-thread:
Written by an agent (Claude Code, claude-opus-4-8). |
…ct deps `link_one_importer` emitted a `pnpm:root added` event for every direct dependency it symlinked, including ones whose symlink already pointed at the target. With the default reporter on, that made `pacquet add <new>` list every already-installed dependency in the install summary instead of only the new one. pnpm skips the event for reused symlinks (`if ((await symlinkDependency(...)).reused) return` in linkDirectDeps.ts). Thread the `reused` flag that `force_symlink_dir` already returns out through `symlink_package` and gate the emit on it, so the summary lists only dependencies whose symlink was actually created this run.
|
Code review by qodo was updated up to the latest commit b801943 |
What
Adds a new
pacquet-default-reportercrate that renders the same terminal output as pnpm's@pnpm/cli.default-reporterforinstall/add/update/remove, and makes it pacquet's default reporter (it previously defaulted tosilent).Covered: the context block, the live
Progress: resolved/reused/downloaded/addedline,Packages: +X -Ywith the+++---bar,Already up to date, thedependencies:/devDependencies:diff summary, big-tarball download lines, lifecycle build-script output (color wheel,│indent, collapsing,└─ Done in), and aDone in …footer.How
The reporter is a
pacquet_reporter::Reportersink whose state lives behind a process-global mutex (the trait'semitis a static method). Each existingpnpm:*LogEventis folded into aReporterStatethat recomputes the terminal frame — this replaces pnpm's RxJS observable graph, which only exists because pnpm's reporter is a separate process reading a log stream. The frame model (fixed progress/footer blocks pinned below scrolling blocks) portsmergeOutputs.ts; each channel renderer ports the matchingreporterForClient/report*.ts.It renders in place on a TTY (cursor-up + clear redraw) and falls back to append-only output off-TTY, matching pnpm. Colors go through
owo-colors, gated soNO_COLORand non-TTY disable them like chalk. Terminal width comes from aTIOCGWINSZioctl. No new third-party dependencies —owo-colors,libc, andserde_jsonwere already in the workspace.A new
pnpm:execution-timechannel was added topacquet-reporterto drive the footer.Parity note
The footer reads
Done in … using pacquet v<version>rather thanusing pnpm v…— printing "pnpm" in pacquet's own output would misreport the tool identity. This is the only intentional text difference; everything else matches pnpm's output.Testing
LogEventsequences through the renderer and assert exact output (progress, stats bar, summary ordering, context, lifecycle, append-only, warning collapse, execution time).pacquet installproduces pnpm-matching frames both piped (append-only) and under a pty (in-place, colored).cargo fmt/clippy --all-targets/cargo doc -D warnings/Dylint (Perfectionist) andtypospass; reporter and CLIrun/init/installtest files pass.This is a pacquet-only change, so no changeset is included.
Written by an agent (Claude Code, claude-opus-4-8).
Summary by CodeRabbit
Release Notes
New Features
Behavior Changes
Bug Fixes
Tests