#!/usr/bin/env node import { execFileSync } from 'node:child_process'; import { statSync } from 'node:fs'; import path from 'node:path'; const repoRoot = process.cwd(); const allowAssetCommit = process.env.ALLOW_ASSET_COMMIT === '1'; const maxBinarySizeBytes = 5 * 1024 * 1024; const tmpCapturePattern = /^tmp-.*\.(png|jpe?g|webp|gif|mp4|mov|webm)$/i; const binaryAssetPattern = /\.(png|jpe?g|webp|gif|svg|mp4|mov|webm|pdf|zip|7z)$/i; const blockedAssetPrefixes = ['public/assets/']; function getStagedPaths() { const output = execFileSync( 'git', ['diff', '--cached', '--name-only', '--diff-filter=ACMR'], { cwd: repoRoot, encoding: 'utf8' } ); return output .split('\n') .map((line) => line.trim()) .filter(Boolean); } function toDisplaySize(bytes) { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } function isBlockedAssetPath(filePath) { return blockedAssetPrefixes.some((prefix) => filePath.startsWith(prefix)); } function main() { const stagedPaths = getStagedPaths(); const violations = []; for (const relativePath of stagedPaths) { const absolutePath = path.join(repoRoot, relativePath); let stats; try { stats = statSync(absolutePath); } catch { continue; } if (!stats.isFile()) continue; const baseName = path.basename(relativePath); const isTmpCapture = tmpCapturePattern.test(baseName); const isBinaryAsset = binaryAssetPattern.test(relativePath); const isOversizedBinary = isBinaryAsset && stats.size > maxBinarySizeBytes; const isBlockedAsset = isBinaryAsset && isBlockedAssetPath(relativePath); if (!isTmpCapture && !isOversizedBinary && !isBlockedAsset) continue; const reasons = []; if (isTmpCapture) reasons.push('temporary capture file'); if (isBlockedAsset) reasons.push('asset path under public/assets'); if (isOversizedBinary) { reasons.push(`binary file exceeds ${toDisplaySize(maxBinarySizeBytes)}`); } violations.push({ relativePath, size: stats.size, reasons, }); } if (violations.length === 0 || allowAssetCommit) { return; } console.error(''); console.error('Blocked commit: staged asset files need an explicit override.'); console.error('Set ALLOW_ASSET_COMMIT=1 only when the asset commit is intentional.'); console.error(''); for (const violation of violations) { console.error( `- ${violation.relativePath} (${toDisplaySize(violation.size)}): ${violation.reasons.join(', ')}` ); } console.error(''); process.exit(1); } main();