2026-04-10 23:27:24 -05:00
$ErrorActionPreference = 'Stop'
$ImageDefault = 'ghcr.io/chopratejas/headroom:latest'
2026-04-11 15:56:18 -05:00
$InstallImage = if ( $env:HEADROOM_DOCKER_IMAGE ) { $env:HEADROOM_DOCKER_IMAGE } else { $ImageDefault }
2026-04-10 23:27:24 -05:00
$InstallDir = Join-Path $HOME '.local\bin'
if ( -not ( Test-Path ( Join-Path $HOME '.local' ) ) ) {
$InstallDir = Join-Path $HOME 'bin'
}
function Write-Info {
param ( [ string ] $Message )
Write-Host " ==> $Message "
}
function Require-Command {
param ( [ string ] $Name )
if ( -not ( Get-Command $Name -ErrorAction SilentlyContinue ) ) {
throw " Missing required command: $Name "
}
}
function Ensure-PathEntry {
param ( [ string ] $PathEntry )
$currentPath = [ Environment ] :: GetEnvironmentVariable ( 'Path' , 'User' )
$parts = @ ( )
if ( $currentPath ) {
$parts = $currentPath -split ';' | Where-Object { $_ }
}
if ( $parts -notcontains $PathEntry ) {
$newPath = @ ( $PathEntry ) + $parts
[ Environment ] :: SetEnvironmentVariable ( 'Path' , ( $newPath -join ';' ) , 'User' )
}
}
function Ensure-ProfileBlock {
param ( [ string ] $PathEntry )
$markerStart = '# >>> headroom docker-native >>>'
$markerEnd = '# <<< headroom docker-native <<<'
2026-04-11 15:56:18 -05:00
$escapedPathEntry = $PathEntry . Replace ( " ' " , " '' " )
2026-04-10 23:27:24 -05:00
$block = @"
$markerStart
2026-04-11 15:56:18 -05:00
if ( -not ( ( `$ env : Path -split ';' ) -contains '$escapedPathEntry' ) ) {
`$ env : Path = '$escapedPathEntry;' + `$ env : Path
2026-04-10 23:27:24 -05:00
}
$markerEnd
" @
$profileDir = Split-Path -Parent $PROFILE
if ( -not ( Test-Path $profileDir ) ) {
New-Item -ItemType Directory -Force -Path $profileDir | Out-Null
}
if ( -not ( Test-Path $PROFILE ) ) {
New-Item -ItemType File -Force -Path $PROFILE | Out-Null
}
$existing = Get-Content -Raw -Path $PROFILE
if ( $existing -notmatch [ regex ] :: Escape ( $markerStart ) ) {
Add-Content -Path $PROFILE -Value " `n $block "
}
}
function Write-Wrapper {
param ( [ string ] $TargetDir )
$wrapperPath = Join-Path $TargetDir 'headroom.ps1'
$cmdPath = Join-Path $TargetDir 'headroom.cmd'
fix: harden persistent install wrappers and review gaps
Align Docker-native wrapper help and runtime behavior with the Python install contract, including persistent deployment metadata, baked install-image defaults, and explicit unsupported wrap targets.
Harden the Python persistent-install path with profile validation, safer provider-scope handling, Windows environment restoration, runtime parity improvements, and rollback-safe apply/update behavior.
Update README, Docker install docs, CI, and focused regressions to cover the Windows BOM failure, wrapper parity, compose coverage, and Docker-native wrap behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 17:34:40 -05:00
$resolvedInstallImage = $InstallImage . Replace ( " ' " , " '' " )
2026-04-10 23:27:24 -05:00
$wrapper = @ '
$ErrorActionPreference = 'Stop'
fix: harden persistent install wrappers and review gaps
Align Docker-native wrapper help and runtime behavior with the Python install contract, including persistent deployment metadata, baked install-image defaults, and explicit unsupported wrap targets.
Harden the Python persistent-install path with profile validation, safer provider-scope handling, Windows environment restoration, runtime parity improvements, and rollback-safe apply/update behavior.
Update README, Docker install docs, CI, and focused regressions to cover the Windows BOM failure, wrapper parity, compose coverage, and Docker-native wrap behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 17:34:40 -05:00
$HeadroomImage = if ( $env:HEADROOM_DOCKER_IMAGE ) { $env:HEADROOM_DOCKER_IMAGE } else { '__HEADROOM_INSTALL_IMAGE__' }
2026-04-10 23:27:24 -05:00
$ContainerHome = if ( $env:HEADROOM_CONTAINER_HOME ) { $env:HEADROOM_CONTAINER_HOME } else { '/tmp/headroom-home' }
$HostHome = $HOME
function Fail {
param ( [ string ] $Message )
throw $Message
}
function Require-Command {
param ( [ string ] $Name )
if ( -not ( Get-Command $Name -ErrorAction SilentlyContinue ) ) {
Fail " Missing required command: $Name "
}
}
function Get-RtkTarget {
$arch = if ( $env:PROCESSOR_ARCHITECTURE -match 'ARM64' ) { 'aarch64' } else { 'x86_64' }
return " ${arch} -pc-windows-msvc "
}
function Ensure-HostDirs {
foreach ( $dir in @ (
( Join-Path $HostHome '.headroom' ) ,
( Join-Path $HostHome '.claude' ) ,
( Join-Path $HostHome '.codex' ) ,
( Join-Path $HostHome '.gemini' )
) ) {
if ( -not ( Test-Path $dir ) ) {
New-Item -ItemType Directory -Force -Path $dir | Out-Null
}
}
}
function Get-PassthroughEnvArgs {
$args = New-Object System . Collections . Generic . List [ string ]
$prefixes = @ (
'HEADROOM_' , 'ANTHROPIC_' , 'OPENAI_' , 'GEMINI_' , 'AWS_' , 'AZURE_' , 'VERTEX_' ,
'GOOGLE_' , 'GOOGLE_CLOUD_' , 'MISTRAL_' , 'GROQ_' , 'OPENROUTER_' , 'XAI_' ,
'TOGETHER_' , 'COHERE_' , 'OLLAMA_' , 'LITELLM_' , 'OTEL_' , 'SUPABASE_' ,
'QDRANT_' , 'NEO4J_' , 'LANGSMITH_'
)
foreach ( $item in Get-ChildItem Env : ) {
foreach ( $prefix in $prefixes ) {
if ( $item . Name . StartsWith ( $prefix , [ System.StringComparison ] :: OrdinalIgnoreCase ) ) {
$args . Add ( '--env' )
$args . Add ( $item . Name )
break
}
}
}
return , $args . ToArray ( )
}
function Get-SharedDockerArgs {
Ensure-HostDirs
$args = New-Object System . Collections . Generic . List [ string ]
$args . Add ( '--workdir' )
$args . Add ( '/workspace' )
$args . Add ( '--env' )
$args . Add ( " HOME= $ContainerHome " )
$args . Add ( '--env' )
$args . Add ( 'PYTHONUNBUFFERED=1' )
2026-04-16 19:19:25 -05:00
# Canonical Headroom filesystem contract (issue #175).
$args . Add ( '--env' )
$args . Add ( " HEADROOM_WORKSPACE_DIR= $ContainerHome /.headroom " )
$args . Add ( '--env' )
$args . Add ( " HEADROOM_CONFIG_DIR= $ContainerHome /.headroom/config " )
2026-04-10 23:27:24 -05:00
$args . Add ( '--volume' )
$args . Add ( " ${PWD} :/workspace " )
$args . Add ( '--volume' )
$args . Add ( ( Join-Path $HostHome '.headroom' ) + " : $ContainerHome /.headroom " )
$args . Add ( '--volume' )
$args . Add ( ( Join-Path $HostHome '.claude' ) + " : $ContainerHome /.claude " )
$args . Add ( '--volume' )
$args . Add ( ( Join-Path $HostHome '.codex' ) + " : $ContainerHome /.codex " )
$args . Add ( '--volume' )
$args . Add ( ( Join-Path $HostHome '.gemini' ) + " : $ContainerHome /.gemini " )
foreach ( $entry in ( Get-PassthroughEnvArgs ) ) {
$args . Add ( $entry )
}
return , $args . ToArray ( )
}
2026-04-11 15:56:18 -05:00
function Add-TtyArgs {
param ( $ArgsList )
if ( -not [ Console ] :: IsInputRedirected -and -not [ Console ] :: IsOutputRedirected ) {
$ArgsList . Add ( '-it' )
return
}
if ( -not [ Console ] :: IsInputRedirected ) {
$ArgsList . Add ( '-i' )
}
if ( -not [ Console ] :: IsOutputRedirected ) {
$ArgsList . Add ( '-t' )
}
}
2026-04-10 23:27:24 -05:00
function Invoke-HeadroomDocker {
param ( [ string[] ] $Arguments )
$dockerArgs = New-Object System . Collections . Generic . List [ string ]
2026-04-11 15:56:18 -05:00
$dockerArgs . AddRange ( [ string[] ] @ ( 'run' , '--rm' ) )
Add-TtyArgs -ArgsList $dockerArgs
2026-04-10 23:27:24 -05:00
$dockerArgs . AddRange ( ( Get-SharedDockerArgs ) )
$dockerArgs . Add ( '--entrypoint' )
$dockerArgs . Add ( 'headroom' )
$dockerArgs . Add ( $HeadroomImage )
foreach ( $arg in $Arguments ) {
$dockerArgs . Add ( $arg )
}
& docker @dockerArgs
if ( $LASTEXITCODE -ne 0 ) {
exit $LASTEXITCODE
}
}
function Wait-Proxy {
param (
[ string ] $ContainerName ,
[ int ] $Port
)
for ( $attempt = 0 ; $attempt -lt 45 ; $attempt + + ) {
try {
Invoke-WebRequest -UseBasicParsing -Uri " http://127.0.0.1: $Port /readyz " | Out-Null
return
} catch {
$running = docker ps - -format '{{.Names}}'
if ( $running -notcontains $ContainerName ) {
break
}
Start-Sleep -Seconds 1
}
}
docker logs $ContainerName | Write-Error
throw " Headroom proxy failed to start on port $Port "
}
function Start-ProxyContainer {
param (
[ int ] $Port ,
[ string[] ] $ProxyArgs
)
$containerName = " headroom-proxy- $Port - $PID "
$dockerArgs = New-Object System . Collections . Generic . List [ string ]
2026-04-11 00:40:16 -05:00
$dockerArgs . AddRange ( [ string[] ] @ ( 'run' , '-d' , '--rm' , '--name' , $containerName , '-p' , " $Port ` : $Port " ) )
2026-04-10 23:27:24 -05:00
$dockerArgs . AddRange ( ( Get-SharedDockerArgs ) )
$dockerArgs . Add ( $HeadroomImage )
$dockerArgs . Add ( '--host' )
$dockerArgs . Add ( '0.0.0.0' )
$dockerArgs . Add ( '--port' )
$dockerArgs . Add ( " $Port " )
foreach ( $arg in $ProxyArgs ) {
$dockerArgs . Add ( $arg )
}
& docker @dockerArgs | Out-Null
if ( $LASTEXITCODE -ne 0 ) {
throw " Failed to start Headroom proxy container "
}
Wait-Proxy -ContainerName $containerName -Port $Port
return $containerName
}
function Stop-ProxyContainer {
param ( [ string ] $ContainerName )
if ( $ContainerName ) {
docker stop $ContainerName | Out-Null
}
}
2026-04-11 15:56:18 -05:00
function Get-PersistentProfileRoot {
param ( [ string ] $Profile )
Assert-ValidProfileName -Profile $Profile
return Join-Path ( Join-Path $HostHome '.headroom\deploy' ) $Profile
}
function Get-PersistentStatePath {
param ( [ string ] $Profile )
return Join-Path ( Get-PersistentProfileRoot -Profile $Profile ) 'docker-native.json'
}
function Get-PersistentManifestPath {
param ( [ string ] $Profile )
return Join-Path ( Get-PersistentProfileRoot -Profile $Profile ) 'manifest.json'
}
function Get-PersistentContainerName {
param ( [ string ] $Profile )
return " headroom- $Profile "
}
function Assert-ValidProfileName {
param ( [ string ] $Profile )
if ( $Profile -notmatch '^[A-Za-z0-9._-]+$' -or $Profile -in @ ( '.' , '..' ) ) {
Fail " Invalid profile name ' $Profile ' "
}
}
function Parse-PortValue {
param ( [ string ] $Value )
$parsed = 0
if ( -not [ int ] :: TryParse ( $Value , [ ref ] $parsed ) -or $parsed -lt 1 -or $parsed -gt 65535 ) {
Fail " Invalid port ' $Value ' "
}
return $parsed
}
function Parse-PositiveIntegerValue {
param ( [ string ] $Value )
$parsed = 0
if ( -not [ int ] :: TryParse ( $Value , [ ref ] $parsed ) -or $parsed -lt 1 ) {
Fail " Invalid value ' $Value ' "
}
return $parsed
}
function Require-OptionValue {
param (
[ string[] ] $Arguments ,
[ int ] $Index ,
[ string ] $Option
)
if ( $Index + 1 -ge $Arguments . Count ) {
Fail " Option $Option requires a value "
}
}
fix: harden persistent install wrappers and review gaps
Align Docker-native wrapper help and runtime behavior with the Python install contract, including persistent deployment metadata, baked install-image defaults, and explicit unsupported wrap targets.
Harden the Python persistent-install path with profile validation, safer provider-scope handling, Windows environment restoration, runtime parity improvements, and rollback-safe apply/update behavior.
Update README, Docker install docs, CI, and focused regressions to cover the Windows BOM failure, wrapper parity, compose coverage, and Docker-native wrap behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 17:34:40 -05:00
function Write-Utf8NoBomFile {
param (
[ string ] $Path ,
[ string ] $Content
)
[ System.IO.File ] :: WriteAllText ( $Path , $Content , [ System.Text.UTF8Encoding ] :: new ( $false ) )
}
2026-04-11 15:56:18 -05:00
function Get-PersistentDockerArgs {
Ensure-HostDirs
$args = New-Object System . Collections . Generic . List [ string ]
$args . Add ( '--workdir' )
$args . Add ( $ContainerHome )
$args . Add ( '--env' )
$args . Add ( " HOME= $ContainerHome " )
$args . Add ( '--env' )
$args . Add ( 'PYTHONUNBUFFERED=1' )
2026-04-16 19:19:25 -05:00
# Canonical Headroom filesystem contract (issue #175).
$args . Add ( '--env' )
$args . Add ( " HEADROOM_WORKSPACE_DIR= $ContainerHome /.headroom " )
$args . Add ( '--env' )
$args . Add ( " HEADROOM_CONFIG_DIR= $ContainerHome /.headroom/config " )
2026-04-11 15:56:18 -05:00
$args . Add ( '--volume' )
$args . Add ( ( Join-Path $HostHome '.headroom' ) + " : $ContainerHome /.headroom " )
$args . Add ( '--volume' )
$args . Add ( ( Join-Path $HostHome '.claude' ) + " : $ContainerHome /.claude " )
$args . Add ( '--volume' )
$args . Add ( ( Join-Path $HostHome '.codex' ) + " : $ContainerHome /.codex " )
$args . Add ( '--volume' )
$args . Add ( ( Join-Path $HostHome '.gemini' ) + " : $ContainerHome /.gemini " )
foreach ( $entry in ( Get-PassthroughEnvArgs ) ) {
$args . Add ( $entry )
}
return , $args . ToArray ( )
}
function Get-ManifestProxyArgs {
param (
[ int ] $Port ,
[ string ] $Backend ,
[ string ] $AnyllmProvider ,
[ string ] $Region ,
[ string ] $Mode ,
[ bool ] $Memory ,
[ bool ] $TelemetryEnabled
)
$args = New-Object System . Collections . Generic . List [ string ]
$args . AddRange ( [ string[] ] @ ( '--host' , '127.0.0.1' , '--port' , " $Port " , '--mode' , $Mode , '--backend' , $Backend ) )
if ( -not $TelemetryEnabled ) {
$args . Add ( '--no-telemetry' )
}
if ( $Memory ) {
$args . AddRange ( [ string[] ] @ ( '--memory' , '--memory-db-path' , " $ContainerHome /.headroom/memory.db " ) )
}
if ( $AnyllmProvider ) {
$args . AddRange ( [ string[] ] @ ( '--anyllm-provider' , $AnyllmProvider ) )
}
if ( $Region ) {
$args . AddRange ( [ string[] ] @ ( '--region' , $Region ) )
}
return , $args . ToArray ( )
}
function Write-PersistentState {
param (
[ string ] $Profile ,
[ string ] $Image ,
[ int ] $Port ,
[ string ] $Backend ,
[ string ] $AnyllmProvider ,
[ string ] $Region ,
[ string ] $Mode ,
[ bool ] $Memory ,
[ bool ] $TelemetryEnabled
)
$root = Get-PersistentProfileRoot -Profile $Profile
New-Item -ItemType Directory -Force -Path $root | Out-Null
$state = [ ordered ] @ {
profile = $Profile
image = $Image
port = $Port
backend = $Backend
anyllm_provider = $AnyllmProvider
region = $Region
proxy_mode = $Mode
memory_enabled = $Memory
telemetry_enabled = $TelemetryEnabled
container_name = Get-PersistentContainerName -Profile $Profile
health_url = " http://127.0.0.1: $Port /readyz "
}
fix: harden persistent install wrappers and review gaps
Align Docker-native wrapper help and runtime behavior with the Python install contract, including persistent deployment metadata, baked install-image defaults, and explicit unsupported wrap targets.
Harden the Python persistent-install path with profile validation, safer provider-scope handling, Windows environment restoration, runtime parity improvements, and rollback-safe apply/update behavior.
Update README, Docker install docs, CI, and focused regressions to cover the Windows BOM failure, wrapper parity, compose coverage, and Docker-native wrap behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 17:34:40 -05:00
Write-Utf8NoBomFile -Path ( Get-PersistentStatePath -Profile $Profile ) -Content ( $state | ConvertTo-Json -Depth 4 )
2026-04-11 15:56:18 -05:00
}
function Write-PersistentManifest {
param (
[ string ] $Profile ,
[ string ] $Image ,
[ int ] $Port ,
[ string ] $Backend ,
[ string ] $AnyllmProvider ,
[ string ] $Region ,
[ string ] $Mode ,
[ bool ] $Memory ,
[ bool ] $TelemetryEnabled ,
[ string[] ] $ProxyArgs
)
$root = Get-PersistentProfileRoot -Profile $Profile
New-Item -ItemType Directory -Force -Path $root | Out-Null
$baseEnv = [ ordered ] @ {
HEADROOM_PORT = " $Port "
HEADROOM_HOST = '127.0.0.1'
HEADROOM_MODE = $Mode
HEADROOM_BACKEND = $Backend
}
$manifest = [ ordered ] @ {
profile = $Profile
preset = 'persistent-docker'
runtime_kind = 'docker'
supervisor_kind = 'none'
scope = 'user'
provider_mode = 'manual'
targets = @ ( )
port = $Port
host = '127.0.0.1'
backend = $Backend
anyllm_provider = if ( $AnyllmProvider ) { $AnyllmProvider } else { $null }
region = if ( $Region ) { $Region } else { $null }
proxy_mode = $Mode
memory_enabled = $Memory
memory_db_path = " $ContainerHome /.headroom/memory.db "
telemetry_enabled = $TelemetryEnabled
image = $Image
service_name = " headroom- $Profile "
container_name = Get-PersistentContainerName -Profile $Profile
health_url = " http://127.0.0.1: $Port /readyz "
base_env = $baseEnv
tool_envs = @ { }
proxy_args = $ProxyArgs
mutations = @ ( )
artifacts = @ ( )
}
fix: harden persistent install wrappers and review gaps
Align Docker-native wrapper help and runtime behavior with the Python install contract, including persistent deployment metadata, baked install-image defaults, and explicit unsupported wrap targets.
Harden the Python persistent-install path with profile validation, safer provider-scope handling, Windows environment restoration, runtime parity improvements, and rollback-safe apply/update behavior.
Update README, Docker install docs, CI, and focused regressions to cover the Windows BOM failure, wrapper parity, compose coverage, and Docker-native wrap behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 17:34:40 -05:00
Write-Utf8NoBomFile -Path ( Get-PersistentManifestPath -Profile $Profile ) -Content ( $manifest | ConvertTo-Json -Depth 8 )
2026-04-11 15:56:18 -05:00
}
function Read-PersistentState {
param ( [ string ] $Profile )
Assert-ValidProfileName -Profile $Profile
$statePath = Get-PersistentStatePath -Profile $Profile
if ( -not ( Test-Path $statePath ) ) {
Fail " No docker-native persistent deployment profile named ' $Profile ' "
}
return Get-Content -Raw -Path $statePath | ConvertFrom-Json
}
function Start-PersistentDockerInstall {
param (
[ string ] $Profile ,
[ string ] $Image ,
[ int ] $Port ,
[ string ] $Backend ,
[ string ] $AnyllmProvider ,
[ string ] $Region ,
[ string ] $Mode ,
[ bool ] $Memory ,
[ bool ] $TelemetryEnabled
)
Assert-ValidProfileName -Profile $Profile
$containerName = Get-PersistentContainerName -Profile $Profile
$proxyArgs = Get-ManifestProxyArgs -Port $Port -Backend $Backend -AnyllmProvider $AnyllmProvider -Region $Region -Mode $Mode -Memory $Memory -TelemetryEnabled $TelemetryEnabled
docker rm -f $containerName | Out-Null 2 > $null
$dockerArgs = New-Object System . Collections . Generic . List [ string ]
$dockerArgs . AddRange ( [ string[] ] @ ( 'run' , '-d' , '--restart' , 'unless-stopped' , '--name' , $containerName , '-p' , " $Port ` : $Port " ) )
$dockerArgs . AddRange ( ( Get-PersistentDockerArgs ) )
fix: harden persistent install wrappers and review gaps
Align Docker-native wrapper help and runtime behavior with the Python install contract, including persistent deployment metadata, baked install-image defaults, and explicit unsupported wrap targets.
Harden the Python persistent-install path with profile validation, safer provider-scope handling, Windows environment restoration, runtime parity improvements, and rollback-safe apply/update behavior.
Update README, Docker install docs, CI, and focused regressions to cover the Windows BOM failure, wrapper parity, compose coverage, and Docker-native wrap behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 17:34:40 -05:00
$dockerArgs . AddRange ( [ string[] ] @ (
'--env' , " HEADROOM_DEPLOYMENT_PROFILE= $Profile " ,
'--env' , 'HEADROOM_DEPLOYMENT_PRESET=persistent-docker' ,
'--env' , 'HEADROOM_DEPLOYMENT_RUNTIME=docker' ,
'--env' , 'HEADROOM_DEPLOYMENT_SUPERVISOR=none' ,
'--env' , 'HEADROOM_DEPLOYMENT_SCOPE=user'
) )
2026-04-11 15:56:18 -05:00
$dockerArgs . Add ( $Image )
$dockerArgs . Add ( '--host' )
$dockerArgs . Add ( '0.0.0.0' )
for ( $i = 2 ; $i -lt $proxyArgs . Count ; $i + + ) {
$dockerArgs . Add ( $proxyArgs [ $i ] )
}
& docker @dockerArgs | Out-Null
if ( $LASTEXITCODE -ne 0 ) {
throw " Failed to start docker-native persistent deployment "
}
try {
Wait-Proxy -ContainerName $containerName -Port $Port
} catch {
docker rm -f $containerName | Out-Null 2 > $null
throw
}
Write-PersistentState -Profile $Profile -Image $Image -Port $Port -Backend $Backend -AnyllmProvider $AnyllmProvider -Region $Region -Mode $Mode -Memory $Memory -TelemetryEnabled $TelemetryEnabled
Write-PersistentManifest -Profile $Profile -Image $Image -Port $Port -Backend $Backend -AnyllmProvider $AnyllmProvider -Region $Region -Mode $Mode -Memory $Memory -TelemetryEnabled $TelemetryEnabled -ProxyArgs $proxyArgs
}
function Stop-PersistentDockerInstall {
param ( [ string ] $Profile )
$state = Read-PersistentState -Profile $Profile
docker stop $state . container_name | Out-Null 2 > $null
docker rm -f $state . container_name | Out-Null 2 > $null
}
function Remove-PersistentDockerInstall {
param ( [ string ] $Profile )
$state = Read-PersistentState -Profile $Profile
docker stop $state . container_name | Out-Null 2 > $null
docker rm -f $state . container_name | Out-Null 2 > $null
$root = Get-PersistentProfileRoot -Profile $Profile
if ( Test-Path $root ) {
Remove-Item -Recurse -Force -Path $root
}
}
function Show-PersistentDockerInstallStatus {
param ( [ string ] $Profile )
$state = Read-PersistentState -Profile $Profile
$status = 'stopped'
$ready = 'no'
$running = docker ps - -format '{{.Names}}'
if ( $running -contains $state . container_name ) {
$status = 'running'
try {
Invoke-WebRequest -UseBasicParsing -Uri $state . health_url | Out-Null
$ready = 'yes'
} catch {
$ready = 'no'
}
}
Write-Host " Profile: $( $state . profile ) "
Write-Host 'Preset: persistent-docker'
Write-Host 'Runtime: docker'
Write-Host 'Supervisor: none'
Write-Host " Port: $( $state . port ) "
Write-Host " Status: $status "
Write-Host " Ready: $ready "
Write-Host " Health URL: $( $state . health_url ) "
}
function Show-InstallHelp {
$lines = @ (
'Usage: headroom install [OPTIONS] COMMAND [ARGS]...' ,
'' ,
' Manage persistent Docker-native Headroom deployments.' ,
'' ,
' The Docker-native wrapper currently supports the persistent-docker preset only.' ,
' Use the Python-native `headroom install` command for persistent-service and' ,
' persistent-task installs, or when you need provider/user/system config mutation.' ,
'' ,
'Options:' ,
' -?, --help Show this message and exit.' ,
'' ,
'Commands:' ,
' apply Install a persistent Docker deployment.' ,
' remove Remove a persistent Docker deployment.' ,
' restart Restart a persistent Docker deployment.' ,
' start Start a persistent Docker deployment.' ,
' status Show persistent Docker deployment status.' ,
' stop Stop a persistent Docker deployment.'
)
Write-Host ( $lines -join [ Environment ] :: NewLine )
}
function Show-InstallApplyHelp {
$lines = @ (
'Usage: headroom install apply [OPTIONS]' ,
'' ,
' Install a persistent Docker deployment.' ,
'' ,
'Options:' ,
' --preset [persistent-docker] Docker-native wrapper supports persistent-docker only.' ,
' --runtime [docker] Docker-native wrapper supports runtime=docker only.' ,
' --profile TEXT Deployment profile name. [default: default]' ,
' -p, --port INTEGER Persistent proxy port. [default: 8787]' ,
' --backend TEXT Proxy backend. [default: anthropic]' ,
' --anyllm-provider TEXT Provider for any-llm backends.' ,
' --region TEXT Cloud region for Bedrock / Vertex style backends.' ,
' --mode TEXT Proxy optimization mode. [default: token]' ,
' --memory Enable persistent memory in the runtime.' ,
' --no-telemetry Disable anonymous telemetry in the runtime.' ,
' --image TEXT Docker image to use. [default: HEADROOM_DOCKER_IMAGE or ghcr.io/chopratejas/headroom:latest]' ,
' -?, --help Show this message and exit.'
)
Write-Host ( $lines -join [ Environment ] :: NewLine )
}
fix: harden persistent install wrappers and review gaps
Align Docker-native wrapper help and runtime behavior with the Python install contract, including persistent deployment metadata, baked install-image defaults, and explicit unsupported wrap targets.
Harden the Python persistent-install path with profile validation, safer provider-scope handling, Windows environment restoration, runtime parity improvements, and rollback-safe apply/update behavior.
Update README, Docker install docs, CI, and focused regressions to cover the Windows BOM failure, wrapper parity, compose coverage, and Docker-native wrap behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 17:34:40 -05:00
function Show-WrapHelp {
$lines = @ (
'Usage: headroom wrap <COMMAND> [OPTIONS] [-- ARGS...]' ,
'' ,
' Launch supported host tools through a Docker-native Headroom proxy.' ,
'' ,
'Supported commands:' ,
' claude' ,
' codex' ,
' aider' ,
' cursor' ,
' openclaw' ,
'' ,
'Notes:' ,
' - GitHub Copilot CLI wrapping is not supported by the Docker-native wrapper.' ,
' - Use the Python-native CLI for unsupported wrap targets.'
)
Write-Host ( $lines -join [ Environment ] :: NewLine )
}
2026-04-11 15:56:18 -05:00
function Parse-InstallApplyArgs {
param ( [ string[] ] $Arguments )
$profile = 'default'
$port = 8787
$backend = 'anthropic'
$anyllmProvider = $null
$region = $null
$mode = 'token'
$memory = $false
$telemetryEnabled = $true
$image = $HeadroomImage
$i = 0
while ( $i -lt $Arguments . Count ) {
$arg = $Arguments [ $i ]
switch -Regex ( $arg ) {
'^--preset$' {
Require-OptionValue -Arguments $Arguments -Index $i -Option '--preset'
if ( $Arguments [ $i + 1 ] -ne 'persistent-docker' ) { Fail 'Docker-native wrapper supports only --preset persistent-docker' }
$i + = 2
continue
}
'^--preset=' {
if ( ( $arg -replace '^--preset=' , '' ) -ne 'persistent-docker' ) { Fail 'Docker-native wrapper supports only --preset persistent-docker' }
$i + = 1
continue
}
'^--runtime$' {
Require-OptionValue -Arguments $Arguments -Index $i -Option '--runtime'
if ( $Arguments [ $i + 1 ] -ne 'docker' ) { Fail 'Docker-native wrapper supports only --runtime docker' }
$i + = 2
continue
}
'^--runtime=' {
if ( ( $arg -replace '^--runtime=' , '' ) -ne 'docker' ) { Fail 'Docker-native wrapper supports only --runtime docker' }
$i + = 1
continue
}
'^(--scope|--providers|--target)$' { Fail 'Docker-native wrapper install does not support provider/user/system mutation flags; use the Python-native CLI for those flows' }
'^(--scope=|--providers=|--target=)' { Fail 'Docker-native wrapper install does not support provider/user/system mutation flags; use the Python-native CLI for those flows' }
'^--profile$' {
Require-OptionValue -Arguments $Arguments -Index $i -Option '--profile'
$profile = $Arguments [ $i + 1 ]
$i + = 2
continue
}
'^--profile=' {
$profile = $arg -replace '^--profile=' , ''
$i + = 1
continue
}
'^(--port|-p)$' {
Require-OptionValue -Arguments $Arguments -Index $i -Option $arg
$port = Parse-PortValue -Value $Arguments [ $i + 1 ]
$i + = 2
continue
}
'^(--port=|-p=)' {
$port = Parse-PortValue -Value ( $arg -replace '^(--port=|-p=)' , '' )
$i + = 1
continue
}
'^--backend$' {
Require-OptionValue -Arguments $Arguments -Index $i -Option '--backend'
$backend = $Arguments [ $i + 1 ]
$i + = 2
continue
}
'^--backend=' {
$backend = $arg -replace '^--backend=' , ''
$i + = 1
continue
}
'^--anyllm-provider$' {
Require-OptionValue -Arguments $Arguments -Index $i -Option '--anyllm-provider'
$anyllmProvider = $Arguments [ $i + 1 ]
$i + = 2
continue
}
'^--anyllm-provider=' {
$anyllmProvider = $arg -replace '^--anyllm-provider=' , ''
$i + = 1
continue
}
'^--region$' {
Require-OptionValue -Arguments $Arguments -Index $i -Option '--region'
$region = $Arguments [ $i + 1 ]
$i + = 2
continue
}
'^--region=' {
$region = $arg -replace '^--region=' , ''
$i + = 1
continue
}
'^--mode$' {
Require-OptionValue -Arguments $Arguments -Index $i -Option '--mode'
$mode = $Arguments [ $i + 1 ]
$i + = 2
continue
}
'^--mode=' {
$mode = $arg -replace '^--mode=' , ''
$i + = 1
continue
}
'^--memory$' {
$memory = $true
$i + = 1
continue
}
'^--no-telemetry$' {
$telemetryEnabled = $false
$i + = 1
continue
}
'^--image$' {
Require-OptionValue -Arguments $Arguments -Index $i -Option '--image'
$image = $Arguments [ $i + 1 ]
$i + = 2
continue
}
'^--image=' {
$image = $arg -replace '^--image=' , ''
$i + = 1
continue
}
fix: harden persistent install wrappers and review gaps
Align Docker-native wrapper help and runtime behavior with the Python install contract, including persistent deployment metadata, baked install-image defaults, and explicit unsupported wrap targets.
Harden the Python persistent-install path with profile validation, safer provider-scope handling, Windows environment restoration, runtime parity improvements, and rollback-safe apply/update behavior.
Update README, Docker install docs, CI, and focused regressions to cover the Windows BOM failure, wrapper parity, compose coverage, and Docker-native wrap behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 17:34:40 -05:00
'^(--help|-\?)$' {
2026-04-11 15:56:18 -05:00
Show-InstallApplyHelp
exit 0
}
default {
Fail " Unsupported option for 'headroom install apply': $arg "
}
}
}
return [ pscustomobject ] @ {
Profile = $profile
Port = $port
Backend = $backend
AnyllmProvider = $anyllmProvider
Region = $region
Mode = $mode
Memory = $memory
TelemetryEnabled = $telemetryEnabled
Image = $image
}
}
function Parse-InstallProfileArgs {
param ( [ string[] ] $Arguments )
$profile = 'default'
$i = 0
while ( $i -lt $Arguments . Count ) {
$arg = $Arguments [ $i ]
switch -Regex ( $arg ) {
'^--profile$' {
Require-OptionValue -Arguments $Arguments -Index $i -Option '--profile'
$profile = $Arguments [ $i + 1 ]
$i + = 2
continue
}
'^--profile=' {
$profile = $arg -replace '^--profile=' , ''
$i + = 1
continue
}
fix: harden persistent install wrappers and review gaps
Align Docker-native wrapper help and runtime behavior with the Python install contract, including persistent deployment metadata, baked install-image defaults, and explicit unsupported wrap targets.
Harden the Python persistent-install path with profile validation, safer provider-scope handling, Windows environment restoration, runtime parity improvements, and rollback-safe apply/update behavior.
Update README, Docker install docs, CI, and focused regressions to cover the Windows BOM failure, wrapper parity, compose coverage, and Docker-native wrap behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 17:34:40 -05:00
'^(--help|-\?)$' {
2026-04-11 15:56:18 -05:00
Show-InstallHelp
exit 0
}
default {
Fail " Unsupported option for 'headroom install': $arg "
}
}
}
return $profile
}
2026-04-10 23:27:24 -05:00
function Invoke-ClaudeRtkInit {
$rtkPath = Join-Path $HostHome '.headroom\bin\rtk.exe'
if ( -not ( Test-Path $rtkPath ) ) {
Write-Warning " rtk was not installed at $rtkPath ; Claude hooks were not registered "
return
}
try {
& $rtkPath init - -global - -auto -patch | Out-Null
} catch {
Write-Warning " Failed to register Claude hooks with rtk; continuing without hook registration "
}
}
2026-05-11 16:30:02 -04:00
function Get-ContextTool {
$value = $env:HEADROOM_CONTEXT_TOOL
if ( [ string ] :: IsNullOrWhiteSpace ( $value ) ) {
return 'rtk'
}
$value = $value . Trim ( ) . ToLowerInvariant ( ) . Replace ( '_' , '-' )
if ( $value -eq 'leanctx' ) {
return 'lean-ctx'
}
if ( $value -ne 'rtk' -and $value -ne 'lean-ctx' ) {
Fail 'HEADROOM_CONTEXT_TOOL must be one of: lean-ctx, rtk'
}
return $value
}
function Invoke-LeanCtxInit {
param ( [ string ] $Agent )
$cmd = Get-Command lean-ctx -ErrorAction SilentlyContinue
if ( -not $cmd ) {
Write-Warning " lean-ctx is not installed on PATH; $Agent lean-ctx setup was skipped "
return
}
try {
& $cmd . Source init - -agent $Agent | Out-Null
} catch {
Write-Warning " Failed to initialize lean-ctx for $Agent ; continuing without lean-ctx setup "
}
}
2026-04-10 23:27:24 -05:00
function Invoke-WithTemporaryEnv {
param (
[ hashtable ] $Environment ,
[ string ] $Command ,
[ string[] ] $Arguments
)
$previous = @ { }
foreach ( $pair in $Environment . GetEnumerator ( ) ) {
$previous [ $pair . Key ] = [ Environment ] :: GetEnvironmentVariable ( $pair . Key , 'Process' )
[ Environment ] :: SetEnvironmentVariable ( $pair . Key , $pair . Value , 'Process' )
}
try {
& $Command @Arguments
return $LASTEXITCODE
} finally {
foreach ( $pair in $Environment . GetEnumerator ( ) ) {
[ Environment ] :: SetEnvironmentVariable ( $pair . Key , $previous [ $pair . Key ] , 'Process' )
}
}
}
2026-04-11 00:04:15 -05:00
function Test-HelpFlag {
param ( [ string[] ] $Arguments )
foreach ( $arg in $Arguments ) {
2026-04-11 00:40:16 -05:00
if ( $arg -eq '--' ) {
break
}
2026-04-11 00:04:15 -05:00
if ( $arg -eq '--help' -or $arg -eq '-?' ) {
return $true
}
}
return $false
}
function Parse-OpenClawWrapArgs {
param ( [ string[] ] $Arguments )
$gatewayProviderIds = New-Object System . Collections . Generic . List [ string ]
$pluginPath = $null
$pluginSpec = 'headroom-ai/openclaw'
$skipBuild = $false
$copy = $false
$proxyPort = 8787
$startupTimeoutMs = 20000
$pythonPath = $null
$noAutoStart = $false
$noRestart = $false
$verbose = $false
$i = 0
while ( $i -lt $Arguments . Count ) {
$arg = $Arguments [ $i ]
switch -Regex ( $arg ) {
'^--plugin-path$' {
2026-04-11 15:56:18 -05:00
Require-OptionValue -Arguments $Arguments -Index $i -Option $arg
2026-04-11 00:04:15 -05:00
$pluginPath = $Arguments [ $i + 1 ]
$i + = 2
continue
}
'^--plugin-path=' {
$pluginPath = $arg -replace '^--plugin-path=' , ''
$i + = 1
continue
}
'^--plugin-spec$' {
2026-04-11 15:56:18 -05:00
Require-OptionValue -Arguments $Arguments -Index $i -Option $arg
2026-04-11 00:04:15 -05:00
$pluginSpec = $Arguments [ $i + 1 ]
$i + = 2
continue
}
'^--plugin-spec=' {
$pluginSpec = $arg -replace '^--plugin-spec=' , ''
$i + = 1
continue
}
'^--skip-build$' {
$skipBuild = $true
$i + = 1
continue
}
'^--copy$' {
$copy = $true
$i + = 1
continue
}
'^--proxy-port$' {
2026-04-11 15:56:18 -05:00
Require-OptionValue -Arguments $Arguments -Index $i -Option $arg
$proxyPort = Parse-PortValue -Value $Arguments [ $i + 1 ]
2026-04-11 00:04:15 -05:00
$i + = 2
continue
}
'^--proxy-port=' {
2026-04-11 15:56:18 -05:00
$proxyPort = Parse-PortValue -Value ( $arg -replace '^--proxy-port=' , '' )
2026-04-11 00:04:15 -05:00
$i + = 1
continue
}
'^--startup-timeout-ms$' {
2026-04-11 15:56:18 -05:00
Require-OptionValue -Arguments $Arguments -Index $i -Option $arg
$startupTimeoutMs = Parse-PositiveIntegerValue -Value $Arguments [ $i + 1 ]
2026-04-11 00:04:15 -05:00
$i + = 2
continue
}
'^--startup-timeout-ms=' {
2026-04-11 15:56:18 -05:00
$startupTimeoutMs = Parse-PositiveIntegerValue -Value ( $arg -replace '^--startup-timeout-ms=' , '' )
2026-04-11 00:04:15 -05:00
$i + = 1
continue
}
'^--gateway-provider-id$' {
2026-04-11 15:56:18 -05:00
Require-OptionValue -Arguments $Arguments -Index $i -Option $arg
2026-04-11 00:04:15 -05:00
$gatewayProviderIds . Add ( $Arguments [ $i + 1 ] )
$i + = 2
continue
}
'^--gateway-provider-id=' {
$gatewayProviderIds . Add ( $arg -replace '^--gateway-provider-id=' , '' )
$i + = 1
continue
}
'^--python-path$' {
2026-04-11 15:56:18 -05:00
Require-OptionValue -Arguments $Arguments -Index $i -Option $arg
2026-04-11 00:04:15 -05:00
$pythonPath = $Arguments [ $i + 1 ]
$i + = 2
continue
}
'^--python-path=' {
$pythonPath = $arg -replace '^--python-path=' , ''
$i + = 1
continue
}
'^--no-auto-start$' {
$noAutoStart = $true
$i + = 1
continue
}
'^--no-restart$' {
$noRestart = $true
$i + = 1
continue
}
'^--verbose$|^-v$' {
$verbose = $true
$i + = 1
continue
}
default {
Fail " Unsupported option for 'headroom wrap openclaw': $arg "
}
}
}
[ pscustomobject ] @ {
PluginPath = $pluginPath
PluginSpec = $pluginSpec
SkipBuild = $skipBuild
Copy = $copy
ProxyPort = $proxyPort
StartupTimeoutMs = $startupTimeoutMs
GatewayProviderIds = $gatewayProviderIds . ToArray ( )
PythonPath = $pythonPath
NoAutoStart = $noAutoStart
NoRestart = $noRestart
Verbose = $verbose
}
}
function Parse-OpenClawUnwrapArgs {
param ( [ string[] ] $Arguments )
$noRestart = $false
$verbose = $false
$i = 0
while ( $i -lt $Arguments . Count ) {
$arg = $Arguments [ $i ]
switch -Regex ( $arg ) {
'^--no-restart$' {
$noRestart = $true
$i + = 1
continue
}
'^--verbose$|^-v$' {
$verbose = $true
$i + = 1
continue
}
default {
Fail " Unsupported option for 'headroom unwrap openclaw': $arg "
}
}
}
[ pscustomobject ] @ {
NoRestart = $noRestart
Verbose = $verbose
}
}
function Invoke-CapturedCommand {
param (
[ string ] $Action ,
[ string ] $Command ,
[ string[] ] $Arguments ,
[ string ] $WorkingDirectory
)
$previous = $null
try {
if ( $WorkingDirectory ) {
$previous = Get-Location
Set-Location $WorkingDirectory
}
$output = ( & $Command @Arguments 2 > & 1 | Out-String ) . Trim ( )
$exitCode = $LASTEXITCODE
} finally {
if ( $previous ) {
Set-Location $previous
}
}
if ( $exitCode -ne 0 ) {
if ( -not $output ) {
$output = " exit code $exitCode "
}
Fail " $Action failed: $output "
}
return $output
}
function Get-OpenClawExistingEntryJson {
$output = ( & openclaw config get plugins . entries . headroom 2 > $null | Out-String ) . Trim ( )
if ( $LASTEXITCODE -ne 0 ) {
return $null
}
return $output
}
function Invoke-OpenClawPrepareEntryJson {
param (
[ string ] $ExistingEntryJson ,
[ pscustomobject ] $Parsed
)
$dockerArgs = New-Object System . Collections . Generic . List [ string ]
2026-04-11 00:40:16 -05:00
$dockerArgs . AddRange ( [ string[] ] @ ( 'run' , '--rm' ) )
2026-04-11 00:04:15 -05:00
$dockerArgs . AddRange ( ( Get-SharedDockerArgs ) )
$dockerArgs . Add ( '--entrypoint' )
$dockerArgs . Add ( 'headroom' )
$dockerArgs . Add ( $HeadroomImage )
2026-04-11 00:40:16 -05:00
$dockerArgs . AddRange ( [ string[] ] @ ( 'wrap' , 'openclaw' , '--prepare-only' , '--proxy-port' , " $( $Parsed . ProxyPort ) " , '--startup-timeout-ms' , " $( $Parsed . StartupTimeoutMs ) " ) )
2026-04-11 00:04:15 -05:00
if ( $ExistingEntryJson ) {
$dockerArgs . Add ( '--existing-entry-json' )
$dockerArgs . Add ( $ExistingEntryJson )
}
if ( $Parsed . PythonPath ) {
$dockerArgs . Add ( '--python-path' )
$dockerArgs . Add ( $Parsed . PythonPath )
}
if ( $Parsed . NoAutoStart ) {
$dockerArgs . Add ( '--no-auto-start' )
}
foreach ( $providerId in $Parsed . GatewayProviderIds ) {
$dockerArgs . Add ( '--gateway-provider-id' )
$dockerArgs . Add ( $providerId )
}
$output = ( & docker @dockerArgs 2 > & 1 | Out-String ) . Trim ( )
if ( $LASTEXITCODE -ne 0 ) {
Fail " Failed to prepare docker-native OpenClaw config: $output "
}
return $output
}
function Invoke-OpenClawPrepareUnwrapEntryJson {
param ( [ string ] $ExistingEntryJson )
$dockerArgs = New-Object System . Collections . Generic . List [ string ]
2026-04-11 00:40:16 -05:00
$dockerArgs . AddRange ( [ string[] ] @ ( 'run' , '--rm' ) )
2026-04-11 00:04:15 -05:00
$dockerArgs . AddRange ( ( Get-SharedDockerArgs ) )
$dockerArgs . Add ( '--entrypoint' )
$dockerArgs . Add ( 'headroom' )
$dockerArgs . Add ( $HeadroomImage )
2026-04-11 00:40:16 -05:00
$dockerArgs . AddRange ( [ string[] ] @ ( 'unwrap' , 'openclaw' , '--prepare-only' ) )
2026-04-11 00:04:15 -05:00
if ( $ExistingEntryJson ) {
$dockerArgs . Add ( '--existing-entry-json' )
$dockerArgs . Add ( $ExistingEntryJson )
}
$output = ( & docker @dockerArgs 2 > & 1 | Out-String ) . Trim ( )
if ( $LASTEXITCODE -ne 0 ) {
Fail " Failed to prepare docker-native OpenClaw unwrap config: $output "
}
return $output
}
function Resolve-OpenClawExtensionsDir {
$configOutput = Invoke-CapturedCommand -Action 'openclaw config file' -Command 'openclaw' -Arguments @ ( 'config' , 'file' )
$configPath = ( $configOutput -split " `r ? `n " ) [ -1 ] . Trim ( )
if ( -not $configPath ) {
Fail 'Unable to resolve OpenClaw config path.'
}
return ( Join-Path ( Split-Path -Parent $configPath ) 'extensions' )
}
function Copy-OpenClawPluginIntoExtensions {
param ( [ string ] $PluginPath )
$distDir = Join-Path $PluginPath 'dist'
$hookShimDir = Join-Path $PluginPath 'hook-shim'
if ( -not ( Test-Path $distDir ) ) {
Fail " Plugin dist folder missing at $distDir . Build the plugin first. "
}
if ( -not ( Test-Path $hookShimDir ) ) {
Fail " Plugin hook-shim folder missing at $hookShimDir . Build the plugin first. "
}
$extensionsDir = Resolve-OpenClawExtensionsDir
$targetDir = Join-Path $extensionsDir 'headroom'
$targetDist = Join-Path $targetDir 'dist'
$targetHookShim = Join-Path $targetDir 'hook-shim'
New-Item -ItemType Directory -Force -Path $targetDir | Out-Null
if ( Test-Path $targetDist ) { Remove-Item -Recurse -Force $targetDist }
if ( Test-Path $targetHookShim ) { Remove-Item -Recurse -Force $targetHookShim }
Copy-Item -Recurse -Force $distDir $targetDist
Copy-Item -Recurse -Force $hookShimDir $targetHookShim
foreach ( $fileName in @ ( 'openclaw.plugin.json' , 'package.json' , 'README.md' ) ) {
$source = Join-Path $PluginPath $fileName
if ( Test-Path $source ) {
Copy-Item -Force $source ( Join-Path $targetDir $fileName )
}
}
return $targetDir
}
function Install-OpenClawPlugin {
param ( [ pscustomobject ] $Parsed )
if ( $Parsed . PluginPath ) {
if ( -not ( Test-Path $Parsed . PluginPath ) ) {
Fail " Plugin path not found: $( $Parsed . PluginPath ) . "
}
if ( -not ( Test-Path ( Join-Path $Parsed . PluginPath 'package.json' ) ) ) {
Fail " Invalid plugin path (missing package.json): $( $Parsed . PluginPath ) "
}
if ( -not ( Test-Path ( Join-Path $Parsed . PluginPath 'openclaw.plugin.json' ) ) ) {
Fail " Invalid plugin path (missing openclaw.plugin.json): $( $Parsed . PluginPath ) "
}
}
if ( $Parsed . PluginPath -and -not $Parsed . SkipBuild ) {
Require-Command npm
Write-Host ' Building OpenClaw plugin (npm install + npm run build)...'
[ void ] ( Invoke-CapturedCommand -Action 'npm install' -Command 'npm' -Arguments @ ( 'install' ) -WorkingDirectory $Parsed . PluginPath )
[ void ] ( Invoke-CapturedCommand -Action 'npm run build' -Command 'npm' -Arguments @ ( 'run' , 'build' ) -WorkingDirectory $Parsed . PluginPath )
}
if ( $Parsed . PluginPath ) {
if ( $Parsed . Copy ) {
$arguments = @ ( 'plugins' , 'install' , '--dangerously-force-unsafe-install' , $Parsed . PluginPath )
$workingDirectory = $null
} else {
$arguments = @ ( 'plugins' , 'install' , '--dangerously-force-unsafe-install' , '--link' , '.' )
$workingDirectory = $Parsed . PluginPath
}
} else {
$arguments = @ ( 'plugins' , 'install' , '--dangerously-force-unsafe-install' , $Parsed . PluginSpec )
$workingDirectory = $null
}
$previous = $null
try {
if ( $workingDirectory ) {
$previous = Get-Location
Set-Location $workingDirectory
}
$installOutput = ( & openclaw @arguments 2 > & 1 | Out-String ) . Trim ( )
$installExitCode = $LASTEXITCODE
} finally {
if ( $previous ) {
Set-Location $previous
}
}
if ( $installExitCode -eq 0 ) {
if ( $Parsed . Verbose -and $installOutput ) {
Write-Host $installOutput
}
return
}
$lowerOutput = $installOutput . ToLowerInvariant ( )
if ( $lowerOutput . Contains ( 'plugin already exists' ) ) {
Write-Host ' Plugin already installed; continuing with configuration/update steps.'
return
}
if ( $Parsed . PluginPath -and -not $Parsed . Copy -and $lowerOutput . Contains ( 'also not a valid hook pack' ) ) {
Write-Host ' OpenClaw linked-path install bug detected; applying extension-path fallback...'
$targetDir = Copy-OpenClawPluginIntoExtensions -PluginPath $Parsed . PluginPath
Write-Host " Fallback plugin copy completed: $targetDir "
return
}
if ( -not $installOutput ) {
$installOutput = " exit code $installExitCode "
}
Fail " openclaw plugins install failed: $installOutput "
}
function Restart-OrStartOpenClawGateway {
$restartOutput = ( & openclaw gateway restart 2 > & 1 | Out-String ) . Trim ( )
if ( $LASTEXITCODE -eq 0 ) {
return [ pscustomobject ] @ { Action = 'restarted' ; Output = $restartOutput }
}
$startOutput = Invoke-CapturedCommand -Action 'openclaw gateway start' -Command 'openclaw' -Arguments @ ( 'gateway' , 'start' )
return [ pscustomobject ] @ { Action = 'started' ; Output = $startOutput }
}
function Invoke-OpenClawWrap {
param ( [ string[] ] $Arguments )
Require-Command openclaw
$parsed = Parse-OpenClawWrapArgs -Arguments $Arguments
$existingEntryJson = Get-OpenClawExistingEntryJson
$entryJson = Invoke-OpenClawPrepareEntryJson -ExistingEntryJson $existingEntryJson -Parsed $parsed
Write-Host " "
Write-Host " ╔═══════════════════════════════════════════════╗ "
Write-Host " ║ HEADROOM WRAP: OPENCLAW ║ "
Write-Host " ╚═══════════════════════════════════════════════╝ "
Write-Host " "
if ( $parsed . PluginPath ) {
Write-Host " Plugin source: local ( $( $parsed . PluginPath ) ) "
} else {
Write-Host " Plugin source: npm ( $( $parsed . PluginSpec ) ) "
}
Write-Host ' Writing plugin configuration...'
[ void ] ( Invoke-CapturedCommand -Action 'openclaw config set plugins.entries.headroom' -Command 'openclaw' -Arguments @ ( 'config' , 'set' , 'plugins.entries.headroom' , $entryJson , '--strict-json' ) )
Write-Host ' Installing OpenClaw plugin with required unsafe-install flag...'
Install-OpenClawPlugin -Parsed $parsed
[ void ] ( Invoke-CapturedCommand -Action 'openclaw config set plugins.slots.contextEngine' -Command 'openclaw' -Arguments @ ( 'config' , 'set' , 'plugins.slots.contextEngine' , '"headroom"' , '--strict-json' ) )
[ void ] ( Invoke-CapturedCommand -Action 'openclaw config validate' -Command 'openclaw' -Arguments @ ( 'config' , 'validate' ) )
if ( $parsed . NoRestart ) {
Write-Host ' Skipping gateway restart (--no-restart).'
Write-Host ' Run `openclaw gateway restart` (or `openclaw gateway start`) to apply plugin changes.'
} else {
Write-Host ' Applying plugin changes to OpenClaw gateway...'
$gatewayResult = Restart-OrStartOpenClawGateway
Write-Host " Gateway $( $gatewayResult . Action ) . "
if ( $parsed . Verbose -and $gatewayResult . Output ) {
Write-Host $gatewayResult . Output
}
}
$inspectOutput = Invoke-CapturedCommand -Action 'openclaw plugins inspect headroom' -Command 'openclaw' -Arguments @ ( 'plugins' , 'inspect' , 'headroom' )
if ( $parsed . Verbose -and $inspectOutput ) {
Write-Host $inspectOutput
}
Write-Host " "
Write-Host " ✓ OpenClaw is configured to use Headroom context compression. "
Write-Host " Plugin: headroom "
Write-Host " Slot: plugins.slots.contextEngine = headroom "
Write-Host " "
}
function Invoke-OpenClawUnwrap {
param ( [ string[] ] $Arguments )
Require-Command openclaw
$parsed = Parse-OpenClawUnwrapArgs -Arguments $Arguments
$existingEntryJson = Get-OpenClawExistingEntryJson
$entryJson = Invoke-OpenClawPrepareUnwrapEntryJson -ExistingEntryJson $existingEntryJson
Write-Host " "
Write-Host " ╔═══════════════════════════════════════════════╗ "
Write-Host " ║ HEADROOM UNWRAP: OPENCLAW ║ "
Write-Host " ╚═══════════════════════════════════════════════╝ "
Write-Host " "
Write-Host ' Disabling Headroom plugin and removing engine mapping...'
[ void ] ( Invoke-CapturedCommand -Action 'openclaw config set plugins.entries.headroom' -Command 'openclaw' -Arguments @ ( 'config' , 'set' , 'plugins.entries.headroom' , $entryJson , '--strict-json' ) )
[ void ] ( Invoke-CapturedCommand -Action 'openclaw config set plugins.slots.contextEngine' -Command 'openclaw' -Arguments @ ( 'config' , 'set' , 'plugins.slots.contextEngine' , '"legacy"' , '--strict-json' ) )
[ void ] ( Invoke-CapturedCommand -Action 'openclaw config validate' -Command 'openclaw' -Arguments @ ( 'config' , 'validate' ) )
if ( $parsed . NoRestart ) {
Write-Host ' Skipping gateway restart (--no-restart).'
Write-Host ' Run `openclaw gateway restart` (or `openclaw gateway start`) to apply unwrap changes.'
} else {
Write-Host ' Applying unwrap changes to OpenClaw gateway...'
$gatewayResult = Restart-OrStartOpenClawGateway
Write-Host " Gateway $( $gatewayResult . Action ) . "
if ( $parsed . Verbose -and $gatewayResult . Output ) {
Write-Host $gatewayResult . Output
}
}
if ( $parsed . Verbose ) {
$inspectOutput = Invoke-CapturedCommand -Action 'openclaw plugins inspect headroom' -Command 'openclaw' -Arguments @ ( 'plugins' , 'inspect' , 'headroom' )
if ( $inspectOutput ) {
Write-Host $inspectOutput
}
}
Write-Host " "
Write-Host " ✓ OpenClaw Headroom wrap removed. "
Write-Host " Plugin: headroom (installed, disabled) "
Write-Host " Slot: plugins.slots.contextEngine = legacy "
Write-Host " "
}
2026-04-10 23:27:24 -05:00
function Parse-WrapArgs {
param ( [ string[] ] $Arguments )
$known = New-Object System . Collections . Generic . List [ string ]
2026-04-11 15:56:18 -05:00
$hostArgs = New-Object System . Collections . Generic . List [ string ]
2026-04-10 23:27:24 -05:00
$port = 8787
$noRtk = $false
$noProxy = $false
$learn = $false
$backend = $null
$anyllm = $null
$region = $null
$i = 0
while ( $i -lt $Arguments . Count ) {
$arg = $Arguments [ $i ]
switch -Regex ( $arg ) {
'^--$' {
for ( $j = $i + 1 ; $j -lt $Arguments . Count ; $j + + ) {
2026-04-11 15:56:18 -05:00
$hostArgs . Add ( $Arguments [ $j ] )
2026-04-10 23:27:24 -05:00
}
$i = $Arguments . Count
continue
}
'^--port$|^-p$' {
2026-04-11 15:56:18 -05:00
Require-OptionValue -Arguments $Arguments -Index $i -Option $arg
$port = Parse-PortValue -Value $Arguments [ $i + 1 ]
2026-04-10 23:27:24 -05:00
$known . Add ( $arg )
$known . Add ( $Arguments [ $i + 1 ] )
$i + = 2
continue
}
'^--port=' {
2026-04-11 15:56:18 -05:00
$port = Parse-PortValue -Value ( $arg -replace '^--port=' , '' )
2026-04-10 23:27:24 -05:00
$known . Add ( $arg )
$i + = 1
continue
}
'^--no-rtk$' {
$noRtk = $true
$known . Add ( $arg )
$i + = 1
continue
}
'^--no-proxy$' {
$noProxy = $true
$known . Add ( $arg )
$i + = 1
continue
}
'^--learn$' {
$learn = $true
$known . Add ( $arg )
$i + = 1
continue
}
'^--verbose$|^-v$' {
$known . Add ( $arg )
$i + = 1
continue
}
'^--backend$' {
2026-04-11 15:56:18 -05:00
Require-OptionValue -Arguments $Arguments -Index $i -Option $arg
2026-04-10 23:27:24 -05:00
$backend = $Arguments [ $i + 1 ]
$known . Add ( $arg )
$known . Add ( $Arguments [ $i + 1 ] )
$i + = 2
continue
}
'^--backend=' {
$backend = $arg -replace '^--backend=' , ''
$known . Add ( $arg )
$i + = 1
continue
}
'^--anyllm-provider$' {
2026-04-11 15:56:18 -05:00
Require-OptionValue -Arguments $Arguments -Index $i -Option $arg
2026-04-10 23:27:24 -05:00
$anyllm = $Arguments [ $i + 1 ]
$known . Add ( $arg )
$known . Add ( $Arguments [ $i + 1 ] )
$i + = 2
continue
}
'^--anyllm-provider=' {
$anyllm = $arg -replace '^--anyllm-provider=' , ''
$known . Add ( $arg )
$i + = 1
continue
}
'^--region$' {
2026-04-11 15:56:18 -05:00
Require-OptionValue -Arguments $Arguments -Index $i -Option $arg
2026-04-10 23:27:24 -05:00
$region = $Arguments [ $i + 1 ]
$known . Add ( $arg )
$known . Add ( $Arguments [ $i + 1 ] )
$i + = 2
continue
}
'^--region=' {
$region = $arg -replace '^--region=' , ''
$known . Add ( $arg )
$i + = 1
continue
}
default {
for ( $j = $i ; $j -lt $Arguments . Count ; $j + + ) {
2026-04-11 15:56:18 -05:00
$hostArgs . Add ( $Arguments [ $j ] )
2026-04-10 23:27:24 -05:00
}
$i = $Arguments . Count
}
}
}
[ pscustomobject ] @ {
KnownArgs = $known . ToArray ( )
2026-04-11 15:56:18 -05:00
HostArgs = $hostArgs . ToArray ( )
2026-04-10 23:27:24 -05:00
Port = $port
NoRtk = $noRtk
NoProxy = $noProxy
Learn = $learn
Backend = $backend
Anyllm = $anyllm
Region = $region
}
}
function Invoke-PrepareOnly {
param (
[ string ] $Tool ,
[ string[] ] $KnownArgs
)
$dockerArgs = New-Object System . Collections . Generic . List [ string ]
2026-04-11 15:56:18 -05:00
$dockerArgs . AddRange ( [ string[] ] @ ( 'run' , '--rm' ) )
Add-TtyArgs -ArgsList $dockerArgs
2026-04-10 23:27:24 -05:00
$dockerArgs . AddRange ( ( Get-SharedDockerArgs ) )
$dockerArgs . Add ( '--env' )
$dockerArgs . Add ( " HEADROOM_RTK_TARGET= $( Get-RtkTarget ) " )
$dockerArgs . Add ( '--entrypoint' )
$dockerArgs . Add ( 'headroom' )
$dockerArgs . Add ( $HeadroomImage )
2026-04-11 00:40:16 -05:00
$dockerArgs . AddRange ( [ string[] ] @ ( 'wrap' , $Tool , '--prepare-only' ) )
2026-04-10 23:27:24 -05:00
foreach ( $arg in $KnownArgs ) {
$dockerArgs . Add ( $arg )
}
& docker @dockerArgs
if ( $LASTEXITCODE -ne 0 ) {
throw " Failed to prepare docker-native wrap for $Tool "
}
}
Require-Command docker
if ( $args . Count -eq 0 ) {
Invoke-HeadroomDocker -Arguments @ ( '--help' )
exit 0
}
switch ( $args [ 0 ] ) {
2026-04-11 15:56:18 -05:00
'install' {
if ( $args . Count -eq 1 -or $args [ 1 ] -eq '--help' -or $args [ 1 ] -eq '-?' ) {
Show-InstallHelp
exit 0
}
$installCommand = $args [ 1 ]
$installArgs = if ( $args . Count -gt 2 ) { $args [ 2 . . ( $args . Count - 1 ) ] } else { @ ( ) }
switch ( $installCommand ) {
'apply' {
$parsed = Parse-InstallApplyArgs -Arguments $installArgs
Start-PersistentDockerInstall -Profile $parsed . Profile -Image $parsed . Image -Port $parsed . Port -Backend $parsed . Backend -AnyllmProvider $parsed . AnyllmProvider -Region $parsed . Region -Mode $parsed . Mode -Memory $parsed . Memory -TelemetryEnabled $parsed . TelemetryEnabled
Write-Host " Installed docker-native persistent deployment ' $( $parsed . Profile ) ' on port $( $parsed . Port ) . "
exit 0
}
'status' {
$profile = Parse-InstallProfileArgs -Arguments $installArgs
Show-PersistentDockerInstallStatus -Profile $profile
exit 0
}
'start' {
$profile = Parse-InstallProfileArgs -Arguments $installArgs
$state = Read-PersistentState -Profile $profile
Start-PersistentDockerInstall -Profile $state . profile -Image $state . image -Port $state . port -Backend $state . backend -AnyllmProvider $state . anyllm_provider -Region $state . region -Mode $state . proxy_mode -Memory ( [ bool ] $state . memory_enabled ) -TelemetryEnabled ( [ bool ] $state . telemetry_enabled )
Write-Host " Started docker-native persistent deployment ' $profile '. "
exit 0
}
'stop' {
$profile = Parse-InstallProfileArgs -Arguments $installArgs
Stop-PersistentDockerInstall -Profile $profile
Write-Host " Stopped docker-native persistent deployment ' $profile '. "
exit 0
}
'restart' {
$profile = Parse-InstallProfileArgs -Arguments $installArgs
$state = Read-PersistentState -Profile $profile
Start-PersistentDockerInstall -Profile $state . profile -Image $state . image -Port $state . port -Backend $state . backend -AnyllmProvider $state . anyllm_provider -Region $state . region -Mode $state . proxy_mode -Memory ( [ bool ] $state . memory_enabled ) -TelemetryEnabled ( [ bool ] $state . telemetry_enabled )
Write-Host " Restarted docker-native persistent deployment ' $profile '. "
exit 0
}
'remove' {
$profile = Parse-InstallProfileArgs -Arguments $installArgs
Remove-PersistentDockerInstall -Profile $profile
Write-Host " Removed docker-native persistent deployment ' $profile '. "
exit 0
}
default {
Fail " Unsupported install target: $installCommand "
}
}
}
2026-04-10 23:27:24 -05:00
'wrap' {
2026-04-11 00:40:16 -05:00
if ( $args . Count -eq 1 -or $args [ 1 ] -eq '--help' -or $args [ 1 ] -eq '-?' ) {
fix: harden persistent install wrappers and review gaps
Align Docker-native wrapper help and runtime behavior with the Python install contract, including persistent deployment metadata, baked install-image defaults, and explicit unsupported wrap targets.
Harden the Python persistent-install path with profile validation, safer provider-scope handling, Windows environment restoration, runtime parity improvements, and rollback-safe apply/update behavior.
Update README, Docker install docs, CI, and focused regressions to cover the Windows BOM failure, wrapper parity, compose coverage, and Docker-native wrap behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 17:34:40 -05:00
Show-WrapHelp
2026-04-11 00:40:16 -05:00
exit 0
}
2026-04-10 23:27:24 -05:00
if ( $args . Count -lt 2 ) {
2026-04-11 00:04:15 -05:00
Fail 'Usage: headroom wrap <claude|codex|aider|cursor|openclaw> [...]'
2026-04-10 23:27:24 -05:00
}
$tool = $args [ 1 ]
$wrapArgs = if ( $args . Count -gt 2 ) { $args [ 2 . . ( $args . Count - 1 ) ] } else { @ ( ) }
2026-04-11 00:04:15 -05:00
fix: harden persistent install wrappers and review gaps
Align Docker-native wrapper help and runtime behavior with the Python install contract, including persistent deployment metadata, baked install-image defaults, and explicit unsupported wrap targets.
Harden the Python persistent-install path with profile validation, safer provider-scope handling, Windows environment restoration, runtime parity improvements, and rollback-safe apply/update behavior.
Update README, Docker install docs, CI, and focused regressions to cover the Windows BOM failure, wrapper parity, compose coverage, and Docker-native wrap behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 17:34:40 -05:00
switch ( $tool ) {
'claude' { }
'codex' { }
'aider' { }
'cursor' { }
'openclaw' { }
default {
Fail " Docker-native wrapper does not support 'wrap $tool '. Supported targets: claude, codex, aider, cursor, openclaw "
}
}
2026-04-11 00:04:15 -05:00
if ( $tool -eq 'openclaw' ) {
if ( Test-HelpFlag -Arguments $wrapArgs ) {
$helpArgs = @ ( 'wrap' , 'openclaw' ) + $wrapArgs
Invoke-HeadroomDocker -Arguments $helpArgs
exit 0
}
Invoke-OpenClawWrap -Arguments $wrapArgs
exit 0
}
2026-04-11 00:40:16 -05:00
if ( Test-HelpFlag -Arguments $wrapArgs ) {
$helpArgs = @ ( 'wrap' , $tool ) + $wrapArgs
Invoke-HeadroomDocker -Arguments $helpArgs
exit 0
}
2026-04-10 23:27:24 -05:00
$parsed = Parse-WrapArgs -Arguments $wrapArgs
2026-05-11 16:30:02 -04:00
$contextTool = Get-ContextTool
2026-04-10 23:27:24 -05:00
$proxyArgs = New-Object System . Collections . Generic . List [ string ]
if ( $parsed . Learn ) { $proxyArgs . Add ( '--learn' ) }
2026-04-11 00:40:16 -05:00
if ( $parsed . Backend ) { $proxyArgs . AddRange ( [ string[] ] @ ( '--backend' , $parsed . Backend ) ) }
if ( $parsed . Anyllm ) { $proxyArgs . AddRange ( [ string[] ] @ ( '--anyllm-provider' , $parsed . Anyllm ) ) }
if ( $parsed . Region ) { $proxyArgs . AddRange ( [ string[] ] @ ( '--region' , $parsed . Region ) ) }
2026-04-10 23:27:24 -05:00
$containerName = $null
try {
if ( -not $parsed . NoProxy ) {
$containerName = Start-ProxyContainer -Port $parsed . Port -ProxyArgs $proxyArgs . ToArray ( )
}
$prepareArgs = New-Object System . Collections . Generic . List [ string ]
foreach ( $arg in $parsed . KnownArgs ) {
$prepareArgs . Add ( $arg )
}
if ( -not $parsed . NoProxy ) {
$prepareArgs . Add ( '--no-proxy' )
}
2026-05-11 16:30:02 -04:00
if ( ( -not $parsed . NoRtk ) -and $contextTool -eq 'lean-ctx' ) {
$prepareArgs . Add ( '--no-rtk' )
}
2026-04-10 23:27:24 -05:00
Invoke-PrepareOnly -Tool $tool -KnownArgs $prepareArgs . ToArray ( )
2026-05-11 16:30:02 -04:00
if ( ( -not $parsed . NoRtk ) -and $contextTool -eq 'lean-ctx' ) {
Invoke-LeanCtxInit -Agent $tool
}
2026-04-10 23:27:24 -05:00
switch ( $tool ) {
'claude' {
2026-05-11 16:30:02 -04:00
if ( ( -not $parsed . NoRtk ) -and $contextTool -eq 'rtk' ) {
Invoke-ClaudeRtkInit
}
2026-04-10 23:27:24 -05:00
$exitCode = Invoke-WithTemporaryEnv -Environment @ { ANTHROPIC_BASE_URL = " http://127.0.0.1: $( $parsed . Port ) " } -Command 'claude' -Arguments $parsed . HostArgs
exit $exitCode
}
'codex' {
$exitCode = Invoke-WithTemporaryEnv -Environment @ { OPENAI_BASE_URL = " http://127.0.0.1: $( $parsed . Port ) /v1 " } -Command 'codex' -Arguments $parsed . HostArgs
exit $exitCode
}
'aider' {
$exitCode = Invoke-WithTemporaryEnv -Environment @ {
OPENAI_API_BASE = " http://127.0.0.1: $( $parsed . Port ) /v1 "
ANTHROPIC_BASE_URL = " http://127.0.0.1: $( $parsed . Port ) "
} -Command 'aider' -Arguments $parsed . HostArgs
exit $exitCode
}
'cursor' {
Write-Host " Headroom proxy is running for Cursor. "
Write-Host " "
Write-Host " OpenAI base URL: http://127.0.0.1: $( $parsed . Port ) /v1 "
Write-Host " Anthropic base URL: http://127.0.0.1: $( $parsed . Port ) "
Write-Host " "
Write-Host " Press Ctrl+C to stop the proxy. "
while ( $true ) { Start-Sleep -Seconds 1 }
}
}
} finally {
Stop-ProxyContainer -ContainerName $containerName
}
}
'unwrap' {
2026-04-11 00:40:16 -05:00
if ( $args . Count -eq 1 -or $args [ 1 ] -eq '--help' -or $args [ 1 ] -eq '-?' ) {
Invoke-HeadroomDocker -Arguments @ ( 'unwrap' , '--help' )
exit 0
}
2026-04-10 23:27:24 -05:00
if ( $args . Count -ge 2 -and $args [ 1 ] -eq 'openclaw' ) {
2026-04-11 00:04:15 -05:00
$unwrapArgs = if ( $args . Count -gt 2 ) { $args [ 2 . . ( $args . Count - 1 ) ] } else { @ ( ) }
if ( Test-HelpFlag -Arguments $unwrapArgs ) {
$helpArgs = @ ( 'unwrap' , 'openclaw' ) + $unwrapArgs
Invoke-HeadroomDocker -Arguments $helpArgs
exit 0
}
Invoke-OpenClawUnwrap -Arguments $unwrapArgs
exit 0
2026-04-10 23:27:24 -05:00
}
Invoke-HeadroomDocker -Arguments $args
}
'proxy' {
$port = 8787
$forwardArgs = New-Object System . Collections . Generic . List [ string ]
foreach ( $arg in $args ) { $forwardArgs . Add ( $arg ) }
for ( $i = 1 ; $i -lt $args . Count ; $i + + ) {
if ( $args [ $i ] -eq '--port' -or $args [ $i ] -eq '-p' ) {
2026-04-11 15:56:18 -05:00
Require-OptionValue -Arguments $args -Index $i -Option $args [ $i ]
$port = Parse-PortValue -Value $args [ $i + 1 ]
2026-04-10 23:27:24 -05:00
break
}
if ( $args [ $i ] -match '^--port=' ) {
2026-04-11 15:56:18 -05:00
$port = Parse-PortValue -Value ( $args [ $i ] -replace '^--port=' , '' )
2026-04-10 23:27:24 -05:00
break
}
}
$dockerArgs = New-Object System . Collections . Generic . List [ string ]
2026-04-11 15:56:18 -05:00
$dockerArgs . AddRange ( [ string[] ] @ ( 'run' , '--rm' ) )
Add-TtyArgs -ArgsList $dockerArgs
$dockerArgs . AddRange ( [ string[] ] @ ( '-p' , " $port ` : $port " ) )
2026-04-10 23:27:24 -05:00
$dockerArgs . AddRange ( ( Get-SharedDockerArgs ) )
$dockerArgs . Add ( '--entrypoint' )
$dockerArgs . Add ( 'headroom' )
$dockerArgs . Add ( $HeadroomImage )
foreach ( $arg in $forwardArgs ) {
$dockerArgs . Add ( $arg )
}
& docker @dockerArgs
exit $LASTEXITCODE
}
default {
Invoke-HeadroomDocker -Arguments $args
}
}
' @
fix: harden persistent install wrappers and review gaps
Align Docker-native wrapper help and runtime behavior with the Python install contract, including persistent deployment metadata, baked install-image defaults, and explicit unsupported wrap targets.
Harden the Python persistent-install path with profile validation, safer provider-scope handling, Windows environment restoration, runtime parity improvements, and rollback-safe apply/update behavior.
Update README, Docker install docs, CI, and focused regressions to cover the Windows BOM failure, wrapper parity, compose coverage, and Docker-native wrap behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 17:34:40 -05:00
$wrapper = $wrapper . Replace ( '__HEADROOM_INSTALL_IMAGE__' , $resolvedInstallImage )
2026-04-11 15:56:18 -05:00
$cmdWrapper = ( [ string][char ] 64 ) + " echo off `r `n powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -File "" %~dp0headroom.ps1 "" %* `r `n "
2026-04-10 23:27:24 -05:00
2026-04-11 15:56:18 -05:00
Set-Content -Path $wrapperPath -Value $wrapper -Encoding utf8
Set-Content -Path $cmdPath -Value $cmdWrapper -Encoding ascii
2026-04-10 23:27:24 -05:00
}
Require-Command docker
docker version | Out-Null
2026-04-11 15:56:18 -05:00
if ( $LASTEXITCODE -ne 0 ) {
throw 'Docker is installed but not available to the current user'
}
2026-04-10 23:27:24 -05:00
New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
Write-Wrapper -TargetDir $InstallDir
Ensure-PathEntry -PathEntry $InstallDir
Ensure-ProfileBlock -PathEntry $InstallDir
2026-04-11 15:56:18 -05:00
if ( $env:HEADROOM_DOCKER_IMAGE ) {
$null = docker image inspect $InstallImage 2 > $null
if ( $LASTEXITCODE -eq 0 ) {
Write-Info " Using existing HEADROOM_DOCKER_IMAGE= $InstallImage "
} else {
Write-Info " Pulling $InstallImage "
docker pull $InstallImage | Out-Null
}
} else {
Write-Info " Pulling $ImageDefault "
docker pull $ImageDefault | Out-Null
}
2026-04-10 23:27:24 -05:00
2026-04-11 15:56:18 -05:00
Write-Host " "
Write-Host " Headroom Docker-native install complete. "
Write-Host " "
2026-04-10 23:27:24 -05:00
Write-Host " Installed wrappers: "
Write-Host " $InstallDir \headroom.ps1 "
Write-Host " $InstallDir \headroom.cmd "
2026-04-11 15:56:18 -05:00
Write-Host " "
Write-Host " Next steps: "
2026-04-10 23:27:24 -05:00
Write-Host " 1. Restart PowerShell "
Write-Host " 2. Try: headroom proxy "
Write-Host " 3. Docs: https://github.com/chopratejas/headroom/blob/main/docs/docker-install.md "