Add app.ps1 dev lifecycle script (start/stop/restart/rebuild/status/logs)

A single PowerShell entry point for the local stack:
- refreshes PATH from the registry so it works in stale terminals
- ensures PostgreSQL is up; starts the Go API and Angular dev server
- tracks PIDs in .run/ and also sweeps the ports on stop
- rebuild (-Clean/-Install), status and logs (-Follow) actions

.run/ (pids, logs) is git-ignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nilesh.p
2026-10-10 11:29:38 +05:30
co-authored by Claude Opus 4.8
parent 9d2b7425b1
commit df9e534e42
2 changed files with 402 additions and 0 deletions
+3
View File
@@ -35,3 +35,6 @@ backend/coverage.out
frontend/coverage/ frontend/coverage/
e2e/evidence-report/ e2e/evidence-report/
e2e/**/*.pdf e2e/**/*.pdf
# --- app.ps1 runtime (pids, logs) -------------------------------------------
.run/
+399
View File
@@ -0,0 +1,399 @@
#requires -Version 5.1
<#
.SYNOPSIS
Start, stop, restart, rebuild and inspect the TravelHub app (Go API + Angular SPA + PostgreSQL).
.DESCRIPTION
One script to drive the local dev stack. It refreshes PATH from the registry
on every run, so it works even in a terminal opened before Go/Node were
installed (a recurring gotcha on this machine).
Processes are tracked by PID files under .run\, and stop also sweeps whatever
is listening on the API/web ports as a fallback, so a crashed or orphaned
server is still cleaned up.
.PARAMETER Action
start Ensure PostgreSQL is up, then launch the API and the web dev server.
stop Stop the API and web dev server (PostgreSQL is left running).
restart stop then start.
rebuild Build the Go binary and the production Angular bundle.
status Show what is running, on which ports, with which PIDs.
logs Print (or follow) the API and web logs.
help This help.
.PARAMETER Dev
start: run the API via `go run` and rely on port-based cleanup, instead of
the compiled binary. The web server is always `ng serve` either way.
.PARAMETER Clean
rebuild: delete build output and caches (bin, dist, .angular) first.
.PARAMETER Install
rebuild: (re)install dependencies -- npm install for frontend/e2e and
`go mod download` -- before building.
.PARAMETER Migrate
start / rebuild: apply database migrations (npm run db:migrate) as well.
.PARAMETER Follow
logs: tail the logs live instead of printing the tail once.
.PARAMETER IncludeDb
stop: also stop the PostgreSQL Windows service (off by default -- it is a
shared service and usually wants to stay up).
.EXAMPLE
.\app.ps1 start
.EXAMPLE
.\app.ps1 rebuild -Clean -Install
.EXAMPLE
.\app.ps1 restart -Dev
.EXAMPLE
.\app.ps1 logs -Follow
#>
[CmdletBinding()]
param(
[Parameter(Position = 0)]
[ValidateSet('start', 'stop', 'restart', 'rebuild', 'status', 'logs', 'help')]
[string]$Action = 'help',
[switch]$Dev,
[switch]$Clean,
[switch]$Install,
[switch]$Migrate,
[switch]$Follow,
[switch]$IncludeDb
)
$ErrorActionPreference = 'Stop'
# --- configuration ---------------------------------------------------------
$Root = $PSScriptRoot
$Backend = Join-Path $Root 'backend'
$Frontend = Join-Path $Root 'frontend'
$RunDir = Join-Path $Root '.run'
$ApiPort = 8080 # overridden below if backend\.env sets PORT
$WebPort = 4200
$DbService = 'postgresql-x64-17'
$ApiPidFile = Join-Path $RunDir 'api.pid'
$WebPidFile = Join-Path $RunDir 'web.pid'
$ApiLog = Join-Path $RunDir 'api.log'
$ApiErrLog = Join-Path $RunDir 'api.err.log'
$WebLog = Join-Path $RunDir 'web.log'
$WebErrLog = Join-Path $RunDir 'web.err.log'
$WebUrl = "http://localhost:$WebPort/"
# --- console helpers -------------------------------------------------------
function Write-Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan }
function Write-Ok($msg) { Write-Host " + $msg" -ForegroundColor Green }
function Write-Warn2($msg){ Write-Host " ! $msg" -ForegroundColor Yellow }
function Write-Err2($msg) { Write-Host " x $msg" -ForegroundColor Red }
# --- environment -----------------------------------------------------------
# Rebuild PATH from the registry so go/node/npm resolve even in a shell that
# started before they were installed. Machine scope first, then User.
function Sync-Path {
$machine = [Environment]::GetEnvironmentVariable('Path', 'Machine')
$user = [Environment]::GetEnvironmentVariable('Path', 'User')
$env:Path = ($machine, $user | Where-Object { $_ }) -join ';'
}
# Resolve an executable, falling back to its standard Windows install path.
function Resolve-Tool($name, [string[]]$fallbacks) {
$cmd = Get-Command $name -ErrorAction SilentlyContinue
if ($cmd) { return $cmd.Source }
foreach ($f in $fallbacks) { if (Test-Path $f) { return $f } }
throw "$name not found on PATH. Install it, or open a fresh terminal so PATH is current."
}
function Get-Go { Resolve-Tool 'go.exe' @('C:\Program Files\Go\bin\go.exe') }
function Get-Npm { Resolve-Tool 'npm.cmd' @('C:\Program Files\nodejs\npm.cmd') }
# Read PORT from backend\.env if present, so the script tracks the real port.
function Read-ApiPort {
$envFile = Join-Path $Backend '.env'
if (-not (Test-Path $envFile)) { return }
$line = Select-String -Path $envFile -Pattern '^\s*PORT\s*=\s*(\d+)' -ErrorAction SilentlyContinue | Select-Object -First 1
if ($line) { $script:ApiPort = [int]$line.Matches[0].Groups[1].Value }
}
# --- process primitives ----------------------------------------------------
function Get-PortPid($port) {
try {
$conns = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction Stop
return ($conns | Select-Object -ExpandProperty OwningProcess -Unique)
} catch {
return @()
}
}
# Kill a process and all of its descendants (npm spawns node; go run spawns the
# compiled binary), so nothing is left holding the port.
function Stop-Tree($procId) {
if (-not $procId) { return }
$children = Get-CimInstance Win32_Process -Filter "ParentProcessId = $procId" -ErrorAction SilentlyContinue
foreach ($c in $children) { Stop-Tree $c.ProcessId }
try { Stop-Process -Id $procId -Force -ErrorAction Stop } catch { }
}
function Stop-ByPidFile($file, $label) {
if (Test-Path $file) {
$procId = (Get-Content $file -ErrorAction SilentlyContinue | Select-Object -First 1)
if ($procId) { Stop-Tree ([int]$procId) }
Remove-Item $file -Force -ErrorAction SilentlyContinue
}
}
function Stop-ByPort($port, $label) {
$killed = $false
foreach ($procId in (Get-PortPid $port)) {
Stop-Tree ([int]$procId)
$killed = $true
}
if ($killed) { Write-Ok "$label (port $port) stopped" }
return $killed
}
function Wait-ForUrl($url, $timeoutSec) {
$deadline = (Get-Date).AddSeconds($timeoutSec)
while ((Get-Date) -lt $deadline) {
try {
Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 3 | Out-Null
return $true
} catch {
Start-Sleep -Milliseconds 500
}
}
return $false
}
function Test-PortUp($port) { return ((Get-PortPid $port).Count -gt 0) }
# --- database --------------------------------------------------------------
function Start-Database {
$svc = Get-Service -Name $DbService -ErrorAction SilentlyContinue
if (-not $svc) {
Write-Warn2 "PostgreSQL service '$DbService' not found -- is PostgreSQL installed?"
return
}
if ($svc.Status -ne 'Running') {
Write-Step "Starting PostgreSQL ($DbService)"
Start-Service $DbService
Write-Ok 'PostgreSQL started'
} else {
Write-Ok 'PostgreSQL already running'
}
}
function Invoke-Migrate {
Write-Step 'Applying database migrations'
$npm = Get-Npm
Push-Location $Root
try { & $npm run db:migrate } finally { Pop-Location }
if ($LASTEXITCODE -ne 0) { throw 'Migrations failed.' }
Write-Ok 'Migrations applied'
}
# --- build -----------------------------------------------------------------
function Build-Backend {
Write-Step 'Building Go API'
$go = Get-Go
Push-Location $Backend
try { & $go build -o 'bin\api.exe' './cmd/api' } finally { Pop-Location }
if ($LASTEXITCODE -ne 0) { throw 'Go build failed.' }
Write-Ok 'backend\bin\api.exe'
}
function Build-Frontend {
Write-Step 'Building Angular bundle'
$npm = Get-Npm
Push-Location $Frontend
try { & $npm run build } finally { Pop-Location }
if ($LASTEXITCODE -ne 0) { throw 'Angular build failed.' }
Write-Ok 'frontend\dist'
}
function Install-Deps {
$npm = Get-Npm
$go = Get-Go
Write-Step 'Installing frontend dependencies'
Push-Location $Frontend; try { & $npm install } finally { Pop-Location }
if (Test-Path (Join-Path $Root 'e2e')) {
Write-Step 'Installing e2e dependencies'
Push-Location (Join-Path $Root 'e2e'); try { & $npm install } finally { Pop-Location }
}
Write-Step 'Downloading Go modules'
Push-Location $Backend; try { & $go mod download } finally { Pop-Location }
Write-Ok 'Dependencies installed'
}
function Invoke-Clean {
Write-Step 'Cleaning build output'
foreach ($p in @(
(Join-Path $Backend 'bin'),
(Join-Path $Frontend 'dist'),
(Join-Path $Frontend '.angular'))) {
if (Test-Path $p) { Remove-Item $p -Recurse -Force; Write-Ok "removed $p" }
}
}
# --- start / stop ----------------------------------------------------------
function Start-Api {
if (Test-PortUp $ApiPort) { Write-Warn2 "API already listening on port $ApiPort -- skipping"; return }
New-Item -ItemType Directory -Force -Path $RunDir | Out-Null
$go = Get-Go
if ($Dev) {
Write-Step "Starting API (go run) on port $ApiPort"
$proc = Start-Process -FilePath $go -ArgumentList 'run', './cmd/api' `
-WorkingDirectory $Backend -WindowStyle Hidden -PassThru `
-RedirectStandardOutput $ApiLog -RedirectStandardError $ApiErrLog
} else {
$api = Join-Path $Backend 'bin\api.exe'
if (-not (Test-Path $api)) { Write-Warn2 'No built binary; building first.'; Build-Backend }
Write-Step "Starting API on port $ApiPort"
$proc = Start-Process -FilePath $api `
-WorkingDirectory $Backend -WindowStyle Hidden -PassThru `
-RedirectStandardOutput $ApiLog -RedirectStandardError $ApiErrLog
}
$proc.Id | Out-File -FilePath $ApiPidFile -Encoding ascii
if (Wait-ForUrl "http://localhost:$ApiPort/api/health" 60) {
Write-Ok "API healthy (PID $($proc.Id)) -> http://localhost:$ApiPort"
} else {
Write-Err2 "API did not become healthy in time. Check: .\app.ps1 logs"
}
}
function Start-Web {
if (Test-PortUp $WebPort) { Write-Warn2 "Web already listening on port $WebPort -- skipping"; return }
New-Item -ItemType Directory -Force -Path $RunDir | Out-Null
$npm = Get-Npm
Write-Step "Starting web dev server on port $WebPort"
$proc = Start-Process -FilePath $npm -ArgumentList 'start' `
-WorkingDirectory $Frontend -WindowStyle Hidden -PassThru `
-RedirectStandardOutput $WebLog -RedirectStandardError $WebErrLog
$proc.Id | Out-File -FilePath $WebPidFile -Encoding ascii
if (Wait-ForUrl $WebUrl 180) {
Write-Ok "Web ready (PID $($proc.Id)) -> $WebUrl"
} else {
Write-Err2 "Web server did not come up in time. Check: .\app.ps1 logs"
}
}
function Start-App {
Start-Database
Start-Api
Start-Web
Write-Host ''
Write-Host "TravelHub is up. Open $WebUrl" -ForegroundColor Green
Write-Host 'Sign in with any demo account (password demo1234), e.g. agent@skyline.test / skyline.'
}
function Stop-App {
Write-Step 'Stopping web dev server'
Stop-ByPidFile $WebPidFile 'web'
[void](Stop-ByPort $WebPort 'web')
Write-Step 'Stopping API'
Stop-ByPidFile $ApiPidFile 'api'
[void](Stop-ByPort $ApiPort 'api')
if ($IncludeDb) {
$svc = Get-Service -Name $DbService -ErrorAction SilentlyContinue
if ($svc -and $svc.Status -eq 'Running') {
Write-Step 'Stopping PostgreSQL'
Stop-Service $DbService
Write-Ok 'PostgreSQL stopped'
}
}
Write-Ok 'Stopped'
}
# --- status / logs ---------------------------------------------------------
function Show-Status {
Write-Step 'TravelHub status'
$svc = Get-Service -Name $DbService -ErrorAction SilentlyContinue
$dbState = if ($svc) { $svc.Status } else { 'not installed' }
function PortLine($label, $port) {
$pids = Get-PortPid $port
if ($pids) { "{0,-18} UP port {1} PID {2}" -f $label, $port, ($pids -join ',') }
else { "{0,-18} down port {1}" -f $label, $port }
}
Write-Host (" {0,-18} {1}" -f 'PostgreSQL', $dbState)
Write-Host (" " + (PortLine 'API' $ApiPort))
Write-Host (" " + (PortLine 'Web (dev server)' $WebPort))
if (Test-PortUp $ApiPort) {
try {
$h = Invoke-RestMethod "http://localhost:$ApiPort/api/health" -TimeoutSec 3
Write-Host (" {0,-18} {1}" -f 'API health', $h.status)
} catch {
Write-Host (" {0,-18} {1}" -f 'API health', 'unreachable')
}
}
}
function Show-Logs {
$logs = @($ApiLog, $ApiErrLog, $WebLog, $WebErrLog) | Where-Object { Test-Path $_ }
if (-not $logs) { Write-Warn2 'No logs yet -- start the app first.'; return }
if ($Follow) {
Write-Step 'Following logs (Ctrl+C to stop)'
Get-Content $logs -Tail 20 -Wait
} else {
foreach ($l in $logs) {
Write-Step (Split-Path $l -Leaf)
Get-Content $l -Tail 25 -ErrorAction SilentlyContinue
Write-Host ''
}
}
}
function Show-Help {
Get-Help $PSCommandPath -Detailed
}
# --- dispatch --------------------------------------------------------------
Sync-Path
Read-ApiPort
switch ($Action) {
'start' {
if ($Migrate) { Start-Database; Invoke-Migrate }
Start-App
}
'stop' { Stop-App }
'restart' { Stop-App; Start-Sleep -Seconds 1; Start-App }
'rebuild' {
if ($Clean) { Invoke-Clean }
if ($Install) { Install-Deps }
Build-Backend
Build-Frontend
if ($Migrate) { Start-Database; Invoke-Migrate }
Write-Host ''
Write-Ok 'Rebuild complete. Run ".\app.ps1 restart" to serve the new build.'
}
'status' { Show-Status }
'logs' { Show-Logs }
default { Show-Help }
}