# Plan: Auditoría Completa + Descomposición del Monolito connapp.py ## Estado Actual — Auditoría de la Arquitectura ### Servicios (✅ Bien planteados) | Servicio | Archivo | Responsabilidad | Estado | |---|---|---|---| | `BaseService` | `base.py` | Config compartida, hooks, validación de nombres reservados | ✅ OK | | `NodeService` | `node_service.py` | CRUD nodos/carpetas, list, move, bulk, connect | ✅ OK | | `ProfileService` | `profile_service.py` | CRUD perfiles, resolución de `@profile` | ✅ OK | | `ConfigService` | `config_service.py` | Settings, encrypt, config folder | ✅ OK | | `ExecutionService` | `execution_service.py` | run/test commands en nodos | ✅ OK | | `ImportExportService` | `import_export_service.py` | YAML import/export | ✅ OK | | `PluginService` | `plugin_service.py` | add/del/enable/disable plugins | ✅ OK | | `SystemService` | `system_service.py` | API start/stop/restart/status | ✅ OK | | `AIService` | `ai_service.py` | ask, sessions, provider config | ⚠️ Parcial* | > \* `AIService` existe pero `connapp._func_ai` bypasea completamente al servicio e instancia directamente `ai(self.config)`. El servicio solo se usa para `list_sessions` y `delete_session`. ### Excepciones (✅ Limpias) La jerarquía `ConnpyError > {NodeNotFoundError, ProfileNotFoundError, etc.}` es correcta. --- ## Problemas Detectados ### 🐛 Bug 1: Métodos duplicados en connapp.py `_case`, `_fzf`, `_idletime`, `_configfolder` y `_ai_config` están definidos **dos veces**: - Primera vez: líneas ~606-627 (versiones viejas sin feedback, sin try/except) - Segunda vez: líneas ~735-768 (versiones nuevas con `printer.success`) Python sobrescribe la primera con la segunda, así que la app funciona, pero el `_func_others` en línea 600 mapea a métodos que llaman a las versiones antiguas (las cuales nunca se ejecutan realmente). **Esto es código muerto que genera confusión.** ### 🐛 Bug 2: `self.config` accesos directos pendientes Quedan 3 accesos directos a `self.config` que rompen SOA: - Línea 250: `self.config.defaultdir` → debería usar `self.config_service` o un accessor - Línea 622-623: `self.config.config["ai"]` → debería usar `self.config_service.get_settings()` (este es el primer `_ai_config` duplicado, se elimina con Bug 1) - Línea 885: `self.myai = ai(self.config, **arguments)` → directo a config, debería pasar por servicio ### 🐛 Bug 3: `self.config = config` duplicado (líneas 59-60) La línea de asignación está repetida dos veces. ### ⚠️ Bug 4: `print()` raw en lugar de `printer` 8 usos de `print()` nativo en connapp.py: - Líneas 434, 547, 646, 834: YAML dumps de `--show`, `list`, `plugin --list` → **reemplazar** con el nuevo `printer.data()` con syntax highlighting - Líneas 604, 619: shell completion/fzf wrapper output → **correcto**, es output que va a `.bashrc`, debe seguir con `print()` crudo - Líneas 904, 939: `print("\r")` spacer en AI → reemplazar con `console.print()` ### ⚠️ Bug 5: `ImportError` en AIService.delete_session Línea 34 de `ai_service.py` referencia `InvalidConfigurationError` pero no lo importa. --- ## Fase 0 — Sistema de Diseño Visual (Rich Output) Antes de migrar código, debemos definir el **lenguaje visual unificado** de toda la CLI. Todos los handlers van a usar las mismas funciones de output. El objetivo es que cada tipo de output tenga una apariencia consistente, profesional y con colores. ### 0.1 Paleta de Colores | Uso | Color Rich | Ejemplo | |---|---|---| | Éxito / OK | `green` | `[✓] Node added successfully` | | Error | `red` | `[✗] Node not found` | | Warning / Info menor | `yellow` | `[!] Plugin already enabled` | | Info / neutral | `cyan` | `[i] Editing: ['router1']` | | Títulos / headers | `bold cyan` | Paneles, reglas | | Data keys (YAML) | `blue` | Syntax highlighting de YAML | | Data values (YAML) | `white`/default | Syntax highlighting de YAML | | AI Engineer | `blue` | Panel border blue | | AI Architect | `medium_purple` | Panel border purple | | Test PASS | `bold green` | `✓ PASS` | | Test FAIL | `bold red` | `✗ FAIL` | | Dim/metadata | `dim` | Token counts, timestamps | ### 0.2 Funciones del printer — Ampliación El módulo `printer.py` actual tiene: `info`, `success`, `error`, `warning`, `custom`, `table`, `start`, `debug`. Hay que agregar funciones nuevas para cubrir todos los tipos de output: #### Nuevas funciones a agregar en `printer.py` | Función | Propósito | Diseño | |---|---|---| | `printer.data(title, content, language="yaml")` | Mostrar datos estructurados (nodo, perfil, lista) con syntax highlighting | Panel con título `bold cyan`, body con `Syntax(content, language)` | | `printer.node_panel(unique, output, status)` | Panel de resultado de ejecución en un nodo | Panel con borde `green`/`red` según status, título con `✓`/`✗`, body con output | | `printer.test_panel(unique, results_dict)` | Panel de resultado de test en un nodo | Igual que node_panel pero con resultados pass/fail por check | | `printer.test_summary(results)` | Resumen consolidado de tests | Múltiples test_panel | | `printer.header(text)` | Separador/título de sección | `Rule(text, style="bold cyan")` | | `printer.kv(key, value)` | Key-value inline | `[bold]{key}[/bold]: {value}` | | `printer.confirm_action(item, action)` | Mensaje pre-confirmación | `[i] {action}: {item}` estilizado | #### Ejemplo concreto: `printer.data()` ```python def data(title, content, language="yaml"): """Display structured data with syntax highlighting inside a panel.""" from rich.syntax import Syntax from rich.panel import Panel syntax = Syntax(content, language, theme="monokai", word_wrap=True) panel = Panel(syntax, title=f"[bold cyan]{title}[/bold cyan]", border_style="dim", expand=False) console.print(panel) ``` **Antes** (actual): ``` [router1] host: 10.0.0.1 protocol: ssh port: '22' user: admin ``` **Después** (nuevo): ``` ╭─ router1 ────────────────────╮ │ host: 10.0.0.1 │ │ protocol: ssh │ │ port: '22' │ │ user: admin │ ╰──────────────────────────────╯ ``` Con syntax highlighting YAML (keys en azul, values en blanco). #### Ejemplo concreto: `printer.node_panel()` ```python def node_panel(unique, output, status): """Display node execution result in a styled panel.""" from rich.panel import Panel from rich.text import Text if status == 0: status_str = "[bold green]✓ PASS[/bold green]" border = "green" else: status_str = f"[bold red]✗ FAIL({status})[/bold red]" border = "red" title = f"[bold]{unique}[/bold] — {status_str}" body = Text(output.strip() + "\n") if output and output.strip() else Text() console.print(Panel(body, title=title, border_style=border)) ``` #### Ejemplo concreto: lista de nodos/perfiles ```python # En list handler, en vez de yaml dump + print: items = node_service.list_nodes() yaml_str = yaml.dump(items, sort_keys=False, default_flow_style=False) printer.data("nodes", yaml_str) # Para plugins: plugins = plugin_service.list_plugins() yaml_str = yaml.dump(plugins, sort_keys=False, default_flow_style=False) printer.data("plugins", yaml_str) ``` ### 0.3 Mapa de Outputs Actuales → Nuevos | Comando | Output actual | Output nuevo | |---|---|---| | `node --show router1` | `printer.custom(name, "")` + `print(yaml)` | `printer.data(name, yaml_str)` | | `profile --show myprofile` | `printer.custom(name, "")` + `print(yaml)` | `printer.data(name, yaml_str)` | | `list nodes` | `printer.custom("nodes", "")` + `print(yaml)` | `printer.data("nodes", yaml_str)` | | `list folders` | `printer.custom("folders", "")` + `print(yaml)` | `printer.data("folders", yaml_str)` | | `list profiles` | `printer.custom("profiles", "")` + `print(yaml)` | `printer.data("profiles", yaml_str)` | | `plugin --list` | `printer.custom("plugins", "")` + `print(yaml)` | `printer.data("plugins", yaml_str)` | | `run node1 "cmd"` | `_print_node_panel()` inline | `printer.node_panel(unique, output, status)` | | YAML run test | `_print_test_summary()` inline | `printer.test_panel()` + `printer.test_summary()` | | `config --*` | `printer.success("Config saved")` | Sin cambio (ya es correcto) | | `ai "query"` | Rich Panel + `print("\r")` | Rich Panel + `console.print()` | | `ai --list` | `printer.table(...)` | Sin cambio (ya es correcto) | | `node -a`, `-e`, `-r` | `printer.success/error` | Sin cambio (ya es correcto) | ### 0.4 Implementación 1. **Editar `connpy/printer.py`** para agregar las nuevas funciones (`data`, `node_panel`, `test_panel`, `test_summary`, `header`, `kv`) 2. Los handlers usarán estas funciones en vez de crear Panels inline 3. `_print_node_panel`, `_print_node_test_panel`, `_print_test_summary` de connapp.py se eliminan y se reemplazan por las funciones del printer > [!IMPORTANT] > La regla es: **toda presentación visual pasa por `printer`**. Los handlers nunca deben importar Rich directamente ni construir Panels. Solo llaman a `printer.data()`, `printer.node_panel()`, etc. Esto garantiza consistencia visual en toda la app. ### 0.5 Argparse con Rich (`rich-argparse`) El output de `--help` de argparse es texto plano sin colores. Usando la librería `rich-argparse` se obtiene un help coloreado como drop-in replacement: ```python from rich_argparse import RichHelpFormatter parser = argparse.ArgumentParser( prog="connpy", description="SSH and Telnet connection manager", formatter_class=RichHelpFormatter # ← solo cambiar esto ) ``` Esto afecta **solo al `--help`**. Los errores de argparse ya los interceptamos con `parser.error = self._custom_error` que usa `printer.error()`. | Output argparse | Estado actual | Con rich-argparse | |---|---|---| | `--help` | Texto monótono plano | Argumentos en cyan, usage en bold, secciones claras | | Errores de parsing | ✅ Ya usa `printer.error()` | Sin cambio | | `--version` | ✅ Ya usa `printer.info()` | Sin cambio | --- ## Fase 1 — Limpieza Pre-Migración Antes de tocar la estructura de archivos, hay que limpiar el código existente. ### 1.1 Implementar las nuevas funciones de `printer.py` - Agregar `data()`, `node_panel()`, `test_panel()`, `test_summary()`, `header()`, `kv()` según lo definido en Fase 0 - Agregar test en `test_printer.py` para las funciones nuevas ### 1.2 Agregar `rich-argparse` - Agregar `rich-argparse` a `requirements.txt` - En `connapp.py`, cambiar `formatter_class=argparse.RawTextHelpFormatter` por `formatter_class=RichHelpFormatter` en todos los parsers (~12 instancias) - Los subparsers que no tienen formatter explícito heredan del padre, así que con poner en `defaultparser` y en los que usan `RawTextHelpFormatter` alcanza ### 1.3 Eliminar métodos duplicados - **Borrar** las definiciones antiguas de `_case`, `_fzf`, `_idletime`, `_configfolder`, `_ai_config` (líneas 603-627) - Dejar solamente las definiciones de líneas 735-768 que tienen feedback con `printer.success` - Mover `_fzf_wrapper` y `_completion` (que son únicos) a la zona cercana a las versiones finales ### 1.4 Corregir asignación duplicada - Remover la línea 60 (`self.config = config` duplicada) ### 1.5 Corregir `self.config.defaultdir` - Agregar método `get_default_dir()` a `ConfigService` que retorne `self.config.defaultdir` - Reemplazar en connapp.py línea 250 ### 1.6 Corregir import faltante en AIService - Agregar `from .exceptions import InvalidConfigurationError` en `ai_service.py` ### 1.7 Reemplazar `print()` raw por printer - Reemplazar los 4 YAML dumps (`print(yaml_output)`) por `printer.data(title, yaml_str)` - Reemplazar `print("\r")` por `console.print()` en las líneas 904 y 939 - Mantener `print()` crudo solo para shell completion/fzf wrapper output (necesita output limpio sin estilos) --- ## Fase 2 — Descomposición del Monolito El objetivo es reducir `connapp.py` de **1803 líneas** a un orquestador limpio de ~200-300 líneas que solo: 1. Define los parsers de argparse 2. Despacha a handlers del paquete `connpy/cli/` ### Estructura propuesta del paquete `connpy/cli/` ``` connpy/cli/ ├── __init__.py # Exporta todos los handlers ├── node_handler.py # _connect, _add, _del, _mod, _show ├── profile_handler.py # _profile_add, _profile_del, _profile_mod, _profile_show ├── config_handler.py # _case, _fzf, _idletime, _configfolder, _ai_config, _completion, _fzf_wrapper ├── run_handler.py # _node_run, _yaml_run, _yaml_generate, _cli_run ├── ai_handler.py # _func_ai (modo single + interactive) ├── api_handler.py # _func_api ├── plugin_handler.py # _func_plugin ├── import_export_handler.py # _func_import, _func_export, _bulk ├── helpers.py # _choose (selector fzf/inquirer) ├── validators.py # Todas las *_validation functions (host, port, protocol, tags, jumphost, etc.) ├── forms.py # _questions_nodes, _questions_edit, _questions_profiles, _questions_bulk └── help_text.py # _help, _print_instructions (completion scripts, YAML template, etc.) ``` ### 2.1 Crear `connpy/cli/__init__.py` - Exportar todas las clases handler - Definir una clase base `CLIHandler` con: - `self.app` → referencia al connapp (para acceder a servicios) - `self.services` → acceso directo a los servicios - Acceso rápido a `printer` (todos los outputs pasan por ahí) ### 2.2 Crear `connpy/cli/helpers.py` Extraer el único método utilitario de UI compartido: | Método | Descripción | |---|---| | `choose(list, name, action, fzf, case)` | Selector inquirer/fzf | > [!NOTE] > Los métodos `_print_node_panel`, `_print_node_test_panel`, `_print_test_summary` ya no van aquí. Ahora viven en `printer.py` como `printer.node_panel()`, `printer.test_panel()`, `printer.test_summary()`. ### 2.3 Crear `connpy/cli/validators.py` Extraer **todas** las funciones de validación de inquirer (~14 funciones): | Función | Uso | |---|---| | `host_validation` | Validar hostname | | `protocol_validation` | Validar protocolo de nodo | | `profile_protocol_validation` | Validar protocolo de perfil | | `port_validation` | Validar puerto de nodo | | `profile_port_validation` | Validar puerto de perfil | | `pass_validation` | Validar password @profile | | `tags_validation` | Validar tags dict | | `profile_tags_validation` | Validar tags de perfil | | `jumphost_validation` | Validar jumphost | | `profile_jumphost_validation` | Validar jumphost de perfil | | `default_validation` | Validación default @profile | | `bulk_node_validation` | Validar nodo en bulk | | `bulk_folder_validation` | Validar folder en bulk | | `bulk_host_validation` | Validar host en bulk | Estas funciones necesitan acceso a `self.profiles`, `self.nodes_list`, `self.folders` y `self.case`. Se les pasará un contexto o se guardará como atributo de la clase. ### 2.4 Crear `connpy/cli/forms.py` Extraer los formularios interactivos de inquirer: | Función | Líneas approx | Descripción | |---|---|---| | `questions_nodes()` | 1378-1450 | Formulario completo de nodo | | `questions_edit()` | 1363-1376 | Checkboxes de qué editar | | `questions_profiles()` | 1452-1512 | Formulario completo de perfil | | `questions_bulk()` | 1514-1545 | Formulario de bulk add | Estas funciones usan validators y servicios (para obtener defaults de nodos/perfiles). ### 2.5 Crear `connpy/cli/help_text.py` Extraer todo el texto estático: | Función | Descripción | |---|---| | `get_help(type, parsers)` | Genera help text (usage, end, node) | | `get_instructions(type)` | Wizard instructions, completion scripts, fzf wrapper, YAML template | Este módulo es **puro texto**, sin dependencias de servicios. ### 2.6 Crear `connpy/cli/node_handler.py` ```python class NodeHandler: def __init__(self, app): self.app = app def connect(self, args) # Actual _connect def add(self, args) # Actual _add def delete(self, args) # Actual _del def modify(self, args) # Actual _mod def show(self, args) # Usa printer.data() para YAML def dispatch(self, args) # Actual _func_node ``` ### 2.7 Crear `connpy/cli/profile_handler.py` ```python class ProfileHandler: def __init__(self, app): self.app = app def add(self, args) # Actual _profile_add def delete(self, args) # Actual _profile_del def modify(self, args) # Actual _profile_mod def show(self, args) # Usa printer.data() para YAML def dispatch(self, args) # Actual _func_profile ``` ### 2.8 Crear `connpy/cli/config_handler.py` ```python class ConfigHandler: def __init__(self, app): self.app = app def set_case(self, args) def set_fzf(self, args) def set_idletime(self, args) def set_config_folder(self, args) def set_ai_config(self, args) def show_completion(self, args) def show_fzf_wrapper(self, args) def dispatch(self, args) ``` ### 2.9 Crear `connpy/cli/run_handler.py` ```python class RunHandler: def __init__(self, app): self.app = app def node_run(self, args) # Usa printer.header() + printer.node_panel() def yaml_run(self, args) # Playbook YAML def yaml_generate(self, args) # Generar template def cli_run(self, script) # Usa printer.header() + printer.node_panel/test_panel def dispatch(self, args) ``` ### 2.10 Crear `connpy/cli/ai_handler.py` ```python class AIHandler: def __init__(self, app): self.app = app def single_question(self, args, myai, session_id) # Modo single shot def interactive_chat(self, args, myai, session_id) # Modo interactivo def list_sessions(self, args) # Usa printer.table() def delete_session(self, args) def dispatch(self, args) ``` ### 2.11 Crear `connpy/cli/api_handler.py` ```python class APIHandler: def __init__(self, app): self.app = app def dispatch(self, args) # Start/stop/restart/debug ``` ### 2.12 Crear `connpy/cli/plugin_handler.py` ```python class PluginHandler: def __init__(self, app): self.app = app def dispatch(self, args) # add/update/del/enable/disable/list # list usa printer.data() para YAML ``` ### 2.13 Crear `connpy/cli/import_export_handler.py` ```python class ImportExportHandler: def __init__(self, app): self.app = app def import_file(self, args) def export_file(self, args) def bulk(self, args) def dispatch_import(self, args) def dispatch_export(self, args) ``` --- ## Fase 2.5 — Auditoría y Correcciones Post-Refactor ✅ Revisión exhaustiva línea por línea de los 12 archivos del paquete `connpy/cli/` comparados contra el `connapp.py` original. Todos los bugs críticos (B1-B7) y mejoras (M1-M5) han sido corregidos.
Detalle de bugs corregidos (click para expandir) ### 🔴 Bugs Críticos (Corregidos) | Bug | Archivo | Problema | Fix | |-----|---------|----------|-----| | B1 | `run_handler.py` | `node_run` pasaba comandos como lista separada en vez de string unido | `commands = [" ".join(args.data[1:])]` | | B2 | `run_handler.py` | `cli_run` no pasaba `folder` ni `prompt` al execution service | Agregados como parámetros opcionales a `ExecutionService` | | B3 | `ai_handler.py` | Sesiones usan `ai_service` pero AI usa `ai()` directo | Validado que ambos leen del mismo storage | | B4 | `ai_handler.py` | Faltaba error msg cuando sesión no carga | Agregado branch else con `printer.error()` | | B5 | `ai_handler.py` | Faltaba mensaje de historial previo al resumir | Agregado `printer.info()` con count de mensajes | | B6 | `ai_handler.py` | `KeyboardInterrupt` mataba el chat entero | Doble try/except: interno (continue) + externo (exit) | | B7 | `api_handler.py` | Lógica `if`/`elif` rota + bypass de `system_service` | Corregido a `elif` y llamadas directas a `connpy.api` | ### 🟢 Mejoras de Calidad (Corregidas) | ID | Archivo | Acción | |----|---------|--------| | M1 | `cli/__init__.py` | Clase `CLIHandler` muerta eliminada | | M2 | `cli/config_handler.py` | Handlers huérfanos eliminados | | M3 | `cli/commands/` | Directorio vacío eliminado | | M4 | `cli/ai_handler.py` | Import no usado eliminado | | M5 | `cli/profile_handler.py` | Import no usado eliminado |
--- ## Fase 2.7 — Sistema de Temas Persistente ✅ Se implementó un sistema de temas centralizado y persistente que permite personalizar todos los colores de la CLI. ### Componentes implementados | Archivo | Cambio | |---------|--------| | `printer.py` | `STYLES`, `DARK_THEME`, `LIGHT_THEME` + función `apply_theme()` con merge y fallback | | `services/config_service.py` | `apply_theme_from_file()` — acepta `dark`, `light`, o path a YAML | | `cli/config_handler.py` | Handler `set_theme` con dispatch y aplicación inmediata | | `connapp.py` | Flag `--theme THEME` + `_apply_app_theme()` que sincroniza printer y `RichHelpFormatter` | ### Características - `connpy config --theme dark` / `light` / `custom.yaml` - Persistido en `config.yaml` bajo `config.theme` - Auto-cargado al inicio vía `connapp._apply_app_theme()` - Fallback: keys faltantes en el YAML usan los defaults de `STYLES` - Afecta: paneles, tablas, AI (Engineer/Architect), y menús `--help` ### Eliminación de colores hardcodeados ✅ Auditoría completa de `ai.py`, `ai_handler.py`, `run_handler.py`, `connapp.py`: **cero colores literales** fuera de `printer.py`. Todos usan aliases semánticos (`engineer`, `architect`, `error`, `warning`, `unavailable`, etc.). --- ## Fase 3 — Dynamic Service Backend (ServiceProvider Pattern) ### Objetivo Hacer que el CLI sea **agnóstico del backend**. En vez de que los handlers accedan a servicios locales hardcodeados (`self.app.node_service`), pasan por un **ServiceProvider** que decide qué implementación usar. Por defecto → servicios locales. Con `--remote` → stubs gRPC (a implementar después). **Zero refactoring del CLI cuando se agregue gRPC.** ### Arquitectura ``` ┌─────────────────────────────────────────────────────┐ │ CLI Handlers │ │ (NodeHandler, ProfileHandler, RunHandler, etc.) │ │ │ │ self.app.services.nodes.list_nodes() │ │ self.app.services.config_svc.update_setting() │ └──────────────────────┬──────────────────────────────┘ │ ┌────────▼────────┐ │ ServiceProvider │ ← decides backend │ │ │ mode = "local" │ (default) │ mode = "remote" │ (--remote / config) └───────┬──┬──────┘ │ │ ┌───────────┘ └───────────┐ ▼ ▼ ┌───────────────┐ ┌─────────────────┐ │ Local Services │ │ gRPC Stubs │ │ (current code) │ │ (Fase 4+, TBD) │ │ │ │ │ │ NodeService │ │ NodeServiceStub │ │ ProfileService │ │ ProfileStub │ │ ConfigService │ │ ConfigStub │ │ ... │ │ ... │ └───────────────┘ └─────────────────┘ ``` ### 3.1 Crear `connpy/services/provider.py` [NEW] Fachada ligera que expone atributos de servicio. El provider recibe un `mode` y un `config` e instancia el backend correcto. ```python class ServiceProvider: """Dynamic service backend. Transparently provides local or remote services.""" def __init__(self, config, mode="local", remote_host=None): self.mode = mode self.config = config self.remote_host = remote_host if mode == "local": self._init_local() elif mode == "remote": self._init_remote() else: raise ValueError(f"Unknown service mode: {mode}") def _init_local(self): from .node_service import NodeService from .profile_service import ProfileService from .config_service import ConfigService from .plugin_service import PluginService from .ai_service import AIService from .system_service import SystemService from .execution_service import ExecutionService from .import_export_service import ImportExportService self.nodes = NodeService(self.config) self.profiles = ProfileService(self.config) self.config_svc = ConfigService(self.config) self.plugins = PluginService(self.config) self.ai = AIService(self.config) self.system = SystemService(self.config) self.execution = ExecutionService(self.config) self.import_export = ImportExportService(self.config) def _init_remote(self): # Fase 4+: gRPC stubs go here raise NotImplementedError( "Remote mode (gRPC) is not yet available. " "Use local mode or wait for the gRPC implementation." ) ``` > [!NOTE] > Los nombres de atributos son cortos y limpios: `services.nodes`, `services.profiles`, `services.config_svc` (evita colisión con `self.config`), `services.execution`, etc. ### 3.2 Refactorizar `connapp.__init__` [MODIFY] Reemplazar las 8 instanciaciones individuales por un único `ServiceProvider`: ```python # ANTES (actual): self.node_service = NodeService(self.config) self.profile_service = ProfileService(self.config) self.config_service = ConfigService(self.config) self.plugin_service = PluginService(self.config) self.ai_service = AIService(self.config) self.system_service = SystemService(self.config) self.execution_service = ExecutionService(self.config) self.import_export_service = ImportExportService(self.config) # DESPUÉS: from .services.provider import ServiceProvider mode = self.config.config.get("service_mode", "local") remote_host = self.config.config.get("remote_host", None) self.services = ServiceProvider(self.config, mode=mode, remote_host=remote_host) ``` ### 3.3 Agregar flags `--service-mode` y `--remote` globales [MODIFY connapp.py] Agregar a `defaultparser`: ```python defaultparser.add_argument("--service-mode", dest="service_mode", choices=["local", "remote"], help="Set the backend service mode (local or remote)") defaultparser.add_argument("--remote", dest="remote_host", metavar="HOST:PORT", help="Connect to a remote connpy service via gRPC (requires --service-mode remote)") ``` Y en `start()`, después del parsing: ```python mode = args.service_mode or self.config.config.get("service_mode", "local") remote_host = args.remote_host or self.config.config.get("remote_host", None) self.services = ServiceProvider(self.config, mode=mode, remote_host=remote_host) ``` ### 3.4 Migrar handlers al nuevo API [MODIFY cli/*.py] Renombrar todas las referencias en los handlers: | Antes | Después | |-------|---------| | `self.app.node_service` | `self.app.services.nodes` | | `self.app.profile_service` | `self.app.services.profiles` | | `self.app.config_service` | `self.app.services.config_svc` | | `self.app.plugin_service` | `self.app.services.plugins` | | `self.app.ai_service` | `self.app.services.ai` | | `self.app.system_service` | `self.app.services.system` | | `self.app.execution_service` | `self.app.services.execution` | | `self.app.import_export_service` | `self.app.services.import_export` | > [!IMPORTANT] > **Estrategia de migración**: Hacer un full rename en todos los handlers en un solo pase (clean break). Son find-replace directos y todos los handlers están en `connpy/cli/`. ### 3.5 Actualizar tests [MODIFY tests/] - Actualizar mocks para usar `app.services.nodes` en vez de `app.node_service` - Agregar test para `ServiceProvider` con modo `local` y verificar que `remote` lanza `NotImplementedError` --- ## Fase 4 — Servidor gRPC y Stubs Remotos Con el `ServiceProvider` en su lugar (Fase 3), la aplicación ahora es agnóstica de si sus servicios se ejecutan localmente o de forma remota. Esta fase consiste en implementar la comunicación gRPC real. ### Arquitectura de gRPC - **Protocol Buffers**: Un único archivo `.proto` (`connpy.proto`) que define todos los mensajes y servicios. - **Servidor (`connpy api -s`)**: Un servidor gRPC que instancia los servicios locales (como lo hacía `ServiceProvider` en modo local) y procesa las peticiones de los clientes. - **Cliente (`connpy --remote `)**: Stubs (proxies) que exponen la misma interfaz de los servicios locales y serializan las llamadas a través de la red hacia el servidor. ### 4.1 Definir proto files (`connpy/proto/connpy.proto`) - Crear los mensajes base: `Node`, `Folder`, `Profile`, `Theme`, `Plugin`, etc. - Definir servicios que mapeen la interfaz existente de los servicios Python: - `NodeService` (list_nodes, list_folders, move_node, bulk_add, etc.) - `ProfileService` (list_profiles, add_profile, get_profile, etc.) - `ConfigService` (get_settings, update_setting, set_config_folder, etc.) - `PluginService` (list_plugins, add_plugin, enable_plugin, etc.) - `ExecutionService` (run_commands) ### 4.2 Generar código Python - Agregar `grpcio` y `grpcio-tools` a `requirements.txt`. - Ejecutar el compilador `protoc` para generar `connpy_pb2.py` y `connpy_pb2_grpc.py`. ### 4.3 Implementar Servidor gRPC (`connpy/grpc/server.py`) - Crear las clases de servidor (ej. `NodeServicer`, `ProfileServicer`) que hereden de las generadas por `protoc`. - Cada Servicer debe recibir una instancia de `Configfile` en su constructor, inicializar el servicio local correspondiente (ej. `NodeService(config)`) y redirigir las llamadas RPC a este servicio local. - **Reemplazo total de la API**: Eliminar completamente el servidor Flask/Waitress actual en `connpy/api.py`. No se mantiene nada de la API REST anterior. Modificar `connpy/cli/api_handler.py` y `connpy/api.py` para que `connpy api -s` arranque exclusivamente este nuevo servidor gRPC en el puerto especificado. ### 4.4 Implementar Stubs en el Cliente (`connpy/grpc/stubs.py`) - Crear las clases proxy (`NodeStub`, `ProfileStub`, etc.) que cumplan con la misma firma que los servicios locales. - Cada Stub debe recibir un `grpc.Channel`, construir el Stub correspondiente generado por `protoc` (ej. `connpy_pb2_grpc.NodeServiceStub`) y llamar a los métodos gRPC serializando/deserializando los argumentos y respuestas. ### 4.5 Conectar Stubs a `ServiceProvider` - En `connpy/services/provider.py`, modificar `_init_remote(self)` para que en lugar de asignar `RemoteStub()`, construya un `grpc.insecure_channel(self.remote_host)`. - Instanciar los stubs reales y asignarlos a las propiedades de la clase (`self.nodes = NodeStub(channel)`, `self.profiles = ProfileStub(channel)`, etc.). - Mantener la inicialización de `ConfigService` en modo mixto (lee de local para saber la configuración de red y temas visuales, pero podría enviar cambios al servidor si es necesario, o mantener las configuraciones de interfaz estrictamente locales). ### 4.6 Manejo de Errores - Envolver las excepciones `grpc.RpcError` en `ConnpyError` del cliente para que los handlers del CLI las impriman limpiamente y no lancen un stacktrace sucio. --- ## Fase 5 — Verificación Final ### 5.1 Correr suite completa ```bash pytest connpy/tests/ ``` ### 5.2 Tests de integración manual - `connpy node -s router1` → Verifica `printer.data()` con syntax highlighting - `connpy list nodes` → Verifica `printer.data()` con panel - `connpy plugin --list` → Verifica `printer.data()` con panel - `connpy config --allow-uppercase true` → Verifica config handler - `connpy run router1 "show version"` → Verifica `printer.node_panel()` - `connpy ai "hello"` → Verifica AI handler - `connpy --remote localhost:50051 list nodes` → Verifica que lanza `NotImplementedError` (hasta Fase 4) ### 5.3 Verificar que `wc -l connpy/connapp.py` < 400 --- ## Resumen de Ejecución | Fase | Estado | Descripción | Archivos involucrados | |---|---|---|---| | **0** | ✅ | Sistema de diseño visual Rich | `connpy/printer.py` | | **1** | ✅ | Limpieza pre-migración + adoptar printer nuevo | `connapp.py`, `ai_service.py`, `config_service.py`, `printer.py` | | **2** | ✅ | Crear paquete `cli/` con 12 módulos | `connpy/cli/*.py` | | **2.5** | ✅ | Auditoría post-refactor: fix bugs B1-B7 + limpieza M1-M5 | `cli/*.py`, `services/execution_service.py` | | **2.7** | ✅ | Sistema de temas persistente + eliminación de colores hardcodeados | `printer.py`, `ai.py`, `ai_handler.py`, `config_service.py`, `config_handler.py`, `connapp.py` | | **3** | ✅ | Dynamic Service Backend (ServiceProvider) | `services/provider.py` (nuevo), `connapp.py`, `cli/*.py`, `tests/` | | **4** | ✅ | Servidor gRPC y Stubs Remotos | `proto/`, `grpc/`, `services/provider.py`, `api_handler.py` | | **4.5** | 📋 | Auditoría Post-gRPC (Context/Sync/Completion) | `cli/helpers.py`, `connapp.py`, `tests/` | | **5** | 📋 | Verificación final | Todos | > [!IMPORTANT] > La Fase 3 completada estableció el `ServiceProvider`. Agregar gRPC en Fase 4 es implementar el servidor y los stubs en el cliente, logrando comunicación real sin tocar la lógica de los handlers del CLI. > [!WARNING] > El método `_func_ai` / `AIHandler` instancia `ai(self.config)` directamente, bypasseando `AIService`. Esto es intencional: el AI necesita estado largo de sesión (`self.myai`) y un refactor completo del AIService sería trabajo aparte. El ServiceProvider no afecta este flujo.