← back to Site Factory
critic/checks/architect.js
81 lines
// architect.js — site structure check.
// For Next.js site dirs (paths containing /app/ at depth 1), verify the
// Steve-standard scaffolding is present.
const fs = require('fs');
const path = require('path');
const SOURCE = 'architect';
function isNextSiteRoot(p) {
// a "site root" has an `app/` subdirectory directly under it
try {
const st = fs.statSync(path.join(p, 'app'));
return st.isDirectory();
} catch {
return false;
}
}
// expand paths to a list of candidate site roots
function findSiteRoots(paths) {
const roots = new Set();
for (const p of paths || []) {
if (!p) continue;
let stat;
try { stat = fs.statSync(p); } catch { continue; }
if (!stat.isDirectory()) continue;
if (isNextSiteRoot(p)) {
roots.add(p);
continue;
}
// also scan one level deep — the orchestrator may pass the parent
let entries = [];
try { entries = fs.readdirSync(p, { withFileTypes: true }); } catch {}
for (const e of entries) {
if (!e.isDirectory()) continue;
const sub = path.join(p, e.name);
if (isNextSiteRoot(sub)) roots.add(sub);
}
}
return [...roots];
}
function fileExists(p) {
try { return fs.statSync(p).isFile(); } catch { return false; }
}
const REQUIRED = [
{ rel: 'app/layout.tsx', severity: 'high', title: 'Missing app/layout.tsx',
fix: 'Create a Next.js app-router root layout at app/layout.tsx (html/body, font, providers).' },
{ rel: 'components/HamburgerPanel.tsx', severity: 'medium', title: 'Missing components/HamburgerPanel.tsx',
fix: 'Add the standard top-right hamburger → right-slide panel component (About / Contact / What it does / Who it’s for / Pricing).' },
{ rel: '.env.example', severity: 'medium', title: 'Missing .env.example',
fix: 'Add .env.example documenting required env vars (no real secrets). Helps new envs / CI / Vercel deploys.' },
{ rel: 'tailwind.config.ts', severity: 'low', title: 'Missing tailwind.config.ts',
fix: 'Add tailwind.config.ts wired to the site palette tokens (primary/secondary/accent/surface/ink).' },
];
async function run({ paths /* , screenshots, domain */ }) {
const findings = [];
const roots = findSiteRoots(paths);
for (const root of roots) {
for (const req of REQUIRED) {
const target = path.join(root, req.rel);
if (!fileExists(target)) {
findings.push({
source: SOURCE,
severity: req.severity,
title: `${req.title} at ${root}`,
detail: `Expected ${target} not found.`,
suggested_fix: req.fix,
});
}
}
}
return findings;
}
module.exports = { run };