makecord-create
Version:
Create advanced Discord Bots with Makecord - Now with TypeScript support
609 lines (535 loc) • 23.7 kB
JavaScript
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();