/** * Sync catalog/index.json with filesystem state: * - Add brands that exist on disk but missing from catalog * - Remove brands in catalog but missing on disk (wipe cleanup) * - Then enrich-catalog runs over the synced list */ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'fs'; import { join } from 'path'; const ROOT = process.cwd(); const CATALOG_PATH = join(ROOT, 'catalog', 'index.json'); const EXTRACT_DIR = join(ROOT, 'extractions'); interface Brand { domain: string; hasDesignMd: boolean; hasTokens: boolean; hasScreenshots: boolean; hasFrontmatter: boolean; accent: string; description: string; sizeKb: number; category: string; dark: boolean; font: string; completeness: number; extractedAt: string; } function makeEntry(domain: string): Brand { return { domain, hasDesignMd: existsSync(join(EXTRACT_DIR, domain, 'DESIGN.md')), hasTokens: existsSync(join(EXTRACT_DIR, domain, 'tokens.json')), hasScreenshots: existsSync(join(EXTRACT_DIR, domain, 'screenshots')), hasFrontmatter: false, accent: '#5e6ad2', description: `Design system extracted from ${domain}`, sizeKb: 0, category: 'SaaS / Product', dark: false, font: 'Inter', completeness: 0, extractedAt: new Date().toISOString(), }; } async function main() { console.log('šŸ”„ Sync catalog/index.json with filesystem state\n'); const catalog = JSON.parse(readFileSync(CATALOG_PATH, 'utf-8')); const existing = new Map(catalog.brands.map((b: Brand) => [b.domain, b])); // List on-disk extractions with at least raw-css.json (real extraction, not orphan) const onDisk = readdirSync(EXTRACT_DIR) .filter(d => !d.startsWith('_')) .filter(d => statSync(join(EXTRACT_DIR, d)).isDirectory()) .filter(d => existsSync(join(EXTRACT_DIR, d, 'raw-css.json'))); console.log(` šŸ“ ${onDisk.length} extractions on disk (with raw-css.json)`); console.log(` šŸ“š ${existing.size} brands in catalog before sync`); // Add missing let added = 0; for (const domain of onDisk) { if (!existing.has(domain)) { existing.set(domain, makeEntry(domain)); added++; console.log(` + ${domain}`); } } // Remove ghost entries (catalog declares but no raw-css.json on disk) let removed = 0; const validDomains = new Set(onDisk); for (const domain of [...existing.keys()]) { if (!validDomains.has(domain)) { // Skip removal only if the brand is in tests/goldens (preserve for tests) const goldenPath = join(ROOT, 'tests', 'goldens', `${domain}.DESIGN.md`); if (existsSync(goldenPath)) { console.log(` ā­ ${domain} (ghost, but golden test present — keep)`); continue; } existing.delete(domain); removed++; console.log(` - ${domain}`); } } const newBrands = [...existing.values()]; catalog.brands = newBrands; catalog.count = newBrands.length; catalog.version = '1.5.0'; catalog.lastSyncedAt = new Date().toISOString(); writeFileSync(CATALOG_PATH, JSON.stringify(catalog, null, 2)); console.log(`\nāœ… Catalog synced`); console.log(` + ${added} added`); console.log(` - ${removed} removed`); console.log(` = ${newBrands.length} total brands`); } main().catch(err => { console.error(err); process.exit(1); });