browser4-cli 4.12.0 → 4.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/browser4-cli-darwin-arm64 +0 -0
- package/bin/browser4-cli-darwin-x64 +0 -0
- package/bin/browser4-cli-linux-arm64 +0 -0
- package/bin/browser4-cli-linux-musl-arm64 +0 -0
- package/bin/browser4-cli-linux-musl-x64 +0 -0
- package/bin/browser4-cli-linux-x64 +0 -0
- package/bin/browser4-cli-win32-x64.exe +0 -0
- package/package.json +1 -1
- package/scripts/README.md +4 -3
- package/scripts/build-all-platforms.ps1 +104 -104
- package/scripts/enumerate-help.ps1 +382 -382
- package/scripts/install-browser4-cli.ps1 +812 -812
- package/scripts/tests/install-browser4-cli.tests.ps1 +476 -462
|
@@ -1,812 +1,812 @@
|
|
|
1
|
-
<#
|
|
2
|
-
.SYNOPSIS
|
|
3
|
-
Install browser4-cli -- download the native binary and set it up on your PATH.
|
|
4
|
-
|
|
5
|
-
.DESCRIPTION
|
|
6
|
-
Detects your OS and CPU architecture, downloads the matching native binary from
|
|
7
|
-
GitHub Releases or Alibaba Cloud OSS, installs it to a user-local directory, and
|
|
8
|
-
optionally adds it to your PATH.
|
|
9
|
-
|
|
10
|
-
Default install location:
|
|
11
|
-
Windows: $env:LOCALAPPDATA\Programs\browser4-cli
|
|
12
|
-
(override with -InstallDir)
|
|
13
|
-
|
|
14
|
-
Download sources (auto-selected by locale; use -Source to override):
|
|
15
|
-
Outside China: 1. GitHub Releases -> 2. Aliyun OSS
|
|
16
|
-
China mainland: 1. Aliyun OSS -> 2. GitHub Releases
|
|
17
|
-
GitHub Releases -- https://github.com/platonai/Browser4
|
|
18
|
-
Aliyun OSS -- https://browser4.oss-cn-beijing.aliyuncs.com
|
|
19
|
-
|
|
20
|
-
.PARAMETER Version
|
|
21
|
-
Release version tag to download (e.g. "v4.11.0" or "v0.1.12-cli").
|
|
22
|
-
Defaults to "latest" which resolves to the most recent stable release.
|
|
23
|
-
|
|
24
|
-
.PARAMETER InstallDir
|
|
25
|
-
Directory to install the binary into.
|
|
26
|
-
Default: $env:LOCALAPPDATA\Programs\browser4-cli
|
|
27
|
-
|
|
28
|
-
.PARAMETER Source
|
|
29
|
-
Force a specific download source: "github" or "oss".
|
|
30
|
-
Default (auto): locale-aware -- OSS first in China mainland, GitHub first elsewhere.
|
|
31
|
-
|
|
32
|
-
.PARAMETER AddToPath
|
|
33
|
-
Add the install directory to the current user's PATH environment variable.
|
|
34
|
-
Default: true.
|
|
35
|
-
|
|
36
|
-
.PARAMETER Silent
|
|
37
|
-
Suppress all non-error output.
|
|
38
|
-
|
|
39
|
-
.PARAMETER DryRun
|
|
40
|
-
Print what would be done without actually doing it.
|
|
41
|
-
|
|
42
|
-
.PARAMETER SkipIfInstalled
|
|
43
|
-
Skip download if the binary already exists at the install path.
|
|
44
|
-
By default the script always downloads the latest version.
|
|
45
|
-
|
|
46
|
-
.PARAMETER SkipLocal
|
|
47
|
-
Skip checking for a locally-bundled binary alongside the script.
|
|
48
|
-
By default the script looks for the platform binary in its own directory
|
|
49
|
-
before downloading -- use this to force a fresh download.
|
|
50
|
-
|
|
51
|
-
.PARAMETER Force
|
|
52
|
-
Force reinstallation even if the binary is already installed at the target path.
|
|
53
|
-
Overrides -SkipIfInstalled and bypasses locked-file workarounds.
|
|
54
|
-
|
|
55
|
-
.PARAMETER Locate
|
|
56
|
-
Print detection results (OS, architecture, script location, China locale)
|
|
57
|
-
and exit without installing. Useful for diagnostics.
|
|
58
|
-
|
|
59
|
-
.EXAMPLE
|
|
60
|
-
# Quick install -- default location, latest version, add to PATH
|
|
61
|
-
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1
|
|
62
|
-
|
|
63
|
-
.EXAMPLE
|
|
64
|
-
# Silent install with a specific version
|
|
65
|
-
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1 -Version "v4.11.0" -Silent
|
|
66
|
-
|
|
67
|
-
.EXAMPLE
|
|
68
|
-
# Install from OSS only, custom directory
|
|
69
|
-
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1 -Source oss -InstallDir "C:\tools\browser4"
|
|
70
|
-
|
|
71
|
-
.EXAMPLE
|
|
72
|
-
# Run diagnostics -- see what the script detects without installing
|
|
73
|
-
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1 -Locate
|
|
74
|
-
|
|
75
|
-
.EXAMPLE
|
|
76
|
-
# Use a locally-bundled binary (place binary next to the script)
|
|
77
|
-
# The script auto-detects binaries in its own directory
|
|
78
|
-
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1
|
|
79
|
-
|
|
80
|
-
.EXAMPLE
|
|
81
|
-
# Force download even when a local binary exists
|
|
82
|
-
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1 -SkipLocal
|
|
83
|
-
|
|
84
|
-
.EXAMPLE
|
|
85
|
-
# Skip download if already installed (opt out of default reinstall)
|
|
86
|
-
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1 -SkipIfInstalled
|
|
87
|
-
|
|
88
|
-
.EXAMPLE
|
|
89
|
-
# For China mainland: OSS is auto-preferred via locale detection,
|
|
90
|
-
# or force it explicitly
|
|
91
|
-
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1 -Source oss
|
|
92
|
-
#>
|
|
93
|
-
|
|
94
|
-
[CmdletBinding()]
|
|
95
|
-
param(
|
|
96
|
-
[string]$Version = "",
|
|
97
|
-
[string]$InstallDir = "",
|
|
98
|
-
[ValidateSet("", "github", "oss")]
|
|
99
|
-
[string]$Source = "",
|
|
100
|
-
[bool]$AddToPath = $true,
|
|
101
|
-
[switch]$Silent,
|
|
102
|
-
[switch]$DryRun,
|
|
103
|
-
[switch]$SkipIfInstalled,
|
|
104
|
-
[switch]$SkipLocal,
|
|
105
|
-
[switch]$Force,
|
|
106
|
-
[switch]$Locate
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
$ErrorActionPreference = "Stop"
|
|
110
|
-
|
|
111
|
-
# ----------------------------------------------
|
|
112
|
-
# Script location -- find ourselves on disk
|
|
113
|
-
# ----------------------------------------------
|
|
114
|
-
|
|
115
|
-
# $PSScriptRoot is the directory containing this script (PS 3+).
|
|
116
|
-
# Falls back to $MyInvocation for edge cases (dot-sourced, PS 2).
|
|
117
|
-
$ScriptDir = if ($PSScriptRoot) {
|
|
118
|
-
$PSScriptRoot
|
|
119
|
-
} elseif ($MyInvocation -and $MyInvocation.MyCommand.Path) {
|
|
120
|
-
Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
121
|
-
} else {
|
|
122
|
-
$null
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
<#
|
|
126
|
-
.SYNOPSIS
|
|
127
|
-
Search for a pre-downloaded binary near the script (bundled/sideload install).
|
|
128
|
-
Returns the full path if found, $null otherwise.
|
|
129
|
-
#>
|
|
130
|
-
function Find-LocalBinary {
|
|
131
|
-
param([string]$BinaryName)
|
|
132
|
-
|
|
133
|
-
if (-not $ScriptDir) { return $null }
|
|
134
|
-
|
|
135
|
-
$localPath = Join-Path $ScriptDir $BinaryName
|
|
136
|
-
if (Test-Path $localPath -PathType Leaf) {
|
|
137
|
-
$size = (Get-Item $localPath).Length
|
|
138
|
-
if ($size -gt 102400) { # > 100 KB minimum
|
|
139
|
-
return $localPath
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return $null
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
<#
|
|
146
|
-
.SYNOPSIS
|
|
147
|
-
Check whether a local binary is usable by querying its version.
|
|
148
|
-
Returns $true if --version executes successfully, $false otherwise.
|
|
149
|
-
#>
|
|
150
|
-
function Test-LocalBinary {
|
|
151
|
-
param([string]$Path)
|
|
152
|
-
|
|
153
|
-
if (-not $Path -or -not (Test-Path $Path)) { return $false }
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
$null = & $Path --version 2>&1
|
|
157
|
-
return ($LASTEXITCODE -eq 0)
|
|
158
|
-
} catch {
|
|
159
|
-
return $false
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
# ----------------------------------------------
|
|
164
|
-
# OS detection (compatible with PS 5.1+)
|
|
165
|
-
# ----------------------------------------------
|
|
166
|
-
|
|
167
|
-
# Avoid assigning to $IsLinux / $IsWindows / $IsMacOS directly --
|
|
168
|
-
# they are read-only automatic variables in PowerShell 7+.
|
|
169
|
-
if ($PSVersionTable.PSVersion.Major -ge 6) {
|
|
170
|
-
$script:OSWin = $IsWindows
|
|
171
|
-
$script:OSLinux = $IsLinux
|
|
172
|
-
$script:OSMac = $IsMacOS
|
|
173
|
-
} else {
|
|
174
|
-
$script:OSWin = [System.Environment]::OSVersion.Platform -eq "Win32NT"
|
|
175
|
-
$script:OSMac = $false
|
|
176
|
-
$script:OSLinux = $false
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
# ----------------------------------------------
|
|
180
|
-
# Helpers
|
|
181
|
-
# ----------------------------------------------
|
|
182
|
-
|
|
183
|
-
function Write-Summary {
|
|
184
|
-
param([string]$Message, [string]$Color = "White")
|
|
185
|
-
if (-not $Silent) { Write-Host $Message -ForegroundColor $Color }
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function Write-Step {
|
|
189
|
-
param([string]$Message)
|
|
190
|
-
if (-not $Silent) { Write-Host " >> $Message" -ForegroundColor Gray }
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function Write-Check {
|
|
194
|
-
param([string]$Message)
|
|
195
|
-
if (-not $Silent) { Write-Host " [v] $Message" -ForegroundColor Green }
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function Write-WarnMsg {
|
|
199
|
-
param([string]$Message)
|
|
200
|
-
if (-not $Silent) { Write-Host " [!] $Message" -ForegroundColor Yellow }
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
# ----------------------------------------------
|
|
204
|
-
# Detection
|
|
205
|
-
# ----------------------------------------------
|
|
206
|
-
|
|
207
|
-
function Get-PlatformKey {
|
|
208
|
-
$arch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { "arm64" } else { "x64" }
|
|
209
|
-
|
|
210
|
-
if ($script:OSWin) {
|
|
211
|
-
return "win32-$arch"
|
|
212
|
-
}
|
|
213
|
-
elseif ($script:OSMac) {
|
|
214
|
-
return "darwin-$arch"
|
|
215
|
-
}
|
|
216
|
-
elseif ($script:OSLinux) {
|
|
217
|
-
# Detect musl
|
|
218
|
-
$isMusl = $false
|
|
219
|
-
try {
|
|
220
|
-
$lddOutput = ldd --version 2>&1
|
|
221
|
-
if ($lddOutput -match "musl") { $isMusl = $true }
|
|
222
|
-
} catch {
|
|
223
|
-
if ((Test-Path "/lib/ld-musl-x86_64.so.1") -or (Test-Path "/lib/ld-musl-aarch64.so.1")) {
|
|
224
|
-
$isMusl = $true
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
$libc = if ($isMusl) { "musl" } else { "" }
|
|
228
|
-
if ($libc) { return "linux-$libc-$arch" } else { return "linux-$arch" }
|
|
229
|
-
}
|
|
230
|
-
else {
|
|
231
|
-
throw "Unsupported OS. Browser4 CLI supports Windows, macOS, and Linux."
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function Get-BinaryName {
|
|
236
|
-
param([string]$PlatformKey)
|
|
237
|
-
return "browser4-cli-$PlatformKey" + $(if ($PlatformKey.StartsWith("win32")) { ".exe" } else { "" })
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function Get-DefaultInstallDir {
|
|
241
|
-
if ($script:OSWin) {
|
|
242
|
-
return Join-Path $env:LOCALAPPDATA "Programs\browser4-cli"
|
|
243
|
-
}
|
|
244
|
-
elseif ($script:OSLinux -or $script:OSMac) {
|
|
245
|
-
# Prefer ~/.local/bin for user installs
|
|
246
|
-
if (Test-Path "$env:HOME/.local/bin") {
|
|
247
|
-
return "$env:HOME/.local/bin"
|
|
248
|
-
}
|
|
249
|
-
return "$env:HOME/.local/bin"
|
|
250
|
-
}
|
|
251
|
-
throw "Unsupported OS"
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
# ----------------------------------------------
|
|
255
|
-
# China mainland locale detection (zero-network)
|
|
256
|
-
# ----------------------------------------------
|
|
257
|
-
|
|
258
|
-
<#
|
|
259
|
-
.SYNOPSIS
|
|
260
|
-
Detect whether the current system is likely in China mainland.
|
|
261
|
-
Uses only local env vars and .NET APIs -- no network calls.
|
|
262
|
-
#>
|
|
263
|
-
function Test-ChinaLocale {
|
|
264
|
-
# 1 -- Locale env vars
|
|
265
|
-
$lang = $env:LC_ALL, $env:LANG, $env:LC_CTYPE, $env:LC_MESSAGES | Where-Object { $_ } | Select-Object -First 1
|
|
266
|
-
if ($lang -and ($lang -match '^zh_CN' -or $lang -match '^zh-CN' -or $lang -match '^Chinese \(Simplified\)_China')) {
|
|
267
|
-
return $true
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
# 2 -- TZ env var
|
|
271
|
-
$tzEnv = $env:TZ
|
|
272
|
-
if ($tzEnv -and ($tzEnv -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$')) {
|
|
273
|
-
return $true
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
# 3 -- .NET TimeZoneInfo (works on Windows and Unix PowerShell 7+)
|
|
277
|
-
try {
|
|
278
|
-
$tzId = [System.TimeZoneInfo]::Local.Id
|
|
279
|
-
if ($tzId -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$') {
|
|
280
|
-
return $true
|
|
281
|
-
}
|
|
282
|
-
} catch {
|
|
283
|
-
# TimeZoneInfo not available (unlikely on PS 5.1+ but guard anyway)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
# 4 -- /etc/timezone (PowerShell on Linux/macOS)
|
|
287
|
-
if (-not $script:OSWin -and (Test-Path '/etc/timezone')) {
|
|
288
|
-
try {
|
|
289
|
-
$tz = Get-Content '/etc/timezone' -Raw -ErrorAction Stop
|
|
290
|
-
if ($tz -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$') {
|
|
291
|
-
return $true
|
|
292
|
-
}
|
|
293
|
-
} catch {
|
|
294
|
-
# Permission or read error -- skip
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return $false
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
# ----------------------------------------------
|
|
302
|
-
# Download URLs
|
|
303
|
-
# ----------------------------------------------
|
|
304
|
-
|
|
305
|
-
$GITHUB_REPO = "platonai/Browser4"
|
|
306
|
-
$OSS_BASE = "https://browser4.oss-cn-beijing.aliyuncs.com"
|
|
307
|
-
$script:ChinaDetected = $false
|
|
308
|
-
|
|
309
|
-
function Get-DownloadUrls {
|
|
310
|
-
param([string]$BinaryName, [string]$VersionTag)
|
|
311
|
-
|
|
312
|
-
$urls = @()
|
|
313
|
-
|
|
314
|
-
$ghBase = "https://github.com/$GITHUB_REPO/releases/download"
|
|
315
|
-
if ($VersionTag) {
|
|
316
|
-
$ghUrl = "$ghBase/$VersionTag/$BinaryName"
|
|
317
|
-
$ossUrl = "$OSS_BASE/releases/download/$VersionTag/$BinaryName"
|
|
318
|
-
} else {
|
|
319
|
-
# Use 'latest' redirect for GitHub, 'latest' symlink for OSS
|
|
320
|
-
$ghUrl = "https://github.com/$GITHUB_REPO/releases/latest/download/$BinaryName"
|
|
321
|
-
$ossUrl = "$OSS_BASE/releases/download/latest/$BinaryName"
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if ($Source -eq "github") {
|
|
325
|
-
$urls += @{ Url = $ghUrl; Label = "GitHub Releases" }
|
|
326
|
-
} elseif ($Source -eq "oss") {
|
|
327
|
-
$urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
|
|
328
|
-
} else {
|
|
329
|
-
if ($script:ChinaDetected) {
|
|
330
|
-
$urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
|
|
331
|
-
$urls += @{ Url = $ghUrl; Label = "GitHub Releases" }
|
|
332
|
-
} else {
|
|
333
|
-
$urls += @{ Url = $ghUrl; Label = "GitHub Releases" }
|
|
334
|
-
$urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return $urls
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function Invoke-Download {
|
|
342
|
-
param([string]$Url, [string]$OutFile, [string]$Label)
|
|
343
|
-
|
|
344
|
-
Write-Step "Trying $Label..."
|
|
345
|
-
Write-Step "URL: $Url"
|
|
346
|
-
|
|
347
|
-
if ($DryRun) {
|
|
348
|
-
Write-Check "[DRY-RUN] Would download to: $OutFile"
|
|
349
|
-
return $true
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
try {
|
|
353
|
-
$ProgressPreference = if ($Silent) { "SilentlyContinue" } else { "Continue" }
|
|
354
|
-
|
|
355
|
-
# Use Invoke-WebRequest with progress bar
|
|
356
|
-
Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing -ErrorAction Stop
|
|
357
|
-
|
|
358
|
-
if (Test-Path $OutFile) {
|
|
359
|
-
$size = (Get-Item $OutFile).Length
|
|
360
|
-
if ($size -gt 102400) { # > 100 KB minimum
|
|
361
|
-
Write-Check "Downloaded $( [math]::Round($size / 1MB, 1) ) MB"
|
|
362
|
-
return $true
|
|
363
|
-
} else {
|
|
364
|
-
Write-WarnMsg "Downloaded file too small ($size bytes) -- may be an error page"
|
|
365
|
-
Remove-Item $OutFile -Force -ErrorAction SilentlyContinue
|
|
366
|
-
return $false
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
Write-WarnMsg "Download appeared to succeed but file not found"
|
|
370
|
-
return $false
|
|
371
|
-
} catch {
|
|
372
|
-
Write-WarnMsg "Failed: $($_.Exception.Message)"
|
|
373
|
-
return $false
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
# ----------------------------------------------
|
|
378
|
-
# Symlinks
|
|
379
|
-
# ----------------------------------------------
|
|
380
|
-
|
|
381
|
-
function New-PlatformLink {
|
|
382
|
-
param([string]$LinkPath, [string]$TargetName, [string]$DisplayName)
|
|
383
|
-
|
|
384
|
-
# Try symbolic link first (works on Unix; on Windows needs Admin or Developer Mode)
|
|
385
|
-
try {
|
|
386
|
-
New-Item -ItemType SymbolicLink -Path $LinkPath -Target $TargetName -Force -ErrorAction Stop | Out-Null
|
|
387
|
-
Write-Check "Created symlink: $DisplayName -> $TargetName"
|
|
388
|
-
return $true
|
|
389
|
-
} catch {
|
|
390
|
-
# Symbolic link failed -- try hard link on Windows
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
# Try hard link (Windows, same volume)
|
|
394
|
-
if ($script:OSWin) {
|
|
395
|
-
try {
|
|
396
|
-
$targetPath = Join-Path (Split-Path $LinkPath -Parent) $TargetName
|
|
397
|
-
New-Item -ItemType HardLink -Path $LinkPath -Target $targetPath -Force -ErrorAction Stop | Out-Null
|
|
398
|
-
Write-Check "Created hard link: $DisplayName -> $TargetName"
|
|
399
|
-
return $true
|
|
400
|
-
} catch {
|
|
401
|
-
# Hard link also failed -- create a .cmd wrapper as last resort
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
# Last resort: .cmd wrapper that forwards all arguments
|
|
405
|
-
try {
|
|
406
|
-
$wrapperContent = '@"%~dp0' + $TargetName + '" %*'
|
|
407
|
-
Set-Content -Path ($LinkPath -replace '\.exe$', '.cmd') -Value $wrapperContent -Force -ErrorAction Stop
|
|
408
|
-
Write-Check "Created wrapper: " + (Split-Path ($LinkPath -replace '\.exe$', '.cmd') -Leaf) + " -> $TargetName"
|
|
409
|
-
return $true
|
|
410
|
-
} catch {
|
|
411
|
-
Write-WarnMsg "Could not create link for $DisplayName (may need admin privileges)"
|
|
412
|
-
return $false
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
Write-WarnMsg "Could not create link for $DisplayName"
|
|
417
|
-
return $false
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function New-Symlinks {
|
|
421
|
-
param([string]$BinaryName, [string]$InstallDir, [string]$PlatformKey)
|
|
422
|
-
|
|
423
|
-
$ext = if ($PlatformKey.StartsWith("win32")) { ".exe" } else { "" }
|
|
424
|
-
|
|
425
|
-
# 1) Always: browser4-cli -> browser4-cli-<platform>
|
|
426
|
-
$linkName = "browser4-cli$ext"
|
|
427
|
-
$linkPath = Join-Path $InstallDir $linkName
|
|
428
|
-
|
|
429
|
-
if ($DryRun) {
|
|
430
|
-
Write-Step "[DRY-RUN] Would create link: $linkName -> $BinaryName"
|
|
431
|
-
} else {
|
|
432
|
-
$null = New-PlatformLink -LinkPath $linkPath -TargetName $BinaryName -DisplayName $linkName
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
# 2) b4 -> browser4-cli-<platform> (only if b4 is our tool or doesn't exist)
|
|
436
|
-
$shortName = "b4$ext"
|
|
437
|
-
$shortPath = Join-Path $InstallDir $shortName
|
|
438
|
-
|
|
439
|
-
$b4Exists = $false
|
|
440
|
-
$b4IsOurs = $false
|
|
441
|
-
|
|
442
|
-
# Check in install dir first --anything here is ours
|
|
443
|
-
if (Test-Path $shortPath) {
|
|
444
|
-
$b4Exists = $true
|
|
445
|
-
$b4IsOurs = $true
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
# Also check b4.cmd wrapper on Windows
|
|
449
|
-
if ($script:OSWin -and -not $b4Exists) {
|
|
450
|
-
$shortCmdPath = Join-Path $InstallDir "b4.cmd"
|
|
451
|
-
if (Test-Path $shortCmdPath) {
|
|
452
|
-
$b4Exists = $true
|
|
453
|
-
$b4IsOurs = $true
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
# Check if b4 is on PATH from somewhere else.
|
|
458
|
-
# Try Get-Command first; fall back to scanning PATH directories manually
|
|
459
|
-
# because Get-Command may miss extensionless executables on Linux/macOS
|
|
460
|
-
# or may cache stale results in some PowerShell versions.
|
|
461
|
-
if (-not $b4Exists) {
|
|
462
|
-
$existingCmd = Get-Command b4 -ErrorAction SilentlyContinue
|
|
463
|
-
if ($existingCmd) {
|
|
464
|
-
$b4Exists = $true
|
|
465
|
-
# Check if it's ours by running --version
|
|
466
|
-
try {
|
|
467
|
-
$b4Version = & b4 --version 2>&1
|
|
468
|
-
if ($b4Version -match "browser4-cli") {
|
|
469
|
-
$b4IsOurs = $true
|
|
470
|
-
}
|
|
471
|
-
} catch {
|
|
472
|
-
# Can't determine --leave it alone
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
# Fallback: scan PATH manually when Get-Command didn't find anything.
|
|
478
|
-
# On Linux/macOS, PowerShell's Get-Command may miss scripts that are
|
|
479
|
-
# executable but not in its command cache, especially in CI containers.
|
|
480
|
-
if (-not $b4Exists) {
|
|
481
|
-
$separator = [System.IO.Path]::PathSeparator
|
|
482
|
-
$pathDirs = @($env:Path -split $separator | Where-Object { $_ })
|
|
483
|
-
foreach ($dir in $pathDirs) {
|
|
484
|
-
$candidatePath = Join-Path $dir $shortName
|
|
485
|
-
if (Test-Path $candidatePath -PathType Leaf) {
|
|
486
|
-
$b4Exists = $true
|
|
487
|
-
try {
|
|
488
|
-
$b4Version = & $candidatePath --version 2>&1
|
|
489
|
-
if ($b4Version -match "browser4-cli") {
|
|
490
|
-
$b4IsOurs = $true
|
|
491
|
-
}
|
|
492
|
-
} catch {
|
|
493
|
-
# Can't determine --leave it alone
|
|
494
|
-
}
|
|
495
|
-
break
|
|
496
|
-
}
|
|
497
|
-
# On Windows, also check for b4.cmd / b4.bat wrappers
|
|
498
|
-
if ($script:OSWin) {
|
|
499
|
-
foreach ($pext in @('.cmd', '.bat')) {
|
|
500
|
-
$candidateWin = Join-Path $dir "b4$pext"
|
|
501
|
-
if (Test-Path $candidateWin -PathType Leaf) {
|
|
502
|
-
$b4Exists = $true
|
|
503
|
-
try {
|
|
504
|
-
$b4Version = & $candidateWin --version 2>&1
|
|
505
|
-
if ($b4Version -match "browser4-cli") {
|
|
506
|
-
$b4IsOurs = $true
|
|
507
|
-
}
|
|
508
|
-
} catch { }
|
|
509
|
-
break
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
if ($b4Exists) { break }
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
if ($b4Exists -and -not $b4IsOurs) {
|
|
518
|
-
Write-WarnMsg "Skipping short link '$shortName': 'b4' is not browser4-cli"
|
|
519
|
-
return
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
if ($DryRun) {
|
|
523
|
-
$action = if ($b4Exists) { "Would update link" } else { "Would create link" }
|
|
524
|
-
Write-Step "[DRY-RUN] $action`: $shortName -> $BinaryName"
|
|
525
|
-
} else {
|
|
526
|
-
$null = New-PlatformLink -LinkPath $shortPath -TargetName $BinaryName -DisplayName $shortName
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
# ----------------------------------------------
|
|
531
|
-
# Helper: replace a binary that may be locked (i.e. currently running)
|
|
532
|
-
# ----------------------------------------------
|
|
533
|
-
|
|
534
|
-
function Set-BinaryFile {
|
|
535
|
-
param(
|
|
536
|
-
[Parameter(Mandatory=$true)] [string]$TargetPath,
|
|
537
|
-
[Parameter(Mandatory=$true)] [string]$SourcePath,
|
|
538
|
-
[Parameter(Mandatory=$false)] [switch]$Move # $true = move temp file, $false = copy
|
|
539
|
-
)
|
|
540
|
-
|
|
541
|
-
if (Test-Path $TargetPath) {
|
|
542
|
-
# Remove any stale .old from a previous upgrade
|
|
543
|
-
$oldPath = "$TargetPath.old"
|
|
544
|
-
try {
|
|
545
|
-
if (Test-Path $oldPath) { Remove-Item $oldPath -Force -ErrorAction Stop }
|
|
546
|
-
} catch { }
|
|
547
|
-
|
|
548
|
-
try {
|
|
549
|
-
Remove-Item $TargetPath -Force -ErrorAction Stop
|
|
550
|
-
} catch {
|
|
551
|
-
# The binary is locked (likely the currently-running process).
|
|
552
|
-
# On Windows we can rename a running executable -- move the old binary
|
|
553
|
-
# out of the way, place the new one alongside, and clean up the old one
|
|
554
|
-
# on the next install/upgrade.
|
|
555
|
-
try {
|
|
556
|
-
Move-Item $TargetPath $oldPath -Force -ErrorAction Stop
|
|
557
|
-
Write-WarnMsg "Existing binary is locked (it may be running)."
|
|
558
|
-
Write-WarnMsg "The old copy will be cleaned up on the next install or upgrade."
|
|
559
|
-
} catch {
|
|
560
|
-
throw "Cannot replace '$TargetPath' -- it is locked and cannot be renamed. Close all browser4-cli processes and try again."
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
# Place the new binary at the target path
|
|
566
|
-
if ($Move) {
|
|
567
|
-
Move-Item $SourcePath $TargetPath -Force -ErrorAction Stop
|
|
568
|
-
} else {
|
|
569
|
-
Copy-Item $SourcePath $TargetPath -Force -ErrorAction Stop
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
# ----------------------------------------------
|
|
574
|
-
# PATH management
|
|
575
|
-
# ----------------------------------------------
|
|
576
|
-
|
|
577
|
-
function Add-DirectoryToUserPath {
|
|
578
|
-
param([string]$Dir)
|
|
579
|
-
|
|
580
|
-
$dirResolved = (Resolve-Path $Dir -ErrorAction SilentlyContinue).Path
|
|
581
|
-
if (-not $dirResolved) { $dirResolved = $Dir }
|
|
582
|
-
|
|
583
|
-
# Read current user PATH
|
|
584
|
-
$currentUserPath = [System.Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::User)
|
|
585
|
-
$paths = if ($currentUserPath) { $currentUserPath -split ";" | Where-Object { $_ } } else { @() }
|
|
586
|
-
|
|
587
|
-
# Check if already present
|
|
588
|
-
$normalized = $paths | ForEach-Object { $rp = Resolve-Path $_ -ErrorAction SilentlyContinue; if ($rp) { $rp.Path } else { $_ } }
|
|
589
|
-
if ($normalized -contains $dirResolved) {
|
|
590
|
-
Write-Check "Already in user PATH: $Dir"
|
|
591
|
-
return
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
if ($DryRun) {
|
|
595
|
-
Write-Check "[DRY-RUN] Would add to user PATH: $Dir"
|
|
596
|
-
return
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
$newPath = if ($currentUserPath) { "$currentUserPath;$Dir" } else { $Dir }
|
|
600
|
-
[System.Environment]::SetEnvironmentVariable("Path", $newPath, [System.EnvironmentVariableTarget]::User)
|
|
601
|
-
Write-Check "Added to user PATH: $Dir"
|
|
602
|
-
|
|
603
|
-
# Also update current session
|
|
604
|
-
$env:Path = "$env:Path;$Dir"
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
# ----------------------------------------------
|
|
608
|
-
# Main
|
|
609
|
-
# ----------------------------------------------
|
|
610
|
-
|
|
611
|
-
function Main {
|
|
612
|
-
Write-Summary "==========================================" -Color Cyan
|
|
613
|
-
Write-Summary " browser4-cli Installer" -Color Cyan
|
|
614
|
-
Write-Summary "==========================================" -Color Cyan
|
|
615
|
-
Write-Summary ""
|
|
616
|
-
|
|
617
|
-
# Auto-detect China mainland locale when no explicit source is given
|
|
618
|
-
if (-not $Source) {
|
|
619
|
-
$script:ChinaDetected = Test-ChinaLocale
|
|
620
|
-
if ($script:ChinaDetected) {
|
|
621
|
-
Write-Step "China mainland locale detected: preferring Aliyun OSS mirror."
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
# Detect platform
|
|
626
|
-
$platformKey = Get-PlatformKey
|
|
627
|
-
$binaryName = Get-BinaryName -PlatformKey $platformKey
|
|
628
|
-
|
|
629
|
-
# -- Locate mode: print diagnostics and exit --
|
|
630
|
-
if ($Locate) {
|
|
631
|
-
Write-Summary "--- Locate / diagnostics ---" -Color Cyan
|
|
632
|
-
Write-Summary ""
|
|
633
|
-
Write-Step "Script dir: $ScriptDir"
|
|
634
|
-
Write-Step "Platform key: $platformKey"
|
|
635
|
-
Write-Step "Binary name: $binaryName"
|
|
636
|
-
Write-Step "Default install: $(Get-DefaultInstallDir)"
|
|
637
|
-
Write-Step "China locale: $script:ChinaDetected"
|
|
638
|
-
Write-Step "Source override: $(if ($Source) { $Source } else { 'auto' })"
|
|
639
|
-
Write-Step "OS: $(if ($script:OSWin) { 'Windows' } elseif ($script:OSMac) { 'macOS' } elseif ($script:OSLinux) { 'Linux' } else { 'Unknown' })"
|
|
640
|
-
|
|
641
|
-
# Check for local binary
|
|
642
|
-
$localPath = Find-LocalBinary -BinaryName $binaryName
|
|
643
|
-
if ($localPath) {
|
|
644
|
-
$localOk = Test-LocalBinary -Path $localPath
|
|
645
|
-
Write-Step "Local binary: $localPath $(if ($localOk) { '(valid)' } else { '(present but --version failed)' })"
|
|
646
|
-
} else {
|
|
647
|
-
Write-Step "Local binary: not found alongside script"
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
# Check for already-installed binary
|
|
651
|
-
$defaultDir = Get-DefaultInstallDir
|
|
652
|
-
$existingPath = Join-Path $defaultDir $binaryName
|
|
653
|
-
if (Test-Path $existingPath) {
|
|
654
|
-
Write-Step "Already installed: $existingPath"
|
|
655
|
-
} else {
|
|
656
|
-
Write-Step "Already installed: not found at $defaultDir"
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
# Show download URLs that would be tried
|
|
660
|
-
$urls = Get-DownloadUrls -BinaryName $binaryName -VersionTag $Version
|
|
661
|
-
Write-Summary ""
|
|
662
|
-
Write-Step "Download order:"
|
|
663
|
-
foreach ($entry in $urls) {
|
|
664
|
-
Write-Step " $($entry.Label): $($entry.Url)"
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
Write-Summary ""
|
|
668
|
-
return
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
Write-Step "Platform: $platformKey"
|
|
672
|
-
Write-Step "Binary: $binaryName"
|
|
673
|
-
|
|
674
|
-
# Determine install directory
|
|
675
|
-
$installDir = if ($InstallDir) { $InstallDir } else { Get-DefaultInstallDir }
|
|
676
|
-
Write-Step "Install: $installDir"
|
|
677
|
-
Write-Summary ""
|
|
678
|
-
|
|
679
|
-
if (-not (Test-Path $installDir)) {
|
|
680
|
-
if (-not $DryRun) {
|
|
681
|
-
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
|
|
682
|
-
}
|
|
683
|
-
Write-Step "Created directory: $installDir"
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
$binaryPath = Join-Path $installDir $binaryName
|
|
687
|
-
|
|
688
|
-
# -- Local binary discovery (bundled/sideload) --
|
|
689
|
-
$useLocalBinary = $false
|
|
690
|
-
if (-not $SkipLocal) {
|
|
691
|
-
$localBinaryPath = Find-LocalBinary -BinaryName $binaryName
|
|
692
|
-
if ($localBinaryPath) {
|
|
693
|
-
Write-Step "Found local binary alongside script: $(Split-Path $localBinaryPath -Leaf)"
|
|
694
|
-
if (Test-LocalBinary -Path $localBinaryPath) {
|
|
695
|
-
Write-Check "Local binary verified (--version OK)"
|
|
696
|
-
$useLocalBinary = $true
|
|
697
|
-
} else {
|
|
698
|
-
Write-WarnMsg "Local binary found but --version check failed -- will download instead"
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
} elseif ($SkipLocal) {
|
|
702
|
-
Write-Step "Skipping local binary check (-SkipLocal)"
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
# Skip download only when --skip-if-installed is set and binary already exists,
|
|
706
|
-
# unless --force overrides it.
|
|
707
|
-
if ((Test-Path $binaryPath) -and (-not $Version) -and $SkipIfInstalled -and (-not $Force) -and (-not $useLocalBinary)) {
|
|
708
|
-
Write-Check "Binary already installed: $binaryPath"
|
|
709
|
-
} elseif ($useLocalBinary) {
|
|
710
|
-
# Copy local binary to install dir
|
|
711
|
-
if (-not $DryRun) {
|
|
712
|
-
Set-BinaryFile -TargetPath $binaryPath -SourcePath $localBinaryPath
|
|
713
|
-
}
|
|
714
|
-
Write-Check "Installed (local): $binaryPath"
|
|
715
|
-
} else {
|
|
716
|
-
# Build download URLs
|
|
717
|
-
$urls = Get-DownloadUrls -BinaryName $binaryName -VersionTag $Version
|
|
718
|
-
if (-not $urls -or $urls.Count -eq 0) {
|
|
719
|
-
throw "No download URLs configured"
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
$downloaded = $false
|
|
723
|
-
$tempFile = [System.IO.Path]::GetTempFileName()
|
|
724
|
-
|
|
725
|
-
foreach ($entry in $urls) {
|
|
726
|
-
if (Invoke-Download -Url $entry.Url -OutFile $tempFile -Label $entry.Label) {
|
|
727
|
-
$downloaded = $true
|
|
728
|
-
break
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
if (-not $downloaded) {
|
|
733
|
-
if (Test-Path $tempFile) { Remove-Item $tempFile -Force }
|
|
734
|
-
throw @"
|
|
735
|
-
Could not download browser4-cli binary.
|
|
736
|
-
|
|
737
|
-
Tried:
|
|
738
|
-
$($urls | ForEach-Object { " - $($_.Label): $($_.Url)" } | Out-String)
|
|
739
|
-
|
|
740
|
-
Please check:
|
|
741
|
-
- Network connectivity
|
|
742
|
-
- The version/tag exists: $Version
|
|
743
|
-
- For GitHub rate limits, set GITHUB_TOKEN environment variable
|
|
744
|
-
- If you have a local copy, place it alongside this script and re-run
|
|
745
|
-
"@
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
# Move from temp to install dir
|
|
749
|
-
if (-not $DryRun) {
|
|
750
|
-
Set-BinaryFile -TargetPath $binaryPath -SourcePath $tempFile -Move
|
|
751
|
-
}
|
|
752
|
-
Write-Check "Installed: $binaryPath"
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
# On Unix, ensure executable bit
|
|
756
|
-
if (-not $script:OSWin) {
|
|
757
|
-
if (-not $DryRun) {
|
|
758
|
-
try { chmod +x $binaryPath 2>$null } catch { }
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
# Create symlinks (browser4-cli -> platform binary, b4 if no conflict)
|
|
763
|
-
Write-Summary ""
|
|
764
|
-
New-Symlinks -BinaryName $binaryName -InstallDir $installDir -PlatformKey $platformKey
|
|
765
|
-
|
|
766
|
-
# Add to PATH
|
|
767
|
-
if ($AddToPath -and $script:OSWin) {
|
|
768
|
-
Write-Summary ""
|
|
769
|
-
Add-DirectoryToUserPath -Dir $installDir
|
|
770
|
-
} elseif ($AddToPath -and -not $script:OSWin) {
|
|
771
|
-
Write-Summary ""
|
|
772
|
-
$shellRc = if (Test-Path "$env:HOME/.zshrc") { "$env:HOME/.zshrc" } elseif (Test-Path "$env:HOME/.bashrc") { "$env:HOME/.bashrc" } elseif (Test-Path "$env:HOME/.bash_profile") { "$env:HOME/.bash_profile" } else { "$env:HOME/.profile" }
|
|
773
|
-
$pathLine = "export PATH=""$installDir`:`$PATH"""
|
|
774
|
-
if (-not $DryRun) {
|
|
775
|
-
if (-not (Select-String -Path $shellRc -Pattern [regex]::Escape($installDir) -ErrorAction SilentlyContinue)) {
|
|
776
|
-
Add-Content -Path $shellRc -Value ""
|
|
777
|
-
Add-Content -Path $shellRc -Value "# browser4-cli"
|
|
778
|
-
Add-Content -Path $shellRc -Value $pathLine
|
|
779
|
-
Write-Check "Added to PATH in $shellRc"
|
|
780
|
-
} else {
|
|
781
|
-
Write-Check "PATH entry already in $shellRc"
|
|
782
|
-
}
|
|
783
|
-
} else {
|
|
784
|
-
Write-Check "[DRY-RUN] Would add to $shellRc"
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
# Verify
|
|
789
|
-
Write-Summary ""
|
|
790
|
-
if (-not $DryRun) {
|
|
791
|
-
try {
|
|
792
|
-
$versionOutput = & $binaryPath --version 2>&1
|
|
793
|
-
Write-Summary "[v] browser4-cli installed successfully" -Color Green
|
|
794
|
-
Write-Summary " Version: $versionOutput"
|
|
795
|
-
} catch {
|
|
796
|
-
Write-Summary "[v] Binary installed at: $binaryPath" -Color Green
|
|
797
|
-
Write-WarnMsg "Could not verify --version (this is normal on first install)"
|
|
798
|
-
}
|
|
799
|
-
} else {
|
|
800
|
-
Write-Summary "[DRY-RUN] Installation plan complete" -Color Yellow
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
Write-Summary ""
|
|
804
|
-
Write-Summary "Run 'browser4-cli --help' to get started." -Color Cyan
|
|
805
|
-
|
|
806
|
-
if ($script:OSWin) {
|
|
807
|
-
Write-Summary "If the command isn't found, restart your terminal or run:"
|
|
808
|
-
Write-Summary " `$env:Path = [System.Environment]::GetEnvironmentVariable('Path','User') + ';' + [System.Environment]::GetEnvironmentVariable('Path','Machine')"
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
Main
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Install browser4-cli -- download the native binary and set it up on your PATH.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Detects your OS and CPU architecture, downloads the matching native binary from
|
|
7
|
+
GitHub Releases or Alibaba Cloud OSS, installs it to a user-local directory, and
|
|
8
|
+
optionally adds it to your PATH.
|
|
9
|
+
|
|
10
|
+
Default install location:
|
|
11
|
+
Windows: $env:LOCALAPPDATA\Programs\browser4-cli
|
|
12
|
+
(override with -InstallDir)
|
|
13
|
+
|
|
14
|
+
Download sources (auto-selected by locale; use -Source to override):
|
|
15
|
+
Outside China: 1. GitHub Releases -> 2. Aliyun OSS
|
|
16
|
+
China mainland: 1. Aliyun OSS -> 2. GitHub Releases
|
|
17
|
+
GitHub Releases -- https://github.com/platonai/Browser4
|
|
18
|
+
Aliyun OSS -- https://browser4.oss-cn-beijing.aliyuncs.com
|
|
19
|
+
|
|
20
|
+
.PARAMETER Version
|
|
21
|
+
Release version tag to download (e.g. "v4.11.0" or "v0.1.12-cli").
|
|
22
|
+
Defaults to "latest" which resolves to the most recent stable release.
|
|
23
|
+
|
|
24
|
+
.PARAMETER InstallDir
|
|
25
|
+
Directory to install the binary into.
|
|
26
|
+
Default: $env:LOCALAPPDATA\Programs\browser4-cli
|
|
27
|
+
|
|
28
|
+
.PARAMETER Source
|
|
29
|
+
Force a specific download source: "github" or "oss".
|
|
30
|
+
Default (auto): locale-aware -- OSS first in China mainland, GitHub first elsewhere.
|
|
31
|
+
|
|
32
|
+
.PARAMETER AddToPath
|
|
33
|
+
Add the install directory to the current user's PATH environment variable.
|
|
34
|
+
Default: true.
|
|
35
|
+
|
|
36
|
+
.PARAMETER Silent
|
|
37
|
+
Suppress all non-error output.
|
|
38
|
+
|
|
39
|
+
.PARAMETER DryRun
|
|
40
|
+
Print what would be done without actually doing it.
|
|
41
|
+
|
|
42
|
+
.PARAMETER SkipIfInstalled
|
|
43
|
+
Skip download if the binary already exists at the install path.
|
|
44
|
+
By default the script always downloads the latest version.
|
|
45
|
+
|
|
46
|
+
.PARAMETER SkipLocal
|
|
47
|
+
Skip checking for a locally-bundled binary alongside the script.
|
|
48
|
+
By default the script looks for the platform binary in its own directory
|
|
49
|
+
before downloading -- use this to force a fresh download.
|
|
50
|
+
|
|
51
|
+
.PARAMETER Force
|
|
52
|
+
Force reinstallation even if the binary is already installed at the target path.
|
|
53
|
+
Overrides -SkipIfInstalled and bypasses locked-file workarounds.
|
|
54
|
+
|
|
55
|
+
.PARAMETER Locate
|
|
56
|
+
Print detection results (OS, architecture, script location, China locale)
|
|
57
|
+
and exit without installing. Useful for diagnostics.
|
|
58
|
+
|
|
59
|
+
.EXAMPLE
|
|
60
|
+
# Quick install -- default location, latest version, add to PATH
|
|
61
|
+
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1
|
|
62
|
+
|
|
63
|
+
.EXAMPLE
|
|
64
|
+
# Silent install with a specific version
|
|
65
|
+
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1 -Version "v4.11.0" -Silent
|
|
66
|
+
|
|
67
|
+
.EXAMPLE
|
|
68
|
+
# Install from OSS only, custom directory
|
|
69
|
+
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1 -Source oss -InstallDir "C:\tools\browser4"
|
|
70
|
+
|
|
71
|
+
.EXAMPLE
|
|
72
|
+
# Run diagnostics -- see what the script detects without installing
|
|
73
|
+
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1 -Locate
|
|
74
|
+
|
|
75
|
+
.EXAMPLE
|
|
76
|
+
# Use a locally-bundled binary (place binary next to the script)
|
|
77
|
+
# The script auto-detects binaries in its own directory
|
|
78
|
+
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1
|
|
79
|
+
|
|
80
|
+
.EXAMPLE
|
|
81
|
+
# Force download even when a local binary exists
|
|
82
|
+
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1 -SkipLocal
|
|
83
|
+
|
|
84
|
+
.EXAMPLE
|
|
85
|
+
# Skip download if already installed (opt out of default reinstall)
|
|
86
|
+
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1 -SkipIfInstalled
|
|
87
|
+
|
|
88
|
+
.EXAMPLE
|
|
89
|
+
# For China mainland: OSS is auto-preferred via locale detection,
|
|
90
|
+
# or force it explicitly
|
|
91
|
+
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1 -Source oss
|
|
92
|
+
#>
|
|
93
|
+
|
|
94
|
+
[CmdletBinding()]
|
|
95
|
+
param(
|
|
96
|
+
[string]$Version = "",
|
|
97
|
+
[string]$InstallDir = "",
|
|
98
|
+
[ValidateSet("", "github", "oss")]
|
|
99
|
+
[string]$Source = "",
|
|
100
|
+
[bool]$AddToPath = $true,
|
|
101
|
+
[switch]$Silent,
|
|
102
|
+
[switch]$DryRun,
|
|
103
|
+
[switch]$SkipIfInstalled,
|
|
104
|
+
[switch]$SkipLocal,
|
|
105
|
+
[switch]$Force,
|
|
106
|
+
[switch]$Locate
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
$ErrorActionPreference = "Stop"
|
|
110
|
+
|
|
111
|
+
# ----------------------------------------------
|
|
112
|
+
# Script location -- find ourselves on disk
|
|
113
|
+
# ----------------------------------------------
|
|
114
|
+
|
|
115
|
+
# $PSScriptRoot is the directory containing this script (PS 3+).
|
|
116
|
+
# Falls back to $MyInvocation for edge cases (dot-sourced, PS 2).
|
|
117
|
+
$ScriptDir = if ($PSScriptRoot) {
|
|
118
|
+
$PSScriptRoot
|
|
119
|
+
} elseif ($MyInvocation -and $MyInvocation.MyCommand.Path) {
|
|
120
|
+
Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
121
|
+
} else {
|
|
122
|
+
$null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
<#
|
|
126
|
+
.SYNOPSIS
|
|
127
|
+
Search for a pre-downloaded binary near the script (bundled/sideload install).
|
|
128
|
+
Returns the full path if found, $null otherwise.
|
|
129
|
+
#>
|
|
130
|
+
function Find-LocalBinary {
|
|
131
|
+
param([string]$BinaryName)
|
|
132
|
+
|
|
133
|
+
if (-not $ScriptDir) { return $null }
|
|
134
|
+
|
|
135
|
+
$localPath = Join-Path $ScriptDir $BinaryName
|
|
136
|
+
if (Test-Path $localPath -PathType Leaf) {
|
|
137
|
+
$size = (Get-Item $localPath).Length
|
|
138
|
+
if ($size -gt 102400) { # > 100 KB minimum
|
|
139
|
+
return $localPath
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return $null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
<#
|
|
146
|
+
.SYNOPSIS
|
|
147
|
+
Check whether a local binary is usable by querying its version.
|
|
148
|
+
Returns $true if --version executes successfully, $false otherwise.
|
|
149
|
+
#>
|
|
150
|
+
function Test-LocalBinary {
|
|
151
|
+
param([string]$Path)
|
|
152
|
+
|
|
153
|
+
if (-not $Path -or -not (Test-Path $Path)) { return $false }
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
$null = & $Path --version 2>&1
|
|
157
|
+
return ($LASTEXITCODE -eq 0)
|
|
158
|
+
} catch {
|
|
159
|
+
return $false
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# ----------------------------------------------
|
|
164
|
+
# OS detection (compatible with PS 5.1+)
|
|
165
|
+
# ----------------------------------------------
|
|
166
|
+
|
|
167
|
+
# Avoid assigning to $IsLinux / $IsWindows / $IsMacOS directly --
|
|
168
|
+
# they are read-only automatic variables in PowerShell 7+.
|
|
169
|
+
if ($PSVersionTable.PSVersion.Major -ge 6) {
|
|
170
|
+
$script:OSWin = $IsWindows
|
|
171
|
+
$script:OSLinux = $IsLinux
|
|
172
|
+
$script:OSMac = $IsMacOS
|
|
173
|
+
} else {
|
|
174
|
+
$script:OSWin = [System.Environment]::OSVersion.Platform -eq "Win32NT"
|
|
175
|
+
$script:OSMac = $false
|
|
176
|
+
$script:OSLinux = $false
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# ----------------------------------------------
|
|
180
|
+
# Helpers
|
|
181
|
+
# ----------------------------------------------
|
|
182
|
+
|
|
183
|
+
function Write-Summary {
|
|
184
|
+
param([string]$Message, [string]$Color = "White")
|
|
185
|
+
if (-not $Silent) { Write-Host $Message -ForegroundColor $Color }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function Write-Step {
|
|
189
|
+
param([string]$Message)
|
|
190
|
+
if (-not $Silent) { Write-Host " >> $Message" -ForegroundColor Gray }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function Write-Check {
|
|
194
|
+
param([string]$Message)
|
|
195
|
+
if (-not $Silent) { Write-Host " [v] $Message" -ForegroundColor Green }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function Write-WarnMsg {
|
|
199
|
+
param([string]$Message)
|
|
200
|
+
if (-not $Silent) { Write-Host " [!] $Message" -ForegroundColor Yellow }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# ----------------------------------------------
|
|
204
|
+
# Detection
|
|
205
|
+
# ----------------------------------------------
|
|
206
|
+
|
|
207
|
+
function Get-PlatformKey {
|
|
208
|
+
$arch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { "arm64" } else { "x64" }
|
|
209
|
+
|
|
210
|
+
if ($script:OSWin) {
|
|
211
|
+
return "win32-$arch"
|
|
212
|
+
}
|
|
213
|
+
elseif ($script:OSMac) {
|
|
214
|
+
return "darwin-$arch"
|
|
215
|
+
}
|
|
216
|
+
elseif ($script:OSLinux) {
|
|
217
|
+
# Detect musl
|
|
218
|
+
$isMusl = $false
|
|
219
|
+
try {
|
|
220
|
+
$lddOutput = ldd --version 2>&1
|
|
221
|
+
if ($lddOutput -match "musl") { $isMusl = $true }
|
|
222
|
+
} catch {
|
|
223
|
+
if ((Test-Path "/lib/ld-musl-x86_64.so.1") -or (Test-Path "/lib/ld-musl-aarch64.so.1")) {
|
|
224
|
+
$isMusl = $true
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
$libc = if ($isMusl) { "musl" } else { "" }
|
|
228
|
+
if ($libc) { return "linux-$libc-$arch" } else { return "linux-$arch" }
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
throw "Unsupported OS. Browser4 CLI supports Windows, macOS, and Linux."
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function Get-BinaryName {
|
|
236
|
+
param([string]$PlatformKey)
|
|
237
|
+
return "browser4-cli-$PlatformKey" + $(if ($PlatformKey.StartsWith("win32")) { ".exe" } else { "" })
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function Get-DefaultInstallDir {
|
|
241
|
+
if ($script:OSWin) {
|
|
242
|
+
return Join-Path $env:LOCALAPPDATA "Programs\browser4-cli"
|
|
243
|
+
}
|
|
244
|
+
elseif ($script:OSLinux -or $script:OSMac) {
|
|
245
|
+
# Prefer ~/.local/bin for user installs
|
|
246
|
+
if (Test-Path "$env:HOME/.local/bin") {
|
|
247
|
+
return "$env:HOME/.local/bin"
|
|
248
|
+
}
|
|
249
|
+
return "$env:HOME/.local/bin"
|
|
250
|
+
}
|
|
251
|
+
throw "Unsupported OS"
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# ----------------------------------------------
|
|
255
|
+
# China mainland locale detection (zero-network)
|
|
256
|
+
# ----------------------------------------------
|
|
257
|
+
|
|
258
|
+
<#
|
|
259
|
+
.SYNOPSIS
|
|
260
|
+
Detect whether the current system is likely in China mainland.
|
|
261
|
+
Uses only local env vars and .NET APIs -- no network calls.
|
|
262
|
+
#>
|
|
263
|
+
function Test-ChinaLocale {
|
|
264
|
+
# 1 -- Locale env vars
|
|
265
|
+
$lang = $env:LC_ALL, $env:LANG, $env:LC_CTYPE, $env:LC_MESSAGES | Where-Object { $_ } | Select-Object -First 1
|
|
266
|
+
if ($lang -and ($lang -match '^zh_CN' -or $lang -match '^zh-CN' -or $lang -match '^Chinese \(Simplified\)_China')) {
|
|
267
|
+
return $true
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# 2 -- TZ env var
|
|
271
|
+
$tzEnv = $env:TZ
|
|
272
|
+
if ($tzEnv -and ($tzEnv -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$')) {
|
|
273
|
+
return $true
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
# 3 -- .NET TimeZoneInfo (works on Windows and Unix PowerShell 7+)
|
|
277
|
+
try {
|
|
278
|
+
$tzId = [System.TimeZoneInfo]::Local.Id
|
|
279
|
+
if ($tzId -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$') {
|
|
280
|
+
return $true
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
# TimeZoneInfo not available (unlikely on PS 5.1+ but guard anyway)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# 4 -- /etc/timezone (PowerShell on Linux/macOS)
|
|
287
|
+
if (-not $script:OSWin -and (Test-Path '/etc/timezone')) {
|
|
288
|
+
try {
|
|
289
|
+
$tz = Get-Content '/etc/timezone' -Raw -ErrorAction Stop
|
|
290
|
+
if ($tz -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$') {
|
|
291
|
+
return $true
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
# Permission or read error -- skip
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return $false
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
# ----------------------------------------------
|
|
302
|
+
# Download URLs
|
|
303
|
+
# ----------------------------------------------
|
|
304
|
+
|
|
305
|
+
$GITHUB_REPO = "platonai/Browser4"
|
|
306
|
+
$OSS_BASE = "https://browser4.oss-cn-beijing.aliyuncs.com"
|
|
307
|
+
$script:ChinaDetected = $false
|
|
308
|
+
|
|
309
|
+
function Get-DownloadUrls {
|
|
310
|
+
param([string]$BinaryName, [string]$VersionTag)
|
|
311
|
+
|
|
312
|
+
$urls = @()
|
|
313
|
+
|
|
314
|
+
$ghBase = "https://github.com/$GITHUB_REPO/releases/download"
|
|
315
|
+
if ($VersionTag) {
|
|
316
|
+
$ghUrl = "$ghBase/$VersionTag/$BinaryName"
|
|
317
|
+
$ossUrl = "$OSS_BASE/releases/download/$VersionTag/$BinaryName"
|
|
318
|
+
} else {
|
|
319
|
+
# Use 'latest' redirect for GitHub, 'latest' symlink for OSS
|
|
320
|
+
$ghUrl = "https://github.com/$GITHUB_REPO/releases/latest/download/$BinaryName"
|
|
321
|
+
$ossUrl = "$OSS_BASE/releases/download/latest/$BinaryName"
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if ($Source -eq "github") {
|
|
325
|
+
$urls += @{ Url = $ghUrl; Label = "GitHub Releases" }
|
|
326
|
+
} elseif ($Source -eq "oss") {
|
|
327
|
+
$urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
|
|
328
|
+
} else {
|
|
329
|
+
if ($script:ChinaDetected) {
|
|
330
|
+
$urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
|
|
331
|
+
$urls += @{ Url = $ghUrl; Label = "GitHub Releases" }
|
|
332
|
+
} else {
|
|
333
|
+
$urls += @{ Url = $ghUrl; Label = "GitHub Releases" }
|
|
334
|
+
$urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return $urls
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function Invoke-Download {
|
|
342
|
+
param([string]$Url, [string]$OutFile, [string]$Label)
|
|
343
|
+
|
|
344
|
+
Write-Step "Trying $Label..."
|
|
345
|
+
Write-Step "URL: $Url"
|
|
346
|
+
|
|
347
|
+
if ($DryRun) {
|
|
348
|
+
Write-Check "[DRY-RUN] Would download to: $OutFile"
|
|
349
|
+
return $true
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
$ProgressPreference = if ($Silent) { "SilentlyContinue" } else { "Continue" }
|
|
354
|
+
|
|
355
|
+
# Use Invoke-WebRequest with progress bar
|
|
356
|
+
Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing -ErrorAction Stop
|
|
357
|
+
|
|
358
|
+
if (Test-Path $OutFile) {
|
|
359
|
+
$size = (Get-Item $OutFile).Length
|
|
360
|
+
if ($size -gt 102400) { # > 100 KB minimum
|
|
361
|
+
Write-Check "Downloaded $( [math]::Round($size / 1MB, 1) ) MB"
|
|
362
|
+
return $true
|
|
363
|
+
} else {
|
|
364
|
+
Write-WarnMsg "Downloaded file too small ($size bytes) -- may be an error page"
|
|
365
|
+
Remove-Item $OutFile -Force -ErrorAction SilentlyContinue
|
|
366
|
+
return $false
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
Write-WarnMsg "Download appeared to succeed but file not found"
|
|
370
|
+
return $false
|
|
371
|
+
} catch {
|
|
372
|
+
Write-WarnMsg "Failed: $($_.Exception.Message)"
|
|
373
|
+
return $false
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
# ----------------------------------------------
|
|
378
|
+
# Symlinks
|
|
379
|
+
# ----------------------------------------------
|
|
380
|
+
|
|
381
|
+
function New-PlatformLink {
|
|
382
|
+
param([string]$LinkPath, [string]$TargetName, [string]$DisplayName)
|
|
383
|
+
|
|
384
|
+
# Try symbolic link first (works on Unix; on Windows needs Admin or Developer Mode)
|
|
385
|
+
try {
|
|
386
|
+
New-Item -ItemType SymbolicLink -Path $LinkPath -Target $TargetName -Force -ErrorAction Stop | Out-Null
|
|
387
|
+
Write-Check "Created symlink: $DisplayName -> $TargetName"
|
|
388
|
+
return $true
|
|
389
|
+
} catch {
|
|
390
|
+
# Symbolic link failed -- try hard link on Windows
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
# Try hard link (Windows, same volume)
|
|
394
|
+
if ($script:OSWin) {
|
|
395
|
+
try {
|
|
396
|
+
$targetPath = Join-Path (Split-Path $LinkPath -Parent) $TargetName
|
|
397
|
+
New-Item -ItemType HardLink -Path $LinkPath -Target $targetPath -Force -ErrorAction Stop | Out-Null
|
|
398
|
+
Write-Check "Created hard link: $DisplayName -> $TargetName"
|
|
399
|
+
return $true
|
|
400
|
+
} catch {
|
|
401
|
+
# Hard link also failed -- create a .cmd wrapper as last resort
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
# Last resort: .cmd wrapper that forwards all arguments
|
|
405
|
+
try {
|
|
406
|
+
$wrapperContent = '@"%~dp0' + $TargetName + '" %*'
|
|
407
|
+
Set-Content -Path ($LinkPath -replace '\.exe$', '.cmd') -Value $wrapperContent -Force -ErrorAction Stop
|
|
408
|
+
Write-Check "Created wrapper: " + (Split-Path ($LinkPath -replace '\.exe$', '.cmd') -Leaf) + " -> $TargetName"
|
|
409
|
+
return $true
|
|
410
|
+
} catch {
|
|
411
|
+
Write-WarnMsg "Could not create link for $DisplayName (may need admin privileges)"
|
|
412
|
+
return $false
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
Write-WarnMsg "Could not create link for $DisplayName"
|
|
417
|
+
return $false
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function New-Symlinks {
|
|
421
|
+
param([string]$BinaryName, [string]$InstallDir, [string]$PlatformKey)
|
|
422
|
+
|
|
423
|
+
$ext = if ($PlatformKey.StartsWith("win32")) { ".exe" } else { "" }
|
|
424
|
+
|
|
425
|
+
# 1) Always: browser4-cli -> browser4-cli-<platform>
|
|
426
|
+
$linkName = "browser4-cli$ext"
|
|
427
|
+
$linkPath = Join-Path $InstallDir $linkName
|
|
428
|
+
|
|
429
|
+
if ($DryRun) {
|
|
430
|
+
Write-Step "[DRY-RUN] Would create link: $linkName -> $BinaryName"
|
|
431
|
+
} else {
|
|
432
|
+
$null = New-PlatformLink -LinkPath $linkPath -TargetName $BinaryName -DisplayName $linkName
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
# 2) b4 -> browser4-cli-<platform> (only if b4 is our tool or doesn't exist)
|
|
436
|
+
$shortName = "b4$ext"
|
|
437
|
+
$shortPath = Join-Path $InstallDir $shortName
|
|
438
|
+
|
|
439
|
+
$b4Exists = $false
|
|
440
|
+
$b4IsOurs = $false
|
|
441
|
+
|
|
442
|
+
# Check in install dir first --anything here is ours
|
|
443
|
+
if (Test-Path $shortPath) {
|
|
444
|
+
$b4Exists = $true
|
|
445
|
+
$b4IsOurs = $true
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
# Also check b4.cmd wrapper on Windows
|
|
449
|
+
if ($script:OSWin -and -not $b4Exists) {
|
|
450
|
+
$shortCmdPath = Join-Path $InstallDir "b4.cmd"
|
|
451
|
+
if (Test-Path $shortCmdPath) {
|
|
452
|
+
$b4Exists = $true
|
|
453
|
+
$b4IsOurs = $true
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
# Check if b4 is on PATH from somewhere else.
|
|
458
|
+
# Try Get-Command first; fall back to scanning PATH directories manually
|
|
459
|
+
# because Get-Command may miss extensionless executables on Linux/macOS
|
|
460
|
+
# or may cache stale results in some PowerShell versions.
|
|
461
|
+
if (-not $b4Exists) {
|
|
462
|
+
$existingCmd = Get-Command b4 -ErrorAction SilentlyContinue
|
|
463
|
+
if ($existingCmd) {
|
|
464
|
+
$b4Exists = $true
|
|
465
|
+
# Check if it's ours by running --version
|
|
466
|
+
try {
|
|
467
|
+
$b4Version = & b4 --version 2>&1
|
|
468
|
+
if ($b4Version -match "browser4-cli") {
|
|
469
|
+
$b4IsOurs = $true
|
|
470
|
+
}
|
|
471
|
+
} catch {
|
|
472
|
+
# Can't determine --leave it alone
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
# Fallback: scan PATH manually when Get-Command didn't find anything.
|
|
478
|
+
# On Linux/macOS, PowerShell's Get-Command may miss scripts that are
|
|
479
|
+
# executable but not in its command cache, especially in CI containers.
|
|
480
|
+
if (-not $b4Exists) {
|
|
481
|
+
$separator = [System.IO.Path]::PathSeparator
|
|
482
|
+
$pathDirs = @($env:Path -split $separator | Where-Object { $_ })
|
|
483
|
+
foreach ($dir in $pathDirs) {
|
|
484
|
+
$candidatePath = Join-Path $dir $shortName
|
|
485
|
+
if (Test-Path $candidatePath -PathType Leaf) {
|
|
486
|
+
$b4Exists = $true
|
|
487
|
+
try {
|
|
488
|
+
$b4Version = & $candidatePath --version 2>&1
|
|
489
|
+
if ($b4Version -match "browser4-cli") {
|
|
490
|
+
$b4IsOurs = $true
|
|
491
|
+
}
|
|
492
|
+
} catch {
|
|
493
|
+
# Can't determine --leave it alone
|
|
494
|
+
}
|
|
495
|
+
break
|
|
496
|
+
}
|
|
497
|
+
# On Windows, also check for b4.cmd / b4.bat wrappers
|
|
498
|
+
if ($script:OSWin) {
|
|
499
|
+
foreach ($pext in @('.cmd', '.bat')) {
|
|
500
|
+
$candidateWin = Join-Path $dir "b4$pext"
|
|
501
|
+
if (Test-Path $candidateWin -PathType Leaf) {
|
|
502
|
+
$b4Exists = $true
|
|
503
|
+
try {
|
|
504
|
+
$b4Version = & $candidateWin --version 2>&1
|
|
505
|
+
if ($b4Version -match "browser4-cli") {
|
|
506
|
+
$b4IsOurs = $true
|
|
507
|
+
}
|
|
508
|
+
} catch { }
|
|
509
|
+
break
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if ($b4Exists) { break }
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if ($b4Exists -and -not $b4IsOurs) {
|
|
518
|
+
Write-WarnMsg "Skipping short link '$shortName': 'b4' is not browser4-cli"
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if ($DryRun) {
|
|
523
|
+
$action = if ($b4Exists) { "Would update link" } else { "Would create link" }
|
|
524
|
+
Write-Step "[DRY-RUN] $action`: $shortName -> $BinaryName"
|
|
525
|
+
} else {
|
|
526
|
+
$null = New-PlatformLink -LinkPath $shortPath -TargetName $BinaryName -DisplayName $shortName
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
# ----------------------------------------------
|
|
531
|
+
# Helper: replace a binary that may be locked (i.e. currently running)
|
|
532
|
+
# ----------------------------------------------
|
|
533
|
+
|
|
534
|
+
function Set-BinaryFile {
|
|
535
|
+
param(
|
|
536
|
+
[Parameter(Mandatory=$true)] [string]$TargetPath,
|
|
537
|
+
[Parameter(Mandatory=$true)] [string]$SourcePath,
|
|
538
|
+
[Parameter(Mandatory=$false)] [switch]$Move # $true = move temp file, $false = copy
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
if (Test-Path $TargetPath) {
|
|
542
|
+
# Remove any stale .old from a previous upgrade
|
|
543
|
+
$oldPath = "$TargetPath.old"
|
|
544
|
+
try {
|
|
545
|
+
if (Test-Path $oldPath) { Remove-Item $oldPath -Force -ErrorAction Stop }
|
|
546
|
+
} catch { }
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
Remove-Item $TargetPath -Force -ErrorAction Stop
|
|
550
|
+
} catch {
|
|
551
|
+
# The binary is locked (likely the currently-running process).
|
|
552
|
+
# On Windows we can rename a running executable -- move the old binary
|
|
553
|
+
# out of the way, place the new one alongside, and clean up the old one
|
|
554
|
+
# on the next install/upgrade.
|
|
555
|
+
try {
|
|
556
|
+
Move-Item $TargetPath $oldPath -Force -ErrorAction Stop
|
|
557
|
+
Write-WarnMsg "Existing binary is locked (it may be running)."
|
|
558
|
+
Write-WarnMsg "The old copy will be cleaned up on the next install or upgrade."
|
|
559
|
+
} catch {
|
|
560
|
+
throw "Cannot replace '$TargetPath' -- it is locked and cannot be renamed. Close all browser4-cli processes and try again."
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
# Place the new binary at the target path
|
|
566
|
+
if ($Move) {
|
|
567
|
+
Move-Item $SourcePath $TargetPath -Force -ErrorAction Stop
|
|
568
|
+
} else {
|
|
569
|
+
Copy-Item $SourcePath $TargetPath -Force -ErrorAction Stop
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
# ----------------------------------------------
|
|
574
|
+
# PATH management
|
|
575
|
+
# ----------------------------------------------
|
|
576
|
+
|
|
577
|
+
function Add-DirectoryToUserPath {
|
|
578
|
+
param([string]$Dir)
|
|
579
|
+
|
|
580
|
+
$dirResolved = (Resolve-Path $Dir -ErrorAction SilentlyContinue).Path
|
|
581
|
+
if (-not $dirResolved) { $dirResolved = $Dir }
|
|
582
|
+
|
|
583
|
+
# Read current user PATH
|
|
584
|
+
$currentUserPath = [System.Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::User)
|
|
585
|
+
$paths = if ($currentUserPath) { $currentUserPath -split ";" | Where-Object { $_ } } else { @() }
|
|
586
|
+
|
|
587
|
+
# Check if already present
|
|
588
|
+
$normalized = $paths | ForEach-Object { $rp = Resolve-Path $_ -ErrorAction SilentlyContinue; if ($rp) { $rp.Path } else { $_ } }
|
|
589
|
+
if ($normalized -contains $dirResolved) {
|
|
590
|
+
Write-Check "Already in user PATH: $Dir"
|
|
591
|
+
return
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if ($DryRun) {
|
|
595
|
+
Write-Check "[DRY-RUN] Would add to user PATH: $Dir"
|
|
596
|
+
return
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
$newPath = if ($currentUserPath) { "$currentUserPath;$Dir" } else { $Dir }
|
|
600
|
+
[System.Environment]::SetEnvironmentVariable("Path", $newPath, [System.EnvironmentVariableTarget]::User)
|
|
601
|
+
Write-Check "Added to user PATH: $Dir"
|
|
602
|
+
|
|
603
|
+
# Also update current session
|
|
604
|
+
$env:Path = "$env:Path;$Dir"
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
# ----------------------------------------------
|
|
608
|
+
# Main
|
|
609
|
+
# ----------------------------------------------
|
|
610
|
+
|
|
611
|
+
function Main {
|
|
612
|
+
Write-Summary "==========================================" -Color Cyan
|
|
613
|
+
Write-Summary " browser4-cli Installer" -Color Cyan
|
|
614
|
+
Write-Summary "==========================================" -Color Cyan
|
|
615
|
+
Write-Summary ""
|
|
616
|
+
|
|
617
|
+
# Auto-detect China mainland locale when no explicit source is given
|
|
618
|
+
if (-not $Source) {
|
|
619
|
+
$script:ChinaDetected = Test-ChinaLocale
|
|
620
|
+
if ($script:ChinaDetected) {
|
|
621
|
+
Write-Step "China mainland locale detected: preferring Aliyun OSS mirror."
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
# Detect platform
|
|
626
|
+
$platformKey = Get-PlatformKey
|
|
627
|
+
$binaryName = Get-BinaryName -PlatformKey $platformKey
|
|
628
|
+
|
|
629
|
+
# -- Locate mode: print diagnostics and exit --
|
|
630
|
+
if ($Locate) {
|
|
631
|
+
Write-Summary "--- Locate / diagnostics ---" -Color Cyan
|
|
632
|
+
Write-Summary ""
|
|
633
|
+
Write-Step "Script dir: $ScriptDir"
|
|
634
|
+
Write-Step "Platform key: $platformKey"
|
|
635
|
+
Write-Step "Binary name: $binaryName"
|
|
636
|
+
Write-Step "Default install: $(Get-DefaultInstallDir)"
|
|
637
|
+
Write-Step "China locale: $script:ChinaDetected"
|
|
638
|
+
Write-Step "Source override: $(if ($Source) { $Source } else { 'auto' })"
|
|
639
|
+
Write-Step "OS: $(if ($script:OSWin) { 'Windows' } elseif ($script:OSMac) { 'macOS' } elseif ($script:OSLinux) { 'Linux' } else { 'Unknown' })"
|
|
640
|
+
|
|
641
|
+
# Check for local binary
|
|
642
|
+
$localPath = Find-LocalBinary -BinaryName $binaryName
|
|
643
|
+
if ($localPath) {
|
|
644
|
+
$localOk = Test-LocalBinary -Path $localPath
|
|
645
|
+
Write-Step "Local binary: $localPath $(if ($localOk) { '(valid)' } else { '(present but --version failed)' })"
|
|
646
|
+
} else {
|
|
647
|
+
Write-Step "Local binary: not found alongside script"
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
# Check for already-installed binary
|
|
651
|
+
$defaultDir = Get-DefaultInstallDir
|
|
652
|
+
$existingPath = Join-Path $defaultDir $binaryName
|
|
653
|
+
if (Test-Path $existingPath) {
|
|
654
|
+
Write-Step "Already installed: $existingPath"
|
|
655
|
+
} else {
|
|
656
|
+
Write-Step "Already installed: not found at $defaultDir"
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
# Show download URLs that would be tried
|
|
660
|
+
$urls = Get-DownloadUrls -BinaryName $binaryName -VersionTag $Version
|
|
661
|
+
Write-Summary ""
|
|
662
|
+
Write-Step "Download order:"
|
|
663
|
+
foreach ($entry in $urls) {
|
|
664
|
+
Write-Step " $($entry.Label): $($entry.Url)"
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
Write-Summary ""
|
|
668
|
+
return
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
Write-Step "Platform: $platformKey"
|
|
672
|
+
Write-Step "Binary: $binaryName"
|
|
673
|
+
|
|
674
|
+
# Determine install directory
|
|
675
|
+
$installDir = if ($InstallDir) { $InstallDir } else { Get-DefaultInstallDir }
|
|
676
|
+
Write-Step "Install: $installDir"
|
|
677
|
+
Write-Summary ""
|
|
678
|
+
|
|
679
|
+
if (-not (Test-Path $installDir)) {
|
|
680
|
+
if (-not $DryRun) {
|
|
681
|
+
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
|
|
682
|
+
}
|
|
683
|
+
Write-Step "Created directory: $installDir"
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
$binaryPath = Join-Path $installDir $binaryName
|
|
687
|
+
|
|
688
|
+
# -- Local binary discovery (bundled/sideload) --
|
|
689
|
+
$useLocalBinary = $false
|
|
690
|
+
if (-not $SkipLocal) {
|
|
691
|
+
$localBinaryPath = Find-LocalBinary -BinaryName $binaryName
|
|
692
|
+
if ($localBinaryPath) {
|
|
693
|
+
Write-Step "Found local binary alongside script: $(Split-Path $localBinaryPath -Leaf)"
|
|
694
|
+
if (Test-LocalBinary -Path $localBinaryPath) {
|
|
695
|
+
Write-Check "Local binary verified (--version OK)"
|
|
696
|
+
$useLocalBinary = $true
|
|
697
|
+
} else {
|
|
698
|
+
Write-WarnMsg "Local binary found but --version check failed -- will download instead"
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
} elseif ($SkipLocal) {
|
|
702
|
+
Write-Step "Skipping local binary check (-SkipLocal)"
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
# Skip download only when --skip-if-installed is set and binary already exists,
|
|
706
|
+
# unless --force overrides it.
|
|
707
|
+
if ((Test-Path $binaryPath) -and (-not $Version) -and $SkipIfInstalled -and (-not $Force) -and (-not $useLocalBinary)) {
|
|
708
|
+
Write-Check "Binary already installed: $binaryPath"
|
|
709
|
+
} elseif ($useLocalBinary) {
|
|
710
|
+
# Copy local binary to install dir
|
|
711
|
+
if (-not $DryRun) {
|
|
712
|
+
Set-BinaryFile -TargetPath $binaryPath -SourcePath $localBinaryPath
|
|
713
|
+
}
|
|
714
|
+
Write-Check "Installed (local): $binaryPath"
|
|
715
|
+
} else {
|
|
716
|
+
# Build download URLs
|
|
717
|
+
$urls = Get-DownloadUrls -BinaryName $binaryName -VersionTag $Version
|
|
718
|
+
if (-not $urls -or $urls.Count -eq 0) {
|
|
719
|
+
throw "No download URLs configured"
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
$downloaded = $false
|
|
723
|
+
$tempFile = [System.IO.Path]::GetTempFileName()
|
|
724
|
+
|
|
725
|
+
foreach ($entry in $urls) {
|
|
726
|
+
if (Invoke-Download -Url $entry.Url -OutFile $tempFile -Label $entry.Label) {
|
|
727
|
+
$downloaded = $true
|
|
728
|
+
break
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (-not $downloaded) {
|
|
733
|
+
if (Test-Path $tempFile) { Remove-Item $tempFile -Force }
|
|
734
|
+
throw @"
|
|
735
|
+
Could not download browser4-cli binary.
|
|
736
|
+
|
|
737
|
+
Tried:
|
|
738
|
+
$($urls | ForEach-Object { " - $($_.Label): $($_.Url)" } | Out-String)
|
|
739
|
+
|
|
740
|
+
Please check:
|
|
741
|
+
- Network connectivity
|
|
742
|
+
- The version/tag exists: $Version
|
|
743
|
+
- For GitHub rate limits, set GITHUB_TOKEN environment variable
|
|
744
|
+
- If you have a local copy, place it alongside this script and re-run
|
|
745
|
+
"@
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
# Move from temp to install dir
|
|
749
|
+
if (-not $DryRun) {
|
|
750
|
+
Set-BinaryFile -TargetPath $binaryPath -SourcePath $tempFile -Move
|
|
751
|
+
}
|
|
752
|
+
Write-Check "Installed: $binaryPath"
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
# On Unix, ensure executable bit
|
|
756
|
+
if (-not $script:OSWin) {
|
|
757
|
+
if (-not $DryRun) {
|
|
758
|
+
try { chmod +x $binaryPath 2>$null } catch { }
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
# Create symlinks (browser4-cli -> platform binary, b4 if no conflict)
|
|
763
|
+
Write-Summary ""
|
|
764
|
+
New-Symlinks -BinaryName $binaryName -InstallDir $installDir -PlatformKey $platformKey
|
|
765
|
+
|
|
766
|
+
# Add to PATH
|
|
767
|
+
if ($AddToPath -and $script:OSWin) {
|
|
768
|
+
Write-Summary ""
|
|
769
|
+
Add-DirectoryToUserPath -Dir $installDir
|
|
770
|
+
} elseif ($AddToPath -and -not $script:OSWin) {
|
|
771
|
+
Write-Summary ""
|
|
772
|
+
$shellRc = if (Test-Path "$env:HOME/.zshrc") { "$env:HOME/.zshrc" } elseif (Test-Path "$env:HOME/.bashrc") { "$env:HOME/.bashrc" } elseif (Test-Path "$env:HOME/.bash_profile") { "$env:HOME/.bash_profile" } else { "$env:HOME/.profile" }
|
|
773
|
+
$pathLine = "export PATH=""$installDir`:`$PATH"""
|
|
774
|
+
if (-not $DryRun) {
|
|
775
|
+
if (-not (Select-String -Path $shellRc -Pattern [regex]::Escape($installDir) -ErrorAction SilentlyContinue)) {
|
|
776
|
+
Add-Content -Path $shellRc -Value ""
|
|
777
|
+
Add-Content -Path $shellRc -Value "# browser4-cli"
|
|
778
|
+
Add-Content -Path $shellRc -Value $pathLine
|
|
779
|
+
Write-Check "Added to PATH in $shellRc"
|
|
780
|
+
} else {
|
|
781
|
+
Write-Check "PATH entry already in $shellRc"
|
|
782
|
+
}
|
|
783
|
+
} else {
|
|
784
|
+
Write-Check "[DRY-RUN] Would add to $shellRc"
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
# Verify
|
|
789
|
+
Write-Summary ""
|
|
790
|
+
if (-not $DryRun) {
|
|
791
|
+
try {
|
|
792
|
+
$versionOutput = & $binaryPath --version 2>&1
|
|
793
|
+
Write-Summary "[v] browser4-cli installed successfully" -Color Green
|
|
794
|
+
Write-Summary " Version: $versionOutput"
|
|
795
|
+
} catch {
|
|
796
|
+
Write-Summary "[v] Binary installed at: $binaryPath" -Color Green
|
|
797
|
+
Write-WarnMsg "Could not verify --version (this is normal on first install)"
|
|
798
|
+
}
|
|
799
|
+
} else {
|
|
800
|
+
Write-Summary "[DRY-RUN] Installation plan complete" -Color Yellow
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
Write-Summary ""
|
|
804
|
+
Write-Summary "Run 'browser4-cli --help' to get started." -Color Cyan
|
|
805
|
+
|
|
806
|
+
if ($script:OSWin) {
|
|
807
|
+
Write-Summary "If the command isn't found, restart your terminal or run:"
|
|
808
|
+
Write-Summary " `$env:Path = [System.Environment]::GetEnvironmentVariable('Path','User') + ';' + [System.Environment]::GetEnvironmentVariable('Path','Machine')"
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
Main
|