@woopsy/mcpanel 2.1.5 → 6.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 +24 -21
- package/dist/commands/commandRouter.js +41 -39
- package/dist/config/configManager.js +1 -1
- package/dist/index.js +70 -69
- package/dist/managers/playitManager.js +66 -6
- package/package.json +1 -1
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
|
108
|
-
|
|
|
109
|
-
|
|
|
110
|
-
|
|
|
111
|
-
|
|
|
112
|
-
|
|
|
113
|
-
|
|
|
114
|
-
|
|
|
115
|
-
|
|
|
116
|
-
|
|
|
117
|
-
|
|
|
118
|
-
|
|
|
119
|
-
|
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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
|
-
-
|
|
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 (
|
|
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
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
72
|
-
'
|
|
73
|
-
'
|
|
74
|
-
'
|
|
75
|
-
'
|
|
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
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
'
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
-
'
|
|
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
|
-
'
|
|
88
|
-
'
|
|
89
|
-
'
|
|
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
|
-
'
|
|
93
|
-
'
|
|
94
|
-
'
|
|
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
|
-
'
|
|
98
|
-
'
|
|
99
|
-
'
|
|
100
|
-
'
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
'
|
|
104
|
-
'
|
|
105
|
-
'
|
|
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
|
|
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
|
|
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
|
|
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 —
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
'
|
|
168
|
-
'
|
|
169
|
-
'
|
|
170
|
-
'
|
|
171
|
-
'
|
|
172
|
-
'
|
|
173
|
-
'
|
|
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
|
-
'
|
|
178
|
-
'
|
|
179
|
-
'
|
|
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
|
|
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. "
|
|
200
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 '
|
|
581
|
+
case 'help':
|
|
584
582
|
console.log(router.getHelpText());
|
|
585
583
|
break;
|
|
586
|
-
case '
|
|
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 '
|
|
602
|
-
case '
|
|
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 '
|
|
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 '
|
|
622
|
+
case 'sync':
|
|
625
623
|
if (args.length === 0) {
|
|
626
|
-
console.log(colors.failure('Syntax:
|
|
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 '
|
|
633
|
-
case '
|
|
630
|
+
case 'info':
|
|
631
|
+
case 'path':
|
|
634
632
|
console.log(router.executeInfo());
|
|
635
633
|
break;
|
|
636
|
-
case '
|
|
634
|
+
case 'start':
|
|
637
635
|
console.log(colors.cyan('Starting server...'));
|
|
638
636
|
console.log(await router.executeStart());
|
|
639
637
|
break;
|
|
640
|
-
case '
|
|
638
|
+
case 'stop':
|
|
641
639
|
console.log(colors.cyan('Stopping server...'));
|
|
642
640
|
console.log(await router.executeStop());
|
|
643
641
|
break;
|
|
644
|
-
case '
|
|
642
|
+
case 'restart':
|
|
645
643
|
console.log(colors.cyan('Restarting server...'));
|
|
646
644
|
console.log(await router.executeRestart());
|
|
647
645
|
break;
|
|
648
|
-
case '
|
|
646
|
+
case 'console':
|
|
649
647
|
enterConsoleMode();
|
|
650
648
|
break;
|
|
651
|
-
case '
|
|
649
|
+
case 'log':
|
|
652
650
|
handleLogCommand();
|
|
653
651
|
break;
|
|
654
|
-
case '
|
|
652
|
+
case 'playit':
|
|
653
|
+
enterTunnelLogView();
|
|
654
|
+
break;
|
|
655
|
+
case 'stats':
|
|
655
656
|
console.log(await router.executeStats());
|
|
656
657
|
break;
|
|
657
|
-
case '
|
|
658
|
+
case 'folder':
|
|
658
659
|
console.log(router.executeFolder());
|
|
659
660
|
break;
|
|
660
|
-
case '
|
|
661
|
+
case 'properties':
|
|
661
662
|
startPropertiesEditor();
|
|
662
663
|
break;
|
|
663
|
-
case '
|
|
664
|
+
case 'java':
|
|
664
665
|
console.log(router.executeJava(args.length ? args.join(' ') : undefined));
|
|
665
666
|
break;
|
|
666
|
-
case '
|
|
667
|
+
case 'update':
|
|
667
668
|
console.log(await router.executeUpdate());
|
|
668
669
|
break;
|
|
669
|
-
case '
|
|
670
|
+
case 'config':
|
|
670
671
|
console.log(router.executeConfig());
|
|
671
672
|
break;
|
|
672
|
-
case '
|
|
673
|
+
case 'backup':
|
|
673
674
|
if (args.length === 0) {
|
|
674
|
-
console.log(colors.failure('Syntax:
|
|
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:
|
|
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:
|
|
690
|
+
console.log(colors.failure('Syntax: backup [create|list|restore]'));
|
|
690
691
|
}
|
|
691
692
|
break;
|
|
692
|
-
case '
|
|
693
|
+
case 'plugins':
|
|
693
694
|
if (args.length === 0) {
|
|
694
|
-
console.log(colors.failure('Syntax:
|
|
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:
|
|
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:
|
|
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:
|
|
713
|
+
console.log(colors.failure('Syntax: plugins [list|install|remove]'));
|
|
713
714
|
}
|
|
714
715
|
break;
|
|
715
|
-
case '
|
|
716
|
+
case 'setup':
|
|
716
717
|
console.log(await router.executeSetup());
|
|
717
718
|
break;
|
|
718
|
-
case '
|
|
719
|
+
case 'tunnel': {
|
|
719
720
|
const sub = (args[0] || '').toLowerCase();
|
|
720
721
|
if (!sub) {
|
|
721
|
-
console.log(colors.failure('Syntax:
|
|
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:
|
|
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
|
|
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('
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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": "
|
|
3
|
+
"version": "6.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": {
|