Files

18 KiB

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 <ruta> [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:

# 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 '<ruta>', 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
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:

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):

    $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:

    $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
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
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:

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

@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
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:<PUERTO>/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).