browser4-cli 0.1.32 → 4.12.0-rc.2
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 +50 -2
- package/bin/browser4-cli-darwin-arm64 +0 -0
- package/bin/browser4-cli-darwin-x64 +0 -0
- package/bin/browser4-cli-linux-arm64 +0 -0
- package/bin/browser4-cli-linux-musl-arm64 +0 -0
- package/bin/browser4-cli-linux-musl-x64 +0 -0
- package/bin/browser4-cli-linux-x64 +0 -0
- package/bin/browser4-cli-win32-x64.exe +0 -0
- package/package.json +3 -4
- package/scripts/install-browser4-cli.ps1 +418 -410
- package/scripts/publish-if-needed.js +3 -1
- package/scripts/tests/install-browser4-cli.tests.ps1 +14 -69
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ browser4-cli htmlsnapshot get text "#main-content"
|
|
|
79
79
|
browser4-cli htmlsnapshot query --sql @query.sql
|
|
80
80
|
browser4-cli htmlsnapshot grep -i "error"
|
|
81
81
|
|
|
82
|
-
# AI-powered extraction and summarization (requires LLM key — see LLM Configuration
|
|
82
|
+
# AI-powered extraction and summarization (requires LLM key — see LLM Configuration below)
|
|
83
83
|
browser4-cli extract "product name, price, and rating as JSON"
|
|
84
84
|
browser4-cli summarize "key points in 3 bullets"
|
|
85
85
|
|
|
@@ -234,11 +234,12 @@ All modes accept `--timeout <ms>` (default: 30000). Load states: `networkidle`,
|
|
|
234
234
|
|
|
235
235
|
| Command | Description |
|
|
236
236
|
|---|---|
|
|
237
|
-
| `screenshot [ref]` | Take a screenshot. `--filename <path>`, `--full-page
|
|
237
|
+
| `screenshot [ref]` | Take a screenshot. `--filename <path>`, `--full-page`, `--viewport <n>`. Optionally of a specific element. |
|
|
238
238
|
| `pdf` | Save page as PDF. `--filename <path>`. |
|
|
239
239
|
|
|
240
240
|
```bash
|
|
241
241
|
browser4-cli screenshot --full-page --filename page.png
|
|
242
|
+
browser4-cli screenshot --viewport 0 --filename top.png
|
|
242
243
|
browser4-cli pdf --filename page.pdf
|
|
243
244
|
```
|
|
244
245
|
|
|
@@ -370,6 +371,30 @@ browser4-cli uninstall --dry-run
|
|
|
370
371
|
browser4-cli uninstall -y
|
|
371
372
|
```
|
|
372
373
|
|
|
374
|
+
### Skills
|
|
375
|
+
|
|
376
|
+
Manage bundled skill files embedded in the browser4-cli binary. Skill files are AI agent
|
|
377
|
+
instructions that always match the installed CLI version.
|
|
378
|
+
|
|
379
|
+
| Command | Description |
|
|
380
|
+
|---|---|
|
|
381
|
+
| `skills` | List all bundled skill names. Same as `skills list`. |
|
|
382
|
+
| `skills list` | List available bundled skills with file counts. |
|
|
383
|
+
| `skills get <name>` | Output a skill's SKILL.md content. `--full` includes references and templates. `--all` outputs every skill. |
|
|
384
|
+
| `skills path [name]` | Print the skills directory path. With a name, prints the path to that skill's subdirectory. |
|
|
385
|
+
|
|
386
|
+
Skill files are unpacked to the versioned installation directory during `browser4-cli install`.
|
|
387
|
+
Set `BROWSER4_SKILLS_DIR` to override the skills directory path.
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
browser4-cli skills # List bundled skills
|
|
391
|
+
browser4-cli skills get browser4-cli # Get the CLI skill's main content
|
|
392
|
+
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
|
|
396
|
+
```
|
|
397
|
+
|
|
373
398
|
### Other
|
|
374
399
|
|
|
375
400
|
| Command | Description |
|
|
@@ -600,6 +625,29 @@ browser4-cli htmlsnapshot inspect ".s-result-item" --depth 6 --max 20
|
|
|
600
625
|
|
|
601
626
|
---
|
|
602
627
|
|
|
628
|
+
### LLM Configuration
|
|
629
|
+
|
|
630
|
+
AI-powered commands (`agent`, `extract`, `summarize`) and X-SQL `llm_*` functions
|
|
631
|
+
require an LLM API key. Configure one provider via environment variables:
|
|
632
|
+
|
|
633
|
+
| Provider | Environment Variables |
|
|
634
|
+
|---|---|
|
|
635
|
+
| DeepSeek | `DEEPSEEK_API_KEY` |
|
|
636
|
+
| OpenRouter | `OPENROUTER_API_KEY`, `OPENROUTER_MODEL_NAME`, `OPENROUTER_BASE_URL` |
|
|
637
|
+
| Volcengine (ByteDance) | `VOLCENGINE_API_KEY`, `VOLCENGINE_MODEL_NAME`, `VOLCENGINE_BASE_URL` |
|
|
638
|
+
| OpenAI-compatible | `OPENAI_API_KEY`, `OPENAI_MODEL_NAME`, `OPENAI_BASE_URL` |
|
|
639
|
+
| Aliyun Qwen (DashScope) | `OPENAI_API_KEY`, `OPENAI_MODEL_NAME`, `OPENAI_BASE_URL` |
|
|
640
|
+
|
|
641
|
+
Example:
|
|
642
|
+
|
|
643
|
+
```bash
|
|
644
|
+
export DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
If no valid LLM key is configured, AI commands fail fast with a clear error at startup.
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
603
651
|
### Agent
|
|
604
652
|
|
|
605
653
|
The `agent` subcommands submit natural-language tasks that the Browser4 backend
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browser4-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "4.12.0-rc.2",
|
|
4
4
|
"description": "Browser automation CLI for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"bin",
|
|
8
8
|
"scripts",
|
|
9
|
-
"skill-data",
|
|
10
9
|
"skills"
|
|
11
10
|
],
|
|
12
11
|
"bin": {
|
|
13
|
-
"browser4-cli": "
|
|
14
|
-
"browser4": "
|
|
12
|
+
"browser4-cli": "bin/browser4-cli.js",
|
|
13
|
+
"browser4": "bin/browser4-cli.js"
|
|
15
14
|
},
|
|
16
15
|
"scripts": {
|
|
17
16
|
"version:sync": "node ../bin/version.mjs cli sync",
|
|
@@ -48,6 +48,10 @@
|
|
|
48
48
|
By default the script looks for the platform binary in its own directory
|
|
49
49
|
before downloading -- use this to force a fresh download.
|
|
50
50
|
|
|
51
|
+
.PARAMETER Force
|
|
52
|
+
Force reinstallation even if the binary is already installed at the target path.
|
|
53
|
+
Overrides -SkipIfInstalled and bypasses locked-file workarounds.
|
|
54
|
+
|
|
51
55
|
.PARAMETER Locate
|
|
52
56
|
Print detection results (OS, architecture, script location, China locale)
|
|
53
57
|
and exit without installing. Useful for diagnostics.
|
|
@@ -89,16 +93,17 @@
|
|
|
89
93
|
|
|
90
94
|
[CmdletBinding()]
|
|
91
95
|
param(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
[string]$Version = "",
|
|
97
|
+
[string]$InstallDir = "",
|
|
98
|
+
[ValidateSet("", "github", "oss")]
|
|
99
|
+
[string]$Source = "",
|
|
100
|
+
[bool]$AddToPath = $true,
|
|
101
|
+
[switch]$Silent,
|
|
102
|
+
[switch]$DryRun,
|
|
103
|
+
[switch]$SkipIfInstalled,
|
|
104
|
+
[switch]$SkipLocal,
|
|
105
|
+
[switch]$Force,
|
|
106
|
+
[switch]$Locate
|
|
102
107
|
)
|
|
103
108
|
|
|
104
109
|
$ErrorActionPreference = "Stop"
|
|
@@ -110,11 +115,11 @@ $ErrorActionPreference = "Stop"
|
|
|
110
115
|
# $PSScriptRoot is the directory containing this script (PS 3+).
|
|
111
116
|
# Falls back to $MyInvocation for edge cases (dot-sourced, PS 2).
|
|
112
117
|
$ScriptDir = if ($PSScriptRoot) {
|
|
113
|
-
|
|
118
|
+
$PSScriptRoot
|
|
114
119
|
} elseif ($MyInvocation -and $MyInvocation.MyCommand.Path) {
|
|
115
|
-
|
|
120
|
+
Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
116
121
|
} else {
|
|
117
|
-
|
|
122
|
+
$null
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
<#
|
|
@@ -123,18 +128,18 @@ $ScriptDir = if ($PSScriptRoot) {
|
|
|
123
128
|
Returns the full path if found, $null otherwise.
|
|
124
129
|
#>
|
|
125
130
|
function Find-LocalBinary {
|
|
126
|
-
|
|
131
|
+
param([string]$BinaryName)
|
|
127
132
|
|
|
128
|
-
|
|
133
|
+
if (-not $ScriptDir) { return $null }
|
|
129
134
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
$localPath = Join-Path $ScriptDir $BinaryName
|
|
136
|
+
if (Test-Path $localPath -PathType Leaf) {
|
|
137
|
+
$size = (Get-Item $localPath).Length
|
|
138
|
+
if ($size -gt 102400) { # > 100 KB minimum
|
|
139
|
+
return $localPath
|
|
140
|
+
}
|
|
135
141
|
}
|
|
136
|
-
|
|
137
|
-
return $null
|
|
142
|
+
return $null
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
<#
|
|
@@ -143,16 +148,16 @@ function Find-LocalBinary {
|
|
|
143
148
|
Returns $true if --version executes successfully, $false otherwise.
|
|
144
149
|
#>
|
|
145
150
|
function Test-LocalBinary {
|
|
146
|
-
|
|
151
|
+
param([string]$Path)
|
|
147
152
|
|
|
148
|
-
|
|
153
|
+
if (-not $Path -or -not (Test-Path $Path)) { return $false }
|
|
149
154
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
try {
|
|
156
|
+
$null = & $Path --version 2>&1
|
|
157
|
+
return ($LASTEXITCODE -eq 0)
|
|
158
|
+
} catch {
|
|
159
|
+
return $false
|
|
160
|
+
}
|
|
156
161
|
}
|
|
157
162
|
|
|
158
163
|
# ----------------------------------------------
|
|
@@ -176,23 +181,23 @@ if ($PSVersionTable.PSVersion.Major -ge 6) {
|
|
|
176
181
|
# ----------------------------------------------
|
|
177
182
|
|
|
178
183
|
function Write-Summary {
|
|
179
|
-
|
|
180
|
-
|
|
184
|
+
param([string]$Message, [string]$Color = "White")
|
|
185
|
+
if (-not $Silent) { Write-Host $Message -ForegroundColor $Color }
|
|
181
186
|
}
|
|
182
187
|
|
|
183
188
|
function Write-Step {
|
|
184
|
-
|
|
185
|
-
|
|
189
|
+
param([string]$Message)
|
|
190
|
+
if (-not $Silent) { Write-Host " >> $Message" -ForegroundColor Gray }
|
|
186
191
|
}
|
|
187
192
|
|
|
188
193
|
function Write-Check {
|
|
189
|
-
|
|
190
|
-
|
|
194
|
+
param([string]$Message)
|
|
195
|
+
if (-not $Silent) { Write-Host " [v] $Message" -ForegroundColor Green }
|
|
191
196
|
}
|
|
192
197
|
|
|
193
198
|
function Write-WarnMsg {
|
|
194
|
-
|
|
195
|
-
|
|
199
|
+
param([string]$Message)
|
|
200
|
+
if (-not $Silent) { Write-Host " [!] $Message" -ForegroundColor Yellow }
|
|
196
201
|
}
|
|
197
202
|
|
|
198
203
|
# ----------------------------------------------
|
|
@@ -200,50 +205,50 @@ function Write-WarnMsg {
|
|
|
200
205
|
# ----------------------------------------------
|
|
201
206
|
|
|
202
207
|
function Get-PlatformKey {
|
|
203
|
-
|
|
208
|
+
$arch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { "arm64" } else { "x64" }
|
|
204
209
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
210
|
+
if ($script:OSWin) {
|
|
211
|
+
return "win32-$arch"
|
|
212
|
+
}
|
|
213
|
+
elseif ($script:OSMac) {
|
|
214
|
+
return "darwin-$arch"
|
|
215
|
+
}
|
|
216
|
+
elseif ($script:OSLinux) {
|
|
217
|
+
# Detect musl
|
|
218
|
+
$isMusl = $false
|
|
219
|
+
try {
|
|
220
|
+
$lddOutput = ldd --version 2>&1
|
|
221
|
+
if ($lddOutput -match "musl") { $isMusl = $true }
|
|
222
|
+
} catch {
|
|
223
|
+
if ((Test-Path "/lib/ld-musl-x86_64.so.1") -or (Test-Path "/lib/ld-musl-aarch64.so.1")) {
|
|
224
|
+
$isMusl = $true
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
$libc = if ($isMusl) { "musl" } else { "" }
|
|
228
|
+
if ($libc) { return "linux-$libc-$arch" } else { return "linux-$arch" }
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
throw "Unsupported OS. Browser4 CLI supports Windows, macOS, and Linux."
|
|
221
232
|
}
|
|
222
|
-
$libc = if ($isMusl) { "musl" } else { "" }
|
|
223
|
-
if ($libc) { return "linux-$libc-$arch" } else { return "linux-$arch" }
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
throw "Unsupported OS. Browser4 CLI supports Windows, macOS, and Linux."
|
|
227
|
-
}
|
|
228
233
|
}
|
|
229
234
|
|
|
230
235
|
function Get-BinaryName {
|
|
231
|
-
|
|
232
|
-
|
|
236
|
+
param([string]$PlatformKey)
|
|
237
|
+
return "browser4-cli-$PlatformKey" + $(if ($PlatformKey.StartsWith("win32")) { ".exe" } else { "" })
|
|
233
238
|
}
|
|
234
239
|
|
|
235
240
|
function Get-DefaultInstallDir {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
elseif ($script:OSLinux -or $script:OSMac) {
|
|
240
|
-
# Prefer ~/.local/bin for user installs
|
|
241
|
-
if (Test-Path "$env:HOME/.local/bin") {
|
|
242
|
-
return "$env:HOME/.local/bin"
|
|
241
|
+
if ($script:OSWin) {
|
|
242
|
+
return Join-Path $env:LOCALAPPDATA "Programs\browser4-cli"
|
|
243
243
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
244
|
+
elseif ($script:OSLinux -or $script:OSMac) {
|
|
245
|
+
# Prefer ~/.local/bin for user installs
|
|
246
|
+
if (Test-Path "$env:HOME/.local/bin") {
|
|
247
|
+
return "$env:HOME/.local/bin"
|
|
248
|
+
}
|
|
249
|
+
return "$env:HOME/.local/bin"
|
|
250
|
+
}
|
|
251
|
+
throw "Unsupported OS"
|
|
247
252
|
}
|
|
248
253
|
|
|
249
254
|
# ----------------------------------------------
|
|
@@ -256,41 +261,41 @@ function Get-DefaultInstallDir {
|
|
|
256
261
|
Uses only local env vars and .NET APIs -- no network calls.
|
|
257
262
|
#>
|
|
258
263
|
function Test-ChinaLocale {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
# 2 -- TZ env var
|
|
266
|
-
$tzEnv = $env:TZ
|
|
267
|
-
if ($tzEnv -and ($tzEnv -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$')) {
|
|
268
|
-
return $true
|
|
269
|
-
}
|
|
264
|
+
# 1 -- Locale env vars
|
|
265
|
+
$lang = $env:LC_ALL, $env:LANG, $env:LC_CTYPE, $env:LC_MESSAGES | Where-Object { $_ } | Select-Object -First 1
|
|
266
|
+
if ($lang -and ($lang -match '^zh_CN' -or $lang -match '^zh-CN' -or $lang -match '^Chinese \(Simplified\)_China')) {
|
|
267
|
+
return $true
|
|
268
|
+
}
|
|
270
269
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
$
|
|
274
|
-
|
|
275
|
-
return $true
|
|
270
|
+
# 2 -- TZ env var
|
|
271
|
+
$tzEnv = $env:TZ
|
|
272
|
+
if ($tzEnv -and ($tzEnv -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$')) {
|
|
273
|
+
return $true
|
|
276
274
|
}
|
|
277
|
-
} catch {
|
|
278
|
-
# TimeZoneInfo not available (unlikely on PS 5.1+ but guard anyway)
|
|
279
|
-
}
|
|
280
275
|
|
|
281
|
-
|
|
282
|
-
if (-not $script:OSWin -and (Test-Path '/etc/timezone')) {
|
|
276
|
+
# 3 -- .NET TimeZoneInfo (works on Windows and Unix PowerShell 7+)
|
|
283
277
|
try {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
278
|
+
$tzId = [System.TimeZoneInfo]::Local.Id
|
|
279
|
+
if ($tzId -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$') {
|
|
280
|
+
return $true
|
|
281
|
+
}
|
|
288
282
|
} catch {
|
|
289
|
-
|
|
283
|
+
# TimeZoneInfo not available (unlikely on PS 5.1+ but guard anyway)
|
|
290
284
|
}
|
|
291
|
-
}
|
|
292
285
|
|
|
293
|
-
|
|
286
|
+
# 4 -- /etc/timezone (PowerShell on Linux/macOS)
|
|
287
|
+
if (-not $script:OSWin -and (Test-Path '/etc/timezone')) {
|
|
288
|
+
try {
|
|
289
|
+
$tz = Get-Content '/etc/timezone' -Raw -ErrorAction Stop
|
|
290
|
+
if ($tz -match '^Asia/(Shanghai|Chongqing|Urumqi|Harbin)$') {
|
|
291
|
+
return $true
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
# Permission or read error -- skip
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return $false
|
|
294
299
|
}
|
|
295
300
|
|
|
296
301
|
# ----------------------------------------------
|
|
@@ -302,71 +307,71 @@ $OSS_BASE = "https://browser4.oss-cn-beijing.aliyuncs.com"
|
|
|
302
307
|
$script:ChinaDetected = $false
|
|
303
308
|
|
|
304
309
|
function Get-DownloadUrls {
|
|
305
|
-
|
|
310
|
+
param([string]$BinaryName, [string]$VersionTag)
|
|
306
311
|
|
|
307
|
-
|
|
312
|
+
$urls = @()
|
|
308
313
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
314
|
+
$ghBase = "https://github.com/$GITHUB_REPO/releases/download"
|
|
315
|
+
if ($VersionTag) {
|
|
316
|
+
$ghUrl = "$ghBase/$VersionTag/$BinaryName"
|
|
317
|
+
$ossUrl = "$OSS_BASE/releases/download/$VersionTag/$BinaryName"
|
|
318
|
+
} else {
|
|
319
|
+
# Use 'latest' redirect for GitHub, 'latest' symlink for OSS
|
|
320
|
+
$ghUrl = "https://github.com/$GITHUB_REPO/releases/latest/download/$BinaryName"
|
|
321
|
+
$ossUrl = "$OSS_BASE/releases/download/latest/$BinaryName"
|
|
322
|
+
}
|
|
318
323
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
} else {
|
|
324
|
-
if ($script:ChinaDetected) {
|
|
325
|
-
$urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
|
|
326
|
-
$urls += @{ Url = $ghUrl; Label = "GitHub Releases" }
|
|
324
|
+
if ($Source -eq "github") {
|
|
325
|
+
$urls += @{ Url = $ghUrl; Label = "GitHub Releases" }
|
|
326
|
+
} elseif ($Source -eq "oss") {
|
|
327
|
+
$urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
|
|
327
328
|
} else {
|
|
328
|
-
|
|
329
|
-
|
|
329
|
+
if ($script:ChinaDetected) {
|
|
330
|
+
$urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
|
|
331
|
+
$urls += @{ Url = $ghUrl; Label = "GitHub Releases" }
|
|
332
|
+
} else {
|
|
333
|
+
$urls += @{ Url = $ghUrl; Label = "GitHub Releases" }
|
|
334
|
+
$urls += @{ Url = $ossUrl; Label = "Aliyun OSS" }
|
|
335
|
+
}
|
|
330
336
|
}
|
|
331
|
-
}
|
|
332
337
|
|
|
333
|
-
|
|
338
|
+
return $urls
|
|
334
339
|
}
|
|
335
340
|
|
|
336
341
|
function Invoke-Download {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
Write-Step "Trying $Label..."
|
|
340
|
-
Write-Step "URL: $Url"
|
|
341
|
-
|
|
342
|
-
if ($DryRun) {
|
|
343
|
-
Write-Check "[DRY-RUN] Would download to: $OutFile"
|
|
344
|
-
return $true
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
try {
|
|
348
|
-
$ProgressPreference = if ($Silent) { "SilentlyContinue" } else { "Continue" }
|
|
342
|
+
param([string]$Url, [string]$OutFile, [string]$Label)
|
|
349
343
|
|
|
350
|
-
|
|
351
|
-
|
|
344
|
+
Write-Step "Trying $Label..."
|
|
345
|
+
Write-Step "URL: $Url"
|
|
352
346
|
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
if ($size -gt 102400) { # > 100 KB minimum
|
|
356
|
-
Write-Check "Downloaded $( [math]::Round($size / 1MB, 1) ) MB"
|
|
347
|
+
if ($DryRun) {
|
|
348
|
+
Write-Check "[DRY-RUN] Would download to: $OutFile"
|
|
357
349
|
return $true
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
$ProgressPreference = if ($Silent) { "SilentlyContinue" } else { "Continue" }
|
|
354
|
+
|
|
355
|
+
# Use Invoke-WebRequest with progress bar
|
|
356
|
+
Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing -ErrorAction Stop
|
|
357
|
+
|
|
358
|
+
if (Test-Path $OutFile) {
|
|
359
|
+
$size = (Get-Item $OutFile).Length
|
|
360
|
+
if ($size -gt 102400) { # > 100 KB minimum
|
|
361
|
+
Write-Check "Downloaded $( [math]::Round($size / 1MB, 1) ) MB"
|
|
362
|
+
return $true
|
|
363
|
+
} else {
|
|
364
|
+
Write-WarnMsg "Downloaded file too small ($size bytes) -- may be an error page"
|
|
365
|
+
Remove-Item $OutFile -Force -ErrorAction SilentlyContinue
|
|
366
|
+
return $false
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
Write-WarnMsg "Download appeared to succeed but file not found"
|
|
370
|
+
return $false
|
|
371
|
+
} catch {
|
|
372
|
+
Write-WarnMsg "Failed: $($_.Exception.Message)"
|
|
361
373
|
return $false
|
|
362
|
-
}
|
|
363
374
|
}
|
|
364
|
-
Write-WarnMsg "Download appeared to succeed but file not found"
|
|
365
|
-
return $false
|
|
366
|
-
} catch {
|
|
367
|
-
Write-WarnMsg "Failed: $($_.Exception.Message)"
|
|
368
|
-
return $false
|
|
369
|
-
}
|
|
370
375
|
}
|
|
371
376
|
|
|
372
377
|
# ----------------------------------------------
|
|
@@ -374,129 +379,96 @@ function Invoke-Download {
|
|
|
374
379
|
# ----------------------------------------------
|
|
375
380
|
|
|
376
381
|
function New-PlatformLink {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
# Try symbolic link first (works on Unix; on Windows needs Admin or Developer Mode)
|
|
380
|
-
try {
|
|
381
|
-
New-Item -ItemType SymbolicLink -Path $LinkPath -Target $TargetName -Force -ErrorAction Stop | Out-Null
|
|
382
|
-
Write-Check "Created symlink: $DisplayName -> $TargetName"
|
|
383
|
-
return $true
|
|
384
|
-
} catch {
|
|
385
|
-
# Symbolic link failed -- try hard link on Windows
|
|
386
|
-
}
|
|
382
|
+
param([string]$LinkPath, [string]$TargetName, [string]$DisplayName)
|
|
387
383
|
|
|
388
|
-
|
|
389
|
-
if ($script:OSWin) {
|
|
384
|
+
# Try symbolic link first (works on Unix; on Windows needs Admin or Developer Mode)
|
|
390
385
|
try {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
return $true
|
|
386
|
+
New-Item -ItemType SymbolicLink -Path $LinkPath -Target $TargetName -Force -ErrorAction Stop | Out-Null
|
|
387
|
+
Write-Check "Created symlink: $DisplayName -> $TargetName"
|
|
388
|
+
return $true
|
|
395
389
|
} catch {
|
|
396
|
-
|
|
390
|
+
# Symbolic link failed -- try hard link on Windows
|
|
397
391
|
}
|
|
398
392
|
|
|
399
|
-
#
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
393
|
+
# Try hard link (Windows, same volume)
|
|
394
|
+
if ($script:OSWin) {
|
|
395
|
+
try {
|
|
396
|
+
$targetPath = Join-Path (Split-Path $LinkPath -Parent) $TargetName
|
|
397
|
+
New-Item -ItemType HardLink -Path $LinkPath -Target $targetPath -Force -ErrorAction Stop | Out-Null
|
|
398
|
+
Write-Check "Created hard link: $DisplayName -> $TargetName"
|
|
399
|
+
return $true
|
|
400
|
+
} catch {
|
|
401
|
+
# Hard link also failed -- create a .cmd wrapper as last resort
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
# Last resort: .cmd wrapper that forwards all arguments
|
|
405
|
+
try {
|
|
406
|
+
$wrapperContent = '@"%~dp0' + $TargetName + '" %*'
|
|
407
|
+
Set-Content -Path ($LinkPath -replace '\.exe$', '.cmd') -Value $wrapperContent -Force -ErrorAction Stop
|
|
408
|
+
Write-Check "Created wrapper: " + (Split-Path ($LinkPath -replace '\.exe$', '.cmd') -Leaf) + " -> $TargetName"
|
|
409
|
+
return $true
|
|
410
|
+
} catch {
|
|
411
|
+
Write-WarnMsg "Could not create link for $DisplayName (may need admin privileges)"
|
|
412
|
+
return $false
|
|
413
|
+
}
|
|
408
414
|
}
|
|
409
|
-
}
|
|
410
415
|
|
|
411
|
-
|
|
412
|
-
|
|
416
|
+
Write-WarnMsg "Could not create link for $DisplayName"
|
|
417
|
+
return $false
|
|
413
418
|
}
|
|
414
419
|
|
|
415
420
|
function New-Symlinks {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
$ext = if ($PlatformKey.StartsWith("win32")) { ".exe" } else { "" }
|
|
421
|
+
param([string]$BinaryName, [string]$InstallDir, [string]$PlatformKey)
|
|
419
422
|
|
|
420
|
-
|
|
421
|
-
$linkName = "browser4-cli$ext"
|
|
422
|
-
$linkPath = Join-Path $InstallDir $linkName
|
|
423
|
+
$ext = if ($PlatformKey.StartsWith("win32")) { ".exe" } else { "" }
|
|
423
424
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
New-PlatformLink -LinkPath $linkPath -TargetName $BinaryName -DisplayName $linkName
|
|
428
|
-
}
|
|
425
|
+
# 1) Always: browser4-cli -> browser4-cli-<platform>
|
|
426
|
+
$linkName = "browser4-cli$ext"
|
|
427
|
+
$linkPath = Join-Path $InstallDir $linkName
|
|
429
428
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
# Check if b4 is already on PATH (conflict with another tool)
|
|
435
|
-
$existingCmd = Get-Command b4 -ErrorAction SilentlyContinue
|
|
436
|
-
if ($existingCmd) {
|
|
437
|
-
Write-WarnMsg "Skipping short link '$shortName': 'b4' already found on PATH ($($existingCmd.Source))"
|
|
438
|
-
return
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
# Check if b4 already exists in the install directory
|
|
442
|
-
if (Test-Path $shortPath) {
|
|
443
|
-
Write-WarnMsg "Skipping short link '$shortName': already exists in $InstallDir"
|
|
444
|
-
return
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
# Also check b4.cmd if on Windows (wrapper fallback)
|
|
448
|
-
if ($script:OSWin) {
|
|
449
|
-
$shortCmdPath = Join-Path $InstallDir "b4.cmd"
|
|
450
|
-
if (Test-Path $shortCmdPath) {
|
|
451
|
-
Write-WarnMsg "Skipping short link '$shortName': 'b4.cmd' already exists in $InstallDir"
|
|
452
|
-
return
|
|
429
|
+
if ($DryRun) {
|
|
430
|
+
Write-Step "[DRY-RUN] Would create link: $linkName -> $BinaryName"
|
|
431
|
+
} else {
|
|
432
|
+
New-PlatformLink -LinkPath $linkPath -TargetName $BinaryName -DisplayName $linkName
|
|
453
433
|
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if ($DryRun) {
|
|
457
|
-
Write-Step "[DRY-RUN] Would create link: $shortName -> $BinaryName"
|
|
458
|
-
} else {
|
|
459
|
-
New-PlatformLink -LinkPath $shortPath -TargetName $BinaryName -DisplayName $shortName
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
# ----------------------------------------------
|
|
464
|
-
# PATH management
|
|
465
|
-
# ----------------------------------------------
|
|
466
|
-
|
|
467
|
-
function Add-DirectoryToUserPath {
|
|
468
|
-
param([string]$Dir)
|
|
469
434
|
|
|
470
|
-
|
|
471
|
-
|
|
435
|
+
# 2) Only if no conflict: b4 -> browser4-cli-<platform>
|
|
436
|
+
$shortName = "b4$ext"
|
|
437
|
+
$shortPath = Join-Path $InstallDir $shortName
|
|
472
438
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
if ($normalized -contains $dirResolved) {
|
|
480
|
-
Write-Check "Already in user PATH: $Dir"
|
|
481
|
-
return
|
|
482
|
-
}
|
|
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
|
+
}
|
|
483
445
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
446
|
+
# Check if b4 already exists in the install directory
|
|
447
|
+
if (Test-Path $shortPath) {
|
|
448
|
+
Write-WarnMsg "Skipping short link '$shortName': already exists in $InstallDir"
|
|
449
|
+
return
|
|
450
|
+
}
|
|
488
451
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
452
|
+
# Also check b4.cmd if on Windows (wrapper fallback)
|
|
453
|
+
if ($script:OSWin) {
|
|
454
|
+
$shortCmdPath = Join-Path $InstallDir "b4.cmd"
|
|
455
|
+
if (Test-Path $shortCmdPath) {
|
|
456
|
+
Write-WarnMsg "Skipping short link '$shortName': 'b4.cmd' already exists in $InstallDir"
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
}
|
|
492
460
|
|
|
493
|
-
|
|
494
|
-
|
|
461
|
+
if ($DryRun) {
|
|
462
|
+
Write-Step "[DRY-RUN] Would create link: $shortName -> $BinaryName"
|
|
463
|
+
} else {
|
|
464
|
+
New-PlatformLink -LinkPath $shortPath -TargetName $BinaryName -DisplayName $shortName
|
|
465
|
+
}
|
|
495
466
|
}
|
|
496
467
|
|
|
497
468
|
# ----------------------------------------------
|
|
498
469
|
# Helper: replace a binary that may be locked (i.e. currently running)
|
|
499
470
|
# ----------------------------------------------
|
|
471
|
+
|
|
500
472
|
function Set-BinaryFile {
|
|
501
473
|
param(
|
|
502
474
|
[Parameter(Mandatory=$true)] [string]$TargetPath,
|
|
@@ -515,7 +487,7 @@ function Set-BinaryFile {
|
|
|
515
487
|
Remove-Item $TargetPath -Force -ErrorAction Stop
|
|
516
488
|
} catch {
|
|
517
489
|
# The binary is locked (likely the currently-running process).
|
|
518
|
-
# On Windows we can rename a running executable
|
|
490
|
+
# On Windows we can rename a running executable -- move the old binary
|
|
519
491
|
# out of the way, place the new one alongside, and clean up the old one
|
|
520
492
|
# on the next install/upgrade.
|
|
521
493
|
try {
|
|
@@ -523,7 +495,7 @@ function Set-BinaryFile {
|
|
|
523
495
|
Write-WarnMsg "Existing binary is locked (it may be running)."
|
|
524
496
|
Write-WarnMsg "The old copy will be cleaned up on the next install or upgrade."
|
|
525
497
|
} catch {
|
|
526
|
-
throw "Cannot replace '$TargetPath'
|
|
498
|
+
throw "Cannot replace '$TargetPath' -- it is locked and cannot be renamed. Close all browser4-cli processes and try again."
|
|
527
499
|
}
|
|
528
500
|
}
|
|
529
501
|
}
|
|
@@ -535,133 +507,169 @@ function Set-BinaryFile {
|
|
|
535
507
|
Copy-Item $SourcePath $TargetPath -Force -ErrorAction Stop
|
|
536
508
|
}
|
|
537
509
|
}
|
|
510
|
+
|
|
538
511
|
# ----------------------------------------------
|
|
539
|
-
#
|
|
512
|
+
# PATH management
|
|
540
513
|
# ----------------------------------------------
|
|
541
514
|
|
|
542
|
-
function
|
|
543
|
-
|
|
544
|
-
Write-Summary " browser4-cli Installer" -Color Cyan
|
|
545
|
-
Write-Summary "==========================================" -Color Cyan
|
|
546
|
-
Write-Summary ""
|
|
515
|
+
function Add-DirectoryToUserPath {
|
|
516
|
+
param([string]$Dir)
|
|
547
517
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
$script:ChinaDetected = Test-ChinaLocale
|
|
551
|
-
if ($script:ChinaDetected) {
|
|
552
|
-
Write-Step "China mainland locale detected: preferring Aliyun OSS mirror."
|
|
553
|
-
}
|
|
554
|
-
}
|
|
518
|
+
$dirResolved = (Resolve-Path $Dir -ErrorAction SilentlyContinue).Path
|
|
519
|
+
if (-not $dirResolved) { $dirResolved = $Dir }
|
|
555
520
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
521
|
+
# Read current user PATH
|
|
522
|
+
$currentUserPath = [System.Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::User)
|
|
523
|
+
$paths = if ($currentUserPath) { $currentUserPath -split ";" | Where-Object { $_ } } else { @() }
|
|
559
524
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
Write-Step "Platform key: $platformKey"
|
|
566
|
-
Write-Step "Binary name: $binaryName"
|
|
567
|
-
Write-Step "Default install: $(Get-DefaultInstallDir)"
|
|
568
|
-
Write-Step "China locale: $script:ChinaDetected"
|
|
569
|
-
Write-Step "Source override: $(if ($Source) { $Source } else { 'auto' })"
|
|
570
|
-
Write-Step "OS: $(if ($script:OSWin) { 'Windows' } elseif ($script:OSMac) { 'macOS' } elseif ($script:OSLinux) { 'Linux' } else { 'Unknown' })"
|
|
571
|
-
|
|
572
|
-
# Check for local binary
|
|
573
|
-
$localPath = Find-LocalBinary -BinaryName $binaryName
|
|
574
|
-
if ($localPath) {
|
|
575
|
-
$localOk = Test-LocalBinary -Path $localPath
|
|
576
|
-
Write-Step "Local binary: $localPath $(if ($localOk) { '(valid)' } else { '(present but --version failed)' })"
|
|
577
|
-
} else {
|
|
578
|
-
Write-Step "Local binary: not found alongside script"
|
|
525
|
+
# Check if already present
|
|
526
|
+
$normalized = $paths | ForEach-Object { $rp = Resolve-Path $_ -ErrorAction SilentlyContinue; if ($rp) { $rp.Path } else { $_ } }
|
|
527
|
+
if ($normalized -contains $dirResolved) {
|
|
528
|
+
Write-Check "Already in user PATH: $Dir"
|
|
529
|
+
return
|
|
579
530
|
}
|
|
580
531
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
if (Test-Path $existingPath) {
|
|
585
|
-
Write-Step "Already installed: $existingPath"
|
|
586
|
-
} else {
|
|
587
|
-
Write-Step "Already installed: not found at $defaultDir"
|
|
532
|
+
if ($DryRun) {
|
|
533
|
+
Write-Check "[DRY-RUN] Would add to user PATH: $Dir"
|
|
534
|
+
return
|
|
588
535
|
}
|
|
589
536
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
Write-
|
|
593
|
-
Write-Step "Download order:"
|
|
594
|
-
foreach ($entry in $urls) {
|
|
595
|
-
Write-Step " $($entry.Label): $($entry.Url)"
|
|
596
|
-
}
|
|
537
|
+
$newPath = if ($currentUserPath) { "$currentUserPath;$Dir" } else { $Dir }
|
|
538
|
+
[System.Environment]::SetEnvironmentVariable("Path", $newPath, [System.EnvironmentVariableTarget]::User)
|
|
539
|
+
Write-Check "Added to user PATH: $Dir"
|
|
597
540
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
541
|
+
# Also update current session
|
|
542
|
+
$env:Path = "$env:Path;$Dir"
|
|
543
|
+
}
|
|
601
544
|
|
|
602
|
-
|
|
603
|
-
|
|
545
|
+
# ----------------------------------------------
|
|
546
|
+
# Main
|
|
547
|
+
# ----------------------------------------------
|
|
604
548
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
549
|
+
function Main {
|
|
550
|
+
Write-Summary "==========================================" -Color Cyan
|
|
551
|
+
Write-Summary " browser4-cli Installer" -Color Cyan
|
|
552
|
+
Write-Summary "==========================================" -Color Cyan
|
|
553
|
+
Write-Summary ""
|
|
609
554
|
|
|
610
|
-
|
|
611
|
-
if (-not $
|
|
612
|
-
|
|
555
|
+
# Auto-detect China mainland locale when no explicit source is given
|
|
556
|
+
if (-not $Source) {
|
|
557
|
+
$script:ChinaDetected = Test-ChinaLocale
|
|
558
|
+
if ($script:ChinaDetected) {
|
|
559
|
+
Write-Step "China mainland locale detected: preferring Aliyun OSS mirror."
|
|
560
|
+
}
|
|
613
561
|
}
|
|
614
|
-
Write-Step "Created directory: $installDir"
|
|
615
|
-
}
|
|
616
562
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
$
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
Write-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
Write-
|
|
630
|
-
|
|
563
|
+
# Detect platform
|
|
564
|
+
$platformKey = Get-PlatformKey
|
|
565
|
+
$binaryName = Get-BinaryName -PlatformKey $platformKey
|
|
566
|
+
|
|
567
|
+
# -- Locate mode: print diagnostics and exit --
|
|
568
|
+
if ($Locate) {
|
|
569
|
+
Write-Summary "--- Locate / diagnostics ---" -Color Cyan
|
|
570
|
+
Write-Summary ""
|
|
571
|
+
Write-Step "Script dir: $ScriptDir"
|
|
572
|
+
Write-Step "Platform key: $platformKey"
|
|
573
|
+
Write-Step "Binary name: $binaryName"
|
|
574
|
+
Write-Step "Default install: $(Get-DefaultInstallDir)"
|
|
575
|
+
Write-Step "China locale: $script:ChinaDetected"
|
|
576
|
+
Write-Step "Source override: $(if ($Source) { $Source } else { 'auto' })"
|
|
577
|
+
Write-Step "OS: $(if ($script:OSWin) { 'Windows' } elseif ($script:OSMac) { 'macOS' } elseif ($script:OSLinux) { 'Linux' } else { 'Unknown' })"
|
|
578
|
+
|
|
579
|
+
# Check for local binary
|
|
580
|
+
$localPath = Find-LocalBinary -BinaryName $binaryName
|
|
581
|
+
if ($localPath) {
|
|
582
|
+
$localOk = Test-LocalBinary -Path $localPath
|
|
583
|
+
Write-Step "Local binary: $localPath $(if ($localOk) { '(valid)' } else { '(present but --version failed)' })"
|
|
584
|
+
} else {
|
|
585
|
+
Write-Step "Local binary: not found alongside script"
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
# Check for already-installed binary
|
|
589
|
+
$defaultDir = Get-DefaultInstallDir
|
|
590
|
+
$existingPath = Join-Path $defaultDir $binaryName
|
|
591
|
+
if (Test-Path $existingPath) {
|
|
592
|
+
Write-Step "Already installed: $existingPath"
|
|
593
|
+
} else {
|
|
594
|
+
Write-Step "Already installed: not found at $defaultDir"
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
# Show download URLs that would be tried
|
|
598
|
+
$urls = Get-DownloadUrls -BinaryName $binaryName -VersionTag $Version
|
|
599
|
+
Write-Summary ""
|
|
600
|
+
Write-Step "Download order:"
|
|
601
|
+
foreach ($entry in $urls) {
|
|
602
|
+
Write-Step " $($entry.Label): $($entry.Url)"
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
Write-Summary ""
|
|
606
|
+
return
|
|
631
607
|
}
|
|
632
|
-
} elseif ($SkipLocal) {
|
|
633
|
-
Write-Step "Skipping local binary check (-SkipLocal)"
|
|
634
|
-
}
|
|
635
608
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
Write-Check "Binary already installed: $binaryPath"
|
|
639
|
-
} elseif ($useLocalBinary) {
|
|
640
|
-
# Copy local binary to install dir
|
|
641
|
-
if (-not $DryRun) {
|
|
642
|
-
Set-BinaryFile -TargetPath $binaryPath -SourcePath $localBinaryPath
|
|
643
|
-
}
|
|
644
|
-
Write-Check "Installed (local): $binaryPath"
|
|
645
|
-
} else {
|
|
646
|
-
# Build download URLs
|
|
647
|
-
$urls = Get-DownloadUrls -BinaryName $binaryName -VersionTag $Version
|
|
648
|
-
if (-not $urls -or $urls.Count -eq 0) {
|
|
649
|
-
throw "No download URLs configured"
|
|
650
|
-
}
|
|
609
|
+
Write-Step "Platform: $platformKey"
|
|
610
|
+
Write-Step "Binary: $binaryName"
|
|
651
611
|
|
|
652
|
-
|
|
653
|
-
$
|
|
612
|
+
# Determine install directory
|
|
613
|
+
$installDir = if ($InstallDir) { $InstallDir } else { Get-DefaultInstallDir }
|
|
614
|
+
Write-Step "Install: $installDir"
|
|
615
|
+
Write-Summary ""
|
|
654
616
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
617
|
+
if (-not (Test-Path $installDir)) {
|
|
618
|
+
if (-not $DryRun) {
|
|
619
|
+
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
|
|
620
|
+
}
|
|
621
|
+
Write-Step "Created directory: $installDir"
|
|
660
622
|
}
|
|
661
623
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
624
|
+
$binaryPath = Join-Path $installDir $binaryName
|
|
625
|
+
|
|
626
|
+
# -- Local binary discovery (bundled/sideload) --
|
|
627
|
+
$useLocalBinary = $false
|
|
628
|
+
if (-not $SkipLocal) {
|
|
629
|
+
$localBinaryPath = Find-LocalBinary -BinaryName $binaryName
|
|
630
|
+
if ($localBinaryPath) {
|
|
631
|
+
Write-Step "Found local binary alongside script: $(Split-Path $localBinaryPath -Leaf)"
|
|
632
|
+
if (Test-LocalBinary -Path $localBinaryPath) {
|
|
633
|
+
Write-Check "Local binary verified (--version OK)"
|
|
634
|
+
$useLocalBinary = $true
|
|
635
|
+
} else {
|
|
636
|
+
Write-WarnMsg "Local binary found but --version check failed -- will download instead"
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
} elseif ($SkipLocal) {
|
|
640
|
+
Write-Step "Skipping local binary check (-SkipLocal)"
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
# Skip download only when --skip-if-installed is set and binary already exists,
|
|
644
|
+
# unless --force overrides it.
|
|
645
|
+
if ((Test-Path $binaryPath) -and (-not $Version) -and $SkipIfInstalled -and (-not $Force) -and (-not $useLocalBinary)) {
|
|
646
|
+
Write-Check "Binary already installed: $binaryPath"
|
|
647
|
+
} elseif ($useLocalBinary) {
|
|
648
|
+
# Copy local binary to install dir
|
|
649
|
+
if (-not $DryRun) {
|
|
650
|
+
Set-BinaryFile -TargetPath $binaryPath -SourcePath $localBinaryPath
|
|
651
|
+
}
|
|
652
|
+
Write-Check "Installed (local): $binaryPath"
|
|
653
|
+
} else {
|
|
654
|
+
# Build download URLs
|
|
655
|
+
$urls = Get-DownloadUrls -BinaryName $binaryName -VersionTag $Version
|
|
656
|
+
if (-not $urls -or $urls.Count -eq 0) {
|
|
657
|
+
throw "No download URLs configured"
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
$downloaded = $false
|
|
661
|
+
$tempFile = [System.IO.Path]::GetTempFileName()
|
|
662
|
+
|
|
663
|
+
foreach ($entry in $urls) {
|
|
664
|
+
if (Invoke-Download -Url $entry.Url -OutFile $tempFile -Label $entry.Label) {
|
|
665
|
+
$downloaded = $true
|
|
666
|
+
break
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (-not $downloaded) {
|
|
671
|
+
if (Test-Path $tempFile) { Remove-Item $tempFile -Force }
|
|
672
|
+
throw @"
|
|
665
673
|
Could not download browser4-cli binary.
|
|
666
674
|
|
|
667
675
|
Tried:
|
|
@@ -673,70 +681,70 @@ Please check:
|
|
|
673
681
|
- For GitHub rate limits, set GITHUB_TOKEN environment variable
|
|
674
682
|
- If you have a local copy, place it alongside this script and re-run
|
|
675
683
|
"@
|
|
676
|
-
|
|
684
|
+
}
|
|
677
685
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
686
|
+
# Move from temp to install dir
|
|
687
|
+
if (-not $DryRun) {
|
|
688
|
+
Set-BinaryFile -TargetPath $binaryPath -SourcePath $tempFile -Move
|
|
689
|
+
}
|
|
690
|
+
Write-Check "Installed: $binaryPath"
|
|
681
691
|
}
|
|
682
|
-
Write-Check "Installed: $binaryPath"
|
|
683
|
-
}
|
|
684
692
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
693
|
+
# On Unix, ensure executable bit
|
|
694
|
+
if (-not $script:OSWin) {
|
|
695
|
+
if (-not $DryRun) {
|
|
696
|
+
try { chmod +x $binaryPath 2>$null } catch { }
|
|
697
|
+
}
|
|
689
698
|
}
|
|
690
|
-
}
|
|
691
699
|
|
|
692
|
-
|
|
693
|
-
Write-Summary ""
|
|
694
|
-
New-Symlinks -BinaryName $binaryName -InstallDir $installDir -PlatformKey $platformKey
|
|
695
|
-
|
|
696
|
-
# Add to PATH
|
|
697
|
-
if ($AddToPath -and $script:OSWin) {
|
|
700
|
+
# Create symlinks (browser4-cli -> platform binary, b4 if no conflict)
|
|
698
701
|
Write-Summary ""
|
|
699
|
-
|
|
700
|
-
|
|
702
|
+
New-Symlinks -BinaryName $binaryName -InstallDir $installDir -PlatformKey $platformKey
|
|
703
|
+
|
|
704
|
+
# Add to PATH
|
|
705
|
+
if ($AddToPath -and $script:OSWin) {
|
|
706
|
+
Write-Summary ""
|
|
707
|
+
Add-DirectoryToUserPath -Dir $installDir
|
|
708
|
+
} elseif ($AddToPath -and -not $script:OSWin) {
|
|
709
|
+
Write-Summary ""
|
|
710
|
+
$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" }
|
|
711
|
+
$pathLine = "export PATH=""$installDir`:`$PATH"""
|
|
712
|
+
if (-not $DryRun) {
|
|
713
|
+
if (-not (Select-String -Path $shellRc -Pattern [regex]::Escape($installDir) -ErrorAction SilentlyContinue)) {
|
|
714
|
+
Add-Content -Path $shellRc -Value ""
|
|
715
|
+
Add-Content -Path $shellRc -Value "# browser4-cli"
|
|
716
|
+
Add-Content -Path $shellRc -Value $pathLine
|
|
717
|
+
Write-Check "Added to PATH in $shellRc"
|
|
718
|
+
} else {
|
|
719
|
+
Write-Check "PATH entry already in $shellRc"
|
|
720
|
+
}
|
|
721
|
+
} else {
|
|
722
|
+
Write-Check "[DRY-RUN] Would add to $shellRc"
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
# Verify
|
|
701
727
|
Write-Summary ""
|
|
702
|
-
$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" }
|
|
703
|
-
$pathLine = "export PATH=""$installDir`:`$PATH"""
|
|
704
728
|
if (-not $DryRun) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
729
|
+
try {
|
|
730
|
+
$versionOutput = & $binaryPath --version 2>&1
|
|
731
|
+
Write-Summary "[v] browser4-cli installed successfully" -Color Green
|
|
732
|
+
Write-Summary " Version: $versionOutput"
|
|
733
|
+
} catch {
|
|
734
|
+
Write-Summary "[v] Binary installed at: $binaryPath" -Color Green
|
|
735
|
+
Write-WarnMsg "Could not verify --version (this is normal on first install)"
|
|
736
|
+
}
|
|
713
737
|
} else {
|
|
714
|
-
|
|
738
|
+
Write-Summary "[DRY-RUN] Installation plan complete" -Color Yellow
|
|
715
739
|
}
|
|
716
|
-
}
|
|
717
740
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
if (-not $DryRun) {
|
|
721
|
-
try {
|
|
722
|
-
$versionOutput = & $binaryPath --version 2>&1
|
|
723
|
-
Write-Summary "[v] browser4-cli installed successfully" -Color Green
|
|
724
|
-
Write-Summary " Version: $versionOutput"
|
|
725
|
-
} catch {
|
|
726
|
-
Write-Summary "[v] Binary installed at: $binaryPath" -Color Green
|
|
727
|
-
Write-WarnMsg "Could not verify --version (this is normal on first install)"
|
|
728
|
-
}
|
|
729
|
-
} else {
|
|
730
|
-
Write-Summary "[DRY-RUN] Installation plan complete" -Color Yellow
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
Write-Summary ""
|
|
734
|
-
Write-Summary "Run 'browser4-cli --help' to get started." -Color Cyan
|
|
741
|
+
Write-Summary ""
|
|
742
|
+
Write-Summary "Run 'browser4-cli --help' to get started." -Color Cyan
|
|
735
743
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
744
|
+
if ($script:OSWin) {
|
|
745
|
+
Write-Summary "If the command isn't found, restart your terminal or run:"
|
|
746
|
+
Write-Summary " `$env:Path = [System.Environment]::GetEnvironmentVariable('Path','User') + ';' + [System.Environment]::GetEnvironmentVariable('Path','Machine')"
|
|
747
|
+
}
|
|
740
748
|
}
|
|
741
749
|
|
|
742
750
|
Main
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
|
|
3
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
4
|
+
# CROSS-PLATFORM: This script must run on Linux, macOS, and Windows.
|
|
5
|
+
# - Use $IsWindows / $IsLinux / $IsMacOS for platform detection.
|
|
6
|
+
# - Use "($IsWindows -or $env:OS -eq 'Windows_NT')" for PS 5.1 compat.
|
|
7
|
+
# - Avoid Windows-only env vars ($env:TEMP) — use $env:TMPDIR fallback.
|
|
8
|
+
# - Guard "chcp" and other Windows-only commands behind platform checks.
|
|
9
|
+
# - Paths: use Join-Path / Split-Path; never bake \ or / as literal.
|
|
10
|
+
# - [System.IO.Path]::IsPathRooted is platform-aware — C:\foo is NOT
|
|
11
|
+
# rooted on Linux; test with platform-appropriate absolute paths.
|
|
12
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
13
|
+
|
|
1
14
|
<#
|
|
2
15
|
.SYNOPSIS
|
|
3
16
|
Tests for install-browser4-cli.ps1
|
|
@@ -30,7 +43,7 @@ function RunScript([string]$scriptArgs, [ref]$exitCode) {
|
|
|
30
43
|
$tmpErr = [System.IO.Path]::GetTempFileName()
|
|
31
44
|
$cmd = "& '$installScript' $scriptArgs 2>'$tmpErr'"
|
|
32
45
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
33
|
-
$psi.FileName = "powershell.exe"
|
|
46
|
+
$psi.FileName = if ($IsWindows -or ($env:OS -eq 'Windows_NT')) { "powershell.exe" } else { "pwsh" }
|
|
34
47
|
$psi.Arguments = "-NoProfile -ExecutionPolicy Bypass -Command `"$cmd`""
|
|
35
48
|
$psi.RedirectStandardOutput = $true
|
|
36
49
|
$psi.UseShellExecute = $false
|
|
@@ -284,74 +297,6 @@ $sb = [ScriptBlock]::Create($scriptContent)
|
|
|
284
297
|
Test "Test-LocalBinary returns false for null" {
|
|
285
298
|
if (Test-LocalBinary -Path $null) { throw "Should be false" }
|
|
286
299
|
}
|
|
287
|
-
|
|
288
|
-
# ── Set-BinaryFile (locked-binary-safe replacement) ──
|
|
289
|
-
Test "Set-BinaryFile creates new file (Copy)" {
|
|
290
|
-
$tmpDir = Join-Path $env:TEMP "b4-test-$(New-Guid)"
|
|
291
|
-
New-Item -ItemType Directory $tmpDir -Force | Out-Null
|
|
292
|
-
try {
|
|
293
|
-
$src = Join-Path $tmpDir "source.exe"
|
|
294
|
-
$dst = Join-Path $tmpDir "target.exe"
|
|
295
|
-
"new-content" | Out-File $src -Encoding ASCII
|
|
296
|
-
Set-BinaryFile -TargetPath $dst -SourcePath $src
|
|
297
|
-
if (-not (Test-Path $dst)) { throw "Target was not created" }
|
|
298
|
-
$actual = (Get-Content $dst -Raw).Trim()
|
|
299
|
-
if ($actual -ne "new-content") { throw "Wrong content: '$actual'" }
|
|
300
|
-
# Source still exists (Copy mode)
|
|
301
|
-
if (-not (Test-Path $src)) { throw "Source should still exist after copy" }
|
|
302
|
-
} finally {
|
|
303
|
-
Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
Test "Set-BinaryFile creates new file (Move)" {
|
|
308
|
-
$tmpDir = Join-Path $env:TEMP "b4-test-$(New-Guid)"
|
|
309
|
-
New-Item -ItemType Directory $tmpDir -Force | Out-Null
|
|
310
|
-
try {
|
|
311
|
-
$src = Join-Path $tmpDir "source.exe"
|
|
312
|
-
$dst = Join-Path $tmpDir "target.exe"
|
|
313
|
-
"moved-content" | Out-File $src -Encoding ASCII
|
|
314
|
-
Set-BinaryFile -TargetPath $dst -SourcePath $src -Move
|
|
315
|
-
if (-not (Test-Path $dst)) { throw "Target was not created" }
|
|
316
|
-
$actual = (Get-Content $dst -Raw).Trim()
|
|
317
|
-
if ($actual -ne "moved-content") { throw "Wrong content: '$actual'" }
|
|
318
|
-
# Source should be gone (Move mode)
|
|
319
|
-
if (Test-Path $src) { throw "Source should not exist after move" }
|
|
320
|
-
} finally {
|
|
321
|
-
Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
Test "Set-BinaryFile replaces existing file (Copy)" {
|
|
326
|
-
$tmpDir = Join-Path $env:TEMP "b4-test-$(New-Guid)"
|
|
327
|
-
New-Item -ItemType Directory $tmpDir -Force | Out-Null
|
|
328
|
-
try {
|
|
329
|
-
$src = Join-Path $tmpDir "source.exe"
|
|
330
|
-
$dst = Join-Path $tmpDir "target.exe"
|
|
331
|
-
"old" | Out-File $dst -Encoding ASCII
|
|
332
|
-
"new" | Out-File $src -Encoding ASCII
|
|
333
|
-
Set-BinaryFile -TargetPath $dst -SourcePath $src
|
|
334
|
-
if ((Get-Content $dst -Raw).Trim() -ne "new") { throw "Content not replaced" }
|
|
335
|
-
} finally {
|
|
336
|
-
Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
Test "Set-BinaryFile replaces existing file (Move)" {
|
|
341
|
-
$tmpDir = Join-Path $env:TEMP "b4-test-$(New-Guid)"
|
|
342
|
-
New-Item -ItemType Directory $tmpDir -Force | Out-Null
|
|
343
|
-
try {
|
|
344
|
-
$src = Join-Path $tmpDir "source.exe"
|
|
345
|
-
$dst = Join-Path $tmpDir "target.exe"
|
|
346
|
-
"old" | Out-File $dst -Encoding ASCII
|
|
347
|
-
"moved" | Out-File $src -Encoding ASCII
|
|
348
|
-
Set-BinaryFile -TargetPath $dst -SourcePath $src -Move
|
|
349
|
-
if ((Get-Content $dst -Raw).Trim() -ne "moved") { throw "Content not replaced" }
|
|
350
|
-
if (Test-Path $src) { throw "Source should not exist after move" }
|
|
351
|
-
} finally {
|
|
352
|
-
Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
300
|
}
|
|
356
301
|
|
|
357
302
|
Write-Host ""
|