Add Chrome DevTools extension for anti-pattern detection

Adds a Manifest V3 Chrome extension that injects the detector when
DevTools opens, with a dedicated panel for browsing findings, a toolbar
popup for quick scan/toggle, and per-rule settings synced via
chrome.storage. Categorizes anti-patterns into AI slop vs quality
issues with visual differentiation (sparkle prefix, panel grouping).
Overlay labels are polished with flush positioning, cycling for
multi-finding elements, and synchronized hover darkening.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-04-06 15:18:36 -07:00
co-authored by Claude Opus 4.6
parent 9ccdb9148a
commit e961d56252
22 changed files with 1921 additions and 214 deletions
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env node
/**
* Builds the Chrome DevTools extension.
*
* 1. Generates the extension variant of the browser detector
* 2. Extracts antipatterns.json for the panel UI
* 3. Optionally packages as a .zip for Chrome Web Store
*
* Run: node scripts/build-extension.js
* node scripts/build-extension.js --zip
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const EXT_DIR = path.join(ROOT, 'extension');
const SOURCE = path.join(ROOT, 'src/detect-antipatterns.mjs');
const DETECTOR_OUTPUT = path.join(EXT_DIR, 'detector/detect.js');
const AP_OUTPUT = path.join(EXT_DIR, 'detector/antipatterns.json');
let code = fs.readFileSync(SOURCE, 'utf-8');
// --- 1. Build detector ---
// Strip shebang
code = code.replace(/^#!.*\n/, '');
// Strip sections between @browser-strip-start / @browser-strip-end markers
code = code.replace(/^\/\/ @browser-strip-start\n[\s\S]*?^\/\/ @browser-strip-end\n?/gm, '');
// Set IS_BROWSER = true (dead-code eliminates Node paths)
code = code.replace(/^const IS_BROWSER = .*$/m, 'const IS_BROWSER = true;');
const output = `/**
* Anti-Pattern Browser Detector for Impeccable (Extension Variant)
* Copyright (c) 2026 Paul Bakaus
* SPDX-License-Identifier: Apache-2.0
*
* GENERATED -- do not edit. Source: detect-antipatterns.mjs
* Rebuild: node scripts/build-extension.js
*/
(function () {
if (typeof window === 'undefined') return;
${code}
})();
`;
fs.mkdirSync(path.dirname(DETECTOR_OUTPUT), { recursive: true });
fs.writeFileSync(DETECTOR_OUTPUT, output);
console.log(`Generated ${path.relative(ROOT, DETECTOR_OUTPUT)} (${(output.length / 1024).toFixed(1)} KB)`);
// --- 2. Extract antipatterns.json ---
const rawSource = fs.readFileSync(SOURCE, 'utf-8');
const apMatch = rawSource.match(/const ANTIPATTERNS = \[([\s\S]*?)\n\];/);
if (apMatch) {
// Convert JS object literals to JSON
const antipatterns = new Function(`return [${apMatch[1]}]`)();
const apJson = antipatterns.map(({ id, name, category }) => ({ id, name, category: category || 'quality' }));
fs.writeFileSync(AP_OUTPUT, JSON.stringify(apJson, null, 2) + '\n');
console.log(`Generated ${path.relative(ROOT, AP_OUTPUT)} (${antipatterns.length} rules)`);
}
// --- 3. Zip packaging ---
if (process.argv.includes('--zip')) {
const archiver = (await import('archiver')).default;
const zipPath = path.join(ROOT, 'dist/impeccable-extension.zip');
fs.mkdirSync(path.dirname(zipPath), { recursive: true });
const zipStream = fs.createWriteStream(zipPath);
const archive = archiver('zip', { zlib: { level: 9 } });
archive.pipe(zipStream);
archive.directory(EXT_DIR, false);
await archive.finalize();
const size = fs.statSync(zipPath).size;
console.log(`Packaged ${path.relative(ROOT, zipPath)} (${(size / 1024).toFixed(1)} KB)`);
}
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env node
/**
* Generates PNG extension icons from SVG using Puppeteer.
*
* Run: node scripts/generate-extension-icons.js
*/
import puppeteer from 'puppeteer';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const ICONS_DIR = path.join(ROOT, 'extension/icons');
const SIZES = [16, 32, 48, 128];
const svgContent = fs.readFileSync(path.join(ICONS_DIR, 'icon.svg'), 'utf-8');
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
for (const size of SIZES) {
await page.setViewport({ width: size, height: size, deviceScaleFactor: 1 });
await page.setContent(`
<!DOCTYPE html>
<html>
<head><style>* { margin: 0; padding: 0; } body { width: ${size}px; height: ${size}px; overflow: hidden; }</style></head>
<body>${svgContent.replace('viewBox="0 0 128 128"', `viewBox="0 0 128 128" width="${size}" height="${size}"`)}</body>
</html>
`);
await page.screenshot({ path: path.join(ICONS_DIR, `icon-${size}.png`), omitBackground: true });
console.log(`Generated icon-${size}.png`);
}
await browser.close();