508 lines
18 KiB
Ruby
508 lines
18 KiB
Ruby
# 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 '../parsers/plan'
|
|
rescue LoadError => e
|
|
puts "⚠ Advertencia: No se pudo cargar PlanParser: #{e.message}"
|
|
puts " Asegúrate de que tools/adn/parsers/plan.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<String>] 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
|