| 1 | #!/usr/bin/env node |
| 2 | // 🤖 Generates the NATIVE seed catalogue — appCatalogReport() for every |
| 3 | // built-in app, as the JSON the iOS/Mac apps bundle |
| 4 | // (ios/LChatCore/Sources/LChatCore/Seed/app-catalog.json, decoded by |
| 5 | // AppRegistry). The seed is what lets native know every built-in app's |
| 6 | // manifest facts (title, glyph, places, presentation, commands) with ZERO |
| 7 | // per-app native code before any webview has reported the live catalogue; |
| 8 | // the runtime appCatalog() pull supersedes it per space (custom + member |
| 9 | // apps only exist there). Run after adding or changing an app: |
| 10 | // |
| 11 | // node tools/gen-app-catalog.mjs --write # regenerate the bundled seed |
| 12 | // node tools/gen-app-catalog.mjs # print to stdout |
| 13 | // |
| 14 | // test/appcatalog-seed.test.mjs pins the committed seed against this |
| 15 | // generator's output, so a drifted seed fails `npm test` — the same |
| 16 | // drift-gate shape as the schema copies (test/schema-engine.test.mjs). |
| 17 | import { readdirSync, writeFileSync, readFileSync } from "node:fs"; |
| 18 | import { fileURLToPath } from "node:url"; |
| 19 | import path from "node:path"; |
| 20 | |
| 21 | const root = path.dirname(path.dirname(fileURLToPath(import.meta.url))); |
| 22 | const appsDir = path.join(root, "web", "static", "apps"); |
| 23 | export const seedPath = path.join( |
| 24 | root, "ios", "LChatCore", "Sources", "LChatCore", "Seed", "app-catalog.json"); |
| 25 | |
| 26 | // appstudio.js needs a real DOM at module scope (it builds its editor |
| 27 | // eagerly), so it can't load headless. It registers no places and no |
| 28 | // commands — nothing a native picker or router reads — so the seed simply |
| 29 | // omits it; spaces that install it still report it via the live pull. |
| 30 | const SKIP = new Set(["appstudio.js"]); |
| 31 | |
| 32 | export async function collect() { |
| 33 | const files = readdirSync(appsDir) |
| 34 | .filter((f) => f.endsWith(".js") && !SKIP.has(f)) |
| 35 | .sort(); |
| 36 | for (const f of files) await import(path.join(appsDir, f)); |
| 37 | const { appCatalogReport } = await import(path.join(appsDir, "..", "apps.js")); |
| 38 | // Sorted by name so regenerating is a stable diff. |
| 39 | return appCatalogReport().sort((a, b) => a.name.localeCompare(b.name)); |
| 40 | } |
| 41 | |
| 42 | const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); |
| 43 | if (isMain) { |
| 44 | const json = JSON.stringify(await collect(), null, 2) + "\n"; |
| 45 | if (process.argv.includes("--write")) { |
| 46 | const before = (() => { try { return readFileSync(seedPath, "utf8"); } catch { return ""; } })(); |
| 47 | writeFileSync(seedPath, json); |
| 48 | console.log(before === json ? `unchanged: ${seedPath}` : `wrote ${seedPath}`); |
| 49 | } else { |
| 50 | process.stdout.write(json); |
| 51 | } |
| 52 | } |
| 53 | |