#!/usr/bin/env node // momme.world site generator. // // commands/*.md is the single source of content. Each file describes one // command: front matter sets metadata, the body is what it prints. // // filename -> command name (underscores become spaces) // name: -> overrides the filename-derived name // help: -> one-liner for the help listing // params: -> argument placeholder shown in help (e.g. "") // prefix: true -> command accepts anything after its name // suggestion: true -> chip button under the terminal + tailnet MOTD hint // hidden: true -> works, but stays out of help // text: true -> included in the plain-text page (curl) // ${args} ${date} ${time} ${ls} ${help} -> resolved when the command runs // // Output goes to k8s/site/ and is COMMITTED: ArgoCD serves those files // straight from the repo, so `npm run build` is part of editing the site. import { readFileSync, writeFileSync, readdirSync, mkdirSync } from "node:fs"; import { join } from "node:path"; import { execSync } from "node:child_process"; const root = new URL("..", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1"); const P = (p) => join(root, p); // ---------- read commands ---------- const read = (file) => { const raw = readFileSync(P("commands/" + file), "utf8").replace(/\r\n/g, "\n"); const m = raw.match(/^---\n([\s\S]*?)\n---\n\n?([\s\S]*)$/); if (!m) throw new Error(`${file}: missing or malformed front matter`); const meta = {}; for (const line of m[1].split("\n")) { const [key, ...rest] = line.split(":"); if (key) meta[key.trim()] = rest.join(":").trim().replace(/^["']|["']$/g, ""); } const name = meta.name || file.replace(/\.md$/, "").replace(/_/g, " "); return { name, help: meta.help ?? "", params: meta.params ?? "", prefix: meta.prefix === "true", suggestion: meta.suggestion === "true", hidden: meta.hidden === "true", text: meta.text === "true", body: m[2].trimEnd(), }; }; const files = readdirSync(P("commands")).filter((f) => f.endsWith(".md")).sort(); const commands = files.map(read); for (const c of commands) { for (const other of commands) { if (other !== c && other.name === c.name) throw new Error(`duplicate command name: ${c.name}`); } } if (new Set(commands.map((c) => c.name.split(" ")[0])).size !== new Set(commands.filter((c) => !c.name.includes(" ")).map((c) => c.name)).size) { // multi-word commands ("project x") must not collide with first words const firsts = new Set(commands.filter((c) => !c.name.includes(" ")).map((c) => c.name)); for (const c of commands.filter((c) => c.name.includes(" "))) { if (firsts.has(c.name.split(" ")[0])) throw new Error(`name collision: ${c.name} vs ${c.name.split(" ")[0]}`); } } // ---------- index.html ---------- const chips = commands .filter((c) => c.suggestion && !c.hidden) .map((c) => ` `) .join("\n"); const banner = readFileSync(P("templates/banner.txt"), "utf8").replace(/\s+$/, ""); const template = readFileSync(P("src/index.html"), "utf8"); for (const [ph, val] of [["", chips], ["", banner]]) { if (!template.includes(ph)) throw new Error(`src/index.html: ${ph} placeholder missing`); } writeFileSync( P("k8s/site/index.html"), template.replace("", chips).replace("", banner) ); // ---------- commands.json (web runtime + telnet both read this) ---------- const json = JSON.stringify({ banner, commands }, null, 1) + "\n"; writeFileSync(P("k8s/site/commands.json"), json); // ---------- hello.txt (what curl sees) ---------- const sections = commands .filter((c) => c.text) .map((c) => { const body = c.body.replace(/\$\{help\}|\$\{ls\}/g, ""); return `${c.name}\n${"=".repeat(c.name.length)}\n${body}`.trimEnd(); }); writeFileSync( P("k8s/site/hello.txt"), banner + "\n\n" + sections.join("\n\n") + "\n\nThe interactive version of this page is at https://momme.world/\n" ); // ---------- copy runtimes ---------- writeFileSync(P("k8s/site/site.js"), readFileSync(P("src/site.js"), "utf8")); writeFileSync(P("k8s/site/telnet.py"), readFileSync(P("src/telnet.py"), "utf8")); // ---------- tailwind ---------- try { execSync("npx --yes @tailwindcss/cli@4 -i src/input.css -o k8s/site/style.css --minify", { cwd: root, stdio: "inherit" }); } catch { console.error("tailwind build failed — is npm install done? (style.css stays as it was)"); process.exit(1); } const words = (n) => String(n).padStart(2); console.log(`built ${words(files.length)} commands -> k8s/site/ (${chips ? commands.filter((c) => c.suggestion).length + " chips" : "no chips"})`);