browser4-cli 0.1.12 → 0.1.14
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/README.md +54 -9
- 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/install-browser4-cli.ps1 +474 -0
- package/scripts/install-browser4-cli.sh +483 -0
- package/scripts/npm-publish-check.js +12 -0
- package/scripts/sync-version.js +55 -18
package/README.md
CHANGED
|
@@ -37,21 +37,66 @@ brew install browser4-cli
|
|
|
37
37
|
cargo install browser4-cli
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
### Standalone Installer Scripts (no npm / Rust / Homebrew needed)
|
|
41
|
+
|
|
42
|
+
Bootstrap the native binary directly with a single command:
|
|
43
|
+
|
|
44
|
+
**Windows (PowerShell):**
|
|
45
|
+
```powershell
|
|
46
|
+
Invoke-WebRequest -Uri "https://browser4.oss-cn-beijing.aliyuncs.com/scripts/install-browser4-cli.ps1" -OutFile "$env:TEMP\install-browser4-cli.ps1"
|
|
47
|
+
powershell -ExecutionPolicy Bypass -File "$env:TEMP\install-browser4-cli.ps1"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Linux / macOS (bash):**
|
|
51
|
+
```bash
|
|
52
|
+
curl -fsSL https://browser4.oss-cn-beijing.aliyuncs.com/scripts/install-browser4-cli.sh | bash
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or run the scripts locally from a cloned repo:
|
|
56
|
+
- `cli/scripts/install-browser4-cli.ps1` (Windows)
|
|
57
|
+
- `cli/scripts/install-browser4-cli.sh` (Linux / macOS / Git Bash)
|
|
58
|
+
|
|
59
|
+
See [Standalone CLI Installer Scripts](../docs/cli-standalone-install.md) for
|
|
60
|
+
the full option reference, supported platforms, and examples.
|
|
61
|
+
|
|
40
62
|
### From Source
|
|
41
63
|
|
|
64
|
+
**Build prerequisites:** Rust (stable, edition 2021), Node.js 24+, pnpm 10+, git.
|
|
65
|
+
|
|
42
66
|
```bash
|
|
43
|
-
git clone https://github.com/platonai/
|
|
44
|
-
cd cli/browser4-cli
|
|
67
|
+
git clone https://github.com/platonai/Browser4.git
|
|
68
|
+
cd Browser4/cli/browser4-cli
|
|
45
69
|
pnpm install
|
|
46
|
-
pnpm build:native #
|
|
70
|
+
pnpm build:native # Compiles the Rust binary (requires https://rustup.rs)
|
|
47
71
|
pnpm link --global # Makes browser4-cli available globally
|
|
48
72
|
```
|
|
49
73
|
|
|
74
|
+
Cross-compilation from Linux (for release builds) additionally requires:
|
|
75
|
+
`cargo-zigbuild`, Zig 0.13.0, `gcc-aarch64-linux-gnu`, and `mingw-w64`.
|
|
76
|
+
See `cli/docker/Dockerfile.build` for a Dockerized build environment.
|
|
77
|
+
|
|
50
78
|
### Requirements
|
|
51
79
|
|
|
52
|
-
- **Chrome**
|
|
53
|
-
- **Java 17+**
|
|
54
|
-
- **Rust**
|
|
80
|
+
- **Chrome** — Latest Chrome installed on your system. The CLI can auto-install Chrome on most platforms when missing.
|
|
81
|
+
- **Java 17+** — Required to run the Browser4 backend (`Browser4.jar`). Eclipse Temurin recommended. JDK 21+ enables best jlink compression when the CLI auto-builds a runtime bundle from source.
|
|
82
|
+
- **Rust** — Only needed when building the CLI from source (see From Source above). The `stable` toolchain (edition 2021) is sufficient.
|
|
83
|
+
|
|
84
|
+
#### Additional requirements for auto-building a runtime bundle from source
|
|
85
|
+
|
|
86
|
+
When the CLI detects a Browser4 repository checkout, it attempts to build a
|
|
87
|
+
self-contained runtime bundle (bundled JRE + dependency JARs) from source
|
|
88
|
+
instead of downloading a pre-built release. This requires:
|
|
89
|
+
|
|
90
|
+
| Tool | Version | Linux | macOS | Windows |
|
|
91
|
+
|------|---------|-------|-------|---------|
|
|
92
|
+
| **Maven** | 3.9+ | via `mvnw` wrapper | via `mvnw` wrapper | via `mvnw.cmd` wrapper |
|
|
93
|
+
| **JDK tools** (`jdeps`, `jlink`) | bundled with JDK 16+ | included in JDK | included in JDK | included in JDK |
|
|
94
|
+
| **PowerShell 7** (`pwsh`) | 7.0+ | **required** | **required** | built-in (`powershell.exe`) |
|
|
95
|
+
| **tar** | any | **required** | **required** | built-in |
|
|
96
|
+
|
|
97
|
+
Set `BROWSER4_CLI_FORCE_REMOTE_BUNDLE=1` to skip the local build and always
|
|
98
|
+
download a pre-built bundle — useful in CI / corporate environments where
|
|
99
|
+
Maven or jlink are unavailable.
|
|
55
100
|
|
|
56
101
|
## Usage
|
|
57
102
|
|
|
@@ -242,7 +287,7 @@ Like other advanced commands, they are intentionally omitted from the global
|
|
|
242
287
|
Use the spaced `agent <subcommand>` form:
|
|
243
288
|
|
|
244
289
|
```shell
|
|
245
|
-
browser4-cli agent run "Open
|
|
290
|
+
browser4-cli agent run "Open browser4.io and summarize the hero section"
|
|
246
291
|
browser4-cli agent status agent-task-1
|
|
247
292
|
browser4-cli agent result agent-task-1
|
|
248
293
|
```
|
|
@@ -344,7 +389,7 @@ browser4-cli swarm submit https://example.com/direct \
|
|
|
344
389
|
--seed-file=./swarm-seeds.txt \
|
|
345
390
|
--deadline=2026-03-30T00:00:00Z \
|
|
346
391
|
--expires=1d \
|
|
347
|
-
--refresh --
|
|
392
|
+
--refresh --store-content
|
|
348
393
|
|
|
349
394
|
# poll and fetch the result
|
|
350
395
|
browser4-cli swarm status scrape-task-4
|
|
@@ -369,7 +414,7 @@ browser4-cli swarm query "https://www.amazon.com/dp/B08PP5MSVB" --sql "
|
|
|
369
414
|
browser4-cli swarm query "https://www.amazon.com/dp/B08PP5MSVB" --sql @query.sql
|
|
370
415
|
|
|
371
416
|
# With seed file and load options:
|
|
372
|
-
browser4-cli swarm query --sql @query.sql --seed-file=./urls.txt --refresh
|
|
417
|
+
browser4-cli swarm query --sql @query.sql --seed-file=./urls.txt --refresh
|
|
373
418
|
```
|
|
374
419
|
|
|
375
420
|
### Notes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -0,0 +1,474 @@
|
|
|
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 (tried in order unless -Source is specified):
|
|
15
|
+
1. GitHub Releases — https://github.com/platonai/Browser4
|
|
16
|
+
2. Aliyun OSS — https://browser4.oss-cn-beijing.aliyuncs.com
|
|
17
|
+
|
|
18
|
+
.PARAMETER Version
|
|
19
|
+
Release version tag to download (e.g. "v4.11.0" or "v0.1.12-cli").
|
|
20
|
+
Defaults to "latest" which resolves to the most recent stable release.
|
|
21
|
+
|
|
22
|
+
.PARAMETER InstallDir
|
|
23
|
+
Directory to install the binary into.
|
|
24
|
+
Default: $env:LOCALAPPDATA\Programs\browser4-cli
|
|
25
|
+
|
|
26
|
+
.PARAMETER Source
|
|
27
|
+
Force a specific download source: "github" or "oss".
|
|
28
|
+
Default: try GitHub first, fall back to OSS.
|
|
29
|
+
|
|
30
|
+
.PARAMETER AddToPath
|
|
31
|
+
Add the install directory to the current user's PATH environment variable.
|
|
32
|
+
Default: true.
|
|
33
|
+
|
|
34
|
+
.PARAMETER Silent
|
|
35
|
+
Suppress all non-error output.
|
|
36
|
+
|
|
37
|
+
.PARAMETER DryRun
|
|
38
|
+
Print what would be done without actually doing it.
|
|
39
|
+
|
|
40
|
+
.EXAMPLE
|
|
41
|
+
# Quick install — default location, latest version, add to PATH
|
|
42
|
+
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1
|
|
43
|
+
|
|
44
|
+
.EXAMPLE
|
|
45
|
+
# Silent install with a specific version
|
|
46
|
+
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1 -Version "v4.11.0" -Silent
|
|
47
|
+
|
|
48
|
+
.EXAMPLE
|
|
49
|
+
# Install from OSS only, custom directory
|
|
50
|
+
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1 -Source oss -InstallDir "C:\tools\browser4"
|
|
51
|
+
#>
|
|
52
|
+
|
|
53
|
+
[CmdletBinding()]
|
|
54
|
+
param(
|
|
55
|
+
[string]$Version = "",
|
|
56
|
+
[string]$InstallDir = "",
|
|
57
|
+
[ValidateSet("github", "oss")]
|
|
58
|
+
[string]$Source = "",
|
|
59
|
+
[bool]$AddToPath = $true,
|
|
60
|
+
[switch]$Silent,
|
|
61
|
+
[switch]$DryRun
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
$ErrorActionPreference = "Stop"
|
|
65
|
+
|
|
66
|
+
# ──────────────────────────────────────────────
|
|
67
|
+
# OS detection (compatible with PS 5.1+)
|
|
68
|
+
# ──────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
$script:IsWin = [System.Environment]::OSVersion.Platform -eq "Win32NT"
|
|
71
|
+
$script:IsMac = [System.Environment]::OSVersion.Platform -eq "Unix" -and (uname -s 2>$null) -eq "Darwin"
|
|
72
|
+
$script:IsLinux = [System.Environment]::OSVersion.Platform -eq "Unix" -and (uname -s 2>$null) -ne "Darwin"
|
|
73
|
+
|
|
74
|
+
# ──────────────────────────────────────────────
|
|
75
|
+
# Helpers
|
|
76
|
+
# ──────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
function Write-Summary {
|
|
79
|
+
param([string]$Message, [string]$Color = "White")
|
|
80
|
+
if (-not $Silent) { Write-Host $Message -ForegroundColor $Color }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function Write-Step {
|
|
84
|
+
param([string]$Message)
|
|
85
|
+
if (-not $Silent) { Write-Host " » $Message" -ForegroundColor Gray }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function Write-Check {
|
|
89
|
+
param([string]$Message)
|
|
90
|
+
if (-not $Silent) { Write-Host " ✓ $Message" -ForegroundColor Green }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function Write-WarnMsg {
|
|
94
|
+
param([string]$Message)
|
|
95
|
+
if (-not $Silent) { Write-Host " ⚠ $Message" -ForegroundColor Yellow }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# ──────────────────────────────────────────────
|
|
99
|
+
# Detection
|
|
100
|
+
# ──────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
function Get-PlatformKey {
|
|
103
|
+
$arch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { "arm64" } else { "x64" }
|
|
104
|
+
|
|
105
|
+
if ($script:IsWin) {
|
|
106
|
+
return "win32-$arch"
|
|
107
|
+
}
|
|
108
|
+
elseif ($script:IsMac) {
|
|
109
|
+
return "darwin-$arch"
|
|
110
|
+
}
|
|
111
|
+
elseif ($script:IsLinux) {
|
|
112
|
+
# Detect musl
|
|
113
|
+
$isMusl = $false
|
|
114
|
+
try {
|
|
115
|
+
$lddOutput = ldd --version 2>&1
|
|
116
|
+
if ($lddOutput -match "musl") { $isMusl = $true }
|
|
117
|
+
} catch {
|
|
118
|
+
if ((Test-Path "/lib/ld-musl-x86_64.so.1") -or (Test-Path "/lib/ld-musl-aarch64.so.1")) {
|
|
119
|
+
$isMusl = $true
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
$libc = if ($isMusl) { "musl" } else { "" }
|
|
123
|
+
if ($libc) { return "linux-$libc-$arch" } else { return "linux-$arch" }
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
throw "Unsupported OS. Browser4 CLI supports Windows, macOS, and Linux."
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function Get-BinaryName {
|
|
131
|
+
param([string]$PlatformKey)
|
|
132
|
+
return "browser4-cli-$PlatformKey" + $(if ($PlatformKey.StartsWith("win32")) { ".exe" } else { "" })
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function Get-DefaultInstallDir {
|
|
136
|
+
if ($script:IsWin) {
|
|
137
|
+
return Join-Path $env:LOCALAPPDATA "Programs\browser4-cli"
|
|
138
|
+
}
|
|
139
|
+
elseif ($script:IsLinux -or $script:IsMac) {
|
|
140
|
+
# Prefer ~/.local/bin for user installs
|
|
141
|
+
if (Test-Path "$env:HOME/.local/bin") {
|
|
142
|
+
return "$env:HOME/.local/bin"
|
|
143
|
+
}
|
|
144
|
+
return "$env:HOME/.local/bin"
|
|
145
|
+
}
|
|
146
|
+
throw "Unsupported OS"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# ──────────────────────────────────────────────
|
|
150
|
+
# Download URLs
|
|
151
|
+
# ──────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
$GITHUB_REPO = "platonai/Browser4"
|
|
154
|
+
$OSS_BASE = "https://browser4.oss-cn-beijing.aliyuncs.com"
|
|
155
|
+
|
|
156
|
+
function Get-DownloadUrls {
|
|
157
|
+
param([string]$BinaryName, [string]$VersionTag)
|
|
158
|
+
|
|
159
|
+
$urls = @()
|
|
160
|
+
|
|
161
|
+
$ghBase = "https://github.com/$GITHUB_REPO/releases/download"
|
|
162
|
+
if ($VersionTag) {
|
|
163
|
+
$ghUrl = "$ghBase/$VersionTag/$BinaryName"
|
|
164
|
+
$ossUrl = "$OSS_BASE/releases/download/$VersionTag/$BinaryName"
|
|
165
|
+
} else {
|
|
166
|
+
# Use 'latest' redirect for GitHub, 'latest' symlink for OSS
|
|
167
|
+
$ghUrl = "https://github.com/$GITHUB_REPO/releases/latest/download/$BinaryName"
|
|
168
|
+
$ossUrl = "$OSS_BASE/releases/download/latest/$BinaryName"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if ($Source -eq "github") {
|
|
172
|
+
$urls += @{ Url = $ghUrl; Label = "GitHub Releases" }
|
|
173
|
+
} elseif ($Source -eq "oss") {
|
|
174
|
+
$urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
|
|
175
|
+
} else {
|
|
176
|
+
$urls += @{ Url = $ghUrl; Label = "GitHub Releases" }
|
|
177
|
+
$urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return $urls
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function Invoke-Download {
|
|
184
|
+
param([string]$Url, [string]$OutFile, [string]$Label)
|
|
185
|
+
|
|
186
|
+
Write-Step "Trying $Label..."
|
|
187
|
+
Write-Step "URL: $Url"
|
|
188
|
+
|
|
189
|
+
if ($DryRun) {
|
|
190
|
+
Write-Check "[DRY-RUN] Would download to: $OutFile"
|
|
191
|
+
return $true
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
$ProgressPreference = if ($Silent) { "SilentlyContinue" } else { "Continue" }
|
|
196
|
+
|
|
197
|
+
# Use Invoke-WebRequest with progress bar
|
|
198
|
+
Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing -ErrorAction Stop
|
|
199
|
+
|
|
200
|
+
if (Test-Path $OutFile) {
|
|
201
|
+
$size = (Get-Item $OutFile).Length
|
|
202
|
+
if ($size -gt 102400) { # > 100 KB minimum
|
|
203
|
+
Write-Check "Downloaded $( [math]::Round($size / 1MB, 1) ) MB"
|
|
204
|
+
return $true
|
|
205
|
+
} else {
|
|
206
|
+
Write-WarnMsg "Downloaded file too small ($size bytes) — may be an error page"
|
|
207
|
+
Remove-Item $OutFile -Force -ErrorAction SilentlyContinue
|
|
208
|
+
return $false
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
Write-WarnMsg "Download appeared to succeed but file not found"
|
|
212
|
+
return $false
|
|
213
|
+
} catch {
|
|
214
|
+
Write-WarnMsg "Failed: $($_.Exception.Message)"
|
|
215
|
+
return $false
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# ──────────────────────────────────────────────
|
|
220
|
+
# Symlinks
|
|
221
|
+
# ──────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
function New-PlatformLink {
|
|
224
|
+
param([string]$LinkPath, [string]$TargetName, [string]$DisplayName)
|
|
225
|
+
|
|
226
|
+
# Try symbolic link first (works on Unix; on Windows needs Admin or Developer Mode)
|
|
227
|
+
try {
|
|
228
|
+
New-Item -ItemType SymbolicLink -Path $LinkPath -Target $TargetName -Force -ErrorAction Stop | Out-Null
|
|
229
|
+
Write-Check "Created symlink: $DisplayName -> $TargetName"
|
|
230
|
+
return $true
|
|
231
|
+
} catch {
|
|
232
|
+
# Symbolic link failed — try hard link on Windows
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
# Try hard link (Windows, same volume)
|
|
236
|
+
if ($script:IsWin) {
|
|
237
|
+
try {
|
|
238
|
+
$targetPath = Join-Path (Split-Path $LinkPath -Parent) $TargetName
|
|
239
|
+
New-Item -ItemType HardLink -Path $LinkPath -Target $targetPath -Force -ErrorAction Stop | Out-Null
|
|
240
|
+
Write-Check "Created hard link: $DisplayName -> $TargetName"
|
|
241
|
+
return $true
|
|
242
|
+
} catch {
|
|
243
|
+
# Hard link also failed — create a .cmd wrapper as last resort
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# Last resort: .cmd wrapper that forwards all arguments
|
|
247
|
+
try {
|
|
248
|
+
$wrapperContent = '@"%~dp0' + $TargetName + '" %*'
|
|
249
|
+
Set-Content -Path ($LinkPath -replace '\.exe$', '.cmd') -Value $wrapperContent -Force -ErrorAction Stop
|
|
250
|
+
Write-Check "Created wrapper: " + (Split-Path ($LinkPath -replace '\.exe$', '.cmd') -Leaf) + " -> $TargetName"
|
|
251
|
+
return $true
|
|
252
|
+
} catch {
|
|
253
|
+
Write-WarnMsg "Could not create link for $DisplayName (may need admin privileges)"
|
|
254
|
+
return $false
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
Write-WarnMsg "Could not create link for $DisplayName"
|
|
259
|
+
return $false
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function New-Symlinks {
|
|
263
|
+
param([string]$BinaryName, [string]$InstallDir, [string]$PlatformKey)
|
|
264
|
+
|
|
265
|
+
$ext = if ($PlatformKey.StartsWith("win32")) { ".exe" } else { "" }
|
|
266
|
+
|
|
267
|
+
# 1) Always: browser4-cli -> browser4-cli-<platform>
|
|
268
|
+
$linkName = "browser4-cli$ext"
|
|
269
|
+
$linkPath = Join-Path $InstallDir $linkName
|
|
270
|
+
|
|
271
|
+
if ($DryRun) {
|
|
272
|
+
Write-Step "[DRY-RUN] Would create link: $linkName -> $BinaryName"
|
|
273
|
+
} else {
|
|
274
|
+
New-PlatformLink -LinkPath $linkPath -TargetName $BinaryName -DisplayName $linkName
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
# 2) Only if no conflict: b4 -> browser4-cli-<platform>
|
|
278
|
+
$shortName = "b4$ext"
|
|
279
|
+
$shortPath = Join-Path $InstallDir $shortName
|
|
280
|
+
|
|
281
|
+
# Check if b4 is already on PATH (conflict with another tool)
|
|
282
|
+
$existingCmd = Get-Command b4 -ErrorAction SilentlyContinue
|
|
283
|
+
if ($existingCmd) {
|
|
284
|
+
Write-WarnMsg "Skipping short link '$shortName': 'b4' already found on PATH ($($existingCmd.Source))"
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
# Check if b4 already exists in the install directory
|
|
289
|
+
if (Test-Path $shortPath) {
|
|
290
|
+
Write-WarnMsg "Skipping short link '$shortName': already exists in $InstallDir"
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
# Also check b4.cmd if on Windows (wrapper fallback)
|
|
295
|
+
if ($script:IsWin) {
|
|
296
|
+
$shortCmdPath = Join-Path $InstallDir "b4.cmd"
|
|
297
|
+
if (Test-Path $shortCmdPath) {
|
|
298
|
+
Write-WarnMsg "Skipping short link '$shortName': 'b4.cmd' already exists in $InstallDir"
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if ($DryRun) {
|
|
304
|
+
Write-Step "[DRY-RUN] Would create link: $shortName -> $BinaryName"
|
|
305
|
+
} else {
|
|
306
|
+
New-PlatformLink -LinkPath $shortPath -TargetName $BinaryName -DisplayName $shortName
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# ──────────────────────────────────────────────
|
|
311
|
+
# PATH management
|
|
312
|
+
# ──────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
function Add-DirectoryToUserPath {
|
|
315
|
+
param([string]$Dir)
|
|
316
|
+
|
|
317
|
+
$dirResolved = (Resolve-Path $Dir -ErrorAction SilentlyContinue).Path
|
|
318
|
+
if (-not $dirResolved) { $dirResolved = $Dir }
|
|
319
|
+
|
|
320
|
+
# Read current user PATH
|
|
321
|
+
$currentUserPath = [System.Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::User)
|
|
322
|
+
$paths = if ($currentUserPath) { $currentUserPath -split ";" | Where-Object { $_ } } else { @() }
|
|
323
|
+
|
|
324
|
+
# Check if already present
|
|
325
|
+
$normalized = $paths | ForEach-Object { $rp = Resolve-Path $_ -ErrorAction SilentlyContinue; if ($rp) { $rp.Path } else { $_ } }
|
|
326
|
+
if ($normalized -contains $dirResolved) {
|
|
327
|
+
Write-Check "Already in user PATH: $Dir"
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if ($DryRun) {
|
|
332
|
+
Write-Check "[DRY-RUN] Would add to user PATH: $Dir"
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
$newPath = if ($currentUserPath) { "$currentUserPath;$Dir" } else { $Dir }
|
|
337
|
+
[System.Environment]::SetEnvironmentVariable("Path", $newPath, [System.EnvironmentVariableTarget]::User)
|
|
338
|
+
Write-Check "Added to user PATH: $Dir"
|
|
339
|
+
|
|
340
|
+
# Also update current session
|
|
341
|
+
$env:Path = "$env:Path;$Dir"
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
# ──────────────────────────────────────────────
|
|
345
|
+
# Main
|
|
346
|
+
# ──────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
function Main {
|
|
349
|
+
Write-Summary "╔════════════════════════════════════════╗" -Color Cyan
|
|
350
|
+
Write-Summary "║ browser4-cli Installer ║" -Color Cyan
|
|
351
|
+
Write-Summary "╚════════════════════════════════════════╝" -Color Cyan
|
|
352
|
+
Write-Summary ""
|
|
353
|
+
|
|
354
|
+
# Detect platform
|
|
355
|
+
$platformKey = Get-PlatformKey
|
|
356
|
+
$binaryName = Get-BinaryName -PlatformKey $platformKey
|
|
357
|
+
Write-Step "Platform: $platformKey"
|
|
358
|
+
Write-Step "Binary: $binaryName"
|
|
359
|
+
|
|
360
|
+
# Determine install directory
|
|
361
|
+
$installDir = if ($InstallDir) { $InstallDir } else { Get-DefaultInstallDir }
|
|
362
|
+
Write-Step "Install: $installDir"
|
|
363
|
+
Write-Summary ""
|
|
364
|
+
|
|
365
|
+
if (-not (Test-Path $installDir)) {
|
|
366
|
+
if (-not $DryRun) {
|
|
367
|
+
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
|
|
368
|
+
}
|
|
369
|
+
Write-Step "Created directory: $installDir"
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
$binaryPath = Join-Path $installDir $binaryName
|
|
373
|
+
|
|
374
|
+
# If binary already exists and no version override, skip download
|
|
375
|
+
if ((Test-Path $binaryPath) -and (-not $Version)) {
|
|
376
|
+
Write-Check "Binary already installed: $binaryPath"
|
|
377
|
+
} else {
|
|
378
|
+
# Build download URLs
|
|
379
|
+
$urls = Get-DownloadUrls -BinaryName $binaryName -VersionTag $Version
|
|
380
|
+
if (-not $urls -or $urls.Count -eq 0) {
|
|
381
|
+
throw "No download URLs configured"
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
$downloaded = $false
|
|
385
|
+
$tempFile = [System.IO.Path]::GetTempFileName()
|
|
386
|
+
|
|
387
|
+
foreach ($entry in $urls) {
|
|
388
|
+
if (Invoke-Download -Url $entry.Url -OutFile $tempFile -Label $entry.Label) {
|
|
389
|
+
$downloaded = $true
|
|
390
|
+
break
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (-not $downloaded) {
|
|
395
|
+
if (Test-Path $tempFile) { Remove-Item $tempFile -Force }
|
|
396
|
+
throw @"
|
|
397
|
+
Could not download browser4-cli binary.
|
|
398
|
+
|
|
399
|
+
Tried:
|
|
400
|
+
$($urls | ForEach-Object { " - $($_.Label): $($_.Url)" } | Out-String)
|
|
401
|
+
|
|
402
|
+
Please check:
|
|
403
|
+
- Network connectivity
|
|
404
|
+
- The version/tag exists: $Version
|
|
405
|
+
- For GitHub rate limits, set GITHUB_TOKEN environment variable
|
|
406
|
+
"@
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
# Move from temp to install dir
|
|
410
|
+
if (-not $DryRun) {
|
|
411
|
+
if (Test-Path $binaryPath) { Remove-Item $binaryPath -Force }
|
|
412
|
+
Move-Item $tempFile $binaryPath -Force
|
|
413
|
+
}
|
|
414
|
+
Write-Check "Installed: $binaryPath"
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
# On Unix, ensure executable bit
|
|
418
|
+
if (-not $script:IsWin) {
|
|
419
|
+
if (-not $DryRun) {
|
|
420
|
+
try { chmod +x $binaryPath 2>$null } catch { }
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
# Create symlinks (browser4-cli -> platform binary, b4 if no conflict)
|
|
425
|
+
Write-Summary ""
|
|
426
|
+
New-Symlinks -BinaryName $binaryName -InstallDir $installDir -PlatformKey $platformKey
|
|
427
|
+
|
|
428
|
+
# Add to PATH
|
|
429
|
+
if ($AddToPath -and $script:IsWin) {
|
|
430
|
+
Write-Summary ""
|
|
431
|
+
Add-DirectoryToUserPath -Dir $installDir
|
|
432
|
+
} elseif ($AddToPath -and -not $script:IsWin) {
|
|
433
|
+
Write-Summary ""
|
|
434
|
+
$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" }
|
|
435
|
+
$pathLine = "export PATH=""$installDir`:`$PATH"""
|
|
436
|
+
if (-not $DryRun) {
|
|
437
|
+
if (-not (Select-String -Path $shellRc -Pattern [regex]::Escape($installDir) -ErrorAction SilentlyContinue)) {
|
|
438
|
+
Add-Content -Path $shellRc -Value ""
|
|
439
|
+
Add-Content -Path $shellRc -Value "# browser4-cli"
|
|
440
|
+
Add-Content -Path $shellRc -Value $pathLine
|
|
441
|
+
Write-Check "Added to PATH in $shellRc"
|
|
442
|
+
} else {
|
|
443
|
+
Write-Check "PATH entry already in $shellRc"
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
Write-Check "[DRY-RUN] Would add to $shellRc"
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
# Verify
|
|
451
|
+
Write-Summary ""
|
|
452
|
+
if (-not $DryRun) {
|
|
453
|
+
try {
|
|
454
|
+
$versionOutput = & $binaryPath --version 2>&1
|
|
455
|
+
Write-Summary "✓ browser4-cli installed successfully" -Color Green
|
|
456
|
+
Write-Summary " Version: $versionOutput"
|
|
457
|
+
} catch {
|
|
458
|
+
Write-Summary "✓ Binary installed at: $binaryPath" -Color Green
|
|
459
|
+
Write-WarnMsg "Could not verify --version (this is normal on first install)"
|
|
460
|
+
}
|
|
461
|
+
} else {
|
|
462
|
+
Write-Summary "[DRY-RUN] Installation plan complete" -Color Yellow
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
Write-Summary ""
|
|
466
|
+
Write-Summary "Run 'browser4-cli --help' to get started." -Color Cyan
|
|
467
|
+
|
|
468
|
+
if ($script:IsWin) {
|
|
469
|
+
Write-Summary "If the command isn't found, restart your terminal or run:"
|
|
470
|
+
Write-Summary " `$env:Path = [System.Environment]::GetEnvironmentVariable('Path','User') + ';' + [System.Environment]::GetEnvironmentVariable('Path','Machine')"
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
Main
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# install-browser4-cli.sh
|
|
3
|
+
# Download and install the browser4-cli native binary.
|
|
4
|
+
#
|
|
5
|
+
# Detects OS, CPU architecture, and libc variant (glibc / musl), downloads
|
|
6
|
+
# the matching binary from GitHub Releases or Alibaba Cloud OSS, and installs
|
|
7
|
+
# it to a user-local directory.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# curl -fsSL https://.../install-browser4-cli.sh | bash
|
|
11
|
+
# curl -fsSL https://.../install-browser4-cli.sh | bash -s -- --silent
|
|
12
|
+
#
|
|
13
|
+
# ./install-browser4-cli.sh [OPTIONS]
|
|
14
|
+
#
|
|
15
|
+
# Options:
|
|
16
|
+
# --version, -v TAG Release tag (e.g. "v4.11.0"). Default: latest.
|
|
17
|
+
# --install-dir, -d DIR Install directory (default: ~/.local/bin).
|
|
18
|
+
# --source SRC Force download source: "github" or "oss".
|
|
19
|
+
# --no-path Skip adding install dir to PATH.
|
|
20
|
+
# --silent, -s Suppress non-error output.
|
|
21
|
+
# --dry-run Print what would be done without doing it.
|
|
22
|
+
# --help, -h Show this message.
|
|
23
|
+
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
# ──────────────────────────────────────────────
|
|
27
|
+
# Globals
|
|
28
|
+
# ──────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
GITHUB_REPO="platonai/Browser4"
|
|
31
|
+
OSS_BASE="https://browser4.oss-cn-beijing.aliyuncs.com"
|
|
32
|
+
|
|
33
|
+
VERSION=""
|
|
34
|
+
INSTALL_DIR=""
|
|
35
|
+
SOURCE=""
|
|
36
|
+
ADD_TO_PATH=true
|
|
37
|
+
SILENT=false
|
|
38
|
+
DRY_RUN=false
|
|
39
|
+
|
|
40
|
+
# ──────────────────────────────────────────────
|
|
41
|
+
# Helpers
|
|
42
|
+
# ──────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
say() { if [[ "$SILENT" != true ]]; then echo -e "$*"; fi; }
|
|
45
|
+
step() { say " → $*"; }
|
|
46
|
+
ok() { say " ✓ $*"; }
|
|
47
|
+
warn() { say " ⚠ $*" >&2; }
|
|
48
|
+
die() { echo "ERROR: $*" >&2; exit 1; }
|
|
49
|
+
|
|
50
|
+
color_cyan='\033[0;36m'
|
|
51
|
+
color_green='\033[0;32m'
|
|
52
|
+
color_yellow='\033[0;33m'
|
|
53
|
+
color_reset='\033[0m'
|
|
54
|
+
|
|
55
|
+
header() {
|
|
56
|
+
if [[ "$SILENT" != true ]]; then
|
|
57
|
+
echo -e "${color_cyan}╔════════════════════════════════════════╗${color_reset}"
|
|
58
|
+
echo -e "${color_cyan}║ browser4-cli Installer ║${color_reset}"
|
|
59
|
+
echo -e "${color_cyan}╚════════════════════════════════════════╝${color_reset}"
|
|
60
|
+
echo ""
|
|
61
|
+
fi
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
usage() {
|
|
65
|
+
cat <<EOF
|
|
66
|
+
Usage: $(basename "$0") [OPTIONS]
|
|
67
|
+
|
|
68
|
+
Download and install the browser4-cli native binary.
|
|
69
|
+
|
|
70
|
+
Options:
|
|
71
|
+
--version, -v TAG Release tag (e.g. "v4.11.0"). Default: latest.
|
|
72
|
+
--install-dir, -d DIR Install directory (default: ~/.local/bin).
|
|
73
|
+
--source SRC Force source: "github" or "oss" (default: try both).
|
|
74
|
+
--no-path Skip adding install dir to shell rc file.
|
|
75
|
+
--silent, -s Suppress non-error output.
|
|
76
|
+
--dry-run Print what would be done without doing it.
|
|
77
|
+
--help, -h Show this message.
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
$(basename "$0") # Install latest to ~/.local/bin
|
|
81
|
+
$(basename "$0") --version v4.11.0 # Install specific version
|
|
82
|
+
$(basename "$0") --source oss --silent # Silent install from Aliyun OSS
|
|
83
|
+
$(basename "$0") --install-dir /usr/local/bin # System-wide install (needs sudo)
|
|
84
|
+
EOF
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# ──────────────────────────────────────────────
|
|
88
|
+
# Argument parsing
|
|
89
|
+
# ──────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
while [[ $# -gt 0 ]]; do
|
|
92
|
+
case "$1" in
|
|
93
|
+
--version|-v)
|
|
94
|
+
shift; [[ -z "${1:-}" ]] && die "--version requires a value"
|
|
95
|
+
VERSION="$1"; shift ;;
|
|
96
|
+
--install-dir|-d)
|
|
97
|
+
shift; [[ -z "${1:-}" ]] && die "--install-dir requires a value"
|
|
98
|
+
INSTALL_DIR="$1"; shift ;;
|
|
99
|
+
--source)
|
|
100
|
+
shift; [[ -z "${1:-}" ]] && die "--source requires a value"
|
|
101
|
+
if [[ "$1" != "github" && "$1" != "oss" ]]; then
|
|
102
|
+
die "--source must be 'github' or 'oss'"
|
|
103
|
+
fi
|
|
104
|
+
SOURCE="$1"; shift ;;
|
|
105
|
+
--no-path) ADD_TO_PATH=false; shift ;;
|
|
106
|
+
--silent|-s) SILENT=true; shift ;;
|
|
107
|
+
--dry-run) DRY_RUN=true; shift ;;
|
|
108
|
+
--help|-h) usage; exit 0 ;;
|
|
109
|
+
*) die "Unknown argument: $1 (use --help)";;
|
|
110
|
+
esac
|
|
111
|
+
done
|
|
112
|
+
|
|
113
|
+
# ──────────────────────────────────────────────
|
|
114
|
+
# Platform detection
|
|
115
|
+
# ──────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
detect_os() {
|
|
118
|
+
case "$(uname -s)" in
|
|
119
|
+
Linux) echo "linux" ;;
|
|
120
|
+
Darwin) echo "darwin" ;;
|
|
121
|
+
MINGW*|MSYS*|CYGWIN*) echo "win32" ;;
|
|
122
|
+
*) die "Unsupported OS: $(uname -s)" ;;
|
|
123
|
+
esac
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
detect_arch() {
|
|
127
|
+
local arch
|
|
128
|
+
arch=$(uname -m)
|
|
129
|
+
case "$arch" in
|
|
130
|
+
x86_64|amd64) echo "x64" ;;
|
|
131
|
+
aarch64|arm64) echo "arm64" ;;
|
|
132
|
+
*) die "Unsupported architecture: $arch" ;;
|
|
133
|
+
esac
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
detect_libc() {
|
|
137
|
+
# Only relevant on Linux
|
|
138
|
+
if [[ "$(uname -s)" != "Linux" ]]; then
|
|
139
|
+
echo ""
|
|
140
|
+
return
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# Check for musl
|
|
144
|
+
if ldd --version 2>&1 | grep -qi musl; then
|
|
145
|
+
echo "musl"
|
|
146
|
+
elif [[ -f /lib/ld-musl-x86_64.so.1 ]] || [[ -f /lib/ld-musl-aarch64.so.1 ]]; then
|
|
147
|
+
echo "musl"
|
|
148
|
+
else
|
|
149
|
+
echo ""
|
|
150
|
+
fi
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
get_platform_key() {
|
|
154
|
+
local os arch libc
|
|
155
|
+
os=$(detect_os)
|
|
156
|
+
arch=$(detect_arch)
|
|
157
|
+
|
|
158
|
+
if [[ "$os" == "linux" ]]; then
|
|
159
|
+
libc=$(detect_libc)
|
|
160
|
+
if [[ -n "$libc" ]]; then
|
|
161
|
+
echo "linux-${libc}-${arch}"
|
|
162
|
+
else
|
|
163
|
+
echo "linux-${arch}"
|
|
164
|
+
fi
|
|
165
|
+
else
|
|
166
|
+
echo "${os}-${arch}"
|
|
167
|
+
fi
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
get_binary_name() {
|
|
171
|
+
local platform_key="$1"
|
|
172
|
+
local ext=""
|
|
173
|
+
if [[ "$platform_key" == win32-* ]]; then
|
|
174
|
+
ext=".exe"
|
|
175
|
+
fi
|
|
176
|
+
echo "browser4-cli-${platform_key}${ext}"
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
get_default_install_dir() {
|
|
180
|
+
local dir="${HOME}/.local/bin"
|
|
181
|
+
# Prefer existing dir
|
|
182
|
+
if [[ -d "$dir" ]]; then
|
|
183
|
+
echo "$dir"
|
|
184
|
+
return
|
|
185
|
+
fi
|
|
186
|
+
# Check if ~/bin exists
|
|
187
|
+
if [[ -d "${HOME}/bin" ]]; then
|
|
188
|
+
echo "${HOME}/bin"
|
|
189
|
+
return
|
|
190
|
+
fi
|
|
191
|
+
echo "$dir"
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# ──────────────────────────────────────────────
|
|
195
|
+
# Download
|
|
196
|
+
# ──────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
check_commands() {
|
|
199
|
+
if ! command -v curl >/dev/null 2>&1; then
|
|
200
|
+
die "'curl' is required but not found. Install curl and retry."
|
|
201
|
+
fi
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
get_download_urls() {
|
|
205
|
+
local binary_name="$1"
|
|
206
|
+
local version_tag="$2"
|
|
207
|
+
local urls=()
|
|
208
|
+
|
|
209
|
+
if [[ -n "$version_tag" ]]; then
|
|
210
|
+
local gh_url="https://github.com/${GITHUB_REPO}/releases/download/${version_tag}/${binary_name}"
|
|
211
|
+
local oss_url="${OSS_BASE}/releases/download/${version_tag}/${binary_name}"
|
|
212
|
+
else
|
|
213
|
+
# Use 'latest' redirect for GitHub, 'latest' symlink for OSS
|
|
214
|
+
local gh_url="https://github.com/${GITHUB_REPO}/releases/latest/download/${binary_name}"
|
|
215
|
+
local oss_url="${OSS_BASE}/releases/download/latest/${binary_name}"
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
case "$SOURCE" in
|
|
219
|
+
github) urls+=("GitHub Releases|${gh_url}") ;;
|
|
220
|
+
oss) urls+=("Aliyun OSS|${oss_url}") ;;
|
|
221
|
+
*)
|
|
222
|
+
urls+=("GitHub Releases|${gh_url}")
|
|
223
|
+
urls+=("Aliyun OSS|${oss_url}")
|
|
224
|
+
;;
|
|
225
|
+
esac
|
|
226
|
+
|
|
227
|
+
printf '%s\n' "${urls[@]}"
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
download_file() {
|
|
231
|
+
local url="$1"
|
|
232
|
+
local dest="$2"
|
|
233
|
+
local label="$3"
|
|
234
|
+
|
|
235
|
+
step "Trying ${label}..."
|
|
236
|
+
step "URL: $url"
|
|
237
|
+
|
|
238
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
239
|
+
ok "[DRY-RUN] Would download to: $dest"
|
|
240
|
+
return 0
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
local http_code
|
|
244
|
+
http_code=$(curl -sSfL -w "%{http_code}" -o "$dest" \
|
|
245
|
+
${GITHUB_TOKEN:+-H "Authorization: Bearer $GITHUB_TOKEN"} \
|
|
246
|
+
"$url" 2>/dev/null) || true
|
|
247
|
+
|
|
248
|
+
if [[ "$http_code" == "200" ]] || [[ "$http_code" == "302" ]]; then
|
|
249
|
+
local size
|
|
250
|
+
size=$(stat -c%s "$dest" 2>/dev/null || stat -f%z "$dest" 2>/dev/null || echo 0)
|
|
251
|
+
|
|
252
|
+
# Sanity check: binary must be > 100 KB
|
|
253
|
+
if [[ "$size" -gt 102400 ]]; then
|
|
254
|
+
local size_mb
|
|
255
|
+
size_mb=$(awk "BEGIN { printf \"%.1f\", $size / 1048576 }")
|
|
256
|
+
ok "Downloaded ${size_mb} MB"
|
|
257
|
+
return 0
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
warn "Downloaded file too small (${size} bytes) — may be an error page"
|
|
261
|
+
rm -f "$dest"
|
|
262
|
+
return 1
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
warn "Failed: HTTP ${http_code}"
|
|
266
|
+
rm -f "$dest"
|
|
267
|
+
return 1
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# ──────────────────────────────────────────────
|
|
271
|
+
# PATH management
|
|
272
|
+
# ──────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
add_to_shell_rc() {
|
|
275
|
+
local dir="$1"
|
|
276
|
+
|
|
277
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
278
|
+
ok "[DRY-RUN] Would add to shell rc: $dir"
|
|
279
|
+
return
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
# Pick the best rc file
|
|
283
|
+
local rc_file=""
|
|
284
|
+
for candidate in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile"; do
|
|
285
|
+
if [[ -f "$candidate" ]]; then
|
|
286
|
+
rc_file="$candidate"
|
|
287
|
+
break
|
|
288
|
+
fi
|
|
289
|
+
done
|
|
290
|
+
|
|
291
|
+
if [[ -z "$rc_file" ]]; then
|
|
292
|
+
rc_file="$HOME/.profile"
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
if grep -qF "$dir" "$rc_file" 2>/dev/null; then
|
|
296
|
+
ok "PATH entry already in $rc_file"
|
|
297
|
+
return
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
{
|
|
301
|
+
echo ""
|
|
302
|
+
echo "# browser4-cli"
|
|
303
|
+
echo "export PATH=\"$dir:\$PATH\""
|
|
304
|
+
} >> "$rc_file"
|
|
305
|
+
|
|
306
|
+
ok "Added to PATH in $rc_file"
|
|
307
|
+
say " Reload with: source $rc_file"
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# ──────────────────────────────────────────────
|
|
311
|
+
# Symlinks
|
|
312
|
+
# ──────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
create_symlinks() {
|
|
315
|
+
local binary_name="$1"
|
|
316
|
+
local install_dir="$2"
|
|
317
|
+
|
|
318
|
+
local ext=""
|
|
319
|
+
if [[ "$binary_name" == *.exe ]]; then
|
|
320
|
+
ext=".exe"
|
|
321
|
+
fi
|
|
322
|
+
|
|
323
|
+
# 1) Always: browser4-cli -> browser4-cli-<platform>
|
|
324
|
+
local link_name="browser4-cli${ext}"
|
|
325
|
+
local link_path="${install_dir}/${link_name}"
|
|
326
|
+
|
|
327
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
328
|
+
step "[DRY-RUN] Would create symlink: ${link_name} -> ${binary_name}"
|
|
329
|
+
else
|
|
330
|
+
ln -sf "$binary_name" "$link_path"
|
|
331
|
+
ok "Created symlink: ${link_name} -> ${binary_name}"
|
|
332
|
+
fi
|
|
333
|
+
|
|
334
|
+
# 2) Only if no conflict: b4 -> browser4-cli-<platform>
|
|
335
|
+
local short_name="b4${ext}"
|
|
336
|
+
local short_path="${install_dir}/${short_name}"
|
|
337
|
+
|
|
338
|
+
if command -v b4 >/dev/null 2>&1; then
|
|
339
|
+
warn "Skipping short link '${short_name}': 'b4' already found on PATH"
|
|
340
|
+
return
|
|
341
|
+
fi
|
|
342
|
+
|
|
343
|
+
if [[ -e "$short_path" ]] || [[ -L "$short_path" ]]; then
|
|
344
|
+
warn "Skipping short link '${short_name}': already exists in ${install_dir}"
|
|
345
|
+
return
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
349
|
+
step "[DRY-RUN] Would create symlink: ${short_name} -> ${binary_name}"
|
|
350
|
+
else
|
|
351
|
+
ln -sf "$binary_name" "$short_path"
|
|
352
|
+
ok "Created symlink: ${short_name} -> ${binary_name}"
|
|
353
|
+
fi
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
# ──────────────────────────────────────────────
|
|
357
|
+
# Main
|
|
358
|
+
# ──────────────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
main() {
|
|
361
|
+
check_commands
|
|
362
|
+
header
|
|
363
|
+
|
|
364
|
+
# Detect platform
|
|
365
|
+
local platform_key binary_name
|
|
366
|
+
platform_key=$(get_platform_key)
|
|
367
|
+
binary_name=$(get_binary_name "$platform_key")
|
|
368
|
+
|
|
369
|
+
step "Platform: $platform_key"
|
|
370
|
+
step "Binary: $binary_name"
|
|
371
|
+
|
|
372
|
+
# Install directory
|
|
373
|
+
if [[ -z "$INSTALL_DIR" ]]; then
|
|
374
|
+
INSTALL_DIR=$(get_default_install_dir)
|
|
375
|
+
fi
|
|
376
|
+
step "Install: $INSTALL_DIR"
|
|
377
|
+
say ""
|
|
378
|
+
|
|
379
|
+
# Ensure install directory exists
|
|
380
|
+
if [[ ! -d "$INSTALL_DIR" ]]; then
|
|
381
|
+
if [[ "$DRY_RUN" != true ]]; then
|
|
382
|
+
mkdir -p "$INSTALL_DIR"
|
|
383
|
+
fi
|
|
384
|
+
step "Created directory: $INSTALL_DIR"
|
|
385
|
+
fi
|
|
386
|
+
|
|
387
|
+
local binary_path="${INSTALL_DIR}/${binary_name}"
|
|
388
|
+
|
|
389
|
+
# Download if needed
|
|
390
|
+
if [[ -f "$binary_path" ]] && [[ -z "$VERSION" ]]; then
|
|
391
|
+
ok "Binary already installed: $binary_path"
|
|
392
|
+
else
|
|
393
|
+
local urls
|
|
394
|
+
IFS=$'\n' read -r -d '' -a urls < <(get_download_urls "$binary_name" "$VERSION" && printf '\0')
|
|
395
|
+
|
|
396
|
+
if [[ ${#urls[@]} -eq 0 ]]; then
|
|
397
|
+
die "No download URLs configured"
|
|
398
|
+
fi
|
|
399
|
+
|
|
400
|
+
local downloaded=false
|
|
401
|
+
local tmpfile
|
|
402
|
+
tmpfile=$(mktemp)
|
|
403
|
+
|
|
404
|
+
# Ensure cleanup
|
|
405
|
+
cleanup() { rm -f "$tmpfile"; }
|
|
406
|
+
trap cleanup EXIT
|
|
407
|
+
|
|
408
|
+
for entry in "${urls[@]}"; do
|
|
409
|
+
local label url
|
|
410
|
+
label="${entry%%|*}"
|
|
411
|
+
url="${entry#*|}"
|
|
412
|
+
|
|
413
|
+
if download_file "$url" "$tmpfile" "$label"; then
|
|
414
|
+
downloaded=true
|
|
415
|
+
break
|
|
416
|
+
fi
|
|
417
|
+
done
|
|
418
|
+
|
|
419
|
+
if [[ "$downloaded" != true ]]; then
|
|
420
|
+
local tried_msg="Could not download browser4-cli binary."$'\n'$'\n'"Tried:"$'\n'
|
|
421
|
+
for entry in "${urls[@]}"; do
|
|
422
|
+
local l="${entry%%|*}" u="${entry#*|}"
|
|
423
|
+
tried_msg+=" - ${l}: ${u}"$'\n'
|
|
424
|
+
done
|
|
425
|
+
tried_msg+=$'\n'"Please check:"$'\n'
|
|
426
|
+
tried_msg+=" - Network connectivity"$'\n'
|
|
427
|
+
tried_msg+=" - The version/tag exists: ${VERSION:-latest}"$'\n'
|
|
428
|
+
tried_msg+=" - For GitHub rate limits, set GITHUB_TOKEN environment variable"
|
|
429
|
+
die "$tried_msg"
|
|
430
|
+
fi
|
|
431
|
+
|
|
432
|
+
# Move from temp to install dir
|
|
433
|
+
if [[ "$DRY_RUN" != true ]]; then
|
|
434
|
+
rm -f "$binary_path"
|
|
435
|
+
mv "$tmpfile" "$binary_path"
|
|
436
|
+
chmod +x "$binary_path"
|
|
437
|
+
fi
|
|
438
|
+
ok "Installed: $binary_path"
|
|
439
|
+
|
|
440
|
+
trap - EXIT
|
|
441
|
+
fi
|
|
442
|
+
|
|
443
|
+
# Ensure executable
|
|
444
|
+
if [[ "$DRY_RUN" != true ]] && [[ -f "$binary_path" ]]; then
|
|
445
|
+
chmod +x "$binary_path" 2>/dev/null || true
|
|
446
|
+
fi
|
|
447
|
+
|
|
448
|
+
# Create symlinks (browser4-cli -> platform binary, b4 if no conflict)
|
|
449
|
+
say ""
|
|
450
|
+
create_symlinks "$binary_name" "$INSTALL_DIR"
|
|
451
|
+
|
|
452
|
+
# Add to PATH
|
|
453
|
+
if [[ "$ADD_TO_PATH" == true ]]; then
|
|
454
|
+
say ""
|
|
455
|
+
add_to_shell_rc "$INSTALL_DIR"
|
|
456
|
+
fi
|
|
457
|
+
|
|
458
|
+
# Verify
|
|
459
|
+
say ""
|
|
460
|
+
if [[ "$DRY_RUN" != true ]]; then
|
|
461
|
+
if version_output=$("$binary_path" --version 2>&1); then
|
|
462
|
+
echo -e "${color_green}✓ browser4-cli installed successfully${color_reset}"
|
|
463
|
+
say " Version: $version_output"
|
|
464
|
+
else
|
|
465
|
+
echo -e "${color_green}✓ Binary installed at: $binary_path${color_reset}"
|
|
466
|
+
warn "Could not verify --version (this is normal on first install)"
|
|
467
|
+
fi
|
|
468
|
+
else
|
|
469
|
+
echo -e "${color_yellow}[DRY-RUN] Installation plan complete${color_reset}"
|
|
470
|
+
fi
|
|
471
|
+
|
|
472
|
+
say ""
|
|
473
|
+
echo -e "${color_cyan}Run 'browser4-cli --help' to get started.${color_reset}"
|
|
474
|
+
|
|
475
|
+
# If ADD_TO_PATH was used, remind about sourcing
|
|
476
|
+
if [[ "$ADD_TO_PATH" == true ]] && [[ "$DRY_RUN" != true ]]; then
|
|
477
|
+
say ""
|
|
478
|
+
say "To use immediately, run:"
|
|
479
|
+
say " export PATH=\"$INSTALL_DIR:\$PATH\""
|
|
480
|
+
fi
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
main
|
|
@@ -9,6 +9,9 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
9
9
|
export const cliRootDir = join(__dirname, '..');
|
|
10
10
|
const packageJsonPath = join(cliRootDir, 'package.json');
|
|
11
11
|
|
|
12
|
+
/** The only valid npm package name for the CLI. */
|
|
13
|
+
const EXPECTED_PACKAGE_NAME = 'browser4-cli';
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* Reads browser4-cli package metadata.
|
|
14
17
|
*
|
|
@@ -16,6 +19,15 @@ const packageJsonPath = join(cliRootDir, 'package.json');
|
|
|
16
19
|
*/
|
|
17
20
|
export function readPackageMetadata() {
|
|
18
21
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
22
|
+
|
|
23
|
+
if (packageJson.name !== EXPECTED_PACKAGE_NAME) {
|
|
24
|
+
console.error(
|
|
25
|
+
`ERROR: cli/package.json name is "${packageJson.name}", expected "${EXPECTED_PACKAGE_NAME}". ` +
|
|
26
|
+
`Refusing to proceed with an unexpected package name.`
|
|
27
|
+
);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
return {
|
|
20
32
|
name: packageJson.name,
|
|
21
33
|
version: packageJson.version,
|
package/scripts/sync-version.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Syncs the CLI version from cli/
|
|
4
|
+
* Syncs the CLI version from cli/VERSION-CLI to all dependent files.
|
|
5
5
|
*
|
|
6
|
-
* cli/
|
|
6
|
+
* cli/VERSION-CLI is the single source of truth for the CLI version. This
|
|
7
7
|
* allows the backend Maven project and the CLI to be published separately with
|
|
8
8
|
* different versions. This script:
|
|
9
|
-
* - Reads the version from cli/
|
|
10
|
-
* - Strips
|
|
9
|
+
* - Reads the version from cli/VERSION-CLI
|
|
10
|
+
* - Strips any "-SNAPSHOT" suffix (if present)
|
|
11
11
|
* - Fetches the latest published version from npm and compares it
|
|
12
12
|
* against the local version, warning when the bump is neither a
|
|
13
13
|
* patch nor a minor increment.
|
|
14
|
-
* - Writes the
|
|
14
|
+
* - Writes the version to cli/package.json
|
|
15
|
+
* - Writes the version to cli/browser4-cli/Cargo.toml
|
|
15
16
|
* - Updates Cargo.lock to match
|
|
16
17
|
*
|
|
17
18
|
* Usage:
|
|
@@ -126,19 +127,26 @@ function checkVersionBump(published, local) {
|
|
|
126
127
|
|
|
127
128
|
const checkOnly = process.argv.includes("--check");
|
|
128
129
|
|
|
129
|
-
//
|
|
130
|
-
const
|
|
131
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
132
|
-
const pkgName = packageJson.name;
|
|
133
|
-
const pkgVersion = packageJson.version;
|
|
130
|
+
// The npm package name (hard-coded — stable across versions)
|
|
131
|
+
const pkgName = "browser4-cli";
|
|
134
132
|
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
// 1. Read the CLI version from cli/VERSION-CLI (the single source of truth)
|
|
134
|
+
const versionCliPath = join(cliDir, "VERSION-CLI");
|
|
135
|
+
let versionRaw;
|
|
136
|
+
try {
|
|
137
|
+
versionRaw = readFileSync(versionCliPath, "utf-8").trim();
|
|
138
|
+
} catch {
|
|
139
|
+
console.error(`ERROR: Cannot read ${versionCliPath}`);
|
|
137
140
|
process.exit(1);
|
|
138
141
|
}
|
|
139
142
|
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
if (!versionRaw) {
|
|
144
|
+
console.error("ERROR: cli/VERSION-CLI is empty.");
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const version = stripSnapshot(versionRaw);
|
|
149
|
+
if (checkOnly) console.log(`cli/VERSION-CLI: ${version}`);
|
|
142
150
|
|
|
143
151
|
// 2. Compare against the latest published version on npm
|
|
144
152
|
const publishedVersion = getLatestPublishedVersion(pkgName);
|
|
@@ -154,7 +162,34 @@ if (publishedVersion) {
|
|
|
154
162
|
console.log(`Latest published: (not found — package may not be published yet)`);
|
|
155
163
|
}
|
|
156
164
|
|
|
157
|
-
//
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// 3. Sync cli/package.json
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
const packageJsonPath = join(cliDir, "package.json");
|
|
169
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
170
|
+
|
|
171
|
+
if (!packageJson.version) {
|
|
172
|
+
console.error("ERROR: cli/package.json does not contain a version field.");
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (packageJson.version !== version) {
|
|
177
|
+
if (checkOnly) {
|
|
178
|
+
console.error(`MISMATCH: ${packageJsonPath} version is "${packageJson.version}", expected "${version}"`);
|
|
179
|
+
process.exitCode = 1;
|
|
180
|
+
} else {
|
|
181
|
+
const oldVersion = packageJson.version;
|
|
182
|
+
packageJson.version = version;
|
|
183
|
+
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
|
|
184
|
+
console.log(` Updated ${packageJsonPath}: ${oldVersion} -> ${version}`);
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
if (!checkOnly) console.log(` ${packageJsonPath} already up to date`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// 4. Sync cli/browser4-cli/Cargo.toml
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
158
193
|
const cargoTomlPath = join(cargoDir, "Cargo.toml");
|
|
159
194
|
let cargoToml = readFileSync(cargoTomlPath, "utf-8");
|
|
160
195
|
const cargoVersionRegex = /^version\s*=\s*"[^"]*"/m;
|
|
@@ -179,7 +214,9 @@ if (cargoVersion !== version) {
|
|
|
179
214
|
if (!checkOnly) console.log(` ${cargoTomlPath} already up to date`);
|
|
180
215
|
}
|
|
181
216
|
|
|
182
|
-
//
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// 5. Update Cargo.lock (only in sync mode)
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
183
220
|
if (!checkOnly && cargoVersion !== version) {
|
|
184
221
|
let lockUpdated = false;
|
|
185
222
|
try {
|
|
@@ -228,12 +265,12 @@ if (!checkOnly && cargoVersion !== version) {
|
|
|
228
265
|
}
|
|
229
266
|
}
|
|
230
267
|
|
|
231
|
-
//
|
|
268
|
+
// 6. Report
|
|
232
269
|
if (checkOnly) {
|
|
233
270
|
if (process.exitCode === 1) {
|
|
234
271
|
console.error("\nVersion mismatch detected! Run 'node cli/scripts/sync-version.js' to fix.");
|
|
235
272
|
} else {
|
|
236
|
-
console.log(`\nAll versions in sync: ${version}`);
|
|
273
|
+
console.log(`\nAll versions in sync with cli/VERSION-CLI: ${version}`);
|
|
237
274
|
}
|
|
238
275
|
} else {
|
|
239
276
|
console.log(`\nVersion sync complete: ${pkgName}@${version}`);
|