#!/usr/bin/env node import * as fs from 'fs'; import * as path from 'path'; import { glob } from 'glob'; interface DocFile { originalPath: string; relativePath: string; content: string; frontmatter: Record; track: 'current' | 'vision'; domain: string; slug: string; title: string; images: string[]; } interface PathMapping { [originalPath: string]: string; } interface MigrationReport { totalFiles: number; migratedFiles: number; currentFiles: number; visionFiles: number; domainDistribution: Record; unclassifiedFiles: string[]; brokenLinks: string[]; pathMapping: PathMapping; } class DocMigrator { private projectRoot: string; private docsRoot: string; private currentRoot: string; private visionRoot: string; private assetsRoot: string; private report: MigrationReport; private pathMapping: PathMapping = {}; constructor(projectRoot: string) { this.projectRoot = projectRoot; this.docsRoot = path.join(projectRoot, 'veza-docs'); this.currentRoot = path.join(this.docsRoot, 'current'); this.visionRoot = path.join(this.docsRoot, 'vision'); this.assetsRoot = path.join(this.docsRoot, 'current', 'assets'); this.report = { totalFiles: 0, migratedFiles: 0, currentFiles: 0, visionFiles: 0, domainDistribution: {}, unclassifiedFiles: [], brokenLinks: [], pathMapping: {} }; } async discoverDocs(): Promise { console.log('🔍 Découverte des fichiers de documentation...'); const patterns = [ '**/*.md', '**/*.mdx' ]; const excludePatterns = [ 'node_modules/**', 'target/**', 'dist/**', 'build/**', 'vendor/**', '.git/**', 'veza-docs/**', 'veza-docs-backup/**', '*.log', '*.pid', '**/node_modules/**', '**/target/**', '**/dist/**', '**/build/**' ]; const allFiles: string[] = []; for (const pattern of patterns) { const files = await glob(pattern, { cwd: this.projectRoot, ignore: excludePatterns }); allFiles.push(...files); } console.log(`📄 ${allFiles.length} fichiers de documentation trouvés`); const docFiles: DocFile[] = []; for (const file of allFiles) { const fullPath = path.join(this.projectRoot, file); const content = fs.readFileSync(fullPath, 'utf-8'); const docFile = this.parseDocFile(file, content); if (docFile) { docFiles.push(docFile); } } return docFiles; } private parseDocFile(relativePath: string, content: string): DocFile | null { const frontmatter = this.extractFrontmatter(content); const track = this.determineTrack(relativePath, content, frontmatter); const domain = this.determineDomain(relativePath, content, frontmatter); const slug = this.generateSlug(relativePath); const title = this.extractTitle(content, relativePath); const images = this.extractImages(content); return { originalPath: path.join(this.projectRoot, relativePath), relativePath, content, frontmatter, track, domain, slug, title, images }; } private extractFrontmatter(content: string): Record { const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/; const match = content.match(frontmatterRegex); if (!match) return {}; const frontmatterText = match[1]; const frontmatter: Record = {}; frontmatterText.split('\n').forEach(line => { const [key, ...valueParts] = line.split(':'); if (key && valueParts.length > 0) { const value = valueParts.join(':').trim(); frontmatter[key.trim()] = value.replace(/^["']|["']$/g, ''); } }); return frontmatter; } private determineTrack(relativePath: string, content: string, frontmatter: Record): 'current' | 'vision' { // Priorité au frontmatter if (frontmatter.track === 'vision' || frontmatter.track === 'current') { return frontmatter.track; } // Mots-clés dans le chemin const visionKeywords = ['vision', 'target', 'future', 'roadmap', 'spec', 'cible', 'but visé']; const pathLower = relativePath.toLowerCase(); if (visionKeywords.some(keyword => pathLower.includes(keyword))) { return 'vision'; } // Mots-clés dans le contenu const contentLower = content.toLowerCase(); if (visionKeywords.some(keyword => contentLower.includes(keyword))) { return 'vision'; } // Par défaut, c'est l'état actuel return 'current'; } private determineDomain(relativePath: string, content: string, frontmatter: Record): string { // Priorité au frontmatter if (frontmatter.domain) { return frontmatter.domain; } const pathLower = relativePath.toLowerCase(); const contentLower = content.toLowerCase(); // Backend if (pathLower.includes('backend') || pathLower.includes('api') || pathLower.includes('go') || contentLower.includes('backend') || contentLower.includes('api') || contentLower.includes('golang')) { return 'backend'; } // Frontend if (pathLower.includes('frontend') || pathLower.includes('react') || pathLower.includes('ui') || pathLower.includes('vite') || contentLower.includes('frontend') || contentLower.includes('react')) { return 'frontend'; } // Rust if (pathLower.includes('rust') || pathLower.includes('chat') || pathLower.includes('stream') || contentLower.includes('rust') || contentLower.includes('cargo')) { return 'rust'; } // Infrastructure if (pathLower.includes('infra') || pathLower.includes('docker') || pathLower.includes('compose') || pathLower.includes('incus') || pathLower.includes('haproxy') || pathLower.includes('redis') || pathLower.includes('postgres') || contentLower.includes('infrastructure') || contentLower.includes('deployment')) { return 'infra'; } // Security if (pathLower.includes('security') || pathLower.includes('auth') || pathLower.includes('waf') || pathLower.includes('coraza') || contentLower.includes('security') || contentLower.includes('auth')) { return 'security'; } // Operations if (pathLower.includes('ops') || pathLower.includes('ci') || pathLower.includes('cd') || pathLower.includes('monitoring') || pathLower.includes('logs') || pathLower.includes('zfs') || contentLower.includes('operations') || contentLower.includes('monitoring')) { return 'ops'; } // Product if (pathLower.includes('product') || pathLower.includes('ux') || pathLower.includes('personas') || contentLower.includes('product') || contentLower.includes('ux') || contentLower.includes('roadmap')) { return 'product'; } // Legal if (pathLower.includes('legal') || pathLower.includes('cgu') || pathLower.includes('mentions') || contentLower.includes('legal') || contentLower.includes('cgu')) { return 'legal'; } return 'misc'; } private generateSlug(relativePath: string): string { const baseName = path.basename(relativePath, path.extname(relativePath)); return baseName .toLowerCase() .replace(/[^a-z0-9-]/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, ''); } private extractTitle(content: string, relativePath: string): string { // Chercher un titre H1 const h1Match = content.match(/^#\s+(.+)$/m); if (h1Match) { return h1Match[1].trim(); } // Utiliser le nom du fichier const baseName = path.basename(relativePath, path.extname(relativePath)); return baseName .replace(/[-_]/g, ' ') .replace(/\b\w/g, l => l.toUpperCase()); } private extractImages(content: string): string[] { const imageRegex = /!\[.*?\]\((.*?)\)/g; const images: string[] = []; let match; while ((match = imageRegex.exec(content)) !== null) { images.push(match[1]); } return images; } async migrateDocs(docFiles: DocFile[]): Promise { console.log('📦 Migration des documents...'); for (const docFile of docFiles) { await this.migrateDocFile(docFile); } // Copier les assets await this.copyAssets(docFiles); } private async migrateDocFile(docFile: DocFile): Promise { const targetDir = docFile.track === 'current' ? this.currentRoot : this.visionRoot; const domainDir = path.join(targetDir, 'domains', docFile.domain); // Créer le répertoire si nécessaire if (!fs.existsSync(domainDir)) { fs.mkdirSync(domainDir, { recursive: true }); } const targetPath = path.join(domainDir, `${docFile.slug}.md`); // Préparer le contenu avec frontmatter const frontmatter = { id: docFile.slug, title: docFile.title, sidebar_label: docFile.title, ...docFile.frontmatter }; const frontmatterText = Object.entries(frontmatter) .map(([key, value]) => `${key}: ${value}`) .join('\n'); const content = `--- ${frontmatterText} --- ${docFile.content}`; // Ajouter le badge de piste const badge = docFile.track === 'current' ? '> NOTE: Cette page reflète l\'ÉTAT ACTUEL.' : '> NOTE: Cette page décrit la CIBLE (but visé).'; const finalContent = content.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, `---\n${frontmatterText}\n---\n\n${badge}\n\n`); fs.writeFileSync(targetPath, finalContent); // Mettre à jour le mapping this.pathMapping[docFile.relativePath] = `/${docFile.track}/domains/${docFile.domain}/${docFile.slug}`; // Mettre à jour les statistiques this.report.migratedFiles++; if (docFile.track === 'current') { this.report.currentFiles++; } else { this.report.visionFiles++; } if (!this.report.domainDistribution[docFile.domain]) { this.report.domainDistribution[docFile.domain] = 0; } this.report.domainDistribution[docFile.domain]++; console.log(`✅ Migré: ${docFile.relativePath} → ${docFile.track}/domains/${docFile.domain}/${docFile.slug}.md`); } private async copyAssets(docFiles: DocFile[]): Promise { console.log('🖼️ Copie des assets...'); const allImages = new Set(); docFiles.forEach(doc => { doc.images.forEach(img => allImages.add(img)); }); for (const imagePath of allImages) { if (imagePath.startsWith('http')) continue; // Ignorer les URLs const sourcePath = path.resolve(this.projectRoot, imagePath); if (fs.existsSync(sourcePath)) { const fileName = path.basename(imagePath); const targetPath = path.join(this.assetsRoot, fileName); fs.copyFileSync(sourcePath, targetPath); console.log(`📷 Copié: ${imagePath} → assets/${fileName}`); } else { this.report.brokenLinks.push(imagePath); console.log(`❌ Image manquante: ${imagePath}`); } } } async generateReport(): Promise { console.log('📊 Génération du rapport de migration...'); this.report.totalFiles = this.report.migratedFiles; this.report.pathMapping = this.pathMapping; const reportPath = path.join(this.docsRoot, '_reports', 'migration_report.md'); const pathMapPath = path.join(this.docsRoot, '_reports', 'path_map.json'); // Générer le rapport Markdown const reportContent = `# Rapport de Migration de Documentation ## Résumé - **Total des fichiers traités**: ${this.report.totalFiles} - **Fichiers migrés**: ${this.report.migratedFiles} - **État actuel**: ${this.report.currentFiles} - **Vision**: ${this.report.visionFiles} ## Distribution par domaine ${Object.entries(this.report.domainDistribution) .map(([domain, count]) => `- **${domain}**: ${count} fichiers`) .join('\n')} ## Fichiers non classés ${this.report.unclassifiedFiles.length > 0 ? this.report.unclassifiedFiles.map(file => `- ${file}`).join('\n') : 'Aucun'} ## Liens brisés ${this.report.brokenLinks.length > 0 ? this.report.brokenLinks.map(link => `- ${link}`).join('\n') : 'Aucun'} ## Mapping des chemins Le mapping complet des anciens vers les nouveaux chemins est disponible dans \`path_map.json\`. `; fs.writeFileSync(reportPath, reportContent); // Générer le mapping JSON fs.writeFileSync(pathMapPath, JSON.stringify(this.pathMapping, null, 2)); console.log(`📋 Rapport généré: ${reportPath}`); console.log(`🗺️ Mapping généré: ${pathMapPath}`); } async run(): Promise { console.log('🚀 Démarrage de la migration de documentation...'); try { const docFiles = await this.discoverDocs(); await this.migrateDocs(docFiles); await this.generateReport(); console.log('✅ Migration terminée avec succès!'); } catch (error) { console.error('❌ Erreur lors de la migration:', error); process.exit(1); } } } // Exécution du script if (require.main === module) { const projectRoot = process.argv[2] || path.join(__dirname, '..', '..'); const migrator = new DocMigrator(projectRoot); migrator.run(); } export { DocMigrator };