kira-crud
Version:
Intelligent CRUD Generator for Laravel and Angular
1,695 lines (1,509 loc) • 64.5 kB
JavaScript
#!/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