@woopsy/mcpanel 1.0.3 → 2.0.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;
@@ -95,7 +96,10 @@ class CommandRouter {
95
96
  ' /stats - System stats + CPU/RAM/disk of the server',
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',
99
+ ' /tray - Run in background, minimize console to system tray',
100
+ ' /background - Synonym for /tray',
98
101
  ' /clear - Clear the screen, scrollback and command history',
102
+ ' /update - Check npm for a newer version of MCPANEL',
99
103
  ' /config - View active application config.json',
100
104
  ' /exit - Close MCPANEL server manager',
101
105
  colors.gray('──────────────────────────────────────────────\n')
@@ -465,6 +469,22 @@ class CommandRouter {
465
469
  this.configManager.updateSettings({ defaultJavaPath: cleanPath });
466
470
  return colors.success(`Java set to "${cleanPath}" (version ${info.version}). It will be used on the next /start.`);
467
471
  }
472
+ /**
473
+ * Executes /update — checks npm for a newer version and prints how to update.
474
+ */
475
+ async executeUpdate() {
476
+ const info = await (0, updateChecker_1.checkForUpdate)(true);
477
+ if (!info) {
478
+ return colors.warning('Could not check for updates (no network connection?).');
479
+ }
480
+ if (info.updateAvailable) {
481
+ return [
482
+ colors.warning(`Update available: ${colors.bold(info.current)} → ${colors.bold(colors.green(info.latest))}`),
483
+ colors.gray('Update with: ') + colors.cyan(`npm i -g ${info.name}@latest`),
484
+ ].join('\n');
485
+ }
486
+ return colors.success(`You're on the latest version (${info.current}).`);
487
+ }
468
488
  /**
469
489
  * Executes /config
470
490
  */
package/dist/index.js CHANGED
@@ -50,7 +50,9 @@ 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");
55
+ const trayManager_1 = require("./managers/trayManager");
54
56
  // Initialize managers
55
57
  const configManager = new configManager_1.ConfigManager();
56
58
  configManager.initialize();
@@ -58,6 +60,7 @@ const processManager = new processManager_1.ProcessManager();
58
60
  const serverManager = new serverManager_1.ServerManager(configManager);
59
61
  const backupManager = new backupManager_1.BackupManager(configManager);
60
62
  const playitManager = new playitManager_1.PlayitManager(configManager);
63
+ const trayManager = new trayManager_1.TrayManager(configManager, processManager, playitManager);
61
64
  const router = new commandRouter_1.CommandRouter(configManager, processManager, serverManager, backupManager, playitManager);
62
65
  const HISTORY_PATH = path.join(configManager_1.APP_ROOT, 'logs', '.history');
63
66
  let currentState = 'COMMAND';
@@ -70,6 +73,12 @@ let consoleActiveServer = '';
70
73
  let logViewServer = '';
71
74
  // Readline interface
72
75
  let rl;
76
+ let CLI_VERSION = '1.0.3';
77
+ try {
78
+ const pkg = JSON.parse(fs.readFileSync(path.join(configManager_1.APP_ROOT, 'package.json'), 'utf-8'));
79
+ CLI_VERSION = pkg.version || '1.0.3';
80
+ }
81
+ catch { /* ignore */ }
73
82
  /**
74
83
  * Renders the figlet "MCPANEL" ASCII banner with a chalk gradient.
75
84
  */
@@ -79,7 +88,7 @@ function renderBanner() {
79
88
  const tints = [chalk_1.default.cyanBright, chalk_1.default.cyan, chalk_1.default.greenBright, chalk_1.default.green, chalk_1.default.green];
80
89
  console.log();
81
90
  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'));
91
+ console.log(chalk_1.default.greenBright.bold(' Minecraft Server Manager') + chalk_1.default.gray(` v${CLI_VERSION}`));
83
92
  }
84
93
  /**
85
94
  * Renders the neofetch / Arch-Linux-style info block for the synced server.
@@ -161,7 +170,7 @@ const COMMAND_LIST = [
161
170
  '/plugins list', '/plugins install', '/plugins remove',
162
171
  '/setup',
163
172
  '/tunnel java', '/tunnel bedrock', '/tunnel status', '/tunnel stop', '/tunnel reset',
164
- '/config', '/clear', '/exit'
173
+ '/config', '/clear', '/update', '/tray', '/background', '/exit'
165
174
  ];
166
175
  // Subcommands offered once "<command> " has been typed.
167
176
  const SUBCOMMANDS = {
@@ -238,6 +247,11 @@ function getStatusBar() {
238
247
  const running = server ? !!processManager.getActiveServer(server.name) : false;
239
248
  const backupsCount = backupManager.listBackups().length;
240
249
  const tunnelStatus = playitManager.getStatus().status;
250
+ // Sync menu state to the system tray
251
+ try {
252
+ trayManager.updateMenu();
253
+ }
254
+ catch { /* ignore */ }
241
255
  const serverStr = !server
242
256
  ? colors.gray('none')
243
257
  : running ? colors.green('Running') : colors.red('Offline');
@@ -554,6 +568,16 @@ async function handleCommandState(line) {
554
568
  console.log(colors.failure(`Failed to clear: ${err.message}`));
555
569
  }
556
570
  break;
571
+ case '/tray':
572
+ case '/background': {
573
+ console.log(colors.info('\nPutting MCPANEL in the background...'));
574
+ console.log(colors.gray('The terminal window will be hidden. Use the system tray icon to restore it.'));
575
+ const success = trayManager.hideConsole();
576
+ if (!success) {
577
+ console.log(colors.failure('Failed to hide console window. Ensure you are running in a supported GUI environment.'));
578
+ }
579
+ break;
580
+ }
557
581
  case '/exit':
558
582
  logger_1.logger.info('Exiting MCPANEL manager.');
559
583
  playitManager.stopTunnel();
@@ -609,6 +633,9 @@ async function handleCommandState(line) {
609
633
  case '/java':
610
634
  console.log(router.executeJava(args.length ? args.join(' ') : undefined));
611
635
  break;
636
+ case '/update':
637
+ console.log(await router.executeUpdate());
638
+ break;
612
639
  case '/config':
613
640
  console.log(router.executeConfig());
614
641
  break;
@@ -720,11 +747,31 @@ async function finishStartup() {
720
747
  currentState = 'COMMAND';
721
748
  promptUser();
722
749
  }
750
+ /**
751
+ * Prints a one-time-per-launch notice if a newer version is on npm. Fail-silent
752
+ * and cached, so it never slows down or blocks startup.
753
+ */
754
+ async function showUpdateNotice() {
755
+ try {
756
+ const info = await (0, updateChecker_1.checkForUpdate)();
757
+ if (info && info.updateAvailable) {
758
+ console.log();
759
+ 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));
760
+ console.log(chalk_1.default.gray(' Update with: ') + chalk_1.default.cyan(`npm i -g ${info.name}@latest`));
761
+ }
762
+ }
763
+ catch {
764
+ // Never let an update check break startup.
765
+ }
766
+ }
723
767
  /**
724
768
  * Main application setup
725
769
  */
726
770
  async function main() {
727
771
  renderBanner();
772
+ await showUpdateNotice();
773
+ // Start the background system tray loop
774
+ await trayManager.start();
728
775
  rl = readline.createInterface({
729
776
  input: process.stdin,
730
777
  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,335 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.TrayManager = void 0;
40
+ const systray2_1 = __importDefault(require("systray2"));
41
+ const path = __importStar(require("path"));
42
+ const fs = __importStar(require("fs"));
43
+ const helpers_1 = require("../utils/helpers");
44
+ const configManager_1 = require("../config/configManager");
45
+ const logger_1 = require("../utils/logger");
46
+ const SysTrayBase = systray2_1.default;
47
+ class WSLSysTray extends SysTrayBase {
48
+ constructor(conf) {
49
+ super(conf);
50
+ }
51
+ async init() {
52
+ const osType = (0, helpers_1.detectOS)();
53
+ if (osType === 'WSL') {
54
+ const binName = "tray_windows_release.exe";
55
+ const nodeModulesBin = path.join(configManager_1.APP_ROOT, 'node_modules', 'systray2', 'traybin', binName);
56
+ this._binPath = nodeModulesBin;
57
+ // Auto chmod +x to ensure the binary is executable from WSL
58
+ try {
59
+ fs.chmodSync(nodeModulesBin, 0o755);
60
+ }
61
+ catch { /* ignore */ }
62
+ return new Promise(async (resolve, reject) => {
63
+ try {
64
+ const child = require('child_process');
65
+ const readline = require('readline');
66
+ this._process = child.spawn(this._binPath, [], {
67
+ windowsHide: true
68
+ });
69
+ this._process.on('error', reject);
70
+ this._rl = readline.createInterface({
71
+ input: this._process.stdout
72
+ });
73
+ const internalIdMap = this.internalIdMap;
74
+ const counter = { id: 1 };
75
+ const addInternalId = (item) => {
76
+ const id = counter.id++;
77
+ internalIdMap.set(id, item);
78
+ if (item.items) {
79
+ item.items.forEach(addInternalId);
80
+ }
81
+ item.__id = id;
82
+ };
83
+ this._conf.menu.items.forEach(addInternalId);
84
+ const loadIcon = async (fileName) => {
85
+ const buffer = await fs.promises.readFile(fileName);
86
+ return buffer.toString('base64');
87
+ };
88
+ const resolveIcon = async (item) => {
89
+ if (item.icon && fs.existsSync(item.icon)) {
90
+ item.icon = await loadIcon(item.icon);
91
+ }
92
+ if (item.items) {
93
+ await Promise.all(item.items.map((sub) => resolveIcon(sub)));
94
+ }
95
+ };
96
+ await resolveIcon(this._conf.menu);
97
+ this.onReady(() => {
98
+ const itemTrimmer = (item) => ({
99
+ title: item.title,
100
+ tooltip: item.tooltip,
101
+ checked: item.checked,
102
+ enabled: item.enabled === undefined ? true : item.enabled,
103
+ hidden: item.hidden,
104
+ items: item.items,
105
+ icon: item.icon,
106
+ isTemplateIcon: item.isTemplateIcon,
107
+ __id: item.__id
108
+ });
109
+ const menuTrimmer = (menu) => ({
110
+ icon: menu.icon,
111
+ title: menu.title,
112
+ tooltip: menu.tooltip,
113
+ items: menu.items.map(itemTrimmer),
114
+ isTemplateIcon: menu.isTemplateIcon
115
+ });
116
+ this.writeLine(JSON.stringify(menuTrimmer(this._conf.menu)));
117
+ resolve();
118
+ });
119
+ }
120
+ catch (err) {
121
+ reject(err);
122
+ }
123
+ });
124
+ }
125
+ else {
126
+ return super.init();
127
+ }
128
+ }
129
+ }
130
+ class TrayManager {
131
+ configManager;
132
+ processManager;
133
+ playitManager;
134
+ systray = null;
135
+ activeHandle = null;
136
+ isInitialized = false;
137
+ // Menu item state tracking
138
+ itemShow = { title: 'Open Console', tooltip: 'Restore terminal window', enabled: true };
139
+ itemHide = { title: 'Hide Console', tooltip: 'Hide terminal window from taskbar', enabled: true };
140
+ itemServerStatus = { title: 'Server: Checking...', tooltip: 'Current Minecraft server status', enabled: false };
141
+ itemServerToggle = { title: 'Start Server', tooltip: 'Toggle server state', enabled: true };
142
+ itemTunnelStatus = { title: 'Tunnel: Checking...', tooltip: 'Current Playit tunnel status', enabled: false };
143
+ itemTunnelToggle = { title: 'Start Tunnel', tooltip: 'Toggle tunnel state', enabled: true };
144
+ itemExit = { title: 'Exit', tooltip: 'Stop server/tunnel and exit', enabled: true };
145
+ constructor(configManager, processManager, playitManager) {
146
+ this.configManager = configManager;
147
+ this.processManager = processManager;
148
+ this.playitManager = playitManager;
149
+ }
150
+ /**
151
+ * Initializes and starts the system tray icon loop
152
+ */
153
+ async start() {
154
+ if (this.isInitialized)
155
+ return true;
156
+ const osType = (0, helpers_1.detectOS)();
157
+ const iconFile = osType === 'Windows' || osType === 'WSL'
158
+ ? path.join(configManager_1.APP_ROOT, 'assets', 'logo.ico')
159
+ : path.join(configManager_1.APP_ROOT, 'assets', 'logo.png');
160
+ try {
161
+ this.systray = new WSLSysTray({
162
+ menu: {
163
+ icon: iconFile,
164
+ title: 'MCPANEL',
165
+ tooltip: 'MCPANEL Server Manager',
166
+ items: [
167
+ this.itemShow,
168
+ this.itemHide,
169
+ systray2_1.default.separator,
170
+ this.itemServerStatus,
171
+ this.itemServerToggle,
172
+ systray2_1.default.separator,
173
+ this.itemTunnelStatus,
174
+ this.itemTunnelToggle,
175
+ systray2_1.default.separator,
176
+ this.itemExit
177
+ ]
178
+ },
179
+ debug: false
180
+ });
181
+ this.systray.onClick((event) => {
182
+ this.handleTrayClick(event).catch((err) => {
183
+ logger_1.logger.error('Error handling tray click event', err);
184
+ });
185
+ });
186
+ await this.systray.ready();
187
+ this.isInitialized = true;
188
+ logger_1.logger.info('System tray initialized successfully.');
189
+ this.updateMenu();
190
+ return true;
191
+ }
192
+ catch (err) {
193
+ this.systray = null;
194
+ logger_1.logger.error('Failed to initialize system tray', err);
195
+ // Fail silently for user prompt, fall back to CLI-only mode gracefully.
196
+ return false;
197
+ }
198
+ }
199
+ /**
200
+ * Dynamically updates the titles and states of the tray menu items
201
+ */
202
+ updateMenu() {
203
+ const tray = this.systray;
204
+ if (!tray || !this.isInitialized)
205
+ return;
206
+ const server = this.configManager.getServer();
207
+ const running = server ? !!this.processManager.getActiveServer(server.name) : false;
208
+ const tunnel = this.playitManager.getStatus();
209
+ // Update server status & toggle label
210
+ this.itemServerStatus.title = `Server: ${running ? 'Running' : 'Offline'}`;
211
+ this.itemServerToggle.title = running ? 'Stop Server' : 'Start Server';
212
+ this.itemServerToggle.enabled = !!server;
213
+ // Update tunnel status & toggle label
214
+ this.itemTunnelStatus.title = `Tunnel: ${tunnel.status}`;
215
+ this.itemTunnelToggle.title = tunnel.status === 'Online' || tunnel.status === 'Connecting' ? 'Stop Tunnel' : 'Start Tunnel';
216
+ // Push updates to the native helper
217
+ tray.sendAction({ type: 'update-item', item: this.itemServerStatus });
218
+ tray.sendAction({ type: 'update-item', item: this.itemServerToggle });
219
+ tray.sendAction({ type: 'update-item', item: this.itemTunnelStatus });
220
+ tray.sendAction({ type: 'update-item', item: this.itemTunnelToggle });
221
+ }
222
+ /**
223
+ * Hides the active terminal window to the background
224
+ */
225
+ hideConsole() {
226
+ const handle = (0, helpers_1.getActiveWindowHandle)();
227
+ if (!handle) {
228
+ logger_1.logger.warn('Could not retrieve active console window handle.');
229
+ return false;
230
+ }
231
+ this.activeHandle = handle;
232
+ const success = (0, helpers_1.hideConsoleWindow)(handle);
233
+ if (success) {
234
+ logger_1.logger.info(`Console window (${handle}) hidden to background.`);
235
+ }
236
+ return success;
237
+ }
238
+ /**
239
+ * Restores the hidden console window back to foreground
240
+ */
241
+ showConsole() {
242
+ const handle = this.activeHandle;
243
+ if (!handle) {
244
+ logger_1.logger.warn('No saved console window handle to restore.');
245
+ // Fallback: try to retrieve current active handle (best effort if not saved)
246
+ const curHandle = (0, helpers_1.getActiveWindowHandle)();
247
+ if (curHandle) {
248
+ return (0, helpers_1.showConsoleWindow)(curHandle);
249
+ }
250
+ return false;
251
+ }
252
+ const success = (0, helpers_1.showConsoleWindow)(handle);
253
+ if (success) {
254
+ logger_1.logger.info(`Console window (${handle}) restored to foreground.`);
255
+ }
256
+ return success;
257
+ }
258
+ /**
259
+ * Handles individual tray menu click actions
260
+ */
261
+ async handleTrayClick(event) {
262
+ const title = event.item.title;
263
+ if (title === 'Open Console') {
264
+ this.showConsole();
265
+ }
266
+ else if (title === 'Hide Console') {
267
+ this.hideConsole();
268
+ }
269
+ else if (title === 'Start Server') {
270
+ const server = this.configManager.getServer();
271
+ if (!server)
272
+ return;
273
+ const jarPath = path.join(server.path, 'server.jar');
274
+ let resolvedJar = jarPath;
275
+ if (!fs.existsSync(jarPath)) {
276
+ const jarFiles = fs.readdirSync(server.path).filter(f => f.endsWith('.jar'));
277
+ if (jarFiles.length > 0) {
278
+ resolvedJar = path.join(server.path, jarFiles[0]);
279
+ }
280
+ }
281
+ try {
282
+ logger_1.logger.info(`Starting Minecraft server "${server.name}" from tray menu...`);
283
+ await this.processManager.startServer(server.name, server.path, resolvedJar, server.ram, this.configManager.getConfig().defaultJavaPath);
284
+ }
285
+ catch (err) {
286
+ logger_1.logger.error('Failed to start server from tray', err);
287
+ }
288
+ this.updateMenu();
289
+ }
290
+ else if (title === 'Stop Server') {
291
+ const server = this.configManager.getServer();
292
+ if (!server)
293
+ return;
294
+ logger_1.logger.info(`Stopping Minecraft server "${server.name}" from tray menu gracefully...`);
295
+ await this.processManager.stopServer(server.name);
296
+ this.updateMenu();
297
+ }
298
+ else if (title === 'Start Tunnel') {
299
+ logger_1.logger.info('Starting playit tunnel agent from tray menu...');
300
+ try {
301
+ const savedSettings = this.configManager.getConfig().playitSettings;
302
+ const type = (savedSettings.tunnelAddress ? 'java' : 'java'); // default to java
303
+ await this.playitManager.setupAndStart(type);
304
+ }
305
+ catch (err) {
306
+ logger_1.logger.error('Failed to start tunnel from tray', err);
307
+ }
308
+ this.updateMenu();
309
+ }
310
+ else if (title === 'Stop Tunnel') {
311
+ logger_1.logger.info('Stopping playit tunnel agent from tray menu...');
312
+ this.playitManager.stopTunnel();
313
+ this.updateMenu();
314
+ }
315
+ else if (title === 'Exit') {
316
+ logger_1.logger.info('Exit requested from system tray. Shutting down MCPANEL...');
317
+ // Stop server
318
+ const server = this.configManager.getServer();
319
+ if (server) {
320
+ await this.processManager.stopServer(server.name);
321
+ }
322
+ // Stop tunnel
323
+ this.playitManager.stopTunnel();
324
+ // Clean exit
325
+ const tray = this.systray;
326
+ if (tray) {
327
+ await tray.kill(true);
328
+ }
329
+ else {
330
+ process.exit(0);
331
+ }
332
+ }
333
+ }
334
+ }
335
+ exports.TrayManager = TrayManager;
@@ -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,14 @@ exports.getDirSize = getDirSize;
42
42
  exports.checkJava = checkJava;
43
43
  exports.findInstalledJavas = findInstalledJavas;
44
44
  exports.getSystemStats = getSystemStats;
45
+ exports.checkForUpdates = checkForUpdates;
46
+ exports.getActiveWindowHandle = getActiveWindowHandle;
47
+ exports.hideConsoleWindow = hideConsoleWindow;
48
+ exports.showConsoleWindow = showConsoleWindow;
45
49
  const fs = __importStar(require("fs"));
46
50
  const os = __importStar(require("os"));
47
51
  const child_process_1 = require("child_process");
52
+ const https = __importStar(require("https"));
48
53
  /**
49
54
  * Detects the runtime OS environment: Windows, WSL, or Linux
50
55
  */
@@ -363,3 +368,146 @@ function getSystemStats() {
363
368
  uptimeSeconds: Math.floor(os.uptime()),
364
369
  };
365
370
  }
371
+ /**
372
+ * Checks npm registry for a newer version of the CLI package.
373
+ * Returns the latest version string if a newer version is available, or null otherwise.
374
+ */
375
+ function checkForUpdates(currentVersion) {
376
+ return new Promise((resolve) => {
377
+ const options = {
378
+ hostname: 'registry.npmjs.org',
379
+ path: '/@woopsy/mcpanel/latest',
380
+ method: 'GET',
381
+ timeout: 2000,
382
+ headers: {
383
+ 'User-Agent': 'mcpanel-cli',
384
+ },
385
+ };
386
+ const req = https.get(options, (res) => {
387
+ if (res.statusCode !== 200) {
388
+ resolve(null);
389
+ return;
390
+ }
391
+ let data = '';
392
+ res.on('data', (chunk) => { data += chunk; });
393
+ res.on('end', () => {
394
+ try {
395
+ const parsed = JSON.parse(data);
396
+ const latest = parsed.version;
397
+ if (latest && isNewerVersion(currentVersion, latest)) {
398
+ resolve(latest);
399
+ }
400
+ else {
401
+ resolve(null);
402
+ }
403
+ }
404
+ catch {
405
+ resolve(null);
406
+ }
407
+ });
408
+ });
409
+ req.on('timeout', () => {
410
+ req.destroy();
411
+ resolve(null);
412
+ });
413
+ req.on('error', () => {
414
+ resolve(null);
415
+ });
416
+ });
417
+ }
418
+ /**
419
+ * Basic semver comparison (a < b)
420
+ */
421
+ function isNewerVersion(current, latest) {
422
+ const cParts = current.split('.').map(Number);
423
+ const lParts = latest.split('.').map(Number);
424
+ for (let i = 0; i < 3; i++) {
425
+ const c = cParts[i] || 0;
426
+ const l = lParts[i] || 0;
427
+ if (l > c)
428
+ return true;
429
+ if (c > l)
430
+ return false;
431
+ }
432
+ return false;
433
+ }
434
+ /**
435
+ * Gets the handle/ID of the active console window.
436
+ * Returns null if not supported or fails.
437
+ */
438
+ function getActiveWindowHandle() {
439
+ const osType = detectOS();
440
+ try {
441
+ if (osType === 'Windows') {
442
+ const cmd = `powershell -NoProfile -Command "Add-Type -MemberDefinition '[DllImport(\\"user32.dll\\")] public static extern IntPtr GetForegroundWindow();' -Name Win32Util -Namespace Win32 -PassThru | Out-Null; [Win32.Win32Util]::GetForegroundWindow()"`;
443
+ return (0, child_process_1.execSync)(cmd).toString().trim();
444
+ }
445
+ else if (osType === 'WSL') {
446
+ const cmd = `powershell.exe -NoProfile -Command "Add-Type -MemberDefinition '[DllImport(\\"user32.dll\\")] public static extern IntPtr GetForegroundWindow();' -Name Win32Util -Namespace Win32 -PassThru | Out-Null; [Win32.Win32Util]::GetForegroundWindow()"`;
447
+ return (0, child_process_1.execSync)(cmd).toString().trim();
448
+ }
449
+ else {
450
+ // Native Linux
451
+ return (0, child_process_1.execSync)('xdotool getactivewindow 2>/dev/null').toString().trim() || null;
452
+ }
453
+ }
454
+ catch {
455
+ return null;
456
+ }
457
+ }
458
+ /**
459
+ * Hides the console window using its handle/ID.
460
+ */
461
+ function hideConsoleWindow(handle) {
462
+ if (!handle)
463
+ return false;
464
+ const osType = detectOS();
465
+ try {
466
+ if (osType === 'Windows') {
467
+ const cmd = `powershell -NoProfile -Command "Add-Type -MemberDefinition '[DllImport(\\"user32.dll\\")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);' -Name Win32Util -Namespace Win32; [Win32.Win32Util]::ShowWindowAsync([IntPtr]${handle}, 0)"`;
468
+ (0, child_process_1.execSync)(cmd);
469
+ return true;
470
+ }
471
+ else if (osType === 'WSL') {
472
+ const cmd = `powershell.exe -NoProfile -Command "Add-Type -MemberDefinition '[DllImport(\\"user32.dll\\")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);' -Name Win32Util -Namespace Win32; [Win32.Win32Util]::ShowWindowAsync([IntPtr]${handle}, 0)"`;
473
+ (0, child_process_1.execSync)(cmd);
474
+ return true;
475
+ }
476
+ else {
477
+ // Native Linux
478
+ (0, child_process_1.execSync)(`xdotool windowunmap ${handle} 2>/dev/null`);
479
+ return true;
480
+ }
481
+ }
482
+ catch {
483
+ return false;
484
+ }
485
+ }
486
+ /**
487
+ * Restores and focuses the console window using its handle/ID.
488
+ */
489
+ function showConsoleWindow(handle) {
490
+ if (!handle)
491
+ return false;
492
+ const osType = detectOS();
493
+ try {
494
+ if (osType === 'Windows') {
495
+ const cmd = `powershell -NoProfile -Command "Add-Type -MemberDefinition '[DllImport(\\"user32.dll\\")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow); [DllImport(\\"user32.dll\\")] public static extern bool SetForegroundWindow(IntPtr hWnd);' -Name Win32Util -Namespace Win32; [Win32.Win32Util]::ShowWindowAsync([IntPtr]${handle}, 9); [Win32.Win32Util]::SetForegroundWindow([IntPtr]${handle})"`;
496
+ (0, child_process_1.execSync)(cmd);
497
+ return true;
498
+ }
499
+ else if (osType === 'WSL') {
500
+ const cmd = `powershell.exe -NoProfile -Command "Add-Type -MemberDefinition '[DllImport(\\"user32.dll\\")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow); [DllImport(\\"user32.dll\\")] public static extern bool SetForegroundWindow(IntPtr hWnd);' -Name Win32Util -Namespace Win32; [Win32.Win32Util]::ShowWindowAsync([IntPtr]${handle}, 9); [Win32.Win32Util]::SetForegroundWindow([IntPtr]${handle})"`;
501
+ (0, child_process_1.execSync)(cmd);
502
+ return true;
503
+ }
504
+ else {
505
+ // Native Linux
506
+ (0, child_process_1.execSync)(`xdotool windowmap ${handle} 2>/dev/null && xdotool windowactivate ${handle} 2>/dev/null`);
507
+ return true;
508
+ }
509
+ }
510
+ catch {
511
+ return false;
512
+ }
513
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@woopsy/mcpanel",
3
- "version": "1.0.3",
3
+ "version": "2.0.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": {
@@ -48,7 +48,8 @@
48
48
  "adm-zip": "^0.5.12",
49
49
  "chalk": "^4.1.2",
50
50
  "figlet": "^1.11.0",
51
- "pidusage": "^4.0.1"
51
+ "pidusage": "^4.0.1",
52
+ "systray2": "^2.1.4"
52
53
  },
53
54
  "devDependencies": {
54
55
  "@types/adm-zip": "^0.5.5",