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
JavaScript
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);
}
};