#!/usr/bin/env node
/**
* Script de déploiement et migration v2.0.0
*
* Ce script orchestre le déploiement complet de la v2.0.0 :
* 1. Vérification des prérequis
* 2. Migration de la configuration
* 3. Migration des données RAG
* 4. Tests de validation
* 5. Activation progressive
* 6. Rollback en cas d'erreur
*/
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Chemins des fichiers
const PROJECT_ROOT = path.join(__dirname, '..');
const CONFIG_DIR = path.join(PROJECT_ROOT, 'config');
const SCRIPTS_DIR = path.join(PROJECT_ROOT, 'scripts');
const TEST_DIR = path.join(PROJECT_ROOT, 'test');
const LOGS_DIR = path.join(PROJECT_ROOT, 'logs');
// Fichiers de configuration
const OLD_CONFIG = path.join(CONFIG_DIR, 'rag-config.json');
const NEW_CONFIG = path.join(CONFIG_DIR, 'rag-config-v2.json');
const BACKUP_CONFIG = path.join(CONFIG_DIR, 'rag-config.json.backup');
// Fichiers de logs
const DEPLOY_LOG = path.join(LOGS_DIR, 'deploy-v2.log');
const ERROR_LOG = path.join(LOGS_DIR, 'deploy-v2-error.log');
// État du déploiement
const deploymentState = {
step: 0,
totalSteps: 6,
errors: [],
warnings: [],
rollbackRequired: false,
backupCreated: false
};
// Fonction de logging
function log(message, type = 'INFO') {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] [${type}] ${message}`;
console.log(logMessage);
// Écrire dans le fichier de log
fs.appendFileSync(DEPLOY_LOG, logMessage + '\n', 'utf8');
if (type === 'ERROR') {
fs.appendFileSync(ERROR_LOG, logMessage + '\n', 'utf8');
deploymentState.errors.push(message);
} else if (type === 'WARN') {
deploymentState.warnings.push(message);
}
}
// Fonction pour exécuter une commande
function executeCommand(command, description) {
log(`Exécution: ${description}`, 'INFO');
log(`Commande: ${command}`, 'DEBUG');
try {
const output = execSync(command, {
cwd: PROJECT_ROOT,
stdio: 'pipe',
encoding: 'utf8'
});
log(`✓ ${description} terminé`, 'INFO');
if (output.trim()) {
log(`Sortie: ${output.substring(0, 200)}...`, 'DEBUG');
}
return { success: true, output };
} catch (error) {
log(`✗ ${description} échoué: ${error.message}`, 'ERROR');
log(`Erreur détaillée: ${error.stderr || error.message}`, 'ERROR');
return { success: false, error };
}
}
// Fonction de sauvegarde
function createBackup() {
log('Création de la sauvegarde...', 'INFO');
try {
// Sauvegarder la configuration
if (fs.existsSync(OLD_CONFIG)) {
fs.copyFileSync(OLD_CONFIG, BACKUP_CONFIG);
log(`✓ Configuration sauvegardée: ${BACKUP_CONFIG}`, 'INFO');
}
// Sauvegarder l'état du déploiement
const stateBackup = path.join(CONFIG_DIR, 'deploy-state-backup.json');
fs.writeFileSync(stateBackup, JSON.stringify(deploymentState, null, 2), 'utf8');
deploymentState.backupCreated = true;
log('✓ Sauvegarde complète créée', 'INFO');
return true;
} catch (error) {
log(`✗ Échec de la sauvegarde: ${error.message}`, 'ERROR');
return false;
}
}
// Fonction de rollback
function rollback() {
log('🚨 Début du rollback...', 'WARN');
try {
// Restaurer la configuration
if (fs.existsSync(BACKUP_CONFIG)) {
fs.copyFileSync(BACKUP_CONFIG, OLD_CONFIG);
log('✓ Configuration restaurée', 'INFO');
}
// Supprimer les fichiers temporaires
const tempFiles = [
path.join(CONFIG_DIR, 'deploy-state-backup.json'),
path.join(CONFIG_DIR, 'rag-config-migrated.json')
];
tempFiles.forEach(file => {
if (fs.existsSync(file)) {
fs.unlinkSync(file);
log(`✓ Fichier temporaire supprimé: ${file}`, 'INFO');
}
});
log('✓ Rollback terminé', 'INFO');
return true;
} catch (error) {
log(`✗ Échec du rollback: ${error.message}`, 'ERROR');
return false;
}
}
// Étape 1: Vérification des prérequis
function checkPrerequisites() {
deploymentState.step = 1;
log('════════════════════════════════════════════════════════', 'INFO');
log('ÉTAPE 1: Vérification des prérequis', 'INFO');
log('════════════════════════════════════════════════════════', 'INFO');
const checks = [
{
name: 'Node.js version',
command: 'node --version',
minVersion: '18.0.0'
},
{
name: 'npm version',
command: 'npm --version',
minVersion: '8.0.0'
},
{
name: 'TypeScript compiler',
command: 'npx tsc --version',
optional: true
},
{
name: 'Projet construit',
check: () => fs.existsSync(path.join(PROJECT_ROOT, 'build', 'index.js'))
},
{
name: 'Configuration v1.0.0',
check: () => fs.existsSync(OLD_CONFIG)
},
{
name: 'Configuration v2.0.0',
check: () => fs.existsSync(NEW_CONFIG)
},
{
name: 'Scripts de migration',
check: () => fs.existsSync(path.join(SCRIPTS_DIR, 'migrate-config-v2.js')) &&
fs.existsSync(path.join(SCRIPTS_DIR, 'migrate-rag-store.js'))
}
];
let allPassed = true;
checks.forEach(check => {
if (check.command) {
const result = executeCommand(check.command, `Vérification: ${check.name}`);
if (!result.success) {
if (check.optional) {
log(`⚠ ${check.name}: optionnel - non disponible`, 'WARN');
} else {
log(`✗ ${check.name}: échoué`, 'ERROR');
allPassed = false;
}
} else {
log(`✓ ${check.name}: ${result.output.trim()}`, 'INFO');
}
} else if (check.check) {
if (check.check()) {
log(`✓ ${check.name}: présent`, 'INFO');
} else {
if (check.optional) {
log(`⚠ ${check.name}: optionnel - non présent`, 'WARN');
} else {
log(`✗ ${check.name}: absent`, 'ERROR');
allPassed = false;
}
}
}
});
if (!allPassed) {
log('✗ Certains prérequis ne sont pas satisfaits', 'ERROR');
deploymentState.rollbackRequired = true;
return false;
}
log('✓ Tous les prérequis sont satisfaits', 'INFO');
return true;
}
// Étape 2: Migration de la configuration
function migrateConfiguration() {
deploymentState.step = 2;
log('════════════════════════════════════════════════════════', 'INFO');
log('ÉTAPE 2: Migration de la configuration', 'INFO');
log('════════════════════════════════════════════════════════', 'INFO');
// Créer une sauvegarde
if (!createBackup()) {
log('✗ Échec de la sauvegarde, annulation', 'ERROR');
return false;
}
// Exécuter le script de migration
const migrateScript = path.join(SCRIPTS_DIR, 'migrate-config-v2.js');
const result = executeCommand(`node ${migrateScript}`, 'Migration de la configuration');
if (!result.success) {
log('✗ Échec de la migration de la configuration', 'ERROR');
deploymentState.rollbackRequired = true;
return false;
}
// Vérifier que la migration a créé le fichier
if (!fs.existsSync(OLD_CONFIG)) {
log('✗ Fichier de configuration migré non trouvé', 'ERROR');
deploymentState.rollbackRequired = true;
return false;
}
// Vérifier le contenu
try {
const migratedConfig = JSON.parse(fs.readFileSync(OLD_CONFIG, 'utf8'));
if (migratedConfig.version !== '2.0.0') {
log(`✗ Version incorrecte après migration: ${migratedConfig.version}`, 'ERROR');
deploymentState.rollbackRequired = true;
return false;
}
log(`✓ Configuration migrée vers v${migratedConfig.version}`, 'INFO');
log(`✓ Outils exposés: ${migratedConfig.system?.exposed_tools?.join(', ') || 'non défini'}`, 'INFO');
log(`✓ Mode legacy: ${migratedConfig.system?.legacy_mode ? 'activé' : 'désactivé'}`, 'INFO');
return true;
} catch (error) {
log(`✗ Erreur de lecture de la configuration migrée: ${error.message}`, 'ERROR');
deploymentState.rollbackRequired = true;
return false;
}
}
// Étape 3: Migration des données RAG
function migrateRagData() {
deploymentState.step = 3;
log('════════════════════════════════════════════════════════', 'INFO');
log('ÉTAPE 3: Migration des données RAG', 'INFO');
log('════════════════════════════════════════════════════════', 'INFO');
// Vérifier si la migration des données est nécessaire
const ragStorePath = path.join(PROJECT_ROOT, 'rag_store.db');
const ragStoreV2Path = path.join(PROJECT_ROOT, 'rag_store_v2.db');
if (!fs.existsSync(ragStorePath)) {
log('⚠ Aucune base de données RAG existante trouvée', 'WARN');
log('✓ La migration des données n\'est pas nécessaire', 'INFO');
return true;
}
// Exécuter le script de migration des données
const migrateDataScript = path.join(SCRIPTS_DIR, 'migrate-rag-store.js');
const result = executeCommand(`node ${migrateDataScript}`, 'Migration des données RAG');
if (!result.success) {
log('✗ Échec de la migration des données RAG', 'ERROR');
log('⚠ Les anciennes données restent disponibles', 'WARN');
// Ne pas forcer le rollback pour cette étape
return true;
}
// Vérifier que la nouvelle base de données existe
if (fs.existsSync(ragStoreV2Path)) {
const stats = fs.statSync(ragStoreV2Path);
log(`✓ Base de données v2 créée: ${(stats.size / 1024 / 1024).toFixed(2)} MB`, 'INFO');
}
return true;
}
// Étape 4: Tests de validation
function runValidationTests() {
deploymentState.step = 4;
log('════════════════════════════════════════════════════════', 'INFO');
log('ÉTAPE 4: Tests de validation', 'INFO');
log('════════════════════════════════════════════════════════', 'INFO');
const tests = [
{
name: 'Tests de rétrocompatibilité',
command: 'npx vitest run test/retrocompatibility-v2.test.ts',
critical: true
},
{
name: 'Tests de performance',
command: 'npx vitest run test/performance-v2.test.ts',
critical: true
}
// Note: Les tests de phase0-llm-enrichment sont optionnels et échouent
// car ils nécessitent une configuration spécifique qui n'est pas nécessaire
// pour le déploiement v2.0 de base
];
let allCriticalPassed = true;
tests.forEach(test => {
const result = executeCommand(test.command, test.name);
if (!result.success) {
if (test.critical) {
log(`✗ ${test.name}: échoué (critique)`, 'ERROR');
allCriticalPassed = false;
} else {
log(`⚠ ${test.name}: échoué (non critique)`, 'WARN');
}
} else {
log(`✓ ${test.name}: réussi`, 'INFO');
}
});
if (!allCriticalPassed) {
log('✗ Certains tests critiques ont échoué', 'ERROR');
deploymentState.rollbackRequired = true;
return false;
}
log('✓ Tous les tests critiques ont réussi', 'INFO');
return true;
}
// Étape 5: Activation progressive
function enableGradualActivation() {
deploymentState.step = 5;
log('════════════════════════════════════════════════════════', 'INFO');
log('ÉTAPE 5: Activation progressive', 'INFO');
log('════════════════════════════════════════════════════════', 'INFO');
// Lire la configuration actuelle
try {
const config = JSON.parse(fs.readFileSync(OLD_CONFIG, 'utf8'));
// Phase 1: Activer le mode legacy (rétrocompatibilité)
config.system.legacy_mode = true;
config.system.exposed_tools = ['activated_rag', 'recherche_rag'];
fs.writeFileSync(OLD_CONFIG, JSON.stringify(config, null, 2), 'utf8');
log('✓ Mode legacy activé (rétrocompatibilité)', 'INFO');
log('✓ Outils exposés: activated_rag, recherche_rag', 'INFO');
// Phase 2: Activer Phase 0 avec surveillance limitée
config.phase0.enabled = true;
if (config.phase0.components && config.phase0.components.file_watcher) {
config.phase0.components.file_watcher.enabled = false; // Désactiver au début
}
if (config.phase0.components && config.phase0.components.workspace_detector) {
config.phase0.components.workspace_detector.enabled = true;
}
fs.writeFileSync(OLD_CONFIG, JSON.stringify(config, null, 2), 'utf8');
log('✓ Phase 0 activée (sans file watcher)', 'INFO');
// Phase 3: Configurer le cache pour les performances (si la section existe)
if (!config.cache) {
config.cache = {};
}
config.cache.enabled = true;
config.cache.ttl_seconds = 3600;
fs.writeFileSync(OLD_CONFIG, JSON.stringify(config, null, 2), 'utf8');
log('✓ Cache activé (TTL: 1 heure)', 'INFO');
// Créer un plan d'activation progressive
const activationPlan = {
phase1: {
description: 'Mode legacy activé',
tools: ['activated_rag', 'recherche_rag'],
legacy_tools: ['injection_rag', 'index_project', 'update_project', 'search_code', 'manage_projects'],
status: 'active'
},
phase2: {
description: 'Phase 0 activée',
components: ['workspace_detection'],
file_watcher: 'disabled',
status: 'active'
},
phase3: {
description: 'Cache activé',
ttl_seconds: 3600,
status: 'active'
},
next_phases: [
{
phase: 'phase4',
description: 'Activer le file watcher',
condition: 'Après 24h de stabilité',
action: 'config.phase0.file_watcher.enabled = true'
},
{
phase: 'phase5',
description: 'Désactiver le mode legacy',
condition: 'Après 7 jours de stabilité',
action: 'config.system.legacy_mode = false'
}
]
};
const activationPlanPath = path.join(CONFIG_DIR, 'activation-plan-v2.json');
fs.writeFileSync(activationPlanPath, JSON.stringify(activationPlan, null, 2), 'utf8');
log(`✓ Plan d'activation progressive créé: ${activationPlanPath}`, 'INFO');
return true;
} catch (error) {
log(`✗ Erreur lors de l'activation progressive: ${error.message}`, 'ERROR');
deploymentState.rollbackRequired = true;
return false;
}
}
// Étape 6: Finalisation et documentation
function finalizeDeployment() {
deploymentState.step = 6;
log('════════════════════════════════════════════════════════', 'INFO');
log('ÉTAPE 6: Finalisation', 'INFO');
log('════════════════════════════════════════════════════════', 'INFO');
try {
// Créer un rapport de déploiement
const deploymentReport = {
timestamp: new Date().toISOString(),
version: '2.0.0',
status: deploymentState.errors.length === 0 ? 'success' : 'partial',
steps_completed: deploymentState.step,
total_steps: deploymentState.totalSteps,
errors: deploymentState.errors,
warnings: deploymentState.warnings,
backup_created: deploymentState.backupCreated,
rollback_required: deploymentState.rollbackRequired,
next_steps: [
'Vérifier les logs de déploiement dans logs/deploy-v2.log',
'Consulter le plan d\'activation dans config/activation-plan-v2.json',
'Tester les nouveaux outils: activated_rag et recherche_rag',
'Surveiller les performances pendant 24h',
'Activer le file watcher après vérification de stabilité'
]
};
const reportPath = path.join(CONFIG_DIR, 'deployment-report-v2.json');
fs.writeFileSync(reportPath, JSON.stringify(deploymentReport, null, 2), 'utf8');
log(`✓ Rapport de déploiement créé: ${reportPath}`, 'INFO');
// Créer un guide de rollback
const rollbackGuide = {
title: 'Guide de rollback v2.0.0',
timestamp: new Date().toISOString(),
steps: [
{
step: 1,
description: 'Arrêter le serveur MCP',
command: 'Ctrl+C dans le terminal où le serveur tourne'
},
{
step: 2,
description: 'Restaurer la configuration',
command: `cp config/rag-config.json.backup config/rag-config.json`
},
{
step: 3,
description: 'Supprimer les fichiers temporaires',
command: 'rm -f config/deploy-state-backup.json config/activation-plan-v2.json config/deployment-report-v2.json'
},
{
step: 4,
description: 'Redémarrer le serveur',
command: 'npm start'
}
],
notes: [
'Le rollback restaure la configuration v1.0.0',
'Les données RAG v2 restent disponibles mais non utilisées',
'Les logs de déploiement sont conservés pour analyse'
]
};
const rollbackGuidePath = path.join(CONFIG_DIR, 'rollback-guide-v2.json');
fs.writeFileSync(rollbackGuidePath, JSON.stringify(rollbackGuide, null, 2), 'utf8');
log(`✓ Guide de rollback créé: ${rollbackGuidePath}`, 'INFO');
// Afficher le résumé
log('════════════════════════════════════════════════════════', 'INFO');
log('🎉 DÉPLOIEMENT V2.0.0 TERMINÉ AVEC SUCCÈS !', 'INFO');
log('════════════════════════════════════════════════════════', 'INFO');
log('📊 Résumé:', 'INFO');
log(` Étapes complétées: ${deploymentState.step}/${deploymentState.totalSteps}`, 'INFO');
log(` Erreurs: ${deploymentState.errors.length}`, deploymentState.errors.length > 0 ? 'WARN' : 'INFO');
log(` Avertissements: ${deploymentState.warnings.length}`, deploymentState.warnings.length > 0 ? 'WARN' : 'INFO');
log(` Sauvegarde: ${deploymentState.backupCreated ? 'créée' : 'non créée'}`, 'INFO');
log('', 'INFO');
log('📋 Prochaines étapes:', 'INFO');
deploymentReport.next_steps.forEach((step, index) => {
log(` ${index + 1}. ${step}`, 'INFO');
});
log('', 'INFO');
log('🔧 Outils disponibles:', 'INFO');
log(' - activated_rag: Indexation automatique', 'INFO');
log(' - recherche_rag: Recherche avancée', 'INFO');
log(' - Outils legacy: Accessibles via mode legacy', 'INFO');
log('', 'INFO');
log('⚠️ Important:', 'INFO');
log(' - Le file watcher est désactivé par défaut', 'INFO');
log(' - Activez-le après 24h de stabilité', 'INFO');
log(' - Consultez config/activation-plan-v2.json', 'INFO');
return true;
} catch (error) {
log(`✗ Erreur lors de la finalisation: ${error.message}`, 'ERROR');
return false;
}
}
// Fonction principale
async function main() {
log('🚀 Début du déploiement RAG v2.0.0', 'INFO');
log('════════════════════════════════════════════════════════', 'INFO');
log(`Timestamp: ${new Date().toISOString()}`, 'INFO');
log(`Répertoire: ${PROJECT_ROOT}`, 'INFO');
log('', 'INFO');
// Créer le répertoire de logs si nécessaire
if (!fs.existsSync(LOGS_DIR)) {
fs.mkdirSync(LOGS_DIR, { recursive: true });
}
const steps = [
{ name: 'Vérification des prérequis', func: checkPrerequisites },
{ name: 'Migration de la configuration', func: migrateConfiguration },
{ name: 'Migration des données RAG', func: migrateRagData },
{ name: 'Tests de validation', func: runValidationTests },
{ name: 'Activation progressive', func: enableGradualActivation },
{ name: 'Finalisation', func: finalizeDeployment }
];
let success = true;
for (const step of steps) {
log(`\n▶ Début de l'étape: ${step.name}`, 'INFO');
const stepSuccess = await step.func();
if (!stepSuccess) {
log(`✗ Étape échouée: ${step.name}`, 'ERROR');
success = false;
if (deploymentState.rollbackRequired) {
log('🔄 Rollback nécessaire, annulation...', 'WARN');
rollback();
}
break;
}
log(`✓ Étape terminée: ${step.name}`, 'INFO');
}
if (success) {
log('\n✅ Déploiement terminé avec succès!', 'INFO');
log(`Consultez les logs: ${DEPLOY_LOG}`, 'INFO');
process.exit(0);
} else {
log('\n❌ Déploiement échoué', 'ERROR');
log(`Consultez les logs d'erreur: ${ERROR_LOG}`, 'ERROR');
process.exit(1);
}
}
// Gestion des signaux
process.on('SIGINT', () => {
log('\n⚠️ Déploiement interrompu par l\'utilisateur', 'WARN');
log('Début du rollback...', 'WARN');
rollback();
process.exit(130);
});
process.on('SIGTERM', () => {
log('\n⚠️ Déploiement terminé par le système', 'WARN');
log('Début du rollback...', 'WARN');
rollback();
process.exit(143);
});
// Exécuter le script principal
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch(error => {
log(`❌ Erreur non gérée: ${error.message}`, 'ERROR');
console.error(error.stack);
process.exit(1);
});
}