kira-crud
Version:
Intelligent CRUD Generator for Laravel and Angular
559 lines (482 loc) • 16.9 kB
JavaScript
#!/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
};