Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
One-time setup that gathers your project's design context and saves it to CLAUDE.md for future sessions.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/detector/cli/main.mjs
1import fs from 'node:fs';2import path from 'node:path';34import { loadDesignSystemForCwd } from '../design-system.mjs';5import { createBrowserDetector, detectUrl } from '../engines/browser/detect-url.mjs';6import { detectHtml } from '../engines/static-html/detect-html.mjs';7import { detectText } from '../engines/regex/detect-text.mjs';8import {9filterDetectionFindings,10readDetectionConfig,11shouldIgnoreDetectionFile,12} from '../../lib/impeccable-config.mjs';13import {14HTML_EXTENSIONS,15buildImportGraph,16detectFrameworkConfig,17isPortListening,18walkDir,19} from '../node/file-system.mjs';2021// ---------------------------------------------------------------------------22// Output formatting23// ---------------------------------------------------------------------------2425function formatFindings(findings, jsonMode) {26if (jsonMode) return JSON.stringify(findings, null, 2);2728const grouped = {};29for (const f of findings) {30if (!grouped[f.file]) grouped[f.file] = [];31grouped[f.file].push(f);32}33const out = [];34for (const [file, items] of Object.entries(grouped)) {35const importNote = items[0]?.importedBy?.length ? ` (imported by ${items[0].importedBy.join(', ')})` : '';36out.push(`\n${file}${importNote}`);37for (const item of items) {38out.push(` ${item.line ? `line ${item.line}: ` : ''}[${item.antipattern}] ${item.snippet}`);39out.push(` → ${item.description}`);40}41}42out.push(`\n${findings.length} anti-pattern${findings.length === 1 ? '' : 's'} found.`);43return out.join('\n');44}4546// ---------------------------------------------------------------------------47// Stdin handling48// ---------------------------------------------------------------------------4950async function handleStdin(options = {}) {51const chunks = [];52for await (const chunk of process.stdin) chunks.push(chunk);53const input = Buffer.concat(chunks).toString('utf-8');54try {55const parsed = JSON.parse(input);56const fp = parsed?.tool_input?.file_path;57if (fp && fs.existsSync(fp)) {58return HTML_EXTENSIONS.has(path.extname(fp).toLowerCase())59? detectHtml(fp, options) : detectText(fs.readFileSync(fp, 'utf-8'), fp, options);60}61} catch { /* not JSON */ }62return detectText(input, '<stdin>', options);63}646566// ---------------------------------------------------------------------------67// CLI68// ---------------------------------------------------------------------------6970async function confirm(question) {71const rl = (await import('node:readline')).default.createInterface({72input: process.stdin, output: process.stderr,73});74return new Promise((resolve) => {75rl.question(`${question} [Y/n] `, (answer) => {76rl.close();77resolve(!answer || /^y(es)?$/i.test(answer.trim()));78});79});80}8182function printUsage() {83console.log(`Usage: impeccable detect [options] [file-or-dir-or-url...]8485Scan files or URLs for UI anti-patterns and design quality issues.8687Options:88--json Output results as JSON89--gpt Also report GPT-specific provider tells (off by default)90--gemini Also report Gemini-specific provider tells (off by default)91--no-config Do not apply project config, detector ignores, or DESIGN.md92--no-design-system Do not load local DESIGN.md / .impeccable/design.json context93--help Show this help message9495Project config:96Respects .impeccable/config.json and .impeccable/config.local.json detector97settings: detector.ignoreRules, detector.ignoreFiles, detector.ignoreValues,98and detector.designSystem.enabled.99100Detection modes:101HTML files Static HTML/CSS analysis (default, catches linked CSS)102Non-HTML files Regex pattern matching (CSS, JSX, TSX, etc.)103URLs Puppeteer full browser rendering (auto-detected)104105Examples:106impeccable detect src/107impeccable detect index.html108impeccable detect https://example.com109impeccable detect --json .110impeccable detect --no-config src/`);111}112113async function detectCli() {114let args = process.argv.slice(2).map(arg => {115if (arg === '-json') return '--json';116if (arg === '-fast') return '--fast';117return arg;118});119if (args[0] === 'detect') args = args.slice(1);120const jsonMode = args.includes('--json');121const helpMode = args.includes('--help');122// --fast (regex-only) is deprecated: since the jsdom removal, the static123// HTML/CSS analysis is fast and covers every rule, so the regex-only path124// only loses coverage for no real speed win. Accept the flag for back-compat125// but ignore it and run the full scan.126if (args.includes('--fast')) {127process.stderr.write(128'Note: --fast is deprecated and ignored. The full scan is fast now and runs every rule.\n',129);130}131const configEnabled = !args.includes('--no-config');132const detectionConfig = configEnabled133? readDetectionConfig(process.cwd())134: { ignoreRules: [], ignoreFiles: [], ignoreValues: [] };135const providers = [];136if (args.includes('--gpt')) providers.push('gpt');137if (args.includes('--gemini')) providers.push('gemini');138const designSystemEnabled = configEnabled && !args.includes('--no-design-system') && detectionConfig.designSystem?.enabled !== false;139const designSystem = designSystemEnabled ? loadDesignSystemForCwd(process.cwd()) : null;140const scanOptions = designSystem ? { providers, designSystem } : { providers };141const targets = args.filter(a => !a.startsWith('--'));142143if (helpMode) { printUsage(); process.exit(0); }144145let allFindings = [];146147if (!process.stdin.isTTY && targets.length === 0) {148allFindings = await handleStdin(scanOptions);149} else {150const paths = targets.length > 0 ? targets : [process.cwd()];151const urlTargetCount = paths.filter(target => /^https?:\/\//i.test(target)).length;152const browserDetector = urlTargetCount > 1 ? await createBrowserDetector() : null;153154try {155for (const target of paths) {156if (/^https?:\/\//i.test(target)) {157try {158const scanner = browserDetector159? (url) => browserDetector.detectUrl(url, scanOptions)160: (url) => detectUrl(url, scanOptions);161allFindings.push(...await scanner(target));162} catch (e) { process.stderr.write(`Error: ${e.message}\n`); }163continue;164}165166const resolved = path.resolve(target);167let stat;168try { stat = fs.statSync(resolved); }169catch { process.stderr.write(`Warning: cannot access ${target}\n`); continue; }170171if (stat.isDirectory()) {172// Check for framework dev server config (skip in JSON mode to avoid polluting output)173if (!jsonMode) {174const fwConfig = detectFrameworkConfig(resolved);175if (fwConfig) {176const probe = await isPortListening(fwConfig.port, fwConfig.fingerprint);177if (probe.listening && probe.matched) {178process.stderr.write(179`\n${fwConfig.name} dev server detected on localhost:${fwConfig.port}.\n` +180`For more accurate results, scan the running site:\n` +181` npx impeccable detect http://localhost:${fwConfig.port}\n\n`182);183} else if (probe.listening && !probe.matched) {184process.stderr.write(185`\n${fwConfig.name} project detected (${path.basename(fwConfig.configPath)}).\n` +186`Port ${fwConfig.port} is in use by another service. Start the ${fwConfig.name} dev server and scan via URL for best results.\n\n`187);188} else {189process.stderr.write(190`\n${fwConfig.name} project detected (${path.basename(fwConfig.configPath)}).\n` +191`Start the dev server and scan via URL for best results:\n` +192` npx impeccable detect http://localhost:${fwConfig.port}\n\n`193);194}195}196}197198const files = walkDir(resolved)199.filter(file => !shouldIgnoreDetectionFile(file, process.cwd(), detectionConfig));200const htmlCount = files.filter(f => HTML_EXTENSIONS.has(path.extname(f).toLowerCase())).length;201202// Warn and confirm if scanning many files (static HTML/CSS processes each HTML file)203if (files.length > 50 && process.stdin.isTTY && !jsonMode) {204process.stderr.write(205`\nFound ${files.length} files (${htmlCount} HTML) in ${target}.\n` +206`Scanning may take a while${htmlCount > 10 ? ' (static HTML/CSS processes each HTML file individually)' : ''}.\n` +207`Target a specific subdirectory to narrow scope.\n`208);209const ok = await confirm('Continue?');210if (!ok) { process.stderr.write('Aborted.\n'); process.exit(0); }211}212213// Build import graph for multi-file awareness214const graph = buildImportGraph(files);215// Build reverse map: file -> set of files that import it216const importedByMap = new Map();217for (const [importer, imports] of graph) {218for (const imported of imports) {219if (!importedByMap.has(imported)) importedByMap.set(imported, new Set());220importedByMap.get(imported).add(importer);221}222}223224for (const file of files) {225const ext = path.extname(file).toLowerCase();226let fileFindings;227if (HTML_EXTENSIONS.has(ext)) {228fileFindings = await detectHtml(file, scanOptions);229} else {230fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, scanOptions);231}232// Annotate findings with import context233const importers = importedByMap.get(file);234if (importers && importers.size > 0) {235const importerNames = [...importers].map(f => path.basename(f));236for (const f of fileFindings) {237f.importedBy = importerNames;238}239}240allFindings.push(...fileFindings);241}242} else if (stat.isFile()) {243if (shouldIgnoreDetectionFile(resolved, process.cwd(), detectionConfig)) continue;244const ext = path.extname(resolved).toLowerCase();245if (HTML_EXTENSIONS.has(ext)) {246allFindings.push(...await detectHtml(resolved, scanOptions));247} else {248allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, scanOptions));249}250}251}252} finally {253if (browserDetector) await browserDetector.close();254}255}256257allFindings = filterDetectionFindings(allFindings, detectionConfig);258259if (allFindings.length > 0) {260if (jsonMode) process.stdout.write(formatFindings(allFindings, true) + '\n');261else process.stderr.write(formatFindings(allFindings, false) + '\n');262process.exit(2);263}264if (jsonMode) process.stdout.write('[]\n');265process.exit(0);266}267268export { formatFindings, handleStdin, confirm, printUsage, detectCli };269