#Requires -Version 5.1 <# .SYNOPSIS Windows entry point for the Vela development environment. .DESCRIPTION Runs natively on Windows (no WSL): the backend is `python -m vela` from the repo .venv, the frontend is Vite from web/. Mirrors ./dev.sh. .\dev.ps1 Start backend + frontend (Vite, with HMR) .\dev.ps1 backend Start the Vela server only (serves web/dist) .\dev.ps1 frontend Start Vite only, proxying to the backend port. Handy for CSS/UI-only work against an already running server. .\dev.ps1 build Build the dashboard into web/dist and stop .\dev.ps1 check Run the hub checks (npm --prefix web run check) .\dev.ps1 setup First-run setup: venv, dependencies, build Options: -BackendPort 7700 -FrontendPort 5173 -NoAutoPort Environment: VELA_BACKEND_PORT, VELA_FRONTEND_PORT, VELA_DATA_DIR, VELA_VENV, VELA_KILL_PORTS=0 to disable port cleanup. .EXAMPLE .\dev.ps1 Start backend on 7700 and Vite on 5173 against a disposable data dir. .EXAMPLE .\dev.ps1 frontend -BackendPort 7701 Start Vite, proxying API calls to a backend already running on 7701. #> $ErrorActionPreference = 'Stop' $ProjectRoot = $PSScriptRoot $WebDir = Join-Path $ProjectRoot 'web' $VenvDir = if ($env:VELA_VENV) { $env:VELA_VENV } else { Join-Path $ProjectRoot '.venv' } $DevDataDir = if ($env:VELA_DATA_DIR) { $env:VELA_DATA_DIR } else { Join-Path $ProjectRoot '.local\dev-data' } function Write-Header($text) { Write-Host Write-Host "=== $text ===" -ForegroundColor Cyan Write-Host } function Get-DevMode { foreach ($arg in $args) { if ($arg -in @('start', 'backend', 'frontend', 'build', 'check', 'setup')) { return $arg } } return 'start' } function Get-DevPort { param( [string[]]$Names, [string]$EnvName, [int]$Default, [string[]]$ArgList ) for ($i = 0; $i -lt $ArgList.Count; $i++) { $arg = [string]$ArgList[$i] foreach ($name in $Names) { if ($arg -eq $name -and ($i + 1) -lt $ArgList.Count) { return [int]$ArgList[$i + 1] } if ($arg.StartsWith("$name=")) { return [int]$arg.Substring($name.Length + 1) } } } $item = Get-Item -Path "Env:$EnvName" -ErrorAction SilentlyContinue if ($item -and $item.Value) { return [int]$item.Value } return $Default } function Test-NoAutoPort([string[]]$ArgList) { return ($ArgList -contains '--no-auto-port') -or ($ArgList -contains '-NoAutoPort') } function Test-PortFree([int]$Port) { $listener = $null try { $listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback, $Port) $listener.Start() return $true } catch { return $false } finally { if ($listener) { $listener.Stop() } } } function Stop-ListenersOnPort { param([int]$Port, [string]$Label) if ($env:VELA_KILL_PORTS -eq '0') { return } $listeners = @(Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique | Where-Object { $_ -and $_ -ne 0 -and $_ -ne $PID }) foreach ($processId in $listeners) { $proc = Get-Process -Id $processId -ErrorAction SilentlyContinue if (-not $proc) { continue } Write-Host "Stopping existing $Label listener on port $Port ($($proc.ProcessName) PID $processId)..." -ForegroundColor Yellow Stop-Process -Id $processId -Force -ErrorAction SilentlyContinue } } function Resolve-DevPort { param( [string]$Name, [int]$Preferred, [int[]]$Reserved = @(), [bool]$NoAutoPort = $false ) if ($Reserved -contains $Preferred) { throw "$Name port $Preferred is already reserved by another service in this launch." } if (Test-PortFree $Preferred) { return @{ Port = $Preferred; Changed = $false } } if ($NoAutoPort) { throw "$Name port $Preferred is already in use. Stop the other process or omit -NoAutoPort." } $upper = [Math]::Min($Preferred + 200, 65535) for ($candidate = $Preferred + 1; $candidate -le $upper; $candidate++) { if ($Reserved -contains $candidate) { continue } if (Test-PortFree $candidate) { return @{ Port = $candidate; Changed = $true } } } throw "Could not find a free $Name port near $Preferred." } function Find-Python { foreach ($name in @('python', 'python3')) { $cmd = Get-Command $name -ErrorAction SilentlyContinue if ($cmd) { & $cmd.Source -c 'import sys' 2>$null | Out-Null if ($LASTEXITCODE -eq 0) { return $cmd.Source } } } return $null } function Get-VenvPython { foreach ($candidate in @( (Join-Path $VenvDir 'Scripts\python.exe'), (Join-Path $VenvDir 'bin\python') )) { if (Test-Path $candidate) { & $candidate -c 'import sys' 2>$null | Out-Null if ($LASTEXITCODE -eq 0) { return $candidate } } } return $null } $script:VelaPy = $null function Ensure-Venv { $py = Get-VenvPython if ($py) { $script:VelaPy = $py; return } $systemPython = Find-Python if (-not $systemPython) { throw 'python is required for local development. Install Python 3.10+ and retry.' } Write-Host "Virtualenv missing or not runnable; creating $VenvDir" -ForegroundColor Yellow & $systemPython -m venv $VenvDir if ($LASTEXITCODE -ne 0) { throw 'Could not create the venv.' } $py = Get-VenvPython if (-not $py) { throw 'The venv was created but its Python is not runnable.' } $script:VelaPy = $py } function Get-RequirementsHash { $requirements = Join-Path $ProjectRoot 'requirements.txt' if (-not (Test-Path $requirements)) { return '' } return (Get-FileHash -Algorithm SHA256 $requirements).Hash.ToLower() } # Install when the venv cannot import the core packages, or when # requirements.txt changed since the last successful install. function Ensure-BackendPackages { $marker = Join-Path $VenvDir '.requirements-sha256' & $script:VelaPy -c 'import fastapi, uvicorn' 2>$null | Out-Null if ($LASTEXITCODE -eq 0) { $want = Get-RequirementsHash $have = if (Test-Path $marker) { (Get-Content $marker -Raw).Trim() } else { '' } if (-not $want -or $want -eq $have) { return } Write-Host 'requirements.txt changed since the last install; syncing...' -ForegroundColor Yellow } Write-Host "Installing Python dependencies into $VenvDir..." -ForegroundColor Yellow & $script:VelaPy -m pip install --upgrade pip if ($LASTEXITCODE -ne 0) { throw 'pip upgrade failed.' } & $script:VelaPy -m pip install -r (Join-Path $ProjectRoot 'requirements.txt') if ($LASTEXITCODE -ne 0) { throw 'pip install failed.' } $want = Get-RequirementsHash if ($want) { Set-Content -Path $marker -Value $want } } function Ensure-WebPackages { if (Test-Path (Join-Path $WebDir 'node_modules')) { return } Write-Host 'web/node_modules is missing; running npm ci...' -ForegroundColor Yellow & npm --prefix $WebDir ci if ($LASTEXITCODE -ne 0) { throw 'npm ci failed.' } } function Build-Dashboard { Ensure-WebPackages Write-Host 'Building the dashboard into web/dist...' -ForegroundColor Yellow & npm --prefix $WebDir run build if ($LASTEXITCODE -ne 0) { throw 'Dashboard build failed.' } } $mode = Get-DevMode @args $backendPort = Get-DevPort -Names @('--backend-port', '-BackendPort') -EnvName 'VELA_BACKEND_PORT' -Default 7700 -ArgList $args $frontendPort = Get-DevPort -Names @('--frontend-port', '-FrontendPort') -EnvName 'VELA_FRONTEND_PORT' -Default 5173 -ArgList $args $noAutoPort = Test-NoAutoPort $args $isHelp = $args -contains '-h' -or $args -contains '--help' if ($isHelp) { Get-Help $PSCommandPath -Detailed exit 0 } function Start-BackendProcess { param([int]$Port, [switch]$PassThru) Ensure-Venv Ensure-BackendPackages New-Item -ItemType Directory -Force -Path $DevDataDir | Out-Null $env:VELA_DATA_DIR = $DevDataDir $argv = @('-m', 'vela', '--no-open-browser', '--host', '127.0.0.1', '--port', "$Port") if ($PassThru) { return Start-Process -FilePath $script:VelaPy -ArgumentList $argv -WorkingDirectory $ProjectRoot -NoNewWindow -PassThru } & $script:VelaPy @argv exit $LASTEXITCODE } function Start-FrontendProcess { param([int]$Port, [string]$BackendUrl, [switch]$PassThru) Ensure-WebPackages $env:VELA_BACKEND_URL = $BackendUrl $argv = @('--prefix', $WebDir, 'run', 'dev', '--', '--host', '127.0.0.1', '--port', "$Port", '--strictPort') if ($PassThru) { return Start-Process -FilePath 'npm.cmd' -ArgumentList $argv -WorkingDirectory $ProjectRoot -NoNewWindow -PassThru } & npm.cmd @argv exit $LASTEXITCODE } switch ($mode) { 'backend' { Stop-ListenersOnPort -Port $backendPort -Label 'backend' $resolved = Resolve-DevPort -Name 'Backend' -Preferred $backendPort -NoAutoPort $noAutoPort Write-Header "Starting Vela backend (http://localhost:$($resolved.Port))" Write-Host " Data dir: $DevDataDir" if (-not (Test-Path (Join-Path $WebDir 'dist'))) { Write-Host ' Note: web/dist is missing; run .\dev.ps1 build or start the frontend for the dashboard.' -ForegroundColor Yellow } if ($resolved.Changed) { Write-Host " Note: backend port $backendPort was busy; using $($resolved.Port)." -ForegroundColor Yellow } Write-Host Start-BackendProcess -Port $resolved.Port } 'frontend' { Stop-ListenersOnPort -Port $frontendPort -Label 'frontend' $resolved = Resolve-DevPort -Name 'Frontend' -Preferred $frontendPort -NoAutoPort $noAutoPort Write-Header "Starting Vite (http://localhost:$($resolved.Port))" Write-Host " API target: http://localhost:$backendPort" Write-Host ' (expects a Vela backend there; .\dev.ps1 backend starts one)' if ($resolved.Changed) { Write-Host " Note: frontend port $frontendPort was busy; using $($resolved.Port)." -ForegroundColor Yellow } Write-Host Start-FrontendProcess -Port $resolved.Port -BackendUrl "http://localhost:$backendPort" } 'build' { Write-Header 'Building the dashboard' Build-Dashboard Write-Host Write-Host 'Done. web/dist is fresh; restart the backend (or reload the page) to see it.' -ForegroundColor Green } 'check' { Write-Header 'Vela checks' Ensure-WebPackages $venvPy = Get-VenvPython if ($venvPy -and -not $env:VELA_TEST_PYTHON) { $env:VELA_TEST_PYTHON = $venvPy } & npm --prefix $WebDir run check exit $LASTEXITCODE } 'setup' { Write-Header 'Vela first-run setup' Ensure-Venv Ensure-BackendPackages Ensure-WebPackages New-Item -ItemType Directory -Force -Path $DevDataDir | Out-Null Build-Dashboard Write-Host Write-Host 'Setup complete. Start the dev environment with .\dev.ps1' -ForegroundColor Green } 'start' { Stop-ListenersOnPort -Port $backendPort -Label 'backend' Stop-ListenersOnPort -Port $frontendPort -Label 'frontend' $backendResolved = Resolve-DevPort -Name 'Backend' -Preferred $backendPort -NoAutoPort $noAutoPort $frontendResolved = Resolve-DevPort -Name 'Frontend' -Preferred $frontendPort -Reserved @($backendResolved.Port) -NoAutoPort $noAutoPort $backendUrl = "http://localhost:$($backendResolved.Port)" Write-Host Write-Host 'Vela Dev Server' -ForegroundColor Cyan Write-Host " Dashboard (Vite, HMR): http://localhost:$($frontendResolved.Port)" Write-Host " Backend: $backendUrl" Write-Host " Health: $backendUrl/api/health" Write-Host " Data dir: $DevDataDir" if ($backendResolved.Changed) { Write-Host " Note: backend port $backendPort was busy; using $($backendResolved.Port)." -ForegroundColor Yellow } if ($frontendResolved.Changed) { Write-Host " Note: frontend port $frontendPort was busy; using $($frontendResolved.Port)." -ForegroundColor Yellow } Write-Host # Prepare dependencies before spawning, so both processes only serve. Ensure-Venv Ensure-BackendPackages Ensure-WebPackages New-Item -ItemType Directory -Force -Path $DevDataDir | Out-Null $env:VELA_DATA_DIR = $DevDataDir $backendProc = Start-Process -FilePath $script:VelaPy -NoNewWindow -PassThru -WorkingDirectory $ProjectRoot ` -ArgumentList @('-m', 'vela', '--no-open-browser', '--host', '127.0.0.1', '--port', "$($backendResolved.Port)") Start-Sleep -Seconds 2 $env:VELA_BACKEND_URL = $backendUrl $frontendProc = Start-Process -FilePath 'npm.cmd' -NoNewWindow -PassThru -WorkingDirectory $ProjectRoot ` -ArgumentList @('--prefix', $WebDir, 'run', 'dev', '--', '--host', '127.0.0.1', '--port', "$($frontendResolved.Port)", '--strictPort') Write-Host 'Press Ctrl+C to stop...' -ForegroundColor DarkGray try { Wait-Process -Id $backendProc.Id, $frontendProc.Id -Any -ErrorAction SilentlyContinue if (-not $backendProc.HasExited -and -not $frontendProc.HasExited) { # No process has exited yet; idle until one does or Ctrl+C lands. while (-not $backendProc.HasExited -and -not $frontendProc.HasExited) { Start-Sleep -Seconds 1 $backendProc.Refresh() $frontendProc.Refresh() } } Write-Host Write-Host 'One dev process stopped; shutting down the rest.' -ForegroundColor Yellow } finally { foreach ($proc in @($backendProc, $frontendProc)) { if ($proc -and -not $proc.HasExited) { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue } } # Vite spawns child node processes; make sure nothing keeps the port. Stop-ListenersOnPort -Port $frontendResolved.Port -Label 'frontend' } Write-Host 'Stopped.' } }