commit 49bdf1ac7609dfcaa68283a6ded8adf8906c899f Author: juanminsanz Date: Sun Sep 20 11:59:56 2026 +0200 20-sep: infra global - OmniRoute watchdog + plugin OpenCode + changelog sobremesa/portatil diff --git a/CHANGELOG_INFRA.md b/CHANGELOG_INFRA.md new file mode 100644 index 0000000..c8fea76 --- /dev/null +++ b/CHANGELOG_INFRA.md @@ -0,0 +1,36 @@ +# Changelog de infraestructura — global (sobremesa <-> portatil <-> VPS) + +> Fichero generado por `scripts/infra_changelog.py`. NO editar a mano. +> Fuente de verdad: `changelog_infra.json`. + +## Pendientes de replicar + +- **2026-09-20-omniroute-arranque-fiable-tarea-programada-vigilante** [omniroute] OmniRoute arranque fiable (tarea programada + vigilante) (aplicado en: sobremesa) +- **2026-09-20-omniroute-505-modelos-expuestos-en-opencode** [omniroute] OmniRoute: 505 modelos expuestos en OpenCode (aplicado en: sobremesa) +- **2026-09-20-plugin-global-opencode-relanza-omniroute** [opencode] Plugin global OpenCode relanza OmniRoute (aplicado en: sobremesa) + +## Historial + +### 2026-09-20 · OmniRoute arranque fiable (tarea programada + vigilante) +- **id**: `2026-09-20-omniroute-arranque-fiable-tarea-programada-vigilante` +- **Equipo**: sobremesa +- **Categoria**: omniroute +- **Transferible**: si +- **Aplicado en**: sobremesa +- **Detalle**: Tarea 'OmniRoute Watchdog' al iniciar sesion + omniroute_watchdog.ps1 (puerto 20128 cada 120s, autocuracion). Sustituye el .vbs fragil. + +### 2026-09-20 · OmniRoute: 505 modelos expuestos en OpenCode +- **id**: `2026-09-20-omniroute-505-modelos-expuestos-en-opencode` +- **Equipo**: sobremesa +- **Categoria**: omniroute +- **Transferible**: si +- **Aplicado en**: sobremesa +- **Detalle**: setup-opencode --only de 5 conexiones probadas (antigravity, claude, opencode-go, opencode, aihorde) + auto/*. Quitar BOM de opencode.jsonc. + +### 2026-09-20 · Plugin global OpenCode relanza OmniRoute +- **id**: `2026-09-20-plugin-global-opencode-relanza-omniroute` +- **Equipo**: sobremesa +- **Categoria**: opencode +- **Transferible**: si +- **Aplicado en**: sobremesa +- **Detalle**: omniroute-bootstrap.ts en ~/.config/opencode/plugins/ lanza el vigilante -Once al arrancar OpenCode en cualquier repo. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4eb2dd5 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# INFRAESTRUCTURA + +Repo de infraestructura compartida entre equipos (sobremesa, portátil, VPS). + +## Contenido + +| Ruta | Qué es | +|------|--------| +| `changelog_infra.json` | Fuente de verdad del **changelog global de infraestructura** (por equipo) | +| `CHANGELOG_INFRA.md` | Versión legible (generada; no editar a mano) | +| `scripts/infra_changelog.py` | Añadir / consultar / marcar cambios | +| `omniroute/omniroute_watchdog.ps1` | Vigilante de OmniRoute (arranque + autocuración) | +| `omniroute/install_omniroute_autostart.ps1` | Registra la tarea programada `OmniRoute Watchdog` | +| `opencode/omniroute-bootstrap.ts` | Plugin GLOBAL de OpenCode: relanza OmniRoute al arrancar OpenCode | +| `scripts/instalar_global.ps1` | Instala el plugin global y la tarea de arranque | + +## Changelog global (sobremesa ↔ portátil) + +Registra cada cambio de infraestructura y si es transferible al otro equipo: + +```powershell +# Añadir un cambio (detecta el equipo automáticamente) +python scripts/infra_changelog.py add --titulo "Instalado Docker" --categoria servicios --transferible si --detalle "..." + +# Ver qué falta replicar en ESTE equipo +python scripts/infra_changelog.py pendientes + +# Marcar como aplicado aquí +python scripts/infra_changelog.py aplicado + +# Resumen +python scripts/infra_changelog.py estado +``` + +El protocolo `re` de cada equipo lee este fichero y avisa de lo pendiente. + +## Instalación en un equipo nuevo (p. ej. el portátil) + +```powershell +git clone https://git.v-encore-lab.com/jjminguez/INFRAESTRUCTURA.git +cd INFRAESTRUCTURA +powershell -ExecutionPolicy Bypass -File scripts/instalar_global.ps1 +``` + +Prerrequisitos de OmniRoute: Node.js + `npm i -g omniroute`, y copiar `~/.omniroute` +(credenciales) y `~/.config/opencode/opencode.jsonc`. diff --git a/changelog_infra.json b/changelog_infra.json new file mode 100644 index 0000000..6a7e2be --- /dev/null +++ b/changelog_infra.json @@ -0,0 +1,41 @@ +{ + "version": 1, + "entries": [ + { + "id": "2026-09-20-omniroute-arranque-fiable-tarea-programada-vigilante", + "fecha": "2026-09-20", + "equipo": "sobremesa", + "categoria": "omniroute", + "titulo": "OmniRoute arranque fiable (tarea programada + vigilante)", + "transferible": true, + "aplicado_en": [ + "sobremesa" + ], + "detalle": "Tarea 'OmniRoute Watchdog' al iniciar sesion + omniroute_watchdog.ps1 (puerto 20128 cada 120s, autocuracion). Sustituye el .vbs fragil." + }, + { + "id": "2026-09-20-omniroute-505-modelos-expuestos-en-opencode", + "fecha": "2026-09-20", + "equipo": "sobremesa", + "categoria": "omniroute", + "titulo": "OmniRoute: 505 modelos expuestos en OpenCode", + "transferible": true, + "aplicado_en": [ + "sobremesa" + ], + "detalle": "setup-opencode --only de 5 conexiones probadas (antigravity, claude, opencode-go, opencode, aihorde) + auto/*. Quitar BOM de opencode.jsonc." + }, + { + "id": "2026-09-20-plugin-global-opencode-relanza-omniroute", + "fecha": "2026-09-20", + "equipo": "sobremesa", + "categoria": "opencode", + "titulo": "Plugin global OpenCode relanza OmniRoute", + "transferible": true, + "aplicado_en": [ + "sobremesa" + ], + "detalle": "omniroute-bootstrap.ts en ~/.config/opencode/plugins/ lanza el vigilante -Once al arrancar OpenCode en cualquier repo." + } + ] +} diff --git a/omniroute/install_omniroute_autostart.ps1 b/omniroute/install_omniroute_autostart.ps1 new file mode 100644 index 0000000..b72d5f2 --- /dev/null +++ b/omniroute/install_omniroute_autostart.ps1 @@ -0,0 +1,98 @@ +<# +.SYNOPSIS + Instala el arranque fiable de OmniRoute al iniciar sesion de Windows. + +.DESCRIPTION + Sustituye el autostart fragil de OmniRoute (un .vbs en la carpeta Inicio, ya + afectado por la deprecacion de VBScript en Windows) por una tarea programada + que ejecuta el vigilante omniroute_watchdog.ps1: + - Se lanza al iniciar sesion del usuario. + - Arranca OmniRoute si no esta corriendo. + - Lo relanza si se cae durante la sesion (autocuracion). + + Pasos: + 1. Desactiva el autostart propio de OmniRoute (OmniRoute.vbs) para evitar dobles arranques. + 2. Registra la tarea programada "OmniRoute Watchdog". + 3. La arranca ahora mismo. + +.NOTES + No requiere privilegios de administrador (tarea del propio usuario). +#> +[CmdletBinding()] +param( + [switch]$NoStart +) + +$ErrorActionPreference = 'Stop' + +$TaskName = 'OmniRoute Watchdog' +$ScriptPath = Join-Path $PSScriptRoot 'omniroute_watchdog.ps1' +$PwshExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' +$NodeExe = 'C:\Program Files\nodejs\node.exe' +$OmniRouteMjs = Join-Path $env:APPDATA 'npm\node_modules\omniroute\bin\omniroute.mjs' +$UserId = "$env:USERDOMAIN\$env:USERNAME" + +Write-Host '== Instalando arranque fiable de OmniRoute ==' -ForegroundColor Cyan + +# 1) Desactivar el autostart VBS propio de OmniRoute ----------------------- +$vbs = Join-Path ([Environment]::GetFolderPath('Startup')) 'OmniRoute.vbs' +if ((Test-Path $NodeExe) -and (Test-Path $OmniRouteMjs)) { + Write-Host ' - Desactivando autostart VBS de OmniRoute...' -ForegroundColor Yellow + $prevEap = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { & $NodeExe $OmniRouteMjs autostart disable 2>&1 | Out-Null } catch { } + $ErrorActionPreference = $prevEap +} +if (Test-Path $vbs) { + Remove-Item -LiteralPath $vbs -Force + Write-Host ' - Eliminado OmniRoute.vbs de la carpeta Inicio.' -ForegroundColor Yellow +} + +# 2) Registrar la tarea programada ----------------------------------------- +if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) { + Write-Host ' - Eliminando tarea anterior...' -ForegroundColor Yellow + Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false +} + +$Action = New-ScheduledTaskAction ` + -Execute $PwshExe ` + -Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$ScriptPath`"" ` + -WorkingDirectory $env:USERPROFILE + +$Trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME + +$Settings = New-ScheduledTaskSettingsSet ` + -AllowStartIfOnBatteries ` + -DontStopIfGoingOnBatteries ` + -StartWhenAvailable ` + -RestartCount 999 ` + -RestartInterval (New-TimeSpan -Minutes 1) ` + -MultipleInstances IgnoreNew +$Settings.ExecutionTimeLimit = 'PT0S' # sin limite de tiempo (vigilante permanente) +$Settings.Hidden = $true + +$Principal = New-ScheduledTaskPrincipal -UserId $UserId -LogonType Interactive -RunLevel Limited + +Register-ScheduledTask ` + -TaskName $TaskName ` + -Action $Action ` + -Trigger $Trigger ` + -Settings $Settings ` + -Principal $Principal ` + -Description 'Arranca y vigila OmniRoute (localhost:20128) al iniciar sesion. Autocuracion si se cae.' ` + -Force | Out-Null + +Write-Host " - Tarea '$TaskName' registrada." -ForegroundColor Green + +# 3) Arrancar ahora --------------------------------------------------------- +if (-not $NoStart) { + Write-Host ' - Arrancando el vigilante ahora...' -ForegroundColor Yellow + Start-ScheduledTask -TaskName $TaskName +} + +Write-Host '' +Write-Host 'Listo. Comandos utiles:' -ForegroundColor Cyan +Write-Host " Get-ScheduledTask -TaskName '$TaskName' | Get-ScheduledTaskInfo" +Write-Host " Start-ScheduledTask -TaskName '$TaskName'" +Write-Host " Stop-ScheduledTask -TaskName '$TaskName'" +Write-Host " Get-Content `"$env:USERPROFILE\.omniroute\watchdog.log`" -Tail 20" diff --git a/omniroute/omniroute_watchdog.ps1 b/omniroute/omniroute_watchdog.ps1 new file mode 100644 index 0000000..4d819e3 --- /dev/null +++ b/omniroute/omniroute_watchdog.ps1 @@ -0,0 +1,174 @@ +<# +.SYNOPSIS + Vigilante (watchdog) de OmniRoute: garantiza que el servidor local este SIEMPRE arriba. + +.DESCRIPTION + OmniRoute escucha en http://127.0.0.1:20128 y tarda ~60 s en quedar listo. + Este vigilante: + - Comprueba el puerto 20128 periodicamente. + - Si no hay proceso, arranca el servidor en segundo plano (oculto, desde un + directorio neutro para no contaminarse con .env de repositorios). + - Si hay proceso pero no responde tras el tiempo de arranque, lo mata y lo relanza. + Se lanza al iniciar sesion mediante la tarea programada "OmniRoute Watchdog". + +.PARAMETER Once + Comprueba/arranca una sola vez, espera a que responda y sale (para pruebas). +.PARAMETER IntervalSeconds + Segundos entre comprobaciones del bucle de vigilancia (por defecto 120). +.PARAMETER StartupTimeoutSeconds + Segundos maximos de espera a que OmniRoute responda antes de considerarlo colgado. +.PARAMETER UseTray + Arranca OmniRoute con icono en la bandeja del sistema (por defecto NO, para maxima + fiabilidad en el arranque desatendido). + +.NOTES + Repo: biblioteca_negocio_prolongo. Regla 24 no aplica (no toca NET/VB/4GL). +#> +[CmdletBinding()] +param( + [switch]$Once, + [int]$IntervalSeconds = 120, + [int]$StartupTimeoutSeconds = 180, + [switch]$UseTray +) + +$ErrorActionPreference = 'SilentlyContinue' + +# --- Configuracion --------------------------------------------------------- +$OmniHost = '127.0.0.1' +$Port = 20128 +$NodeExe = 'C:\Program Files\nodejs\node.exe' +$OmniRouteMjs = Join-Path $env:APPDATA 'npm\node_modules\omniroute\bin\omniroute.mjs' +$WorkDir = $env:USERPROFILE # directorio neutro (sin .env de repos) +$LogDir = Join-Path $env:USERPROFILE '.omniroute' +$LogFile = Join-Path $LogDir 'watchdog.log' +$MaxLogBytes = 1MB + +# --- Utilidades ------------------------------------------------------------ +function Write-Log { + param([string]$Message, [string]$Level = 'INFO') + $line = '{0} [{1}] {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message + if (-not (Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir -Force | Out-Null } + if ((Test-Path $LogFile) -and ((Get-Item $LogFile).Length -gt $MaxLogBytes)) { + Move-Item -LiteralPath $LogFile -Destination "$LogFile.1" -Force + } + Add-Content -LiteralPath $LogFile -Value $line -Encoding UTF8 +} + +function Resolve-NodeExe { + if (Test-Path $NodeExe) { return $NodeExe } + $cmd = Get-Command node -ErrorAction SilentlyContinue + if ($cmd) { return $cmd.Source } + return $NodeExe +} + +function Test-OmniRoutePort { + $client = New-Object System.Net.Sockets.TcpClient + try { + $iar = $client.BeginConnect($OmniHost, $Port, $null, $null) + if ($iar.AsyncWaitHandle.WaitOne(2000)) { + $client.EndConnect($iar) + return $true + } + return $false + } catch { + return $false + } finally { + try { $client.Close() } catch { } + } +} + +function Get-OmniRouteProcesses { + Get-CimInstance Win32_Process -Filter "Name='node.exe'" -ErrorAction SilentlyContinue | + Where-Object { + $_.CommandLine -and + ($_.CommandLine -like '*omniroute*') -and + ($_.CommandLine -like '*serve*') + } +} + +function Start-OmniRouteServer { + $node = Resolve-NodeExe + if (-not (Test-Path $OmniRouteMjs)) { + Write-Log "No se encuentra omniroute.mjs en $OmniRouteMjs" 'ERROR' + return + } + $argList = @("`"$OmniRouteMjs`"", 'serve', '--no-open') + if ($UseTray) { $argList += '--tray' } else { $argList += '--no-tray' } + + Start-Process -FilePath $node ` + -ArgumentList $argList ` + -WorkingDirectory $WorkDir ` + -WindowStyle Hidden + Write-Log "Servidor OmniRoute lanzado (tray=$([bool]$UseTray))." +} + +function Wait-OmniRouteUp { + param([int]$TimeoutSeconds) + for ($i = 0; $i -lt $TimeoutSeconds; $i += 3) { + Start-Sleep -Seconds 3 + if (Test-OmniRoutePort) { return $true } + } + return $false +} + +# --- Bloqueo de instancia unica ------------------------------------------- +$mutex = New-Object System.Threading.Mutex($false, 'Local\OmniRouteWatchdog') +if (-not $mutex.WaitOne(0)) { + Write-Log 'Ya hay otro vigilante en ejecucion. Saliendo.' 'WARN' + exit 0 +} + +# --- Modo puntual (-Once) -------------------------------------------------- +if ($Once) { + if (Test-OmniRoutePort) { + Write-Log "Comprobacion puntual: OmniRoute ya responde en $Port." 'OK' + exit 0 + } + $procs = @(Get-OmniRouteProcesses) + if ($procs.Count -eq 0) { + Write-Log 'Comprobacion puntual: OmniRoute caido. Arrancando...' 'WARN' + Start-OmniRouteServer + } else { + Write-Log "Comprobacion puntual: proceso presente (PID $($procs[0].ProcessId)), esperando puerto..." 'WARN' + } + if (Wait-OmniRouteUp -TimeoutSeconds $StartupTimeoutSeconds) { + Write-Log "Comprobacion puntual: OmniRoute arriba en $Port." 'OK' + exit 0 + } + Write-Log "Comprobacion puntual: OmniRoute NO respondio en $StartupTimeoutSeconds s." 'ERROR' + exit 1 +} + +# --- Bucle de vigilancia --------------------------------------------------- +Write-Log 'Vigilante de OmniRoute iniciado.' +$downSince = $null + +while ($true) { + if (Test-OmniRoutePort) { + if ($downSince) { Write-Log "OmniRoute vuelve a responder en $Port." 'OK' } + $downSince = $null + } + else { + $procs = @(Get-OmniRouteProcesses) + if ($procs.Count -gt 0) { + if (-not $downSince) { + $downSince = Get-Date + Write-Log "Proceso OmniRoute presente (PID $($procs[0].ProcessId)) pero $Port aun no responde; esperando arranque (~60 s)..." + } + elseif (((Get-Date) - $downSince).TotalSeconds -gt $StartupTimeoutSeconds) { + Write-Log "OmniRoute lleva mas de $StartupTimeoutSeconds s sin responder. Matando procesos colgados y relanzando." 'WARN' + $procs | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } + Start-Sleep -Seconds 3 + Start-OmniRouteServer + $downSince = Get-Date + } + } + else { + Write-Log "OmniRoute CAIDO (puerto $Port cerrado). Arrancando servidor..." 'WARN' + Start-OmniRouteServer + $downSince = Get-Date + } + } + Start-Sleep -Seconds $IntervalSeconds +} diff --git a/opencode/omniroute-bootstrap.ts b/opencode/omniroute-bootstrap.ts new file mode 100644 index 0000000..0320da1 --- /dev/null +++ b/opencode/omniroute-bootstrap.ts @@ -0,0 +1,67 @@ +/** + * OmniRoute Bootstrap — plugin GLOBAL de OpenCode + * + * Objetivo: que al arrancar OpenCode (en CUALQUIER repositorio) se asegure de que + * OmniRoute esta levantado en localhost:20128, lanzando el vigilante en modo `-Once`. + * + * No depende de ningun repositorio: vive en la config global de OpenCode + * (~/.config/opencode/plugins/) y el vigilante canonico esta en el repo INFRAESTRUCTURA. + * + * Instalacion: `scripts/instalar_global.ps1` (copia este fichero a la carpeta global + * de plugins de OpenCode y registra la tarea de arranque). + * + * Nota: el vigilante ya corre como tarea programada al iniciar sesion; este plugin es + * la red de seguridad para cuando OpenCode arranca sin que el vigilante este vivo. + */ + +import type { Plugin } from "@opencode-ai/plugin" +import { spawn } from "node:child_process" +import { existsSync } from "node:fs" + +// --- Configuracion --------------------------------------------------------- + +// Vigilante canonico (repo INFRAESTRUCTURA). Se puede sobreescribir con OMNIROUTE_WATCHDOG. +const WATCHDOG = + process.env.OMNIROUTE_WATCHDOG ?? + "C:\\Users\\juanm\\Documents\\Github\\INFRAESTRUCTURA\\omniroute\\omniroute_watchdog.ps1" + +const POWERSHELL = + process.env.SystemRoot + ? `${process.env.SystemRoot}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe` + : "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" + +// Evita lanzar el vigilante mas de una vez por proceso de OpenCode. +let launched = false + +function ensureOmniRoute(): void { + if (launched) return + launched = true + try { + if (!existsSync(WATCHDOG)) return + const child = spawn( + POWERSHELL, + [ + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-WindowStyle", + "Hidden", + "-File", + WATCHDOG, + "-Once", + ], + { detached: true, stdio: "ignore", windowsHide: true }, + ) + child.unref() + } catch { + // Silencioso: el arranque de OpenCode no debe romperse por OmniRoute. + } +} + +// --- Plugin ---------------------------------------------------------------- + +export const OmniRouteBootstrap: Plugin = async () => { + ensureOmniRoute() + return {} +} diff --git a/scripts/infra_changelog.py b/scripts/infra_changelog.py new file mode 100644 index 0000000..864f863 --- /dev/null +++ b/scripts/infra_changelog.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +"""Changelog global de infraestructura (sobremesa <-> portatil <-> VPS). + +Fichero unico para saber que cambios de infraestructura se han hecho en cada equipo +y cuales hay que replicar en el otro. Fuente de verdad: `changelog_infra.json` +(en este repo). Se genera `CHANGELOG_INFRA.md` para lectura humana. + +Uso: + python scripts/infra_changelog.py add --titulo "..." --categoria omniroute \ + --transferible si --detalle "..." [--equipo sobremesa] [--id slug] + python scripts/infra_changelog.py pendientes [--json] + python scripts/infra_changelog.py aplicado [--equipo sobremesa] + python scripts/infra_changelog.py listar [--equipo X] + python scripts/infra_changelog.py estado + python scripts/infra_changelog.py render +""" + +from __future__ import annotations + +import argparse +import json +import platform +import re +import socket +import subprocess +import sys +from datetime import date +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +JSON_FILE = REPO / "changelog_infra.json" +MD_FILE = REPO / "CHANGELOG_INFRA.md" + +CATEGORIAS = ["omniroute", "opencode", "red", "servicios", "drivers", "memoria", "otros"] + + +# --- Deteccion de equipo --------------------------------------------------- + +def _powershell(query: str) -> str: + """Ejecuta una consulta CIM via PowerShell y devuelve la salida.""" + try: + out = subprocess.run( + ["powershell", "-NoProfile", "-NonInteractive", "-Command", query], + capture_output=True, text=True, timeout=15, + ).stdout + return out.strip() + except Exception: + return "" + + +def _huella_hardware() -> str: + """CPU + modelo del equipo (Windows). wmic ya no existe en Windows 11 nuevo.""" + cpu = _powershell("(Get-CimInstance Win32_Processor).Name") + modelo = _powershell("(Get-CimInstance Win32_ComputerSystem).Model") + if not cpu and not modelo: + cpu = platform.processor() or "" + return f"{cpu} | {modelo}".lower() + + +def detectar_equipo() -> str: + """Devuelve 'sobremesa', 'portatil' o el hostname si no se reconoce.""" + huella = _huella_hardware() + if "7900x" in huella or "b650" in huella: + return "sobremesa" + if "255hx" in huella or "vector" in huella or "ultra 7" in huella: + return "portatil" + return socket.gethostname().lower() + + +# --- Persistencia ---------------------------------------------------------- + +def cargar() -> dict: + if JSON_FILE.exists(): + return json.loads(JSON_FILE.read_text(encoding="utf-8")) + return {"version": 1, "entries": []} + + +def guardar(data: dict) -> None: + JSON_FILE.write_text( + json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" + ) + render(data) + + +def _slug(texto: str) -> str: + s = re.sub(r"[^a-z0-9]+", "-", texto.lower()).strip("-") + return s[:60] or "cambio" + + +# --- Render markdown ------------------------------------------------------- + +def render(data: dict | None = None) -> None: + data = data or cargar() + entries = data.get("entries", []) + lineas: list[str] = [] + lineas.append("# Changelog de infraestructura — global (sobremesa <-> portatil <-> VPS)") + lineas.append("") + lineas.append("> Fichero generado por `scripts/infra_changelog.py`. NO editar a mano.") + lineas.append("> Fuente de verdad: `changelog_infra.json`.") + lineas.append("") + lineas.append("## Pendientes de replicar") + lineas.append("") + pend = [e for e in entries if e.get("transferible") and len(e.get("aplicado_en", [])) < 2] + if pend: + for e in pend: + equipos = "|".join(e.get("aplicado_en", [])) or "-" + lineas.append(f"- **{e['id']}** [{e.get('categoria','otros')}] {e['titulo']} " + f"(aplicado en: {equipos})") + else: + lineas.append("_(nada pendiente)_") + lineas.append("") + lineas.append("## Historial") + lineas.append("") + for e in sorted(entries, key=lambda x: x.get("fecha", ""), reverse=True): + lineas.append(f"### {e.get('fecha','?')} · {e.get('titulo','')}") + lineas.append(f"- **id**: `{e['id']}`") + lineas.append(f"- **Equipo**: {e.get('equipo','?')}") + lineas.append(f"- **Categoria**: {e.get('categoria','otros')}") + lineas.append(f"- **Transferible**: {'si' if e.get('transferible') else 'no'}") + lineas.append(f"- **Aplicado en**: {', '.join(e.get('aplicado_en', [])) or '-'}") + if e.get("detalle"): + lineas.append(f"- **Detalle**: {e['detalle']}") + lineas.append("") + MD_FILE.write_text("\n".join(lineas), encoding="utf-8") + + +# --- Comandos -------------------------------------------------------------- + +def cmd_add(args: argparse.Namespace) -> int: + data = cargar() + equipo = args.equipo or detectar_equipo() + entrada_id = args.id or f"{date.today().isoformat()}-{_slug(args.titulo)}" + if any(e["id"] == entrada_id for e in data["entries"]): + print(f"Ya existe una entrada con id '{entrada_id}'.", file=sys.stderr) + return 1 + data["entries"].append({ + "id": entrada_id, + "fecha": args.fecha or date.today().isoformat(), + "equipo": equipo, + "categoria": args.categoria, + "titulo": args.titulo, + "transferible": args.transferible == "si", + "aplicado_en": [equipo], + "detalle": args.detalle or "", + }) + guardar(data) + print(f"Anadido '{entrada_id}' (equipo={equipo}, transferible={args.transferible}).") + return 0 + + +def _cargar_para_lectura() -> dict: + return cargar() + + +def cmd_pendientes(args: argparse.Namespace) -> int: + data = _cargar_para_lectura() + equipo = args.equipo or detectar_equipo() + pend = [ + e for e in data.get("entries", []) + if e.get("transferible") and equipo not in e.get("aplicado_en", []) + ] + if args.json: + print(json.dumps(pend, ensure_ascii=False, indent=2)) + return 0 + if not pend: + print(f"Sin pendientes de replicar en '{equipo}'.") + return 0 + print(f"Pendientes de replicar en '{equipo}':") + for e in pend: + print(f" - {e['id']} [{e.get('categoria')}] {e['titulo']} " + f"(origen: {e.get('equipo')}, aplicado en: {', '.join(e.get('aplicado_en', []))})") + return 0 + + +def cmd_aplicado(args: argparse.Namespace) -> int: + data = cargar() + equipo = args.equipo or detectar_equipo() + for e in data.get("entries", []): + if e["id"] == args.id: + if equipo not in e.get("aplicado_en", []): + e.setdefault("aplicado_en", []).append(equipo) + guardar(data) + print(f"Marcado '{args.id}' como aplicado en '{equipo}'.") + else: + print(f"'{args.id}' ya estaba aplicado en '{equipo}'.") + return 0 + print(f"No existe la entrada '{args.id}'.", file=sys.stderr) + return 1 + + +def cmd_listar(args: argparse.Namespace) -> int: + data = cargar() + entries = data.get("entries", []) + if args.equipo: + entries = [e for e in entries if e.get("equipo") == args.equipo] + for e in sorted(entries, key=lambda x: x.get("fecha", ""), reverse=True): + print(f"{e.get('fecha','?')} [{e.get('categoria','otros')}] {e['titulo']} " + f"(equipo={e.get('equipo')}, transf={'si' if e.get('transferible') else 'no'}, " + f"aplicado en: {', '.join(e.get('aplicado_en', [])) or '-'})") + return 0 + + +def cmd_estado(args: argparse.Namespace) -> int: + equipo = detectar_equipo() + data = cargar() + total = len(data.get("entries", [])) + pend = [ + e for e in data.get("entries", []) + if e.get("transferible") and equipo not in e.get("aplicado_en", []) + ] + print(f"Equipo actual: {equipo}") + print(f"Entradas totales: {total}") + print(f"Pendientes de replicar aqui: {len(pend)}") + return 0 + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser(description="Changelog global de infraestructura.") + sub = p.add_subparsers(dest="cmd", required=True) + + a = sub.add_parser("add", help="Anadir una entrada") + a.add_argument("--titulo", required=True) + a.add_argument("--categoria", default="otros", choices=CATEGORIAS) + a.add_argument("--transferible", default="si", choices=["si", "no"]) + a.add_argument("--detalle", default="") + a.add_argument("--equipo", default=None) + a.add_argument("--fecha", default=None) + a.add_argument("--id", default=None) + a.set_defaults(func=cmd_add) + + pe = sub.add_parser("pendientes", help="Ver pendientes de replicar en este equipo") + pe.add_argument("--equipo", default=None) + pe.add_argument("--json", action="store_true") + pe.set_defaults(func=cmd_pendientes) + + ap = sub.add_parser("aplicado", help="Marcar una entrada como aplicada en este equipo") + ap.add_argument("id") + ap.add_argument("--equipo", default=None) + ap.set_defaults(func=cmd_aplicado) + + li = sub.add_parser("listar", help="Listar entradas") + li.add_argument("--equipo", default=None) + li.set_defaults(func=cmd_listar) + + es = sub.add_parser("estado", help="Resumen") + es.set_defaults(func=cmd_estado) + + re_ = sub.add_parser("render", help="Regenerar CHANGELOG_INFRA.md") + re_.set_defaults(func=lambda a: (render(), 0)[1]) + + args = p.parse_args(argv) + return args.func(args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/instalar_global.ps1 b/scripts/instalar_global.ps1 new file mode 100644 index 0000000..d02843f --- /dev/null +++ b/scripts/instalar_global.ps1 @@ -0,0 +1,88 @@ +<# +.SYNOPSIS + Instala en el equipo actual la infraestructura global de OmniRoute + OpenCode. + +.DESCRIPTION + 1. Copia el plugin global `omniroute-bootstrap.ts` a la carpeta de plugins de OpenCode + (~/.config/opencode/plugins/). Asi OpenCode relanza OmniRoute al arrancar, en + cualquier repositorio. + 2. Registra/actualiza la tarea programada `OmniRoute Watchdog` apuntando al vigilante + canonico de este repo (INFRAESTRUCTURA). + 3. Arranca la tarea. + + No requiere administrador. Idempotente: se puede re-ejecutar tras un git pull. + +.NOTES + Repo: INFRAESTRUCTURA. +#> +[CmdletBinding()] +param( + [switch]$NoStart +) + +$ErrorActionPreference = 'Stop' + +$RepoRoot = Split-Path -Parent $PSScriptRoot +$TaskName = 'OmniRoute Watchdog' +$PwshExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' +$Watchdog = Join-Path $RepoRoot 'omniroute\omniroute_watchdog.ps1' +$PluginSrc = Join-Path $RepoRoot 'opencode\omniroute-bootstrap.ts' +$PluginDir = Join-Path $env:USERPROFILE '.config\opencode\plugins' +$PluginDst = Join-Path $PluginDir 'omniroute-bootstrap.ts' +$UserId = "$env:USERDOMAIN\$env:USERNAME" + +Write-Host '== Instalando infraestructura global (OmniRoute + OpenCode) ==' -ForegroundColor Cyan + +# 1) Plugin global de OpenCode --------------------------------------------- +if (-not (Test-Path $PluginDir)) { New-Item -ItemType Directory -Force -Path $PluginDir | Out-Null } +Copy-Item -LiteralPath $PluginSrc -Destination $PluginDst -Force +Write-Host " - Plugin global instalado en $PluginDst" -ForegroundColor Green + +# 2) Tarea programada ------------------------------------------------------ +if (-not (Test-Path $Watchdog)) { throw "No se encuentra el vigilante: $Watchdog" } + +if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) { + Write-Host ' - Eliminando tarea anterior...' -ForegroundColor Yellow + Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false +} + +$Action = New-ScheduledTaskAction ` + -Execute $PwshExe ` + -Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$Watchdog`"" ` + -WorkingDirectory $env:USERPROFILE + +$Trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME + +$Settings = New-ScheduledTaskSettingsSet ` + -AllowStartIfOnBatteries ` + -DontStopIfGoingOnBatteries ` + -StartWhenAvailable ` + -RestartCount 999 ` + -RestartInterval (New-TimeSpan -Minutes 1) ` + -MultipleInstances IgnoreNew +$Settings.ExecutionTimeLimit = 'PT0S' +$Settings.Hidden = $true + +$Principal = New-ScheduledTaskPrincipal -UserId $UserId -LogonType Interactive -RunLevel Limited + +Register-ScheduledTask ` + -TaskName $TaskName ` + -Action $Action ` + -Trigger $Trigger ` + -Settings $Settings ` + -Principal $Principal ` + -Description 'Arranca y vigila OmniRoute (localhost:20128) al iniciar sesion. Autocuracion.' ` + -Force | Out-Null + +Write-Host " - Tarea '$TaskName' registrada -> $Watchdog" -ForegroundColor Green + +# 3) Arrancar -------------------------------------------------------------- +if (-not $NoStart) { + Start-ScheduledTask -TaskName $TaskName + Write-Host ' - Vigilante arrancado.' -ForegroundColor Yellow +} + +Write-Host '' +Write-Host 'Listo. Comprobar:' -ForegroundColor Cyan +Write-Host " Get-ScheduledTask -TaskName '$TaskName' | Get-ScheduledTaskInfo" +Write-Host " Get-Content `"$env:USERPROFILE\.omniroute\watchdog.log`" -Tail 20"