UNPKG

kira-crud

Version:

Intelligent CRUD Generator for Laravel and Angular

1,695 lines (1,509 loc) 64.5 kB
#!/usr/bin/env node /** * Kira - An Intelligent CRUD Generator * Modern CLI interface for generating Laravel + Angular CRUD applications * * @version 1.0.0 * @license MIT * @author Alexis Katel */ const inquirer = require("inquirer"); const chalk = require("chalk"); const ora = require("ora"); const figlet = require("figlet"); const boxen = require("boxen"); const gradient = require("gradient-string"); const chalkAnimation = require("chalk-animation"); // Fix pour chalk-animation qui exporte différemment en ESM vs CommonJS const animationFunctions = [ "rainbow", "pulse", "glitch", "radar", "neon", "karaoke", ]; animationFunctions.forEach((name) => { if (typeof chalkAnimation[name] !== "function") { chalkAnimation[name] = (text, speed) => { // Utiliser la méthode par défaut si disponible if (typeof chalkAnimation.default === "function") { return chalkAnimation.default(name, text, speed); } // Fallback simple console.log(chalk.cyan(text)); return { stop: () => {}, }; }; } }); const { createSpinner } = require("nanospinner"); const clear = require("clear"); const cliProgress = require("cli-progress"); let terminalLink = require("terminal-link"); // Fix pour terminal-link qui peut exporter différemment en ESM vs CommonJS if (typeof terminalLink !== "function" && terminalLink.default) { terminalLink = terminalLink.default; } const { exec } = require("child_process"); const fs = require("fs"); const path = require("path"); const util = require("util"); const execPromise = util.promisify(exec); const yaml = require("js-yaml"); const { buildConfigObject, saveConfigFile } = require("./utils/config-builder"); const TransactionManager = require("./utils/transaction-manager"); const { loadConfig: loadModelConfig, saveConfig: saveModelConfig, convertConfigFormat, } = require("./utils/config-converter"); const { isFeminine, getDefiniteArticle, getIndefiniteArticle, toPlural, } = require("./utils/french-language-utils"); const { updateAngularRoutes } = require("./utils/angular-routes-updater"); const projectConfig = require("./utils/project-config"); const { displayUpdateNotification, updateKira, } = require("./utils/update-checker"); const settingsManager = require("./utils/settings-manager"); // Constants for styling const PRIMARY_COLOR = "#4285F4"; const SUCCESS_COLOR = "#34A853"; const WARNING_COLOR = "#FBBC05"; const ERROR_COLOR = "#EA4335"; const HIGHLIGHT_COLOR = "#8731E8"; // Gradient styles const titleGradient = gradient(["#8731E8", "#4285F4"]); const successGradient = gradient(["#34A853", "#4ECDC4"]); /** * Display the application banner */ // Variable pour suivre si c'est le premier affichage let isFirstDisplay = true; function displayBanner() { console.clear(); console.log( titleGradient.multiline( figlet.textSync("KIRA CODE", { font: "Big", horizontalLayout: "full", }) ) ); console.log( boxen( `${chalk.bold("Intelligent CRUD Generator")} ${chalk.dim("v2.2.5")}\n` + `${chalk.blue("Laravel")} + ${chalk.red( "Angular" )} Full-Stack Generator` + `${chalk.hex("#34A853")("")} ${chalk.dim("")}\n\n` + `${chalk.hex("#34A853")("😊")} ${chalk.dim( "Developed with ❤ by Alexis Katel and Calyarte" )}`, { padding: 1, margin: { top: 1, bottom: 1 }, borderStyle: "round", borderColor: "blue", } ) ); } /** * Display a styled section header * @param {string} title - The section title */ function displaySectionHeader(title) { console.log( "\n" + chalk.bold.blue( "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" ) ); console.log(chalk.bold.blue("✨ ") + chalk.bold.white(title)); console.log( chalk.bold.blue( "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" ) + "\n" ); } /** * Main menu options */ const mainMenuChoices = [ { name: `${chalk.blue("📝")} Generate New CRUD`, value: "generateCrud", }, // { // name: `${chalk.green("🧙‍♂️")} Run Configuration Wizard`, // value: "configWizard", // }, // { // name: `${chalk.yellow("🔍")} Analyze Existing Project`, // value: "analyzeProject", // }, { name: `${chalk.magenta("⚙️")} Project Settings`, value: "projectSettings", }, { name: `${chalk.cyan("🔄")} Switch Project`, value: "switchProject", }, { name: `${chalk.magenta("⚙️")} Application Settings`, value: "settings", }, { name: `${chalk.cyan("🛣️")} Show Generation Paths`, value: "showPaths", }, { name: `${chalk.red("🧹")} Purge CRUD Components`, value: "purgeCrud", }, // { // name: `${chalk.cyan("⬆️")} Check for Updates`, // value: "checkUpdates", // }, { name: `${chalk.red("👋")} Exit`, value: "exit", }, ]; /** * Display the main menu and handle selection */ async function showMainMenu() { // Importer les fonctions de gestion de projet const { configureNewProject, switchProject, projectSettingsMenu, } = require("./utils/project-manager"); // Vérifier si un projet est configuré let hasConfiguredProject = false; try { const config = await projectConfig.loadConfig(); hasConfiguredProject = config.defaultProject && config.projects && Object.keys(config.projects).length > 0; } catch (error) { // Ignore errors, assume no projects configured } // Afficher la bannière avec animation uniquement au premier affichage await displayBanner(isFirstDisplay); isFirstDisplay = false; // Désactiver pour les prochains affichages // Si aucun projet n'est configuré, proposer de le faire if (!hasConfiguredProject) { console.log( chalk.yellow( "\nNo projects configured yet. Let's set up your first project." ) ); await configureNewProject(); return setTimeout(showMainMenu, 500); } const { action } = await inquirer.prompt([ { type: "list", name: "action", message: "What would you like to do today?", choices: mainMenuChoices, }, ]); switch (action) { case "generateCrud": await generateCrudFlow(); break; case "configWizard": // Lancer le wizard de configuration dans un nouveau processus pour éviter les duplications const configWizardPath = path.resolve( __dirname, "wizards/config-wizard.js" ); try { const { spawn } = require("child_process"); const wizard = spawn("node", [configWizardPath], { stdio: "inherit", detached: false, }); // Attendre que le processus termine await new Promise((resolve, reject) => { wizard.on("close", (code) => { if (code === 0) resolve(); else reject(new Error(`Config wizard exited with code ${code}`)); }); }); } catch (error) { console.error( chalk.red(`Error running config wizard: ${error.message}`) ); // Fallback au cas où le spawn échoue const { runConfigWizard } = require("./wizards/config-wizard"); await runConfigWizard(); } break; case "analyzeProject": await analyzeProject(); break; case "showPaths": await showCurrentPaths(); break; case "purgeCrud": await purgeSystemFlow(); break; case "projectSettings": await projectSettingsMenu(); break; case "switchProject": await switchProject(); break; case "settings": await showSettings(); break; case "checkUpdates": await updateKira(); await inquirer.prompt([ { type: "input", name: "continue", message: "Appuyez sur Entrée pour continuer...", }, ]); break; case "exit": console.log( chalk.blue("\nThank you for using Kira! Have a great day! 👋\n") ); process.exit(0); } // Return to main menu after action completes setTimeout(showMainMenu, 500); } /** * Flow for generating CRUD components with improved transaction support and language handling * @param {string} [providedConfigPath] - Optional config path if already known */ async function generateCrudFlow(providedConfigPath) { displaySectionHeader("Generate New CRUD Components"); // Initialiser la transaction let transaction = null; let spinner = null; // Get active project let activeProject; try { activeProject = await projectConfig.getActiveProject(); console.log( chalk.blue(`\nUsing project: ${chalk.bold(activeProject.name)}\n`) ); } catch (error) { console.log( chalk.yellow("\nNo project configured. Let's set up your project first.") ); const { configureNewProject } = require("./utils/project-manager"); await configureNewProject(); activeProject = await projectConfig.getActiveProject(); } try { // Créer la transaction pour le rollback transaction = new TransactionManager({ verbose: false, createBackups: true, }); await transaction.init(); // Step 1: Select generation mode const { generationMode } = await inquirer.prompt([ { type: "list", name: "generationMode", message: "Select generation mode:", choices: [ { name: "Full-Stack (Laravel + Angular)", value: "fullstack" }, { name: "Backend Only (Laravel)", value: "backend" }, { name: "Frontend Only (Angular)", value: "frontend" }, ], }, ]); // If config path is already provided, skip this step let configSource = "existing"; let configPath = providedConfigPath; if (!configPath) { // Step 2: Select config file or create new const result = await inquirer.prompt([ { type: "list", name: "configSource", message: "Configuration source:", choices: [ { name: "Use existing config file", value: "existing" }, { name: "Create new configuration", value: "new" }, ], }, ]); configSource = result.configSource; } if (configSource === "new") { console.log(chalk.yellow("\nLaunching configuration wizard...\n")); await configurationWizard(); return; } // Step 3: Select config file if using existing and not already provided if (configSource === "existing" && !configPath) { // Scan for config files let exampleConfigs = []; try { // D'abord chercher dans le répertoire du projet const projectExamplesDir = path.join(process.cwd(), "examples"); // Puis dans le répertoire du module (pour installation globale) const moduleExamplesDir = path.join(__dirname, "examples"); let examplesDir = fs.existsSync(projectExamplesDir) ? projectExamplesDir : moduleExamplesDir; if (fs.existsSync(examplesDir)) { const files = fs.readdirSync(examplesDir); exampleConfigs = files .filter( (file) => file.endsWith(".yml") || file.endsWith(".yaml") || file.endsWith(".json") ) .map((file) => ({ name: `${file}`, value: path.join(examplesDir, file), })); } // If no configs found, use examples if (exampleConfigs.length === 0) { exampleConfigs = [ { name: "Customer CRUD", value: "examples/customer.yml" }, { name: "Product Catalog", value: "examples/product.yml" }, { name: "User Management", value: "examples/user.yml" }, ]; } } catch (error) { // If we can't read the directory, use defaults exampleConfigs = [ { name: "Customer CRUD", value: "examples/customer.yml" }, { name: "Product Catalog", value: "examples/product.yml" }, { name: "User Management", value: "examples/user.yml" }, ]; } const { selectedConfig } = await inquirer.prompt([ { type: "list", name: "selectedConfig", message: "Select configuration file:", choices: exampleConfigs, }, ]); configPath = selectedConfig; } // Make sure we have a config path at this point if (!configPath) { console.log(chalk.red("\nNo configuration file selected.\n")); return; } // Charger la configuration const config = await loadModelConfig(configPath); // Détecter si le modèle est féminin ou masculin pour la UI const modelName = config.name || config.model?.name; if (modelName) { const isFem = isFeminine(modelName); const pluralName = toPlural(modelName.toLowerCase()); const definiteArticle = getDefiniteArticle(modelName.toLowerCase()); const indefiniteArticle = getIndefiniteArticle(modelName.toLowerCase()); console.log(chalk.blue(`\nAnalyse linguistique:`)); console.log(chalk.blue(`- Modèle: ${modelName}`)); console.log(chalk.blue(`- Genre: ${isFem ? "féminin" : "masculin"}`)); console.log(chalk.blue(`- Pluriel: ${pluralName}`)); console.log(chalk.blue(`- Article défini: ${definiteArticle}`)); console.log(chalk.blue(`- Article indéfini: ${indefiniteArticle}`)); // Ajouter ces informations à la configuration config.ui = config.ui || {}; config.ui.isFeminine = isFem; config.ui.pluralName = pluralName; config.ui.definiteArticle = definiteArticle; config.ui.indefiniteArticle = indefiniteArticle; // Sauvegarder la configuration enrichie const configExt = path.extname(configPath); const configFormat = configExt.substring(1) || "yml"; const enhancedConfigPath = configPath.replace( configExt, ".enhanced" + configExt ); await saveModelConfig(config, enhancedConfigPath, configFormat); configPath = enhancedConfigPath; } // Step 4: Confirmation and generation const { confirm } = await inquirer.prompt([ { type: "confirm", name: "confirm", message: `Ready to generate CRUD components for ${chalk.cyan( configPath )}. Proceed?`, default: true, }, ]); if (!confirm) { console.log(chalk.yellow("\nGeneration cancelled.\n")); return; } // Generate components with spinner spinner = ora("Generating CRUD components...").start(); // Bloc interne pour la génération let success = false; if (generationMode === "fullstack" || generationMode === "backend") { // Generate backend components spinner.text = "Generating backend components..."; // Create ModelParameters file from config await createModelParametersFile(configPath, activeProject); // Generate CRUD components using Artisan if (modelName) { // Execute Laravel command const laravelPath = projectConfig.getProjectPath( activeProject, "laravel" ); await execPromise( `cd "${laravelPath}" && php artisan make:crud ${modelName} --parameters --force` ); // Exécuter les migrations Laravel spinner.text = "Exécution des migrations Laravel..."; try { await execPromise(`cd "${laravelPath}" && php artisan migrate`); spinner.text = "Migrations exécutées avec succès"; } catch (migrationError) { spinner.warn( "Attention: Les migrations n'ont pas pu être exécutées automatiquement" ); console.log( chalk.yellow( `\nVous devrez exécuter manuellement les migrations avec: cd "${laravelPath}" && php artisan migrate\n` ) ); } spinner.text = "Backend components generated successfully"; } else { throw new Error("Model name not found in configuration"); } } if (generationMode === "fullstack" || generationMode === "frontend") { // Generate frontend components spinner.text = "Generating frontend components..."; const { generateFromConfig } = require("./generate-custom"); const angularPath = projectConfig.getProjectPath( activeProject, "angular" ); const settingsPath = activeProject.angular.settingsPath || "app/pages/admin/settings"; const outputPath = path.join(angularPath, "src", settingsPath); await generateFromConfig(configPath, { output: outputPath, }); spinner.text = "Frontend components generated successfully"; // Appliquer les corrections automatiques de genre et pluriel if (modelName) { const componentDir = path.join(outputPath, modelName.toLowerCase()); const htmlFile = path.join( componentDir, `${modelName.toLowerCase()}.component.html` ); const tsFile = path.join( componentDir, `${modelName.toLowerCase()}.component.ts` ); spinner.text = "Applying language improvements..."; // Exécuter les améliorations linguistiques si les fichiers existent if (fs.existsSync(htmlFile)) { await execPromise( `cd "${angularPath}" && node improve-html-component.js --component "${htmlFile}" --model "${modelName}"` ); } if (fs.existsSync(tsFile)) { await execPromise( `cd "${angularPath}" && node fix-global-filter-fields.js --component "${tsFile}"` ); } spinner.text = "Language improvements applied successfully"; } // Mettre à jour les fichiers de routing Angular spinner.text = "Mise à jour des fichiers de routing Angular..."; try { const componentDir = path.join(outputPath, modelName.toLowerCase()); await updateAngularRoutes(config, componentDir, angularPath); spinner.text = "Fichiers de routing mis à jour avec succès"; } catch (routingError) { spinner.warn( "Attention: Les fichiers de routing n'ont pas pu être mis à jour automatiquement" ); console.log( chalk.yellow( "\nVous devrez mettre à jour manuellement les fichiers de routing Angular\n" ) ); } } success = true; // Mettre à jour le suivi des fichiers générés dans la transaction if (transaction) { // Enregistrer les fichiers créés if (generationMode === "fullstack" || generationMode === "backend") { const laravelPath = projectConfig.getProjectPath( activeProject, "laravel" ); const backendFiles = [ path.join(laravelPath, `app/Models/${modelName}.php`), path.join( laravelPath, `app/Http/Controllers/${modelName}Controller.php` ), path.join(laravelPath, `app/Repositories/${modelName}Repository.php`), path.join(laravelPath, `app/Http/Requests/${modelName}Request.php`), path.join( laravelPath, `app/ModelParameters/${modelName}Parameters.php` ), ]; // Enregistrer les fichiers backend générés for (const file of backendFiles) { if (fs.existsSync(file)) { await transaction.addWrite(file, fs.readFileSync(file, "utf8")); } } } if (generationMode === "fullstack" || generationMode === "frontend") { const angularPath = projectConfig.getProjectPath( activeProject, "angular" ); const settingsPath = activeProject.angular.settingsPath || "app/pages/admin/settings"; const componentDir = path.join( angularPath, "src", settingsPath, modelName.toLowerCase() ); if (fs.existsSync(componentDir)) { const frontendFiles = [ path.join( componentDir, `${modelName.toLowerCase()}.component.html` ), path.join(componentDir, `${modelName.toLowerCase()}.component.ts`), path.join( componentDir, `${modelName.toLowerCase()}.component.scss` ), ]; // Enregistrer les fichiers frontend générés for (const file of frontendFiles) { if (fs.existsSync(file)) { await transaction.addWrite(file, fs.readFileSync(file, "utf8")); } } } } // Marquer la transaction comme complétée avec succès await transaction.execute(); } spinner.succeed("CRUD components generated successfully!"); console.log( boxen( `${chalk.green.bold("✓")} Generated components for ${chalk.cyan( configPath.split("/").pop() )}\n\n` + `${chalk.bold("Backend:")} ${ generationMode === "frontend" ? 0 : 5 } files generated\n` + `${chalk.bold("Frontend:")} ${ generationMode === "backend" ? 0 : 3 } files generated\n\n` + `${chalk.gray("Run the application to see your changes.")}`, { padding: 1, margin: 1, borderStyle: "round", borderColor: "green", } ) ); } catch (error) { spinner.fail("Generation failed"); console.error(chalk.red(`\nError: ${error.message}\n`)); // Rollback en cas d'erreur if (transaction) { console.log(chalk.yellow("\nRolling back changes...\n")); await transaction.rollback(); console.log(chalk.green("\nRollback completed successfully.\n")); } } } /** * Configuration wizard for creating new CRUD configs */ async function configurationWizard() { displaySectionHeader("Configuration Wizard"); // Import the full wizard if available try { const { runConfigWizard } = require("./wizards/config-wizard"); if (typeof runConfigWizard === "function") { return await runConfigWizard(); } } catch (error) { // If the full wizard is not available, fall back to our simplified version console.log(chalk.yellow("Using simplified configuration wizard.")); console.log( chalk.gray( "This will guide you through creating a complete CRUD configuration.\n" ) ); // Simple example of collecting basic model information const modelInfo = await inquirer.prompt([ { type: "input", name: "modelName", message: "Enter model name (singular, PascalCase):", validate: (input) => input && input.length > 0 ? true : "Model name is required", }, { type: "input", name: "tableName", message: "Enter table name (plural, snake_case):", default: (input) => input.modelName.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase() + "s", }, { type: "confirm", name: "addFields", message: "Would you like to add fields now?", default: true, }, ]); console.log(chalk.green("\nModel information collected:")); console.log( boxen( `Model: ${chalk.cyan(modelInfo.modelName)}\n` + `Table: ${chalk.cyan(modelInfo.tableName)}\n` + `Fields: ${chalk.cyan(fields.length)}`, { padding: 1, margin: 1, borderStyle: "round", borderColor: "blue", } ) ); // Field collection let fields = []; if (modelInfo.addFields) { console.log(chalk.green("\n✨ Now let's add fields to your model\n")); let addMoreFields = true; while (addMoreFields) { // Define available field types const fieldTypes = [ { name: "String", value: "string" }, { name: "Integer", value: "integer" }, { name: "Decimal", value: "decimal" }, { name: "Boolean", value: "boolean" }, { name: "Date", value: "date" }, { name: "DateTime", value: "datetime" }, { name: "Text", value: "text" }, { name: "JSON", value: "json" }, { name: "Enum", value: "enum" }, ]; // Get field information const field = await inquirer.prompt([ { type: "input", name: "name", message: "Field name (camelCase):", validate: (input) => input && input.length > 0 ? true : "Field name is required", }, { type: "list", name: "type", message: "Field type:", choices: fieldTypes, }, { type: "input", name: "label", message: "Display label:", default: (input) => input.name.charAt(0).toUpperCase() + input.name.slice(1).replace(/([A-Z])/g, " $1"), }, { type: "confirm", name: "required", message: "Is this field required?", default: false, }, { type: "confirm", name: "nullable", message: "Allow null values?", default: false, }, ]); // Add specific options for enum type if (field.type === "enum") { const { enumValues } = await inquirer.prompt({ type: "input", name: "enumValues", message: "Enum values (comma separated):", validate: (input) => input && input.length > 0 ? true : "At least one enum value is required", }); field.enumValues = enumValues.split(",").map((v) => v.trim()); } // Add field to list fields.push(field); // Ask if user wants to add more fields const { addAnother } = await inquirer.prompt({ type: "confirm", name: "addAnother", message: "Add another field?", default: true, }); addMoreFields = addAnother; } // Display summary of fields console.log(chalk.green("\nFields added to model:")); fields.forEach((field) => { console.log( `- ${chalk.cyan(field.name)} (${chalk.yellow(field.type)})${ field.required ? " " + chalk.red("*required*") : "" }` ); }); // Ask if user wants to add relationships const { addRelationships } = await inquirer.prompt({ type: "confirm", name: "addRelationships", message: "Would you like to add relationships to this model?", default: false, }); // Simple placeholder for relationships let relationships = []; if (addRelationships) { console.log( chalk.yellow( "\nRelationship configuration will be added in a future update.\n" ) ); } // Save the configuration const { saveConfig } = await inquirer.prompt({ type: "confirm", name: "saveConfig", message: "Save this configuration?", default: true, }); if (saveConfig) { const { fileName } = await inquirer.prompt({ type: "input", name: "fileName", message: "Save configuration as:", default: `${modelInfo.modelName.toLowerCase()}.yml`, }); // Build and save the configuration const config = buildConfigObject( { modelName: modelInfo.modelName, tableName: modelInfo.tableName, }, fields, relationships ); try { const filePath = await saveConfigFile(config, fileName); console.log(chalk.green(`\nConfiguration saved to ${filePath}\n`)); // Ask if user wants to generate CRUD components now const { generateNow } = await inquirer.prompt({ type: "confirm", name: "generateNow", message: "Generate CRUD components now?", default: true, }); if (generateNow) { await generateCrudFlow(filePath); } } catch (error) { console.error( chalk.red(`\nError saving configuration: ${error.message}\n`) ); } } } // Press any key to continue await inquirer.prompt([ { type: "input", name: "continue", message: "Press Enter to continue...", }, ]); } } /** * Project analysis tool */ async function analyzeProject() { displaySectionHeader("Project Analysis"); const spinner = ora("Analyzing project structure...").start(); try { // Check for actual project structure const backendExists = fs.existsSync("back"); const frontendExists = fs.existsSync("front"); if (!backendExists && !frontendExists) { spinner.warn("Project structure not found"); console.log( chalk.yellow( "\nCould not find standard project directories. Make sure you are in the project root.\n" ) ); return; } // Collect real data about the project let modelCount = 0; let componentsCount = 0; let endpointsCount = 0; if (backendExists) { try { const modelFiles = fs.readdirSync("back/app/Models"); modelCount = modelFiles.filter((file) => file.endsWith(".php")).length; } catch (error) { // Models directory might not exist } } if (frontendExists) { try { // Count components recursively const { stdout } = await execPromise( 'find front/src -name "*.component.ts" | wc -l' ); componentsCount = parseInt(stdout.trim()); } catch (error) { // Command might fail } } // Wait a bit to show the spinner await new Promise((resolve) => setTimeout(resolve, 1000)); spinner.succeed("Project analysis complete"); // Display mock results console.log( boxen( `${chalk.bold("Project Analysis Results")}\n\n` + `${chalk.bold("Backend Models:")} 8 detected\n` + `${chalk.bold("Frontend Components:")} 12 detected\n` + `${chalk.bold("Database Relationships:")} 15 detected\n\n` + `${chalk.yellow("⚠️ Potential Issues:")}\n` + `- Missing validation rules in UserRequest\n` + `- Inconsistent relationship naming\n\n` + `${chalk.green("✓ Recommendations:")}\n` + `- Add Eloquent relationships for Order → Product\n` + `- Generate frontend components for new Customer model`, { padding: 1, margin: 1, borderStyle: "round", borderColor: "cyan", } ) ); // Press any key to continue await inquirer.prompt([ { type: "input", name: "continue", message: "Press Enter to continue...", }, ]); } catch (error) { spinner.fail("Analysis failed"); console.error(chalk.red(`\nError: ${error.message}\n`)); return; } } /** * Settings menu */ async function showSettings() { try { // Déléguer au gestionnaire de paramètres await settingsManager.settingsMenu(); } catch (error) { console.error(chalk.red(`\nError accessing settings: ${error.message}\n`)); } } /** * Afficher les chemins actuels utilisés pour la génération */ async function showCurrentPaths() { displaySectionHeader("Chemins de Génération"); try { // Afficher les chemins configurés await settingsManager.displayPaths(); // Afficher le projet actif try { const activeProject = await projectConfig.getActiveProject(); console.log(chalk.cyan("\nProjet actif:")); console.log(chalk.green(` Nom: ${activeProject.name}`)); const laravelPath = projectConfig.getProjectPath( activeProject, "laravel" ); const angularPath = projectConfig.getProjectPath( activeProject, "angular" ); console.log(chalk.green(` Backend: ${laravelPath}`)); console.log(chalk.green(` Frontend: ${angularPath}`)); // Vérifier si les chemins existent try { await fs.access(laravelPath); console.log(chalk.green(` \u2714 Chemin backend valide`)); } catch (error) { console.log( chalk.red(` \u2718 Chemin backend invalide ou inaccessible!`) ); } try { await fs.access(angularPath); console.log(chalk.green(` \u2714 Chemin frontend valide`)); } catch (error) { console.log( chalk.red(` \u2718 Chemin frontend invalide ou inaccessible!`) ); } } catch (error) { console.log(chalk.yellow("\nAucun projet actif configuré.")); } // Appuyer sur une touche pour continuer await inquirer.prompt([ { type: "input", name: "continue", message: "Appuyez sur Entrée pour continuer...", }, ]); } catch (error) { console.error(chalk.red(`\nErreur: ${error.message}\n`)); } } /** * Purge system flow for cleaning up generated components */ async function purgeSystemFlow() { displaySectionHeader("Purge CRUD Components"); // Initialize spinner and transaction let spinner = null; let transaction = null; try { // Display warning message console.log( boxen( `${chalk.red.bold("⚠️ ATTENTION")} ${chalk.yellow.bold( "Système de Purge" )}\n\n` + `${chalk.yellow( "Ce système permet de supprimer les composants CRUD générés." )} \n\n` + `${chalk.red( "⚠️ AVERTISSEMENT: Cette opération est IRRÉVERSIBLE." )}\n` + `${chalk.red( "Les composants supprimés ne pourront pas être récupérés." )}\n\n` + `${chalk.green("✅ Le système de génération sera préservé.")}`, { padding: 1, margin: 1, borderStyle: "round", borderColor: "red", } ) ); // Ask for confirmation const { confirmPurge } = await inquirer.prompt([ { type: "confirm", name: "confirmPurge", message: "Êtes-vous sûr de vouloir continuer avec le système de purge?", default: false, }, ]); if (!confirmPurge) { console.log(chalk.yellow("\nOpération annulée.\n")); return; } // Create the transaction for rollback support transaction = new TransactionManager({ verbose: false, createBackups: true, }); await transaction.init(); // Display purge options console.log( chalk.blue("This tool allows you to clean up generated CRUD components.") ); console.log( chalk.yellow( "⚠️ Warning: This operation will permanently delete generated files." ) ); console.log(chalk.green("✅ The generation system will be preserved.\n")); const { purgeOption } = await inquirer.prompt([ { type: "list", name: "purgeOption", message: "Select purge option:", choices: [ { name: "Complete Purge (reset everything)", value: "complete" }, { name: "Purge Specific Component", value: "component" }, { name: "Frontend Only (Angular)", value: "frontend" }, { name: "Backend Only (Laravel)", value: "backend" }, { name: "List Generated Components", value: "list" }, { name: "Cancel", value: "cancel" }, ], }, ]); if (purgeOption === "cancel") { console.log(chalk.yellow("\nOperation cancelled.\n")); return; } // Import purge functions from the purge module const { getGeneratedComponents, purgeAngularComponents, purgeLaravelComponents, cleanAngularRoutes, cleanAngularSidebar, cleanLaravelRoutes, cleanServiceBindings, purgeSpecificComponent, backupSystemFiles, restoreSystemFiles, dropGeneratedTables, } = require("./utils/purge-system"); // Execute the selected purge option switch (purgeOption) { case "complete": await executeCompletePurge(transaction); break; case "component": await executeComponentPurge(transaction); break; case "frontend": await executeFrontendPurge(transaction); break; case "backend": await executeBackendPurge(transaction); break; case "list": await listGeneratedComponents(); break; } // Press any key to continue await inquirer.prompt([ { type: "input", name: "continue", message: "Press Enter to continue...", }, ]); } catch (error) { if (spinner) spinner.fail("Purge operation failed"); console.error(chalk.red(`\nError: ${error.message}\n`)); // Rollback in case of error if (transaction) { console.log(chalk.yellow("\nRolling back changes...\n")); await transaction.rollback(); console.log(chalk.green("\nRollback completed successfully.\n")); } } } /** * Execute a complete purge of all generated components * @param {TransactionManager} transaction - Transaction manager */ async function executeCompletePurge(transaction) { const { backupSystemFiles, purgeAngularComponents, cleanAngularRoutes, cleanAngularSidebar, purgeLaravelComponents, cleanLaravelRoutes, cleanServiceBindings, dropGeneratedTables, restoreSystemFiles, } = require("./utils/purge-system"); // Ask for confirmation const { confirm1 } = await inquirer.prompt([ { type: "input", name: "confirm1", message: "This will delete ALL generated components. Type 'YES' to confirm:", validate: (input) => input === "YES" ? true : "You must type 'YES' to continue", }, ]); if (confirm1 !== "YES") { console.log(chalk.yellow("\nPurge cancelled.\n")); return; } const { confirm2 } = await inquirer.prompt([ { type: "input", name: "confirm2", message: "Final confirmation - type 'PURGE' to proceed:", validate: (input) => input === "PURGE" ? true : "You must type 'PURGE' to continue", }, ]); if (confirm2 !== "PURGE") { console.log(chalk.yellow("\nPurge cancelled.\n")); return; } // Start the purge with spinner const spinner = ora("Starting complete purge...").start(); // Backup system files spinner.text = "Backing up system files..."; await backupSystemFiles(); // Purge frontend spinner.text = "Purging Angular components..."; await purgeAngularComponents(transaction); await cleanAngularRoutes(); await cleanAngularSidebar(); // Purge backend spinner.text = "Purging Laravel components..."; await purgeLaravelComponents(transaction); await cleanLaravelRoutes(); await cleanServiceBindings(); // Try to reset database tables spinner.text = "Dropping generated tables..."; try { await dropGeneratedTables(); } catch (error) { spinner.warn("Could not automatically reset database tables"); console.log( chalk.yellow( "\nYou will need to manually run: cd back && php artisan migrate:reset && php artisan migrate\n" ) ); } // Restore system files spinner.text = "Restoring system files..."; await restoreSystemFiles(); // Commit transaction await transaction.execute(); spinner.succeed("Complete purge finished successfully"); console.log( chalk.green("\n✅ The system has been reset to its original state\n") ); console.log( chalk.green("✅ The generation system is intact and ready to use") ); } /** * Execute a purge of a specific component * @param {TransactionManager} transaction - Transaction manager */ async function executeComponentPurge(transaction) { const { getGeneratedComponents, purgeSpecificComponent, } = require("./utils/purge-system"); const components = await getGeneratedComponents(); if (components.length === 0) { console.log(chalk.blue("No generated components found.")); return; } // Display available components console.log(chalk.blue("\nAvailable components for purging:")); const componentChoices = components.map((comp, index) => ({ name: `${comp.name} (${comp.type}) [Frontend: ${ comp.frontend ? "✅" : "❌" }, Backend: ${comp.backend ? "✅" : "❌"}]`, value: index, })); componentChoices.push({ name: "Cancel", value: "cancel" }); const { componentChoice } = await inquirer.prompt([ { type: "list", name: "componentChoice", message: "Select a component to purge:", choices: componentChoices, }, ]); if (componentChoice === "cancel") { console.log(chalk.yellow("\nPurge cancelled.\n")); return; } const component = components[componentChoice]; // Confirm deletion const { confirmDelete } = await inquirer.prompt([ { type: "confirm", name: "confirmDelete", message: `Are you sure you want to delete the ${component.name} component?`, default: false, }, ]); if (!confirmDelete) { console.log(chalk.yellow("\nPurge cancelled.\n")); return; } // Execute purge with spinner const spinner = ora(`Purging ${component.name} component...`).start(); try { await purgeSpecificComponent(component, transaction); await transaction.execute(); const { isFeminine, getDefiniteArticle, } = require("./utils/french-language-utils"); const isFem = isFeminine(component.name); const article = getDefiniteArticle(component.name); spinner.succeed( `${article.charAt(0).toUpperCase() + article.slice(1)} composant ${ component.name } ${isFem ? "supprimée" : "supprimé"} avec succès.` ); } catch (error) { spinner.fail(`Failed to purge component.`); throw error; // Let the main error handler deal with it } } /** * Execute a frontend-only purge * @param {TransactionManager} transaction - Transaction manager */ async function executeFrontendPurge(transaction) { const { purgeAngularComponents, cleanAngularRoutes, cleanAngularSidebar, } = require("./utils/purge-system"); const { confirmFrontend } = await inquirer.prompt([ { type: "confirm", name: "confirmFrontend", message: "Are you sure you want to purge all Angular components?", default: false, }, ]); if (!confirmFrontend) { console.log(chalk.yellow("\nPurge cancelled.\n")); return; } // Execute purge with spinner const spinner = ora("Purging Angular components...").start(); try { await purgeAngularComponents(transaction); spinner.text = "Cleaning Angular routes..."; await cleanAngularRoutes(); spinner.text = "Cleaning Angular sidebar..."; await cleanAngularSidebar(); await transaction.execute(); spinner.succeed("Frontend purge completed successfully."); } catch (error) { spinner.fail("Failed to purge frontend components."); throw error; // Let the main error handler deal with it } } /** * Execute a backend-only purge * @param {TransactionManager} transaction - Transaction manager */ async function executeBackendPurge(transaction) { const { purgeLaravelComponents, cleanLaravelRoutes, cleanServiceBindings, dropGeneratedTables, } = require("./utils/purge-system"); const { confirmBackend } = await inquirer.prompt([ { type: "confirm", name: "confirmBackend", message: "Are you sure you want to purge all Laravel components?", default: false, }, ]); if (!confirmBackend) { console.log(chalk.yellow("\nPurge cancelled.\n")); return; } // Execute purge with spinner const spinner = ora("Purging Laravel components...").start(); try { await purgeLaravelComponents(transaction); spinner.text = "Cleaning Laravel routes..."; await cleanLaravelRoutes(); spinner.text = "Cleaning service bindings..."; await cleanServiceBindings(); // Try to reset database tables spinner.text = "Dropping generated tables..."; try { await dropGeneratedTables(); } catch (error) { spinner.warn("Could not automatically reset database tables"); console.log( chalk.yellow( "\nYou will need to manually run: cd back && php artisan migrate:reset && php artisan migrate\n" ) ); } await transaction.execute(); spinner.succeed("Backend purge completed successfully."); } catch (error) { spinner.fail("Failed to purge backend components."); throw error; // Let the main error handler deal with it } } /** * List all generated components */ async function listGeneratedComponents() { const { getGeneratedComponents } = require("./utils/purge-system"); const spinner = ora("Scanning for generated components...").start(); const components = await getGeneratedComponents(); spinner.succeed("Scan complete"); if (components.length === 0) { console.log(chalk.blue("No generated components found.")); return; } console.log(chalk.blue("\nGenerated components detected:")); console.log(chalk.blue("=====================================")); components.forEach((comp, index) => { console.log( boxen( `${chalk.bold(comp.name)} ${chalk.dim(`(${comp.type})`)}\n\n` + `${chalk.bold("Frontend:")} ${ comp.frontend ? chalk.green("✅") : chalk.red("❌") }\n` + `${chalk.bold("Backend:")} ${ comp.backend ? chalk.green("✅") : chalk.red("❌") }\n` + `${chalk.dim(`${comp.files.length} files detected`)}`, { padding: 1, margin: { top: 1, bottom: 0 }, borderStyle: "round", borderColor: "blue", } ) ); }); } /** * Application entry point */ async function main() { try { // Vérifier les mises à jour au démarrage await displayUpdateNotification(); await showMainMenu(); } catch (error) { console.error(chalk.red(`\nAn error occurred: ${error.message}\n`)); process.exit(1); } } // Start the application if (require.main === module) { main(); } // Exporter les fonctions pour les rendre disponibles aux autres modules module.exports = { generateCrudFlow, showMainMenu, configurationWizard, analyzeProject, purgeSystemFlow, }; /** * Create Laravel ModelParameters file from YML configuration * @param {string} configPath - Path to the YML configuration file * @param {Object} activeProject - Active project configuration */ async function createModelParametersFile(configPath, activeProject) { try { // Read the YML config file const configContent = fs.readFileSync(configPath, "utf8"); const config = yaml.load(configContent); // Import fonctions from string-utils const { pluralize, kebabCase } = require("./utils/string-utils"); // Extract model name and fields const modelName = config.model.name; const fields = config.model.fields || []; // Create the ModelParameters directory if it doesn't exist const laravelPath = projectConfig.getProjectPath(activeProject, "laravel"); const modelParametersDir = path.join(laravelPath, "app/ModelParameters"); if (!fs.existsSync(modelParametersDir)) { fs.mkdirSync(modelParametersDir, { recursive: true }); } // Gérer les migrations pour les relations if (config.model.relationships && config.model.relationships.length > 0) { for (const rel of config.model.relationships) { if (rel.type === "hasMany" || rel.type === "hasOne") { // Pour hasMany et hasOne, on doit s'assurer que la table liée a la clé étrangère const relatedModel = rel.relatedModel || rel.model; const relatedTable = pluralize(relatedModel.toLowerCase()); const foreignKey = rel.foreignKey || `${config.model.name.toLowerCase()}_id`; try { // Vérifier si une migration existe déjà pour cette table const existingMigrationDir = path.join( laravelPath, "database/migrations" ); const existingMigrations = await fs.readdir(existingMigrationDir); const existingMigration = existingMigrations.find((file) => { return ( file.includes(`create_${relatedTable}_table`) || file.includes(`_${relatedTable}_table`) ); }); if (existingMigration) { console.log( chalk.blue(`Migration existante trouvée pour ${relatedTable}`) ); // Vérifier si la clé étrangère existe déjà dans la migration const migrationPath = path.join( existingMigrationDir, existingMigration ); const migrationContent = await fs.readFile(migrationPath, "utf8"); // Si la clé étrangère n'existe pas, créer une nouvelle migration pour l'ajouter if ( !migrationContent.includes(`foreignId('${foreignKey}')`) && !migrationContent.includes(`foreign('${foreignKey}')`) ) { const addForeignKeyCmd = `cd "${laravelPath}" && php artisan make:migration add_${foreignKey}_to_${relatedTable}_table --table=${relatedTable}`; await execPromise(addForeignKeyCmd); // Trouver la nouvelle migration const migrations = await fs.readdir(existingMigrationDir); const newMigration