scripts/dev.ps1
Rama main · réplica de lectura del repositorio original.
<#
.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
}
}
}