From 65ffb99fe12f01c8b8c6e179251ede9bec12ff56 Mon Sep 17 00:00:00 2001 From: Alex Z Date: Fri, 29 May 2026 19:47:10 -0700 Subject: [PATCH] fix(cli): suppress legacy task spinner when stdout is not a TTY MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v2.102.0 ported branches/secrets/orgs/etc. to TypeScript. The new textOutputLayer.task starts a @clack spinner after 200ms regardless of whether stdout is a TTY, so commands like `supabase branches list --output json` in CI emit cursor-hide and animated-frame ANSI to stdout ahead of the JSON, breaking jq parsing. Skip the spinner entirely when stdout is not a TTY — the animated frames are meaningless without a terminal, and stdout there is being consumed by a pipe / redirect / CI runner. Fixes #5397 --- apps/cli/src/shared/output/output.layer.ts | 14 ++++++++++++++ .../src/shared/output/output.layer.unit.test.ts | 15 +++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/apps/cli/src/shared/output/output.layer.ts b/apps/cli/src/shared/output/output.layer.ts index 1b16c86ed7..4312c0a5a0 100644 --- a/apps/cli/src/shared/output/output.layer.ts +++ b/apps/cli/src/shared/output/output.layer.ts @@ -181,6 +181,20 @@ export const textOutputLayer = Layer.effect( : Effect.sync(() => log.info(JSON.stringify(event))), task: (message: string) => Effect.sync(() => { + if (!tty.stdoutIsTty) { + // Non-TTY stdout (CI, pipe, redirect) — suppress the @clack spinner, + // which writes cursor-hide and animated-frame ANSI to stdout and + // pollutes machine-parsed output. See supabase/cli#5397. + const noop = () => Effect.void; + return { + message: noop, + succeed: noop, + fail: noop, + info: noop, + cancel: noop, + clear: noop, + }; + } let shown = false; let settled = false; let currentMessage = message; diff --git a/apps/cli/src/shared/output/output.layer.unit.test.ts b/apps/cli/src/shared/output/output.layer.unit.test.ts index 7592e67897..34299ffdb9 100644 --- a/apps/cli/src/shared/output/output.layer.unit.test.ts +++ b/apps/cli/src/shared/output/output.layer.unit.test.ts @@ -154,6 +154,21 @@ describe("Output", () => { }).pipe(Effect.provide(layer)), ); + it.effect("task suppresses the spinner entirely when stdout is not a TTY", () => + Effect.gen(function* () { + vi.useFakeTimers(); + const out = yield* Output; + const task = yield* out.task("Fetching branches..."); + vi.advanceTimersByTime(1000); + yield* task.clear(); + + expect(mockClack.spinnerFactory).not.toHaveBeenCalled(); + expect(mockClack.spinnerHandle.start).not.toHaveBeenCalled(); + }).pipe( + Effect.provide(textOutputLayer.pipe(Layer.provide(mockTty({ stdoutIsTty: false })))), + ), + ); + it.effect("task prefixes continuation lines for multiline completions", () => Effect.gen(function* () { vi.useFakeTimers();