Files
SGF/scripts/sgf.ps1
T
2026-10-29 18:43:29 +02:00

136 lines
7.3 KiB
PowerShell

param(
[ValidateSet('start', 'stop', 'status', 'frontend', 'php', 'composer', 'npm', 'test', 'migrate')]
[string]$Action = 'status',
[Parameter(ValueFromRemainingArguments = $true)] [string[]]$ToolArguments
)
$ErrorActionPreference = 'Stop'
$sgfProject = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
if ($sgfProject -ne 'C:\laragon\www\sgf-tempo') { throw 'Unexpected project path.' }
$sgfTools = 'C:\laragon\tools\sgf-tempo'
$sgfData = 'C:\laragon\data\sgf-tempo'
$sgfCluster = Join-Path $sgfData 'postgres-17'
$sgfPg = 'C:\Program Files\PostgreSQL\17\bin'
$sgfPhp = "$sgfTools\php-8.4.27\php.exe"
$sgfNode = "$sgfTools\node-v24.22.1-win-arm64\node.exe"
$sgfComposer = "$sgfTools\composer-2.10.3\composer.phar"
$sgfNpmCli = "$sgfTools\node-v24.22.1-win-arm64\node_modules\npm\bin\npm-cli.js"
$sgfRun = Join-Path $sgfData 'run'
$env:PATH = "$sgfTools\php-8.4.27;$sgfTools\node-v24.22.1-win-arm64;" + $env:PATH
$env:COMPOSER_HOME = "$sgfTools\composer-home"
$env:COMPOSER_CACHE_DIR = "$sgfTools\composer-cache"
$env:npm_config_cache = "$sgfTools\npm-cache"
$env:npm_config_userconfig = "$sgfTools\npmrc"
Remove-Item Env:COMPOSER_AUTH -ErrorAction SilentlyContinue
Set-Location -LiteralPath $sgfProject
New-Item -ItemType Directory -Path $sgfRun -Force | Out-Null
function Assert-FreePort([int]$Port) {
if (Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue) {
throw "Port $Port is occupied. No process was stopped."
}
}
function Start-SgfDatabase {
$listeners = @(Get-NetTCPConnection -LocalPort 55432 -State Listen -ErrorAction SilentlyContinue)
if ($listeners.Count) {
$pidFile = Join-Path $sgfCluster 'postmaster.pid'
if (!(Test-Path -LiteralPath $pidFile)) { throw 'Port 55432 belongs to another process.' }
$clusterPid = [int](Get-Content -LiteralPath $pidFile -TotalCount 1)
$process = Get-CimInstance Win32_Process -Filter "ProcessId = $clusterPid"
$normalizedCommand = if ($process) { $process.CommandLine.Replace('/', '\') } else { '' }
if (($listeners.OwningProcess -notcontains $clusterPid) -or !$process -or $process.ExecutablePath -ne "$sgfPg\postgres.exe" -or $normalizedCommand -notlike "*$sgfCluster*") {
throw 'Port 55432 is not owned by the SGF cluster. No process was stopped.'
}
return
}
Assert-FreePort 55432
& "$sgfPg\pg_ctl.exe" -D $sgfCluster -l (Join-Path $sgfData 'postgres.log') -w start
if ($LASTEXITCODE) { throw 'SGF database start failed.' }
}
function Start-SgfProcess([string]$Name, [string]$Executable, [string[]]$Arguments, [int]$Port) {
Assert-FreePort $Port
$process = Start-Process -FilePath $Executable -ArgumentList $Arguments -WorkingDirectory $sgfProject -WindowStyle Hidden -PassThru -RedirectStandardOutput "$sgfRun\$Name.stdout.log" -RedirectStandardError "$sgfRun\$Name.stderr.log"
$record = @{pid=$process.Id; executable=$Executable; project=$sgfProject; port=$Port; started=$process.StartTime.ToUniversalTime().ToString('o')}
$record | ConvertTo-Json | Set-Content -LiteralPath "$sgfRun\$Name.json" -Encoding UTF8
$ready = $false
for ($attempt = 0; $attempt -lt 40; $attempt++) {
$process.Refresh()
if ($process.HasExited) { throw "$Name exited. See SGF run logs." }
$listeners = @(Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue)
if ($listeners.OwningProcess -contains $process.Id) { $ready = $true; break }
Start-Sleep -Milliseconds 250
}
if (!$ready) { throw "$Name did not become ready. No other process was stopped." }
Write-Output "$Name ready at http://127.0.0.1:$Port"
}
switch ($Action) {
'start' {
Assert-FreePort 8000
Start-SgfDatabase
Start-SgfProcess 'web' $sgfPhp @('-S', '127.0.0.1:8000', '-t', 'public', 'scripts/router.php') 8000
}
'frontend' {
Start-SgfProcess 'vite' $sgfNode @('node_modules/vite/bin/vite.js', '--host', '127.0.0.1', '--port', '5173', '--strictPort') 5173
}
'status' {
& "$sgfPg\pg_ctl.exe" -D $sgfCluster status
Get-NetTCPConnection -LocalPort 55432,8000,5173 -State Listen -ErrorAction SilentlyContinue | Select-Object LocalAddress,LocalPort,OwningProcess
}
'stop' {
foreach ($name in @('vite', 'web')) {
$recordFile = "$sgfRun\$name.json"
if (Test-Path -LiteralPath $recordFile) {
$record = Get-Content -LiteralPath $recordFile -Raw | ConvertFrom-Json
$process = Get-Process -Id $record.pid -ErrorAction SilentlyContinue
if ($process) {
if ($process.Path -ne $record.executable -or $record.project -ne $sgfProject -or $process.StartTime.ToUniversalTime().ToString('o') -ne $record.started) {
throw "Process identity mismatch for $name; refusing to stop it."
}
Stop-Process -Id $process.Id
}
Remove-Item -LiteralPath $recordFile
if ($name -eq 'vite') {
$hotFile = Join-Path $sgfProject 'public\hot'
if ((Test-Path -LiteralPath $hotFile) -and (Get-Content -LiteralPath $hotFile -Raw).Trim() -eq 'http://127.0.0.1:5173') {
Remove-Item -LiteralPath $hotFile
}
}
}
}
& "$sgfPg\pg_ctl.exe" -D $sgfCluster -m fast -w stop
if ($LASTEXITCODE -ne 0 -and (Test-Path -LiteralPath "$sgfCluster\postmaster.pid")) { throw 'SGF cluster stop failed.' }
}
'php' { & $sgfPhp @ToolArguments; if ($LASTEXITCODE) { throw 'PHP command failed.' } }
'composer' { & $sgfPhp $sgfComposer @ToolArguments; if ($LASTEXITCODE) { throw 'Composer command failed.' } }
'npm' { & $sgfNode $sgfNpmCli @ToolArguments; if ($LASTEXITCODE) { throw 'npm command failed.' } }
'test' {
$demoManifest = Join-Path $sgfData 'secrets\browser-fixture.json'
if (Test-Path -LiteralPath $demoManifest) {
$demoFixture = Get-Content -LiteralPath $demoManifest -Raw | ConvertFrom-Json
if ($demoFixture.product_demo) { throw 'Integrated team demo preserved in sgf_tempo_test. Do not reset it for regression tests; use a separate test environment.' }
}
if (Test-Path -LiteralPath "$sgfRun\browser-proof.json") { throw 'Stop the browser proof before tests reset sgf_tempo_test.' }
Start-SgfDatabase
& $sgfPhp 'vendor/phpunit/phpunit/phpunit' @ToolArguments
if ($LASTEXITCODE) { throw 'Tests failed.' }
}
'migrate' {
Start-SgfDatabase
$credentials = Get-Content -LiteralPath "$sgfData\secrets\database.json" -Raw | ConvertFrom-Json
$env:APP_ENV = 'local'
$env:DB_CONNECTION = 'pgsql'
$env:DB_HOST = '127.0.0.1'
$env:DB_PORT = '55432'
$env:DB_DATABASE = 'sgf_tempo_dev'
$env:DB_USERNAME = 'sgf_dev_owner'
$env:DB_PASSWORD = $credentials.dev_owner
$env:DB_URL = ''
try {
& $sgfPhp artisan migrate --no-interaction
if ($LASTEXITCODE) { throw 'Development migration failed.' }
} finally {
foreach ($name in @('APP_ENV','DB_CONNECTION','DB_HOST','DB_PORT','DB_DATABASE','DB_USERNAME','DB_PASSWORD','DB_URL')) { Remove-Item "Env:$name" -ErrorAction SilentlyContinue }
}
}
}