#!/usr/bin/env node // Refresh docs/LIBS.md from the live crema-manifest. Reads this project's // tsconfig.json + app.css to figure out which libs/themes are "wired in", // clones the manifest, and emits a markdown table for each (wired vs // available). Run after `crema add `, or whenever you want a fresh // catalog snapshot for the development LLM to consult. // // Usage: npm run sync-libs // // The output is intentionally compact — terse one-line descriptions, alpha // sort, no front-matter. CLAUDE.md points at LIBS.md so the LLM finds it. import { execSync } from "node:child_process" import { mkdtempSync, readFileSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" import { join, resolve, dirname } from "node:path" import { fileURLToPath } from "node:url" const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..") const MANIFEST_REPO = "https://git.sky-ai.com/CremaUIStudio/crema-manifest.git" function readJson(path) { return JSON.parse(readFileSync(path, "utf8")) } function readJsonc(path) { // tsconfig.json sometimes has // and /* */ comments. Strip them, but only // outside strings (so https:// URLs inside string values survive). const raw = readFileSync(path, "utf8") let out = "" let i = 0 let inString = false while (i < raw.length) { const c = raw[i] const c2 = raw[i + 1] if (inString) { out += c if (c === "\\" && i + 1 < raw.length) { out += raw[i + 1] i += 2 continue } if (c === '"') inString = false i++ continue } if (c === '"') { inString = true out += c i++ continue } if (c === "/" && c2 === "/") { while (i < raw.length && raw[i] !== "\n") i++ continue } if (c === "/" && c2 === "*") { i += 2 while (i < raw.length - 1 && !(raw[i] === "*" && raw[i + 1] === "/")) i++ i += 2 continue } out += c i++ } return JSON.parse(out) } function cloneManifest() { const dir = mkdtempSync(join(tmpdir(), "crema-manifest-")) execSync(`git clone --depth 1 --quiet ${MANIFEST_REPO} ${dir}`, { stdio: "ignore", }) return readJson(join(dir, "manifest.json")) } function detectWiredLibs(tsconfigPath) { const ts = readJsonc(tsconfigPath) const paths = ts?.compilerOptions?.paths ?? {} const wired = new Set() for (const key of Object.keys(paths)) { const m = /^@crema\/([a-z0-9-]+)$/.exec(key) if (m) wired.add(m[1]) } return [...wired].sort() } function detectWiredThemes(cssPath) { const css = readFileSync(cssPath, "utf8") const re = /@import\s+["']\.\.\/\.\.\/lib-theme-([a-z0-9-]+)\/theme\.css["']/g const wired = new Set() let m while ((m = re.exec(css)) !== null) wired.add(m[1]) return [...wired].sort() } function row(entry) { const desc = (entry.description ?? "").replace(/\s+/g, " ").trim() const oneline = desc.length > 140 ? desc.slice(0, 137) + "…" : desc return `| \`${entry.name}\` | \`${entry.alias ?? `@crema/${entry.name}`}\` | ${oneline} |` } function themeRow(entry) { const desc = (entry.description ?? "").replace(/\s+/g, " ").trim() const oneline = desc.length > 160 ? desc.slice(0, 157) + "…" : desc return `| \`${entry.name}\` | ${entry.darkMode ?? "—"} | ${oneline} |` } function buildMarkdown(manifest, wiredLibs, wiredThemes) { const libs = [...(manifest.libs ?? [])].sort((a, b) => a.name.localeCompare(b.name), ) const themes = [...(manifest.themes ?? [])].sort((a, b) => a.name.localeCompare(b.name), ) const wiredSet = new Set(wiredLibs) const wiredThemeSet = new Set(wiredThemes) const wiredLibEntries = libs.filter((l) => wiredSet.has(l.name)) const availableLibEntries = libs.filter((l) => !wiredSet.has(l.name)) const wiredThemeEntries = themes.filter((t) => wiredThemeSet.has(t.name)) const availableThemeEntries = themes.filter((t) => !wiredThemeSet.has(t.name)) const wiredOnlyNotInManifest = wiredLibs.filter( (n) => !libs.some((l) => l.name === n), ) const lines = [] lines.push( `# Available Crema libs`, ``, `> Generated by \`scripts/sync-libs.mjs\` from crema-manifest@${manifest.version}.`, `> Run \`npm run sync-libs\` to refresh.`, ``, `Every \`@crema/*-ui\` lib is its own git repo at`, `\`https://git.sky-ai.com/CremaUIStudio/lib--ui\`. To add one, clone it`, `as a sibling of this project, then add the tsconfig path entry and the`, `\`@source\` line to \`app/app.css\` (the marker comments make this easy).`, ``, `## Wired in this project (${wiredLibEntries.length})`, ``, `These are importable from your code right now (\`import { … } from "@crema/"\`):`, ``, `| Lib | Alias | Purpose |`, `|---|---|---|`, ...wiredLibEntries.map(row), ``, ) if (wiredOnlyNotInManifest.length) { lines.push( `> ⚠ Wired in tsconfig but not in the manifest catalog: ${wiredOnlyNotInManifest.map((n) => `\`${n}\``).join(", ")}. Probably an internal or experimental lib.`, ``, ) } lines.push( `## Active theme${wiredThemeEntries.length === 1 ? "" : "s"}`, ``, `| Theme | Dark mode | Purpose |`, `|---|---|---|`, ...(wiredThemeEntries.length ? wiredThemeEntries.map(themeRow) : ["| (none detected) | — | — |"]), ``, `## Available to add (${availableLibEntries.length})`, ``, `From the manifest. To wire one in: \`crema add \` (CLI), or`, `manually clone the repo + edit \`tsconfig.json\` paths + \`app/app.css\``, `\`@source\`.`, ``, `| Lib | Alias | Purpose |`, `|---|---|---|`, ...availableLibEntries.map(row), ``, ) if (availableThemeEntries.length) { lines.push( `## Other themes (${availableThemeEntries.length})`, ``, `Swap by changing the \`@import "../../lib-theme-/theme.css"\` line at the top of \`app/app.css\`.`, ``, `| Theme | Dark mode | Purpose |`, `|---|---|---|`, ...availableThemeEntries.map(themeRow), ``, ) } return lines.join("\n") } function main() { console.log("→ cloning manifest…") const manifest = cloneManifest() console.log(` manifest@${manifest.version} (${(manifest.libs ?? []).length} libs, ${(manifest.themes ?? []).length} themes)`) const wiredLibs = detectWiredLibs(join(ROOT, "tsconfig.json")) console.log(`→ wired libs: ${wiredLibs.length} (${wiredLibs.join(", ") || "(none)"})`) const wiredThemes = detectWiredThemes(join(ROOT, "app", "app.css")) console.log(`→ wired themes: ${wiredThemes.length} (${wiredThemes.join(", ") || "(none)"})`) const md = buildMarkdown(manifest, wiredLibs, wiredThemes) const outPath = join(ROOT, "docs", "LIBS.md") writeFileSync(outPath, md + "\n") console.log(`→ wrote ${outPath} (${md.length} chars)`) } main()