bridgerator 1.0.7 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -15
- package/assets/bridgerator.ico +0 -0
- package/assets/icon-16.png +0 -0
- package/assets/icon-20.png +0 -0
- package/assets/icon-24.png +0 -0
- package/assets/icon-256.png +0 -0
- package/assets/icon-32.png +0 -0
- package/assets/icon-48.png +0 -0
- package/assets/icon-64.png +0 -0
- package/assets/logo.svg +1 -0
- package/install.ps1 +465 -43
- package/npm-shrinkwrap.json +8621 -0
- package/package.json +55 -49
- package/recipes/backup-folder.json +31 -0
- package/recipes/clean-downloads.json +48 -0
- package/recipes/disk-report.json +66 -0
- package/recipes/sort-folder.json +49 -0
- package/recipes/update-apps.json +25 -0
- package/src/ask-lib.ps1 +230 -0
- package/src/ask.mjs +51 -0
- package/src/ask.ps1 +17 -0
- package/src/automations.mjs +673 -0
- package/src/batch.mjs +221 -0
- package/src/bridge.mjs +743 -29
- package/src/cli.mjs +561 -53
- package/src/confirm.mjs +418 -0
- package/src/dc-config-hooks.mjs +18 -8
- package/src/dc-isolate.mjs +5 -0
- package/src/delegate.mjs +555 -0
- package/src/docs-tool.mjs +494 -0
- package/src/doctor.mjs +200 -0
- package/src/events.mjs +21 -0
- package/src/extra-tools.mjs +122 -0
- package/src/guard.mjs +34 -0
- package/src/notice.mjs +86 -23
- package/src/pair.mjs +41 -6
- package/src/pc-check.mjs +112 -0
- package/src/pc-check.ps1 +103 -0
- package/src/pc-text.mjs +94 -0
- package/src/pdf-font.mjs +245 -0
- package/src/photos-tool.mjs +187 -0
- package/src/photos.ps1 +145 -0
- package/src/plan.mjs +90 -0
- package/src/policy.mjs +293 -109
- package/src/queue.mjs +194 -0
- package/src/recent.mjs +48 -0
- package/src/recipes.mjs +108 -0
- package/src/recycle.mjs +185 -0
- package/src/recycle.ps1 +110 -0
- package/src/screen-tool.mjs +185 -0
- package/src/screen.ps1 +285 -0
- package/src/search-tool.mjs +189 -0
- package/src/search.ps1 +129 -0
- package/src/settings-tool.mjs +186 -0
- package/src/setup-tool.mjs +129 -0
- package/src/software-tool.mjs +185 -0
- package/src/telemetry.mjs +86 -0
- package/src/tool-lib.mjs +177 -0
- package/src/tool-text.mjs +45 -0
- package/src/tray.mjs +97 -0
- package/src/tray.ps1 +359 -0
- package/src/undo.mjs +313 -0
- package/src/win.mjs +17 -0
- package/ui/app.css +388 -0
- package/ui/app.js +868 -0
- package/ui/i18n/en.json +598 -0
- package/ui/i18n/ru.json +598 -0
- package/ui/index.html +26 -0
- package/vendor/webview2/LICENSE.txt +27 -0
- package/vendor/webview2/Microsoft.Web.WebView2.Core.dll +0 -0
- package/vendor/webview2/Microsoft.Web.WebView2.WinForms.dll +0 -0
- package/vendor/webview2/NOTICE.txt +85 -0
- package/vendor/webview2/SHA256SUMS.txt +4 -0
- package/vendor/webview2/SOURCE.txt +1 -0
- package/vendor/webview2/arm64/WebView2Loader.dll +0 -0
- package/vendor/webview2/x64/WebView2Loader.dll +0 -0
package/src/cli.mjs
CHANGED
|
@@ -1,22 +1,72 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// bridgerator setup [--token <t> | --pair] [--relay <wss url>] [--allow <dir>]... [--deny <dir>]...
|
|
3
|
-
// (no key yet: pairs this PC through the
|
|
4
|
-
//
|
|
2
|
+
// bridgerator.cmd setup [--token <t> | --pair] [--relay <wss url>] [--allow <dir>]... [--deny <dir>]... [--read-only <dir>]... [--language en|ru] [--no-telemetry]
|
|
3
|
+
// → config + autostart + start (no key yet, or the stored key was rejected: pairs this PC through the
|
|
4
|
+
// browser — the signed-in owner confirms a code)
|
|
5
|
+
// bridgerator.cmd run | stop | status | doctor [--json] | confirm on|off [category] | read-only add|remove <dir> | telemetry on|off | uninstall [--all | --keep-config] | --version
|
|
5
6
|
import fs from 'node:fs';
|
|
6
7
|
import os from 'node:os';
|
|
7
8
|
import path from 'node:path';
|
|
9
|
+
import crypto from 'node:crypto';
|
|
8
10
|
import { spawn, execFileSync } from 'node:child_process';
|
|
9
11
|
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
+
import { isSea } from 'node:sea';
|
|
13
|
+
import { startDesktopCommander, connect, checkRelay } from './bridge.mjs';
|
|
14
|
+
import { pair, siteOf, checkKey, machineFp } from './pair.mjs';
|
|
15
|
+
import { ACCOUNT_URL } from './notice.mjs';
|
|
16
|
+
import { send, VERSION } from './telemetry.mjs';
|
|
17
|
+
import { restoreIni, setIni, readSettings, writeAtomic, CATEGORIES } from './confirm.mjs';
|
|
18
|
+
import { showNotice } from './notice.mjs';
|
|
19
|
+
import { startTray } from './tray.mjs';
|
|
20
|
+
import { POWERSHELL, SCHTASKS, CMD, WSCRIPT, WHOAMI, REG, psArgs } from './win.mjs';
|
|
21
|
+
import { runDoctor, formatDoctor } from './doctor.mjs';
|
|
22
|
+
import { removeTasks } from './automations.mjs';
|
|
23
|
+
|
|
24
|
+
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; // never talk to the relay with TLS checks off
|
|
12
25
|
|
|
13
26
|
const DEFAULT_RELAY = 'wss://mcp.bridgerator.recondeck.com/link';
|
|
14
|
-
const
|
|
27
|
+
const OLD_TASK = 'Bridgerator'; // ≤ 1.0.7: one name for every user of the PC
|
|
15
28
|
const home = os.homedir();
|
|
16
|
-
const
|
|
29
|
+
const APPDATA = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
30
|
+
const LOCALAPPDATA = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
31
|
+
// BRIDGERATOR_HOME only for the test runner (node --test) or with an explicit --home on the command line.
|
|
32
|
+
const HOME_ARG = (() => { const i = process.argv.indexOf('--home'); return i > 0 ? process.argv[i + 1] : null; })();
|
|
33
|
+
const DIR = HOME_ARG || (process.env.NODE_TEST_CONTEXT && process.env.BRIDGERATOR_HOME) || path.join(process.platform === 'win32' ? APPDATA : path.join(home, '.config'), 'Bridgerator');
|
|
17
34
|
const CONFIG = path.join(DIR, 'config.json');
|
|
18
35
|
const LOG = path.join(DIR, 'bridge.log');
|
|
36
|
+
const VBS = path.join(DIR, 'run-hidden.vbs');
|
|
37
|
+
const INI = path.join(DIR, 'confirm.ini'); // on-PC confirmation settings (confirm.mjs), read on every call
|
|
38
|
+
// File copies for Undo (undo.mjs): %LOCALAPPDATA%\Bridgerator\snapshots (always excluded); next to a moved home in tests.
|
|
39
|
+
const SNAP_DIR = DIR === path.join(APPDATA, 'Bridgerator') ? path.join(LOCALAPPDATA, 'Bridgerator', 'snapshots') : path.join(DIR, 'snapshots');
|
|
40
|
+
// Automations (scheduled scripts, automations.mjs): inside the always-excluded %LOCALAPPDATA%\Bridgerator.
|
|
41
|
+
const SCRIPTS_DIR = DIR === path.join(APPDATA, 'Bridgerator') ? path.join(LOCALAPPDATA, 'Bridgerator', 'scripts') : path.join(DIR, 'scripts');
|
|
19
42
|
const SELF = fileURLToPath(import.meta.url);
|
|
43
|
+
const PKG_DIR = path.dirname(path.dirname(SELF)); // …\node_modules\bridgerator
|
|
44
|
+
// Private Node.js the installer puts in when there is none (or < 22) — see install.ps1.
|
|
45
|
+
const NODE_DIR = path.join(LOCALAPPDATA, 'Bridgerator', 'node');
|
|
46
|
+
// The Windows app without Node (install.ps1 default): bridgerator.exe + the package files in this folder.
|
|
47
|
+
// LOGIC: see critical-logic/install-release.md ("Windows app without Node").
|
|
48
|
+
/**
|
|
49
|
+
* setup / update: the new config = everything the person set before (theme, language, notifications,
|
|
50
|
+
* tool settings…: owner 21.10 "no setting is ever lost on an update") + setup's own keys on top.
|
|
51
|
+
* confirm / confirmTools = keys of an earlier draft (confirm.ini now).
|
|
52
|
+
*/
|
|
53
|
+
export function setupConfig(prev, own) {
|
|
54
|
+
const { confirm: _c, confirmTools: _t, ...c } = { ...(prev || {}), ...own };
|
|
55
|
+
return c;
|
|
56
|
+
}
|
|
57
|
+
export const APP_DIR = path.join(LOCALAPPDATA, 'Bridgerator', 'app');
|
|
58
|
+
/**
|
|
59
|
+
* Where the program files are: { exe: false } = npm package (removed with npm); { exe: true, dir, ours }
|
|
60
|
+
* = bridgerator.exe running from `dir` (= the package folder); `ours` only for the installer's folder
|
|
61
|
+
* %LOCALAPPDATA%\Bridgerator\app — a build run from anywhere else is never deleted by uninstall.
|
|
62
|
+
*/
|
|
63
|
+
export function programFiles({ sea = isSea(), execPath = process.execPath, pkgDir = PKG_DIR, appDir = APP_DIR } = {}) {
|
|
64
|
+
if (!sea) return { exe: false };
|
|
65
|
+
const key = (d) => path.resolve(d).toLowerCase().replace(/[\\/]+$/, '');
|
|
66
|
+
const dir = path.dirname(execPath);
|
|
67
|
+
return { exe: true, dir, ours: key(dir) === key(appDir) && key(pkgDir) === key(appDir) };
|
|
68
|
+
}
|
|
69
|
+
const CWD = process.cwd(); // relative --allow/--deny are resolved against it (we chdir below)
|
|
20
70
|
|
|
21
71
|
function parse(argv) {
|
|
22
72
|
const o = { _: [] };
|
|
@@ -26,7 +76,7 @@ function parse(argv) {
|
|
|
26
76
|
const k = a.slice(2), v = argv[i + 1];
|
|
27
77
|
if (v === undefined || v.startsWith('--')) { o[k] = true; continue; }
|
|
28
78
|
i++;
|
|
29
|
-
if (k === 'allow' || k === 'deny') (o[k] ||= []).push(v); else o[k] = v;
|
|
79
|
+
if (k === 'allow' || k === 'deny' || k === 'read-only') (o[k] ||= []).push(v); else o[k] = v;
|
|
30
80
|
}
|
|
31
81
|
return o;
|
|
32
82
|
}
|
|
@@ -34,6 +84,7 @@ function parse(argv) {
|
|
|
34
84
|
function readConfig() {
|
|
35
85
|
try { return JSON.parse(fs.readFileSync(CONFIG, 'utf8')); } catch { return null; }
|
|
36
86
|
}
|
|
87
|
+
const writeConfig = (cfg) => { fs.mkdirSync(DIR, { recursive: true }); writeAtomic(CONFIG, JSON.stringify(cfg, null, 2)); };
|
|
37
88
|
|
|
38
89
|
function log(line) {
|
|
39
90
|
const s = `${new Date().toISOString()} ${line}`;
|
|
@@ -44,94 +95,551 @@ function log(line) {
|
|
|
44
95
|
} catch { /* log dir not writable — console only */ }
|
|
45
96
|
}
|
|
46
97
|
|
|
98
|
+
const q = (s) => `'${String(s).replace(/'/g, "''")}'`;
|
|
99
|
+
const ps = (script) => execFileSync(POWERSHELL, psArgs(script), { stdio: 'pipe' }).toString();
|
|
100
|
+
|
|
101
|
+
// One scheduled task per Windows user: Bridgerator-<SID>.
|
|
102
|
+
// Parsers of Windows tool output (any locale: by structure, never by label text).
|
|
103
|
+
/** `whoami /user /fo csv /nh` ("domain\name","S-1-…") → the user SID: the last field, never the name. */
|
|
104
|
+
export const sidFrom = (out) => (String(out).match(/"(S-1-[\d-]+)"\s*$/m) || [])[1];
|
|
105
|
+
/** `reg query HKLM\SOFTWARE\Microsoft\Cryptography /v MachineGuid` → the GUID. */
|
|
106
|
+
export const machineGuidFrom = (out) => (String(out).match(/MachineGuid\s+REG_SZ\s+(\S+)/) || [])[1];
|
|
107
|
+
let sidCache;
|
|
108
|
+
function taskName() {
|
|
109
|
+
sidCache ||= sidFrom(execFileSync(WHOAMI, ['/user', '/fo', 'csv', '/nh']).toString());
|
|
110
|
+
if (!sidCache) throw new Error('could not read this Windows user id (whoami /user)');
|
|
111
|
+
return `Bridgerator-${sidCache}`;
|
|
112
|
+
}
|
|
113
|
+
// Fingerprint for pairing (pair.machineFp): MachineGuid from the registry + this user's SID, hashed.
|
|
114
|
+
export function thisPcFp() {
|
|
115
|
+
if (process.platform !== 'win32') return '';
|
|
116
|
+
try {
|
|
117
|
+
const out = execFileSync(REG, ['query', 'HKLM\\SOFTWARE\\Microsoft\\Cryptography', '/v', 'MachineGuid', '/reg:64'], { stdio: 'pipe' }).toString();
|
|
118
|
+
return machineFp(machineGuidFrom(out), taskName().replace(/^Bridgerator-/, ''));
|
|
119
|
+
} catch { return ''; }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The old shared task is removed only when it runs OUR script (another user's task stays).
|
|
123
|
+
const dropOldTaskPs = () => `$o = Get-ScheduledTask -TaskName ${q(OLD_TASK)} -ErrorAction SilentlyContinue; if ($o -and ((($o.Actions | ForEach-Object { $_.Arguments }) -join ' ').ToLower().Contains(${q(VBS.toLowerCase())}))) { Unregister-ScheduledTask -TaskName ${q(OLD_TASK)} -Confirm:$false }`;
|
|
124
|
+
|
|
47
125
|
// Hidden autostart at logon of THIS user (not SYSTEM): the assistant gets the user's rights only.
|
|
48
126
|
function installAutostart() {
|
|
49
127
|
if (process.platform !== 'win32') return 'autostart: not Windows — start `bridgerator run` yourself';
|
|
50
|
-
|
|
51
|
-
const cmd = `"${process.execPath}" "${SELF}" run`;
|
|
52
|
-
fs.writeFileSync(vbs, `CreateObject("WScript.Shell").Run "${cmd.replace(/"/g, '""')}", 0, False\r\n`);
|
|
128
|
+
fs.writeFileSync(VBS, '\ufeff' + hiddenVbs(process.execPath, SELF), 'utf16le');
|
|
53
129
|
// Registered through PowerShell's ScheduledTasks module: `schtasks /Create … wscript.exe …` spawned
|
|
54
130
|
// from node is refused (EPERM) on clean Windows 11. No 72 h execution limit (the task default
|
|
55
|
-
// would silently stop the bridge after three days);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
`$a = New-ScheduledTaskAction -Execute 'wscript.exe' -Argument ${q(`"${vbs}"`)}`,
|
|
131
|
+
// would silently stop the bridge after three days); normal priority (the task default is below normal).
|
|
132
|
+
ps([
|
|
133
|
+
`$a = New-ScheduledTaskAction -Execute ${q(WSCRIPT)} -Argument ${q(`//B //NoLogo "${VBS}"`)}`,
|
|
59
134
|
'$t = New-ScheduledTaskTrigger -AtLogOn -User "$env:USERDOMAIN\\$env:USERNAME"',
|
|
60
|
-
'$s = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 1) -MultipleInstances IgnoreNew',
|
|
135
|
+
'$s = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 1) -MultipleInstances IgnoreNew -Priority 4',
|
|
61
136
|
'$p = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\\$env:USERNAME" -LogonType Interactive -RunLevel Limited',
|
|
62
|
-
`Register-ScheduledTask -TaskName ${q(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return
|
|
137
|
+
`Register-ScheduledTask -TaskName ${q(taskName())} -Action $a -Trigger $t -Settings $s -Principal $p -Force | Out-Null`,
|
|
138
|
+
dropOldTaskPs(),
|
|
139
|
+
].join('; '));
|
|
140
|
+
return `autostart: scheduled task "${taskName()}" (at logon, hidden)`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Hidden; waits for the bridge and restarts it after a crash (exit code ≠ 0) with a growing pause
|
|
144
|
+
// (10 s … 30 min, back to 10 s after a run of 10+ min); exit 0 = stopped on purpose (computer
|
|
145
|
+
// unlinked). Quits when node.exe or the package is gone (uninstalled). No WSH error boxes.
|
|
146
|
+
// Saved as UTF-16 with BOM: WSH reads a BOM-less file as ANSI, so a Cyrillic & co. profile path
|
|
147
|
+
// (C:\Users\Иван) would break the autostart.
|
|
148
|
+
export function hiddenVbs(node, app) {
|
|
149
|
+
const s = (x) => `"${String(x).replace(/"/g, '""')}"`;
|
|
150
|
+
return [
|
|
151
|
+
'On Error Resume Next',
|
|
152
|
+
'Set sh = CreateObject("WScript.Shell")',
|
|
153
|
+
'Set fso = CreateObject("Scripting.FileSystemObject")',
|
|
154
|
+
`node = ${s(node)}`,
|
|
155
|
+
`app = ${s(app)}`,
|
|
156
|
+
'wait = 10',
|
|
157
|
+
'Do',
|
|
158
|
+
' If Not fso.FileExists(node) Or Not fso.FileExists(app) Then WScript.Quit 3',
|
|
159
|
+
' t = Now',
|
|
160
|
+
' Err.Clear',
|
|
161
|
+
' rc = sh.Run("""" & node & """ """ & app & """ run", 0, True)',
|
|
162
|
+
// Could not start (an antivirus holds the file, …): retried like a crash, with the same growing pause.
|
|
163
|
+
' If Err.Number <> 0 Then rc = -1',
|
|
164
|
+
' Err.Clear',
|
|
165
|
+
' If rc = 0 Then WScript.Quit 0',
|
|
166
|
+
' If DateDiff("s", t, Now) > 600 Then wait = 10',
|
|
167
|
+
' WScript.Sleep wait * 1000',
|
|
168
|
+
' wait = wait * 2',
|
|
169
|
+
' If wait > 1800 Then wait = 1800',
|
|
170
|
+
'Loop',
|
|
171
|
+
'',
|
|
172
|
+
].join('\r\n');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// THE process matcher (install.ps1 carries the same lines for old versions without `stop`): in this
|
|
176
|
+
// Windows session only — the autostart loop (wscript running our run-hidden.vbs; first, or it
|
|
177
|
+
// restarts the bridge), and node.exe / bridgerator.exe / rg.exe / powershell.exe whose command line or
|
|
178
|
+
// executable is inside the package folder (bridge, engine child, ripgrep, tray helper).
|
|
179
|
+
const matchPs = (pkgDir, vbs, selfPid) => `$me = (Get-Process -Id $PID).SessionId; $pkg = ${q(pkgDir.toLowerCase().replace(/[\\/]+$/, '') + '\\')}; $vbs = ${q(vbs.toLowerCase())}
|
|
180
|
+
$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 |
|
|
181
|
+
Where-Object { $s = ("$($_.CommandLine) $($_.ExecutablePath)").ToLower().Replace('/', '\\'); $_.SessionId -eq $me -and $_.ProcessId -ne ${Number(selfPid)} -and ($s.Contains($pkg) -or $s.Contains($vbs)) } |
|
|
182
|
+
Sort-Object { $_.Name -ne 'wscript.exe' })`;
|
|
183
|
+
// The tray helper (powershell.exe) removes its icon itself once the bridge is gone (a kill leaves a
|
|
184
|
+
// ghost icon): it gets 2 s after the rest is stopped, then the kill. Then the window's WebView2
|
|
185
|
+
// processes left behind (msedgewebview2.exe with OUR user data folder, this session only; V-8).
|
|
186
|
+
export const webviewStopPs = (dataDir) => `$wvd = ${q(`--user-data-dir=${dataDir}`.toLowerCase())}; $me = (Get-Process -Id $PID).SessionId
|
|
187
|
+
$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 })
|
|
188
|
+
$wv | Where-Object { $ids -contains $_.ProcessId -or $ids -contains $_.ParentProcessId } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }`;
|
|
189
|
+
export function stopPs(pkgDir, vbs, selfPid, webviewDir = path.join(LOCALAPPDATA, 'Bridgerator', 'webview')) {
|
|
190
|
+
return `${matchPs(pkgDir, vbs, selfPid)}
|
|
191
|
+
$all | Where-Object { $_.Name -ne 'powershell.exe' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
|
|
192
|
+
$tray = @($all | Where-Object { $_.Name -eq 'powershell.exe' }); $end = (Get-Date).AddSeconds(2)
|
|
193
|
+
while ($tray.Count -and (Get-Date) -lt $end -and @($tray | Where-Object { Get-Process -Id $_.ProcessId -ErrorAction SilentlyContinue }).Count) { Start-Sleep -Milliseconds 200 }
|
|
194
|
+
$tray | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
|
|
195
|
+
${webviewStopPs(webviewDir)}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Is the bridge process running (same matcher as stop, nothing is stopped)?
|
|
199
|
+
function isRunning() {
|
|
200
|
+
if (process.platform !== 'win32') return false;
|
|
201
|
+
try { return ps(`${matchPs(PKG_DIR, VBS, process.pid)}\n@($all | Where-Object { $_.Name -eq "node.exe" -or $_.Name -eq "bridgerator.exe" }).Count`).trim() !== '0'; } catch { return false; }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* One "offline" notice per outage, a minute after it began (if `notify()` shows it → true): the timer is
|
|
206
|
+
* armed once per outage — reconnect attempts that fail again do not restart it. → (linkState) => void
|
|
207
|
+
*/
|
|
208
|
+
export function offlineWatch(notify, ms = 60000) {
|
|
209
|
+
let timer = null, told = false;
|
|
210
|
+
return (state) => {
|
|
211
|
+
if (state === 'online') { clearTimeout(timer); timer = null; told = false; }
|
|
212
|
+
else if (state === 'offline' && !told && !timer) timer = setTimeout(() => { timer = null; told = notify() === true; }, ms);
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** The first line of `status`, in plain words, from "is it running" + the end of bridge.log. */
|
|
217
|
+
export function statusLine(running, logLines) {
|
|
218
|
+
let state = null;
|
|
219
|
+
for (let i = logLines.length - 1; i >= 0 && !state; i--) {
|
|
220
|
+
const l = logLines[i];
|
|
221
|
+
if (/no longer linked|rejected the device token/.test(l)) state = { rejected: true };
|
|
222
|
+
else { const m = l.match(/registered as "([^"]+)"/); if (m) state = { online: m[1] }; else if (/disconnected \(/.test(l)) state = { offline: true }; }
|
|
223
|
+
}
|
|
224
|
+
if (state?.rejected) return 'Bridgerator: key rejected — run the install command again';
|
|
225
|
+
if (!running) return 'Bridgerator is not running (it starts at the next sign-in; or run the install command again)';
|
|
226
|
+
if (state?.online) return `Bridgerator is running — online as ${state.online}`;
|
|
227
|
+
return 'Bridgerator is running — not connected yet (reconnecting)';
|
|
66
228
|
}
|
|
67
229
|
|
|
68
230
|
function stopRunning() {
|
|
69
231
|
if (process.platform !== 'win32') return;
|
|
70
|
-
|
|
71
|
-
const ps = `Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object { $_.CommandLine -like '*bridgerator*cli.mjs*run*' -and $_.ProcessId -ne ${process.pid} } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }`;
|
|
72
|
-
try { execFileSync('powershell', ['-NoProfile', '-Command', ps], { stdio: 'pipe' }); } catch { /* none running */ }
|
|
232
|
+
try { ps(stopPs(PKG_DIR, VBS, process.pid)); } catch { /* none running */ }
|
|
73
233
|
}
|
|
74
234
|
|
|
75
235
|
// Start through the scheduled task itself — same as at logon, and not a child of this console
|
|
76
236
|
// (closing the installer window or a job object around it must not kill the bridge).
|
|
77
237
|
function startHidden() {
|
|
78
|
-
if (process.platform === 'win32') execFileSync(
|
|
238
|
+
if (process.platform === 'win32') execFileSync(SCHTASKS, ['/Run', '/TN', taskName()], { stdio: 'pipe' });
|
|
79
239
|
else spawn(process.execPath, [SELF, 'run'], { detached: true, stdio: 'ignore' }).unref();
|
|
80
240
|
}
|
|
81
241
|
|
|
242
|
+
// Removes `dir` from the user PATH, keeping its registry type (REG_EXPAND_SZ stays expandable).
|
|
243
|
+
const removeFromUserPathPs = (dir) => `$k = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true); if ($k -and $null -ne $k.GetValue('Path')) { $kind = $k.GetValueKind('Path'); $old = $k.GetValue('Path', '', 'DoNotExpandEnvironmentNames'); $new = ($old -split ';' | Where-Object { $_ -and $_.TrimEnd('\\') -ne ${q(dir)} }) -join ';'; if ($new -ne $old) { $k.SetValue('Path', $new, $kind) } }`;
|
|
244
|
+
|
|
245
|
+
// wss://mcp.host/link → https://mcp.host/device/revoke
|
|
246
|
+
export const revokeUrl = (relay) => { const u = new URL(relay); return `https://${u.host}/device/revoke`; };
|
|
247
|
+
// A key the server no longer knows (401: the computer was already removed in the account) is fine.
|
|
248
|
+
// 404 is NOT: the relay answers an unknown key with 401 only, so 404 = wrong address / old server.
|
|
249
|
+
export async function revokeKey(cfg, { fetchFn = fetch } = {}) {
|
|
250
|
+
const r = await fetchFn(revokeUrl(cfg.relay), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ token: cfg.token }), signal: AbortSignal.timeout(5000) });
|
|
251
|
+
if (!r.ok && r.status !== 401) throw new Error(`HTTP ${r.status}`);
|
|
252
|
+
}
|
|
253
|
+
// The account page of the site this config talks to (a staging install points to staging).
|
|
254
|
+
/**
|
|
255
|
+
* The tray's confirmations on/off: confirm.ini written, then the gate told. Only a failed write is
|
|
256
|
+
* "not written" + the confirm_locked notice (V-9); an error of the gate is logged as what it is (VR-5).
|
|
257
|
+
*/
|
|
258
|
+
export function trayConfirm(on, { ini, dc, log, notice = showNotice, set = setIni }) {
|
|
259
|
+
try { set(ini, 'confirm', 'enabled', on ? 'true' : 'false'); } catch (e) { log(`[bridgerator] confirm.ini not written (${e.code || e.message}) — setting unchanged`); notice({ kind: 'confirm_locked' }, log); return false; }
|
|
260
|
+
try { dc?.acceptConfirm?.({ enabled: on }); } catch (e) { log(`[bridgerator] tray confirm toggle: ${e?.message || e}`); }
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
export const accountUrlOf = (cfg) => { try { return cfg?.relay ? `${siteOf(cfg.relay)}/app` : ACCOUNT_URL; } catch { return ACCOUNT_URL; } };
|
|
264
|
+
|
|
265
|
+
// npm.cmd by absolute path only: the one next to the node.exe running us (its global folder holds this
|
|
266
|
+
// package), else the first on the PATH — never a relative entry, the current or the home folder (a
|
|
267
|
+
// planted npm.cmd there must not run). → null when none.
|
|
268
|
+
export function findNpm({ execPath = process.execPath, pathEnv = process.env.Path ?? process.env.PATH ?? '', cwd = process.cwd(), home = os.homedir(), exists = fs.existsSync } = {}) {
|
|
269
|
+
const near = path.join(path.dirname(execPath), 'npm.cmd');
|
|
270
|
+
if (exists(near)) return near;
|
|
271
|
+
const key = (d) => path.resolve(d).toLowerCase().replace(/[\\/]+$/, '');
|
|
272
|
+
const bad = new Set([key(cwd), key(home)]);
|
|
273
|
+
for (const raw of String(pathEnv).split(';')) {
|
|
274
|
+
const d = raw.trim().replace(/^"|"$/g, '');
|
|
275
|
+
if (!d || !path.isAbsolute(d) || !/^[a-z]:[\\/]/i.test(d) || bad.has(key(d))) continue;
|
|
276
|
+
const f = path.join(d, 'npm.cmd');
|
|
277
|
+
if (exists(f)) return f;
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* cmd.exe line for uninstall --all: wait ~3 s (this process exits), delete the folders, then their parent if empty.
|
|
284
|
+
* The paths come in environment variables (removeLaterEnv): cmd expands %VAR% once, so a % in a path
|
|
285
|
+
* (a folder named %NAME%) is never expanded again.
|
|
286
|
+
*/
|
|
287
|
+
export function removeLaterCmd(dirs) {
|
|
288
|
+
const ping = path.join(path.dirname(CMD), 'PING.EXE');
|
|
289
|
+
return [`"${ping}" -n 4 127.0.0.1 >nul`, ...dirs.map((d, i) => `rmdir /s /q "%BRRM${i}%"`), 'rmdir "%BRRMP%"'].join(' & ');
|
|
290
|
+
}
|
|
291
|
+
export const removeLaterEnv = (dirs, parent) => ({ ...Object.fromEntries(dirs.map((d, i) => [`BRRM${i}`, d])), BRRMP: parent });
|
|
292
|
+
/**
|
|
293
|
+
* cmd.exe arguments that run removeLaterCmd as written (spawned with windowsVerbatimArguments). With more
|
|
294
|
+
* than two quotes cmd /c drops the FIRST and the LAST quote of the line: /s + one outer pair makes that
|
|
295
|
+
* rule take exactly the outer pair.
|
|
296
|
+
*/
|
|
297
|
+
export const removeLaterArgs = (dirs) => ['/d', '/s', '/c', `"${removeLaterCmd(dirs)}"`];
|
|
298
|
+
/**
|
|
299
|
+
* Starts the delayed delete detached. cwd = %SystemRoot%: cmd's working folder inside a folder it deletes
|
|
300
|
+
* (uninstall run from the app folder) would lock it. → Promise<Error | null> (null = cmd.exe started).
|
|
301
|
+
*/
|
|
302
|
+
export function startRemoveLater(dirs, parent, { spawnFn = spawn } = {}) {
|
|
303
|
+
return new Promise((resolve) => {
|
|
304
|
+
try {
|
|
305
|
+
const c = spawnFn(CMD, removeLaterArgs(dirs), { detached: true, stdio: 'ignore', windowsHide: true, windowsVerbatimArguments: true, cwd: path.dirname(path.dirname(CMD)), env: { ...process.env, ...removeLaterEnv(dirs, parent) } });
|
|
306
|
+
c.once('error', resolve); c.once('spawn', () => resolve(null)); c.unref();
|
|
307
|
+
} catch (e) { resolve(e); }
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Uninstall deletes only what Bridgerator itself creates there, then the folder if it is empty.
|
|
312
|
+
export const OUR_FILES = ['config.json', 'confirm.ini', 'queue.json', 'recent.jsonl', 'usage.json', 'bridge.log', 'bridge.log.1', 'run-hidden.vbs', 'confirm.approved', 'delegate.json'];
|
|
313
|
+
export function removeOurFiles(dir) {
|
|
314
|
+
if (!fs.existsSync(dir)) return; // already gone: nothing to remove
|
|
315
|
+
for (const n of OUR_FILES) fs.rmSync(path.join(dir, n), { force: true, maxRetries: 3 });
|
|
316
|
+
fs.rmSync(path.join(dir, 'dc'), { recursive: true, force: true, maxRetries: 3 }); // the engine's own config folder
|
|
317
|
+
let names = [];
|
|
318
|
+
try { names = fs.readdirSync(dir); } catch { /* gone meanwhile */ }
|
|
319
|
+
for (const n of names) if (/\.\d+\.tmp$/.test(n)) fs.rmSync(path.join(dir, n), { force: true });
|
|
320
|
+
try { fs.rmdirSync(dir); } catch { /* something else is in there: left alone */ }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Only a folder that is clearly ours is deleted: …\Bridgerator under APPDATA / LOCALAPPDATA, or one
|
|
324
|
+
// holding our config.json (custom BRIDGERATOR_HOME).
|
|
325
|
+
export function isOurDir(dir, roots = [APPDATA, LOCALAPPDATA]) {
|
|
326
|
+
const d = path.resolve(dir);
|
|
327
|
+
if (path.basename(d).toLowerCase() === 'bridgerator' && roots.some((r) => path.resolve(r).toLowerCase() === path.dirname(d).toLowerCase())) return true;
|
|
328
|
+
try { const c = JSON.parse(fs.readFileSync(path.join(d, 'config.json'), 'utf8')); return !!(c.relay && c.token); } catch { return false; }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// OneDrive root: online-only placeholders would be downloaded by searches/listings.
|
|
332
|
+
// Nothing extra is excluded by default (owner 21.10: OneDrive too). An install from before kept
|
|
333
|
+
// exactly [OneDrive] as its default — that entry goes; a list the person changed stays.
|
|
334
|
+
const defaultDenied = () => [];
|
|
335
|
+
/** The setup summary's "excluded" line: only what really applies (sensitive folders only when that switch is on). */
|
|
336
|
+
export const excludedLine = (cfg) => [...(cfg.deniedDirectories || []), `built-in: Bridgerator's own folders${cfg.protectSensitive ? ', AppData, credentials (.ssh, .aws, …), Startup, PowerShell profiles' : ''}`].join(', ');
|
|
337
|
+
/** 'ru' when the Windows display language or regional format is Russian, else 'en'. */
|
|
338
|
+
export function osLanguage(locales = [Intl.DateTimeFormat().resolvedOptions().locale, process.env.LANG]) {
|
|
339
|
+
return locales.some((l) => /^ru([-_.]|$)/i.test(String(l || ''))) ? 'ru' : 'en';
|
|
340
|
+
}
|
|
341
|
+
const keptDenied = (list) => (Array.isArray(list) && !(list.length === 1 && process.env.OneDrive && list[0].toLowerCase() === process.env.OneDrive.toLowerCase()) ? list : []);
|
|
342
|
+
|
|
82
343
|
async function main() {
|
|
83
344
|
const o = parse(process.argv.slice(2));
|
|
84
345
|
const cmd = o._[0];
|
|
346
|
+
// Before anything is created: the installer runs a fresh bridgerator.exe --version as its start check.
|
|
347
|
+
if (cmd === 'version' || (!cmd && o.version === true)) { console.log(VERSION); return; }
|
|
348
|
+
// Own folder as the working directory: no DLL / module lookups from wherever we were started.
|
|
349
|
+
if (cmd !== 'uninstall') { fs.mkdirSync(DIR, { recursive: true }); process.chdir(DIR); } else process.chdir(home);
|
|
350
|
+
|
|
351
|
+
if (cmd === 'setup' && o._[1] === 'done') {
|
|
352
|
+
// The person skips the guided setup in ChatGPT (bridgerator_setup).
|
|
353
|
+
const cfg = readConfig();
|
|
354
|
+
if (!cfg) throw new Error(`not configured (${CONFIG})`);
|
|
355
|
+
writeConfig({ ...cfg, setupDone: true });
|
|
356
|
+
console.log('Guided setup marked as done.');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
85
359
|
if (cmd === 'setup') {
|
|
86
|
-
const prev = readConfig() || {};
|
|
87
360
|
if (o.token === true) throw new Error('--token needs the device key from your Bridgerator account');
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
361
|
+
// Checked before pairing: a typo must not cost a freshly confirmed key.
|
|
362
|
+
for (const k of ['allow', 'deny', 'read-only', 'relay', 'language']) if (o[k] === true) throw new Error(`--${k} needs a value`);
|
|
363
|
+
const language = o.language === undefined ? undefined : String(o.language).toLowerCase();
|
|
364
|
+
if (language !== undefined && !['en', 'ru'].includes(language)) throw new Error('--language must be en or ru');
|
|
365
|
+
let prev = readConfig() || {};
|
|
366
|
+
const relay = checkRelay(o.relay || prev.relay || DEFAULT_RELAY);
|
|
367
|
+
// --pair: get a new key through the browser even if one is configured. Without it, a stored key
|
|
368
|
+
// the relay rejects (computer removed in the account) is replaced the same way; offline = keep it.
|
|
369
|
+
// The running bridge keeps working until the new key is in hand.
|
|
370
|
+
let paired = !o.token && (o.pair || !prev.token);
|
|
371
|
+
if (!o.token && !paired && (await checkKey(siteOf(relay), prev.token)) === 'rejected') {
|
|
372
|
+
console.log("This computer was removed from your account — let's connect it again.");
|
|
373
|
+
paired = true;
|
|
374
|
+
}
|
|
375
|
+
// Pairing errors are plain messages for the person (code expired, too many attempts): exit 3, no stack.
|
|
376
|
+
// A network / TLS failure is no clean stop: a plain error (exit 1), so the installer rolls back.
|
|
377
|
+
const linked = paired ? await pair(siteOf(relay), os.hostname(), { fp: thisPcFp() }).catch((e) => {
|
|
378
|
+
if (e instanceof TypeError || e?.name === 'AbortError' || e?.name === 'TimeoutError') throw new Error(`could not reach ${siteOf(relay)} (${e.cause?.code || e.message}) — check the internet connection and run the install command again`);
|
|
379
|
+
e.friendly = true; throw e;
|
|
380
|
+
}) : null;
|
|
381
|
+
const token = o.token || (linked ? linked.token : prev.token);
|
|
382
|
+
if (linked) console.log(`Confirmed — this computer is linked to ${linked.email ? `the account ${linked.email}` : 'your account'}.`);
|
|
383
|
+
const abs = (list) => list.map((d) => path.resolve(CWD, d));
|
|
384
|
+
const build = () => ({
|
|
94
385
|
relay,
|
|
95
386
|
token,
|
|
96
|
-
|
|
97
|
-
|
|
387
|
+
// Empty = all folders (the always-blocked list still applies); a list = only those folders.
|
|
388
|
+
allowedDirectories: o.allow ? abs(o.allow) : prev.allowedDirectories || [],
|
|
389
|
+
// Added to the built-in exclusions (policy.builtinDenied), which can't be removed.
|
|
390
|
+
deniedDirectories: o.deny ? abs(o.deny) : prev.deniedDirectories ? keptDenied(prev.deniedDirectories) : defaultDenied(),
|
|
391
|
+
// "Protect sensitive folders" (policy.sensitiveDenied): off unless the person turned it on. Written
|
|
392
|
+
// explicitly: the bridge and its settings-file watcher read a missing key as on.
|
|
393
|
+
protectSensitive: prev.protectSensitive === true,
|
|
394
|
+
// Readable, never changed by the file tools (the terminal is not bound by folders).
|
|
395
|
+
readOnlyDirectories: o['read-only'] ? abs(o['read-only']) : prev.readOnlyDirectories || [],
|
|
98
396
|
// blockedCommands: absent = Desktop Commander's own default list (format, diskpart, reg, net, sc, runas, …).
|
|
99
397
|
...(prev.blockedCommands ? { blockedCommands: prev.blockedCommands } : {}),
|
|
100
398
|
disabledTools: prev.disabledTools || [],
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
399
|
+
installId: prev.installId || crypto.randomUUID(),
|
|
400
|
+
telemetryEnabled: o['no-telemetry'] ? false : prev.telemetryEnabled !== false,
|
|
401
|
+
bridgeVersion: VERSION,
|
|
402
|
+
setupDone: prev.setupDone === true, // set by bridgerator_setup (Apply on the PC) or `setup done`
|
|
403
|
+
// Window language: the person's choice stays; a first config takes the installer's (Windows) language.
|
|
404
|
+
language: prev.language || language || osLanguage(),
|
|
405
|
+
});
|
|
406
|
+
let cfg = build();
|
|
407
|
+
try {
|
|
408
|
+
stopRunning();
|
|
409
|
+
// Pairing can take minutes while the old bridge runs, and it may save a setting while it stops:
|
|
410
|
+
// built again from the file as it is now, so nothing the person changed meanwhile is lost.
|
|
411
|
+
prev = readConfig() || prev; cfg = build();
|
|
412
|
+
writeConfig(setupConfig(prev, cfg));
|
|
413
|
+
restoreIni(INI);
|
|
414
|
+
console.log(`config: ${CONFIG}`);
|
|
415
|
+
console.log(`allowed: ${cfg.allowedDirectories.length ? cfg.allowedDirectories.join(', ') : 'all folders (except the always-blocked ones)'}`);
|
|
416
|
+
console.log(`excluded: ${excludedLine(cfg)}`);
|
|
417
|
+
if (cfg.readOnlyDirectories.length) console.log(`read-only: ${cfg.readOnlyDirectories.join(', ')}`);
|
|
418
|
+
console.log(`confirm: ${readSettings(INI).enabled ? 'on' : 'off'} (a window on this PC before deletions / system commands; settings: ${INI}; bridgerator.cmd confirm on|off)`);
|
|
419
|
+
if (!cfg.protectSensitive) console.log('protect sensitive folders: off (startup programs, PowerShell profiles, .ssh, cloud logins, AppData) — turn on in the Bridgerator window → Safety');
|
|
420
|
+
console.log(installAutostart());
|
|
421
|
+
startHidden();
|
|
422
|
+
} catch (e) {
|
|
423
|
+
try { startHidden(); } catch { /* no task yet */ } // the bridge that ran before keeps running
|
|
424
|
+
throw e;
|
|
425
|
+
}
|
|
110
426
|
console.log(`started in background — log: ${LOG}`);
|
|
427
|
+
console.log(`usage statistics: ${cfg.telemetryEnabled ? 'on (anonymous counts only; turn off: bridgerator.cmd telemetry off)' : 'off'}`);
|
|
111
428
|
console.log('Next: in ChatGPT add the Bridgerator app and sign in with the same account.');
|
|
429
|
+
if (!cfg.setupDone) console.log('Then open ChatGPT and say: Set up Bridgerator');
|
|
430
|
+
const event = !prev.token ? 'install' : prev.bridgeVersion !== VERSION ? 'update' : null;
|
|
431
|
+
if (event) await send(cfg, event);
|
|
112
432
|
return;
|
|
113
433
|
}
|
|
114
434
|
if (cmd === 'run') {
|
|
115
|
-
|
|
116
|
-
if (!cfg) throw new Error(`no config at ${CONFIG} — run
|
|
117
|
-
|
|
118
|
-
|
|
435
|
+
let cfg = readConfig();
|
|
436
|
+
if (!cfg) throw new Error(`no config at ${CONFIG} — run the install command from ${ACCOUNT_URL}`);
|
|
437
|
+
checkRelay(cfg.relay);
|
|
438
|
+
// A config from before 21.10 has no protectSensitive: written as off (the new default), explicitly.
|
|
439
|
+
if (typeof cfg.protectSensitive !== 'boolean') { cfg = { ...cfg, protectSensitive: false }; writeConfig(cfg); }
|
|
440
|
+
restoreIni(INI);
|
|
441
|
+
// The engine's on-disk call logs (arguments + output) are patched off; old ones are removed.
|
|
442
|
+
for (const f of ['tool-history.jsonl', 'claude_tool_call.log']) fs.rmSync(path.join(DIR, 'dc', f), { force: true });
|
|
443
|
+
// Tray helper first (its questions are instant); its menu / window requests go to the bridge.
|
|
444
|
+
let dc = null, pauseTimer = null;
|
|
445
|
+
const offlineLink = offlineWatch(() => {
|
|
446
|
+
if (dc.linkState === 'online' || readConfig()?.notify?.offline === false) return false;
|
|
447
|
+
showNotice({ kind: 'offline' }, log); return true;
|
|
448
|
+
});
|
|
449
|
+
const tray = startTray({
|
|
450
|
+
log, account: accountUrlOf(cfg),
|
|
451
|
+
onMenu: (a, m) => {
|
|
452
|
+
if (a === 'quit') { log('[bridgerator] quit from the tray (starts again at the next sign-in)'); tray?.close(); process.exit(0); }
|
|
453
|
+
if (a === 'pause') dc?.setPaused(true, m.minutes);
|
|
454
|
+
if (a === 'resume') dc?.setPaused(false);
|
|
455
|
+
// confirm.ini held by another program (V-9): the state stays, the person is told.
|
|
456
|
+
if (a === 'confirm') {
|
|
457
|
+
trayConfirm(!!m.on, { ini: INI, dc, log });
|
|
458
|
+
pushTray();
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
onRpc: (m, p, paths) => { if (!dc) throw new Error('starting'); return dc.rpc(m, p, paths); },
|
|
462
|
+
onPower: (pw) => { if (dc) dc.power = pw; },
|
|
463
|
+
});
|
|
464
|
+
// The tray mirrors the bridge: state dot, PC name, queue count, confirm, and the pay item / badge.
|
|
465
|
+
const pushTray = () => {
|
|
466
|
+
if (!tray || !dc) return;
|
|
467
|
+
const b = dc.banner();
|
|
468
|
+
tray.state({
|
|
469
|
+
state: dc.paused ? 'paused' : dc.linkState, name: dc.cfgName?.() || dc.linkName, todo: dc.queue ? dc.queue.counts().todo : 0,
|
|
470
|
+
confirm: dc.mode ? dc.mode().confirm : readSettings(INI).enabled ? 'on' : 'off', theme: dc.popup().theme, lang: dc.popup().lang, awake: dc.wantAwake(),
|
|
471
|
+
plan: b.kind === 'none' ? null : { label: b.action === 'renew' ? 'Renew Pro…' : 'Upgrade to Pro…', url: b.url, badge: b.tone },
|
|
472
|
+
});
|
|
473
|
+
};
|
|
474
|
+
dc = await startDesktopCommander({ ...cfg, dcConfigDir: path.join(DIR, 'dc'), configDir: DIR, confirmIni: INI, configFile: CONFIG, snapshotsDir: SNAP_DIR, scriptsDir: SCRIPTS_DIR }, log, { ask: tray ? (q) => tray.ask(q) : undefined });
|
|
475
|
+
dc.cfgName = () => readConfig()?.deviceName;
|
|
476
|
+
dc.openWindow = (page) => tray?.open(page);
|
|
477
|
+
dc.onPlan = pushTray;
|
|
478
|
+
dc.onLink = (state, name) => {
|
|
479
|
+
dc.linkState = state; if (name) dc.linkName = name;
|
|
480
|
+
offlineLink(state);
|
|
481
|
+
pushTray();
|
|
482
|
+
};
|
|
483
|
+
dc.setPaused = (v, minutes) => {
|
|
484
|
+
clearTimeout(pauseTimer);
|
|
485
|
+
dc.paused = v; log(`[bridgerator] ${v ? `paused${minutes ? ` for ${minutes} min` : ''}` : 'resumed'}`);
|
|
486
|
+
if (v && Number.isInteger(minutes) && minutes > 0 && minutes <= 24 * 60) pauseTimer = setTimeout(() => dc.setPaused(false), minutes * 60000);
|
|
487
|
+
pushTray(); dc.onMode(dc.mode());
|
|
488
|
+
};
|
|
489
|
+
setInterval(pushTray, 5000).unref();
|
|
490
|
+
// Window actions that need the CLI: autostart on/off, unlink, uninstall.
|
|
491
|
+
dc.hooks = {
|
|
492
|
+
autostart: (on) => execFileSync(SCHTASKS, ['/Change', '/TN', taskName(), on ? '/ENABLE' : '/DISABLE'], { stdio: 'pipe' }),
|
|
493
|
+
unlink: async () => {
|
|
494
|
+
const c = readConfig();
|
|
495
|
+
if (c?.token) await revokeKey(c).catch((e) => log(`[bridgerator] could not remove this computer's key on the server (${e.message}) — remove the computer in your account`));
|
|
496
|
+
const now = readConfig(); // read again after the network call: a change made meanwhile is kept
|
|
497
|
+
if (now) writeConfig({ ...now, token: undefined });
|
|
498
|
+
log('[bridgerator] unlinked from the Bridgerator window — run the install command to connect again');
|
|
499
|
+
tray?.close(); process.exit(0);
|
|
500
|
+
},
|
|
501
|
+
uninstall: () => {
|
|
502
|
+
spawn(process.execPath, [SELF, 'uninstall', '--all'], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
connect({ ...cfg, usageFile: path.join(DIR, 'usage.json') }, dc, log);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (cmd === 'doctor') {
|
|
509
|
+
// What the link needs, in plain words with what to do (doctor.mjs). Exit 1 when something fails.
|
|
510
|
+
let tn; try { tn = process.platform === 'win32' ? taskName() : undefined; } catch { /* doctor finds the task itself */ }
|
|
511
|
+
const r = await runDoctor(readConfig(), { version: VERSION, running: process.platform === 'win32' ? isRunning() : undefined, taskName: tn, configDir: DIR });
|
|
512
|
+
console.log(o.json ? JSON.stringify(r, null, 2) : formatDoctor(r));
|
|
513
|
+
if (!r.ok) process.exitCode = 1;
|
|
119
514
|
return;
|
|
120
515
|
}
|
|
516
|
+
if (cmd === 'stop') { stopRunning(); console.log('Bridgerator stopped (it starts again at the next logon).'); return; }
|
|
121
517
|
if (cmd === 'status') {
|
|
122
518
|
const cfg = readConfig();
|
|
123
|
-
if (!cfg) { console.log(`not
|
|
124
|
-
|
|
125
|
-
try {
|
|
519
|
+
if (!cfg) { console.log(`Bridgerator is not set up on this PC — run the install command from ${ACCOUNT_URL}`); return; }
|
|
520
|
+
let tail = [];
|
|
521
|
+
try { tail = fs.readFileSync(LOG, 'utf8').split('\n').filter(Boolean); } catch { /* no log yet */ }
|
|
522
|
+
console.log(statusLine(isRunning(), tail));
|
|
523
|
+
if (o.json) console.log(JSON.stringify({ ...cfg, token: `${String(cfg.token).slice(0, 4)}…` }, null, 2));
|
|
524
|
+
else console.log(`settings: ${CONFIG}\nlog: ${LOG}\n(details: bridgerator.cmd status --json)`);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (cmd === 'confirm') {
|
|
528
|
+
// confirm.ini is read on every call: no restart. `confirm on <category>` turns one check back on;
|
|
529
|
+
// plain `confirm on` also brings back the default checks (deletions, system commands).
|
|
530
|
+
const [, v, cat] = o._;
|
|
531
|
+
if (!['on', 'off'].includes(v) || (cat && !CATEGORIES.includes(cat))) throw new Error(`usage: bridgerator.cmd confirm on|off [${CATEGORIES.join('|')}]`);
|
|
532
|
+
restoreIni(INI);
|
|
533
|
+
// `confirm off <category>`: only that check goes off, the window stays as it is.
|
|
534
|
+
if (v === 'on' || !cat) setIni(INI, 'confirm', 'enabled', v === 'on' ? 'true' : 'false');
|
|
535
|
+
if (v === 'on') for (const c of cat ? [cat] : ['delete', 'dangerous_commands']) setIni(INI, 'ask', c, 'true');
|
|
536
|
+
else if (cat) setIni(INI, 'ask', cat, 'false');
|
|
537
|
+
// Turning protection off from a script (not a console the person types in): the running bridge
|
|
538
|
+
// applies it only after Apply in a notification on this PC.
|
|
539
|
+
if (v === 'off' && !process.stdin.isTTY) console.log('Bridgerator asks on this PC before turning protection off — click Apply in the notification.');
|
|
540
|
+
if (v === 'off' && cat) { console.log(`confirmations for ${cat} off (the window stays ${readSettings(INI).enabled ? 'on' : 'off'}) — settings: ${INI}`); return; }
|
|
541
|
+
console.log(`confirmation window ${v}${v === 'on' ? ` (asks for: ${Object.entries(readSettings(INI).ask).filter(([, x]) => x).map(([k]) => k).join(', ') || 'nothing'})` : ''} — settings: ${INI}`);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (cmd === 'telemetry' || cmd === 'read-only') {
|
|
545
|
+
const cfg = readConfig();
|
|
546
|
+
if (!cfg) throw new Error(`not configured (${CONFIG})`);
|
|
547
|
+
const [, v, dir] = o._;
|
|
548
|
+
let next;
|
|
549
|
+
if (cmd === 'read-only') {
|
|
550
|
+
if (!['add', 'remove'].includes(v) || !dir) throw new Error('usage: bridgerator.cmd read-only add|remove <folder>');
|
|
551
|
+
const d = path.resolve(CWD, dir), list = (cfg.readOnlyDirectories || []).filter((x) => x.toLowerCase() !== d.toLowerCase());
|
|
552
|
+
next = { ...cfg, readOnlyDirectories: v === 'add' ? [...list, d] : list };
|
|
553
|
+
console.log(`read-only folders: ${next.readOnlyDirectories.join(', ') || '-'}`);
|
|
554
|
+
} else {
|
|
555
|
+
if (!['on', 'off'].includes(v)) throw new Error('usage: bridgerator.cmd telemetry on|off');
|
|
556
|
+
next = { ...cfg, telemetryEnabled: v === 'on' };
|
|
557
|
+
console.log(`usage statistics ${v}`);
|
|
558
|
+
}
|
|
559
|
+
writeConfig(next);
|
|
560
|
+
// A running bridge picks the change up within seconds (its settings-file watcher); lowering
|
|
561
|
+
// protection waits for Apply in a notification on this PC.
|
|
562
|
+
if (isRunning()) console.log('The running Bridgerator applies this in a few seconds (turning protection off: click Apply in the notification on this PC).');
|
|
126
563
|
return;
|
|
127
564
|
}
|
|
128
565
|
if (cmd === 'uninstall') {
|
|
566
|
+
// uninstall --all: everything (THE uninstall for customers); --keep-config: keep key + folder settings.
|
|
567
|
+
const cfg = readConfig();
|
|
568
|
+
if (cfg) await send(cfg, 'uninstall');
|
|
569
|
+
// Best effort: the relay forgets this device key, so a reinstall doesn't become "<name>-2".
|
|
570
|
+
if (cfg?.token && !o['keep-config']) await revokeKey(cfg).catch((e) => console.log(`Could not remove this computer's key on the server (${e.message}) — remove the computer in your account (below).`));
|
|
571
|
+
if (process.platform === 'win32') {
|
|
572
|
+
try {
|
|
573
|
+
ps([
|
|
574
|
+
`Unregister-ScheduledTask -TaskName ${q(taskName())} -Confirm:$false -ErrorAction SilentlyContinue`,
|
|
575
|
+
dropOldTaskPs(),
|
|
576
|
+
"Remove-Item 'HKCU:\\Software\\Classes\\AppUserModelId\\Bridgerator' -Recurse -Force -ErrorAction SilentlyContinue",
|
|
577
|
+
].join('; '));
|
|
578
|
+
} catch { /* not installed */ }
|
|
579
|
+
// Scheduled automations (Task Scheduler \Bridgerator\) must not run after an uninstall, --keep-config too.
|
|
580
|
+
const t = await removeTasks({ dir: SCRIPTS_DIR });
|
|
581
|
+
if (t.removed || t.errors.length) console.log(`Scheduled automations removed: ${t.removed}${t.errors.length ? ` (not removed: ${t.errors.join('; ')})` : ''}`);
|
|
582
|
+
}
|
|
129
583
|
stopRunning();
|
|
130
|
-
|
|
131
|
-
|
|
584
|
+
console.log('Bridgerator stopped, autostart removed.');
|
|
585
|
+
if (!o['keep-config']) {
|
|
586
|
+
if (isOurDir(DIR)) { removeOurFiles(DIR); console.log(`Removed Bridgerator's files from ${DIR} (device key, settings, task queue, engine data and history).`); }
|
|
587
|
+
else console.log(`Not deleting ${DIR}: it doesn't look like a Bridgerator folder — remove it yourself if it is.`);
|
|
588
|
+
// The automations' scripts and run logs (only a folder that is clearly ours: it has our runner / index).
|
|
589
|
+
if (['automations.json', 'runner.ps1'].some((n) => fs.existsSync(path.join(SCRIPTS_DIR, n)))) fs.rmSync(SCRIPTS_DIR, { recursive: true, force: true, maxRetries: 3 });
|
|
590
|
+
// The tray window's browser data and installer leftovers under %LOCALAPPDATA%\Bridgerator.
|
|
591
|
+
// (+ the compiled helpers cache and a previous app folder an interrupted update left behind)
|
|
592
|
+
try { for (const n of fs.readdirSync(path.join(LOCALAPPDATA, 'Bridgerator'))) if (n === 'webview' || n === 'snapshots' || n === 'cache' || /^tmp-[0-9a-f]{32}$/.test(n) || /^app\.old-[0-9a-f]{32}$/.test(n)) fs.rmSync(path.join(LOCALAPPDATA, 'Bridgerator', n), { recursive: true, force: true, maxRetries: 3 }); } catch { /* none */ }
|
|
593
|
+
}
|
|
594
|
+
const ownNode = fs.existsSync(path.join(NODE_DIR, 'node.exe'));
|
|
595
|
+
const prog = programFiles();
|
|
596
|
+
if (o.all) {
|
|
597
|
+
console.log('Removing the program files...');
|
|
598
|
+
// Folders this very process may run from (bridgerator.exe, node.exe): deleted a few seconds after we exit.
|
|
599
|
+
const later = [];
|
|
600
|
+
if (prog.exe) {
|
|
601
|
+
try { ps(removeFromUserPathPs(prog.dir)); } catch { /* PATH left as is */ }
|
|
602
|
+
if (prog.ours) later.push(prog.dir);
|
|
603
|
+
else console.log(`This Bridgerator runs from ${prog.dir} — delete that folder yourself.`);
|
|
604
|
+
} else {
|
|
605
|
+
// The npm next to the node.exe running us is the one whose global folder holds this package.
|
|
606
|
+
const npm = findNpm();
|
|
607
|
+
if (!npm) console.log('Could not find npm.cmd next to node.exe or on the PATH — remove the package yourself: npm.cmd rm -g bridgerator');
|
|
608
|
+
else try { execFileSync(CMD, ['/d', '/c', npm, 'rm', '-g', 'bridgerator', '--loglevel=error'], { stdio: 'inherit' }); } catch { console.log('Could not remove the npm package — run: npm.cmd rm -g bridgerator'); }
|
|
609
|
+
}
|
|
610
|
+
if (ownNode) {
|
|
611
|
+
try { ps(removeFromUserPathPs(NODE_DIR)); } catch { /* PATH left as is */ }
|
|
612
|
+
later.push(NODE_DIR);
|
|
613
|
+
// npm's cache, only when the installer created it for that private Node.js (marker file).
|
|
614
|
+
try {
|
|
615
|
+
const cache = fs.readFileSync(path.join(LOCALAPPDATA, 'Bridgerator', 'created-npm-cache'), 'utf8').trim();
|
|
616
|
+
if (cache && path.resolve(cache).toLowerCase() === path.join(LOCALAPPDATA, 'npm-cache').toLowerCase()) fs.rmSync(cache, { recursive: true, force: true, maxRetries: 3 });
|
|
617
|
+
fs.rmSync(path.join(LOCALAPPDATA, 'Bridgerator', 'created-npm-cache'), { force: true });
|
|
618
|
+
} catch { /* not ours */ }
|
|
619
|
+
}
|
|
620
|
+
if (later.length) {
|
|
621
|
+
// Reported only once cmd.exe has really started (a failed start says so).
|
|
622
|
+
const e = await startRemoveLater(later, path.join(LOCALAPPDATA, 'Bridgerator'));
|
|
623
|
+
if (e) console.log(`Could not remove ${later.join(', ')} (${e.message}) — delete ${later.length > 1 ? 'these folders' : 'this folder'} yourself.`);
|
|
624
|
+
else for (const d of later) console.log(`${d === NODE_DIR ? 'The Node.js that Bridgerator installed' : 'The Bridgerator program folder'} (${d}) is removed in a few seconds.`);
|
|
625
|
+
}
|
|
626
|
+
} else if (prog.exe) {
|
|
627
|
+
console.log('To remove the program files: bridgerator.cmd uninstall --all');
|
|
628
|
+
} else {
|
|
629
|
+
console.log('To remove the program files: npm.cmd rm -g bridgerator');
|
|
630
|
+
if (ownNode) console.log(`and the Node.js that Bridgerator installed: Remove-Item -Recurse -Force '${NODE_DIR}'`);
|
|
631
|
+
console.log('(or remove everything at once: bridgerator.cmd uninstall --all)');
|
|
632
|
+
}
|
|
633
|
+
console.log(`Also remove this computer in your account: ${accountUrlOf(cfg)}`);
|
|
132
634
|
return;
|
|
133
635
|
}
|
|
134
|
-
console.log('usage: bridgerator setup [--token <key> | --pair] [--allow <dir>]... [--deny <dir>]... | run | status | uninstall');
|
|
636
|
+
console.log('usage: bridgerator.cmd setup [--token <key> | --pair] [--allow <dir>]... [--deny <dir>]... [--read-only <dir>]... [--language en|ru] [--no-telemetry] | run | stop | status | doctor [--json] | confirm on|off [category] | read-only add|remove <dir> | telemetry on|off | uninstall [--all | --keep-config] | --version');
|
|
135
637
|
}
|
|
136
638
|
|
|
137
|
-
|
|
639
|
+
// The hidden bridge has no console: its startup errors go to bridge.log too.
|
|
640
|
+
main().catch((e) => {
|
|
641
|
+
if (e?.friendly) { console.log(`\n${e.message}`); process.exit(3); }
|
|
642
|
+
const msg = `[bridgerator] ${e?.message || e}`;
|
|
643
|
+
if (process.argv[2] === 'run') log(msg); else console.error(msg);
|
|
644
|
+
process.exit(1);
|
|
645
|
+
});
|