Last active
January 13, 2026 13:19
-
-
Save bogorad/66604d2c521126e337e8433ba374d528 to your computer and use it in GitHub Desktop.
Run opencode on windows using git-bash via server/client
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # opencode.ps1 - Self-updating SST OpenCode wrapper (Windows) | |
| <# | |
| .SYNOPSIS | |
| Self-updating wrapper for the SST OpenCode CLI on Windows. | |
| .DESCRIPTION | |
| This script manages the installation, update, and execution lifecycle of the opencode binary. | |
| Key behaviors: | |
| 1. Update Management: Checks GitHub releases for updates and caches the binary in %TEMP%. | |
| 2. Server Lifecycle: | |
| - Ensures port 8192 is free (killing old instances if necessary). | |
| - Spawns the server as a background job. | |
| - Waits for the server to listen before connecting. | |
| 3. Session Handling: Attaches the interactive client or passes arguments to the running server. | |
| 4. Cleanup: Ensures server processes are terminated upon script exit. | |
| Requirements: | |
| - Git Bash installed at standard location (C:/Program Files/Git/git-bash.exe) | |
| #> | |
| $ErrorActionPreference = "Stop" | |
| # --- CONFIGURATION --- | |
| # 1. Target & Repo | |
| $Repo = "anomalyco/opencode" | |
| $Temp = ([System.IO.Path]::GetTempPath()) -replace '\\', '/' | |
| $Target = (Join-Path $Temp "opencode.exe") -replace '\\', '/' | |
| # 2. Architecture & Asset Mapping | |
| # Logic: Map OS Architecture -> SST Release Asset Name | |
| # Note: Windows Arm64 must use x64 via emulation as no native asset exists. | |
| $osArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture | |
| $Config = switch ($osArch) | |
| { | |
| "X64" | |
| { @{ Arch = "windows-x64"; Ext = "zip" } | |
| } | |
| "Arm64" | |
| { @{ Arch = "windows-x64"; Ext = "zip" } | |
| } # Fallback to x64 emulation | |
| default | |
| { throw "Unsupported architecture: $osArch" | |
| } | |
| } | |
| # --- 1. VERSION CHECK --- | |
| $currentVer = "none" | |
| if (Test-Path -LiteralPath $Target) | |
| { | |
| try | |
| { | |
| # Redirect stderr to merge with stdout for grep/match | |
| $verOut = & $Target --version 2>&1 | |
| if ($verOut -match 'v?([0-9]+\.[0-9]+\.[0-9]+)') | |
| { | |
| $currentVer = $matches[0] | |
| } | |
| } catch | |
| { | |
| $currentVer = "none" | |
| } | |
| } | |
| # Get Latest Release from GitHub API | |
| try | |
| { | |
| $latestResponse = Invoke-WebRequest ` | |
| -Uri "https://api.github.com/repos/$Repo/releases/latest" ` | |
| -Headers @{ "User-Agent" = "opencode-win-wrapper" } ` | |
| -UseBasicParsing | |
| $latestJson = $latestResponse.Content | ConvertFrom-Json | |
| $latestVer = $latestJson.tag_name | |
| } catch | |
| { | |
| Write-Error "Error: Failed to check for updates." | |
| exit 1 | |
| } | |
| # Normalize versions (remove 'v' prefix) | |
| $normCurrent = $currentVer.TrimStart('v') | |
| $normLatest = $latestVer.TrimStart('v') | |
| # --- 2. UPDATE LOGIC --- | |
| if ($latestVer -and $normCurrent -ne $normLatest) | |
| { | |
| Write-Host "[opencode] Update needed: $currentVer -> $latestVer ($($Config.Arch))" | |
| # Construct URL based on strict asset mapping | |
| $downloadUrl = "https://github.com/$Repo/releases/download/$latestVer/opencode-$($Config.Arch).$($Config.Ext)" | |
| # Create unique temp directory for extraction | |
| $tmpDirPath = (Join-Path $Temp ([Guid]::NewGuid().ToString())) -replace '\\', '/' | |
| New-Item -ItemType Directory -Path $tmpDirPath -Force | Out-Null | |
| $archivePath = (Join-Path $tmpDirPath "file.$($Config.Ext)") -replace '\\', '/' | |
| Write-Host "Downloading $downloadUrl..." | |
| try | |
| { | |
| Invoke-WebRequest -Uri $downloadUrl -OutFile $archivePath -UseBasicParsing | |
| } catch | |
| { | |
| Write-Error "[opencode] Download failed." | |
| Remove-Item -Recurse -Force $tmpDirPath -ErrorAction SilentlyContinue | |
| exit 1 | |
| } | |
| # Extraction Logic | |
| try | |
| { | |
| Expand-Archive -Path $archivePath -DestinationPath $tmpDirPath -Force | |
| } catch | |
| { | |
| Write-Error "Error: Failed to extract archive." | |
| Remove-Item -Recurse -Force $tmpDirPath -ErrorAction SilentlyContinue | |
| exit 1 | |
| } | |
| # Locate Binary (Standard Windows release zip contains 'opencode.exe' at root) | |
| $binaryPath = (Join-Path $tmpDirPath "opencode.exe") -replace '\\', '/' | |
| if (Test-Path -LiteralPath $binaryPath) | |
| { | |
| try | |
| { | |
| # Move to target location (overwrite) | |
| Move-Item -LiteralPath $binaryPath -Destination $Target -Force | |
| Write-Host "[opencode] Updated successfully." | |
| } catch | |
| { | |
| Write-Error "Error: Failed to install binary to $Target. Ensure it is not currently running." | |
| Remove-Item -Recurse -Force $tmpDirPath -ErrorAction SilentlyContinue | |
| exit 1 | |
| } | |
| } else | |
| { | |
| Write-Error "Error: 'opencode.exe' not found in downloaded archive." | |
| Get-ChildItem -Recurse $tmpDirPath # Debug listing | |
| Remove-Item -Recurse -Force $tmpDirPath -ErrorAction SilentlyContinue | |
| exit 1 | |
| } | |
| # Cleanup | |
| Remove-Item -Recurse -Force $tmpDirPath -ErrorAction SilentlyContinue | |
| } | |
| # --- 3. EXECUTION --- | |
| if (-not (Test-Path -LiteralPath $Target)) | |
| { | |
| Write-Error "Error: Binary missing at $Target" | |
| exit 1 | |
| } | |
| # 1. Set the specific environment variable | |
| $ENV:OPENCODE_GIT_BASH_PATH = "C:/Program Files/Git/git-bash.exe" | |
| # 2. Verify Git Bash exists | |
| if (-not (Test-Path -LiteralPath $ENV:OPENCODE_GIT_BASH_PATH)) | |
| { | |
| throw "Git Bash executable not found at: $ENV:OPENCODE_GIT_BASH_PATH" | |
| } | |
| # 3. Check and kill process on port 8192 | |
| $Port = 8192 | |
| Write-Host "[opencode] Checking port $Port..." | |
| $Connection = Get-NetTCPConnection -LocalPort $Port -ErrorAction SilentlyContinue | |
| if ($Connection) { | |
| $TargetPid = $Connection.OwningProcess | |
| Write-Host "[opencode] Port $Port in use by PID $TargetPid. Killing..." | |
| Stop-Process -Id $TargetPid -Force -ErrorAction Stop | |
| } | |
| # 4. Spawn new server session | |
| Write-Host "[opencode] Starting server on port $Port..." | |
| # Use Start-Job to run the server in the background, hidden from the UI. | |
| # We rely on port 8192 check for cleanup on next run. | |
| $ServerJob = Start-Job -ScriptBlock { | |
| param($Target, $Port, $EnvPath) | |
| $ENV:OPENCODE_GIT_BASH_PATH = $EnvPath | |
| & $Target serve --port $Port | |
| } -ArgumentList $Target, $Port, $ENV:OPENCODE_GIT_BASH_PATH | |
| try { | |
| # 5. Robust Wait for Server | |
| # Wait up to 10 seconds for the port to start listening | |
| $ServerReady = $false | |
| for ($i = 0; $i -lt 20; $i++) { | |
| $Check = Get-NetTCPConnection -LocalPort $Port -ErrorAction SilentlyContinue | |
| if ($Check -and $Check.State -eq 'Listen') { | |
| $ServerReady = $true | |
| break | |
| } | |
| Start-Sleep -Milliseconds 500 | |
| } | |
| if (-not $ServerReady) { | |
| throw "Server failed to start listening on port $Port within 10 seconds." | |
| } | |
| # 6. Attach | |
| if ($args.Count -eq 0) { | |
| # Interactive attach using the correct subcommand | |
| Write-Host "[opencode] Attaching to server..." | |
| & $Target attach http://localhost:$Port | |
| } else { | |
| # Args provided -> run command attached to server | |
| $argStr = $args -join " " | |
| Write-Host "[opencode] Attaching to server and running command..." | |
| & $Target run --attach http://localhost:$Port $argStr | |
| } | |
| } finally { | |
| # 7. Cleanup: Find process on port 8192 and kill it | |
| # We rely on port check for cleanup; orphaned jobs are killed on next run. | |
| Write-Host "[opencode] Cleaning up server on port $Port..." | |
| $CleanupConnection = Get-NetTCPConnection -LocalPort $Port -ErrorAction SilentlyContinue | |
| if ($CleanupConnection) { | |
| $CleanupPid = $CleanupConnection.OwningProcess | |
| Write-Host "[opencode] Killing server process (PID: $CleanupPid)..." | |
| Stop-Process -Id $CleanupPid -Force -ErrorAction SilentlyContinue | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment