diff --git a/src/__tests__/output.test.ts b/src/__tests__/output.test.ts new file mode 100644 index 0000000..04dfe5a --- /dev/null +++ b/src/__tests__/output.test.ts @@ -0,0 +1,32 @@ +import {describe, it, expect, vi, afterEach} from 'vitest'; +import {set_quiet, info, success, warn} from '../utils/output'; + +describe('output quiet mode', ()=>{ + afterEach(()=>{ + set_quiet(false); + vi.restoreAllMocks(); + }); + + it('suppresses info and success on stderr when quiet', ()=>{ + const spy = vi.spyOn(console, 'error').mockImplementation(()=>{}); + set_quiet(true); + info('resolving profile'); + success('logged in'); + expect(spy).not.toHaveBeenCalled(); + }); + + it('still emits warnings when quiet (they are not progress noise)', ()=>{ + const spy = vi.spyOn(console, 'error').mockImplementation(()=>{}); + set_quiet(true); + warn('token expires soon'); + expect(spy).toHaveBeenCalledOnce(); + }); + + it('emits info and success normally when not quiet', ()=>{ + const spy = vi.spyOn(console, 'error').mockImplementation(()=>{}); + set_quiet(false); + info('resolving profile'); + success('logged in'); + expect(spy).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/index.ts b/src/index.ts index f0e396c..ff67449 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import {profile_command} from './commands/profile'; import {team_command} from './commands/team'; import {api_command} from './commands/api'; import {CliError} from './utils/errors'; +import {set_quiet} from './utils/output'; // Route every command through commander's throwing mode so usage errors reach // our handler and map to exit code 2 (vs 1 for runtime/API failures). @@ -32,8 +33,13 @@ const build_program = (): Command=>{ .option('--json', 'Output compact JSON to stdout') .option('--pretty', 'Output indented JSON to stdout') .option('--verbose', 'Print the full request/response to stderr, credentials redacted (api)') + .option('-q, --quiet', 'Suppress progress messages on stderr (warnings and errors still shown)') .showHelpAfterError(); + // Apply --quiet before any command action runs, so progress output is + // silenced for the whole invocation. + program.hook('preAction', ()=>{set_quiet(program.opts().quiet === true);}); + program.addCommand(auth_command); program.addCommand(profile_command); program.addCommand(team_command); diff --git a/src/utils/output.ts b/src/utils/output.ts index 8e5cb44..c15bf5d 100644 --- a/src/utils/output.ts +++ b/src/utils/output.ts @@ -26,9 +26,14 @@ const safe_record = (record: Credential_record): Record=>{ const is_tty = process.stdout.isTTY === true; -const success = (msg: string): void=>console.error(pc.green(`✓ ${msg}`)); +// --quiet silences routine progress on stderr (info/success). Warnings and +// errors are never suppressed — they carry information the user still needs. +let quiet_mode = false; +const set_quiet = (on: boolean): void=>{quiet_mode = on;}; + +const success = (msg: string): void=>{if (!quiet_mode) console.error(pc.green(`✓ ${msg}`));}; const warn = (msg: string): void=>console.error(pc.yellow(`⚠ ${msg}`)); -const info = (msg: string): void=>console.error(pc.dim(msg)); +const info = (msg: string): void=>{if (!quiet_mode) console.error(pc.dim(msg));}; type Print_opts = { json?: boolean; @@ -55,5 +60,5 @@ const print = (data: unknown, opts: Print_opts = {}): void=>{ process.stdout.write(serialize(data, opts) + '\n'); }; -export {is_tty, pc, REDACTED, redact, safe_record, success, warn, info, print}; +export {is_tty, pc, REDACTED, redact, safe_record, set_quiet, success, warn, info, print}; export type {Print_opts};