browser4-cli 0.1.12 → 0.1.14

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