icoa-cli 2.19.66 → 2.19.68
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/lib/exam-state.d.ts +2 -2
- package/dist/lib/exam-state.js +16 -24
- package/dist/repl.js +25 -5
- 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/lib/exam-state.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExamState } from '../types/index.js';
|
|
2
|
-
export declare function getExamState(): ExamState | null;
|
|
3
|
-
export declare function getDemoState(): ExamState | null;
|
|
4
2
|
export declare function getRealExamState(): ExamState | null;
|
|
3
|
+
export declare function getDemoState(): ExamState | null;
|
|
4
|
+
export declare function getExamState(): ExamState | null;
|
|
5
5
|
export declare function saveExamState(state: ExamState): void;
|
|
6
6
|
export declare function clearExamState(examId?: string): void;
|
|
7
7
|
export declare function isExamActive(): boolean;
|
package/dist/lib/exam-state.js
CHANGED
|
@@ -12,28 +12,8 @@ function demoStateFile() {
|
|
|
12
12
|
function resolveStateFile(examId) {
|
|
13
13
|
return examId === 'demo-free' ? demoStateFile() : stateFile();
|
|
14
14
|
}
|
|
15
|
-
export function
|
|
16
|
-
|
|
17
|
-
const files = [stateFile(), demoStateFile()];
|
|
18
|
-
let latest = null;
|
|
19
|
-
let latestTime = 0;
|
|
20
|
-
for (const f of files) {
|
|
21
|
-
if (!existsSync(f))
|
|
22
|
-
continue;
|
|
23
|
-
try {
|
|
24
|
-
const state = JSON.parse(readFileSync(f, 'utf-8'));
|
|
25
|
-
const t = new Date(state.session.confirmedAt || state.session.startedAt).getTime();
|
|
26
|
-
if (t > latestTime) {
|
|
27
|
-
latest = state;
|
|
28
|
-
latestTime = t;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
catch { }
|
|
32
|
-
}
|
|
33
|
-
return latest;
|
|
34
|
-
}
|
|
35
|
-
export function getDemoState() {
|
|
36
|
-
const f = demoStateFile();
|
|
15
|
+
export function getRealExamState() {
|
|
16
|
+
const f = stateFile();
|
|
37
17
|
if (!existsSync(f))
|
|
38
18
|
return null;
|
|
39
19
|
try {
|
|
@@ -43,8 +23,8 @@ export function getDemoState() {
|
|
|
43
23
|
return null;
|
|
44
24
|
}
|
|
45
25
|
}
|
|
46
|
-
export function
|
|
47
|
-
const f =
|
|
26
|
+
export function getDemoState() {
|
|
27
|
+
const f = demoStateFile();
|
|
48
28
|
if (!existsSync(f))
|
|
49
29
|
return null;
|
|
50
30
|
try {
|
|
@@ -54,6 +34,18 @@ export function getRealExamState() {
|
|
|
54
34
|
return null;
|
|
55
35
|
}
|
|
56
36
|
}
|
|
37
|
+
export function getExamState() {
|
|
38
|
+
// Real exam ALWAYS takes priority over demo when both exist.
|
|
39
|
+
// Reasoning: real exam is token-gated, has a timer, and represents a serious
|
|
40
|
+
// commitment. Demo is casual practice. If a real exam is in progress, all
|
|
41
|
+
// shared commands (exam q N, exam answer, ai4ctf, ctf4ai) must target the
|
|
42
|
+
// real exam — never silently fall back to demo questions, even if demo was
|
|
43
|
+
// run more recently. (Demo→Exam→Finals progression principle, exam.md §0)
|
|
44
|
+
const real = getRealExamState();
|
|
45
|
+
if (real)
|
|
46
|
+
return real;
|
|
47
|
+
return getDemoState();
|
|
48
|
+
}
|
|
57
49
|
export function saveExamState(state) {
|
|
58
50
|
const f = resolveStateFile(state.session.examId);
|
|
59
51
|
writeFileSync(f, JSON.stringify(state, null, 2));
|
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() {
|
|
@@ -334,19 +344,29 @@ export async function startRepl(program, resumeMode) {
|
|
|
334
344
|
const rl = createInterface({
|
|
335
345
|
input: process.stdin,
|
|
336
346
|
output: process.stdout,
|
|
337
|
-
prompt:
|
|
347
|
+
prompt: computePrompt(),
|
|
338
348
|
terminal: true,
|
|
339
349
|
});
|
|
340
350
|
let processing = false;
|
|
341
351
|
setReplMode(true);
|
|
342
352
|
startLogSync();
|
|
353
|
+
// Wrap rl.prompt() so it always picks up current state (icoa/demo/exam mode).
|
|
354
|
+
// This keeps the prompt accurate after exam token entry, exam submit, demo finish, etc.
|
|
355
|
+
// Chat modes (ai4ctf/ctf4ai) set their own prompt via setPrompt and bypass this.
|
|
356
|
+
const _origPrompt = rl.prompt.bind(rl);
|
|
357
|
+
rl.prompt = (preserveCursor) => {
|
|
358
|
+
if (!isChatActive() && !isCtf4aiActive()) {
|
|
359
|
+
rl.setPrompt(computePrompt());
|
|
360
|
+
}
|
|
361
|
+
_origPrompt(preserveCursor);
|
|
362
|
+
};
|
|
343
363
|
rl.prompt();
|
|
344
364
|
rl.on('line', async (line) => {
|
|
345
365
|
if (processing)
|
|
346
366
|
return;
|
|
347
367
|
const input = line.trim();
|
|
348
368
|
if (!input) {
|
|
349
|
-
rl.setPrompt(isChatActive() ? chalk.magenta('ai4ctf> ') :
|
|
369
|
+
rl.setPrompt(isChatActive() ? chalk.magenta('ai4ctf> ') : computePrompt());
|
|
350
370
|
rl.prompt();
|
|
351
371
|
return;
|
|
352
372
|
}
|
|
@@ -356,7 +376,7 @@ export async function startRepl(program, resumeMode) {
|
|
|
356
376
|
const result = await handleChatMessage(input);
|
|
357
377
|
processing = false;
|
|
358
378
|
if (result === 'exit') {
|
|
359
|
-
rl.setPrompt(
|
|
379
|
+
rl.setPrompt(computePrompt());
|
|
360
380
|
}
|
|
361
381
|
rl.prompt();
|
|
362
382
|
return;
|
|
@@ -367,7 +387,7 @@ export async function startRepl(program, resumeMode) {
|
|
|
367
387
|
const result = await handleCtf4aiMessage(input);
|
|
368
388
|
processing = false;
|
|
369
389
|
if (result === 'exit' || result === 'solved') {
|
|
370
|
-
rl.setPrompt(
|
|
390
|
+
rl.setPrompt(computePrompt());
|
|
371
391
|
}
|
|
372
392
|
rl.prompt();
|
|
373
393
|
return;
|