From b16cee50789405e3d97c2344ce8446ad7b624b25 Mon Sep 17 00:00:00 2001 From: ivanmaierg Date: Wed, 13 May 2026 09:49:50 -0300 Subject: [PATCH] fix(cli): withLoading no-ops when stream is not a TTY MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isTTY=false is now an unconditional short-circuit, regardless of FORCE_COLOR. Painting spinner frames into a pipe corrupts downstream consumers (captured stdout, CI buffers, log files), so the spinner must never animate when the stream is not a real terminal. isSpinnerEnabled still honors FORCE_COLOR for callers that want to ask the question, but withLoading's stronger contract — documented in its own header comment — wins on the no-op path. --- src/cli/loading.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/cli/loading.ts b/src/cli/loading.ts index 949aaed..e1e1e68 100644 --- a/src/cli/loading.ts +++ b/src/cli/loading.ts @@ -34,6 +34,10 @@ export function isSpinnerEnabled(stream: NodeJS.WriteStream): boolean { // withLoading — wraps an async fn with a Braille spinner on stream (stderr). // TTY-false path: zero writes, zero overhead, fn result passed through unchanged. +// This is an UNCONDITIONAL no-op — we never paint a spinner into a pipe, +// regardless of FORCE_COLOR. Animated cursor returns into a non-TTY would +// corrupt downstream consumers (logs, captured stdout, CI buffers). +// TTY-true path: still honors isSpinnerEnabled so NO_COLOR / FORCE_COLOR=0 can suppress. // TTY-true path: initial frame written before interval, cleanup in finally. // Result transparency: never wraps, unwraps, or catches fn's value (R12). export async function withLoading( @@ -41,6 +45,9 @@ export async function withLoading( fn: () => Promise, options?: WithLoadingOptions, ): Promise { + if (stream.isTTY !== true) { + return fn(); + } if (!isSpinnerEnabled(stream)) { return fn(); }