Refactor(ADN): Optimización de la arquitectura y reparación de subcomandos

- Reestructuración del CLI en módulos discretos (core/, comando/, config/, clientes/)
- Migración del punto de entrada tools/adn.rb hacia el launcher ejecutable tools/adn/run
- Simplificación de nombres de comandos (remoción prefijo subcomando_)
- Corrección de rutas base transversales (PROJECT_ROOT, BITACORAS_DIR)
- Reescritura integral de sentencias require_relative
- Restauración de sintaxis en archivo sync.rb truncado
This commit is contained in:
Ricardo Monla
2026-03-07 09:45:21 -03:00
parent 39e3e130e6
commit c4c761e408
13 changed files with 365 additions and 75 deletions
+77
View File
@@ -0,0 +1,77 @@
# 🧬 ADN Tools (Núcleo Autónomo)
## Resumen Ejecutivo
`tools/adn` es el motor de ejecución y automatización del sistema **ADN (Artificial DNA)**. Implementa la lógica necesaria para garantizar la "Armonía Integral" de la infraestructura mediante validación proactiva, gestión de eventos y protocolos de ejecución autónomos. Actúa como el puente técnico entre las especificaciones declarativas (hebras `.md`) y la realidad operativa del servidor.
---
## 🔍 Análisis Estructural
### Arquitectura
El proyecto sigue un patrón **Modular y Dirigido por Eventos**:
- **Entry Point**: `tools/adn.rb` centraliza la interfaz de línea de comandos (CLI).
- **Bus de Eventos**: `ADN::Eventos` (Singleton) gestiona la comunicación desacoplada entre componentes.
- **Motor de Triggers**: `ADN::Triggers` (Singleton) orquestra acciones automáticas basadas en la configuración de `adn/triggers.yml`.
- **Estrategia de Subcomandos**: Cada funcionalidad mayor reside en su propio archivo `subcomando_*.rb`, facilitando la extensibilidad.
### Archivos Críticos
1. **`adn.rb`**: El orquestador principal. Si este falla, se pierde la interfaz de gestión.
2. **`adn/eventos.rb`**: El "sistema nervioso". Permite que un cambio en una bitácora dispare validaciones o backups.
3. **`adn/validador.rb`**: El "guardián" de la calidad. Protege la integridad de las bitácoras y fichas de nodos.
4. **`adn/triggers.rb`**: Implementa la lógica de reacción del sistema.
---
## ⚙️ Configuración y Tecnologías
### Stack Tecnológico
- **Lenguaje**: Ruby (Enfoque en scripting robusto y legible).
- **Configuración**: YAML (`adn/triggers.yml` y `tools/adn/config.yml`).
- **Logging**: JSON estructurado para integración con sistemas de monitoreo externos.
- **Hooks**: Integración con Git mediante `tools/adn/hooks/pre-commit`.
### Comandos Esenciales
```bash
# Validar cumplimiento de bitácoras
./tools/adn/run validador
# Ejecutar backup seguro de un nodo
./tools/adn/run backup <nombre-nodo>
# Ver salud del sistema
./tools/adn/run salud --json
# Sincronizar bitácoras con la API
./tools/adn/run sync
```
---
## 🧠 Contexto para IA (Directivas)
### Convenciones Proyectuales
- **Singleton**: Los motores centrales (Eventos, Triggers, Logger) deben accederse siempre mediante su instancia única.
- **Bitácora-First**: Ninguna acción operativa debe ejecutarse sin antes registrarse en la bitácora del día actual.
- **Seguridad**: Nunca exponer secretos en `stdout`. Usar `ns8-candados run` para inyección de entorno.
### Áreas Delicadas / Deuda Técnica
- ⚠️ **Sincronización Batch**: El sistema de triggers utiliza `stubs` para muchas acciones de la Fase 2 que aún deben desarrollarse.
- ⚠️ **Modo Watch**: El validador en tiempo real (basado en `listen`) es una tarea pendiente de la Fase 1.
- ⚠️ **Métricas**: La extracción de telemetría desde las bitácoras es incipiente y requiere mayor robustez en el parser.
---
## 🚀 Próximos Pasos
### Tareas Prioritarias
1. **Implementar Triggers**: Desarrollar la lógica real para las 14 acciones definidas en `adn/triggers.yml`.
2. **Refinar `backup`**: Añadir soporte para protocolos XEN y Rclone dentro del subcomando unificado.
3. **Dashboard de Salud**: Desarrollar la vista visual de `./tools/adn/run salud --dashboard`.
### Preguntas para el Desarrollador
- ¿Cómo se deben manejar las colisiones de eventos si dos triggers disparan acciones contradictorias?
- ¿Es necesario persistir el estado del bus de eventos o es puramente efímero?
- ¿Qué nivel de criticidad tiene el fallo de un trigger en la operación general?
---
*Documentación generada automáticamente siguiendo el estándar `docs/prompt/documentacion.md`.*
+191
View File
@@ -0,0 +1,191 @@
# frozen_string_literal: true
require 'optparse'
require 'fileutils'
require 'date'
module ADN
class SubcomandoBackup
def initialize(args, logger)
@args = args
@logger = logger
@opciones = { modo: 'stop', storage: 'local', compress: 'gzip' }
end
def ejecutar
parser = OptionParser.new do |opts|
opts.banner = "Uso: ./tools/adn/run backup <nodo> [OPCIONES]"
opts.separator ""
opts.separator "Ejecuta un backup remoto siguiendo el protocolo ADN"
opts.separator ""
opts.separator "Opciones:"
opts.on("--mode MODO", "Modo de backup (stop, snapshot, suspend. Default: stop)") do |m|
@opciones[:modo] = m
end
opts.on("-h", "--help", "Mostrar esta ayuda") do
puts opts
exit
end
end
begin
parser.parse!(@args)
rescue OptionParser::InvalidOption => e
@logger.error("Opción inválida: #{e.message}")
puts parser
exit 1
end
if @args.empty?
@logger.error("Se requiere nombre de nodo")
puts parser
exit 1
end
nodo = @args[0]
ejecutar_backup(nodo)
end
private
def ejecutar_backup(nodo)
@logger.info("Iniciando flujo ADN-Backup para nodo: #{nodo}")
# 1. Obtener contexto del nodo
datos = obtener_contexto_nodo(nodo)
unless datos
@logger.error("No se pudo determinar el contexto técnico para #{nodo}")
exit 1
end
@logger.info("Contexto validado: VM #{datos[:vmid]} en #{datos[:host_name]} (#{datos[:host_ip]})")
# 2. Registrar inicio en bitácora (Bitácora-First)
registrar_inicio_bitacora(nodo, datos)
# 3. Ejecución segura vía ns8-candados
@logger.info("Solicitando autorización y ejecutando backup remoto...")
comando_vzdump = "vzdump #{datos[:vmid]} --mode #{@opciones[:modo]} --storage #{@opciones[:storage]} --compress #{@opciones[:compress]}"
cmd_seguro = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{datos[:host_ip]} \"#{comando_vzdump}\""
# Usamos el binario de ruby y la ruta absoluta para ns8-candados
candados_path = File.join(ADN::PROJECT_ROOT, 'tools', 'ns8-candados', 'ns8-candados.rb')
# Ejecutar con ns8-candados run
# Nota: el comando 'authorize' debe haberse ejecutado antes o lo incluimos
puts "#{Color::DIM}🔓 Abriendo candado para sesión de backup...#{Color::RESET}"
system("ruby #{candados_path} authorize > /dev/null 2>&1")
success = system("ruby #{candados_path} run admindasu SSHPASS '#{cmd_seguro}'")
# 4. Finalizar y cerrar
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
if success
@logger.exito("Backup de #{nodo} completado exitosamente.")
registrar_fin_bitacora(nodo, true)
else
@logger.error("La ejecución del backup falló.")
registrar_fin_bitacora(nodo, false)
exit 1
end
end
def obtener_contexto_nodo(nombre_nodo)
ruta_nodo = File.join(ADN::NODOS_DIR, "#{nombre_nodo}.md")
unless File.exist?(ruta_nodo)
@logger.error("No existe el archivo de nodo: #{ruta_nodo}")
return nil
end
contenido = File.read(ruta_nodo)
# Buscamos VMID (puede decir "VM ID: 100" o "vmid: 100")
vmid = contenido.match(/(?:VM ID|vmid):\s*(\d+)/i)&.[](1)
# Buscamos el host (especificamente el link o nombre entre backticks)
# Formato: - **Padre/Host**: `srv-dasu`
host_name = contenido.match(/Padre\/Host\*\*:?\s*`([^`]+)`/)&.[](1)
host_name ||= contenido.match(/Host\*\*:?\s*`([^`]+)`/)&.[](1)
unless vmid && host_name
@logger.error("Contexto incompleto en #{nombre_nodo}.md: VMID=#{vmid}, Host=#{host_name}")
return nil
end
# Buscar IP del host
ruta_host = File.join(ADN::NODOS_DIR, "#{host_name}.md")
host_ip = nil
if File.exist?(ruta_host)
c_host = File.read(ruta_host)
# IP en tabla: | IP | `10.0.10.1` |
host_ip ||= c_host.match(/\|\s*IP[^\|]*\|\s*`([^`]+)`\s*\|/)&.[](1)
# IP en lista: - **IP**: `10.0.10.1`
host_ip ||= c_host.match(/IP\*\*:?\s*`([^`]+)`/)&.[](1)
# IP simple: IP: 10.0.10.1
host_ip ||= c_host.match(/IP:\s*([\d\.]+)/)&.[](1)
end
unless host_ip
@logger.error("No se encontró IP para el host #{host_name}")
return nil
end
if host_ip && host_ip.include?('/')
host_ip = host_ip.split('/')[0]
end
{ vmid: vmid, host_name: host_name, host_ip: host_ip }
end
def registrar_inicio_bitacora(nodo, datos)
fecha = Date.today.iso8601
ruta = File.join(ADN::BITACORAS_DIR, "#{fecha}.md")
return unless File.exist?(ruta)
hora = Time.now.strftime('%H:%M')
# Si ya existe la sección del nodo, añadimos la línea. Si no, creamos la sección.
contenido = File.read(ruta)
entrada = <<~ENTRADA
### #{nodo}
#### ⏳ - BKP-ADN - Backup Autónomo (Protocolo ADN)
Ejecución de backup gestionada por `tools/adn.rb` para la VM #{datos[:vmid]} en el host #{datos[:host_name]}.
| I | F | Descripción | E |
| :--- | :--- | :--- | :--- |
| #{hora} | | Inicio de backup remoto vía SSH (Seguro). | [R] ⏳ |
ENTRADA
unless contenido.include?("### #{nodo}")
# Insertar antes del footer de la IA
nuevo_contenido = contenido.sub(/<!-- 🤖 PREMISAS/, "#{entrada}\n\n<!-- 🤖 PREMISAS")
File.write(ruta, nuevo_contenido)
end
end
def registrar_fin_bitacora(nodo, exito)
fecha = Date.today.iso8601
ruta = File.join(ADN::BITACORAS_DIR, "#{fecha}.md")
return unless File.exist?(ruta)
hora = Time.now.strftime('%H:%M')
estado = exito ? '✅' : '⚠️'
desc = exito ? 'Backup completado exitosamente.' : 'Fallo en la ejecución del backup.'
contenido = File.read(ruta)
# Actualizar la línea de tiempo
linea_nueva = "| 08:31 | #{hora} | #{desc} | [R] #{estado} |"
# Buscamos la tarea BKP-ADN
nuevo_contenido = contenido.gsub(/#### ⏳ - BKP-ADN/, "#### #{estado} - BKP-ADN")
nuevo_contenido = nuevo_contenido.gsub(/\| \d{2}:\d{2} \| \| Inicio de backup remoto.*/, linea_nueva)
File.write(ruta, nuevo_contenido)
end
end
end
@@ -18,7 +18,7 @@
# • adn db estadisticas — Mostrar estadísticas del sistema
#
# Uso:
# ruby tools/adn.rb db [SUBCOMANDO] [OPCIONES]
# ./tools/adn/run db [SUBCOMANDO] [OPCIONES]
require 'optparse'
require 'date'
@@ -26,8 +26,8 @@ require 'time'
# Cargar módulo BitacorasDB
begin
require_relative '../bitacoras-db/core/bitacora_db'
require_relative '../bitacoras-db/importers/md_importer'
require_relative '../../bitacoras-db/core/bitacora_db'
require_relative '../../bitacoras-db/importers/md_importer'
rescue LoadError => e
# Fallback para desarrollo
puts "⚠ Advertencia: No se pudo cargar BitacoraDB o MDImporter: #{e.message}"
@@ -908,7 +908,7 @@ module ADN
# Muestra la ayuda general del subcomando
def mostrar_ayuda
puts "#{Color::BOLD}Uso: ruby tools/adn.rb db [SUBCOMANDO] [OPCIONES]#{Color::RESET}"
puts "#{Color::BOLD}Uso: ./tools/adn/run db [SUBCOMANDO] [OPCIONES]#{Color::RESET}"
puts ""
puts "#{Color::BOLD}Subcomandos disponibles:#{Color::RESET}"
puts ""
@@ -924,10 +924,10 @@ module ADN
puts " #{Color::CYAN}estadisticas#{Color::RESET} Mostrar estadísticas del sistema"
puts ""
puts "#{Color::BOLD}Ejemplos:#{Color::RESET}"
puts " ruby tools/adn.rb db salud:bd"
puts " ruby tools/adn.rb db bitacora:crear --resumen \"Inicio de proyecto\""
puts " ruby tools/adn.rb db evento:crear --inicio 09:00 --descripcion \"Reunión\" --nodo srv-ns8"
puts " ruby tools/adn.rb db nodo:listar --tipo servidor"
puts " ./tools/adn/run db salud:bd"
puts " ./tools/adn/run db bitacora:crear --resumen \"Inicio de proyecto\""
puts " ./tools/adn/run db evento:crear --inicio 09:00 --descripcion \"Reunión\" --nodo srv-ns8"
puts " ./tools/adn/run db nodo:listar --tipo servidor"
puts ""
puts "#{Color::YELLOW}⚠ Nota:#{Color::RESET} Requiere PostgreSQL en ejecución (puerto 5433)"
puts " Iniciar contenedor: #{Color::DIM}cd dtic-BITACORAs && docker-compose up -d#{Color::RESET}"
@@ -21,7 +21,7 @@
# • adn plan ayuda — Mostrar ayuda completa
#
# Uso:
# ruby tools/adn.rb plan [SUBCOMANDO] [OPCIONES]
# ./tools/adn/run plan [SUBCOMANDO] [OPCIONES]
require 'optparse'
require 'date'
@@ -31,7 +31,7 @@ require 'yaml'
# Cargar módulo PlanParser
begin
require_relative 'plan/parser'
require_relative '../plan/parser'
rescue LoadError => e
puts "⚠ Advertencia: No se pudo cargar PlanParser: #{e.message}"
puts " Asegúrate de que tools/adn/plan/parser.rb existe"
@@ -487,7 +487,7 @@ module ADN
# Muestra la ayuda general del subcomando
def mostrar_ayuda
puts "#{Color::BOLD}Uso: ruby tools/adn.rb plan [SUBCOMANDO] [OPCIONES]#{Color::RESET}"
puts "#{Color::BOLD}Uso: ./tools/adn/run plan [SUBCOMANDO] [OPCIONES]#{Color::RESET}"
puts ""
puts "#{Color::BOLD}Subcomandos disponibles:#{Color::RESET}"
puts ""
@@ -504,11 +504,11 @@ module ADN
puts " #{Color::DIM}--output ARCHIVO#{Color::RESET} Guardar salida en archivo"
puts ""
puts "#{Color::BOLD}Ejemplos:#{Color::RESET}"
puts " #{Color::DIM}ruby tools/adn.rb plan progreso --detallado#{Color::RESET}"
puts " #{Color::DIM}ruby tools/adn.rb plan actualizar 1.1 1.2 2.3 --notas \"Implementado\"#{Color::RESET}"
puts " #{Color::DIM}ruby tools/adn.rb plan reporte --formato markdown --output reporte.md#{Color::RESET}"
puts " #{Color::DIM}ruby tools/adn.rb plan validar --reparar#{Color::RESET}"
puts " #{Color::DIM}ruby tools/adn.rb plan exportar --formato json --output progreso.json#{Color::RESET}"
puts " #{Color::DIM}./tools/adn/run plan progreso --detallado#{Color::RESET}"
puts " #{Color::DIM}./tools/adn/run plan actualizar 1.1 1.2 2.3 --notas \"Implementado\"#{Color::RESET}"
puts " #{Color::DIM}./tools/adn/run plan reporte --formato markdown --output reporte.md#{Color::RESET}"
puts " #{Color::DIM}./tools/adn/run plan validar --reparar#{Color::RESET}"
puts " #{Color::DIM}./tools/adn/run plan exportar --formato json --output progreso.json#{Color::RESET}"
puts ""
puts "#{Color::YELLOW}⚠ Archivo de plan por defecto:#{Color::RESET} #{File.basename(DEFAULT_PLAN_PATH)}"
puts " #{Color::DIM}#{DEFAULT_PLAN_PATH}#{Color::RESET}"
@@ -6,7 +6,7 @@
# Este subcomando permite sincronizar bitácoras en formato Markdown con la base de datos
# de dtic-BITACORAs mediante la API REST.
require_relative 'clients/dtic_bitacoras_api'
require_relative '../clientes/dtic_bitacoras_api'
require 'optparse'
require 'fileutils'
@@ -33,7 +33,7 @@ module ADN
def ejecutar
parser = OptionParser.new do |opts|
opts.banner = "Uso: ruby tools/adn.rb sync [OPCIONES] [RUTA_O_FECHA]"
opts.banner = "Uso: ./tools/adn/run sync [OPCIONES] [RUTA_O_FECHA]"
opts.separator ""
opts.separator "Sincroniza archivos Markdown con la API de dtic-BITACORAs"
opts.separator ""
@@ -272,5 +272,11 @@ module ADN
stats = @client.estadisticas
puts "\n#{Color::CYAN}📈 Estadísticas de dtic-BITACORAs#{Color::RESET}"
puts "#{Color::DIM}#{'-' * 40}#{Color::RESET}"
puts " #{Color::GREEN}Nodos en sistema:#{Color::RESET} #{stats[:nodos]}"
puts " #{Color::GREEN}Bitácoras en sistema:#{Color::RESET}
puts " #{Color::GREEN}Nodos en sistema:#{Color::RESET} #{stats[:nodos] || 0}"
puts " #{Color::GREEN}Bitácoras en sistema:#{Color::RESET} #{stats[:bitacoras] || 0}"
rescue => e
puts " #{Color::RED}No se pudieron obtener estadísticas: #{e.message}#{Color::RESET}"
end
end
end
end
@@ -12,8 +12,8 @@
# 📚 Referencia: docs/plan/260307-1400_Plan_Mejoras_ADN.md
require 'optparse'
require_relative 'triggers'
require_relative 'eventos'
require_relative '../core/triggers'
require_relative '../core/eventos'
module ADN
class SubcomandoTriggers
@@ -70,7 +70,7 @@ module ADN
# Crea el parser de opciones
def crear_parser
OptionParser.new do |opts|
opts.banner = "Uso: ruby tools/adn.rb triggers [OPCIONES]"
opts.banner = "Uso: ./tools/adn/run triggers [OPCIONES]"
opts.separator ""
opts.separator "Gestiona el sistema de triggers para sincronización automática del ADN"
opts.separator ""
@@ -116,11 +116,11 @@ module ADN
puts " Motor de triggers: tools/adn/triggers.rb"
puts ""
puts "Ejemplos:"
puts " ruby tools/adn.rb triggers --listar"
puts " ruby tools/adn.rb triggers --registrar"
puts " ruby tools/adn.rb triggers --test bitacoras_0"
puts " ruby tools/adn.rb triggers --forzar"
puts " ruby tools/adn.rb triggers --limpiar"
puts " ./tools/adn/run triggers --listar"
puts " ./tools/adn/run triggers --registrar"
puts " ./tools/adn/run triggers --test bitacoras_0"
puts " ./tools/adn/run triggers --forzar"
puts " ./tools/adn/run triggers --limpiar"
puts ""
puts "📚 Documentación: docs/plan/260307-1400_Plan_Mejoras_ADN.md"
end
@@ -211,7 +211,7 @@ module ADN
unless trigger_id
@logger.error("Se requiere ID de trigger para probar (ej: bitacoras_0)")
puts "#{Color::RED}✗ Se requiere ID de trigger para probar#{Color::RESET}"
puts " Ejemplo: ruby tools/adn.rb triggers --test bitacoras_0"
puts " Ejemplo: ./tools/adn/run triggers --test bitacoras_0"
puts " Usa --listar para ver IDs disponibles"
exit 1
end
@@ -14,7 +14,7 @@
require 'yaml'
require 'singleton'
require_relative '../adn'
require_relative 'eventos'
module ADN
Executable
+846
View File
@@ -0,0 +1,846 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/adn.rb — Interfaz de línea de comandos unificada para el sistema ADN
# ============================================================================
# 🤖 DIRECTIVA IA: Esta herramienta es el punto de entrada principal para la
# automatización del ADN. Su objetivo es reducir progresivamente la dependencia
# de la IA mediante scripts Ruby autónomos.
# ============================================================================
# Uso: ./tools/adn/run <subcomando> [opciones] [argumentos]
#
# Subcomandos disponibles:
# validador [--watch] [--fix] Validar cumplimiento del ADN en bitácoras
# contexto <nodo> [--cmd] Obtener contexto topológico de un nodo
# salud [--dashboard] [--json] Ver salud general del sistema ADN
# generar bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD)
# generar nodo <nombre> Generar ficha de nodo
# generar proyecto <código> Generar manifiesto de proyecto
# evolucion <propuesta.md> Gestionar propuesta de evolución del ADN
# ayuda [subcomando] Mostrar ayuda detallada
#
# Documentación completa: docs/plan/260307-1400_Plan_Mejoras_ADN.md
require 'optparse'
require 'json'
require 'fileutils'
require 'date'
require 'time'
module ADN
# El script ahora está en tools/adn/run, por lo que la raíz es 2 niveles arriba
PROJECT_ROOT = File.expand_path('../..', __dir__)
NODOS_DIR = File.join(PROJECT_ROOT, 'nodos')
PROYECTOS_DIR = File.join(PROJECT_ROOT, 'proyectos')
# Corrección a las rutas de bitácoras y logs
BITACORAS_DIR = File.join(PROJECT_ROOT, 'docs', 'bitacoras')
LOGS_DIR = File.join(PROJECT_ROOT, 'docs', 'bitacoras', '_hist')
# Aseguramos que los directorios existan
FileUtils.mkdir_p(LOGS_DIR) unless Dir.exist?(LOGS_DIR)
FileUtils.mkdir_p(BITACORAS_DIR) unless Dir.exist?(BITACORAS_DIR)
end
require_relative 'core/logger'
require_relative 'core/eventos'
require_relative 'core/triggers'
require_relative 'core/validador'
require_relative 'clientes/dtic_bitacoras_api'
require_relative 'comando/db'
require_relative 'comando/plan'
require_relative 'comando/backup'
require_relative 'comando/sync'
require_relative 'comando/triggers'
# ─── Colores para terminal ───────────────────────────────────────────────
module Color
RESET = "\e[0m"
BOLD = "\e[1m"
GREEN = "\e[32m"
CYAN = "\e[36m"
YELLOW = "\e[33m"
RED = "\e[31m"
MAGENTA = "\e[35m"
DIM = "\e[2m"
end
# ─── Logger estructurado ─────────────────────────────────────────────────
# El logger está implementado en adn/logger.rb
ADNLogger = ADN::Logger
# ─── Subcomando: Validador ───────────────────────────────────────────────
class SubcomandoValidador
def initialize(args, logger)
@args = args
@logger = logger
@opciones = { watch: false, fix: false }
end
def ejecutar
parser = OptionParser.new do |opts|
opts.banner = "Uso: ./tools/adn/run validador [OPCIONES]"
opts.separator ""
opts.separator "Valida el cumplimiento del ADN en las bitácoras"
opts.separator ""
opts.separator "Opciones:"
opts.on("--watch", "Monitorizar cambios en tiempo real") do
@opciones[:watch] = true
end
opts.on("--fix", "Intentar corregir errores automáticamente") do
@opciones[:fix] = true
end
opts.on("-h", "--help", "Mostrar esta ayuda") do
puts opts
exit
end
end
begin
parser.parse!(@args)
rescue OptionParser::InvalidOption => e
@logger.error("Opción inválida: #{e.message}")
puts parser
exit 1
end
@logger.info("Iniciando validador del ADN", @opciones)
if @opciones[:watch]
@logger.info("Modo watch activado (no implementado aún)")
puts "#{Color::YELLOW}⚠ El modo watch será implementado en Fase 1 completa#{Color::RESET}"
# TODO: Implementar con gem 'listen'
end
# Inicializar validador
validador = ADN::Validador.new(nil, @logger)
# Ejecutar validación
resultados = validador.validar_bitacoras(@opciones)
# Mostrar resultados
mostrar_resultados(resultados, validador)
end
private
def mostrar_resultados(resultados, validador)
total = resultados[:total]
validas = resultados[:validas]
con_errores = resultados[:con_errores]
con_advertencias = resultados[:con_advertencias]
puts "\n#{Color::CYAN}📊 Resultados de validación:#{Color::RESET}"
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
puts " #{Color::GREEN}Bitácoras totales:#{Color::RESET} #{total}"
puts " #{Color::GREEN}Válidas:#{Color::RESET} #{validas}"
if con_errores > 0
puts " #{Color::RED}Con errores:#{Color::RESET} #{con_errores}"
else
puts " #{Color::GREEN}Con errores:#{Color::RESET} 0"
end
if con_advertencias > 0
puts " #{Color::YELLOW}Con advertencias:#{Color::RESET} #{con_advertencias}"
else
puts " #{Color::GREEN}Con advertencias:#{Color::RESET} 0"
end
# Mostrar detalles de errores y advertencias
mostrar_detalles(resultados[:detalles])
# Resumen final
if con_errores == 0 && con_advertencias == 0
@logger.exito("Validación completada: #{total} bitácoras OK")
puts "#{Color::GREEN}✓ Todas las bitácoras cumplen con el formato del ADN#{Color::RESET}"
elsif con_errores == 0
@logger.advertencia("Validación completada con advertencias")
puts "#{Color::YELLOW}⚠ Validación completada con #{con_advertencias} advertencia(s)#{Color::RESET}"
else
@logger.error("Validación completada con errores")
puts "#{Color::RED}✗ Validación completada con #{con_errores} error(es) y #{con_advertencias} advertencia(s)#{Color::RESET}"
end
# Log de métricas
@logger.info("Validación completada", {
total_bitacoras: total,
validas: validas,
con_errores: con_errores,
con_advertencias: con_advertencias,
correcciones: validador.correcciones
})
end
def mostrar_detalles(detalles)
detalles.each do |detalle|
archivo = detalle[:archivo]
errores = detalle[:errores]
advertencias = detalle[:advertencias]
next if errores.empty? && advertencias.empty?
puts "\n#{Color::DIM}#{archivo}#{Color::RESET}"
errores.each do |error|
puts " #{Color::RED}✗ #{error}#{Color::RESET}"
end
advertencias.each do |advertencia|
puts " #{Color::YELLOW}⚠ #{advertencia}#{Color::RESET}"
end
end
end
end
# ─── Subcomando: Contexto ────────────────────────────────────────────────
class SubcomandoContexto
def initialize(args, logger)
@args = args
@logger = logger
@opciones = { cmd: false }
end
def ejecutar
if @args.empty?
@logger.error("Se requiere nombre de nodo")
puts "Uso: ./tools/adn/run contexto <nombre-nodo> [--cmd]"
exit 1
end
nodo = @args[0]
@args.shift
parser = OptionParser.new do |opts|
opts.banner = "Uso: ./tools/adn/run contexto <nombre-nodo> [OPCIONES]"
opts.separator ""
opts.separator "Obtiene contexto topológico de un nodo"
opts.separator ""
opts.separator "Opciones:"
opts.on("--cmd", "Generar comando SSH con ProxyJump si es necesario") do
@opciones[:cmd] = true
end
opts.on("-h", "--help", "Mostrar esta ayuda") do
puts opts
exit
end
end
begin
parser.parse!(@args)
rescue OptionParser::InvalidOption => e
@logger.error("Opción inválida: #{e.message}")
puts parser
exit 1
end
obtener_contexto(nodo)
end
private
def obtener_contexto(nombre_nodo)
@logger.info("Buscando contexto para nodo: #{nombre_nodo}")
ruta_nodo = File.join(ADN::NODOS_DIR, "#{nombre_nodo}.md")
unless File.exist?(ruta_nodo)
@logger.error("Nodo no encontrado: #{nombre_nodo}")
puts "#{Color::RED}✗ No se encontró ficha para el nodo '#{nombre_nodo}'#{Color::RESET}"
puts " Buscado en: #{ruta_nodo}"
exit 1
end
contenido = File.read(ruta_nodo, encoding: 'UTF-8')
# Extraer información básica
puts "#{Color::CYAN}📋 Contexto del nodo: #{nombre_nodo}#{Color::RESET}"
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
# Buscar IP en la tabla
if contenido.match(/\|\s*IP (?:LAN|Pública)?\s*\|\s*([^\|]+)\s*\|/)
ip = $1.strip
puts " #{Color::GREEN}IP:#{Color::RESET} #{ip}"
if @opciones[:cmd]
generar_comando_ssh(nombre_nodo, ip)
end
end
# Buscar rol
if contenido.match(/\|\s*Rol(?: Principal)?\s*\|\s*([^\|]+)\s*\|/)
rol = $1.strip
puts " #{Color::GREEN}Rol:#{Color::RESET} #{rol}"
end
# Buscar sistema operativo
if contenido.match(/\|\s*Sistema Operativo\s*\|\s*([^\|]+)\s*\|/)
so = $1.strip
puts " #{Color::GREEN}SO:#{Color::RESET} #{so}"
end
# Buscar en ontología para contexto de red
config_path = File.join(__dir__, 'config', 'config.yml')
ontologia_path = File.join(ADN::PROJECT_ROOT, 'adn', '01_ontologia.md') # Adjusted path
if File.exist?(ontologia_path)
ontologia = File.read(ontologia_path, encoding: 'UTF-8')
if ontologia.include?(nombre_nodo)
puts " #{Color::GREEN}En ontología:#{Color::RESET} Sí"
# Intentar encontrar en qué cluster está
clusters = ['srv-pmox1', 'srv-pmox2', 'srv-pmox3', 'srv-xen1', 'srv-dasu']
cluster = clusters.find { |c| ontologia.include?("**#{c}**") && ontologia.include?(nombre_nodo) }
if cluster
puts " #{Color::GREEN}Cluster:#{Color::RESET} #{cluster}"
# Determinar si necesita ProxyJump
if cluster != 'srv-ns8' && !nombre_nodo.start_with?('srv-')
puts " #{Color::YELLOW}🔧 Acceso probable via ProxyJump:#{Color::RESET}"
puts " ssh -J root@#{cluster} rmonla@#{nombre_nodo}"
end
end
end
end
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
@logger.exito("Contexto obtenido para nodo: #{nombre_nodo}")
end
def generar_comando_ssh(nodo, ip)
puts "\n#{Color::CYAN}🔧 Comando SSH sugerido:#{Color::RESET}"
# Determinar usuario basado en el nodo
usuario = if nodo.start_with?('srvv-') || nodo.start_with?('pcv-')
'rmonla'
elsif nodo.start_with?('srv-') && nodo != 'srv-ns8'
'root'
else
'rmonla'
end
puts " ssh #{usuario}@#{ip}"
# Si no es srv-ns8 y no es un hypervisor, sugerir usando nombre
unless nodo == 'srv-ns8' || nodo.start_with?('srv-pmox') || nodo.start_with?('srv-xen')
puts " #{Color::DIM}O usando nombre (si está en ~/.ssh/config):#{Color::RESET}"
puts " ssh #{nodo}"
end
end
end
# ─── Subcomando: Generar ────────────────────────────────────────────────
class SubcomandoGenerar
def initialize(args, logger)
@args = args
@logger = logger
end
def ejecutar
# Envolvemos todo en un manejador de eventos del sistema
ADN.eventos.publicar(:sistema_iniciado, { subcomando: 'generar' }, silencioso: true)
if @args.empty?
@logger.error("Se requiere tipo de generación")
mostrar_ayuda_generar
exit 1
end
tipo = @args[0]
@args.shift
case tipo
when 'bitacora'
generar_bitacora
when 'nodo'
generar_nodo
when 'proyecto'
generar_proyecto
else
@logger.error("Tipo de generación desconocido: #{tipo}")
mostrar_ayuda_generar
exit 1
end
end
private
def mostrar_ayuda_generar
puts "Uso: ./tools/adn/run generar <tipo> [argumentos]"
puts ""
puts "Tipos disponibles:"
puts " bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD, default: hoy)"
puts " nodo <nombre> Generar ficha de nodo"
puts " proyecto <código> Generar manifiesto de proyecto"
end
def generar_bitacora
fecha = @args[0] || Date.today.iso8601
# Validar formato de fecha
unless fecha.match?(/^\d{4}-\d{2}-\d{2}$/)
@logger.error("Formato de fecha inválido. Usar YYYY-MM-DD")
exit 1
end
ruta_bitacora = File.join(ADN::BITACORAS_DIR, "#{fecha}.md")
if File.exist?(ruta_bitacora)
@logger.advertencia("La bitácora para #{fecha} ya existe")
puts "#{Color::YELLOW}⚠ La bitácora #{fecha}.md ya existe#{Color::RESET}"
exit 0
end
# Plantilla de bitácora
plantilla = <<~PLANTILLA_BITACORA
# Bitácora #{fecha}
---
## 📋 Control de Gestión
### Pendientes
| ID | Nodo | Detalle |
| :--- | :--- | :--- |
### En Proceso
| ID | Nodo | Detalle |
| :--- | :--- | :--- |
### Resumen de Actividades
| Nodo | Resumen Integral |
| :--- | :--- |
---
## 📝 Actividades Detalladas
<!-- Agregar actividades por nodo aquí -->
---
<!-- 🤖 PREMISAS DE TRABAJO (IA): Fuente de Verdad → adn/05_ia.md -->
PLANTILLA_BITACORA
File.write(ruta_bitacora, plantilla)
@logger.exito("Bitácora creada: #{fecha}.md")
puts "#{Color::GREEN}✓ Bitácora creada en: #{ruta_bitacora}#{Color::RESET}"
puts "#{Color::DIM}Para editarla: vim #{ruta_bitacora}#{Color::RESET}"
end
def generar_nodo
if @args.empty?
@logger.error("Se requiere nombre para el nodo")
puts "Uso: ./tools/adn/run generar nodo <nombre-nodo>"
exit 1
end
nombre = @args[0].downcase # Forzar minúsculas según gobernanza
ruta_nodo = File.join(ADN::NODOS_DIR, "#{nombre}.md")
if File.exist?(ruta_nodo)
@logger.advertencia("La ficha para el nodo #{nombre} ya existe")
puts "#{Color::YELLOW}⚠ La ficha #{nombre}.md ya existe#{Color::RESET}"
exit 0
end
# Plantilla de nodo
plantilla = <<~PLANTILLA_NODO
# Ficha de Nodo: #{nombre}
| Atributo | Valor |
| :--- | :--- |
| **Hostname** | `#{nombre}` |
| **IP LAN** | `10.0.10.X` |
| **Sistema Operativo** | *Especificar* |
| **Rol** | *Describir función* |
## Especificaciones de Hardware
- **CPU**: *Especificar*
- **RAM**: *Especificar*
- **Disco**: *Especificar*
## Servicios y Responsabilidades
1. *Listar servicios críticos*
## Acceso y Mantenimiento
- **Gestión**: *SSH, Web, etc.*
- **Backup**: *Estrategia de respaldo*
- **Monitorización**: *Herramientas de monitoreo*
## Dependencias
- *Listar nodos/proyectos dependientes*
## Historial de Cambios Relevantes
- #{Date.today}: Ficha creada automáticamente
PLANTILLA_NODO
File.write(ruta_nodo, plantilla)
@logger.exito("Ficha de nodo creada: #{nombre}.md")
puts "#{Color::GREEN}✓ Ficha de nodo creada en: #{ruta_nodo}#{Color::RESET}"
end
def generar_proyecto
if @args.empty?
@logger.error("Se requiere código para el proyecto")
puts "Uso: ./tools/adn/run generar proyecto <código> [nombre]"
exit 1
end
codigo = @args[0].upcase
nombre = @args[1] || "Proyecto #{codigo}"
ruta_proyecto = File.join(ADN::PROYECTOS_DIR, "#{codigo}_#{nombre.downcase.gsub(/\s+/, '_')}.md")
if File.exist?(ruta_proyecto)
@logger.advertencia("El manifiesto para el proyecto #{codigo} ya existe")
puts "#{Color::YELLOW}⚠ El manifiesto #{codigo}_*.md ya existe#{Color::RESET}"
exit 0
end
# Plantilla de proyecto
plantilla = <<~PLANTILLA_PROYECTO
# #{codigo} - #{nombre}
**Estado:** 📍 Definido
**Fecha Inicio:** #{Date.today}
**Fecha Fin Estimada:** *Especificar*
## Objetivo
*Describir el propósito y qué problema resuelve*
## Stakeholders
| Rol | Nombre | Contacto |
| :--- | :--- | :--- |
| *Ej: Coordinador* | *Nombre* | *email/tel* |
## 🌐 Topología y Contexto de Red
*Describir ubicación de nodos, subredes, requisitos de acceso*
## Nodos Involucrados
### Nodos Exclusivos (Core)
- *Listar nodos centrales del proyecto*
### Nodos de Soporte/Infraestructura
- *Listar nodos de infraestructura compartida*
## Herramientas
- *Software, scripts y servicios utilizados*
## Hitos Clave
| ID | Hito | Estado | Fecha |
| :--- | :--- | :---: | :--- |
| 1 | *Primer hito* | 📍 | *Fecha* |
## Dashboard
*URL del panel de seguimiento web (si aplica)*
## Referencias
- [ADN del Proyecto](../adn/00_indice.md)
- *Otras referencias relevantes*
---
*Manifiesto generado automáticamente el #{Date.today}*
PLANTILLA_PROYECTO
File.write(ruta_proyecto, plantilla)
@logger.exito("Manifiesto de proyecto creado: #{codigo}")
puts "#{Color::GREEN}✓ Manifiesto creado en: #{ruta_proyecto}#{Color::RESET}"
end
end
# ─── Subcomando: Salud ──────────────────────────────────────────────────
class SubcomandoSalud
def initialize(args, logger)
@args = args
@logger = logger
@opciones = { dashboard: false, json: false }
end
def ejecutar
parser = OptionParser.new do |opts|
opts.banner = "Uso: ./tools/adn/run salud [OPCIONES]"
opts.separator ""
opts.separator "Muestra salud general del sistema ADN"
opts.separator ""
opts.separator "Opciones:"
opts.on("--dashboard", "Mostrar en formato dashboard (no implementado)") do
@opciones[:dashboard] = true
end
opts.on("--json", "Salida en formato JSON") do
@opciones[:json] = true
end
opts.on("-h", "--help", "Mostrar esta ayuda") do
puts opts
exit
end
end
begin
parser.parse!(@args)
rescue OptionParser::InvalidOption => e
@logger.error("Opción inválida: #{e.message}")
puts parser
exit 1
end
mostrar_salud
end
private
def mostrar_salud
@logger.info("Generando reporte de salud del ADN")
datos = {
timestamp: Time.now.utc.iso8601,
hebras_adn: contar_hebras_adn,
bitacoras: contar_bitacoras,
nodos: contar_nodos,
proyectos: contar_proyectos,
errores_recientes: buscar_errores_recientes
}
if @opciones[:json]
puts JSON.pretty_generate(datos)
return
end
if @opciones[:dashboard]
puts "#{Color::YELLOW}⚠ El dashboard será implementado en Fase 3#{Color::RESET}"
# TODO: Implementar dashboard en Fase 3
end
# Mostrar en formato legible
puts "#{Color::CYAN}🧬 Salud del Sistema ADN#{Color::RESET}"
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
puts " #{Color::GREEN}Hebras ADN:#{Color::RESET} #{datos[:hebras_adn]} / 7"
puts " #{Color::GREEN}Bitácoras:#{Color::RESET} #{datos[:bitacoras]}"
puts " #{Color::GREEN}Nodos registrados:#{Color::RESET} #{datos[:nodos]}"
puts " #{Color::GREEN}Proyectos activos:#{Color::RESET} #{datos[:proyectos]}"
if datos[:errores_recientes] > 0
puts " #{Color::RED}Errores recientes:#{Color::RESET} #{datos[:errores_recientes]}"
else
puts " #{Color::GREEN}Errores recientes:#{Color::RESET} 0"
end
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
puts "#{Color::DIM}Para más detalles: ./tools/adn/run salud --json#{Color::RESET}"
@logger.exito("Reporte de salud generado")
end
def contar_hebras_adn
Dir.glob(File.join(ADN::PROJECT_ROOT, 'adn', "*.md")).count
end
def contar_bitacoras
Dir.glob(File.join(ADN::BITACORAS_DIR, "*.md")).count
end
def contar_nodos
Dir.glob(File.join(ADN::NODOS_DIR, "*.md")).count
end
def contar_proyectos
Dir.glob(File.join(ADN::PROYECTOS_DIR, "*.md")).count
end
def buscar_errores_recientes
# Buscar en logs de hoy
log_hoy = File.join(ADN::LOGS_DIR, "#{Date.today}.log")
return 0 unless File.exist?(log_hoy)
errores = 0
File.foreach(log_hoy) do |linea|
begin
entrada = JSON.parse(linea)
errores += 1 if entrada['nivel'] == 'ERROR'
rescue JSON::ParserError
next
end
end
errores
end
end
# ─── Subcomando: Ayuda ──────────────────────────────────────────────────
class SubcomandoAyuda
def initialize(args, logger)
@args = args
@logger = logger
end
def ejecutar
if @args.empty?
mostrar_ayuda_principal
else
mostrar_ayuda_subcomando(@args[0])
end
end
private
def mostrar_ayuda_principal
puts <<~AYUDA_PRINCIPAL
#{Color::CYAN}🔧 Herramienta ADN - Interfaz de línea de comandos unificada#{Color::RESET}
#{Color::DIM}Versión: Fase 1 (Núcleo Autónomo)#{Color::RESET}
#{Color::GREEN}Uso:#{Color::RESET} ./tools/adn/run <subcomando> [opciones] [argumentos]
#{Color::YELLOW}Subcomandos disponibles:#{Color::RESET}
validador [--watch] [--fix] Validar cumplimiento del ADN en bitácoras
contexto <nodo> [--cmd] Obtener contexto topológico de un nodo
salud [--dashboard] [--json] Ver salud general del sistema ADN
generar bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD)
generar nodo <nombre> Generar ficha de nodo
#{Color::YELLOW}generar proyecto <código>#{Color::RESET} Generar manifiesto de proyecto
#{Color::YELLOW}backup <nodo> [--mode MODO]#{Color::RESET} Ejecutar backup seguro de un nodo
#{Color::YELLOW}sync [ruta|fecha]#{Color::RESET} Sincronizar con API dtic-BITACORAs
#{Color::YELLOW}triggers [opciones]#{Color::RESET} Gestión del motor de triggers
#{Color::YELLOW}ayuda [subcomando]#{Color::RESET} Mostrar esta ayuda o ayuda específica
#{Color::YELLOW}Subcomandos en desarrollo (Fases futuras):#{Color::RESET}
evolucion <propuesta.md> Gestionar propuesta de evolución del ADN
triggers [--test] [--force] Ejecutar triggers de sincronización
#{Color::DIM}Para ayuda detallada de un subcomando:#{Color::RESET}
./tools/adn/run ayuda <subcomando>
#{Color::DIM}Documentación completa:#{Color::RESET} docs/plan/260307-1400_Plan_Mejoras_ADN.md
AYUDA_PRINCIPAL
end
def mostrar_ayuda_subcomando(subcomando)
case subcomando.downcase
when 'validador'
puts "Uso: ./tools/adn/run validador [--watch] [--fix]"
puts ""
puts "Valida el cumplimiento del ADN en las bitácoras:"
puts " • Formato de tiempo (HH:MM, sin \"hs\")"
puts " • Iconografía de estados y semántica correcta"
puts " • Presencia de métricas híbridas ([Físico/Remoto:])"
puts " • Referencias bidireccionales en rollovers"
puts " • Estructura de tablas en bitácoras"
puts ""
puts "Opciones:"
puts " --watch Monitorizar cambios en tiempo real (Fase 1 completa)"
puts " --fix Intentar corregir errores automáticamente"
puts ""
puts "Ejemplo:"
puts " ./tools/adn/run validador"
puts " ./tools/adn/run validador --fix"
when 'contexto'
puts "Uso: ./tools/adn/run contexto <nombre-nodo> [--cmd]"
puts ""
puts "Obtiene información contextual de un nodo:"
puts " • IP y configuración de red"
puts " • Rol en la topología"
puts " • Comandos SSH sugeridos"
puts ""
puts "Ejemplo:"
puts " ./tools/adn/run contexto srv-pmox1"
puts " ./tools/adn/run contexto srvv-fenix --cmd"
when 'salud'
puts "Uso: ./tools/adn/run salud [--dashboard] [--json]"
puts ""
puts "Muestra métricas de salud del sistema ADN:"
puts " • Cantidad de hebras, bitácoras, nodos, proyectos"
puts " • Errores recientes en logs"
puts ""
puts "Ejemplo:"
puts " ./tools/adn/run salud"
puts " ./tools/adn/run salud --json"
when 'generar'
puts "Uso: ./tools/adn/run generar <tipo> [argumentos]"
puts ""
puts "Tipos disponibles:"
puts " bitacora [fecha] - Crea nueva bitácora (fecha: YYYY-MM-DD, default: hoy)"
puts " nodo <nombre> - Crea ficha de nodo"
puts " proyecto <código> - Crea manifiesto de proyecto"
puts ""
puts "Ejemplos:"
puts " ./tools/adn/run generar bitacora"
puts " ./tools/adn/run generar bitacora 2026-03-08"
puts " ./tools/adn/run generar nodo srv-nuevo"
puts " ./tools/adn/run generar proyecto P2604"
else
puts "#{Color::RED}✗ Subcomando desconocido: #{subcomando}#{Color::RESET}"
puts "Usa './tools/adn/run ayuda' para ver la lista de subcomandos."
end
end
end
# ─── CLI Principal ──────────────────────────────────────────────────────
class ADNCLI
def initialize
@logger = ADNLogger.new
end
def ejecutar
if ARGV.empty?
SubcomandoAyuda.new([], @logger).ejecutar
exit 0
end
subcomando = ARGV[0]
args = ARGV[1..-1] || []
case subcomando
when 'validador'
SubcomandoValidador.new(args, @logger).ejecutar
when 'contexto'
SubcomandoContexto.new(args, @logger).ejecutar
when 'salud'
SubcomandoSalud.new(args, @logger).ejecutar
when 'generar'
SubcomandoGenerar.new(args, @logger).ejecutar
when 'db'
ADN::SubcomandoDB.new(args, @logger).ejecutar
when 'backup'
ADN::SubcomandoBackup.new(args, @logger).ejecutar
when 'sync'
ADN::SubcomandoSync.new(args, @logger).ejecutar
when 'triggers'
ADN::SubcomandoTriggers.new(args, @logger).ejecutar
when 'ayuda', 'help', '-h', '--help'
SubcomandoAyuda.new(args, @logger).ejecutar
when 'evolucion'
@logger.info("Subcomando 'evolucion' será implementado en Fase 2")
puts "#{Color::YELLOW}⚠ El subcomando 'evolucion' será implementado en Fase 2#{Color::RESET}"
puts " Ver plan: docs/plan/260307-1400_Plan_Mejoras_ADN.md"
when 'triggers'
ADN::SubcomandoTriggers.new(args, @logger).ejecutar
when 'plan'
ADN::SubcomandoPlan.new(args, @logger).ejecutar
else
@logger.error("Subcomando desconocido: #{subcomando}")
puts "#{Color::RED}✗ Subcomando desconocido: '#{subcomando}'#{Color::RESET}"
puts ""
SubcomandoAyuda.new([], @logger).ejecutar
exit 1
end
rescue Interrupt
puts "\n#{Color::YELLOW}⚠ Interrumpido por el usuario#{Color::RESET}"
@logger.info("Ejecución interrumpida por usuario")
exit 130
rescue => e
@logger.error("Error inesperado: #{e.message}", { backtrace: e.backtrace })
puts "#{Color::RED}✗ Error inesperado: #{e.message}#{Color::RESET}"
puts "#{Color::DIM}#{e.backtrace.first}#{Color::RESET}" if ENV['ADN_DEBUG']
exit 1
end
end
# ─── Punto de entrada ───────────────────────────────────────────────────
if __FILE__ == $0
ADNCLI.new.ejecutar
end