@woopsy/mcpanel 2.1.5 → 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,44 +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 - 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',
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 log - Stream live playit relay logs in this terminal (read-only)',
83
- ' /tunnel stop - Stop the playit tunnel agent',
84
- ' /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)',
85
86
  '',
86
87
  colors.bold(colors.green('Backup Commands')),
87
- ' /backup create - Create a backup ZIP of the server',
88
- ' /backup list - List all available backups',
89
- ' /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',
90
91
  '',
91
92
  colors.bold(colors.green('Plugin Commands')),
92
- ' /plugins list - List installed plugins',
93
- ' /plugins install <url> - Download and install a plugin JAR',
94
- ' /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',
95
96
  '',
96
97
  colors.bold(colors.green('System Commands')),
97
- ' /stats - System stats + CPU/RAM/disk of the server',
98
- ' /java [path] - Show/list Java runtimes, or set the one used to launch',
99
- ' /folder - Open the server folder in the file explorer',
100
- ' /tray - Run in background, minimize console to system tray',
101
- ' /background - Synonym for /tray',
102
- ' /clear - Clear the screen, scrollback and command history',
103
- ' /update - Check npm for a newer version of MCPANEL',
104
- ' /config - View active application config.json',
105
- ' /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',
106
108
  colors.gray('──────────────────────────────────────────────\n')
107
109
  ].join('\n');
108
110
  }
@@ -130,7 +132,7 @@ class CommandRouter {
130
132
  executeInfo() {
131
133
  const server = this.configManager.getServer();
132
134
  if (!server) {
133
- 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.');
134
136
  }
135
137
  const activeInfo = this.processManager.getActiveServer(server.name);
136
138
  const statusStr = activeInfo
@@ -151,7 +153,7 @@ class CommandRouter {
151
153
  async executeStart() {
152
154
  const server = this.configManager.getServer();
153
155
  if (!server) {
154
- 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.');
155
157
  }
156
158
  if (this.processManager.getActiveServer(server.name)) {
157
159
  return colors.warning(`Server "${server.name}" is already running.`);
@@ -170,7 +172,7 @@ class CommandRouter {
170
172
  }
171
173
  try {
172
174
  await this.processManager.startServer(server.name, server.path, resolvedJar, server.ram, this.configManager.getConfig().defaultJavaPath);
173
- 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.`);
174
176
  }
175
177
  catch (err) {
176
178
  return colors.failure(`Failed to start server: ${err.message}`);
@@ -437,7 +439,7 @@ class CommandRouter {
437
439
  // If this says "Not saved", /tunnel will re-claim a new agent every run.
438
440
  const hasSecret = !!this.playitManager.getSecret();
439
441
  output.push('');
440
- 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')}`);
441
443
  output.push(colors.gray(`Config file: ${this.configManager.getConfigPath()}`));
442
444
  output.push('');
443
445
  return output.join('\n');
@@ -464,7 +466,7 @@ class CommandRouter {
464
466
  lines.push(` ${colors.green(j.version.padEnd(10))} ${j.path}`);
465
467
  }
466
468
  lines.push('');
467
- lines.push(colors.gray('Switch with: /java <path>'));
469
+ lines.push(colors.gray('Switch with: java <path>'));
468
470
  }
469
471
  lines.push('');
470
472
  return lines.join('\n');
@@ -475,7 +477,7 @@ class CommandRouter {
475
477
  return colors.failure(`No working Java found at "${cleanPath}".`);
476
478
  }
477
479
  this.configManager.updateSettings({ defaultJavaPath: cleanPath });
478
- 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.`);
479
481
  }
480
482
  /**
481
483
  * Executes /update — checks npm for a newer version and prints how to update.
@@ -542,7 +544,7 @@ class CommandRouter {
542
544
  }
543
545
  catch (err) {
544
546
  if (err.message && err.message.includes('NotAllowedWithReadOnly')) {
545
- 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.');
546
548
  }
547
549
  return colors.failure(`Failed to create tunnel: ${err.message}`);
548
550
  }
@@ -554,7 +556,7 @@ class CommandRouter {
554
556
  */
555
557
  async executeSetup() {
556
558
  if (this.playitManager.getSecret()) {
557
- 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.');
558
560
  }
559
561
  try {
560
562
  await this.playitManager.ensureSecret({
@@ -571,7 +573,7 @@ class CommandRouter {
571
573
  },
572
574
  onStatus: (msg) => console.log(colors.info(msg)),
573
575
  });
574
- 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.');
575
577
  }
576
578
  catch (err) {
577
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 log', '/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', 'log', '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');
@@ -349,7 +351,7 @@ function showPropertiesMenu() {
349
351
  function startPropertiesEditor() {
350
352
  const server = configManager.getServer();
351
353
  if (!server) {
352
- console.log(colors.failure('No server connected. Use /sync <path>.'));
354
+ console.log(colors.failure('No server connected. Use sync <path>.'));
353
355
  currentState = 'COMMAND';
354
356
  promptUser();
355
357
  return;
@@ -384,7 +386,7 @@ function enterConsoleMode() {
384
386
  return;
385
387
  }
386
388
  if (!processManager.getActiveServer(server.name)) {
387
- 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.`));
388
390
  currentState = 'COMMAND';
389
391
  promptUser();
390
392
  return;
@@ -424,13 +426,13 @@ function handleLogCommand() {
424
426
  logViewServer = server.name;
425
427
  currentState = 'LOG_VIEW';
426
428
  console.log(colors.bold(colors.magenta(`\n--- Live Server Logs: ${server.name} ---`)));
427
- console.log(colors.gray('Read-only. Type /back or /exit to return to MCPANEL shell.\n'));
429
+ console.log(colors.gray('Read-only. Type back or exit to return to MCPANEL shell.\n'));
428
430
  if (fs.existsSync(logPath)) {
429
431
  const logs = fs.readFileSync(logPath, 'utf-8').split('\n');
430
432
  process.stdout.write(logs.slice(-30).join('\n') + '\n');
431
433
  }
432
434
  if (!running) {
433
- console.log(colors.warning('Server is not running yet — lines will appear once you /start it.'));
435
+ console.log(colors.warning('Server is not running yet — lines will appear once you start it.'));
434
436
  }
435
437
  processManager.registerConsoleStream(server.name, (data) => {
436
438
  process.stdout.write(data);
@@ -444,13 +446,13 @@ function enterTunnelLogView() {
444
446
  const logPath = logger_1.logger.getTunnelLogPath();
445
447
  currentState = 'TUNNEL_LOG_VIEW';
446
448
  console.log(colors.bold(colors.magenta('\n--- Live Tunnel Logs (playit relay) ---')));
447
- console.log(colors.gray('Read-only. Type /back or /exit to return to MCPANEL shell.\n'));
449
+ console.log(colors.gray('Read-only. Type back or exit to return to MCPANEL shell.\n'));
448
450
  if (fs.existsSync(logPath)) {
449
451
  const logs = fs.readFileSync(logPath, 'utf-8').split('\n');
450
452
  process.stdout.write(logs.slice(-30).join('\n') + '\n');
451
453
  }
452
454
  if (!playitManager.isAgentRunning()) {
453
- console.log(colors.warning('Tunnel agent is not running — start it with /tunnel java or /tunnel bedrock.'));
455
+ console.log(colors.warning('Tunnel agent is not running — start it with tunnel java or tunnel bedrock.'));
454
456
  }
455
457
  playitManager.registerTunnelStream((data) => {
456
458
  process.stdout.write(data);
@@ -468,11 +470,11 @@ async function handleLine(line) {
468
470
  break;
469
471
  case 'WIZARD_SYNC_PATH': {
470
472
  if (!trimmed) {
471
- 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).'));
472
474
  promptUser();
473
475
  break;
474
476
  }
475
- if (trimmed === '/exit') {
477
+ if (trimmed === 'exit' || trimmed === '/exit') {
476
478
  process.exit(0);
477
479
  }
478
480
  try {
@@ -550,14 +552,14 @@ async function handleLine(line) {
550
552
  }
551
553
  break;
552
554
  case 'LOG_VIEW':
553
- // Read-only: only /back or /exit leaves; everything else is ignored.
554
- 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') {
555
557
  exitLogView();
556
558
  }
557
559
  break;
558
560
  case 'TUNNEL_LOG_VIEW':
559
- // Read-only: only /back or /exit leaves; everything else is ignored.
560
- if (trimmed === '/exit' || trimmed === '/back') {
561
+ // Read-only: only back or exit leaves; everything else is ignored.
562
+ if (trimmed === 'exit' || trimmed === 'back' || trimmed === '/exit' || trimmed === '/back') {
561
563
  exitTunnelLogView();
562
564
  }
563
565
  break;
@@ -572,18 +574,14 @@ async function handleCommandState(line) {
572
574
  return;
573
575
  }
574
576
  const parts = line.split(/\s+/);
575
- 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(/^\//, '');
576
579
  const args = parts.slice(1);
577
- if (!line.startsWith('/')) {
578
- console.log(colors.failure(`Unknown command: "${line}". All commands must start with "/". Type /help for assistance.`));
579
- promptUser();
580
- return;
581
- }
582
580
  switch (cmd) {
583
- case '/help':
581
+ case 'help':
584
582
  console.log(router.getHelpText());
585
583
  break;
586
- case '/clear':
584
+ case 'clear':
587
585
  try {
588
586
  fs.writeFileSync(HISTORY_PATH, '', 'utf-8');
589
587
  if (rl) {
@@ -598,8 +596,8 @@ async function handleCommandState(line) {
598
596
  console.log(colors.failure(`Failed to clear: ${err.message}`));
599
597
  }
600
598
  break;
601
- case '/tray':
602
- case '/background': {
599
+ case 'tray':
600
+ case 'background': {
603
601
  console.log(colors.info('\nPutting MCPANEL in the background...'));
604
602
  console.log(colors.gray('The terminal window will be hidden. Use the system tray icon to restore it.'));
605
603
  const success = trayManager.hideConsole();
@@ -608,7 +606,7 @@ async function handleCommandState(line) {
608
606
  }
609
607
  break;
610
608
  }
611
- case '/exit':
609
+ case 'exit':
612
610
  logger_1.logger.info('Exiting MCPANEL manager.');
613
611
  playitManager.stopTunnel();
614
612
  console.log(colors.cyan('\nStopping the server if running...'));
@@ -621,57 +619,60 @@ async function handleCommandState(line) {
621
619
  console.log(colors.success('Goodbye!'));
622
620
  process.exit(0);
623
621
  break;
624
- case '/sync':
622
+ case 'sync':
625
623
  if (args.length === 0) {
626
- console.log(colors.failure('Syntax: /sync <path-to-server-folder>'));
624
+ console.log(colors.failure('Syntax: sync <path-to-server-folder>'));
627
625
  }
628
626
  else {
629
627
  console.log(router.executeSync(args.join(' ')));
630
628
  }
631
629
  break;
632
- case '/info':
633
- case '/path':
630
+ case 'info':
631
+ case 'path':
634
632
  console.log(router.executeInfo());
635
633
  break;
636
- case '/start':
634
+ case 'start':
637
635
  console.log(colors.cyan('Starting server...'));
638
636
  console.log(await router.executeStart());
639
637
  break;
640
- case '/stop':
638
+ case 'stop':
641
639
  console.log(colors.cyan('Stopping server...'));
642
640
  console.log(await router.executeStop());
643
641
  break;
644
- case '/restart':
642
+ case 'restart':
645
643
  console.log(colors.cyan('Restarting server...'));
646
644
  console.log(await router.executeRestart());
647
645
  break;
648
- case '/console':
646
+ case 'console':
649
647
  enterConsoleMode();
650
648
  break;
651
- case '/log':
649
+ case 'log':
652
650
  handleLogCommand();
653
651
  break;
654
- case '/stats':
652
+ case 'playit':
653
+ enterTunnelLogView();
654
+ break;
655
+ case 'stats':
655
656
  console.log(await router.executeStats());
656
657
  break;
657
- case '/folder':
658
+ case 'folder':
658
659
  console.log(router.executeFolder());
659
660
  break;
660
- case '/properties':
661
+ case 'properties':
661
662
  startPropertiesEditor();
662
663
  break;
663
- case '/java':
664
+ case 'java':
664
665
  console.log(router.executeJava(args.length ? args.join(' ') : undefined));
665
666
  break;
666
- case '/update':
667
+ case 'update':
667
668
  console.log(await router.executeUpdate());
668
669
  break;
669
- case '/config':
670
+ case 'config':
670
671
  console.log(router.executeConfig());
671
672
  break;
672
- case '/backup':
673
+ case 'backup':
673
674
  if (args.length === 0) {
674
- console.log(colors.failure('Syntax: /backup [create|list|restore]'));
675
+ console.log(colors.failure('Syntax: backup [create|list|restore]'));
675
676
  }
676
677
  else if (args[0].toLowerCase() === 'create') {
677
678
  console.log(router.executeBackupCreate());
@@ -681,44 +682,44 @@ async function handleCommandState(line) {
681
682
  }
682
683
  else if (args[0].toLowerCase() === 'restore') {
683
684
  if (!args[1])
684
- console.log(colors.failure('Syntax: /backup restore <backup-id>'));
685
+ console.log(colors.failure('Syntax: backup restore <backup-id>'));
685
686
  else
686
687
  console.log(router.executeBackupRestore(args[1]));
687
688
  }
688
689
  else {
689
- console.log(colors.failure('Syntax: /backup [create|list|restore]'));
690
+ console.log(colors.failure('Syntax: backup [create|list|restore]'));
690
691
  }
691
692
  break;
692
- case '/plugins':
693
+ case 'plugins':
693
694
  if (args.length === 0) {
694
- console.log(colors.failure('Syntax: /plugins [list|install|remove]'));
695
+ console.log(colors.failure('Syntax: plugins [list|install|remove]'));
695
696
  }
696
697
  else if (args[0].toLowerCase() === 'list') {
697
698
  console.log(router.executePluginsList());
698
699
  }
699
700
  else if (args[0].toLowerCase() === 'install') {
700
701
  if (!args[1])
701
- console.log(colors.failure('Syntax: /plugins install <plugin-url>'));
702
+ console.log(colors.failure('Syntax: plugins install <plugin-url>'));
702
703
  else
703
704
  console.log(await router.executePluginsInstall(args[1]));
704
705
  }
705
706
  else if (args[0].toLowerCase() === 'remove') {
706
707
  if (!args[1])
707
- console.log(colors.failure('Syntax: /plugins remove <plugin-name>'));
708
+ console.log(colors.failure('Syntax: plugins remove <plugin-name>'));
708
709
  else
709
710
  console.log(router.executePluginsRemove(args[1]));
710
711
  }
711
712
  else {
712
- console.log(colors.failure('Syntax: /plugins [list|install|remove]'));
713
+ console.log(colors.failure('Syntax: plugins [list|install|remove]'));
713
714
  }
714
715
  break;
715
- case '/setup':
716
+ case 'setup':
716
717
  console.log(await router.executeSetup());
717
718
  break;
718
- case '/tunnel': {
719
+ case 'tunnel': {
719
720
  const sub = (args[0] || '').toLowerCase();
720
721
  if (!sub) {
721
- console.log(colors.failure('Syntax: /tunnel [java|bedrock|status|log|stop|reset]'));
722
+ console.log(colors.failure('Syntax: tunnel [java|bedrock|status|log|stop|reset]'));
722
723
  }
723
724
  else if (sub === 'java' || sub === 'bedrock') {
724
725
  console.log(await router.executeTunnelCreate(sub));
@@ -745,7 +746,7 @@ async function handleCommandState(line) {
745
746
  console.log(await router.executeTunnelReset());
746
747
  }
747
748
  else {
748
- console.log(colors.failure('Syntax: /tunnel [java|bedrock|status|log|stop|reset]'));
749
+ console.log(colors.failure('Syntax: tunnel [java|bedrock|status|log|stop|reset]'));
749
750
  }
750
751
  break;
751
752
  }
@@ -755,7 +756,7 @@ async function handleCommandState(line) {
755
756
  console.log(colors.failure(`Unknown command: "${cmd}".`) + ' ' + colors.gray(`Did you mean: ${suggestions.join(', ')} ?`));
756
757
  }
757
758
  else {
758
- 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.`));
759
760
  }
760
761
  break;
761
762
  }
@@ -776,7 +777,7 @@ async function finishStartup() {
776
777
  catch {
777
778
  // Continue despite download failure (tunnel will fail until resolved).
778
779
  }
779
- 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');
780
781
  currentState = 'COMMAND';
781
782
  promptUser();
782
783
  }
@@ -825,7 +826,7 @@ async function main() {
825
826
  exitLogView();
826
827
  }
827
828
  else if (currentState === 'WIZARD_SYNC_PATH') {
828
- 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.'));
829
830
  promptUser();
830
831
  }
831
832
  else if (currentState !== 'COMMAND') {
@@ -834,7 +835,7 @@ async function main() {
834
835
  promptUser();
835
836
  }
836
837
  else {
837
- console.log(colors.info('\nType /exit to exit MCPANEL.'));
838
+ console.log(colors.info('\nType exit to exit MCPANEL.'));
838
839
  promptUser();
839
840
  }
840
841
  });
@@ -258,6 +258,56 @@ class PlayitManager {
258
258
  const tunnels = (rd?.tunnels || []);
259
259
  return tunnels.find((t) => t.proto === proto) || null;
260
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
+ }
261
311
  /** Creates a new tunnel via the API (replaces the broken `tunnels prepare` CLI). */
262
312
  async createApiTunnel(type, agentId, secret) {
263
313
  const body = {
@@ -270,7 +320,7 @@ class PlayitManager {
270
320
  data: {
271
321
  agent_id: agentId,
272
322
  local_ip: '127.0.0.1',
273
- local_port: type === 'java' ? 25565 : 19132,
323
+ local_port: this.getLocalPort(type),
274
324
  },
275
325
  },
276
326
  enabled: true,
@@ -333,12 +383,12 @@ class PlayitManager {
333
383
  }
334
384
  if (status === 'UserRejected') {
335
385
  this.tunnelStatus.claimUrl = null;
336
- 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.');
337
387
  }
338
388
  }
339
389
  if (!approved) {
340
390
  this.tunnelStatus.claimUrl = null;
341
- 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.');
342
392
  }
343
393
  // Exchange the approved code for the 64-char agent secret.
344
394
  callbacks.onStatus?.('Approved! Retrieving your agent secret...');
@@ -353,7 +403,7 @@ class PlayitManager {
353
403
  }
354
404
  if (!secret) {
355
405
  this.tunnelStatus.claimUrl = null;
356
- 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.');
357
407
  }
358
408
  this.configManager.setPlayitSecret(secret);
359
409
  this.tunnelStatus.claimUrl = null;
@@ -393,7 +443,17 @@ class PlayitManager {
393
443
  tunnel = this.findTunnel(rd, type);
394
444
  }
395
445
  if (!tunnel) {
396
- 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}`);
397
457
  }
398
458
  }
399
459
  const { address, port } = this.tunnelAddress(tunnel);
@@ -430,7 +490,7 @@ class PlayitManager {
430
490
  }
431
491
  }
432
492
  throw new Error(`${lastErr?.message || 'AgentVersionTooOld'} — the playit agent did not register in time. ` +
433
- `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.`);
434
494
  }
435
495
  /** Spawns the long-running daemon that relays tunnel traffic. */
436
496
  startAgent(secret) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@woopsy/mcpanel",
3
- "version": "2.1.5",
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": {