create-robtic-app
Version:
CLI to quickly scaffold a Discord bot using robtic-discord-startup
248 lines (214 loc) • 9.15 kB
JavaScript
/**
* Copyright (c) 2025, RobTic, RoBo.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// /!\ DO NOT MODIFY THIS FILE /!\
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// The only job of create-robtic-app is to init the repository and then
// forward all the commands to the local version of create-robtic-app.
//
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// /!\ DO NOT MODIFY THIS FILE /!\
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const { Command } = require('commander');
const chalk = require('chalk');
const { execSync } = require('child_process');
const { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, rmSync } = require('fs');
const { join } = require('path');
const validateProjectName = require('validate-npm-package-name');
const packageJson = require('./package.json');
const program = new Command()
const SUPPORTED_PACKAGE_MANAGERS = ['bun', 'yarn', 'npm', 'pnpm'];
const DEPENDENCIES = ['robtic-discord-startup', 'create-robtic-app', 'discord.js'];
/**
* Determines the package manager based on user agent
* @returns {string} Package manager name
*/
function detectPackageManager() {
const userAgent = process.env.npm_config_user_agent || '';
const manager = SUPPORTED_PACKAGE_MANAGERS.find(pm => userAgent.startsWith(pm));
if (!manager) {
console.error(chalk.red('❌ Could not detect supported package manager.'));
console.log(chalk.gray(`User agent: ${userAgent}`));
process.exit(1);
}
return manager;
}
/**
* Generates package manager-specific commands
* @param {string} pm - Package manager name
* @returns {Object} Install and dev install commands
*/
function getPackageManagerCommands(pm) {
const commands = {
bun: { install: 'bun add', devInstall: 'bun add -d' },
yarn: { install: 'yarn add', devInstall: 'yarn add -D' },
npm: { install: 'npm i', devInstall: 'npm i --save-dev' },
pnpm: { install: 'pnpm install', devInstall: 'pnpm install --save-dev' }
};
return commands[pm];
}
/**
* Generates scripts for package.json
* @param {string} template - Project template (js/ts)
* @param {string} pm - Package manager
* @returns {Object} Filtered scripts object
*/
function generateScripts(template, pm) {
const isTs = template === 'ts';
const runner = pm === 'npm' || pm === 'pnpm' ? 'node' : pm;
const scripts = {
start: `${isTs && pm !== 'bun' ? 'tsc && ' : ''}${runner} run src/index.${template}`,
...(isTs && { build: 'tsc' }),
test: `${isTs && pm !== 'bun' ? 'tsc && ' : ''}${runner} run src/index.${template}`
};
return Object.fromEntries(Object.entries(scripts).filter(([_, v]) => v));
}
/**
* Validates project name
* @param {string} appName - Project name
*/
function validateAppName(appName) {
const validation = validateProjectName(appName);
if (!validation.validForNewPackages) {
console.error(chalk.red(`Cannot create project named ${chalk.blueBright(`"${appName}"`)}`));
[...(validation.errors || []), ...(validation.warnings || [])].forEach(error => {
console.error(chalk.red(` * ${error}`));
});
process.exit(1);
}
if (DEPENDENCIES.includes(appName)) {
console.error(
chalk.red(`Cannot create project named ${chalk.blueBright(`"${appName}"`)} due to dependency conflict.\n`) +
chalk.blue(DEPENDENCIES.map(dep => ` ${dep}`).join('\n')) +
chalk.red('\n\nPlease choose a different project name.')
);
process.exit(1);
}
}
/**
* Resolves project path
* @param {string} appName - Project name
* @returns {string} Project path
*/
function resolveProjectPath(appName) {
return appName === '.' ? process.cwd() : join(process.cwd(), appName);
}
/**
* Sets up project directory
* @param {string} appName - Project name
* @param {string} projectPath - Project path
*/
function setupProjectDirectory(appName, projectPath) {
const folderName = appName === '.' ? projectPath.split(/[\\/]/).pop() : appName;
validateAppName(folderName);
if (appName !== '.') {
if (existsSync(projectPath)) {
console.error(chalk.red(`❌ Folder "${appName}" already exists.`));
process.exit(1);
}
mkdirSync(projectPath);
console.log(chalk.green(`✅ Created folder: ${appName}`));
} else {
console.log(chalk.yellow(`⚡ Using current directory: ${folderName}`));
}
}
/**
* Validates template existence
* @param {string} template - Template type
* @param {string} templatePath - Template path
*/
function validateTemplate(template, templatePath, appName) {
if (!existsSync(templatePath)) {
console.error(chalk.red(`[RobTic] Template "${template}" not found at ${templatePath}.`));
rmSync(appName, { recursive: true, force: true })
process.exit(1);
}
}
/**
* Installs dependencies
* @param {string} template - Project template
* @param {Object} commands - Package manager commands
*/
function installDependencies(template, { install, devInstall }) {
try {
execSync(`${install} robtic-discord-startup discord.js`, { stdio: 'inherit' });
} catch (error) {
console.error(chalk.red(`[RobTic] Failed to install dependencies: ${error.message}`));
console.log(chalk.cyan('Retrying with --force...'));
execSync(`${install} robtic-discord-startup discord.js --force`, { stdio: 'inherit' });
}
if (template === 'ts') {
const devDeps = SUPPORTED_PACKAGE_MANAGERS.includes('bun') ? 'typescript @types/node' : 'typescript @types/node tsx';
execSync(`${devInstall} ${devDeps}`, { stdio: 'inherit' });
}
}
/**
* full start code
* @param {string} projectName
* @param {string} options
*/
function start(projectName, options) {
const packageManager = detectPackageManager();
const { install, devInstall } = getPackageManagerCommands(packageManager);
const projectPath = resolveProjectPath(projectName);
const folderName = projectName === '.' ? projectPath.split(/[\\/]/).pop() : projectName;
const templatePath = join(__dirname, 'templates', options.template);
const pkgJsonPath = join(projectPath, 'package.json');
setupProjectDirectory(projectName, projectPath);
validateTemplate(options.template, templatePath, folderName);
console.log(chalk.blueBright(`[RobTic] Creating ${folderName} with ${options.template} template using ${packageManager}...`));
try {
cpSync(templatePath, projectPath, { recursive: true });
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
pkgJson.name = folderName;
pkgJson.scripts = generateScripts(options.template, packageManager);
if (packageManager !== 'bun' && options.template === 'ts') {
pkgJson.devDependencies = { ...pkgJson.devDependencies, tsx: '^4.19.1' };
}
writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
process.chdir(projectPath);
console.log(chalk.blueBright(`[RobTic] Installing dependencies with ${packageManager}...`));
installDependencies(options.template, { install, devInstall });
console.log(chalk.greenBright(`[RobTic] Done! Run your bot and enjoy! 🚀`));
console.log(chalk.cyan(` cd ${folderName}`));
console.log(chalk.cyan(` ${packageManager} run start`));
} catch (error) {
console.error(chalk.red(`[RobTic] Failed to create project: ${error.message}`));
process.exit(1);
}
}
/**
* Help Message
*/
const HelpMessageBefore = `
🌟 ${chalk.blue("Welcome to Robtic CLI!")} 🌟
${chalk.bold("Create")}, ${chalk.bold("manage")}, and ${chalk.bold("launch")} your ${chalk.blueBright("Discord")} bots with ease.
`
const HelpMessageAfter = `
${chalk.bold("Need more help? Join our Discord server")}
`
/**
* Initializes the CLI program
*/
function init() {
program
.name(packageJson.name)
.version(packageJson.version)
.description(packageJson.description)
.argument('<project-name>', 'Name of the project directory')
.option('-t, --template <type>', 'Select project type', 'js')
.helpOption("-h, --help", 'Show help for Robtic CLI')
.showHelpAfterError(true)
.allowUnknownOption(true)
.addHelpText("before", HelpMessageBefore)
.addHelpText("after", HelpMessageAfter)
.action((projectName, options) => start(projectName, options));
program.parse(process.argv);
}
module.exports = { init };