Skip to content

CLI: route picospinner output to stderr so --format=json stays pipeable - #3219

Merged
nightnei merged 4 commits into
trunkfrom
fix-cli-spinner-stdout-leak
Apr 29, 2026
Merged

CLI: route picospinner output to stderr so --format=json stays pipeable#3219
nightnei merged 4 commits into
trunkfrom
fix-cli-spinner-stdout-leak

Conversation

@nightnei

@nightnei nightnei commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Related issues

Fixes STU-1637
Related to #3035 (ora → picospinner swap that introduced the regression)

How AI was used in this PR

Assisted with investigation and writing code. We had a few iterations, I thoroughly reviewed and tested it.

Proposed Changes

ora writes to stderr by default. picospinner hard-codes process.stdout.write(...) inside its Renderer and exposes no option to change the stream. As a result, shipping wp-studio@1.7.9 started emitting spinner frames on stdout, alongside the JSON payload of commands like site list --format=json — breaking | jq and any scripted consumer.

Testing Instructions

  1. npm run cli:build
  2. node apps/cli/dist/cli/main.mjs site list --format=json | jq
  3. Assert that you don't see errors (previously you would get jq: parse error: Invalid numeric literal at line 1, column 3)
  4. node apps/cli/dist/cli/main.mjs site list
  5. Assert that still works well
picospinner hard-codes process.stdout.write in its Renderer, so spinner
frames leaked into the JSON payload of commands like `site list
--format=json`, breaking `| jq`. Wrap the shared renderer's render and
onComponentFinish methods to temporarily redirect stdout writes to
stderr. Regression introduced in #3035 (ora → picospinner) which shipped
in 1.7.9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nightnei nightnei self-assigned this Apr 23, 2026
@nightnei
nightnei requested review from a team, bcotrim and fredrikekelund April 23, 2026 20:33
@wpmobilebot

wpmobilebot commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

📊 Performance Test Results

Comparing 44eade0 vs trunk

app-size

Metric trunk 44eade0 Diff Change
App Size (Mac) 1454.57 MB 1492.16 MB +37.59 MB 🔴 2.6%

site-editor

Metric trunk 44eade0 Diff Change
load 1825 ms 1759 ms 66 ms 🟢 -3.6%

site-startup

Metric trunk 44eade0 Diff Change
siteCreation 8097 ms 8080 ms 17 ms ⚪ 0.0%
siteStartup 4956 ms 4948 ms 8 ms ⚪ 0.0%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff)

@fredrikekelund

Copy link
Copy Markdown
Contributor

I already mentioned this on Slack, but to connect the dots: I think a better solution would be to silence the Logger output when the --format json option is passed. We could pass an isSilent parameter to the Logger constructor to make the change easy.

@fredrikekelund fredrikekelund left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@nightnei had to persuade me that printing spinners to stderr is the right thing to do, but after seeing https://clig.dev/#the-basics, I was convinced 👍

This change is substantive enough that I think it almost warrants just switching back to ora… This works, though, so I'm fine with proceeding. Although I would encourage you to make an upstream contribution to picospinner, @nightnei. This could be a config property on the DisplayOptions object passed to the Spinner constructor.

As for the specifics of this PR, I suggest patching Spinner instead of renderer, to avoid touching private methods.

Comment thread apps/cli/index.ts Outdated
@@ -1,3 +1,4 @@
import 'cli/lib/picospinner-stderr-patch';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's move this to apps/cli/logger.ts

Comment thread apps/cli/lib/picospinner-stderr-patch.ts Outdated
Comment on lines +8 to +11
* Wrap the renderer's two write-emitting methods so any `process.stdout.write`
* calls they make are rerouted to stderr for the duration of the call. This
* avoids duplicating picospinner internals and survives upstream changes as
* long as `render` and `onComponentFinish` remain the only write paths.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we make my suggested change, let's also update this comment.

nightnei and others added 2 commits April 29, 2026 13:11
Co-authored-by: Fredrik Rombach Ekelund <fredrik@f26d.dev>
@nightnei
nightnei merged commit f2e6af3 into trunk Apr 29, 2026
10 checks passed
@nightnei
nightnei deleted the fix-cli-spinner-stdout-leak branch April 29, 2026 13:11
@nightnei

Copy link
Copy Markdown
Contributor Author

@fredrikekelund JFYI I have created PR to picospinner - tinylibs/picospinner#13

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants