cartorio-plataforma-cli
Version:
CLI para gerenciar a Plataforma do Cartório de Criciúma com sistema de módulos
447 lines (391 loc) • 18.1 kB
JavaScript
/**
* Módulo para geração de comandos Docker para construção e execução de serviços
*/
const os = require('os');
const path = require('path');
const fs = require('fs');
const chalk = require('chalk');
/**
* Classe para gerar comandos Docker para a plataforma
*/
class DockerCommandGenerator {
/**
* Cria um novo gerador de comandos Docker
* @param {ModuleRegistry} moduleRegistry - Registro de módulos da plataforma
*/
constructor(moduleRegistry) {
this.registry = moduleRegistry;
this.isWindows = os.platform() === 'win32';
this.projectDir = null; // Armazenar o diretório do projeto encontrado
}
/**
* Encontra o diretório principal do projeto
* @returns {string} Diretório principal do projeto ou diretório atual se não encontrado
*/
findProjectDir() {
// Retornar diretório já encontrado se disponível
if (this.projectDir) {
return this.projectDir;
}
const possibleLocations = [
process.cwd(), // Diretório atual
path.resolve(process.cwd(), '..'), // Diretório pai
path.resolve(process.cwd(), '../..'), // Dois níveis acima
path.resolve(__dirname, '../..'), // Dois níveis acima do diretório do script
path.resolve(__dirname, '../../..'), // Três níveis acima do diretório do script
// Locais específicos para quando executado pela CLI instalada globalmente
path.resolve(process.env.PWD || process.cwd())
];
console.log(`Procurando diretório do projeto nas seguintes localizações:`);
possibleLocations.forEach(loc => console.log(` - ${loc}`));
// Busca arquivos marcadores em cada local
for (const location of possibleLocations) {
// Verificar se o diretório existe
if (!fs.existsSync(location)) {
console.log(`❌ Diretório não existe: ${location}`);
continue;
}
console.log(`Verificando diretório: ${location}`);
// Primeiro, tentar encontrar o docker-compose.bake.hcl diretamente
if (fs.existsSync(path.join(location, 'docker-compose.bake.hcl'))) {
this.projectDir = location;
console.log(`✅ Diretório do projeto encontrado via docker-compose.bake.hcl: ${this.projectDir}`);
return this.projectDir;
}
// Depois, procurar por uma estrutura de diretório específica da plataforma
// Verificar se contém um diretório 'plataforma-infra'
const infraPath = path.join(location, 'plataforma-infra');
if (fs.existsSync(infraPath)) {
// Verificar se o docker-compose.bake.hcl está dentro do plataforma-infra
if (fs.existsSync(path.join(infraPath, 'docker-compose.bake.hcl'))) {
this.projectDir = infraPath; // Usar o diretório plataforma-infra, não o diretório pai
console.log(`✅ Diretório do projeto encontrado via plataforma-infra: ${this.projectDir}`);
return this.projectDir;
}
}
// Se encontrar um package.json, verificar se tem "name": "plataforma-cli"
const packagePath = path.join(location, 'package.json');
if (fs.existsSync(packagePath)) {
try {
const packageData = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
if (packageData.name === 'plataforma-cli') {
// Encontramos o package.json da CLI, o diretório do projeto está acima
const projectPathFromCli = path.resolve(location, '..');
if (fs.existsSync(path.join(projectPathFromCli, 'docker-compose.bake.hcl'))) {
this.projectDir = projectPathFromCli;
console.log(`✅ Diretório do projeto encontrado via package.json: ${this.projectDir}`);
return this.projectDir;
}
}
} catch (error) {
// Ignorar erros na leitura do package.json
console.log(`⚠️ Erro ao ler package.json em ${location}: ${error.message}`);
}
}
}
// Se chegar aqui, tentar encontrar qualquer arquivo de Docker
for (const location of possibleLocations) {
const dockerFiles = [
'docker-compose.yml',
'docker-compose.yaml',
'Dockerfile'
];
for (const file of dockerFiles) {
if (fs.existsSync(path.join(location, file))) {
this.projectDir = location;
console.log(`✅ Diretório com arquivo Docker encontrado: ${this.projectDir}`);
return this.projectDir;
}
}
}
// Adicional: procurar por pastas que contêm módulos
for (const location of possibleLocations) {
try {
const items = fs.readdirSync(location, { withFileTypes: true });
for (const item of items) {
if (item.isDirectory() &&
(item.name.includes('-frontend') ||
item.name.includes('-backend') ||
item.name === 'plataforma-infra' ||
item.name === 'root-config')) {
this.projectDir = location;
console.log(`✅ Diretório raiz encontrado pela detecção de módulos: ${this.projectDir}`);
return this.projectDir;
}
}
} catch (error) {
// Ignorar erros na leitura do diretório
}
}
// Fallback para o diretório atual
this.projectDir = process.cwd();
console.warn(`⚠️ Não foi possível determinar diretório principal do projeto. Usando: ${this.projectDir}`);
return this.projectDir;
}
/**
* Cria um comando para execução em um diretório específico
* @param {string} baseDir - Diretório base onde o comando deve ser executado
* @param {string} command - Comando a ser executado
* @returns {string} - Comando completo com mudança de diretório
*/
createCommand(baseDir, command) {
if (!baseDir || typeof baseDir !== 'string') {
console.warn(`⚠️ Aviso: baseDir inválido (${baseDir})`);
baseDir = this.findProjectDir();
}
// Verificar se o baseDir existe
if (!fs.existsSync(baseDir)) {
console.warn(`⚠️ Aviso: O diretório ${baseDir} não existe.`);
// Tentar usar o diretório do projeto encontrado
baseDir = this.findProjectDir();
console.log(`✅ Usando diretório do projeto: ${baseDir}`);
}
const absolutePath = path.resolve(baseDir);
console.log(`📂 Executando comando no diretório: ${absolutePath}`);
// Escape de caracteres especiais no caminho para evitar problemas no shell
const escapedPath = this.isWindows
? `"${absolutePath}"`
: absolutePath.replace(/(["\s'$`\\])/g, '\\$1');
if (this.isWindows) {
return `cd /d ${escapedPath} && ${command}`;
} else {
return `cd ${escapedPath} && ${command}`;
}
}
/**
* Gera um comando de Docker Buildx Bake para construir imagens
* @param {string} env - Ambiente (dev, local, prod)
* @param {string} profileType - Tipo de profile (infra, core, modules, full, custom)
* @param {Array} moduleList - Lista de módulos selecionados (para custom)
* @returns {Object} - Comando e detalhes para executar
*/
generateBakeCommand(env, profileType, moduleList = []) {
const envBuild = this.isWindows ? `set BUILDX_BAKE_ENTITLEMENTS_FS=0 &&` : `BUILDX_BAKE_ENTITLEMENTS_FS=0`;
let bakeCommand;
// Encontrar diretório do projeto
const projectDir = this.findProjectDir();
// Buscar o arquivo bake.hcl
const bakeFilePath = path.join(projectDir, 'docker-compose.bake.hcl');
if (!fs.existsSync(bakeFilePath)) {
console.warn(`⚠️ Aviso: Arquivo docker-compose.bake.hcl não encontrado em ${projectDir}`);
return {
command: null,
message: 'Arquivo docker-compose.bake.hcl não encontrado. Verifique a estrutura do projeto.'
};
}
console.log(`✅ Usando arquivo de bake: ${bakeFilePath}`);
// Verificar quais targets existem no arquivo bake.hcl
let availableTargets = [];
try {
const bakeContent = fs.readFileSync(bakeFilePath, 'utf8');
// Módulos conhecidos que devem funcionar e identificação de patterns de targets no bake.hcl
const knownTargets = [
'root-config-local', 'plataforma-frontend-local', 'plataforma-backend-local',
'modulo1-frontend-local', 'modulo1-backend-local', 'minutador-frontend-local',
'minutador-backend-local'
];
// Procurar por definições de targets no arquivo bake (padrão "target.*" ou "group.*")
const targetMatches = bakeContent.match(/target\s*"([^"]+)"/g) || [];
const groupMatches = bakeContent.match(/group\s*"([^"]+)"/g) || [];
// Extrair os nomes dos targets dos matches
const extractedTargets = [
...targetMatches.map(m => m.match(/target\s*"([^"]+)"/)[1]),
...groupMatches.map(m => m.match(/group\s*"([^"]+)"/)[1])
];
// Combinar com targets conhecidos
availableTargets = [...new Set([...knownTargets, ...extractedTargets])];
console.log(chalk.dim('Debug: Targets disponíveis:'), availableTargets.join(', '));
} catch (error) {
console.warn(`⚠️ Aviso: Erro ao ler targets do arquivo bake.hcl: ${error.message}`);
// Se não puder ler os targets, supor que os conhecidos estão disponíveis
availableTargets = [
'root-config-local', 'plataforma-frontend-local', 'plataforma-backend-local',
'modulo1-frontend-local', 'modulo1-backend-local', 'minutador-frontend-local',
'minutador-backend-local'
];
}
// Determinar o grupo de bake com base no tipo de perfil
if (profileType === 'custom' && moduleList.length > 0) {
// Filtrar módulos de infraestrutura, pois não precisam ser construídos
const buildableModules = moduleList.filter(module => module.type !== 'infrastructure');
// Gerar targets específicos do bake para os módulos selecionados
const targets = buildableModules.map(module => {
if (module.profiles && module.profiles[env]) {
return module.profiles[env];
}
// Target padrão de fallback baseado no ID do módulo
if (module.id) {
return `${module.id}-${env}`;
}
return null;
}).filter(target => target !== null);
// Exibir aviso sobre infraestrutura
const infraModules = moduleList.filter(m => m.type === 'infrastructure');
if (infraModules.length > 0) {
console.log(chalk.blue('ℹ️ Os serviços de infraestrutura não precisam ser construídos (usam imagens oficiais)'));
}
// Filtrar apenas os targets que existem no arquivo bake
const existingTargets = targets.filter(target => availableTargets.includes(target));
const missingTargets = targets.filter(target => !availableTargets.includes(target));
if (missingTargets.length > 0) {
console.log(chalk.yellow(`⚠️ Alguns targets não foram encontrados no arquivo bake.hcl e serão ignorados: ${missingTargets.join(', ')}`));
}
console.log(chalk.dim('Debug: Targets para bake:'), existingTargets.join(', '));
if (existingTargets.length > 0) {
bakeCommand = `${envBuild} docker buildx bake -f "${bakeFilePath}" ${existingTargets.join(' ')}`;
} else {
return {
command: null,
message: 'Nenhum target válido encontrado para construção. Verifique se os módulos foram definidos no arquivo bake.hcl.'
};
}
} else {
// Usar grupos predefinidos no arquivo bake
let bakeGroup;
if (profileType === 'all') {
bakeGroup = env; // Usar grupo completo para 'all'
} else if (profileType === 'infra') {
// Aviso sobre infraestrutura
console.log(chalk.blue('ℹ️ Os serviços de infraestrutura não precisam ser construídos (usam imagens oficiais)'));
return {
command: null,
message: 'Os serviços de infraestrutura não precisam ser construídos, pois usam imagens oficiais.'
};
} else {
bakeGroup = `${env}-${profileType}`;
// Verificar se o grupo existe no arquivo bake
if (!availableTargets.includes(bakeGroup)) {
console.log(chalk.yellow(`⚠️ O grupo de targets '${bakeGroup}' não foi encontrado no arquivo bake.hcl.`));
// Usar grupo padrão se disponível
if (availableTargets.includes(env)) {
bakeGroup = env;
console.log(chalk.blue(`ℹ️ Usando grupo padrão '${env}' em vez disso.`));
} else {
return {
command: null,
message: `O grupo de targets '${bakeGroup}' não existe. Verifique o arquivo bake.hcl.`
};
}
}
}
bakeCommand = `${envBuild} docker buildx bake -f "${bakeFilePath}" ${bakeGroup}`;
}
return {
command: bakeCommand,
env: env === 'dev' ? 'development' : (env === 'prod' ? 'production' : 'local')
};
}
/**
* Gera um comando Docker Compose para iniciar serviços
* @param {string} env - Ambiente (dev, local, prod)
* @param {string} profileType - Tipo de profile (infra, core, modules, full, custom)
* @param {Array} moduleList - Lista de módulos selecionados (para custom)
* @returns {string} - Comando Docker Compose para iniciar os serviços
*/
generateComposeCommand(env, profileType, moduleList = []) {
// Definir profiles padrão por ambiente e tipo
const defaultProfiles = {
// Ambiente de desenvolvimento
'dev': {
'infra': ['dev-infra'],
'core': ['dev-core', 'dev-infra'],
'all': ['dev']
},
// Ambiente local
'local': {
'infra': ['local-infra'],
'core': ['local-core', 'local-infra'],
'all': ['local']
},
// Ambiente de produção
'prod': {
'infra': ['prod-infra'],
'core': ['prod-core', 'prod-infra'],
'all': ['prod']
}
};
// Inicializar arrays vazios
const profiles = [];
let envVars = [];
let envCmd = this.isWindows ? `set ENVIRONMENT=${env} &&` : `ENVIRONMENT=${env}`;
// Encontrar diretório do projeto
const projectDir = this.findProjectDir();
// Verificar se existe um arquivo docker-compose.yml no diretório do projeto
const composeFilePath = path.join(projectDir, 'docker-compose.yml');
if (!fs.existsSync(composeFilePath)) {
console.warn(`⚠️ Aviso: Arquivo docker-compose.yml não encontrado em ${projectDir}`);
} else {
console.log(`✅ Usando arquivo de compose: ${composeFilePath}`);
}
// Selecionar profiles com base no ambiente e tipo
if (profileType === 'custom' && moduleList.length > 0) {
// Para perfil customizado, usar o perfil de infraestrutura do ambiente
// e adicionar os serviços específicos via variáveis de ambiente
profiles.push(...defaultProfiles[env]['infra']);
// Adicionar variáveis de ambiente para os módulos selecionados
moduleList.forEach(module => {
if (module.id && module.type) {
// Substituir hífens por sublinhados no ID do módulo
const moduleId = module.id.replace(/-/g, '_').toUpperCase();
const moduleType = module.type.toUpperCase();
const moduleVar = `INCLUDE_${moduleId}_${moduleType}=true`;
envVars.push(moduleVar);
}
});
} else {
// Para perfis padrão, usar os profiles definidos
if (defaultProfiles[env] && defaultProfiles[env][profileType]) {
profiles.push(...defaultProfiles[env][profileType]);
// Adicionar variáveis de ambiente para todos os módulos
if (profileType === 'all' && this.registry) {
const allModules = this.registry.getAllModules();
allModules.forEach(module => {
if (module.id && module.type) {
// Substituir hífens por sublinhados no ID do módulo
const moduleId = module.id.replace(/-/g, '_').toUpperCase();
const moduleType = module.type.toUpperCase();
const moduleVar = `INCLUDE_${moduleId}_${moduleType}=true`;
envVars.push(moduleVar);
}
});
} else if (profileType === 'core' && this.registry) {
// Para core, adicionar apenas os módulos core
const coreModules = this.registry.getModulesByCategory('core');
coreModules.forEach(module => {
if (module.id && module.type) {
// Substituir hífens por sublinhados no ID do módulo
const moduleId = module.id.replace(/-/g, '_').toUpperCase();
const moduleType = module.type.toUpperCase();
const moduleVar = `INCLUDE_${moduleId}_${moduleType}=true`;
envVars.push(moduleVar);
}
});
}
} else {
// Fallback para o ambiente padrão
profiles.push(env);
}
}
// Verificar se temos algum perfil selecionado
if (profiles.length === 0) {
profiles.push(env); // Usar o ambiente como perfil padrão
}
console.log(chalk.dim('Debug: Profiles para compose:'), profiles.join(', '));
// Construir o comando com os profiles
const profileArgs = profiles.map(p => `--profile ${p}`).join(' ');
// Adicionar variáveis de ambiente ao comando
if (envVars.length > 0) {
const envVarsStr = envVars.join(' ');
envCmd = this.isWindows
? envVars.map(v => `set ${v} &&`).join(' ') + ` ${envCmd}`
: `${envVarsStr} ${envCmd}`;
}
// Verificar se o arquivo .env existe
const envFilePath = path.join(projectDir, `.env.${env}`);
if (!fs.existsSync(envFilePath)) {
console.warn(`⚠️ Aviso: Arquivo .env.${env} não encontrado em ${projectDir}`);
}
return `${envCmd} docker compose ${profileArgs} --env-file .env.${env} up -d`;
}
}
module.exports = DockerCommandGenerator;