# Shared utilities for Windows scripts # Provides: colors, progress indicators, interactive detection Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # Detect if running in interactive shell function Test-Interactive { # Check if both input and output are from terminal # In non-interactive environments (CI/CD), this will be false $isInteractive = [Environment]::UserInteractive -and (-not [Console]::IsInputRedirected) -and (-not [Console]::IsOutputRedirected) return $isInteractive } # Global spinner state $script:SpinnerJob = $null $script:SpinnerFrames = @('⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏') # Logging functions function Write-Info { param([string]$Message) if (Test-Interactive) { Write-Host $Message -ForegroundColor Blue } else { Write-Output $Message } } function Write-Success { param([string]$Message) if (Test-Interactive) { Write-Host $Message -ForegroundColor Green } else { Write-Output $Message } } function Write-Warning-Custom { param([string]$Message) if (Test-Interactive) { Write-Host $Message -ForegroundColor Yellow } else { Write-Output "WARNING: $Message" } } function Write-Error-Custom { param([string]$Message) if (Test-Interactive) { Write-Host $Message -ForegroundColor Red } else { Write-Output "ERROR: $Message" *>&1 } } function Write-Step { param([string]$Message) if (Test-Interactive) { Write-Host $Message -ForegroundColor Magenta } else { Write-Output $Message } } function Write-Result { param([string]$Message) if (Test-Interactive) { Write-Host $Message -ForegroundColor Cyan } else { Write-Output $Message } } # Spinner functions (only for interactive shells) function Start-Spinner { param([string]$Message = "Processing...") if (-not (Test-Interactive)) { Write-Output $Message return } $script:SpinnerJob = Start-Job -ScriptBlock { param($Frames, $Msg) $i = 0 while ($true) { $frame = $Frames[$i % $Frames.Length] [Console]::SetCursorPosition(0, [Console]::CursorTop) Write-Host -NoNewline "$frame $Msg" Start-Sleep -Milliseconds 100 $i++ } } -ArgumentList $script:SpinnerFrames, $Message } function Stop-Spinner { if ($script:SpinnerJob -ne $null) { Stop-Job -Job $script:SpinnerJob -ErrorAction SilentlyContinue Remove-Job -Job $script:SpinnerJob -Force -ErrorAction SilentlyContinue $script:SpinnerJob = $null if (Test-Interactive) { # Clear the spinner line [Console]::SetCursorPosition(0, [Console]::CursorTop) Write-Host (' ' * ([Console]::WindowWidth - 1)) -NoNewline [Console]::SetCursorPosition(0, [Console]::CursorTop) } } } function Update-Spinner { param([string]$Message) if (-not (Test-Interactive)) { Write-Output $Message return } if ($script:SpinnerJob -ne $null) { Stop-Spinner Start-Spinner -Message $Message } } # Execute command with progress indicator function Invoke-WithProgress { param( [string]$Message, [scriptblock]$ScriptBlock ) if (Test-Interactive) { Start-Spinner -Message $Message try { $output = & $ScriptBlock 2>&1 $exitCode = $LASTEXITCODE Stop-Spinner if ($exitCode -eq 0 -or $null -eq $exitCode) { Write-Success "✓ $Message - Done!" } else { Write-Error-Custom "✗ $Message - Failed!" if ($output) { Write-Output $output } } return $exitCode } catch { Stop-Spinner Write-Error-Custom "✗ $Message - Failed!" throw } } else { # Non-interactive: just show message and run Write-Output "→ $Message" try { & $ScriptBlock $exitCode = $LASTEXITCODE if ($exitCode -eq 0 -or $null -eq $exitCode) { Write-Output "✓ $Message - Done!" } else { Write-Output "✗ $Message - Failed!" } return $exitCode } catch { Write-Output "✗ $Message - Failed!" throw } } } # Check system requirements function Test-SystemRequirements { param( [int]$MinCpu = 4, [int]$MinRamGB = 6, [int]$MinDiskGB = 20 ) Write-Step "Checking system requirements..." # Check CPU cores $cpuCount = [Environment]::ProcessorCount if ($cpuCount -lt $MinCpu) { Write-Error-Custom "Insufficient CPU cores: $cpuCount (minimum: $MinCpu)" return $false } # Check RAM $totalMemBytes = (Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory $totalMemGB = [Math]::Round($totalMemBytes / 1GB, 2) if ($totalMemGB -lt $MinRamGB) { Write-Error-Custom "Insufficient RAM: ${totalMemGB}GB (minimum: ${MinRamGB}GB)" return $false } # Check disk space $currentDrive = Split-Path -Qualifier $PWD $driveLetter = $currentDrive.Substring(0, 1) $freeGB = [Math]::Round((Get-PSDrive -Name $driveLetter).Free / 1GB, 2) if ($freeGB -lt $MinDiskGB) { Write-Error-Custom "Insufficient disk space: ${freeGB}GB (minimum: ${MinDiskGB}GB)" return $false } Write-Success "System requirements met: CPU cores=$cpuCount, RAM=${totalMemGB}GB, FreeDisk=${freeGB}GB" return $true } # Generate monitoring/web.yml from PROMETHEUS_USER and PROMETHEUS_PASSWORD in .env function New-PrometheusWebConfig { if (-not (Test-Path '.env')) { return } $envContent = Get-Content '.env' $promUserLine = $envContent | Where-Object { $_ -match '^PROMETHEUS_USER=' } | Select-Object -First 1 $promPassLine = $envContent | Where-Object { $_ -match '^PROMETHEUS_PASSWORD=' } | Select-Object -First 1 if (-not $promUserLine -or -not $promPassLine) { Write-Warning-Custom "PROMETHEUS_USER or PROMETHEUS_PASSWORD not set in .env, skipping web.yml generation" return } $promUser = ($promUserLine -replace '^PROMETHEUS_USER=', '').Trim('"') $promPassword = ($promPassLine -replace '^PROMETHEUS_PASSWORD=', '').Trim('"') if ([string]::IsNullOrEmpty($promUser) -or [string]::IsNullOrEmpty($promPassword)) { Write-Warning-Custom "PROMETHEUS_USER or PROMETHEUS_PASSWORD is empty, skipping web.yml generation" return } $rawHash = docker run --rm httpd:2-alpine htpasswd -nbBC 12 "" "$promPassword" 2>$null if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrEmpty($rawHash)) { Write-Warning-Custom "Failed to generate Prometheus password hash" return } # htpasswd outputs ":$2y$..." - strip leading colon and whitespace $hash = $rawHash.TrimStart(':').Trim() $content = "basic_auth_users:`n ${promUser}: ${hash}`n" [System.IO.File]::WriteAllText((Join-Path $PWD 'monitoring/web.yml'), $content) Write-Success "Prometheus web config generated" } # Confirm action function Confirm-Action { param([string]$Prompt = "Are you sure?") if (-not (Test-Interactive)) { # In non-interactive mode, assume yes return $true } $response = Read-Host "$Prompt [y/N]" return $response -match '^[yY]([eE][sS])?$' } # Ensure spinner cleanup on exit if ($MyInvocation.MyCommand.ScriptBlock.Module) { $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { Stop-Spinner } }