param( [string]$Workspace = "D:\workspace\vignette", [int]$ApiPort = 8001, [int]$EnginePort = 9099, [string]$Python = "$env:LOCALAPPDATA\Programs\Python\Python311\python.exe", [string]$Cloudflared = "$env:LOCALAPPDATA\Microsoft\WinGet\Links\cloudflared.exe", [string]$CloudflaredConfig = "$env:USERPROFILE\.cloudflared\vignette-config.yml", [switch]$SkipEngineRestart, [switch]$SkipCloudflaredRestart ) $ErrorActionPreference = "Stop" $ApiDir = Join-Path $Workspace "apps\api" $OutLog = Join-Path $ApiDir "api.public.out.log" $ErrLog = Join-Path $ApiDir "api.public.err.log" $EngineOutLog = Join-Path $ApiDir "engine.public.out.log" $EngineErrLog = Join-Path $ApiDir "engine.public.err.log" function Get-JsonHealth { param( [string]$Uri, [int]$TimeoutSec = 5 ) try { Invoke-RestMethod -Uri $Uri -TimeoutSec $TimeoutSec } catch { $null } } function Wait-JsonHealth { param( [string]$Uri, [scriptblock]$IsHealthy, [int]$TimeoutSec = 30 ) $deadline = (Get-Date).AddSeconds($TimeoutSec) do { $health = Get-JsonHealth -Uri $Uri -TimeoutSec 5 if ($null -ne $health -and (& $IsHealthy $health)) { return $health } Start-Sleep -Seconds 1 } while ((Get-Date) -lt $deadline) throw "Timed out waiting for healthy response from $Uri" } function Stop-UvicornByPort { param( [string]$AppImport, [int]$Port ) Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -and $_.CommandLine -like "*uvicorn $AppImport*" -and $_.CommandLine -like "*--port $Port*" } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force } } if (!(Test-Path $Python)) { throw "Python 3.11 not found at $Python" } if (!(Test-Path $ApiDir)) { throw "API directory not found at $ApiDir" } $engineHealth = Get-JsonHealth -Uri "http://127.0.0.1:$EnginePort/health" if ($SkipEngineRestart) { if ($null -eq $engineHealth -or -not $engineHealth.ok) { throw "Engine gateway is not healthy on http://127.0.0.1:$EnginePort/health" } } elseif ($null -eq $engineHealth -or -not $engineHealth.ok) { Stop-UvicornByPort -AppImport "engine_gateway.gateway:app" -Port $EnginePort Start-Process -WindowStyle Hidden -FilePath $Python ` -ArgumentList @("-m", "uvicorn", "engine_gateway.gateway:app", "--host", "127.0.0.1", "--port", "$EnginePort") ` -WorkingDirectory $ApiDir ` -RedirectStandardOutput $EngineOutLog ` -RedirectStandardError $EngineErrLog ` -PassThru | Out-Null $engineHealth = Wait-JsonHealth ` -Uri "http://127.0.0.1:$EnginePort/health" ` -IsHealthy { param($health) $health.ok -eq $true } ` -TimeoutSec 30 } Stop-UvicornByPort -AppImport "app.main:app" -Port $ApiPort $env:ENVIRONMENT = "prod" $env:ENGINE_URL = "http://127.0.0.1:$EnginePort" $env:ENGINE_MODE = "claude_cli" $env:AUTH_DEV_LOGIN_ENABLED = "false" $env:AUTO_SEED_PERSONAS = "false" $env:ALLOW_SEED_PERSONA_FALLBACK = "false" $env:FRONTEND_BASE_URL = "https://vignette.chanpaca.net" $env:CORS_ORIGINS = '["https://vignette.chanpaca.net","https://vignette-b1q.pages.dev","http://localhost:5170","http://localhost:5171","http://localhost:5172","http://localhost:5173","http://localhost:5174","http://localhost:5175","http://localhost:5176","http://localhost:5177","http://localhost:5178","http://localhost:5179","http://localhost:5180","http://127.0.0.1:5170","http://127.0.0.1:5171","http://127.0.0.1:5172","http://127.0.0.1:5173","http://127.0.0.1:5174","http://127.0.0.1:5175","http://127.0.0.1:5176","http://127.0.0.1:5177","http://127.0.0.1:5178","http://127.0.0.1:5179","http://127.0.0.1:5180"]' $proc = Start-Process -WindowStyle Hidden -FilePath $Python ` -ArgumentList @("-m", "uvicorn", "app.main:app", "--host", "127.0.0.1", "--port", "$ApiPort") ` -WorkingDirectory $ApiDir ` -RedirectStandardOutput $OutLog ` -RedirectStandardError $ErrLog ` -PassThru Start-Sleep -Seconds 3 $health = Wait-JsonHealth ` -Uri "http://127.0.0.1:$ApiPort/health" ` -IsHealthy { param($health) $health.environment -eq "prod" -and $health.db -and $health.engine } ` -TimeoutSec 30 if ($health.environment -ne "prod" -or -not $health.db -or -not $health.engine) { throw "Public API health is not production-safe: $($health | ConvertTo-Json -Compress)" } if (!$SkipCloudflaredRestart) { if (!(Test-Path $Cloudflared)) { throw "cloudflared not found at $Cloudflared" } if (!(Test-Path $CloudflaredConfig)) { throw "cloudflared config not found at $CloudflaredConfig" } $lines = Get-Content $CloudflaredConfig $insideApiIngress = $false $updated = $false $nextLines = foreach ($line in $lines) { if ($line -match "hostname:\s*api-vignette\.chanpaca\.net") { $insideApiIngress = $true $line continue } if ($insideApiIngress -and $line -match "^\s*service:\s*http://127\.0\.0\.1:\d+\s*$") { $updated = $true $insideApiIngress = $false " service: http://127.0.0.1:$ApiPort" continue } if ($insideApiIngress -and $line -match "^\s*-\s+") { $insideApiIngress = $false } $line } if (!$updated) { throw "Could not find api-vignette.chanpaca.net localhost service in $CloudflaredConfig" } Set-Content -Encoding UTF8 -Path $CloudflaredConfig -Value $nextLines Get-CimInstance Win32_Process | Where-Object { $_.Name -eq "cloudflared.exe" -and $_.CommandLine -like "*vignette-config.yml*" } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force } Start-Sleep -Seconds 2 Start-Process -WindowStyle Hidden -FilePath $Cloudflared ` -ArgumentList @("tunnel", "--config", $CloudflaredConfig, "run") ` -RedirectStandardOutput (Join-Path $Workspace "cloudflared.public.out.log") ` -RedirectStandardError (Join-Path $Workspace "cloudflared.public.err.log") ` -PassThru | Out-Null } Write-Output "Engine gateway healthy on http://127.0.0.1:$EnginePort" Write-Output "Public API running on http://127.0.0.1:$ApiPort with PID $($proc.Id)" Write-Output "Health: $($health | ConvertTo-Json -Compress)"