my-provider-dash-manager 0.2.8

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/LICENSE ADDED
@@ -0,0 +1,63 @@
1
+ # My Provider Dash Manager — Licence
2
+
3
+ Copyright © 2026 Eduard Secureanu (GitHub: [FiredMosquito831](https://github.com/FiredMosquito831)).
4
+ All rights reserved.
5
+
6
+ Source-available. You may **use** this software, personally and commercially. You may **not** modify
7
+ it, redistribute changed versions, or present it as your own work.
8
+
9
+ ## 1. Permitted
10
+
11
+ 1. **Use, without charge, for any purpose — personal or commercial.** You may run the application on
12
+ any number of machines, including inside a company, and use it in the course of paid work.
13
+ 2. **Reading and studying the source**, including for security review.
14
+ 3. **Redistributing the software verbatim and unmodified**, keeping this licence and the attribution
15
+ below intact, provided you charge nothing for the software itself.
16
+ 4. **Private, local changes solely to make the software run in your own environment** (for example a
17
+ build fix). Such changes may not be published or distributed.
18
+
19
+ ## 2. Not permitted
20
+
21
+ 1. **Modifying the software** for distribution, or publishing modified or derivative versions, forks
22
+ with changes, ports, or rebrands.
23
+ 2. **Presenting the work, in whole or in part, as your own**, or removing or obscuring authorship,
24
+ copyright notices, or this licence.
25
+ 3. **Sublicensing, selling, or renting the software**, or including it in a paid product or service
26
+ offering where the software itself is what is being paid for.
27
+ 4. Using the author's name to endorse anything without written permission.
28
+
29
+ ## 3. Attribution
30
+
31
+ If you mention, describe, review, demonstrate, publish about, or otherwise refer to this software
32
+ anywhere — an article, video, README, documentation, presentation, social post, or product page —
33
+ you must credit the author and link the project repository:
34
+
35
+ > My Provider Dash Manager by FiredMosquito831 —
36
+ > https://github.com/FiredMosquito831/my-provider-dash-manager
37
+
38
+ ## 4. Contributions
39
+
40
+ Contributions are welcome only as issues, or as pull requests that the author may accept at their
41
+ sole discretion. By submitting a contribution you assign to the author the rights necessary to
42
+ license it under these terms.
43
+
44
+ ## 5. No warranty; no liability
45
+
46
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
47
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
48
+ IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
49
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
50
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
51
+
52
+ The software manages sessions and credentials for third-party services. You are responsible for
53
+ complying with those services' terms, and for the security of the machine it runs on.
54
+
55
+ ## 6. Termination
56
+
57
+ Any breach of sections 1–3 terminates the rights granted here automatically. Rights are reinstated if
58
+ the breach is corrected promptly after notice.
59
+
60
+ ---
61
+
62
+ *This is a bespoke source-available licence, not an OSI-approved open-source licence: it deliberately
63
+ withholds the right to modify and redistribute. Third-party dependencies keep their own licences.*
package/README.md ADDED
@@ -0,0 +1,228 @@
1
+ # My Provider Dash Manager
2
+
3
+ **One desktop app for every cloud account you own.** Keep several Vercel, Netlify, Supabase,
4
+ Cloudflare, Railway, Render and GitHub accounts signed in at the same time, each in a fully isolated
5
+ session, and switch between them instantly — the way a browser handles tabs, but built around
6
+ *accounts* instead of pages.
7
+
8
+ <sub>Windows desktop app · Electron 43 · local-first · no telemetry, no account, no server ·
9
+ source-available licence: free for personal and commercial use</sub>
10
+
11
+ ---
12
+
13
+ ## Install
14
+
15
+ Download the latest installer from
16
+ [Releases](https://github.com/FiredMosquito831/my-provider-dash-manager/releases) and run it.
17
+
18
+ The installer is not code-signed yet, so SmartScreen shows a warning: choose **More info → Run
19
+ anyway**.
20
+
21
+ ### Updates
22
+
23
+ The Home screen has an **Updates** panel: it shows your installed version, the latest release, the
24
+ release notes, and gives you *Check for updates*, *Download*, and *Restart & install* buttons with
25
+ download progress. Nothing is downloaded or installed without you pressing the button, and the app
26
+ checks quietly on startup and once an hour after that.
27
+
28
+ The repository is public, so updates need no setup. (If you ever fork this into a private repo, the
29
+ panel lets you store a GitHub token, encrypted with Windows DPAPI, so the feed stays readable.)
30
+
31
+ ### Install from the command line
32
+
33
+ ```bash
34
+ npx my-provider-dash-manager # fetch the newest release and run its installer
35
+ npx my-provider-dash-manager -- --silent # install without the wizard
36
+ npx my-provider-dash-manager -- --download-only # just fetch the .exe
37
+ ```
38
+
39
+ From a checkout of this repository, the same thing is `npm run install:latest`.
40
+
41
+ It prints the latest version and its release notes and verifies the download really is a Windows
42
+ executable before running it. No authentication is needed; a `GH_TOKEN` is used only if present.
43
+
44
+ ### Run from source
45
+
46
+ ```bash
47
+ npm install
48
+ npm start
49
+ ```
50
+
51
+ ---
52
+
53
+ ## What it looks like
54
+
55
+ **Home — every service and account in one grid, with the account rail on the left.**
56
+ Each provider lists its accounts; hovering one reveals open / sleep / close / fill-login / connect-token / manage.
57
+
58
+ ![Home screen](docs/screenshots/home.png)
59
+
60
+ **A real dashboard, signed in, inside the app.** The rail and tab strip stay put, so Home is always one click away.
61
+
62
+ ![A dashboard open in the app](docs/screenshots/dashboard.png)
63
+
64
+ **The top bar is only your open tabs** — filter them, or group them by provider with collapsible sections.
65
+ Slept tabs stay in the strip (dimmed) and resume on click.
66
+
67
+ ![Tabs grouped by provider](docs/screenshots/tabs-grouped.png)
68
+
69
+ **Updates and saved logins live on Home**: installed vs latest version with release notes and an update
70
+ button, plus login remembering, auto-fill and password import.
71
+
72
+ ![Updates and saved logins](docs/screenshots/settings.png)
73
+
74
+ <sub>Screenshots use a demo profile with placeholder account names.</sub>
75
+
76
+ ---
77
+
78
+ ## Why it exists
79
+
80
+ Every cloud provider assumes you have exactly one identity. The moment you have a personal account
81
+ and a client account on the same service, the browser fights you:
82
+
83
+ | The usual pain | What this app does |
84
+ |---|---|
85
+ | Signing out of one account to reach another | Every account stays signed in, in its own storage partition |
86
+ | Juggling browser profiles, one window each | One window: providers on the left, open accounts as tabs on top |
87
+ | "Which account am I actually in right now?" | Every tab and rail row is colour-coded and labelled by account |
88
+ | Losing your workspace on restart | The open-tab set is remembered and restored |
89
+ | Re-typing passwords for the fifth account | Logins are captured per account and refilled automatically |
90
+ | Ads and blinding white dashboards | Built-in ad-blocking and dark mode, no extensions required |
91
+
92
+ It is not a browser and not a scraper. It is a **container for identities**: real dashboards, real
93
+ sessions, one place.
94
+
95
+ ---
96
+
97
+ ## Features
98
+
99
+ ### Accounts and sessions
100
+ - **Unlimited accounts per service.** Each account gets its own Chromium storage partition — separate
101
+ cookies, localStorage, IndexedDB and cache. Accounts can never see each other's session.
102
+ - **Sessions persist across restarts.** Sign in once; you stay signed in (verified by an automated
103
+ restart test that counts real cookies after relaunch).
104
+ - **Instant switching.** Active tabs stay warm, so switching is a visibility toggle, not a reload.
105
+ - **Warm-tab limit with sleeping.** Each live dashboard costs roughly 125 MB, so the app keeps a
106
+ configurable number warm (default 5) and sleeps the rest. Slept tabs stay in the strip and resume on
107
+ click, and sleeping returns the app to about 350 MB. Measured: 5 tabs ≈ 1.0 GB, 10 ≈ 1.6 GB, all
108
+ slept ≈ 0.35 GB (private memory, the figure Task Manager reports).
109
+ - **Tab session memory.** The set of open tabs is saved and restored on the next launch: recent ones
110
+ come back warm, the rest asleep, and you land on the Home grid.
111
+ - **Per-account proxy** (optional) with proper connection flushing.
112
+
113
+ ### Layout
114
+ - **Left rail — your identities.** Providers listed vertically, accounts nested underneath, each with
115
+ hover actions: open, sleep, close, fill login, connect an API token, manage. Collapse any provider.
116
+ - **Top bar — open tabs only.** A filter box and a group-by-provider toggle with collapsible groups.
117
+ - **Home** is always one click (or `Esc`) away: a grid of every service and account with live status.
118
+ - Keyboard: `Ctrl+W` sleep, `Ctrl+Shift+W` sleep all, `Ctrl+Tab` cycle, `Ctrl+1…9` jump.
119
+
120
+ ### Saved logins
121
+ - **Remembers logins you type** inside an account tab, bound to *that* account, so two accounts on the
122
+ same service refill with different credentials.
123
+ - **Auto-fills sign-in pages** when it recognises one (toggleable), plus a manual fill action.
124
+ - **Imports existing passwords** from installed Chromium browsers (Chrome, Edge, Brave, Vivaldi,
125
+ Opera) or from a CSV exported by any browser, including Firefox.
126
+ - Everything is encrypted at rest with Windows DPAPI. Passwords never reach the app's own UI process.
127
+ - **Origin-bound**: a login is tied to the account *and* the service's own domains. A password typed on
128
+ another site reached from that tab (an OAuth hand-off, a look-alike page) is never captured as that
129
+ account's login, and a saved password is never filled into a page outside the service's domains.
130
+
131
+ ### API status
132
+ Paste a read-only API token into an account and its Home card shows live status — project counts and
133
+ names for every built-in service, plus latest deploy state on Vercel. Tokens are validated before
134
+ being stored, encrypted with DPAPI, revocable in one click, and never leave the main process.
135
+
136
+ ### Content
137
+ - **Ad-blocking** driven by EasyList (~52,000 blocked hosts plus generic cosmetic rules, refreshed
138
+ weekly) applied per account partition. Main-frame navigation is never blocked.
139
+ - **Dark mode**: sites that support `prefers-color-scheme` go dark natively, and an optional
140
+ force-dark inversion covers the ones that don't.
141
+
142
+ ### Extensibility
143
+ - **Add any service by URL** — it gets its own rail row and isolated per-account sessions.
144
+ - **Service plugins**: drop a small JSON manifest into `plugins/` (or install it from the UI) to
145
+ define a service and, optionally, how to read its API for status cards.
146
+
147
+ ---
148
+
149
+ ---
150
+
151
+ ## Using it
152
+
153
+ 1. **Add an account** — click `+` next to a provider in the left rail, name it, then choose *Log in*
154
+ or *Create a new account*. The service's real page opens inside that account's isolated session.
155
+ 2. **Sign in normally.** Use email/password or GitHub. Avoid "Continue with Google": Google blocks
156
+ sign-in inside embedded browsers, so use the **Browser** button to finish that flow in your system
157
+ browser instead.
158
+ 3. **Switch accounts** by clicking a tab or a rail row. Everything stays signed in.
159
+ 4. **Connect a token** (optional) via the link icon on any account to light up its status card.
160
+ 5. **Import passwords** (optional) from Home → *Import from browser* or *Import CSV*.
161
+
162
+ ### Multiple accounts and provider terms
163
+
164
+ The app surfaces each provider's stance instead of hiding it. GitHub and Railway allow one account
165
+ per person and the rail says so — use their organisations and workspaces instead. Vercel, Netlify,
166
+ Supabase and Cloudflare tolerate separate accounts but watch for abuse. Account creation is always
167
+ manual and guided; nothing is ever automated, because automated signup violates several providers'
168
+ acceptable-use policies.
169
+
170
+ ---
171
+
172
+ ## How it works
173
+
174
+ Each account is a `persist:<service>__<account>` Chromium partition rendered in its own
175
+ `WebContentsView`. The app window composes those views beside a native rail and tab strip, so the
176
+ chrome is always reachable. Account pages run sandboxed with context isolation, deny-by-default
177
+ permissions, and an isolated-world preload that handles login capture and filling without exposing
178
+ anything to the page.
179
+
180
+ State lives in your user-data folder: `accounts.json` (the registry), `session.json` (open tabs),
181
+ `settings.json`, `services.json` (custom services), `plugins/`, plus DPAPI-encrypted `tokens.json`
182
+ and `credentials.json`.
183
+
184
+ ### Security posture, honestly
185
+
186
+ Cookies, tokens and saved passwords are encrypted at rest with Windows DPAPI, which protects against
187
+ another user on the machine, a stolen disk, and casual copying. It does **not** protect against
188
+ malware running as you — no desktop app can, which is why the app never masks device identity and
189
+ keeps API tokens scoped and revocable. Saved logins are additionally bound to the service's own origins,
190
+ so they cannot be captured from — or filled into — an unrelated site. Nothing syncs anywhere: there is
191
+ no server, no account and no telemetry.
192
+
193
+ ---
194
+
195
+ ## Development
196
+
197
+ ```bash
198
+ npm start # run the app
199
+ npm run smoke # sessions, registry, partitions
200
+ npm run smoke:restore# sessions survive a restart (real cookies)
201
+ npm run smoke:tokens # DPAPI round-trip + live token rejection
202
+ npm run smoke:creds # import, per-account recall, encryption at rest
203
+ npm run smoke:autofill # real page: form detection, fill, capture
204
+ npm run smoke:updates # version comparison + real release-feed check
205
+ npm run capture # screenshot the running shell
206
+ npm run spike # memory benchmark
207
+ npm run dist # build the Windows installer
208
+ npm run install:latest # fetch and run the latest released installer
209
+ npm start -- --print-paths # show the app name, version and data folder
210
+ npm start -- --open vercel::personal # open an account on launch
211
+ ```
212
+
213
+ The suite is 35 checks across five modes.
214
+
215
+ `MAM_USER_DATA=<dir>` points any command at an isolated profile.
216
+
217
+ Releases are cut by pushing a `v*` tag: GitHub Actions builds the NSIS installer, uploads it with the
218
+ update feed, and publishes the release.
219
+
220
+ ## Licence
221
+
222
+ Source-available, © 2026 FiredMosquito831 — see [LICENSE](LICENSE).
223
+
224
+ **Free to use, personally and commercially.** You may not modify or redistribute changed versions, or
225
+ present the work as your own. If you mention the app anywhere, credit the author and link this repo:
226
+
227
+ > My Provider Dash Manager by FiredMosquito831 —
228
+ > https://github.com/FiredMosquito831/my-provider-dash-manager
package/package.json ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "name": "my-provider-dash-manager",
3
+ "version": "0.2.8",
4
+ "description": "Manage many accounts per cloud service (Vercel, Netlify, Supabase, Cloudflare, Railway, Render, GitHub) in one desktop app \u2014 isolated persistent sessions, one tab per account.",
5
+ "main": "src/main.js",
6
+ "scripts": {
7
+ "start": "electron .",
8
+ "spike": "electron . --spike",
9
+ "smoke": "electron . --smoke",
10
+ "smoke:restore": "electron . --smoke-restore",
11
+ "smoke:tokens": "electron . --smoke-tokens",
12
+ "capture": "electron . --capture",
13
+ "dist": "electron-builder --win",
14
+ "smoke:creds": "electron . --smoke-creds",
15
+ "smoke:autofill": "electron . --smoke-autofill",
16
+ "internal:save": "git --git-dir=.internal-git --work-tree=. add -f CLAUDE.md docs/WORKING-NOTES.md && git --git-dir=.internal-git --work-tree=. commit -m \"internal notes snapshot\"",
17
+ "internal:log": "git --git-dir=.internal-git --work-tree=. log --oneline",
18
+ "smoke:updates": "electron . --smoke-updates",
19
+ "install:latest": "node scripts/install-latest.js"
20
+ },
21
+ "devDependencies": {
22
+ "electron": "^43.0.0",
23
+ "electron-builder": "^26.0.0"
24
+ },
25
+ "dependencies": {
26
+ "electron-updater": "^6.6.0"
27
+ },
28
+ "build": {
29
+ "appId": "com.firedmosquito.my-provider-dash-manager",
30
+ "productName": "My Provider Dash Manager",
31
+ "files": [
32
+ "src/**/*",
33
+ "package.json"
34
+ ],
35
+ "win": {
36
+ "target": [
37
+ "nsis"
38
+ ]
39
+ },
40
+ "nsis": {
41
+ "oneClick": false,
42
+ "allowToChangeInstallationDirectory": true
43
+ },
44
+ "publish": {
45
+ "provider": "github",
46
+ "owner": "FiredMosquito831",
47
+ "repo": "my-provider-dash-manager"
48
+ },
49
+ "artifactName": "My-Provider-Dash-Manager-Setup-${version}.${ext}"
50
+ },
51
+ "license": "SEE LICENSE IN LICENSE",
52
+ "bin": {
53
+ "my-provider-dash-manager": "scripts/install-latest.js"
54
+ },
55
+ "files": [
56
+ "scripts/install-latest.js",
57
+ "README.md",
58
+ "LICENSE"
59
+ ],
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "git+https://github.com/FiredMosquito831/my-provider-dash-manager.git"
63
+ },
64
+ "homepage": "https://github.com/FiredMosquito831/my-provider-dash-manager",
65
+ "bugs": {
66
+ "url": "https://github.com/FiredMosquito831/my-provider-dash-manager/issues"
67
+ },
68
+ "author": "FiredMosquito831",
69
+ "keywords": [
70
+ "multi-account",
71
+ "account-manager",
72
+ "vercel",
73
+ "netlify",
74
+ "supabase",
75
+ "cloudflare",
76
+ "railway",
77
+ "render",
78
+ "github",
79
+ "electron",
80
+ "dashboard",
81
+ "sessions",
82
+ "windows"
83
+ ],
84
+ "os": [
85
+ "win32"
86
+ ],
87
+ "publishConfig": {
88
+ "access": "public"
89
+ },
90
+ "engines": {
91
+ "node": ">=18"
92
+ }
93
+ }
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ // Fetch the latest published release and run its installer.
3
+ //
4
+ // npm run install:latest download + launch the installer
5
+ // npm run install:latest -- --silent install without the wizard UI
6
+ // npm run install:latest -- --download-only
7
+ //
8
+ // Auth: the repository is private, so a token is required — GH_TOKEN/GITHUB_TOKEN from the
9
+ // environment, or whatever `gh auth token` returns. Making the repo public removes that need.
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const path = require('path');
13
+ const https = require('https');
14
+ const { execFileSync, spawn } = require('child_process');
15
+
16
+ const REPO = 'FiredMosquito831/my-provider-dash-manager';
17
+ const args = process.argv.slice(2);
18
+ const silent = args.includes('--silent');
19
+ const downloadOnly = args.includes('--download-only');
20
+
21
+ function token() {
22
+ if (process.env.GH_TOKEN) return process.env.GH_TOKEN;
23
+ if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
24
+ try { return execFileSync('gh', ['auth', 'token'], { encoding: 'utf8' }).trim(); } catch { return null; }
25
+ }
26
+
27
+ // accept: 'application/vnd.github+json' for API calls, 'application/octet-stream' for asset bytes.
28
+ // Without the octet-stream Accept, the asset endpoint returns JSON metadata instead of the file.
29
+ function get(url, tok, accept = 'application/vnd.github+json', redirects = 0) {
30
+ return new Promise((resolve, reject) => {
31
+ const headers = { 'User-Agent': 'my-provider-dash-manager-installer', Accept: accept };
32
+ if (tok) headers.Authorization = `Bearer ${tok}`;
33
+ https.get(url, { headers }, res => {
34
+ if ([301, 302, 307, 308].includes(res.statusCode) && res.headers.location) {
35
+ if (redirects > 5) return reject(new Error('too many redirects'));
36
+ res.resume();
37
+ // S3 asset redirects reject an Authorization header, so drop it after the first hop
38
+ return resolve(get(res.headers.location, null, accept, redirects + 1));
39
+ }
40
+ resolve(res);
41
+ }).on('error', reject);
42
+ });
43
+ }
44
+
45
+ async function json(url, tok) {
46
+ const res = await get(url, tok);
47
+ const body = await new Promise((resolve, reject) => {
48
+ let d = ''; res.setEncoding('utf8');
49
+ res.on('data', c => { d += c; }); res.on('end', () => resolve(d)); res.on('error', reject);
50
+ });
51
+ if (res.statusCode === 404) throw new Error('Release not found. The repo is private — set GH_TOKEN or run `gh auth login`.');
52
+ if (res.statusCode === 401 || res.statusCode === 403) throw new Error('GitHub rejected the token (needs read access to this repo).');
53
+ if (res.statusCode >= 400) throw new Error(`GitHub API returned HTTP ${res.statusCode}`);
54
+ return JSON.parse(body);
55
+ }
56
+
57
+ function download(url, tok, dest, size) {
58
+ return new Promise(async (resolve, reject) => {
59
+ const res = await get(url, tok, 'application/octet-stream');
60
+ if (res.statusCode >= 400) return reject(new Error(`download failed: HTTP ${res.statusCode}`));
61
+ const out = fs.createWriteStream(dest);
62
+ let done = 0; let lastPct = -1;
63
+ res.on('data', c => {
64
+ done += c.length;
65
+ if (size) {
66
+ const pct = Math.floor((done / size) * 100);
67
+ if (pct !== lastPct && pct % 5 === 0) { lastPct = pct; process.stdout.write(`\r downloading… ${pct}%`); }
68
+ }
69
+ });
70
+ res.pipe(out);
71
+ out.on('finish', () => { process.stdout.write('\r downloading… 100%\n'); out.close(() => resolve(dest)); });
72
+ out.on('error', reject);
73
+ res.on('error', reject);
74
+ });
75
+ }
76
+
77
+ (async () => {
78
+ const tok = token();
79
+ if (!tok) console.warn('! No GitHub token found (GH_TOKEN or `gh auth login`) — this will fail while the repo is private.\n');
80
+
81
+ console.log(`Looking up the latest release of ${REPO}…`);
82
+ const rel = await json(`https://api.github.com/repos/${REPO}/releases/latest`, tok);
83
+ const version = String(rel.tag_name || '').replace(/^v/, '');
84
+ const asset = (rel.assets || []).find(a => a.name.endsWith('.exe'));
85
+ if (!asset) throw new Error(`Release ${rel.tag_name} has no .exe asset.`);
86
+
87
+ let installed = null;
88
+ try { installed = require('../package.json').version; } catch {}
89
+ console.log(` latest: ${version}${installed ? ` (this checkout: ${installed})` : ''}`);
90
+ if (rel.body) console.log(`\n${String(rel.body).trim().split('\n').slice(0, 12).join('\n')}\n`);
91
+
92
+ const dest = path.join(os.tmpdir(), asset.name);
93
+ console.log(`Downloading ${asset.name} (${(asset.size / 1048576).toFixed(1)} MB)…`);
94
+ await download(asset.url, tok, dest, asset.size); // .url (not browser_download_url) works for private repos
95
+
96
+ // Never hand a non-executable to spawn(): the asset endpoint returns JSON metadata unless the
97
+ // request asks for octet-stream, and that failure is otherwise silent.
98
+ const stat = fs.statSync(dest);
99
+ const head = Buffer.alloc(2);
100
+ const fd = fs.openSync(dest, 'r'); fs.readSync(fd, head, 0, 2, 0); fs.closeSync(fd);
101
+ if (head.toString('latin1') !== 'MZ') throw new Error(`downloaded file is not a Windows executable (${stat.size} bytes)`);
102
+ if (asset.size && stat.size !== asset.size) throw new Error(`size mismatch: expected ${asset.size} bytes, got ${stat.size}`);
103
+ console.log(` verified: ${(stat.size / 1048576).toFixed(1)} MB Windows executable`);
104
+
105
+ if (downloadOnly) { console.log(`\nSaved to ${dest}`); return; }
106
+
107
+ console.log(`\nLaunching the installer${silent ? ' (silent)' : ''}…`);
108
+ const child = spawn(dest, silent ? ['/S'] : [], { detached: true, stdio: 'ignore', shell: false });
109
+ child.unref();
110
+ console.log('The installer is running. It is unsigned, so SmartScreen may ask you to confirm.');
111
+ })().catch(err => {
112
+ console.error(`\nFailed: ${err.message}`);
113
+ process.exit(1);
114
+ });