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>
This commit is contained in:
212
scripts/sync-libs.mjs
Normal file
212
scripts/sync-libs.mjs
Normal file
@@ -0,0 +1,212 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user