@woopsy/mcpanel 1.0.1 → 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 +20 -0
- package/dist/commands/commandRouter.js +49 -0
- package/dist/index.js +34 -2
- package/dist/managers/playitManager.js +124 -50
- package/dist/services/updateChecker.js +140 -0
- package/dist/utils/helpers.js +65 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -144,6 +144,26 @@ npm run dev # run from TypeScript (ts-node)
|
|
|
144
144
|
npm run build # compile to dist/
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
### Releasing (maintainers)
|
|
148
|
+
|
|
149
|
+
Publishing is automated by GitHub Actions (`.github/workflows/publish.yml`). You never
|
|
150
|
+
run `npm publish` by hand.
|
|
151
|
+
|
|
152
|
+
**One-time setup:**
|
|
153
|
+
1. Create an npm **Automation** token: npmjs.com → Access Tokens → Generate New Token →
|
|
154
|
+
*Automation* (these bypass 2FA in CI).
|
|
155
|
+
2. Add it to the repo: GitHub → Settings → Secrets and variables → Actions → New repository
|
|
156
|
+
secret → name `NPM_TOKEN`.
|
|
157
|
+
|
|
158
|
+
**Every release after that:**
|
|
159
|
+
```bash
|
|
160
|
+
npm version patch # or minor / major — bumps package.json and creates a git tag
|
|
161
|
+
git push --follow-tags # pushes the commit + tag; CI builds and publishes to npm
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The workflow skips automatically if that version is already on npm, and `npm install -g @woopsy/mcpanel@latest`
|
|
165
|
+
picks up the new version once it's green.
|
|
166
|
+
|
|
147
167
|
---
|
|
148
168
|
|
|
149
169
|
## License
|
|
@@ -42,6 +42,7 @@ const path = __importStar(require("path"));
|
|
|
42
42
|
const colors = __importStar(require("../utils/colors"));
|
|
43
43
|
const helpers_1 = require("../utils/helpers");
|
|
44
44
|
const downloadService_1 = require("../services/downloadService");
|
|
45
|
+
const updateChecker_1 = require("../services/updateChecker");
|
|
45
46
|
const pidusage_1 = __importDefault(require("pidusage"));
|
|
46
47
|
class CommandRouter {
|
|
47
48
|
configManager;
|
|
@@ -74,6 +75,7 @@ class CommandRouter {
|
|
|
74
75
|
' /properties - Edit server.properties interactively',
|
|
75
76
|
'',
|
|
76
77
|
colors.bold(colors.green('Tunnel Commands (Playit.gg)')),
|
|
78
|
+
' /setup - One-time Playit account claim (browser approval)',
|
|
77
79
|
' /tunnel java - Auto-create & start a Java tunnel, returns address',
|
|
78
80
|
' /tunnel bedrock - Auto-create & start a Bedrock tunnel, returns address',
|
|
79
81
|
' /tunnel status - Check tunnel status, address and latency',
|
|
@@ -95,6 +97,7 @@ class CommandRouter {
|
|
|
95
97
|
' /java [path] - Show/list Java runtimes, or set the one used to launch',
|
|
96
98
|
' /folder - Open the server folder in the file explorer',
|
|
97
99
|
' /clear - Clear the screen, scrollback and command history',
|
|
100
|
+
' /update - Check npm for a newer version of MCPANEL',
|
|
98
101
|
' /config - View active application config.json',
|
|
99
102
|
' /exit - Close MCPANEL server manager',
|
|
100
103
|
colors.gray('──────────────────────────────────────────────\n')
|
|
@@ -464,6 +467,22 @@ class CommandRouter {
|
|
|
464
467
|
this.configManager.updateSettings({ defaultJavaPath: cleanPath });
|
|
465
468
|
return colors.success(`Java set to "${cleanPath}" (version ${info.version}). It will be used on the next /start.`);
|
|
466
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* Executes /update — checks npm for a newer version and prints how to update.
|
|
472
|
+
*/
|
|
473
|
+
async executeUpdate() {
|
|
474
|
+
const info = await (0, updateChecker_1.checkForUpdate)(true);
|
|
475
|
+
if (!info) {
|
|
476
|
+
return colors.warning('Could not check for updates (no network connection?).');
|
|
477
|
+
}
|
|
478
|
+
if (info.updateAvailable) {
|
|
479
|
+
return [
|
|
480
|
+
colors.warning(`Update available: ${colors.bold(info.current)} → ${colors.bold(colors.green(info.latest))}`),
|
|
481
|
+
colors.gray('Update with: ') + colors.cyan(`npm i -g ${info.name}@latest`),
|
|
482
|
+
].join('\n');
|
|
483
|
+
}
|
|
484
|
+
return colors.success(`You're on the latest version (${info.current}).`);
|
|
485
|
+
}
|
|
467
486
|
/**
|
|
468
487
|
* Executes /config
|
|
469
488
|
*/
|
|
@@ -514,6 +533,36 @@ class CommandRouter {
|
|
|
514
533
|
return colors.failure(`Failed to create tunnel: ${err.message}`);
|
|
515
534
|
}
|
|
516
535
|
}
|
|
536
|
+
/**
|
|
537
|
+
* Executes /setup — runs the one-time Playit agent claim (browser approval),
|
|
538
|
+
* saving the agent secret. Works on all platforms (HTTP claim, no playit-cli).
|
|
539
|
+
* After this, /tunnel java|bedrock is fully automatic.
|
|
540
|
+
*/
|
|
541
|
+
async executeSetup() {
|
|
542
|
+
if (this.playitManager.getSecret()) {
|
|
543
|
+
return colors.warning('Playit is already set up (agent secret saved). Run /tunnel reset first if you want to re-claim.');
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
await this.playitManager.ensureSecret({
|
|
547
|
+
onClaimUrl: (url) => {
|
|
548
|
+
const opened = (0, helpers_1.openInBrowser)(url);
|
|
549
|
+
console.log(`\n🔗 ${colors.bold('Approve the agent in your browser to finish setup.')}`);
|
|
550
|
+
if (opened) {
|
|
551
|
+
console.log(colors.gray('Your browser was opened automatically — sign in and click Approve.'));
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
console.log(colors.gray('Open this link, sign in (free account), and click Approve:'));
|
|
555
|
+
}
|
|
556
|
+
console.log(colors.underline(colors.cyan(url)));
|
|
557
|
+
},
|
|
558
|
+
onStatus: (msg) => console.log(colors.info(msg)),
|
|
559
|
+
});
|
|
560
|
+
return colors.success('Playit is set up! You can now run /tunnel java or /tunnel bedrock.');
|
|
561
|
+
}
|
|
562
|
+
catch (err) {
|
|
563
|
+
return colors.failure(`Setup failed: ${err.message}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
517
566
|
/**
|
|
518
567
|
* Executes /tunnel reset — clears the saved secret so the agent can be re-claimed.
|
|
519
568
|
*/
|
package/dist/index.js
CHANGED
|
@@ -50,6 +50,7 @@ const playitManager_1 = require("./managers/playitManager");
|
|
|
50
50
|
const commandRouter_1 = require("./commands/commandRouter");
|
|
51
51
|
const colors = __importStar(require("./utils/colors"));
|
|
52
52
|
const helpers_1 = require("./utils/helpers");
|
|
53
|
+
const updateChecker_1 = require("./services/updateChecker");
|
|
53
54
|
const logger_1 = require("./utils/logger");
|
|
54
55
|
// Initialize managers
|
|
55
56
|
const configManager = new configManager_1.ConfigManager();
|
|
@@ -70,6 +71,12 @@ let consoleActiveServer = '';
|
|
|
70
71
|
let logViewServer = '';
|
|
71
72
|
// Readline interface
|
|
72
73
|
let rl;
|
|
74
|
+
let CLI_VERSION = '1.0.3';
|
|
75
|
+
try {
|
|
76
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(configManager_1.APP_ROOT, 'package.json'), 'utf-8'));
|
|
77
|
+
CLI_VERSION = pkg.version || '1.0.3';
|
|
78
|
+
}
|
|
79
|
+
catch { /* ignore */ }
|
|
73
80
|
/**
|
|
74
81
|
* Renders the figlet "MCPANEL" ASCII banner with a chalk gradient.
|
|
75
82
|
*/
|
|
@@ -79,7 +86,7 @@ function renderBanner() {
|
|
|
79
86
|
const tints = [chalk_1.default.cyanBright, chalk_1.default.cyan, chalk_1.default.greenBright, chalk_1.default.green, chalk_1.default.green];
|
|
80
87
|
console.log();
|
|
81
88
|
lines.forEach((line, i) => console.log((tints[i] || chalk_1.default.green)(line)));
|
|
82
|
-
console.log(chalk_1.default.greenBright.bold(' Minecraft Server Manager') + chalk_1.default.gray(
|
|
89
|
+
console.log(chalk_1.default.greenBright.bold(' Minecraft Server Manager') + chalk_1.default.gray(` v${CLI_VERSION}`));
|
|
83
90
|
}
|
|
84
91
|
/**
|
|
85
92
|
* Renders the neofetch / Arch-Linux-style info block for the synced server.
|
|
@@ -159,8 +166,9 @@ const COMMAND_LIST = [
|
|
|
159
166
|
'/stats', '/folder', '/properties', '/java',
|
|
160
167
|
'/backup create', '/backup list', '/backup restore',
|
|
161
168
|
'/plugins list', '/plugins install', '/plugins remove',
|
|
169
|
+
'/setup',
|
|
162
170
|
'/tunnel java', '/tunnel bedrock', '/tunnel status', '/tunnel stop', '/tunnel reset',
|
|
163
|
-
'/config', '/clear', '/exit'
|
|
171
|
+
'/config', '/clear', '/update', '/exit'
|
|
164
172
|
];
|
|
165
173
|
// Subcommands offered once "<command> " has been typed.
|
|
166
174
|
const SUBCOMMANDS = {
|
|
@@ -608,6 +616,9 @@ async function handleCommandState(line) {
|
|
|
608
616
|
case '/java':
|
|
609
617
|
console.log(router.executeJava(args.length ? args.join(' ') : undefined));
|
|
610
618
|
break;
|
|
619
|
+
case '/update':
|
|
620
|
+
console.log(await router.executeUpdate());
|
|
621
|
+
break;
|
|
611
622
|
case '/config':
|
|
612
623
|
console.log(router.executeConfig());
|
|
613
624
|
break;
|
|
@@ -654,6 +665,9 @@ async function handleCommandState(line) {
|
|
|
654
665
|
console.log(colors.failure('Syntax: /plugins [list|install|remove]'));
|
|
655
666
|
}
|
|
656
667
|
break;
|
|
668
|
+
case '/setup':
|
|
669
|
+
console.log(await router.executeSetup());
|
|
670
|
+
break;
|
|
657
671
|
case '/tunnel': {
|
|
658
672
|
const sub = (args[0] || '').toLowerCase();
|
|
659
673
|
if (!sub) {
|
|
@@ -716,11 +730,29 @@ async function finishStartup() {
|
|
|
716
730
|
currentState = 'COMMAND';
|
|
717
731
|
promptUser();
|
|
718
732
|
}
|
|
733
|
+
/**
|
|
734
|
+
* Prints a one-time-per-launch notice if a newer version is on npm. Fail-silent
|
|
735
|
+
* and cached, so it never slows down or blocks startup.
|
|
736
|
+
*/
|
|
737
|
+
async function showUpdateNotice() {
|
|
738
|
+
try {
|
|
739
|
+
const info = await (0, updateChecker_1.checkForUpdate)();
|
|
740
|
+
if (info && info.updateAvailable) {
|
|
741
|
+
console.log();
|
|
742
|
+
console.log(chalk_1.default.yellow(' ⚡ Update available: ') + chalk_1.default.gray(info.current) + chalk_1.default.gray(' → ') + chalk_1.default.greenBright.bold(info.latest));
|
|
743
|
+
console.log(chalk_1.default.gray(' Update with: ') + chalk_1.default.cyan(`npm i -g ${info.name}@latest`));
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
// Never let an update check break startup.
|
|
748
|
+
}
|
|
749
|
+
}
|
|
719
750
|
/**
|
|
720
751
|
* Main application setup
|
|
721
752
|
*/
|
|
722
753
|
async function main() {
|
|
723
754
|
renderBanner();
|
|
755
|
+
await showUpdateNotice();
|
|
724
756
|
rl = readline.createInterface({
|
|
725
757
|
input: process.stdin,
|
|
726
758
|
output: process.stdout,
|
|
@@ -37,6 +37,7 @@ exports.PlayitManager = void 0;
|
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const https = __importStar(require("https"));
|
|
40
|
+
const crypto = __importStar(require("crypto"));
|
|
40
41
|
const child_process_1 = require("child_process");
|
|
41
42
|
const configManager_1 = require("../config/configManager");
|
|
42
43
|
const helpers_1 = require("../utils/helpers");
|
|
@@ -209,6 +210,41 @@ class PlayitManager {
|
|
|
209
210
|
getRunData(secret) {
|
|
210
211
|
return this.apiPost('/agents/rundata', {}, secret);
|
|
211
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Unauthenticated POST for the claim endpoints. Unlike apiPost it returns the
|
|
215
|
+
* raw { status, data } envelope and does NOT throw on `status:"fail"` — a fail
|
|
216
|
+
* (e.g. "CodeNotFound" while waiting for approval) is a normal polling state.
|
|
217
|
+
*/
|
|
218
|
+
apiClaim(apiPath, body) {
|
|
219
|
+
const payload = JSON.stringify(body || {});
|
|
220
|
+
const url = new URL(PLAYIT_API + apiPath);
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
const req = https.request({
|
|
223
|
+
hostname: url.hostname,
|
|
224
|
+
path: url.pathname,
|
|
225
|
+
method: 'POST',
|
|
226
|
+
headers: {
|
|
227
|
+
'Content-Type': 'application/json',
|
|
228
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
229
|
+
},
|
|
230
|
+
}, (res) => {
|
|
231
|
+
let data = '';
|
|
232
|
+
res.on('data', (c) => { data += c; });
|
|
233
|
+
res.on('end', () => {
|
|
234
|
+
try {
|
|
235
|
+
const parsed = JSON.parse(data);
|
|
236
|
+
resolve({ status: parsed.status, data: parsed.data });
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
reject(new Error(`Bad claim response: ${data.slice(0, 200)}`));
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
req.on('error', reject);
|
|
244
|
+
req.write(payload);
|
|
245
|
+
req.end();
|
|
246
|
+
});
|
|
247
|
+
}
|
|
212
248
|
/** Picks the public address from a rundata tunnel entry. */
|
|
213
249
|
tunnelAddress(tunnel) {
|
|
214
250
|
const port = tunnel?.port?.from ?? tunnel?.local_port ?? 0;
|
|
@@ -262,81 +298,92 @@ class PlayitManager {
|
|
|
262
298
|
});
|
|
263
299
|
});
|
|
264
300
|
}
|
|
265
|
-
/**
|
|
301
|
+
/**
|
|
302
|
+
* Ensures a write-capable agent secret, driving the one-time claim if needed.
|
|
303
|
+
*
|
|
304
|
+
* The claim is done entirely over the playit HTTP API (no playit-cli), so it
|
|
305
|
+
* works identically on Windows, WSL, Linux and macOS. The agent binary is only
|
|
306
|
+
* needed later, for the traffic relay.
|
|
307
|
+
*/
|
|
266
308
|
async ensureSecret(callbacks = {}) {
|
|
267
309
|
const existing = this.getSecret();
|
|
268
310
|
if (existing)
|
|
269
311
|
return existing;
|
|
270
|
-
|
|
312
|
+
// A claim code is just a short random hex string generated client-side.
|
|
313
|
+
const code = crypto.randomBytes(5).toString('hex');
|
|
314
|
+
const claimBody = { code, agent_type: 'self-managed', version: 'mcpanel 1.0' };
|
|
271
315
|
callbacks.onStatus?.('Generating a new agent claim code...');
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
throw new Error('Failed to generate a claim code.');
|
|
275
|
-
const url = (await this.runCli(['claim', 'url', code, '--name', 'mcpanel', '--type', 'self-managed'])).trim();
|
|
316
|
+
await this.apiClaim('/claim/setup', claimBody);
|
|
317
|
+
const url = `https://playit.gg/claim/${code}`;
|
|
276
318
|
this.tunnelStatus.claimUrl = url;
|
|
277
319
|
callbacks.onClaimUrl?.(url);
|
|
278
|
-
|
|
279
|
-
|
|
320
|
+
// Poll setup (which also keeps the code alive) until the user approves.
|
|
321
|
+
callbacks.onStatus?.('Waiting for you to approve the agent in your browser (this only happens once)...');
|
|
322
|
+
const deadline = Date.now() + 5 * 60 * 1000; // 5 minutes
|
|
323
|
+
let approved = false;
|
|
324
|
+
while (Date.now() < deadline) {
|
|
325
|
+
await this.sleep(3000);
|
|
326
|
+
const res = await this.apiClaim('/claim/setup', claimBody);
|
|
327
|
+
const status = typeof res.data === 'string' ? res.data : '';
|
|
328
|
+
if (status === 'UserAccepted') {
|
|
329
|
+
approved = true;
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
if (status === 'UserRejected') {
|
|
333
|
+
this.tunnelStatus.claimUrl = null;
|
|
334
|
+
throw new Error('Claim was rejected in the browser. Run /setup to try again.');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (!approved) {
|
|
338
|
+
this.tunnelStatus.claimUrl = null;
|
|
339
|
+
throw new Error('Timed out waiting for approval. Open the link, click Approve, then run /setup again.');
|
|
340
|
+
}
|
|
341
|
+
// Exchange the approved code for the 64-char agent secret.
|
|
342
|
+
callbacks.onStatus?.('Approved! Retrieving your agent secret...');
|
|
343
|
+
let secret = '';
|
|
344
|
+
for (let i = 0; i < 10 && !secret; i++) {
|
|
345
|
+
const ex = await this.apiClaim('/claim/exchange', { code });
|
|
346
|
+
const match = JSON.stringify(ex.data ?? '').match(/[a-f0-9]{64}/i);
|
|
347
|
+
if (ex.status === 'success' && match)
|
|
348
|
+
secret = match[0].toLowerCase();
|
|
349
|
+
else
|
|
350
|
+
await this.sleep(2000);
|
|
351
|
+
}
|
|
352
|
+
if (!secret) {
|
|
353
|
+
this.tunnelStatus.claimUrl = null;
|
|
354
|
+
throw new Error('Could not retrieve the agent secret after approval. Run /setup to try again.');
|
|
355
|
+
}
|
|
280
356
|
this.configManager.setPlayitSecret(secret);
|
|
281
357
|
this.tunnelStatus.claimUrl = null;
|
|
282
358
|
callbacks.onStatus?.('Agent claimed and linked. Secret saved — future tunnels are fully automatic.');
|
|
283
359
|
return secret;
|
|
284
360
|
}
|
|
285
|
-
/** Spawns `claim exchange` and resolves once a 64-char hex secret is printed. */
|
|
286
|
-
exchangeClaim(code) {
|
|
287
|
-
return new Promise((resolve, reject) => {
|
|
288
|
-
const cliPath = this.getCliPath();
|
|
289
|
-
const child = (0, child_process_1.spawn)(cliPath, ['claim', 'exchange', code, '--wait', '0'], {
|
|
290
|
-
cwd: path.dirname(cliPath),
|
|
291
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
292
|
-
});
|
|
293
|
-
this.claimProcess = child;
|
|
294
|
-
let buffer = '';
|
|
295
|
-
const scan = (data) => {
|
|
296
|
-
buffer += stripAnsi(data.toString());
|
|
297
|
-
const match = buffer.match(/[a-f0-9]{64}/i);
|
|
298
|
-
if (match) {
|
|
299
|
-
this.claimProcess = null;
|
|
300
|
-
try {
|
|
301
|
-
child.kill();
|
|
302
|
-
}
|
|
303
|
-
catch { /* ignore */ }
|
|
304
|
-
resolve(match[0].toLowerCase());
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
child.stdout?.on('data', scan);
|
|
308
|
-
child.stderr?.on('data', scan);
|
|
309
|
-
child.on('close', (codeExit) => {
|
|
310
|
-
if (this.claimProcess === child) {
|
|
311
|
-
this.claimProcess = null;
|
|
312
|
-
const match = buffer.match(/[a-f0-9]{64}/i);
|
|
313
|
-
if (match)
|
|
314
|
-
resolve(match[0].toLowerCase());
|
|
315
|
-
else
|
|
316
|
-
reject(new Error(`Claim was not completed (exit ${codeExit}). Visit the link, then try again.`));
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
child.on('error', (err) => { this.claimProcess = null; reject(err); });
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
361
|
// ---------------------------------------------------------------------------
|
|
323
362
|
// Full automated setup
|
|
324
363
|
// ---------------------------------------------------------------------------
|
|
325
364
|
/**
|
|
326
|
-
* One-call entry point: ensures binary + secret,
|
|
327
|
-
*
|
|
365
|
+
* One-call entry point: ensures binary + secret, starts the relay daemon (so
|
|
366
|
+
* the agent registers its version with playit), creates the tunnel via the
|
|
367
|
+
* API, and returns the live tunnel status.
|
|
368
|
+
*
|
|
369
|
+
* The relay MUST be started before creating a tunnel — playit rejects
|
|
370
|
+
* /tunnels/create with "AgentVersionTooOld" until a current agent has
|
|
371
|
+
* connected and reported its version.
|
|
328
372
|
*/
|
|
329
373
|
async setupAndStart(type, callbacks = {}) {
|
|
330
374
|
await this.ensureBinary();
|
|
331
375
|
const secret = await this.ensureSecret(callbacks);
|
|
332
376
|
this.tunnelStatus.status = 'Connecting';
|
|
333
377
|
this.tunnelStatus.type = type;
|
|
378
|
+
// Start the relay first so the agent connects and registers its version.
|
|
379
|
+
callbacks.onStatus?.('Starting tunnel agent...');
|
|
380
|
+
await this.startAgent(secret);
|
|
334
381
|
callbacks.onStatus?.('Checking your playit account for an existing tunnel...');
|
|
335
382
|
let rd = await this.getRunData(secret);
|
|
336
383
|
let tunnel = this.findTunnel(rd, type);
|
|
337
384
|
if (!tunnel) {
|
|
338
385
|
callbacks.onStatus?.(`Creating ${type} tunnel...`);
|
|
339
|
-
await this.
|
|
386
|
+
await this.createTunnelWithRetry(type, rd.agent_id, secret, callbacks);
|
|
340
387
|
// Poll until the tunnel leaves "pending" and gets a public address.
|
|
341
388
|
for (let i = 0; i < 15 && !tunnel; i++) {
|
|
342
389
|
await this.sleep(3000);
|
|
@@ -351,11 +398,38 @@ class PlayitManager {
|
|
|
351
398
|
this.tunnelStatus.address = address;
|
|
352
399
|
this.tunnelStatus.port = port;
|
|
353
400
|
this.configManager.updatePlayitTunnel({ tunnelAddress: address, tunnelPort: Number(port) });
|
|
354
|
-
callbacks.onStatus?.('Starting tunnel relay...');
|
|
355
|
-
await this.startAgent(secret);
|
|
356
401
|
this.tunnelStatus.status = 'Online';
|
|
357
402
|
return this.tunnelStatus;
|
|
358
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Creates a tunnel, retrying on "AgentVersionTooOld" — that error means the
|
|
406
|
+
* freshly-started agent hasn't finished registering its version yet, so we
|
|
407
|
+
* wait and retry (refreshing the agent_id) a few times.
|
|
408
|
+
*/
|
|
409
|
+
async createTunnelWithRetry(type, agentId, secret, callbacks) {
|
|
410
|
+
let lastErr;
|
|
411
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
412
|
+
try {
|
|
413
|
+
await this.createApiTunnel(type, agentId, secret);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
lastErr = err;
|
|
418
|
+
if (!/AgentVersionTooOld/i.test(err.message || ''))
|
|
419
|
+
throw err;
|
|
420
|
+
callbacks.onStatus?.('Waiting for the agent to finish registering with playit...');
|
|
421
|
+
await this.sleep(4000);
|
|
422
|
+
try {
|
|
423
|
+
const rd = await this.getRunData(secret);
|
|
424
|
+
if (rd?.agent_id)
|
|
425
|
+
agentId = rd.agent_id;
|
|
426
|
+
}
|
|
427
|
+
catch { /* keep previous agentId */ }
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
throw new Error(`${lastErr?.message || 'AgentVersionTooOld'} — the playit agent did not register in time. ` +
|
|
431
|
+
`Make sure the server can reach playit.gg, then try /tunnel again.`);
|
|
432
|
+
}
|
|
359
433
|
/** Spawns the long-running daemon that relays tunnel traffic. */
|
|
360
434
|
startAgent(secret) {
|
|
361
435
|
return new Promise((resolve) => {
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.isNewer = isNewer;
|
|
37
|
+
exports.checkForUpdate = checkForUpdate;
|
|
38
|
+
const https = __importStar(require("https"));
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const configManager_1 = require("../config/configManager");
|
|
42
|
+
/**
|
|
43
|
+
* Lightweight "is there a newer version on npm?" checker.
|
|
44
|
+
*
|
|
45
|
+
* - Reads the installed name/version from the package's own package.json.
|
|
46
|
+
* - Asks the npm registry's dist-tags endpoint for the latest version.
|
|
47
|
+
* - Caches the result (logs/.update-check.json) so we only hit the network
|
|
48
|
+
* every few hours, keeping startup fast.
|
|
49
|
+
* - Fully fail-silent: no network / offline / parse error => returns null.
|
|
50
|
+
*/
|
|
51
|
+
const CACHE_FILE = path.join(configManager_1.APP_ROOT, 'logs', '.update-check.json');
|
|
52
|
+
const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // re-check at most every 6h
|
|
53
|
+
const FETCH_TIMEOUT_MS = 2500;
|
|
54
|
+
function readPkg() {
|
|
55
|
+
try {
|
|
56
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(configManager_1.APP_ROOT, 'package.json'), 'utf-8'));
|
|
57
|
+
if (pkg && pkg.name && pkg.version)
|
|
58
|
+
return { name: pkg.name, version: pkg.version };
|
|
59
|
+
}
|
|
60
|
+
catch { /* ignore */ }
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function parseVer(v) {
|
|
64
|
+
// Strip any pre-release/build suffix, then split into numeric parts.
|
|
65
|
+
return v.split('-')[0].split('.').map((n) => parseInt(n, 10) || 0);
|
|
66
|
+
}
|
|
67
|
+
/** True if `latest` is a higher semver than `current`. */
|
|
68
|
+
function isNewer(latest, current) {
|
|
69
|
+
const a = parseVer(latest);
|
|
70
|
+
const b = parseVer(current);
|
|
71
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
72
|
+
const x = a[i] || 0;
|
|
73
|
+
const y = b[i] || 0;
|
|
74
|
+
if (x > y)
|
|
75
|
+
return true;
|
|
76
|
+
if (x < y)
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
function fetchLatest(name) {
|
|
82
|
+
// dist-tags is a tiny payload: {"latest":"1.2.3", ...}
|
|
83
|
+
const url = `https://registry.npmjs.org/-/package/${name.replace('/', '%2F')}/dist-tags`;
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
const req = https.get(url, { timeout: FETCH_TIMEOUT_MS }, (res) => {
|
|
86
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
87
|
+
res.resume();
|
|
88
|
+
resolve(null);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
let data = '';
|
|
92
|
+
res.on('data', (c) => { data += c; });
|
|
93
|
+
res.on('end', () => {
|
|
94
|
+
try {
|
|
95
|
+
resolve(JSON.parse(data).latest || null);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
resolve(null);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
req.on('error', () => resolve(null));
|
|
103
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function readCache() {
|
|
107
|
+
try {
|
|
108
|
+
const c = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
|
|
109
|
+
if (c && typeof c.latest === 'string' && typeof c.checkedAt === 'number')
|
|
110
|
+
return c;
|
|
111
|
+
}
|
|
112
|
+
catch { /* ignore */ }
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
function writeCache(latest) {
|
|
116
|
+
try {
|
|
117
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify({ latest, checkedAt: Date.now() }), 'utf-8');
|
|
118
|
+
}
|
|
119
|
+
catch { /* ignore */ }
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Returns update info, or null if it couldn't be determined.
|
|
123
|
+
* Pass force=true (e.g. for an explicit /update command) to bypass the cache.
|
|
124
|
+
*/
|
|
125
|
+
async function checkForUpdate(force = false) {
|
|
126
|
+
const pkg = readPkg();
|
|
127
|
+
if (!pkg)
|
|
128
|
+
return null;
|
|
129
|
+
if (!force) {
|
|
130
|
+
const cached = readCache();
|
|
131
|
+
if (cached && Date.now() - cached.checkedAt < CHECK_INTERVAL_MS) {
|
|
132
|
+
return { name: pkg.name, current: pkg.version, latest: cached.latest, updateAvailable: isNewer(cached.latest, pkg.version) };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const latest = await fetchLatest(pkg.name);
|
|
136
|
+
if (!latest)
|
|
137
|
+
return null;
|
|
138
|
+
writeCache(latest);
|
|
139
|
+
return { name: pkg.name, current: pkg.version, latest, updateAvailable: isNewer(latest, pkg.version) };
|
|
140
|
+
}
|
package/dist/utils/helpers.js
CHANGED
|
@@ -42,9 +42,11 @@ exports.getDirSize = getDirSize;
|
|
|
42
42
|
exports.checkJava = checkJava;
|
|
43
43
|
exports.findInstalledJavas = findInstalledJavas;
|
|
44
44
|
exports.getSystemStats = getSystemStats;
|
|
45
|
+
exports.checkForUpdates = checkForUpdates;
|
|
45
46
|
const fs = __importStar(require("fs"));
|
|
46
47
|
const os = __importStar(require("os"));
|
|
47
48
|
const child_process_1 = require("child_process");
|
|
49
|
+
const https = __importStar(require("https"));
|
|
48
50
|
/**
|
|
49
51
|
* Detects the runtime OS environment: Windows, WSL, or Linux
|
|
50
52
|
*/
|
|
@@ -363,3 +365,66 @@ function getSystemStats() {
|
|
|
363
365
|
uptimeSeconds: Math.floor(os.uptime()),
|
|
364
366
|
};
|
|
365
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* Checks npm registry for a newer version of the CLI package.
|
|
370
|
+
* Returns the latest version string if a newer version is available, or null otherwise.
|
|
371
|
+
*/
|
|
372
|
+
function checkForUpdates(currentVersion) {
|
|
373
|
+
return new Promise((resolve) => {
|
|
374
|
+
const options = {
|
|
375
|
+
hostname: 'registry.npmjs.org',
|
|
376
|
+
path: '/@woopsy/mcpanel/latest',
|
|
377
|
+
method: 'GET',
|
|
378
|
+
timeout: 2000,
|
|
379
|
+
headers: {
|
|
380
|
+
'User-Agent': 'mcpanel-cli',
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
const req = https.get(options, (res) => {
|
|
384
|
+
if (res.statusCode !== 200) {
|
|
385
|
+
resolve(null);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
let data = '';
|
|
389
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
390
|
+
res.on('end', () => {
|
|
391
|
+
try {
|
|
392
|
+
const parsed = JSON.parse(data);
|
|
393
|
+
const latest = parsed.version;
|
|
394
|
+
if (latest && isNewerVersion(currentVersion, latest)) {
|
|
395
|
+
resolve(latest);
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
resolve(null);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
resolve(null);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
req.on('timeout', () => {
|
|
407
|
+
req.destroy();
|
|
408
|
+
resolve(null);
|
|
409
|
+
});
|
|
410
|
+
req.on('error', () => {
|
|
411
|
+
resolve(null);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Basic semver comparison (a < b)
|
|
417
|
+
*/
|
|
418
|
+
function isNewerVersion(current, latest) {
|
|
419
|
+
const cParts = current.split('.').map(Number);
|
|
420
|
+
const lParts = latest.split('.').map(Number);
|
|
421
|
+
for (let i = 0; i < 3; i++) {
|
|
422
|
+
const c = cParts[i] || 0;
|
|
423
|
+
const l = lParts[i] || 0;
|
|
424
|
+
if (l > c)
|
|
425
|
+
return true;
|
|
426
|
+
if (c > l)
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
return false;
|
|
430
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@woopsy/mcpanel",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "MCPANEL — a terminal-based, single-server Minecraft server manager with an Arch/neofetch-style UI, live logs, backups, plugins and Playit.gg tunnels.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"dev": "ts-node src/index.ts",
|
|
19
19
|
"prod": "node dist/index.js",
|
|
20
20
|
"prepublishOnly": "npm run build",
|
|
21
|
+
"push": "node scripts/push.js",
|
|
21
22
|
"install:global": "npm run build && npm install -g ."
|
|
22
23
|
},
|
|
23
24
|
"keywords": [
|