#!/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 [opciones] [argumentos] # # Subcomandos disponibles: # validador [--watch] [--fix] Validar cumplimiento del ADN en bitácoras # contexto [--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 Generar ficha de nodo # generar proyecto Generar manifiesto de proyecto # conocimiento asimilar Asimilar documentación al ADN # evolucion 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, 'docs', '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' require_relative 'comando/conocimiento' require_relative 'comando/cierre' require_relative 'comando/inicio' require_relative 'core/conciliador' # ─── 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 [--cmd]" exit 1 end nodo = @args[0] @args.shift parser = OptionParser.new do |opts| opts.banner = "Uso: ./tools/adn/run contexto [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 [argumentos]" puts "" puts "Tipos disponibles:" puts " bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD, default: hoy)" puts " nodo Generar ficha de nodo" puts " proyecto 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 --- 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 " 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 [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 [opciones] [argumentos] #{Color::YELLOW}Subcomandos disponibles:#{Color::RESET} validador [--watch] [--fix] Validar cumplimiento del ADN en bitácoras contexto [--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 Generar ficha de nodo #{Color::YELLOW}generar proyecto #{Color::RESET} Generar manifiesto de proyecto #{Color::YELLOW}backup [--mode MODO]#{Color::RESET} Ejecutar backup seguro de un nodo #{Color::YELLOW}sync [ruta|fecha]#{Color::RESET} Sincronizar con API dtic-BITACORAs #{Color::YELLOW}inicio de jornada [presencial|remoto]#{Color::RESET} Inicio de jornada laboral #{Color::YELLOW}cierre de jornada [presencial|remoto]#{Color::RESET} Cierre de jornada laboral #{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 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 #{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 [--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 [argumentos]" puts "" puts "Tipos disponibles:" puts " bitacora [fecha] - Crea nueva bitácora (fecha: YYYY-MM-DD, default: hoy)" puts " nodo - Crea ficha de nodo" puts " proyecto - 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] # Saneamiento preventivo de pendientes (con cooldown de 5 minutos para performance) ejecutar_saneamiento_preventivo(subcomando) 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 'conocimiento' ADN::SubcomandoConocimiento.new(args, @logger).ejecutar when 'db' ADN::SubcomandoDB.new(args, @logger).ejecutar when 'backup', 'backups' ADN::SubcomandoBackup.new(args, @logger).ejecutar when 'sync' ADN::SubcomandoSync.new(args, @logger).ejecutar when 'triggers' ADN::SubcomandoTriggers.new(args, @logger).ejecutar when 'cierre' ADN::SubcomandoCierre.new(args, @logger).ejecutar when 'inicio' ADN::SubcomandoInicio.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 private def ejecutar_saneamiento_preventivo(subcomando) # No sanear si es ayuda o db (para evitar bucles o lentitud en info rápida) return if ['ayuda', 'help', 'db', 'salud', '-h', '--help'].include?(subcomando) archivo_cooldown = "/tmp/adn_sanity_cooldown" cooldown_segundo = 300 # 5 minutos if File.exist?(archivo_cooldown) last_run = File.mtime(archivo_cooldown) return if (Time.now - last_run) < cooldown_segundo end # Ejecutar conciliación begin # Tocamos el archivo primero para evitar que otras instancias concurrentes lo lancen FileUtils.touch(archivo_cooldown) ADN::Conciliador.new(@logger).ejecutar(silencioso: true) rescue => e @logger.advertencia("No se pudo completar el saneamiento preventivo: #{e.message}") end end end # ─── Punto de entrada ─────────────────────────────────────────────────── if __FILE__ == $0 ADNCLI.new.ejecutar end