#!/usr/bin/env ts-node /** * bank.ts — Clone Architect Component Bank CLI * * Usage: * npx tsx scripts/bank.ts register --all * npx tsx scripts/bank.ts register --domain addictsneakers.com * npx tsx scripts/bank.ts query --type button --tag dark * npx tsx scripts/bank.ts query --domain addictsneakers.com * npx tsx scripts/bank.ts stats * npx tsx scripts/bank.ts show */ import * as fs from 'fs'; import * as path from 'path'; import { registerDomain, BANK_DIR, BankIndex, BankCatalog, BankIndexEntry } from './bank-register.js'; const EXTRACTIONS_DIR = path.join(process.cwd(), 'extractions'); // ─── CLI Arg Parsing ────────────────────────────────────────────────────────── function parseArgs(argv: string[]): { cmd: string; flags: Record } { const args = argv.slice(2); const cmd = args[0] || 'stats'; const flags: Record = {}; for (let i = 1; i < args.length; i++) { const a = args[i]; if (a.startsWith('--')) { const key = a.slice(2); const next = args[i + 1]; if (next && !next.startsWith('--')) { flags[key] = next; i++; } else { flags[key] = true; } } else if (!a.startsWith('--')) { // positional after command (e.g., bank show ) if (!flags['_pos']) flags['_pos'] = a; else if (!flags['_pos2']) flags['_pos2'] = a; } } return { cmd, flags }; } // ─── Load Bank ──────────────────────────────────────────────────────────────── function loadIndex(): BankIndex { const p = path.join(BANK_DIR, '_index.json'); if (!fs.existsSync(p)) return { version: '1.0.0', updatedAt: '', components: [] }; return JSON.parse(fs.readFileSync(p, 'utf-8')); } function loadCatalog(): BankCatalog { const p = path.join(BANK_DIR, '_catalog.json'); if (!fs.existsSync(p)) return { version: '1.0.0', updatedAt: '', byType: {}, byDomain: {}, byTags: {} }; return JSON.parse(fs.readFileSync(p, 'utf-8')); } // ─── Commands ──────────────────────────────────────────────────────────────── async function cmdRegister(flags: Record): Promise { console.log(''); console.log('┌─────────────────────────────────────────┐'); console.log('│ 🏦 Clone Architect — Component Bank │'); console.log('│ Command: register │'); console.log('└─────────────────────────────────────────┘'); console.log(''); if (flags['all']) { // Register all domains that have tokens.json const domains = fs.readdirSync(EXTRACTIONS_DIR).filter(d => { const stat = fs.statSync(path.join(EXTRACTIONS_DIR, d)); if (!stat.isDirectory()) return false; return fs.existsSync(path.join(EXTRACTIONS_DIR, d, 'tokens.json')); }); console.log(`📦 ${domains.length} extractions trouvées — enregistrement en cours...\n`); let total = 0; for (const d of domains) { total += await registerDomain(d, true); } console.log(`\n✅ Total : ${total} composants dans la banque`); printBankSummary(); } else if (flags['domain']) { const domain = flags['domain'] as string; console.log(`📦 Enregistrement de ${domain}...\n`); const count = await registerDomain(domain, true); if (count > 0) { console.log(`\n✅ ${count} composants enregistrés`); } } else { console.log('Usage:'); console.log(' npx tsx scripts/bank.ts register --all'); console.log(' npx tsx scripts/bank.ts register --domain '); } } function cmdQuery(flags: Record): void { const index = loadIndex(); if (index.components.length === 0) { console.log('⚠️ La banque est vide. Lance d\'abord: npx tsx scripts/bank.ts register --all'); return; } let results = [...index.components]; // Filters if (flags['type']) results = results.filter(c => c.type === flags['type']); if (flags['domain']) results = results.filter(c => c.domain === flags['domain'] || c.domain.includes(flags['domain'] as string)); if (flags['tag']) results = results.filter(c => c.tags.includes(flags['tag'] as string)); if (flags['dark']) results = results.filter(c => c.isDark === true); if (flags['light']) results = results.filter(c => c.isDark === false); const limit = flags['limit'] ? parseInt(flags['limit'] as string) : 30; const total = results.length; results = results.slice(0, limit); console.log(''); console.log(`🔍 ${total} composant(s) trouvé(s)${results.length < total ? ` (affichage: ${results.length})` : ''}`); if (flags['type'] || flags['domain'] || flags['tag'] || flags['dark'] || flags['light']) { const applied = []; if (flags['type']) applied.push(`type=${flags['type']}`); if (flags['domain']) applied.push(`domain=${flags['domain']}`); if (flags['tag']) applied.push(`tag=${flags['tag']}`); if (flags['dark']) applied.push('dark'); if (flags['light']) applied.push('light'); console.log(` Filtres: ${applied.join(', ')}`); } console.log(''); if (results.length === 0) { console.log(' Aucun résultat. Essaie sans filtres ou avec d\'autres valeurs.'); return; } for (const c of results) { const darkLabel = c.isDark ? '🌑' : '☀️ '; console.log(` ${darkLabel} [${c.id}]`); console.log(` ${c.domain} / ${c.type} / "${c.label}"`); console.log(` Accent: ${c.accent} | Tags: ${c.tags.filter(t => !['button','card','heading','input','badge','link'].includes(t)).join(', ')}`); console.log(''); } console.log(`─────────────────────────────────────────────────`); console.log(`💡 Détail: npx tsx scripts/bank.ts show `); } function cmdShow(flags: Record): void { const id = flags['_pos'] as string || flags['id'] as string; if (!id) { console.error('Usage: npx tsx scripts/bank.ts show '); return; } const snapshotPath = path.join(BANK_DIR, id, 'snapshot.json'); if (!fs.existsSync(snapshotPath)) { console.error(`❌ Composant introuvable: ${id}`); const index = loadIndex(); const similar = index.components.filter(c => c.id.includes(id.split('--')[0])); if (similar.length) { console.log(' Suggestions:'); for (const s of similar.slice(0, 5)) console.log(` ${s.id}`); } return; } const snap = JSON.parse(fs.readFileSync(snapshotPath, 'utf-8')); console.log(''); console.log(`┌── Component Snapshot ────────────────────────────`); console.log(`│ ID: ${snap.id}`); console.log(`│ Domain: ${snap.domain}`); console.log(`│ Type: ${snap.componentType} (${snap.componentLabel})`); console.log(`│ URL: ${snap.url}`); console.log(`│ Dark: ${snap.siteTokens.isDark ? 'yes' : 'no'}`); console.log(`│ Tags: ${snap.tags.join(', ')}`); console.log(`└──────────────────────────────────────────────────`); console.log(''); console.log('Site Tokens:'); console.log(` Background: ${snap.siteTokens.colors.background}`); console.log(` Text: ${snap.siteTokens.colors.text}`); console.log(` Accent: ${snap.siteTokens.colors.accent}`); console.log(` Font: ${snap.siteTokens.typography.primary}`); console.log(` Radius md: ${snap.siteTokens.borderRadius.md}`); console.log(''); console.log('Component Styles:'); const styles = snap.styles; const styleKeys = ['backgroundColor', 'color', 'padding', 'borderRadius', 'border', 'fontSize', 'fontWeight', 'fontFamily', 'boxShadow', 'width', 'height']; for (const k of styleKeys) { if (styles[k] && styles[k] !== 'none' && styles[k] !== 'normal') { console.log(` ${k}: ${styles[k]}`); } } if (snap.designMdPath) { console.log(''); console.log(`📄 DESIGN.md: bank/${snap.designMdPath}`); } if (snap.screenshotPath) { console.log(`🖼 Screenshot: bank/${snap.screenshotPath}`); } } function cmdStats(): void { const index = loadIndex(); const catalog = loadCatalog(); const total = index.components.length; const domains = Object.keys(catalog.byDomain).length; console.log(''); console.log('┌─────────────────────────────────────────────────┐'); console.log('│ 📊 Clone Architect — Component Bank Stats │'); console.log('└─────────────────────────────────────────────────┘'); console.log(''); if (total === 0) { console.log(' La banque est vide. Lance: npx tsx scripts/bank.ts register --all'); return; } console.log(` Total composants : ${total}`); console.log(` Domaines : ${domains}`); console.log(` Dernière màj : ${index.updatedAt ? new Date(index.updatedAt).toLocaleString('fr-FR') : 'N/A'}`); console.log(''); // By type console.log(' Par type:'); const typeEntries = Object.entries(catalog.byType).sort((a, b) => b[1].length - a[1].length); const maxTypeCount = typeEntries[0]?.[1].length || 1; for (const [type, ids] of typeEntries) { const bar = '█'.repeat(Math.round((ids.length / maxTypeCount) * 20)); console.log(` ${type.padEnd(12)} ${String(ids.length).padStart(3)} ${bar}`); } console.log(''); // By domain console.log(' Par domaine:'); const domainEntries = Object.entries(catalog.byDomain).sort((a, b) => b[1].length - a[1].length); for (const [domain, ids] of domainEntries) { const dark = index.components.find(c => c.domain === domain)?.isDark; const themeIcon = dark ? '🌑' : '☀️ '; console.log(` ${themeIcon} ${domain.padEnd(35)} ${String(ids.length).padStart(3)} composants`); } console.log(''); // By tag (top 10) console.log(' Tags populaires:'); const tagEntries = Object.entries(catalog.byTags) .filter(([t]) => !['button','card','heading','input','badge','link','form','typography'].includes(t)) .sort((a, b) => b[1].length - a[1].length) .slice(0, 12); for (const [tag, ids] of tagEntries) { const bar = '▪'.repeat(Math.min(ids.length, 20)); console.log(` ${tag.padEnd(20)} ${String(ids.length).padStart(3)} ${bar}`); } console.log(''); } function printBankSummary(): void { const index = loadIndex(); console.log(''); console.log(`📊 Banque mise à jour — ${index.components.length} composants au total`); console.log(` Commandes disponibles:`); console.log(` npx tsx scripts/bank.ts stats`); console.log(` npx tsx scripts/bank.ts query --type button`); console.log(` npx tsx scripts/bank.ts inject [--retheme domain] [--preset name]`); console.log(` npx tsx scripts/bank.ts diff `); } // ─── Diff: compare tokens of 2 domains ─────────────────────────────────────── function cmdDiff(flags: Record): void { const domain1 = flags['_pos'] as string; const domain2 = flags['_pos2'] as string; if (!domain1 || !domain2) { console.error('Usage: npx tsx scripts/bank.ts diff '); return; } const load = (domain: string) => { const p = path.join(EXTRACTIONS_DIR, domain, 'tokens.json'); if (!fs.existsSync(p)) return null; const t = JSON.parse(fs.readFileSync(p, 'utf-8')); return { bg: t.colors?.background?.primary || '?', text: t.colors?.text?.primary || '?', accent: t.colors?.accent?.primary || '?', border: t.colors?.border || '?', font: t.typography?.fontFamily?.primary || '?', radiusMd: t.borderRadius?.md || '?', spacingMd: t.spacing?.md || '?', }; }; const t1 = load(domain1); const t2 = load(domain2); if (!t1) { console.error(`❌ Tokens not found: ${domain1}`); return; } if (!t2) { console.error(`❌ Tokens not found: ${domain2}`); return; } console.log(''); console.log(`┌─────────────────────────────────────────────────────────────┐`); console.log(`│ 🔍 Token Diff: ${domain1} vs ${domain2}`); console.log(`└─────────────────────────────────────────────────────────────┘`); console.log(''); const rows: [string, string, string][] = [ ['Background', t1.bg, t2.bg], ['Text', t1.text, t2.text], ['Accent', t1.accent, t2.accent], ['Border', t1.border, t2.border], ['Font', t1.font, t2.font], ['Radius (md)', t1.radiusMd, t2.radiusMd], ['Spacing (md)', t1.spacingMd, t2.spacingMd], ]; let changes = 0; for (const [label, v1, v2] of rows) { const same = v1 === v2; const marker = same ? ' =' : ' ≠'; if (!same) changes++; console.log(` ${label.padEnd(14)} ${v1.padEnd(30)} ${marker} ${v2}`); } console.log(''); console.log(` ${changes} différence(s) sur 7 tokens comparés`); console.log(''); } // ─── Main ───────────────────────────────────────────────────────────────────── (async () => { const { cmd, flags } = parseArgs(process.argv); switch (cmd) { case 'register': await cmdRegister(flags); break; case 'query': case 'search': cmdQuery(flags); break; case 'show': case 'get': cmdShow(flags); break; case 'inject': { // Delegate to bank-inject.ts const { execSync } = await import('child_process'); const injectArgs = process.argv.slice(3).join(' '); try { execSync(`npx tsx scripts/bank-inject.ts ${injectArgs}`, { stdio: 'inherit', cwd: process.cwd() }); } catch { process.exit(1); } break; } case 'retheme': { // Alias: inject with --retheme or --preset const { execSync } = await import('child_process'); const rethemeArgs = process.argv.slice(3).join(' '); try { execSync(`npx tsx scripts/bank-inject.ts ${rethemeArgs}`, { stdio: 'inherit', cwd: process.cwd() }); } catch { process.exit(1); } break; } case 'diff': cmdDiff(flags); break; case 'stats': default: cmdStats(); } })();