@woopsy/mcpanel 1.0.0 → 1.0.3

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
@@ -144,6 +144,26 @@ npm run dev # run from TypeScript (ts-node)
144
144
  npm run build # compile to dist/
145
145
  ```
146
146
 
147
+ ### Releasing (maintainers)
148
+
149
+ Publishing is automated by GitHub Actions (`.github/workflows/publish.yml`). You never
150
+ run `npm publish` by hand.
151
+
152
+ **One-time setup:**
153
+ 1. Create an npm **Automation** token: npmjs.com → Access Tokens → Generate New Token →
154
+ *Automation* (these bypass 2FA in CI).
155
+ 2. Add it to the repo: GitHub → Settings → Secrets and variables → Actions → New repository
156
+ secret → name `NPM_TOKEN`.
157
+
158
+ **Every release after that:**
159
+ ```bash
160
+ npm version patch # or minor / major — bumps package.json and creates a git tag
161
+ git push --follow-tags # pushes the commit + tag; CI builds and publishes to npm
162
+ ```
163
+
164
+ The workflow skips automatically if that version is already on npm, and `npm install -g @woopsy/mcpanel@latest`
165
+ picks up the new version once it's green.
166
+
147
167
  ---
148
168
 
149
169
  ## License
@@ -74,6 +74,7 @@ class CommandRouter {
74
74
  ' /properties - Edit server.properties interactively',
75
75
  '',
76
76
  colors.bold(colors.green('Tunnel Commands (Playit.gg)')),
77
+ ' /setup - One-time Playit account claim (browser approval)',
77
78
  ' /tunnel java - Auto-create & start a Java tunnel, returns address',
78
79
  ' /tunnel bedrock - Auto-create & start a Bedrock tunnel, returns address',
79
80
  ' /tunnel status - Check tunnel status, address and latency',
@@ -514,6 +515,36 @@ class CommandRouter {
514
515
  return colors.failure(`Failed to create tunnel: ${err.message}`);
515
516
  }
516
517
  }
518
+ /**
519
+ * Executes /setup — runs the one-time Playit agent claim (browser approval),
520
+ * saving the agent secret. Works on all platforms (HTTP claim, no playit-cli).
521
+ * After this, /tunnel java|bedrock is fully automatic.
522
+ */
523
+ async executeSetup() {
524
+ if (this.playitManager.getSecret()) {
525
+ return colors.warning('Playit is already set up (agent secret saved). Run /tunnel reset first if you want to re-claim.');
526
+ }
527
+ try {
528
+ await this.playitManager.ensureSecret({
529
+ onClaimUrl: (url) => {
530
+ const opened = (0, helpers_1.openInBrowser)(url);
531
+ console.log(`\n🔗 ${colors.bold('Approve the agent in your browser to finish setup.')}`);
532
+ if (opened) {
533
+ console.log(colors.gray('Your browser was opened automatically — sign in and click Approve.'));
534
+ }
535
+ else {
536
+ console.log(colors.gray('Open this link, sign in (free account), and click Approve:'));
537
+ }
538
+ console.log(colors.underline(colors.cyan(url)));
539
+ },
540
+ onStatus: (msg) => console.log(colors.info(msg)),
541
+ });
542
+ return colors.success('Playit is set up! You can now run /tunnel java or /tunnel bedrock.');
543
+ }
544
+ catch (err) {
545
+ return colors.failure(`Setup failed: ${err.message}`);
546
+ }
547
+ }
517
548
  /**
518
549
  * Executes /tunnel reset — clears the saved secret so the agent can be re-claimed.
519
550
  */
package/dist/index.js CHANGED
@@ -159,6 +159,7 @@ const COMMAND_LIST = [
159
159
  '/stats', '/folder', '/properties', '/java',
160
160
  '/backup create', '/backup list', '/backup restore',
161
161
  '/plugins list', '/plugins install', '/plugins remove',
162
+ '/setup',
162
163
  '/tunnel java', '/tunnel bedrock', '/tunnel status', '/tunnel stop', '/tunnel reset',
163
164
  '/config', '/clear', '/exit'
164
165
  ];
@@ -654,6 +655,9 @@ async function handleCommandState(line) {
654
655
  console.log(colors.failure('Syntax: /plugins [list|install|remove]'));
655
656
  }
656
657
  break;
658
+ case '/setup':
659
+ console.log(await router.executeSetup());
660
+ break;
657
661
  case '/tunnel': {
658
662
  const sub = (args[0] || '').toLowerCase();
659
663
  if (!sub) {
@@ -37,6 +37,7 @@ exports.PlayitManager = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const https = __importStar(require("https"));
40
+ const crypto = __importStar(require("crypto"));
40
41
  const child_process_1 = require("child_process");
41
42
  const configManager_1 = require("../config/configManager");
42
43
  const helpers_1 = require("../utils/helpers");
@@ -209,6 +210,41 @@ class PlayitManager {
209
210
  getRunData(secret) {
210
211
  return this.apiPost('/agents/rundata', {}, secret);
211
212
  }
213
+ /**
214
+ * Unauthenticated POST for the claim endpoints. Unlike apiPost it returns the
215
+ * raw { status, data } envelope and does NOT throw on `status:"fail"` — a fail
216
+ * (e.g. "CodeNotFound" while waiting for approval) is a normal polling state.
217
+ */
218
+ apiClaim(apiPath, body) {
219
+ const payload = JSON.stringify(body || {});
220
+ const url = new URL(PLAYIT_API + apiPath);
221
+ return new Promise((resolve, reject) => {
222
+ const req = https.request({
223
+ hostname: url.hostname,
224
+ path: url.pathname,
225
+ method: 'POST',
226
+ headers: {
227
+ 'Content-Type': 'application/json',
228
+ 'Content-Length': Buffer.byteLength(payload),
229
+ },
230
+ }, (res) => {
231
+ let data = '';
232
+ res.on('data', (c) => { data += c; });
233
+ res.on('end', () => {
234
+ try {
235
+ const parsed = JSON.parse(data);
236
+ resolve({ status: parsed.status, data: parsed.data });
237
+ }
238
+ catch {
239
+ reject(new Error(`Bad claim response: ${data.slice(0, 200)}`));
240
+ }
241
+ });
242
+ });
243
+ req.on('error', reject);
244
+ req.write(payload);
245
+ req.end();
246
+ });
247
+ }
212
248
  /** Picks the public address from a rundata tunnel entry. */
213
249
  tunnelAddress(tunnel) {
214
250
  const port = tunnel?.port?.from ?? tunnel?.local_port ?? 0;
@@ -262,63 +298,66 @@ class PlayitManager {
262
298
  });
263
299
  });
264
300
  }
265
- /** Ensures a write-capable agent secret, driving the one-time claim if needed. */
301
+ /**
302
+ * Ensures a write-capable agent secret, driving the one-time claim if needed.
303
+ *
304
+ * The claim is done entirely over the playit HTTP API (no playit-cli), so it
305
+ * works identically on Windows, WSL, Linux and macOS. The agent binary is only
306
+ * needed later, for the traffic relay.
307
+ */
266
308
  async ensureSecret(callbacks = {}) {
267
309
  const existing = this.getSecret();
268
310
  if (existing)
269
311
  return existing;
270
- await this.ensureBinary();
312
+ // A claim code is just a short random hex string generated client-side.
313
+ const code = crypto.randomBytes(5).toString('hex');
314
+ const claimBody = { code, agent_type: 'self-managed', version: 'mcpanel 1.0' };
271
315
  callbacks.onStatus?.('Generating a new agent claim code...');
272
- const code = (await this.runCli(['claim', 'generate'])).replace(/[^a-f0-9]/gi, '');
273
- if (!code)
274
- throw new Error('Failed to generate a claim code.');
275
- const url = (await this.runCli(['claim', 'url', code, '--name', 'mcpanel', '--type', 'self-managed'])).trim();
316
+ await this.apiClaim('/claim/setup', claimBody);
317
+ const url = `https://playit.gg/claim/${code}`;
276
318
  this.tunnelStatus.claimUrl = url;
277
319
  callbacks.onClaimUrl?.(url);
278
- callbacks.onStatus?.('Waiting for the agent to be claimed (this only happens once)...');
279
- const secret = await this.exchangeClaim(code);
320
+ // Poll setup (which also keeps the code alive) until the user approves.
321
+ callbacks.onStatus?.('Waiting for you to approve the agent in your browser (this only happens once)...');
322
+ const deadline = Date.now() + 5 * 60 * 1000; // 5 minutes
323
+ let approved = false;
324
+ while (Date.now() < deadline) {
325
+ await this.sleep(3000);
326
+ const res = await this.apiClaim('/claim/setup', claimBody);
327
+ const status = typeof res.data === 'string' ? res.data : '';
328
+ if (status === 'UserAccepted') {
329
+ approved = true;
330
+ break;
331
+ }
332
+ if (status === 'UserRejected') {
333
+ this.tunnelStatus.claimUrl = null;
334
+ throw new Error('Claim was rejected in the browser. Run /setup to try again.');
335
+ }
336
+ }
337
+ if (!approved) {
338
+ this.tunnelStatus.claimUrl = null;
339
+ throw new Error('Timed out waiting for approval. Open the link, click Approve, then run /setup again.');
340
+ }
341
+ // Exchange the approved code for the 64-char agent secret.
342
+ callbacks.onStatus?.('Approved! Retrieving your agent secret...');
343
+ let secret = '';
344
+ for (let i = 0; i < 10 && !secret; i++) {
345
+ const ex = await this.apiClaim('/claim/exchange', { code });
346
+ const match = JSON.stringify(ex.data ?? '').match(/[a-f0-9]{64}/i);
347
+ if (ex.status === 'success' && match)
348
+ secret = match[0].toLowerCase();
349
+ else
350
+ await this.sleep(2000);
351
+ }
352
+ if (!secret) {
353
+ this.tunnelStatus.claimUrl = null;
354
+ throw new Error('Could not retrieve the agent secret after approval. Run /setup to try again.');
355
+ }
280
356
  this.configManager.setPlayitSecret(secret);
281
357
  this.tunnelStatus.claimUrl = null;
282
358
  callbacks.onStatus?.('Agent claimed and linked. Secret saved — future tunnels are fully automatic.');
283
359
  return secret;
284
360
  }
285
- /** Spawns `claim exchange` and resolves once a 64-char hex secret is printed. */
286
- exchangeClaim(code) {
287
- return new Promise((resolve, reject) => {
288
- const cliPath = this.getCliPath();
289
- const child = (0, child_process_1.spawn)(cliPath, ['claim', 'exchange', code, '--wait', '0'], {
290
- cwd: path.dirname(cliPath),
291
- stdio: ['ignore', 'pipe', 'pipe'],
292
- });
293
- this.claimProcess = child;
294
- let buffer = '';
295
- const scan = (data) => {
296
- buffer += stripAnsi(data.toString());
297
- const match = buffer.match(/[a-f0-9]{64}/i);
298
- if (match) {
299
- this.claimProcess = null;
300
- try {
301
- child.kill();
302
- }
303
- catch { /* ignore */ }
304
- resolve(match[0].toLowerCase());
305
- }
306
- };
307
- child.stdout?.on('data', scan);
308
- child.stderr?.on('data', scan);
309
- child.on('close', (codeExit) => {
310
- if (this.claimProcess === child) {
311
- this.claimProcess = null;
312
- const match = buffer.match(/[a-f0-9]{64}/i);
313
- if (match)
314
- resolve(match[0].toLowerCase());
315
- else
316
- reject(new Error(`Claim was not completed (exit ${codeExit}). Visit the link, then try again.`));
317
- }
318
- });
319
- child.on('error', (err) => { this.claimProcess = null; reject(err); });
320
- });
321
- }
322
361
  // ---------------------------------------------------------------------------
323
362
  // Full automated setup
324
363
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@woopsy/mcpanel",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
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": {
@@ -18,6 +18,7 @@
18
18
  "dev": "ts-node src/index.ts",
19
19
  "prod": "node dist/index.js",
20
20
  "prepublishOnly": "npm run build",
21
+ "push": "node scripts/push.js",
21
22
  "install:global": "npm run build && npm install -g ."
22
23
  },
23
24
  "keywords": [