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

- Reestructuración del CLI en módulos discretos (core/, comando/, config/, clientes/)
- Migración del punto de entrada tools/adn.rb hacia el launcher ejecutable tools/adn/run
- Simplificación de nombres de comandos (remoción prefijo subcomando_)
- Corrección de rutas base transversales (PROJECT_ROOT, BITACORAS_DIR)
- Reescritura integral de sentencias require_relative
- Restauración de sintaxis en archivo sync.rb truncado
This commit is contained in:
Ricardo Monla
2026-03-07 09:45:21 -03:00
parent 39e3e130e6
commit c4c761e408
13 changed files with 365 additions and 75 deletions
+225
View File
@@ -0,0 +1,225 @@
# frozen_string_literal: true
# tools/adn/logger.rb — Logger estructurado JSON para el sistema ADN
# ====================================================================
# Este módulo proporciona logging estructurado en JSON para todas las
# herramientas del sistema ADN. Los logs se almacenan en `logs/adn/`
# con rotación diaria automática.
require 'json'
require 'fileutils'
require 'date'
require 'time'
module ADN
class Logger
# 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
# Inicializa el logger
#
# @param opts [Hash] Opciones de configuración
# @option opts [String] :directorio Directorio para logs (default: logs/adn/)
# @option opts [Boolean] :terminal Mostrar logs en terminal (default: true)
# @option opts [Boolean] :colores Usar colores en terminal (default: true)
# @option opts [String] :archivo Nombre base del archivo de log (default: fecha actual)
def initialize(opts = {})
@directorio = opts[:directorio] || File.join(File.expand_path('../../..', __dir__), 'logs', 'adn')
@mostrar_terminal = opts.fetch(:terminal, true)
@usar_colores = opts.fetch(:colores, true)
@archivo_base = opts[:archivo] || Date.today.iso8601
# Crear directorio si no existe
FileUtils.mkdir_p(@directorio) unless Dir.exist?(@directorio)
# Ruta completa del archivo de log
@ruta_log = File.join(@directorio, "#{@archivo_base}.log")
# Buffer para logs (útil para pruebas o modo batch)
@buffer = []
end
# Registra un mensaje de nivel DEBUG
#
# @param mensaje [String] Mensaje descriptivo
# @param datos [Hash] Datos adicionales estructurados
# @param opts [Hash] Opciones adicionales
# @option opts [Boolean] :silencioso No mostrar en terminal
def debug(mensaje, datos = {}, opts = {})
escribir('DEBUG', mensaje, datos, opts)
mostrar_terminal('🐛', mensaje, Color::DIM, opts) if debug_activado?
end
# Registra un mensaje de nivel INFO
#
# @param mensaje [String] Mensaje descriptivo
# @param datos [Hash] Datos adicionales estructurados
# @param opts [Hash] Opciones adicionales
# @option opts [Boolean] :silencioso No mostrar en terminal
def info(mensaje, datos = {}, opts = {})
escribir('INFO', mensaje, datos, opts)
mostrar_terminal('', mensaje, Color::CYAN, opts)
end
# Registra un mensaje de nivel ÉXITO
#
# @param mensaje [String] Mensaje descriptivo
# @param datos [Hash] Datos adicionales estructurados
# @param opts [Hash] Opciones adicionales
# @option opts [Boolean] :silencioso No mostrar en terminal
def exito(mensaje, datos = {}, opts = {})
escribir('EXITO', mensaje, datos, opts)
mostrar_terminal('✓', mensaje, Color::GREEN, opts)
end
# Registra un mensaje de nivel ADVERTENCIA
#
# @param mensaje [String] Mensaje descriptivo
# @param datos [Hash] Datos adicionales estructurados
# @param opts [Hash] Opciones adicionales
# @option opts [Boolean] :silencioso No mostrar en terminal
def advertencia(mensaje, datos = {}, opts = {})
escribir('ADVERTENCIA', mensaje, datos, opts)
mostrar_terminal('⚠', mensaje, Color::YELLOW, opts)
end
# Alias inglés para advertencia
#
# @param mensaje [String] Mensaje descriptivo
# @param datos [Hash] Datos adicionales estructurados
# @param opts [Hash] Opciones adicionales
# @option opts [Boolean] :silencioso No mostrar en terminal
def warn(mensaje, datos = {}, opts = {})
advertencia(mensaje, datos, opts)
end
# Registra un mensaje de nivel ERROR
#
# @param mensaje [String] Mensaje descriptivo
# @param datos [Hash] Datos adicionales estructurados
# @param opts [Hash] Opciones adicionales
# @option opts [Boolean] :silencioso No mostrar en terminal
def error(mensaje, datos = {}, opts = {})
escribir('ERROR', mensaje, datos, opts)
mostrar_terminal('✗', mensaje, Color::RED, opts)
end
# Obtiene los logs almacenados en buffer
#
# @return [Array<Hash>] Logs en buffer
def buffer
@buffer.dup
end
# Limpia el buffer interno
def limpiar_buffer
@buffer.clear
end
# Cierra cualquier recurso del logger (archivos abiertos, etc.)
def cerrar
# Por ahora no hay recursos persistentes abiertos
true
end
# Verifica si el debug está activado (variable de entorno ADN_DEBUG)
#
# @return [Boolean]
def debug_activado?
ENV['ADN_DEBUG'] == '1' || ENV['ADN_DEBUG'] == 'true'
end
# Ruta actual del archivo de log
#
# @return [String]
def ruta_log
@ruta_log
end
private
# Escribe una entrada de log en formato JSON
#
# @param nivel [String] Nivel del log
# @param mensaje [String] Mensaje descriptivo
# @param datos [Hash] Datos adicionales estructurados
# @param opts [Hash] Opciones adicionales
def escribir(nivel, mensaje, datos, opts)
entrada = {
timestamp: Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S%z'),
nivel: nivel,
mensaje: mensaje,
datos: datos.reject { |k, _| k == :silencioso }, # Excluir opciones internas
herramienta: opts[:herramienta] || determinar_herramienta_llamante
}
# Agregar al buffer
@buffer << entrada.dup
# Escribir al archivo
File.open(@ruta_log, 'a') do |archivo|
archivo.puts(entrada.to_json)
end
end
# Muestra el mensaje en terminal (si está habilitado)
#
# @param icono [String] Icono para representar el nivel
# @param mensaje [String] Mensaje a mostrar
# @param color [String] Código ANSI de color
# @param opts [Hash] Opciones adicionales
def mostrar_terminal(icono, mensaje, color, opts)
return unless @mostrar_terminal && !opts[:silencioso]
if @usar_colores
puts "#{color}#{icono} #{mensaje}#{Color::RESET}"
else
puts "#{icono} #{mensaje}"
end
end
# Intenta determinar qué herramienta llamó al logger
#
# @return [String] Nombre de la herramienta
def determinar_herramienta_llamante
# Obtener el backtrace y buscar llamadas desde tools/adn/
caller_locations.each do |loc|
ruta = loc.absolute_path || loc.path
if ruta.include?('tools/adn')
# Extraer nombre del archivo sin extensión
nombre = File.basename(ruta, '.*')
return "adn.#{nombre}" unless nombre.empty?
end
end
'adn.desconocido'
end
end
# Logger global preconfigurado para uso rápido
#
# @return [ADN::Logger]
def self.logger
@logger ||= Logger.new
end
# Configura el logger global
#
# @param opts [Hash] Opciones de configuración
# @return [ADN::Logger]
def self.configurar_logger(opts = {})
@logger = Logger.new(opts)
end
end