Ctrl K
ComunidadGuías…

scripts/dev.ps1

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<#
.SYNOPSIS
    Start the full Maestro dev stack on Windows: infra (Docker), backend
    (FastAPI/uvicorn) and frontend (Next.js).

.DESCRIPTION
    - Brings up PostgreSQL, MongoDB and Qdrant via docker-compose (skippable).
    - Creates/uses the backend virtualenv, installs deps, runs Alembic migrations,
      seeds the featured marketplace teams and launches uvicorn in this terminal.
    - Installs frontend deps and launches `npm run dev` in this terminal.
    - Both services share the current terminal; press Ctrl+C to stop them.

.EXAMPLE
    ./scripts/dev.ps1
    ./scripts/dev.ps1 -SkipInfra
    ./scripts/dev.ps1 -SkipSeed
    ./scripts/dev.ps1 -BackendPort 8001 -FrontendPort 3001
#>
[CmdletBinding()]
param(
    [switch]$SkipInfra,
    [switch]$SkipSeed,
    [int]$BackendPort = 8000,
    [int]$FrontendPort = 3000
)

$ErrorActionPreference = 'Stop'
$RepoRoot = Split-Path -Parent $PSScriptRoot
$Backend = Join-Path $RepoRoot 'backend'
$Frontend = Join-Path $RepoRoot 'frontend'

function Write-Step($msg) { Write-Host "`n==> $msg" -ForegroundColor Cyan }

# Blocks until every compose service with a healthcheck reports healthy. Without
# this the script raced straight into pip/alembic while the four containers were
# still warming up, so the Docker memory spike and the Next.js compile — the two
# heaviest phases — landed in the same window and exhausted host RAM.
function Wait-Infra([int]$TimeoutSeconds = 90) {
    $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
    while ((Get-Date) -lt $deadline) {
        # `docker compose ps` emits one JSON object per line (not a JSON array).
        $lines = @(docker compose ps --format json 2>$null) | Where-Object { $_ }
        if ($lines) {
            $states = $lines | ForEach-Object {
                try { $_ | ConvertFrom-Json } catch { $null }
            } | Where-Object { $_ }
            # Qdrant declares no healthcheck; an empty Health means "running is enough".
            $pending = $states | Where-Object { $_.Health -and $_.Health -ne 'healthy' }
            if ($states.Count -gt 0 -and -not $pending) { return }
        }
        Start-Sleep -Seconds 2
    }
    Write-Warning "Infra did not report healthy within ${TimeoutSeconds}s; continuing anyway."
}

# Turbopack's persistent dev cache is disabled in next.config.ts, so .next should
# stay small. This is a backstop: a stale multi-GB .next gets memory-mapped on
# dev-server start and is exactly what produced `os error 1450` before.
function Clear-StaleNextCache([string]$Path, [int]$ThresholdMB = 600) {
    if (-not (Test-Path $Path)) { return }
    $bytes = (Get-ChildItem $Path -Recurse -File -Force -ErrorAction SilentlyContinue |
        Measure-Object -Property Length -Sum).Sum
    $sizeMB = [int]($bytes / 1MB)
    if ($sizeMB -lt $ThresholdMB) { return }
    Write-Warning "frontend/.next is ${sizeMB}MB (threshold ${ThresholdMB}MB); removing it to avoid an out-of-memory dev start."
    Remove-Item $Path -Recurse -Force -ErrorAction SilentlyContinue
}

function Clear-Port([int]$Port) {
    $conns = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue
    if (-not $conns) { return }
    $pids = $conns | Select-Object -ExpandProperty OwningProcess -Unique
    foreach ($procId in $pids) {
        $proc = Get-Process -Id $procId -ErrorAction SilentlyContinue
        if ($proc) {
            Write-Warning "Port $Port is in use by '$($proc.ProcessName)' (PID $procId); killing it."
            # /T kills the whole tree: dev servers spawn child processes
            taskkill /PID $procId /T /F | Out-Null
        }
    }
    Start-Sleep -Milliseconds 500
}

# --- 1. Infra (Docker) ----------------------------------------------------
if (-not $SkipInfra) {
    if (Get-Command docker -ErrorAction SilentlyContinue) {
        Write-Step 'Starting infra (Postgres, MongoDB, Qdrant) via docker-compose'
        Push-Location $RepoRoot
        try {
            docker compose up -d
            Write-Host 'Waiting for infra to become healthy...'
            Wait-Infra
        }
        catch { Write-Warning "docker compose failed: $_" }
        Pop-Location
    }
    else {
        Write-Warning 'Docker not found; skipping infra. Use -SkipInfra to silence this.'
    }
}

# --- 2. Backend -----------------------------------------------------------
Write-Step 'Preparing backend'
$Venv = Join-Path $Backend '.venv'
$VenvPy = Join-Path $Venv 'Scripts\python.exe'

# Backend requires Python >=3.11 (see CLAUDE.md). A venv built on an older
# interpreter cannot resolve pins such as alembic (needs >=3.10) and fails the
# pip install below with a confusing "no matching distribution" error, so the
# newest available interpreter is selected and a stale venv is recreated.
function Test-Py311 {
    param([string]$Exe, [string[]]$Pre)
    $probe = @($Pre) + @('-c', 'import sys; sys.exit(0 if sys.version_info >= (3, 11) else 1)')
    & $Exe @probe 2>$null
    return ($LASTEXITCODE -eq 0)
}

if ((Test-Path $VenvPy) -and (-not (Test-Py311 $VenvPy @()))) {
    Write-Host 'Existing virtualenv uses an unsupported Python; recreating...'
    Remove-Item -Recurse -Force $Venv
}

if (-not (Test-Path $VenvPy)) {
    $candidates = New-Object System.Collections.ArrayList
    if (Get-Command py -ErrorAction SilentlyContinue) {
        foreach ($v in '3.13', '3.12', '3.11') { [void]$candidates.Add(@{ Exe = 'py'; Pre = @("-$v") }) }
        [void]$candidates.Add(@{ Exe = 'py'; Pre = @('-3') })
    }
    foreach ($name in 'python3.13', 'python3.12', 'python3.11', 'python', 'python3') {
        if (Get-Command $name -ErrorAction SilentlyContinue) { [void]$candidates.Add(@{ Exe = $name; Pre = @() }) }
    }
    $chosen = $null
    foreach ($c in $candidates) { if (Test-Py311 $c.Exe $c.Pre) { $chosen = $c; break } }
    if ($null -eq $chosen) {
        Write-Error 'Python >=3.11 is required but was not found. Install it from python.org and re-run.'
        exit 1
    }
    Write-Host "Creating virtualenv with $($chosen.Exe) $($chosen.Pre -join ' ')..."
    $createArgs = @($chosen.Pre) + @('-m', 'venv', $Venv)
    & $chosen.Exe @createArgs
}

if (-not (Test-Path (Join-Path $Backend '.env'))) {
    Copy-Item (Join-Path $RepoRoot '.env.example') (Join-Path $Backend '.env')
    Write-Warning 'Created backend/.env from .env.example — set real secrets before production.'
}

Write-Host 'Installing backend dependencies...'
& $VenvPy -m pip install --disable-pip-version-check -q -r (Join-Path $Backend 'requirements.txt')

Write-Host 'Running database migrations (alembic upgrade head)...'
# Must run with the backend as CWD: alembic.ini resolves both `script_location`
# and `prepend_sys_path` relative to the working directory, not to the ini file.
# Passing -c alone is not enough — invoking this script from scripts/ made
# alembic look for ./alembic and fail with "Path doesn't exist: alembic".
Push-Location $Backend
try { & $VenvPy -m alembic -c 'alembic.ini' upgrade head }
catch { Write-Warning "Alembic migration failed (is Postgres up?): $_" }
finally { Pop-Location }

if (-not $SkipSeed) {
    Write-Host 'Seeding featured marketplace teams (idempotent)...'
    Push-Location $Backend
    # Seeding is cosmetic: a missing Mongo must not stop the dev stack coming up.
    try { & $VenvPy -m app.scripts.seed_marketplace }
    catch { Write-Warning "Marketplace seed failed (is MongoDB up?): $_" }
    finally { Pop-Location }
}

Clear-Port $BackendPort
Write-Step "Launching backend on http://localhost:$BackendPort"
$BackendProc = Start-Process -FilePath $VenvPy `
    -ArgumentList '-m', 'uvicorn', 'app.main:app', '--reload', '--reload-dir', 'app', '--port', "$BackendPort" `
    -WorkingDirectory $Backend -NoNewWindow -PassThru

# --- 3. Frontend ----------------------------------------------------------
Write-Step 'Preparing frontend'
if (-not (Test-Path (Join-Path $Frontend 'node_modules'))) {
    Write-Host 'Installing frontend dependencies...'
    Push-Location $Frontend
    cmd.exe /c 'npm install'
    Pop-Location
}

if (-not (Test-Path (Join-Path $Frontend '.env.local'))) {
    Copy-Item (Join-Path $Frontend '.env.local.example') (Join-Path $Frontend '.env.local')
}

Clear-StaleNextCache (Join-Path $Frontend '.next')

Clear-Port $FrontendPort
Write-Step "Launching frontend on http://localhost:$FrontendPort"
# Bound the V8 heap so a runaway dev server dies with a heap error instead of
# dragging the whole host into swap. This covers the JS side only — Turbopack's
# native memory-mapped cache is outside V8, which is why the real fix is
# `turbopackFileSystemCacheForDev: false` in next.config.ts.
$PrevNodeOptions = $env:NODE_OPTIONS
$env:NODE_OPTIONS = '--max-old-space-size=4096'
$FrontendProc = Start-Process -FilePath 'cmd.exe' `
    -ArgumentList '/c', "npm run dev -- --port $FrontendPort" `
    -WorkingDirectory $Frontend -NoNewWindow -PassThru
# The child has inherited it; don't leave it set in the caller's session.
$env:NODE_OPTIONS = $PrevNodeOptions

Write-Host "`nMaestro is starting:" -ForegroundColor Green
Write-Host "  Backend : http://localhost:$BackendPort  (docs: /docs)"
Write-Host "  Frontend: http://localhost:$FrontendPort"
Write-Host 'Both services run in this terminal; press Ctrl+C to stop them.'

# --- 4. Wait & cleanup ------------------------------------------------------
try {
    Wait-Process -Id $BackendProc.Id, $FrontendProc.Id -ErrorAction SilentlyContinue
}
finally {
    # Kill whole process trees: uvicorn --reload and npm spawn child processes
    foreach ($proc in @($BackendProc, $FrontendProc)) {
        if ($proc -and -not $proc.HasExited) {
            taskkill /PID $proc.Id /T /F | Out-Null
        }
    }
}