getpicked 1.0.0 → 1.1.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.
Files changed (2) hide show
  1. package/index.js +83 -18
  2. package/package.json +4 -2
package/index.js CHANGED
@@ -2,12 +2,38 @@
2
2
 
3
3
  import blessed from "blessed";
4
4
  import WebSocket from "ws";
5
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { homedir } from "node:os";
8
+ import { randomUUID } from "node:crypto";
5
9
 
6
10
  const isLocal = process.argv.includes("--local");
7
11
  const SERVER_URL = process.env.PORTO_SERVER ||
8
12
  (isLocal ? "ws://localhost:8111/ws" : "wss://shapely-insect.spcf.app/ws");
9
13
 
10
- // ─── Colors ──────────────────────────────────────────────────────
14
+ // ─── Device Identity ─────────────────────────────────────────
15
+ const IDENTITY_DIR = join(homedir(), ".getpicked");
16
+ const IDENTITY_FILE = join(IDENTITY_DIR, "identity.json");
17
+
18
+ function loadIdentity() {
19
+ try {
20
+ return JSON.parse(readFileSync(IDENTITY_FILE, "utf-8"));
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ function saveIdentity(identity) {
27
+ try {
28
+ mkdirSync(IDENTITY_DIR, { recursive: true });
29
+ writeFileSync(IDENTITY_FILE, JSON.stringify(identity, null, 2));
30
+ } catch {}
31
+ }
32
+
33
+ let identity = loadIdentity() || { device_id: randomUUID(), secret: null };
34
+ saveIdentity(identity);
35
+
36
+ // ─── Colors ──────────────────────────────────────────────────
11
37
  const C = {
12
38
  bg: "#0a0a0f",
13
39
  panel: "#111118",
@@ -25,10 +51,10 @@ const C = {
25
51
  dark: "#181820",
26
52
  };
27
53
 
28
- // ─── Screen ──────────────────────────────────────────────────────
54
+ // ─── Screen ──────────────────────────────────────────────────
29
55
  const screen = blessed.screen({
30
56
  smartCSR: true,
31
- title: "Porto",
57
+ title: "getpicked",
32
58
  fullUnicode: true,
33
59
  });
34
60
 
@@ -53,14 +79,20 @@ function clearSceneKeys() {
53
79
 
54
80
  function clearScreen() {
55
81
  clearSceneKeys();
56
- // Detach all children
57
82
  while (screen.children.length) {
58
83
  screen.children[0].detach();
59
84
  }
60
85
  }
61
86
 
62
- // Global exit
63
- screen.key(["C-c"], () => process.exit(0));
87
+ // Track active WebSocket for clean shutdown
88
+ let activeWs = null;
89
+
90
+ screen.key(["C-c"], () => {
91
+ if (activeWs) {
92
+ try { activeWs.close(); } catch {}
93
+ }
94
+ process.exit(0);
95
+ });
64
96
 
65
97
  // ═══════════════════════════════════════════════════════════════════
66
98
  // MAIN MENU
@@ -314,7 +346,6 @@ function showUsernameInput(thenJoin = false) {
314
346
  else showMainMenu();
315
347
  }
316
348
 
317
- // Single keypress handler for all input — avoids conflicts with screen.key()
318
349
  function onKeypress(ch, key) {
319
350
  if (!key) return;
320
351
 
@@ -441,11 +472,14 @@ function showLobby() {
441
472
  let myId = null;
442
473
  let pingInterval = null;
443
474
  let renderInterval = null;
475
+ let connectTimeout = null;
444
476
  let dead = false;
445
477
 
446
478
  function leave() {
447
479
  clearInterval(pingInterval);
448
480
  clearInterval(renderInterval);
481
+ clearTimeout(connectTimeout);
482
+ activeWs = null;
449
483
  if (ws) {
450
484
  try { ws.close(); } catch {}
451
485
  }
@@ -458,10 +492,24 @@ function showLobby() {
458
492
  headerBox.setContent(`{center}{${C.yellow}-fg}⟳ Connecting...{/}{/center}`);
459
493
  screen.render();
460
494
 
461
- const wsUrl = `${SERVER_URL}?username=${encodeURIComponent(username)}`;
462
- ws = new WebSocket(wsUrl);
495
+ const params = new URLSearchParams({
496
+ username,
497
+ device_id: identity.device_id,
498
+ });
499
+ if (identity.secret) params.set("secret", identity.secret);
500
+
501
+ ws = new WebSocket(`${SERVER_URL}?${params}`, { handshakeTimeout: 10000 });
502
+ activeWs = ws;
503
+
504
+ // Manual connection timeout fallback
505
+ connectTimeout = setTimeout(() => {
506
+ if (ws.readyState !== WebSocket.OPEN) {
507
+ ws.terminate();
508
+ }
509
+ }, 12000);
463
510
 
464
511
  ws.on("open", () => {
512
+ clearTimeout(connectTimeout);
465
513
  pingInterval = setInterval(() => {
466
514
  if (ws.readyState === WebSocket.OPEN) {
467
515
  ws.send(JSON.stringify({ type: "ping" }));
@@ -470,7 +518,21 @@ function showLobby() {
470
518
  });
471
519
 
472
520
  ws.on("message", (raw) => {
473
- const msg = JSON.parse(raw.toString());
521
+ let msg;
522
+ try {
523
+ msg = JSON.parse(raw.toString());
524
+ } catch {
525
+ return;
526
+ }
527
+
528
+ if (msg.type === "welcome") {
529
+ // Persist server-issued auth credentials
530
+ identity.device_id = msg.device_id;
531
+ identity.secret = msg.secret;
532
+ saveIdentity(identity);
533
+ return;
534
+ }
535
+
474
536
  if (msg.type === "lobby_state") {
475
537
  if (msg.your_id) myId = msg.your_id;
476
538
  lobbyState = msg;
@@ -481,6 +543,9 @@ function showLobby() {
481
543
 
482
544
  ws.on("close", () => {
483
545
  clearInterval(pingInterval);
546
+ clearInterval(renderInterval);
547
+ clearTimeout(connectTimeout);
548
+ activeWs = null;
484
549
  dead = true;
485
550
  headerBox.setContent(`{center}{${C.red}-fg}● Disconnected{/}{/center}`);
486
551
  footerBox.setContent(`{center}{${C.dim}-fg}Press Q or Esc to return{/}{/center}`);
@@ -489,6 +554,9 @@ function showLobby() {
489
554
 
490
555
  ws.on("error", () => {
491
556
  clearInterval(pingInterval);
557
+ clearInterval(renderInterval);
558
+ clearTimeout(connectTimeout);
559
+ activeWs = null;
492
560
  dead = true;
493
561
  headerBox.setContent(`{center}{${C.red}-fg}● Connection failed — is the server running?{/}{/center}`);
494
562
  footerBox.setContent(`{center}{${C.dim}-fg}Press Q or Esc to return{/}{/center}`);
@@ -501,7 +569,6 @@ function showLobby() {
501
569
  }, 200);
502
570
 
503
571
  // 256-color index 231 = true #ffffff white, NOT remappable by terminal themes
504
- // (palette indices 0-15 like SGR 97 are theme-dependent and render as grey)
505
572
  const W = "\x1b[38;5;231m";
506
573
  const B = "\x1b[1m";
507
574
  const R = "\x1b[0m";
@@ -513,7 +580,7 @@ function showLobby() {
513
580
 
514
581
  const elapsed = (Date.now() - stateReceivedAt) / 1000;
515
582
 
516
- // ── Header ── uses {|} for left/right split
583
+ // ── Header ──
517
584
  if (state === "waiting") {
518
585
  const t = Math.max(0, Math.ceil(time_remaining - elapsed));
519
586
  const lid = lobbyState.lobby_id;
@@ -521,7 +588,7 @@ function showLobby() {
521
588
  `{${C.green}-fg}● LOBBY{/} {${C.dim}-fg}#${lid}{/}{|}${W}${player_count}${R}{${C.dim}-fg}/${max_players}{/} {${C.yellow}-fg}⏱ ${t}s{/}`
522
589
  );
523
590
  const barW = screen.width;
524
- const filled = Math.round((player_count / max_players) * barW);
591
+ const filled = max_players > 0 ? Math.round((player_count / max_players) * barW) : 0;
525
592
  progressBox.setContent(
526
593
  `{${C.green}-fg}${"▀".repeat(Math.min(filled, barW))}{/}` +
527
594
  `{${C.dark}-fg}${"▀".repeat(Math.max(0, barW - filled))}{/}`
@@ -547,7 +614,7 @@ function showLobby() {
547
614
  progressBox.setContent(`{${C.green}-fg}${"▀".repeat(screen.width)}{/}`);
548
615
  }
549
616
 
550
- // ── Grid ── uses raw ANSI for white names
617
+ // ── Grid ──
551
618
  const colW = 26;
552
619
  const availW = gridBox.width - 4;
553
620
  const cols = Math.max(1, Math.floor(availW / colW));
@@ -567,7 +634,6 @@ function showLobby() {
567
634
  nameAnsi = `{${C.dim}-fg}`;
568
635
  }
569
636
  } else if (state === "countdown") {
570
- // All dots dark during countdown — suspense, no winner known
571
637
  dot = `{${C.dark}-fg}●{/}`;
572
638
  nameAnsi = `{${C.dark}-fg}`;
573
639
  } else if (state === "reveal") {
@@ -593,7 +659,6 @@ function showLobby() {
593
659
  : player.username;
594
660
 
595
661
  const pad = " ".repeat(Math.max(1, 22 - name.length));
596
- // Use {/} reset for tag-based styles, \x1b[0m for ANSI-based
597
662
  const resetStr = nameAnsi.startsWith("\x1b") ? R : "{/}";
598
663
  row += ` ${dot} ${nameAnsi}${name}${resetStr}${pad}`;
599
664
  c++;
@@ -612,7 +677,7 @@ function showLobby() {
612
677
 
613
678
  gridBox.setContent(lines.join("\n\n"));
614
679
 
615
- // ── Footer ── uses {|} for left/right split
680
+ // ── Footer ──
616
681
  if (state === "waiting") {
617
682
  footerBox.setContent(
618
683
  `{${C.text}-fg}You: ${W}${B}${username}${R}{|}{${C.dim}-fg}Q{/} {${C.text}-fg}leave lobby{/}`
@@ -651,5 +716,5 @@ function showLobby() {
651
716
  }
652
717
  }
653
718
 
654
- // ─── Go ──────────────────────────────────────────────────────────
719
+ // ─── Go ──────────────────────────────────────────────────────
655
720
  showMainMenu();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getpicked",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Join a lobby, enter the raffle, win the draw",
5
5
  "bin": {
6
6
  "getpicked": "./index.js"
@@ -9,9 +9,11 @@
9
9
  "keywords": ["raffle", "game", "multiplayer", "terminal", "cli"],
10
10
  "license": "MIT",
11
11
  "files": ["index.js"],
12
+ "engines": {
13
+ "node": ">=18.0.0"
14
+ },
12
15
  "dependencies": {
13
16
  "blessed": "^0.1.81",
14
- "blessed-contrib": "^4.11.0",
15
17
  "ws": "^8.16.0"
16
18
  }
17
19
  }