First cut

This commit is contained in:
kadil
2026-04-17 16:08:31 -05:00
parent d10105ac00
commit 4ee4cb8e7c
58 changed files with 3243 additions and 1 deletions

View File

@@ -0,0 +1,68 @@
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { stringifyCanonical } from '@dynamic-sites/shared';
import { writeAuditLog } from '../db.js';
import { logger } from '../logger.js';
const REPO_ROOT = process.env.REPO_ROOT || '.';
const MAX_BACKUPS = 20;
function ensureDir(dir: string) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
function fileHash(content: string): string {
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 12);
}
/**
* Atomic write of canonical JSON to a repo-relative path.
* Creates a pre-write backup and audit log entry.
*/
export function writeContentFile(
repoRelativePath: string,
data: unknown,
opts?: { proposalId?: string; source?: string }
): void {
const absPath = path.join(REPO_ROOT, repoRelativePath);
const canonical = stringifyCanonical(data);
// Pre-write backup
let beforeHash: string | undefined;
if (fs.existsSync(absPath)) {
const existing = fs.readFileSync(absPath, 'utf-8');
beforeHash = fileHash(existing);
const backupDir = path.join(REPO_ROOT, 'content', '.backups', repoRelativePath);
ensureDir(backupDir);
const ts = new Date().toISOString().replace(/[:.]/g, '-');
fs.copyFileSync(absPath, path.join(backupDir, `${ts}.json`));
// Prune old backups
const backups = fs.readdirSync(backupDir).sort();
while (backups.length > MAX_BACKUPS) {
const oldest = backups.shift()!;
fs.unlinkSync(path.join(backupDir, oldest));
}
}
// Atomic write: temp file + rename
ensureDir(path.dirname(absPath));
const tmpPath = absPath + '.tmp.' + process.pid;
fs.writeFileSync(tmpPath, canonical, 'utf-8');
fs.renameSync(tmpPath, absPath);
const afterHash = fileHash(canonical);
// Audit log
writeAuditLog({
proposalId: opts?.proposalId,
repoRelativePath,
beforeHash,
afterHash,
source: opts?.source || 'http',
});
logger.info({ event: 'content.written', path: repoRelativePath, size: canonical.length }, 'Content file written');
}