#!/usr/bin/env node import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; const FILENAME = fileURLToPath(import.meta.url); const REPO_ROOT = path.resolve(FILENAME, "..", ".."); const ROUTES_DIR = path.join(REPO_ROOT, "apps", "web", "app", "routes"); export const ROUTE_INVENTORY_BUDGETS = Object.freeze({ admin: 167, tenant: 78, public_help_docs_internal: 58, optional: 208, total: 474, }); const OPTIONAL_ROUTE_PATTERNS = Object.freeze([ /^help(\/|$)/, /^mcp(\/|$)/, /^internal\/api\/__workflows(\/|$)/, /^admin\/?__workflows(\/|$)/, /^admin\/ai(\/|$)/, /^admin\/chat(\/|$)/, /^admin\/crm(\.|\/|$)/, /^admin\/email-marketing(\.|\/|$)/, /^admin\/help-desk(\.|\/|$)/, /^admin\/knowledge-base(\.|\/|$)/, /^admin\/portals(\.|\/|$)/, /^admin\/prompts(\.|\/|$)/, /^app\.\$tenant\/__widgets(\/|$)/, /^app\.\$tenant\/__workflows(\/|$)/, /^app\.\$tenant\/chat(\/|$)/, /^app\.\$tenant\/crm(\.|\/|$)/, /^app\.\$tenant\/email-marketing(\.|\/|$)/, /^app\.\$tenant\/internal\/ai(\/|$)/, /^app\.\$tenant\/__portals(\/|$)/, /^app\.\$tenant\/sites(\/|$)/, ]); function isRouteFile(fileName) { return /\.(ts|tsx)$/.test(fileName) && !/\.test\.(ts|tsx)$/.test(fileName) && !/\.spec\.(ts|tsx)$/.test(fileName) && !/\.server\.(ts|tsx)$/.test(fileName); } async function walkRoutes(dir = ROUTES_DIR) { const entries = await fs.readdir(dir, { withFileTypes: true }); const files = []; for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { files.push(...(await walkRoutes(fullPath))); } else if (entry.isFile() && isRouteFile(entry.name)) { files.push(path.relative(ROUTES_DIR, fullPath).split(path.sep).join("/")); } } return files.sort((left, right) => left.localeCompare(right)); } export function classifyRouteSurface(relativePath) { if (OPTIONAL_ROUTE_PATTERNS.some((pattern) => pattern.test(relativePath))) { return "optional"; } if (relativePath.startsWith("admin") || relativePath.startsWith("admin/")) { return "admin"; } if (relativePath.startsWith("app.$tenant")) { return "tenant"; } return "public_help_docs_internal"; } export function classifyRouteModule(relativePath) { const surface = classifyRouteSurface(relativePath); if (surface === "optional") { return "optional"; } if (surface === "admin" || surface === "tenant") { return "default-enabled"; } return "core"; } export async function scanRouteInventory() { const files = await walkRoutes(); const surfaces = { admin: 0, tenant: 0, public_help_docs_internal: 0, optional: 0, total: files.length }; const classifications = { core: 0, "default-enabled": 0, optional: 0 }; for (const file of files) { surfaces[classifyRouteSurface(file)] += 1; classifications[classifyRouteModule(file)] += 1; } return { files, surfaces, classifications }; } export function evaluateRouteInventory(inventory, budgets = ROUTE_INVENTORY_BUDGETS) { return Object.entries(budgets).map(([surface, budget]) => { const count = inventory.surfaces[surface] ?? 0; return { surface, count, budget, pass: count <= budget }; }); } export async function runRouteInventoryGate({ logger = console } = {}) { const inventory = await scanRouteInventory(); const results = evaluateRouteInventory(inventory); const failures = results.filter((result) => !result.pass); for (const result of results) { const writer = result.pass ? logger.log : logger.error; writer(`${result.pass ? "PASS" : "FAIL"} route_inventory surface=${result.surface} count=${result.count} budget=${result.budget}`); } logger.log( `Route classifications: core=${inventory.classifications.core} default-enabled=${inventory.classifications["default-enabled"]} optional=${inventory.classifications.optional}` ); logger.log(`Summary: ${results.length - failures.length} passed, ${failures.length} failed`); return { inventory, results, failures }; } async function main() { const outcome = await runRouteInventoryGate(); if (outcome.failures.length > 0) { process.exit(1); } } const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === FILENAME; if (invokedDirectly) { main().catch((error) => { // eslint-disable-next-line no-console -- CLI gate must surface fatal errors on stderr. console.error(error); process.exit(1); }); }