887 lines
29 KiB
Ruby
Executable File
887 lines
29 KiB
Ruby
Executable File
#!/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
|
|
# conocimiento asimilar <archivo> Asimilar documentación al ADN
|
|
# 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, '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 <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}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 <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]
|
|
|
|
# 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
|