Files
crema-app-aifirst-template/scripts/sync-libs.mjs
jules 8cd58052dd feat: scripts/sync-libs.mjs + docs/LIBS.md
Generates a compact lib catalog from the live crema-manifest, sorted into
"wired in this project" and "available to add" tables. The development LLM
(Claude Code, Cursor, etc.) reads this when answering "is there a lib for
X" — saves it from re-deriving the answer or making one up.

The script reads the project's tsconfig.json paths (for wired libs) and
app/app.css @import lines (for the active theme), clones the manifest, and
emits docs/LIBS.md with stable formatting.

CLAUDE.md updated to point at LIBS.md (was pointing directly at the live
manifest URL — slower for an LLM that wants a quick scan).

Run after `crema add <name>` or whenever you want a refresh:

  npm run sync-libs

Auto-detects:
- Wired libs from tsconfig.json @crema/* paths
- Active theme from app/app.css `@import "../../lib-theme-*/theme.css"` lines

Output is intentionally compact — terse one-line descriptions, alpha sort,
no front-matter. ~9KB for the current 5-wired / 50-available split.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 18:47:40 +10:00

213 lines
6.7 KiB
JavaScript

#!/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 <lib>`, 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-<name>-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/<name>"\`):`,
``,
`| 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 <name>\` (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-<name>/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()