Skip to content

Instantly share code, notes, and snippets.

@bogorad
Last active January 13, 2026 13:19
Show Gist options
  • Select an option

  • Save bogorad/66604d2c521126e337e8433ba374d528 to your computer and use it in GitHub Desktop.

Select an option

Save bogorad/66604d2c521126e337e8433ba374d528 to your computer and use it in GitHub Desktop.
Run opencode on windows using git-bash via server/client
# 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