// ======================================== // FICHIER: ConfigManager.js // RESPONSABILITÉ: Gestion CRUD des configurations modulaires et pipelines // STOCKAGE: Fichiers JSON dans configs/ et configs/pipelines/ // ======================================== const fs = require('fs').promises; const path = require('path'); const { logSh } = require('./ErrorReporting'); const { PipelineDefinition } = require('./pipeline/PipelineDefinition'); class ConfigManager { constructor() { this.configDir = path.join(__dirname, '../configs'); this.pipelinesDir = path.join(__dirname, '../configs/pipelines'); this.ensureConfigDir(); } async ensureConfigDir() { try { await fs.mkdir(this.configDir, { recursive: true }); await fs.mkdir(this.pipelinesDir, { recursive: true }); logSh(`📁 Dossiers configs vérifiés: ${this.configDir}`, 'DEBUG'); } catch (error) { logSh(`⚠️ Erreur création dossier configs: ${error.message}`, 'WARNING'); } } /** * Sauvegarder une configuration * @param {string} name - Nom de la configuration * @param {object} config - Configuration modulaire * @returns {object} - { success: true, name: sanitizedName } */ async saveConfig(name, config) { const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.configDir, `${sanitizedName}.json`); const configData = { name: sanitizedName, displayName: name, config, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; await fs.writeFile(filePath, JSON.stringify(configData, null, 2), 'utf-8'); logSh(`💾 Config sauvegardée: ${name} → ${sanitizedName}.json`, 'INFO'); return { success: true, name: sanitizedName }; } /** * Charger une configuration * @param {string} name - Nom de la configuration * @returns {object} - Configuration complète */ async loadConfig(name) { const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.configDir, `${sanitizedName}.json`); try { const data = await fs.readFile(filePath, 'utf-8'); const configData = JSON.parse(data); logSh(`📂 Config chargée: ${name}`, 'DEBUG'); return configData; } catch (error) { logSh(`❌ Config non trouvée: ${name}`, 'ERROR'); throw new Error(`Configuration "${name}" non trouvée`); } } /** * Lister toutes les configurations * @returns {array} - Liste des configurations avec métadonnées */ async listConfigs() { try { const files = await fs.readdir(this.configDir); const jsonFiles = files.filter(f => f.endsWith('.json')); const configs = await Promise.all( jsonFiles.map(async (file) => { const filePath = path.join(this.configDir, file); const data = await fs.readFile(filePath, 'utf-8'); const configData = JSON.parse(data); return { name: configData.name, displayName: configData.displayName || configData.name, createdAt: configData.createdAt, updatedAt: configData.updatedAt }; }) ); // Trier par date de mise à jour (plus récent en premier) return configs.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt) ); } catch (error) { logSh(`⚠️ Erreur listing configs: ${error.message}`, 'WARNING'); return []; } } /** * Supprimer une configuration * @param {string} name - Nom de la configuration * @returns {object} - { success: true } */ async deleteConfig(name) { const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.configDir, `${sanitizedName}.json`); await fs.unlink(filePath); logSh(`🗑️ Config supprimée: ${name}`, 'INFO'); return { success: true }; } /** * Vérifier si une configuration existe * @param {string} name - Nom de la configuration * @returns {boolean} */ async configExists(name) { const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.configDir, `${sanitizedName}.json`); try { await fs.access(filePath); return true; } catch { return false; } } /** * Mettre à jour une configuration existante * @param {string} name - Nom de la configuration * @param {object} config - Nouvelle configuration * @returns {object} - { success: true, name: sanitizedName } */ async updateConfig(name, config) { const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.configDir, `${sanitizedName}.json`); // Charger config existante pour garder createdAt const existingData = await this.loadConfig(name); const configData = { name: sanitizedName, displayName: name, config, createdAt: existingData.createdAt, // Garder date création updatedAt: new Date().toISOString() }; await fs.writeFile(filePath, JSON.stringify(configData, null, 2), 'utf-8'); logSh(`♻️ Config mise à jour: ${name}`, 'INFO'); return { success: true, name: sanitizedName }; } // ======================================== // PIPELINE MANAGEMENT // ======================================== /** * Sauvegarder un pipeline * @param {object} pipelineDefinition - Définition complète du pipeline * @returns {object} - { success: true, name: sanitizedName } */ async savePipeline(pipelineDefinition) { // Validation du pipeline const validation = PipelineDefinition.validate(pipelineDefinition); if (!validation.valid) { throw new Error(`Pipeline invalide: ${validation.errors.join(', ')}`); } const sanitizedName = pipelineDefinition.name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.pipelinesDir, `${sanitizedName}.json`); // Ajouter metadata de sauvegarde const pipelineData = { ...pipelineDefinition, metadata: { ...pipelineDefinition.metadata, savedAt: new Date().toISOString() } }; await fs.writeFile(filePath, JSON.stringify(pipelineData, null, 2), 'utf-8'); logSh(`💾 Pipeline sauvegardé: ${pipelineDefinition.name} → ${sanitizedName}.json`, 'INFO'); return { success: true, name: sanitizedName }; } /** * Charger un pipeline * @param {string} name - Nom du pipeline * @returns {object} - Pipeline complet */ async loadPipeline(name) { const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.pipelinesDir, `${sanitizedName}.json`); try { const data = await fs.readFile(filePath, 'utf-8'); const pipeline = JSON.parse(data); // Validation du pipeline chargé const validation = PipelineDefinition.validate(pipeline); if (!validation.valid) { throw new Error(`Pipeline chargé invalide: ${validation.errors.join(', ')}`); } logSh(`📂 Pipeline chargé: ${name}`, 'DEBUG'); return pipeline; } catch (error) { logSh(`❌ Pipeline non trouvé: ${name}`, 'ERROR'); throw new Error(`Pipeline "${name}" non trouvé`); } } /** * Lister tous les pipelines * @returns {array} - Liste des pipelines avec métadonnées */ async listPipelines() { try { const files = await fs.readdir(this.pipelinesDir); const jsonFiles = files.filter(f => f.endsWith('.json')); const pipelines = await Promise.all( jsonFiles.map(async (file) => { const filePath = path.join(this.pipelinesDir, file); const data = await fs.readFile(filePath, 'utf-8'); const pipeline = JSON.parse(data); // Obtenir résumé du pipeline const summary = PipelineDefinition.getSummary(pipeline); return { name: pipeline.name, description: pipeline.description, steps: summary.totalSteps, summary: summary.summary, estimatedDuration: summary.duration.formatted, tags: pipeline.metadata?.tags || [], createdAt: pipeline.metadata?.created, savedAt: pipeline.metadata?.savedAt }; }) ); // Trier par date de sauvegarde (plus récent en premier) return pipelines.sort((a, b) => { const dateA = new Date(a.savedAt || a.createdAt || 0); const dateB = new Date(b.savedAt || b.createdAt || 0); return dateB - dateA; }); } catch (error) { logSh(`⚠️ Erreur listing pipelines: ${error.message}`, 'WARNING'); return []; } } /** * Supprimer un pipeline * @param {string} name - Nom du pipeline * @returns {object} - { success: true } */ async deletePipeline(name) { const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.pipelinesDir, `${sanitizedName}.json`); await fs.unlink(filePath); logSh(`🗑️ Pipeline supprimé: ${name}`, 'INFO'); return { success: true }; } /** * Vérifier si un pipeline existe * @param {string} name - Nom du pipeline * @returns {boolean} */ async pipelineExists(name) { const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.pipelinesDir, `${sanitizedName}.json`); try { await fs.access(filePath); return true; } catch { return false; } } /** * Mettre à jour un pipeline existant * @param {string} name - Nom du pipeline * @param {object} pipelineDefinition - Nouvelle définition * @returns {object} - { success: true, name: sanitizedName } */ async updatePipeline(name, pipelineDefinition) { // Validation const validation = PipelineDefinition.validate(pipelineDefinition); if (!validation.valid) { throw new Error(`Pipeline invalide: ${validation.errors.join(', ')}`); } const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_'); const filePath = path.join(this.pipelinesDir, `${sanitizedName}.json`); // Charger pipeline existant pour garder metadata originale let existingMetadata = {}; try { const existing = await this.loadPipeline(name); existingMetadata = existing.metadata || {}; } catch { // Pipeline n'existe pas encore, on continue } const pipelineData = { ...pipelineDefinition, metadata: { ...existingMetadata, ...pipelineDefinition.metadata, created: existingMetadata.created || pipelineDefinition.metadata?.created, updated: new Date().toISOString(), savedAt: new Date().toISOString() } }; await fs.writeFile(filePath, JSON.stringify(pipelineData, null, 2), 'utf-8'); logSh(`♻️ Pipeline mis à jour: ${name}`, 'INFO'); return { success: true, name: sanitizedName }; } /** * Cloner un pipeline * @param {string} sourceName - Nom du pipeline source * @param {string} newName - Nom du nouveau pipeline * @returns {object} - { success: true, name: sanitizedName } */ async clonePipeline(sourceName, newName) { const sourcePipeline = await this.loadPipeline(sourceName); const clonedPipeline = PipelineDefinition.clone(sourcePipeline, newName); return await this.savePipeline(clonedPipeline); } } module.exports = { ConfigManager };