Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: auto-register disk pieces via filesystem scan

docs.js now walks disks/ at cold start and synthesizes entries for
every .mjs / .lisp not already in the curated `pieces` map. Pieces
that export `meta()` are listed (hidden:false); others are registered
hidden but still resolvable at /docs/pieces:<name>. Curated entries
always win over auto-entries. Description is extracted from the piece
header comment. Cache is lifetime-of-process in prod, per-request in
dev. Closes the "new piece appears in list/autocomplete with no manual
docs.js edit" gap — this surfaces ~170 existing pieces that export
meta but lacked curated entries; to suppress, add `hidden: true` to
their curated entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+99 -5
+99 -5
system/netlify/functions/docs.js
··· 17 17 import { respond } from "../../backend/http.mjs"; 18 18 import { defaultTemplateStringProcessor as html } from "../../public/aesthetic.computer/lib/helpers.mjs"; 19 19 import { getCommandDescription } from "../../public/aesthetic.computer/lib/prompt-commands.mjs"; 20 + import fs from "node:fs"; 21 + import path from "node:path"; 22 + import { fileURLToPath } from "node:url"; 20 23 const dev = process.env.CONTEXT === "dev"; 21 24 const { keys } = Object; 25 + 26 + // 🔎 Auto-piece discovery 27 + // Walks the disks/ folder and synthesizes docs entries for any .mjs or .lisp 28 + // file that isn't already in the curated `pieces` object below. A piece is 29 + // auto-listed (hidden:false) if it exports a `meta()` — the template includes 30 + // this, so intentional pieces show up; throwaway scratch files without a 31 + // meta export remain hidden but are still resolvable via /docs/pieces:name. 32 + const DISKS_DIR = path.resolve( 33 + path.dirname(fileURLToPath(import.meta.url)), 34 + "../../public/aesthetic.computer/disks", 35 + ); 36 + 37 + const META_EXPORT_PATTERNS = [ 38 + /export\s*\{[^}]*\bmeta\b[^}]*\}/, 39 + /export\s+(async\s+)?function\s+meta\b/, 40 + /export\s+(const|let|var)\s+meta\b/, 41 + /export\s+default\s+function\s+meta\b/, 42 + ]; 43 + 44 + function extractHeaderDesc(src, commentStart) { 45 + const lines = src.split("\n"); 46 + const descLines = []; 47 + let sawFirstComment = false; 48 + for (const raw of lines) { 49 + const line = raw.trim(); 50 + if (!line) { 51 + if (descLines.length > 0) break; 52 + continue; 53 + } 54 + if (line.startsWith("/*")) break; 55 + if (!line.startsWith(commentStart)) break; 56 + let content = line.slice(commentStart.length).replace(/^\s+/, ""); 57 + if (!sawFirstComment) { 58 + sawFirstComment = true; 59 + continue; // skip "Name, YY.MM.DD…" header line 60 + } 61 + if (!content) break; 62 + if (/^(todo|readme|region|#region|#endregion)\b/i.test(content)) break; 63 + descLines.push(content); 64 + if (descLines.join(" ").length > 140) break; 65 + } 66 + let desc = descLines.join(" ").trim(); 67 + if (desc.length > 160) desc = desc.slice(0, 157) + "…"; 68 + return desc; 69 + } 70 + 71 + let AUTO_PIECES_CACHE = null; 72 + function scanAutoPieces() { 73 + if (AUTO_PIECES_CACHE && !dev) return AUTO_PIECES_CACHE; 74 + const out = {}; 75 + let entries; 76 + try { 77 + entries = fs.readdirSync(DISKS_DIR, { withFileTypes: true }); 78 + } catch (e) { 79 + console.warn("docs.js auto-scan: readdir failed", e.message); 80 + AUTO_PIECES_CACHE = out; 81 + return out; 82 + } 83 + for (const entry of entries) { 84 + if (!entry.isFile()) continue; 85 + const file = entry.name; 86 + if (file.startsWith("_") || file.startsWith(".")) continue; 87 + const ext = path.extname(file); 88 + if (ext !== ".mjs" && ext !== ".lisp") continue; 89 + const name = file.slice(0, -ext.length); 90 + let src = ""; 91 + try { 92 + src = fs.readFileSync(path.join(DISKS_DIR, file), "utf8"); 93 + } catch (e) { 94 + continue; 95 + } 96 + const commentStart = ext === ".lisp" ? ";" : "//"; 97 + const desc = extractHeaderDesc(src, commentStart); 98 + const hasMeta = 99 + ext === ".lisp" || META_EXPORT_PATTERNS.some((r) => r.test(src)); 100 + out[name] = { 101 + sig: name, 102 + desc, 103 + done: false, 104 + hidden: !hasMeta, 105 + auto: true, 106 + }; 107 + } 108 + AUTO_PIECES_CACHE = out; 109 + return out; 110 + } 111 + 112 + function mergeAutoPieces(curated) { 113 + const auto = scanAutoPieces(); 114 + for (const name in auto) { 115 + if (!curated[name]) curated[name] = auto[name]; 116 + } 117 + } 22 118 23 119 // GET A user's `sub` id from either their handle or email address. 24 120 export async function handler(event, context) { ··· 3703 3799 examples: ["camera", "camera:under"], 3704 3800 done: true, 3705 3801 }, 3706 - carry: { 3707 - sig: "carry", 3708 - desc: "Learn base 10 by feel — tap columns, watch ten become one.", 3709 - done: false, 3710 - }, 3711 3802 chat: { 3712 3803 sig: "chat", 3713 3804 desc: "Chat with handles.", ··· 4640 4731 } 4641 4732 }); 4642 4733 }; 4734 + 4735 + // Auto-register any disk piece not already in the curated `pieces` map. 4736 + mergeAutoPieces(docs.pieces); 4643 4737 4644 4738 fillCommandDocs(docs.prompts, "prompt"); 4645 4739 fillCommandDocs(docs.pieces, "piece");