param( [string]$Workspace = "D:\workspace\vignette", [int]$ApiPort = 8001, [int]$WebPort = 5174, [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]$SkipWebRestart, [switch]$RouteCloudflareDns, [string]$CloudflareTunnelName = "vignette", [switch]$SkipCloudflaredRestart ) $ErrorActionPreference = "Stop" $ApiDir = Join-Path $Workspace "apps\api" $WebDir = Join-Path $Workspace "apps\web" $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" $WebOutLog = Join-Path $Workspace "web.public.out.log" $WebErrLog = Join-Path $Workspace "web.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 Wait-HttpStatus { param( [string]$Uri, [int]$TimeoutSec = 30 ) $deadline = (Get-Date).AddSeconds($TimeoutSec) do { try { $response = Invoke-WebRequest -UseBasicParsing -Uri $Uri -TimeoutSec 5 if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) { return $response } } catch { Start-Sleep -Seconds 1 } } while ((Get-Date) -lt $deadline) throw "Timed out waiting for HTTP 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 } } function Stop-NodeByPortHint { param([int]$Port) Get-CimInstance Win32_Process | Where-Object { $_.Name -eq "node.exe" -and $_.CommandLine -and $_.CommandLine -like "*vite*preview*" -and $_.CommandLine -like "*$Port*" } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force } } function ConvertTo-CompactJson { param([object]$Value) ConvertTo-Json -InputObject $Value -Compress } function Set-CloudflaredIngress { param( [string]$ConfigPath, [string[]]$ApiHostnames, [string[]]$WebHostnames, [int]$ApiPortValue, [int]$WebPortValue ) $lines = Get-Content -Encoding UTF8 -Path $ConfigPath $ingressIndex = -1 for ($i = 0; $i -lt $lines.Count; $i++) { if ($lines[$i] -match "^\s*ingress:\s*$") { $ingressIndex = $i break } } if ($ingressIndex -lt 0) { throw "Could not find ingress: in $ConfigPath" } $nextLines = @() if ($ingressIndex -gt 0) { $nextLines += $lines[0..($ingressIndex - 1)] } $nextLines += "ingress:" foreach ($hostname in $WebHostnames) { $nextLines += " - hostname: $hostname" $nextLines += " service: http://127.0.0.1:$WebPortValue" } foreach ($hostname in $ApiHostnames) { $nextLines += " - hostname: $hostname" $nextLines += " service: http://127.0.0.1:$ApiPortValue" } $nextLines += " - service: http_status:404" Set-Content -Encoding UTF8 -Path $ConfigPath -Value $nextLines } if (!(Test-Path $Python)) { throw "Python 3.11 not found at $Python" } if (!(Test-Path $ApiDir)) { throw "API directory not found at $ApiDir" } if (!(Test-Path $WebDir)) { throw "Web directory not found at $WebDir" } $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" $frontendOrigins = @("https://vignette.chanpaca.net", "https://vnet.18ka.net", "https://vignette-b1q.pages.dev") $localViteOrigins = @() foreach ($port in 5170..5180) { $localViteOrigins += "http://localhost:$port" $localViteOrigins += "http://127.0.0.1:$port" } $env:CORS_ORIGINS = ConvertTo-CompactJson -Value ($frontendOrigins + $localViteOrigins) $env:FRONTEND_ORIGIN_MAP = ConvertTo-CompactJson -Value ([ordered]@{ "api-vignette.chanpaca.net" = "https://vignette.chanpaca.net" "api-vnet.18ka.net" = "https://vnet.18ka.net" }) $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 (!$SkipWebRestart) { Stop-NodeByPortHint -Port $WebPort $build = Start-Process -FilePath "cmd.exe" ` -ArgumentList @("/c", "npm run build") ` -WorkingDirectory $WebDir ` -NoNewWindow ` -Wait ` -PassThru if ($build.ExitCode -ne 0) { throw "Web build failed with exit code $($build.ExitCode)" } Start-Process -WindowStyle Hidden -FilePath "cmd.exe" ` -ArgumentList @("/c", "npm run preview -- --host 127.0.0.1 --port $WebPort") ` -WorkingDirectory $WebDir ` -RedirectStandardOutput $WebOutLog ` -RedirectStandardError $WebErrLog ` -PassThru | Out-Null Wait-HttpStatus -Uri "http://127.0.0.1:$WebPort/" -TimeoutSec 30 | Out-Null } if (!$SkipCloudflaredRestart) { if (!(Test-Path $Cloudflared)) { throw "cloudflared not found at $Cloudflared" } if (!(Test-Path $CloudflaredConfig)) { throw "cloudflared config not found at $CloudflaredConfig" } $apiHostnames = @("api-vignette.chanpaca.net", "api-vnet.18ka.net") $webHostnames = @("vnet.18ka.net") if ($RouteCloudflareDns) { foreach ($hostname in ($webHostnames + $apiHostnames)) { & $Cloudflared tunnel route dns $CloudflareTunnelName $hostname if ($LASTEXITCODE -ne 0) { Write-Warning "cloudflared DNS route failed for $hostname" } } } Set-CloudflaredIngress ` -ConfigPath $CloudflaredConfig ` -ApiHostnames $apiHostnames ` -WebHostnames $webHostnames ` -ApiPortValue $ApiPort ` -WebPortValue $WebPort 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)" if (!$SkipWebRestart) { Write-Output "Public vnet web preview running on http://127.0.0.1:$WebPort" } Write-Output "Health: $($health | ConvertTo-Json -Compress)"