browser4-cli 0.1.11 → 0.1.13

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 CHANGED
@@ -37,6 +37,28 @@ 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
 
42
64
  ```bash
@@ -242,7 +264,7 @@ Like other advanced commands, they are intentionally omitted from the global
242
264
  Use the spaced `agent <subcommand>` form:
243
265
 
244
266
  ```shell
245
- browser4-cli agent run "Open example.com and summarize the hero section"
267
+ browser4-cli agent run "Open browser4.io and summarize the hero section"
246
268
  browser4-cli agent status agent-task-1
247
269
  browser4-cli agent result agent-task-1
248
270
  ```
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser4-cli",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Browser automation CLI for AI agents",
5
5
  "type": "module",
6
6
  "files": [
@@ -0,0 +1,379 @@
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
+ # PATH management
221
+ # ──────────────────────────────────────────────
222
+
223
+ function Add-DirectoryToUserPath {
224
+ param([string]$Dir)
225
+
226
+ $dirResolved = (Resolve-Path $Dir -ErrorAction SilentlyContinue).Path
227
+ if (-not $dirResolved) { $dirResolved = $Dir }
228
+
229
+ # Read current user PATH
230
+ $currentUserPath = [System.Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::User)
231
+ $paths = if ($currentUserPath) { $currentUserPath -split ";" | Where-Object { $_ } } else { @() }
232
+
233
+ # Check if already present
234
+ $normalized = $paths | ForEach-Object { $rp = Resolve-Path $_ -ErrorAction SilentlyContinue; if ($rp) { $rp.Path } else { $_ } }
235
+ if ($normalized -contains $dirResolved) {
236
+ Write-Check "Already in user PATH: $Dir"
237
+ return
238
+ }
239
+
240
+ if ($DryRun) {
241
+ Write-Check "[DRY-RUN] Would add to user PATH: $Dir"
242
+ return
243
+ }
244
+
245
+ $newPath = if ($currentUserPath) { "$currentUserPath;$Dir" } else { $Dir }
246
+ [System.Environment]::SetEnvironmentVariable("Path", $newPath, [System.EnvironmentVariableTarget]::User)
247
+ Write-Check "Added to user PATH: $Dir"
248
+
249
+ # Also update current session
250
+ $env:Path = "$env:Path;$Dir"
251
+ }
252
+
253
+ # ──────────────────────────────────────────────
254
+ # Main
255
+ # ──────────────────────────────────────────────
256
+
257
+ function Main {
258
+ Write-Summary "╔════════════════════════════════════════╗" -Color Cyan
259
+ Write-Summary "║ browser4-cli Installer ║" -Color Cyan
260
+ Write-Summary "╚════════════════════════════════════════╝" -Color Cyan
261
+ Write-Summary ""
262
+
263
+ # Detect platform
264
+ $platformKey = Get-PlatformKey
265
+ $binaryName = Get-BinaryName -PlatformKey $platformKey
266
+ Write-Step "Platform: $platformKey"
267
+ Write-Step "Binary: $binaryName"
268
+
269
+ # Determine install directory
270
+ $installDir = if ($InstallDir) { $InstallDir } else { Get-DefaultInstallDir }
271
+ Write-Step "Install: $installDir"
272
+ Write-Summary ""
273
+
274
+ if (-not (Test-Path $installDir)) {
275
+ if (-not $DryRun) {
276
+ New-Item -ItemType Directory -Path $installDir -Force | Out-Null
277
+ }
278
+ Write-Step "Created directory: $installDir"
279
+ }
280
+
281
+ $binaryPath = Join-Path $installDir $binaryName
282
+
283
+ # If binary already exists and no version override, skip download
284
+ if ((Test-Path $binaryPath) -and (-not $Version)) {
285
+ Write-Check "Binary already installed: $binaryPath"
286
+ } else {
287
+ # Build download URLs
288
+ $urls = Get-DownloadUrls -BinaryName $binaryName -VersionTag $Version
289
+ if (-not $urls -or $urls.Count -eq 0) {
290
+ throw "No download URLs configured"
291
+ }
292
+
293
+ $downloaded = $false
294
+ $tempFile = [System.IO.Path]::GetTempFileName()
295
+
296
+ foreach ($entry in $urls) {
297
+ if (Invoke-Download -Url $entry.Url -OutFile $tempFile -Label $entry.Label) {
298
+ $downloaded = $true
299
+ break
300
+ }
301
+ }
302
+
303
+ if (-not $downloaded) {
304
+ if (Test-Path $tempFile) { Remove-Item $tempFile -Force }
305
+ throw @"
306
+ Could not download browser4-cli binary.
307
+
308
+ Tried:
309
+ $($urls | ForEach-Object { " - $($_.Label): $($_.Url)" } | Out-String)
310
+
311
+ Please check:
312
+ - Network connectivity
313
+ - The version/tag exists: $Version
314
+ - For GitHub rate limits, set GITHUB_TOKEN environment variable
315
+ "@
316
+ }
317
+
318
+ # Move from temp to install dir
319
+ if (-not $DryRun) {
320
+ if (Test-Path $binaryPath) { Remove-Item $binaryPath -Force }
321
+ Move-Item $tempFile $binaryPath -Force
322
+ }
323
+ Write-Check "Installed: $binaryPath"
324
+ }
325
+
326
+ # On Unix, ensure executable bit
327
+ if (-not $script:IsWin) {
328
+ if (-not $DryRun) {
329
+ try { chmod +x $binaryPath 2>$null } catch { }
330
+ }
331
+ }
332
+
333
+ # Add to PATH
334
+ if ($AddToPath -and $script:IsWin) {
335
+ Write-Summary ""
336
+ Add-DirectoryToUserPath -Dir $installDir
337
+ } elseif ($AddToPath -and -not $script:IsWin) {
338
+ Write-Summary ""
339
+ $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" }
340
+ $pathLine = "export PATH=""$installDir`:`$PATH"""
341
+ if (-not $DryRun) {
342
+ if (-not (Select-String -Path $shellRc -Pattern [regex]::Escape($installDir) -ErrorAction SilentlyContinue)) {
343
+ Add-Content -Path $shellRc -Value ""
344
+ Add-Content -Path $shellRc -Value "# browser4-cli"
345
+ Add-Content -Path $shellRc -Value $pathLine
346
+ Write-Check "Added to PATH in $shellRc"
347
+ } else {
348
+ Write-Check "PATH entry already in $shellRc"
349
+ }
350
+ } else {
351
+ Write-Check "[DRY-RUN] Would add to $shellRc"
352
+ }
353
+ }
354
+
355
+ # Verify
356
+ Write-Summary ""
357
+ if (-not $DryRun) {
358
+ try {
359
+ $versionOutput = & $binaryPath --version 2>&1
360
+ Write-Summary "✓ browser4-cli installed successfully" -Color Green
361
+ Write-Summary " Version: $versionOutput"
362
+ } catch {
363
+ Write-Summary "✓ Binary installed at: $binaryPath" -Color Green
364
+ Write-WarnMsg "Could not verify --version (this is normal on first install)"
365
+ }
366
+ } else {
367
+ Write-Summary "[DRY-RUN] Installation plan complete" -Color Yellow
368
+ }
369
+
370
+ Write-Summary ""
371
+ Write-Summary "Run 'browser4-cli --help' to get started." -Color Cyan
372
+
373
+ if ($script:IsWin) {
374
+ Write-Summary "If the command isn't found, restart your terminal or run:"
375
+ Write-Summary " `$env:Path = [System.Environment]::GetEnvironmentVariable('Path','User') + ';' + [System.Environment]::GetEnvironmentVariable('Path','Machine')"
376
+ }
377
+ }
378
+
379
+ Main
@@ -0,0 +1,433 @@
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
+ # Main
312
+ # ──────────────────────────────────────────────
313
+
314
+ main() {
315
+ check_commands
316
+ header
317
+
318
+ # Detect platform
319
+ local platform_key binary_name
320
+ platform_key=$(get_platform_key)
321
+ binary_name=$(get_binary_name "$platform_key")
322
+
323
+ step "Platform: $platform_key"
324
+ step "Binary: $binary_name"
325
+
326
+ # Install directory
327
+ if [[ -z "$INSTALL_DIR" ]]; then
328
+ INSTALL_DIR=$(get_default_install_dir)
329
+ fi
330
+ step "Install: $INSTALL_DIR"
331
+ say ""
332
+
333
+ # Ensure install directory exists
334
+ if [[ ! -d "$INSTALL_DIR" ]]; then
335
+ if [[ "$DRY_RUN" != true ]]; then
336
+ mkdir -p "$INSTALL_DIR"
337
+ fi
338
+ step "Created directory: $INSTALL_DIR"
339
+ fi
340
+
341
+ local binary_path="${INSTALL_DIR}/${binary_name}"
342
+
343
+ # Download if needed
344
+ if [[ -f "$binary_path" ]] && [[ -z "$VERSION" ]]; then
345
+ ok "Binary already installed: $binary_path"
346
+ else
347
+ local urls
348
+ IFS=$'\n' read -r -d '' -a urls < <(get_download_urls "$binary_name" "$VERSION" && printf '\0')
349
+
350
+ if [[ ${#urls[@]} -eq 0 ]]; then
351
+ die "No download URLs configured"
352
+ fi
353
+
354
+ local downloaded=false
355
+ local tmpfile
356
+ tmpfile=$(mktemp)
357
+
358
+ # Ensure cleanup
359
+ cleanup() { rm -f "$tmpfile"; }
360
+ trap cleanup EXIT
361
+
362
+ for entry in "${urls[@]}"; do
363
+ local label url
364
+ label="${entry%%|*}"
365
+ url="${entry#*|}"
366
+
367
+ if download_file "$url" "$tmpfile" "$label"; then
368
+ downloaded=true
369
+ break
370
+ fi
371
+ done
372
+
373
+ if [[ "$downloaded" != true ]]; then
374
+ local tried_msg="Could not download browser4-cli binary."$'\n'$'\n'"Tried:"$'\n'
375
+ for entry in "${urls[@]}"; do
376
+ local l="${entry%%|*}" u="${entry#*|}"
377
+ tried_msg+=" - ${l}: ${u}"$'\n'
378
+ done
379
+ tried_msg+=$'\n'"Please check:"$'\n'
380
+ tried_msg+=" - Network connectivity"$'\n'
381
+ tried_msg+=" - The version/tag exists: ${VERSION:-latest}"$'\n'
382
+ tried_msg+=" - For GitHub rate limits, set GITHUB_TOKEN environment variable"
383
+ die "$tried_msg"
384
+ fi
385
+
386
+ # Move from temp to install dir
387
+ if [[ "$DRY_RUN" != true ]]; then
388
+ rm -f "$binary_path"
389
+ mv "$tmpfile" "$binary_path"
390
+ chmod +x "$binary_path"
391
+ fi
392
+ ok "Installed: $binary_path"
393
+
394
+ trap - EXIT
395
+ fi
396
+
397
+ # Ensure executable
398
+ if [[ "$DRY_RUN" != true ]] && [[ -f "$binary_path" ]]; then
399
+ chmod +x "$binary_path" 2>/dev/null || true
400
+ fi
401
+
402
+ # Add to PATH
403
+ if [[ "$ADD_TO_PATH" == true ]]; then
404
+ say ""
405
+ add_to_shell_rc "$INSTALL_DIR"
406
+ fi
407
+
408
+ # Verify
409
+ say ""
410
+ if [[ "$DRY_RUN" != true ]]; then
411
+ if version_output=$("$binary_path" --version 2>&1); then
412
+ echo -e "${color_green}✓ browser4-cli installed successfully${color_reset}"
413
+ say " Version: $version_output"
414
+ else
415
+ echo -e "${color_green}✓ Binary installed at: $binary_path${color_reset}"
416
+ warn "Could not verify --version (this is normal on first install)"
417
+ fi
418
+ else
419
+ echo -e "${color_yellow}[DRY-RUN] Installation plan complete${color_reset}"
420
+ fi
421
+
422
+ say ""
423
+ echo -e "${color_cyan}Run 'browser4-cli --help' to get started.${color_reset}"
424
+
425
+ # If ADD_TO_PATH was used, remind about sourcing
426
+ if [[ "$ADD_TO_PATH" == true ]] && [[ "$DRY_RUN" != true ]]; then
427
+ say ""
428
+ say "To use immediately, run:"
429
+ say " export PATH=\"$INSTALL_DIR:\$PATH\""
430
+ fi
431
+ }
432
+
433
+ main
@@ -181,12 +181,14 @@ if (cargoVersion !== version) {
181
181
 
182
182
  // 4. Update Cargo.lock (only in sync mode)
183
183
  if (!checkOnly && cargoVersion !== version) {
184
+ let lockUpdated = false;
184
185
  try {
185
186
  execSync("cargo update -p browser4-cli --offline", {
186
187
  cwd: cargoDir,
187
188
  stdio: "pipe",
188
189
  });
189
190
  console.log(" Updated Cargo.lock");
191
+ lockUpdated = true;
190
192
  } catch {
191
193
  try {
192
194
  execSync("cargo update -p browser4-cli", {
@@ -194,8 +196,34 @@ if (!checkOnly && cargoVersion !== version) {
194
196
  stdio: "pipe",
195
197
  });
196
198
  console.log(" Updated Cargo.lock");
199
+ lockUpdated = true;
197
200
  } catch (e) {
198
- console.error(` Warning: Could not update Cargo.lock: ${e.message}`);
201
+ console.error(` Warning: Could not update Cargo.lock via cargo: ${e.message}`);
202
+ }
203
+ }
204
+
205
+ // Fallback: edit Cargo.lock directly when cargo update fails (e.g. due to
206
+ // lock file version mismatch between the installed cargo and the lock file).
207
+ if (!lockUpdated) {
208
+ const cargoLockPath = join(cargoDir, "Cargo.lock");
209
+ try {
210
+ let lockContent = readFileSync(cargoLockPath, "utf-8");
211
+ // Match the [[package]] block for browser4-cli and replace its version.
212
+ // Works for both v3 and v4 lock file formats.
213
+ const lockPkgRegex = new RegExp(
214
+ `(\\[\\[package\\]\\][\\r\\n]+\\s*name\\s*=\\s*"browser4-cli"[\\r\\n]+\\s*version\\s*=\\s*)"[^"]*"`,
215
+ "m"
216
+ );
217
+ if (lockPkgRegex.test(lockContent)) {
218
+ lockContent = lockContent.replace(lockPkgRegex, `$1"${version}"`);
219
+ writeFileSync(cargoLockPath, lockContent);
220
+ console.log(` Updated Cargo.lock directly: ${cargoVersion} -> ${version}`);
221
+ lockUpdated = true;
222
+ } else {
223
+ console.error(" Warning: Could not find browser4-cli entry in Cargo.lock");
224
+ }
225
+ } catch (e2) {
226
+ console.error(` Warning: Could not update Cargo.lock directly: ${e2.message}`);
199
227
  }
200
228
  }
201
229
  }