package bootstrap import ( "context" "fmt" "strings" ) type CommandSpec struct { Name string Args []string } type Action struct { Name string Phase Phase Probe CommandSpec Commands []CommandSpec AI bool } const refreshPath = `$env:Path=[Environment]::GetEnvironmentVariable('Path','Machine')+';'+[Environment]::GetEnvironmentVariable('Path','User')+';C:\ProgramData\chocolatey\bin'; if (Get-Command fnm -ErrorAction SilentlyContinue) { fnm env --shell powershell | Invoke-Expression }; ` func powerShell(script string) CommandSpec { return CommandSpec{ Name: "powershell.exe", Args: []string{"-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", refreshPath + script}, } } func checkedNativePowerShell(command string) CommandSpec { return powerShell(`$ErrorActionPreference='Stop'; ` + command + `; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }`) } func HostActions(opts Options) []Action { actions := []Action{ { Name: "Visual Studio Community 2026", Phase: PhaseHost, Probe: powerShell(`if (Test-Path "${env:ProgramFiles}\Microsoft Visual Studio\2026\Community\Common7\IDE\devenv.exe") { exit 0 } else { exit 1 }`), Commands: []CommandSpec{ powerShell(fmt.Sprintf( `$tempExe = Join-Path $env:TEMP 'vs_community.exe'; `+ `if (Test-Path $tempExe) { Remove-Item -Force $tempExe }; `+ `[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; `+ `Write-Host 'Downloading Visual Studio 2026 bootstrapper...'; `+ `Invoke-WebRequest -Uri 'https://aka.ms/vs/17/release/vs_community.exe' -OutFile $tempExe -UseBasicParsing -ErrorAction Stop; `+ `Unblock-File -Path $tempExe; `+ `Write-Host 'Installing Visual Studio 2026 with workloads...'; `+ `$p = Start-Process -FilePath $tempExe -ArgumentList '--passive --wait --config "%s"' -Wait -PassThru; `+ `if ($p.ExitCode -ne 0 -and $p.ExitCode -ne 3010) { throw 'Visual Studio installation failed with exit code ' + $p.ExitCode }`, mustInstallAssetPath("assets/visual-studio-community.vsconfig"), )), }, }, { Name: "fnm and Node.js LTS", Phase: PhaseHost, Probe: powerShell(`fnm exec --using=lts-latest node --version | Out-Null; if ($LASTEXITCODE -ne 0) { exit 1 }`), Commands: []CommandSpec{ powerShell(`$ErrorActionPreference='Stop' function Invoke-Native { param([Parameter(Mandatory=$true)][string]$File, [Parameter(Mandatory=$true)][string[]]$Arguments) & $File @Arguments if ($LASTEXITCODE -ne 0) { throw "$File $($Arguments -join ' ') failed with exit code $LASTEXITCODE" } } $scoop = Get-Command scoop -ErrorAction SilentlyContinue $scoopShim = Join-Path $HOME 'scoop\shims\scoop.ps1' if ($scoop) { Invoke-Native $scoop.Source @('install', 'fnm') } elseif (Test-Path $scoopShim) { Invoke-Native $scoopShim @('install', 'fnm') } elseif (Get-Command choco -ErrorAction SilentlyContinue) { Invoke-Native 'choco' @('install', 'fnm', '-y', '--no-progress') } else { [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 Invoke-RestMethod -Uri 'https://community.chocolatey.org/install.ps1' | Invoke-Expression $env:Path=[Environment]::GetEnvironmentVariable('Path','Machine')+';'+[Environment]::GetEnvironmentVariable('Path','User') Invoke-Native 'choco' @('install', 'fnm', '-y', '--no-progress') } $env:Path=[Environment]::GetEnvironmentVariable('Path','Machine')+';'+[Environment]::GetEnvironmentVariable('Path','User') Invoke-Native 'fnm' @('install', '--lts') Invoke-Native 'fnm' @('default', 'lts-latest')`), }, }, { Name: "pyenv-win through Scoop or Chocolatey", Phase: PhaseHost, Probe: powerShell(`pyenv --version | Out-Null; if ($LASTEXITCODE -ne 0) { exit 1 }`), Commands: []CommandSpec{ powerShell(`$ErrorActionPreference='Stop' function Invoke-Native { param([Parameter(Mandatory=$true)][string]$File, [Parameter(Mandatory=$true)][string[]]$Arguments) & $File @Arguments if ($LASTEXITCODE -ne 0) { throw "$File $($Arguments -join ' ') failed with exit code $LASTEXITCODE" } } $scoop = Get-Command scoop -ErrorAction SilentlyContinue $scoopShim = Join-Path $HOME 'scoop\shims\scoop.ps1' if ($scoop) { Invoke-Native $scoop.Source @('install', 'pyenv') } elseif (Test-Path $scoopShim) { Invoke-Native $scoopShim @('install', 'pyenv') } elseif (Get-Command choco -ErrorAction SilentlyContinue) { Invoke-Native 'choco' @('install', 'pyenv-win', '-y', '--no-progress') } else { [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 Invoke-RestMethod -Uri 'https://community.chocolatey.org/install.ps1' | Invoke-Expression $env:Path=[Environment]::GetEnvironmentVariable('Path','Machine')+';'+[Environment]::GetEnvironmentVariable('Path','User') Invoke-Native 'choco' @('install', 'pyenv-win', '-y', '--no-progress') }`), }, }, { Name: "latest stable Python through pyenv-win", Phase: PhaseHost, Probe: powerShell(`$v = pyenv version-name; if ([string]::IsNullOrWhiteSpace($v) -or $v -eq 'system') { exit 1 }`), Commands: []CommandSpec{ powerShell(`$ErrorActionPreference='Stop' function Invoke-Native { param([Parameter(Mandatory=$true)][string]$File, [Parameter(Mandatory=$true)][string[]]$Arguments) & $File @Arguments if ($LASTEXITCODE -ne 0) { throw "$File $($Arguments -join ' ') failed with exit code $LASTEXITCODE" } } $env:Path=[Environment]::GetEnvironmentVariable('Path','Machine')+';'+[Environment]::GetEnvironmentVariable('Path','User') $out = Invoke-Native 'pyenv' @('install', '-l') $v = $out -split '\r?\n' | Where-Object { $_ -match '^\s*3\.\d+\.\d+\s*$' } | Sort-Object -Descending | Select-Object -First 1 if (-not $v) { throw 'No stable Python 3.x version found' } $v = $v.Trim() Invoke-Native 'pyenv' @('install', '-s', $v) Invoke-Native 'pyenv' @('global', $v) Invoke-Native 'pyenv' @('exec', 'python', '-m', 'pip', 'install', '--upgrade', 'pip') `), }, }, { Name: "semgrep", Phase: PhaseHost, Probe: powerShell(`$env:Path=[Environment]::GetEnvironmentVariable('Path','Machine')+';'+[Environment]::GetEnvironmentVariable('Path','User'); pyenv exec semgrep --version | Out-Null; if ($LASTEXITCODE -ne 0) { exit 1 }`), Commands: []CommandSpec{ checkedNativePowerShell(`$env:Path=[Environment]::GetEnvironmentVariable('Path','Machine')+';'+[Environment]::GetEnvironmentVariable('Path','User'); pyenv exec pip install semgrep`), }, }, } actions = append(actions, Action{ Name: "VLC default media player associations", Phase: PhaseHost, Probe: powerShell(`$stateDir = Join-Path $env:ProgramData 'bootstrap_windows_env'; if (-not (Test-Path (Join-Path $stateDir 'vlc-default-media.done'))) { exit 1 }`), Commands: []CommandSpec{ powerShell(`$stateDir = Join-Path $env:ProgramData 'bootstrap_windows_env' if (-not (Test-Path $stateDir)) { New-Item -ItemType Directory -Force -Path $stateDir | Out-Null } $vlcPaths = @( (Join-Path $env:ProgramFiles 'VideoLAN\VLC\vlc.exe'), (Join-Path ${env:ProgramFiles(x86)} 'VideoLAN\VLC\vlc.exe') ) | Where-Object { $_ -and (Test-Path $_) } if ($vlcPaths.Count -eq 0 -and -not (Get-Command vlc.exe -ErrorAction SilentlyContinue)) { throw 'VLC is not installed or is not discoverable.' } $extensions = @( '.3g2', '.3gp', '.3gp2', '.3gpp', '.amv', '.asf', '.avi', '.divx', '.flac', '.flv', '.m2ts', '.m4a', '.m4v', '.mka', '.mkv', '.mov', '.mp2', '.mp3', '.mp4', '.mp4v', '.mpeg', '.mpg', '.mts', '.oga', '.ogg', '.ogm', '.ogv', '.opus', '.ts', '.vob', '.wav', '.webm', '.wma', '.wmv' ) $registered = @{} $capabilityPaths = @( 'HKLM:\SOFTWARE\Clients\Media\VLC\Capabilities\FileAssociations', 'HKLM:\SOFTWARE\VideoLAN\VLC\Capabilities\FileAssociations', 'HKLM:\SOFTWARE\Applications\vlc.exe\Capabilities\FileAssociations', 'HKCU:\SOFTWARE\Clients\Media\VLC\Capabilities\FileAssociations', 'HKCU:\SOFTWARE\Applications\vlc.exe\Capabilities\FileAssociations' ) foreach ($path in $capabilityPaths) { if (-not (Test-Path $path)) { continue } $props = Get-ItemProperty $path foreach ($ext in $extensions) { if ($props.PSObject.Properties.Name -contains $ext) { $registered[$ext] = [string]$props.$ext } } } $xmlPath = Join-Path $stateDir 'VlcDefaultAssociations.xml' $lines = New-Object System.Collections.Generic.List[string] $lines.Add('') $lines.Add('') foreach ($ext in $extensions) { $progId = $registered[$ext] if ([string]::IsNullOrWhiteSpace($progId)) { $progId = 'VLC' + $ext } $safeExt = [System.Security.SecurityElement]::Escape($ext) $safeProgId = [System.Security.SecurityElement]::Escape($progId) $lines.Add(" ") } $lines.Add('') $lines | Set-Content -Path $xmlPath -Encoding UTF8 dism.exe /Online /Import-DefaultAppAssociations:$xmlPath if ($LASTEXITCODE -ne 0) { throw "dism.exe default association import failed with exit code $LASTEXITCODE" } Set-Content -Path (Join-Path $stateDir 'vlc-default-media.done') -Value (Get-Date -Format o) -Encoding UTF8 Write-Output "Imported VLC default media associations from $xmlPath. Windows may still require user confirmation for an existing signed-in profile."`), }, }) return actions } func CustomActions(opts Options) []Action { actions := []Action{ { Name: "agy", Phase: PhaseCustom, AI: true, Probe: powerShell(`agy --version`), Commands: []CommandSpec{ powerShell(`Invoke-RestMethod https://antigravity.google/cli/install.ps1 | Invoke-Expression`), }, }, { Name: "Playwright CLI", Phase: PhaseCustom, Probe: powerShell(`npm list --global playwright --depth=0 | Out-Null; if ($LASTEXITCODE -ne 0) { exit 1 }`), Commands: []CommandSpec{ checkedNativePowerShell(`npm install --global playwright`), checkedNativePowerShell(`playwright install`), }, }, { Name: "GitHub CLI gh-repo-bootstrap extension", Phase: PhaseCustom, Probe: powerShell(`if (-not (gh extension list | Select-String 'JMR-dev/gh-repo-bootstrap')) { exit 1 }`), Commands: []CommandSpec{ checkedNativePowerShell(`gh extension install JMR-dev/gh-repo-bootstrap`), }, }, { Name: "Claude Code", Phase: PhaseCustom, AI: true, Probe: powerShell(`npm list --global '@anthropic-ai/claude-code' --depth=0 | Out-Null; if ($LASTEXITCODE -ne 0) { exit 1 }`), Commands: []CommandSpec{ checkedNativePowerShell(`npm install --global '@anthropic-ai/claude-code'`), }, }, { Name: "OpenAI Codex CLI", Phase: PhaseCustom, AI: true, Probe: powerShell(`npm list --global '@openai/codex' --depth=0 | Out-Null; if ($LASTEXITCODE -ne 0) { exit 1 }`), Commands: []CommandSpec{ checkedNativePowerShell(`npm install --global '@openai/codex'`), }, }, { Name: "GitHub Copilot CLI", Phase: PhaseCustom, AI: true, Probe: powerShell(`npm list --global '@github/copilot' --depth=0 | Out-Null; if ($LASTEXITCODE -ne 0) { exit 1 }`), Commands: []CommandSpec{ checkedNativePowerShell(`npm install --global '@github/copilot'`), }, }, } if !opts.NoAI { return actions } filtered := make([]Action, 0, len(actions)) for _, action := range actions { if !action.AI { filtered = append(filtered, action) } } return filtered } type ActionState struct { Action Action Installed bool CheckErr error } func CheckActions(ctx context.Context, runner Runner, actions []Action) []ActionState { states := make([]ActionState, 0, len(actions)) for _, action := range actions { result := runner.Run(ctx, action.Probe.Name, action.Probe.Args...) states = append(states, ActionState{Action: action, Installed: result.Err == nil}) } return states } func PendingActions(states []ActionState) []Action { var pending []Action for _, state := range states { if !state.Installed { pending = append(pending, state.Action) } } return pending } func UnknownActionStates(actions []Action) []ActionState { states := make([]ActionState, 0, len(actions)) for _, action := range actions { states = append(states, ActionState{Action: action}) } return states } func ExecuteActions(ctx context.Context, runner Runner, actions []Action) Issues { var issues Issues for _, action := range actions { for _, command := range action.Commands { result := runner.Run(ctx, command.Name, command.Args...) if result.Err != nil { output := strings.TrimSpace(result.CombinedOutput()) issues = append(issues, Issue{Step: action.Name, Err: fmt.Errorf("%w: %s", result.Err, output)}) break } } } return issues }