UNPKG

@lenne.tech/cli

Version:

lenne.Tech CLI: lt

504 lines (503 loc) 20.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const crypto = __importStar(require("crypto")); const net = __importStar(require("net")); const path_1 = require("path"); /** * Find next available port starting from a given port */ function findAvailablePort(startPort_1) { return __awaiter(this, arguments, void 0, function* (startPort, maxAttempts = 100) { for (let port = startPort; port < startPort + maxAttempts; port++) { if (yield isPortAvailable(port)) { return port; } } throw new Error(`No available port found between ${startPort} and ${startPort + maxAttempts}`); }); } /** * Check if a port is available */ function isPortAvailable(port) { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve) => { const server = net.createServer(); server.once('error', () => { resolve(false); }); server.once('listening', () => { server.close(); resolve(true); }); server.listen(port, '0.0.0.0'); }); }); } /** * Validate instance name for Docker compatibility * Docker container names must match: ^[a-zA-Z0-9][a-zA-Z0-9_.-]*$ */ function validateInstanceName(name) { // Docker container names must start with alphanumeric and can contain alphanumeric, underscore, period, hyphen const dockerNamePattern = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/; if (!name || name.length === 0) { return { error: 'Instance name cannot be empty', isValid: false }; } if (name.length > 128) { return { error: 'Instance name cannot exceed 128 characters', isValid: false }; } if (!dockerNamePattern.test(name)) { // Check for common issues to provide helpful error messages if (/[äöüÄÖÜß]/.test(name)) { return { error: `Instance name contains umlauts (${name}). Docker container names only allow: letters (a-z, A-Z), numbers (0-9), underscores (_), periods (.), and hyphens (-)`, isValid: false, }; } if (/\s/.test(name)) { return { error: 'Instance name cannot contain spaces', isValid: false }; } if (/^[^a-zA-Z0-9]/.test(name)) { return { error: 'Instance name must start with a letter or number', isValid: false }; } return { error: `Instance name "${name}" contains invalid characters. Only letters (a-z, A-Z), numbers (0-9), underscores (_), periods (.), and hyphens (-) are allowed`, isValid: false, }; } return { isValid: true }; } /** * Setup a new local Directus Docker instance */ const NewCommand = { alias: ['ds'], description: 'Setup Docker instance', hidden: false, name: 'docker-setup', run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p; // Retrieve the tools we need const { config, filesystem, parameters, print: { error, info, spin, success, warning }, prompt, system, template, } = toolbox; // Check if Docker is installed if (!system.which('docker')) { error('Docker is not installed. Please install Docker first.'); return; } // Load configuration const ltConfig = config.loadConfig(); // Parse CLI arguments const cliName = parameters.options.name || parameters.options.n; const cliVersion = parameters.options.version || parameters.options.v; const cliDatabase = parameters.options.database || parameters.options.db; const cliPort = parameters.options.port || parameters.options.p; // Determine noConfirm with priority: CLI > command > global > default const noConfirm = config.getNoConfirm({ cliValue: parameters.options.noConfirm, commandConfig: (_b = (_a = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _a === void 0 ? void 0 : _a.directus) === null || _b === void 0 ? void 0 : _b.dockerSetup, config: ltConfig, }); // Get configuration values const configName = (_e = (_d = (_c = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _c === void 0 ? void 0 : _c.directus) === null || _d === void 0 ? void 0 : _d.dockerSetup) === null || _e === void 0 ? void 0 : _e.name; const configVersion = (_h = (_g = (_f = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _f === void 0 ? void 0 : _f.directus) === null || _g === void 0 ? void 0 : _g.dockerSetup) === null || _h === void 0 ? void 0 : _h.version; const configDatabase = (_l = (_k = (_j = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _j === void 0 ? void 0 : _j.directus) === null || _k === void 0 ? void 0 : _k.dockerSetup) === null || _l === void 0 ? void 0 : _l.database; // Determine instance name let instanceName; if (cliName && typeof cliName === 'string') { instanceName = cliName; } else if (configName) { instanceName = configName; info(`Using instance name from lt.config: ${instanceName}`); } else if (noConfirm) { instanceName = 'directus'; info(`Using default instance name: ${instanceName}`); } else { const nameResponse = yield prompt.ask({ initial: 'directus', message: 'Enter instance name:', name: 'name', type: 'input', }); instanceName = nameResponse.name; } if (!instanceName) { error('Instance name is required!'); return; } // Validate instance name for Docker compatibility const validation = validateInstanceName(instanceName); if (!validation.isValid) { error(validation.error); return; } // Determine Directus version let version; if (cliVersion && typeof cliVersion === 'string') { version = cliVersion; } else if (configVersion) { version = configVersion; info(`Using Directus version from lt.config: ${version}`); } else if (noConfirm) { version = 'latest'; info(`Using default Directus version: ${version}`); } else { const versionResponse = yield prompt.ask({ initial: 'latest', message: 'Enter Directus version (e.g., latest, 10, 10.8.0):', name: 'version', type: 'input', }); version = versionResponse.version; } if (!version) { error('Directus version is required!'); return; } // Determine database type let database; const databaseChoices = [ { message: 'PostgreSQL (recommended)', name: 'postgres' }, { message: 'MySQL', name: 'mysql' }, { message: 'SQLite', name: 'sqlite' }, ]; if (cliDatabase && typeof cliDatabase === 'string') { const validDatabases = ['postgres', 'postgresql', 'mysql', 'sqlite']; const normalizedDb = cliDatabase.toLowerCase(); if (!validDatabases.includes(normalizedDb)) { error(`Invalid database type: ${cliDatabase}. Valid options: postgres, mysql, sqlite`); return; } database = (normalizedDb === 'postgresql' ? 'postgres' : normalizedDb); } else if (configDatabase) { database = configDatabase; info(`Using database type from lt.config: ${database}`); } else if (noConfirm) { database = 'postgres'; info(`Using default database type: ${database}`); } else { const result = yield prompt.ask({ choices: databaseChoices, initial: 0, message: 'Select database type:', name: 'database', type: 'select', }); database = result.database; } if (!database) { error('Database type is required!'); return; } // Determine instance directory const directusDir = (0, path_1.join)(filesystem.homedir(), '.lt', 'directus', instanceName); const instanceExists = filesystem.exists(directusDir); // Check if instance already exists if (instanceExists && !parameters.options.update) { if (noConfirm) { error(`Instance "${instanceName}" already exists. Use --update to modify it.`); return; } const shouldUpdate = yield prompt.confirm(`Instance "${instanceName}" already exists. Update it?`); if (!shouldUpdate) { info('Operation cancelled.'); return; } } if (instanceExists) { info(`Updating existing instance: ${instanceName}`); } // Read existing .env if updating to preserve secrets const existingEnv = {}; if (instanceExists) { const envPath = (0, path_1.join)(directusDir, '.env'); if (filesystem.exists(envPath)) { const envContent = filesystem.read(envPath); if (envContent) { // Parse .env file envContent.split('\n').forEach((line) => { const match = line.match(/^([A-Z_]+)=(.+)$/); if (match) { existingEnv[match[1]] = match[2].replace(/^['"]|['"]$/g, ''); } }); } } } // Generate random secrets (or use existing ones if updating) const generateSecret = () => { return crypto.randomBytes(32).toString('hex'); }; const keySecret = existingEnv.KEY || generateSecret(); const adminSecret = existingEnv.SECRET || generateSecret(); // Database configuration (use existing passwords if updating) const dbConfig = { mysql: { adminPassword: existingEnv.MYSQL_ROOT_PASSWORD || generateSecret(), client: 'mysql', database: 'directus', image: 'mysql:8', password: existingEnv.DB_PASSWORD || generateSecret(), port: 3306, user: 'directus', }, postgres: { client: 'pg', database: 'directus', image: 'postgres:16', password: existingEnv.DB_PASSWORD || generateSecret(), port: 5432, user: 'directus', }, sqlite: { client: 'sqlite3', database: '/directus/database/data.db', image: null, // SQLite doesn't need a separate container password: null, port: null, user: null, }, }; const selectedDbConfig = dbConfig[database]; // Determine port (CLI > existing > config > auto-detect) let directusPort; const configPort = (_p = (_o = (_m = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _m === void 0 ? void 0 : _m.directus) === null || _o === void 0 ? void 0 : _o.dockerSetup) === null || _p === void 0 ? void 0 : _p.port; if (cliPort && typeof cliPort !== 'boolean') { directusPort = Number.parseInt(String(cliPort), 10); if (Number.isNaN(directusPort) || directusPort < 1 || directusPort > 65535) { error(`Invalid port: ${cliPort}. Must be between 1 and 65535.`); return; } info(`Using port from CLI: ${directusPort}`); } else if (existingEnv.DIRECTUS_PORT) { directusPort = Number.parseInt(existingEnv.DIRECTUS_PORT, 10); info(`Using existing port: ${directusPort}`); } else if (configPort) { directusPort = configPort; info(`Using port from lt.config: ${directusPort}`); } else { // Auto-detect available port starting from 8055 const portSpin = spin('Finding available port'); try { directusPort = yield findAvailablePort(8055); portSpin.succeed(); info(`Found available port: ${directusPort}`); } catch (portError) { portSpin.fail('Failed to find available port'); if (portError instanceof Error) { error(portError.message); } return; } } // Create instance directory const dirSpin = spin('Preparing instance directory'); try { filesystem.dir(directusDir); dirSpin.succeed(); } catch (dirError) { dirSpin.fail('Failed to create instance directory'); if (dirError instanceof Error) { error(dirError.message); } return; } // Generate docker-compose.yml const composeSpin = spin('Generating docker-compose.yml'); try { yield template.generate({ props: { dbConfig: selectedDbConfig, dbType: database, instanceName, version, }, target: (0, path_1.join)(directusDir, 'docker-compose.yml'), template: 'directus/docker-compose.yml.ejs', }); composeSpin.succeed(); } catch (composeError) { composeSpin.fail('Failed to generate docker-compose.yml'); if (composeError instanceof Error) { error(composeError.message); } return; } // Generate .env file const envSpin = spin('Generating .env file'); try { yield template.generate({ props: { adminEmail: 'admin@example.com', adminPassword: 'admin', adminSecret, dbConfig: selectedDbConfig, dbType: database, keySecret, port: directusPort, version, }, target: (0, path_1.join)(directusDir, '.env'), template: 'directus/.env.ejs', }); envSpin.succeed(); } catch (envError) { envSpin.fail('Failed to generate .env file'); if (envError instanceof Error) { error(envError.message); } return; } // Generate README.md const readmeSpin = spin('Generating README.md'); try { yield template.generate({ props: { dbType: database, instanceName, port: directusPort, }, target: (0, path_1.join)(directusDir, 'README.md'), template: 'directus/README.md.ejs', }); readmeSpin.succeed(); } catch (readmeError) { readmeSpin.fail('Failed to generate README.md'); if (readmeError instanceof Error) { error(readmeError.message); } return; } // Stop existing containers if updating if (instanceExists) { const stopSpin = spin('Stopping existing containers'); try { yield system.run(`cd ${directusDir} && docker-compose down`); stopSpin.succeed(); } catch (stopError) { stopSpin.fail('Failed to stop existing containers'); if (stopError instanceof Error) { error(stopError.message); } } } // Start Directus with docker-compose const startSpin = spin('Starting Directus instance'); try { yield system.run(`cd ${directusDir} && docker-compose up -d`); startSpin.succeed(); } catch (startError) { startSpin.fail('Failed to start Directus'); if (startError instanceof Error) { error(startError.message); } return; } // Success message success(`Directus Docker setup ${instanceExists ? 'updated' : 'created'} successfully!`); info(''); info('Configuration stored at:'); info(` ${directusDir}`); info(''); info('Instance details:'); info(` - Name: ${instanceName}`); info(` - Version: ${version}`); info(` - Database: ${database}`); info(` - Port: ${directusPort}`); info(''); // Only display secrets if this is a new instance (not updating) if (!existingEnv.KEY) { warning('Generated secrets (SAVE THESE):'); info(` KEY: ${keySecret}`); info(` SECRET: ${adminSecret}`); if (selectedDbConfig.password) { info(` DB_PASSWORD: ${selectedDbConfig.password}`); } if (database === 'mysql' && dbConfig.mysql.adminPassword) { info(` MYSQL_ROOT_PASSWORD: ${dbConfig.mysql.adminPassword}`); } info(''); } info('Default admin credentials:'); info(' Email: admin@example.com'); info(' Password: admin'); info(` URL: http://localhost:${directusPort}`); info(''); info(''); info('Management commands:'); info(` Start: cd ${directusDir} && docker-compose up -d`); info(` Stop: cd ${directusDir} && docker-compose down`); info(` Restart: cd ${directusDir} && docker-compose restart`); info(` Logs: cd ${directusDir} && docker-compose logs -f`); info(''); info(`Full documentation: ${directusDir}/README.md`); // Exit if not running from menu if (!toolbox.parameters.options.fromGluegunMenu) { process.exit(); } // For tests return `${instanceExists ? 'updated' : 'created'} directus docker setup ${instanceName}`; }), }; exports.default = NewCommand;