Files
fusion/packages/cli/src/output.ts
gsxdsm b31bee03a8 FN-8576: add global quiet flag to CLI
Add a global quiet mode that suppresses informational CLI stdout without hiding requested results.

- Parse --quiet/-q and FUSION_QUIET with command and output exemptions.
- Route result output and interactive prompts around the reversible stdout gate.
- Document the flag and cover quiet output, prompts, and argument parsing.

Files changed:
 .changeset/fn-8576-cli-quiet-flag.md               |   7 ++
 docs/cli-reference.md                              |   8 ++
 packages/cli/src/__tests__/bin.test.ts             |  15 +++
 packages/cli/src/__tests__/cli-quiet-mode.test.ts  |  63 +++++++++++++
 .../__tests__/cli-quiet-prompt-surfaces.test.ts    |  33 +++++++
 packages/cli/src/bin.ts                            |  39 ++++++--
 packages/cli/src/commands/experiment-finalize.ts   |   3 +-
 packages/cli/src/commands/git.ts                   |  17 ++--
 packages/cli/src/commands/goals.ts                 |   4 +-
 packages/cli/src/commands/mission.ts               |   8 +-
 packages/cli/src/commands/node.ts                  |   4 +-
 packages/cli/src/commands/onboard.ts               |   4 +-
 packages/cli/src/commands/org-import.ts            |   5 +-
 packages/cli/src/commands/plugin.ts                |  14 ++-
 packages/cli/src/commands/port-prompt.ts           |   4 +-
 packages/cli/src/commands/project.ts               |   6 +-
 packages/cli/src/commands/research.ts              |   3 +-
 packages/cli/src/commands/task.ts                  | 101 +++++++++++---------
 packages/cli/src/commands/update.ts                |   3 +-
 packages/cli/src/commands/workflow.ts              |  13 +--
 packages/cli/src/output.ts                         | 104 +++++++++++++++++++++
 packages/cli/src/project-resolver.ts               |  20 ++--
 22 files changed, 388 insertions(+), 90 deletions(-)

Fusion-Task-Id: FN-8576

Fusion-Task-Lineage: 2493d6d5-bbd9-4fc2-b903-950457ef30b0

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-25 00:54:46 -07:00

105 lines
3.5 KiB
TypeScript

import { Writable } from "node:stream";
/*
* FNXC:CliQuietMode 2026-07-16-00:00:
* Quiet mode suppresses console and raw stdout chatter while preserving stderr,
* interactive prompts, and result-bearing output. JSON, help/version, and
* exempt live surfaces (including the Ink TUI) resolve quiet off per invocation.
* The presence-preserving flag beats the environment, and this dynamic gate is
* reversible so repeated in-process CLI runs never leak output state.
*/
const originalConsoleLog = console.log;
const originalConsoleInfo = console.info;
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
let quietMode = false;
let gateInstalled = false;
function isTruthyEnvFlag(value: string | undefined): boolean {
if (value === undefined) return false;
return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
}
export function resolveQuietMode({ flag, env }: { flag?: boolean; env?: string }): boolean {
return flag === undefined ? isTruthyEnvFlag(env) : flag;
}
export function setQuietMode(enabled: boolean): void {
quietMode = enabled;
}
export function isQuietMode(): boolean {
return quietMode;
}
export function resetQuietMode(): void {
quietMode = false;
}
/** Install one dynamic stdout gate; reinstallation deliberately never double-wraps. */
export function installQuietGate(): void {
if (gateInstalled) return;
gateInstalled = true;
console.log = (...args: unknown[]) => {
if (!isQuietMode()) originalConsoleLog(...args);
};
console.info = (...args: unknown[]) => {
if (!isQuietMode()) originalConsoleInfo(...args);
};
process.stdout.write = ((chunk: unknown, encoding?: unknown, callback?: unknown): boolean => {
if (!isQuietMode()) {
return originalStdoutWrite(chunk as never, encoding as never, callback as never);
}
const done = typeof encoding === "function" ? encoding : callback;
if (typeof done === "function") {
done();
}
return true;
}) as typeof process.stdout.write;
}
export function uninstallQuietGate(): void {
if (!gateInstalled) return;
console.log = originalConsoleLog;
console.info = originalConsoleInfo;
process.stdout.write = originalStdoutWrite as typeof process.stdout.write;
gateInstalled = false;
}
/** Write machine-consumable command output without consulting the quiet gate. */
export function result(text: string): void {
originalStdoutWrite(text);
}
/**
* Readline needs stdout's terminal metadata as well as its writer. This proxy
* bypasses quiet only for prompt rendering while forwarding all other stream
* behaviour to the real stdout instance.
*/
export function promptOutputStream(): NodeJS.WritableStream {
const proxy = new Writable({
write(chunk, encoding, callback) {
originalStdoutWrite(chunk, encoding, callback);
},
});
return new Proxy(proxy, {
get(target, property, receiver) {
if (property === "columns" || property === "rows" || property === "isTTY") {
return Reflect.get(process.stdout, property);
}
if (property === "write") {
return (chunk: unknown, encoding?: unknown, callback?: unknown) =>
originalStdoutWrite(chunk as never, encoding as never, callback as never);
}
const value = Reflect.get(target, property, receiver);
if (value !== undefined) return typeof value === "function" ? value.bind(target) : value;
const stdoutValue = Reflect.get(process.stdout, property);
return typeof stdoutValue === "function" ? stdoutValue.bind(process.stdout) : stdoutValue;
},
}) as unknown as NodeJS.WritableStream;
}