diff --git a/cli/engine/cli/main.mjs b/cli/engine/cli/main.mjs index 84e7a826e..89a53579a 100644 --- a/cli/engine/cli/main.mjs +++ b/cli/engine/cli/main.mjs @@ -22,6 +22,10 @@ import { // Output formatting // --------------------------------------------------------------------------- +function formatFindingSummary(count) { + return `${count} anti-pattern${count === 1 ? '' : 's'} found.`; +} + function formatFindings(findings, jsonMode) { if (jsonMode) return JSON.stringify(findings, null, 2); @@ -39,7 +43,7 @@ function formatFindings(findings, jsonMode) { out.push(` → ${item.description}`); } } - out.push(`\n${findings.length} anti-pattern${findings.length === 1 ? '' : 's'} found.`); + out.push(`\n${formatFindingSummary(findings.length)}`); return out.join('\n'); } @@ -86,6 +90,7 @@ Scan files or URLs for UI anti-patterns and design quality issues. Options: --json Output results as JSON + --quiet In text mode, only print the final findings count --gpt Also report GPT-specific provider tells (off by default) --gemini Also report Gemini-specific provider tells (off by default) --no-config Do not apply project config, detector ignores, or DESIGN.md @@ -118,6 +123,7 @@ async function detectCli() { }); if (args[0] === 'detect') args = args.slice(1); const jsonMode = args.includes('--json'); + const quietMode = args.includes('--quiet'); const helpMode = args.includes('--help'); // --fast (regex-only) is deprecated: since the jsdom removal, the static // HTML/CSS analysis is fast and covers every rule, so the regex-only path @@ -169,8 +175,8 @@ async function detectCli() { catch { process.stderr.write(`Warning: cannot access ${target}\n`); continue; } if (stat.isDirectory()) { - // Check for framework dev server config (skip in JSON mode to avoid polluting output) - if (!jsonMode) { + // Check for framework dev server config (skip in JSON/quiet modes to avoid polluting output) + if (!jsonMode && !quietMode) { const fwConfig = detectFrameworkConfig(resolved); if (fwConfig) { const probe = await isPortListening(fwConfig.port, fwConfig.fingerprint); @@ -200,7 +206,7 @@ async function detectCli() { const htmlCount = files.filter(f => HTML_EXTENSIONS.has(path.extname(f).toLowerCase())).length; // Warn and confirm if scanning many files (static HTML/CSS processes each HTML file) - if (files.length > 50 && process.stdin.isTTY && !jsonMode) { + if (files.length > 50 && process.stdin.isTTY && !jsonMode && !quietMode) { process.stderr.write( `\nFound ${files.length} files (${htmlCount} HTML) in ${target}.\n` + `Scanning may take a while${htmlCount > 10 ? ' (static HTML/CSS processes each HTML file individually)' : ''}.\n` + @@ -258,6 +264,7 @@ async function detectCli() { if (allFindings.length > 0) { if (jsonMode) process.stdout.write(formatFindings(allFindings, true) + '\n'); + else if (quietMode) process.stderr.write(formatFindingSummary(allFindings.length) + '\n'); else process.stderr.write(formatFindings(allFindings, false) + '\n'); process.exit(2); } diff --git a/tests/detect-antipatterns.test.js b/tests/detect-antipatterns.test.js index 4eda7638a..838d7a6c3 100644 --- a/tests/detect-antipatterns.test.js +++ b/tests/detect-antipatterns.test.js @@ -879,6 +879,7 @@ describe('CLI', () => { const { stdout, code } = run('--help'); expect(code).toBe(0); expect(stdout).toContain('Usage:'); + expect(stdout).toContain('--quiet'); }); test('detect subcommand is not treated as a scan target', () => { @@ -906,6 +907,32 @@ describe('CLI', () => { expect(parsed.length).toBeGreaterThan(0); }); + test('--quiet suppresses text details and keeps the summary exit signal', () => { + const { stdout, stderr, code } = run('--quiet', path.join(FIXTURES, 'should-flag.html')); + expect(code).toBe(2); + expect(stdout).toBe(''); + expect(stderr.trim()).toMatch(/^[1-9]\d* anti-patterns? found\.$/); + expect(stderr).not.toContain('side-tab'); + expect(stderr).not.toContain('line '); + }); + + test('--quiet stays silent on clean files', () => { + const { stdout, stderr, code } = run('--quiet', path.join(FIXTURES, 'should-pass.html')); + expect(code).toBe(0); + expect(stdout).toBe(''); + expect(stderr).toBe(''); + }); + + test('--quiet does not affect JSON output', () => { + const { stdout, stderr, code } = run('--json', '--quiet', path.join(FIXTURES, 'should-flag.html')); + expect(code).toBe(2); + expect(stderr).toBe(''); + const parsed = JSON.parse(stdout.trim()); + expect(parsed).toBeArray(); + expect(parsed.length).toBeGreaterThan(0); + expect(parsed.some(f => f.antipattern === 'side-tab')).toBe(true); + }); + test('-json alias outputs valid JSON', () => { const { stdout, stderr, code } = run('-json', path.join(FIXTURES, 'should-flag.html')); expect(code).toBe(2); @@ -1745,6 +1772,15 @@ describe('CLI -- dev server suggestion', () => { expect(stderr).toContain('3000'); }); + test('--quiet suppresses framework URL scan suggestions', () => { + const { stderr, code } = run('--quiet', path.join(FIXTURES, 'framework-next-tailwind')); + expect(code).toBe(2); + expect(stderr.trim()).toMatch(/^[1-9]\d* anti-patterns? found\.$/); + expect(stderr).not.toContain('Next.js'); + expect(stderr).not.toContain('3000'); + expect(stderr).not.toContain('Start the dev server'); + }); + test('suggests URL scan when Vite config found', () => { const { stderr } = run(path.join(FIXTURES, 'framework-vite')); expect(stderr).toContain('Vite');