UNPKG

cartorio-plataforma-cli

Version:

CLI para gerenciar a Plataforma do Cartório de Criciúma com sistema de módulos

498 lines (417 loc) 18.5 kB
const inquirer = require('inquirer'); const chalk = require('chalk'); const { execSync } = require('child_process'); const ora = require('ora'); const path = require('path'); const boxen = require('boxen'); const os = require('os'); const ModuleRegistry = require('../lib/module-registry'); const DockerCommandGenerator = require('../lib/docker-command-generator'); /** * Função que inicia os componentes da plataforma */ module.exports = async function start(options) { let env = options.env || 'local'; let buildImages = options.build !== false; // Por padrão, constrói as imagens let profileType = 'full'; // Tipo de profile a ser usado: 'infra', 'core', 'modules', 'full', 'custom' let selectedModules = []; // Carregar o registro de módulos const spinner = ora('Carregando módulos...').start(); const registry = new ModuleRegistry(); // Criar gerador de comandos const commandGenerator = new DockerCommandGenerator(registry); // Encontra o diretório raiz do projeto const projectRoot = commandGenerator.findProjectDir(); // Carregar os módulos usando o diretório do projeto encontrado registry.loadModules(projectRoot); // Após carregar os módulos, exibir quantidade por tipo const allModules = registry.getAllModules(); const moduleTypes = { core: registry.getModulesByCategory('core').length, frontend: registry.getModulesByCategory('frontends').length, backend: registry.getModulesByCategory('backends').length, infrastructure: registry.getModulesByCategory('infrastructure').length }; const moduleCount = allModules.length; spinner.succeed(`${moduleCount} módulos carregados (${moduleTypes.core} core, ${moduleTypes.frontend} frontend, ${moduleTypes.backend} backend, ${moduleTypes.infrastructure} infra)`); // Modo interativo if (options.interactive) { // Selecionar o ambiente const { selectedEnv } = await inquirer.prompt([ { type: 'list', name: 'selectedEnv', message: 'Selecione o ambiente:', choices: [ { name: 'Desenvolvimento (dev)', value: 'dev' }, { name: 'Local (local)', value: 'local' }, { name: 'Produção (prod)', value: 'prod' } ], default: env } ]); env = selectedEnv; // Perguntar se deseja construir as imagens const { shouldBuild } = await inquirer.prompt([ { type: 'confirm', name: 'shouldBuild', message: 'Deseja construir as imagens antes de iniciar os serviços?', default: true } ]); buildImages = shouldBuild; // Selecionar configuração predefinida ou personalizada const { preset } = await inquirer.prompt([ { type: 'list', name: 'preset', message: 'Escolha uma configuração predefinida ou personalizada:', choices: [ { name: 'Tudo (Infra + Core + Módulos)', value: 'all' }, { name: 'Infraestrutura', value: 'infra' }, { name: 'Infraestrutura + Core', value: 'core' }, { name: 'Personalizado', value: 'custom' } ] } ]); // Define o tipo de profile baseado na seleção profileType = preset === 'all' ? 'all' : preset === 'infra' ? 'infra' : preset === 'core' ? 'core' : 'custom'; // Para custom, exibir seleção de módulos if (profileType === 'custom') { // Obter todos os módulos registrados const allModules = registry.getAllModules(); // Agrupar por tipo para exibição const modulesByType = {}; allModules.forEach(module => { const type = module.type; if (!modulesByType[type]) { modulesByType[type] = []; } modulesByType[type].push(module); }); console.log(chalk.dim('Debug: Tipos de módulos encontrados:'), Object.keys(modulesByType).join(', ')); // Criar uma única lista com todos os módulos separados por tipo const allChoices = []; // Adicionar módulos core if (modulesByType.core && modulesByType.core.length > 0) { allChoices.push(new inquirer.Separator(chalk.bold.cyan('=== MÓDULOS CORE ==='))); modulesByType.core.forEach(module => { allChoices.push({ name: `${module.name} (${module.id})`, value: module, checked: true }); }); } // Adicionar módulos de infraestrutura if (modulesByType.infrastructure && modulesByType.infrastructure.length > 0) { allChoices.push(new inquirer.Separator(chalk.bold.yellow('=== SERVIÇOS DE INFRAESTRUTURA ==='))); modulesByType.infrastructure.forEach(module => { allChoices.push({ name: `${module.name} (${module.id})`, value: module, checked: true }); }); } // Adicionar módulos frontend if (modulesByType.frontend && modulesByType.frontend.length > 0) { allChoices.push(new inquirer.Separator(chalk.bold.green('=== MÓDULOS FRONTEND ==='))); modulesByType.frontend.forEach(module => { allChoices.push({ name: `${module.name} (${module.id}-${module.type})`, value: module, checked: true }); }); } // Adicionar módulos backend if (modulesByType.backend && modulesByType.backend.length > 0) { allChoices.push(new inquirer.Separator(chalk.bold.blue('=== MÓDULOS BACKEND ==='))); modulesByType.backend.forEach(module => { allChoices.push({ name: `${module.name} (${module.id}-${module.type})`, value: module, checked: true }); }); } // Exibir uma única lista com todos os módulos const { selectedModulesList } = await inquirer.prompt([ { type: 'checkbox', name: 'selectedModulesList', message: 'Selecione os módulos que deseja iniciar:', choices: allChoices, pageSize: 15 } ]); selectedModules = selectedModulesList; } // Confirmação final const moduleTypes = {}; selectedModules.forEach(module => { if (!moduleTypes[module.type]) { moduleTypes[module.type] = []; } moduleTypes[module.type].push(module.name); }); let confirmMessage = ` Ambiente: ${chalk.green(env)} `; if (profileType !== 'custom') { // Para perfis predefinidos if (profileType === 'infra') { confirmMessage += `Tipo: ${chalk.yellow('Infraestrutura')}`; } else if (profileType === 'core') { confirmMessage += `Tipo: ${chalk.yellow('Infraestrutura + Core')}`; } else if (profileType === 'all') { confirmMessage += `Tipo: ${chalk.yellow('Tudo (Infraestrutura + Core + Módulos)')}`; } } else { // Para perfil customizado confirmMessage += `Tipo: ${chalk.yellow('Personalizado')}\n`; // Listar módulos selecionados por tipo Object.keys(moduleTypes).forEach(type => { if (moduleTypes[type].length > 0) { confirmMessage += `${type.charAt(0).toUpperCase() + type.slice(1)}: ${chalk.cyan(moduleTypes[type].join(', '))}\n`; } }); } confirmMessage += `\nConstruir imagens: ${buildImages ? chalk.green('Sim') : chalk.yellow('Não')}`; confirmMessage += `\n\nDeseja continuar?`; const { confirmStart } = await inquirer.prompt([ { type: 'confirm', name: 'confirmStart', message: confirmMessage, default: true } ]); if (!confirmStart) { console.log(chalk.yellow('Operação cancelada.')); return; } } else { // Processamento de argumentos de linha de comando // Verificar se a construção de imagens foi explicitamente desabilitada if (options.noBuild) { buildImages = false; } // Verificar o tipo de perfil baseado nas opções if (options.infra) { profileType = 'infra'; } else if (options.core) { profileType = 'core'; } else if (options.all) { profileType = 'all'; } // Para modo custom, verificar módulos específicos if (options.custom) { profileType = 'custom'; // Carregar módulos específicos if (options.modules) { const moduleIds = options.modules.split(','); console.log(chalk.dim(`Debug: Buscando módulos: ${moduleIds.join(', ')}`)); moduleIds.forEach(moduleId => { // O formato esperado é id:type (ex: senhas-frontend:frontend) const [id, type] = moduleId.split(':'); if (id && type) { console.log(chalk.dim(`Debug: Buscando módulo com id=${id}, type=${type}`)); // Buscar todos os módulos e filtrar pelo tipo e ID const allModules = registry.getAllModules(); // Primeiro tentar encontrar uma correspondência exata let matchedModule = null; // Buscar por ID exato e tipo exato matchedModule = allModules.find(m => m.id === id && m.type === type); if (!matchedModule) { // Buscar um módulo que tenha o mesmo ID sem considerar o tipo matchedModule = allModules.find(m => m.id === id); if (matchedModule) { console.log(chalk.green(`✅ Módulo encontrado por ID: ${matchedModule.name} (${matchedModule.id}:${matchedModule.type})`)); } } else { console.log(chalk.green(`✅ Módulo encontrado: ${matchedModule.name} (${matchedModule.id}:${matchedModule.type})`)); } // Se ainda não encontrou, tentar pelo nome if (!matchedModule) { // Buscar o módulo pelo nome 'Senhas' do tipo frontend/backend const modulesByName = allModules.filter(m => m.name.toLowerCase() === id.replace(/-/g, ' ').toLowerCase() && m.type === type ); if (modulesByName.length > 0) { matchedModule = modulesByName[0]; console.log(chalk.green(`✅ Módulo encontrado por nome: ${matchedModule.name} (${matchedModule.id}:${matchedModule.type})`)); } } if (matchedModule) { selectedModules.push(matchedModule); } else { console.warn(chalk.yellow(`⚠️ Módulo não encontrado: ${id}:${type}`)); console.log(chalk.yellow(` Módulos disponíveis do tipo '${type}':`)); // Mostrar módulos disponíveis do tipo especificado const availableOfType = allModules.filter(m => m.type === type); if (availableOfType.length > 0) { availableOfType.forEach(m => { console.log(chalk.yellow(` - ${m.name} (${m.id}:${m.type})`)); }); } else { console.log(chalk.yellow(` Nenhum módulo do tipo '${type}' encontrado.`)); } } } else { console.warn(chalk.yellow(`⚠️ Formato inválido para módulo: ${moduleId}. Use o formato id:tipo`)); } }); } } } // Exibir informações sobre os serviços sendo iniciados let startMessage = chalk.blue(`🚀 Iniciando componentes da plataforma`) + '\n\n' + chalk.white(`Ambiente: ${chalk.green(env)}`); if (profileType !== 'custom') { startMessage += '\n' + chalk.white(`Tipo: ${chalk.yellow(profileType)}`); } else { startMessage += '\n' + chalk.white(`Tipo: ${chalk.yellow('Personalizado')}`); startMessage += '\n' + chalk.white(`Módulos: ${selectedModules.length > 0 ? chalk.cyan(selectedModules.map(m => m.name).join(', ')) : chalk.yellow('Nenhum')}`); } startMessage += buildImages ? '\n' + chalk.white(`Construção de imagens: ${chalk.green('Habilitada')}`) : '\n' + chalk.white(`Construção de imagens: ${chalk.yellow('Desabilitada')}`); console.log( boxen( startMessage, { padding: 1, borderColor: 'blue', borderStyle: 'round' } ) ); const startSpinner = ora('Iniciando serviços...').start(); try { // Criar volumes necessários antes de iniciar os serviços startSpinner.text = 'Criando volumes compartilhados...'; try { // Criação explícita dos volumes com nomes consistentes const volumeCommands = [ 'docker volume create plataforma_consul-data || true', 'docker volume create plataforma_postgres-data || true', 'docker volume create plataforma_consul-agent-data || true', // Remover possíveis volumes antigos que podem conflitar os.platform() === 'win32' ? 'docker volume rm consul-data consul-agent-data postgres-data 2>nul || echo "Ignorando erros de remoção de volumes (esperado)"' : 'docker volume rm consul-data consul-agent-data postgres-data 2>/dev/null || true' ]; for (const cmd of volumeCommands) { execSync(cmd, { stdio: 'pipe' }); } startSpinner.succeed(chalk.green('Volumes criados com sucesso!')); } catch (volError) { startSpinner.warn(chalk.yellow('Aviso: Problemas ao criar volumes (eles podem já existir)')); console.error(chalk.dim(volError.message)); } // Construir imagens usando Docker Buildx Bake if (buildImages) { startSpinner.start('Preparando construção de imagens...'); // Verificar se há módulos que precisam de construção de imagem (não-infraestrutura) const hasModulesRequiringBuild = selectedModules.some(module => module.type !== 'infrastructure'); if (!hasModulesRequiringBuild && profileType === 'custom') { startSpinner.info(chalk.blue('ℹ️ Os serviços de infraestrutura não precisam ser construídos (usam imagens oficiais)')); } else { // Gerar comando de bake const bakeResult = commandGenerator.generateBakeCommand(env, profileType, selectedModules); if (bakeResult.command) { startSpinner.text = `Construindo imagens para ${env}...`; // Executar o comando em um diretório correto const bakeCommand = commandGenerator.createCommand(projectRoot, bakeResult.command); console.log(chalk.dim(`\nExecutando: ${bakeCommand}\n`)); try { execSync(bakeCommand, { stdio: 'inherit', env: { ...process.env, ENV: bakeResult.env, BUILDX_BAKE_ENTITLEMENTS_FS: '0' } }); startSpinner.succeed(chalk.green('Imagens construídas com sucesso!')); } catch (buildError) { startSpinner.fail(chalk.red('Erro ao construir imagens!')); console.error(buildError); // Perguntar se deseja continuar mesmo com falha na construção if (options.interactive) { const { continueDespiteError } = await inquirer.prompt([ { type: 'confirm', name: 'continueDespiteError', message: 'Houve um erro na construção das imagens. Deseja continuar mesmo assim?', default: false } ]); if (!continueDespiteError) { console.log(chalk.yellow('Operação cancelada devido a erros na construção.')); return; } } else { // Em modo não interativo, abortar em caso de erro na construção console.log(chalk.yellow('Operação abortada devido a erros na construção.')); return; } } } else if (bakeResult.message) { startSpinner.info(chalk.blue(bakeResult.message)); } } } startSpinner.start('Iniciando serviços com Docker Compose...'); // Gerar comando compose para iniciar serviços const composeCommand = commandGenerator.generateComposeCommand(env, profileType, selectedModules); // Executar o comando compose const fullCommand = commandGenerator.createCommand(projectRoot, composeCommand); console.log(chalk.dim(`\nExecutando: ${fullCommand}\n`)); execSync(fullCommand, { stdio: 'inherit' }); startSpinner.succeed(chalk.green('Serviços iniciados com sucesso!')); // Exibir URLs de acesso const domainSuffix = env === 'prod' ? '.cartoriocriciumasc.com.br' : '.localhost'; // Obter todos os módulos ativos para exibir URLs let activeModules = []; if (profileType === 'custom') { activeModules = selectedModules; } else { // Para perfis predefinidos, obter módulos para o ambiente const allModulesForEnv = registry.getModulesForEnv(env); if (profileType === 'infra') { activeModules = allModulesForEnv.filter(m => m.type === 'infrastructure'); } else if (profileType === 'core') { activeModules = allModulesForEnv.filter(m => m.type === 'core' || m.type === 'infrastructure'); } else if (profileType === 'all') { activeModules = allModulesForEnv; } } // Mostrar URLs para os módulos ativos activeModules.forEach(module => { if (module.services) { module.services.forEach(service => { if (service.name && service.port) { console.log(chalk.green(`${module.type === 'frontend' ? '🖥️' : '🔌'} ${module.name} (${service.name}): http://${module.id}.${service.name}${domainSuffix}`)); } }); } }); // Sempre exibir Consul e Traefik em ambientes não-prod if (env !== 'prod') { console.log(chalk.cyan(`\n🔍 Consul Dashboard: http://consul${domainSuffix}`)); console.log(chalk.cyan(`🔄 Traefik Dashboard: http://traefik${domainSuffix}`)); } } catch (error) { startSpinner.fail(chalk.red('Erro ao iniciar serviços!')); console.error(error); process.exit(1); } };