browser4-cli 0.1.15 → 0.1.17

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.
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.15",
3
+ "version": "0.1.17",
4
4
  "description": "Browser automation CLI for AI agents",
5
5
  "type": "module",
6
6
  "files": [
@@ -154,12 +154,60 @@ function Get-DefaultInstallDir {
154
154
  throw "Unsupported OS"
155
155
  }
156
156
 
157
+ # ──────────────────────────────────────────────
158
+ # China mainland locale detection (zero-network)
159
+ # ──────────────────────────────────────────────
160
+
161
+ <#
162
+ .SYNOPSIS
163
+ Detect whether the current system is likely in China mainland.
164
+ Uses only local env vars and .NET APIs — no network calls.
165
+ #>
166
+ function Test-ChinaLocale {
167
+ # 1 — Locale env vars
168
+ $lang = $env:LC_ALL, $env:LANG, $env:LC_CTYPE, $env:LC_MESSAGES | Where-Object { $_ } | Select-Object -First 1
169
+ if ($lang -and ($lang -match '^zh_CN' -or $lang -match '^zh-CN' -or $lang -match '^Chinese \(Simplified\)_China')) {
170
+ return $true
171
+ }
172
+
173
+ # 2 — TZ env var
174
+ $tzEnv = $env:TZ
175
+ if ($tzEnv -and ($tzEnv -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$')) {
176
+ return $true
177
+ }
178
+
179
+ # 3 — .NET TimeZoneInfo (works on Windows and Unix PowerShell 7+)
180
+ try {
181
+ $tzId = [System.TimeZoneInfo]::Local.Id
182
+ if ($tzId -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$') {
183
+ return $true
184
+ }
185
+ } catch {
186
+ # TimeZoneInfo not available (unlikely on PS 5.1+ but guard anyway)
187
+ }
188
+
189
+ # 4 — /etc/timezone (PowerShell on Linux/macOS)
190
+ if (-not $script:OSWin -and (Test-Path '/etc/timezone')) {
191
+ try {
192
+ $tz = Get-Content '/etc/timezone' -Raw -ErrorAction Stop
193
+ if ($tz -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$') {
194
+ return $true
195
+ }
196
+ } catch {
197
+ # Permission or read error — skip
198
+ }
199
+ }
200
+
201
+ return $false
202
+ }
203
+
157
204
  # ──────────────────────────────────────────────
158
205
  # Download URLs
159
206
  # ──────────────────────────────────────────────
160
207
 
161
208
  $GITHUB_REPO = "platonai/Browser4"
162
209
  $OSS_BASE = "https://browser4.oss-cn-beijing.aliyuncs.com"
210
+ $script:ChinaDetected = $false
163
211
 
164
212
  function Get-DownloadUrls {
165
213
  param([string]$BinaryName, [string]$VersionTag)
@@ -181,8 +229,13 @@ function Get-DownloadUrls {
181
229
  } elseif ($Source -eq "oss") {
182
230
  $urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
183
231
  } else {
184
- $urls += @{ Url = $ghUrl; Label = "GitHub Releases" }
185
- $urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
232
+ if ($script:ChinaDetected) {
233
+ $urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
234
+ $urls += @{ Url = $ghUrl; Label = "GitHub Releases" }
235
+ } else {
236
+ $urls += @{ Url = $ghUrl; Label = "GitHub Releases" }
237
+ $urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
238
+ }
186
239
  }
187
240
 
188
241
  return $urls
@@ -359,6 +412,14 @@ function Main {
359
412
  Write-Summary "╚════════════════════════════════════════╝" -Color Cyan
360
413
  Write-Summary ""
361
414
 
415
+ # Auto-detect China mainland locale when no explicit source is given
416
+ if (-not $Source) {
417
+ $script:ChinaDetected = Test-ChinaLocale
418
+ if ($script:ChinaDetected) {
419
+ Write-Step "China mainland locale detected: preferring Aliyun OSS mirror."
420
+ }
421
+ }
422
+
362
423
  # Detect platform
363
424
  $platformKey = Get-PlatformKey
364
425
  $binaryName = Get-BinaryName -PlatformKey $platformKey
@@ -36,6 +36,7 @@ SOURCE=""
36
36
  ADD_TO_PATH=true
37
37
  SILENT=false
38
38
  DRY_RUN=false
39
+ CHINA_DETECTED=false
39
40
 
40
41
  # ──────────────────────────────────────────────
41
42
  # Helpers
@@ -110,6 +111,35 @@ while [[ $# -gt 0 ]]; do
110
111
  esac
111
112
  done
112
113
 
114
+ # ──────────────────────────────────────────────
115
+ # China mainland locale detection (zero-network)
116
+ # ──────────────────────────────────────────────
117
+
118
+ detect_china_locale() {
119
+ # 1 — Locale env vars
120
+ local lang
121
+ lang="${LC_ALL:-${LANG:-${LC_CTYPE:-${LC_MESSAGES:-}}}}"
122
+ case "$lang" in
123
+ zh_CN*|zh-CN*|"Chinese (Simplified)_China"*) return 0 ;;
124
+ esac
125
+
126
+ # 2 — TZ env var
127
+ case "${TZ:-}" in
128
+ Asia/Shanghai|Asia/Chongqing|Asia/Urumqi|Asia/Harbin) return 0 ;;
129
+ esac
130
+
131
+ # 3 — /etc/timezone
132
+ if [[ -f /etc/timezone ]]; then
133
+ local tz
134
+ tz=$(cat /etc/timezone 2>/dev/null || true)
135
+ case "$tz" in
136
+ Asia/Shanghai|Asia/Chongqing|Asia/Urumqi|Asia/Harbin) return 0 ;;
137
+ esac
138
+ fi
139
+
140
+ return 1
141
+ }
142
+
113
143
  # ──────────────────────────────────────────────
114
144
  # Platform detection
115
145
  # ──────────────────────────────────────────────
@@ -140,14 +170,34 @@ detect_libc() {
140
170
  return
141
171
  fi
142
172
 
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 ""
173
+ # Check for musl via ldd --version (guarded against missing ldd)
174
+ if command -v ldd >/dev/null 2>&1; then
175
+ if ldd --version 2>&1 | grep -qi musl; then
176
+ echo "musl"
177
+ return
178
+ fi
150
179
  fi
180
+
181
+ # Check for musl loader — covers common architectures
182
+ # x86_64, aarch64, armhf (32-bit ARM), i386, riscv64, s390x, ppc64le, mips64
183
+ local musl_loader
184
+ for musl_loader in \
185
+ /lib/ld-musl-x86_64.so.1 \
186
+ /lib/ld-musl-aarch64.so.1 \
187
+ /lib/ld-musl-armhf.so.1 \
188
+ /lib/ld-musl-i386.so.1 \
189
+ /lib/ld-musl-riscv64.so.1 \
190
+ /lib/ld-musl-s390x.so.1 \
191
+ /lib/ld-musl-ppc64le.so.1 \
192
+ /lib/ld-musl-mips64.so.1 \
193
+ /lib/ld-musl-mipsel.so.1; do
194
+ if [[ -f "$musl_loader" ]]; then
195
+ echo "musl"
196
+ return
197
+ fi
198
+ done
199
+
200
+ echo ""
151
201
  }
152
202
 
153
203
  get_platform_key() {
@@ -196,8 +246,14 @@ get_default_install_dir() {
196
246
  # ──────────────────────────────────────────────
197
247
 
198
248
  check_commands() {
199
- if ! command -v curl >/dev/null 2>&1; then
200
- die "'curl' is required but not found. Install curl and retry."
249
+ local missing=()
250
+ for cmd in curl mktemp stat awk grep ln mkdir chmod; do
251
+ if ! command -v "$cmd" >/dev/null 2>&1; then
252
+ missing+=("$cmd")
253
+ fi
254
+ done
255
+ if [[ ${#missing[@]} -gt 0 ]]; then
256
+ die "Required command(s) not found: ${missing[*]}. Install them and retry."
201
257
  fi
202
258
  }
203
259
 
@@ -219,8 +275,13 @@ get_download_urls() {
219
275
  github) urls+=("GitHub Releases|${gh_url}") ;;
220
276
  oss) urls+=("Aliyun OSS|${oss_url}") ;;
221
277
  *)
222
- urls+=("GitHub Releases|${gh_url}")
223
- urls+=("Aliyun OSS|${oss_url}")
278
+ if [[ "$CHINA_DETECTED" == true ]]; then
279
+ urls+=("Aliyun OSS|${oss_url}")
280
+ urls+=("GitHub Releases|${gh_url}")
281
+ else
282
+ urls+=("GitHub Releases|${gh_url}")
283
+ urls+=("Aliyun OSS|${oss_url}")
284
+ fi
224
285
  ;;
225
286
  esac
226
287
 
@@ -240,10 +301,27 @@ download_file() {
240
301
  return 0
241
302
  fi
242
303
 
243
- local http_code
304
+ local curl_stderr
305
+ curl_stderr=$(mktemp)
306
+ local http_code curl_exit
307
+
244
308
  http_code=$(curl -sSfL -w "%{http_code}" -o "$dest" \
245
309
  ${GITHUB_TOKEN:+-H "Authorization: Bearer $GITHUB_TOKEN"} \
246
- "$url" 2>/dev/null) || true
310
+ "$url" 2>"$curl_stderr")
311
+ curl_exit=$?
312
+
313
+ if [[ $curl_exit -ne 0 ]]; then
314
+ local stderr_msg
315
+ stderr_msg=$(<"$curl_stderr")
316
+ rm -f "$curl_stderr" "$dest"
317
+ if [[ -n "$stderr_msg" ]]; then
318
+ warn "curl error (exit ${curl_exit}): ${stderr_msg}"
319
+ else
320
+ warn "curl failed with exit code ${curl_exit} (no stderr output)"
321
+ fi
322
+ return 1
323
+ fi
324
+ rm -f "$curl_stderr"
247
325
 
248
326
  if [[ "$http_code" == "200" ]] || [[ "$http_code" == "302" ]]; then
249
327
  local size
@@ -297,12 +375,38 @@ add_to_shell_rc() {
297
375
  return
298
376
  fi
299
377
 
378
+ # Ensure the file ends with a newline before appending, so the new
379
+ # content isn't glued to the last existing line.
380
+ if [[ -s "$rc_file" ]]; then
381
+ local last_byte
382
+ last_byte=$(tail -c1 "$rc_file" 2>/dev/null || true)
383
+ if [[ -n "$last_byte" ]]; then
384
+ echo "" >> "$rc_file"
385
+ fi
386
+ fi
387
+
388
+ # Use a lock file to prevent concurrent append races (best-effort).
389
+ local lock_file="${rc_file}.browser4-install.lock"
390
+ local lock_fd=9
391
+ # Wait up to 10 seconds for another install process to release the lock.
392
+ local waited=0
393
+ while ! (umask 0002 && command -v flock >/dev/null 2>&1 && flock -n 9 2>/dev/null); do
394
+ if [[ $waited -ge 10 ]]; then
395
+ warn "Could not acquire lock on ${rc_file} after 10s; appending anyway"
396
+ break
397
+ fi
398
+ sleep 0.5
399
+ waited=$((waited + 1))
400
+ done 9>"$lock_file"
401
+
300
402
  {
301
- echo ""
302
403
  echo "# browser4-cli"
303
404
  echo "export PATH=\"$dir:\$PATH\""
304
405
  } >> "$rc_file"
305
406
 
407
+ # Clean up (flock auto-releases when fd 9 is closed)
408
+ rm -f "$lock_file" 2>/dev/null || true
409
+
306
410
  ok "Added to PATH in $rc_file"
307
411
  say " Reload with: source $rc_file"
308
412
  }
@@ -361,6 +465,14 @@ main() {
361
465
  check_commands
362
466
  header
363
467
 
468
+ # Auto-detect China mainland locale when no explicit source is given
469
+ if [[ -z "$SOURCE" ]]; then
470
+ if detect_china_locale; then
471
+ CHINA_DETECTED=true
472
+ step "China mainland locale detected: preferring Aliyun OSS mirror."
473
+ fi
474
+ fi
475
+
364
476
  # Detect platform
365
477
  local platform_key binary_name
366
478
  platform_key=$(get_platform_key)
@@ -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');