Files
gold_land_server/scripts/dev-local-stack.ps1
T
safavimohadeseh78andCursor dfb20ca92c
Backend CI / type-check, build, tests (push) Successful in 1m0s
feat: prepare six-lesson trial backend with reliable rewards and operations
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-12 14:35:42 +03:30

281 lines
9.2 KiB
PowerShell

# 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
}