From d731cc61f7bf11d95eb17603edbf3e6a20946a4e Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Sun, 22 Mar 2026 18:55:28 -0500 Subject: [PATCH 1/4] fix(analyze): respect --log-level in output and JSON --- src/commands/analyze.meta.ts | 11 ++++- src/commands/analyze.ts | 44 ++++++++++++++++---- src/test/cli.test.ts | 81 +++++++++++++++++++++++++++++++++++- 3 files changed, 125 insertions(+), 11 deletions(-) diff --git a/src/commands/analyze.meta.ts b/src/commands/analyze.meta.ts index 8e9717a..cc7a525 100644 --- a/src/commands/analyze.meta.ts +++ b/src/commands/analyze.meta.ts @@ -7,7 +7,7 @@ export const meta = { choices: ['debug', 'info', 'warn', 'error'], default: 'info', description: - 'Set the log level and the minimum severity that causes a non-zero exit code (debug | info | warn | error)' + 'Which severities are printed (pretty output) and included in JSON messages unless --json-full is set; also the minimum severity for a non-zero exit code (debug | info | warn | error)' }, categories: { type: 'string', @@ -24,7 +24,14 @@ export const meta = { json: { type: 'boolean', default: false, - description: 'Output results as JSON to stdout' + description: + 'Output results as JSON to stdout (messages respect --log-level unless --json-full)' + }, + 'json-full': { + type: 'boolean', + default: false, + description: + 'With --json, include every diagnostic in messages regardless of --log-level (exit code still follows --log-level). Ignored without --json.' } } } as const; diff --git a/src/commands/analyze.ts b/src/commands/analyze.ts index 9c9b990..6152d73 100644 --- a/src/commands/analyze.ts +++ b/src/commands/analyze.ts @@ -7,6 +7,7 @@ import {report} from '../index.js'; import {enableDebug} from '../logger.js'; import {wrapAnsi} from 'fast-wrap-ansi'; import {parseCategories} from '../categories.js'; +import type {Message} from '../types.js'; function formatBytes(bytes: number) { const units = ['B', 'KB', 'MB', 'GB']; @@ -33,12 +34,25 @@ const FAIL_THRESHOLD_RANK: Record = { debug: 0 }; +/** Unknown severities are treated like warnings so they are not silently dropped. */ +function messageSeverityRank(m: Message): number { + return SEVERITY_RANK[m.severity] ?? 2; +} + +function messagesVisibleAtLogLevel( + messages: Message[], + thresholdRank: number +): Message[] { + return messages.filter((m) => messageSeverityRank(m) >= thresholdRank); +} + export async function run(ctx: CommandContext) { // Gunshi passes subcommand name as first positional; path is optional second const providedPath = ctx.positionals.length > 1 ? ctx.positionals[1] : undefined; const logLevel = ctx.values['log-level']; const jsonOutput = ctx.values['json']; + const jsonFull = ctx.values['json-full']; let root: string | undefined; // Enable debug output based on log level @@ -94,12 +108,16 @@ export async function run(ctx: CommandContext) { }); const thresholdRank = FAIL_THRESHOLD_RANK[logLevel] ?? 0; + const visibleMessages = messagesVisibleAtLogLevel(messages, thresholdRank); const hasFailingMessages = thresholdRank > 0 && - messages.some((m) => SEVERITY_RANK[m.severity] >= thresholdRank); + messages.some((m) => messageSeverityRank(m) >= thresholdRank); if (jsonOutput) { - process.stdout.write(JSON.stringify({stats, messages}, null, 2) + '\n'); + const jsonMessages = jsonFull ? messages : visibleMessages; + process.stdout.write( + JSON.stringify({stats, messages: jsonMessages}, null, 2) + '\n' + ); if (hasFailingMessages) { process.exit(1); } @@ -156,8 +174,16 @@ export async function run(ctx: CommandContext) { prompts.log.info('Results:'); prompts.log.message('', {spacing: 0}); - // Display tool analysis results - if (messages.length > 0) { + // Display tool analysis results (severity-filtered by --log-level) + if (messages.length > 0 && visibleMessages.length === 0) { + prompts.log.message( + styleText( + 'dim', + `${messages.length} issue(s) below --log-level ${logLevel}; use a lower threshold to see them.` + ), + {spacing: 0} + ); + } else if (visibleMessages.length > 0) { const width = process.stdout?.columns ?? 80; const maxContentWidth = Math.max(20, width - 4); @@ -167,9 +193,11 @@ export async function run(ctx: CommandContext) { .map((line, i) => (i === 0 ? ` ${bullet} ${line}` : ` ${line}`)) .join('\n'); - const errorMessages = messages.filter((m) => m.severity === 'error'); - const warningMessages = messages.filter((m) => m.severity === 'warning'); - const suggestionMessages = messages.filter( + const errorMessages = visibleMessages.filter((m) => m.severity === 'error'); + const warningMessages = visibleMessages.filter( + (m) => m.severity === 'warning' + ); + const suggestionMessages = visibleMessages.filter( (m) => m.severity === 'suggestion' ); @@ -212,7 +240,7 @@ export async function run(ctx: CommandContext) { const errorCount = errorMessages.length; const warningCount = warningMessages.length; const suggestionCount = suggestionMessages.length; - const fixableCount = messages.filter( + const fixableCount = visibleMessages.filter( (m) => m.fixableBy === 'migrate' ).length; const parts: string[] = []; diff --git a/src/test/cli.test.ts b/src/test/cli.test.ts index ae3af8d..03d4d4e 100644 --- a/src/test/cli.test.ts +++ b/src/test/cli.test.ts @@ -20,7 +20,16 @@ const stripVersion = (str: string): string => ); const normalizeStderr = (str: string): string => - str.replace(/\(node:\d+\)/g, '(node:)'); + str + .replace(/\(node:\d+\)/g, '(node:)') + .split('\n') + .filter( + (line) => + !line.includes("NO_COLOR' env is ignored") && + !line.includes('--trace-warnings') + ) + .join('\n') + .replace(/\n+$/, ''); const basicChalkFixture = path.join( __dirname, @@ -155,6 +164,28 @@ describe('analyze exit codes', () => { ); expect(code).toBe(0); }); + + it('with --log-level=error hides warnings when there are no errors', async () => { + const {stdout, stderr, code} = await runCliProcess( + ['analyze', '--log-level=error'], + basicChalkFixture + ); + expect(code).toBe(0); + const output = stdout + stderr; + expect(output).not.toContain('Warnings:'); + expect(output).toContain('below --log-level error'); + }); + + it('with --log-level=warn shows warnings but not suggestions', async () => { + const {stdout, stderr, code} = await runCliProcess( + ['analyze', '--log-level=warn'], + basicChalkFixture + ); + expect(code).toBe(1); + const output = stdout + stderr; + expect(output).toContain('Warnings:'); + expect(output).not.toContain('Suggestions:'); + }); }); describe('analyze --json', () => { @@ -189,6 +220,54 @@ describe('analyze --json', () => { const parsed = JSON.parse(stdout); expect(parsed.messages.length).toBeGreaterThan(0); }); + + it('filters JSON messages to match --log-level=error', async () => { + const {stdout, code} = await runCliProcess( + ['analyze', '--json', '--log-level=error'], + basicChalkFixture + ); + expect(code).toBe(0); + const parsed = JSON.parse(stdout); + expect(parsed.messages).toEqual([]); + }); + + it('--json-full includes all messages when --log-level=error', async () => { + const {stdout, code} = await runCliProcess( + [ + 'analyze', + '--json', + '--json-full', + '--log-level=error' + ], + basicChalkFixture + ); + expect(code).toBe(0); + const parsed = JSON.parse(stdout); + expect(parsed.messages.length).toBeGreaterThanOrEqual(2); + expect( + parsed.messages.some((m: {severity: string}) => m.severity === 'warning') + ).toBe(true); + expect( + parsed.messages.some( + (m: {severity: string}) => m.severity === 'suggestion' + ) + ).toBe(true); + }); + + it('JSON with --log-level=warn omits suggestions', async () => { + const {stdout, code} = await runCliProcess( + ['analyze', '--json', '--log-level=warn'], + basicChalkFixture + ); + expect(code).toBe(1); + const parsed = JSON.parse(stdout); + expect(parsed.messages.length).toBeGreaterThan(0); + expect( + parsed.messages.every( + (m: {severity: string}) => m.severity !== 'suggestion' + ) + ).toBe(true); + }); }); describe('analyze fixable summary', () => { From 4dfaf5f956b5a6a71465306dd541f22f71a8c7a4 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Sun, 22 Mar 2026 18:57:38 -0500 Subject: [PATCH 2/4] format --- src/test/cli.test.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/test/cli.test.ts b/src/test/cli.test.ts index 03d4d4e..6d74a1a 100644 --- a/src/test/cli.test.ts +++ b/src/test/cli.test.ts @@ -233,12 +233,7 @@ describe('analyze --json', () => { it('--json-full includes all messages when --log-level=error', async () => { const {stdout, code} = await runCliProcess( - [ - 'analyze', - '--json', - '--json-full', - '--log-level=error' - ], + ['analyze', '--json', '--json-full', '--log-level=error'], basicChalkFixture ); expect(code).toBe(0); From 8078cd1154e9e05368b2bb6448f5c7672b01d8ef Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Sun, 22 Mar 2026 19:00:55 -0500 Subject: [PATCH 3/4] chore: rollback normalizeStderr func --- src/test/cli.test.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/test/cli.test.ts b/src/test/cli.test.ts index 6d74a1a..cb9e79c 100644 --- a/src/test/cli.test.ts +++ b/src/test/cli.test.ts @@ -20,16 +20,7 @@ const stripVersion = (str: string): string => ); const normalizeStderr = (str: string): string => - str - .replace(/\(node:\d+\)/g, '(node:)') - .split('\n') - .filter( - (line) => - !line.includes("NO_COLOR' env is ignored") && - !line.includes('--trace-warnings') - ) - .join('\n') - .replace(/\n+$/, ''); + str.replace(/\(node:\d+\)/g, '(node:)'); const basicChalkFixture = path.join( __dirname, From f6f28576c0b23a05276385ef9c3b3ef2cf8c75e2 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Wed, 22 Apr 2026 18:09:32 -0500 Subject: [PATCH 4/4] refactor: enhance logging options with --quiet and --report-level flags --- src/commands/analyze.meta.ts | 23 ++++++++++++------- src/commands/analyze.ts | 34 +++++++++++++++++----------- src/test/cli.test.ts | 43 +++++++++++++++++++++++++++++++++--- 3 files changed, 76 insertions(+), 24 deletions(-) diff --git a/src/commands/analyze.meta.ts b/src/commands/analyze.meta.ts index cc7a525..4744a99 100644 --- a/src/commands/analyze.meta.ts +++ b/src/commands/analyze.meta.ts @@ -7,7 +7,20 @@ export const meta = { choices: ['debug', 'info', 'warn', 'error'], default: 'info', description: - 'Which severities are printed (pretty output) and included in JSON messages unless --json-full is set; also the minimum severity for a non-zero exit code (debug | info | warn | error)' + 'Minimum severity for a non-zero exit code, and enables debug logging when set to debug (debug | info | warn | error)' + }, + quiet: { + type: 'boolean', + default: false, + description: + 'Only show errors in Results and JSON messages (same idea as ESLint --quiet). Overrides --report-level.' + }, + 'report-level': { + type: 'enum', + choices: ['auto', 'debug', 'info', 'warn', 'error'], + default: 'auto', + description: + 'Which severities appear in Results and in JSON messages when not using --quiet. auto: follow --log-level (debug | info | warn | error)' }, categories: { type: 'string', @@ -25,13 +38,7 @@ export const meta = { type: 'boolean', default: false, description: - 'Output results as JSON to stdout (messages respect --log-level unless --json-full)' - }, - 'json-full': { - type: 'boolean', - default: false, - description: - 'With --json, include every diagnostic in messages regardless of --log-level (exit code still follows --log-level). Ignored without --json.' + 'Output results as JSON to stdout (messages follow --quiet or resolved --report-level)' } } } as const; diff --git a/src/commands/analyze.ts b/src/commands/analyze.ts index 6152d73..d5a32b9 100644 --- a/src/commands/analyze.ts +++ b/src/commands/analyze.ts @@ -39,7 +39,7 @@ function messageSeverityRank(m: Message): number { return SEVERITY_RANK[m.severity] ?? 2; } -function messagesVisibleAtLogLevel( +function messagesAtOrAboveSeverityThreshold( messages: Message[], thresholdRank: number ): Message[] { @@ -51,8 +51,9 @@ export async function run(ctx: CommandContext) { const providedPath = ctx.positionals.length > 1 ? ctx.positionals[1] : undefined; const logLevel = ctx.values['log-level']; + const quiet = ctx.values['quiet']; + const reportLevel = ctx.values['report-level']; const jsonOutput = ctx.values['json']; - const jsonFull = ctx.values['json-full']; let root: string | undefined; // Enable debug output based on log level @@ -108,15 +109,23 @@ export async function run(ctx: CommandContext) { }); const thresholdRank = FAIL_THRESHOLD_RANK[logLevel] ?? 0; - const visibleMessages = messagesVisibleAtLogLevel(messages, thresholdRank); + const effectiveReportLevel = quiet + ? 'error' + : reportLevel === 'auto' + ? logLevel + : reportLevel; + const reportThresholdRank = FAIL_THRESHOLD_RANK[effectiveReportLevel] ?? 0; + const visibleMessages = messagesAtOrAboveSeverityThreshold( + messages, + reportThresholdRank + ); const hasFailingMessages = thresholdRank > 0 && messages.some((m) => messageSeverityRank(m) >= thresholdRank); if (jsonOutput) { - const jsonMessages = jsonFull ? messages : visibleMessages; process.stdout.write( - JSON.stringify({stats, messages: jsonMessages}, null, 2) + '\n' + JSON.stringify({stats, messages: visibleMessages}, null, 2) + '\n' ); if (hasFailingMessages) { process.exit(1); @@ -174,15 +183,14 @@ export async function run(ctx: CommandContext) { prompts.log.info('Results:'); prompts.log.message('', {spacing: 0}); - // Display tool analysis results (severity-filtered by --log-level) + // Display tool analysis results (severity-filtered by --quiet / --report-level) if (messages.length > 0 && visibleMessages.length === 0) { - prompts.log.message( - styleText( - 'dim', - `${messages.length} issue(s) below --log-level ${logLevel}; use a lower threshold to see them.` - ), - {spacing: 0} - ); + const dimHint = quiet + ? `${messages.length} issue(s) hidden by --quiet (errors only in output). Omit --quiet or lower --report-level to see them.` + : reportLevel === 'auto' + ? `${messages.length} issue(s) below --report-level ${effectiveReportLevel} (--report-level auto, same as --log-level); set --report-level to a lower value to see them.` + : `${messages.length} issue(s) below --report-level ${effectiveReportLevel}; set --report-level to a lower value to see them.`; + prompts.log.message(styleText('dim', dimHint), {spacing: 0}); } else if (visibleMessages.length > 0) { const width = process.stdout?.columns ?? 80; const maxContentWidth = Math.max(20, width - 4); diff --git a/src/test/cli.test.ts b/src/test/cli.test.ts index cb9e79c..f11f47d 100644 --- a/src/test/cli.test.ts +++ b/src/test/cli.test.ts @@ -164,7 +164,7 @@ describe('analyze exit codes', () => { expect(code).toBe(0); const output = stdout + stderr; expect(output).not.toContain('Warnings:'); - expect(output).toContain('below --log-level error'); + expect(output).toMatch(/below --report-level error/); }); it('with --log-level=warn shows warnings but not suggestions', async () => { @@ -177,6 +177,17 @@ describe('analyze exit codes', () => { expect(output).toContain('Warnings:'); expect(output).not.toContain('Suggestions:'); }); + + it('--quiet hides non-errors like ESLint (default log-level still fails on warnings)', async () => { + const {stdout, stderr, code} = await runCliProcess( + ['analyze', '--quiet'], + basicChalkFixture + ); + expect(code).toBe(1); + const output = stdout + stderr; + expect(output).not.toContain('Warnings:'); + expect(output).toContain('hidden by --quiet'); + }); }); describe('analyze --json', () => { @@ -222,9 +233,9 @@ describe('analyze --json', () => { expect(parsed.messages).toEqual([]); }); - it('--json-full includes all messages when --log-level=error', async () => { + it('--report-level=info includes all messages when --log-level=error', async () => { const {stdout, code} = await runCliProcess( - ['analyze', '--json', '--json-full', '--log-level=error'], + ['analyze', '--json', '--log-level=error', '--report-level=info'], basicChalkFixture ); expect(code).toBe(0); @@ -240,6 +251,32 @@ describe('analyze --json', () => { ).toBe(true); }); + it('--quiet JSON omits warnings when there are no errors', async () => { + const {stdout, code} = await runCliProcess( + ['analyze', '--json', '--quiet', '--log-level=error'], + basicChalkFixture + ); + expect(code).toBe(0); + const parsed = JSON.parse(stdout); + expect(parsed.messages).toEqual([]); + }); + + it('--quiet overrides --report-level=info for JSON messages', async () => { + const {stdout, code} = await runCliProcess( + [ + 'analyze', + '--json', + '--quiet', + '--log-level=error', + '--report-level=info' + ], + basicChalkFixture + ); + expect(code).toBe(0); + const parsed = JSON.parse(stdout); + expect(parsed.messages).toEqual([]); + }); + it('JSON with --log-level=warn omits suggestions', async () => { const {stdout, code} = await runCliProcess( ['analyze', '--json', '--log-level=warn'],