- 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
283 lines
10 KiB
Ruby
283 lines
10 KiB
Ruby
|
|
# frozen_string_literal: true
|
|
|
|
# tools/adn/subcomando_sync.rb — Subcomando para sincronizar archivos MD con API dtic-BITACORAs
|
|
# =============================================================================================
|
|
# Este subcomando permite sincronizar bitácoras en formato Markdown con la base de datos
|
|
# de dtic-BITACORAs mediante la API REST.
|
|
|
|
require_relative '../clientes/dtic_bitacoras_api'
|
|
require 'optparse'
|
|
require 'fileutils'
|
|
|
|
module ADN
|
|
class SubcomandoSync
|
|
def initialize(args, logger)
|
|
@args = args
|
|
@logger = logger
|
|
@opciones = {
|
|
sobrescribir: false,
|
|
validar_primero: true,
|
|
batch: false,
|
|
fecha: nil,
|
|
ruta: nil
|
|
}
|
|
@resultados = {
|
|
archivos_procesados: 0,
|
|
archivos_exitosos: 0,
|
|
archivos_fallidos: 0,
|
|
entradas_sincronizadas: 0,
|
|
errores: []
|
|
}
|
|
end
|
|
|
|
def ejecutar
|
|
parser = OptionParser.new do |opts|
|
|
opts.banner = "Uso: ./tools/adn/run sync [OPCIONES] [RUTA_O_FECHA]"
|
|
opts.separator ""
|
|
opts.separator "Sincroniza archivos Markdown con la API de dtic-BITACORAs"
|
|
opts.separator ""
|
|
opts.separator "Opciones:"
|
|
opts.on("--sobrescribir", "Sobrescribir entradas existentes en la base de datos") do
|
|
@opciones[:sobrescribir] = true
|
|
end
|
|
opts.on("--no-validar", "No validar archivos antes de sincronizar") do
|
|
@opciones[:validar_primero] = false
|
|
end
|
|
opts.on("--batch", "Sincronizar todos los archivos en el directorio bitacoras/") do
|
|
@opciones[:batch] = true
|
|
end
|
|
opts.on("--fecha FECHA", "Sincronizar bitácora específica (formato: YYYY-MM-DD)") do |f|
|
|
@opciones[:fecha] = f
|
|
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
|
|
|
|
# Determinar qué sincronizar
|
|
if @args.any? && !@opciones[:batch] && !@opciones[:fecha]
|
|
@opciones[:ruta] = @args[0]
|
|
end
|
|
|
|
ejecutar_sincronizacion
|
|
end
|
|
|
|
private
|
|
|
|
def ejecutar_sincronizacion
|
|
@logger.info("Iniciando sincronización con API dtic-BITACORAs", @opciones)
|
|
|
|
# Inicializar cliente API
|
|
begin
|
|
@client = DticBitacorasApi::Client.new(logger: @logger)
|
|
salud = @client.salud
|
|
@logger.exito("API dtic-BITACORAs disponible: #{salud['status']}")
|
|
rescue DticBitacorasApi::ApiError => e
|
|
@logger.error("No se puede conectar a la API: #{e.message}")
|
|
mostrar_conexion_fallback
|
|
exit 1
|
|
rescue DticBitacorasApi::ConfigError => e
|
|
@logger.error("Error de configuración: #{e.message}")
|
|
puts "Configura la URL de la API en tools/adn/config.yml o con export DTIC_API_URL=http://localhost:3002"
|
|
exit 1
|
|
end
|
|
|
|
# Determinar archivos a sincronizar
|
|
archivos = determinar_archivos_a_sincronizar
|
|
|
|
if archivos.empty?
|
|
@logger.advertencia("No se encontraron archivos para sincronizar")
|
|
mostrar_estadisticas
|
|
exit 0
|
|
end
|
|
|
|
puts "\n#{Color::CYAN}🔄 Sincronizando #{archivos.size} archivo(s) con dtic-BITACORAs#{Color::RESET}"
|
|
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
|
|
|
|
# Procesar cada archivo
|
|
archivos.each_with_index do |archivo, index|
|
|
procesar_archivo(archivo, index + 1, archivos.size)
|
|
end
|
|
|
|
mostrar_estadisticas
|
|
end
|
|
|
|
def determinar_archivos_a_sincronizar
|
|
archivos = []
|
|
|
|
if @opciones[:batch]
|
|
# Sincronizar todos los archivos en bitacoras/
|
|
patron = File.join(ADN::BITACORAS_DIR, "*.md")
|
|
archivos = Dir.glob(patron).sort
|
|
elsif @opciones[:fecha]
|
|
# Sincronizar archivo específico por fecha
|
|
ruta = File.join(ADN::BITACORAS_DIR, "#{@opciones[:fecha]}.md")
|
|
archivos = File.exist?(ruta) ? [ruta] : []
|
|
elsif @opciones[:ruta]
|
|
# Sincronizar archivo específico por ruta
|
|
archivos = [File.expand_path(@opciones[:ruta])]
|
|
else
|
|
# Sincronizar archivo del día actual
|
|
fecha_hoy = Date.today.strftime('%Y-%m-%d')
|
|
ruta_hoy = File.join(ADN::BITACORAS_DIR, "#{fecha_hoy}.md")
|
|
archivos = File.exist?(ruta_hoy) ? [ruta_hoy] : []
|
|
end
|
|
|
|
archivos
|
|
end
|
|
|
|
def procesar_archivo(ruta_archivo, indice, total)
|
|
nombre = File.basename(ruta_archivo)
|
|
puts "\n#{Color::CYAN}[#{indice}/#{total}] #{nombre}#{Color::RESET}"
|
|
|
|
unless File.exist?(ruta_archivo)
|
|
@resultados[:errores] << "#{nombre}: Archivo no encontrado"
|
|
@resultados[:archivos_fallidos] += 1
|
|
puts " #{Color::RED}✗ Archivo no encontrado#{Color::RESET}"
|
|
return
|
|
end
|
|
|
|
# Extraer fecha del nombre del archivo
|
|
fecha_match = nombre.match(/(\d{4}-\d{2}-\d{2})\.md/)
|
|
unless fecha_match
|
|
@resultados[:errores] << "#{nombre}: Formato de nombre inválido (debe ser YYYY-MM-DD.md)"
|
|
@resultados[:archivos_fallidos] += 1
|
|
puts " #{Color::RED}✗ Formato de nombre inválido#{Color::RESET}"
|
|
return
|
|
end
|
|
|
|
fecha = fecha_match[1]
|
|
|
|
# Validar formato del archivo si está habilitado
|
|
if @opciones[:validar_primero]
|
|
unless validar_archivo(ruta_archivo)
|
|
@resultados[:errores] << "#{nombre}: Falló validación previa"
|
|
@resultados[:archivos_fallidos] += 1
|
|
return
|
|
end
|
|
end
|
|
|
|
# Sincronizar con la API
|
|
begin
|
|
resultado = @client.sincronizar_md(ruta_archivo, sobrescribir: @opciones[:sobrescribir])
|
|
|
|
@resultados[:archivos_procesados] += 1
|
|
@resultados[:archivos_exitosos] += 1
|
|
@resultados[:entradas_sincronizadas] += resultado[:entradas_procesadas]
|
|
|
|
puts " #{Color::GREEN}✓ Sincronizado exitosamente#{Color::RESET}"
|
|
puts " #{Color::DIM}Bitácora ID: #{resultado[:bitacora_id]}#{Color::RESET}"
|
|
puts " #{Color::DIM}Entradas: #{resultado[:entradas_procesadas]}#{Color::RESET}"
|
|
|
|
@logger.info("Archivo sincronizado: #{nombre}", resultado)
|
|
rescue DticBitacorasApi::ApiError => e
|
|
@resultados[:archivos_procesados] += 1
|
|
@resultados[:archivos_fallidos] += 1
|
|
@resultados[:errores] << "#{nombre}: #{e.message}"
|
|
|
|
puts " #{Color::RED}✗ Error de API: #{e.message}#{Color::RESET}"
|
|
@logger.error("Error sincronizando #{nombre}: #{e.message}")
|
|
rescue => e
|
|
@resultados[:archivos_procesados] += 1
|
|
@resultados[:archivos_fallidos] += 1
|
|
@resultados[:errores] << "#{nombre}: Error inesperado - #{e.message}"
|
|
|
|
puts " #{Color::RED}✗ Error inesperado: #{e.message}#{Color::RESET}"
|
|
@logger.error("Error inesperado en #{nombre}: #{e.message}", backtrace: e.backtrace)
|
|
end
|
|
end
|
|
|
|
def validar_archivo(ruta_archivo)
|
|
puts " #{Color::CYAN}🔍 Validando formato...#{Color::RESET}"
|
|
|
|
contenido = File.read(ruta_archivo, encoding: 'UTF-8')
|
|
errores_validacion = []
|
|
|
|
# Validar formato básico
|
|
unless contenido.include?('## 📝 Actividades Detalladas')
|
|
errores_validacion << "Falta sección '## 📝 Actividades Detalladas'"
|
|
end
|
|
|
|
# Validar que tenga al menos una tabla de entradas
|
|
unless contenido.match(/\|\s*[I]\s*\|\s*[F]\s*\|\s*Descripción\s*\|\s*[E]\s*\|/)
|
|
errores_validacion << "No se encontró tabla de entradas I-F-D-E"
|
|
end
|
|
|
|
# Validar formato de tiempo
|
|
if contenido.match(/\d{1,2}:\d{2}\s*hs/)
|
|
errores_validacion << "Formato de tiempo incorrecto (contiene 'hs')"
|
|
end
|
|
|
|
if errores_validacion.empty?
|
|
puts " #{Color::GREEN}✓ Validación exitosa#{Color::RESET}"
|
|
true
|
|
else
|
|
puts " #{Color::YELLOW}⚠ Advertencias de validación:#{Color::RESET}"
|
|
errores_validacion.each { |e| puts " - #{e}" }
|
|
puts " #{Color::DIM}Continuando con sincronización...#{Color::RESET}"
|
|
true # Continuar a pesar de advertencias
|
|
end
|
|
end
|
|
|
|
def mostrar_estadisticas
|
|
puts "\n#{Color::CYAN}📊 Resultados de sincronización#{Color::RESET}"
|
|
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
|
|
|
|
puts " #{Color::GREEN}Archivos procesados:#{Color::RESET} #{@resultados[:archivos_procesados]}"
|
|
puts " #{Color::GREEN}Archivos exitosos:#{Color::RESET} #{@resultados[:archivos_exitosos]}"
|
|
|
|
if @resultados[:archivos_fallidos] > 0
|
|
puts " #{Color::RED}Archivos fallidos:#{Color::RESET} #{@resultados[:archivos_fallidos]}"
|
|
else
|
|
puts " #{Color::GREEN}Archivos fallidos:#{Color::RESET} 0"
|
|
end
|
|
|
|
puts " #{Color::GREEN}Entradas sincronizadas:#{Color::RESET} #{@resultados[:entradas_sincronizadas]}"
|
|
|
|
# Mostrar estadísticas de la API
|
|
mostrar_estadisticas_api
|
|
|
|
# Mostrar errores si los hay
|
|
unless @resultados[:errores].empty?
|
|
puts "\n#{Color::YELLOW}⚠ Errores encontrados:#{Color::RESET}"
|
|
@resultados[:errores].each do |error|
|
|
puts " #{Color::RED}• #{error}#{Color::RESET}"
|
|
end
|
|
end
|
|
|
|
# Resumen final
|
|
if @resultados[:archivos_fallidos] == 0
|
|
@logger.exito("Sincronización completada exitosamente")
|
|
puts "\n#{Color::GREEN}✅ Sincronización completada exitosamente#{Color::RESET}"
|
|
else
|
|
@logger.advertencia("Sincronización completada con errores")
|
|
puts "\n#{Color::YELLOW}⚠ Sincronización completada con #{@resultados[:archivos_fallidos]} error(es)#{Color::RESET}"
|
|
end
|
|
|
|
@logger.info("Resumen de sincronización", @resultados)
|
|
end
|
|
|
|
def mostrar_estadisticas_api
|
|
begin
|
|
stats = @client.estadisticas
|
|
puts "\n#{Color::CYAN}📈 Estadísticas de dtic-BITACORAs#{Color::RESET}"
|
|
puts "#{Color::DIM}#{'-' * 40}#{Color::RESET}"
|
|
puts " #{Color::GREEN}Nodos en sistema:#{Color::RESET} #{stats[:nodos] || 0}"
|
|
puts " #{Color::GREEN}Bitácoras en sistema:#{Color::RESET} #{stats[:bitacoras] || 0}"
|
|
rescue => e
|
|
puts " #{Color::RED}No se pudieron obtener estadísticas: #{e.message}#{Color::RESET}"
|
|
end
|
|
end
|
|
end
|
|
end
|