browser4-cli 0.1.16 → 0.1.18
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 +11 -8
- 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 +0 -0
- package/scripts/install-browser4-cli.ps1 +220 -8
- package/scripts/install-browser4-cli.sh +183 -6
- package/scripts/postinstall.js +79 -11
package/README.md
CHANGED
|
@@ -152,7 +152,10 @@ The tables below mirror the commands surfaced by the global `browser4-cli help`
|
|
|
152
152
|
| `uncheck <ref>` | Uncheck a checkbox or radio button |
|
|
153
153
|
| `drag <startRef> <endRef>` | Drag and drop between two elements |
|
|
154
154
|
| `snapshot` | Capture accessibility snapshot |
|
|
155
|
-
| `eval <expression> [ref]` | Evaluate JavaScript on the page or a target element |
|
|
155
|
+
| `eval <expression> [ref]` | Evaluate JavaScript on the page or a target element. Use `--file=<path>` to read the expression from a file. |
|
|
156
|
+
| `get <mode> <selector> [name]` | Extract data from a page element. Modes: `text`, `html`, `box`, `styles`, `property`, `attr`. `name` is required for `property` and `attr`. |
|
|
157
|
+
| `scroll <direction> <pixels>` | Scroll the page. Direction: `up`, `down`, `left`, or `right`. |
|
|
158
|
+
| `wait [target]` | Wait for a condition. Positional: selector or milliseconds. Options: `--text=<text>`, `--url=<glob>`, `--load=<state>` (networkidle/domcontentloaded), `--fn=<JS expr>`. |
|
|
156
159
|
| `dialog-accept [prompt]` | Accept a dialog |
|
|
157
160
|
| `dialog-dismiss` | Dismiss a dialog |
|
|
158
161
|
| `resize <w> <h>` | Resize the browser window |
|
|
@@ -193,12 +196,12 @@ The tables below mirror the commands surfaced by the global `browser4-cli help`
|
|
|
193
196
|
|
|
194
197
|
| Command | Description |
|
|
195
198
|
|---|---|
|
|
196
|
-
| `tab-list` | List all tabs |
|
|
199
|
+
| `tab-list` | List all tabs with their zero-based index |
|
|
197
200
|
| `tab-new [url]` | Create a new tab |
|
|
198
|
-
| `tab-close [index]` | Close a
|
|
199
|
-
| `tab-select <index>` | Select a
|
|
201
|
+
| `tab-close [index]` | Close a tab by its zero-based index |
|
|
202
|
+
| `tab-select <index>` | Select a tab by its zero-based index |
|
|
200
203
|
|
|
201
|
-
|
|
204
|
+
Run `tab-list` first to find each tab's zero-based index. Pass that index to `tab-select` or `tab-close`.
|
|
202
205
|
|
|
203
206
|
#### Browser storage
|
|
204
207
|
|
|
@@ -515,7 +518,7 @@ browser4-cli open --headed https://browser4.io
|
|
|
515
518
|
browser4-cli open --headless https://browser4.io
|
|
516
519
|
|
|
517
520
|
# Navigate to a page — auto-opens a session if none is active
|
|
518
|
-
browser4-cli goto https://
|
|
521
|
+
browser4-cli goto https://browser4.io
|
|
519
522
|
|
|
520
523
|
# Inspect the page — note the eN labels on interactive nodes
|
|
521
524
|
browser4-cli snapshot
|
|
@@ -545,10 +548,10 @@ browser4-cli open --server http://localhost:9090
|
|
|
545
548
|
# Advanced: execute multiple commands in one process (batch mode)
|
|
546
549
|
# Batch mode only supports DOM operations. You must run `open` separately first.
|
|
547
550
|
browser4-cli open
|
|
548
|
-
browser4-cli batch "goto https://
|
|
551
|
+
browser4-cli batch "goto https://browser4.io" "snapshot"
|
|
549
552
|
|
|
550
553
|
# Advanced: stop on the first batch failure
|
|
551
|
-
browser4-cli batch --bail "goto https://
|
|
554
|
+
browser4-cli batch --bail "goto https://browser4.io" "click e1" "screenshot"
|
|
552
555
|
|
|
553
556
|
# Advanced: batch mode for form filling (recommended use case)
|
|
554
557
|
browser4-cli batch "fill e1 'John Doe'" "fill e2 'john@example.com'" "click e3"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
File without changes
|
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
Windows: $env:LOCALAPPDATA\Programs\browser4-cli
|
|
12
12
|
(override with -InstallDir)
|
|
13
13
|
|
|
14
|
-
Download sources (
|
|
15
|
-
1. GitHub Releases
|
|
16
|
-
|
|
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
|
|
17
19
|
|
|
18
20
|
.PARAMETER Version
|
|
19
21
|
Release version tag to download (e.g. "v4.11.0" or "v0.1.12-cli").
|
|
@@ -25,7 +27,7 @@
|
|
|
25
27
|
|
|
26
28
|
.PARAMETER Source
|
|
27
29
|
Force a specific download source: "github" or "oss".
|
|
28
|
-
Default:
|
|
30
|
+
Default (auto): locale-aware — OSS first in China mainland, GitHub first elsewhere.
|
|
29
31
|
|
|
30
32
|
.PARAMETER AddToPath
|
|
31
33
|
Add the install directory to the current user's PATH environment variable.
|
|
@@ -37,6 +39,15 @@
|
|
|
37
39
|
.PARAMETER DryRun
|
|
38
40
|
Print what would be done without actually doing it.
|
|
39
41
|
|
|
42
|
+
.PARAMETER SkipLocal
|
|
43
|
+
Skip checking for a locally-bundled binary alongside the script.
|
|
44
|
+
By default the script looks for the platform binary in its own directory
|
|
45
|
+
before downloading — use this to force a fresh download.
|
|
46
|
+
|
|
47
|
+
.PARAMETER Locate
|
|
48
|
+
Print detection results (OS, architecture, script location, China locale)
|
|
49
|
+
and exit without installing. Useful for diagnostics.
|
|
50
|
+
|
|
40
51
|
.EXAMPLE
|
|
41
52
|
# Quick install — default location, latest version, add to PATH
|
|
42
53
|
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1
|
|
@@ -48,6 +59,24 @@
|
|
|
48
59
|
.EXAMPLE
|
|
49
60
|
# Install from OSS only, custom directory
|
|
50
61
|
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1 -Source oss -InstallDir "C:\tools\browser4"
|
|
62
|
+
|
|
63
|
+
.EXAMPLE
|
|
64
|
+
# Run diagnostics — see what the script detects without installing
|
|
65
|
+
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1 -Locate
|
|
66
|
+
|
|
67
|
+
.EXAMPLE
|
|
68
|
+
# Use a locally-bundled binary (place binary next to the script)
|
|
69
|
+
# The script auto-detects binaries in its own directory
|
|
70
|
+
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1
|
|
71
|
+
|
|
72
|
+
.EXAMPLE
|
|
73
|
+
# Force download even when a local binary exists
|
|
74
|
+
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1 -SkipLocal
|
|
75
|
+
|
|
76
|
+
.EXAMPLE
|
|
77
|
+
# For China mainland: OSS is auto-preferred via locale detection,
|
|
78
|
+
# or force it explicitly
|
|
79
|
+
powershell -ExecutionPolicy Bypass -File install-browser4-cli.ps1 -Source oss
|
|
51
80
|
#>
|
|
52
81
|
|
|
53
82
|
[CmdletBinding()]
|
|
@@ -58,11 +87,65 @@ param(
|
|
|
58
87
|
[string]$Source = "",
|
|
59
88
|
[bool]$AddToPath = $true,
|
|
60
89
|
[switch]$Silent,
|
|
61
|
-
[switch]$DryRun
|
|
90
|
+
[switch]$DryRun,
|
|
91
|
+
[switch]$SkipLocal,
|
|
92
|
+
[switch]$Locate
|
|
62
93
|
)
|
|
63
94
|
|
|
64
95
|
$ErrorActionPreference = "Stop"
|
|
65
96
|
|
|
97
|
+
# ──────────────────────────────────────────────
|
|
98
|
+
# Script location — find ourselves on disk
|
|
99
|
+
# ──────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
# $PSScriptRoot is the directory containing this script (PS 3+).
|
|
102
|
+
# Falls back to $MyInvocation for edge cases (dot-sourced, PS 2).
|
|
103
|
+
$ScriptDir = if ($PSScriptRoot) {
|
|
104
|
+
$PSScriptRoot
|
|
105
|
+
} elseif ($MyInvocation -and $MyInvocation.MyCommand.Path) {
|
|
106
|
+
Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
107
|
+
} else {
|
|
108
|
+
$null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
<#
|
|
112
|
+
.SYNOPSIS
|
|
113
|
+
Search for a pre-downloaded binary near the script (bundled/sideload install).
|
|
114
|
+
Returns the full path if found, $null otherwise.
|
|
115
|
+
#>
|
|
116
|
+
function Find-LocalBinary {
|
|
117
|
+
param([string]$BinaryName)
|
|
118
|
+
|
|
119
|
+
if (-not $ScriptDir) { return $null }
|
|
120
|
+
|
|
121
|
+
$localPath = Join-Path $ScriptDir $BinaryName
|
|
122
|
+
if (Test-Path $localPath -PathType Leaf) {
|
|
123
|
+
$size = (Get-Item $localPath).Length
|
|
124
|
+
if ($size -gt 102400) { # > 100 KB minimum
|
|
125
|
+
return $localPath
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return $null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
<#
|
|
132
|
+
.SYNOPSIS
|
|
133
|
+
Check whether a local binary is usable by querying its version.
|
|
134
|
+
Returns $true if --version executes successfully, $false otherwise.
|
|
135
|
+
#>
|
|
136
|
+
function Test-LocalBinary {
|
|
137
|
+
param([string]$Path)
|
|
138
|
+
|
|
139
|
+
if (-not $Path -or -not (Test-Path $Path)) { return $false }
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
$null = & $Path --version 2>&1
|
|
143
|
+
return ($LASTEXITCODE -eq 0)
|
|
144
|
+
} catch {
|
|
145
|
+
return $false
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
66
149
|
# ──────────────────────────────────────────────
|
|
67
150
|
# OS detection (compatible with PS 5.1+)
|
|
68
151
|
# ──────────────────────────────────────────────
|
|
@@ -154,12 +237,60 @@ function Get-DefaultInstallDir {
|
|
|
154
237
|
throw "Unsupported OS"
|
|
155
238
|
}
|
|
156
239
|
|
|
240
|
+
# ──────────────────────────────────────────────
|
|
241
|
+
# China mainland locale detection (zero-network)
|
|
242
|
+
# ──────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
<#
|
|
245
|
+
.SYNOPSIS
|
|
246
|
+
Detect whether the current system is likely in China mainland.
|
|
247
|
+
Uses only local env vars and .NET APIs — no network calls.
|
|
248
|
+
#>
|
|
249
|
+
function Test-ChinaLocale {
|
|
250
|
+
# 1 — Locale env vars
|
|
251
|
+
$lang = $env:LC_ALL, $env:LANG, $env:LC_CTYPE, $env:LC_MESSAGES | Where-Object { $_ } | Select-Object -First 1
|
|
252
|
+
if ($lang -and ($lang -match '^zh_CN' -or $lang -match '^zh-CN' -or $lang -match '^Chinese \(Simplified\)_China')) {
|
|
253
|
+
return $true
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# 2 — TZ env var
|
|
257
|
+
$tzEnv = $env:TZ
|
|
258
|
+
if ($tzEnv -and ($tzEnv -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$')) {
|
|
259
|
+
return $true
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
# 3 — .NET TimeZoneInfo (works on Windows and Unix PowerShell 7+)
|
|
263
|
+
try {
|
|
264
|
+
$tzId = [System.TimeZoneInfo]::Local.Id
|
|
265
|
+
if ($tzId -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$') {
|
|
266
|
+
return $true
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
# TimeZoneInfo not available (unlikely on PS 5.1+ but guard anyway)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
# 4 — /etc/timezone (PowerShell on Linux/macOS)
|
|
273
|
+
if (-not $script:OSWin -and (Test-Path '/etc/timezone')) {
|
|
274
|
+
try {
|
|
275
|
+
$tz = Get-Content '/etc/timezone' -Raw -ErrorAction Stop
|
|
276
|
+
if ($tz -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$') {
|
|
277
|
+
return $true
|
|
278
|
+
}
|
|
279
|
+
} catch {
|
|
280
|
+
# Permission or read error — skip
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return $false
|
|
285
|
+
}
|
|
286
|
+
|
|
157
287
|
# ──────────────────────────────────────────────
|
|
158
288
|
# Download URLs
|
|
159
289
|
# ──────────────────────────────────────────────
|
|
160
290
|
|
|
161
291
|
$GITHUB_REPO = "platonai/Browser4"
|
|
162
292
|
$OSS_BASE = "https://browser4.oss-cn-beijing.aliyuncs.com"
|
|
293
|
+
$script:ChinaDetected = $false
|
|
163
294
|
|
|
164
295
|
function Get-DownloadUrls {
|
|
165
296
|
param([string]$BinaryName, [string]$VersionTag)
|
|
@@ -181,8 +312,13 @@ function Get-DownloadUrls {
|
|
|
181
312
|
} elseif ($Source -eq "oss") {
|
|
182
313
|
$urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
|
|
183
314
|
} else {
|
|
184
|
-
$
|
|
185
|
-
|
|
315
|
+
if ($script:ChinaDetected) {
|
|
316
|
+
$urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
|
|
317
|
+
$urls += @{ Url = $ghUrl; Label = "GitHub Releases" }
|
|
318
|
+
} else {
|
|
319
|
+
$urls += @{ Url = $ghUrl; Label = "GitHub Releases" }
|
|
320
|
+
$urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
|
|
321
|
+
}
|
|
186
322
|
}
|
|
187
323
|
|
|
188
324
|
return $urls
|
|
@@ -359,9 +495,60 @@ function Main {
|
|
|
359
495
|
Write-Summary "╚════════════════════════════════════════╝" -Color Cyan
|
|
360
496
|
Write-Summary ""
|
|
361
497
|
|
|
498
|
+
# Auto-detect China mainland locale when no explicit source is given
|
|
499
|
+
if (-not $Source) {
|
|
500
|
+
$script:ChinaDetected = Test-ChinaLocale
|
|
501
|
+
if ($script:ChinaDetected) {
|
|
502
|
+
Write-Step "China mainland locale detected: preferring Aliyun OSS mirror."
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
362
506
|
# Detect platform
|
|
363
507
|
$platformKey = Get-PlatformKey
|
|
364
508
|
$binaryName = Get-BinaryName -PlatformKey $platformKey
|
|
509
|
+
|
|
510
|
+
# ── Locate mode: print diagnostics and exit ──
|
|
511
|
+
if ($Locate) {
|
|
512
|
+
Write-Summary "─── Locate / diagnostics ───" -Color Cyan
|
|
513
|
+
Write-Summary ""
|
|
514
|
+
Write-Step "Script dir: $ScriptDir"
|
|
515
|
+
Write-Step "Platform key: $platformKey"
|
|
516
|
+
Write-Step "Binary name: $binaryName"
|
|
517
|
+
Write-Step "Default install: $(Get-DefaultInstallDir)"
|
|
518
|
+
Write-Step "China locale: $script:ChinaDetected"
|
|
519
|
+
Write-Step "Source override: $(if ($Source) { $Source } else { 'auto' })"
|
|
520
|
+
Write-Step "OS: $(if ($script:OSWin) { 'Windows' } elseif ($script:OSMac) { 'macOS' } elseif ($script:OSLinux) { 'Linux' } else { 'Unknown' })"
|
|
521
|
+
|
|
522
|
+
# Check for local binary
|
|
523
|
+
$localPath = Find-LocalBinary -BinaryName $binaryName
|
|
524
|
+
if ($localPath) {
|
|
525
|
+
$localOk = Test-LocalBinary -Path $localPath
|
|
526
|
+
Write-Step "Local binary: $localPath $(if ($localOk) { '(valid)' } else { '(present but --version failed)' })"
|
|
527
|
+
} else {
|
|
528
|
+
Write-Step "Local binary: not found alongside script"
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
# Check for already-installed binary
|
|
532
|
+
$defaultDir = Get-DefaultInstallDir
|
|
533
|
+
$existingPath = Join-Path $defaultDir $binaryName
|
|
534
|
+
if (Test-Path $existingPath) {
|
|
535
|
+
Write-Step "Already installed: $existingPath"
|
|
536
|
+
} else {
|
|
537
|
+
Write-Step "Already installed: not found at $defaultDir"
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
# Show download URLs that would be tried
|
|
541
|
+
$urls = Get-DownloadUrls -BinaryName $binaryName -VersionTag $Version
|
|
542
|
+
Write-Summary ""
|
|
543
|
+
Write-Step "Download order:"
|
|
544
|
+
foreach ($entry in $urls) {
|
|
545
|
+
Write-Step " $($entry.Label): $($entry.Url)"
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
Write-Summary ""
|
|
549
|
+
return
|
|
550
|
+
}
|
|
551
|
+
|
|
365
552
|
Write-Step "Platform: $platformKey"
|
|
366
553
|
Write-Step "Binary: $binaryName"
|
|
367
554
|
|
|
@@ -379,9 +566,33 @@ function Main {
|
|
|
379
566
|
|
|
380
567
|
$binaryPath = Join-Path $installDir $binaryName
|
|
381
568
|
|
|
569
|
+
# ── Local binary discovery (bundled/sideload) ──
|
|
570
|
+
$useLocalBinary = $false
|
|
571
|
+
if (-not $SkipLocal) {
|
|
572
|
+
$localBinaryPath = Find-LocalBinary -BinaryName $binaryName
|
|
573
|
+
if ($localBinaryPath) {
|
|
574
|
+
Write-Step "Found local binary alongside script: $(Split-Path $localBinaryPath -Leaf)"
|
|
575
|
+
if (Test-LocalBinary -Path $localBinaryPath) {
|
|
576
|
+
Write-Check "Local binary verified (--version OK)"
|
|
577
|
+
$useLocalBinary = $true
|
|
578
|
+
} else {
|
|
579
|
+
Write-WarnMsg "Local binary found but --version check failed — will download instead"
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
} elseif ($SkipLocal) {
|
|
583
|
+
Write-Step "Skipping local binary check (-SkipLocal)"
|
|
584
|
+
}
|
|
585
|
+
|
|
382
586
|
# If binary already exists and no version override, skip download
|
|
383
|
-
if ((Test-Path $binaryPath) -and (-not $Version)) {
|
|
587
|
+
if ((Test-Path $binaryPath) -and (-not $Version) -and (-not $useLocalBinary)) {
|
|
384
588
|
Write-Check "Binary already installed: $binaryPath"
|
|
589
|
+
} elseif ($useLocalBinary) {
|
|
590
|
+
# Copy local binary to install dir
|
|
591
|
+
if (-not $DryRun) {
|
|
592
|
+
if (Test-Path $binaryPath) { Remove-Item $binaryPath -Force }
|
|
593
|
+
Copy-Item $localBinaryPath $binaryPath -Force
|
|
594
|
+
}
|
|
595
|
+
Write-Check "Installed (local): $binaryPath"
|
|
385
596
|
} else {
|
|
386
597
|
# Build download URLs
|
|
387
598
|
$urls = Get-DownloadUrls -BinaryName $binaryName -VersionTag $Version
|
|
@@ -411,6 +622,7 @@ Please check:
|
|
|
411
622
|
- Network connectivity
|
|
412
623
|
- The version/tag exists: $Version
|
|
413
624
|
- For GitHub rate limits, set GITHUB_TOKEN environment variable
|
|
625
|
+
- If you have a local copy, place it alongside this script and re-run
|
|
414
626
|
"@
|
|
415
627
|
}
|
|
416
628
|
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
# --version, -v TAG Release tag (e.g. "v4.11.0"). Default: latest.
|
|
17
17
|
# --install-dir, -d DIR Install directory (default: ~/.local/bin).
|
|
18
18
|
# --source SRC Force download source: "github" or "oss".
|
|
19
|
+
# Default (auto): locale-aware — OSS first for China mainland.
|
|
19
20
|
# --no-path Skip adding install dir to PATH.
|
|
21
|
+
# --skip-local Skip checking for a locally-bundled binary.
|
|
22
|
+
# --locate Print detection results and exit (no install).
|
|
20
23
|
# --silent, -s Suppress non-error output.
|
|
21
24
|
# --dry-run Print what would be done without doing it.
|
|
22
25
|
# --help, -h Show this message.
|
|
@@ -36,6 +39,10 @@ SOURCE=""
|
|
|
36
39
|
ADD_TO_PATH=true
|
|
37
40
|
SILENT=false
|
|
38
41
|
DRY_RUN=false
|
|
42
|
+
SKIP_LOCAL=false
|
|
43
|
+
LOCATE_MODE=false
|
|
44
|
+
CHINA_DETECTED=false
|
|
45
|
+
SCRIPT_DIR=""
|
|
39
46
|
|
|
40
47
|
# ──────────────────────────────────────────────
|
|
41
48
|
# Helpers
|
|
@@ -61,6 +68,49 @@ header() {
|
|
|
61
68
|
fi
|
|
62
69
|
}
|
|
63
70
|
|
|
71
|
+
# ──────────────────────────────────────────────
|
|
72
|
+
# Script location — find ourselves on disk
|
|
73
|
+
# ──────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
detect_script_dir() {
|
|
76
|
+
# BASH_SOURCE works even when sourced; prefer it over $0.
|
|
77
|
+
if [[ -n "${BASH_SOURCE[0]:-}" ]] && [[ "${BASH_SOURCE[0]}" != "bash" ]] && [[ "${BASH_SOURCE[0]}" != *stdin* ]]; then
|
|
78
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
79
|
+
elif [[ -n "${0:-}" ]] && [[ "$0" != "bash" ]] && [[ "$0" != "-bash" ]] && [[ -f "$0" ]]; then
|
|
80
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
81
|
+
fi
|
|
82
|
+
# If piped via curl | bash, SCRIPT_DIR stays empty — no local binaries available.
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Search for a pre-downloaded binary near the script (bundled/sideload install).
|
|
86
|
+
# Echoes the full path on success; returns non-zero when not found.
|
|
87
|
+
find_local_binary() {
|
|
88
|
+
local binary_name="$1"
|
|
89
|
+
if [[ -z "$SCRIPT_DIR" ]]; then
|
|
90
|
+
return 1
|
|
91
|
+
fi
|
|
92
|
+
local local_path="${SCRIPT_DIR}/${binary_name}"
|
|
93
|
+
if [[ -f "$local_path" ]]; then
|
|
94
|
+
local size
|
|
95
|
+
size=$(stat -c%s "$local_path" 2>/dev/null || stat -f%z "$local_path" 2>/dev/null || echo 0)
|
|
96
|
+
if [[ "$size" -gt 102400 ]]; then # > 100 KB minimum
|
|
97
|
+
echo "$local_path"
|
|
98
|
+
return 0
|
|
99
|
+
fi
|
|
100
|
+
fi
|
|
101
|
+
return 1
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Check whether a local binary is usable by querying its version.
|
|
105
|
+
# Returns 0 if --version executes successfully, non-zero otherwise.
|
|
106
|
+
test_local_binary() {
|
|
107
|
+
local path="$1"
|
|
108
|
+
if [[ -z "$path" ]] || [[ ! -f "$path" ]]; then
|
|
109
|
+
return 1
|
|
110
|
+
fi
|
|
111
|
+
"$path" --version >/dev/null 2>&1
|
|
112
|
+
}
|
|
113
|
+
|
|
64
114
|
usage() {
|
|
65
115
|
cat <<EOF
|
|
66
116
|
Usage: $(basename "$0") [OPTIONS]
|
|
@@ -70,8 +120,11 @@ Download and install the browser4-cli native binary.
|
|
|
70
120
|
Options:
|
|
71
121
|
--version, -v TAG Release tag (e.g. "v4.11.0"). Default: latest.
|
|
72
122
|
--install-dir, -d DIR Install directory (default: ~/.local/bin).
|
|
73
|
-
--source SRC Force source: "github" or "oss"
|
|
123
|
+
--source SRC Force source: "github" or "oss".
|
|
124
|
+
Default (auto): OSS first for China mainland, GitHub first elsewhere.
|
|
74
125
|
--no-path Skip adding install dir to shell rc file.
|
|
126
|
+
--skip-local Skip checking for a locally-bundled binary alongside this script.
|
|
127
|
+
--locate Print detection results and exit without installing.
|
|
75
128
|
--silent, -s Suppress non-error output.
|
|
76
129
|
--dry-run Print what would be done without doing it.
|
|
77
130
|
--help, -h Show this message.
|
|
@@ -81,6 +134,9 @@ Examples:
|
|
|
81
134
|
$(basename "$0") --version v4.11.0 # Install specific version
|
|
82
135
|
$(basename "$0") --source oss --silent # Silent install from Aliyun OSS
|
|
83
136
|
$(basename "$0") --install-dir /usr/local/bin # System-wide install (needs sudo)
|
|
137
|
+
$(basename "$0") --locate # Run diagnostics (no install)
|
|
138
|
+
$(basename "$0") --skip-local # Force download, ignore bundled binary
|
|
139
|
+
$(basename "$0") --source oss # Force Aliyun OSS (China mainland)
|
|
84
140
|
EOF
|
|
85
141
|
}
|
|
86
142
|
|
|
@@ -103,6 +159,8 @@ while [[ $# -gt 0 ]]; do
|
|
|
103
159
|
fi
|
|
104
160
|
SOURCE="$1"; shift ;;
|
|
105
161
|
--no-path) ADD_TO_PATH=false; shift ;;
|
|
162
|
+
--skip-local) SKIP_LOCAL=true; shift ;;
|
|
163
|
+
--locate) LOCATE_MODE=true; shift ;;
|
|
106
164
|
--silent|-s) SILENT=true; shift ;;
|
|
107
165
|
--dry-run) DRY_RUN=true; shift ;;
|
|
108
166
|
--help|-h) usage; exit 0 ;;
|
|
@@ -110,6 +168,35 @@ while [[ $# -gt 0 ]]; do
|
|
|
110
168
|
esac
|
|
111
169
|
done
|
|
112
170
|
|
|
171
|
+
# ──────────────────────────────────────────────
|
|
172
|
+
# China mainland locale detection (zero-network)
|
|
173
|
+
# ──────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
detect_china_locale() {
|
|
176
|
+
# 1 — Locale env vars
|
|
177
|
+
local lang
|
|
178
|
+
lang="${LC_ALL:-${LANG:-${LC_CTYPE:-${LC_MESSAGES:-}}}}"
|
|
179
|
+
case "$lang" in
|
|
180
|
+
zh_CN*|zh-CN*|"Chinese (Simplified)_China"*) return 0 ;;
|
|
181
|
+
esac
|
|
182
|
+
|
|
183
|
+
# 2 — TZ env var
|
|
184
|
+
case "${TZ:-}" in
|
|
185
|
+
Asia/Shanghai|Asia/Chongqing|Asia/Urumqi|Asia/Harbin) return 0 ;;
|
|
186
|
+
esac
|
|
187
|
+
|
|
188
|
+
# 3 — /etc/timezone
|
|
189
|
+
if [[ -f /etc/timezone ]]; then
|
|
190
|
+
local tz
|
|
191
|
+
tz=$(cat /etc/timezone 2>/dev/null || true)
|
|
192
|
+
case "$tz" in
|
|
193
|
+
Asia/Shanghai|Asia/Chongqing|Asia/Urumqi|Asia/Harbin) return 0 ;;
|
|
194
|
+
esac
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
return 1
|
|
198
|
+
}
|
|
199
|
+
|
|
113
200
|
# ──────────────────────────────────────────────
|
|
114
201
|
# Platform detection
|
|
115
202
|
# ──────────────────────────────────────────────
|
|
@@ -245,8 +332,13 @@ get_download_urls() {
|
|
|
245
332
|
github) urls+=("GitHub Releases|${gh_url}") ;;
|
|
246
333
|
oss) urls+=("Aliyun OSS|${oss_url}") ;;
|
|
247
334
|
*)
|
|
248
|
-
|
|
249
|
-
|
|
335
|
+
if [[ "$CHINA_DETECTED" == true ]]; then
|
|
336
|
+
urls+=("Aliyun OSS|${oss_url}")
|
|
337
|
+
urls+=("GitHub Releases|${gh_url}")
|
|
338
|
+
else
|
|
339
|
+
urls+=("GitHub Releases|${gh_url}")
|
|
340
|
+
urls+=("Aliyun OSS|${oss_url}")
|
|
341
|
+
fi
|
|
250
342
|
;;
|
|
251
343
|
esac
|
|
252
344
|
|
|
@@ -430,11 +522,70 @@ main() {
|
|
|
430
522
|
check_commands
|
|
431
523
|
header
|
|
432
524
|
|
|
525
|
+
# Locate ourselves on disk (only works when run as a file, not piped)
|
|
526
|
+
detect_script_dir
|
|
527
|
+
|
|
528
|
+
# Auto-detect China mainland locale when no explicit source is given
|
|
529
|
+
if [[ -z "$SOURCE" ]]; then
|
|
530
|
+
if detect_china_locale; then
|
|
531
|
+
CHINA_DETECTED=true
|
|
532
|
+
step "China mainland locale detected: preferring Aliyun OSS mirror."
|
|
533
|
+
fi
|
|
534
|
+
fi
|
|
535
|
+
|
|
433
536
|
# Detect platform
|
|
434
537
|
local platform_key binary_name
|
|
435
538
|
platform_key=$(get_platform_key)
|
|
436
539
|
binary_name=$(get_binary_name "$platform_key")
|
|
437
540
|
|
|
541
|
+
# ── Locate mode: print diagnostics and exit ──
|
|
542
|
+
if [[ "$LOCATE_MODE" == true ]]; then
|
|
543
|
+
echo -e "${color_cyan}─── Locate / diagnostics ───${color_reset}"
|
|
544
|
+
echo ""
|
|
545
|
+
step "Script dir: ${SCRIPT_DIR:-'(not available — piped via curl?)'}"
|
|
546
|
+
step "Platform key: $platform_key"
|
|
547
|
+
step "Binary name: $binary_name"
|
|
548
|
+
step "Default install: $(get_default_install_dir)"
|
|
549
|
+
step "China locale: $CHINA_DETECTED"
|
|
550
|
+
step "Source override: ${SOURCE:-auto}"
|
|
551
|
+
step "OS: $(uname -s)"
|
|
552
|
+
|
|
553
|
+
# Check for local binary
|
|
554
|
+
local locate_local
|
|
555
|
+
if locate_local=$(find_local_binary "$binary_name" 2>/dev/null); then
|
|
556
|
+
local local_status="(present but --version failed)"
|
|
557
|
+
if test_local_binary "$locate_local"; then
|
|
558
|
+
local_status="(valid)"
|
|
559
|
+
fi
|
|
560
|
+
step "Local binary: ${locate_local} ${local_status}"
|
|
561
|
+
else
|
|
562
|
+
step "Local binary: not found alongside script"
|
|
563
|
+
fi
|
|
564
|
+
|
|
565
|
+
# Check for already-installed binary
|
|
566
|
+
local default_dir existing_path
|
|
567
|
+
default_dir=$(get_default_install_dir)
|
|
568
|
+
existing_path="${default_dir}/${binary_name}"
|
|
569
|
+
if [[ -f "$existing_path" ]]; then
|
|
570
|
+
step "Already installed: $existing_path"
|
|
571
|
+
else
|
|
572
|
+
step "Already installed: not found at $default_dir"
|
|
573
|
+
fi
|
|
574
|
+
|
|
575
|
+
# Show download URLs that would be tried
|
|
576
|
+
local locate_urls
|
|
577
|
+
IFS=$'\n' read -r -d '' -a locate_urls < <(get_download_urls "$binary_name" "$VERSION" && printf '\0')
|
|
578
|
+
echo ""
|
|
579
|
+
step "Download order:"
|
|
580
|
+
for entry in "${locate_urls[@]}"; do
|
|
581
|
+
local l="${entry%%|*}" u="${entry#*|}"
|
|
582
|
+
step " ${l}: ${u}"
|
|
583
|
+
done
|
|
584
|
+
|
|
585
|
+
echo ""
|
|
586
|
+
return
|
|
587
|
+
fi
|
|
588
|
+
|
|
438
589
|
step "Platform: $platform_key"
|
|
439
590
|
step "Binary: $binary_name"
|
|
440
591
|
|
|
@@ -455,9 +606,34 @@ main() {
|
|
|
455
606
|
|
|
456
607
|
local binary_path="${INSTALL_DIR}/${binary_name}"
|
|
457
608
|
|
|
458
|
-
#
|
|
459
|
-
|
|
609
|
+
# ── Local binary discovery (bundled/sideload) ──
|
|
610
|
+
local use_local_binary=false
|
|
611
|
+
local local_binary_path=""
|
|
612
|
+
if [[ "$SKIP_LOCAL" != true ]]; then
|
|
613
|
+
if local_binary_path=$(find_local_binary "$binary_name" 2>/dev/null) && [[ -n "$local_binary_path" ]]; then
|
|
614
|
+
step "Found local binary alongside script: $(basename "$local_binary_path")"
|
|
615
|
+
if test_local_binary "$local_binary_path"; then
|
|
616
|
+
ok "Local binary verified (--version OK)"
|
|
617
|
+
use_local_binary=true
|
|
618
|
+
else
|
|
619
|
+
warn "Local binary found but --version check failed — will download instead"
|
|
620
|
+
fi
|
|
621
|
+
fi
|
|
622
|
+
else
|
|
623
|
+
step "Skipping local binary check (--skip-local)"
|
|
624
|
+
fi
|
|
625
|
+
|
|
626
|
+
# Install binary: local copy > already installed > download
|
|
627
|
+
if [[ -f "$binary_path" ]] && [[ -z "$VERSION" ]] && [[ "$use_local_binary" != true ]]; then
|
|
460
628
|
ok "Binary already installed: $binary_path"
|
|
629
|
+
elif [[ "$use_local_binary" == true ]]; then
|
|
630
|
+
# Copy local binary to install dir
|
|
631
|
+
if [[ "$DRY_RUN" != true ]]; then
|
|
632
|
+
rm -f "$binary_path"
|
|
633
|
+
cp "$local_binary_path" "$binary_path"
|
|
634
|
+
chmod +x "$binary_path"
|
|
635
|
+
fi
|
|
636
|
+
ok "Installed (local): $binary_path"
|
|
461
637
|
else
|
|
462
638
|
local urls
|
|
463
639
|
IFS=$'\n' read -r -d '' -a urls < <(get_download_urls "$binary_name" "$VERSION" && printf '\0')
|
|
@@ -494,7 +670,8 @@ main() {
|
|
|
494
670
|
tried_msg+=$'\n'"Please check:"$'\n'
|
|
495
671
|
tried_msg+=" - Network connectivity"$'\n'
|
|
496
672
|
tried_msg+=" - The version/tag exists: ${VERSION:-latest}"$'\n'
|
|
497
|
-
tried_msg+=" - For GitHub rate limits, set GITHUB_TOKEN environment variable"
|
|
673
|
+
tried_msg+=" - For GitHub rate limits, set GITHUB_TOKEN environment variable"$'\n'
|
|
674
|
+
tried_msg+=" - If you have a local copy, place it alongside this script and re-run"
|
|
498
675
|
die "$tried_msg"
|
|
499
676
|
fi
|
|
500
677
|
|
package/scripts/postinstall.js
CHANGED
|
@@ -45,9 +45,65 @@ const packageJson = JSON.parse(
|
|
|
45
45
|
const version = packageJson.version;
|
|
46
46
|
const commandNames = Object.keys(packageJson.bin || { 'browser4-cli': './bin/browser4-cli.js' });
|
|
47
47
|
|
|
48
|
-
//
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// China mainland locale detection (zero-network — uses only local env / Intl)
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
function isChinaLocale() {
|
|
53
|
+
// 1 — Locale env vars (Unix, Git Bash, WSL, Docker)
|
|
54
|
+
const lang = process.env.LC_ALL
|
|
55
|
+
|| process.env.LANG
|
|
56
|
+
|| process.env.LC_CTYPE
|
|
57
|
+
|| process.env.LC_MESSAGES
|
|
58
|
+
|| '';
|
|
59
|
+
if (/^(?:zh_CN|zh-CN|Chinese \(Simplified\)_China)/i.test(lang)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 2 — TZ env var (Docker, CI)
|
|
64
|
+
const tz = process.env.TZ || '';
|
|
65
|
+
if (/^Asia\/(Shanghai|Chongqing|Urumqi|Harbin)$/.test(tz)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 3 — Intl timezone API (V8/Node.js, no filesystem access needed)
|
|
70
|
+
try {
|
|
71
|
+
const resolvedTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
72
|
+
if (/^Asia\/(Shanghai|Chongqing|Urumqi|Harbin)$/.test(resolvedTz)) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// Intl not available (very old Node) — skip
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Download URLs (locale-aware ordering)
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
49
86
|
const GITHUB_REPO = 'platonai/Browser4';
|
|
50
|
-
const
|
|
87
|
+
const DOWNLOAD_URL_GITHUB = `https://github.com/${GITHUB_REPO}/releases/download/v${version}/${binaryName}`;
|
|
88
|
+
const DOWNLOAD_URL_OSS = `https://browser4.oss-cn-beijing.aliyuncs.com/releases/download/v${version}/${binaryName}`;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Return download URLs ordered by locale preference. `BROWSER4_RELEASES_BASE_URL`
|
|
92
|
+
* takes precedence; otherwise China mainland users get OSS-first ordering.
|
|
93
|
+
*/
|
|
94
|
+
function getDownloadUrls() {
|
|
95
|
+
// Single-source override
|
|
96
|
+
const envOverride = (process.env.BROWSER4_RELEASES_BASE_URL || '').trim();
|
|
97
|
+
if (envOverride) {
|
|
98
|
+
const base = envOverride.replace(/\/+$/, '');
|
|
99
|
+
return [`${base}/download/v${version}/${binaryName}`];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (isChinaLocale()) {
|
|
103
|
+
return [DOWNLOAD_URL_OSS, DOWNLOAD_URL_GITHUB];
|
|
104
|
+
}
|
|
105
|
+
return [DOWNLOAD_URL_GITHUB, DOWNLOAD_URL_OSS];
|
|
106
|
+
}
|
|
51
107
|
|
|
52
108
|
async function downloadFile(url, dest) {
|
|
53
109
|
return new Promise((resolve, reject) => {
|
|
@@ -130,19 +186,31 @@ async function main() {
|
|
|
130
186
|
}
|
|
131
187
|
|
|
132
188
|
console.log(`Downloading native binary for ${platformKey}...`);
|
|
133
|
-
console.log(`URL: ${DOWNLOAD_URL}`);
|
|
134
189
|
|
|
135
|
-
|
|
136
|
-
await downloadFile(DOWNLOAD_URL, binaryPath);
|
|
190
|
+
const urls = getDownloadUrls();
|
|
137
191
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
192
|
+
let lastError = null;
|
|
193
|
+
for (const url of urls) {
|
|
194
|
+
console.log(`URL: ${url}`);
|
|
195
|
+
try {
|
|
196
|
+
await downloadFile(url, binaryPath);
|
|
197
|
+
|
|
198
|
+
// Make executable on Unix
|
|
199
|
+
if (platform() !== 'win32') {
|
|
200
|
+
chmodSync(binaryPath, 0o755);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
console.log(`✓ Downloaded native binary: ${binaryName}`);
|
|
204
|
+
lastError = null;
|
|
205
|
+
break;
|
|
206
|
+
} catch (err) {
|
|
207
|
+
console.log(`Download failed: ${err.message}`);
|
|
208
|
+
lastError = err;
|
|
141
209
|
}
|
|
210
|
+
}
|
|
142
211
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
console.log(`Could not download native binary: ${err.message}`);
|
|
212
|
+
if (lastError) {
|
|
213
|
+
console.log(`Could not download native binary from any mirror: ${lastError.message}`);
|
|
146
214
|
console.log('');
|
|
147
215
|
console.log('To build the native binary locally:');
|
|
148
216
|
console.log(' 1. Install Rust: https://rustup.rs');
|