spb/spbgit Public MIT
SPB Git — the platform hosting itself
JavaScript 73.9%
CSS 11.7%
Nunjucks 11.6%
Shell 2.7%
1/**2 * ─────────────────────────────────────────────3 * SPB Git — Personal Git Platform4 * ─────────────────────────────────────────────5 * Author : Simon-Pierre Boucher6 * Contact : contact@spboucher.ai7 * File : scripts/inject-headers.mjs8 * Purpose : Bulk-inject the mandatory author header into project files9 * License : MIT © Simon-Pierre Boucher10 * ─────────────────────────────────────────────11 */1213import { execFileSync } from 'node:child_process';14import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';15import { basename, extname, join, relative } from 'node:path';16import process from 'node:process';1718const ROOT = new URL('..', import.meta.url).pathname;1920const EXEMPT_PATTERNS = [21 /\.json$/, /\.jsonl$/, /\.lock$/,22 /(^|\/)LICENSE$/,23 /\.(woff2?|ttf|otf|eot|png|jpe?g|gif|ico|webp|zip|gz|zst|pdf)$/i,24 /(^|\/)\.DS_Store$/,25 /(^|\/)\.env$/,26 /(^|\/)node_modules\//, /(^|\/)dev\//, /(^|\/)coverage\//,27 /(^|\/)dist\//, /(^|\/)logs\//, /(^|\/)\.git\//, /(^|\/)\.claude\//,28 /(^|\/)src\/web\/assets\/vendor\//,29];3031const REQUIRED_MARKERS = ['SPB Git', 'Simon-Pierre Boucher', 'contact@spboucher.ai'];3233/**34 * Build the header block for a given comment style.35 * @param {string} file repo-relative path36 * @param {'block'|'hash'|'html'|'dash'|'njk'} style37 */38function buildHeader(file, style) {39 const lines = [40 '─────────────────────────────────────────────',41 ' SPB Git — Personal Git Platform',42 '─────────────────────────────────────────────',43 ' Author : Simon-Pierre Boucher',44 ' Contact : contact@spboucher.ai',45 ` File : ${file}`,46 ' Purpose : (added by inject-headers)',47 ' License : MIT © Simon-Pierre Boucher',48 '─────────────────────────────────────────────',49 ];50 switch (style) {51 case 'block':52 return `/**\n${lines.map((l) => ` * ${l}`).join('\n')}\n */\n\n`;53 case 'hash':54 return `${lines.map((l) => `# ${l}`).join('\n')}\n\n`;55 case 'html':56 return `<!--\n${lines.map((l) => ` ${l}`).join('\n')}\n-->\n\n`;57 case 'dash':58 return `${lines.map((l) => `-- ${l}`).join('\n')}\n\n`;59 case 'njk':60 return `{#\n${lines.map((l) => ` ${l}`).join('\n')}\n#}\n`;61 default:62 throw new Error(`unknown style ${style}`);63 }64}6566/** @returns {'block'|'hash'|'html'|'dash'|'njk'|null} */67function styleFor(file) {68 const ext = extname(file).toLowerCase();69 const base = basename(file);70 if (['.js', '.mjs', '.cjs', '.ts', '.css', '.scss'].includes(ext)) return 'block';71 if (['.py', '.sh', '.bash', '.yml', '.yaml', '.toml', '.service', '.gitignore', '.gitattributes', '.example'].includes(ext)) return 'hash';72 if (base === 'Dockerfile' || base === 'Makefile' || base === '.gitignore' || base === '.gitattributes') return 'hash';73 if (['.html', '.md', '.vue', '.svg'].includes(ext)) return 'html';74 if (ext === '.sql') return 'dash';75 if (ext === '.njk') return 'njk';76 return null;77}7879function listFiles() {80 try {81 const out = execFileSync('git', ['ls-files'], { cwd: ROOT, encoding: 'utf8' });82 const files = out.split('\n').filter(Boolean);83 if (files.length > 0) return files;84 } catch {85 /* fall through */86 }87 const acc = [];88 const walk = (dir) => {89 for (const entry of readdirSync(dir)) {90 const full = join(dir, entry);91 const rel = relative(ROOT, full);92 if (EXEMPT_PATTERNS.some((re) => re.test(rel + (statSync(full).isDirectory() ? '/' : '')))) continue;93 if (statSync(full).isDirectory()) walk(full);94 else acc.push(rel);95 }96 };97 walk(ROOT);98 return acc;99}100101let injected = 0;102for (const file of listFiles()) {103 if (EXEMPT_PATTERNS.some((re) => re.test(file))) continue;104 const style = styleFor(file);105 if (!style) continue;106 const full = join(ROOT, file);107 let content;108 try {109 content = readFileSync(full, 'utf8');110 } catch {111 continue;112 }113 const head = content.split('\n').slice(0, 40).join('\n');114 if (REQUIRED_MARKERS.every((m) => head.includes(m))) continue;115 const header = buildHeader(file, style);116 if (content.startsWith('#!')) {117 const nl = content.indexOf('\n') + 1;118 content = content.slice(0, nl) + header + content.slice(nl);119 } else {120 content = header + content;121 }122 writeFileSync(full, content);123 console.log(`+ header → ${file}`);124 injected += 1;125}126console.log(injected > 0 ? `Injected ${injected} header(s).` : 'Nothing to do — all headers present.');127process.exit(0);128