#!/usr/bin/env node const fs = require('fs'); const path = require('path'); // Configuración const CONFIG = { // Rutas de entrada (FontAwesome Pro) FA_METADATA: path.join(__dirname, '..', 'assets', 'fontawesome', 'metadata', 'icons.json'), FA_CATEGORIES: path.join(__dirname, '..', 'assets', 'fontawesome', 'metadata', 'categories.yml'), // Rutas de salida OUTPUT_DIR: path.join(__dirname, '..', 'assets', 'processed'), OUTPUT_METADATA: path.join(__dirname, '..', 'assets', 'processed', 'metadata.json'), OUTPUT_STYLES: path.join(__dirname, '..', 'assets', 'processed', 'styles.json'), OUTPUT_CATEGORIES: path.join(__dirname, '..', 'assets', 'processed', 'categories.json'), // Configuración de procesamiento MIN_ICONS_TO_PROCESS: 100, VERSION: '2.0.0' }; // Colores para consola const colors = { reset: '\x1b[0m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', red: '\x1b[31m', magenta: '\x1b[35m', cyan: '\x1b[36m' }; // Banner console.log(colors.cyan + '='.repeat(70)); console.log(' EXTRACTOR DE METADATA FONTAWESOME PRO'); console.log(' Versión: ' + CONFIG.VERSION); console.log('='.repeat(70) + colors.reset + '\n'); /** * Crear directorios necesarios */ function createDirectories() { console.log(colors.blue + '📁 Creando directorios...' + colors.reset); const dirs = [ CONFIG.OUTPUT_DIR, path.join(CONFIG.OUTPUT_DIR, 'backup'), path.join(CONFIG.OUTPUT_DIR, 'logs') ]; dirs.forEach(dir => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); console.log(` ✅ Creado: ${path.relative(process.cwd(), dir)}`); } else { console.log(` 📁 Existe: ${path.relative(process.cwd(), dir)}`); } }); console.log(''); } /** * Verificar archivos de entrada */ function checkInputFiles() { console.log(colors.blue + '🔍 Verificando archivos de entrada...' + colors.reset); const files = [ { path: CONFIG.FA_METADATA, name: 'icons.json', required: true }, { path: CONFIG.FA_CATEGORIES, name: 'categories.yml', required: false } ]; let allFound = true; files.forEach(file => { if (fs.existsSync(file.path)) { const stats = fs.statSync(file.path); console.log(` ✅ ${file.name}: ${(stats.size / 1024).toFixed(2)} KB`); if (file.name === 'icons.json') { try { const content = fs.readFileSync(file.path, 'utf8'); const data = JSON.parse(content); console.log(` Íconos en metadata: ${Object.keys(data).length}`); } catch (error) { console.log(` ❌ ${file.name}: Error de formato - ${error.message}`); allFound = false; } } } else { if (file.required) { console.log(` ❌ ${file.name}: NO ENCONTRADO (requerido)`); allFound = false; } else { console.log(` ⚠️ ${file.name}: No encontrado (opcional)`); } } }); console.log(''); return allFound; } /** * Hacer backup de archivos existentes */ function backupExistingFiles() { console.log(colors.blue + '💾 Creando respaldo de archivos existentes...' + colors.reset); const filesToBackup = [ CONFIG.OUTPUT_METADATA, CONFIG.OUTPUT_STYLES, CONFIG.OUTPUT_CATEGORIES ]; const backupDir = path.join(CONFIG.OUTPUT_DIR, 'backup', new Date().toISOString().replace(/[:.]/g, '-')); if (!fs.existsSync(backupDir)) { fs.mkdirSync(backupDir, { recursive: true }); } let backedUpCount = 0; filesToBackup.forEach(file => { if (fs.existsSync(file)) { const fileName = path.basename(file); const backupPath = path.join(backupDir, fileName); try { fs.copyFileSync(file, backupPath); console.log(` ✅ Respaldo: ${fileName}`); backedUpCount++; } catch (error) { console.log(` ❌ Error respaldando ${fileName}: ${error.message}`); } } }); if (backedUpCount > 0) { console.log(` 📊 Total respaldados: ${backedUpCount} archivos`); console.log(` 📍 Ubicación: ${path.relative(process.cwd(), backupDir)}`); } else { console.log(' ⚠️ No hay archivos existentes para respaldar'); } console.log(''); return backupDir; } /** * Extraer y procesar metadata de íconos */ function extractIconMetadata() { console.log(colors.blue + '📊 Extrayendo metadata de íconos...' + colors.reset); try { // Leer archivo original const content = fs.readFileSync(CONFIG.FA_METADATA, 'utf8'); const rawMetadata = JSON.parse(content); const totalIcons = Object.keys(rawMetadata).length; console.log(` 📈 Total íconos encontrados: ${totalIcons}`); if (totalIcons < CONFIG.MIN_ICONS_TO_PROCESS) { throw new Error(`Muy pocos íconos (${totalIcons}). ¿Metadata corrupto?`); } // Procesar cada ícono const processedIcons = {}; let processedCount = 0; let stylesFound = new Set(); let categoriesFound = new Set(); Object.entries(rawMetadata).forEach(([iconName, iconData]) => { processedCount++; // Mostrar progreso cada 1000 íconos if (processedCount % 1000 === 0) { console.log(` ⏳ Procesados: ${processedCount}/${totalIcons} íconos...`); } // Extraer información básica const processedIcon = { id: iconName, name: iconName, label: iconData.label || formatIconName(iconName), unicode: iconData.unicode || '', version: iconData.version || '6.0.0', search: iconData.search || {} }; // Estilos disponibles if (iconData.styles && Array.isArray(iconData.styles)) { processedIcon.styles = iconData.styles.map(style => style.toLowerCase()); iconData.styles.forEach(style => stylesFound.add(style.toLowerCase())); } else { processedIcon.styles = []; } // Categorías if (iconData.categories && Array.isArray(iconData.categories)) { processedIcon.categories = iconData.categories; iconData.categories.forEach(cat => categoriesFound.add(cat)); } else { processedIcon.categories = ['uncategorized']; categoriesFound.add('uncategorized'); } // Información adicional if (iconData.svg) { processedIcon.svg = { width: iconData.svg.width, height: iconData.svg.height, path: iconData.svg.path }; } // Términos de búsqueda mejorados const searchTerms = new Set(); // Términos originales if (iconData.search && iconData.search.terms) { iconData.search.terms.forEach(term => { if (term && typeof term === 'string') { searchTerms.add(term.toLowerCase()); } }); } // Agregar nombre y label searchTerms.add(iconName.toLowerCase()); if (iconData.label) { searchTerms.add(iconData.label.toLowerCase()); // Variantes sin espacios searchTerms.add(iconData.label.toLowerCase().replace(/\s+/g, '-')); } // Agregar sinónimos comunes addCommonSynonyms(iconName, searchTerms); processedIcon.searchTerms = Array.from(searchTerms); processedIcon.searchCount = searchTerms.size; // Agregar a resultados processedIcons[iconName] = processedIcon; }); console.log(`\n ✅ Procesados: ${processedCount} íconos`); console.log(` 🎨 Estilos encontrados: ${stylesFound.size}`); console.log(` 📁 Categorías encontradas: ${categoriesFound.size}`); // Guardar metadata procesado const metadataOutput = { meta: { generated: new Date().toISOString(), source: 'FontAwesome Pro', version: CONFIG.VERSION, totalIcons: processedCount, totalStyles: stylesFound.size, totalCategories: categoriesFound.size }, icons: processedIcons, statistics: { styles: Array.from(stylesFound).sort(), categories: Array.from(categoriesFound).sort(), iconsPerStyle: countIconsPerStyle(processedIcons), iconsPerCategory: countIconsPerCategory(processedIcons) } }; fs.writeFileSync(CONFIG.OUTPUT_METADATA, JSON.stringify(metadataOutput, null, 2), 'utf8'); console.log(`\n 💾 Metadata guardado: ${CONFIG.OUTPUT_METADATA}`); console.log(` 📊 Tamaño: ${(fs.statSync(CONFIG.OUTPUT_METADATA).size / 1024 / 1024).toFixed(2)} MB`); return metadataOutput; } catch (error) { console.error(colors.red + ` ❌ Error procesando metadata: ${error.message}` + colors.reset); throw error; } } /** * Formatear nombre de ícono */ function formatIconName(iconName) { return iconName .split('-') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } /** * Agregar sinónimos comunes */ function addCommonSynonyms(iconName, searchTerms) { const synonyms = { 'home': ['house', 'casa', 'hogar', 'dwelling', 'residence'], 'user': ['person', 'profile', 'persona', 'perfil', 'account', 'human'], 'heart': ['love', 'like', 'favorite', 'amor', 'favorito', 'affection'], 'star': ['favorite', 'rating', 'estrella', 'calificación', 'rate', 'review'], 'settings': ['cog', 'gear', 'preferences', 'configuración', 'ajustes', 'options'], 'bell': ['notification', 'alert', 'notificación', 'alerta', 'ring', 'chime'], 'envelope': ['email', 'mail', 'correo', 'message', 'letter'], 'calendar': ['date', 'schedule', 'fecha', 'calendario', 'agenda', 'planner'], 'clock': ['time', 'hour', 'tiempo', 'hora', 'watch', 'timer'], 'search': ['find', 'lookup', 'buscar', 'encontrar', 'seek', 'locate'], 'plus': ['add', 'new', 'create', 'agregar', 'añadir', 'insert'], 'minus': ['remove', 'delete', 'subtract', 'quitar', 'eliminar', 'restar'], 'check': ['verify', 'confirm', 'mark', 'verificar', 'confirmar', 'tick'], 'times': ['close', 'exit', 'cancel', 'cerrar', 'salir', 'cancelar'], 'trash': ['delete', 'remove', 'garbage', 'eliminar', 'basura', 'waste'], 'edit': ['modify', 'change', 'update', 'modificar', 'cambiar', 'actualizar'], 'download': ['save', 'get', 'fetch', 'descargar', 'guardar', 'obtener'], 'upload': ['send', 'share', 'post', 'subir', 'enviar', 'compartir'], 'print': ['printer', 'hardcopy', 'paper', 'imprimir', 'copiar', 'document'] }; Object.entries(synonyms).forEach(([key, words]) => { if (iconName.includes(key)) { words.forEach(word => searchTerms.add(word)); } }); } /** * Contar íconos por estilo */ function countIconsPerStyle(icons) { const counts = {}; Object.values(icons).forEach(icon => { if (icon.styles) { icon.styles.forEach(style => { counts[style] = (counts[style] || 0) + 1; }); } }); return counts; } /** * Contar íconos por categoría */ function countIconsPerCategory(icons) { const counts = {}; Object.values(icons).forEach(icon => { if (icon.categories) { icon.categories.forEach(category => { counts[category] = (counts[category] || 0) + 1; }); } }); return counts; } /** * Extraer información de estilos */ function extractStyleInfo(metadata) { console.log(colors.blue + '\n🎨 Extrayendo información de estilos...' + colors.reset); // Definición de todos los estilos posibles const allStyles = { 'solid': { id: 'solid', name: 'Solid', prefix: 'fas', class: 'fas', weight: 900, pro: true, sharp: false, family: 'Font Awesome 6 Pro', file: 'fa-solid-900.woff2' }, 'regular': { id: 'regular', name: 'Regular', prefix: 'far', class: 'far', weight: 400, pro: true, sharp: false, family: 'Font Awesome 6 Pro', file: 'fa-regular-400.woff2' }, 'light': { id: 'light', name: 'Light', prefix: 'fal', class: 'fal', weight: 300, pro: true, sharp: false, family: 'Font Awesome 6 Pro', file: 'fa-light-300.woff2' }, 'thin': { id: 'thin', name: 'Thin', prefix: 'fat', class: 'fat', weight: 100, pro: true, sharp: false, family: 'Font Awesome 6 Pro', file: 'fa-thin-100.woff2' }, 'duotone': { id: 'duotone', name: 'Duotone', prefix: 'fad', class: 'fad', weight: 900, pro: true, sharp: false, family: 'Font Awesome 6 Duotone', file: 'fa-duotone-900.woff2' }, 'sharp-solid': { id: 'sharp-solid', name: 'Sharp Solid', prefix: 'fass', class: 'fass', weight: 900, pro: true, sharp: true, family: 'Font Awesome 6 Sharp', file: 'fa-sharp-solid-900.woff2' }, 'sharp-regular': { id: 'sharp-regular', name: 'Sharp Regular', prefix: 'fasr', class: 'fasr', weight: 400, pro: true, sharp: true, family: 'Font Awesome 6 Sharp', file: 'fa-sharp-regular-400.woff2' }, 'sharp-light': { id: 'sharp-light', name: 'Sharp Light', prefix: 'fasl', class: 'fasl', weight: 300, pro: true, sharp: true, family: 'Font Awesome 6 Sharp', file: 'fa-sharp-light-300.woff2' }, 'sharp-thin': { id: 'sharp-thin', name: 'Sharp Thin', prefix: 'fast', class: 'fast', weight: 100, pro: true, sharp: true, family: 'Font Awesome 6 Sharp', file: 'fa-sharp-thin-100.woff2' }, 'sharp-duotone': { id: 'sharp-duotone', name: 'Sharp Duotone', prefix: 'fasd', class: 'fasd', weight: 900, pro: true, sharp: true, family: 'Font Awesome 6 Sharp Duotone', file: 'fa-sharp-duotone-900.woff2' }, 'brands': { id: 'brands', name: 'Brands', prefix: 'fab', class: 'fab', weight: 400, pro: false, sharp: false, family: 'Font Awesome 6 Brands', file: 'fa-brands-400.woff2' } }; // Contar íconos por estilo (desde metadata) const styleCounts = metadata.statistics.iconsPerStyle || {}; // Enriquecer información de estilos const stylesInfo = {}; Object.keys(allStyles).forEach(styleId => { const style = allStyles[styleId]; const count = styleCounts[styleId] || 0; stylesInfo[styleId] = { ...style, count: count, availableInMetadata: count > 0, percentage: metadata.meta.totalIcons > 0 ? ((count / metadata.meta.totalIcons) * 100).toFixed(1) : '0.0' }; }); // Guardar información de estilos const stylesOutput = { meta: { generated: new Date().toISOString(), totalStyles: Object.keys(stylesInfo).length, stylesWithIcons: Object.values(stylesInfo).filter(s => s.count > 0).length }, styles: stylesInfo, statistics: { totalIconsByStyle: styleCounts, styleCoverage: calculateStyleCoverage(stylesInfo, metadata.meta.totalIcons) } }; fs.writeFileSync(CONFIG.OUTPUT_STYLES, JSON.stringify(stylesOutput, null, 2), 'utf8'); console.log(` ✅ Información de estilos guardada: ${CONFIG.OUTPUT_STYLES}`); // Mostrar resumen console.log('\n 📊 RESUMEN DE ESTILOS:'); Object.values(stylesInfo) .filter(style => style.count > 0) .sort((a, b) => b.count - a.count) .forEach(style => { console.log(` ${style.name.padEnd(20)}: ${style.count.toString().padStart(5)} íconos (${style.percentage}%)`); }); return stylesOutput; } /** * Calcular cobertura de estilos */ function calculateStyleCoverage(stylesInfo, totalIcons) { const coverage = {}; Object.values(stylesInfo).forEach(style => { if (style.count > 0) { coverage[style.id] = { hasIcons: true, count: style.count, percentage: ((style.count / totalIcons) * 100).toFixed(1) }; } else { coverage[style.id] = { hasIcons: false, count: 0, percentage: '0.0' }; } }); return coverage; } /** * Extraer y procesar categorías */ function extractCategories(metadata) { console.log(colors.blue + '\n📁 Extrayendo información de categorías...' + colors.reset); // Mapeo de categorías de FontAwesome a nombres en español const categoriesMap = { 'accessibility': 'Accesibilidad', 'alert': 'Alertas', 'animals': 'Animales', 'arrows': 'Flechas', 'automotive': 'Automotriz', 'buildings': 'Edificios', 'business': 'Negocios', 'camping': 'Camping', 'charity': 'Caridad', 'charts-diagrams': 'Gráficos', 'childhood': 'Infancia', 'clothing-fashion': 'Ropa & Moda', 'coding': 'Programación', 'communication': 'Comunicación', 'connectivity': 'Conectividad', 'construction': 'Construcción', 'design': 'Diseño', 'devices-hardware': 'Dispositivos', 'document': 'Documentos', 'editing': 'Edición', 'education': 'Educación', 'emoji': 'Emoji', 'energy': 'Energía', 'files': 'Archivos', 'film-video': 'Video', 'food-beverage': 'Comida', 'fruits-vegetables': 'Frutas', 'gaming': 'Juegos', 'gender': 'Género', 'halloween': 'Halloween', 'hands': 'Manos', 'holidays': 'Fiestas', 'household': 'Hogar', 'humanitarian': 'Humanitario', 'logistics': 'Logística', 'maps': 'Mapas', 'maritime': 'Marítimo', 'marketing': 'Marketing', 'mathematics': 'Matemáticas', 'medical-health': 'Salud', 'money': 'Dinero', 'moving': 'Mudanza', 'music-audio': 'Música', 'nature': 'Naturaleza', 'numbers': 'Números', 'photos-images': 'Fotos', 'political': 'Política', 'punctuation-symbols': 'Símbolos', 'religion': 'Religión', 'science': 'Ciencia', 'science-fiction': 'Sci-Fi', 'security': 'Seguridad', 'shapes': 'Formas', 'shopping': 'Compras', 'social': 'Social', 'spinners': 'Spinners', 'sports-fitness': 'Deportes', 'text-formatting': 'Texto', 'time': 'Tiempo', 'toggle': 'Toggle', 'transportation': 'Transporte', 'travel': 'Viajes', 'users-people': 'Personas', 'weather': 'Clima', 'writing': 'Escritura', 'uncategorized': 'Sin Categoría' }; // Obtener conteos de categorías desde metadata const categoryCounts = metadata.statistics.iconsPerCategory || {}; // Procesar categorías const categories = []; // Categorías especiales categories.push({ id: 'all', name: 'Todos los Íconos', icon: 'fas fa-th', count: metadata.meta.totalIcons, type: 'special' }); categories.push({ id: 'favorites', name: 'Favoritos', icon: 'fas fa-star', count: 0, type: 'special' }); categories.push({ id: 'recent', name: 'Recientes', icon: 'fas fa-history', count: 0, type: 'special' }); // Categorías de FontAwesome Object.entries(categoryCounts).forEach(([categoryId, count]) => { const categoryName = categoriesMap[categoryId] || formatCategoryName(categoryId); categories.push({ id: categoryId, name: categoryName, icon: getCategoryIcon(categoryId), count: count, type: 'fontawesome' }); }); // Ordenar categorías categories.sort((a, b) => { // Categorías especiales primero if (a.type === 'special' && b.type !== 'special') return -1; if (a.type !== 'special' && b.type === 'special') return 1; // Luego por nombre return a.name.localeCompare(b.name); }); // Guardar categorías const categoriesOutput = { meta: { generated: new Date().toISOString(), totalCategories: categories.length, fontawesomeCategories: Object.keys(categoryCounts).length }, categories: categories, mapping: categoriesMap }; fs.writeFileSync(CONFIG.OUTPUT_CATEGORIES, JSON.stringify(categoriesOutput, null, 2), 'utf8'); console.log(` ✅ Categorías guardadas: ${CONFIG.OUTPUT_CATEGORIES}`); console.log(` 📊 Total categorías: ${categories.length}`); // Mostrar top 10 categorías console.log('\n 🏆 TOP 10 CATEGORÍAS:'); categories .filter(cat => cat.type === 'fontawesome') .sort((a, b) => b.count - a.count) .slice(0, 10) .forEach((cat, index) => { console.log(` ${index + 1}. ${cat.name.padEnd(25)}: ${cat.count} íconos`); }); return categoriesOutput; } /** * Formatear nombre de categoría */ function formatCategoryName(categoryId) { return categoryId .split('-') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } /** * Obtener ícono para categoría */ function getCategoryIcon(categoryId) { const iconMap = { 'accessibility': 'fas fa-universal-access', 'alert': 'fas fa-exclamation-triangle', 'animals': 'fas fa-paw', 'arrows': 'fas fa-arrow-right', 'buildings': 'fas fa-building', 'business': 'fas fa-briefcase', 'coding': 'fas fa-code', 'communication': 'fas fa-comments', 'design': 'fas fa-palette', 'devices-hardware': 'fas fa-laptop', 'education': 'fas fa-graduation-cap', 'files': 'fas fa-folder', 'food-beverage': 'fas fa-utensils', 'health': 'fas fa-heartbeat', 'household': 'fas fa-home', 'maps': 'fas fa-map', 'music-audio': 'fas fa-music', 'nature': 'fas fa-leaf', 'shopping': 'fas fa-shopping-cart', 'sports-fitness': 'fas fa-football-ball', 'travel': 'fas fa-plane', 'users-people': 'fas fa-user-friends', 'weather': 'fas fa-cloud-sun', 'writing': 'fas fa-pen-fancy' }; return iconMap[categoryId] || 'fas fa-folder'; } /** * Generar reporte de procesamiento */ function generateReport(metadata, styles, categories) { console.log(colors.blue + '\n📋 Generando reporte de procesamiento...' + colors.reset); const reportPath = path.join(CONFIG.OUTPUT_DIR, 'logs', `report-${new Date().toISOString().replace(/[:.]/g, '-')}.json`); const report = { meta: { generated: new Date().toISOString(), version: CONFIG.VERSION, duration: new Date() - startTime }, summary: { totalIcons: metadata.meta.totalIcons, totalStyles: styles.meta.totalStyles, stylesWithIcons: styles.meta.stylesWithIcons, totalCategories: categories.meta.totalCategories }, files: { metadata: CONFIG.OUTPUT_METADATA, styles: CONFIG.OUTPUT_STYLES, categories: CONFIG.OUTPUT_CATEGORIES }, statistics: { iconsPerStyle: metadata.statistics.iconsPerStyle, iconsPerCategory: metadata.statistics.iconsPerCategory, styleCoverage: styles.statistics.styleCoverage } }; fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf8'); console.log(` ✅ Reporte guardado: ${reportPath}`); return report; } /** * Función principal */ async function main() { global.startTime = new Date(); try { console.log(colors.green + '🚀 Iniciando extracción de metadata...' + colors.reset); // 1. Crear directorios createDirectories(); // 2. Verificar archivos de entrada if (!checkInputFiles()) { console.log(colors.red + '❌ Archivos requeridos no encontrados. Abortando.' + colors.reset); process.exit(1); } // 3. Hacer backup backupExistingFiles(); // 4. Extraer metadata de íconos const metadata = extractIconMetadata(); // 5. Extraer información de estilos const styles = extractStyleInfo(metadata); // 6. Extraer y procesar categorías const categories = extractCategories(metadata); // 7. Generar reporte const report = generateReport(metadata, styles, categories); // 8. Mostrar resumen final const duration = ((new Date() - startTime) / 1000).toFixed(2); console.log(colors.green + '\n' + '='.repeat(70)); console.log(' ✅ EXTRACCIÓN COMPLETADA EXITOSAMENTE'); console.log('='.repeat(70) + colors.reset); console.log(`\n📊 RESUMEN FINAL:`); console.log(` • Íconos procesados: ${metadata.meta.totalIcons}`); console.log(` • Estilos encontrados: ${styles.meta.stylesWithIcons}/${styles.meta.totalStyles}`); console.log(` • Categorías: ${categories.meta.totalCategories}`); console.log(` • Tiempo total: ${duration} segundos`); console.log(`\n📁 ARCHIVOS GENERADOS:`); console.log(` • ${path.relative(process.cwd(), CONFIG.OUTPUT_METADATA)}`); console.log(` • ${path.relative(process.cwd(), CONFIG.OUTPUT_STYLES)}`); console.log(` • ${path.relative(process.cwd(), CONFIG.OUTPUT_CATEGORIES)}`); console.log(`\n🚀 PRÓXIMOS PASOS:`); console.log(` • Ejecuta: npm run build`); console.log(` • Luego instala el plugin en Logseq`); console.log(colors.green + '\n🎉 ¡Listo para construir la base de datos final!' + colors.reset); } catch (error) { console.error(colors.red + '\n❌ ERROR CRÍTICO:' + colors.reset); console.error(` ${error.message}`); console.error(`\n🔧 Solución:`); console.error(` 1. Verifica que tengas FontAwesome 6 Pro instalado`); console.error(` 2. Asegúrate de que metadata/icons.json sea válido`); console.error(` 3. Intenta ejecutar 'npm run verify' primero`); process.exit(1); } } // Ejecutar if (require.main === module) { main(); } // Exportar funciones para uso en otros scripts module.exports = { extractIconMetadata, extractStyleInfo, extractCategories, createDirectories, checkInputFiles, CONFIG };