siesa-agents-dev
Version:
Paquete para instalar y configurar agentes SIESA en tu proyecto
401 lines (333 loc) • 14 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const readline = require('readline');
class SiesaBmadInstaller {
constructor() {
// Definir las carpetas primero (nombres en el paquete vs nombres finales)
this.folderMappings = [
{ source: 'bmad-core', target: '.bmad-core' },
{ source: 'vscode', target: '.vscode' },
{ source: 'github', target: '.github' },
{ source: 'claude', target: '.claude' }
];
this.targetDir = process.cwd();
// Intentar múltiples ubicaciones posibles para el paquete
this.packageDir = this.findPackageDir();
}
showBanner() {
console.log('\n');
console.log('███████╗██╗███████╗███████╗ █████╗ █████╗ ██████╗ ███████╗███╗ ██╗████████╗███████╗');
console.log('██╔════╝██║██╔════╝██╔════╝██╔══██╗ ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝██╔════╝');
console.log('███████╗██║█████╗ ███████╗███████║ ███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ ███████╗');
console.log('╚════██║██║██╔══╝ ╚════██║██╔══██║ ██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║');
console.log('███████║██║███████╗███████║██║ ██║ ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ███████║');
console.log('╚══════╝╚═╝╚══════╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝');
console.log('');
}
findPackageDir() {
// Opción 1: directorio padre del bin (instalación normal)
let packageDir = path.dirname(__dirname);
// Opción 2: si estamos en npx, buscar en node_modules
if (!this.hasRequiredFolders(packageDir)) {
// Para npm 6.14+ y npx, intentar ubicaciones alternativas
const possiblePaths = [
path.join(__dirname, '..'), // bin -> paquete
path.resolve(__dirname, '..'), // alternativa con resolve
path.resolve(__dirname, '..', '..', 'siesa-bmad-agents'), // desde node_modules
// Intentar buscar basado en __dirname específicamente para npx
path.resolve(__dirname.replace(/\\bin$|\/bin$/, '')),
process.cwd(), // directorio actual como último recurso
];
for (const possiblePath of possiblePaths) {
if (this.hasRequiredFolders(possiblePath)) {
packageDir = possiblePath;
break;
}
}
}
return packageDir;
}
hasRequiredFolders(dir) {
return this.folderMappings.some(mapping => {
const folderPath = path.join(dir, mapping.source);
return fs.existsSync(folderPath);
});
}
async install() {
this.showBanner();
console.log(' Instalando SIESA Agents...');
try {
// Verificar si ya existe una instalación
const hasExistingInstallation = this.checkExistingInstallation();
if (hasExistingInstallation) {
console.log('🔄 Instalación existente detectada. Actualizando...');
await this.update();
} else {
console.log('✨ Nueva instalación...');
await this.performInstallation();
}
console.log('✅ SIESA Agents instalado correctamente!');
this.showPostInstallMessage();
} catch (error) {
console.error('❌ Error durante la instalación:', error.message);
process.exit(1);
}
}
checkExistingInstallation() {
return this.folderMappings.some(mapping => {
const targetPath = path.join(this.targetDir, mapping.target);
return fs.existsSync(targetPath);
});
}
async checkModifiedFiles() {
const modifiedFiles = [];
for (const mapping of this.folderMappings) {
const sourcePath = path.join(this.packageDir, mapping.source);
const targetPath = path.join(this.targetDir, mapping.target);
if (!fs.existsSync(sourcePath) || !fs.existsSync(targetPath)) {
continue;
}
// Obtener todos los archivos recursivamente
const sourceFiles = await this.getAllFiles(sourcePath);
for (const sourceFile of sourceFiles) {
const relativePath = path.relative(sourcePath, sourceFile);
const targetFile = path.join(targetPath, relativePath);
if (fs.existsSync(targetFile)) {
try {
// Comparar contenido de archivos
const sourceContent = await fs.readFile(sourceFile, 'utf8');
const targetContent = await fs.readFile(targetFile, 'utf8');
if (sourceContent !== targetContent) {
modifiedFiles.push({
folder: mapping.target,
file: relativePath,
fullPath: targetFile
});
}
} catch (error) {
// Si no se puede leer como texto, comparar como buffer (archivos binarios)
const sourceBuffer = await fs.readFile(sourceFile);
const targetBuffer = await fs.readFile(targetFile);
if (!sourceBuffer.equals(targetBuffer)) {
modifiedFiles.push({
folder: mapping.target,
file: relativePath,
fullPath: targetFile
});
}
}
}
}
}
return modifiedFiles;
}
async getAllFiles(dir) {
const files = [];
const items = await fs.readdir(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = await fs.stat(fullPath);
if (stat.isDirectory()) {
const subFiles = await this.getAllFiles(fullPath);
files.push(...subFiles);
} else {
files.push(fullPath);
}
}
return files;
}
async promptUser(modifiedFiles) {
console.log('\n⚠️ Se detectaron archivos modificados:');
// Agrupar por carpeta
const filesByFolder = {};
modifiedFiles.forEach(item => {
if (!filesByFolder[item.folder]) {
filesByFolder[item.folder] = [];
}
filesByFolder[item.folder].push(item.file);
});
// Mostrar archivos modificados por carpeta
Object.keys(filesByFolder).forEach(folder => {
console.log(`\n📁 ${folder}:`);
filesByFolder[folder].forEach(file => {
console.log(` - ${file}`);
});
});
console.log('\n¿Qué deseas hacer?');
console.log('1. Reemplazar todos los archivos (se perderán las modificaciones)');
console.log('2. Hacer backup de archivos modificados (se agregarán con sufijo _bk)');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
rl.question('\nElige una opción (1 o 2): ', (answer) => {
rl.close();
resolve(answer.trim());
});
});
}
async backupModifiedFiles(modifiedFiles) {
console.log('\n🔄 Creando backup de archivos modificados...');
for (const item of modifiedFiles) {
const originalPath = item.fullPath;
const backupPath = this.getBackupPath(originalPath);
try {
await fs.copy(originalPath, backupPath);
// Determinar tipo de backup para mostrar mensaje apropiado
const backupName = path.basename(backupPath);
const isVersionedBackup = backupName.includes('_bk_');
if (isVersionedBackup) {
console.log(`✓ Backup versionado: ${path.relative(this.targetDir, backupPath)}`);
} else {
console.log(`✓ Backup creado: ${path.relative(this.targetDir, backupPath)}`);
}
} catch (error) {
console.warn(`⚠️ Error creando backup de ${item.file}: ${error.message}`);
}
}
}
getBackupPath(filePath) {
const dir = path.dirname(filePath);
const ext = path.extname(filePath);
const name = path.basename(filePath, ext);
// Primer intento: archivo_bk.ext
const basicBackupPath = path.join(dir, `${name}_bk${ext}`);
// Si no existe, usar el nombre básico
if (!fs.existsSync(basicBackupPath)) {
return basicBackupPath;
}
// Si ya existe _bk, crear versión con timestamp
const now = new Date();
const timestamp = now.getFullYear().toString() +
(now.getMonth() + 1).toString().padStart(2, '0') +
now.getDate().toString().padStart(2, '0') + '_' +
now.getHours().toString().padStart(2, '0') +
now.getMinutes().toString().padStart(2, '0') +
now.getSeconds().toString().padStart(2, '0');
return path.join(dir, `${name}_bk_${timestamp}${ext}`);
}
async performUpdateWithBackups() {
for (const mapping of this.folderMappings) {
const sourcePath = path.join(this.packageDir, mapping.source);
const targetPath = path.join(this.targetDir, mapping.target);
if (fs.existsSync(sourcePath)) {
// Copiar archivos selectivamente, preservando los _bk
await this.copyWithBackupPreservation(sourcePath, targetPath);
} else {
console.warn(`⚠️ Carpeta ${mapping.source} no encontrada en el paquete`);
}
}
}
async copyWithBackupPreservation(sourcePath, targetPath) {
// Obtener todos los archivos backup existentes
const backupFiles = await this.findBackupFiles(targetPath);
// Copiar la carpeta completa sobrescribiendo
await fs.copy(sourcePath, targetPath, {
overwrite: true,
recursive: true
});
// Restaurar los archivos backup
for (const backupFile of backupFiles) {
const backupSourcePath = backupFile.tempPath;
const backupTargetPath = backupFile.originalPath;
try {
await fs.copy(backupSourcePath, backupTargetPath);
// Limpiar archivo temporal
await fs.remove(backupSourcePath);
} catch (error) {
console.warn(`⚠️ Error restaurando backup ${backupFile.relativePath}: ${error.message}`);
}
}
}
async findBackupFiles(targetPath) {
if (!fs.existsSync(targetPath)) {
return [];
}
const backupFiles = [];
const allFiles = await this.getAllFiles(targetPath);
for (const filePath of allFiles) {
const fileName = path.basename(filePath);
// Detectar archivos _bk y _bk_timestamp
if (fileName.includes('_bk')) {
const tempPath = path.join(require('os').tmpdir(), `backup_${Date.now()}_${fileName}`);
const relativePath = path.relative(targetPath, filePath);
// Crear copia temporal del backup
await fs.copy(filePath, tempPath);
backupFiles.push({
originalPath: filePath,
tempPath: tempPath,
relativePath: relativePath
});
}
}
return backupFiles;
}
async performInstallation() {
for (const mapping of this.folderMappings) {
const sourcePath = path.join(this.packageDir, mapping.source);
const targetPath = path.join(this.targetDir, mapping.target);
if (fs.existsSync(sourcePath)) {
await fs.copy(sourcePath, targetPath, {
overwrite: true,
recursive: true
});
} else {
console.warn(`⚠️ Carpeta ${mapping.source} no encontrada en el paquete`);
}
}
}
async update() {
// Verificar archivos modificados
console.log('🔍 Verificando archivos modificados...');
const modifiedFiles = await this.checkModifiedFiles();
let hasBackups = false;
if (modifiedFiles.length > 0) {
const userChoice = await this.promptUser(modifiedFiles);
if (userChoice === '2') {
// Crear backup de archivos modificados
await this.backupModifiedFiles(modifiedFiles);
hasBackups = true;
} else if (userChoice !== '1') {
console.log('❌ Opción no válida. Cancelando actualización.');
return;
}
console.log('\n🔄 Procediendo con la actualización...');
} else {
console.log('✓ No se detectaron archivos modificados.');
}
// Si hay backups, hacer actualización preservando backups
if (hasBackups) {
await this.performUpdateWithBackups();
} else {
// Si no hay backups, hacer actualización normal (remover y copiar)
for (const mapping of this.folderMappings) {
const targetPath = path.join(this.targetDir, mapping.target);
if (fs.existsSync(targetPath)) {
await fs.remove(targetPath);
}
}
// Realizar instalación nueva
await this.performInstallation();
}
}
showPostInstallMessage() {
console.log('\n📚 Carpetas instaladas:');
this.folderMappings.forEach(mapping => {
const targetPath = path.join(this.targetDir, mapping.target);
if (fs.existsSync(targetPath)) {
console.log(` ✓ ${mapping.target}`);
}
});
console.log('\n🎉 ¡Instalación completada!');
console.log('💡 Las carpetas han sido instaladas en tu directorio actual.');
console.log('🔧 Puedes ejecutar "npx siesa-agents" nuevamente para actualizar.');
}
}
// Ejecutar instalación si el script es llamado directamente
if (require.main === module) {
const installer = new SiesaBmadInstaller();
installer.install();
}
module.exports = SiesaBmadInstaller;