# frozen_string_literal: true # tools/adn/subcomando_plan.rb — Subcomando para seguimiento y gestión del plan ADN # ============================================================================== # Este subcomando proporciona herramientas para el seguimiento, actualización y # reporte del plan integral de integración ADN con dtic-BITACORAs. # # Características principales: # • Parseo y análisis del archivo de plan Markdown # • Actualización del estado de tareas (pendiente/completado) # • Generación de reportes de progreso en múltiples formatos # • Validación de integridad del plan # • Exportación de datos de progreso # # Comandos disponibles: # • adn plan progreso [OPCIONES] — Mostrar progreso actual del plan # • adn plan actualizar [TAREAS] — Marcar tareas como completadas # • adn plan reporte [--formato] — Generar reporte detallado # • adn plan validar — Validar integridad del plan # • adn plan exportar [--output] — Exportar datos de progreso # • adn plan ayuda — Mostrar ayuda completa # # Uso: # ./tools/adn/run plan [SUBCOMANDO] [OPCIONES] require 'optparse' require 'date' require 'time' require 'json' require 'yaml' # Cargar módulo PlanParser begin require_relative '../plan/parser' rescue LoadError => e puts "⚠ Advertencia: No se pudo cargar PlanParser: #{e.message}" puts " Asegúrate de que tools/adn/plan/parser.rb existe" end module ADN class SubcomandoPlan 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__) # Inicializa el subcomando con argumentos y logger # # @param args [Array] Argumentos de línea de comandos # @param logger [ADNLogger] Instancia de logger para registro def initialize(args, logger) @args = args @logger = logger @plan_path = DEFAULT_PLAN_PATH end # Ejecuta el subcomando según los argumentos proporcionados def ejecutar if @args.empty? || @args[0] == 'ayuda' || @args[0] == 'help' || @args[0] == '-h' || @args[0] == '--help' mostrar_ayuda return end subcomando = @args[0] sub_args = @args[1..-1] || [] case subcomando when 'progreso' comando_progreso(sub_args) when 'actualizar' comando_actualizar(sub_args) when 'reporte' comando_reporte(sub_args) when 'validar' comando_validar(sub_args) when 'exportar' comando_exportar(sub_args) else @logger.error("Subcomando desconocido: #{subcomando}") puts "#{Color::RED}✗ Subcomando desconocido: '#{subcomando}'#{Color::RESET}" puts "" mostrar_ayuda exit 1 end rescue Interrupt puts "\n#{Color::YELLOW}⚠ Operación interrumpida por el usuario#{Color::RESET}" @logger.info("Operación de plan interrumpida por usuario") exit 130 rescue => e @logger.error("Error en operación de plan: #{e.message}", { backtrace: e.backtrace }) puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" puts "#{Color::DIM}#{e.backtrace.first}#{Color::RESET}" if ENV['ADN_DEBUG'] exit 1 end private # Comando: progreso - Mostrar progreso actual del plan def comando_progreso(args) opts = { detallado: false, formato: 'texto' } parser = OptionParser.new do |opt| opt.banner = "Uso: adn plan progreso [OPCIONES]" opt.on("--detallado", "Mostrar progreso detallado por fase y semana") do opts[:detallado] = true end opt.on("--formato FORMATO", "Formato de salida (texto, json, markdown)") do |f| opts[:formato] = f.downcase end opt.on("--plan RUTA", "Ruta al archivo de plan (default: #{File.basename(@plan_path)})") do |ruta| @plan_path = File.expand_path(ruta) end opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit end end begin parser.parse!(args) rescue OptionParser::InvalidOption => e puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" puts parser exit 1 end puts "#{Color::CYAN}📊 Analizando progreso del plan ADN BITACORAs...#{Color::RESET}" puts " #{Color::DIM}Archivo: #{@plan_path}#{Color::RESET}" puts "" begin parser_obj = Plan::PlanParser.new(@plan_path, @logger) parser_obj.parse_file if opts[:formato] == 'json' puts parser_obj.progress_report(format: :json) elsif opts[:formato] == 'markdown' puts parser_obj.progress_report(format: :markdown) else # Formato texto (default) puts parser_obj.progress_report(format: :text) if opts[:detallado] puts "" puts "#{Color::BOLD}📈 DETALLE POR FASES Y SEMANAS:#{Color::RESET}" stats = parser_obj.progress_stats stats[:phases].each do |phase| puts "" puts " #{Color::CYAN}FASE #{phase[:number]}: #{phase[:name]}#{Color::RESET}" puts " Progreso: #{phase[:percentage]}% (#{phase[:completed]}/#{phase[:total]} tareas)" # Mostrar tareas de esta fase phase_tasks = parser_obj.tasks_by_phase(phase[:number]) completed_tasks = phase_tasks.select { |t| t[:completed] } pending_tasks = phase_tasks.reject { |t| t[:completed] } if completed_tasks.any? puts " #{Color::GREEN}✓ Completadas:#{Color::RESET}" completed_tasks.first(3).each do |task| puts " • #{task[:id]}: #{task[:description]}" end if completed_tasks.size > 3 puts " ... y #{completed_tasks.size - 3} más" end end if pending_tasks.any? puts " #{Color::YELLOW}⏳ Pendientes:#{Color::RESET}" pending_tasks.first(3).each do |task| puts " • #{task[:id]}: #{task[:description]}" end if pending_tasks.size > 3 puts " ... y #{pending_tasks.size - 3} más" end end end end end rescue => e puts "#{Color::RED}✗ Error analizando el plan: #{e.message}#{Color::RESET}" puts " #{Color::DIM}Asegúrate de que el archivo existe y tiene el formato correcto#{Color::RESET}" exit 1 end end # Comando: actualizar - Marcar tareas como completadas def comando_actualizar(args) if args.empty? puts "#{Color::YELLOW}⚠ Se requiere al menos una tarea para actualizar#{Color::RESET}" puts " Uso: adn plan actualizar TAREA1 [TAREA2 ...] [OPCIONES]" exit 1 end opts = { notas: nil, guardar: true } parser = OptionParser.new do |opt| opt.banner = "Uso: adn plan actualizar TAREA1 [TAREA2 ...] [OPCIONES]" opt.on("--notas TEXTO", "Agregar notas a las tareas actualizadas") do |texto| opts[:notas] = texto end opt.on("--no-guardar", "No guardar cambios en el archivo (solo simular)") do opts[:guardar] = false end opt.on("--plan RUTA", "Ruta al archivo de plan") do |ruta| @plan_path = File.expand_path(ruta) end opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit end end # Separar tareas de opciones task_args = [] begin parser.parse!(args) task_args = args # Los argumentos restantes son las tareas rescue OptionParser::InvalidOption => e puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" puts parser exit 1 end if task_args.empty? puts "#{Color::YELLOW}⚠ No se especificaron tareas para actualizar#{Color::RESET}" puts " Ejemplo: adn plan actualizar 1.1 1.2 2.3" exit 1 end puts "#{Color::CYAN}🔄 Actualizando tareas del plan ADN BITACORAs...#{Color::RESET}" puts " #{Color::DIM}Tareas: #{task_args.join(', ')}#{Color::RESET}" puts " #{Color::DIM}Notas: #{opts[:notas] || '(ninguna)'}#{Color::RESET}" puts " #{Color::DIM}Guardar: #{opts[:guardar]}#{Color::RESET}" puts "" begin parser_obj = Plan::PlanParser.new(@plan_path, @logger) parser_obj.parse_file resultados = parser_obj.complete_tasks(task_args, opts[:notas]) if resultados[:updated].any? puts "#{Color::GREEN}✅ Tareas actualizadas exitosamente:#{Color::RESET}" resultados[:updated].each do |task_id| task = parser_obj.find_task_by_id(task_id) puts " • #{task_id}: #{task[:description]}" end if opts[:guardar] if parser_obj.save_changes puts "" puts "#{Color::GREEN}✓ Cambios guardados en #{@plan_path}#{Color::RESET}" else puts "" puts "#{Color::YELLOW}⚠ No se realizaron cambios o no se pudieron guardar#{Color::RESET}" end else puts "" puts "#{Color::YELLOW}⚠ Modo simulación - cambios no guardados#{Color::RESET}" puts " Usa --guardar para persistir los cambios" end end if resultados[:failed].any? puts "" puts "#{Color::YELLOW}⚠ Tareas no encontradas:#{Color::RESET}" resultados[:failed].each do |task_id| puts " • #{task_id}" end end # Mostrar progreso actualizado if resultados[:updated].any? puts "" stats = parser_obj.progress_stats puts " #{Color::BOLD}Progreso actualizado: #{stats[:overall_percentage]}%#{Color::RESET}" puts " (#{stats[:completed_tasks]}/#{stats[:total_tasks]} tareas completadas)" end rescue => e puts "#{Color::RED}✗ Error actualizando tareas: #{e.message}#{Color::RESET}" exit 1 end end # Comando: reporte - Generar reporte detallado def comando_reporte(args) opts = { formato: 'texto', output: nil } parser = OptionParser.new do |opt| opt.banner = "Uso: adn plan reporte [OPCIONES]" opt.on("--formato FORMATO", "Formato del reporte (texto, json, markdown)") do |f| opts[:formato] = f.downcase end opt.on("--output ARCHIVO", "Guardar reporte en archivo (en lugar de mostrar)") do |archivo| opts[:output] = archivo end opt.on("--plan RUTA", "Ruta al archivo de plan") do |ruta| @plan_path = File.expand_path(ruta) end opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit end end begin parser.parse!(args) rescue OptionParser::InvalidOption => e puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" puts parser exit 1 end puts "#{Color::CYAN}📋 Generando reporte del plan ADN BITACORAs...#{Color::RESET}" puts " #{Color::DIM}Formato: #{opts[:formato]}#{Color::RESET}" puts " #{Color::DIM}Archivo: #{@plan_path}#{Color::RESET}" puts "" begin parser_obj = Plan::PlanParser.new(@plan_path, @logger) parser_obj.parse_file reporte = parser_obj.progress_report(format: opts[:formato].to_sym) if opts[:output] File.write(opts[:output], reporte, encoding: 'UTF-8') puts "#{Color::GREEN}✅ Reporte guardado en: #{opts[:output]}#{Color::RESET}" puts " #{Color::DIM}Tamaño: #{reporte.bytesize} bytes#{Color::RESET}" else puts reporte end rescue => e puts "#{Color::RED}✗ Error generando reporte: #{e.message}#{Color::RESET}" exit 1 end end # Comando: validar - Validar integridad del plan def comando_validar(args) opts = { reparar: false } parser = OptionParser.new do |opt| opt.banner = "Uso: adn plan validar [OPCIONES]" opt.on("--reparar", "Intentar reparar problemas automáticamente") do opts[:reparar] = true end opt.on("--plan RUTA", "Ruta al archivo de plan") do |ruta| @plan_path = File.expand_path(ruta) end opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit end end begin parser.parse!(args) rescue OptionParser::InvalidOption => e puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" puts parser exit 1 end puts "#{Color::CYAN}🔍 Validando integridad del plan ADN BITACORAs...#{Color::RESET}" puts " #{Color::DIM}Archivo: #{@plan_path}#{Color::RESET}" puts " #{Color::DIM}Reparar: #{opts[:reparar]}#{Color::RESET}" puts "" begin parser_obj = Plan::PlanParser.new(@plan_path, @logger) parser_obj.parse_file resultados = parser_obj.validate_plan if resultados[:valid] puts "#{Color::GREEN}✅ Plan válido#{Color::RESET}" puts " • #{resultados[:tasks_count]} tareas detectadas" puts " • #{resultados[:phases_count]} fases detectadas" else puts "#{Color::RED}✗ Plan contiene errores:#{Color::RESET}" resultados[:errors].each do |error| puts " • #{error}" end end if resultados[:warnings].any? puts "" puts "#{Color::YELLOW}⚠ Advertencias encontradas:#{Color::RESET}" resultados[:warnings].each do |warning| puts " • #{warning}" end end if opts[:reparar] && (!resultados[:valid] || resultados[:warnings].any?) puts "" puts "#{Color::CYAN}🔄 Intentando reparar automáticamente...#{Color::RESET}" # Aquí se podrían implementar reparaciones automáticas puts " #{Color::DIM}(Reparación automática no implementada aún)#{Color::RESET}" end rescue => e puts "#{Color::RED}✗ Error validando el plan: #{e.message}#{Color::RESET}" exit 1 end end # Comando: exportar - Exportar datos de progreso def comando_exportar(args) opts = { formato: 'json', output: nil } parser = OptionParser.new do |opt| opt.banner = "Uso: adn plan exportar [OPCIONES]" opt.on("--formato FORMATO", "Formato de exportación (json, yaml)") do |f| opts[:formato] = f.downcase end opt.on("--output ARCHIVO", "Archivo de salida (default: progreso_YYYYMMDD.json)") do |archivo| opts[:output] = archivo end opt.on("--plan RUTA", "Ruta al archivo de plan") do |ruta| @plan_path = File.expand_path(ruta) end opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit end end begin parser.parse!(args) rescue OptionParser::InvalidOption => e puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" puts parser exit 1 end puts "#{Color::CYAN}💾 Exportando datos de progreso del plan...#{Color::RESET}" puts " #{Color::DIM}Formato: #{opts[:formato]}#{Color::RESET}" puts " #{Color::DIM}Archivo: #{@plan_path}#{Color::RESET}" puts "" begin parser_obj = Plan::PlanParser.new(@plan_path, @logger) parser_obj.parse_file if opts[:formato] == 'yaml' datos = { metadata: parser_obj.instance_variable_get(:@metadata), progress: parser_obj.progress_stats, tasks: parser_obj.instance_variable_get(:@tasks).map { |t| t.reject { |k| k == :line_number } }, phases: parser_obj.instance_variable_get(:@phases) } contenido = YAML.dump(datos) extension = '.yaml' else contenido = parser_obj.export_progress extension = '.json' end output_file = opts[:output] || "progreso_#{Time.now.strftime('%Y%m%d')}#{extension}" File.write(output_file, contenido, encoding: 'UTF-8') puts "#{Color::GREEN}✅ Datos exportados exitosamente:#{Color::RESET}" puts " • Archivo: #{output_file}" puts " • Tamaño: #{contenido.bytesize} bytes" puts " • Tareas: #{parser_obj.instance_variable_get(:@tasks).size}" puts " • Fases: #{parser_obj.instance_variable_get(:@phases).size}" rescue => e puts "#{Color::RED}✗ Error exportando datos: #{e.message}#{Color::RESET}" exit 1 end end # Muestra la ayuda general del subcomando def mostrar_ayuda puts "#{Color::BOLD}Uso: ./tools/adn/run plan [SUBCOMANDO] [OPCIONES]#{Color::RESET}" puts "" puts "#{Color::BOLD}Subcomandos disponibles:#{Color::RESET}" puts "" puts " #{Color::CYAN}progreso [OPCIONES]#{Color::RESET} Mostrar progreso actual del plan" puts " #{Color::CYAN}actualizar TAREAS [OPCIONES]#{Color::RESET} Marcar tareas como completadas" puts " #{Color::CYAN}reporte [OPCIONES]#{Color::RESET} Generar reporte detallado" puts " #{Color::CYAN}validar [OPCIONES]#{Color::RESET} Validar integridad del plan" puts " #{Color::CYAN}exportar [OPCIONES]#{Color::RESET} Exportar datos de progreso" puts " #{Color::CYAN}ayuda#{Color::RESET} Mostrar esta ayuda" puts "" puts "#{Color::BOLD}Opciones comunes:#{Color::RESET}" puts " #{Color::DIM}--plan RUTA#{Color::RESET} Ruta al archivo de plan" puts " #{Color::DIM}--formato FORMATO#{Color::RESET} Formato de salida (texto, json, markdown, yaml)" puts " #{Color::DIM}--output ARCHIVO#{Color::RESET} Guardar salida en archivo" puts "" puts "#{Color::BOLD}Ejemplos:#{Color::RESET}" puts " #{Color::DIM}./tools/adn/run plan progreso --detallado#{Color::RESET}" puts " #{Color::DIM}./tools/adn/run plan actualizar 1.1 1.2 2.3 --notas \"Implementado\"#{Color::RESET}" puts " #{Color::DIM}./tools/adn/run plan reporte --formato markdown --output reporte.md#{Color::RESET}" puts " #{Color::DIM}./tools/adn/run plan validar --reparar#{Color::RESET}" puts " #{Color::DIM}./tools/adn/run plan exportar --formato json --output progreso.json#{Color::RESET}" puts "" puts "#{Color::YELLOW}⚠ Archivo de plan por defecto:#{Color::RESET} #{File.basename(DEFAULT_PLAN_PATH)}" puts " #{Color::DIM}#{DEFAULT_PLAN_PATH}#{Color::RESET}" end end end