Files
ai-code-app/scripts/guard-staged-assets.mjs

96 lines
2.6 KiB
JavaScript
Executable File

#!/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();