From 605d36cac472f5ee4684926b7b3e00ac90877793 Mon Sep 17 00:00:00 2001 From: Ricardo Monla Date: Tue, 10 Mar 2026 18:18:32 -0300 Subject: [PATCH] =?UTF-8?q?Refactor(ADN):=20Atomizaci=C3=B3n=20del=20monol?= =?UTF-8?q?ito=20adn/run=20y=20centralizaci=C3=B3n=20de=20la=20clase=20Col?= =?UTF-8?q?or?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/adn/comando/ayuda.rb | 111 +++++ tools/adn/comando/cierre.rb | 10 +- tools/adn/comando/contexto.rb | 139 +++++++ tools/adn/comando/generar.rb | 230 +++++++++++ tools/adn/comando/inicio.rb | 10 +- tools/adn/comando/plan.rb | 12 +- tools/adn/comando/salud.rb | 121 ++++++ tools/adn/comando/validador.rb | 128 ++++++ tools/adn/core/colores.rb | 16 + tools/adn/core/logger.rb | 12 +- tools/adn/run | 729 +-------------------------------- 11 files changed, 755 insertions(+), 763 deletions(-) create mode 100644 tools/adn/comando/ayuda.rb create mode 100644 tools/adn/comando/contexto.rb create mode 100644 tools/adn/comando/generar.rb create mode 100644 tools/adn/comando/salud.rb create mode 100644 tools/adn/comando/validador.rb create mode 100644 tools/adn/core/colores.rb diff --git a/tools/adn/comando/ayuda.rb b/tools/adn/comando/ayuda.rb new file mode 100644 index 00000000..f7588ab7 --- /dev/null +++ b/tools/adn/comando/ayuda.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require_relative '../core/colores' + +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 diff --git a/tools/adn/comando/cierre.rb b/tools/adn/comando/cierre.rb index e27ac172..b4944e65 100644 --- a/tools/adn/comando/cierre.rb +++ b/tools/adn/comando/cierre.rb @@ -9,15 +9,7 @@ require_relative '../../bitacoras-db/core/bitacora_db' module ADN class SubcomandoCierre - module Color - RESET = "\e[0m" - BOLD = "\e[1m" - GREEN = "\e[32m" - CYAN = "\e[36m" - YELLOW = "\e[33m" - RED = "\e[31m" - DIM = "\e[2m" - end + require_relative '../core/colores' def initialize(args, logger) @args = args diff --git a/tools/adn/comando/contexto.rb b/tools/adn/comando/contexto.rb new file mode 100644 index 00000000..b23ed46a --- /dev/null +++ b/tools/adn/comando/contexto.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +require 'optparse' +require_relative '../core/colores' + +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') + 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 diff --git a/tools/adn/comando/generar.rb b/tools/adn/comando/generar.rb new file mode 100644 index 00000000..7ebb58f0 --- /dev/null +++ b/tools/adn/comando/generar.rb @@ -0,0 +1,230 @@ +# frozen_string_literal: true + +require 'date' +require_relative '../core/colores' + +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 diff --git a/tools/adn/comando/inicio.rb b/tools/adn/comando/inicio.rb index 3f5c21ee..a3abb35b 100644 --- a/tools/adn/comando/inicio.rb +++ b/tools/adn/comando/inicio.rb @@ -9,15 +9,7 @@ require_relative '../../bitacoras-db/core/bitacora_db' module ADN class SubcomandoInicio - module Color - RESET = "\e[0m" - BOLD = "\e[1m" - GREEN = "\e[32m" - CYAN = "\e[36m" - YELLOW = "\e[33m" - RED = "\e[31m" - DIM = "\e[2m" - end + require_relative '../core/colores' def initialize(args, logger) @args = args diff --git a/tools/adn/comando/plan.rb b/tools/adn/comando/plan.rb index bbd8807c..3060a2fd 100644 --- a/tools/adn/comando/plan.rb +++ b/tools/adn/comando/plan.rb @@ -39,17 +39,7 @@ end module ADN class SubcomandoPlan - # Colores ANSI para terminal (consistentes con adn.rb) - 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 + require_relative '../core/colores' # Ruta por defecto del archivo de plan DEFAULT_PLAN_PATH = File.expand_path('../../docs/plan/260308-1200_Plan_Integral_ADN_BITACORAs.md', __dir__) diff --git a/tools/adn/comando/salud.rb b/tools/adn/comando/salud.rb new file mode 100644 index 00000000..756c528e --- /dev/null +++ b/tools/adn/comando/salud.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'optparse' +require 'json' +require_relative '../core/colores' + +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 diff --git a/tools/adn/comando/validador.rb b/tools/adn/comando/validador.rb new file mode 100644 index 00000000..61aeeb1b --- /dev/null +++ b/tools/adn/comando/validador.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require 'optparse' +require_relative '../core/colores' +require_relative '../core/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 diff --git a/tools/adn/core/colores.rb b/tools/adn/core/colores.rb new file mode 100644 index 00000000..242d7bcd --- /dev/null +++ b/tools/adn/core/colores.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# tools/adn/core/colores.rb — ADN Tools Colors utility +# ==================================================================== +# Consolidación de módulos de Color (Menos es Más) para uso en CLI. + +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 diff --git a/tools/adn/core/logger.rb b/tools/adn/core/logger.rb index 5535aba4..f784979d 100644 --- a/tools/adn/core/logger.rb +++ b/tools/adn/core/logger.rb @@ -16,17 +16,7 @@ module ADN # Niveles de log soportados NIVELES = %w[DEBUG INFO EXITO ADVERTENCIA ERROR].freeze - # Colores ANSI para terminal (opcional) - 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 + require_relative 'colores' # Inicializa el logger # diff --git a/tools/adn/run b/tools/adn/run index 27d6aef0..4f45ebcf 100755 --- a/tools/adn/run +++ b/tools/adn/run @@ -57,734 +57,17 @@ 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 +require_relative 'core/colores' # ─── 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 +require_relative 'comando/validador' +require_relative 'comando/contexto' +require_relative 'comando/generar' +require_relative 'comando/salud' +require_relative 'comando/ayuda' # ─── CLI Principal ────────────────────────────────────────────────────── class ADNCLI