/** * ───────────────────────────────────────────── * SPB Git — Personal Git Platform * ───────────────────────────────────────────── * Author : Simon-Pierre Boucher * Contact : contact@spboucher.ai * File : scripts/check-headers.mjs * Purpose : CI gate — fail if any tracked file lacks the author header * License : MIT © Simon-Pierre Boucher * ───────────────────────────────────────────── */ import { execFileSync } from 'node:child_process'; import { readFileSync, readdirSync, statSync } from 'node:fs'; import { extname, join, relative } from 'node:path'; import process from 'node:process'; const ROOT = new URL('..', import.meta.url).pathname; /** Files that structurally cannot (or should not) carry a comment header. */ const EXEMPT_PATTERNS = [ /(^|\/)package(-lock)?\.json$/, /\.json$/, /\.jsonl$/, /\.lock$/, /(^|\/)LICENSE$/, /\.(woff2?|ttf|otf|eot|png|jpe?g|gif|ico|webp|zip|gz|zst|pdf)$/i, /(^|\/)\.DS_Store$/, /(^|\/)\.env$/, /(^|\/)node_modules\//, /(^|\/)dev\//, /(^|\/)coverage\//, /(^|\/)dist\//, /(^|\/)logs\//, /(^|\/)\.git\//, /(^|\/)\.claude\//, /(^|\/)src\/web\/assets\/vendor\//, ]; const REQUIRED_MARKERS = ['SPB Git', 'Simon-Pierre Boucher', 'contact@spboucher.ai']; const HEAD_LINES = 40; /** @returns {string[]} repo-relative paths of files to check */ function listFiles() { try { const out = execFileSync('git', ['ls-files'], { cwd: ROOT, encoding: 'utf8' }); const files = out.split('\n').filter(Boolean); if (files.length > 0) return files; } catch { /* not a git repo yet — fall through to fs walk */ } const acc = []; const walk = (dir) => { for (const entry of readdirSync(dir)) { const full = join(dir, entry); const rel = relative(ROOT, full); if (EXEMPT_PATTERNS.some((re) => re.test(rel + (statSync(full).isDirectory() ? '/' : '')))) continue; if (statSync(full).isDirectory()) walk(full); else acc.push(rel); } }; walk(ROOT); return acc; } function isExempt(path) { return EXEMPT_PATTERNS.some((re) => re.test(path)); } function hasHeader(path) { let content; try { content = readFileSync(join(ROOT, path), 'utf8'); } catch { return false; } const head = content.split('\n').slice(0, HEAD_LINES).join('\n'); return REQUIRED_MARKERS.every((marker) => head.includes(marker)); } const failures = []; for (const file of listFiles()) { if (isExempt(file)) continue; const ext = extname(file); if (ext === '' && !/(^|\/)(Dockerfile|Makefile|\.gitignore|\.gitattributes)$/.test(file)) { // extensionless files other than well-known ones: still required to carry a header } if (!hasHeader(file)) failures.push(file); } if (failures.length > 0) { console.error('✗ Missing SPB Git author header in:'); for (const f of failures) console.error(` - ${f}`); console.error(`\n${failures.length} file(s) failed. Run: npm run inject:headers`); process.exit(1); } console.log('✓ All files carry the SPB Git author header.');