icoa-cli 2.19.80 → 2.19.82

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.
@@ -272,8 +272,125 @@ export function registerEnvCommand(program) {
272
272
  const envCmd = program.command('env').description('Manage competition environment');
273
273
  envCmd.command('status').alias('check').description('Check all 109 tools').action(() => showStatus());
274
274
  envCmd.command('setup').description('Install all Python libraries + system tools').action(async () => { await installAll(); });
275
+ envCmd.command('python').description('Show Python 3.12 install guide for your platform').action(() => showPythonInstallGuide());
275
276
  envCmd.action(() => showStatus());
276
277
  }
278
+ // Lightweight Python 3.12 install guide for Selection mode (no 109-tool check).
279
+ // Detects distro on Linux so contestants get the right command the first time.
280
+ function showPythonInstallGuide() {
281
+ const os = platform();
282
+ console.log();
283
+ console.log(chalk.bold.white(' Python 3.12 — Installation Guide'));
284
+ console.log(chalk.gray(' ─────────────────────────────────────────────'));
285
+ console.log();
286
+ let currentPy = '';
287
+ let pyStatus = 'missing';
288
+ // Prefer the versioned binary if available. On macOS with Homebrew, `python3`
289
+ // often points to the latest (e.g. 3.13) while python@3.12 is also installed
290
+ // at /opt/homebrew/opt/python@3.12/bin/python3.12. Don't force the user to
291
+ // reinstall just because the default changed.
292
+ const pyProbes = [
293
+ 'python3.12 --version',
294
+ '/opt/homebrew/opt/python@3.12/bin/python3.12 --version',
295
+ '/usr/local/opt/python@3.12/bin/python3.12 --version',
296
+ 'python3 --version',
297
+ ];
298
+ for (const cmd of pyProbes) {
299
+ try {
300
+ const out = execSync(cmd, { encoding: 'utf-8', timeout: 3000 }).trim();
301
+ const ver = out.replace('Python ', '');
302
+ const parts = ver.split('.').map(Number);
303
+ // Only accept as "ok" if this probe returned 3.12. Otherwise keep trying.
304
+ if (parts[0] === 3 && parts[1] === 12) {
305
+ currentPy = ver;
306
+ pyStatus = 'ok';
307
+ break;
308
+ }
309
+ // Remember the last-seen version for fallback reporting
310
+ currentPy = ver;
311
+ if (parts[0] === 3 && parts[1] >= 10 && parts[1] < 12)
312
+ pyStatus = 'old';
313
+ else if (parts[0] === 3 && parts[1] > 12)
314
+ pyStatus = 'new';
315
+ else
316
+ pyStatus = 'missing';
317
+ }
318
+ catch { /* try next probe */ }
319
+ }
320
+ if (pyStatus === 'ok') {
321
+ console.log(chalk.green(` ✓ Python ${currentPy} — you're good!`));
322
+ console.log(chalk.gray(' Exam toolkit ready. Next: ') + chalk.bold.cyan('exam setup'));
323
+ console.log();
324
+ return;
325
+ }
326
+ if (pyStatus === 'old') {
327
+ console.log(chalk.yellow(` ⚠ Python ${currentPy} — works, but 3.12 recommended`));
328
+ }
329
+ else if (pyStatus === 'new') {
330
+ console.log(chalk.yellow(` ⚠ Python ${currentPy} — some packages (pwntools, scapy) lack wheels`));
331
+ console.log(chalk.gray(' Downgrading to 3.12 is strongly recommended'));
332
+ }
333
+ else {
334
+ console.log(chalk.red(' ✗ Python 3 not found'));
335
+ }
336
+ console.log();
337
+ if (os === 'darwin') {
338
+ console.log(chalk.bold.white(' macOS (Homebrew)'));
339
+ console.log();
340
+ console.log(chalk.green(' brew install python@3.12'));
341
+ }
342
+ else if (os === 'linux') {
343
+ let distro = 'ubuntu';
344
+ try {
345
+ const release = execSync('cat /etc/os-release 2>/dev/null', { encoding: 'utf-8', timeout: 2000 });
346
+ if (/fedora|rhel|centos/i.test(release))
347
+ distro = 'fedora';
348
+ else if (/arch|manjaro/i.test(release))
349
+ distro = 'arch';
350
+ else
351
+ distro = 'ubuntu';
352
+ }
353
+ catch { }
354
+ if (distro === 'ubuntu') {
355
+ console.log(chalk.bold.white(' Ubuntu / Debian / Kali (deadsnakes PPA)'));
356
+ console.log();
357
+ console.log(chalk.gray(' Copy-paste this one-liner:'));
358
+ console.log();
359
+ console.log(chalk.green(' sudo add-apt-repository ppa:deadsnakes/ppa -y && \\'));
360
+ console.log(chalk.green(' sudo apt update && \\'));
361
+ console.log(chalk.green(' sudo apt install -y python3.12 python3.12-venv python3.12-distutils && \\'));
362
+ console.log(chalk.green(' sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1'));
363
+ }
364
+ else if (distro === 'fedora') {
365
+ console.log(chalk.bold.white(' Fedora / RHEL / CentOS'));
366
+ console.log();
367
+ console.log(chalk.green(' sudo dnf install -y python3.12 python3.12-pip'));
368
+ }
369
+ else if (distro === 'arch') {
370
+ console.log(chalk.bold.white(' Arch / Manjaro (via pyenv)'));
371
+ console.log();
372
+ console.log(chalk.green(' sudo pacman -S pyenv && pyenv install 3.12 && pyenv global 3.12'));
373
+ }
374
+ }
375
+ else if (os === 'win32') {
376
+ console.log(chalk.bold.white(' Windows (WSL recommended)'));
377
+ console.log();
378
+ console.log(chalk.gray(' WSL + Ubuntu (best):'));
379
+ console.log(chalk.green(' wsl --install'));
380
+ console.log(chalk.gray(' Then inside Ubuntu, run the Ubuntu commands.'));
381
+ console.log();
382
+ console.log(chalk.gray(' Or native:'));
383
+ console.log(chalk.green(' winget install Python.Python.3.12'));
384
+ }
385
+ else {
386
+ console.log(chalk.gray(' https://www.python.org/downloads/'));
387
+ }
388
+ console.log();
389
+ console.log(chalk.white(' After installing, verify:'));
390
+ console.log(chalk.bold.cyan(' env python') + chalk.gray(' # re-check Python'));
391
+ console.log(chalk.bold.cyan(' exam setup') + chalk.gray(' # install the 13 exam packages'));
392
+ console.log();
393
+ }
277
394
  function showStatus() {
278
395
  console.log();
279
396
  const os = platform();
package/dist/repl.js CHANGED
@@ -100,6 +100,39 @@ const VERSION = '2.5.1';
100
100
  // State 0 (no demo yet): recommend demo, hide exam setup / exam <token>
101
101
  // State 1 (demo done, no setup): show demo completion + setup CTA, hide token
102
102
  // State 2 (demo done + setup done): show token entry CTA
103
+ // Quick Python version check (used in Selection menu startup warning).
104
+ // Returns {ok, version, status}. Silent/defensive — any error means 'missing'.
105
+ function checkPython() {
106
+ // Probe python3.12 first — macOS Homebrew installs python@3.12 alongside a
107
+ // newer default python3, and we don't want to flag a 3.12-ready machine just
108
+ // because the default alias moved to 3.13.
109
+ const probes = [
110
+ 'python3.12 --version',
111
+ '/opt/homebrew/opt/python@3.12/bin/python3.12 --version',
112
+ '/usr/local/opt/python@3.12/bin/python3.12 --version',
113
+ 'python3 --version',
114
+ ];
115
+ let lastVersion = '';
116
+ let lastStatus = 'missing';
117
+ for (const cmd of probes) {
118
+ try {
119
+ const out = execSyncFn(cmd, { encoding: 'utf-8', timeout: 2000 }).trim();
120
+ const version = out.replace('Python ', '');
121
+ const [maj, min] = version.split('.').map(Number);
122
+ if (maj === 3 && min === 12)
123
+ return { ok: true, version, status: 'ok' };
124
+ lastVersion = version;
125
+ if (maj === 3 && min >= 10 && min < 12)
126
+ lastStatus = 'old';
127
+ else if (maj === 3 && min > 12)
128
+ lastStatus = 'new';
129
+ else
130
+ lastStatus = 'missing';
131
+ }
132
+ catch { /* try next probe */ }
133
+ }
134
+ return { ok: lastStatus !== 'missing', version: lastVersion, status: lastStatus };
135
+ }
103
136
  function printSelectionMenu() {
104
137
  const stats = getDemoStats();
105
138
  const setupDone = isExamSetupComplete();
@@ -107,6 +140,22 @@ function printSelectionMenu() {
107
140
  console.log();
108
141
  console.log(' ' + chalk.cyan.bold('[Selection Mode]'));
109
142
  console.log();
143
+ // Proactive Python check: only warn when it actually matters (user past demo
144
+ // and heading toward exam setup). Missing or version >= 3.13 shows yellow
145
+ // note pointing at `env python`. Quiet when 3.10-3.12 are installed.
146
+ if (stats.attempts > 0) {
147
+ const py = checkPython();
148
+ if (py.status === 'missing') {
149
+ console.log(chalk.yellow(' ⚠ Python not detected. For exam practical questions:'));
150
+ console.log(chalk.gray(' → ') + chalk.bold.cyan('env python') + chalk.gray(' (platform install guide)'));
151
+ console.log();
152
+ }
153
+ else if (py.status === 'new') {
154
+ console.log(chalk.yellow(` ⚠ Python ${py.version} may lack CTF wheels. Python 3.12 recommended:`));
155
+ console.log(chalk.gray(' → ') + chalk.bold.cyan('env python') + chalk.gray(' (install guide)'));
156
+ console.log();
157
+ }
158
+ }
110
159
  if (stats.attempts === 0) {
111
160
  // State 0: brand new user. Only demo matters right now.
112
161
  console.log(chalk.white(' New here? Start with ') + chalk.bold.cyan('demo') + chalk.white(' — it takes a few minutes.'));
@@ -751,7 +800,7 @@ export async function startRepl(program, resumeMode) {
751
800
  }
752
801
  const cmd = input.split(/\s+/)[0].toLowerCase();
753
802
  // ─── Mode-based command filtering ───
754
- const selectionCommands = ['exam', 'demo', 'retry', 'nations', 'next', 'prev', 'continue', 'setup', 'lang', 'ref', 'ai4ctf', 'ctf4ai', 'mark', 'unmark', 'review', 'submit'];
803
+ const selectionCommands = ['exam', 'demo', 'retry', 'nations', 'next', 'prev', 'continue', 'setup', 'lang', 'ref', 'ai4ctf', 'ctf4ai', 'mark', 'unmark', 'review', 'submit', 'env'];
755
804
  const organizerCommands = ['join', 'exam', 'demo', 'retry', 'next', 'prev', 'logout', 'setup', 'lang', 'ref', 'ctf', 'mark', 'unmark', 'review', 'submit'];
756
805
  // Selection mode allows shell commands prefixed with ! — contestants
757
806
  // need !python3, !cat, !base64 etc. for practical questions (Q31-Q40).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.80",
3
+ "version": "2.19.82",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {