import fs from 'fs';
import path from 'path';
import { computeKPIs } from '../kpis/compute.js';
import { evaluateBudgets } from '../policies/check_budgets.js';
const DEFAULT_INDEX = 'data/timemachine/index.json';
const SAFE_RELATIVE_PATHS = [
'sites/blackroad/public',
'docs',
'pulses',
'data',
'frontend/public',
];
function escapeRegExp(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function createGuard(projectRoot) {
const safeRoots = SAFE_RELATIVE_PATHS.map((relative) => path.resolve(projectRoot, relative));
return function assertSafe(targetPath) {
const resolved = path.resolve(targetPath);
for (const safeRoot of safeRoots) {
if (resolved.startsWith(safeRoot)) {
return;
}
}
throw new Error(`Path ${resolved} is outside the SAFE_PATHS whitelist.`);
};
}
function ensureDirectory(filePath) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
}
function ensurePreload(projectRoot, assertSafe) {
const htmlPath = path.resolve(projectRoot, 'sites/blackroad/public/index.html');
if (!fs.existsSync(htmlPath)) return false;
assertSafe(htmlPath);
const original = fs.readFileSync(htmlPath, 'utf-8');
const imgMatch = original.match(/
]*src="([^"]+)"[^>]*>/i);
if (!imgMatch) return false;
const [imgTag, src] = imgMatch;
let updated = original;
if (!/fetchpriority=/i.test(imgTag)) {
const replacement = imgTag.replace('
`;
const preloadRegex = new RegExp(`]*rel="preload"[^>]*href="${escapeRegExp(src)}"`, 'i');
if (!preloadRegex.test(updated)) {
updated = updated.replace('', ` ${preloadTag}\n`);
}
if (updated !== original) {
fs.writeFileSync(htmlPath, updated, 'utf-8');
return true;
}
return false;
}
function ensureCacheHeaders(projectRoot, assertSafe) {
const headersPath = path.resolve(projectRoot, 'sites/blackroad/public/_headers');
assertSafe(headersPath);
const desiredBlocks = [
'/*\n Cache-Control: public, max-age=600\n',
'/assets/*\n Cache-Control: public, max-age=31536000, immutable\n',
];
let content = '';
if (fs.existsSync(headersPath)) {
content = fs.readFileSync(headersPath, 'utf-8');
}
let changed = false;
for (const block of desiredBlocks) {
if (!content.includes(block)) {
content = `${content.trim()}\n${block}`.trim() + '\n';
changed = true;
}
}
if (changed || !fs.existsSync(headersPath)) {
ensureDirectory(headersPath);
fs.writeFileSync(headersPath, `${content.trim()}\n`, 'utf-8');
return true;
}
return false;
}
function listHtmlFiles(baseDir) {
if (!fs.existsSync(baseDir)) return [];
const files = [];
const stack = [baseDir];
while (stack.length) {
const current = stack.pop();
const entries = fs.readdirSync(current, { withFileTypes: true });
for (const entry of entries) {
const entryPath = path.join(current, entry.name);
if (entry.isDirectory()) {
stack.push(entryPath);
} else if (entry.name.endsWith('.html')) {
files.push(entryPath);
}
}
}
return files;
}
function ensureAltAttributes(projectRoot, assertSafe) {
const publicDir = path.resolve(projectRoot, 'sites/blackroad/public');
if (!fs.existsSync(publicDir)) return false;
const files = listHtmlFiles(publicDir);
let changed = false;
for (const filePath of files) {
assertSafe(filePath);
const original = fs.readFileSync(filePath, 'utf-8');
const updated = original.replace(/
]*?)>/gi, (match) => {
if (/alt\s*=/.test(match)) {
return match;
}
const selfClosing = match.endsWith('/>');
const suffix = selfClosing ? '/>' : '>';
const trimmed = match.slice(0, match.length - suffix.length);
return `${trimmed} alt=""${suffix}`;
});
if (updated !== original) {
fs.writeFileSync(filePath, updated, 'utf-8');
changed = true;
}
}
return changed;
}
function refreshSitemap(projectRoot, assertSafe) {
const sitemapPath = path.resolve(projectRoot, 'sites/blackroad/public/sitemap.xml');
const robotsPath = path.resolve(projectRoot, 'sites/blackroad/public/robots.txt');
const today = new Date().toISOString().split('T')[0];
let changed = false;
if (fs.existsSync(sitemapPath)) {
assertSafe(sitemapPath);
const original = fs.readFileSync(sitemapPath, 'utf-8');
let updated = original;
if (/[^<]*<\/lastmod>/i.test(original)) {
updated = original.replace(/[^<]*<\/lastmod>/i, `${today}`);
} else {
updated = original.replace('', ` ${today}\n`);
}
if (updated !== original) {
fs.writeFileSync(sitemapPath, updated, 'utf-8');
// tools/autobuilder/make_fixes.js
// Usage: node tools/autobuilder/make_fixes.js --index data/timemachine/index.json
import fs from "node:fs";
import path from "node:path";
const args = Object.fromEntries(process.argv.slice(2).map((x,i,arr)=>{
if (x.startsWith("--")) return [x.replace(/^--/,""), arr[i+1] && !arr[i+1].startsWith("--") ? arr[i+1] : true];
return [null,null];
}).filter(Boolean));
const INDEX = args.index || "data/timemachine/index.json";
function ensureDir(p){ fs.mkdirSync(path.dirname(p), {recursive:true}); }
function log(action, payload={}){
const line = JSON.stringify({ts:new Date().toISOString(), action, ...payload});
ensureDir("data/aiops/actions.jsonl");
fs.appendFileSync("data/aiops/actions.jsonl", line+"\n");
}
function upsertFile(p, content, tag){
const exists = fs.existsSync(p);
fs.mkdirSync(path.dirname(p), {recursive:true});
fs.writeFileSync(p, content, "utf8");
log("write_file", {path:p, tag, existed:exists});
}
const index = JSON.parse(fs.readFileSync(INDEX, "utf8"));
//
// 1) HTML tweaks (index.html) — add fetchpriority + alt; add preload hint if missing.
//
const INDEX_HTML = "sites/blackroad/public/index.html";
if (fs.existsSync(INDEX_HTML)) {
let html = fs.readFileSync(INDEX_HTML, "utf8");
let changed = false;
// add fetchpriority="high" to first
if not present
html = html.replace(/
]*?)>/i, (m, attrs)=>{
if (/fetchpriority=/i.test(attrs)) return m;
changed = true;
if (!/alt=/i.test(attrs)) attrs += ' alt=""';
return `
`;
});
// add a generic preload for first stylesheet/script if no preload present
if (!/rel=["']preload["']/.test(html)) {
const mCss = html.match(/href=["']([^"']+\.css)["']/i);
const mJs = html.match(/src=["']([^"']+\.js)["']/i);
if (mCss) {
const link = ``;
html = html.replace(//i, `\n ${link}`);
changed = true;
} else if (mJs) {
const link = ``;
html = html.replace(//i, `\n ${link}`);
changed = true;
}
}
const robotsLine = 'Sitemap: /sitemap.xml';
assertSafe(robotsPath);
let robotsContent = '';
if (fs.existsSync(robotsPath)) {
robotsContent = fs.readFileSync(robotsPath, 'utf-8');
}
if (!robotsContent.includes(robotsLine)) {
robotsContent = `${robotsContent.trim()}\n${robotsLine}`.trim() + '\n';
ensureDirectory(robotsPath);
fs.writeFileSync(robotsPath, robotsContent, 'utf-8');
changed = true;
}
return changed;
}
function runBudgetCheck(indexPath) {
if (!indexPath || !fs.existsSync(indexPath)) {
return [];
}
const index = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
const report = computeKPIs(index);
return evaluateBudgets(report);
}
function parseArgs(argv) {
const options = {
indexPath: DEFAULT_INDEX,
projectRoot: process.cwd(),
skipBudgets: false,
};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === '--index') {
options.indexPath = argv[i + 1];
i += 1;
} else if (arg === '--root') {
options.projectRoot = argv[i + 1];
i += 1;
} else if (arg === '--no-budget-check') {
options.skipBudgets = true;
}
}
return options;
}
function main(argv) {
const options = parseArgs(argv);
const assertSafe = createGuard(options.projectRoot);
const actions = [];
if (ensurePreload(options.projectRoot, assertSafe)) actions.push('preload');
if (ensureCacheHeaders(options.projectRoot, assertSafe)) actions.push('cache-headers');
if (ensureAltAttributes(options.projectRoot, assertSafe)) actions.push('alt-text');
if (refreshSitemap(options.projectRoot, assertSafe)) actions.push('sitemap');
if (!actions.length) {
console.log('Autobuilder: no changes applied.');
} else {
console.log(`Autobuilder: applied fixes -> ${actions.join(', ')}`);
}
let exitCode = 0;
if (!options.skipBudgets) {
const issues = runBudgetCheck(options.indexPath ? path.resolve(options.indexPath) : null);
if (issues.length) {
exitCode = 1;
for (const issue of issues) {
console.error(`Budget violation: ${issue}`);
}
}
}
process.exit(exitCode);
}
if (import.meta.url === `file://${process.argv[1]}`) {
main(process.argv.slice(2));
}
if (changed) {
fs.writeFileSync(INDEX_HTML, html, "utf8");
log("modify_html", {path: INDEX_HTML});
}
}
//
// 2) Static hosting headers — write _headers for cache policy (safe)
//
const HEADERS = "sites/blackroad/public/_headers";
if (!fs.existsSync(HEADERS)) {
const content = `/*
Cache-Control: no-store
/assets/*
Cache-Control: public, max-age=604800, immutable
`;
upsertFile(HEADERS, content, "cache_headers");
}
//
// 3) robots.txt & sitemap.xml if missing
//
const ROBOTS = "sites/blackroad/public/robots.txt";
if (!fs.existsSync(ROBOTS)) {
upsertFile(ROBOTS, `User-agent: *\nAllow: /\nSitemap: https://blackroad.io/sitemap.xml\n`, "robots");
}
const SITEMAP = "sites/blackroad/public/sitemap.xml";
if (!fs.existsSync(SITEMAP)) {
upsertFile(SITEMAP, `
https://blackroad.io/
https://blackroad.io/portal
https://blackroad.io/status
`, "sitemap");
}
console.log("Autobuilder completed.");