# Sleevecast Dock installer for Windows. # irm https://get.sleevecast.com/dock.ps1 | iex # It checks this PC and installs or starts what the Dock needs (WSL 2 and Docker Desktop), then installs the Dock in # %USERPROFILE%\.sleevecast\dock and enrolls it (a code to approve in the browser). It asks once before installing # anything; installing needs one Windows administrator prompt. If Windows must restart, the installer carries on by # itself after you sign in again. Run it again any time: it only does what is missing, and an enrolled Dock is # updated in place with its enrollment, keys and sleeves kept. # Check only, change nothing: & ([scriptblock]::Create((irm https://get.sleevecast.com/dock.ps1))) -Check # Install without asking: & ([scriptblock]::Create((irm https://get.sleevecast.com/dock.ps1))) -Yes param([switch]$Check, [switch]$Yes) & { param([bool]$Check, [bool]$Yes) # Native tools (docker, wsl) write progress to stderr; under 'Stop' Windows PowerShell would treat that as a failure. $ErrorActionPreference = 'Continue' $ProgressPreference = 'SilentlyContinue' [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 $DockImage = 'ghcr.io/blackcrowsoftware/sleevecast-dock@sha256:b25e3eebc0dc739fce05375279ce92c31a392ebcc9aa1630e125a4ce3d13b70d' $ProxyImage = 'tecnativa/docker-socket-proxy:v0.5.0@sha256:1f5038b54f06c3e18422902cf00ba21803d1c97805aae032e5e6673d532d3459' $MinDocker = [version]'26.0' $MinWsl = [version]'2.1.5' $Api = 'https://api.sleevecast.com' $Console = 'https://app.sleevecast.com' $InstallCmd = 'irm https://get.sleevecast.com/dock.ps1 | iex' $DockerExe = Join-Path $env:ProgramFiles 'Docker\Docker\Docker Desktop.exe' $DockerBin = Join-Path $env:ProgramFiles 'Docker\Docker\resources\bin' $Dir = Join-Path $env:USERPROFILE '.sleevecast\dock' $script:actions = New-Object System.Collections.Generic.List[string] $script:plan = New-Object System.Collections.Generic.List[string] $script:failed = $false function Ok($t) { Write-Host ' [ ok ] ' -ForegroundColor Green -NoNewline; Write-Host $t } function Info($t) { Write-Host " $t" } function Warn($t) { Write-Host ' [warn] ' -ForegroundColor Yellow -NoNewline; Write-Host $t } function Need($t, $action, $what) { Write-Host ' [need] ' -ForegroundColor Yellow -NoNewline; Write-Host $t; $script:actions.Add($action); $script:plan.Add($what) } function Fail($t) { Write-Host ' [fail] ' -ForegroundColor Red -NoNewline; Write-Host $t; $script:failed = $true } function Step($t) { Write-Host ''; Write-Host $t -ForegroundColor Cyan } function Wants($a) { $script:actions.Contains($a) } function Stop-Install($t) { Write-Host ''; Write-Host $t -ForegroundColor Red; throw 'SLEEVECAST_STOP' } function Get-DockerServer { if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { if (Test-Path $DockerBin) { $env:Path = "$env:Path;$DockerBin" } else { return $null } } $v = & docker version --format '{{.Server.Version}}' 2>$null if ($LASTEXITCODE -eq 0 -and $v) { return "$v".Trim() } return $null } function To-Version($s) { try { [version](($s -replace '^[^0-9]*', '') -replace '[^0-9.].*$', '') } catch { [version]'0.0' } } function Get-WslVersion { if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) { return $null } $env:WSL_UTF8 = '1' $out = & wsl.exe --version 2>$null if ($LASTEXITCODE -ne 0 -or -not $out) { return $null } $m = [regex]::Match(($out -join "`n"), '(\d+\.\d+\.\d+)') if ($m.Success) { return [version]$m.Groups[1].Value } return $null } try { Write-Host 'Sleevecast Dock installer' -ForegroundColor Cyan Step 'Checking this PC' $os = Get-CimInstance Win32_OperatingSystem $build = [int]$os.BuildNumber $arch = $env:PROCESSOR_ARCHITECTURE if ($os.ProductType -ne 1) { Fail "$($os.Caption) is a server edition; Docker Desktop runs on Windows 10 and 11. Use a Linux machine or VM with: curl -fsSL https://get.sleevecast.com/dock.sh | sh" } elseif (-not [Environment]::Is64BitOperatingSystem) { Fail 'The Dock needs 64-bit Windows.' } elseif ($build -lt 19045) { Fail "$($os.Caption) (build $build) is too old; Docker Desktop needs Windows 10 22H2 or Windows 11 23H2 or later. Run Windows Update." } elseif ($build -ge 22000 -and $build -lt 22631) { Fail "$($os.Caption) (build $build) is too old; Docker Desktop needs Windows 11 23H2 or later. Run Windows Update." } else { Ok "$($os.Caption) (build $build, $arch)" } if ($arch -eq 'ARM64') { Warn 'Sleeve images are x86-64 for now and may not run on Windows on ARM.' } $memGb = [math]::Round($os.TotalVisibleMemorySize / 1MB) if ($memGb -ge 8) { Ok "$memGb GB memory" } else { Warn "$memGb GB memory; Docker Desktop plus sleeves want 8 GB or more" } $sysDrive = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='$($env:SystemDrive)'" $freeGb = [math]::Floor($sysDrive.FreeSpace / 1GB) if ($freeGb -ge 20) { Ok "$freeGb GB free on $($env:SystemDrive)" } else { Warn "$freeGb GB free on $($env:SystemDrive); Docker Desktop and sleeves want about 20 GB" } try { Invoke-WebRequest -UseBasicParsing -TimeoutSec 15 "$Api/healthz" -ErrorAction Stop | Out-Null; Ok 'Sleevecast is reachable (outbound HTTPS)' } catch { Fail "Cannot reach $Api over HTTPS. The Dock needs outbound port 443 to *.sleevecast.com (check a proxy or firewall)." } # Virtualization and WSL 2 (Docker Desktop's engine runs in WSL 2). $hyper = [bool](Get-CimInstance Win32_ComputerSystem).HypervisorPresent $firmware = [bool]((Get-CimInstance Win32_Processor | Select-Object -First 1).VirtualizationFirmwareEnabled) $wsl = Get-WslVersion $reboot = $false if (-not $hyper -and -not $firmware) { Fail 'Hardware virtualization is turned off. Turn on Intel VT-x / AMD-V (SVM) in the PC''s BIOS/UEFI settings, then run this again.' } elseif (-not $wsl) { Need 'WSL 2 is not installed' 'wsl' 'Install WSL 2 (Windows Subsystem for Linux); Windows then needs one restart' $reboot = $true } elseif ($wsl -lt $MinWsl) { Need "WSL $wsl is too old (Docker Desktop needs $MinWsl+)" 'wslupdate' 'Update WSL' } elseif (-not $hyper) { Need "WSL $wsl is installed, but Windows has not started its virtual machine platform" 'wsl' 'Turn on the virtual machine platform for WSL; Windows then needs one restart' $reboot = $true } else { Ok "WSL $wsl" } # Docker Desktop. $dd = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Docker Desktop' -ErrorAction SilentlyContinue $server = $null if (-not (Test-Path $DockerExe)) { Need 'Docker Desktop is not installed' 'docker' 'Install Docker Desktop (downloaded from docker.com; free for personal use, education and businesses under 250 people and $10M revenue, otherwise it needs a paid Docker subscription)' } else { $server = Get-DockerServer if ($server) { $osType = (& docker version --format '{{.Server.Os}}' 2>$null) if ((To-Version $server) -lt $MinDocker) { Need "Docker $server is too old (the Dock needs $MinDocker+)" 'docker' 'Update Docker Desktop' } else { Ok "Docker Desktop $($dd.DisplayVersion) is running (Docker $server)" } if ("$osType".Trim() -eq 'windows') { Need 'Docker Desktop is set to Windows containers' 'linuxengine' 'Switch Docker Desktop to Linux containers' } & docker compose version *> $null if ($LASTEXITCODE -ne 0) { Need 'Docker Compose v2 is missing' 'docker' 'Update Docker Desktop (it includes Compose)' } } elseif (-not $reboot) { Need "Docker Desktop $($dd.DisplayVersion) is installed but not running" 'start' 'Start Docker Desktop and wait for its engine' } else { Info "Docker Desktop $($dd.DisplayVersion) is installed; it starts after the restart." } } if (Test-Path (Join-Path $Dir 'compose.yaml')) { Ok "Dock folder $Dir exists (this run updates it)" } else { Info "The Dock will be installed in $Dir" } if ($script:failed) { Stop-Install 'Fix the [fail] items above, then run the installer again.' } $planText = ($script:plan | ForEach-Object { " - $_" }) -join "`n" if ($reboot) { $planText += "`n - Restart Windows (the installer carries on by itself after you sign in)" } $planText += "`n - Install and enroll the Dock" if ($Check) { Write-Host '' if ($script:actions.Count -eq 0) { Write-Host 'Everything the Dock needs is here. Run the installer without -Check to install it.' -ForegroundColor Green } else { Write-Host 'The installer would:'; Write-Host $planText } return } # ------------------------------------------------------------ install what is missing if ($script:actions.Count -gt 0) { Write-Host ''; Write-Host 'To finish, the installer will:'; Write-Host $planText if (-not $Yes) { $a = Read-Host "`nContinue? [Y/n]" if ($a -and $a -notmatch '^(y|yes)$') { Write-Host 'Nothing was changed.'; return } } } # Everything that needs administrator rights runs in one elevated step (one Windows prompt). $admin = @() if (Wants 'wsl') { $admin += '"wsl: " + ((Start-Process wsl.exe -ArgumentList ''--install'',''--no-distribution'' -Wait -PassThru -WindowStyle Hidden).ExitCode)' } if (Wants 'wslupdate') { $admin += '"wslupdate: " + ((Start-Process wsl.exe -ArgumentList ''--update'' -Wait -PassThru -WindowStyle Hidden).ExitCode)' } if (Wants 'docker') { Step 'Downloading Docker Desktop (about 600 MB)' $plat = 'amd64'; if ($arch -eq 'ARM64') { $plat = 'arm64' } $installer = Join-Path $env:TEMP 'DockerDesktopInstaller.exe' $wc = New-Object Net.WebClient $wc.DownloadFile("https://desktop.docker.com/win/main/$plat/Docker%20Desktop%20Installer.exe", $installer) # Load the module from this PowerShell's own folder (a PSModulePath inherited from PowerShell 7 breaks the default lookup). Import-Module (Join-Path $PSHOME 'Modules\Microsoft.PowerShell.Security\Microsoft.PowerShell.Security.psd1') -ErrorAction SilentlyContinue $sig = Get-AuthenticodeSignature $installer if ($sig.Status -ne 'Valid' -or $sig.SignerCertificate.Subject -notmatch 'Docker') { Remove-Item $installer -Force; Stop-Install 'The Docker Desktop download is not signed by Docker; nothing was installed.' } Ok 'Downloaded and verified (signed by Docker)' $admin += "`"docker: `" + ((Start-Process '$installer' -ArgumentList 'install','--quiet','--accept-license','--backend=wsl-2' -Wait -PassThru).ExitCode)" $admin += "net localgroup docker-users `"$env:USERDOMAIN\$env:USERNAME`" /add 2>&1 | Out-Null" } if ($admin.Count -gt 0) { Step 'Installing (approve the Windows administrator prompt)' $log = Join-Path $env:TEMP 'sleevecast-install.log' $elevated = Join-Path $env:TEMP 'sleevecast-install-admin.ps1' $body = "`$ErrorActionPreference = 'Continue'`n& {`n" + ($admin -join "`n") + "`n} *> '$log'`n" [IO.File]::WriteAllText($elevated, $body, (New-Object Text.UTF8Encoding $false)) Remove-Item $log -ErrorAction SilentlyContinue try { Start-Process powershell.exe -Verb RunAs -Wait -WindowStyle Hidden -ErrorAction Stop -ArgumentList '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$elevated`"" } catch { Stop-Install 'The administrator prompt was declined; nothing was installed. Run the installer again when ready.' } $result = if (Test-Path $log) { Get-Content $log -Raw } else { '' } Remove-Item $elevated -ErrorAction SilentlyContinue if (Wants 'docker') { $m = [regex]::Match($result, 'docker: (-?\d+)') if (-not $m.Success -or ($m.Groups[1].Value -ne '0' -and $m.Groups[1].Value -ne '3010')) { Stop-Install "Docker Desktop did not install (exit code $($m.Groups[1].Value)). See $log, or install it from https://www.docker.com/products/docker-desktop/ and run this again." } Ok 'Docker Desktop installed' Remove-Item $installer -ErrorAction SilentlyContinue } if (Wants 'wsl') { if ($result -notmatch 'wsl: 0') { Stop-Install "WSL did not install. See $log, or run 'wsl --install' in an administrator PowerShell, restart, and run this again." } Ok 'WSL 2 installed' } if (Wants 'wslupdate') { Ok 'WSL updated' } } if ($reboot) { $cmd = "powershell.exe -NoProfile -ExecutionPolicy Bypass -NoExit -Command `"$InstallCmd`"" New-Item -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce' -Force | Out-Null Set-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce' -Name 'SleevecastDockInstaller' -Value $cmd Write-Host '' Write-Host 'Windows needs a restart to finish WSL. After you sign in again, the installer opens and carries on by itself.' -ForegroundColor Yellow if (-not $Yes) { $a = Read-Host 'Restart now? [Y/n]' if (-not $a -or $a -match '^(y|yes)$') { Restart-Computer -Force } } Write-Host 'Restart Windows when you are ready.' return } if ((Wants 'start') -or (Wants 'docker')) { Step 'Starting Docker Desktop' Start-Process $DockerExe Write-Host ' waiting for the Docker engine (the first start takes a minute or two)...' } if (Wants 'linuxengine') { & (Join-Path $env:ProgramFiles 'Docker\Docker\DockerCli.exe') -SwitchLinuxEngine; Start-Sleep 5 } $deadline = (Get-Date).AddMinutes(5) do { $server = Get-DockerServer; if ($server) { break }; Start-Sleep 3 } while ((Get-Date) -lt $deadline) if (-not $server) { Stop-Install 'Docker Desktop did not start within 5 minutes. Open Docker Desktop, finish any first-run prompts, wait until it says "Engine running", then run this again. If Windows asked you to sign out, do that first.' } if ((To-Version $server) -lt $MinDocker) { Stop-Install "Docker $server is still older than $MinDocker; update Docker Desktop and run this again." } & docker compose version *> $null if ($LASTEXITCODE -ne 0) { Stop-Install 'Docker Compose v2 is missing; update Docker Desktop and run this again.' } if ($script:actions.Count -gt 0) { Ok "Docker $server is ready" } # ------------------------------------------------------------ the Dock Step 'Installing the Dock' New-Item -ItemType Directory -Force -Path $Dir -ErrorAction Stop | Out-Null $name = ($env:COMPUTERNAME).ToLowerInvariant() $compose = @' # Sleevecast Dock, written by the installer. The Dock creates its leaf node and sleeves itself (labelled # sleevecast.dock). Docker access goes only through the socket proxy, on a network nothing else joins. name: sleevecast-dock services: socket-proxy: image: tecnativa/docker-socket-proxy:v0.5.0@sha256:1f5038b54f06c3e18422902cf00ba21803d1c97805aae032e5e6673d532d3459 restart: unless-stopped environment: CONTAINERS: "1" IMAGES: "1" NETWORKS: "1" VOLUMES: "1" INFO: "1" VERSION: "1" POST: "1" ALLOW_START: "1" ALLOW_STOP: "1" ALLOW_RESTARTS: "1" EXEC: "0" BUILD: "0" SWARM: "0" SECRETS: "0" PLUGINS: "0" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro networks: [ctl] read_only: true tmpfs: [/run, /tmp] # the proxy renders its haproxy config into /tmp at start dock: image: ghcr.io/blackcrowsoftware/sleevecast-dock@sha256:b25e3eebc0dc739fce05375279ce92c31a392ebcc9aa1630e125a4ce3d13b70d restart: unless-stopped depends_on: [socket-proxy] environment: DOCKER_HOST: tcp://socket-proxy:2375 DOCK_NAME: "__DOCK_NAME__" DOCK_HOST_OS: "__HOST_OS__" volumes: - data:/data networks: [ctl, bus] read_only: true tmpfs: [/tmp] cap_drop: [ALL] security_opt: ["no-new-privileges:true"] networks: ctl: name: sleevecast-ctl internal: true bus: name: sleevecast-bus volumes: data: name: sleevecast-dock-data '@ $compose = $compose.Replace('ghcr.io/blackcrowsoftware/sleevecast-dock@sha256:b25e3eebc0dc739fce05375279ce92c31a392ebcc9aa1630e125a4ce3d13b70d', $DockImage).Replace('tecnativa/docker-socket-proxy:v0.5.0@sha256:1f5038b54f06c3e18422902cf00ba21803d1c97805aae032e5e6673d532d3459', $ProxyImage).Replace('__DOCK_NAME__', $name).Replace('__HOST_OS__', 'windows') [IO.File]::WriteAllText((Join-Path $Dir 'compose.yaml'), $compose, (New-Object Text.UTF8Encoding $false)) Push-Location $Dir try { docker compose pull --quiet docker compose up -d --remove-orphans if ($LASTEXITCODE -ne 0) { Stop-Install 'docker compose up failed; see the messages above.' } docker compose exec -T dock /app/dock status *> $null if ($LASTEXITCODE -ne 0) { Step 'Enrolling the Dock' docker compose exec dock /app/dock enroll --name $name } else { Ok 'This Dock is already enrolled; it was updated in place.' } } finally { Pop-Location } Write-Host '' Write-Host "Done. Next: open $Console/welcome to add your model key and start your first sleeve." -ForegroundColor Green Write-Host '' Write-Host "Dock folder: $Dir" Write-Host 'Status: docker compose exec dock /app/dock status (run in that folder)' Write-Host 'Model key: docker compose exec dock /app/dock keys set anthropic-key (or add it on the Keys page)' Write-Host 'Uninstall: docker compose exec dock /app/dock uninstall; docker compose down' } catch { if ("$_" -ne 'SLEEVECAST_STOP') { Write-Host ''; Write-Host "The installer stopped: $_" -ForegroundColor Red } } } $Check.IsPresent $Yes.IsPresent