Private/Docker-Helpers.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. <# .SYNOPSIS Docker lifecycle helpers for Show-TaxonomyEditor container mode. .DESCRIPTION Internal functions that manage the Docker container for the Taxonomy Editor web server. Not exported — called exclusively by Show-TaxonomyEditor. #> function Assert-DockerAvailable { <# .SYNOPSIS Verifies Docker CLI is installed and the daemon is reachable. #> [CmdletBinding()] param() $docker = Get-Command docker -ErrorAction SilentlyContinue if (-not $docker) { $msg = @( 'Docker is required but not installed.' '' 'Install Docker Desktop from:' ' https://www.docker.com/products/docker-desktop/' '' 'After installing, ensure Docker Desktop is running, then try again.' ) -join "`n" throw (New-ActionableError ` -Goal 'Launch Taxonomy Editor in container mode' ` -Problem 'Docker CLI not found on PATH' ` -Location 'Assert-DockerAvailable' ` -NextSteps @( 'Install Docker Desktop from https://www.docker.com/products/docker-desktop/' 'Ensure Docker Desktop is running' 'Run Show-TaxonomyEditor again' )) } # Check that the daemon is responsive try { $null = docker info 2>&1 if ($LASTEXITCODE -ne 0) { throw 'Docker daemon not responding' } } catch { throw (New-ActionableError ` -Goal 'Connect to Docker daemon' ` -Problem 'Docker is installed but the daemon is not running' ` -Location 'Assert-DockerAvailable' ` -NextSteps @( 'Start Docker Desktop' 'Wait for the Docker icon to show "running"' 'Run Show-TaxonomyEditor again' )) } } function Test-DockerImageExists { <# .SYNOPSIS Returns $true if the specified Docker image is available locally. #> [CmdletBinding()] param([string]$ImageName) $result = docker images -q $ImageName 2>&1 return ($null -ne $result -and $result.Trim().Length -gt 0) } function Pull-TaxonomyEditorImage { <# .SYNOPSIS Pulls the latest Taxonomy Editor Docker image. #> [CmdletBinding()] param([string]$ImageName = 'aitriad/taxonomy-editor:latest') Write-Step "Pulling Taxonomy Editor image ($ImageName)" Write-Info 'This is a one-time download (~1.4 GB). Subsequent starts are instant.' docker pull $ImageName if ($LASTEXITCODE -ne 0) { throw (New-ActionableError ` -Goal 'Download Taxonomy Editor container image' ` -Problem "docker pull failed with exit code $LASTEXITCODE" ` -Location 'Pull-TaxonomyEditorImage' ` -NextSteps @( 'Check your internet connection' 'Verify Docker Hub is accessible: docker pull hello-world' 'If behind a proxy, configure Docker proxy settings' )) } Write-OK 'Image pulled successfully.' } function Get-ContainerName { <# .SYNOPSIS Returns the container name for a given port. #> [CmdletBinding()] param([int]$Port) return "aitriad-editor-$Port" } function Test-TaxonomyContainerRunning { <# .SYNOPSIS Returns $true if a Taxonomy Editor container is running on the given port. #> [CmdletBinding()] param([int]$Port) $name = Get-ContainerName -Port $Port $result = docker ps --filter "name=$name" --format '{{.Names}}' 2>&1 return ($result -eq $name) } function Stop-TaxonomyContainer { <# .SYNOPSIS Stops a running Taxonomy Editor container. #> [CmdletBinding()] param([int]$Port) $name = Get-ContainerName -Port $Port if (Test-TaxonomyContainerRunning -Port $Port) { Write-Step "Stopping Taxonomy Editor ($name)" docker stop $name 2>&1 | Out-Null Write-OK 'Stopped.' } else { Write-Info "No running container found for port $Port." } } function Get-TaxonomyContainerStatus { <# .SYNOPSIS Displays the status of the Taxonomy Editor container. #> [CmdletBinding()] param([int]$Port) $name = Get-ContainerName -Port $Port if (Test-TaxonomyContainerRunning -Port $Port) { Write-OK "Taxonomy Editor is running at http://localhost:$Port (container: $name)" } else { Write-Info "Taxonomy Editor is not running on port $Port." } } function Wait-ForHealthEndpoint { <# .SYNOPSIS Polls the server health endpoint until it responds or timeout expires. #> [CmdletBinding()] param( [int]$Port, [int]$TimeoutSeconds = 30 ) $url = "http://localhost:$Port/health" $deadline = (Get-Date).AddSeconds($TimeoutSeconds) Write-Info 'Waiting for editor to be ready...' while ((Get-Date) -lt $deadline) { try { $resp = Invoke-RestMethod -Uri $url -TimeoutSec 2 -ErrorAction SilentlyContinue if ($resp.status -eq 'ok') { return $true } } catch { # Server not ready yet } Start-Sleep -Milliseconds 500 } Write-Warn "Server did not become ready within $TimeoutSeconds seconds." Write-Info "Check container logs: docker logs $(Get-ContainerName -Port $Port)" return $false } function Test-ContainerVersionCompat { <# .SYNOPSIS Queries the container health endpoint and warns if its version differs from the installed module version. #> [CmdletBinding()] param([int]$Port) try { $health = Invoke-RestMethod -Uri "http://localhost:$Port/health" -TimeoutSec 3 -ErrorAction SilentlyContinue if ($health.version) { $moduleVersion = (Get-Module AITriad -ErrorAction SilentlyContinue)?.Version?.ToString() if (-not $moduleVersion) { $moduleVersion = (Import-PowerShellDataFile -Path (Join-Path $PSScriptRoot '../AITriad.psd1')).ModuleVersion } if ($moduleVersion -and $health.version -ne $moduleVersion) { Write-Warn "Container version ($($health.version)) differs from module version ($moduleVersion)." Write-Info 'Run Show-TaxonomyEditor -Pull to update the container image.' } } } catch { # Non-critical — don't block startup for a version check failure } } function Get-ApiKeyEnvArgs { <# .SYNOPSIS Builds Docker --env flags for any AI API keys found in the environment. #> [CmdletBinding()] param() $args = @() $keyVars = @('GEMINI_API_KEY', 'ANTHROPIC_API_KEY', 'GROQ_API_KEY', 'AI_API_KEY', 'AI_MODEL') foreach ($var in $keyVars) { $val = [System.Environment]::GetEnvironmentVariable($var) if ($val) { $args += '--env' $args += "$var=$val" } } return $args } |