@woopsy/mcpanel 1.1.0 → 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.
@@ -96,6 +96,8 @@ class CommandRouter {
96
96
  ' /stats - System stats + CPU/RAM/disk of the server',
97
97
  ' /java [path] - Show/list Java runtimes, or set the one used to launch',
98
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',
99
101
  ' /clear - Clear the screen, scrollback and command history',
100
102
  ' /update - Check npm for a newer version of MCPANEL',
101
103
  ' /config - View active application config.json',
package/dist/index.js CHANGED
@@ -52,6 +52,7 @@ const colors = __importStar(require("./utils/colors"));
52
52
  const helpers_1 = require("./utils/helpers");
53
53
  const updateChecker_1 = require("./services/updateChecker");
54
54
  const logger_1 = require("./utils/logger");
55
+ const trayManager_1 = require("./managers/trayManager");
55
56
  // Initialize managers
56
57
  const configManager = new configManager_1.ConfigManager();
57
58
  configManager.initialize();
@@ -59,6 +60,7 @@ const processManager = new processManager_1.ProcessManager();
59
60
  const serverManager = new serverManager_1.ServerManager(configManager);
60
61
  const backupManager = new backupManager_1.BackupManager(configManager);
61
62
  const playitManager = new playitManager_1.PlayitManager(configManager);
63
+ const trayManager = new trayManager_1.TrayManager(configManager, processManager, playitManager);
62
64
  const router = new commandRouter_1.CommandRouter(configManager, processManager, serverManager, backupManager, playitManager);
63
65
  const HISTORY_PATH = path.join(configManager_1.APP_ROOT, 'logs', '.history');
64
66
  let currentState = 'COMMAND';
@@ -168,7 +170,7 @@ const COMMAND_LIST = [
168
170
  '/plugins list', '/plugins install', '/plugins remove',
169
171
  '/setup',
170
172
  '/tunnel java', '/tunnel bedrock', '/tunnel status', '/tunnel stop', '/tunnel reset',
171
- '/config', '/clear', '/update', '/exit'
173
+ '/config', '/clear', '/update', '/tray', '/background', '/exit'
172
174
  ];
173
175
  // Subcommands offered once "<command> " has been typed.
174
176
  const SUBCOMMANDS = {
@@ -245,6 +247,11 @@ function getStatusBar() {
245
247
  const running = server ? !!processManager.getActiveServer(server.name) : false;
246
248
  const backupsCount = backupManager.listBackups().length;
247
249
  const tunnelStatus = playitManager.getStatus().status;
250
+ // Sync menu state to the system tray
251
+ try {
252
+ trayManager.updateMenu();
253
+ }
254
+ catch { /* ignore */ }
248
255
  const serverStr = !server
249
256
  ? colors.gray('none')
250
257
  : running ? colors.green('Running') : colors.red('Offline');
@@ -561,6 +568,16 @@ async function handleCommandState(line) {
561
568
  console.log(colors.failure(`Failed to clear: ${err.message}`));
562
569
  }
563
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
+ }
564
581
  case '/exit':
565
582
  logger_1.logger.info('Exiting MCPANEL manager.');
566
583
  playitManager.stopTunnel();
@@ -753,6 +770,8 @@ async function showUpdateNotice() {
753
770
  async function main() {
754
771
  renderBanner();
755
772
  await showUpdateNotice();
773
+ // Start the background system tray loop
774
+ await trayManager.start();
756
775
  rl = readline.createInterface({
757
776
  input: process.stdin,
758
777
  output: process.stdout,
@@ -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;
@@ -43,6 +43,9 @@ exports.checkJava = checkJava;
43
43
  exports.findInstalledJavas = findInstalledJavas;
44
44
  exports.getSystemStats = getSystemStats;
45
45
  exports.checkForUpdates = checkForUpdates;
46
+ exports.getActiveWindowHandle = getActiveWindowHandle;
47
+ exports.hideConsoleWindow = hideConsoleWindow;
48
+ exports.showConsoleWindow = showConsoleWindow;
46
49
  const fs = __importStar(require("fs"));
47
50
  const os = __importStar(require("os"));
48
51
  const child_process_1 = require("child_process");
@@ -428,3 +431,83 @@ function isNewerVersion(current, latest) {
428
431
  }
429
432
  return false;
430
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.1.0",
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",