Repository navigation
Expand file tree
/
Copy pathinstall.ps1
More file actions
301 lines (282 loc) · 16.6 KB
/
Copy pathinstall.ps1
File metadata and controls
301 lines (282 loc) · 16.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# Installs browse on Windows: downloads the browse binary for this computer from Anomaly's release service into
# %USERPROFILE%\.local\bin (or %XDG_BIN_HOME%), then hands over to `browse install`, which does the rest (PATH, your
# agents, pairing with your browser). Served at https://opencode.ai/browse/install.ps1; the PowerShell twin of ./install.
#
# irm https://opencode.ai/browse/install.ps1 | iex
#
# `irm | iex` can't take arguments. To pass some, run the script as a script block:
#
# & ([scriptblock]::Create((irm https://opencode.ai/browse/install.ps1))) --remote
# & ([scriptblock]::Create((irm https://opencode.ai/browse/install.ps1))) --version 0.0.7 --agent claude
#
# or set them in the environment first: $env:VERSION = "0.0.7"; $env:BROWSE_CHANNEL = "dev"
#
# Options:
# -h, --help Show this help
# -v, --version <version> Install a specific version from npm (or $env:VERSION)
# --channel <channel> Install the latest version on a release channel: stable (default), beta or dev.
# `latest` means stable (or $env:BROWSE_CHANNEL)
# -b, --binary <path> Install from a local browse.exe instead of downloading
# --no-setup Only install the binary; don't run `browse install`
# Anything else goes to `browse install`, e.g. --remote, --agent claude or --no-modify-path.
# Windows PowerShell 5.1 can fail to autoload Get-FileHash when a PowerShell 7 module directory earlier in PSModulePath
# shadows Microsoft.PowerShell.Utility, so hash a file with .NET directly: the same lowercase hex on 5.1 and 7+.
function Get-FileDigest {
param([string]$Path, [string]$Algorithm)
$hash = switch ($Algorithm) {
"SHA256" { [System.Security.Cryptography.SHA256]::Create() }
"SHA512" { [System.Security.Cryptography.SHA512]::Create() }
default { throw "unsupported hash algorithm $Algorithm" }
}
$stream = $null
try {
$stream = [System.IO.File]::OpenRead($Path)
$bytes = $hash.ComputeHash($stream)
return ([BitConverter]::ToString($bytes) -replace "-", "").ToLowerInvariant()
} finally {
if ($stream) { $stream.Dispose() }
$hash.Dispose()
}
}
function Install-browse {
param([string[]]$Arguments)
# Function scope: this doesn't leak to the caller's session. It covers the property probing below and the
# dot-sourced download blocks, which run in this same scope, under a caller's `Set-StrictMode -Version Latest`.
Set-StrictMode -Off
# Everything runs in this function so a failure stops it without closing the window `irm | iex` runs in.
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$requested = $env:VERSION
$pinned = [bool]$env:VERSION
$channel = if ($env:BROWSE_CHANNEL) { $env:BROWSE_CHANNEL } else { "stable" }
$binary = $null
$noSetup = $false
$setupArgs = @()
for ($i = 0; $i -lt $Arguments.Count; $i++) {
$arg = [string]$Arguments[$i]
switch -Regex ($arg) {
"^(-h|--help|-Help)$" {
Write-Host @"
browse installer
Usage:
irm https://opencode.ai/browse/install.ps1 | iex
& ([scriptblock]::Create((irm https://opencode.ai/browse/install.ps1))) [options] [browse install options]
Options:
-h, --help Show this help
-v, --version <version> Install a specific version from npm (e.g., 0.0.7), or set `$env:VERSION
--channel <channel> Install the latest version on a release channel: stable (default), beta or dev.
`latest` means stable, or set `$env:BROWSE_CHANNEL
-b, --binary <path> Install from a local browse.exe instead of downloading
--no-setup Only install the binary; don't run ``browse install``
Anything else goes to ``browse install``, e.g. --remote, --agent claude or --no-modify-path.
"@
return
}
"^(-v|--version|-Version)$" {
if ($i + 1 -ge $Arguments.Count) { throw "--version requires a version argument" }
$requested = [string]$Arguments[++$i]
$pinned = $true
continue
}
"^(--channel|-Channel)$" {
if ($i + 1 -ge $Arguments.Count) { throw "--channel requires a tag" }
$channel = [string]$Arguments[++$i]
continue
}
"^(-b|--binary|-Binary)$" {
if ($i + 1 -ge $Arguments.Count) { throw "--binary requires a path argument" }
$binary = [string]$Arguments[++$i]
continue
}
"^(--no-setup|-NoSetup)$" {
$noSetup = $true
continue
}
default { $setupArgs += $arg }
}
}
# `latest` was the npm tag and is now an alias for the release service's stable channel.
if ($channel -eq "latest") { $channel = "stable" }
if ($channel -notin @("stable", "beta", "dev")) { throw "unknown channel $channel (stable, beta or dev)" }
# Windows PowerShell 5.1 may still default to TLS 1.0, which the npm registry refuses.
try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch {}
$installDir = if ($env:XDG_BIN_HOME -and [IO.Path]::IsPathRooted($env:XDG_BIN_HOME)) { $env:XDG_BIN_HOME } else { Join-Path $env:USERPROFILE ".local\bin" }
$exe = Join-Path $installDir "browse.exe"
New-Item -ItemType Directory -Force -Path $installDir | Out-Null
$tmp = Join-Path ([IO.Path]::GetTempPath()) "browse_install_$([Guid]::NewGuid().ToString('N'))"
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
try {
if ($binary) {
if (-not (Test-Path -LiteralPath $binary -PathType Leaf)) { throw "Binary not found at $binary" }
Write-Host ""
Write-Host "Installing browse from: $binary"
$source = (Resolve-Path -LiteralPath $binary).Path
} else {
$arch = $null
try { $arch = [string][System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture } catch {}
if (-not $arch) { $arch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } }
$target = switch -Regex ($arch) {
"^(X64|AMD64)$" { "x64" }
# Windows on ARM runs the x64 build under emulation; the baseline build doesn't need AVX2.
"^(Arm64|ARM64)$" { "x64-baseline" }
default { throw "Unsupported architecture: $arch. browse runs on 64-bit Windows." }
}
if ($target -eq "x64") {
$avx2 = $false
try {
$avx2 = (Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)
} catch {}
if (-not $avx2) { $target = "x64-baseline" }
}
$package = "@opencode/browse-windows-$target"
$encoded = "@opencode%2fbrowse-windows-$target"
if ($pinned) {
$version = $requested -replace "^v", ""
if (-not $version) { throw "--version needs a version, e.g. 0.0.14" }
# Keep the version metadata: its dist.integrity is the tarball's SHA-512.
try { $metadata = Invoke-RestMethod -UseBasicParsing "https://registry.npmjs.org/$encoded/$version" }
catch {
$status = $null
try { $status = [int]$_.Exception.Response.StatusCode } catch {}
if ($status -eq 404) { throw "browse $version is not available for windows-$target. Available versions: https://www.npmjs.com/package/${package}?activeTab=versions" }
throw "Failed to fetch package information: $($_.Exception.Message)"
}
$integrity = [string]$metadata.dist.integrity
if (-not $integrity) {
throw "$package@$version has no published SHA-512 integrity; nothing was installed"
}
$sri = [regex]::Match($integrity, '(?:^|\s)sha512-([A-Za-z0-9+/]+={0,2})(?:\s|$)')
if (-not $sri.Success) {
throw "$package@$version has no usable SHA-512 integrity; nothing was installed"
}
try { $expectedBytes = [Convert]::FromBase64String($sri.Groups[1].Value) }
catch { throw "$package@$version has an invalid SHA-512 integrity; nothing was installed" }
if ($expectedBytes.Length -ne 64) {
throw "$package@$version has an invalid SHA-512 integrity; nothing was installed"
}
$expectedSha512 = ([BitConverter]::ToString($expectedBytes) -replace "-", "").ToLowerInvariant()
} else {
# The channel's current release from the Anomaly release service (anomalyco/release docs/protocol.md). The
# manifest names the file for this target; its size and SHA-256 must match before it is used.
$service = if ($env:OPENCODE_BROWSER_RELEASE_SERVICE) { $env:OPENCODE_BROWSER_RELEASE_SERVICE } else { "https://anoma.ly/release" }
$service = $service.TrimEnd("/")
if (-not $service.EndsWith("/release")) { throw "OPENCODE_BROWSER_RELEASE_SERVICE must end in /release, not $service" }
$manifestUrl = "$service/browse/cli/$channel.json"
# PowerShell 7 validates the User-Agent as RFC 7231 product tokens, so the channel and action live in
# one product and a comment instead of extra `/`-separated segments.
try { $manifest = Invoke-RestMethod -UseBasicParsing -UserAgent "browse/$channel (install)" $manifestUrl }
catch {
$status = $null
try { $status = [int]$_.Exception.Response.StatusCode } catch {}
if ($status -eq 404) { throw "no $channel release at ${manifestUrl}: $($_.Exception.Message)" }
throw "could not read the $channel release manifest at ${manifestUrl}: $($_.Exception.Message)"
}
if ($manifest.app -ne "browse" -or $manifest.artifact -ne "cli" -or $manifest.channel -ne $channel) {
throw "$manifestUrl is not browse's $channel CLI manifest"
}
$version = [string]$manifest.release.version
if (-not $version) { throw "the $channel release has no version" }
$entry = $manifest.release.targets.PSObject.Properties["windows-$target"]
if (-not $entry) { throw "the $channel release has no windows-$target build" }
$url = [string]$entry.Value.url
if (-not $url.StartsWith("/")) { throw "unexpected download URL $url" }
$url = ($service -replace "/release$", "") + $url
$expectedSha = [string]$entry.Value.sha256
$expectedSize = [long]$entry.Value.size
$filename = Split-Path $url -Leaf
}
}
$download = {
$filename = "browse-windows-$target-$version.tgz"
$archive = Join-Path $tmp $filename
Write-Host ""
Write-Host "Installing browse version: $version"
Invoke-WebRequest -UseBasicParsing -Uri "https://registry.npmjs.org/$package/-/$filename" -OutFile $archive
$actualSha = Get-FileDigest -Path $archive -Algorithm SHA512
if ($actualSha -ne $expectedSha512) {
throw "checksum mismatch for $package@$version ($filename): expected $expectedSha512, got $actualSha; nothing was installed"
}
# Windows 10 and later ship bsdtar as System32\tar.exe; another tar on PATH (Git's) can't read C:\ paths.
$tar = Join-Path $env:SystemRoot "System32\tar.exe"
if (-not (Test-Path -LiteralPath $tar)) { $tar = "tar.exe" }
& $tar -xzf $archive -C $tmp
if ($LASTEXITCODE -ne 0) { throw "Could not extract $filename" }
$source = Join-Path $tmp "package\bin\browse.exe"
if (-not (Test-Path -LiteralPath $source)) { throw "$filename has no package\bin\browse.exe" }
}
$downloadService = {
$archive = Join-Path $tmp $filename
Write-Host ""
Write-Host "Installing browse version: $version ($channel)"
Invoke-WebRequest -UseBasicParsing -UserAgent "browse/$version ($channel; cli)" -Uri $url -OutFile $archive
$actualSha = Get-FileDigest -Path $archive -Algorithm SHA256
if (-not $expectedSha -or $actualSha -ne $expectedSha.ToLowerInvariant()) {
throw "checksum mismatch for $filename (expected $(if ($expectedSha) { $expectedSha } else { 'none' }), got $actualSha)"
}
$actualSize = (Get-Item -LiteralPath $archive).Length
if ($expectedSize -and $actualSize -ne $expectedSize) { throw "size mismatch for $filename (expected $expectedSize, got $actualSize)" }
# Windows 10 and later ship bsdtar as System32\tar.exe; another tar on PATH (Git's) can't read C:\ paths.
$tar = Join-Path $env:SystemRoot "System32\tar.exe"
if (-not (Test-Path -LiteralPath $tar)) { $tar = "tar.exe" }
& $tar -xzf $archive -C $tmp
if ($LASTEXITCODE -ne 0) { throw "Could not extract $filename" }
$source = Join-Path $tmp "package\bin\browse.exe"
if (-not (Test-Path -LiteralPath $source)) { throw "$filename has no package\bin\browse.exe" }
}
if (-not $binary) {
if (-not $pinned) {
. $downloadService
} else {
foreach ($delay in 0, 15, 30, 60, 75) {
if ($delay) {
Write-Warning "The $version tarball isn't available yet; retrying in ${delay}s."
Start-Sleep -Seconds $delay
}
try { . $download; break } catch {
$response = $_.Exception.Response
if ([int]$response.StatusCode -ne 404) { throw }
if ($delay -ne 75) { continue }
if ($pinned -or $channel -notin 'latest', 'dev') { throw }
# Windows PowerShell uses HttpWebResponse; PowerShell Core uses HttpResponseMessage.
$uri = if ($response.ResponseUri) { $response.ResponseUri } else { $response.RequestMessage.RequestUri }
$versions = (Invoke-RestMethod -UseBasicParsing ([uri]::new($uri, "/$encoded"))).versions.PSObject.Properties.Name
$pattern = if ($channel -eq 'dev') { '^\d+\.\d+\.\d+-dev\.\d+(\.\d+)?$' } else { '^\d+\.\d+\.\d+$' }
$ordered = @($versions | Where-Object { $_ -match $pattern } | Sort-Object -Descending -Property { [version]($_ -split '-')[0] }, { [long]($_ -split '\.')[3] }, { [long]($_ -split '\.')[4] })
$index = [array]::IndexOf($ordered, $version)
if ($index -lt 0 -or $index + 1 -ge $ordered.Count) { throw }
$previous = $ordered[$index + 1]
Write-Warning "browse $version is still unavailable; falling back to $previous ($channel)."
$version = $previous
. $download
}
}
}
}
# A running browse.exe (the relay) can't be overwritten, but it can be renamed: move it aside, then put the new
# one in its place. Copies moved aside earlier are deleted once nothing runs them.
Get-ChildItem -LiteralPath $installDir -Filter "browse.exe.old-*" -ErrorAction SilentlyContinue | ForEach-Object {
Remove-Item -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue
}
$staged = "$exe.new"
Copy-Item -LiteralPath $source -Destination $staged -Force
if (Test-Path -LiteralPath $exe) {
Move-Item -LiteralPath $exe -Destination "$exe.old-$([Guid]::NewGuid().ToString('N').Substring(0, 8))" -Force
}
Move-Item -LiteralPath $staged -Destination $exe -Force
} finally {
Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue
}
if ($noSetup) {
Write-Host ""
Write-Host "Run $exe install to add browse to your agents and pair it with your browser."
return
}
Write-Host ""
& $exe install @setupArgs
}
try {
Install-browse -Arguments $args
} catch {
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
$global:LASTEXITCODE = 1
}