# Starts Child Gold Land backend + frontend for local dev. # Manages only processes started by this script. No npm install / rebuild on each run. # # Usage: # pwsh -File .\scripts\dev-local-stack.ps1 # pwsh -File .\scripts\dev-local-stack.ps1 -UseTestPorts -SmokeTest # alt ports + health/login probe then exit # # Default ports: backend 3000, frontend 5174. Test ports: 3010 / 5184 (when 3000/5174 are busy). param( [switch]$UseTestPorts, [switch]$SmokeTest ) $ErrorActionPreference = 'Stop' $BackendRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path $WorkspaceRoot = (Resolve-Path (Join-Path $BackendRoot '..')).Path $FrontendRoot = Join-Path $WorkspaceRoot 'frontend' if ($UseTestPorts) { $BackendPort = 3010 $FrontendPort = 5190 } else { $BackendPort = 3000 $FrontendPort = 5174 } $HealthUrl = "http://127.0.0.1:${BackendPort}/health" $FrontendUrl = "http://localhost:${FrontendPort}/" $LoginProxyUrl = "http://localhost:${FrontendPort}/api/auth/login" $script:ManagedProcesses = @() $script:TempDbDir = $null function Write-Info([string]$Message) { Write-Host $Message -ForegroundColor Cyan } function Write-Warn([string]$Message) { Write-Host $Message -ForegroundColor Yellow } function Write-Err([string]$Message) { Write-Host $Message -ForegroundColor Red } function Test-NodeEngines { if (-not (Get-Command node -ErrorAction SilentlyContinue)) { throw 'Node.js not found in PATH. Install Node ^22.18.0 or >=24.12.0 (see .nvmrc).' } $raw = (node -v).TrimStart('v') $ver = [version]$raw $ok = ($ver.Major -eq 22 -and $ver -ge [version]'22.18.0') -or ($ver.Major -ge 24 -and $ver -ge [version]'24.12.0') if (-not $ok) { throw "Node v$raw is not supported. Required: ^22.18.0 or >=24.12.0. Run: nvm install 22.18.0 && nvm use 22.18.0" } Write-Info "Node $(node -v) at $((Get-Command node).Source)" } function Test-RepoLayout { if (-not (Test-Path (Join-Path $BackendRoot 'package.json'))) { throw "Backend package.json not found at $BackendRoot" } if (-not (Test-Path (Join-Path $FrontendRoot 'package.json'))) { throw "Frontend not found at `"$FrontendRoot`" (expected sibling of backend-nest)." } } function Test-DependenciesPresent { if (-not (Test-Path (Join-Path $BackendRoot 'node_modules'))) { throw "Backend node_modules missing. Run once: cd `"$BackendRoot`"; npm install" } if (-not (Test-Path (Join-Path $FrontendRoot 'node_modules'))) { throw "Frontend node_modules missing. Run once: cd `"$FrontendRoot`"; npm install" } } function Get-ListenProcessId([int]$Port) { $conn = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 if ($conn) { return [int]$conn.OwningProcess } return $null } function Assert-PortFree([int]$Port, [string]$Label) { $ownerPid = Get-ListenProcessId $Port if ($null -ne $ownerPid) { $name = (Get-Process -Id $ownerPid -ErrorAction SilentlyContinue).ProcessName throw "${Label} port $Port is in use (PID $ownerPid, $name). Use -UseTestPorts or free the port." } } function New-BackendProcessEnvironment { $envMap = @{} foreach ($entry in [System.Environment]::GetEnvironmentVariables('Process').GetEnumerator()) { $envMap[$entry.Key] = [string]$entry.Value } $envMap['PORT'] = [string]$BackendPort $envMap['HOST'] = '0.0.0.0' $envMap['NODE_ENV'] = 'development' $envMap['CORS_ORIGIN'] = "http://localhost:${FrontendPort}" if ($UseTestPorts) { $script:TempDbDir = Join-Path ([System.IO.Path]::GetTempPath()) ("cgl-devstack-" + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $script:TempDbDir -Force | Out-Null $envMap['DB_PATH'] = Join-Path $script:TempDbDir 'app.db' Write-Info "Test backend DB: $($envMap['DB_PATH'])" } return $envMap } function Start-ProcessWithEnv { param( [string]$FilePath, [string[]]$ArgumentList, [string]$WorkingDirectory, [hashtable]$Environment ) $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $FilePath $psi.WorkingDirectory = $WorkingDirectory $psi.UseShellExecute = $false $psi.CreateNoWindow = $true if ($ArgumentList -and $ArgumentList.Count -gt 0) { $quoted = $ArgumentList | ForEach-Object { if ($_ -match '\s') { "`"$_`"" } else { $_ } } $psi.Arguments = $quoted -join ' ' } foreach ($entry in [System.Environment]::GetEnvironmentVariables('Process').GetEnumerator()) { $psi.EnvironmentVariables[[string]$entry.Key] = [string]$entry.Value } foreach ($key in $Environment.Keys) { $psi.EnvironmentVariables[$key] = [string]$Environment[$key] } $proc = [System.Diagnostics.Process]::Start($psi) if (-not $proc) { throw "Failed to start process: $FilePath" } return $proc } function Start-Backend { $mainJs = Join-Path $BackendRoot 'dist\main.js' if (-not (Test-Path $mainJs)) { throw "Missing dist/main.js. Run once: cd `"$BackendRoot`"; npm run build" } $node = (Get-Command node).Source $envMap = New-BackendProcessEnvironment Write-Info "Starting backend on port $BackendPort ..." $p = Start-ProcessWithEnv -FilePath $node -ArgumentList @($mainJs) -WorkingDirectory $BackendRoot -Environment $envMap $script:ManagedProcesses += $p Start-Sleep -Milliseconds 500 if ($p.HasExited) { throw "Backend exited immediately (exit $($p.ExitCode)). Build with current Node and check dist/main.js." } return $p } function Start-Frontend { $npm = (Get-Command npm.cmd -ErrorAction SilentlyContinue).Source if (-not $npm) { $npm = (Get-Command npm -ErrorAction SilentlyContinue).Source } if (-not $npm) { throw 'npm not found in PATH.' } $envMap = @{} foreach ($entry in [System.Environment]::GetEnvironmentVariables('Process').GetEnumerator()) { $envMap[$entry.Key] = [string]$entry.Value } $envMap['VITE_DEV_PORT'] = [string]$FrontendPort $envMap['VITE_DEV_API_TARGET'] = "http://127.0.0.1:${BackendPort}" Write-Info "Starting frontend on port $FrontendPort (proxy -> $($envMap['VITE_DEV_API_TARGET'])) ..." $p = Start-ProcessWithEnv -FilePath $npm -ArgumentList @('run', 'dev', '--', '--port', [string]$FrontendPort) ` -WorkingDirectory $FrontendRoot -Environment $envMap $script:ManagedProcesses += $p return $p } function Wait-BackendHealth { param([int]$TimeoutSeconds = 90) Write-Info "Waiting for backend health (max ${TimeoutSeconds}s) ..." $deadline = (Get-Date).AddSeconds($TimeoutSeconds) while ((Get-Date) -lt $deadline) { foreach ($p in $script:ManagedProcesses) { if ($p.HasExited) { throw "Managed process exited early (PID $($p.Id), exit $($p.ExitCode))." } } try { $r = Invoke-WebRequest -Uri $HealthUrl -UseBasicParsing -TimeoutSec 3 if ($r.StatusCode -eq 200) { Write-Info "Backend health OK: $HealthUrl" return } } catch { Start-Sleep -Seconds 2 } } throw "Backend health timed out: $HealthUrl" } function Wait-FrontendHttp { param([int]$TimeoutSeconds = 120) Write-Info "Waiting for frontend (max ${TimeoutSeconds}s) ..." $deadline = (Get-Date).AddSeconds($TimeoutSeconds) while ((Get-Date) -lt $deadline) { try { $r = Invoke-WebRequest -Uri $FrontendUrl -UseBasicParsing -TimeoutSec 3 if ($r.StatusCode -eq 200) { Write-Info "Frontend OK: $FrontendUrl" return } } catch { Start-Sleep -Seconds 2 } } throw "Frontend HTTP timed out: $FrontendUrl" } function Test-ProxyLogin { $body = '{"username":"demo","password":"123456"}' $r = Invoke-WebRequest -Uri $LoginProxyUrl -Method POST -Body $body -ContentType 'application/json' -UseBasicParsing if ($r.StatusCode -ne 200) { throw "Proxy login failed: HTTP $($r.StatusCode)" } Write-Info "Proxy login OK: $LoginProxyUrl" } function Stop-ManagedProcesses { foreach ($p in $script:ManagedProcesses) { if ($p -and -not $p.HasExited) { Write-Info "Stopping PID $($p.Id) ..." try { & taskkill.exe /PID $p.Id /T /F 2>$null | Out-Null } catch { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue } } } $script:ManagedProcesses = @() if ($script:TempDbDir -and (Test-Path $script:TempDbDir)) { Remove-Item -Recurse -Force $script:TempDbDir -ErrorAction SilentlyContinue $script:TempDbDir = $null } } try { Write-Info 'Child Gold Land — local dev stack' if ($UseTestPorts) { Write-Warn 'Using test ports 3010 / 5190 (temp SQLite when backend starts fresh DB).' } Test-NodeEngines Test-RepoLayout Test-DependenciesPresent Assert-PortFree -Port $BackendPort -Label 'Backend' Assert-PortFree -Port $FrontendPort -Label 'Frontend' Start-Backend | Out-Null Start-Frontend | Out-Null Wait-BackendHealth if ($SmokeTest) { Wait-FrontendHttp Test-ProxyLogin Write-Info 'SmokeTest passed.' exit 0 } Write-Host '' Write-Info "Frontend: $FrontendUrl" Write-Info "Backend health: $HealthUrl" Write-Info 'Demo login: demo / 123456' Write-Warn 'Press Ctrl+C to stop only processes started by this script.' Write-Host '' while ($true) { Start-Sleep -Seconds 2 foreach ($p in $script:ManagedProcesses) { if ($p.HasExited) { Write-Err "Process PID $($p.Id) exited unexpectedly (code $($p.ExitCode))." exit 1 } } } } catch { Write-Err $_.Exception.Message exit 1 } finally { Stop-ManagedProcesses }