browser4-cli 4.12.0-rc.2 → 4.12.0

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
@@ -88,7 +88,7 @@ browser4-cli agent run "Search amazon for mechanical keyboards, compare the top
88
88
 
89
89
  # Parallel scraping with swarm
90
90
  browser4-cli swarm create --max-open-tabs 12 --display-mode HEADLESS
91
- browser4-cli swarm submit --seed-file ./urls.txt --refresh --store-content
91
+ browser4-cli swarm submit --seed-file ./urls.txt --refresh
92
92
  browser4-cli swarm result scrape-task-1
93
93
 
94
94
  # Batch multiple commands
@@ -247,16 +247,19 @@ browser4-cli pdf --filename page.pdf
247
247
 
248
248
  | Command | Description |
249
249
  |---|---|
250
- | `tab-list` | List all tabs with their zero-based index. |
250
+ | `tab-list` | List all tabs with their zero-based index and stable GUID. Use `--json` for full GUIDs. |
251
251
  | `tab-new [url]` | Open a new tab. |
252
- | `tab-close [index]` | Close a tab (current tab if no index). |
253
- | `tab-select <index>` | Switch to a tab by index. |
252
+ | `tab-close [index]` | Close a tab (current tab if no index). Use `--guid <guid>` for GUID-based close. |
253
+ | `tab-select <index>` | Switch to a tab by index. Use `--guid <guid>` for GUID-based select. |
254
254
 
255
255
  ```bash
256
256
  browser4-cli tab-list
257
+ browser4-cli tab-list --json # JSON output with full GUIDs
257
258
  browser4-cli tab-select 1
259
+ browser4-cli tab-select --guid 1B46D74FB # select by stable GUID
258
260
  browser4-cli tab-new https://example.com
259
261
  browser4-cli tab-close 1
262
+ browser4-cli tab-close --guid 1B46D74FB # close by stable GUID
260
263
  ```
261
264
 
262
265
  ### Element inspection
@@ -382,17 +385,21 @@ instructions that always match the installed CLI version.
382
385
  | `skills list` | List available bundled skills with file counts. |
383
386
  | `skills get <name>` | Output a skill's SKILL.md content. `--full` includes references and templates. `--all` outputs every skill. |
384
387
  | `skills path [name]` | Print the skills directory path. With a name, prints the path to that skill's subdirectory. |
388
+ | `skills unpack [dest]` | Unpack bundled skill files to a directory (defaults to the skills directory). |
385
389
 
386
390
  Skill files are unpacked to the versioned installation directory during `browser4-cli install`.
391
+ Use `skills unpack` to refresh or relocate skill files without reinstalling.
387
392
  Set `BROWSER4_SKILLS_DIR` to override the skills directory path.
388
393
 
389
394
  ```bash
390
- browser4-cli skills # List bundled skills
391
- browser4-cli skills get browser4-cli # Get the CLI skill's main content
395
+ browser4-cli skills # List bundled skills
396
+ browser4-cli skills get browser4-cli # Get the CLI skill's main content
392
397
  browser4-cli skills get browser4-cli --full # Include references and templates
393
- browser4-cli skills get --all # Output every skill
394
- browser4-cli skills path # Print skills root directory
395
- browser4-cli skills path browser4-cli # Print path to a specific skill
398
+ browser4-cli skills get --all # Output every skill
399
+ browser4-cli skills path # Print skills root directory
400
+ browser4-cli skills path browser4-cli # Print path to a specific skill
401
+ browser4-cli skills unpack # Unpack to default skills directory
402
+ browser4-cli skills unpack /custom/path # Unpack to a custom directory
396
403
  ```
397
404
 
398
405
  ### Other
@@ -489,7 +496,7 @@ browser4-cli htmlsnapshot
489
496
  browser4-cli htmlsnapshot get <field> [selector] [name]
490
497
  browser4-cli htmlsnapshot get all <field> [selector] [name]
491
498
  browser4-cli htmlsnapshot query [url] --sql <query>
492
- browser4-cli htmlsnapshot export [--file <path>]
499
+ browser4-cli htmlsnapshot export [--file <path>] [--clean]
493
500
  browser4-cli htmlsnapshot summary
494
501
  browser4-cli htmlsnapshot grep [OPTIONS] <pattern>
495
502
  browser4-cli htmlsnapshot inspect [selector] [--max N] [--depth D]
@@ -581,10 +588,11 @@ browser4-cli htmlsnapshot query --sql "
581
588
 
582
589
  #### htmlsnapshot export
583
590
 
584
- Save the full snapshot HTML to a local file.
591
+ Save the full snapshot HTML to a local file. Use `--clean` to strip `<script>`, `<style>`, comments, and non-standard attributes — producing minimal HTML ideal for LLM consumption.
585
592
 
586
593
  ```bash
587
594
  browser4-cli htmlsnapshot export --file snapshot.html
595
+ browser4-cli htmlsnapshot export --file page.html --clean
588
596
  ```
589
597
 
590
598
  #### htmlsnapshot summary
@@ -735,13 +743,12 @@ line; `#` comments and blank lines ignored), or both.
735
743
  | `--expires <duration>` | Cache expiration (e.g. `1d`, `1h`) |
736
744
  | `--refresh` | Force fresh fetch, ignore cache |
737
745
  | `--parse` | Parse page immediately after fetching |
738
- | `--store-content` | Persist page content to storage |
739
746
 
740
747
  ```bash
741
748
  # Submit URLs from a file
742
749
  browser4-cli swarm submit --seed-file ./urls.txt \
743
750
  --deadline 2026-03-30T00:00:00Z \
744
- --expires 1d --refresh --store-content
751
+ --expires 1d --refresh
745
752
 
746
753
  # Submit with inline X-SQL
747
754
  browser4-cli swarm submit "https://www.amazon.com/dp/B08PP5MSVB" --sql "
@@ -800,7 +807,6 @@ browser4-cli crawl <url> [options]
800
807
  | `-a`, `--args` | — | Additional LoadOptions passthrough (e.g. `-a "-nMaxRetry 5"`) |
801
808
  | `--refresh` | — | Force fresh fetch |
802
809
  | `--parse` | — | Parse pages immediately after fetching |
803
- | `--store-content` | — | Persist page content |
804
810
  | `--expires` | — | Cache expiration (e.g. `1d`, `1h`, `30m`) |
805
811
  | `-p`, `--priority` | — | Queue priority (lower = higher) |
806
812
  | `--ignore-url-query` | — | Strip query params during URL normalization |
@@ -821,7 +827,7 @@ browser4-cli crawl "https://shop.example.com" \
821
827
  --top-links 10
822
828
 
823
829
  # Deep crawl with refresh and content storage
824
- browser4-cli crawl "https://example.com" --depth 3 --refresh --store-content
830
+ browser4-cli crawl "https://example.com" --depth 3 --refresh
825
831
 
826
832
  # Background crawl — submit and return immediately
827
833
  browser4-cli crawl "https://example.com" -ol "a[href]" --background
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.12.0-rc.2",
3
+ "version": "4.12.0",
4
4
  "description": "Browser automation CLI for AI agents",
5
5
  "type": "module",
6
6
  "files": [
@@ -429,39 +429,101 @@ function New-Symlinks {
429
429
  if ($DryRun) {
430
430
  Write-Step "[DRY-RUN] Would create link: $linkName -> $BinaryName"
431
431
  } else {
432
- New-PlatformLink -LinkPath $linkPath -TargetName $BinaryName -DisplayName $linkName
432
+ $null = New-PlatformLink -LinkPath $linkPath -TargetName $BinaryName -DisplayName $linkName
433
433
  }
434
434
 
435
- # 2) Only if no conflict: b4 -> browser4-cli-<platform>
435
+ # 2) b4 -> browser4-cli-<platform> (only if b4 is our tool or doesn't exist)
436
436
  $shortName = "b4$ext"
437
437
  $shortPath = Join-Path $InstallDir $shortName
438
438
 
439
- # Check if b4 is already on PATH (conflict with another tool)
440
- $existingCmd = Get-Command b4 -ErrorAction SilentlyContinue
441
- if ($existingCmd) {
442
- Write-WarnMsg "Skipping short link '$shortName': 'b4' already found on PATH ($($existingCmd.Source))"
443
- return
444
- }
439
+ $b4Exists = $false
440
+ $b4IsOurs = $false
445
441
 
446
- # Check if b4 already exists in the install directory
442
+ # Check in install dir first --anything here is ours
447
443
  if (Test-Path $shortPath) {
448
- Write-WarnMsg "Skipping short link '$shortName': already exists in $InstallDir"
449
- return
444
+ $b4Exists = $true
445
+ $b4IsOurs = $true
450
446
  }
451
447
 
452
- # Also check b4.cmd if on Windows (wrapper fallback)
453
- if ($script:OSWin) {
448
+ # Also check b4.cmd wrapper on Windows
449
+ if ($script:OSWin -and -not $b4Exists) {
454
450
  $shortCmdPath = Join-Path $InstallDir "b4.cmd"
455
451
  if (Test-Path $shortCmdPath) {
456
- Write-WarnMsg "Skipping short link '$shortName': 'b4.cmd' already exists in $InstallDir"
457
- return
452
+ $b4Exists = $true
453
+ $b4IsOurs = $true
458
454
  }
459
455
  }
460
456
 
457
+ # Check if b4 is on PATH from somewhere else.
458
+ # Try Get-Command first; fall back to scanning PATH directories manually
459
+ # because Get-Command may miss extensionless executables on Linux/macOS
460
+ # or may cache stale results in some PowerShell versions.
461
+ if (-not $b4Exists) {
462
+ $existingCmd = Get-Command b4 -ErrorAction SilentlyContinue
463
+ if ($existingCmd) {
464
+ $b4Exists = $true
465
+ # Check if it's ours by running --version
466
+ try {
467
+ $b4Version = & b4 --version 2>&1
468
+ if ($b4Version -match "browser4-cli") {
469
+ $b4IsOurs = $true
470
+ }
471
+ } catch {
472
+ # Can't determine --leave it alone
473
+ }
474
+ }
475
+ }
476
+
477
+ # Fallback: scan PATH manually when Get-Command didn't find anything.
478
+ # On Linux/macOS, PowerShell's Get-Command may miss scripts that are
479
+ # executable but not in its command cache, especially in CI containers.
480
+ if (-not $b4Exists) {
481
+ $separator = [System.IO.Path]::PathSeparator
482
+ $pathDirs = @($env:Path -split $separator | Where-Object { $_ })
483
+ foreach ($dir in $pathDirs) {
484
+ $candidatePath = Join-Path $dir $shortName
485
+ if (Test-Path $candidatePath -PathType Leaf) {
486
+ $b4Exists = $true
487
+ try {
488
+ $b4Version = & $candidatePath --version 2>&1
489
+ if ($b4Version -match "browser4-cli") {
490
+ $b4IsOurs = $true
491
+ }
492
+ } catch {
493
+ # Can't determine --leave it alone
494
+ }
495
+ break
496
+ }
497
+ # On Windows, also check for b4.cmd / b4.bat wrappers
498
+ if ($script:OSWin) {
499
+ foreach ($pext in @('.cmd', '.bat')) {
500
+ $candidateWin = Join-Path $dir "b4$pext"
501
+ if (Test-Path $candidateWin -PathType Leaf) {
502
+ $b4Exists = $true
503
+ try {
504
+ $b4Version = & $candidateWin --version 2>&1
505
+ if ($b4Version -match "browser4-cli") {
506
+ $b4IsOurs = $true
507
+ }
508
+ } catch { }
509
+ break
510
+ }
511
+ }
512
+ if ($b4Exists) { break }
513
+ }
514
+ }
515
+ }
516
+
517
+ if ($b4Exists -and -not $b4IsOurs) {
518
+ Write-WarnMsg "Skipping short link '$shortName': 'b4' is not browser4-cli"
519
+ return
520
+ }
521
+
461
522
  if ($DryRun) {
462
- Write-Step "[DRY-RUN] Would create link: $shortName -> $BinaryName"
523
+ $action = if ($b4Exists) { "Would update link" } else { "Would create link" }
524
+ Write-Step "[DRY-RUN] $action`: $shortName -> $BinaryName"
463
525
  } else {
464
- New-PlatformLink -LinkPath $shortPath -TargetName $BinaryName -DisplayName $shortName
526
+ $null = New-PlatformLink -LinkPath $shortPath -TargetName $BinaryName -DisplayName $shortName
465
527
  }
466
528
  }
467
529
 
@@ -497,25 +497,48 @@ create_symlinks() {
497
497
  ok "Created symlink: ${link_name} -> ${binary_name}"
498
498
  fi
499
499
 
500
- # 2) Only if no conflict: b4 -> browser4-cli-<platform>
500
+ # 2) b4 -> browser4-cli-<platform> (only if b4 is our tool or doesn't exist)
501
501
  local short_name="b4${ext}"
502
502
  local short_path="${install_dir}/${short_name}"
503
503
 
504
- if command -v b4 >/dev/null 2>&1; then
505
- warn "Skipping short link '${short_name}': 'b4' already found on PATH"
506
- return
507
- fi
504
+ local b4_exists=false
505
+ local b4_is_ours=false
508
506
 
507
+ # Check in install dir first — anything here is ours
509
508
  if [[ -e "$short_path" ]] || [[ -L "$short_path" ]]; then
510
- warn "Skipping short link '${short_name}': already exists in ${install_dir}"
509
+ b4_exists=true
510
+ b4_is_ours=true
511
+ fi
512
+
513
+ # Check if b4 is on PATH from somewhere else
514
+ if [[ "$b4_exists" != true ]]; then
515
+ if command -v b4 >/dev/null 2>&1; then
516
+ b4_exists=true
517
+ # Check if it's ours by running --version
518
+ if b4 --version 2>&1 | grep -qi "browser4-cli"; then
519
+ b4_is_ours=true
520
+ fi
521
+ fi
522
+ fi
523
+
524
+ if [[ "$b4_exists" == true ]] && [[ "$b4_is_ours" != true ]]; then
525
+ warn "Skipping short link '${short_name}': 'b4' is not browser4-cli"
511
526
  return
512
527
  fi
513
528
 
514
529
  if [[ "$DRY_RUN" == true ]]; then
515
- step "[DRY-RUN] Would create symlink: ${short_name} -> ${binary_name}"
530
+ if [[ "$b4_exists" == true ]]; then
531
+ step "[DRY-RUN] Would update symlink: ${short_name} -> ${binary_name}"
532
+ else
533
+ step "[DRY-RUN] Would create symlink: ${short_name} -> ${binary_name}"
534
+ fi
516
535
  else
517
536
  ln -sf "$binary_name" "$short_path"
518
- ok "Created symlink: ${short_name} -> ${binary_name}"
537
+ if [[ "$b4_exists" == true ]]; then
538
+ ok "Updated symlink: ${short_name} -> ${binary_name}"
539
+ else
540
+ ok "Created symlink: ${short_name} -> ${binary_name}"
541
+ fi
519
542
  fi
520
543
  }
521
544
 
@@ -301,6 +301,157 @@ $sb = [ScriptBlock]::Create($scriptContent)
301
301
 
302
302
  Write-Host ""
303
303
 
304
+ # ── New-Symlinks (b4 link logic) ──
305
+ Write-Host "--- New-Symlinks ---" -ForegroundColor Cyan
306
+
307
+ & {
308
+ $Silent = $true
309
+ $DryRun = $false
310
+ $SkipLocal = $false
311
+ $Locate = $false
312
+ $Source = ""
313
+ $Version = ""
314
+ $InstallDir = ""
315
+ $AddToPath = $true
316
+
317
+ . $sb
318
+
319
+ $testPlatformKey = if ($script:OSWin) { "win32-x64" } else { "linux-x64" }
320
+ $testExt = if ($testPlatformKey.StartsWith("win32")) { ".exe" } else { "" }
321
+ $testBinaryName = "browser4-cli-$testPlatformKey$testExt"
322
+ $testShortName = "b4$testExt"
323
+
324
+ $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "b4-install-test-$(Get-Random)"
325
+ New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
326
+
327
+ try {
328
+ # Create the dummy platform binary that New-PlatformLink needs
329
+ $dummyBinary = Join-Path $tempDir $testBinaryName
330
+ "dummy" | Out-File $dummyBinary
331
+
332
+ $testShortPath = Join-Path $tempDir $testShortName
333
+ $testCmdPath = Join-Path $tempDir "b4.cmd"
334
+
335
+ function Clear-B4Links {
336
+ if (Test-Path $testShortPath) { Remove-Item $testShortPath -Force -ErrorAction SilentlyContinue }
337
+ if (Test-Path $testCmdPath) { Remove-Item $testCmdPath -Force -ErrorAction SilentlyContinue }
338
+ }
339
+
340
+ # ── Scenario 1: b4 does not exist → should create ──
341
+ Clear-B4Links
342
+
343
+ New-Symlinks -BinaryName $testBinaryName -InstallDir $tempDir -PlatformKey $testPlatformKey
344
+
345
+ Test "New-Symlinks creates b4 when it does not exist" {
346
+ if (-not ((Test-Path $testShortPath) -or (Test-Path $testCmdPath))) {
347
+ throw "b4 link was not created (neither symlink nor .cmd wrapper found in $tempDir)"
348
+ }
349
+ }
350
+
351
+ # ── Scenario 2: b4 exists in install dir → should update ──
352
+ Clear-B4Links
353
+
354
+ # Create initial link
355
+ New-Symlinks -BinaryName $testBinaryName -InstallDir $tempDir -PlatformKey $testPlatformKey
356
+
357
+ $existingPath = if (Test-Path $testShortPath) { $testShortPath }
358
+ elseif (Test-Path $testCmdPath) { $testCmdPath }
359
+ else { $null }
360
+
361
+ if ($existingPath) {
362
+ $beforeTime = (Get-Item $existingPath).LastWriteTime
363
+ Start-Sleep -Milliseconds 200
364
+
365
+ New-Symlinks -BinaryName $testBinaryName -InstallDir $tempDir -PlatformKey $testPlatformKey
366
+
367
+ $afterPath = if (Test-Path $testShortPath) { $testShortPath }
368
+ elseif (Test-Path $testCmdPath) { $testCmdPath }
369
+ else { $null }
370
+
371
+ Test "New-Symlinks updates b4 when it already exists in install dir" {
372
+ if (-not $afterPath) {
373
+ throw "b4 link disappeared after update"
374
+ }
375
+ if ((Get-Item $afterPath).LastWriteTime -le $beforeTime) {
376
+ throw "b4 link was not updated (timestamp unchanged)"
377
+ }
378
+ }
379
+ } else {
380
+ Write-Host " SKIP New-Symlinks updates b4 (precondition: initial link creation failed)" -ForegroundColor Yellow
381
+ }
382
+
383
+ # ── Scenario 3: foreign b4 on PATH → should NOT create b4 in install dir ──
384
+ Clear-B4Links
385
+
386
+ $foreignDir = Join-Path ([System.IO.Path]::GetTempPath()) "b4-foreign-test-$(Get-Random)"
387
+ New-Item -ItemType Directory -Path $foreignDir -Force | Out-Null
388
+ try {
389
+ # Create fake b4 that is NOT browser4-cli
390
+ $foreignB4 = Join-Path $foreignDir $testShortName
391
+ if ($script:OSWin) {
392
+ # Use .cmd so Get-Command finds it via PATHEXT
393
+ $foreignCmd = Join-Path $foreignDir "b4.cmd"
394
+ "@echo off`r`necho NotBrowser4" | Out-File $foreignCmd -Encoding ASCII
395
+ } else {
396
+ "#!/bin/sh`necho NotBrowser4" | Out-File $foreignB4 -Encoding ASCII
397
+ & { chmod +x $foreignB4 2>$null } | Out-Null
398
+ }
399
+
400
+ $oldPath = $env:Path
401
+ $env:Path = "$foreignDir$([System.IO.Path]::PathSeparator)$oldPath"
402
+ try {
403
+ New-Symlinks -BinaryName $testBinaryName -InstallDir $tempDir -PlatformKey $testPlatformKey
404
+
405
+ Test "New-Symlinks skips b4 when foreign b4 is on PATH" {
406
+ if ((Test-Path $testShortPath) -or (Test-Path $testCmdPath)) {
407
+ throw "b4 link was created even though a non-browser4-cli b4 is on PATH"
408
+ }
409
+ }
410
+ } finally {
411
+ $env:Path = $oldPath
412
+ }
413
+ } finally {
414
+ Remove-Item $foreignDir -Recurse -Force -ErrorAction SilentlyContinue
415
+ }
416
+
417
+ # ── Scenario 4: b4 on PATH is browser4-cli → should create b4 link ──
418
+ Clear-B4Links
419
+
420
+ $oursDir = Join-Path ([System.IO.Path]::GetTempPath()) "b4-ours-test-$(Get-Random)"
421
+ New-Item -ItemType Directory -Path $oursDir -Force | Out-Null
422
+ try {
423
+ $oursB4 = Join-Path $oursDir $testShortName
424
+ if ($script:OSWin) {
425
+ $oursCmd = Join-Path $oursDir "b4.cmd"
426
+ "@echo off`r`necho browser4-cli v4.12.0" | Out-File $oursCmd -Encoding ASCII
427
+ } else {
428
+ "#!/bin/sh`necho browser4-cli v4.12.0" | Out-File $oursB4 -Encoding ASCII
429
+ & { chmod +x $oursB4 2>$null } | Out-Null
430
+ }
431
+
432
+ $oldPath = $env:Path
433
+ $env:Path = "$oursDir$([System.IO.Path]::PathSeparator)$oldPath"
434
+ try {
435
+ New-Symlinks -BinaryName $testBinaryName -InstallDir $tempDir -PlatformKey $testPlatformKey
436
+
437
+ Test "New-Symlinks creates b4 when b4 on PATH is browser4-cli" {
438
+ if (-not ((Test-Path $testShortPath) -or (Test-Path $testCmdPath))) {
439
+ throw "b4 link was not created even though PATH b4 is browser4-cli"
440
+ }
441
+ }
442
+ } finally {
443
+ $env:Path = $oldPath
444
+ }
445
+ } finally {
446
+ Remove-Item $oursDir -Recurse -Force -ErrorAction SilentlyContinue
447
+ }
448
+ } finally {
449
+ Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue
450
+ }
451
+ }
452
+
453
+ Write-Host ""
454
+
304
455
  # ── Summary ──
305
456
  Write-Host "============================================" -ForegroundColor Cyan
306
457
  $total = $pass + $fail
@@ -303,6 +303,141 @@ test "empty --version with dry-run doesn't download" bash -c "
303
303
 
304
304
  echo ""
305
305
 
306
+ # ── create_symlinks (b4 link logic) ─────────────────────
307
+
308
+ echo "--- create_symlinks ---" | cyan
309
+
310
+ # Run symlink tests using the already-sourced FUNC_TEST_SCRIPT.
311
+ SYMLINK_TEST_SCRIPT="$(mktemp)"
312
+ SYMLINK_RESULT="$(mktemp)"
313
+ trap "rm -f '$FUNC_TEST_SCRIPT' '$SYMLINK_TEST_SCRIPT' '$SYMLINK_RESULT'" EXIT
314
+
315
+ cat > "$SYMLINK_TEST_SCRIPT" << 'SYMLINKEOF'
316
+ src="$1"
317
+ result_file="$2"
318
+ set -- # clear positional params so the sourced install script doesn't parse our args
319
+ source "$src" >/dev/null 2>&1
320
+
321
+ # Detect platform for test names
322
+ case "$(uname -s)" in
323
+ MINGW*|MSYS*|CYGWIN*) test_platform="win32-x64"; test_ext=".exe" ;;
324
+ *) test_platform="linux-x64"; test_ext="" ;;
325
+ esac
326
+ test_binary="browser4-cli-${test_platform}${test_ext}"
327
+ test_short="b4${test_ext}"
328
+
329
+ temp_dir="${TMPDIR:-/tmp}/b4-install-test-$$"
330
+ mkdir -p "$temp_dir"
331
+ cd "$temp_dir" || exit 1
332
+
333
+ pass=0
334
+ fail=0
335
+
336
+ do_test() {
337
+ local name="$1" fn="$2"
338
+ if bash -c "$fn" 2>/dev/null; then
339
+ pass=$((pass + 1))
340
+ printf '\033[0;32m PASS %s\033[0m\n' "$name"
341
+ else
342
+ fail=$((fail + 1))
343
+ printf '\033[0;31m FAIL %s\033[0m\n' "$name"
344
+ fi
345
+ }
346
+
347
+ # Create dummy platform binary (required by create_symlinks)
348
+ echo "dummy" > "$test_binary"
349
+
350
+ # ── Scenario 1: b4 does not exist → should create ──
351
+ rm -f "$test_short" "b4.cmd" 2>/dev/null || true
352
+ create_symlinks "$test_binary" "$temp_dir" >/dev/null 2>&1
353
+
354
+ do_test "create_symlinks creates b4 when it does not exist" \
355
+ "[[ -e '$test_short' || -L '$test_short' || -e '$temp_dir/b4.cmd' ]]"
356
+
357
+ # ── Scenario 2: b4 exists in install dir → should update ──
358
+ rm -f "$test_short" "$temp_dir/b4.cmd" 2>/dev/null || true
359
+ create_symlinks "$test_binary" "$temp_dir" >/dev/null 2>&1
360
+
361
+ existing=""
362
+ if [[ -e "$test_short" ]] || [[ -L "$test_short" ]]; then
363
+ existing="$test_short"
364
+ elif [[ -e "$temp_dir/b4.cmd" ]]; then
365
+ existing="$temp_dir/b4.cmd"
366
+ fi
367
+
368
+ if [[ -n "$existing" ]]; then
369
+ # Remove the link and verify create_symlinks recreates it
370
+ rm -f "$existing"
371
+ [[ ! -e "$test_short" ]] && [[ ! -L "$test_short" ]] && [[ ! -e "$temp_dir/b4.cmd" ]] || true
372
+
373
+ create_symlinks "$test_binary" "$temp_dir" >/dev/null 2>&1
374
+
375
+ after_path=""
376
+ if [[ -e "$test_short" ]] || [[ -L "$test_short" ]]; then
377
+ after_path="$test_short"
378
+ elif [[ -e "$temp_dir/b4.cmd" ]]; then
379
+ after_path="$temp_dir/b4.cmd"
380
+ fi
381
+
382
+ do_test "create_symlinks updates b4 when it already exists in install dir" \
383
+ "[[ -n '$after_path' ]]"
384
+ else
385
+ printf ' SKIP create_symlinks updates b4 (precondition: initial link creation failed)\n'
386
+ fi
387
+
388
+ # ── Scenario 3: foreign b4 on PATH → should NOT create b4 ──
389
+ rm -f "$test_short" "$temp_dir/b4.cmd" 2>/dev/null || true
390
+
391
+ foreign_dir="${TMPDIR:-/tmp}/b4-foreign-$$"
392
+ mkdir -p "$foreign_dir"
393
+ foreign_b4="${foreign_dir}/${test_short}"
394
+ printf '#!/bin/sh\necho NotBrowser4\n' > "$foreign_b4"
395
+ chmod +x "$foreign_b4" 2>/dev/null || true
396
+
397
+ OLD_PATH="$PATH"
398
+ PATH="${foreign_dir}:${PATH}"
399
+ create_symlinks "$test_binary" "$temp_dir" >/dev/null 2>&1
400
+ PATH="$OLD_PATH"
401
+
402
+ do_test "create_symlinks skips b4 when foreign b4 is on PATH" \
403
+ "! [[ -e '$test_short' || -L '$test_short' || -e '$temp_dir/b4.cmd' ]]"
404
+
405
+ rm -rf "$foreign_dir"
406
+
407
+ # ── Scenario 4: b4 on PATH is browser4-cli → should create b4 ──
408
+ rm -f "$test_short" "$temp_dir/b4.cmd" 2>/dev/null || true
409
+
410
+ ours_dir="${TMPDIR:-/tmp}/b4-ours-$$"
411
+ mkdir -p "$ours_dir"
412
+ ours_b4="${ours_dir}/${test_short}"
413
+ printf '#!/bin/sh\necho browser4-cli v4.12.0\n' > "$ours_b4"
414
+ chmod +x "$ours_b4" 2>/dev/null || true
415
+
416
+ OLD_PATH="$PATH"
417
+ PATH="${ours_dir}:${PATH}"
418
+ create_symlinks "$test_binary" "$temp_dir" >/dev/null 2>&1
419
+ PATH="$OLD_PATH"
420
+
421
+ do_test "create_symlinks creates b4 when b4 on PATH is browser4-cli" \
422
+ "[[ -e '$test_short' || -L '$test_short' || -e '$temp_dir/b4.cmd' ]]"
423
+
424
+ rm -rf "$ours_dir"
425
+
426
+ # Cleanup temp dir
427
+ rm -rf "$temp_dir"
428
+
429
+ # Write results to the result file for the parent to parse
430
+ printf '%d\n%d\n' "$pass" "$fail" > "$result_file"
431
+ SYMLINKEOF
432
+
433
+ bash "$SYMLINK_TEST_SCRIPT" "$FUNC_TEST_SCRIPT" "$SYMLINK_RESULT"
434
+ symlink_pass=$(head -1 "$SYMLINK_RESULT")
435
+ symlink_fail=$(tail -1 "$SYMLINK_RESULT")
436
+ PASS=$((PASS + ${symlink_pass:-0}))
437
+ FAIL=$((FAIL + ${symlink_fail:-0}))
438
+
439
+ echo ""
440
+
306
441
  # ── Summary ────────────────────────────────────────────
307
442
 
308
443
  cyan "============================================"