#!/usr/bin/env node const fs = require('fs'); const path = require('path'); /** * Script pour nettoyer automatiquement les doublons dans les fichiers de skills * Usage: node scripts/clean-duplicates.js */ // Configuration const SKILLS_DIR = path.join(__dirname, '..', 'data', 'skills'); const BACKUP_DIR = path.join(__dirname, '..', 'backups', 'skills'); // Couleurs pour la console const colors = { red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', reset: '\x1b[0m', bold: '\x1b[1m' }; function log(message, color = 'reset') { console.log(`${colors[color]}${message}${colors.reset}`); } function logHeader(message) { log(`\n${colors.bold}${colors.blue}${'='.repeat(60)}${colors.reset}`); log(`${colors.bold}${colors.blue}${message}${colors.reset}`); log(`${colors.bold}${colors.blue}${'='.repeat(60)}${colors.reset}`); } function logSubHeader(message) { log(`\n${colors.bold}${colors.cyan}${message}${colors.reset}`); log(`${colors.cyan}${'-'.repeat(message.length)}${colors.reset}`); } /** * Crée une sauvegarde des fichiers avant modification */ function createBackup() { try { if (!fs.existsSync(BACKUP_DIR)) { fs.mkdirSync(BACKUP_DIR, { recursive: true }); } const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupPath = path.join(BACKUP_DIR, `skills-backup-${timestamp}`); if (!fs.existsSync(backupPath)) { fs.mkdirSync(backupPath, { recursive: true }); } const files = fs.readdirSync(SKILLS_DIR); files.forEach(file => { if (file.endsWith('.json')) { const sourcePath = path.join(SKILLS_DIR, file); const destPath = path.join(backupPath, file); fs.copyFileSync(sourcePath, destPath); } }); log(`💾 Sauvegarde créée dans: ${backupPath}`, 'green'); return backupPath; } catch (error) { log(`❌ Erreur lors de la création de la sauvegarde: ${error.message}`, 'red'); return null; } } /** * Lit et parse un fichier JSON de skills */ function parseSkillsFile(filePath) { try { const content = fs.readFileSync(filePath, 'utf8'); return JSON.parse(content); } catch (error) { log(`❌ Erreur lors de la lecture de ${filePath}: ${error.message}`, 'red'); return null; } } /** * Sauvegarde un fichier JSON de skills */ function saveSkillsFile(filePath, data) { try { const content = JSON.stringify(data, null, 2); fs.writeFileSync(filePath, content, 'utf8'); return true; } catch (error) { log(`❌ Erreur lors de la sauvegarde de ${filePath}: ${error.message}`, 'red'); return false; } } /** * Trouve tous les fichiers de skills */ function findSkillsFiles() { try { const files = fs.readdirSync(SKILLS_DIR); return files .filter(file => file.endsWith('.json')) .map(file => path.join(SKILLS_DIR, file)); } catch (error) { log(`❌ Erreur lors de la lecture du répertoire ${SKILLS_DIR}: ${error.message}`, 'red'); return []; } } /** * Analyse tous les fichiers et collecte les informations */ function analyzeSkills() { const files = findSkillsFiles(); const allSkills = []; const categoryStats = {}; files.forEach(filePath => { const fileName = path.basename(filePath, '.json'); const data = parseSkillsFile(filePath); if (data) { const category = data.category; const skillsCount = data.skills.length; categoryStats[category] = { filePath, fileName, count: skillsCount, data }; data.skills.forEach(skill => { allSkills.push({ id: skill.id, name: skill.name, category: category, filePath, fileName, skill }); }); } }); return { allSkills, categoryStats }; } /** * Détecte les doublons par ID */ function findDuplicateIds(allSkills) { const idCounts = {}; const duplicates = []; allSkills.forEach(skill => { if (!idCounts[skill.id]) { idCounts[skill.id] = []; } idCounts[skill.id].push(skill); }); Object.entries(idCounts).forEach(([id, skills]) => { if (skills.length > 1) { duplicates.push({ id, count: skills.length, occurrences: skills }); } }); return duplicates; } /** * Détermine la catégorie la plus appropriée pour un skill */ function determineBestCategory(occurrences) { // Priorité des catégories (plus spécifique en premier) const categoryPriority = { 'Testing': 1, 'Security': 2, 'Mobile': 3, 'Design': 4, 'Data': 5, 'Frontend': 6, 'Backend': 7, 'DevOps': 8, 'Culture': 9 }; let bestOccurrence = occurrences[0]; let bestPriority = categoryPriority[bestOccurrence.category] || 999; occurrences.forEach(occurrence => { const priority = categoryPriority[occurrence.category] || 999; if (priority < bestPriority) { bestPriority = priority; bestOccurrence = occurrence; } }); return bestOccurrence; } /** * Nettoie les doublons en gardant une version par catégorie */ function cleanDuplicates(duplicateIds, categoryStats) { const cleanedFiles = new Set(); const removedSkills = []; duplicateIds.forEach(duplicate => { const { id, occurrences } = duplicate; // Détermine la meilleure catégorie const bestOccurrence = determineBestCategory(occurrences); log(`🔧 Traitement du doublon "${id}" (${occurrences.length} occurrences)`, 'yellow'); log(` ✅ Gardé dans: ${bestOccurrence.category} (${bestOccurrence.fileName}.json)`, 'green'); // Supprime les doublons des autres catégories occurrences.forEach(occurrence => { if (occurrence.filePath !== bestOccurrence.filePath) { const categoryData = categoryStats[occurrence.category]; if (categoryData) { // Supprime le skill dupliqué const skillIndex = categoryData.data.skills.findIndex(s => s.id === id); if (skillIndex !== -1) { const removedSkill = categoryData.data.skills.splice(skillIndex, 1)[0]; removedSkills.push({ id: removedSkill.id, name: removedSkill.name, from: occurrence.category, file: occurrence.fileName }); log(` 🗑️ Supprimé de: ${occurrence.category} (${occurrence.fileName}.json)`, 'red'); cleanedFiles.add(occurrence.filePath); } } } }); }); return { cleanedFiles, removedSkills }; } /** * Sauvegarde les fichiers modifiés */ function saveModifiedFiles(cleanedFiles, categoryStats) { let savedCount = 0; let errorCount = 0; cleanedFiles.forEach(filePath => { const fileName = path.basename(filePath, '.json'); const categoryData = Object.values(categoryStats).find(stat => stat.fileName === fileName); if (categoryData && saveSkillsFile(filePath, categoryData.data)) { savedCount++; log(`💾 Fichier sauvegardé: ${fileName}.json`, 'green'); } else { errorCount++; log(`❌ Erreur lors de la sauvegarde: ${fileName}.json`, 'red'); } }); return { savedCount, errorCount }; } /** * Génère un rapport de nettoyage */ function generateCleaningReport(duplicateIds, removedSkills, savedCount, errorCount) { const report = []; report.push('='.repeat(80)); report.push('RAPPORT DE NETTOYAGE DES DOUBLONS - SKILLS'); report.push('='.repeat(80)); report.push(`Généré le: ${new Date().toLocaleString('fr-FR')}`); report.push(''); // Résumé des actions report.push('📊 RÉSUMÉ DES ACTIONS'); report.push('-'.repeat(30)); report.push(`Doublons traités: ${duplicateIds.length}`); report.push(`Skills supprimés: ${removedSkills.length}`); report.push(`Fichiers modifiés: ${savedCount}`); report.push(`Erreurs: ${errorCount}`); report.push(''); // Détails des doublons traités if (duplicateIds.length > 0) { report.push('🔧 DOUBLONS TRAITÉS'); report.push('-'.repeat(30)); duplicateIds.forEach(duplicate => { const bestOccurrence = determineBestCategory(duplicate.occurrences); report.push(`ID: "${duplicate.id}"`); report.push(` ✅ Gardé dans: ${bestOccurrence.category}`); duplicate.occurrences.forEach(occurrence => { if (occurrence.filePath !== bestOccurrence.filePath) { report.push(` 🗑️ Supprimé de: ${occurrence.category}`); } }); report.push(''); }); } // Détails des skills supprimés if (removedSkills.length > 0) { report.push('🗑️ SKILLS SUPPRIMÉS'); report.push('-'.repeat(30)); removedSkills.forEach(skill => { report.push(`• ${skill.name} (ID: ${skill.id}) - Supprimé de ${skill.from} (${skill.file}.json)`); }); report.push(''); } return report.join('\n'); } /** * Fonction principale */ function main() { try { logHeader('NETTOYAGE AUTOMATIQUE DES DOUBLONS'); // Création de la sauvegarde logSubHeader('CRÉATION DE LA SAUVEGARDE'); const backupPath = createBackup(); if (!backupPath) { log('❌ Impossible de continuer sans sauvegarde', 'red'); return; } // Analyse des fichiers logSubHeader('ANALYSE DES FICHIERS'); const { allSkills, categoryStats } = analyzeSkills(); if (allSkills.length === 0) { log('❌ Aucun skill trouvé. Vérifiez le répertoire.', 'red'); return; } log(`📁 Fichiers analysés: ${Object.keys(categoryStats).length}`); log(`🔍 Total de skills: ${allSkills.length}`); // Détection des doublons logSubHeader('DÉTECTION DES DOUBLONS'); const duplicateIds = findDuplicateIds(allSkills); if (duplicateIds.length === 0) { log('🎉 Aucun doublon détecté ! Aucune action requise.', 'green'); return; } log(`🚨 Doublons détectés: ${duplicateIds.length}`, 'red'); // Nettoyage des doublons logSubHeader('NETTOYAGE DES DOUBLONS'); const { cleanedFiles, removedSkills } = cleanDuplicates(duplicateIds, categoryStats); // Sauvegarde des fichiers modifiés logSubHeader('SAUVEGARDE DES FICHIERS MODIFIÉS'); const { savedCount, errorCount } = saveModifiedFiles(cleanedFiles, categoryStats); // Génération du rapport const report = generateCleaningReport(duplicateIds, removedSkills, savedCount, errorCount); // Sauvegarde du rapport const reportPath = path.join(__dirname, '..', 'cleaning-report.txt'); fs.writeFileSync(reportPath, report, 'utf8'); // Résumé final logHeader('RÉSUMÉ FINAL'); if (errorCount === 0) { log('🎉 Nettoyage terminé avec succès !', 'green'); log(`📊 ${duplicateIds.length} doublons traités, ${removedSkills.length} skills supprimés`, 'green'); log(`💾 Rapport sauvegardé dans: ${reportPath}`, 'green'); } else { log(`⚠️ Nettoyage terminé avec ${errorCount} erreur(s)`, 'yellow'); log('Consultez le rapport pour plus de détails.', 'yellow'); } } catch (error) { log(`❌ Erreur fatale: ${error.message}`, 'red'); process.exit(1); } } // Exécution du script if (require.main === module) { main(); } module.exports = { createBackup, analyzeSkills, findDuplicateIds, cleanDuplicates, generateCleaningReport };