SPB Git

spb/spbgit Public MIT

SPB Git — the platform hosting itself

JavaScript 73.9% CSS 11.7% Nunjucks 11.6% Shell 2.7%
4.9 KB · 128 lines javascript
Raw Blame History
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