# ============================================================ # OpenCode Portable - installer for Windows # # What it does: # 1. downloads opencode-portable.rar (if it is not present) # 2. unpacks it to C:\opencode-portable # 3. starts C:\opencode-portable\opencode.cmd # # Default archive: C:\Users\\Downloads\opencode-portable.rar # Default URL: https://112.ftp.sh/opencode-portable.rar # # Run: # powershell -NoProfile -ExecutionPolicy Bypass -File .\install-opencode-portable.ps1 # # Useful options: # -Force reinstall (the old folder is kept as a backup) # -NoLaunch install only, do not start OpenCode # -Url URL another download address # -Archive P use another local archive # -Destination D install into another folder (default C:\) # # RAR is unpacked by: Windows tar (Win10/11), installed 7-Zip, # WinRAR, or a portable 7-Zip that is downloaded automatically. # ZIP archives are supported as well. # ============================================================ [CmdletBinding()] param( [string]$Url = 'https://112.ftp.sh/opencode-portable.rar', [string]$Archive = '', [string]$Destination = 'C:\', [switch]$Force, [switch]$NoLaunch ) $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8 } catch { } try { [Net.ServicePointManager]::SecurityProtocol = ` [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch { } $script:TempDirs = @() function Write-Step { param([string]$Text) Write-Host '' Write-Host ('=== ' + $Text + ' ===') -ForegroundColor Cyan } function Write-Ok { param([string]$Text) Write-Host (' [OK] ' + $Text) -ForegroundColor Green } function Write-Info { param([string]$Text) Write-Host (' [INFO] ' + $Text) -ForegroundColor Gray } function Write-Warn { param([string]$Text) Write-Host (' [WARN] ' + $Text) -ForegroundColor Yellow } function Get-DefaultArchivePath { $base = '' try { $base = [Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile) } catch { } if (-not $base) { $base = $env:USERPROFILE } if (-not $base) { $base = $env:HOME } if (-not $base) { throw 'Cannot determine the user profile folder.' } return (Join-Path (Join-Path $base 'Downloads') 'opencode-portable.rar') } function Test-WriteAccess { param([string]$Dir) if (-not (Test-Path -LiteralPath $Dir -PathType Container)) { New-Item -ItemType Directory -Path $Dir -Force -ErrorAction Stop | Out-Null } $probe = Join-Path $Dir ('.opencode-write-' + [Guid]::NewGuid().ToString('N')) try { New-Item -ItemType Directory -Path $probe -ErrorAction Stop | Out-Null Remove-Item -LiteralPath $probe -Force -ErrorAction SilentlyContinue return $true } catch { return $false } } function Get-RemoteFile { param( [string]$Address, [string]$Path, [int]$TimeoutSec = 600 ) $dir = Split-Path -Parent $Path if ($dir -and -not (Test-Path -LiteralPath $dir -PathType Container)) { New-Item -ItemType Directory -Path $dir -Force -ErrorAction Stop | Out-Null } $part = $Path + '.part' if (Test-Path -LiteralPath $part) { Remove-Item -LiteralPath $part -Force -ErrorAction SilentlyContinue } Write-Info ('Downloading: ' + $Address) try { $iwr = Get-Command 'Invoke-WebRequest' -ErrorAction SilentlyContinue if ($iwr) { Invoke-WebRequest -Uri $Address -OutFile $part -UseBasicParsing -TimeoutSec $TimeoutSec } else { $wc = New-Object System.Net.WebClient try { $wc.Headers.Add('User-Agent', 'opencode-portable-installer') $wc.DownloadFile($Address, $part) } finally { $wc.Dispose() } } } catch { Remove-Item -LiteralPath $part -Force -ErrorAction SilentlyContinue $message = 'Failed to download ' + $Address + ': ' + $_.Exception.Message if ($_.Exception.Message -match '404|Not Found') { $message += ' Check that the archive is available at this URL, ' + 'or pass a local file with -Archive.' } throw $message } if (-not (Test-Path -LiteralPath $part -PathType Leaf)) { throw ('File was not downloaded: ' + $Address) } $length = (Get-Item -LiteralPath $part).Length if ($length -lt 1024) { Remove-Item -LiteralPath $part -Force -ErrorAction SilentlyContinue throw ('Downloaded file is too small: ' + $Address) } if (Test-Path -LiteralPath $Path) { Remove-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue } Move-Item -LiteralPath $part -Destination $Path -Force Write-Ok ('Downloaded: ' + [Math]::Round($length / 1MB, 1) + ' MB -> ' + $Path) } function Find-Exe { param( [string]$Name, [string[]]$Candidates = @() ) foreach ($candidate in $Candidates) { if ($candidate -and (Test-Path -LiteralPath $candidate -PathType Leaf)) { return $candidate } } $cmd = Get-Command $Name -ErrorAction SilentlyContinue | Select-Object -First 1 if ($cmd) { if ($cmd.Path) { return $cmd.Path } if ($cmd.Source) { return $cmd.Source } if ($cmd.Definition) { return $cmd.Definition } } return $null } function Find-7Zip { $candidates = @() if ($env:ProgramFiles) { $candidates += (Join-Path $env:ProgramFiles '7-Zip\7z.exe') } $pf86 = ${env:ProgramFiles(x86)} if ($pf86) { $candidates += (Join-Path $pf86 '7-Zip\7z.exe') } if ($env:LOCALAPPDATA) { $candidates += (Join-Path $env:LOCALAPPDATA 'Programs\7-Zip\7z.exe') } return (Find-Exe '7z.exe' $candidates) } function Find-UnRar { $candidates = @() if ($env:ProgramFiles) { $candidates += (Join-Path $env:ProgramFiles 'WinRAR\UnRAR.exe') } $pf86 = ${env:ProgramFiles(x86)} if ($pf86) { $candidates += (Join-Path $pf86 'WinRAR\UnRAR.exe') } return (Find-Exe 'UnRAR.exe' $candidates) } function Find-TarReader { param([string]$ArchivePath) $tar = Find-Exe 'tar.exe' @() if (-not $tar) { return $null } try { & $tar -tf $ArchivePath 2>&1 | Out-Null if ($LASTEXITCODE -eq 0) { return $tar } } catch { } return $null } function Find-PortableRoot { param([string]$Stage) $launcher = Get-ChildItem -LiteralPath $Stage -Recurse -Filter 'opencode.cmd' ` -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $launcher) { return $null } $root = Split-Path -Parent $launcher.FullName $exe = Join-Path $root 'bin\opencode.exe' if (Test-Path -LiteralPath $exe -PathType Leaf) { return $root } return $null } function Invoke-ZipExtract { param( [string]$ArchivePath, [string]$Stage ) Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Stop [System.IO.Compression.ZipFile]::ExtractToDirectory($ArchivePath, $Stage) } function Invoke-7ZipExtract { param( [string]$Exe, [string]$ArchivePath, [string]$Stage ) & $Exe x $ArchivePath ('-o' + $Stage) -y | Out-Null return ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq 1) } function Get-Portable7Zip { Write-Info '7-Zip not found, downloading a standalone unpacker...' $is64 = $false try { $is64 = [Environment]::Is64BitOperatingSystem } catch { } if (-not $is64) { if ($env:PROCESSOR_ARCHITECTURE -match '64' -or $env:PROCESSOR_ARCHITEW6432 -match '64') { $is64 = $true } } $versions = @() try { $page = (Invoke-WebRequest -Uri 'https://www.7-zip.org/download.html' ` -UseBasicParsing -TimeoutSec 30).Content if ($page -match 'Download 7-Zip\s+(\d+)\.(\d+)') { $versions += ($Matches[1] + $Matches[2].PadLeft(2, '0')) } } catch { } $versions += @('2605', '2501', '2409') $versions = $versions | Select-Object -Unique foreach ($version in $versions) { $work = Join-Path ([IO.Path]::GetTempPath()) ('opencode-7zip-' + [Guid]::NewGuid().ToString('N')) try { New-Item -ItemType Directory -Path $work -Force -ErrorAction Stop | Out-Null $msiName = '7z' + $version + '.msi' if ($is64) { $msiName = '7z' + $version + '-x64.msi' } $msi = Join-Path $work '7z.msi' Get-RemoteFile ('https://www.7-zip.org/a/' + $msiName) $msi 120 $target = Join-Path $work 'files' New-Item -ItemType Directory -Path $target -Force -ErrorAction Stop | Out-Null $msiArgs = @('/a', ('"' + $msi + '"'), '/qn', ('TARGETDIR="' + $target + '"')) $msiExe = Join-Path $env:SystemRoot 'System32\msiexec.exe' $proc = Start-Process -FilePath $msiExe -ArgumentList $msiArgs ` -Wait -PassThru -WindowStyle Hidden if ($proc.ExitCode -ne 0 -and $proc.ExitCode -ne 3010) { throw ('msiexec returned code ' + $proc.ExitCode) } $sevenZip = Get-ChildItem -LiteralPath $target -Recurse -Filter '7z.exe' ` -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $sevenZip) { throw '7z.exe not found after MSI extraction.' } $script:TempDirs += $work return $sevenZip.FullName } catch { Write-Warn ('Could not prepare 7-Zip ' + $version + ': ' + $_.Exception.Message) Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue } } throw 'Could not get 7-Zip. Install 7-Zip or WinRAR and try again.' } function Expand-PortableArchive { param( [string]$ArchivePath, [string]$Stage ) $ext = [IO.Path]::GetExtension($ArchivePath).ToLowerInvariant() $installed7 = Find-7Zip $tar = Find-TarReader $ArchivePath $unrar = Find-UnRar $attempts = @() if ($ext -eq '.zip') { $attempts += @{ Name = 'ZIP (.NET)'; Kind = 'zip' } if ($tar) { $attempts += @{ Name = 'Windows tar'; Kind = 'tar'; Exe = $tar } } if ($installed7) { $attempts += @{ Name = '7-Zip'; Kind = '7z'; Exe = $installed7 } } } elseif ($ext -eq '.rar') { if ($tar) { $attempts += @{ Name = 'Windows tar'; Kind = 'tar'; Exe = $tar } } if ($installed7) { $attempts += @{ Name = '7-Zip'; Kind = '7z'; Exe = $installed7 } } if ($unrar) { $attempts += @{ Name = 'UnRAR'; Kind = 'unrar'; Exe = $unrar } } } elseif ($ext -eq '.7z') { if ($installed7) { $attempts += @{ Name = '7-Zip'; Kind = '7z'; Exe = $installed7 } } if ($tar) { $attempts += @{ Name = 'Windows tar'; Kind = 'tar'; Exe = $tar } } } else { if ($installed7) { $attempts += @{ Name = '7-Zip'; Kind = '7z'; Exe = $installed7 } } if ($tar) { $attempts += @{ Name = 'Windows tar'; Kind = 'tar'; Exe = $tar } } if ($unrar) { $attempts += @{ Name = 'UnRAR'; Kind = 'unrar'; Exe = $unrar } } } foreach ($attempt in $attempts) { if (Test-Path -LiteralPath $Stage) { Remove-Item -LiteralPath $Stage -Recurse -Force -ErrorAction SilentlyContinue } New-Item -ItemType Directory -Path $Stage -Force -ErrorAction Stop | Out-Null Write-Info ('Extracting with: ' + $attempt.Name) $ok = $false try { if ($attempt.Kind -eq 'zip') { Invoke-ZipExtract -ArchivePath $ArchivePath -Stage $Stage $ok = $true } elseif ($attempt.Kind -eq 'tar') { & $attempt.Exe -xf $ArchivePath -C $Stage | Out-Null $ok = ($LASTEXITCODE -eq 0) } elseif ($attempt.Kind -eq '7z') { $ok = Invoke-7ZipExtract -Exe $attempt.Exe -ArchivePath $ArchivePath -Stage $Stage } elseif ($attempt.Kind -eq 'unrar') { & $attempt.Exe x -o+ -y $ArchivePath ($Stage + '\') | Out-Null $ok = ($LASTEXITCODE -eq 0) } } catch { Write-Warn ($attempt.Name + ': ' + $_.Exception.Message) } if ($ok) { $root = Find-PortableRoot $Stage if ($root) { return $root } Write-Warn ($attempt.Name + ': opencode.cmd / bin\opencode.exe not found.') } } Write-Info 'Trying standalone 7-Zip...' $portable7 = Get-Portable7Zip if (Test-Path -LiteralPath $Stage) { Remove-Item -LiteralPath $Stage -Recurse -Force -ErrorAction SilentlyContinue } New-Item -ItemType Directory -Path $Stage -Force -ErrorAction Stop | Out-Null if (Invoke-7ZipExtract -Exe $portable7 -ArchivePath $ArchivePath -Stage $Stage) { $root = Find-PortableRoot $Stage if ($root) { return $root } } throw 'Could not extract the archive, or it does not contain opencode.cmd / bin\opencode.exe.' } function Install-PortableFiles { param( [string]$SourceRoot, [string]$Destination, [string]$AppName ) $appDir = Join-Path $Destination $AppName if ((Split-Path -Leaf $SourceRoot) -ne $AppName) { $renamed = Join-Path (Split-Path -Parent $SourceRoot) $AppName if (Test-Path -LiteralPath $renamed) { throw ('Folder already exists: ' + $renamed) } Move-Item -LiteralPath $SourceRoot -Destination $renamed -Force $SourceRoot = $renamed } $backup = $null if (Test-Path -LiteralPath $appDir) { $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' $backup = $appDir + '.backup-' + $stamp Write-Info ('Old version: ' + $backup) Move-Item -LiteralPath $appDir -Destination $backup -Force } try { Move-Item -LiteralPath $SourceRoot -Destination $Destination -Force } catch { if ($backup -and -not (Test-Path -LiteralPath $appDir) -and (Test-Path -LiteralPath $backup)) { Move-Item -LiteralPath $backup -Destination $appDir -Force -ErrorAction SilentlyContinue } throw ('Could not move OpenCode to ' + $Destination + '. ' + 'Close OpenCode and try again. ' + $_.Exception.Message) } if ($backup) { Write-Ok ('Old version kept: ' + $backup) } return $appDir } function Start-OpenCode { param([string]$AppDir) $launcher = Join-Path $AppDir 'opencode.cmd' if (-not (Test-Path -LiteralPath $launcher -PathType Leaf)) { throw ('Launcher not found: ' + $launcher) } Write-Ok ('Starting: ' + $launcher) Start-Process -FilePath $launcher -WorkingDirectory $AppDir | Out-Null } # ------------------------------------------------------------ # Main # ------------------------------------------------------------ $AppName = 'opencode-portable' if (-not $Archive) { $Archive = Get-DefaultArchivePath } try { $Archive = [IO.Path]::GetFullPath($Archive) } catch { } Write-Step 'OpenCode Portable' Write-Info ('Archive: ' + $Archive) Write-Info ('Install to: ' + (Join-Path $Destination $AppName)) $appDir = Join-Path $Destination $AppName $launcher = Join-Path $appDir 'opencode.cmd' $exe = Join-Path $appDir 'bin\opencode.exe' $alreadyInstalled = (Test-Path -LiteralPath $launcher -PathType Leaf) -and ` (Test-Path -LiteralPath $exe -PathType Leaf) if ($alreadyInstalled -and -not $Force) { Write-Ok 'OpenCode is already installed, skipping extraction.' Write-Info 'Use -Force to reinstall.' if (-not $NoLaunch) { Start-OpenCode -AppDir $appDir } return } if (-not (Test-WriteAccess $Destination)) { throw ('No write access to ' + $Destination + '. Run PowerShell as Administrator.') } $archiveReady = $false if (Test-Path -LiteralPath $Archive -PathType Leaf) { if ((Get-Item -LiteralPath $Archive).Length -ge 1MB) { $archiveReady = $true Write-Ok ('Using archive: ' + $Archive) } else { Write-Warn 'Archive is incomplete, downloading again.' Remove-Item -LiteralPath $Archive -Force -ErrorAction SilentlyContinue } } if (-not $archiveReady) { if (-not $Url) { throw ('Archive not found: ' + $Archive + '. Pass a local file with -Archive or a URL with -Url.') } if (Test-Path -LiteralPath $Url -PathType Leaf) { Copy-Item -LiteralPath $Url -Destination $Archive -Force Write-Ok ('Copied: ' + $Url + ' -> ' + $Archive) } else { Get-RemoteFile -Address $Url -Path $Archive } } $stage = Join-Path $Destination ('.opencode-tmp-' + [Guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $stage -Force -ErrorAction Stop | Out-Null try { $stagedRoot = Expand-PortableArchive -ArchivePath $Archive -Stage $stage Write-Ok ('Extracted: ' + $stagedRoot) $appDir = Install-PortableFiles -SourceRoot $stagedRoot -Destination $Destination -AppName $AppName Write-Ok ('Installed: ' + $appDir) $exe = Join-Path $appDir 'bin\opencode.exe' $launcher = Join-Path $appDir 'opencode.cmd' if (-not (Test-Path -LiteralPath $exe -PathType Leaf)) { throw ('File not found: ' + $exe) } if (-not (Test-Path -LiteralPath $launcher -PathType Leaf)) { throw ('File not found: ' + $launcher) } if (-not $NoLaunch) { Start-OpenCode -AppDir $appDir } else { Write-Ok ('Done. Start manually: ' + $launcher) } Write-Host '' Write-Host 'Done.' -ForegroundColor Green } finally { if ($stage -and (Test-Path -LiteralPath $stage)) { Remove-Item -LiteralPath $stage -Recurse -Force -ErrorAction SilentlyContinue } foreach ($tempDir in $script:TempDirs) { Remove-Item -LiteralPath $tempDir -Recurse -Force -ErrorAction SilentlyContinue } }