@lenne.tech/cli
Version:
lenne.Tech CLI: lt
504 lines (503 loc) • 20.9 kB
JavaScript
;
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;