745 lines
34 KiB
Markdown
745 lines
34 KiB
Markdown
# 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.
|
|
|
|
<details>
|
|
<summary>Detalle de bugs corregidos (click para expandir)</summary>
|
|
|
|
### 🔴 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 |
|
|
|
|
</details>
|
|
|
|
---
|
|
|
|
## 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 <host>`)**: 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.
|
|
|
|
|