UNPKG

makecord-create

Version:

Create advanced Discord Bots with Makecord - Now with TypeScript support

609 lines (535 loc) 23.7 kB
#!/usr/bin/env node import inquirer from 'inquirer'; import * as fsPromises from 'fs/promises'; import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, lstatSync, copyFileSync } from 'fs'; import path from 'path'; import chalk from 'chalk'; import gradient from 'gradient-string'; import boxen from 'boxen'; import { fileURLToPath } from 'url'; import { Command } from 'commander'; import figlet from 'figlet'; import { Listr } from 'listr2'; import logSymbols from 'log-symbols'; import updateNotifier from 'update-notifier'; import Conf from 'conf'; // Initialize configuration store const config = new Conf({ projectName: 'makecord', defaults: { lastUsedOptions: {}, recentProjects: [] } }); // Check for updates const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')); updateNotifier({ pkg }).notify(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Initialize CLI const program = new Command(); program .name('makecord-create') .description('Create advanced Discord bots with ease') .version(pkg.version) .option('-y, --yes', 'Skip prompts and use default or saved settings') .option('-t, --typescript', 'Use TypeScript template') .option('--no-install', 'Skip installing dependencies') .option('--debug', 'Show detailed error messages for debugging') .parse(process.argv); const options = program.opts(); // Display welcome message console.log( boxen( gradient.pastel.multiline( figlet.textSync('Makecord', { font: 'Small' }) + `\n⭐ Version ${pkg.version} ⭐\n` + 'https://npmjs.com/makecord-create\n' + `Discord.js v14.19.3` ), { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'cyan', title: 'Discord Bot Creator', titleAlignment: 'center' } ) ); // Get last used settings const lastUsedOptions = config.get('lastUsedOptions'); // Define questions with improved options const questions = [ { type: 'input', name: 'projectName', message: 'Enter the project name (you can use the existing folder by leaving it blank):', default: lastUsedOptions.projectName || '', validate: input => { if (input && !input.match(/^[a-zA-Z0-9-_]+$/)) { return 'Project name can only contain letters, numbers, hyphens, and underscores'; } return true; } }, { type: 'list', name: 'language', message: 'Choose project language:', choices: ['JavaScript', 'TypeScript'], default: options.typescript ? 'TypeScript' : (lastUsedOptions.language || 'JavaScript') }, { type: 'list', name: 'database', message: 'Select database support:', choices: [ { name: 'None', value: 'none' }, { name: 'MongoDB', value: 'mongodb' }, { name: 'SQLite', value: 'sqlite' }, { name: 'PostgreSQL', value: 'postgres' } ], default: lastUsedOptions.database || 'none' }, { type: 'confirm', name: 'restApi', message: 'Add REST API support?', default: lastUsedOptions.restApi !== undefined ? lastUsedOptions.restApi : true }, { type: 'confirm', name: 'slashCommands', message: 'Add Discord slash commands support?', default: lastUsedOptions.slashCommands !== undefined ? lastUsedOptions.slashCommands : true }, { type: 'confirm', name: 'hotReload', message: 'Enable hot-reloading for development?', default: lastUsedOptions.hotReload !== undefined ? lastUsedOptions.hotReload : true }, { type: 'list', name: 'commandHandler', message: 'Choose command handler type:', choices: ['Classic', 'Advanced with categories', 'Minimal'], default: lastUsedOptions.commandHandler || 'Advanced with categories' }, { type: 'confirm', name: 'docker', message: 'Add Docker support?', default: lastUsedOptions.docker !== undefined ? lastUsedOptions.docker : false }, { type: 'confirm', name: 'github', message: 'Add GitHub Actions workflow?', default: lastUsedOptions.github !== undefined ? lastUsedOptions.github : false }, { type: 'confirm', name: 'installDeps', message: 'Install dependencies automatically?', default: options.install !== undefined ? options.install : (lastUsedOptions.installDeps !== undefined ? lastUsedOptions.installDeps : true), when: () => options.install === undefined } ]; /** * Asynchronously copies a folder and its contents * @param {string} from - Source directory * @param {string} to - Destination directory * @returns {Promise<void>} */ async function copyFolder(from, to) { try { // Validate parameters if (!from || !to) { throw new Error(`Invalid parameters: from=${from}, to=${to}`); } // Check if source directory exists if (!existsSync(from)) { throw new Error(`Source directory does not exist: ${from}`); } // Create destination directory if it doesn't exist if (!existsSync(to)) { mkdirSync(to, { recursive: true }); } // Read source directory const entries = await fsPromises.readdir(from, { withFileTypes: true }); // Process each entry await Promise.all(entries.map(async (entry) => { const fromPath = path.join(from, entry.name); const toPath = path.join(to, entry.name); if (entry.isDirectory()) { // Recursively copy subdirectories await copyFolder(fromPath, toPath); } else { // Copy files await fsPromises.copyFile(fromPath, toPath); } })); return true; } catch (error) { console.error(`Error copying folder: ${error.message}`); throw new Error(`Failed to copy folder from ${from} to ${to}: ${error.message}`); } } /** * Creates a new Discord bot project based on user selections */ async function createProject() { try { // Skip prompts if --yes flag is used let answers; if (options.yes) { answers = { ...lastUsedOptions, projectName: lastUsedOptions.projectName || '', installDeps: options.install !== undefined ? options.install : (lastUsedOptions.installDeps || true) }; console.log(chalk.blue('Using saved or default settings...')); } else { // Get user input answers = await inquirer.prompt(questions); } // Display project details console.log('\n' + chalk.cyan('Project Details:')); console.log(chalk.yellow('Project Name: ') + (answers.projectName || 'Current Folder')); console.log(chalk.yellow('Language: ') + answers.language); console.log(chalk.yellow('Database: ') + (answers.database !== 'none' ? answers.database : 'None')); console.log(chalk.yellow('REST API: ') + (answers.restApi ? 'Yes' : 'No')); console.log(chalk.yellow('Slash Commands: ') + (answers.slashCommands ? 'Yes' : 'No')); console.log(chalk.yellow('Hot Reload: ') + (answers.hotReload ? 'Yes' : 'No')); console.log(chalk.yellow('Command Handler: ') + answers.commandHandler); console.log(chalk.yellow('Docker Support: ') + (answers.docker ? 'Yes' : 'No')); console.log(chalk.yellow('GitHub Actions: ') + (answers.github ? 'Yes' : 'No')); // Confirm settings if not using --yes flag if (!options.yes) { const confirmation = await inquirer.prompt([ { type: 'confirm', name: 'confirm', message: 'Do you confirm these settings?', default: true } ]); if (!confirmation.confirm) { console.log(chalk.red('Operation cancelled.')); process.exit(0); } } // Save settings for future use config.set('lastUsedOptions', answers); // Create project tasks const tasks = new Listr([ { title: 'Setting up project directory', task: (ctx) => { // Define project path ctx.projectPath = answers.projectName ? path.join(process.cwd(), answers.projectName) : process.cwd(); // Create project directory if needed if (answers.projectName && !existsSync(ctx.projectPath)) { mkdirSync(ctx.projectPath, { recursive: true }); } return ctx; } }, { title: 'Copying base template', task: async (ctx, task) => { try { // Validate context if (!ctx.projectPath) { throw new Error('Project path is not defined. Please try again.'); } // Select template based on language choice const templateDir = answers.language.toLowerCase(); const templatePath = path.join(__dirname, 'templates', templateDir); task.output = `Using template: ${templatePath}`; // Check if template exists if (!existsSync(templatePath)) { throw new Error(`Template for ${answers.language} not found. Please create the template directory first.`); } // Copy template files task.output = `Copying files to: ${ctx.projectPath}`; await copyFolder(templatePath, ctx.projectPath); task.output = 'Template files copied successfully'; } catch (error) { console.error('Error in copying template:', error); throw error; } } }, { title: 'Configuring package.json', task: async (ctx) => { const packageJsonPath = path.join(ctx.projectPath, 'package.json'); // Check if package.json exists if (!existsSync(packageJsonPath)) { throw new Error('package.json not found in template'); } // Update package.json const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); packageJson.name = answers.projectName || path.basename(process.cwd()); // Add TypeScript dependencies if needed if (answers.language === 'TypeScript') { packageJson.devDependencies = { ...packageJson.devDependencies, "typescript": "^5.3.3", "@types/node": "^20.10.5", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0" }; // Update scripts for TypeScript packageJson.scripts = { ...packageJson.scripts, "build": "tsc", "dev": "ts-node-dev --respawn src/index.ts", "start": "node dist/index.js" }; } // Store package.json for other tasks ctx.packageJson = packageJson; ctx.packageJsonPath = packageJsonPath; } }, { title: 'Adding database support', enabled: () => answers.database !== 'none', task: async (ctx) => { const dbTemplatePath = path.join(__dirname, 'templates', 'database', answers.database); const dbFilePath = path.join(ctx.projectPath, 'src', 'database'); // Check if database template exists if (!existsSync(dbTemplatePath)) { throw new Error(`Database template for ${answers.database} not found`); } // Create database directory if (!existsSync(dbFilePath)) { mkdirSync(dbFilePath, { recursive: true }); } // Copy database files await copyFolder(dbTemplatePath, dbFilePath); // Add database dependencies switch (answers.database) { case 'mongodb': ctx.packageJson.dependencies.mongoose = '^7.6.5'; break; case 'sqlite': ctx.packageJson.dependencies.sqlite3 = '^5.1.6'; ctx.packageJson.dependencies.sequelize = '^6.35.2'; break; case 'postgres': ctx.packageJson.dependencies.pg = '^8.11.3'; ctx.packageJson.dependencies['pg-hstore'] = '^2.3.4'; ctx.packageJson.dependencies.sequelize = '^6.35.2'; break; } } }, { title: 'Adding REST API support', enabled: () => answers.restApi, task: async (ctx) => { const apiTemplatePath = path.join(__dirname, 'templates', 'features', 'rest-api'); const apiTargetPath = path.join(ctx.projectPath, 'src', 'api'); // Check if API template exists if (!existsSync(apiTemplatePath)) { throw new Error('REST API template not found'); } // Copy API files await copyFolder(apiTemplatePath, apiTargetPath); // Add API dependencies ctx.packageJson.dependencies.express = '^4.18.2'; ctx.packageJson.dependencies['express-rate-limit'] = '^7.1.5'; ctx.packageJson.dependencies.cors = '^2.8.5'; ctx.packageJson.dependencies.helmet = '^7.1.0'; } }, { title: 'Adding slash commands support', enabled: () => answers.slashCommands, task: async (ctx) => { const slashTemplatePath = path.join(__dirname, 'templates', 'features', 'slash-commands'); const slashTargetPath = path.join(ctx.projectPath, 'src', 'commands', 'slash'); // Check if slash commands template exists if (!existsSync(slashTemplatePath)) { throw new Error('Slash commands template not found'); } // Create slash commands directory if (!existsSync(slashTargetPath)) { mkdirSync(slashTargetPath, { recursive: true }); } // Copy slash commands files await copyFolder(slashTemplatePath, slashTargetPath); } }, { title: 'Adding hot-reload support', enabled: () => answers.hotReload, task: async (ctx) => { const hotReloadPath = path.join(__dirname, 'templates', 'features', 'hot-reload'); const targetPath = path.join(ctx.projectPath, 'src', 'utils'); // Check if hot-reload template exists if (!existsSync(hotReloadPath)) { throw new Error('Hot-reload template not found'); } // Create utils directory if (!existsSync(targetPath)) { mkdirSync(targetPath, { recursive: true }); } // Copy hot-reload files await copyFolder(hotReloadPath, targetPath); // Add hot-reload dependencies ctx.packageJson.dependencies.chokidar = '^3.5.3'; // Add dev script if not already added if (!ctx.packageJson.scripts.dev) { ctx.packageJson.scripts.dev = answers.language === 'TypeScript' ? 'ts-node-dev --respawn src/index.ts' : 'nodemon src/index.js'; } } }, { title: 'Setting up command handler', task: async (ctx) => { // Format handler name for directory const handlerDirName = answers.commandHandler.toLowerCase() .replace(/ with categories/g, '_with_categories') .replace(/ /g, '_'); const handlerPath = path.join(__dirname, 'templates', 'handlers', handlerDirName); const targetHandlerPath = path.join(ctx.projectPath, 'src', 'handlers'); // Check if handler template exists if (!existsSync(handlerPath)) { throw new Error(`Command handler template for "${answers.commandHandler}" not found`); } // Create handlers directory if (!existsSync(targetHandlerPath)) { mkdirSync(targetHandlerPath, { recursive: true }); } // Copy handler files await copyFolder(handlerPath, targetHandlerPath); } }, { title: 'Adding Docker support', enabled: () => answers.docker, task: async (ctx) => { const dockerTemplatePath = path.join(__dirname, 'templates', 'features', 'docker'); // Check if Docker template exists if (!existsSync(dockerTemplatePath)) { throw new Error('Docker template not found'); } // Copy Docker files await copyFolder(dockerTemplatePath, ctx.projectPath); } }, { title: 'Adding GitHub Actions workflow', enabled: () => answers.github, task: async (ctx) => { const githubTemplatePath = path.join(__dirname, 'templates', 'features', 'github'); const githubTargetPath = path.join(ctx.projectPath, '.github'); // Check if GitHub template exists if (!existsSync(githubTemplatePath)) { throw new Error('GitHub Actions template not found'); } // Create .github directory if (!existsSync(githubTargetPath)) { mkdirSync(githubTargetPath, { recursive: true }); } // Copy GitHub files await copyFolder(githubTemplatePath, githubTargetPath); } }, { title: 'Saving package.json', task: async (ctx) => { // Write updated package.json writeFileSync(ctx.packageJsonPath, JSON.stringify(ctx.packageJson, null, 2)); } }, { title: 'Installing dependencies', enabled: () => answers.installDeps, task: async (ctx, task) => { return new Promise(async (resolve) => { try { const { execSync } = await import('child_process'); task.output = 'Running npm install...'; execSync('npm install', { cwd: ctx.projectPath, stdio: 'pipe' }); // Add project to recent projects list const recentProjects = config.get('recentProjects') || []; config.set('recentProjects', [ { path: ctx.projectPath, name: ctx.packageJson.name, created: new Date().toISOString() }, ...recentProjects.slice(0, 4) // Keep only 5 most recent ]); resolve(); } catch (error) { task.skip('Failed to install dependencies. Please run npm install manually.'); resolve(); } }); } } ], { concurrent: false, rendererOptions: { showSubtasks: true, collapse: false } }); // Run all tasks with initial context const context = await tasks.run({}); // Display success message console.log('\n' + gradient.pastel.multiline( logSymbols.success + ' Project successfully created!' )); // Show next steps console.log('\n' + gradient.pastel('⭐ Next steps: ⭐')); if (answers.projectName) { console.log(chalk.cyan(`cd ${answers.projectName}`)); } if (!answers.installDeps) { console.log(chalk.cyan('npm install')); } if (answers.language === 'TypeScript') { console.log(chalk.cyan('npm run build')); } console.log(chalk.cyan(answers.hotReload ? 'npm run dev' : 'npm start')); if (answers.docker) { console.log(chalk.cyan('docker-compose up -d')); } console.log('\n' + gradient.pastel('⭐ Happy Coding! ⭐')); // Add project to recent projects if (context && context.projectPath && context.packageJson) { const recentProjects = config.get('recentProjects') || []; if (!recentProjects.some(p => p.path === context.projectPath)) { config.set('recentProjects', [ { path: context.projectPath, name: context.packageJson.name, created: new Date().toISOString() }, ...recentProjects.slice(0, 4) // Keep only 5 most recent ]); } } } catch (error) { console.error('\n' + chalk.red('An error occurred:'), error.message); console.log(chalk.yellow('For more details, run with --debug flag')); if (options.debug) { console.error(error); } process.exit(1); } } // Start project creation createProject();