#!/usr/bin/env node import { readFile, readdir, writeFile } from "node:fs/promises"; import { dirname, extname, join } from "node:path"; import { fileURLToPath } from "node:url"; const repoRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); const sitemapPath = join(repoRoot, "system/public/sitemap.html"); const disksPath = join(repoRoot, "system/public/aesthetic.computer/disks"); const functionsPath = join(repoRoot, "system/netlify/functions"); const publicEndpoints = [ ["POST", "store-kidlisp", "Publish KidLisp source"], ["POST", "store-clock", "Publish a clock melody"], ["GET", "chat-messages", "Read and search public chat"], ["POST", "store-piece", "Publish a JavaScript piece"], ["POST", "track-media", "Register uploaded media"], ]; function escapeHTML(value) { return value .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } function routeHref(prefix, name) { return `${prefix}/${name.split("/").map(encodeURIComponent).join("/")}`; } function routeList(names, prefix) { return names .map((name) => { const label = escapeHTML(name); const href = escapeHTML(routeHref(prefix, name)); return `
${label}
`; }) .join("\n"); } function hasHandler(source) { return /export\s+(?:(?:async\s+)?function|const|let|var)\s+handler\b|export\s+default\s+|exports\.handler\s*=|module\.exports\s*=/.test(source); } const diskFiles = (await readdir(disksPath)) .filter((name) => [".mjs", ".lisp"].includes(extname(name))) .sort((a, b) => a.localeCompare(b)); const javascriptDisks = diskFiles .filter((name) => name.endsWith(".mjs")) .map((name) => name.slice(0, -4)); const kidlispDisks = diskFiles .filter((name) => name.endsWith(".lisp")) .map((name) => name.slice(0, -5)); const javascriptRoutes = new Set(javascriptDisks); const kidlispOnlyDisks = kidlispDisks.filter((name) => !javascriptRoutes.has(name)); const diskRouteCount = javascriptDisks.length + kidlispOnlyDisks.length; const functionFiles = (await readdir(functionsPath)) .filter((name) => [".mjs", ".js"].includes(extname(name))); const functionHandlers = new Set(); for (const file of functionFiles) { const source = await readFile(join(functionsPath, file), "utf8"); if (hasHandler(source)) functionHandlers.add(file.slice(0, -extname(file).length)); } const handlers = [...functionHandlers].sort((a, b) => a.localeCompare(b)); const supportedNames = new Set(publicEndpoints.map(([, name]) => name)); const internalHandlers = handlers.filter((name) => !supportedNames.has(name)); const diskInventory = `
JavaScript (${javascriptDisks.length})
${routeList(javascriptDisks, "")}
KidLisp-only (${kidlispOnlyDisks.length})
${routeList(kidlispOnlyDisks, "")}
Platform & legacy routes
support
privacy-policy.html
aesthetic-direct
bundle
dollhouse
`; const supportedRoutes = publicEndpoints .map(([method, name, description]) => `
${method} /api/${name} — ${description}
`) .join("\n"); const apiCard = `
API api.aesthetic.computer ${publicEndpoints.length} supported endpoints ยท ${handlers.length} handlers
The public contract is documented at api.aesthetic.computer and available as JSON. Lith also registers product, administration, and compatibility handlers under /api/:fn; those are not all stable public integrations.
Supported public HTTP API
${supportedRoutes}
GET /api?format=json — Machine-readable reference
Protocol and platform references
mcp — remote MCP server
docs — piece API
docs.json — piece API JSON
Other registered handlers (${internalHandlers.length})
${routeList(internalHandlers, "/api")}
`; let html = await readFile(sitemapPath, "utf8"); html = html .replace(//, ``) .replace(/
[\s\S]*?<\/div>\n\n
Disk Routes: ${diskRouteCount}
JavaScript Files: ${javascriptDisks.length}
KidLisp Files: ${kidlispDisks.length}
Function Handlers: ${handlers.length}
(?:~?\d+) disk routes<\/span>/, `${diskRouteCount} disk routes`) .replace(/(
The main platform\.[\s\S]*?<\/div>\n)[\s\S]*?(
Dynamic Route Patterns<\/div>)/, `$1${diskInventory}$2`) .replace(/[\s\S]*?(?=)/, apiCard); if (process.argv.includes("--check")) { const current = await readFile(sitemapPath, "utf8"); if (current !== html) { console.error("sitemap.html is out of date; run npm run sitemap:update"); process.exitCode = 1; } else { console.log(`sitemap.html is current: ${diskRouteCount} disk routes, ${handlers.length} function handlers`); } } else { await writeFile(sitemapPath, html); console.log(`Updated sitemap.html: ${diskRouteCount} disk routes, ${handlers.length} function handlers`); }