20-sep: infra global - OmniRoute watchdog + plugin OpenCode + changelog sobremesa/portatil

This commit is contained in:
juanminsanz
2026-09-20 11:59:56 +02:00
commit 49bdf1ac76
8 changed files with 806 additions and 0 deletions

36
CHANGELOG_INFRA.md Normal file
View File

@@ -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.

46
README.md Normal file
View File

@@ -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 <id>
# 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`.

41
changelog_infra.json Normal file
View File

@@ -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."
}
]
}

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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 {}
}

256
scripts/infra_changelog.py Normal file
View File

@@ -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 <id> [--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())

View File

@@ -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"