#!/usr/bin/env ruby # frozen_string_literal: true ENV['TZ'] = 'America/Argentina/Buenos_Aires' # 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: ./adn/tools/run [opciones] [argumentos] # # Subcomandos disponibles: # validador [--watch] [--fix] Validar cumplimiento del ADN en bitácoras # contexto [--cmd] Obtener contexto topológico de un nodo # nodos [opciones] Gestión de nodos (agrupar, listar, buscar) # jornada [modo] [hora] Gestión de jornada (iniciar, cerrar, estado) # 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 # tailscale [opciones] Gestión de identidades Tailscale # conocimiento asimilar Asimilar documentación al ADN # evolucion Gestionar propuesta de evolución del ADN # msp Gestión de MSPs (auditar, registrar, listar) # proy Generación y gestión de proyectos # candados [opciones] Gestión de secretos y autorización SSH # ayuda [subcomando] Mostrar ayuda detallada # # Documentación completa: docs/ambito/dtic-ADN/A01_dtic-ADN.md require 'optparse' require 'json' require 'fileutils' require 'date' require 'time' require_relative 'core/constants' module ADN # 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 'cli/triggers/eventos' require_relative 'cli/triggers/motor' require_relative 'cli/db' require_relative 'cli/plan' # require_relative 'cli/backup' # Archivado → _hist/ (A03.P001) require_relative 'cli/triggers' require_relative 'cli/conocimiento' require_relative 'cli/jornada' require_relative 'cli/proceso' require_relative 'cli/estados' require_relative 'cli/tailscale' require_relative 'cli/ssh' require_relative 'cli/candados' require_relative 'cli/network' require_relative 'core/conciliador' require_relative 'core/colores' # ─── Logger estructurado ───────────────────────────────────────────────── # El logger está implementado en adn/logger.rb ADNLogger = ADN::Logger require_relative 'cli/validador' require_relative 'cli/contexto' require_relative 'cli/generar' require_relative 'cli/salud' require_relative 'cli/ayuda' require_relative 'cli/nodos' require_relative 'cli/ambitos' require_relative 'cli/msp' require_relative 'cli/wzombi' require_relative 'cli/dasuten' require_relative 'cli/digis' require_relative 'cli/dns' require_relative 'bkps/bkps' require_relative 'cli/dron' require_relative 'proy/proy' # ─── CLI Principal ────────────────────────────────────────────────────── class ADNCLI def initialize @logger = ADNLogger.new end def ejecutar if ARGV.empty? ADN::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' ADN::SubcomandoSalud.new(args, @logger).ejecutar when 'generar' SubcomandoGenerar.new(args, @logger).ejecutar when 'nodos' ADN::SubcomandoNodos.new(args, @logger).ejecutar when 'ambitos' ADN::SubcomandoAmbitos.new(args, @logger).ejecutar when 'conocimiento' ADN::SubcomandoConocimiento.new(args, @logger).ejecutar when 'db' ADN::SubcomandoDB.new(args, @logger).ejecutar when 'backup', 'backups' ADN::SubcomandoBkps.new(args, @logger).ejecutar # Redirigido desde legacy backup → bkps when 'triggers' ADN::SubcomandoTriggers.new(args, @logger).ejecutar when 'jornada' ADN::SubcomandoJornada.new(args, @logger).ejecutar when 'proceso' ADN::SubcomandoProceso.new(args, @logger).ejecutar when 'estados' ADN::SubcomandoEstados.new(args, @logger).ejecutar when 'network' ADN::SubcomandoNetwork.new(args, @logger).ejecutar when 'tailscale', 'ts' ADN::SubcomandoTailscale.new(args, @logger).ejecutar when 'inicio' # Compatibilidad: redirigir a jornada iniciar ADN::SubcomandoJornada.new(['iniciar'] + args, @logger).ejecutar when 'cierre' # Compatibilidad: redirigir a jornada cerrar ADN::SubcomandoJornada.new(['cerrar'] + args, @logger).ejecutar when 'ayuda', 'help', '-h', '--help' ADN::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 'plan' ADN::SubcomandoPlan.new(args, @logger).ejecutar when 'msp' SubcomandoMsp.new(args, @logger).ejecutar when 'proy' ADN::SubcomandoProy.new(args, @logger).ejecutar when 'ssh' ADN::SubcomandoSSH.new(args, @logger).ejecutar when 'network' ADN::SubcomandoNetwork.new(args, @logger).ejecutar when 'candados', 'secrets' ADN::SubcomandoCandados.new(args, @logger).ejecutar when 'wzombi', 'w-zombi' ADN::SubcomandoWZombi.new(args, @logger).ejecutar when 'dasuten' ADN::SubcomandoDasuten.new(args, @logger).ejecutar when 'bkps' ADN::SubcomandoBkps.new(args, @logger).ejecutar when 'dron', 'copiloto', 'novato' ADN::SubcomandoDron.new(args, @logger).ejecutar when 'digis' ADN::SubcomandoDigis.new(args, @logger).ejecutar when 'dns' ADN::SubcomandoDNS.new(args, @logger).ejecutar else @logger.error("Subcomando desconocido: #{subcomando}") puts "#{Color::RED}✗ Subcomando desconocido: '#{subcomando}'#{Color::RESET}" puts "" ADN::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 OptionParser::InvalidOption => e @logger.error("Opción inválida: #{e.message}") puts "#{Color::RED}✗ Error: opción inválida #{e.message}#{Color::RESET}" puts "Usa './adn/tools/run ayuda' para ver las opciones disponibles" exit 1 rescue ADN::Error => e @logger.error("Error de aplicación: #{e.message}") puts "#{Color::RED}✗ #{e.message}#{Color::RESET}" exit 1 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(3).join("\n")}#{Color::RESET}" if ENV['ADN_DEBUG'] puts "\n#{Color::YELLOW}💡 Ejecuta con ADN_DEBUG=1 para ver el trace completo#{Color::RESET}" 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