#!/usr/bin/env tsx import { parseArgs } from 'node:util'; import { DockerImageNotFoundError } from './docker-image-not-found-error'; import type { N8NConfig, N8NStack } from './n8n-test-container-creation'; import { createN8NStack } from './n8n-test-container-creation'; import { BASE_PERFORMANCE_PLANS, isValidPerformancePlan } from './performance-plans'; // ANSI colors for terminal output const colors = { reset: '\x1b[0m', bright: '\x1b[1m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', red: '\x1b[31m', cyan: '\x1b[36m', }; const log = { info: (msg: string) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`), success: (msg: string) => console.log(`${colors.green}✓${colors.reset} ${msg}`), error: (msg: string) => console.error(`${colors.red}✗${colors.reset} ${msg}`), warn: (msg: string) => console.warn(`${colors.yellow}⚠${colors.reset} ${msg}`), header: (msg: string) => console.log(`\n${colors.bright}${colors.cyan}${msg}${colors.reset}\n`), }; function showHelp() { console.log(` ${colors.bright}n8n Stack Manager${colors.reset} Start n8n containers for development and testing. ${colors.yellow}Usage:${colors.reset} npm run stack [options] ${colors.yellow}Options:${colors.reset} --postgres Use PostgreSQL instead of SQLite --queue Enable queue mode (requires PostgreSQL) --mains Number of main instances (default: 1) --workers Number of worker instances (default: 1) --name Project name for parallel runs --env KEY=VALUE Set environment variables --plan Use performance plan preset (${Object.keys(BASE_PERFORMANCE_PLANS).join(', ')}) --help, -h Show this help ${colors.yellow}Performance Plans:${colors.reset} ${Object.entries(BASE_PERFORMANCE_PLANS) .map( ([name, plan]) => ` ${name.padEnd(12)} ${plan.memory}GB RAM, ${plan.cpu} CPU cores - SQLite only`, ) .join('\n')} ${colors.yellow}Environment Variables:${colors.reset} • N8N_DOCKER_IMAGE= Use a custom Docker image (default: n8nio/n8n:local) ${colors.yellow}Examples:${colors.reset} ${colors.bright}# Simple SQLite instance${colors.reset} npm run stack ${colors.bright}# PostgreSQL database${colors.reset} npm run stack --postgres ${colors.bright}# Queue mode (automatically uses PostgreSQL)${colors.reset} npm run stack --queue ${colors.bright}# Custom scaling${colors.reset} npm run stack --queue --mains 3 --workers 5 ${colors.bright}# With environment variables${colors.reset} npm run stack --postgres --env N8N_LOG_LEVEL=info --env N8N_ENABLED_MODULES=insights ${colors.bright}# Performance plan presets${colors.reset} ${Object.keys(BASE_PERFORMANCE_PLANS) .map((name) => ` npm run stack --plan ${name}`) .join('\n')} ${colors.bright}# Parallel instances${colors.reset} npm run stack --name test-1 npm run stack --name test-2 ${colors.yellow}Notes:${colors.reset} • SQLite is the default database (no external dependencies) • Queue mode requires PostgreSQL and enables horizontal scaling • Use --name for running multiple instances in parallel • Performance plans simulate cloud constraints (SQLite only, resource-limited) • Press Ctrl+C to stop all containers `); } async function main() { const { values } = parseArgs({ args: process.argv.slice(2), options: { help: { type: 'boolean', short: 'h' }, postgres: { type: 'boolean' }, queue: { type: 'boolean' }, mains: { type: 'string' }, workers: { type: 'string' }, name: { type: 'string' }, env: { type: 'string', multiple: true }, plan: { type: 'string' }, }, allowPositionals: false, }); // Show help if requested if (values.help) { showHelp(); process.exit(0); } // Build configuration const config: N8NConfig = { postgres: values.postgres ?? false, projectName: values.name ?? `n8n-stack-${Math.random().toString(36).substring(7)}`, }; // Handle queue mode if (values.queue ?? values.mains ?? values.workers) { const mains = parseInt(values.mains ?? '1', 10); const workers = parseInt(values.workers ?? '1', 10); if (isNaN(mains) || isNaN(workers) || mains < 1 || workers < 0) { log.error('Invalid mains or workers count'); process.exit(1); } config.queueMode = { mains, workers }; if (!values.queue && (values.mains ?? values.workers)) { log.warn('--mains and --workers imply queue mode'); } } if (values.plan) { const planName = values.plan; if (!isValidPerformancePlan(planName)) { log.error(`Invalid performance plan: ${values.plan}`); log.error(`Available plans: ${Object.keys(BASE_PERFORMANCE_PLANS).join(', ')}`); process.exit(1); } const plan = BASE_PERFORMANCE_PLANS[planName]; if (values.postgres) { log.warn('Performance plans use SQLite only. PostgreSQL option ignored.'); } if (values.queue || values.mains || values.workers) { log.warn('Performance plans use SQLite only. Queue mode ignored.'); } config.resourceQuota = plan; config.postgres = false; // Force SQLite for performance plans config.queueMode = false; // Force single instance for performance plans log.info( `Using ${planName} performance plan: ${plan.memory}GB RAM, ${plan.cpu} CPU cores (SQLite only)`, ); } // Parse environment variables if (values.env && values.env.length > 0) { config.env = {}; for (const envStr of values.env) { const [key, ...valueParts] = envStr.split('='); const value = valueParts.join('='); // Handle values with = in them if (key && value) { config.env[key] = value; } else { log.warn(`Invalid env format: ${envStr} (expected KEY=VALUE)`); } } } log.header('Starting n8n Stack'); log.info(`Project name: ${config.projectName}`); displayConfig(config); let stack: N8NStack; try { log.info('Starting containers...'); try { stack = await createN8NStack(config); } catch (error) { if (error instanceof DockerImageNotFoundError) { log.error(error.message); process.exit(1); } throw error; } log.success('All containers started successfully!'); console.log(''); log.info(`n8n URL: ${colors.bright}${colors.green}${stack.baseUrl}${colors.reset}`); } catch (error) { log.error(`Failed to start: ${error as string}`); process.exit(1); } } function displayConfig(config: N8NConfig) { const dockerImage = process.env.N8N_DOCKER_IMAGE ?? 'n8nio/n8n:local'; log.info(`Docker image: ${dockerImage}`); // Determine actual database // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const usePostgres = config.postgres || config.queueMode; log.info(`Database: ${usePostgres ? 'PostgreSQL' : 'SQLite'}`); if (config.queueMode) { const qm = typeof config.queueMode === 'boolean' ? { mains: 1, workers: 1 } : config.queueMode; log.info(`Queue mode: ${qm.mains} main(s), ${qm.workers} worker(s)`); if (!config.postgres) { log.info('(PostgreSQL automatically enabled for queue mode)'); } if (qm.mains && qm.mains > 1) { log.info('(load balancer will be configured)'); } } else { log.info('Queue mode: disabled'); } if (config.resourceQuota) { log.info( `Resource limits: ${config.resourceQuota.memory}GB RAM, ${config.resourceQuota.cpu} CPU cores`, ); } if (config.env) { const envCount = Object.keys(config.env).length; if (envCount > 0) { log.info(`Environment variables: ${envCount} custom variable(s)`); Object.entries(config.env).forEach(([key, value]) => { console.log(` ${key}=${value}`); }); } } if (process.env.TESTCONTAINERS_REUSE_ENABLE === 'true') { log.info('Container reuse: enabled (containers will persist)'); } } // Run if executed directly if (require.main === module) { main().catch((error) => { log.error(`Unexpected error: ${error}`); process.exit(1); }); }