icoa-cli 2.19.67 → 2.19.69

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.
@@ -1898,6 +1898,52 @@ export function registerExamCommand(program) {
1898
1898
  console.log(chalk.gray(' Expected: 1-2 minutes · ~150MB disk'));
1899
1899
  console.log();
1900
1900
  // Step 1: Check Python 3.12+
1901
+ const printPythonInstallGuide = (reason, currentVersion) => {
1902
+ const platform = process.platform;
1903
+ console.log();
1904
+ if (reason === 'missing') {
1905
+ printError('Python 3 not found.');
1906
+ }
1907
+ else {
1908
+ printError(`Python ${currentVersion} found, but 3.12+ required for the exam.`);
1909
+ }
1910
+ console.log();
1911
+ console.log(chalk.bold.white(' How to install Python 3.12:'));
1912
+ console.log();
1913
+ if (platform === 'darwin') {
1914
+ console.log(chalk.yellow(' macOS (Homebrew, recommended):'));
1915
+ console.log(chalk.green(' brew install python@3.12'));
1916
+ console.log();
1917
+ console.log(chalk.gray(' Or download installer:'));
1918
+ console.log(chalk.gray(' https://www.python.org/downloads/macos/'));
1919
+ }
1920
+ else if (platform === 'linux') {
1921
+ console.log(chalk.yellow(' Ubuntu / Debian (deadsnakes PPA):'));
1922
+ console.log(chalk.green(' sudo add-apt-repository ppa:deadsnakes/ppa -y'));
1923
+ console.log(chalk.green(' sudo apt update'));
1924
+ console.log(chalk.green(' sudo apt install -y python3.12 python3.12-venv python3.12-distutils'));
1925
+ console.log(chalk.gray(' sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1'));
1926
+ console.log();
1927
+ console.log(chalk.yellow(' Fedora / RHEL:'));
1928
+ console.log(chalk.green(' sudo dnf install -y python3.12'));
1929
+ console.log();
1930
+ console.log(chalk.yellow(' Arch / Manjaro:'));
1931
+ console.log(chalk.green(' sudo pacman -S python'));
1932
+ }
1933
+ else if (platform === 'win32') {
1934
+ console.log(chalk.yellow(' Windows (winget, recommended):'));
1935
+ console.log(chalk.green(' winget install Python.Python.3.12'));
1936
+ console.log();
1937
+ console.log(chalk.gray(' Or download installer:'));
1938
+ console.log(chalk.gray(' https://www.python.org/downloads/windows/'));
1939
+ }
1940
+ else {
1941
+ console.log(chalk.gray(' https://www.python.org/downloads/'));
1942
+ }
1943
+ console.log();
1944
+ console.log(chalk.white(' After installing, run: ') + chalk.bold.cyan('exam setup'));
1945
+ console.log();
1946
+ };
1901
1947
  const pythonBin = 'python3';
1902
1948
  let pythonVersion = '';
1903
1949
  try {
@@ -1905,15 +1951,13 @@ export function registerExamCommand(program) {
1905
1951
  pythonVersion = raw.replace('Python ', '');
1906
1952
  const parts = pythonVersion.split('.').map(Number);
1907
1953
  if (parts[0] < 3 || (parts[0] === 3 && parts[1] < 12)) {
1908
- printError(`Python ${pythonVersion} found, but 3.12+ required.`);
1909
- printInfo('Install: https://www.python.org/downloads/');
1954
+ printPythonInstallGuide('too_old', pythonVersion);
1910
1955
  return;
1911
1956
  }
1912
1957
  console.log(chalk.green(` ✓ Python ${pythonVersion}`));
1913
1958
  }
1914
1959
  catch {
1915
- printError('Python 3 not found.');
1916
- printInfo('Install: https://www.python.org/downloads/');
1960
+ printPythonInstallGuide('missing');
1917
1961
  return;
1918
1962
  }
1919
1963
  // Step 2: Check pip
package/dist/repl.js CHANGED
@@ -6,7 +6,7 @@ import { isActivated, activateToken, isFreeCommand, isDeviceMatch, recordExit, r
6
6
  import { setReplMode } from './lib/ui.js';
7
7
  import { isChatActive, handleChatMessage } from './commands/ai4ctf.js';
8
8
  import { isCtf4aiActive, handleCtf4aiMessage } from './commands/ctf4ai-demo.js';
9
- import { getExamState } from './lib/exam-state.js';
9
+ import { getExamState, getRealExamState, getDemoState } from './lib/exam-state.js';
10
10
  import { getDemoStats } from './lib/demo-stats.js';
11
11
  import { isExamSetupComplete } from './lib/exam-setup.js';
12
12
  import { DEMO_PICK_SIZE, DEMO_POOL_SIZE } from './lib/demo-exam.js';
@@ -17,6 +17,16 @@ import { startLogSync, stopLogSync } from './lib/log-sync.js';
17
17
  import { existsSync, mkdirSync } from 'node:fs';
18
18
  import { join } from 'node:path';
19
19
  import { homedir } from 'node:os';
20
+ // Compute the REPL prompt based on current state.
21
+ // Real exam wins over demo (matches getExamState() priority). Chat modes have
22
+ // their own prompts and are handled separately.
23
+ function computePrompt() {
24
+ if (getRealExamState())
25
+ return chalk.cyan('exam> ');
26
+ if (getDemoState())
27
+ return chalk.yellow('demo> ');
28
+ return chalk.green('icoa> ');
29
+ }
20
30
  // Competition workspace — all system commands restricted here
21
31
  const WORKSPACE = join(homedir(), 'icoa-workspace');
22
32
  function ensureWorkspace() {
@@ -91,6 +101,23 @@ export async function startRepl(program, resumeMode) {
91
101
  const connected = isConnected();
92
102
  const realExit = process.exit.bind(process);
93
103
  const activated = isActivated();
104
+ // Auto-cleanup: clear demo state once per version upgrade.
105
+ // Demo is free practice with no time pressure, no scoring, no real loss
106
+ // for users. Old versions may have left demo state in incompatible formats
107
+ // (pre-v2.19.45 shared state, pre-v2.19.67 timestamp confusion, etc.).
108
+ // Real exam state is NEVER auto-cleared — it may contain in-progress answers.
109
+ if (config.demoCleanedForVersion !== VERSION) {
110
+ try {
111
+ const { existsSync, unlinkSync } = await import('node:fs');
112
+ const { join } = await import('node:path');
113
+ const { getIcoaDir } = await import('./lib/config.js');
114
+ const demoFile = join(getIcoaDir(), 'demo-state.json');
115
+ if (existsSync(demoFile))
116
+ unlinkSync(demoFile);
117
+ }
118
+ catch { }
119
+ saveConfig({ demoCleanedForVersion: VERSION });
120
+ }
94
121
  // ─── Mode selection (every launch) ───
95
122
  const { select: selectMode, confirm: confirmMode } = await import('@inquirer/prompts');
96
123
  const savedMode = config.mode || '';
@@ -334,19 +361,29 @@ export async function startRepl(program, resumeMode) {
334
361
  const rl = createInterface({
335
362
  input: process.stdin,
336
363
  output: process.stdout,
337
- prompt: chalk.green('icoa> '),
364
+ prompt: computePrompt(),
338
365
  terminal: true,
339
366
  });
340
367
  let processing = false;
341
368
  setReplMode(true);
342
369
  startLogSync();
370
+ // Wrap rl.prompt() so it always picks up current state (icoa/demo/exam mode).
371
+ // This keeps the prompt accurate after exam token entry, exam submit, demo finish, etc.
372
+ // Chat modes (ai4ctf/ctf4ai) set their own prompt via setPrompt and bypass this.
373
+ const _origPrompt = rl.prompt.bind(rl);
374
+ rl.prompt = (preserveCursor) => {
375
+ if (!isChatActive() && !isCtf4aiActive()) {
376
+ rl.setPrompt(computePrompt());
377
+ }
378
+ _origPrompt(preserveCursor);
379
+ };
343
380
  rl.prompt();
344
381
  rl.on('line', async (line) => {
345
382
  if (processing)
346
383
  return;
347
384
  const input = line.trim();
348
385
  if (!input) {
349
- rl.setPrompt(isChatActive() ? chalk.magenta('ai4ctf> ') : chalk.green('icoa> '));
386
+ rl.setPrompt(isChatActive() ? chalk.magenta('ai4ctf> ') : computePrompt());
350
387
  rl.prompt();
351
388
  return;
352
389
  }
@@ -356,7 +393,7 @@ export async function startRepl(program, resumeMode) {
356
393
  const result = await handleChatMessage(input);
357
394
  processing = false;
358
395
  if (result === 'exit') {
359
- rl.setPrompt(chalk.green('icoa> '));
396
+ rl.setPrompt(computePrompt());
360
397
  }
361
398
  rl.prompt();
362
399
  return;
@@ -367,7 +404,7 @@ export async function startRepl(program, resumeMode) {
367
404
  const result = await handleCtf4aiMessage(input);
368
405
  processing = false;
369
406
  if (result === 'exit' || result === 'solved') {
370
- rl.setPrompt(chalk.green('icoa> '));
407
+ rl.setPrompt(computePrompt());
371
408
  }
372
409
  rl.prompt();
373
410
  return;
@@ -101,6 +101,7 @@ export interface IcoaConfig {
101
101
  accessToken: string;
102
102
  deviceFingerprint: string;
103
103
  lastVersion: string;
104
+ demoCleanedForVersion?: string;
104
105
  sessionCookie: string;
105
106
  country: string;
106
107
  mode: IcoaMode | '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.67",
3
+ "version": "2.19.69",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {