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 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 browser tab by zero-based index |
199
- | `tab-select <index>` | Select a browser tab by zero-based index |
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
- Use `tab-list` first to find the zero-based tab index you want to select or close.
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://playwright.dev
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://playwright.dev" "snapshot"
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://playwright.dev" "click e1" "screenshot"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser4-cli",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Browser automation CLI for AI agents",
5
5
  "type": "module",
6
6
  "files": [
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 (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
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: try GitHub first, fall back to OSS.
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
- $urls += @{ Url = $ghUrl; Label = "GitHub Releases" }
185
- $urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
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" (default: try both).
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
- urls+=("GitHub Releases|${gh_url}")
249
- urls+=("Aliyun OSS|${oss_url}")
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
- # Download if needed
459
- if [[ -f "$binary_path" ]] && [[ -z "$VERSION" ]]; then
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
 
@@ -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
- // GitHub release URL
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 DOWNLOAD_URL = `https://github.com/${GITHUB_REPO}/releases/download/v${version}/${binaryName}`;
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
- try {
136
- await downloadFile(DOWNLOAD_URL, binaryPath);
190
+ const urls = getDownloadUrls();
137
191
 
138
- // Make executable on Unix
139
- if (platform() !== 'win32') {
140
- chmodSync(binaryPath, 0o755);
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
- console.log(`✓ Downloaded native binary: ${binaryName}`);
144
- } catch (err) {
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');