my-provider-dash-manager 0.2.8 → 0.3.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 CHANGED
@@ -31,12 +31,23 @@ panel lets you store a GitHub token, encrypted with Windows DPAPI, so the feed s
31
31
  ### Install from the command line
32
32
 
33
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
34
+ npx my-provider-dash-manager # install (or update) and launch
37
35
  ```
38
36
 
39
- From a checkout of this repository, the same thing is `npm run install:latest`.
37
+ The same CLI manages the whole lifecycle — install it globally with
38
+ `npm i -g my-provider-dash-manager` and the commands are also available as `mpdm`:
39
+
40
+ | Command | What it does |
41
+ |---|---|
42
+ | `my-provider-dash-manager` | Install or update to the newest release, then launch |
43
+ | `… install` / `… update` | Install or update only |
44
+ | `… start` | Launch the installed app |
45
+ | `… status` | Installed version vs latest, plus where the app and your data live |
46
+ | `… where` | Print the install and data paths |
47
+ | `… uninstall` | Run the uninstaller (your accounts and sessions are kept) |
48
+
49
+ Flags: `--silent` (no wizard), `--download-only`, `--force`, `--version`, `--help`.
50
+ From a checkout of this repository the equivalent is `npm run install:latest`.
40
51
 
41
52
  It prints the latest version and its release notes and verifies the download really is a Windows
42
53
  executable before running it. No authentication is needed; a `GH_TOKEN` is used only if present.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-provider-dash-manager",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
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
5
  "main": "src/main.js",
6
6
  "scripts": {
@@ -50,9 +50,11 @@
50
50
  },
51
51
  "license": "SEE LICENSE IN LICENSE",
52
52
  "bin": {
53
- "my-provider-dash-manager": "scripts/install-latest.js"
53
+ "my-provider-dash-manager": "scripts/cli.js",
54
+ "mpdm": "scripts/cli.js"
54
55
  },
55
56
  "files": [
57
+ "scripts/cli.js",
56
58
  "scripts/install-latest.js",
57
59
  "README.md",
58
60
  "LICENSE"
package/scripts/cli.js ADDED
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ // Command line for My Provider Dash Manager.
3
+ //
4
+ // npx my-provider-dash-manager install (or update) and launch the app
5
+ // npx my-provider-dash-manager install install/update only, do not launch
6
+ // npx my-provider-dash-manager update alias of install — always fetches the newest release
7
+ // npx my-provider-dash-manager start launch the installed app
8
+ // npx my-provider-dash-manager status what is installed, what is available, where data lives
9
+ // npx my-provider-dash-manager uninstall run the app's uninstaller
10
+ // npx my-provider-dash-manager where print the install and data paths
11
+ //
12
+ // Flags: --silent (no wizard) · --download-only · --version · --help
13
+ const fs = require('fs');
14
+ const os = require('os');
15
+ const path = require('path');
16
+ const { spawn, execFileSync } = require('child_process');
17
+ const { latestRelease, downloadAsset } = require('./install-latest.js');
18
+
19
+ const PRODUCT = 'My Provider Dash Manager';
20
+ const DATA_DIR = path.join(process.env.APPDATA || os.homedir(), 'multi-acc-manager');
21
+ const args = process.argv.slice(2);
22
+ const flags = new Set(args.filter(a => a.startsWith('-')));
23
+ const cmd = (args.find(a => !a.startsWith('-')) || 'default').toLowerCase();
24
+
25
+ function pkgVersion() {
26
+ try { return require('../package.json').version; } catch { return 'unknown'; }
27
+ }
28
+
29
+ // Where NSIS puts a per-user install, plus the usual per-machine fallbacks.
30
+ function installedExe() {
31
+ const candidates = [
32
+ path.join(process.env.LOCALAPPDATA || '', 'Programs', PRODUCT, `${PRODUCT}.exe`),
33
+ path.join(process.env.PROGRAMFILES || '', PRODUCT, `${PRODUCT}.exe`),
34
+ path.join(process.env['PROGRAMFILES(X86)'] || '', PRODUCT, `${PRODUCT}.exe`),
35
+ ];
36
+ return candidates.find(p => p && fs.existsSync(p)) || null;
37
+ }
38
+
39
+ function installedUninstaller() {
40
+ const exe = installedExe();
41
+ if (!exe) return null;
42
+ const dir = path.dirname(exe);
43
+ const u = fs.readdirSync(dir).find(f => /^Uninstall .*\.exe$/i.test(f));
44
+ return u ? path.join(dir, u) : null;
45
+ }
46
+
47
+ // Windows reports a four-part ProductVersion (0.2.3.0); releases are three-part (0.2.3).
48
+ function normaliseVersion(v) {
49
+ if (!v) return null;
50
+ const parts = String(v).trim().split('.').slice(0, 3);
51
+ while (parts.length < 3) parts.push('0');
52
+ return parts.join('.');
53
+ }
54
+
55
+ function installedVersion() {
56
+ const exe = installedExe();
57
+ if (!exe) return null;
58
+ try {
59
+ const out = execFileSync('powershell.exe', ['-NoProfile', '-Command',
60
+ `(Get-Item -LiteralPath '${exe.replace(/'/g, "''")}').VersionInfo.ProductVersion`],
61
+ { encoding: 'utf8', timeout: 15000 });
62
+ return normaliseVersion(out.trim()) || null;
63
+ } catch { return null; }
64
+ }
65
+
66
+ function launch() {
67
+ const exe = installedExe();
68
+ if (!exe) throw new Error('The app is not installed yet — run: npx my-provider-dash-manager install');
69
+ spawn(exe, [], { detached: true, stdio: 'ignore' }).unref();
70
+ console.log(`Launched ${PRODUCT}.`);
71
+ }
72
+
73
+ async function install({ launchAfter }) {
74
+ const rel = await latestRelease();
75
+ const have = installedVersion();
76
+ if (have === rel.version && !flags.has('--force')) {
77
+ console.log(`Already on the latest version (${have}).`);
78
+ if (launchAfter) launch();
79
+ return;
80
+ }
81
+ console.log(have ? `Installed: ${have} → updating to ${rel.version}` : `Installing ${rel.version}`);
82
+ if (rel.notes) console.log(`\n${rel.notes.split('\n').slice(0, 10).join('\n')}\n`);
83
+ const dest = await downloadAsset(rel);
84
+ if (flags.has('--download-only')) { console.log(`\nSaved to ${dest}`); return; }
85
+ console.log(`\nRunning the installer${flags.has('--silent') ? ' (silent)' : ''}…`);
86
+ const child = spawn(dest, flags.has('--silent') ? ['/S'] : [], { detached: true, stdio: 'ignore' });
87
+ child.unref();
88
+ console.log('The installer is unsigned, so SmartScreen may ask you to confirm.');
89
+ if (launchAfter && flags.has('--silent')) {
90
+ setTimeout(() => { try { launch(); } catch (e) { console.error(e.message); } }, 15000);
91
+ }
92
+ }
93
+
94
+ async function status() {
95
+ const have = installedVersion();
96
+ const exe = installedExe();
97
+ let latest = null;
98
+ try { latest = (await latestRelease()).version; } catch (e) { latest = `unavailable (${e.message})`; }
99
+ console.log(`${PRODUCT}`);
100
+ console.log(` installed: ${have || 'not installed'}`);
101
+ console.log(` latest: ${latest}`);
102
+ console.log(` cli: ${pkgVersion()}`);
103
+ console.log(` app: ${exe || '—'}`);
104
+ console.log(` data: ${fs.existsSync(DATA_DIR) ? DATA_DIR : DATA_DIR + ' (not created yet)'}`);
105
+ if (have && typeof latest === 'string' && have !== latest && !latest.startsWith('unavailable')) {
106
+ console.log(`\nAn update is available — run: npx my-provider-dash-manager update`);
107
+ }
108
+ }
109
+
110
+ function where() {
111
+ console.log(`app: ${installedExe() || 'not installed'}`);
112
+ console.log(`data: ${DATA_DIR}`);
113
+ }
114
+
115
+ function uninstall() {
116
+ const u = installedUninstaller();
117
+ if (!u) throw new Error('No uninstaller found — the app does not appear to be installed.');
118
+ console.log('Starting the uninstaller. Your accounts and sessions in the data folder are NOT removed;');
119
+ console.log(`delete ${DATA_DIR} yourself if you want them gone.`);
120
+ spawn(u, [], { detached: true, stdio: 'ignore' }).unref();
121
+ }
122
+
123
+ function help() {
124
+ console.log(`${PRODUCT} — CLI ${pkgVersion()}
125
+
126
+ npx my-provider-dash-manager install (or update) and launch
127
+ npx my-provider-dash-manager install install/update only
128
+ npx my-provider-dash-manager update always fetch the newest release
129
+ npx my-provider-dash-manager start launch the installed app
130
+ npx my-provider-dash-manager status installed vs latest version, paths
131
+ npx my-provider-dash-manager where print install and data paths
132
+ npx my-provider-dash-manager uninstall run the uninstaller (keeps your data)
133
+
134
+ Flags: --silent --download-only --force --version --help
135
+ Windows only. https://github.com/FiredMosquito831/my-provider-dash-manager`);
136
+ }
137
+
138
+ (async () => {
139
+ if (flags.has('--help') || flags.has('-h') || cmd === 'help') return help();
140
+ if (flags.has('--version') || flags.has('-v') || cmd === 'version') return console.log(pkgVersion());
141
+ if (process.platform !== 'win32' && cmd !== 'status') {
142
+ throw new Error('This app is Windows-only.');
143
+ }
144
+ switch (cmd) {
145
+ case 'default': return install({ launchAfter: true });
146
+ case 'install': case 'update': case 'upgrade': return install({ launchAfter: false });
147
+ case 'start': case 'run': case 'launch': return launch();
148
+ case 'status': return status();
149
+ case 'where': case 'paths': return where();
150
+ case 'uninstall': case 'remove': return uninstall();
151
+ default: console.error(`Unknown command: ${cmd}\n`); return help();
152
+ }
153
+ })().catch(err => {
154
+ console.error(`\nFailed: ${err.message}`);
155
+ process.exit(1);
156
+ });
@@ -1,12 +1,9 @@
1
1
  #!/usr/bin/env node
2
- // Fetch the latest published release and run its installer.
2
+ // Fetches the latest published release and runs its installer.
3
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.
4
+ // Used directly (`npm run install:latest`) and as the engine behind scripts/cli.js.
5
+ // The repository is public, so no authentication is needed; GH_TOKEN is used only if present
6
+ // (which keeps this working if the repo is ever made private again).
10
7
  const fs = require('fs');
11
8
  const os = require('os');
12
9
  const path = require('path');
@@ -14,28 +11,25 @@ const https = require('https');
14
11
  const { execFileSync, spawn } = require('child_process');
15
12
 
16
13
  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
14
 
21
15
  function token() {
22
16
  if (process.env.GH_TOKEN) return process.env.GH_TOKEN;
23
17
  if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
24
- try { return execFileSync('gh', ['auth', 'token'], { encoding: 'utf8' }).trim(); } catch { return null; }
18
+ try { return execFileSync('gh', ['auth', 'token'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); }
19
+ catch { return null; }
25
20
  }
26
21
 
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.
22
+ // accept: JSON for API calls, octet-stream for asset bytes. Without the octet-stream Accept the
23
+ // asset endpoint returns metadata instead of the file — a silent failure that yields a 1.7 KB "exe".
29
24
  function get(url, tok, accept = 'application/vnd.github+json', redirects = 0) {
30
25
  return new Promise((resolve, reject) => {
31
- const headers = { 'User-Agent': 'my-provider-dash-manager-installer', Accept: accept };
26
+ const headers = { 'User-Agent': 'my-provider-dash-manager', Accept: accept };
32
27
  if (tok) headers.Authorization = `Bearer ${tok}`;
33
28
  https.get(url, { headers }, res => {
34
29
  if ([301, 302, 307, 308].includes(res.statusCode) && res.headers.location) {
35
30
  if (redirects > 5) return reject(new Error('too many redirects'));
36
31
  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));
32
+ return resolve(get(res.headers.location, null, accept, redirects + 1)); // S3 rejects our auth header
39
33
  }
40
34
  resolve(res);
41
35
  }).on('error', reject);
@@ -48,67 +42,79 @@ async function json(url, tok) {
48
42
  let d = ''; res.setEncoding('utf8');
49
43
  res.on('data', c => { d += c; }); res.on('end', () => resolve(d)); res.on('error', reject);
50
44
  });
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).');
45
+ if (res.statusCode === 404) throw new Error('No published release found for this repository.');
46
+ if (res.statusCode === 401 || res.statusCode === 403) throw new Error('GitHub rejected the request (rate limit or token).');
53
47
  if (res.statusCode >= 400) throw new Error(`GitHub API returned HTTP ${res.statusCode}`);
54
48
  return JSON.parse(body);
55
49
  }
56
50
 
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/, '');
51
+ /** The newest release: { version, notes, asset, url }. */
52
+ async function latestRelease() {
53
+ const rel = await json(`https://api.github.com/repos/${REPO}/releases/latest`, token());
84
54
  const asset = (rel.assets || []).find(a => a.name.endsWith('.exe'));
85
55
  if (!asset) throw new Error(`Release ${rel.tag_name} has no .exe asset.`);
56
+ return {
57
+ version: String(rel.tag_name || '').replace(/^v/, ''),
58
+ notes: rel.body ? String(rel.body).trim() : '',
59
+ url: rel.html_url,
60
+ asset,
61
+ };
62
+ }
86
63
 
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`);
64
+ /** Downloads the release asset to temp and verifies it really is a Windows executable. */
65
+ function downloadAsset(rel, onProgress) {
66
+ const dest = path.join(os.tmpdir(), rel.asset.name);
67
+ return new Promise(async (resolve, reject) => {
68
+ try {
69
+ const res = await get(rel.asset.url, token(), 'application/octet-stream');
70
+ if (res.statusCode >= 400) return reject(new Error(`download failed: HTTP ${res.statusCode}`));
71
+ const out = fs.createWriteStream(dest);
72
+ let done = 0; let lastPct = -1;
73
+ res.on('data', c => {
74
+ done += c.length;
75
+ const pct = Math.floor((done / rel.asset.size) * 100);
76
+ if (pct !== lastPct && pct % 5 === 0) {
77
+ lastPct = pct;
78
+ if (onProgress) onProgress(pct);
79
+ else process.stdout.write(`\r downloading… ${pct}%`);
80
+ }
81
+ });
82
+ res.pipe(out);
83
+ res.on('error', reject);
84
+ out.on('error', reject);
85
+ out.on('finish', () => out.close(() => {
86
+ if (!onProgress) process.stdout.write('\r downloading… 100%\n');
87
+ try {
88
+ const stat = fs.statSync(dest);
89
+ const head = Buffer.alloc(2);
90
+ const fd = fs.openSync(dest, 'r'); fs.readSync(fd, head, 0, 2, 0); fs.closeSync(fd);
91
+ if (head.toString('latin1') !== 'MZ') throw new Error(`downloaded file is not a Windows executable (${stat.size} bytes)`);
92
+ if (rel.asset.size && stat.size !== rel.asset.size) throw new Error(`size mismatch: expected ${rel.asset.size}, got ${stat.size}`);
93
+ } catch (e) { return reject(e); }
94
+ resolve(dest);
95
+ }));
96
+ } catch (e) { reject(e); }
97
+ });
98
+ }
104
99
 
105
- if (downloadOnly) { console.log(`\nSaved to ${dest}`); return; }
100
+ module.exports = { latestRelease, downloadAsset, REPO };
106
101
 
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
- });
102
+ // Standalone use: download and run, honouring --silent / --download-only.
103
+ if (require.main === module) {
104
+ const args = process.argv.slice(2);
105
+ (async () => {
106
+ console.log(`Looking up the latest release of ${REPO}…`);
107
+ const rel = await latestRelease();
108
+ let installed = null;
109
+ try { installed = require('../package.json').version; } catch {}
110
+ console.log(` latest: ${rel.version}${installed ? ` (this checkout: ${installed})` : ''}`);
111
+ if (rel.notes) console.log(`\n${rel.notes.split('\n').slice(0, 12).join('\n')}\n`);
112
+ console.log(`Downloading ${rel.asset.name} (${(rel.asset.size / 1048576).toFixed(1)} MB)…`);
113
+ const dest = await downloadAsset(rel);
114
+ console.log(` verified: ${(fs.statSync(dest).size / 1048576).toFixed(1)} MB Windows executable`);
115
+ if (args.includes('--download-only')) return console.log(`\nSaved to ${dest}`);
116
+ console.log(`\nLaunching the installer${args.includes('--silent') ? ' (silent)' : ''}…`);
117
+ spawn(dest, args.includes('--silent') ? ['/S'] : [], { detached: true, stdio: 'ignore' }).unref();
118
+ console.log('The installer is unsigned, so SmartScreen may ask you to confirm.');
119
+ })().catch(err => { console.error(`\nFailed: ${err.message}`); process.exit(1); });
120
+ }