diff --git a/docs/superpowers/plans/2026-08-22-bat-doctor.md b/docs/superpowers/plans/2026-08-22-bat-doctor.md new file mode 100644 index 0000000..1ce24a0 --- /dev/null +++ b/docs/superpowers/plans/2026-08-22-bat-doctor.md @@ -0,0 +1,409 @@ +# Arranque automático e inteligente (.bat doctor) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Doble clic en `start-server.bat` abre siempre la aplicación: el nuevo `doctor.ps1` detecta y repara el entorno (Python falso/faltante, dependencias, ffmpeg) antes de delegar al launcher existente. + +**Architecture:** Separación entorno vs ciclo de vida. Nuevo `scripts/doctor.ps1` prepara el entorno y llama a `scripts/start-server.ps1` (lógica de puertos/healthz/browser intacta, único cambio: parámetro `-PythonExe`). Los `.bat` ganan regla de ventana: error → ventana persistente con log; éxito → cierre automático. + +**Tech Stack:** Windows PowerShell 5.1 (sin PS7), cmd batch, winget como instalador de Python. + +**Spec:** `docs/superpowers/specs/2026-08-22-bat-doctor-design.md` + +## Global Constraints + +- PS 5.1 estricto: sin operador ternario, sin `??`, sin `-AsHashtable`. Compatibilidad obligatoria. +- Mensajes **sin tildes** (convención de los scripts existentes: "automaticamente", "no se encontro") para sobrevivir cambios de codepage. +- El doctor NUNCA importa `yt_scraper.webapp.app` (abre la DB real del proyecto y corre migraciones/backfill — gotcha documentado en CLAUDE.md). El import-test usa módulos terceros directamente. +- Lógica existente de puertos/healthz/cancelación en `start-server.ps1`: intocable. Únicos cambios permitidos: parámetro `-PythonExe` (default `python`) y copys. +- Sin dependencias nuevas; winget es la única vía de instalación de Python. +- Exit codes doctor: 0 ok · 10 sin Python instalable · 11 deps no reparadas · resto propagado del launcher. + +--- + +### Task 1: scripts/doctor.ps1 — diagnóstico y reparación de entorno + +**Files:** +- Create: `scripts/doctor.ps1` + +**Interfaces:** +- Consumes: `.run/server.info` (línea `PORT PID TIMESTAMP`), ejecutables del sistema (`py`, `python`, `python3`, `winget`, `ffmpeg`). +- Produces: invoca `scripts/start-server.ps1 -PythonExe [args...]` pasando args posicionales restantes. Exit codes 0/10/11 según Global Constraints. Flag `-CheckOnly`: solo diagnostica, no muta nada, mismo esquema de exit codes. + +- [ ] **Step 1: Escribir doctor.ps1 completo** + +Crear `scripts/doctor.ps1` con este contenido exacto: + +```powershell +# doctor.ps1 -- prepara el entorno para arrancar yt-scraper y delega en +# start-server.ps1. Pensado para usuarios no tecnicos: doble clic y listo. +# +# Uso: powershell -File doctor.ps1 [-CheckOnly] [args para start-server] +# -CheckOnly solo diagnostica e informa que haria; no instala ni abre nada. +# +# Exit codes: 0 ok | 10 sin Python instalable | 11 dependencias no reparadas +# (cualquier otro codigo viene propagado de start-server.ps1) + +param( + [switch]$CheckOnly +) + +$ErrorActionPreference = 'Stop' +$root = Split-Path -Parent $PSScriptRoot +Set-Location -LiteralPath $root +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} + +function Out-Line($msg, $color = $null) { + if ($color) { Write-Host $msg -ForegroundColor $color } + else { Write-Host $msg } + [Console]::Out.Flush() +} + +function Test-PortOpen($port, $timeoutMs = 350) { + $client = New-Object System.Net.Sockets.TcpClient + try { + $iar = $client.BeginConnect('127.0.0.1', [int]$port, $null, $null) + if (-not $iar.AsyncWaitHandle.WaitOne($timeoutMs)) { return $false } + $client.EndConnect($iar) + return $true + } catch { return $false } + finally { $client.Close() } +} + +# Devuelve la ruta del exe si el candidato es un Python >= 3.10 REAL; +# $null si no existe, es el stub de la Microsoft Store o es demasiado viejo. +function Resolve-PythonCandidate($tokens) { + $name = $tokens[0] + $cmd = Get-Command $name -ErrorAction SilentlyContinue + if (-not $cmd) { return $null } + # Stub de la Microsoft Store: vive en WindowsApps y no ejecuta nada. + if ($cmd.Source -and $cmd.Source -like '*WindowsApps*') { return $null } + try { + $out = & $cmd.Source @($tokens | Select-Object -Skip 1) --version 2>&1 + if ($LASTEXITCODE -ne 0) { return $null } + $s = ($out | Out-String).Trim() + if ($s -notmatch '^Python\s+(\d+)\.(\d+)') { return $null } + if ([int]$Matches[1] -lt 3 -or ([int]$Matches[1] -eq 3 -and [int]$Matches[2] -lt 10)) { return $null } + return $cmd.Source + } catch { return $null } +} + +Out-Line "" +Out-Line " [yt-scraper] verificando tu sistema..." Cyan + +# --- (1) Server ya vivo? Abrir navegador y terminar. ----------------------- +if (Test-Path '.run\server.info') { + $parts = ((Get-Content '.run\server.info' -TotalCount 1) -split '\s+') + if ($parts.Count -ge 1 -and $parts[0] -match '^\d+$' -and (Test-PortOpen ([int]$parts[0]) 350)) { + Out-Line " [ok] El servidor ya estaba corriendo - abriendo..." Green + Out-Line " http://127.0.0.1:$($parts[0])" + if (-not $CheckOnly) { try { Start-Process "http://127.0.0.1:$($parts[0])" } catch {} } + exit 0 + } +} + +# --- (2) Resolver Python ---------------------------------------------------- +$candidates = @(,@('py','-3')) + @(,@('python')) + @(,@('python3')) +$pythonExe = $null +foreach ($cand in $candidates) { + $pythonExe = Resolve-PythonCandidate $cand + if ($pythonExe) { break } +} + +if (-not $pythonExe) { + Out-Line " [info] Python no esta instalado. Instalandolo automaticamente (~25 MB)..." Yellow + if ($CheckOnly) { + Out-Line " [check] AQUI: winget install Python.Python.3.12 + busqueda de ruta nueva" DarkGray + exit 10 + } + $winget = Get-Command winget -ErrorAction SilentlyContinue + if (-not $winget) { + Out-Line " [!] No pude instalar Python automaticamente (falta winget)." Red + Out-Line " Abriendo la pagina de descarga de Python..." Gray + Out-Line " Instalalo (marca 'Add python.exe to PATH') y vuelve a hacer doble clic." Gray + try { Start-Process 'https://www.python.org/downloads/' } catch {} + exit 10 + } + & winget install --id Python.Python.3.12 --silent --accept-package-agreements --accept-source-agreements | Out-Null + if ($LASTEXITCODE -ne 0) { + Out-Line " [!] La instalacion de Python fallo (codigo $LASTEXITCODE). Reinstala manualmente:" Red + Out-Line " https://www.python.org/downloads/" Gray + exit 10 + } + # El PATH de esta sesion no se refresca: sondear rutas conocidas. + $fresh = @() + $fresh += Get-ChildItem "$env:LOCALAPPDATA\Programs\Python\Python3*\python.exe" -ErrorAction SilentlyContinue + $fresh += Get-ChildItem 'C:\Program Files\Python3*\python.exe' -ErrorAction SilentlyContinue + foreach ($f in ($fresh | Sort-Object FullName -Descending)) { + $pythonExe = Resolve-PythonCandidate @($f.FullName) + if ($pythonExe) { break } + } + if (-not $pythonExe) { $pythonExe = Resolve-PythonCandidate @('py','-3') } + if (-not $pythonExe) { + Out-Line " [!] Se instalo Python pero no lo encuentro. Cierra esta ventana," Red + Out-Line " abre una nueva e intenta de nuevo (el PATH se refresca al reabrir)." Gray + exit 10 + } +} +Out-Line " [ok] Python encontrado: $pythonExe" DarkGray + +# --- (3) Dependencias -------------------------------------------------------- +$importTest = 'import fastapi, uvicorn, sse_starlette, jinja2, yaml, yt_dlp, requests, slugify' +$depsOk = $false +try { + & $pythonExe -c $importTest *> $null + $depsOk = ($LASTEXITCODE -eq 0) +} catch { $depsOk = $false } + +if (-not $depsOk) { + if ($CheckOnly) { + Out-Line " [check] AQUI: instalar dependencias -> & '$pythonExe' -m pip install -e `".[web]`"" DarkGray + exit 11 + } + Out-Line " [info] Instalando las piezas que faltan por primera vez (puede tardar 1-2 min)..." Yellow + try { + & $pythonExe -m pip --version *> $null + if ($LASTEXITCODE -ne 0) { & $pythonExe -m ensurepip --upgrade | Out-Null } + } catch { + & $pythonExe -m ensurepip --upgrade | Out-Null + } + & $pythonExe -m pip install -e ".[web]" + if ($LASTEXITCODE -ne 0) { + Out-Line " [!] No pude instalar las dependencias (revisa tu conexion a internet)" Red + Out-Line " y vuelve a hacer doble clic en start-server.bat" Gray + exit 11 + } +} +if ($CheckOnly) { Out-Line " [check] dependencias: OK" DarkGray } + +# --- (4) ffmpeg (opcional: solo audio) -------------------------------------- +if (-not (Get-Command ffmpeg -ErrorAction SilentlyContinue)) { + Out-Line " [aviso] La descarga de AUDIO no estara disponible (falta ffmpeg)." Yellow + Out-Line " Todo lo demas funciona perfecto. Puedes ignorarlo." Gray +} + +# --- (5) Delegar al launcher ------------------------------------------------- +if ($CheckOnly) { + Out-Line " [check] AQUI: lanzaria scripts\start-server.ps1 -PythonExe '$pythonExe'" DarkGray + Out-Line "" + Out-Line " Diagnostico completo. Todo listo para arrancar." Green + exit 0 +} + +Out-Line "" +& (Join-Path $PSScriptRoot 'start-server.ps1') -PythonExe $pythonExe @args +exit $LASTEXITCODE +``` + +- [ ] **Step 2: Verificar sintaxis y modo diagnóstico** + +Run: `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\doctor.ps1 -CheckOnly` +Expected: líneas `[yt-scraper] verificando tu sistema...`, `[ok] Python encontrado: ...`, `[check] dependencias: OK` o `[check] AQUI: instalar...`, `[check] AQUI: lanzaria ... start-server.ps1 -PythonExe ''`, `Diagnostico completo.` — y **ningún efecto colateral** (sin browser, sin installs). Exit 0 si el entorno actual está sano. + +Nota: el server sigue vivo en 8001 de la sesión anterior → si `.run/server.info` apunta ahí, la salida esperada es `[ok] El servidor ya estaba corriendo - abriendo...` **sin abrir navegador** (por -CheckOnly) y exit 0. Ambas salidas son correctas; registrar cuál ocurrió. + +- [ ] **Step 3: Commit** + +```bash +git add scripts/doctor.ps1 +git commit -m "feat: doctor.ps1 prepara entorno (python/deps) antes del arranque" +``` + +### Task 2: start-server.ps1 — parámetro -PythonExe + pulido de copys + +**Files:** +- Modify: `scripts/start-server.ps1` (línea ~1 param block nuevo; línea ~131 uso del parámetro; copys de mensajes visibles) + +**Interfaces:** +- Consumes: nada nuevo. +- Produces: `param([string]$PythonExe = 'python')` — consumido por doctor.ps1 Task 1 vía `-PythonExe $pythonExe`. + +- [ ] **Step 1: Añadir param block** + +Al inicio del archivo (antes de `$ErrorActionPreference`), añadir: + +```powershell +param( + # Ruta absoluta o nombre del interprete Python que lanza uvicorn. + # doctor.ps1 la resuelve porque tras una instalacion fresca el PATH + # de esta sesion aun no ve el Python nuevo. + [string]$PythonExe = 'python' +) +``` + +- [ ] **Step 2: Usar el parámetro en la línea de uvicorn** + +Reemplazar (línea ~131): + +```powershell + $cmdLine = '/c python -m uvicorn yt_scraper.webapp.app:app --host 127.0.0.1 --port ' + $candidate + ' --log-level info > ".run\server.log" 2>&1' +``` + +por: + +```powershell + $cmdLine = '/c "' + $PythonExe + '" -m uvicorn yt_scraper.webapp.app:app --host 127.0.0.1 --port ' + $candidate + ' --log-level info > ".run\server.log" 2>&1' +``` + +(Comillas alrededor de la ruta: necesarios si contiene espacios.) + +- [ ] **Step 3: Pulir copys (solo texto, cero lógica)** + +Sustituir estos mensajes por versión amigable (mantener colores y estructura): + +| Original | Nuevo | +|---|---| +| `" [yt-scraper] iniciando servidor local..." Cyan` | igual (ya es claro) | +| `" [info] server.info apuntaba a puerto={0} pid={1} pero esta obsoleto (portBusy=False pidAlive={2}); limpiando."` | `" [info] Habia un registro viejo de otra sesion; limpiandolo..." Yellow` | +| `" [scan] puertos candidatos: $($candidates -join ', ')" DarkGray` | eliminar línea (ruido técnico) | +| `" [try] lanzando uvicorn en puerto $candidate ..." Gray` | `" [...] Encendiendo el servidor (intento $candidate)..." Gray` | +| `" cmd PID=$($proc.Id) -> ventana minimizada (.run\server.log)"` | `" El servidor corre en segundo plano. Registro: .run\server.log"` | +| `" [warn] puerto $candidate no respondio en ...s; probando siguiente"` | `" [warn] Este intento no respondio; probando el siguiente..." Yellow` | +| `" [ok] puerto $port aceptando conexiones (t=$([int]$boundAt.TotalSeconds)s)" Green` | `" [ok] Servidor encendido." Green` | +| `" (esperando /healthz para confirmar arranque completo...)" DarkGray` | `" Confirmando que todo cargo bien..." DarkGray` | +| `" [warn] puerto abierto pero /healthz no respondio; revisa .run\server.log" Yellow` | `" [warn] El servidor abrio pero tardo en responder; revisa .run\server.log" Yellow` | + +Las líneas `URL:` / `log:` / `stop:` se mantienen (útiles también para no técnicos). + +- [ ] **Step 4: Verificar camino feliz con server vivo** + +Run: `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-server.ps1` +Expected: `[ok] servidor ya activo en puerto 8001` + apertura de navegador + exit 0. (El server sigue corriendo desde la sesión anterior.) + +- [ ] **Step 5: Commit** + +```bash +git add scripts/start-server.ps1 +git commit -m "feat: start-server.ps1 acepta -PythonExe y copys amigables" +``` + +### Task 3: stop-server.ps1 — pulido de copys (sin lógica nueva) + +**Files:** +- Modify: `scripts/stop-server.ps1` (solo strings de Out-Line) + +**Interfaces:** +- Consumes/Produces: nada cambia funcionalmente. + +- [ ] **Step 1: Pulir copys** + +| Original | Nuevo | +|---|---| +| `" [yt-scraper] deteniendo servidor (PID $pidv, puerto $port)..." Cyan` | `" [yt-scraper] Apagando el servidor..." Cyan` | +| `" [kill] puerto $port ocupado por PID $($_.OwningProcess); terminando..." Gray` | `" [kill] Cerrando un proceso que quedaba suelto..." Gray` | +| `" [yt-scraper] no hay servidor registrado. Buscando procesos uvicorn sueltos..." Cyan` | `" [yt-scraper] Buscando servidores que quedaron sueltos..." Cyan` | +| `" [info] el PID $pidv ya no existe; el servidor estaba muerto." Yellow` | `" [info] El servidor ya estaba apagado." Yellow` | +| `" [warn] el puerto $port sigue ocupado; revisa procesos python manualmente." Yellow` | `" [warn] Algo sigue ocupando la conexion. Reinicia la PC si vuelve a pasar." Yellow` | + +- [ ] **Step 2: Verificar stop limpio** + +Run: `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\stop-server.ps1` +Expected: `Apagando el servidor...` → `puerto liberado, servidor detenido.` y el proceso uvicorn de 8001 muerto (`Invoke-WebRequest http://127.0.0.1:8001/api/dashboard` falla). + +- [ ] **Step 3: Commit** + +```bash +git add scripts/stop-server.ps1 +git commit -m "chore: copys amigables en stop-server.ps1" +``` + +### Task 4: Reglas de ventana en los .bat + +**Files:** +- Modify: `start-server.bat` +- Modify: `stop-server.bat` + +**Interfaces:** +- Consumes: exit codes de doctor.ps1 (Task 1) y stop-server.ps1. + +- [ ] **Step 1: Reescribir start-server.bat** + +Contenido exacto: + +```bat +@echo off +chcp 65001 >nul +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\doctor.ps1" %* +set EC=%errorlevel% +if not "%EC%"=="0" ( + echo. + echo ^[!] Algo fallo. Esta ventana queda abierta para que puedas leerlo. + echo Si llamas a alguien por ayuda, enviale una captura de pantalla. + echo. + pause +) else ( + timeout /t 5 /nobreak >nul +) +exit /b %EC% +``` + +- [ ] **Step 2: Reescribir stop-server.bat** + +Contenido exacto: + +```bat +@echo off +chcp 65001 >nul +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\stop-server.ps1" %* +set EC=%errorlevel% +if not "%EC%"=="0" ( + echo. + echo ^[!] Algo fallo al apagar. Esta ventana queda abierta. + echo. + pause +) +exit /b %EC% +``` + +(Éxito en stop → cierra de inmediato: apagar es instantáneo, no hace falta resumen.) + +- [ ] **Step 3: Verificar reglas de ventana simulando error** + +Run (en cmd, PATH vacío para forzar rama sin Python): +`cmd /c "set PATH=C:\Windows\System32 && start-server.bat"` +Expected: ventana muestra mensaje de instalación de Python/winget y queda abierta en `Presione una tecla...`. (Verificación manual del executor: lanzar con `cmd /c` captura el texto; confirmar que `pause` está presente en el flujo de error.) + +Nota: en este entorno el executor valida la rama de éxito automáticamente (Task 5); la rama de error se valida leyendo el flujo: `EC≠0` → `pause`. + +- [ ] **Step 4: Commit** + +```bash +git add start-server.bat stop-server.bat +git commit -m "feat: .bat con ventanas inteligentes (persistente en error, autocierre en exito)" +``` + +### Task 5: Verificación end-to-end completa + +**Files:** ninguno nuevo (verificación). + +**Interfaces:** +- Consumes: todo lo anterior. + +- [ ] **Step 1: Estado inicial limpio** + +Confirmar sin servidor vivo: `powershell -NoProfile -Command "Test-NetConnection -ComputerName 127.0.0.1 -Port 8001 -InformationLevel Quiet -WarningAction SilentlyContinue"` +Expected: `False`. (Si algo vive en 8000-8100, ejecutar `scripts\stop-server.ps1` primero.) + +- [ ] **Step 2: Arranque completo vía doctor** + +Run: `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\doctor.ps1` +Expected: secuencia `[ok] Python encontrado` → `[check]/dependencias OK implícito` → salida del launcher `Servidor encendido` en algún puerto 8000-8100 → `Confirmando que todo cargo bien...` + `/healthz OK`. Exit 0. + +- [ ] **Step 3: Health check externo** + +Run: `powershell -NoProfile -Command "(Invoke-WebRequest -UseBasicParsing http://127.0.0.1:/healthz -TimeoutSec 5).StatusCode"` +Expected: `200`. + +- [ ] **Step 4: Segunda invocación = camino rápido** + +Run: `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\doctor.ps1` +Expected: `[ok] El servidor ya estaba corriendo - abriendo...` + navegador abierto + exit 0, en < 2 s. + +- [ ] **Step 5: Dejar el servidor corriendo para el usuario + informe final** + +No apagar al terminar: el usuario quedó con la app funcionando. Informar URL final y qué cambió. + +- [ ] **Step 6: Sin commit** (no hay archivos nuevos; verificar `git status` limpio) + +Run: `git status --short` +Expected: sin cambios pendientes (todo commiteado en Tasks 1-4).