Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions PORTABLE-GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# OpenCode Portable Guide

Run OpenCode from a self-contained directory — no global install, no PATH changes, no files written to your home folder. Everything lives inside `.opencode_portable/` next to the wrapper script.

## Directory Layout

```
.opencode_portable/
├── bin/ # OpenCode binary
├── config/ # XDG_CONFIG_HOME (settings, skills)
├── data/ # XDG_DATA_HOME
├── cache/ # XDG_CACHE_HOME
├── state/ # XDG_STATE_HOME
└── .version # Currently cached version
```

## Prerequisites

| Platform | Required |
|----------|----------|
| macOS | `curl`, `unzip` (both ship with macOS) |
| Linux (glibc) | `curl`, `tar` |
| Linux (Alpine/musl) | `curl`, `tar` — install via `apk add curl tar` |
| Windows | PowerShell 5.1+ (built into Windows 10/11) |

All platforms need **`git`** if you plan to install external skill plugins.

## Download the Wrapper Script

**macOS / Linux** — requires `curl` (listed in Prerequisites above):

```bash
curl -fLO https://raw.githubusercontent.com/anomalyco/opencode/dev/opencode-portable.sh
chmod +x opencode-portable.sh
```

**Windows (PowerShell)** — requires PowerShell 5.1+ (built into Windows 10/11):

```powershell
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/anomalyco/opencode/dev/opencode-portable.ps1" -OutFile "opencode-portable.ps1"
```

## Quick Start

### macOS (Apple Silicon & Intel)

The script auto-detects your architecture, including Rosetta translation.

```bash
chmod +x opencode-portable.sh
./opencode-portable.sh
```

### Linux (Ubuntu/Debian, Fedora/RHEL, Arch)

```bash
chmod +x opencode-portable.sh
./opencode-portable.sh
```

The script automatically detects musl-based distros (Alpine) and selects the correct binary. On x64, it also detects AVX2 support and falls back to a baseline build if needed.

**Alpine/musl note** — ensure `curl` and `tar` are installed:

```bash
apk add curl tar
```

### Windows (PowerShell)

```powershell
Set-ExecutionPolicy -Scope Process Bypass
.\opencode-portable.ps1
```

The `Scope Process` setting only applies to the current terminal session.

## Version Pinning & Updating

### Pin a specific version

**Option A** — environment variable (highest priority):

```bash
# macOS/Linux
OPENCODE_PORTABLE_VERSION=1.0.180 ./opencode-portable.sh

# Windows
$env:OPENCODE_PORTABLE_VERSION = "1.0.180"
.\opencode-portable.ps1
```

**Option B** — create a `.opencode-version` file next to the script:

```
1.0.180
```

The leading `v` is stripped automatically, so both `1.0.180` and `v1.0.180` work.

### Force update to the latest (or pinned) version

```bash
# macOS/Linux
./opencode-portable.sh --portable-update

# Windows
.\opencode-portable.ps1 -PortableUpdate
```

### Check the cached version

```bash
# macOS/Linux
./opencode-portable.sh --portable-version

# Windows
.\opencode-portable.ps1 -PortableVersion
```

## Troubleshooting

**"Permission denied" when running the shell script**
```bash
chmod +x opencode-portable.sh
```

**PowerShell blocks script execution**
```powershell
Set-ExecutionPolicy -Scope Process Bypass
```
This only affects the current session. For a persistent change, use `-Scope CurrentUser`.

**"curl: command not found" (Linux)**
```bash
# Debian/Ubuntu
sudo apt install curl

# Fedora/RHEL
sudo dnf install curl

# Alpine
apk add curl
```

**"tar: command not found" (Alpine)**
```bash
apk add tar
```

**Binary downloads but OpenCode won't start**
Check that the download completed successfully. A partial or corrupted download can be fixed with:
```bash
./opencode-portable.sh --portable-update # macOS/Linux
.\opencode-portable.ps1 -PortableUpdate # Windows
```
213 changes: 213 additions & 0 deletions opencode-portable.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Portable wrapper for OpenCode on Windows.
.DESCRIPTION
Downloads and runs OpenCode from a self-contained directory next to this
script. All config, data, cache and state stay within .opencode_portable/
so nothing is written to your user profile.
.PARAMETER PortableHelp
Show wrapper usage and exit.
.PARAMETER PortableUpdate
Force re-download of the OpenCode binary.
.PARAMETER PortableVersion
Show the currently cached version and exit.
#>
param(
[switch]$PortableHelp,
[switch]$PortableUpdate,
[switch]$PortableVersion,
[Parameter(ValueFromRemainingArguments)]
[string[]]$PassthroughArgs
)

$ErrorActionPreference = "Stop"

# ---------------------------------------------------------------------------
# Self-location & portable directory
# ---------------------------------------------------------------------------
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$PortableDir = Join-Path $ScriptDir ".opencode_portable"
$BinPath = Join-Path $PortableDir "bin\opencode.exe"
$VersionFile = Join-Path $PortableDir ".version"

# ---------------------------------------------------------------------------
# --PortableHelp
# ---------------------------------------------------------------------------
if ($PortableHelp) {
@"
OpenCode Portable Wrapper (PowerShell)

Usage: .\opencode-portable.ps1 [-PortableHelp] [-PortableUpdate] [-PortableVersion] [opencode args...]

Wrapper options (consumed by this script):
-PortableHelp Show this help message
-PortableUpdate Force re-download of the OpenCode binary
-PortableVersion Show the currently cached version

All other arguments are passed through to the OpenCode binary.

Environment variables:
OPENCODE_PORTABLE_VERSION Pin a specific version (e.g. 1.0.180)

Version pinning:
You can also create a .opencode-version file next to this script
containing the desired version number (one line, e.g. "1.0.180").

Portable directory layout:
.opencode_portable\
+-- bin\ Binary
+-- config\ XDG_CONFIG_HOME
+-- data\ XDG_DATA_HOME
+-- cache\ XDG_CACHE_HOME
+-- state\ XDG_STATE_HOME
+-- .version Cached version string
"@
exit 0
}

# ---------------------------------------------------------------------------
# --PortableVersion
# ---------------------------------------------------------------------------
if ($PortableVersion) {
if (Test-Path $VersionFile) {
Write-Host "Cached version: $(Get-Content $VersionFile -Raw)".Trim()
} else {
Write-Host "No version cached yet."
}
exit 0
}

# ---------------------------------------------------------------------------
# Create portable directory structure
# ---------------------------------------------------------------------------
foreach ($sub in @("bin", "config", "data", "cache", "state")) {
$p = Join-Path $PortableDir $sub
if (-not (Test-Path $p)) { New-Item -ItemType Directory -Path $p -Force | Out-Null }
}

# ---------------------------------------------------------------------------
# XDG environment isolation
# ---------------------------------------------------------------------------
$env:XDG_CONFIG_HOME = Join-Path $PortableDir "config"
$env:XDG_DATA_HOME = Join-Path $PortableDir "data"
$env:XDG_CACHE_HOME = Join-Path $PortableDir "cache"
$env:XDG_STATE_HOME = Join-Path $PortableDir "state"
$env:OPENCODE_DISABLE_AUTOUPDATE = "true"

# ---------------------------------------------------------------------------
# Determine pinned version
# ---------------------------------------------------------------------------
$PinnedVersion = $env:OPENCODE_PORTABLE_VERSION
if (-not $PinnedVersion) {
$versionFilePath = Join-Path $ScriptDir ".opencode-version"
if (Test-Path $versionFilePath) {
$PinnedVersion = (Get-Content $versionFilePath -Raw).Trim()
}
}
if ($PinnedVersion) {
$PinnedVersion = $PinnedVersion -replace '^v', ''
}

# ---------------------------------------------------------------------------
# Decide whether a download is needed
# ---------------------------------------------------------------------------
$NeedDownload = $false
if ($PortableUpdate) {
$NeedDownload = $true
} elseif (-not (Test-Path $BinPath)) {
$NeedDownload = $true
} elseif ($PinnedVersion -and (Test-Path $VersionFile)) {
$cached = (Get-Content $VersionFile -Raw).Trim()
if ($cached -ne $PinnedVersion) {
$NeedDownload = $true
}
}

# ---------------------------------------------------------------------------
# Download logic
# ---------------------------------------------------------------------------
if ($NeedDownload) {
# --- platform detection -------------------------------------------------
$os = "windows"
$arch = switch ($env:PROCESSOR_ARCHITECTURE) {
"AMD64" { "x64" }
"x86" { "x64" } # 32-bit on 64-bit — use x64 binary
default {
Write-Host "Unsupported architecture: $env:PROCESSOR_ARCHITECTURE" -ForegroundColor Red
exit 1
}
}

# AVX2 / baseline detection via IsProcessorFeaturePresent(40)
$needsBaseline = $false
try {
Add-Type -MemberDefinition @"
[DllImport("kernel32.dll")]
public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);
"@ -Name Kernel32 -Namespace Win32Portable -ErrorAction Stop
$hasAVX2 = [Win32Portable.Kernel32]::IsProcessorFeaturePresent(40)
if (-not $hasAVX2) { $needsBaseline = $true }
} catch {
# If detection fails, assume baseline for safety
$needsBaseline = $true
}

$target = "$os-$arch"
if ($needsBaseline) { $target += "-baseline" }
$filename = "opencode-$target.zip"

# --- resolve version & URL ----------------------------------------------
if ($PinnedVersion) {
$url = "https://github.com/anomalyco/opencode/releases/download/v$PinnedVersion/$filename"
$specificVersion = $PinnedVersion
} else {
$url = "https://github.com/anomalyco/opencode/releases/latest/download/$filename"
try {
$releaseInfo = Invoke-RestMethod -Uri "https://api.github.com/repos/anomalyco/opencode/releases/latest" -UseBasicParsing
$specificVersion = $releaseInfo.tag_name -replace '^v', ''
} catch {
Write-Host "Failed to fetch latest version information" -ForegroundColor Red
exit 1
}
}

# --- download & extract -------------------------------------------------
$tmpDir = Join-Path $env:TEMP "opencode_portable_$PID"
if (Test-Path $tmpDir) { Remove-Item $tmpDir -Recurse -Force }
New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null

try {
Write-Host "Downloading OpenCode v$specificVersion ($target)..." -ForegroundColor DarkGray

$archivePath = Join-Path $tmpDir $filename
# Use .NET WebClient for better performance on large files
$wc = New-Object System.Net.WebClient
$wc.DownloadFile($url, $archivePath)

Expand-Archive -Path $archivePath -DestinationPath $tmpDir -Force

$srcBin = Join-Path $tmpDir "opencode.exe"
if (-not (Test-Path $srcBin)) {
Write-Host "Error: opencode.exe not found in archive" -ForegroundColor Red
exit 1
}
Copy-Item $srcBin $BinPath -Force

Set-Content -Path $VersionFile -Value $specificVersion -NoNewline
Write-Host "OpenCode v$specificVersion ready." -ForegroundColor Green
} finally {
Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
}
}

# ---------------------------------------------------------------------------
# Execute
# ---------------------------------------------------------------------------
if (-not (Test-Path $BinPath)) {
Write-Host "Error: OpenCode binary not found at $BinPath" -ForegroundColor Red
exit 1
}

& $BinPath @PassthroughArgs
exit $LASTEXITCODE
Loading
Loading