param( [switch]$SkipInfra, [switch]$SkipMigrate, [switch]$SkipApi, [switch]$SkipWeb, [switch]$WithGateway, [switch]$OnlyMinio ) $ErrorActionPreference = 'Stop' $root = Resolve-Path (Join-Path $PSScriptRoot '..') $logDir = Join-Path $root 'logs' New-Item -ItemType Directory -Force -Path $logDir | Out-Null function Test-CommandAvailable { param([string]$Name) return [bool](Get-Command $Name -ErrorAction SilentlyContinue) } function Test-PortOpen { param([int]$Port) return [bool](Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue) } function Test-TcpPort { param( [string]$HostName, [int]$Port ) try { $client = [System.Net.Sockets.TcpClient]::new() $connect = $client.BeginConnect($HostName, $Port, $null, $null) if (-not $connect.AsyncWaitHandle.WaitOne(1000)) { $client.Close() return $false } $client.EndConnect($connect) $client.Close() return $true } catch { return $false } } function Start-LoggedProcess { param( [string]$Name, [string]$FilePath, [string[]]$ArgumentList, [string]$WorkingDirectory, [string]$OutLog, [string]$ErrLog ) Write-Host "Starting $Name..." Start-Process ` -FilePath $FilePath ` -ArgumentList $ArgumentList ` -WorkingDirectory $WorkingDirectory ` -RedirectStandardOutput $OutLog ` -RedirectStandardError $ErrLog ` -WindowStyle Hidden | Out-Null } function Wait-ForTcpPort { param( [string]$Name, [string]$HostName, [int]$Port, [int]$TimeoutSeconds = 20 ) $deadline = (Get-Date).AddSeconds($TimeoutSeconds) while ((Get-Date) -lt $deadline) { if (Test-TcpPort -HostName $HostName -Port $Port) { Write-Host "$Name is listening on ${HostName}:$Port." return $true } Start-Sleep -Milliseconds 500 } Write-Host "$Name is not listening on ${HostName}:$Port after ${TimeoutSeconds}s." return $false } function Find-LocalPostgresBin { $candidates = @( 'C:\cmpp-platform-local\pgsql\bin', 'C:\cmpp-platform-local\PostgreSQL16\bin', 'C:\Program Files\PostgreSQL\16\bin' ) foreach ($candidate in $candidates) { if ((Test-Path (Join-Path $candidate 'pg_ctl.exe')) -and (Test-Path (Join-Path $candidate 'pg_isready.exe'))) { return $candidate } } $pgCtl = Get-Command 'pg_ctl.exe' -ErrorAction SilentlyContinue if ($pgCtl) { return Split-Path $pgCtl.Source -Parent } return $null } function Start-LocalPostgres { if (Test-TcpPort -HostName '127.0.0.1' -Port 5432) { Write-Host 'PostgreSQL is already reachable on 127.0.0.1:5432.' return $true } $pgBin = Find-LocalPostgresBin $pgData = 'C:\cmpp-platform-local\postgres-data' if (-not $pgBin -or -not (Test-Path (Join-Path $pgData 'PG_VERSION'))) { Write-Host 'PostgreSQL is not reachable and no local PostgreSQL data directory was found.' Write-Host 'Expected Docker Compose or local data at C:\cmpp-platform-local\postgres-data.' return $false } Write-Host "Starting local PostgreSQL from $pgData..." & (Join-Path $pgBin 'pg_ctl.exe') -D $pgData -l (Join-Path $logDir 'postgres.log') start | Out-Host Start-Sleep -Seconds 2 if (Test-Path (Join-Path $pgBin 'pg_isready.exe')) { & (Join-Path $pgBin 'pg_isready.exe') -h 127.0.0.1 -p 5432 -U cmpp -d cmpp_platform | Out-Host } return (Wait-ForTcpPort -Name 'PostgreSQL' -HostName '127.0.0.1' -Port 5432 -TimeoutSeconds 30) } function Start-LocalRedis { if (Test-TcpPort -HostName '127.0.0.1' -Port 6379) { Write-Host 'Redis is already reachable on 127.0.0.1:6379.' return $true } $redisServer = Get-Command 'redis-server' -ErrorAction SilentlyContinue if (-not $redisServer) { Write-Host 'Redis is not reachable and redis-server was not found in PATH.' return $false } $redisDir = Join-Path $root '.local-data\redis' New-Item -ItemType Directory -Force -Path $redisDir | Out-Null Start-LoggedProcess ` -Name 'Redis' ` -FilePath $redisServer.Source ` -ArgumentList @('--port', '6379', '--dir', $redisDir, '--dbfilename', 'dump.rdb') ` -WorkingDirectory $root ` -OutLog (Join-Path $logDir 'redis.out.log') ` -ErrLog (Join-Path $logDir 'redis.err.log') return (Wait-ForTcpPort -Name 'Redis' -HostName '127.0.0.1' -Port 6379 -TimeoutSeconds 15) } function Find-LocalMinio { $candidates = @( 'C:\cmpp-platform-local\minio.exe', 'C:\cmpp-platform-local\minio\minio.exe' ) foreach ($candidate in $candidates) { if (Test-Path $candidate) { return $candidate } } $minio = Get-Command 'minio.exe' -ErrorAction SilentlyContinue if ($minio) { return $minio.Source } return $null } function Start-LocalMinio { if ((Test-TcpPort -HostName '127.0.0.1' -Port 9000) -and (Test-TcpPort -HostName '127.0.0.1' -Port 9001)) { Write-Host 'MinIO is already reachable on 127.0.0.1:9000/9001.' return $true } $minio = Find-LocalMinio if (-not $minio) { Write-Host 'MinIO is not reachable and minio.exe was not found.' Write-Host 'Expected minio.exe at C:\cmpp-platform-local\minio.exe or in PATH.' return $false } $minioData = 'C:\cmpp-platform-local\minio-data' New-Item -ItemType Directory -Force -Path $minioData | Out-Null if (-not $env:MINIO_ROOT_USER) { $env:MINIO_ROOT_USER = 'cmpp_minio' } if (-not $env:MINIO_ROOT_PASSWORD) { $env:MINIO_ROOT_PASSWORD = 'cmpp_minio_password' } Start-LoggedProcess ` -Name 'MinIO' ` -FilePath $minio ` -ArgumentList @('server', $minioData, '--address', ':9000', '--console-address', ':9001') ` -WorkingDirectory (Split-Path $minio -Parent) ` -OutLog (Join-Path $logDir 'minio.out.log') ` -ErrLog (Join-Path $logDir 'minio.err.log') $apiReady = Wait-ForTcpPort -Name 'MinIO API' -HostName '127.0.0.1' -Port 9000 -TimeoutSeconds 20 $consoleReady = Wait-ForTcpPort -Name 'MinIO Console' -HostName '127.0.0.1' -Port 9001 -TimeoutSeconds 20 return ($apiReady -and $consoleReady) } function Test-Minio { if ((Test-TcpPort -HostName '127.0.0.1' -Port 9000) -and (Test-TcpPort -HostName '127.0.0.1' -Port 9001)) { Write-Host 'MinIO ports 9000/9001 are reachable.' return $true } Write-Host 'MinIO is not fully reachable on 9000/9001. File upload smoke may be blocked.' return $false } Write-Host "CMPP local service startup" Write-Host "Workspace: $root" $minioReady = $false if ($OnlyMinio) { $minioReady = Start-LocalMinio if (-not $minioReady) { exit 1 } Write-Host '' Write-Host 'MinIO startup command issued. Useful URLs:' Write-Host ' MinIO API: http://localhost:9000' Write-Host ' MinIO Console: http://localhost:9001' Write-Host '' Write-Host "Logs: $logDir" return } if (-not $SkipInfra) { if (Test-CommandAvailable 'docker') { Write-Host "Starting PostgreSQL, Redis and MinIO via Docker Compose..." & docker compose -f (Join-Path $root 'infra/docker-compose.yml') up -d Wait-ForTcpPort -Name 'PostgreSQL' -HostName '127.0.0.1' -Port 5432 -TimeoutSeconds 30 | Out-Null Wait-ForTcpPort -Name 'Redis' -HostName '127.0.0.1' -Port 6379 -TimeoutSeconds 20 | Out-Null $minioReady = Test-Minio } else { Write-Host "Docker CLI is not available. Trying local PostgreSQL and Redis fallback..." Start-LocalPostgres | Out-Null Start-LocalRedis | Out-Null $minioReady = Start-LocalMinio } } else { $minioReady = Test-Minio } if (-not $minioReady) { $localObjectRoot = Join-Path $root '.local-data\object-storage' New-Item -ItemType Directory -Force -Path $localObjectRoot | Out-Null $env:OBJECT_STORAGE_DRIVER = 'local' $env:OBJECT_STORAGE_LOCAL_ROOT = $localObjectRoot Write-Host "Object storage fallback enabled: $localObjectRoot" } if (-not $SkipInfra -and -not $SkipMigrate -and (Test-TcpPort -HostName '127.0.0.1' -Port 5432)) { Write-Host 'Applying Prisma migrations...' & npm.cmd --prefix api run prisma:migrate:deploy } if (-not $SkipApi) { if (Test-PortOpen 3000) { Write-Host "API port 3000 is already listening; leaving the existing API process untouched." } else { Start-LoggedProcess ` -Name 'NestJS API' ` -FilePath 'npm.cmd' ` -ArgumentList @('--prefix', 'api', 'run', 'start:dev') ` -WorkingDirectory $root ` -OutLog (Join-Path $logDir 'api-dev.out.log') ` -ErrLog (Join-Path $logDir 'api-dev.err.log') } } if (-not $SkipWeb) { if ((Test-PortOpen 5173) -or (Test-PortOpen 4173) -or (Test-PortOpen 4174)) { Write-Host "A frontend port is already listening on 5173/4173/4174; leaving the existing web process untouched." } else { Start-LoggedProcess ` -Name 'frontend preview' ` -FilePath 'npm.cmd' ` -ArgumentList @('run', 'dev') ` -WorkingDirectory $root ` -OutLog (Join-Path $logDir 'frontend-dev.out.log') ` -ErrLog (Join-Path $logDir 'frontend-dev.err.log') } } if ($WithGateway) { if (-not (Test-CommandAvailable 'go')) { Write-Host "Go CLI is not available. Skipping gateway startup." } elseif (Test-PortOpen 8090) { Write-Host "Gateway health port 8090 is already listening; leaving the existing gateway process untouched." } else { Start-LoggedProcess ` -Name 'Go gateway' ` -FilePath 'go' ` -ArgumentList @('run', './cmd/gateway') ` -WorkingDirectory (Join-Path $root 'gateway') ` -OutLog (Join-Path $logDir 'gateway.out.log') ` -ErrLog (Join-Path $logDir 'gateway.err.log') } } Write-Host '' Write-Host 'Startup command issued. Useful URLs:' Write-Host ' API: http://localhost:3000/api' Write-Host ' Frontend: http://localhost:5173 or http://localhost:4173' if ($minioReady) { Write-Host ' MinIO: http://localhost:9001' } else { Write-Host ' MinIO: not running; using local object storage fallback when API is started by this script' } Write-Host '' Write-Host "Logs: $logDir"