@woopsy/mcpanel 2.1.4 → 5.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.
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
  # MCPANEL
13
13
 
14
14
  A terminal-based **single-server** Minecraft server manager with an Arch/neofetch-style
15
- startup screen. Connect one server folder and control it with simple slash commands —
15
+ startup screen. Connect one server folder and control it with simple commands —
16
16
  start/stop, live logs in a separate window, backups, plugins, `server.properties` editing,
17
17
  Java switching, and one-click [Playit.gg](https://playit.gg) tunnels so friends can join.
18
18
 
@@ -41,7 +41,7 @@ mcpanel>
41
41
 
42
42
  - **Node.js 22+** — <https://nodejs.org>
43
43
  - **Java** matching your Minecraft version (e.g. MC 26.x needs **Java 25**, MC 1.20–1.21 needs **Java 21**).
44
- MCPANEL can list and switch between installed JVMs with `/java`.
44
+ MCPANEL can list and switch between installed JVMs with `java`.
45
45
 
46
46
  ---
47
47
 
@@ -96,41 +96,44 @@ npx @woopsy/mcpanel
96
96
  it to `/mnt/c/...` automatically.
97
97
  3. MCPANEL detects the server type + Minecraft version, saves it, and drops you at the prompt.
98
98
 
99
- To connect a different server later: `/sync <path>`.
99
+ To connect a different server later: `sync <path>`.
100
100
 
101
101
  ---
102
102
 
103
103
  ## Commands
104
104
 
105
+ Commands are typed **without a leading slash** — just `start`, `tunnel java`, `playit`, etc.
106
+
105
107
  | Command | What it does |
106
108
  |---|---|
107
- | `/start` · `/stop` · `/restart` | Control the server process |
108
- | `/console` | Interactive console (type commands sent to the server) |
109
- | `/log` | Open **live logs in a new terminal window** (`tail -f`) |
110
- | `/info` | Server path, type, version and status |
111
- | `/sync <path>` | Connect a different server folder |
112
- | `/properties` | Edit `server.properties` interactively |
113
- | `/java [path]` | Show/list installed JVMs, or set the one used to launch |
114
- | `/stats` | System + server CPU / RAM / disk usage |
115
- | `/folder` | Open the server folder in your file explorer |
116
- | `/backup create` · `list` · `restore <id>` | Manage ZIP backups |
117
- | `/plugins list` · `install <url>` · `remove <name>` | Manage plugins |
118
- | `/tunnel java` · `bedrock` · `status` · `stop` · `reset` | Playit.gg tunnels |
119
- | `/config` · `/clear` · `/help` · `/exit` | Utilities |
120
-
121
- Type `/help` inside MCPANEL for the full menu, and use **Tab** for autocompletion.
109
+ | `start` · `stop` · `restart` | Control the server process |
110
+ | `console` | Interactive console (type commands sent to the server) |
111
+ | `log` | Open **live logs in a new terminal window** (`tail -f`) |
112
+ | `info` | Server path, type, version and status |
113
+ | `sync <path>` | Connect a different server folder |
114
+ | `properties` | Edit `server.properties` interactively |
115
+ | `java [path]` | Show/list installed JVMs, or set the one used to launch |
116
+ | `stats` | System + server CPU / RAM / disk usage |
117
+ | `folder` | Open the server folder in your file explorer |
118
+ | `backup create` · `list` · `restore <id>` | Manage ZIP backups |
119
+ | `plugins list` · `install <url>` · `remove <name>` | Manage plugins |
120
+ | `tunnel java` · `bedrock` · `status` · `log` · `stop` · `reset` | Playit.gg tunnels |
121
+ | `playit` | Stream live playit.gg relay logs (shortcut for `tunnel log`) |
122
+ | `config` · `clear` · `help` · `exit` | Utilities |
123
+
124
+ Type `help` inside MCPANEL for the full menu, and use **Tab** for autocompletion.
122
125
 
123
126
  ---
124
127
 
125
128
  ## Notes
126
129
 
127
130
  - **Single server by design.** MCPANEL manages exactly one server (the one you sync).
128
- - **Playit tunnel.** The first `/tunnel` claims a free Playit agent in your browser once;
131
+ - **Playit tunnel.** The first `tunnel` claims a free Playit agent in your browser once;
129
132
  the binary is downloaded automatically. Your secret is stored locally in `config.json`
130
133
  (which is git-ignored — don't commit it).
131
- - **`/log` in a new window.** On WSL it opens via the Windows console; on Linux it uses your
134
+ - **`log` in a new window.** On WSL it opens via the Windows console; on Linux it uses your
132
135
  terminal emulator; on macOS it uses Terminal. If none is available it falls back to an
133
- in-place read-only view (`/back` to exit).
136
+ in-place read-only view (`back` to exit).
134
137
 
135
138
  ---
136
139
 
@@ -65,43 +65,46 @@ class CommandRouter {
65
65
  colors.bold(colors.cyan('\nMCPANEL Help Menu')),
66
66
  colors.gray('──────────────────────────────────────────────'),
67
67
  colors.bold(colors.green('Server Commands')),
68
- ' /start - Start the Minecraft server',
69
- ' /stop - Stop the server gracefully',
70
- ' /restart - Restart the server',
71
- ' /console - Enter the interactive server console',
72
- ' /log - Open live server logs in a new terminal window',
73
- ' /info - Show server path, type, version and status',
74
- ' /sync <path> - Connect a different server folder',
75
- ' /properties - Edit server.properties interactively',
68
+ ' start - Start the Minecraft server',
69
+ ' stop - Stop the server gracefully',
70
+ ' restart - Restart the server',
71
+ ' console - Enter the interactive server console',
72
+ ' log - Stream live server logs in this terminal (read-only)',
73
+ ' info - Show server path, type, version and status',
74
+ ' sync <path> - Connect a different server folder',
75
+ ' properties - Edit server.properties interactively',
76
76
  '',
77
77
  colors.bold(colors.green('Tunnel Commands (Playit.gg)')),
78
- ' /setup - One-time Playit account claim (browser approval)',
79
- ' /tunnel java - Auto-create & start a Java tunnel, returns address',
80
- ' /tunnel bedrock - Auto-create & start a Bedrock tunnel, returns address',
81
- ' /tunnel status - Check tunnel status, address and latency',
82
- ' /tunnel stop - Stop the playit tunnel agent',
83
- ' /tunnel reset - Clear saved agent secret (re-claim on next tunnel)',
78
+ ' setup - One-time Playit account claim (browser approval)',
79
+ ' tunnel java - Auto-create & start a Java tunnel, returns address',
80
+ ' tunnel bedrock - Auto-create & start a Bedrock tunnel, returns address',
81
+ ' tunnel status - Check tunnel status, address and latency',
82
+ ' tunnel log - Stream live playit relay logs in this terminal (read-only)',
83
+ ' playit - Shortcut for tunnel log (live playit.gg logs)',
84
+ ' tunnel stop - Stop the playit tunnel agent',
85
+ ' tunnel reset - Clear saved agent secret (re-claim on next tunnel)',
84
86
  '',
85
87
  colors.bold(colors.green('Backup Commands')),
86
- ' /backup create - Create a backup ZIP of the server',
87
- ' /backup list - List all available backups',
88
- ' /backup restore <id> - Restore the server from a backup ID',
88
+ ' backup create - Create a backup ZIP of the server',
89
+ ' backup list - List all available backups',
90
+ ' backup restore <id> - Restore the server from a backup ID',
89
91
  '',
90
92
  colors.bold(colors.green('Plugin Commands')),
91
- ' /plugins list - List installed plugins',
92
- ' /plugins install <url> - Download and install a plugin JAR',
93
- ' /plugins remove <name> - Remove an installed plugin',
93
+ ' plugins list - List installed plugins',
94
+ ' plugins install <url> - Download and install a plugin JAR',
95
+ ' plugins remove <name> - Remove an installed plugin',
94
96
  '',
95
97
  colors.bold(colors.green('System Commands')),
96
- ' /stats - System stats + CPU/RAM/disk of the server',
97
- ' /java [path] - Show/list Java runtimes, or set the one used to launch',
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',
101
- ' /clear - Clear the screen, scrollback and command history',
102
- ' /update - Check npm for a newer version of MCPANEL',
103
- ' /config - View active application config.json',
104
- ' /exit - Close MCPANEL server manager',
98
+ ' stats - System stats + CPU/RAM/disk of the server',
99
+ ' java [path] - Show/list Java runtimes, or set the one used to launch',
100
+ ' folder - Open the server folder in the file explorer',
101
+ ' tray - Run in background, minimize console to system tray',
102
+ ' background - Synonym for tray',
103
+ ' clear - Clear the screen, scrollback and command history',
104
+ ' update - Check npm for a newer version of MCPANEL',
105
+ ' config - View active application config.json',
106
+ ' help - Show this menu',
107
+ ' exit - Close MCPANEL server manager',
105
108
  colors.gray('──────────────────────────────────────────────\n')
106
109
  ].join('\n');
107
110
  }
@@ -129,7 +132,7 @@ class CommandRouter {
129
132
  executeInfo() {
130
133
  const server = this.configManager.getServer();
131
134
  if (!server) {
132
- return colors.failure('No server connected. Use /sync <path> to connect one.');
135
+ return colors.failure('No server connected. Use sync <path> to connect one.');
133
136
  }
134
137
  const activeInfo = this.processManager.getActiveServer(server.name);
135
138
  const statusStr = activeInfo
@@ -150,7 +153,7 @@ class CommandRouter {
150
153
  async executeStart() {
151
154
  const server = this.configManager.getServer();
152
155
  if (!server) {
153
- return colors.failure('No server connected. Use /sync <path> to connect one.');
156
+ return colors.failure('No server connected. Use sync <path> to connect one.');
154
157
  }
155
158
  if (this.processManager.getActiveServer(server.name)) {
156
159
  return colors.warning(`Server "${server.name}" is already running.`);
@@ -169,7 +172,7 @@ class CommandRouter {
169
172
  }
170
173
  try {
171
174
  await this.processManager.startServer(server.name, server.path, resolvedJar, server.ram, this.configManager.getConfig().defaultJavaPath);
172
- return colors.success(`Server "${server.name}" started. Use /log to watch live logs or /console to enter the console.`);
175
+ return colors.success(`Server "${server.name}" started. Use log to watch live logs or console to enter the console.`);
173
176
  }
174
177
  catch (err) {
175
178
  return colors.failure(`Failed to start server: ${err.message}`);
@@ -436,7 +439,7 @@ class CommandRouter {
436
439
  // If this says "Not saved", /tunnel will re-claim a new agent every run.
437
440
  const hasSecret = !!this.playitManager.getSecret();
438
441
  output.push('');
439
- output.push(`Agent secret: ${hasSecret ? colors.green('Saved (will reuse this agent)') : colors.red('Not saved — /tunnel will claim a new agent')}`);
442
+ output.push(`Agent secret: ${hasSecret ? colors.green('Saved (will reuse this agent)') : colors.red('Not saved — tunnel will claim a new agent')}`);
440
443
  output.push(colors.gray(`Config file: ${this.configManager.getConfigPath()}`));
441
444
  output.push('');
442
445
  return output.join('\n');
@@ -463,7 +466,7 @@ class CommandRouter {
463
466
  lines.push(` ${colors.green(j.version.padEnd(10))} ${j.path}`);
464
467
  }
465
468
  lines.push('');
466
- lines.push(colors.gray('Switch with: /java <path>'));
469
+ lines.push(colors.gray('Switch with: java <path>'));
467
470
  }
468
471
  lines.push('');
469
472
  return lines.join('\n');
@@ -474,7 +477,7 @@ class CommandRouter {
474
477
  return colors.failure(`No working Java found at "${cleanPath}".`);
475
478
  }
476
479
  this.configManager.updateSettings({ defaultJavaPath: cleanPath });
477
- return colors.success(`Java set to "${cleanPath}" (version ${info.version}). It will be used on the next /start.`);
480
+ return colors.success(`Java set to "${cleanPath}" (version ${info.version}). It will be used on the next start.`);
478
481
  }
479
482
  /**
480
483
  * Executes /update — checks npm for a newer version and prints how to update.
@@ -541,7 +544,7 @@ class CommandRouter {
541
544
  }
542
545
  catch (err) {
543
546
  if (err.message && err.message.includes('NotAllowedWithReadOnly')) {
544
- return colors.failure('The agent secret is read-only. Run /tunnel reset and try again to re-claim it.');
547
+ return colors.failure('The agent secret is read-only. Run tunnel reset and try again to re-claim it.');
545
548
  }
546
549
  return colors.failure(`Failed to create tunnel: ${err.message}`);
547
550
  }
@@ -553,7 +556,7 @@ class CommandRouter {
553
556
  */
554
557
  async executeSetup() {
555
558
  if (this.playitManager.getSecret()) {
556
- return colors.warning('Playit is already set up (agent secret saved). Run /tunnel reset first if you want to re-claim.');
559
+ return colors.warning('Playit is already set up (agent secret saved). Run tunnel reset first if you want to re-claim.');
557
560
  }
558
561
  try {
559
562
  await this.playitManager.ensureSecret({
@@ -570,7 +573,7 @@ class CommandRouter {
570
573
  },
571
574
  onStatus: (msg) => console.log(colors.info(msg)),
572
575
  });
573
- return colors.success('Playit is set up! You can now run /tunnel java or /tunnel bedrock.');
576
+ return colors.success('Playit is set up! You can now run tunnel java or tunnel bedrock.');
574
577
  }
575
578
  catch (err) {
576
579
  return colors.failure(`Setup failed: ${err.message}`);
@@ -226,7 +226,7 @@ class ConfigManager {
226
226
  // a brand-new agent (and orphaning the old one) on the next launch.
227
227
  if (!this.isPlayitSecretPersisted(secret)) {
228
228
  throw new Error(`Could not persist the playit agent secret to ${CONFIG_PATH}. ` +
229
- `Check that the folder exists and is writable, then run /tunnel again.`);
229
+ `Check that the folder exists and is writable, then run tunnel again.`);
230
230
  }
231
231
  }
232
232
  /** Confirms the agent secret is readable back from disk (not just in memory). */
package/dist/index.js CHANGED
@@ -164,23 +164,24 @@ async function ensurePlayitSetup() {
164
164
  // Master list of command templates — single source of truth for both
165
165
  // tab-completion and "did you mean" suggestions.
166
166
  const COMMAND_LIST = [
167
- '/help', '/start', '/stop', '/restart', '/console', '/log', '/info', '/sync',
168
- '/stats', '/folder', '/properties', '/java',
169
- '/backup create', '/backup list', '/backup restore',
170
- '/plugins list', '/plugins install', '/plugins remove',
171
- '/setup',
172
- '/tunnel java', '/tunnel bedrock', '/tunnel status', '/tunnel stop', '/tunnel reset',
173
- '/config', '/clear', '/update', '/tray', '/background', '/exit'
167
+ 'help', 'start', 'stop', 'restart', 'console', 'log', 'info', 'sync',
168
+ 'stats', 'folder', 'properties', 'java',
169
+ 'backup create', 'backup list', 'backup restore',
170
+ 'plugins list', 'plugins install', 'plugins remove',
171
+ 'setup',
172
+ 'tunnel java', 'tunnel bedrock', 'tunnel status', 'tunnel log', 'tunnel stop', 'tunnel reset',
173
+ 'playit',
174
+ 'config', 'clear', 'update', 'tray', 'background', 'exit'
174
175
  ];
175
176
  // Subcommands offered once "<command> " has been typed.
176
177
  const SUBCOMMANDS = {
177
- '/tunnel': ['java', 'bedrock', 'status', 'stop', 'reset'],
178
- '/backup': ['create', 'list', 'restore'],
179
- '/plugins': ['list', 'install', 'remove'],
178
+ 'tunnel': ['java', 'bedrock', 'status', 'log', 'stop', 'reset'],
179
+ 'backup': ['create', 'list', 'restore'],
180
+ 'plugins': ['list', 'install', 'remove'],
180
181
  };
181
182
  /** Returns top-level commands that share a prefix with the typed token. */
182
183
  function suggestCommands(token) {
183
- if (!token || !token.startsWith('/'))
184
+ if (!token)
184
185
  return [];
185
186
  const tops = Array.from(new Set(COMMAND_LIST.map(c => c.split(' ')[0])));
186
187
  return tops.filter(c => c.startsWith(token) && c !== token);
@@ -196,8 +197,8 @@ function completer(line) {
196
197
  const parts = lineTrimmed.split(/\s+/);
197
198
  const cmd = parts[0];
198
199
  const arg = parts.slice(1).join(' ');
199
- // Completing the command word itself (e.g. "/cl" -> "/clear")
200
- if (line.startsWith('/') && !line.includes(' ')) {
200
+ // Completing the command word itself (e.g. "cl" -> "clear")
201
+ if (!line.includes(' ')) {
201
202
  const hits = COMMAND_LIST.filter(c => c.startsWith(lineTrimmed));
202
203
  return [hits.length ? hits : COMMAND_LIST, line];
203
204
  }
@@ -230,7 +231,8 @@ function loadHistory() {
230
231
  }
231
232
  }
232
233
  function saveHistoryLine(line) {
233
- if (!line || line.trim().length === 0 || line.startsWith('/exit'))
234
+ const t = line.trim();
235
+ if (!t || t === 'exit' || t === '/exit')
234
236
  return;
235
237
  try {
236
238
  fs.appendFileSync(HISTORY_PATH, `${line.trim()}\n`, 'utf-8');
@@ -287,6 +289,17 @@ function exitLogView() {
287
289
  console.log(colors.info('\nReturned to MCPANEL shell.'));
288
290
  promptUser();
289
291
  }
292
+ /**
293
+ * Exits the in-place live tunnel-log view.
294
+ */
295
+ function exitTunnelLogView() {
296
+ if (currentState !== 'TUNNEL_LOG_VIEW')
297
+ return;
298
+ playitManager.unregisterTunnelStream();
299
+ currentState = 'COMMAND';
300
+ console.log(colors.info('\nReturned to MCPANEL shell.'));
301
+ promptUser();
302
+ }
290
303
  /**
291
304
  * Prompt loop builder
292
305
  */
@@ -313,7 +326,7 @@ function promptUser() {
313
326
  rl.setPrompt(colors.bold(`Enter new value for ${propertiesContext.selectedKey}: `));
314
327
  rl.prompt();
315
328
  }
316
- else if (currentState === 'CONSOLE' || currentState === 'LOG_VIEW') {
329
+ else if (currentState === 'CONSOLE' || currentState === 'LOG_VIEW' || currentState === 'TUNNEL_LOG_VIEW') {
317
330
  // Log/console streaming has no custom prompt.
318
331
  rl.setPrompt('');
319
332
  }
@@ -338,7 +351,7 @@ function showPropertiesMenu() {
338
351
  function startPropertiesEditor() {
339
352
  const server = configManager.getServer();
340
353
  if (!server) {
341
- console.log(colors.failure('No server connected. Use /sync <path>.'));
354
+ console.log(colors.failure('No server connected. Use sync <path>.'));
342
355
  currentState = 'COMMAND';
343
356
  promptUser();
344
357
  return;
@@ -373,7 +386,7 @@ function enterConsoleMode() {
373
386
  return;
374
387
  }
375
388
  if (!processManager.getActiveServer(server.name)) {
376
- console.log(colors.failure(`Server "${server.name}" is not running. Start it first using /start.`));
389
+ console.log(colors.failure(`Server "${server.name}" is not running. Start it first using start.`));
377
390
  currentState = 'COMMAND';
378
391
  promptUser();
379
392
  return;
@@ -393,8 +406,8 @@ function enterConsoleMode() {
393
406
  });
394
407
  }
395
408
  /**
396
- * /log — opens live server logs in a NEW terminal window (tail -f). Falls back
397
- * to a read-only in-place stream if no terminal emulator could be launched.
409
+ * /log — streams live server logs read-only inside THIS terminal (like
410
+ * /console, but without sending commands). Type /back or /exit to return.
398
411
  */
399
412
  function handleLogCommand() {
400
413
  const server = configManager.getServer();
@@ -403,7 +416,6 @@ function handleLogCommand() {
403
416
  return;
404
417
  }
405
418
  const logPath = logger_1.logger.getServerLogPath(server.name);
406
- // Ensure the file exists so `tail -f` has something to follow.
407
419
  if (!fs.existsSync(logPath)) {
408
420
  try {
409
421
  fs.writeFileSync(logPath, '', 'utf-8');
@@ -411,27 +423,41 @@ function handleLogCommand() {
411
423
  catch { /* ignore */ }
412
424
  }
413
425
  const running = !!processManager.getActiveServer(server.name);
414
- const opened = (0, helpers_1.openTerminalTail)(logPath, `MCPANEL Logs - ${server.name}`);
415
- if (opened) {
416
- console.log(colors.success('Live server logs opened in a new terminal window.'));
417
- if (!running) {
418
- console.log(colors.warning('Server is not running yet — log lines will appear once you /start it.'));
419
- }
420
- return;
421
- }
422
- // Fallback: stream the logs read-only inside this shell.
423
- console.log(colors.warning('Could not open a separate terminal window — showing logs here instead.'));
424
426
  logViewServer = server.name;
425
427
  currentState = 'LOG_VIEW';
426
- console.log(colors.bold(colors.magenta(`\n--- Live Logs: ${server.name} (type /back to return) ---`)));
428
+ console.log(colors.bold(colors.magenta(`\n--- Live Server Logs: ${server.name} ---`)));
429
+ console.log(colors.gray('Read-only. Type back or exit to return to MCPANEL shell.\n'));
427
430
  if (fs.existsSync(logPath)) {
428
431
  const logs = fs.readFileSync(logPath, 'utf-8').split('\n');
429
432
  process.stdout.write(logs.slice(-30).join('\n') + '\n');
430
433
  }
434
+ if (!running) {
435
+ console.log(colors.warning('Server is not running yet — lines will appear once you start it.'));
436
+ }
431
437
  processManager.registerConsoleStream(server.name, (data) => {
432
438
  process.stdout.write(data);
433
439
  });
434
440
  }
441
+ /**
442
+ * /tunnel log — streams the live playit relay log read-only in THIS terminal.
443
+ * Seeds from tunnel.log, then follows the running relay's output. /back to exit.
444
+ */
445
+ function enterTunnelLogView() {
446
+ const logPath = logger_1.logger.getTunnelLogPath();
447
+ currentState = 'TUNNEL_LOG_VIEW';
448
+ console.log(colors.bold(colors.magenta('\n--- Live Tunnel Logs (playit relay) ---')));
449
+ console.log(colors.gray('Read-only. Type back or exit to return to MCPANEL shell.\n'));
450
+ if (fs.existsSync(logPath)) {
451
+ const logs = fs.readFileSync(logPath, 'utf-8').split('\n');
452
+ process.stdout.write(logs.slice(-30).join('\n') + '\n');
453
+ }
454
+ if (!playitManager.isAgentRunning()) {
455
+ console.log(colors.warning('Tunnel agent is not running — start it with tunnel java or tunnel bedrock.'));
456
+ }
457
+ playitManager.registerTunnelStream((data) => {
458
+ process.stdout.write(data);
459
+ });
460
+ }
435
461
  /**
436
462
  * Command line loop orchestrator
437
463
  */
@@ -444,11 +470,11 @@ async function handleLine(line) {
444
470
  break;
445
471
  case 'WIZARD_SYNC_PATH': {
446
472
  if (!trimmed) {
447
- console.log(colors.failure('Please enter a folder path (or type /exit to quit).'));
473
+ console.log(colors.failure('Please enter a folder path (or type exit to quit).'));
448
474
  promptUser();
449
475
  break;
450
476
  }
451
- if (trimmed === '/exit') {
477
+ if (trimmed === 'exit' || trimmed === '/exit') {
452
478
  process.exit(0);
453
479
  }
454
480
  try {
@@ -526,11 +552,17 @@ async function handleLine(line) {
526
552
  }
527
553
  break;
528
554
  case 'LOG_VIEW':
529
- // Read-only: only /back or /exit leaves; everything else is ignored.
530
- if (trimmed === '/exit' || trimmed === '/back') {
555
+ // Read-only: only back or exit leaves; everything else is ignored.
556
+ if (trimmed === 'exit' || trimmed === 'back' || trimmed === '/exit' || trimmed === '/back') {
531
557
  exitLogView();
532
558
  }
533
559
  break;
560
+ case 'TUNNEL_LOG_VIEW':
561
+ // Read-only: only back or exit leaves; everything else is ignored.
562
+ if (trimmed === 'exit' || trimmed === 'back' || trimmed === '/exit' || trimmed === '/back') {
563
+ exitTunnelLogView();
564
+ }
565
+ break;
534
566
  }
535
567
  }
536
568
  /**
@@ -542,18 +574,14 @@ async function handleCommandState(line) {
542
574
  return;
543
575
  }
544
576
  const parts = line.split(/\s+/);
545
- const cmd = parts[0].toLowerCase();
577
+ // Commands have no leading slash, but tolerate one for muscle memory / old history.
578
+ const cmd = parts[0].toLowerCase().replace(/^\//, '');
546
579
  const args = parts.slice(1);
547
- if (!line.startsWith('/')) {
548
- console.log(colors.failure(`Unknown command: "${line}". All commands must start with "/". Type /help for assistance.`));
549
- promptUser();
550
- return;
551
- }
552
580
  switch (cmd) {
553
- case '/help':
581
+ case 'help':
554
582
  console.log(router.getHelpText());
555
583
  break;
556
- case '/clear':
584
+ case 'clear':
557
585
  try {
558
586
  fs.writeFileSync(HISTORY_PATH, '', 'utf-8');
559
587
  if (rl) {
@@ -568,8 +596,8 @@ async function handleCommandState(line) {
568
596
  console.log(colors.failure(`Failed to clear: ${err.message}`));
569
597
  }
570
598
  break;
571
- case '/tray':
572
- case '/background': {
599
+ case 'tray':
600
+ case 'background': {
573
601
  console.log(colors.info('\nPutting MCPANEL in the background...'));
574
602
  console.log(colors.gray('The terminal window will be hidden. Use the system tray icon to restore it.'));
575
603
  const success = trayManager.hideConsole();
@@ -578,7 +606,7 @@ async function handleCommandState(line) {
578
606
  }
579
607
  break;
580
608
  }
581
- case '/exit':
609
+ case 'exit':
582
610
  logger_1.logger.info('Exiting MCPANEL manager.');
583
611
  playitManager.stopTunnel();
584
612
  console.log(colors.cyan('\nStopping the server if running...'));
@@ -591,57 +619,60 @@ async function handleCommandState(line) {
591
619
  console.log(colors.success('Goodbye!'));
592
620
  process.exit(0);
593
621
  break;
594
- case '/sync':
622
+ case 'sync':
595
623
  if (args.length === 0) {
596
- console.log(colors.failure('Syntax: /sync <path-to-server-folder>'));
624
+ console.log(colors.failure('Syntax: sync <path-to-server-folder>'));
597
625
  }
598
626
  else {
599
627
  console.log(router.executeSync(args.join(' ')));
600
628
  }
601
629
  break;
602
- case '/info':
603
- case '/path':
630
+ case 'info':
631
+ case 'path':
604
632
  console.log(router.executeInfo());
605
633
  break;
606
- case '/start':
634
+ case 'start':
607
635
  console.log(colors.cyan('Starting server...'));
608
636
  console.log(await router.executeStart());
609
637
  break;
610
- case '/stop':
638
+ case 'stop':
611
639
  console.log(colors.cyan('Stopping server...'));
612
640
  console.log(await router.executeStop());
613
641
  break;
614
- case '/restart':
642
+ case 'restart':
615
643
  console.log(colors.cyan('Restarting server...'));
616
644
  console.log(await router.executeRestart());
617
645
  break;
618
- case '/console':
646
+ case 'console':
619
647
  enterConsoleMode();
620
648
  break;
621
- case '/log':
649
+ case 'log':
622
650
  handleLogCommand();
623
651
  break;
624
- case '/stats':
652
+ case 'playit':
653
+ enterTunnelLogView();
654
+ break;
655
+ case 'stats':
625
656
  console.log(await router.executeStats());
626
657
  break;
627
- case '/folder':
658
+ case 'folder':
628
659
  console.log(router.executeFolder());
629
660
  break;
630
- case '/properties':
661
+ case 'properties':
631
662
  startPropertiesEditor();
632
663
  break;
633
- case '/java':
664
+ case 'java':
634
665
  console.log(router.executeJava(args.length ? args.join(' ') : undefined));
635
666
  break;
636
- case '/update':
667
+ case 'update':
637
668
  console.log(await router.executeUpdate());
638
669
  break;
639
- case '/config':
670
+ case 'config':
640
671
  console.log(router.executeConfig());
641
672
  break;
642
- case '/backup':
673
+ case 'backup':
643
674
  if (args.length === 0) {
644
- console.log(colors.failure('Syntax: /backup [create|list|restore]'));
675
+ console.log(colors.failure('Syntax: backup [create|list|restore]'));
645
676
  }
646
677
  else if (args[0].toLowerCase() === 'create') {
647
678
  console.log(router.executeBackupCreate());
@@ -651,44 +682,44 @@ async function handleCommandState(line) {
651
682
  }
652
683
  else if (args[0].toLowerCase() === 'restore') {
653
684
  if (!args[1])
654
- console.log(colors.failure('Syntax: /backup restore <backup-id>'));
685
+ console.log(colors.failure('Syntax: backup restore <backup-id>'));
655
686
  else
656
687
  console.log(router.executeBackupRestore(args[1]));
657
688
  }
658
689
  else {
659
- console.log(colors.failure('Syntax: /backup [create|list|restore]'));
690
+ console.log(colors.failure('Syntax: backup [create|list|restore]'));
660
691
  }
661
692
  break;
662
- case '/plugins':
693
+ case 'plugins':
663
694
  if (args.length === 0) {
664
- console.log(colors.failure('Syntax: /plugins [list|install|remove]'));
695
+ console.log(colors.failure('Syntax: plugins [list|install|remove]'));
665
696
  }
666
697
  else if (args[0].toLowerCase() === 'list') {
667
698
  console.log(router.executePluginsList());
668
699
  }
669
700
  else if (args[0].toLowerCase() === 'install') {
670
701
  if (!args[1])
671
- console.log(colors.failure('Syntax: /plugins install <plugin-url>'));
702
+ console.log(colors.failure('Syntax: plugins install <plugin-url>'));
672
703
  else
673
704
  console.log(await router.executePluginsInstall(args[1]));
674
705
  }
675
706
  else if (args[0].toLowerCase() === 'remove') {
676
707
  if (!args[1])
677
- console.log(colors.failure('Syntax: /plugins remove <plugin-name>'));
708
+ console.log(colors.failure('Syntax: plugins remove <plugin-name>'));
678
709
  else
679
710
  console.log(router.executePluginsRemove(args[1]));
680
711
  }
681
712
  else {
682
- console.log(colors.failure('Syntax: /plugins [list|install|remove]'));
713
+ console.log(colors.failure('Syntax: plugins [list|install|remove]'));
683
714
  }
684
715
  break;
685
- case '/setup':
716
+ case 'setup':
686
717
  console.log(await router.executeSetup());
687
718
  break;
688
- case '/tunnel': {
719
+ case 'tunnel': {
689
720
  const sub = (args[0] || '').toLowerCase();
690
721
  if (!sub) {
691
- console.log(colors.failure('Syntax: /tunnel [java|bedrock|status|stop|reset]'));
722
+ console.log(colors.failure('Syntax: tunnel [java|bedrock|status|log|stop|reset]'));
692
723
  }
693
724
  else if (sub === 'java' || sub === 'bedrock') {
694
725
  console.log(await router.executeTunnelCreate(sub));
@@ -708,11 +739,14 @@ async function handleCommandState(line) {
708
739
  else if (sub === 'status') {
709
740
  console.log(router.executeTunnelStatus());
710
741
  }
742
+ else if (sub === 'log') {
743
+ enterTunnelLogView();
744
+ }
711
745
  else if (sub === 'reset') {
712
746
  console.log(await router.executeTunnelReset());
713
747
  }
714
748
  else {
715
- console.log(colors.failure('Syntax: /tunnel [java|bedrock|status|stop|reset]'));
749
+ console.log(colors.failure('Syntax: tunnel [java|bedrock|status|log|stop|reset]'));
716
750
  }
717
751
  break;
718
752
  }
@@ -722,7 +756,7 @@ async function handleCommandState(line) {
722
756
  console.log(colors.failure(`Unknown command: "${cmd}".`) + ' ' + colors.gray(`Did you mean: ${suggestions.join(', ')} ?`));
723
757
  }
724
758
  else {
725
- console.log(colors.failure(`Unknown command: "${cmd}". Type /help for available commands.`));
759
+ console.log(colors.failure(`Unknown command: "${cmd}". Type help for available commands.`));
726
760
  }
727
761
  break;
728
762
  }
@@ -743,7 +777,7 @@ async function finishStartup() {
743
777
  catch {
744
778
  // Continue despite download failure (tunnel will fail until resolved).
745
779
  }
746
- console.log('\nType ' + chalk_1.default.cyan('/help') + ' for available commands\n');
780
+ console.log('\nType ' + chalk_1.default.cyan('help') + ' for available commands\n');
747
781
  currentState = 'COMMAND';
748
782
  promptUser();
749
783
  }
@@ -792,7 +826,7 @@ async function main() {
792
826
  exitLogView();
793
827
  }
794
828
  else if (currentState === 'WIZARD_SYNC_PATH') {
795
- console.log(colors.info('\nType /exit to quit, or enter a server folder path.'));
829
+ console.log(colors.info('\nType exit to quit, or enter a server folder path.'));
796
830
  promptUser();
797
831
  }
798
832
  else if (currentState !== 'COMMAND') {
@@ -801,7 +835,7 @@ async function main() {
801
835
  promptUser();
802
836
  }
803
837
  else {
804
- console.log(colors.info('\nType /exit to exit MCPANEL.'));
838
+ console.log(colors.info('\nType exit to exit MCPANEL.'));
805
839
  promptUser();
806
840
  }
807
841
  });
@@ -58,6 +58,8 @@ class PlayitManager {
58
58
  playitProcess = null;
59
59
  claimProcess = null;
60
60
  tunnelStatus;
61
+ // Optional live consumer of relay output, used by the inline `/tunnel log` view.
62
+ tunnelLogCallback = null;
61
63
  constructor(configManager) {
62
64
  this.configManager = configManager;
63
65
  this.tunnelStatus = this.offlineStatus();
@@ -256,6 +258,56 @@ class PlayitManager {
256
258
  const tunnels = (rd?.tunnels || []);
257
259
  return tunnels.find((t) => t.proto === proto) || null;
258
260
  }
261
+ /**
262
+ * Reads the server's actual listen port from server.properties so the tunnel
263
+ * forwards traffic to where the server is really bound. Falls back to the
264
+ * protocol defaults (Java 25565 / Bedrock 19132) when it can't be determined.
265
+ */
266
+ getLocalPort(type) {
267
+ const fallback = type === 'java' ? 25565 : 19132;
268
+ try {
269
+ const server = this.configManager.getServer();
270
+ if (!server)
271
+ return fallback;
272
+ const txt = fs.readFileSync(path.join(server.path, 'server.properties'), 'utf-8');
273
+ const m = txt.match(/^\s*server-port\s*=\s*(\d{1,5})\s*$/m);
274
+ if (m) {
275
+ const p = parseInt(m[1], 10);
276
+ if (p > 0 && p < 65536)
277
+ return p;
278
+ }
279
+ }
280
+ catch { /* fall back to default */ }
281
+ return fallback;
282
+ }
283
+ /**
284
+ * Self-healing: if a reused tunnel's origin still points at the wrong local
285
+ * port (e.g. created when the server used a different server-port, or with the
286
+ * old hardcoded default), repoint it to the server's current port via the API.
287
+ * Returns the port the tunnel now forwards to. No-op when already correct.
288
+ */
289
+ async reconcileTunnelPort(tunnel, type, secret, callbacks = {}) {
290
+ const desired = this.getLocalPort(type);
291
+ const current = Number(tunnel?.local_port);
292
+ const currentIp = tunnel?.local_ip || '127.0.0.1';
293
+ if (current === desired && currentIp === '127.0.0.1')
294
+ return desired;
295
+ callbacks.onStatus?.(`Adjusting tunnel to match your server port (${current || '?'} → ${desired})...`);
296
+ // playit's /tunnels/update requires the full origin: tunnel_id, local_ip,
297
+ // local_port and the (required) enabled flag. agent_id is intentionally
298
+ // omitted — sending a different one is rejected (ChangingAgentIdNotAllowed),
299
+ // and omitting it keeps the tunnel on its current agent.
300
+ await this.apiPost('/tunnels/update', {
301
+ tunnel_id: tunnel.id,
302
+ local_ip: '127.0.0.1',
303
+ local_port: desired,
304
+ enabled: tunnel.disabled == null,
305
+ }, secret);
306
+ tunnel.local_port = desired;
307
+ tunnel.local_ip = '127.0.0.1';
308
+ logger_1.logger.info(`Reconciled playit tunnel ${tunnel.id} local port ${current} -> ${desired}`);
309
+ return desired;
310
+ }
259
311
  /** Creates a new tunnel via the API (replaces the broken `tunnels prepare` CLI). */
260
312
  async createApiTunnel(type, agentId, secret) {
261
313
  const body = {
@@ -268,7 +320,7 @@ class PlayitManager {
268
320
  data: {
269
321
  agent_id: agentId,
270
322
  local_ip: '127.0.0.1',
271
- local_port: type === 'java' ? 25565 : 19132,
323
+ local_port: this.getLocalPort(type),
272
324
  },
273
325
  },
274
326
  enabled: true,
@@ -331,12 +383,12 @@ class PlayitManager {
331
383
  }
332
384
  if (status === 'UserRejected') {
333
385
  this.tunnelStatus.claimUrl = null;
334
- throw new Error('Claim was rejected in the browser. Run /setup to try again.');
386
+ throw new Error('Claim was rejected in the browser. Run setup to try again.');
335
387
  }
336
388
  }
337
389
  if (!approved) {
338
390
  this.tunnelStatus.claimUrl = null;
339
- throw new Error('Timed out waiting for approval. Open the link, click Approve, then run /setup again.');
391
+ throw new Error('Timed out waiting for approval. Open the link, click Approve, then run setup again.');
340
392
  }
341
393
  // Exchange the approved code for the 64-char agent secret.
342
394
  callbacks.onStatus?.('Approved! Retrieving your agent secret...');
@@ -351,7 +403,7 @@ class PlayitManager {
351
403
  }
352
404
  if (!secret) {
353
405
  this.tunnelStatus.claimUrl = null;
354
- throw new Error('Could not retrieve the agent secret after approval. Run /setup to try again.');
406
+ throw new Error('Could not retrieve the agent secret after approval. Run setup to try again.');
355
407
  }
356
408
  this.configManager.setPlayitSecret(secret);
357
409
  this.tunnelStatus.claimUrl = null;
@@ -391,7 +443,17 @@ class PlayitManager {
391
443
  tunnel = this.findTunnel(rd, type);
392
444
  }
393
445
  if (!tunnel) {
394
- throw new Error('Tunnel was created but no public address appeared yet. Try /tunnel status shortly.');
446
+ throw new Error('Tunnel was created but no public address appeared yet. Try tunnel status shortly.');
447
+ }
448
+ }
449
+ else {
450
+ // Reusing an existing tunnel: make sure it still forwards to the server's
451
+ // current port, so the user never has to touch port config by hand.
452
+ try {
453
+ await this.reconcileTunnelPort(tunnel, type, secret, callbacks);
454
+ }
455
+ catch (err) {
456
+ logger_1.logger.warn(`Could not auto-adjust tunnel port: ${err.message}`);
395
457
  }
396
458
  }
397
459
  const { address, port } = this.tunnelAddress(tunnel);
@@ -428,7 +490,7 @@ class PlayitManager {
428
490
  }
429
491
  }
430
492
  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.`);
493
+ `Make sure the server can reach playit.gg, then try tunnel again.`);
432
494
  }
433
495
  /** Spawns the long-running daemon that relays tunnel traffic. */
434
496
  startAgent(secret) {
@@ -456,11 +518,13 @@ class PlayitManager {
456
518
  const chunk = stripAnsi(d.toString());
457
519
  logger_1.logger.logTunnel(`[stdout] ${chunk.trim()}`);
458
520
  this.parsePlayitOutput(chunk);
521
+ this.tunnelLogCallback?.(chunk);
459
522
  });
460
523
  this.playitProcess.stderr?.on('data', (d) => {
461
524
  const chunk = stripAnsi(d.toString());
462
525
  logger_1.logger.logTunnel(`[stderr] ${chunk.trim()}`);
463
526
  this.parsePlayitOutput(chunk);
527
+ this.tunnelLogCallback?.(chunk);
464
528
  });
465
529
  this.playitProcess.on('close', (code) => {
466
530
  logger_1.logger.logTunnel(`Playit relay exited with code ${code}`);
@@ -502,6 +566,18 @@ class PlayitManager {
502
566
  getStatus() {
503
567
  return this.tunnelStatus;
504
568
  }
569
+ /** True when the relay daemon process is currently running. */
570
+ isAgentRunning() {
571
+ return this.playitProcess !== null;
572
+ }
573
+ /** Streams live relay output (stdout/stderr) to the given consumer. */
574
+ registerTunnelStream(cb) {
575
+ this.tunnelLogCallback = cb;
576
+ }
577
+ /** Stops streaming live relay output. */
578
+ unregisterTunnelStream() {
579
+ this.tunnelLogCallback = null;
580
+ }
505
581
  /** Clears the saved secret so the agent can be re-claimed from scratch. */
506
582
  async resetSecret() {
507
583
  this.stopTunnel();
@@ -77,6 +77,10 @@ exports.logger = {
77
77
  // Return path to the runtime console log for the server
78
78
  return path.join(LOGS_DIR, `server-${serverName.toLowerCase()}.log`);
79
79
  },
80
+ getTunnelLogPath() {
81
+ ensureLogsDirExists();
82
+ return path.join(LOGS_DIR, 'tunnel.log');
83
+ },
80
84
  writeServerConsoleLog(serverName, data) {
81
85
  ensureLogsDirExists();
82
86
  const filePath = path.join(LOGS_DIR, `server-${serverName.toLowerCase()}.log`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@woopsy/mcpanel",
3
- "version": "2.1.4",
3
+ "version": "5.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": {