UNPKG

kira-crud

Version:

Intelligent CRUD Generator for Laravel and Angular

559 lines (482 loc) 16.9 kB
#!/usr/bin/env node /** * Configuration Wizard * Interactive CLI for generating CRUD configuration files */ const inquirer = require('inquirer'); const chalk = require('chalk'); const ora = require('ora'); const figlet = require('figlet'); const boxen = require('boxen'); const path = require('path'); const fs = require('fs').promises; const gradient = require('gradient-string'); const yaml = require('js-yaml'); const { execSync } = require('child_process'); // Import wizards const { runModelWizard } = require('./model-wizard'); const { validateConfig, displayValidationResults } = require('../utils/config-validator'); const { buildConfigObject, saveConfigFile } = require('../utils/config-builder'); // Constants for styling const titleGradient = gradient(['#8731E8', '#4285F4']); /** * Display the wizard banner */ function displayBanner() { console.clear(); // console.log( // titleGradient.multiline( // figlet.textSync('KIRA CONFIG', { // font: 'Big', // horizontalLayout: "full", // }) // ) // ); console.log( boxen( `${chalk.bold('Kira Configuration Wizard')} ${chalk.dim('v1.0.0')}\n` + `Create complete CRUD configurations with ease`, { 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('📝')} Create New Model Configuration`, value: 'newModel' }, // { // name: `${chalk.green('⚡')} Quick Model Configuration`, // value: 'quickModel' // }, { name: `${chalk.green('🔄')} Edit Existing Configuration`, value: 'editConfig' }, // { // name: `${chalk.yellow('🔍')} Validate Configuration`, // value: 'validateConfig' // }, // { // name: `${chalk.magenta('📊')} Analyze Database for Models`, // value: 'analyzeDb' // }, { name: `${chalk.red('👋')} Exit`, value: 'exit' } ]; /** * Find existing configuration files * @returns {Promise<Array>} Array of configuration files */ async function findConfigFiles() { try { // Look in the examples directory and the current directory const directories = ['examples', '.']; let configFiles = []; for (const dir of directories) { try { const files = await fs.readdir(dir); const configs = files .filter(file => file.endsWith('.yml') || file.endsWith('.yaml') || file.endsWith('.json')) .map(file => ({ name: `${file} (${dir})`, value: path.join(dir, file) })); configFiles = [...configFiles, ...configs]; } catch (error) { // Directory might not exist, that's fine } } return configFiles; } catch (error) { console.error(chalk.red(`Error finding configuration files: ${error.message}`)); return []; } } /** * Analyze database to suggest models */ async function analyzeDatabaseForModels() { displaySectionHeader('Database Analysis'); const spinner = ora('Analyzing database structure...').start(); try { // This is a placeholder for actual database analysis // In a real implementation, you would connect to the database and extract schema information await new Promise(resolve => setTimeout(resolve, 2000)); const databaseTables = [ { name: 'users', columns: ['id', 'name', 'email', 'password', 'created_at', 'updated_at'] }, { name: 'posts', columns: ['id', 'title', 'content', 'user_id', 'created_at', 'updated_at'] }, { name: 'comments', columns: ['id', 'post_id', 'user_id', 'content', 'created_at', 'updated_at'] }, { name: 'categories', columns: ['id', 'name', 'created_at', 'updated_at'] }, { name: 'post_category', columns: ['post_id', 'category_id', 'created_at', 'updated_at'] } ]; spinner.succeed('Database analysis complete'); console.log(chalk.green('\nFound the following tables:')); databaseTables.forEach(table => { console.log(`\n${chalk.cyan(table.name)}`); console.log(`Columns: ${table.columns.join(', ')}`); }); // Check for potential relationships console.log(chalk.yellow('\nPotential relationships detected:')); console.log(`- ${chalk.cyan('users')} has many ${chalk.cyan('posts')} (via user_id)`); console.log(`- ${chalk.cyan('users')} has many ${chalk.cyan('comments')} (via user_id)`); console.log(`- ${chalk.cyan('posts')} has many ${chalk.cyan('comments')} (via post_id)`); console.log(`- ${chalk.cyan('posts')} belongs to many ${chalk.cyan('categories')} (via post_category)`); // Ask if user wants to generate config for a table const { generateForTable } = await inquirer.prompt({ type: 'confirm', name: 'generateForTable', message: 'Would you like to generate a configuration for one of these tables?', default: true }); if (generateForTable) { const { selectedTable } = await inquirer.prompt({ type: 'list', name: 'selectedTable', message: 'Select a table to generate configuration for:', choices: databaseTables.map(table => ({ name: table.name, value: table })) }); // Convert snake_case to PascalCase for model name const modelName = selectedTable.name .split('_') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(''); console.log(chalk.green(`\nGenerating configuration for ${selectedTable.name} (Model: ${modelName})`)); // This would feed initial data to the model wizard // For now, we'll just call the model wizard directly await runModelWizard(); } } catch (error) { spinner.fail('Database analysis failed'); console.error(chalk.red(`\nError: ${error.message}`)); } } /** * Edit an existing configuration file */ async function editExistingConfig() { displaySectionHeader('Edit Existing Configuration'); const configFiles = await findConfigFiles(); if (configFiles.length === 0) { console.log(chalk.yellow('No configuration files found.')); return; } const { selectedConfig } = await inquirer.prompt({ type: 'list', name: 'selectedConfig', message: 'Select a configuration file to edit:', choices: configFiles }); try { const spinner = ora('Loading configuration...').start(); // Load the configuration file const content = await fs.readFile(selectedConfig, 'utf8'); let config; if (selectedConfig.endsWith('.yml') || selectedConfig.endsWith('.yaml')) { config = yaml.load(content); } else if (selectedConfig.endsWith('.json')) { config = JSON.parse(content); } else { throw new Error('Unsupported file format'); } spinner.succeed('Configuration loaded successfully'); // Display the configuration summary console.log(chalk.green('\nConfiguration Summary:')); console.log(boxen( `Model: ${chalk.cyan(config.model?.name || 'Not defined')}\n` + `Table: ${chalk.cyan(config.model?.tableName || 'Not defined')}\n` + `Fields: ${chalk.cyan(config.model?.fields?.length || 0)}\n` + `Relationships: ${chalk.cyan(config.model?.relationships?.length || 0)}`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'blue' } )); // Options for editing const { editAction } = await inquirer.prompt({ type: 'list', name: 'editAction', message: 'What would you like to do?', choices: [ { name: 'Add new field', value: 'addField' }, { name: 'Add new relationship', value: 'addRelation' }, { name: 'Edit existing field', value: 'editField' }, { name: 'Edit existing relationship', value: 'editRelation' }, { name: 'Remove field', value: 'removeField' }, { name: 'Remove relationship', value: 'removeRelation' }, { name: 'Edit UI options', value: 'editUi' }, { name: 'Cancel', value: 'cancel' } ] }); if (editAction === 'cancel') { return; } console.log(chalk.yellow('\nThis feature is currently under development.')); // This would contain the logic for each editing action // For now, it's just a placeholder } catch (error) { console.error(chalk.red(`\nError: ${error.message}`)); } } /** * Validate a configuration file */ async function validateConfigFile() { displaySectionHeader('Validate Configuration'); const configFiles = await findConfigFiles(); if (configFiles.length === 0) { console.log(chalk.yellow('No configuration files found.')); return; } const { selectedConfig } = await inquirer.prompt({ type: 'list', name: 'selectedConfig', message: 'Select a configuration file to validate:', choices: configFiles }); const spinner = ora('Validating configuration...').start(); try { // Load the configuration file const content = await fs.readFile(selectedConfig, 'utf8'); let config; if (selectedConfig.endsWith('.yml') || selectedConfig.endsWith('.yaml')) { config = yaml.load(content); } else if (selectedConfig.endsWith('.json')) { config = JSON.parse(content); } else { throw new Error('Unsupported file format'); } // Validate the configuration const validationResult = validateConfig(config); spinner.succeed('Validation complete'); // Display validation results displayValidationResults(validationResult); } catch (error) { spinner.fail('Validation failed'); console.error(chalk.red(`\nError: ${error.message}`)); } } /** * Display the main menu and handle selection */ async function showMainMenu() { displayBanner(); const { action } = await inquirer.prompt([ { type: 'list', name: 'action', message: 'What would you like to do?', choices: mainMenuChoices } ]); switch (action) { case 'newModel': await runModelWizard(); break; case 'quickModel': await runQuickModelWizard(); break; case 'editConfig': await editExistingConfig(); break; case 'validateConfig': await validateConfigFile(); break; case 'analyzeDb': await analyzeDatabaseForModels(); break; case 'exit': console.log(chalk.blue('\nThank you for using the KIRA Configuration Wizard! 👋\n')); process.exit(0); } // Ask if the user wants to return to the main menu const { returnToMenu } = await inquirer.prompt({ type: 'confirm', name: 'returnToMenu', message: 'Return to main menu?', default: true }); if (returnToMenu) { setTimeout(showMainMenu, 500); } else { console.log(chalk.blue('\nThank you for using the KIRA Configuration Wizard! 👋\n')); process.exit(0); } } /** * Application entry point */ async function main() { try { await showMainMenu(); } catch (error) { console.error(chalk.red(`\nAn error occurred: ${error.message}\n`)); process.exit(1); } } // Allow direct execution or import if (require.main === module) { main(); } /** * Run a simplified quick model wizard */ async function runQuickModelWizard() { displaySectionHeader('Quick Model Configuration'); // Step 1: Basic model information const modelInfo = await inquirer.prompt([ { type: 'input', name: 'name', message: 'Model name (PascalCase):', validate: input => input && input.length > 0 ? true : 'Model name is required' }, { type: 'input', name: 'tableName', message: 'Database table name (snake_case):', default: input => input.name.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase() + 's' }, { type: 'list', name: 'architecture', message: 'Backend architecture:', choices: [ { name: 'Advanced (Repository, Service, etc.)', value: 'advanced' }, { name: 'Classic (Controller, Model only)', value: 'classic' } ], default: 'advanced' } ]); // Step 2: Collect fields console.log(chalk.green('\n✨ Now let\'s configure fields\n')); const fields = []; let addMoreFields = true; while (addMoreFields) { // 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: [ { 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' } ] }, { type: 'confirm', name: 'required', message: 'Is this field required?', default: false } ]); // Add to fields array 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; } // Step 3: Save configuration const { fileName } = await inquirer.prompt({ type: 'input', name: 'fileName', message: 'Save configuration as:', default: `${modelInfo.name.toLowerCase()}.yml` }); // Build and save config const config = await buildConfigObject( { name: modelInfo.name, tableName: modelInfo.tableName, displayName: modelInfo.name }, fields, [], // No relationships in quick wizard {}, // Default UI options {}, // Default route options { architecture: modelInfo.architecture } // Backend architecture ); try { const filePath = await saveConfigFile(config, fileName); console.log(chalk.green(`\nConfiguration saved to ${filePath}`)); // Ask if user wants to generate code const { generateNow } = await inquirer.prompt({ type: 'confirm', name: 'generateNow', message: 'Generate CRUD components now?', default: true }); if (generateNow) { try { console.log(chalk.blue('Lancement de la génération CRUD...')); // Utiliser le script dédié à la génération CRUD const generateCrudPath = path.resolve(__dirname, '../generate-crud.js'); // Exécuter le script directement en l'important try { const { runGenerator } = require(generateCrudPath); runGenerator(filePath); } catch (importError) { console.error(chalk.red(`Erreur lors de l'importation du générateur: ${importError.message}`)); // Fallback: exécuter en tant que processus séparé const { exec } = require('child_process'); exec(`node "${generateCrudPath}" "${filePath}"`, (error, stdout, stderr) => { // Afficher la sortie if (stdout) console.log(stdout); if (stderr) console.error(chalk.red(stderr)); if (error) { console.log(chalk.yellow('\nErreur lors de la génération. Veuillez utiliser le menu principal.\n')); } }); } console.log(chalk.green('La génération a été lancée. Utilisez le menu principal pour plus d\'options.')); } catch (error) { console.log(chalk.yellow(`\nErreur lors de la préparation de la génération: ${error.message}\nVeuillez utiliser le menu principal pour générer le CRUD.\n`)); } } return config; } catch (error) { console.error(chalk.red(`\nError: ${error.message}`)); } } module.exports = { runConfigWizard: main, runModelWizard, runQuickModelWizard };