mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-03 15:43:31 +08:00
132 lines
3.8 KiB
JavaScript
132 lines
3.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* PostToolUse hook: lightweight frontend design-quality reminder.
|
|
*
|
|
* This stays self-contained inside ECC. It does not call remote models or
|
|
* install packages. The goal is to catch obviously generic UI drift and keep
|
|
* frontend edits aligned with ECC's stronger design standards.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const FRONTEND_EXTENSIONS = /\.(astro|css|html|jsx|scss|svelte|tsx|vue)$/i;
|
|
const MAX_STDIN = 1024 * 1024;
|
|
|
|
const GENERIC_SIGNALS = [
|
|
{ pattern: /\bget started\b/i, label: '"Get Started" CTA copy' },
|
|
{ pattern: /\blearn more\b/i, label: '"Learn more" CTA copy' },
|
|
{ pattern: /\bgrid-cols-(3|4)\b/, label: 'uniform multi-card grid' },
|
|
{ pattern: /\bbg-gradient-to-[trbl]/, label: 'stock gradient utility usage' },
|
|
{ pattern: /\btext-center\b/, label: 'centered default layout cues' },
|
|
{ pattern: /\bfont-(sans|inter)\b/i, label: 'default font utility' },
|
|
];
|
|
|
|
const CHECKLIST = [
|
|
'visual hierarchy with real contrast',
|
|
'intentional spacing rhythm',
|
|
'depth, layering, or overlap',
|
|
'purposeful hover and focus states',
|
|
'color and typography that feel specific',
|
|
];
|
|
|
|
function getFilePaths(input) {
|
|
const toolInput = input?.tool_input || {};
|
|
if (toolInput.file_path) {
|
|
return [String(toolInput.file_path)];
|
|
}
|
|
|
|
if (Array.isArray(toolInput.edits)) {
|
|
return toolInput.edits
|
|
.map(edit => String(edit?.file_path || ''))
|
|
.filter(Boolean);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
function readContent(filePath) {
|
|
try {
|
|
return fs.readFileSync(path.resolve(filePath), 'utf8');
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function detectSignals(content) {
|
|
return GENERIC_SIGNALS.filter(signal => signal.pattern.test(content)).map(signal => signal.label);
|
|
}
|
|
|
|
function buildWarning(frontendPaths, findings) {
|
|
const pathLines = frontendPaths.map(fp => ` - ${fp}`).join('\n');
|
|
const signalLines = findings.length > 0
|
|
? findings.map(item => ` - ${item}`).join('\n')
|
|
: ' - no obvious canned-template strings detected';
|
|
|
|
return [
|
|
'[Hook] DESIGN CHECK: frontend file(s) modified:',
|
|
pathLines,
|
|
'[Hook] Review for generic/template drift. Frontend should have:',
|
|
CHECKLIST.map(item => ` - ${item}`).join('\n'),
|
|
'[Hook] Heuristic signals:',
|
|
signalLines,
|
|
].join('\n');
|
|
}
|
|
|
|
function run(inputOrRaw) {
|
|
let input;
|
|
let rawInput = inputOrRaw;
|
|
|
|
try {
|
|
if (typeof inputOrRaw === 'string') {
|
|
rawInput = inputOrRaw;
|
|
input = inputOrRaw.trim() ? JSON.parse(inputOrRaw) : {};
|
|
} else {
|
|
input = inputOrRaw || {};
|
|
rawInput = JSON.stringify(inputOrRaw ?? {});
|
|
}
|
|
} catch {
|
|
return { exitCode: 0, stdout: typeof rawInput === 'string' ? rawInput : '' };
|
|
}
|
|
|
|
const filePaths = getFilePaths(input);
|
|
const frontendPaths = filePaths.filter(filePath => FRONTEND_EXTENSIONS.test(filePath));
|
|
|
|
if (frontendPaths.length === 0) {
|
|
return { exitCode: 0, stdout: typeof rawInput === 'string' ? rawInput : '' };
|
|
}
|
|
|
|
const findings = [];
|
|
for (const filePath of frontendPaths) {
|
|
const content = readContent(filePath);
|
|
findings.push(...detectSignals(content));
|
|
}
|
|
|
|
return {
|
|
exitCode: 0,
|
|
stdout: typeof rawInput === 'string' ? rawInput : '',
|
|
stderr: buildWarning(frontendPaths, findings),
|
|
};
|
|
}
|
|
|
|
module.exports = { run };
|
|
|
|
if (require.main === module) {
|
|
let raw = '';
|
|
process.stdin.setEncoding('utf8');
|
|
process.stdin.on('data', chunk => {
|
|
if (raw.length < MAX_STDIN) {
|
|
const remaining = MAX_STDIN - raw.length;
|
|
raw += chunk.substring(0, remaining);
|
|
}
|
|
});
|
|
process.stdin.on('end', () => {
|
|
const result = run(raw);
|
|
if (result.stderr) process.stderr.write(`${result.stderr}\n`);
|
|
process.stdout.write(typeof result.stdout === 'string' ? result.stdout : raw);
|
|
process.exit(Number.isInteger(result.exitCode) ? result.exitCode : 0);
|
|
});
|
|
}
|