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>
105 lines
3.5 KiB
TypeScript
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;
|
|
}
|