@woopsy/mcpanel 1.0.3 → 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.
@@ -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;
@@ -96,6 +97,7 @@ class CommandRouter {
96
97
  ' /java [path] - Show/list Java runtimes, or set the one used to launch',
97
98
  ' /folder - Open the server folder in the file explorer',
98
99
  ' /clear - Clear the screen, scrollback and command history',
100
+ ' /update - Check npm for a newer version of MCPANEL',
99
101
  ' /config - View active application config.json',
100
102
  ' /exit - Close MCPANEL server manager',
101
103
  colors.gray('──────────────────────────────────────────────\n')
@@ -465,6 +467,22 @@ class CommandRouter {
465
467
  this.configManager.updateSettings({ defaultJavaPath: cleanPath });
466
468
  return colors.success(`Java set to "${cleanPath}" (version ${info.version}). It will be used on the next /start.`);
467
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
+ }
468
486
  /**
469
487
  * Executes /config
470
488
  */
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(' v1.0.0'));
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.
@@ -161,7 +168,7 @@ const COMMAND_LIST = [
161
168
  '/plugins list', '/plugins install', '/plugins remove',
162
169
  '/setup',
163
170
  '/tunnel java', '/tunnel bedrock', '/tunnel status', '/tunnel stop', '/tunnel reset',
164
- '/config', '/clear', '/exit'
171
+ '/config', '/clear', '/update', '/exit'
165
172
  ];
166
173
  // Subcommands offered once "<command> " has been typed.
167
174
  const SUBCOMMANDS = {
@@ -609,6 +616,9 @@ async function handleCommandState(line) {
609
616
  case '/java':
610
617
  console.log(router.executeJava(args.length ? args.join(' ') : undefined));
611
618
  break;
619
+ case '/update':
620
+ console.log(await router.executeUpdate());
621
+ break;
612
622
  case '/config':
613
623
  console.log(router.executeConfig());
614
624
  break;
@@ -720,11 +730,29 @@ async function finishStartup() {
720
730
  currentState = 'COMMAND';
721
731
  promptUser();
722
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
+ }
723
750
  /**
724
751
  * Main application setup
725
752
  */
726
753
  async function main() {
727
754
  renderBanner();
755
+ await showUpdateNotice();
728
756
  rl = readline.createInterface({
729
757
  input: process.stdin,
730
758
  output: process.stdout,
@@ -362,20 +362,28 @@ class PlayitManager {
362
362
  // Full automated setup
363
363
  // ---------------------------------------------------------------------------
364
364
  /**
365
- * One-call entry point: ensures binary + secret, creates the tunnel via the
366
- * API, starts the relay daemon, and returns the live tunnel status.
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.
367
372
  */
368
373
  async setupAndStart(type, callbacks = {}) {
369
374
  await this.ensureBinary();
370
375
  const secret = await this.ensureSecret(callbacks);
371
376
  this.tunnelStatus.status = 'Connecting';
372
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);
373
381
  callbacks.onStatus?.('Checking your playit account for an existing tunnel...');
374
382
  let rd = await this.getRunData(secret);
375
383
  let tunnel = this.findTunnel(rd, type);
376
384
  if (!tunnel) {
377
385
  callbacks.onStatus?.(`Creating ${type} tunnel...`);
378
- await this.createApiTunnel(type, rd.agent_id, secret);
386
+ await this.createTunnelWithRetry(type, rd.agent_id, secret, callbacks);
379
387
  // Poll until the tunnel leaves "pending" and gets a public address.
380
388
  for (let i = 0; i < 15 && !tunnel; i++) {
381
389
  await this.sleep(3000);
@@ -390,11 +398,38 @@ class PlayitManager {
390
398
  this.tunnelStatus.address = address;
391
399
  this.tunnelStatus.port = port;
392
400
  this.configManager.updatePlayitTunnel({ tunnelAddress: address, tunnelPort: Number(port) });
393
- callbacks.onStatus?.('Starting tunnel relay...');
394
- await this.startAgent(secret);
395
401
  this.tunnelStatus.status = 'Online';
396
402
  return this.tunnelStatus;
397
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
+ }
398
433
  /** Spawns the long-running daemon that relays tunnel traffic. */
399
434
  startAgent(secret) {
400
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
+ }
@@ -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",
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": {