Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' function Write-Step { param([string]$Message) Write-Host "==> $Message" -ForegroundColor Cyan } function Write-Note { param([string]$Message) Write-Host " -> $Message" -ForegroundColor DarkGray } function Assert-Command { param([string]$Name) if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { throw "Required command '$Name' was not found in PATH." } } function Resolve-AbsolutePath { param( [Parameter(Mandatory = $true)][string]$BasePath, [Parameter(Mandatory = $true)][string]$Path ) if ([System.IO.Path]::IsPathRooted($Path)) { return [System.IO.Path]::GetFullPath($Path) } return [System.IO.Path]::GetFullPath((Join-Path $BasePath $Path)) } function Invoke-Checked { param( [Parameter(Mandatory = $true)][string]$FilePath, [Parameter(Mandatory = $true)][string[]]$Arguments, [string]$WorkingDirectory ) if ($WorkingDirectory) { Push-Location $WorkingDirectory } try { & $FilePath @Arguments if ($LASTEXITCODE -ne 0) { throw "Command failed: $FilePath $($Arguments -join ' ')" } } finally { if ($WorkingDirectory) { Pop-Location } } } function Invoke-DockerCompose { param( [Parameter(Mandatory = $true)][string]$ComposeFile, [Parameter(Mandatory = $true)][string[]]$Arguments, [string]$ProjectName, [string]$WorkingDirectory ) $dockerArgs = @('compose', '-f', $ComposeFile) if ($ProjectName) { $dockerArgs += @('-p', $ProjectName) } $dockerArgs += $Arguments Invoke-Checked -FilePath 'docker' -Arguments $dockerArgs -WorkingDirectory $WorkingDirectory } function Wait-ForHttpOk { param( [Parameter(Mandatory = $true)][string]$Url, [int]$TimeoutSeconds = 120 ) $deadline = (Get-Date).AddSeconds($TimeoutSeconds) while ((Get-Date) -lt $deadline) { try { $response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 10 if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 300) { return } } catch { Start-Sleep -Seconds 2 continue } Start-Sleep -Seconds 2 } throw "Timed out waiting for a healthy response from $Url." }