browser4-cli 4.13.17 → 4.13.19

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": "4.13.17",
3
+ "version": "4.13.19",
4
4
  "description": "Browser automation CLI for AI agents",
5
5
  "type": "module",
6
6
  "files": [
package/scripts/README.md CHANGED
@@ -78,12 +78,13 @@ Runs a full CLI smoke test suitable for CI and local development:
78
78
  ./smoke-test-runtime-bundle.sh <cli-binary> <bundle-archive> [test-port] [timeout-secs]
79
79
  ```
80
80
 
81
- ### Install script tests
81
+ ### Script tests
82
82
 
83
83
  | File | Purpose |
84
84
  |------|---------|
85
85
  | `tests/install-browser4-cli.tests.sh` | Unit tests for the Unix install script |
86
86
  | `tests/install-browser4-cli.tests.ps1` | Unit tests for the Windows install script (Pester) |
87
+ | `tests/wait-for-npm-version.tests.sh` | Unit tests for `wait-for-npm-version.sh` (stubbed `npm`, no network) |
87
88
 
88
89
  ## Publish (npm)
89
90
 
@@ -94,6 +95,7 @@ Runs a full CLI smoke test suitable for CI and local development:
94
95
  | `publish-if-needed.js` | Publishes to npm only when the local version differs from the registry. Uses `--tag next` for prerelease versions (containing `-`). |
95
96
  | `sync-readme.mjs` | Temporarily copies the repository root `README.md` to `cli/README.md` for npm pack/publish, then restores the original CLI README in `postpack`. |
96
97
  | `postinstall.js` | npm `postinstall` hook: downloads the platform native binary after `npm install` |
98
+ | `wait-for-npm-version.sh` | Waits until a published version is actually visible on npm; called by the release workflows after `npm publish` |
97
99
 
98
100
  ### Version check
99
101
 
@@ -113,6 +115,23 @@ node scripts/publish-if-needed.js --dry-run # print what would happen
113
115
 
114
116
  Optional env: `BROWSER4_CLI_NPM_REMOTE_VERSION` to override the remote version for testing.
115
117
 
118
+ ### Verifying a publish landed
119
+
120
+ `npm publish` exiting 0 only means the registry accepted the upload — npm processes
121
+ publishes asynchronously and answers with *"Your package is being processed and may take a
122
+ few minutes to become available"*, after which the version can stay invisible to
123
+ `npm view` for a while. `wait-for-npm-version.sh` polls until the exact version is
124
+ visible (10 minutes by default) and only then lets the release proceed:
125
+
126
+ ```shell
127
+ bash scripts/wait-for-npm-version.sh browser4-cli 4.13.18 # 10 min budget, 15 s interval
128
+ bash scripts/wait-for-npm-version.sh browser4-cli 4.13.18 120 5 # 2 min budget, 5 s interval
129
+ ```
130
+
131
+ Exit code 0 = version visible; 1 = still not visible when the budget expired (the message
132
+ includes the last registry answer and manual re-check commands). Both `release.yml` and
133
+ `release-cli.yml` call it in their `Verify npm package was published` step.
134
+
116
135
  ### README sync for npm package
117
136
 
118
137
  `cli/package.json` wires the README sync into npm packaging hooks:
@@ -50,7 +50,8 @@
50
50
 
51
51
  .PARAMETER Force
52
52
  Force reinstallation even if the binary is already installed at the target path.
53
- Overrides -SkipIfInstalled and bypasses locked-file workarounds.
53
+ Overrides -SkipIfInstalled (the locked-file workaround in Set-BinaryFile is
54
+ always attempted, with or without this switch).
54
55
 
55
56
  .PARAMETER SkipBackend
56
57
  Skip installing/upgrading the Browser4 backend (runtime bundle).
@@ -115,6 +116,17 @@ param(
115
116
 
116
117
  $ErrorActionPreference = "Stop"
117
118
 
119
+ # Windows PowerShell 5.1 on an older .NET defaults to TLS 1.0, which GitHub and
120
+ # the OSS mirror both refuse ("Could not create SSL/TLS secure channel"). Pin
121
+ # 1.2 when the runtime offers it; PowerShell 6+ already negotiates correctly.
122
+ if ($PSVersionTable.PSVersion.Major -lt 6) {
123
+ try {
124
+ [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
125
+ } catch {
126
+ # Runtime without TLS 1.2 support -- keep its default
127
+ }
128
+ }
129
+
118
130
  # ----------------------------------------------
119
131
  # Script location -- find ourselves on disk
120
132
  # ----------------------------------------------
@@ -280,16 +292,28 @@ function Test-ChinaLocale {
280
292
  return $true
281
293
  }
282
294
 
283
- # 3 -- .NET TimeZoneInfo (works on Windows and Unix PowerShell 7+)
295
+ # 3 -- .NET TimeZoneInfo (works on Windows and Unix PowerShell 7+).
296
+ # Windows reports Windows ids ("China Standard Time"); the IANA ids only
297
+ # appear on Unix, so matching IANA alone made this branch dead on Windows.
284
298
  try {
285
299
  $tzId = [System.TimeZoneInfo]::Local.Id
286
- if ($tzId -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$') {
300
+ if ($tzId -eq 'China Standard Time' -or $tzId -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$') {
287
301
  return $true
288
302
  }
289
303
  } catch {
290
304
  # TimeZoneInfo not available (unlikely on PS 5.1+ but guard anyway)
291
305
  }
292
306
 
307
+ # 3b -- current culture: a Chinese Windows install reports zh-CN even when
308
+ # no locale environment variables are set.
309
+ try {
310
+ if ([System.Globalization.CultureInfo]::CurrentCulture.Name -match '^zh-(CN|Hans)') {
311
+ return $true
312
+ }
313
+ } catch {
314
+ # Culture lookup failed -- not fatal
315
+ }
316
+
293
317
  # 4 -- /etc/timezone (PowerShell on Linux/macOS)
294
318
  if (-not $script:OSWin -and (Test-Path '/etc/timezone')) {
295
319
  try {
@@ -345,6 +369,43 @@ function Get-DownloadUrls {
345
369
  return $urls
346
370
  }
347
371
 
372
+ <#
373
+ .SYNOPSIS
374
+ Check that a downloaded file really is a native executable.
375
+ Magic bytes are the cheapest integrity signal that needs no network and no
376
+ published checksum file: an HTML error page or a truncated body that happens
377
+ to be larger than the size threshold is rejected here.
378
+ #>
379
+ function Test-BinaryMagic {
380
+ param([string]$Path)
381
+
382
+ if (-not (Test-Path $Path)) { return $false }
383
+ # Read exactly four bytes through .NET: works on 5.1 and 7.x alike
384
+ # (Get-Content -Encoding Byte is gone in PowerShell 7).
385
+ $bytes = New-Object byte[] 4
386
+ $read = 0
387
+ try {
388
+ $stream = [System.IO.File]::OpenRead($Path)
389
+ try {
390
+ $read = $stream.Read($bytes, 0, 4)
391
+ } finally {
392
+ $stream.Dispose()
393
+ }
394
+ } catch {
395
+ return $false
396
+ }
397
+ if ($read -lt 4) { return $false }
398
+
399
+ $magic = ($bytes | ForEach-Object { $_.ToString('x2') }) -join ''
400
+
401
+ switch -Regex ($magic) {
402
+ '^7f454c46' { return $true } # ELF (linux glibc / musl)
403
+ '^4d5a' { return $true } # PE / MZ (windows)
404
+ '^(feedface|feedfacf|cefaedfe|cffaedfe|cafebabe)$' { return $true } # Mach-O, incl. fat
405
+ default { return $false }
406
+ }
407
+ }
408
+
348
409
  function Invoke-Download {
349
410
  param([string]$Url, [string]$OutFile, [string]$Label)
350
411
 
@@ -359,12 +420,16 @@ function Invoke-Download {
359
420
  try {
360
421
  $ProgressPreference = if ($Silent) { "SilentlyContinue" } else { "Continue" }
361
422
 
362
- # Use Invoke-WebRequest with progress bar
363
423
  Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing -ErrorAction Stop
364
424
 
365
425
  if (Test-Path $OutFile) {
366
426
  $size = (Get-Item $OutFile).Length
367
427
  if ($size -gt 102400) { # > 100 KB minimum
428
+ if (-not (Test-BinaryMagic -Path $OutFile)) {
429
+ Write-WarnMsg "Downloaded file is not a native executable (bad magic bytes) -- refusing it"
430
+ Remove-Item $OutFile -Force -ErrorAction SilentlyContinue
431
+ return $false
432
+ }
368
433
  Write-Check "Downloaded $( [math]::Round($size / 1MB, 1) ) MB"
369
434
  return $true
370
435
  } else {
@@ -586,29 +651,39 @@ function Add-DirectoryToUserPath {
586
651
 
587
652
  $dirResolved = (Resolve-Path $Dir -ErrorAction SilentlyContinue).Path
588
653
  if (-not $dirResolved) { $dirResolved = $Dir }
654
+ # Trailing separators would defeat the duplicate check below.
655
+ $dirResolved = $dirResolved.TrimEnd('\', '/')
589
656
 
590
657
  # Read current user PATH
591
658
  $currentUserPath = [System.Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::User)
592
659
  $paths = if ($currentUserPath) { $currentUserPath -split ";" | Where-Object { $_ } } else { @() }
593
660
 
594
- # Check if already present
595
- $normalized = $paths | ForEach-Object { $rp = Resolve-Path $_ -ErrorAction SilentlyContinue; if ($rp) { $rp.Path } else { $_ } }
596
- if ($normalized -contains $dirResolved) {
597
- Write-Check "Already in user PATH: $Dir"
661
+ # Check if already present. Compare normalized, case-insensitively: Windows
662
+ # paths differ in case, a relative entry has to be resolved first, and a
663
+ # trailing separator is not a different directory.
664
+ $normalized = $paths | ForEach-Object {
665
+ $rp = Resolve-Path $_ -ErrorAction SilentlyContinue
666
+ $p = if ($rp) { $rp.Path } else { $_ }
667
+ $p.TrimEnd('\', '/')
668
+ }
669
+ if ($normalized | Where-Object { $_.Equals($dirResolved, [System.StringComparison]::OrdinalIgnoreCase) }) {
670
+ Write-Check "Already in user PATH: $dirResolved"
598
671
  return
599
672
  }
600
673
 
601
674
  if ($DryRun) {
602
- Write-Check "[DRY-RUN] Would add to user PATH: $Dir"
675
+ Write-Check "[DRY-RUN] Would add to user PATH: $dirResolved"
603
676
  return
604
677
  }
605
678
 
606
- $newPath = if ($currentUserPath) { "$currentUserPath;$Dir" } else { $Dir }
679
+ # Append the resolved absolute path: `-InstallDir .\b4` used to write a
680
+ # relative entry into the persistent user PATH.
681
+ $newPath = if ($currentUserPath) { "$currentUserPath;$dirResolved" } else { $dirResolved }
607
682
  [System.Environment]::SetEnvironmentVariable("Path", $newPath, [System.EnvironmentVariableTarget]::User)
608
- Write-Check "Added to user PATH: $Dir"
683
+ Write-Check "Added to user PATH: $dirResolved"
609
684
 
610
685
  # Also update current session
611
- $env:Path = "$env:Path;$Dir"
686
+ $env:Path = "$env:Path;$dirResolved"
612
687
  }
613
688
 
614
689
  # ----------------------------------------------
@@ -622,10 +697,16 @@ function Add-DirectoryToUserPath {
622
697
  #>
623
698
  function Get-BackendAction {
624
699
  param([string]$StatusOutput)
625
- if ($StatusOutput -match 'Installed bundle: not installed') {
700
+ # Only a status that positively reports an installed bundle means upgrade.
701
+ # An empty status (no CLI yet, or `status` itself failed) must not turn a
702
+ # fresh machine into an `upgrade`.
703
+ if ($StatusOutput -match 'Installed bundle:\s*not installed') {
626
704
  return "install"
627
705
  }
628
- return "upgrade"
706
+ if ($StatusOutput -match 'Installed bundle:') {
707
+ return "upgrade"
708
+ }
709
+ return "install"
629
710
  }
630
711
 
631
712
  <#
@@ -635,28 +716,34 @@ function Get-BackendAction {
635
716
  - no backend installed yet -> browser4-cli install
636
717
  - backend already present -> browser4-cli upgrade (to the latest)
637
718
  Passes -Version through as --tag so the backend matches the CLI version.
638
- Non-fatal: failures print guidance and leave the CLI usable.
719
+ Returns $true when the backend is in place (or was skipped on request) and
720
+ $false when the step failed -- the caller turns that into the exit code, so a
721
+ broken backend is visible to scripts even though the CLI stays usable.
639
722
  #>
640
723
  function Install-Backend {
641
724
  param([string]$CliPath, [string]$Tag)
642
725
 
643
726
  if ($SkipBackend) {
644
727
  Write-Step "Skipping backend install/upgrade (-SkipBackend)"
645
- return
728
+ return $true
646
729
  }
647
730
 
648
731
  Write-Step "Checking for an existing Browser4 backend..."
649
732
 
650
- $statusOutput = ""
651
- try {
652
- $statusOutput = (& $CliPath status 2>&1 | Out-String)
653
- } catch {
654
- # Binary missing or not runnable yet (e.g. --dry-run) -- fall through.
733
+ # -DryRun must not touch the installed CLI at all: ask what would happen
734
+ # instead of running `status`.
735
+ $action = "install"
736
+ if (-not $DryRun) {
655
737
  $statusOutput = ""
738
+ try {
739
+ $statusOutput = (& $CliPath status 2>&1 | Out-String)
740
+ } catch {
741
+ # Binary missing or not runnable -- treat as "no backend yet".
742
+ $statusOutput = ""
743
+ }
744
+ $action = Get-BackendAction -StatusOutput $statusOutput
656
745
  }
657
746
 
658
- $action = Get-BackendAction -StatusOutput $statusOutput
659
-
660
747
  $backendArgs = @($action)
661
748
  if ($Tag) { $backendArgs += @('--tag', $Tag) }
662
749
 
@@ -668,9 +755,10 @@ function Install-Backend {
668
755
 
669
756
  if ($DryRun) {
670
757
  Write-Check "[DRY-RUN] Would run: $CliPath $($backendArgs -join ' ')"
671
- return
758
+ return $true
672
759
  }
673
760
 
761
+ $backendOk = $false
674
762
  if ($Silent) {
675
763
  $backendOutput = ""
676
764
  try {
@@ -678,7 +766,8 @@ function Install-Backend {
678
766
  } catch {
679
767
  $backendOutput = $_.Exception.Message
680
768
  }
681
- if ($LASTEXITCODE -eq 0) {
769
+ $backendOk = ($LASTEXITCODE -eq 0)
770
+ if ($backendOk) {
682
771
  Write-Check "Backend $action succeeded ($($backendArgs -join ' '))."
683
772
  } else {
684
773
  Write-WarnMsg "Backend $action failed ($($backendArgs -join ' ')). Retry with: browser4-cli $($backendArgs -join ' ')"
@@ -686,12 +775,15 @@ function Install-Backend {
686
775
  }
687
776
  } else {
688
777
  & $CliPath @backendArgs
689
- if ($LASTEXITCODE -eq 0) {
778
+ $backendOk = ($LASTEXITCODE -eq 0)
779
+ if ($backendOk) {
690
780
  Write-Check "Backend $action succeeded ($($backendArgs -join ' '))."
691
781
  } else {
692
782
  Write-WarnMsg "Backend $action failed ($($backendArgs -join ' ')). Retry with: browser4-cli $($backendArgs -join ' ')"
693
783
  }
694
784
  }
785
+
786
+ return $backendOk
695
787
  }
696
788
 
697
789
  # ----------------------------------------------
@@ -704,6 +796,19 @@ function Main {
704
796
  Write-Summary "==========================================" -Color Cyan
705
797
  Write-Summary ""
706
798
 
799
+ # Validate -Version before it is interpolated into a download URL: a value
800
+ # like '../../evil' used to produce a valid-looking URL and also reached
801
+ # `--tag`. A bare semver gets the 'v' prefix, as in the Unix installer.
802
+ if ($Version) {
803
+ if ($Version -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]*$') {
804
+ throw "-Version must be a release tag such as v4.13.18 (got '$Version')"
805
+ }
806
+ if ($Version -match '^[0-9]+\.[0-9]+\.[0-9]+') {
807
+ Write-Step "Using release tag 'v$Version' (release tags are prefixed with 'v')"
808
+ $Version = "v$Version"
809
+ }
810
+ }
811
+
707
812
  # Auto-detect China mainland locale when no explicit source is given
708
813
  if (-not $Source) {
709
814
  $script:ChinaDetected = Test-ChinaLocale
@@ -723,7 +828,7 @@ function Main {
723
828
  Write-Step "Script dir: $ScriptDir"
724
829
  Write-Step "Platform key: $platformKey"
725
830
  Write-Step "Binary name: $binaryName"
726
- Write-Step "Default install: $(Get-DefaultInstallDir)"
831
+ Write-Step "Default install: $(if ($InstallDir) { $InstallDir } else { Get-DefaultInstallDir })"
727
832
  Write-Step "China locale: $script:ChinaDetected"
728
833
  Write-Step "Source override: $(if ($Source) { $Source } else { 'auto' })"
729
834
  Write-Step "OS: $(if ($script:OSWin) { 'Windows' } elseif ($script:OSMac) { 'macOS' } elseif ($script:OSLinux) { 'Linux' } else { 'Unknown' })"
@@ -810,18 +915,20 @@ function Main {
810
915
  }
811
916
 
812
917
  $downloaded = $false
918
+ # The temp file is always cleaned up, including on -DryRun (where the
919
+ # download never consumes it) and on the failure path.
813
920
  $tempFile = [System.IO.Path]::GetTempFileName()
814
921
 
815
- foreach ($entry in $urls) {
816
- if (Invoke-Download -Url $entry.Url -OutFile $tempFile -Label $entry.Label) {
817
- $downloaded = $true
818
- break
922
+ try {
923
+ foreach ($entry in $urls) {
924
+ if (Invoke-Download -Url $entry.Url -OutFile $tempFile -Label $entry.Label) {
925
+ $downloaded = $true
926
+ break
927
+ }
819
928
  }
820
- }
821
929
 
822
- if (-not $downloaded) {
823
- if (Test-Path $tempFile) { Remove-Item $tempFile -Force }
824
- throw @"
930
+ if (-not $downloaded) {
931
+ throw @"
825
932
  Could not download browser4-cli binary.
826
933
 
827
934
  Tried:
@@ -833,13 +940,16 @@ Please check:
833
940
  - For GitHub rate limits, set GITHUB_TOKEN environment variable
834
941
  - If you have a local copy, place it alongside this script and re-run
835
942
  "@
836
- }
943
+ }
837
944
 
838
- # Move from temp to install dir
839
- if (-not $DryRun) {
840
- Set-BinaryFile -TargetPath $binaryPath -SourcePath $tempFile -Move
945
+ # Move from temp to install dir
946
+ if (-not $DryRun) {
947
+ Set-BinaryFile -TargetPath $binaryPath -SourcePath $tempFile -Move
948
+ }
949
+ Write-Check "Installed: $binaryPath"
950
+ } finally {
951
+ if (Test-Path $tempFile) { Remove-Item $tempFile -Force -ErrorAction SilentlyContinue }
841
952
  }
842
- Write-Check "Installed: $binaryPath"
843
953
  }
844
954
 
845
955
  # On Unix, ensure executable bit
@@ -875,16 +985,28 @@ Please check:
875
985
  }
876
986
  }
877
987
 
878
- # Verify
988
+ # Verify. A binary that cannot report its version is a failed install --
989
+ # report it red and exit non-zero instead of printing a green success line.
879
990
  Write-Summary ""
991
+ $script:ExitCode = 0
880
992
  if (-not $DryRun) {
993
+ $versionOutput = ""
994
+ $versionOk = $false
881
995
  try {
882
- $versionOutput = & $binaryPath --version 2>&1
996
+ $versionOutput = (& $binaryPath --version 2>&1 | Out-String).Trim()
997
+ $versionOk = ($LASTEXITCODE -eq 0) -and ($versionOutput -match '\d')
998
+ } catch {
999
+ $versionOk = $false
1000
+ }
1001
+
1002
+ if ($versionOk) {
883
1003
  Write-Summary "[v] browser4-cli installed successfully" -Color Green
884
1004
  Write-Summary " Version: $versionOutput"
885
- } catch {
886
- Write-Summary "[v] Binary installed at: $binaryPath" -Color Green
887
- Write-WarnMsg "Could not verify --version (this is normal on first install)"
1005
+ } else {
1006
+ Write-Summary "[x] Installed $binaryPath but it cannot run:" -Color Red
1007
+ Write-Summary " $versionOutput"
1008
+ Write-WarnMsg "The binary may be corrupt or built for a different platform. Delete it and re-run the installer."
1009
+ $script:ExitCode = 1
888
1010
  }
889
1011
  } else {
890
1012
  Write-Summary "[DRY-RUN] Installation plan complete" -Color Yellow
@@ -893,8 +1015,12 @@ Please check:
893
1015
  # Install / upgrade the Browser4 backend (runtime bundle) using the CLI
894
1016
  # binary we just installed. Fresh machines get `browser4-cli install`;
895
1017
  # machines that already have a backend get `browser4-cli upgrade`.
1018
+ # A backend failure keeps the CLI usable but must be visible to scripts
1019
+ # and CI, so it is reflected in the exit code.
896
1020
  Write-Summary ""
897
- Install-Backend -CliPath $binaryPath -Tag $Version
1021
+ if ((Install-Backend -CliPath $binaryPath -Tag $Version) -eq $false) {
1022
+ $script:ExitCode = 1
1023
+ }
898
1024
 
899
1025
  Write-Summary ""
900
1026
  Write-Summary "Run 'browser4-cli --help' to get started." -Color Cyan
@@ -903,6 +1029,8 @@ Please check:
903
1029
  Write-Summary "If the command isn't found, restart your terminal or run:"
904
1030
  Write-Summary " `$env:Path = [System.Environment]::GetEnvironmentVariable('Path','User') + ';' + [System.Environment]::GetEnvironmentVariable('Path','Machine')"
905
1031
  }
1032
+
1033
+ exit $script:ExitCode
906
1034
  }
907
1035
 
908
1036
  Main