Skip to content

feat(default-reporter): render pnpm-identical visual install output in pacquet#12431

Merged
zkochan merged 3 commits into
mainfrom
feat/pacquet-default-reporter
Jun 15, 2026
Merged

feat(default-reporter): render pnpm-identical visual install output in pacquet#12431
zkochan merged 3 commits into
mainfrom
feat/pacquet-default-reporter

Conversation

@zkochan

@zkochan zkochan commented Jun 15, 2026

Copy link
Copy Markdown
Member

What

Adds a new pacquet-default-reporter crate that renders the same terminal output as pnpm's @pnpm/cli.default-reporter for install / add / update / remove, and makes it pacquet's default reporter (it previously defaulted to silent).

Covered: the context block, the live Progress: resolved/reused/downloaded/added line, Packages: +X -Y with the +++--- bar, Already up to date, the dependencies: / devDependencies: diff summary, big-tarball download lines, lifecycle build-script output (color wheel, indent, collapsing, └─ Done in), and a Done in … footer.

How

The reporter is a pacquet_reporter::Reporter sink whose state lives behind a process-global mutex (the trait's emit is a static method). Each existing pnpm:* LogEvent is folded into a ReporterState that 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) ports mergeOutputs.ts; each channel renderer ports the matching reporterForClient/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 so NO_COLOR and non-TTY disable them like chalk. Terminal width comes from a TIOCGWINSZ ioctl. No new third-party dependenciesowo-colors, libc, and serde_json were already in the workspace.

A new pnpm:execution-time channel was added to pacquet-reporter to drive the footer.

Parity note

The footer reads Done in … using pacquet v<version> rather than using 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

  • 13 frame-level tests drive LogEvent sequences through the renderer and assert exact output (progress, stats bar, summary ordering, context, lifecycle, append-only, warning collapse, execution time).
  • Verified against the live registry: pacquet install produces 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) and typos pass; reporter and CLI run/init/install test 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

    • Introduced a pnpm-style default visual terminal reporter with progress coalescing, stats bars, dependency summaries, and colorized formatting.
    • Added per-command execution-time reporting after commands complete.
  • Behavior Changes

    • The default reporter is now enabled by default for improved out-of-the-box terminal output.
  • Bug Fixes

    • Prevented duplicate “root added” messages when direct-dependency symlinks are reused.
  • Tests

    • Added rendering/coalescing coverage for the new reporter and added a symlink reuse test.

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.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 15, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0)

Grey Divider


Action required

1. Unbounded lifecycle output buffering 🐞 Bug ⛨ Security
Description
The default reporter pushes every lifecycle stdio line into an in-memory Vec until the script exits,
so a dependency that prints large output can cause unbounded memory growth and potentially crash
pacquet (OOM). This is reachable during normal installs because lifecycle events are emitted per
output line and the DefaultReporter is now the default.
Code

pacquet/crates/default-reporter/src/state.rs[R736-739]

+            LifecycleMessage::Stdio { line, stdio, .. } => {
+                let formatted = self.format_indented_output(line, *stdio);
+                self.lifecycle.get_mut(key).unwrap().output.push(formatted);
+            }
Evidence
update_lifecycle_cache appends every stdio line into LifecycleEntry.output with no cap, while
lifecycle output is emitted one event per line by the executor’s spawn_line_pump, so a
large-output script drives unbounded allocations until Exit.

pacquet/crates/default-reporter/src/state.rs[709-757]
pacquet/crates/executor/src/lifecycle.rs[446-482]

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

## Issue description
`ReporterState` buffers lifecycle stdio output lines unboundedly in `LifecycleEntry.output` and only collapses at render time. A chatty/malicious lifecycle script can print arbitrarily many lines, causing the reporter to allocate unbounded memory and potentially OOM/crash.
### Issue Context
Lifecycle output is produced line-by-line by `spawn_line_pump`, which emits one `LifecycleMessage::Stdio` per line. The default reporter stores every formatted line until `Exit`.
### Fix Focus Areas
- pacquet/crates/default-reporter/src/state.rs[709-757]
- pacquet/crates/executor/src/lifecycle.rs[446-482]
### Suggested fix approach
- Replace `LifecycleEntry.output: Vec<String>` with a bounded buffer (e.g., `VecDeque<String>` + `total_lines: usize`).
- Keep only the last N lines (matching the UI’s “show last 10 lines” behavior) while incrementing `total_lines` for collapsed count.
- Decide behavior for non-zero exits:
- Option A (safe): still keep last N and show “(output truncated)” rather than attempting to print everything.
- Option B (full fidelity): stream full output to a temp file while keeping last N in memory; on failure, include a pointer/path to the file (or optionally print the file content up to a limit).
- Ensure the rendering logic uses `total_lines` (not buffer length) for the collapsed line count.

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


2. Command moved by matches! ✓ Resolved 🐞 Bug ≡ Correctness
Description
CliArgs::run uses matches!(command, ...) to compute is_install_family, which moves command
and makes the later match command { ... } a use-after-move (compile error). This blocks building
the CLI.
Code

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

+        let started_at = now_millis();
+        let is_install_family = matches!(
+            command,
+            CliCommand::Add(_)
+                | CliCommand::Update(_)
+                | CliCommand::Remove(_)
+                | CliCommand::Install(_)
+                | CliCommand::Dlx(_),
+        );
Evidence
matches!(command, ...) is computed before match command { ... }, so command must remain
available. The current code matches by value and then later dispatches on command again.

pacquet/crates/cli/src/cli_args.rs[199-215]
pacquet/crates/cli/src/cli_args.rs[262-299]

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

## Issue description
`matches!(command, ...)` consumes `command` (non-`Copy`), but `command` is used later in `match command { ... }`, causing a Rust ownership error.
### Issue Context
`CliArgs::run(self, ...)` owns `command` and later dispatches on it. `matches!` expands to a `match` over the expression, so it will move unless you match on a reference.
### Fix
Change to `matches!(&command, ...)` (or equivalent) so `command` is only borrowed.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args.rs[206-216]

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



Remediation recommended

3. Symlink warning not surfaced 🐞 Bug ◔ Observability
Description
symlink_package now returns ForceSymlinkOutcome (including a warning when an existing file/dir
had to be moved aside), but the callers ignore outcome.warning, so pacquet can rename/move
occupants silently. This hides important diagnostics about surprising filesystem mutations (and
intended pnpm parity behavior).
Code

pacquet/crates/package-manager/src/symlink_direct_dependencies.rs[R439-455]

+        let outcome = symlink_package(target, &modules_dir.join(name_str)).map_err(|source| {
           SymlinkDirectDependenciesError::SymlinkPackage {
               importer_id: importer_id.to_string(),
               name: name_str.clone(),
               source,
           }
       })?;

+        // Only a freshly-created symlink is a `pnpm:root added`. A symlink
+        // already pointing at the target is "reused", and pnpm skips the
+        // event for it (`if ((await symlinkDependency(...)).reused) return`
+        // at linkDirectDeps.ts:127-129), so a `pacquet add <new>` summary
+        // lists only the new dependency, not every already-linked one.
+        if outcome.reused {
+            return Ok(());
+        }
+
Evidence
force_symlink_dir is designed to return a user-facing warning when it had to move aside a
non-symlink occupant, but the updated call sites only inspect reused (or drop the outcome
entirely), so that warning can never reach any reporter output.

pacquet/crates/fs/src/symlink_dir.rs[142-154]
pacquet/crates/fs/src/symlink_dir.rs[230-262]
pacquet/crates/package-manager/src/symlink_direct_dependencies.rs[436-455]
pacquet/crates/package-manager/src/create_symlink_layout.rs[57-79]

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

## Issue description
`pacquet_fs::force_symlink_dir` returns `ForceSymlinkOutcome { reused, warning }`, where `warning` is set when a non-symlink occupant had to be moved/removed to create the symlink. The updated `symlink_package` API now returns this outcome, but higher-level linkers drop `warning`, so users never learn that pacquet renamed something to `.ignored_*`.
### Issue Context
- `ForceSymlinkOutcome.warning` is explicitly documented as user-facing and is populated when an occupant is moved aside.
- `symlink_direct_dependencies` already has a `Reporter` generic and emits other user-facing events, so it’s the natural place to surface this.
### Fix Focus Areas
- pacquet/crates/package-manager/src/symlink_direct_dependencies.rs[439-455]
### Suggested fix
1. After `symlink_package(...)` returns `outcome`, if `outcome.warning.is_some()`, emit a warning-level reporter event (e.g. `LogEvent::Pnpm(PnpmLog { level: LogLevel::Warn, message: warning, prefix: prefix.clone() })`) so it appears in both the default reporter and `--reporter=ndjson`.
2. Keep the existing `outcome.reused` logic as-is (still skip `pnpm:root added` when reused).

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


4. Misleading approve-builds guidance 🐞 Bug ≡ Correctness
Description
The default reporter’s ignored-scripts message instructs users to run pnpm approve-builds, but
pacquet has no such CLI subcommand/interactive flow. This produces incorrect remediation guidance in
a user-facing warning path.
Code

pacquet/crates/default-reporter/src/state.rs[R846-853]

+    fn on_ignored_scripts(&mut self, log: &IgnoredScriptsLog) {
+        if log.package_names.is_empty() {
+            return;
+        }
+        let list = log.package_names.join(", ");
+        self.push_block(format!(
+            "Ignored build scripts: {list}.\nRun \"pnpm approve-builds\" to pick which dependencies should be allowed to run scripts.",
+        ));
Evidence
The reporter hard-codes a pnpm approve-builds instruction, but the pacquet CLI does not expose an
approve-builds command and explicitly notes that the interactive approve-builds flow is not
ported.

pacquet/crates/default-reporter/src/state.rs[846-853]
pacquet/crates/cli/src/cli_args.rs[117-148]
pacquet/crates/cli/src/cli_args/dlx.rs[33-40]
pacquet/crates/config/src/lib.rs[1001-1014]

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

## Issue description
The default reporter currently prints: `Run "pnpm approve-builds" ...` when build scripts are ignored. Pacquet does not implement `approve-builds`, so the message is not actionable.
### Issue Context
- Pacquet already models script allow/deny via config (`allow_builds` / `dangerously_allow_all_builds`).
- Pacquet CLI subcommands do not include `approve-builds`, and docs indicate the interactive prompt was not ported.
### Fix Focus Areas
- pacquet/crates/default-reporter/src/state.rs[846-854]
### Suggested fix
Update the message to a pacquet-actionable remediation, e.g.:
- Point users to configuring `pnpm-workspace.yaml` (`pnpm.allowBuilds` / `dangerouslyAllowAllBuilds`) or the relevant pacquet config mechanism.
- Avoid mentioning `pnpm approve-builds` unless pacquet actually supports it.

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


5. No terminal resize handling 🐞 Bug ☼ Reliability
Description
The in-place TTY renderer captures terminal column width once in Sink::new and then uses that
cached value for soft-wrap row counting/cursor-up behavior, so resizing the terminal mid-command can
corrupt redraw and leave stray lines. Refresh columns/width periodically (e.g., before each frame
write) and adjust prev_rows when the width changes.
Code

pacquet/crates/default-reporter/src/lib.rs[R104-116]

+    fn new() -> Self {
+        let is_tty = std::io::stdout().is_terminal();
+        let append_only = !is_tty || FORCE_APPEND_ONLY.get().copied().unwrap_or(false);
+        let columns = if is_tty { terminal_columns().unwrap_or(80) } else { 80 };
+        // pnpm's `outputMaxWidth`: `columns - 2` on a TTY, else 80.
+        let width = if is_tty { columns.saturating_sub(2) } else { 80 };
+        let colors = Colors { enabled: is_tty && std::env::var_os("NO_COLOR").is_none() };
+        let state = ReporterState::new(cwd(), width, colors, append_only);
+        // pnpm's `throttleProgress`: 200ms in place, 1000ms append-only.
+        let throttle =
+            if append_only { Duration::from_secs(1) } else { Duration::from_millis(200) };
+        Sink { state, columns, prev_rows: 0, throttle, last_write: None }
+    }
Evidence
Sink::new computes and stores a single columns value, and write_output derives prev_rows
from that cached width for cursor movement; there is no later refresh path, so terminal resizes can
desynchronize wrapping vs cursor-up math.

pacquet/crates/default-reporter/src/lib.rs[103-172]

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

## Issue description
`Sink` caches `columns`/`width` once at startup. If the user resizes the terminal while an install is running, `count_rows` will compute cursor-up moves using stale width and the reporter will redraw incorrectly.
### Issue Context
`Sink::new` sets `columns` and `width` once; later `write_output` uses `self.columns` for `count_rows` and the `ReporterState` continues to use the original `width` for truncation/layout.
### Fix Focus Areas
- pacquet/crates/default-reporter/src/lib.rs[103-172]
### Suggested fix approach
- On each `Output::Frame` write (TTY only), re-query `terminal_columns()`.
- If the value changed:
- update `self.columns`
- update the `ReporterState` width (add a method like `state.set_width(new_width)`), and/or reconstruct `ReporterState`’s width-dependent rendering parameters
- consider resetting `prev_rows` to 0 (or force a full clear) to avoid cursor-up based on old wrap rules.
- Keep the non-TTY/append-only path unchanged.

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


View more (4)
6. Non-monotonic execution timing 🐞 Bug ≡ Correctness
Description
The new Done in ... footer duration is derived from Unix-epoch timestamps taken from SystemTime,
so clock adjustments (NTP, VM suspend/resume) can make the displayed duration incorrect (including
collapsing to 0 via saturating_sub). Use a monotonic Instant for elapsed time and set `ended_at
= started_at + elapsed_ms` to preserve the epoch-shaped wire fields while keeping duration stable.
Code

pacquet/crates/cli/src/cli_args.rs[R507-509]

+fn now_millis() -> u128 {
+    std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_millis())
+}
Evidence
CLI timestamps are taken from SystemTime/UNIX_EPOCH, and the default reporter displays duration
by subtracting them; this subtraction is sensitive to wall-clock jumps.

pacquet/crates/cli/src/cli_args.rs[507-509]
pacquet/crates/default-reporter/src/state.rs[952-958]

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

## Issue description
`ExecutionTimeLog` uses epoch-millis derived from `SystemTime` at both start and end. If the system clock changes during the command, the computed duration can be wrong.
### Issue Context
The reporter computes elapsed as `ended_at.saturating_sub(started_at)` and formats it for the footer.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args.rs[160-192]
- pacquet/crates/cli/src/cli_args.rs[216-437]
- pacquet/crates/cli/src/cli_args.rs[507-509]
- pacquet/crates/default-reporter/src/state.rs[952-958]
### Suggested fix approach
- Record both:
- `started_at_epoch = now_millis()` (for wire shape)
- `started_instant = Instant::now()` (monotonic)
- At the end, compute `elapsed_ms = started_instant.elapsed().as_millis()` and set:
- `ended_at = started_at_epoch + elapsed_ms`
- Emit `ExecutionTimeLog { started_at: started_at_epoch, ended_at }`.
- Apply the same approach for the fast-path emission to keep behavior consistent.

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


7. Fast path skips Done footer ✓ Resolved 🐞 Bug ≡ Correctness
Description
The install up-to-date fast path returns from main() before CliArgs::run emits
pnpm:execution-time, so DefaultReporter never prints the Done in ... using pacquet v... footer
on that path. This creates inconsistent output vs the non-fast-path and pnpm parity expectations.
Code

pacquet/crates/cli/src/cli_args.rs[R177-180]

+        pacquet_default_reporter::set_cwd(dir.to_string_lossy().into_owned());
+        let emit = reporter_emit(self.reporter);
   install_args.finished_via_up_to_date_fast_path(&dir, &config, emit)
}
Evidence
main() exits immediately on fast-path success. The fast-path implementation emits only Pnpm and
Summary, while the footer is rendered only when ExecutionTime is handled by the default
reporter.

pacquet/crates/cli/src/lib.rs[14-31]
pacquet/crates/cli/src/cli_args.rs[161-180]
pacquet/crates/cli/src/cli_args/install.rs[276-322]
pacquet/crates/default-reporter/src/state.rs[952-959]

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

## Issue description
When `finished_via_install_fast_path()` succeeds, `main()` returns early and the new end-of-command `pnpm:execution-time` emit site in `CliArgs::run()` is never reached, so the default reporter footer is missing.
### Issue Context
- Fast path currently emits only `pnpm` (Already up to date) + `pnpm:summary`.
- `pacquet-default-reporter` renders the footer exclusively from `LogEvent::ExecutionTime`.
### Fix
Emit `LogEvent::ExecutionTime` on the fast-path-success case too (using the selected reporter sink), e.g.:
- capture `started_at` before running the fast path and emit `ExecutionTimeLog { started_at, ended_at: now_millis() }` right before returning `Ok(())`, or
- have `finished_via_install_fast_path` accept a `started_at` and emit on success.
### Fix Focus Areas
- pacquet/crates/cli/src/lib.rs[22-31]
- pacquet/crates/cli/src/cli_args.rs[161-180]
- pacquet/crates/cli/src/cli_args/install.rs[276-322]

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


8. Unthrottled frame redraws ✓ Resolved 🐞 Bug ➹ Performance
Description
DefaultReporter recomputes and flushes output on every LogEvent, including per-package
pnpm:progress events, with no throttling/coalescing. Compared to pnpm’s explicit
throttleProgress, this can create significant extra CPU + terminal I/O on large installs.
Code

pacquet/crates/default-reporter/src/lib.rs[R62-67]

+impl Reporter for DefaultReporter {
+    fn emit(event: &LogEvent) {
+        let mut sink = SINK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
+        let output = sink.state.handle(event);
+        sink.write(output);
+    }
Evidence
Pacquet’s progress channel is per-package and therefore high-frequency. The new default reporter
renders/flushes per event, while pnpm’s reporter explicitly throttles progress updates before
rendering.

pacquet/crates/default-reporter/src/lib.rs[62-111]
pacquet/crates/default-reporter/src/state.rs[291-339]
pacquet/crates/reporter/src/lib.rs[68-74]
pnpm/src/reporter/index.ts[18-54]
cli/default-reporter/src/reporterForClient/reportProgress.ts[61-72]

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

## Issue description
The reporter updates synchronously per event (render + write + flush). `pnpm:progress` is per-package and high-volume; pnpm’s default reporter intentionally throttles progress rendering to avoid overwhelming the terminal.
### Issue Context
- pnpm sets `throttleProgress` (200ms on TTY, 1000ms append-only) and applies `throttleTime()` to the progress stream.
- The Rust reporter currently renders the full frame in `finish()` and the sink flushes on every emit.
### Fix
Introduce time-based throttling/coalescing for frame writes (especially progress updates), e.g.:
- track `last_render_at: Instant` in the sink/state,
- always fold events into state, but return `Output::None` (skip write) if called sooner than the throttle interval and the event is not “terminal” (e.g. Stage::ImportingDone, Summary, ExecutionTime, Lifecycle Exit),
- ensure a final render is forced at command end (ExecutionTime) and possibly at stage transitions.
### Fix Focus Areas
- pacquet/crates/default-reporter/src/lib.rs[62-111]
- pacquet/crates/default-reporter/src/state.rs[291-339]

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


9. Unsanitized lifecycle terminal output 🐞 Bug ⛨ Security
Description
DefaultReporter prints lifecycle script and stdio line strings directly to the terminal
without stripping control/escape sequences; with the reporter now default, untrusted dependency
output can inject ANSI/OSC sequences into the user’s terminal. This can spoof output, hide
warnings/errors, or manipulate terminal state.
Code

pacquet/crates/default-reporter/src/state.rs[R796-842]

+    fn stream_lifecycle(&mut self, message: &LifecycleMessage) -> String {
+        let (stage, _dep_path, wd) = lifecycle_ids(message);
+        let prefix = self.lifecycle_prefix(wd, stage);
+        match message {
+            LifecycleMessage::Exit { exit_code, .. } => {
+                if *exit_code == 0 {
+                    format!("{prefix}: Done")
+                } else {
+                    format!("{prefix}: Failed")
+                }
+            }
+            LifecycleMessage::Script { script, .. } => format!("{prefix}$ {script}"),
+            LifecycleMessage::Stdio { line, stdio, .. } => {
+                let line = match stdio {
+                    LifecycleStdio::Stderr => self.colors.grey(line),
+                    LifecycleStdio::Stdout => line.clone(),
+                };
+                format!("{prefix}: {line}")
+            }
+        }
+    }
+
+    fn lifecycle_prefix(&mut self, wd: &str, stage: &str) -> String {
+        let idx = if let Some(idx) = self.lifecycle_colors.get(wd) {
+            *idx
+        } else {
+            let idx = self.color_wheel % COLOR_WHEEL.len();
+            self.lifecycle_colors.insert(wd.to_string(), idx);
+            self.color_wheel += 1;
+            idx
+        };
+        let painted = COLOR_WHEEL[idx](&self.colors, &format_prefix(&self.cwd, wd));
+        format!("{painted} {}", self.colors.cyan_bright(stage))
+    }
+
+    fn format_indented_status(&self, status: &str) -> String {
+        format!("{} {status}", self.colors.magenta_bright("└─"))
+    }
+
+    fn format_indented_output(&self, line: &str, stdio: LifecycleStdio) -> String {
+        let cut = cut_line(line, self.width as isize - 2);
+        let line = match stdio {
+            LifecycleStdio::Stderr => self.colors.grey(&cut),
+            LifecycleStdio::Stdout => cut,
+        };
+        format!("{} {line}", self.colors.magenta_bright("│"))
+    }
Evidence
The log schema explicitly defines lifecycle stdio as raw script output, and the new reporter
interpolates that raw text into terminal output. Since the reporter is now the default, this attack
surface is enabled for most users by default.

pacquet/crates/cli/src/cli_args.rs[71-73]
pacquet/crates/reporter/src/lib.rs[548-569]
pacquet/crates/default-reporter/src/state.rs[796-842]

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

## Issue description
Lifecycle output (`pnpm:lifecycle`) is derived from spawned scripts’ stdout/stderr. The default reporter currently forwards this text directly into terminal output, allowing terminal control-sequence injection.
### Issue Context
`LifecycleMessage::Stdio.line` is documented as the raw output line from the spawned script. The reporter formats it into the frame without escaping/sanitization.
### Fix
Before rendering lifecycle `script` and `line` text, sanitize untrusted strings by removing (or escaping) ASCII control characters and common ANSI/OSC escape sequences (at minimum `\x1b` + following control sequence), while preserving printable text. Consider gating this behind a compatibility flag if strict pnpm parity is required.
### Fix Focus Areas
- pacquet/crates/default-reporter/src/state.rs[796-842]
- pacquet/crates/reporter/src/lib.rs[548-569]

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


Grey Divider

Qodo Logo

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c9656b7d-bbb0-4416-ba57-cb82c017449a

📥 Commits

Reviewing files that changed from the base of the PR and between 5332dcd and b801943.

📒 Files selected for processing (4)
  • pacquet/crates/package-manager/src/create_symlink_layout.rs
  • pacquet/crates/package-manager/src/symlink_direct_dependencies.rs
  • pacquet/crates/package-manager/src/symlink_direct_dependencies/tests.rs
  • pacquet/crates/package-manager/src/symlink_package.rs

📝 Walkthrough

Walkthrough

A new pacquet-default-reporter crate is introduced implementing a pnpm-style visual terminal reporter with ANSI color utilities, string/path formatting helpers, and a ReporterState event-folding renderer. The LogEvent enum gains an ExecutionTime variant. The CLI's default reporter changes from Silent to Default, and command dispatch is refactored around a new InstallPipeline struct. Symlink outcome tracking is refined to prevent duplicate event emission when symlinks are reused.

Changes

Default Reporter and Event Handling Refinement

Layer / File(s) Summary
ExecutionTime event contract
pacquet/crates/reporter/src/lib.rs
LogEvent gains ExecutionTime(ExecutionTimeLog) for pnpm:execution-time; ExecutionTimeLog struct adds started_at/ended_at with camelCase serialization.
Default reporter crate setup, colors, and format utilities
pacquet/crates/default-reporter/Cargo.toml, pacquet/crates/default-reporter/src/colors.rs, pacquet/crates/default-reporter/src/format.rs
Creates the crate manifest with libc, owo-colors, serde_json dependencies; implements Colors struct with conditional ANSI styling; adds formatting helpers for bytes, durations, ANSI-aware visible width, path normalization, prefix trimming, gutter rendering, and path containment.
ReporterState event-folding renderer
pacquet/crates/default-reporter/src/state.rs
Implements ReporterState with Output enum and Frame/BlockSlot model; handle dispatcher routes all LogEvent variants to handlers for context, progress, tarball downloads, stats, dependency summaries, lifecycle scripts, warnings, config-deps, lockfile verification, pnpm log passthrough, and execution-time footers.
DefaultReporter global Sink and TTY rendering
pacquet/crates/default-reporter/src/lib.rs
Implements DefaultReporter with global Mutex<Sink>, TTY detection, ioctl-based terminal width, ANSI cursor-control frame redraws vs. append-only output, soft-wrap row counting, event coalescing for progress events, and set_cwd/set_package_version configuration functions.
CLI wiring and InstallPipeline refactor
Cargo.toml, pacquet/crates/cli/Cargo.toml, pacquet/crates/cli/src/cli_args.rs, pacquet/crates/cli/src/lib.rs
Registers workspace dependency; changes --reporter default to Default; seeds CWD and package version before events; refactors Add/Update/Remove/Dlx/Install dispatch to Box::pin(...).await? with reporter generics; introduces InstallPipeline struct, reporter_emit helper, now_millis, and execution-time footer emission for install-family commands.
ReporterState render tests and coalescing unit tests
pacquet/crates/default-reporter/tests/render.rs, pacquet/crates/default-reporter/src/tests.rs, pacquet/crates/cli/tests/run.rs
Adds frame-level integration tests for all ReporterState rendering paths (progress, stats, summary, context, lifecycle, warnings, append-only, execution-time footer) and unit tests for event coalescing; updates CLI test comment to reflect new default reporter behavior.
Symlink outcome tracking to prevent duplicate events
pacquet/crates/package-manager/src/symlink_package.rs, pacquet/crates/package-manager/src/symlink_direct_dependencies.rs, pacquet/crates/package-manager/src/create_symlink_layout.rs, pacquet/crates/package-manager/src/symlink_direct_dependencies/tests.rs
Updates symlink_package to return ForceSymlinkOutcome instead of (), allowing detection of symlink reuse; adds early-return in symlink_direct_dependencies when a symlink is reused to skip redundant pnpm:root added event emission; includes test validating no added events are emitted on subsequent runs for reused symlinks.

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…"
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

  • pnpm/pnpm#11633: Implements the visual default reporter for terminal output specified in the Rust Roadmap's Stage 1 Tier 4 item, introducing the new pacquet-default-reporter crate with state-based rendering, formatting, and color support to achieve visual parity with pnpm's install output.

Possibly related PRs

  • pnpm/pnpm#12024: Both PRs modify pacquet/crates/package-manager/src/symlink_direct_dependencies.rs to refine logic around direct-dependency symlink creation and pnpm:root added event emission.

Suggested labels

area: cli/dlx

Suggested reviewers

  • KSXGitHub

Poem

🐇 A brand new reporter has arrived,
With colors and frames, our terminal's revived!
Done in X ms — the footer now shows,
Through TTY or pipe, the progress bar glows.
From Silent to Default, the rabbit rejoiced,
With owo_colors bright, our logs have a voice! 🎨

🚥 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 accurately summarizes the main change: introducing a new default reporter that renders pnpm-identical visual install output.
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 feat/pacquet-default-reporter

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.

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

Copy link
Copy Markdown

PR Summary by Qodo

Add pnpm-style default reporter and make it pacquet’s default output
✨ Enhancement 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

Walkthroughs

Description
• 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.
Diagram
graph 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
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Make `Reporter` instance-based (no global mutex)
  • ➕ Avoids process-global state and mutex contention/poisoning edge cases
  • ➕ Allows multiple independent reporters (e.g., per command or per workspace) more naturally
  • ➕ Enables dependency injection for output streams (better testing and embedding)
  • ➖ Requires a breaking change across the workspace where Reporter::emit is assumed static
  • ➖ More plumbing through async install pipelines and hooks to carry a reporter instance
  • ➖ Bigger migration than needed to land pnpm-parity output quickly
2. Use a terminal control library (e.g., crossterm) for redraw/size
  • ➕ More portable terminal sizing and cursor control across platforms
  • ➕ Can simplify low-level ANSI handling and reduce custom ioctl logic
  • ➖ Introduces new third-party dependencies (explicitly avoided here)
  • ➖ May not match pnpm’s exact output semantics without additional work
  • ➖ Potentially increases binary size and maintenance surface
3. Model pnpm’s separate-process reporter (spawn + stream)
  • ➕ Closer architectural parity with pnpm’s observable-driven pipeline
  • ➕ Isolates rendering from command execution (robustness if renderer fails)
  • ➖ More operational complexity (IPC, lifecycle, error propagation)
  • ➖ Harder to keep ordering/flush semantics correct under concurrency
  • ➖ Overkill for an in-process Rust CLI with an existing event model

Recommendation: Given the existing Reporter trait shape (static emit) and the explicit goal of pnpm-identical UX without new dependencies, the PR’s approach—folding pnpm:* events into a mutex-guarded in-process state machine and rendering either in-place (TTY) or append-only (non-TTY)—is the most pragmatic path. The main follow-up worth considering is a longer-term refactor toward an instance-based reporter API to eliminate global state and make embedding/testability even cleaner, but that’s appropriately out of scope for landing parity output now.

Grey Divider

File Changes

Enhancement (7)
cli_args.rs Make 'default' reporter the CLI default and emit execution-time footer +135/-67

Make 'default' reporter the CLI default and emit execution-time footer

• Adds a 'Default' reporter type and makes it the '--reporter' default, while keeping 'ndjson' and 'silent'. Introduces a reporter-dispatch helper ('reporter_emit'), seeds the default reporter with cwd and command start time, emits 'pnpm:execution-time' after install-family commands, and refactors install into an 'InstallPipeline' to reduce duplicated generic wiring.

pacquet/crates/cli/src/cli_args.rs


lib.rs Seed default reporter with pacquet version before first log event +3/-0

Seed default reporter with pacquet version before first log event

• Initializes 'pacquet-default-reporter' with the pacquet version early so fast-path installs can still render the correct 'Done in … using pacquet v…' footer.

pacquet/crates/cli/src/lib.rs


colors.rs Add chalk-like color palette wrapper over 'owo-colors' +51/-0

Add chalk-like color palette wrapper over 'owo-colors'

• Implements a 'Colors' helper that gates styling behind an 'enabled' flag (TTY + 'NO_COLOR' semantics). Includes a pnpm-matching '[WARN]' label style and named helpers corresponding to pnpm/chalk usage.

pacquet/crates/default-reporter/src/colors.rs


format.rs Port pnpm reporter formatting utilities (bytes, ms, truncation, paths) +193/-0

Port pnpm reporter formatting utilities (bytes, ms, truncation, paths)

• Adds helpers equivalent to pnpm’s JS utilities: pretty-bytes/pretty-ms formatting, visible width skipping ANSI escapes, truncation for lifecycle lines, and path normalization/relative/zooming helpers used by frame renderers.

pacquet/crates/default-reporter/src/format.rs


lib.rs Implement 'DefaultReporter' sink with TTY redraw + append-only fallback +143/-0

Implement 'DefaultReporter' sink with TTY redraw + append-only fallback

• Introduces the 'DefaultReporter' implementing 'pacquet_reporter::Reporter' using a process-global mutex-protected sink/state. Detects TTY and terminal width, controls ANSI cursor movement to redraw frames in place, and exposes 'set_cwd' / 'set_package_version' one-time initialization hooks.

pacquet/crates/default-reporter/src/lib.rs


state.rs Add event-folding state machine that renders pnpm-like frames +1056/-0

Add event-folding state machine that renders pnpm-like frames

• Implements 'ReporterState' that folds 'LogEvent's into a frame model with scrolling blocks and pinned fixed blocks (progress/footer). Renders context, live progress counts, big-tarball downloads, packages +/- bar, dependency diff summary, lifecycle script output (including collapsing rules), warning collapsing after five, and the execution-time footer.

pacquet/crates/default-reporter/src/state.rs


lib.rs Add 'pnpm:execution-time' log event and payload type +20/-0

Add 'pnpm:execution-time' log event and payload type

• Extends the 'LogEvent' enum with 'ExecutionTime' and adds the corresponding 'ExecutionTimeLog' struct using pnpm’s camelCase wire shape. Enables reporters to render a command-level 'Done in …' footer.

pacquet/crates/reporter/src/lib.rs


Tests (2)
run.rs Update run test expectations for non-silent default reporter +4/-5

Update run test expectations for non-silent default reporter

• Adjusts documentation/comments in the test to reflect that the default reporter is now non-silent, and only '--reporter=silent' suppresses the stderr '$ <script>' echo.

pacquet/crates/cli/tests/run.rs


render.rs Add frame-level tests asserting pnpm-equivalent output rendering +318/-0

Add frame-level tests asserting pnpm-equivalent output rendering

• Adds targeted tests that drive sequences of 'LogEvent's through 'ReporterState' and assert exact rendered frames/lines across progress, stats bar, summary grouping, context gating, execution-time footer, warning collapse behavior, append-only mode, and lifecycle indentation.

pacquet/crates/default-reporter/tests/render.rs


Other (3)
Cargo.toml Register 'pacquet-default-reporter' as a workspace crate +1/-0

Register 'pacquet-default-reporter' as a workspace crate

• Adds the new 'pacquet-default-reporter' crate path to the workspace members/dependencies so it can be used from other crates.

Cargo.toml


Cargo.toml Add CLI dependency on 'pacquet-default-reporter' +1/-0

Add CLI dependency on 'pacquet-default-reporter'

• Wires the CLI crate to depend on the new default reporter so it can be selected via '--reporter' and used as the new default.

pacquet/crates/cli/Cargo.toml


Cargo.toml Introduce new 'pacquet-default-reporter' crate manifest +25/-0

Introduce new 'pacquet-default-reporter' crate manifest

• Defines the new crate, depends only on existing workspace deps ('pacquet-reporter', 'libc', 'owo-colors', 'serde_json'), and adds snapshot/assertion test dependencies.

pacquet/crates/default-reporter/Cargo.toml


Grey Divider

Qodo Logo

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      7.3±0.34ms   595.3 KB/sec    1.03      7.5±0.38ms   580.4 KB/sec

Comment thread pacquet/crates/cli/src/cli_args.rs
Comment thread pacquet/crates/cli/src/cli_args.rs Outdated
Comment thread pacquet/crates/default-reporter/src/lib.rs
Comment thread pacquet/crates/default-reporter/src/state.rs
@codecov-commenter

codecov-commenter commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.22222% with 290 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.83%. Comparing base (b6826b7) to head (b801943).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/default-reporter/src/state.rs 71.96% 201 Missing ⚠️
pacquet/crates/default-reporter/src/format.rs 75.00% 32 Missing ⚠️
pacquet/crates/default-reporter/src/lib.rs 63.63% 32 Missing ⚠️
pacquet/crates/cli/src/cli_args.rs 79.12% 19 Missing ⚠️
pacquet/crates/default-reporter/src/colors.rs 58.33% 5 Missing ⚠️
...quet/crates/package-manager/src/symlink_package.rs 50.00% 1 Missing ⚠️
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.
📢 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.

@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: 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 lift

Add explicit append-only reporter mode to keep CLI parity with pnpm.

Line 101-Line 103 documents that append-only is not selectable, which makes --reporter=append-only diverge 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 win

Avoid 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

📥 Commits

Reviewing files that changed from the base of the PR and between b6826b7 and 3ce1550.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • Cargo.toml
  • pacquet/crates/cli/Cargo.toml
  • pacquet/crates/cli/src/cli_args.rs
  • pacquet/crates/cli/src/lib.rs
  • pacquet/crates/cli/tests/run.rs
  • pacquet/crates/default-reporter/Cargo.toml
  • pacquet/crates/default-reporter/src/colors.rs
  • pacquet/crates/default-reporter/src/format.rs
  • pacquet/crates/default-reporter/src/lib.rs
  • pacquet/crates/default-reporter/src/state.rs
  • pacquet/crates/default-reporter/tests/render.rs
  • pacquet/crates/reporter/src/lib.rs

Comment thread pacquet/crates/cli/src/cli_args.rs
Comment thread pacquet/crates/default-reporter/src/format.rs
@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.197 ± 0.140 4.015 4.372 1.97 ± 0.10
pacquet@main 4.179 ± 0.178 3.923 4.464 1.96 ± 0.11
pnpr@HEAD 2.217 ± 0.129 1.965 2.358 1.04 ± 0.07
pnpr@main 2.128 ± 0.076 2.054 2.293 1.00
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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 606.8 ± 11.3 590.0 630.8 1.00
pacquet@main 636.8 ± 20.0 613.6 674.2 1.05 ± 0.04
pnpr@HEAD 668.5 ± 23.0 645.1 711.1 1.10 ± 0.04
pnpr@main 711.1 ± 123.9 648.5 1056.3 1.17 ± 0.21
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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.143 ± 0.051 4.069 4.229 1.93 ± 0.12
pacquet@main 4.164 ± 0.045 4.096 4.245 1.94 ± 0.12
pnpr@HEAD 2.150 ± 0.128 2.007 2.397 1.00
pnpr@main 2.154 ± 0.114 2.018 2.352 1.00 ± 0.08
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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.326 ± 0.017 1.301 1.353 2.01 ± 0.11
pacquet@main 1.299 ± 0.034 1.260 1.383 1.97 ± 0.11
pnpr@HEAD 0.669 ± 0.096 0.628 0.939 1.02 ± 0.15
pnpr@main 0.658 ± 0.034 0.640 0.753 1.00
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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.971 ± 0.028 2.932 3.030 4.58 ± 0.10
pacquet@main 3.016 ± 0.032 2.957 3.052 4.65 ± 0.11
pnpr@HEAD 0.649 ± 0.013 0.633 0.673 1.00
pnpr@main 0.671 ± 0.038 0.631 0.771 1.03 ± 0.06
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
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12431
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
4,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%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12431
Testbedpnpr

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,150.30 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
648.51 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
668.83 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,216.90 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
668.49 ms
🐰 View full continuous benchmarking report in Bencher

…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).
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 15, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 5332dcd

@zkochan

zkochan commented Jun 15, 2026

Copy link
Copy Markdown
Member Author

Addressed the review in 5332dcd882:

  • Fast-path footer (Qodo Add one more layer in .store for prod vs dev (vs others?) #2): the up-to-date short-circuit now emits pnpm:execution-time, so Done in … renders there too.
  • Throttling (Qodo Question: Does it work with nvm? #3): progress redraws coalesce (200 ms in place / 1 s append-only); other events render immediately, final frame forced.
  • --reporter=append-only (CodeRabbit, outside-diff): added as an explicit value that forces append-only rendering even on a TTY, matching pnpm’s reporter values.
  • matches! borrow (Qodo option to for .store location #1): switched to &command (was a false-positive move).
  • contains_path doc (CodeRabbit): kept pnpm’s substring String.includes semantics, clarified the doc.

Declined, with rationale in-thread:

  • Lifecycle output sanitization (Qodo Possibly helpful? #4): pnpm forwards build-script output verbatim; stripping would diverge from the port’s source of truth.

Written by an agent (Claude Code, claude-opus-4-8).

Comment thread pacquet/crates/default-reporter/src/state.rs
Comment thread pacquet/crates/default-reporter/src/lib.rs
Comment thread pacquet/crates/cli/src/cli_args.rs
…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.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 15, 2026

Copy link
Copy Markdown

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

Comment thread pacquet/crates/package-manager/src/symlink_direct_dependencies.rs
Comment thread pacquet/crates/default-reporter/src/state.rs
@zkochan zkochan merged commit 2b81344 into main Jun 15, 2026
26 of 27 checks passed
@zkochan zkochan deleted the feat/pacquet-default-reporter branch June 15, 2026 21:54
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.

2 participants