#!/usr/bin/env node import figlet from 'figlet'; import { Command } from 'commander'; import { execSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import chalk from 'chalk'; import degit from 'degit'; console.log(figlet.textSync('Hexabot')); // Configuration const FOLDER = path.resolve(process.cwd(), './docker'); /** * Check if the docker folder exists, otherwise prompt the user to cd into the correct folder. */ const checkDockerFolder = (): void => { if (!fs.existsSync(FOLDER)) { console.error( chalk.red( `Error: The 'docker' folder is not found in the current directory.`, ), ); console.error( chalk.yellow( `Please make sure you're in the Hexabot project directory and try again.`, ), ); console.log(chalk.cyan(`Example: cd path/to/hexabot`)); process.exit(1); // Exit the script if the folder is not found } }; // Initialize Commander const program = new Command(); // Helper Functions /** * Generate Docker Compose file arguments based on provided services. * @param services List of services * @param type Optional type ('dev' | 'prod') * @returns String of Docker Compose file arguments */ const generateComposeFiles = ( services: string[], type?: 'dev' | 'prod', ): string => { let files = [`-f ${path.join(FOLDER, 'docker-compose.yml')}`]; services.forEach((service) => { files.push(`-f ${path.join(FOLDER, `docker-compose.${service}.yml`)}`); if (type) { const serviceTypeFile = path.join( FOLDER, `docker-compose.${service}.${type}.yml`, ); if (fs.existsSync(serviceTypeFile)) { files.push(`-f ${serviceTypeFile}`); } } }); if (type) { const mainTypeFile = path.join(FOLDER, `docker-compose.${type}.yml`); if (fs.existsSync(mainTypeFile)) { files.push(`-f ${mainTypeFile}`); } } return files.join(' '); }; /** * Execute a Docker Compose command. * @param args Additional arguments for the docker compose command */ const dockerCompose = (args: string): void => { try { execSync(`docker compose ${args}`, { stdio: 'inherit' }); } catch (error) { console.error(chalk.red('Error executing Docker Compose command.')); process.exit(1); } }; /** * Execute a Docker Exec command. * @param container Container for the docker exec command * @param options Additional options for the docker exec command * @param command Command to be executed within the container */ const dockerExec = ( container: string, command: string, options?: string ): void => { try { execSync(`docker exec -it ${options} ${container} ${command}`, { stdio: 'inherit', }); } catch (error) { console.error(chalk.red('Error executing Docker Exec command.')); process.exit(1); } }; /** * Parse the comma-separated service list. * @param serviceString Comma-separated list of services * @returns Array of services */ const parseServices = (serviceString: string): string[] => { return serviceString .split(',') .map((service) => service.trim()) .filter((s) => s); }; // Check if the docker folder exists checkDockerFolder(); // Commands program .name('Hexabot') .description('A CLI to manage your Hexabot chatbot instance') .version('1.0.0'); program .command('init') .description('Initialize the environment by copying .env.example to .env') .action(() => { const envPath = path.join(FOLDER, '.env'); const exampleEnvPath = path.join(FOLDER, '.env.example'); if (fs.existsSync(envPath)) { console.log(chalk.yellow('.env file already exists.')); } else { fs.copyFileSync(exampleEnvPath, envPath); console.log(chalk.green('Copied .env.example to .env')); } }); program .command('start') .description('Start specified services with Docker Compose') .option( '--enable ', 'Comma-separated list of services to enable', '', ) .action((options) => { const services = parseServices(options.enable); const composeArgs = generateComposeFiles(services); dockerCompose(`${composeArgs} up -d`); }); program .command('dev') .description( 'Start specified services in development mode with Docker Compose', ) .option( '--enable ', 'Comma-separated list of services to enable', '', ) .action((options) => { const services = parseServices(options.enable); const composeArgs = generateComposeFiles(services, 'dev'); dockerCompose(`${composeArgs} up --build -d`); }); program .command('migrate [args...]') .description('Run database migrations') .action((args) => { const migrateArgs = args.join(' '); dockerExec('api', `npm run migrate ${migrateArgs}`, '--user $(id -u):$(id -g)'); }); program .command('start-prod') .description( 'Start specified services in production mode with Docker Compose', ) .option( '--enable ', 'Comma-separated list of services to enable', '', ) .action((options) => { const services = parseServices(options.enable); const composeArgs = generateComposeFiles(services, 'prod'); dockerCompose(`${composeArgs} up -d`); }); program .command('stop') .description('Stop specified Docker Compose services') .option('--enable ', 'Comma-separated list of services to stop', '') .action((options) => { const services = parseServices(options.enable); const composeArgs = generateComposeFiles(services); dockerCompose(`${composeArgs} down`); }); program .command('destroy') .description('Destroy specified Docker Compose services and remove volumes') .option( '--enable ', 'Comma-separated list of services to destroy', '', ) .action((options) => { const services = parseServices(options.enable); const composeArgs = generateComposeFiles(services); dockerCompose(`${composeArgs} down -v`); }); // Add install command to install extensions (e.g., channels, plugins) program .command('install') .description('Install an extension for Hexabot') .argument('', 'The type of extension (e.g., channel, plugin)') .argument( '', 'GitHub repository for the extension (user/repo format)', ) .action(async (type, repository) => { // Define the target folder based on the extension type let targetFolder = ''; switch (type) { case 'channel': targetFolder = 'api/src/extensions/channels/'; break; case 'plugin': targetFolder = 'api/src/extensions/plugins/'; break; default: console.error(chalk.red(`Unknown extension type: ${type}`)); process.exit(1); } // Get the last part of the repository name const repoName = repository.split('/').pop(); // If the repo name starts with "hexabot--", remove that prefix const extensionName = repoName.startsWith(`hexabot-${type}-`) ? repoName.replace(`hexabot-${type}-`, '') : repoName; const extensionPath = path.resolve( process.cwd(), targetFolder, extensionName, ); // Check if the extension folder already exists if (fs.existsSync(extensionPath)) { console.error( chalk.red(`Error: Extension already exists at ${extensionPath}`), ); process.exit(1); } try { console.log( chalk.cyan(`Fetching ${repository} into ${extensionPath}...`), ); // Use degit to fetch the repository without .git history const emitter = degit(repository); await emitter.clone(extensionPath); console.log(chalk.cyan('Running npm install in the api/ folder...')); // Run npm install in the api folder to install dependencies execSync('npm run preinstall', { cwd: path.resolve(process.cwd(), 'api'), stdio: 'inherit', }); execSync('npm install', { cwd: path.resolve(process.cwd(), 'api'), stdio: 'inherit', }); console.log( chalk.green(`Successfully installed ${extensionName} as a ${type}.`), ); } catch (error) { console.error(chalk.red('Error during installation:'), error); process.exit(1); } }); // Parse arguments program.parse(process.argv); // If no command is provided, display help if (!process.argv.slice(2).length) { program.outputHelp(); }