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 +63 -0
- package/README.md +228 -0
- package/package.json +93 -0
- package/scripts/install-latest.js +114 -0
- package/src/main.js +1259 -0
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+
});
|