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.
- package/dist/commands/exam.js +48 -4
- package/dist/repl.js +42 -5
- package/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
package/dist/commands/exam.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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> ') :
|
|
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(
|
|
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(
|
|
407
|
+
rl.setPrompt(computePrompt());
|
|
371
408
|
}
|
|
372
409
|
rl.prompt();
|
|
373
410
|
return;
|
package/dist/types/index.d.ts
CHANGED