- New modular pipeline architecture allowing custom workflow combinations
- Per-step LLM provider configuration (Claude, OpenAI, Gemini, Deepseek, Moonshot, Mistral)
- Visual pipeline builder and runner interfaces with drag-and-drop
- 10 predefined pipeline templates (minimal-test to originality-bypass)
- Pipeline CRUD operations via ConfigManager and REST API
- Fix variable resolution in instructions (HTML tags were breaking {{variables}})
- Fix hardcoded LLM providers in AdversarialCore
- Add TESTS_LLM_PROVIDER.md documentation with validation results
- Update dashboard to disable legacy config editor
API Endpoints:
- POST /api/pipeline/save, execute, validate, estimate
- GET /api/pipeline/list, modules, templates
Backward compatible with legacy modular workflow system.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
361 lines
11 KiB
JavaScript
361 lines
11 KiB
JavaScript
// ========================================
|
|
// 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 };
|