bridgerator 1.0.8 → 1.1.1

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.
Files changed (76) hide show
  1. package/README.md +176 -110
  2. package/assets/bridgerator.ico +0 -0
  3. package/assets/icon-16.png +0 -0
  4. package/assets/icon-20.png +0 -0
  5. package/assets/icon-24.png +0 -0
  6. package/assets/icon-256.png +0 -0
  7. package/assets/icon-32.png +0 -0
  8. package/assets/icon-48.png +0 -0
  9. package/assets/icon-64.png +0 -0
  10. package/assets/logo.svg +1 -0
  11. package/install.ps1 +456 -56
  12. package/npm-shrinkwrap.json +8621 -0
  13. package/package.json +55 -49
  14. package/recipes/backup-folder.json +31 -0
  15. package/recipes/clean-downloads.json +48 -0
  16. package/recipes/disk-report.json +66 -0
  17. package/recipes/sort-folder.json +49 -0
  18. package/recipes/update-apps.json +25 -0
  19. package/src/ask-lib.ps1 +230 -0
  20. package/src/ask.mjs +51 -0
  21. package/src/ask.ps1 +17 -0
  22. package/src/automations.mjs +673 -0
  23. package/src/batch.mjs +221 -0
  24. package/src/bridge.mjs +720 -29
  25. package/src/cli.mjs +529 -74
  26. package/src/confirm.mjs +418 -0
  27. package/src/dc-config-hooks.mjs +18 -8
  28. package/src/dc-isolate.mjs +5 -0
  29. package/src/delegate.mjs +555 -0
  30. package/src/docs-tool.mjs +494 -0
  31. package/src/doctor.mjs +200 -0
  32. package/src/events.mjs +21 -0
  33. package/src/extra-tools.mjs +124 -0
  34. package/src/guard.mjs +34 -0
  35. package/src/notice.mjs +84 -23
  36. package/src/pair.mjs +27 -4
  37. package/src/pc-check.mjs +112 -0
  38. package/src/pc-check.ps1 +103 -0
  39. package/src/pc-text.mjs +120 -0
  40. package/src/pdf-font.mjs +245 -0
  41. package/src/photos-tool.mjs +187 -0
  42. package/src/photos.ps1 +145 -0
  43. package/src/plan.mjs +90 -0
  44. package/src/policy.mjs +293 -124
  45. package/src/queue.mjs +194 -0
  46. package/src/recent.mjs +48 -0
  47. package/src/recipes.mjs +108 -0
  48. package/src/recycle.mjs +185 -0
  49. package/src/recycle.ps1 +110 -0
  50. package/src/screen-tool.mjs +185 -0
  51. package/src/screen.ps1 +285 -0
  52. package/src/search-tool.mjs +189 -0
  53. package/src/search.ps1 +129 -0
  54. package/src/settings-tool.mjs +186 -0
  55. package/src/setup-tool.mjs +129 -0
  56. package/src/software-tool.mjs +185 -0
  57. package/src/telemetry.mjs +86 -0
  58. package/src/tool-lib.mjs +177 -0
  59. package/src/tool-text.mjs +45 -0
  60. package/src/tray.mjs +97 -0
  61. package/src/tray.ps1 +359 -0
  62. package/src/undo.mjs +313 -0
  63. package/src/win.mjs +17 -0
  64. package/ui/app.css +388 -0
  65. package/ui/app.js +868 -0
  66. package/ui/i18n/en.json +598 -0
  67. package/ui/i18n/ru.json +598 -0
  68. package/ui/index.html +26 -0
  69. package/vendor/webview2/LICENSE.txt +27 -0
  70. package/vendor/webview2/Microsoft.Web.WebView2.Core.dll +0 -0
  71. package/vendor/webview2/Microsoft.Web.WebView2.WinForms.dll +0 -0
  72. package/vendor/webview2/NOTICE.txt +85 -0
  73. package/vendor/webview2/SHA256SUMS.txt +4 -0
  74. package/vendor/webview2/SOURCE.txt +1 -0
  75. package/vendor/webview2/arm64/WebView2Loader.dll +0 -0
  76. package/vendor/webview2/x64/WebView2Loader.dll +0 -0
package/install.ps1 CHANGED
@@ -1,107 +1,507 @@
1
1
  # Bridgerator installer for Windows (PowerShell 5.1+). Source: npm package "bridgerator" (https://www.npmjs.com/package/bridgerator)
2
2
  # Usage (one line, from your Bridgerator account page):
3
3
  # & ([scriptblock]::Create((irm https://bridgerator.recondeck.com/install.ps1)))
4
- # A browser tab opens: sign in and confirm this computer — no key to paste. Run again to update
4
+ # A browser tab opens: sign in and confirm this computer - no key to paste. Run again to update
5
5
  # (keeps the key and folders). Servers / headless PCs: add -Token <device key> from the account page.
6
- # What it does: installs Node.js LTS if missing (portable zip, no admin), installs the bridgerator npm package,
7
- # then runs `bridgerator.cmd setup` (pairing in the browser if there is no key yet, config + hidden autostart
8
- # at logon for THIS user, no admin rights),
6
+ # What it does: downloads the signed Bridgerator app for Windows (bridgerator.exe + its files, no Node.js
7
+ # needed) from bridgerator.recondeck.com, checks it (SHA256 from the download list, every file), puts it
8
+ # into %LOCALAPPDATA%\Bridgerator\app, then runs `bridgerator.exe setup` (pairing in the browser if there
9
+ # is no key yet, config + hidden autostart at logon for THIS user, no admin rights),
9
10
  # then opens the welcome page in the default browser (skip with -NoBrowser).
11
+ # An older Bridgerator (npm package, 1.0.x) is replaced; its key and settings are kept.
12
+ # -UseNpm: the npm package instead (installs Node.js 24 LTS if there is no Node.js 22+ - portable zip, no
13
+ # admin - then the bridgerator package, SHA256-checked, no install scripts).
14
+ # -NoTelemetry: no anonymous usage counts (see https://bridgerator.recondeck.com/privacy).
15
+ # Works under the default Restricted execution policy: only built-in cmdlets and .NET are used.
10
16
  param(
11
17
  [string]$Token,
12
- [string]$Package = 'https://bridgerator.recondeck.com/bridgerator.tgz',
18
+ [string]$Package,
19
+ [string]$Sha256,
20
+ [string]$AppZip,
13
21
  [string[]]$Allow,
14
22
  [string[]]$Deny,
15
- [switch]$NoBrowser
23
+ [switch]$UseNpm,
24
+ [switch]$NoBrowser,
25
+ [switch]$NoTelemetry
16
26
  )
17
27
  $ErrorActionPreference = 'Stop'
18
- $paired = $Token -or (Test-Path (Join-Path $env:APPDATA 'Bridgerator\config.json'))
28
+ # SHA256 of the bridgerator.tgz published next to this script (written by release.sh).
29
+ $PackageSha256 = '990b1a9754310fcf75530ac6365f149843037adcff88cf7e33b4bed7e03ce211'
30
+ # SHA256 of bridgerator-win.json, the Windows app download list published next to this script
31
+ # (written by scripts/build-exe.mjs / release.sh). It names the zip per architecture and its SHA256.
32
+ $WinManifestSha256 = '5ba96127dc260e8469a0cf2432a11963c11b96417131432e563fa56092cc949d'
33
+ $Site = 'https://bridgerator.recondeck.com'
34
+ # The npm package comes from the same site as this script (a staging copy rewrites only $Site / $Relay).
35
+ $DefaultPackage = "$Site/bridgerator.tgz"
36
+ if (-not $Package) { $Package = $DefaultPackage }
37
+ # Relay for setup: always the relay of the site this script came from, so a PC paired elsewhere (staging)
38
+ # moves here (its old key is rejected -> pairing again). A staging build rewrites both lines.
39
+ $Relay = 'wss://mcp.bridgerator.recondeck.com/link'
40
+ $Root = Join-Path $env:LOCALAPPDATA 'Bridgerator'
41
+ $AppDir = Join-Path $Root 'app'
42
+ $NpmPkg = Join-Path $env:APPDATA 'npm\node_modules\bridgerator'
43
+ # The npm package: asked for (-UseNpm), or a package given (-Package <tgz>, as before 1.0.9).
44
+ $npmMode = [bool]$UseNpm -or $PSBoundParameters.ContainsKey('Package')
45
+ $firstInstall = -not (Test-Path (Join-Path $env:APPDATA 'Bridgerator\config.json'))
46
+ $paired = $Token -or -not $firstInstall
47
+ # All temporary files go here, never to %TEMP%: on a Cyrillic & co. user name %TEMP% is an 8.3 short
48
+ # path (C:\Users\3C8A~1\...) that Move-Item / Remove-Item can't handle. Removed in the end, always.
49
+ $Work = Join-Path $env:LOCALAPPDATA "Bridgerator\tmp-$([guid]::NewGuid().ToString('N'))"
50
+ function Remove-Work { try { if ([IO.Directory]::Exists($Work)) { [IO.Directory]::Delete($Work, $true) } } catch {} }
51
+ $NpmFlags = @('--no-audit', '--no-fund', '--update-notifier=false', '--loglevel=error')
52
+ $Sys32 = Join-Path $env:SystemRoot 'System32'
19
53
 
20
- # Private Node.js (portable zip) used when there is none or it is older than 20; it goes FIRST on
21
- # the PATH so an old system Node does not shadow it.
54
+ # Private Node.js (portable zip) used when there is no Node.js 22+; it goes FIRST on the PATH so an
55
+ # old system Node does not shadow it.
22
56
  $NodeDir = Join-Path $env:LOCALAPPDATA 'Bridgerator\node'
23
57
  function Refresh-Path {
24
58
  $first = if (Test-Path (Join-Path $NodeDir 'node.exe')) { $NodeDir }
25
59
  $env:Path = (@($first, [Environment]::GetEnvironmentVariable('Path', 'Machine'), [Environment]::GetEnvironmentVariable('Path', 'User')) | Where-Object { $_ }) -join ';'
26
60
  }
61
+ # Where a 1.0.x npm package can be: npm's usual global folder, or the installer's private Node.js (its
62
+ # global folder is the Node folder itself). -> the package folders that exist.
63
+ function Get-NpmBridges { @($NpmPkg, (Join-Path $NodeDir 'node_modules\bridgerator')) | Where-Object { [IO.File]::Exists((Join-Path $_ 'package.json')) } }
64
+ # One spelling per folder: full path, / -> \, no trailing \ (except a drive root: C:\), 8.3 short parts (C:\Users\3C8A~1) made long.
65
+ # npm's prefix and %APPDATA% can name the same folder differently; compared case-insensitively (Test-SamePath).
66
+ function Get-LongPath([string]$p) {
67
+ $p = [IO.Path]::GetFullPath($p.Replace('/', '\')).TrimEnd('\')
68
+ if ($p -match '^[A-Za-z]:$') { $p += '\' } # a drive root keeps its \ (C: alone = that drive's current folder)
69
+ $parent = [IO.Path]::GetDirectoryName($p)
70
+ if (-not $parent -or -not $p.Contains('~')) { return $p }
71
+ $base = Get-LongPath $parent; $leaf = [IO.Path]::GetFileName($p)
72
+ # The file system answers a short name with the long one.
73
+ try { $hit = @([IO.Directory]::GetFileSystemEntries($base, $leaf)); if ($hit.Count -eq 1) { return $hit[0].TrimEnd('\') } } catch {}
74
+ return (Join-Path $base $leaf)
75
+ }
76
+ function Test-SamePath([string]$a, [string]$b) { $a -and $b -and [string]::Equals((Get-LongPath $a), (Get-LongPath $b), [StringComparison]::OrdinalIgnoreCase) }
77
+ function Test-InFolder([string]$p, [string]$dir) { if (-not $p) { return $false }; $l = Get-LongPath $p; $d = Get-LongPath $dir; [string]::Equals($l, $d, [StringComparison]::OrdinalIgnoreCase) -or $l.StartsWith($d.TrimEnd('\') + '\', [StringComparison]::OrdinalIgnoreCase) }
78
+
79
+ # User PATH through the registry: keeps it REG_EXPAND_SZ with %VARS% unexpanded.
80
+ function Add-UserPath([string]$dir, [switch]$First) {
81
+ $k = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment')
82
+ $userPath = [string]$k.GetValue('Path', '', 'DoNotExpandEnvironmentNames')
83
+ $parts = @($userPath -split ';' | Where-Object { $_ })
84
+ $key = $dir.TrimEnd('\')
85
+ $keys = @($parts | ForEach-Object { [Environment]::ExpandEnvironmentVariables($_).TrimEnd('\') })
86
+ # -First: also moves an entry that is already there to the front (an older bridgerator.cmd must not shadow it).
87
+ if ($keys -notcontains $key -or ($First -and $keys[0] -ne $key)) {
88
+ $rest = @(for ($i = 0; $i -lt $parts.Count; $i++) { if ($keys[$i] -ne $key) { $parts[$i] } })
89
+ $new = if ($First) { @($dir) + $rest } else { $rest + @($dir) }
90
+ $k.SetValue('Path', ($new -join ';'), [Microsoft.Win32.RegistryValueKind]::ExpandString)
91
+ # Tell Explorer the environment changed (new windows see the new PATH).
92
+ [Environment]::SetEnvironmentVariable('BRIDGERATOR_PATH_REFRESH', '1', 'User'); [Environment]::SetEnvironmentVariable('BRIDGERATOR_PATH_REFRESH', $null, 'User')
93
+ }
94
+ $k.Close()
95
+ }
96
+
97
+ function Get-Sha256([string]$file) {
98
+ $f = [IO.File]::OpenRead($file)
99
+ try { -join ([Security.Cryptography.SHA256]::Create().ComputeHash($f) | ForEach-Object { $_.ToString('x2') }) } finally { $f.Close() }
100
+ }
101
+
102
+ # Same matcher as `bridgerator.cmd stop` (src/cli.mjs stopPs) - for versions that don't have `stop` yet:
103
+ # this Windows session only; the autostart loop first, then node/bridgerator/rg running from the package
104
+ # folder. $pkg = the package folder (npm: ...\node_modules\bridgerator; app: %LOCALAPPDATA%\Bridgerator\app),
105
+ # $cmd = what runs its `stop` (bridgerator.cmd / bridgerator.exe).
106
+ function Stop-Bridge([string]$pkgDir, [string]$cmd) {
107
+ $cli = Join-Path $pkgDir 'src\cli.mjs'
108
+ # Its own `stop` first; if that fails (e.g. node.exe of an npm install not found), the matcher below.
109
+ if ((Test-Path $cli) -and (Test-Path $cmd) -and ((Get-Content $cli -Raw) -match "cmd === 'stop'")) { & $cmd stop | Out-Null; if ($LASTEXITCODE -eq 0) { Stop-OurWebView; return } }
110
+ $me = (Get-Process -Id $PID).SessionId
111
+ $pkg = $pkgDir.ToLower().TrimEnd('\') + '\'
112
+ $vbs = (Join-Path $env:APPDATA 'Bridgerator\run-hidden.vbs').ToLower()
113
+ $all = @(Get-CimInstance Win32_Process -Filter "Name='wscript.exe' OR Name='node.exe' OR Name='bridgerator.exe' OR Name='rg.exe' OR Name='powershell.exe'" -ErrorAction SilentlyContinue |
114
+ Where-Object { $s = ("$($_.CommandLine) $($_.ExecutablePath)").ToLower().Replace('/', '\'); $_.SessionId -eq $me -and $_.ProcessId -ne $PID -and ($s.Contains($pkg) -or $s.Contains($vbs)) } |
115
+ Sort-Object { $_.Name -ne 'wscript.exe' })
116
+ $all | Where-Object { $_.Name -ne 'powershell.exe' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
117
+ # The tray helper removes its icon itself once the bridge is gone (a kill leaves a ghost icon): 2 s, then the kill.
118
+ $tray = @($all | Where-Object { $_.Name -eq 'powershell.exe' }); $end = (Get-Date).AddSeconds(2)
119
+ while ($tray.Count -and (Get-Date) -lt $end -and @($tray | Where-Object { Get-Process -Id $_.ProcessId -ErrorAction SilentlyContinue }).Count) { Start-Sleep -Milliseconds 200 }
120
+ $tray | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
121
+ Stop-OurWebView
122
+ }
123
+ # The old window's WebView2 processes (msedgewebview2.exe with our user data folder, this session): an
124
+ # older `stop` leaves them running (V-8).
125
+ function Stop-OurWebView {
126
+ $me = (Get-Process -Id $PID).SessionId
127
+ $wvd = ('--user-data-dir=' + (Join-Path $env:LOCALAPPDATA 'Bridgerator\webview')).ToLower()
128
+ $wv = @(Get-CimInstance Win32_Process -Filter "Name='msedgewebview2.exe'" -ErrorAction SilentlyContinue | Where-Object { $_.SessionId -eq $me }); $ids = @($wv | Where-Object { "$($_.CommandLine)".ToLower().Replace('"', '').Contains($wvd) } | ForEach-Object { $_.ProcessId })
129
+ $wv | Where-Object { $ids -contains $_.ProcessId -or $ids -contains $_.ParentProcessId } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
130
+ }
131
+
132
+ # Back to the bridge that ran before (an update that failed must not leave the PC unreachable).
133
+ function Start-OldBridge {
134
+ $sid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
135
+ foreach ($n in @("Bridgerator-$sid", 'Bridgerator')) { & (Join-Path $Sys32 'schtasks.exe') /Run /TN $n 2>$null | Out-Null; if ($LASTEXITCODE -eq 0) { return } }
136
+ }
27
137
 
28
138
  $admin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
29
- if ($admin) { Write-Warning "This window runs as Administrator. Bridgerator is installed for THIS Windows user only ($env:USERNAME), with this user's rights. To use it as another user, run the install command in that user's normal (not Administrator) PowerShell." }
139
+ # Bridgerator runs with the rights of the signed-in user, never as Administrator.
140
+ if ($admin) { Write-Host 'Run this in a normal (non-admin) PowerShell window: this one runs as Administrator.' -ForegroundColor Yellow; return }
30
141
 
31
142
  [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
32
143
  $ProgressPreference = 'SilentlyContinue'
33
144
 
34
- # No winget on many clean Windows installs: take the official Node.js LTS zip into the user's
145
+ # No winget on many clean Windows installs: take the official Node.js 24 LTS zip into the user's
35
146
  # profile (no admin rights), checked against nodejs.org SHASUMS256, and put it on the user PATH.
36
147
  function Install-PortableNode {
37
148
  $dir = $NodeDir
38
149
  $arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'x64' }
39
- $rel = (Invoke-RestMethod 'https://nodejs.org/dist/index.json') | Where-Object { $_.lts -and ($_.files -contains "win-$arch-zip") } | Select-Object -First 1
40
- if (-not $rel) { throw 'Could not find a Node.js LTS build. Install Node.js 20+ from https://nodejs.org and run this again.' }
150
+ $rel = (Invoke-RestMethod 'https://nodejs.org/dist/index.json') | Where-Object { $_.lts -and $_.version -like 'v24.*' -and ($_.files -contains "win-$arch-zip") } | Select-Object -First 1
151
+ if (-not $rel) { throw 'Could not find a Node.js 24 LTS build. Install Node.js 22+ from https://nodejs.org and run this again.' }
41
152
  $name = "node-$($rel.version)-win-$arch"
42
- Write-Host "Node.js not found - downloading Node.js $($rel.version) into $dir ..."
43
- $tmp = Join-Path $env:TEMP "bridgerator-$([guid]::NewGuid().ToString('N'))"
44
- New-Item -ItemType Directory -Path $tmp | Out-Null
153
+ Write-Host "Downloading Node.js $($rel.version) for Bridgerator into $dir ..."
154
+ $tmp = Join-Path $Work 'node'
155
+ [void][IO.Directory]::CreateDirectory($tmp)
45
156
  try {
46
157
  $zip = Join-Path $tmp "$name.zip"
47
158
  Invoke-WebRequest "https://nodejs.org/dist/$($rel.version)/$name.zip" -OutFile $zip -UseBasicParsing
48
159
  $sums = (Invoke-WebRequest "https://nodejs.org/dist/$($rel.version)/SHASUMS256.txt" -UseBasicParsing).Content
49
160
  $want = ($sums -split "`n" | Where-Object { $_ -match "\s$name\.zip$" }) -replace '\s.*$', ''
50
- $got = (Get-FileHash $zip -Algorithm SHA256).Hash
51
- if (-not $want -or $got -ne $want.Trim().ToUpper()) { throw 'Node.js download failed the checksum check.' }
161
+ if (-not $want -or (Get-Sha256 $zip) -ne $want.Trim().ToLower()) { throw 'Node.js download failed the checksum check.' }
52
162
  Write-Host 'Node.js checksum OK (SHA256, nodejs.org)'
53
- Expand-Archive $zip -DestinationPath $tmp -Force
54
- if (Test-Path $dir) { Remove-Item $dir -Recurse -Force }
55
- New-Item -ItemType Directory -Path (Split-Path $dir) -Force | Out-Null
56
- Move-Item (Join-Path $tmp $name) $dir
57
- } finally { Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue }
58
- # User PATH through the registry: keeps it REG_EXPAND_SZ with %VARS% unexpanded.
59
- $k = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment')
60
- $userPath = [string]$k.GetValue('Path', '', 'DoNotExpandEnvironmentNames')
61
- if (($userPath -split ';') -notcontains $dir) { $k.SetValue('Path', ((@($dir) + ($userPath -split ';')) | Where-Object { $_ }) -join ';', [Microsoft.Win32.RegistryValueKind]::ExpandString) }
62
- $k.Close()
63
- # Tell Explorer the environment changed (new windows see the new PATH).
64
- [Environment]::SetEnvironmentVariable('BRIDGERATOR_PATH_REFRESH', '1', 'User'); [Environment]::SetEnvironmentVariable('BRIDGERATOR_PATH_REFRESH', $null, 'User')
163
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
164
+ [IO.Compression.ZipFile]::ExtractToDirectory($zip, $tmp)
165
+ # Everything running from the Node folder (the old bridge, its engine child) holds node.exe: matched by the folder itself.
166
+ if ([IO.Directory]::Exists($dir)) { Stop-Bridge $dir (Join-Path $dir 'bridgerator.cmd'); [IO.Directory]::Delete($dir, $true) }
167
+ [void][IO.Directory]::CreateDirectory((Split-Path $dir))
168
+ [IO.Directory]::Move((Join-Path $tmp $name), $dir)
169
+ } finally { try { [IO.Directory]::Delete($tmp, $true) } catch {} }
170
+ # npm's cache of this private Node.js: removed by `bridgerator.cmd uninstall --all` only if we created it.
171
+ $cache = Join-Path $env:LOCALAPPDATA 'npm-cache'
172
+ if (-not (Test-Path $cache)) { Set-Content -Path (Join-Path $env:LOCALAPPDATA 'Bridgerator\created-npm-cache') -Value $cache }
173
+ Add-UserPath $dir -First
65
174
  Refresh-Path
66
175
  }
67
176
 
68
- # A running bridge (and its engine / ripgrep children, the hidden autostart loop) locks the package
69
- # and the private Node.js files: stop them before updating.
70
- Get-CimInstance Win32_Process -Filter "Name='wscript.exe' OR Name='node.exe' OR Name='rg.exe'" -ErrorAction SilentlyContinue |
71
- Where-Object { $s = "$($_.CommandLine) $($_.ExecutablePath)".Replace('/', '\'); $s -like '*\node_modules\bridgerator\*' -or $s -like '*\Bridgerator\run-hidden.vbs*' } |
72
- Sort-Object { $_.Name -ne 'wscript.exe' } |
73
- ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
177
+ # ---- The Windows app (default): bridgerator.exe + its files, no Node.js. LOGIC: critical-logic/install-release.md
178
+ function Remove-UserPath([string]$dir) {
179
+ $k = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true)
180
+ if (-not $k) { return }
181
+ try {
182
+ if ($null -ne $k.GetValue('Path')) {
183
+ $kind = $k.GetValueKind('Path'); $old = [string]$k.GetValue('Path', '', 'DoNotExpandEnvironmentNames')
184
+ $new = ($old -split ';' | Where-Object { $_ -and [Environment]::ExpandEnvironmentVariables($_).TrimEnd('\') -ne $dir.TrimEnd('\') }) -join ';'
185
+ if ($new -ne $old) { $k.SetValue('Path', $new, $kind) }
186
+ }
187
+ } finally { $k.Close() }
188
+ }
189
+
190
+ # An antivirus scan or the search indexer can hold a file for a moment.
191
+ function Move-Dir([string]$from, [string]$to) {
192
+ for ($i = 1; ; $i++) { try { [IO.Directory]::Move($from, $to); return } catch { if ($i -ge 10) { throw }; Start-Sleep -Milliseconds 500 } }
193
+ }
194
+ function Remove-Dir([string]$dir) {
195
+ for ($i = 1; ; $i++) { try { if ([IO.Directory]::Exists($dir)) { [IO.Directory]::Delete($dir, $true) }; return } catch { if ($i -ge 10) { throw }; Start-Sleep -Milliseconds 500 } }
196
+ }
197
+
198
+ function Get-Arch {
199
+ $a = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }
200
+ if ($a -eq 'ARM64') { 'arm64' } elseif ($a -eq 'AMD64') { 'x64' } else { $null }
201
+ }
202
+
203
+ # bridgerator-win.json (its own SHA256 already checked) -> the zip for this architecture.
204
+ function Read-AppManifest([string]$file, [string]$arch) {
205
+ $m = [IO.File]::ReadAllText($file, [Text.Encoding]::UTF8) | ConvertFrom-Json
206
+ $e = $m.$arch
207
+ if (-not $e -or [string]$e.file -notmatch "^bridgerator-[0-9]+\.[0-9]+\.[0-9]+-win-$arch\.zip$" -or [string]$e.sha256 -notmatch '^[0-9a-f]{64}$' -or [string]$m.version -notmatch '^[0-9]+\.[0-9]+\.[0-9]+$') { throw "The Bridgerator download list has no build for this PC ($arch)." }
208
+ @{ file = [string]$e.file; sha256 = [string]$e.sha256; version = [string]$m.version; size = [long]$e.size }
209
+ }
210
+
211
+ # The unpacked app: every file of its SHA256SUMS.txt must match (an antivirus that removed or changed a
212
+ # file shows here), then the new bridgerator.exe must start and name its version - all before the
213
+ # running Bridgerator is touched.
214
+ function Test-App([string]$dir, [string]$version) {
215
+ $sums = Join-Path $dir 'SHA256SUMS.txt'
216
+ if (-not [IO.File]::Exists($sums)) { throw 'The Bridgerator download is incomplete (no SHA256SUMS.txt).' }
217
+ $sha = [Security.Cryptography.SHA256]::Create(); $n = 0; $top = @{}
218
+ foreach ($line in [IO.File]::ReadAllLines($sums)) {
219
+ if (-not $line) { continue }
220
+ if ($line -notmatch '^([0-9a-f]{64}) \*([^:*?"<>|]+)$') { throw 'The Bridgerator file list is damaged.' }
221
+ $want = $Matches[1]; $rel = $Matches[2]
222
+ if ($rel.StartsWith('/') -or ($rel -split '/') -contains '..') { throw 'The Bridgerator file list is damaged.' }
223
+ $f = Join-Path $dir ($rel -replace '/', '\')
224
+ $h = $null
225
+ if ([IO.File]::Exists($f)) { $s = [IO.File]::OpenRead($f); try { $h = [BitConverter]::ToString($sha.ComputeHash($s)).Replace('-', '').ToLower() } finally { $s.Close() } }
226
+ if ($h -ne $want) { throw "A Bridgerator file failed the checksum check: $rel (an antivirus may have removed it)." }
227
+ if ($rel -notmatch '/') { $top[$rel] = $true }
228
+ $n++
229
+ }
230
+ # The folder goes FIRST on the user PATH: only listed files in its root (nothing extra can shadow a command).
231
+ foreach ($f in [IO.Directory]::GetFiles($dir)) { $nm = [IO.Path]::GetFileName($f); if ($nm -ne 'SHA256SUMS.txt' -and -not $top.ContainsKey($nm)) { throw "The Bridgerator download has an unexpected file: $nm." } }
232
+ if ($n -lt 100 -or -not [IO.File]::Exists((Join-Path $dir 'bridgerator.exe'))) { throw 'The Bridgerator download is incomplete.' }
233
+ $v = (& (Join-Path $dir 'bridgerator.exe') --version | Out-String).Trim()
234
+ if ($LASTEXITCODE -ne 0 -or -not $v -or ($version -and $v -ne $version)) { throw "The new Bridgerator did not start (answer: '$v'). An antivirus may block it - try again later, or run this command with -UseNpm." }
235
+ return $v
236
+ }
237
+
238
+ # Download + checks -> the checked app folder inside the work folder. Custom -AppZip (https or a local
239
+ # file) only with its -Sha256; the default one by the download list whose SHA256 this script carries.
240
+ function Get-App {
241
+ if ($AppZip) {
242
+ $want = $Sha256.Trim().ToLower(); $version = $null; $src = $AppZip
243
+ } else {
244
+ $mf = Join-Path $Work 'bridgerator-win.json'
245
+ Invoke-WebRequest "$Site/bridgerator-win.json" -OutFile $mf -UseBasicParsing
246
+ if ((Get-Sha256 $mf) -ne $WinManifestSha256) { throw 'The Bridgerator download list failed the checksum check - try again in a minute.' }
247
+ $arch = Get-Arch
248
+ $m = Read-AppManifest $mf $arch
249
+ $want = $m.sha256; $version = $m.version; $src = "$Site/$($m.file)"
250
+ Write-Host "Downloading Bridgerator $version for Windows ($arch, $([math]::Round($m.size / 1MB)) MB)..."
251
+ }
252
+ $zip = $src
253
+ if ($src -match '^https://') { $zip = Join-Path $Work 'app.zip'; Invoke-WebRequest $src -OutFile $zip -UseBasicParsing }
254
+ if ((Get-Sha256 $zip) -ne $want) { throw 'The Bridgerator download failed the checksum check - try again in a minute.' }
255
+ Write-Host 'Download checksum OK (SHA256). Unpacking and checking the files...'
256
+ $dir = Join-Path $Work 'app'
257
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
258
+ [IO.Compression.ZipFile]::ExtractToDirectory($zip, $dir)
259
+ if ($zip -ne $src) { [IO.File]::Delete($zip) }
260
+ $v = Test-App $dir $version
261
+ Write-Host "Bridgerator $v checked."
262
+ return $dir
263
+ }
264
+
265
+ # Stops whatever Bridgerator runs (this app, or the npm package of 1.0.x) and puts the checked app in
266
+ # place; the previous app folder stays as app.old-<guid> until the new one is set up. -> that folder or $null.
267
+ function Switch-App([string]$new) {
268
+ $old = $null
269
+ try {
270
+ if ([IO.File]::Exists((Join-Path $AppDir 'bridgerator.exe'))) { Stop-Bridge $AppDir (Join-Path $AppDir 'bridgerator.exe') }
271
+ foreach ($p in Get-NpmBridges) { Stop-Bridge $p (Join-Path (Split-Path (Split-Path $p)) 'bridgerator.cmd') }
272
+ if ([IO.Directory]::Exists($AppDir)) { $o = Join-Path $Root "app.old-$([guid]::NewGuid().ToString('N'))"; Move-Dir $AppDir $o; $old = $o }
273
+ } catch { Start-OldBridge; throw }
274
+ try { Move-Dir $new $AppDir } catch { if ($old) { try { Move-Dir $old $AppDir } catch {} }; Start-OldBridge; throw }
275
+ return $old
276
+ }
277
+
278
+ # The npm package of Bridgerator 1.0.x after the move to the app (its key and settings stay in
279
+ # %APPDATA%\Bridgerator), and the private Node.js its installer added (same rules as uninstall --all).
280
+ function Remove-NpmBridge([string[]]$keep = @()) {
281
+ foreach ($p in Get-NpmBridges) {
282
+ # The package this run installed is never removed, however npm spells its folder.
283
+ if (@($keep | Where-Object { Test-SamePath $_ $p }).Count) { continue }
284
+ $npmDir = Split-Path (Split-Path $p)
285
+ try {
286
+ Remove-Dir $p
287
+ foreach ($n in 'bridgerator', 'bridgerator.cmd', 'bridgerator.ps1') { $f = Join-Path $npmDir $n; if ([IO.File]::Exists($f)) { [IO.File]::Delete($f) } }
288
+ Write-Host 'Removed the old Bridgerator npm package (key and settings kept).'
289
+ } catch { Write-Host "Could not remove the old Bridgerator npm package $p ($_) - it is not used any more." }
290
+ }
291
+ if (-not @($keep | Where-Object { Test-InFolder $_ $NodeDir }).Count) { Remove-PrivateNode }
292
+ }
293
+
294
+ # The private Node.js of an installer (folder, its PATH entry, npm's cache when we created it - marker file).
295
+ function Remove-PrivateNode {
296
+ # Other global npm packages installed with it (not npm / corepack / Bridgerator): the folder stays.
297
+ $mods = Join-Path $NodeDir 'node_modules'
298
+ $foreign = @(if ([IO.Directory]::Exists($mods)) { [IO.Directory]::GetDirectories($mods) | ForEach-Object { [IO.Path]::GetFileName($_) } | Where-Object { @('npm', 'corepack', 'bridgerator', '.bin') -notcontains $_ } })
299
+ if ($foreign.Count) { Write-Host "Kept the Node.js the installer added ($NodeDir): other programs use it ($($foreign -join ', '))." }
300
+ elseif ([IO.Directory]::Exists($NodeDir)) {
301
+ try { Remove-UserPath $NodeDir; Remove-Dir $NodeDir; Write-Host "Removed the Node.js the installer added ($NodeDir)." } catch { Write-Host "Could not remove $NodeDir ($_) - it is not used any more." }
302
+ }
303
+ $marker = Join-Path $Root 'created-npm-cache'
304
+ if ([IO.File]::Exists($marker)) {
305
+ $c = ([IO.File]::ReadAllText($marker)).Trim()
306
+ if ($c -and $c.TrimEnd('\') -eq (Join-Path $env:LOCALAPPDATA 'npm-cache')) { try { Remove-Dir $c } catch { Write-Host "Could not remove npm's cache $c ($_)." } }
307
+ [IO.File]::Delete($marker)
308
+ }
309
+ }
310
+
311
+ # After `bridgerator.exe setup`: 0 = done (the old folder goes; the npm package of 1.0.x too); 3 = a clear
312
+ # stop before anything changed (pairing code expired): the new app stays, the old link runs again; any
313
+ # other failure puts the previous app folder back and starts it.
314
+ function Complete-App([int]$rc, $old, [bool]$fromNpm) {
315
+ if ($rc -ne 0 -and $rc -ne 3) {
316
+ if ($old) {
317
+ try { Stop-Bridge $AppDir (Join-Path $AppDir 'bridgerator.exe'); Remove-Dir $AppDir; Move-Dir $old $AppDir } catch { Write-Host "Could not put the previous Bridgerator back ($_) - run this command again." }
318
+ }
319
+ Start-OldBridge; return
320
+ }
321
+ if ($old) { try { Remove-Dir $old } catch { Write-Host "Could not remove the previous app folder $old ($_)." } }
322
+ if ($rc -ne 0) { Start-OldBridge; return }
323
+ if ($fromNpm) { Remove-NpmBridge }
324
+ }
325
+
326
+ # One install at a time for this user (a second run would stop the first one's setup and roll it back).
327
+ $lock = New-Object Threading.Mutex($false, "Local\Bridgerator-Install-$([Security.Principal.WindowsIdentity]::GetCurrent().User.Value)")
328
+ $locked = $false
329
+ try { $locked = $lock.WaitOne(0) } catch [Threading.AbandonedMutexException] { $locked = $true }
330
+ if (-not $locked) { Write-Host 'Another Bridgerator install is running in another window - wait until it ends.' -ForegroundColor Yellow; $lock.Dispose(); return }
331
+ $nodeFresh = $false
332
+ # -UseNpm: a bridge this run stopped (the app, a 1.0.x in the other npm folder, the one npm replaces) runs again on any failure.
333
+ $restartOld = $false
334
+ function Undo-NpmRun {
335
+ # A Node.js this run put in goes again (folder, PATH entry, npm-cache marker).
336
+ if ($nodeFresh) { Stop-Bridge $NodeDir (Join-Path $NodeDir 'bridgerator.cmd'); Remove-PrivateNode }
337
+ if ($restartOld) { Start-OldBridge }
338
+ }
339
+ try {
340
+ [void][IO.Directory]::CreateDirectory($Work)
341
+ if ($npmMode) {
342
+ # The package: downloaded, checked against the SHA256 above, installed without running any
343
+ # install scripts of it or its dependencies (no telemetry, no downloads at install time).
344
+ $tgz = $Package
345
+ # The package is always checked: our default needs the hash written by release.sh, any other
346
+ # package needs -Sha256; downloads only over https.
347
+ if ($Package -match '^http://') { Write-Host 'The package address must start with https://' -ForegroundColor Yellow; return }
348
+ if ($Package -ne $DefaultPackage -and -not $Sha256) { Write-Host 'A custom -Package needs its -Sha256 too.' -ForegroundColor Yellow; return }
349
+ if ($Package -eq $DefaultPackage -and -not $Sha256 -and -not $PackageSha256) { Write-Host 'This copy of the installer has no package checksum - use the install command from your account page.' -ForegroundColor Yellow; return }
350
+ $want = if ($Sha256) { $Sha256 } else { $PackageSha256 }
351
+ $dl = $null
352
+ if ($Package -match '^https://') {
353
+ $dl = Join-Path $Work 'bridgerator.tgz'
354
+ Invoke-WebRequest $Package -OutFile $dl -UseBasicParsing
355
+ $tgz = $dl
356
+ }
357
+ if ((Get-Sha256 $tgz) -ne $want.Trim().ToLower()) { throw 'The Bridgerator package failed the checksum check - try again in a minute.' }
358
+ Write-Host 'Package checksum OK (SHA256)'
74
359
 
360
+ # Node.js only after the package passed its check (a failed check must leave nothing behind).
75
361
  if (Test-Path (Join-Path $NodeDir 'node.exe')) { $env:Path = "$NodeDir;$env:Path" }
76
- if (-not (Get-Command node -ErrorAction SilentlyContinue)) { Install-PortableNode }
77
- $major = [int]((& node -v).TrimStart('v').Split('.')[0])
78
- if ($major -lt 20) {
79
- Write-Host "Node.js $(& node -v) is too old (20+ needed) - Bridgerator gets its own copy, your Node.js stays as is."
362
+ $major = 0
363
+ if (Get-Command node -ErrorAction SilentlyContinue) { $major = [int]((& node -v).TrimStart('v').Split('.')[0]) }
364
+ if ($major -lt 22) {
365
+ if ($major) { Write-Host "Node.js $(& node -v) is too old (22+ needed) - Bridgerator gets its own copy, your Node.js stays as is." }
366
+ $nodeFresh = -not [IO.Directory]::Exists($NodeDir)
80
367
  Install-PortableNode
81
368
  }
369
+ # Whatever Node.js runs npm now: its folder stays (the private one is removed only when it is not in use).
370
+ $nodeInUse = Split-Path ((Get-Command node -ErrorAction SilentlyContinue).Source)
82
371
 
83
- # The engine's PDF export brings puppeteer, whose postinstall downloads Chrome (~700 MB);
84
- # skipped here, PDF export fetches a browser itself the first time it is used.
372
+ $prefix = (& npm.cmd prefix -g --update-notifier=false | Out-String).Trim()
85
373
  $env:PUPPETEER_SKIP_DOWNLOAD = '1'
86
- Write-Host "Installing Bridgerator from $Package - this takes 2-4 minutes, please wait..."
87
- & npm.cmd install -g $Package --no-audit --no-fund --loglevel=error
88
- if ($LASTEXITCODE -ne 0) { throw 'npm install failed' }
89
- Refresh-Path
374
+ Write-Host 'Installing Bridgerator - this usually takes 2-6 minutes, please wait...'
375
+ try {
376
+ # The running bridge keeps working during the update; only when a locked file blocks npm it is stopped.
377
+ & npm.cmd install -g $tgz --ignore-scripts @NpmFlags
378
+ if ($LASTEXITCODE -ne 0) {
379
+ Write-Host 'Retrying with the running Bridgerator stopped...'
380
+ $restartOld = $true; Stop-Bridge (Join-Path $prefix 'node_modules\bridgerator') (Join-Path $prefix 'bridgerator.cmd')
381
+ & npm.cmd install -g $tgz --ignore-scripts @NpmFlags
382
+ if ($LASTEXITCODE -ne 0) { throw 'npm could not install Bridgerator (its message is above). Check the internet connection - npm needs registry.npmjs.org - and run the install command again, or install without -UseNpm.' }
383
+ }
384
+ } finally { if ($dl) { try { [IO.File]::Delete($dl) } catch {} } }
385
+ # Back from the app to the npm package: the app stops now (one bridge per PC), its folder goes after setup.
386
+ $dropApp = [IO.File]::Exists((Join-Path $AppDir 'bridgerator.exe'))
387
+ if ($dropApp) { $restartOld = $true; Stop-Bridge $AppDir (Join-Path $AppDir 'bridgerator.exe') }
388
+ # A 1.0.x package in the OTHER npm folder (npm's own / the private Node.js) stops too; it goes after setup.
389
+ $newPkg = Join-Path $prefix 'node_modules\bridgerator'
390
+ $keepNpm = @($newPkg, $nodeInUse)
391
+ foreach ($p in Get-NpmBridges) { if (-not (Test-SamePath $p $newPkg)) { $restartOld = $true; Stop-Bridge $p (Join-Path (Split-Path (Split-Path $p)) 'bridgerator.cmd') } }
392
+ } else {
393
+ # The app: plain stops first (nothing downloaded yet), then download + every check (Get-App); the running
394
+ # Bridgerator is stopped only right before setup (Switch-App).
395
+ if ($AppZip -match '^http://') { Write-Host 'The app address must start with https://' -ForegroundColor Yellow; return }
396
+ if ($AppZip -and -not $Sha256) { Write-Host 'A custom -AppZip needs its -Sha256 too.' -ForegroundColor Yellow; return }
397
+ if (-not $AppZip -and -not $WinManifestSha256) { Write-Host 'This copy of the installer has no app checksum - use the install command from your account page.' -ForegroundColor Yellow; return }
398
+ if (-not $AppZip -and -not (Get-Arch)) { Write-Host 'Bridgerator needs 64-bit Windows (x64 or ARM64).' -ForegroundColor Yellow; return }
399
+ # A 1.0.x npm package (npm's folder or the private Node.js) or the old installer's private Node.js: removed after setup.
400
+ $fromNpm = [bool](Get-NpmBridges) -or [IO.Directory]::Exists($NodeDir)
401
+ $newApp = Get-App
402
+ }
403
+
404
+ # WebView2 runtime (the tray window). Usually part of Windows 11; if missing, Microsoft's Evergreen
405
+ # bootstrapper is downloaded, its Authenticode signature checked (valid, signed by Microsoft
406
+ # Corporation) and run per user without admin. Any failure only means the tray opens the account
407
+ # page instead of its window.
408
+ function Test-WebView2 {
409
+ foreach ($k in 'HKCU:\Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'HKLM:\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}') {
410
+ try { $pv = (Get-ItemProperty $k -ErrorAction Stop).pv; if ($pv -and $pv -ne '0.0.0.0') { return $true } } catch {}
411
+ }
412
+ return $false
413
+ }
414
+ function Test-MicrosoftSigned([string]$file) {
415
+ # Get-AuthenticodeSignature is not available under the Restricted policy: WinVerifyTrust directly.
416
+ Add-Type -TypeDefinition @'
417
+ using System; using System.Runtime.InteropServices;
418
+ public static class BrTrust {
419
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct FileInfo { public int cbStruct; public string path; public IntPtr hFile; public IntPtr known; }
420
+ [StructLayout(LayoutKind.Sequential)] struct Data { public int cbStruct; public IntPtr policy; public IntPtr sip; public int ui; public int revocation; public int choice; public IntPtr file; public int action; public IntPtr state; public IntPtr url; public int prov; public int context; public IntPtr sig; }
421
+ [DllImport("wintrust.dll", CharSet = CharSet.Unicode)] static extern int WinVerifyTrust(IntPtr hwnd, ref Guid action, ref Data data);
422
+ public static int Verify(string path) {
423
+ var fi = new FileInfo { cbStruct = Marshal.SizeOf(typeof(FileInfo)), path = path };
424
+ IntPtr pfi = Marshal.AllocHGlobal(fi.cbStruct); Marshal.StructureToPtr(fi, pfi, false);
425
+ // WTD_REVOKE_WHOLECHAIN + WTD_REVOCATION_CHECK_CHAIN: a revoked certificate anywhere in the chain fails.
426
+ var d = new Data { cbStruct = Marshal.SizeOf(typeof(Data)), ui = 2, revocation = 1, choice = 1, file = pfi, action = 1, prov = 0x40 };
427
+ var g = new Guid("00AAC56B-CD44-11d0-8CC2-00C04FC295EE");
428
+ int r = WinVerifyTrust(new IntPtr(-1), ref g, ref d);
429
+ d.action = 2; WinVerifyTrust(new IntPtr(-1), ref g, ref d);
430
+ Marshal.FreeHGlobal(pfi); return r;
431
+ }
432
+ }
433
+ '@
434
+ if ([BrTrust]::Verify($file) -ne 0) { return $false }
435
+ $cert = New-Object Security.Cryptography.X509Certificates.X509Certificate2 ([Security.Cryptography.X509Certificates.X509Certificate]::CreateFromSignedFile($file))
436
+ if ($cert.Subject -notmatch '(^|, )O=Microsoft Corporation(,|$)') { return $false }
437
+ # The chain must end in a Microsoft root and nothing in it may be revoked.
438
+ $chain = New-Object Security.Cryptography.X509Certificates.X509Chain
439
+ $chain.ChainPolicy.RevocationMode = 'Online'; $chain.ChainPolicy.RevocationFlag = 'EntireChain'
440
+ if (-not $chain.Build($cert)) { return $false }
441
+ $root = $chain.ChainElements[$chain.ChainElements.Count - 1].Certificate
442
+ return $root.Subject -match 'CN=Microsoft Root Certificate Authority' -and $root.Subject -match 'O=Microsoft Corporation'
443
+ }
444
+ if (-not (Test-WebView2)) {
445
+ try {
446
+ Write-Host 'Installing the Microsoft WebView2 runtime for the Bridgerator window...'
447
+ $wvb = Join-Path $Work 'MicrosoftEdgeWebview2Setup.exe'
448
+ Invoke-WebRequest 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile $wvb -UseBasicParsing
449
+ # Held open (others may only read) from the check until it has run: it can't be swapped in between.
450
+ $hold = [IO.File]::Open($wvb, 'Open', 'Read', 'Read')
451
+ try {
452
+ if (-not (Test-MicrosoftSigned $wvb)) { throw 'the WebView2 installer is not signed by Microsoft' }
453
+ $p = Start-Process -FilePath $wvb -ArgumentList '/silent', '/install' -Wait -PassThru
454
+ } finally { $hold.Dispose() }
455
+ if ($p.ExitCode -ne 0) { throw "exit code $($p.ExitCode)" }
456
+ # A policy / antivirus can block it while the bootstrapper still ends with 0.
457
+ if (-not (Test-WebView2)) { throw 'blocked on this PC' }
458
+ } catch { Write-Host "WebView2 runtime not installed ($_) - Bridgerator works anyway; its window opens the account page instead." -ForegroundColor Yellow }
459
+ }
90
460
 
91
461
  $setupArgs = @('setup')
92
462
  if ($Token) { $setupArgs += @('--token', $Token) }
463
+ if ($Relay) { $setupArgs += @('--relay', $Relay) }
93
464
  foreach ($a in $Allow) { $setupArgs += @('--allow', $a) }
94
465
  foreach ($d in $Deny) { $setupArgs += @('--deny', $d) }
95
- if (-not $paired) { Write-Host 'Next: a browser tab opens - sign in with the same account you use for Bridgerator and click "Connect this computer".' }
96
- & bridgerator.cmd @setupArgs
97
- if ($LASTEXITCODE -ne 0) { throw 'bridgerator setup failed' }
466
+ if ($NoTelemetry) { $setupArgs += '--no-telemetry' }
467
+ # Language of the window on a first config (setup keeps a language the person chose).
468
+ $lang = if ([Globalization.CultureInfo]::CurrentUICulture.Name -like 'ru*' -or [Globalization.CultureInfo]::CurrentCulture.Name -like 'ru*') { 'ru' } else { 'en' }
469
+ $setupArgs += @('--language', $lang)
470
+ # The app: only now the running Bridgerator stops and the checked folder goes in place.
471
+ if (-not $npmMode) { $oldApp = Switch-App $newApp }
472
+ try {
473
+ # Called by full path (the PATH of this window may not have the folder yet); the folder goes on
474
+ # the user PATH so `bridgerator.cmd` works in new windows.
475
+ if ($npmMode) { Add-UserPath $prefix; $br = Join-Path $prefix 'bridgerator.cmd' } else { Add-UserPath $AppDir -First; $br = Join-Path $AppDir 'bridgerator.exe' }
476
+ Refresh-Path
477
+ if (-not $paired) { Write-Host 'Next: a browser tab opens - sign in with the same account you use for Bridgerator and click "Connect this computer".' }
478
+ & $br @setupArgs
479
+ $rc = $LASTEXITCODE
480
+ } catch { if (-not $npmMode) { Complete-App 1 $oldApp $fromNpm }; throw }
481
+ if (-not $npmMode) { Complete-App $rc $oldApp $fromNpm }
482
+ elseif ($rc -eq 0) {
483
+ if ($dropApp) { try { Remove-Dir $AppDir; Remove-UserPath $AppDir } catch { Write-Host "Could not remove the Bridgerator app folder $AppDir ($_)." } }
484
+ Remove-NpmBridge $keepNpm
485
+ }
486
+ # 3 = stopped with a clear message already shown (pairing code expired, ...): no red error on top;
487
+ # -UseNpm rolls back like a failure (fresh Node.js out, the stopped bridge back).
488
+ if ($rc -eq 3) { if ($npmMode) { Undo-NpmRun }; return }
489
+ if ($rc -ne 0) { throw 'bridgerator setup failed' }
98
490
  Write-Host 'Done. Bridgerator starts in the background in about 10 seconds. Check any time with: bridgerator.cmd status'
491
+ Write-Host 'Open ChatGPT and say: Set up Bridgerator'
99
492
 
100
- # Next steps (connect ChatGPT, example prompts) on the welcome page; skipped for -NoBrowser and
101
- # non-interactive sessions (scheduled tasks, remote shells). Never fails the install.
102
- $welcome = 'https://bridgerator.recondeck.com/welcome/'
103
- if (-not $NoBrowser -and [Environment]::UserInteractive) {
104
- try { Start-Process $welcome } catch { Write-Host "Next steps: $welcome" }
105
- } else {
106
- Write-Host "Next steps: $welcome"
493
+ # Next steps (connect ChatGPT, example prompts) on the welcome page of the site this script came from -
494
+ # after the FIRST install only; skipped for -NoBrowser and non-interactive sessions. Russian Windows
495
+ # (display language or regional format) gets the Russian page.
496
+ $welcome = "$Site/$(if ($lang -eq 'ru') { 'ru/' })welcome/"
497
+ if ($firstInstall) {
498
+ if (-not $NoBrowser -and [Environment]::UserInteractive) {
499
+ try { Start-Process $welcome } catch { Write-Host "Next steps: $welcome" }
500
+ } else {
501
+ Write-Host "Next steps: $welcome"
502
+ }
107
503
  }
504
+ } catch {
505
+ if ($npmMode) { Undo-NpmRun }
506
+ throw
507
+ } finally { Remove-Work; try { $lock.ReleaseMutex() } catch {}; $lock.Dispose() }