/** * ───────────────────────────────────────────── * SPB Git — Personal Git Platform * ───────────────────────────────────────────── * Author : Simon-Pierre Boucher * Contact : contact@spboucher.ai * File : scripts/inject-headers.mjs * Purpose : Bulk-inject the mandatory author header into project files * License : MIT © Simon-Pierre Boucher * ───────────────────────────────────────────── */ import { execFileSync } from 'node:child_process'; import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs'; import { basename, extname, join, relative } from 'node:path'; import process from 'node:process'; const ROOT = new URL('..', import.meta.url).pathname; const EXEMPT_PATTERNS = [ /\.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']; /** * Build the header block for a given comment style. * @param {string} file repo-relative path * @param {'block'|'hash'|'html'|'dash'|'njk'} style */ function buildHeader(file, style) { const lines = [ '─────────────────────────────────────────────', ' SPB Git — Personal Git Platform', '─────────────────────────────────────────────', ' Author : Simon-Pierre Boucher', ' Contact : contact@spboucher.ai', ` File : ${file}`, ' Purpose : (added by inject-headers)', ' License : MIT © Simon-Pierre Boucher', '─────────────────────────────────────────────', ]; switch (style) { case 'block': return `/**\n${lines.map((l) => ` * ${l}`).join('\n')}\n */\n\n`; case 'hash': return `${lines.map((l) => `# ${l}`).join('\n')}\n\n`; case 'html': return `\n\n`; case 'dash': return `${lines.map((l) => `-- ${l}`).join('\n')}\n\n`; case 'njk': return `{#\n${lines.map((l) => ` ${l}`).join('\n')}\n#}\n`; default: throw new Error(`unknown style ${style}`); } } /** @returns {'block'|'hash'|'html'|'dash'|'njk'|null} */ function styleFor(file) { const ext = extname(file).toLowerCase(); const base = basename(file); if (['.js', '.mjs', '.cjs', '.ts', '.css', '.scss'].includes(ext)) return 'block'; if (['.py', '.sh', '.bash', '.yml', '.yaml', '.toml', '.service', '.gitignore', '.gitattributes', '.example'].includes(ext)) return 'hash'; if (base === 'Dockerfile' || base === 'Makefile' || base === '.gitignore' || base === '.gitattributes') return 'hash'; if (['.html', '.md', '.vue', '.svg'].includes(ext)) return 'html'; if (ext === '.sql') return 'dash'; if (ext === '.njk') return 'njk'; return null; } 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 { /* fall through */ } 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; } let injected = 0; for (const file of listFiles()) { if (EXEMPT_PATTERNS.some((re) => re.test(file))) continue; const style = styleFor(file); if (!style) continue; const full = join(ROOT, file); let content; try { content = readFileSync(full, 'utf8'); } catch { continue; } const head = content.split('\n').slice(0, 40).join('\n'); if (REQUIRED_MARKERS.every((m) => head.includes(m))) continue; const header = buildHeader(file, style); if (content.startsWith('#!')) { const nl = content.indexOf('\n') + 1; content = content.slice(0, nl) + header + content.slice(nl); } else { content = header + content; } writeFileSync(full, content); console.log(`+ header → ${file}`); injected += 1; } console.log(injected > 0 ? `Injected ${injected} header(s).` : 'Nothing to do — all headers present.'); process.exit(0);