cyberhub-pracenv 1.1.0 → 1.1.1

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/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ All notable changes to **cyberhub-pracenv** (`chpe`) are documented in this file
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.1.1] - 2026-06-26
9
+
10
+ ### Added
11
+ - **`changelog` command** — view this changelog from inside the platform so you
12
+ can see what has changed across versions without leaving the shell.
13
+ - **`requirements` command (alias `rq`)** — show the rank ladder and how many
14
+ more points are needed to reach the next rank. The rank thresholds now live in
15
+ one place (`ui.RANKS`) shared by `rankFor` and this view.
16
+ - **`scoreboard` command** — an operative leaderboard listing every user with
17
+ their rank, points, and solved count. A secret `sb` shortcut is also available.
18
+ - A secret `ch` shortcut for the `challenges`/`ls` listing.
19
+
20
+ ### Changed
21
+ - `help` now lists the `changelog`, `requirements`, and `scoreboard` commands.
22
+ The `ch` and `sb` shortcuts are intentionally left undocumented.
23
+
8
24
  ## [1.1.0] - 2026-06-25
9
25
 
10
26
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyberhub-pracenv",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "A beginner-friendly, fully terminal-based cybersecurity (CTF) practice platform in the ICOA terminal style.",
5
5
  "keywords": [
6
6
  "cybersecurity",
package/src/commands.js CHANGED
@@ -6,6 +6,7 @@
6
6
  //
7
7
  // The command table at the bottom drives both dispatch and the `help` listing.
8
8
 
9
+ const fs = require('fs');
9
10
  const path = require('path');
10
11
  const store = require('./store');
11
12
  const auth = require('./auth');
@@ -38,6 +39,9 @@ function cmdHelp(ctx) {
38
39
  ['get <id> <file>', 'Copy an attached file into your launch directory'],
39
40
  ['submit <id> <flag>', 'Submit a flag to solve a challenge'],
40
41
  ['stats (whoami)', 'Show your full user breakdown'],
42
+ ['scoreboard', 'Show the operative leaderboard'],
43
+ ['requirements (rq)', 'Show the points needed to rank up'],
44
+ ['changelog', 'Show what has changed across versions'],
41
45
  ['admin', 'Unlock admin mode (requires the admin password)'],
42
46
  ['clear', 'Clear the screen'],
43
47
  ['help', 'Show this help'],
@@ -445,6 +449,83 @@ function cmdClear(ctx) {
445
449
  print(ctx, c.title('CyberHub Practice Environment') + c.dim(' — type `help` for commands'));
446
450
  }
447
451
 
452
+ // Show the bundled CHANGELOG.md so operatives can see what has changed without
453
+ // leaving the platform. The markdown is lightly styled for the terminal.
454
+ function cmdChangelog(ctx) {
455
+ const file = path.join(__dirname, '..', 'CHANGELOG.md');
456
+ let text;
457
+ try {
458
+ text = fs.readFileSync(file, 'utf8');
459
+ } catch (_err) {
460
+ print(ctx, c.err('Changelog is not available.'));
461
+ return;
462
+ }
463
+ print(ctx, c.title('Changelog'));
464
+ for (const line of text.replace(/\s+$/, '').split('\n')) {
465
+ if (/^#{1,6}\s/.test(line)) {
466
+ print(ctx, c.accent(line.replace(/^#{1,6}\s+/, '')));
467
+ } else if (/^\s*[-*]\s/.test(line)) {
468
+ print(ctx, c.dim(' • ') + line.replace(/^\s*[-*]\s+/, ''));
469
+ } else {
470
+ print(ctx, line);
471
+ }
472
+ }
473
+ }
474
+
475
+ // Show the rank ladder and how far the operative is from the next rank.
476
+ function cmdRequirements(ctx) {
477
+ const profile = store.getProfile();
478
+ const points = profile.points || 0;
479
+ const current = ui.rankFor(points);
480
+
481
+ print(ctx, c.title('Rank requirements'));
482
+ const rows = ui.RANKS.map((r) => ({
483
+ cur: r.name === current ? '➤' : ' ',
484
+ rank: r.name,
485
+ points: r.min,
486
+ }));
487
+ print(ctx, ui.table(rows, [
488
+ { key: 'cur', label: ' ', color: c.ok },
489
+ { key: 'rank', label: 'Rank', color: c.accent },
490
+ { key: 'points', label: 'Min points' },
491
+ ]));
492
+
493
+ print(ctx, '');
494
+ print(ctx, ` ${c.bold('You')} : ${c.accent(current)} with ${c.ok(points)} pts`);
495
+ const next = ui.nextRank(points);
496
+ if (next) {
497
+ print(ctx, c.dim(` ${next.min - points} more point(s) to reach ${next.name}.`));
498
+ } else {
499
+ print(ctx, c.dim(' You have reached the top rank. Outstanding.'));
500
+ }
501
+ }
502
+
503
+ // Show a leaderboard of operatives. The platform uses a single local profile,
504
+ // so the scoreboard ranks that operative — but it is built to render any number
505
+ // of profiles so it keeps working if more are ever added.
506
+ function cmdScoreboard(ctx) {
507
+ const total = store.listChallenges().length;
508
+ const users = [store.getProfile()];
509
+ const rows = users
510
+ .slice()
511
+ .sort((a, b) => (b.points || 0) - (a.points || 0))
512
+ .map((u, i) => ({
513
+ pos: i + 1,
514
+ name: u.name,
515
+ rank: ui.rankFor(u.points || 0),
516
+ points: u.points || 0,
517
+ solved: `${(u.solved || []).length}/${total}`,
518
+ }));
519
+ print(ctx, c.title('Scoreboard'));
520
+ print(ctx, ui.table(rows, [
521
+ { key: 'pos', label: '#', color: c.dim },
522
+ { key: 'name', label: 'Operative', color: c.accent },
523
+ { key: 'rank', label: 'Rank' },
524
+ { key: 'points', label: 'Pts', color: c.ok },
525
+ { key: 'solved', label: 'Solved' },
526
+ ]));
527
+ }
528
+
448
529
  function cmdExit(ctx) {
449
530
  print(ctx, c.dim('Stay sharp. Goodbye.'));
450
531
  ctx.requestExit();
@@ -459,6 +540,7 @@ const COMMANDS = {
459
540
  '?': cmdHelp,
460
541
  challenges: cmdChallenges,
461
542
  ls: cmdChallenges,
543
+ ch: cmdChallenges, // secret shortcut (intentionally undocumented)
462
544
  open: cmdOpen,
463
545
  show: cmdOpen,
464
546
  files: cmdFiles,
@@ -469,6 +551,11 @@ const COMMANDS = {
469
551
  stats: cmdStats,
470
552
  whoami: cmdStats,
471
553
  profile: cmdStats,
554
+ scoreboard: cmdScoreboard,
555
+ sb: cmdScoreboard, // secret shortcut (intentionally undocumented)
556
+ requirements: cmdRequirements,
557
+ rq: cmdRequirements,
558
+ changelog: cmdChangelog,
472
559
  admin: cmdAdmin,
473
560
  login: cmdAdmin,
474
561
  logout: cmdLogout,
package/src/ui.js CHANGED
@@ -95,12 +95,30 @@ function progressBar(done, total, width = 24) {
95
95
  return `${bar} ${safeDone}/${safeTotal} ${c.dim(`(${pct}%)`)}`;
96
96
  }
97
97
 
98
+ // The rank ladder: the minimum points required to hold each rank, ascending.
99
+ // rankFor() and the in-platform `requirements` command both read from this so
100
+ // the thresholds live in exactly one place.
101
+ const RANKS = [
102
+ { name: 'Recruit', min: 0 },
103
+ { name: 'Script Kiddie', min: 100 },
104
+ { name: 'Pentester', min: 300 },
105
+ { name: 'Operator', min: 600 },
106
+ { name: 'Elite', min: 1000 },
107
+ ];
108
+
98
109
  function rankFor(points) {
99
- if (points >= 1000) return 'Elite';
100
- if (points >= 600) return 'Operator';
101
- if (points >= 300) return 'Pentester';
102
- if (points >= 100) return 'Script Kiddie';
103
- return 'Recruit';
110
+ const pts = Number(points) || 0;
111
+ let rank = RANKS[0].name;
112
+ for (const r of RANKS) {
113
+ if (pts >= r.min) rank = r.name;
114
+ }
115
+ return rank;
116
+ }
117
+
118
+ // The next rank above the given points, or null if already at the top.
119
+ function nextRank(points) {
120
+ const pts = Number(points) || 0;
121
+ return RANKS.find((r) => r.min > pts) || null;
104
122
  }
105
123
 
106
124
  module.exports = {
@@ -109,5 +127,7 @@ module.exports = {
109
127
  welcomeScreen,
110
128
  table,
111
129
  progressBar,
130
+ RANKS,
112
131
  rankFor,
132
+ nextRank,
113
133
  };