icoa-cli 2.19.99 → 2.19.101
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/ai4ctf.js +1 -700
- package/dist/commands/connect.js +1 -66
- package/dist/commands/ctf.js +1 -620
- package/dist/commands/ctf4ai-demo.js +1 -525
- package/dist/commands/env.js +1 -737
- package/dist/commands/exam.js +1 -2353
- package/dist/commands/files.js +1 -52
- package/dist/commands/hint.js +1 -119
- package/dist/commands/lang.js +1 -155
- package/dist/commands/log.js +1 -163
- package/dist/commands/note.js +1 -32
- package/dist/commands/ref.js +1 -63
- package/dist/commands/setup.js +1 -103
- package/dist/commands/shell.js +1 -55
- package/dist/commands/theme.js +1 -50
- package/dist/index.js +1 -225
- package/dist/lib/access.js +1 -246
- package/dist/lib/budget.js +1 -42
- package/dist/lib/colors.js +1 -21
- package/dist/lib/config.js +1 -60
- package/dist/lib/ctfd-client.js +1 -274
- package/dist/lib/demo-exam.js +1 -249
- package/dist/lib/demo-flags.js +1 -27
- package/dist/lib/demo-stats.js +1 -65
- package/dist/lib/exam-client.js +1 -57
- package/dist/lib/exam-setup.js +1 -23
- package/dist/lib/exam-state.js +1 -112
- package/dist/lib/gemini.js +1 -235
- package/dist/lib/i18n.js +1 -273
- package/dist/lib/log-sync.js +1 -110
- package/dist/lib/logger.js +1 -59
- package/dist/lib/paper-upgrade.js +1 -117
- package/dist/lib/platform.js +1 -86
- package/dist/lib/sandbox.js +1 -93
- package/dist/lib/terminal.js +1 -49
- package/dist/lib/theme.js +1 -108
- package/dist/lib/translation.js +1 -66
- package/dist/lib/ui.js +1 -80
- package/dist/lib/update-check.js +1 -102
- package/dist/postinstall.js +1 -48
- package/dist/repl.js +1 -1259
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +1 -38
- package/package.json +6 -2
- package/translations/sw/i18n-snippet.ts +1 -0
package/dist/repl.js
CHANGED
|
@@ -1,1259 +1 @@
|
|
|
1
|
-
import { createInterface } from 'node:readline';
|
|
2
|
-
import { spawn, execSync as execSyncFn } from 'node:child_process';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import { isConnected, getConfig, saveConfig } from './lib/config.js';
|
|
5
|
-
import { isActivated, activateToken, isFreeCommand, isDeviceMatch, recordExit, recordResume, isFirstRunOrUpgrade, markVersionSeen } from './lib/access.js';
|
|
6
|
-
import { setReplMode } from './lib/ui.js';
|
|
7
|
-
import { isChatActive, handleChatMessage } from './commands/ai4ctf.js';
|
|
8
|
-
import { isCtf4aiActive, handleCtf4aiMessage } from './commands/ctf4ai-demo.js';
|
|
9
|
-
import { getExamState, getRealExamState, getDemoState } from './lib/exam-state.js';
|
|
10
|
-
import { getDemoStats } from './lib/demo-stats.js';
|
|
11
|
-
import { isExamSetupComplete } from './lib/exam-setup.js';
|
|
12
|
-
import { DEMO_PICK_SIZE, DEMO_POOL_SIZE } from './lib/demo-exam.js';
|
|
13
|
-
import { isNativeWindowsCmd } from './lib/platform.js';
|
|
14
|
-
import { resetTerminalTheme } from './lib/theme.js';
|
|
15
|
-
import { ensureSandbox, runInSandbox, isDockerAvailable } from './lib/sandbox.js';
|
|
16
|
-
import { logCommand } from './lib/logger.js';
|
|
17
|
-
import { startLogSync, stopLogSync } from './lib/log-sync.js';
|
|
18
|
-
import { existsSync, mkdirSync } from 'node:fs';
|
|
19
|
-
import { join } from 'node:path';
|
|
20
|
-
import { homedir } from 'node:os';
|
|
21
|
-
// Pre-import commonly-used CTF modules on interactive Python startup.
|
|
22
|
-
// Failures are silent — selectors import only what's available so a partial
|
|
23
|
-
// exam setup doesn't crash the shell. Creates ~/.icoa/python-startup.py once.
|
|
24
|
-
const PYTHON_STARTUP_CONTENT = `# ICOA exam interactive startup — auto-loaded by PYTHONSTARTUP
|
|
25
|
-
import base64, struct, hashlib, re, json, os, sys, binascii
|
|
26
|
-
try: import requests
|
|
27
|
-
except ImportError: pass
|
|
28
|
-
try: from Crypto.Cipher import AES
|
|
29
|
-
except ImportError: pass
|
|
30
|
-
try: from Crypto.Util.Padding import pad, unpad
|
|
31
|
-
except ImportError: pass
|
|
32
|
-
try: from pwn import xor, p32, u32, p64, u64
|
|
33
|
-
except ImportError: pass
|
|
34
|
-
try: import bs4
|
|
35
|
-
except ImportError: pass
|
|
36
|
-
try: import numpy as np
|
|
37
|
-
except ImportError: pass
|
|
38
|
-
`;
|
|
39
|
-
function ensurePythonStartup() {
|
|
40
|
-
const icoaDir = join(homedir(), '.icoa');
|
|
41
|
-
if (!existsSync(icoaDir))
|
|
42
|
-
mkdirSync(icoaDir, { recursive: true });
|
|
43
|
-
const path = join(icoaDir, 'python-startup.py');
|
|
44
|
-
if (!existsSync(path)) {
|
|
45
|
-
const { writeFileSync } = require('node:fs');
|
|
46
|
-
writeFileSync(path, PYTHON_STARTUP_CONTENT);
|
|
47
|
-
}
|
|
48
|
-
return path;
|
|
49
|
-
}
|
|
50
|
-
function printPythonBanner() {
|
|
51
|
-
console.log();
|
|
52
|
-
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
53
|
-
console.log(chalk.bold.white(' Python ready — ICOA exam toolkit pre-loaded'));
|
|
54
|
-
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
55
|
-
console.log();
|
|
56
|
-
console.log(chalk.white(' Already imported: ') + chalk.gray('base64, struct, hashlib, re, json, binascii'));
|
|
57
|
-
console.log(chalk.white(' Also available: ') + chalk.gray('requests, bs4, numpy, AES, pad/unpad, xor, p32/u32/p64/u64'));
|
|
58
|
-
console.log();
|
|
59
|
-
console.log(chalk.yellow(' Quick examples:'));
|
|
60
|
-
console.log(chalk.gray(' base64.b64decode("aGVsbG8=") ') + chalk.gray('# decode base64'));
|
|
61
|
-
console.log(chalk.gray(' bytes.fromhex("48656c6c6f") ') + chalk.gray('# hex → bytes'));
|
|
62
|
-
console.log(chalk.gray(' "ICOA{x}".encode() ') + chalk.gray('# str → bytes'));
|
|
63
|
-
console.log(chalk.gray(' [chr(c) for c in [73,67,79,65]] ') + chalk.gray('# ASCII codes'));
|
|
64
|
-
console.log(chalk.gray(' xor(bytes.fromhex("0a2b"), b"IC") ') + chalk.gray('# pwntools XOR'));
|
|
65
|
-
console.log();
|
|
66
|
-
console.log(chalk.gray(' Exit: ') + chalk.white('exit()') + chalk.gray(' or Ctrl-D'));
|
|
67
|
-
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
68
|
-
console.log();
|
|
69
|
-
}
|
|
70
|
-
// Compute the REPL prompt based on current state.
|
|
71
|
-
// Real exam wins over demo (matches getExamState() priority). Chat modes have
|
|
72
|
-
// their own prompts and are handled separately.
|
|
73
|
-
function computePrompt() {
|
|
74
|
-
if (getRealExamState())
|
|
75
|
-
return chalk.cyan('exam> ');
|
|
76
|
-
if (getDemoState())
|
|
77
|
-
return chalk.yellow('demo> ');
|
|
78
|
-
return chalk.green('icoa> ');
|
|
79
|
-
}
|
|
80
|
-
// Competition workspace — all system commands restricted here
|
|
81
|
-
const WORKSPACE = join(homedir(), 'icoa-workspace');
|
|
82
|
-
function ensureWorkspace() {
|
|
83
|
-
if (!existsSync(WORKSPACE))
|
|
84
|
-
mkdirSync(WORKSPACE, { recursive: true });
|
|
85
|
-
return WORKSPACE;
|
|
86
|
-
}
|
|
87
|
-
// Blocked commands — security risk
|
|
88
|
-
const BLOCKED_COMMANDS = new Set([
|
|
89
|
-
'sudo', 'su', 'doas', 'pkexec', // privilege escalation
|
|
90
|
-
'brew', 'apt', 'apt-get', 'yum', 'choco', // package managers (use env setup)
|
|
91
|
-
'npm', 'npx', 'pip', 'pip3', // package install (use env setup)
|
|
92
|
-
'shutdown', 'reboot', 'halt', // system control
|
|
93
|
-
'mkfs', 'fdisk', 'dd', // disk operations
|
|
94
|
-
'iptables', 'ufw', // firewall
|
|
95
|
-
]);
|
|
96
|
-
const INTERCEPT = '__REPL_NO_EXIT__';
|
|
97
|
-
const VERSION = '2.5.1';
|
|
98
|
-
// National Selection main menu — shown on REPL entry and when user types `back`
|
|
99
|
-
// after finishing the demo flow. Progressive onboarding per spec
|
|
100
|
-
// docs/superpowers/specs/2026-04-13-exam-national-selection-design.md §4.1:
|
|
101
|
-
// State 0 (no demo yet): recommend demo, hide exam setup / exam <token>
|
|
102
|
-
// State 1 (demo done, no setup): show demo completion + setup CTA, hide token
|
|
103
|
-
// State 2 (demo done + setup done): show token entry CTA
|
|
104
|
-
// Quick Python version check (used in Selection menu startup warning).
|
|
105
|
-
// Returns {ok, version, status}. Silent/defensive — any error means 'missing'.
|
|
106
|
-
function checkPython() {
|
|
107
|
-
// Probe python3.12 first — macOS Homebrew installs python@3.12 alongside a
|
|
108
|
-
// newer default python3, and we don't want to flag a 3.12-ready machine just
|
|
109
|
-
// because the default alias moved to 3.13.
|
|
110
|
-
// On Windows (cmd/PowerShell), the binary is `python` not `python3`, and the
|
|
111
|
-
// Python launcher `py -3` is also common. Include these so Windows K-12
|
|
112
|
-
// students don't get a spurious "Python missing" warning when Python IS
|
|
113
|
-
// installed. We also stdio: ignore stderr so cmd.exe's "not recognized as
|
|
114
|
-
// internal or external command" error doesn't leak to the user.
|
|
115
|
-
const probes = [
|
|
116
|
-
'python3.12 --version',
|
|
117
|
-
'/opt/homebrew/opt/python@3.12/bin/python3.12 --version',
|
|
118
|
-
'/usr/local/opt/python@3.12/bin/python3.12 --version',
|
|
119
|
-
'python3 --version',
|
|
120
|
-
'python --version', // Windows default name
|
|
121
|
-
'py -3.12 --version', // Windows Python Launcher
|
|
122
|
-
'py -3 --version', // Windows Python Launcher (any 3.x)
|
|
123
|
-
];
|
|
124
|
-
let lastVersion = '';
|
|
125
|
-
let lastStatus = 'missing';
|
|
126
|
-
for (const cmd of probes) {
|
|
127
|
-
try {
|
|
128
|
-
const out = execSyncFn(cmd, {
|
|
129
|
-
encoding: 'utf-8',
|
|
130
|
-
timeout: 2000,
|
|
131
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
132
|
-
}).trim();
|
|
133
|
-
const version = out.replace('Python ', '');
|
|
134
|
-
const [maj, min] = version.split('.').map(Number);
|
|
135
|
-
if (maj === 3 && min === 12)
|
|
136
|
-
return { ok: true, version, status: 'ok' };
|
|
137
|
-
lastVersion = version;
|
|
138
|
-
if (maj === 3 && min >= 10 && min < 12)
|
|
139
|
-
lastStatus = 'old';
|
|
140
|
-
else if (maj === 3 && min > 12)
|
|
141
|
-
lastStatus = 'new';
|
|
142
|
-
else
|
|
143
|
-
lastStatus = 'missing';
|
|
144
|
-
}
|
|
145
|
-
catch { /* try next probe */ }
|
|
146
|
-
}
|
|
147
|
-
return { ok: lastStatus !== 'missing', version: lastVersion, status: lastStatus };
|
|
148
|
-
}
|
|
149
|
-
function printSelectionMenu() {
|
|
150
|
-
const stats = getDemoStats();
|
|
151
|
-
const setupDone = isExamSetupComplete();
|
|
152
|
-
const demoLine = `Free practice — ${DEMO_PICK_SIZE} questions (from pool of ${DEMO_POOL_SIZE})`;
|
|
153
|
-
// v2.19.97 — Windows cmd K-12 entry path. Skip setup prompts + Python
|
|
154
|
-
// warnings for cmd users; they're routed to C paper (MCQ-only) which
|
|
155
|
-
// needs zero external tools.
|
|
156
|
-
const cmdMode = isNativeWindowsCmd();
|
|
157
|
-
console.log();
|
|
158
|
-
console.log(' ' + chalk.cyan.bold('[Selection Mode]'));
|
|
159
|
-
console.log();
|
|
160
|
-
if (cmdMode) {
|
|
161
|
-
console.log(chalk.gray(' Platform: ') + chalk.white('Windows cmd.exe') + chalk.gray(' — routed to Paper C (MCQ-only, 45 min, 70 pts, zero extra tools)'));
|
|
162
|
-
console.log();
|
|
163
|
-
}
|
|
164
|
-
else if (stats.attempts > 0) {
|
|
165
|
-
// Proactive Python check only matters for non-cmd users heading toward
|
|
166
|
-
// exam setup. Missing or version >= 3.13 shows yellow note pointing at
|
|
167
|
-
// `env python`. Quiet when 3.10-3.12 are installed.
|
|
168
|
-
const py = checkPython();
|
|
169
|
-
if (py.status === 'missing') {
|
|
170
|
-
console.log(chalk.yellow(' ⚠ Python not detected. For exam practical questions:'));
|
|
171
|
-
console.log(chalk.gray(' → ') + chalk.bold.cyan('env python') + chalk.gray(' (platform install guide)'));
|
|
172
|
-
console.log();
|
|
173
|
-
}
|
|
174
|
-
else if (py.status === 'new') {
|
|
175
|
-
console.log(chalk.yellow(` ⚠ Python ${py.version} may lack CTF wheels. Python 3.12 recommended:`));
|
|
176
|
-
console.log(chalk.gray(' → ') + chalk.bold.cyan('env python') + chalk.gray(' (install guide)'));
|
|
177
|
-
console.log();
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if (stats.attempts === 0) {
|
|
181
|
-
// State 0: brand new user. Only demo matters right now.
|
|
182
|
-
console.log(chalk.white(' New here? Start with ') + chalk.bold.cyan('demo') + chalk.white(' — it takes a few minutes.'));
|
|
183
|
-
console.log();
|
|
184
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
185
|
-
console.log(chalk.bold.cyan(' demo') + chalk.gray(` ${demoLine}`));
|
|
186
|
-
console.log(chalk.white(' lang') + chalk.gray(' List all supported languages'));
|
|
187
|
-
console.log(chalk.white(' lang es') + chalk.gray(' Switch language (e.g. lang es, lang zh, lang fr)'));
|
|
188
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
189
|
-
}
|
|
190
|
-
else if (!setupDone && !cmdMode) {
|
|
191
|
-
// State 1: demo done at least once, but exam environment not installed.
|
|
192
|
-
// Windows cmd users skip this state entirely — they don't need setup.
|
|
193
|
-
const plural = stats.attempts === 1 ? 'attempt' : 'attempts';
|
|
194
|
-
console.log(chalk.green(' ✓ Demo completed ') + chalk.gray(`(${stats.attempts} ${plural}${stats.bestPercentage > 0 ? ` · best ${stats.bestPercentage}%` : ''})`));
|
|
195
|
-
console.log(chalk.yellow(' → Next: prepare your environment for the real exam.'));
|
|
196
|
-
console.log();
|
|
197
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
198
|
-
console.log(chalk.white(' demo') + chalk.gray(` ${demoLine}`));
|
|
199
|
-
console.log(chalk.bold.yellow(' exam setup') + chalk.gray(' Install tools for national selection (~150MB)'));
|
|
200
|
-
console.log(chalk.white(' lang') + chalk.gray(' List all supported languages'));
|
|
201
|
-
console.log(chalk.white(' lang es') + chalk.gray(' Switch language (e.g. lang es, lang zh, lang fr)'));
|
|
202
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
// State 2: ready to enter exam. For cmd users this is reached immediately
|
|
206
|
-
// (no setup gate); for others it requires exam setup completion.
|
|
207
|
-
const plural = stats.attempts === 1 ? 'attempt' : 'attempts';
|
|
208
|
-
if (stats.attempts > 0) {
|
|
209
|
-
console.log(chalk.green(' ✓ Demo completed ') + chalk.gray(`(${stats.attempts} ${plural})`));
|
|
210
|
-
}
|
|
211
|
-
if (!cmdMode) {
|
|
212
|
-
console.log(chalk.green(' ✓ Environment ready'));
|
|
213
|
-
}
|
|
214
|
-
console.log(chalk.yellow(' → Enter your exam token to begin.'));
|
|
215
|
-
console.log(chalk.gray(' (10-char code from your organizer, starts with your country code like ') + chalk.cyan('UA') + chalk.gray(' — case-insensitive)'));
|
|
216
|
-
console.log();
|
|
217
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
218
|
-
console.log(chalk.bold.yellow(' exam <token>') + chalk.gray(' Enter exam (primary action — use your organizer-issued token)'));
|
|
219
|
-
console.log(chalk.gray(' format: ') + chalk.white('exam UAxxxxxxxx') + chalk.gray(' (2-letter country prefix + 8 chars)'));
|
|
220
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
221
|
-
console.log(chalk.gray(' Other commands:'));
|
|
222
|
-
console.log(chalk.white(' demo') + chalk.gray(` ${demoLine}`));
|
|
223
|
-
if (!cmdMode) {
|
|
224
|
-
console.log(chalk.white(' exam setup') + chalk.gray(' Re-verify tool environment'));
|
|
225
|
-
}
|
|
226
|
-
console.log(chalk.white(' lang') + chalk.gray(' List all supported languages'));
|
|
227
|
-
console.log(chalk.white(' lang es') + chalk.gray(' Switch language (e.g. lang es, lang zh, lang fr)'));
|
|
228
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
229
|
-
}
|
|
230
|
-
console.log(chalk.gray(' ') +
|
|
231
|
-
chalk.gray('Tip: ') + chalk.cyan('help') + chalk.gray(' for commands · ') +
|
|
232
|
-
chalk.cyan('Ctrl+C') + chalk.gray(' pauses · ') +
|
|
233
|
-
chalk.cyan('quit') + chalk.gray(' closes'));
|
|
234
|
-
console.log();
|
|
235
|
-
}
|
|
236
|
-
export async function startRepl(program, resumeMode) {
|
|
237
|
-
const config = getConfig();
|
|
238
|
-
const connected = isConnected();
|
|
239
|
-
const realExit = process.exit.bind(process);
|
|
240
|
-
const activated = isActivated();
|
|
241
|
-
// Auto-cleanup: clear demo state once per version upgrade.
|
|
242
|
-
// Demo is free practice with no time pressure, no scoring, no real loss
|
|
243
|
-
// for users. Old versions may have left demo state in incompatible formats
|
|
244
|
-
// (pre-v2.19.45 shared state, pre-v2.19.67 timestamp confusion, etc.).
|
|
245
|
-
// Real exam state is NEVER auto-cleared — it may contain in-progress answers.
|
|
246
|
-
if (config.demoCleanedForVersion !== VERSION) {
|
|
247
|
-
try {
|
|
248
|
-
const { existsSync, unlinkSync } = await import('node:fs');
|
|
249
|
-
const { join } = await import('node:path');
|
|
250
|
-
const { getIcoaDir } = await import('./lib/config.js');
|
|
251
|
-
const demoFile = join(getIcoaDir(), 'demo-state.json');
|
|
252
|
-
if (existsSync(demoFile))
|
|
253
|
-
unlinkSync(demoFile);
|
|
254
|
-
}
|
|
255
|
-
catch { }
|
|
256
|
-
saveConfig({ demoCleanedForVersion: VERSION });
|
|
257
|
-
}
|
|
258
|
-
// ─── Mode selection (every launch) ───
|
|
259
|
-
const { select: selectMode, confirm: confirmMode } = await import('@inquirer/prompts');
|
|
260
|
-
const savedMode = config.mode || '';
|
|
261
|
-
const modeChoices = [
|
|
262
|
-
{ name: ` ${chalk.bold('National Selection')} ${chalk.gray('—')} ${chalk.gray('demo, exam (lightweight)')}`, value: 'selection' },
|
|
263
|
-
{ name: ` ${chalk.bold('International Olympiad')} ${chalk.gray('—')} ${chalk.gray('CTF × AI (~500MB, advanced)')}`, value: 'olympiad' },
|
|
264
|
-
{ name: ` ${chalk.bold('National/Regional Partner')} ${chalk.gray('—')} ${chalk.gray('organizer tools (tokens, competitions)')}`, value: 'organizer' },
|
|
265
|
-
{ name: ` ${chalk.gray('About ICOA')} ${chalk.gray('·')} ${chalk.gray('Info & contact')}`, value: 'about' },
|
|
266
|
-
];
|
|
267
|
-
console.log(chalk.gray(' Use ') + chalk.yellow('↑') + chalk.gray(' or ') + chalk.yellow('↓') + chalk.gray(' to select, ') + chalk.yellow('Enter') + chalk.gray(' to confirm.'));
|
|
268
|
-
console.log();
|
|
269
|
-
let mode = '';
|
|
270
|
-
while (!mode) {
|
|
271
|
-
const selected = await selectMode({
|
|
272
|
-
message: 'Mode',
|
|
273
|
-
choices: modeChoices,
|
|
274
|
-
default: savedMode || 'selection',
|
|
275
|
-
});
|
|
276
|
-
if (selected === 'about') {
|
|
277
|
-
console.clear();
|
|
278
|
-
console.log();
|
|
279
|
-
console.log(chalk.cyan(' ═══════════════════════════════════════════════════'));
|
|
280
|
-
console.log(chalk.bold.yellow(' ICOA') + chalk.white(' — AI-Native CLI OS for Cyber & AI Security'));
|
|
281
|
-
console.log(chalk.gray(' Olympiad & Competition · K-12 to University'));
|
|
282
|
-
console.log(chalk.cyan(' ───────────────────────────────────────────────────'));
|
|
283
|
-
console.log();
|
|
284
|
-
console.log(chalk.bold.white(' What Makes ICOA Different'));
|
|
285
|
-
console.log(chalk.gray(' · AI-native AI teammate, AI adversary, AI translation'));
|
|
286
|
-
console.log(chalk.gray(' · CLI OS Complete competition environment in terminal'));
|
|
287
|
-
console.log(chalk.gray(' · 110 tools pwntools, z3, gdb, nmap, sleuthkit... pre-configured'));
|
|
288
|
-
console.log(chalk.gray(' · Global scale 15,000+ concurrent exams · 15 languages'));
|
|
289
|
-
console.log();
|
|
290
|
-
console.log(chalk.bold.white(' Competition Format'));
|
|
291
|
-
console.log(' ' + chalk.green.bold('AI4CTF') + chalk.gray(' [Day 1] AI as teammate — 5hr jeopardy CTF'));
|
|
292
|
-
console.log(' ' + chalk.red.bold('CTF4AI') + chalk.gray(' [Day 2] Challenge AI — adversarial ML, red-team'));
|
|
293
|
-
console.log();
|
|
294
|
-
console.log(chalk.white(' Sydney, Australia') + chalk.gray(' · Jun 27 - Jul 2, 2026 · 40+ countries'));
|
|
295
|
-
console.log();
|
|
296
|
-
console.log(chalk.bold.white(' Organized by') + chalk.gray(' ASRA (Australia) · ICO Foundation Inc'));
|
|
297
|
-
console.log(chalk.bold.white(' Contact ') + chalk.cyan(' australia@icoa2026.au · accreditation@icoa2026.au'));
|
|
298
|
-
console.log(chalk.bold.white(' Website ') + chalk.cyan.underline(' https://icoa2026.au'));
|
|
299
|
-
console.log(chalk.cyan(' ═══════════════════════════════════════════════════'));
|
|
300
|
-
console.log();
|
|
301
|
-
console.log(chalk.gray(' Press ') + chalk.yellow('Enter') + chalk.gray(' to return...'));
|
|
302
|
-
await new Promise((resolve) => {
|
|
303
|
-
const onKey = (_chunk) => {
|
|
304
|
-
process.stdin.removeListener('data', onKey);
|
|
305
|
-
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
306
|
-
process.stdin.setRawMode(false);
|
|
307
|
-
}
|
|
308
|
-
process.stdin.pause();
|
|
309
|
-
resolve();
|
|
310
|
-
};
|
|
311
|
-
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
312
|
-
process.stdin.setRawMode(true);
|
|
313
|
-
}
|
|
314
|
-
process.stdin.resume();
|
|
315
|
-
process.stdin.once('data', onKey);
|
|
316
|
-
});
|
|
317
|
-
console.clear();
|
|
318
|
-
// Loop back to mode selection
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
mode = selected;
|
|
322
|
-
}
|
|
323
|
-
if (mode === 'olympiad' && savedMode !== 'olympiad') {
|
|
324
|
-
console.log();
|
|
325
|
-
console.log(chalk.yellow(' This mode will download ~500MB of CTF tools and AI models.'));
|
|
326
|
-
const proceed = await confirmMode({ message: 'Continue?', default: true });
|
|
327
|
-
if (!proceed) {
|
|
328
|
-
mode = 'selection';
|
|
329
|
-
console.log(chalk.gray(' Switched to National Selection mode.'));
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
if (mode !== savedMode)
|
|
333
|
-
saveConfig({ mode });
|
|
334
|
-
console.log();
|
|
335
|
-
// First run or upgrade: prompt to install env (olympiad only)
|
|
336
|
-
if (mode === 'olympiad' && isFirstRunOrUpgrade(VERSION)) {
|
|
337
|
-
markVersionSeen(VERSION);
|
|
338
|
-
console.log(chalk.gray(' Checking competition environment...'));
|
|
339
|
-
// Quick check: count missing Python libs
|
|
340
|
-
const { execSync } = await import('node:child_process');
|
|
341
|
-
const checks = [
|
|
342
|
-
{ name: 'pwntools', cmd: 'python3 -c "import pwn"' },
|
|
343
|
-
{ name: 'z3-solver', cmd: 'python3 -c "import z3"' },
|
|
344
|
-
{ name: 'numpy', cmd: 'python3 -c "import numpy"' },
|
|
345
|
-
{ name: 'requests', cmd: 'python3 -c "import requests"' },
|
|
346
|
-
];
|
|
347
|
-
let missing = 0;
|
|
348
|
-
for (const c of checks) {
|
|
349
|
-
try {
|
|
350
|
-
execSync(c.cmd, { stdio: 'ignore' });
|
|
351
|
-
}
|
|
352
|
-
catch {
|
|
353
|
-
missing++;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
if (missing > 0) {
|
|
357
|
-
console.log(chalk.yellow(` ${missing} core libraries missing.`));
|
|
358
|
-
try {
|
|
359
|
-
const { confirm } = await import('@inquirer/prompts');
|
|
360
|
-
const yes = await confirm({
|
|
361
|
-
message: ' Install competition Python libraries now?',
|
|
362
|
-
default: true,
|
|
363
|
-
theme: { prefix: '', style: { message: (t) => chalk.green(t), defaultAnswer: (t) => chalk.green(t) } },
|
|
364
|
-
});
|
|
365
|
-
if (yes) {
|
|
366
|
-
console.log();
|
|
367
|
-
// Trigger env setup — use `icoa` command directly
|
|
368
|
-
const { execSync: ex } = await import('node:child_process');
|
|
369
|
-
ex('icoa env setup', { stdio: 'inherit' });
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
catch {
|
|
373
|
-
console.log(chalk.gray(' Run ') + chalk.white('env setup') + chalk.gray(' later to install.'));
|
|
374
|
-
}
|
|
375
|
-
console.log();
|
|
376
|
-
}
|
|
377
|
-
else {
|
|
378
|
-
console.log(chalk.green(' All core libraries ready.'));
|
|
379
|
-
console.log();
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
// Handle resume
|
|
383
|
-
if (resumeMode) {
|
|
384
|
-
const info = recordResume();
|
|
385
|
-
if (info) {
|
|
386
|
-
const mins = Math.floor(info.awaySeconds / 60);
|
|
387
|
-
const secs = info.awaySeconds % 60;
|
|
388
|
-
console.log(chalk.yellow(` Session resumed. Away: ${mins}m ${secs}s | Total exits: ${info.exitCount}`));
|
|
389
|
-
console.log();
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
// ─── Mode-specific welcome ───
|
|
393
|
-
if (mode === 'selection') {
|
|
394
|
-
printSelectionMenu();
|
|
395
|
-
}
|
|
396
|
-
else if (mode === 'organizer') {
|
|
397
|
-
console.log(chalk.yellow.bold(' [National/Regional Partner]'));
|
|
398
|
-
console.log();
|
|
399
|
-
console.log(chalk.bold.white(' ██╗ ██████╗ ██████╗ █████╗'));
|
|
400
|
-
console.log(chalk.bold.white(' ██║██╔════╝██╔═══██╗██╔══██╗'));
|
|
401
|
-
console.log(chalk.bold.white(' ██║██║ ██║ ██║███████║'));
|
|
402
|
-
console.log(chalk.bold.white(' ██║██║ ██║ ██║██╔══██║'));
|
|
403
|
-
console.log(chalk.bold.white(' ██║╚██████╗╚██████╔╝██║ ██║'));
|
|
404
|
-
console.log(chalk.bold.white(' ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝'));
|
|
405
|
-
console.log();
|
|
406
|
-
console.log(chalk.yellow(' International Cyber Olympiad in AI 2026'));
|
|
407
|
-
console.log(chalk.bold.magenta(' The World\'s First AI-Native CLI Operating System'));
|
|
408
|
-
console.log(chalk.bold.magenta(' for Cybersecurity & AI Security Competition'));
|
|
409
|
-
console.log(chalk.bold.magenta(' and Olympiad for K-12'));
|
|
410
|
-
console.log(chalk.gray(' Sydney, Australia · Jun 27 - Jul 2, 2026'));
|
|
411
|
-
console.log();
|
|
412
|
-
console.log(chalk.white(' Vision'));
|
|
413
|
-
console.log(chalk.gray(' Building a global pipeline for youth cyber & AI'));
|
|
414
|
-
console.log(chalk.gray(' security talent through education and competition.'));
|
|
415
|
-
console.log();
|
|
416
|
-
console.log(chalk.white(' Capacity'));
|
|
417
|
-
console.log(chalk.gray(' 15,000+ concurrent online examinations'));
|
|
418
|
-
console.log(chalk.gray(' National selection, training, and education support'));
|
|
419
|
-
console.log();
|
|
420
|
-
console.log(chalk.white(' Olympic Spirit'));
|
|
421
|
-
console.log(chalk.gray(' Excellence · Friendship · Respect'));
|
|
422
|
-
console.log();
|
|
423
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
424
|
-
console.log(chalk.white(' New country accreditation & support:'));
|
|
425
|
-
console.log(chalk.cyan(' australia@icoa2026.au'));
|
|
426
|
-
console.log(chalk.cyan(' accreditation@icoa2026.au'));
|
|
427
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
428
|
-
console.log();
|
|
429
|
-
if (connected) {
|
|
430
|
-
console.log(chalk.green(` Logged in as ${config.userName}`));
|
|
431
|
-
console.log(chalk.white(' exam list') + chalk.gray(' Manage exams'));
|
|
432
|
-
console.log(chalk.white(' logout') + chalk.gray(' Disconnect'));
|
|
433
|
-
}
|
|
434
|
-
else {
|
|
435
|
-
console.log(chalk.white(' join <url>') + chalk.gray(' Connect to manage exams'));
|
|
436
|
-
}
|
|
437
|
-
console.log();
|
|
438
|
-
}
|
|
439
|
-
else {
|
|
440
|
-
// Olympiad mode: full flow with activate/device checks
|
|
441
|
-
if (activated && !isDeviceMatch()) {
|
|
442
|
-
console.log(chalk.red(' Token was activated on a different device.'));
|
|
443
|
-
console.log(chalk.gray(' Contact organizer for assistance.'));
|
|
444
|
-
console.log();
|
|
445
|
-
}
|
|
446
|
-
else if (connected) {
|
|
447
|
-
console.log(chalk.green.bold(` Welcome back, ${config.userName}!`));
|
|
448
|
-
console.log(chalk.gray(` Connected to ${config.ctfdUrl}`));
|
|
449
|
-
console.log();
|
|
450
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
451
|
-
console.log(chalk.white(' Ready to compete? Start here:'));
|
|
452
|
-
console.log();
|
|
453
|
-
console.log(chalk.bold.cyan(' challenges') + chalk.gray(' Browse challenges by category'));
|
|
454
|
-
console.log(chalk.white(' status') + chalk.gray(' Your score & hint budget'));
|
|
455
|
-
console.log(chalk.white(' scoreboard') + chalk.gray(' Live rankings'));
|
|
456
|
-
console.log(chalk.white(' help') + chalk.gray(' Full command list'));
|
|
457
|
-
console.log();
|
|
458
|
-
console.log(chalk.gray(' Tool environment:'));
|
|
459
|
-
console.log(chalk.white(' env') + chalk.gray(' See which of the 110 CTF tools are installed'));
|
|
460
|
-
console.log(chalk.white(' env setup') + chalk.gray(' Install anything missing (~5 min, one-time)'));
|
|
461
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
462
|
-
console.log(chalk.gray(' Tip: ') + chalk.cyan('help') + chalk.gray(' · ') + chalk.cyan('Ctrl+C') + chalk.gray(' pauses · ') + chalk.cyan('quit') + chalk.gray(' closes'));
|
|
463
|
-
console.log();
|
|
464
|
-
}
|
|
465
|
-
else if (activated) {
|
|
466
|
-
ensureWorkspace();
|
|
467
|
-
console.log(chalk.green.bold(' Welcome, competitor!'));
|
|
468
|
-
console.log(chalk.gray(` Workspace: ${WORKSPACE}`));
|
|
469
|
-
console.log();
|
|
470
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
471
|
-
console.log(chalk.white(' Get started:'));
|
|
472
|
-
console.log();
|
|
473
|
-
console.log(chalk.white(' Step 1 ') + chalk.bold.cyan('join <url>') + chalk.gray(' Connect to competition server'));
|
|
474
|
-
console.log(chalk.white(' Step 2 ') + chalk.bold.cyan('challenges') + chalk.gray(' Browse & solve challenges'));
|
|
475
|
-
console.log(chalk.white(' Step 3 ') + chalk.bold.cyan('hint') + chalk.gray(' Ask AI when stuck'));
|
|
476
|
-
console.log();
|
|
477
|
-
console.log(chalk.gray(' Before Step 1 — make sure your tools are ready:'));
|
|
478
|
-
console.log(chalk.white(' env') + chalk.gray(' See which of the 110 CTF tools are installed'));
|
|
479
|
-
console.log(chalk.white(' env setup') + chalk.gray(' Install anything missing (~5 min, one-time)'));
|
|
480
|
-
console.log();
|
|
481
|
-
console.log(chalk.gray(' Also: ') + chalk.white('help') + chalk.gray(' all commands'));
|
|
482
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
483
|
-
console.log(chalk.gray(' Tip: ') + chalk.cyan('Ctrl+C') + chalk.gray(' pauses · ') + chalk.cyan('exit') + chalk.gray(' → menu · ') + chalk.cyan('quit') + chalk.gray(' closes CLI'));
|
|
484
|
-
console.log();
|
|
485
|
-
}
|
|
486
|
-
else {
|
|
487
|
-
console.log(chalk.bold.white(' Welcome to ICOA CLI — International Olympiad'));
|
|
488
|
-
console.log();
|
|
489
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
490
|
-
console.log(chalk.white(' To begin, activate your competition token:'));
|
|
491
|
-
console.log();
|
|
492
|
-
console.log(chalk.bold.cyan(' activate <token>'));
|
|
493
|
-
console.log();
|
|
494
|
-
console.log(chalk.gray(' While waiting, explore:'));
|
|
495
|
-
console.log(chalk.white(' ref linux') + chalk.gray(' Quick reference for Linux'));
|
|
496
|
-
console.log(chalk.white(' ref web') + chalk.gray(' Quick reference for Web'));
|
|
497
|
-
console.log(chalk.white(' env') + chalk.gray(' See which of the 110 CTF tools are installed'));
|
|
498
|
-
console.log(chalk.white(' env setup') + chalk.gray(' Install anything missing (~5 min, one-time)'));
|
|
499
|
-
console.log(chalk.white(' help') + chalk.gray(' All available commands'));
|
|
500
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
501
|
-
console.log(chalk.gray(' Tip: ') + chalk.cyan('Ctrl+C') + chalk.gray(' pauses · ') + chalk.cyan('exit') + chalk.gray(' → menu · ') + chalk.cyan('quit') + chalk.gray(' closes CLI'));
|
|
502
|
-
console.log();
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
program.exitOverride();
|
|
506
|
-
program.configureOutput({
|
|
507
|
-
writeErr: () => { },
|
|
508
|
-
writeOut: (str) => { console.log(str); },
|
|
509
|
-
});
|
|
510
|
-
const rl = createInterface({
|
|
511
|
-
input: process.stdin,
|
|
512
|
-
output: process.stdout,
|
|
513
|
-
prompt: computePrompt(),
|
|
514
|
-
terminal: true,
|
|
515
|
-
});
|
|
516
|
-
let processing = false;
|
|
517
|
-
setReplMode(true);
|
|
518
|
-
startLogSync();
|
|
519
|
-
// Wrap rl.prompt() so it always picks up current state (icoa/demo/exam mode).
|
|
520
|
-
// This keeps the prompt accurate after exam token entry, exam submit, demo finish, etc.
|
|
521
|
-
// Chat modes (ai4ctf/ctf4ai) set their own prompt via setPrompt and bypass this.
|
|
522
|
-
const _origPrompt = rl.prompt.bind(rl);
|
|
523
|
-
rl.prompt = (preserveCursor) => {
|
|
524
|
-
if (!isChatActive() && !isCtf4aiActive()) {
|
|
525
|
-
rl.setPrompt(computePrompt());
|
|
526
|
-
}
|
|
527
|
-
_origPrompt(preserveCursor);
|
|
528
|
-
};
|
|
529
|
-
rl.prompt();
|
|
530
|
-
rl.on('line', async (line) => {
|
|
531
|
-
if (processing)
|
|
532
|
-
return;
|
|
533
|
-
const input = line.trim();
|
|
534
|
-
if (!input) {
|
|
535
|
-
rl.setPrompt(isChatActive() ? chalk.magenta('ai4ctf> ') : computePrompt());
|
|
536
|
-
rl.prompt();
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
// If in AI chat mode, route to chat handler
|
|
540
|
-
if (isChatActive()) {
|
|
541
|
-
processing = true;
|
|
542
|
-
const result = await handleChatMessage(input);
|
|
543
|
-
processing = false;
|
|
544
|
-
if (result === 'exit') {
|
|
545
|
-
rl.setPrompt(computePrompt());
|
|
546
|
-
}
|
|
547
|
-
rl.prompt();
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
// If in CTF4AI challenge mode, route to ctf4ai handler
|
|
551
|
-
if (isCtf4aiActive()) {
|
|
552
|
-
processing = true;
|
|
553
|
-
const result = await handleCtf4aiMessage(input);
|
|
554
|
-
processing = false;
|
|
555
|
-
if (result === 'exit' || result === 'solved') {
|
|
556
|
-
rl.setPrompt(computePrompt());
|
|
557
|
-
}
|
|
558
|
-
rl.prompt();
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
// Log ALL commands for audit trail
|
|
562
|
-
logCommand(input);
|
|
563
|
-
// `exit` — soft exit: acts like `back` and surfaces the menu. Only
|
|
564
|
-
// `quit` / `q` actually close the CLI. Rationale: in the demo flow the
|
|
565
|
-
// user bounces between main prompt and sub-flows (ai4ctf / ctf4ai /
|
|
566
|
-
// demo exam); typing `exit` at the main prompt to mean "leave the demo"
|
|
567
|
-
// is a very common mistake and should never nuke the whole session.
|
|
568
|
-
if (input === 'exit') {
|
|
569
|
-
if (getExamState()) {
|
|
570
|
-
console.log();
|
|
571
|
-
console.log(chalk.yellow(' ⚠ An exam is in progress.'));
|
|
572
|
-
console.log(chalk.white(' To return to menu without losing progress, type: ') + chalk.bold.cyan('back'));
|
|
573
|
-
console.log(chalk.white(' To fully close ICOA CLI, type: ') + chalk.bold.cyan('quit'));
|
|
574
|
-
console.log(chalk.gray(' Your progress is auto-saved either way.'));
|
|
575
|
-
console.log();
|
|
576
|
-
rl.prompt();
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
console.log();
|
|
580
|
-
console.log(chalk.gray(' ') + chalk.white('exit') + chalk.gray(' returns to the main menu. To fully close ICOA CLI, type ') + chalk.bold.cyan('quit') + chalk.gray('.'));
|
|
581
|
-
if (mode === 'selection') {
|
|
582
|
-
printSelectionMenu();
|
|
583
|
-
}
|
|
584
|
-
rl.prompt();
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
// Explicit quit — `quit` or `q` always closes the CLI.
|
|
588
|
-
if (input === 'quit' || input === 'q') {
|
|
589
|
-
if (getExamState()) {
|
|
590
|
-
console.log();
|
|
591
|
-
console.log(chalk.yellow(' ⚠ An exam is in progress — progress is auto-saved.'));
|
|
592
|
-
console.log(chalk.gray(' Closing anyway. Resume with: ') + chalk.white('icoa --resume'));
|
|
593
|
-
console.log();
|
|
594
|
-
}
|
|
595
|
-
stopLogSync();
|
|
596
|
-
recordExit();
|
|
597
|
-
console.log(chalk.gray(' Session saved. Use ') + chalk.white('icoa --resume') + chalk.gray(' to continue.'));
|
|
598
|
-
resetTerminalTheme();
|
|
599
|
-
realExit(0);
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
// "back" — return to main menu.
|
|
603
|
-
// Real exam: show "Exam paused", preserve state (server timer is ticking).
|
|
604
|
-
// Active demo: show pause message, keep state so `exam q N` can resume.
|
|
605
|
-
// Stale demo: auto-clear and show menu (demo from a previous session the
|
|
606
|
-
// user long abandoned — hanging state makes the menu lie).
|
|
607
|
-
// Nothing: show selection menu.
|
|
608
|
-
// A demo is "active" if `startedAt` is within the last 30 minutes. That window
|
|
609
|
-
// covers an intentional "back to check something and come right back" case but
|
|
610
|
-
// clears anything left over from a prior session.
|
|
611
|
-
// T3-9: `menu` is a universal alias for `back` — more discoverable for
|
|
612
|
-
// K-12 beginners. Both drop to the Selection menu (or pause active exam).
|
|
613
|
-
if (input === 'back' || input === 'menu') {
|
|
614
|
-
const state = getExamState();
|
|
615
|
-
const isRealExam = state && state.session.examId !== 'demo-free';
|
|
616
|
-
const isActiveDemo = state && state.session.examId === 'demo-free' && (() => {
|
|
617
|
-
const started = new Date(state.session.startedAt || 0).getTime();
|
|
618
|
-
return Date.now() - started < 30 * 60 * 1000;
|
|
619
|
-
})();
|
|
620
|
-
if (isRealExam) {
|
|
621
|
-
console.log();
|
|
622
|
-
console.log(chalk.gray(' Exam paused. Your progress is saved.'));
|
|
623
|
-
console.log(chalk.white(' Resume: exam q 1') + chalk.gray(' · ') + chalk.white('exam review') + chalk.gray(' · ') + chalk.white('exam submit'));
|
|
624
|
-
console.log();
|
|
625
|
-
}
|
|
626
|
-
else if (isActiveDemo) {
|
|
627
|
-
const answered = Object.keys(state.answers).length;
|
|
628
|
-
const total = state.session.questionCount;
|
|
629
|
-
console.log();
|
|
630
|
-
console.log(chalk.gray(` Demo paused (${answered}/${total} answered). Resume with: `) + chalk.white(`exam q 1`));
|
|
631
|
-
console.log(chalk.gray(' Or type ') + chalk.white('demo') + chalk.gray(' to restart.'));
|
|
632
|
-
console.log();
|
|
633
|
-
}
|
|
634
|
-
else {
|
|
635
|
-
// Stale demo state from a past session — clear it so the menu reflects
|
|
636
|
-
// reality. Demo is free-practice, user can always restart.
|
|
637
|
-
if (state && state.session.examId === 'demo-free') {
|
|
638
|
-
const { clearExamState } = await import('./lib/exam-state.js');
|
|
639
|
-
clearExamState('demo-free');
|
|
640
|
-
}
|
|
641
|
-
const cfg = getConfig();
|
|
642
|
-
fetch('https://practice.icoa2026.au/api/icoa/demo-stats', {
|
|
643
|
-
method: 'POST',
|
|
644
|
-
headers: { 'Content-Type': 'application/json' },
|
|
645
|
-
body: JSON.stringify({
|
|
646
|
-
type: 'post-report-back',
|
|
647
|
-
lang: cfg.language || 'en',
|
|
648
|
-
timestamp: new Date().toISOString(),
|
|
649
|
-
}),
|
|
650
|
-
signal: AbortSignal.timeout(5000),
|
|
651
|
-
}).catch(() => { });
|
|
652
|
-
if (mode === 'selection') {
|
|
653
|
-
printSelectionMenu();
|
|
654
|
-
}
|
|
655
|
-
else {
|
|
656
|
-
console.log(chalk.gray(' Already at main menu.'));
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
rl.prompt();
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
// Help — during exam, route to exam help; otherwise show REPL help
|
|
663
|
-
if (input === 'help' || input === '?') {
|
|
664
|
-
if (getExamState()) {
|
|
665
|
-
processing = true;
|
|
666
|
-
try {
|
|
667
|
-
await program.parseAsync(['node', 'icoa', 'exam', 'help']);
|
|
668
|
-
}
|
|
669
|
-
catch { }
|
|
670
|
-
processing = false;
|
|
671
|
-
rl.prompt();
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
printReplHelp(isActivated(), mode);
|
|
675
|
-
rl.prompt();
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
// "more help" — during exam, unlock bonus helps
|
|
679
|
-
if (input.toLowerCase() === 'more help') {
|
|
680
|
-
if (getExamState()) {
|
|
681
|
-
processing = true;
|
|
682
|
-
try {
|
|
683
|
-
await program.parseAsync(['node', 'icoa', 'exam', 'more-help']);
|
|
684
|
-
}
|
|
685
|
-
catch { }
|
|
686
|
-
processing = false;
|
|
687
|
-
rl.prompt();
|
|
688
|
-
return;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
// "continue" — after demo, guide to ai4ctf then ctf4ai
|
|
692
|
-
if (input.toLowerCase() === 'continue') {
|
|
693
|
-
console.log();
|
|
694
|
-
console.log(chalk.green.bold(' ═══ AI4CTF — AI as Your Teammate ═══'));
|
|
695
|
-
console.log();
|
|
696
|
-
console.log(chalk.white(' In AI4CTF, you solve cybersecurity challenges'));
|
|
697
|
-
console.log(chalk.white(' with AI by your side.'));
|
|
698
|
-
console.log();
|
|
699
|
-
console.log(chalk.white(' In competition, you get AI help at 3 levels:'));
|
|
700
|
-
console.log(chalk.yellow(' hint a') + chalk.gray(' General guidance (50 uses)'));
|
|
701
|
-
console.log(chalk.yellow(' hint b') + chalk.gray(' Deep analysis (10 uses)'));
|
|
702
|
-
console.log(chalk.yellow(' hint c') + chalk.gray(' Critical assist (2 uses)'));
|
|
703
|
-
console.log();
|
|
704
|
-
console.log(chalk.white(' Try it now! Type: ') + chalk.bold.green('ai4ctf'));
|
|
705
|
-
console.log(chalk.gray(' Chat freely with your AI teammate. Type "exit" when done.'));
|
|
706
|
-
console.log();
|
|
707
|
-
console.log(chalk.gray(' After ai4ctf, try: ') + chalk.bold.red('ctf4ai') + chalk.gray(' — trick the AI into saying "koala"'));
|
|
708
|
-
console.log();
|
|
709
|
-
rl.prompt();
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
// ICOA exam token detection: legacy "ICOA-PE-001" or new Crockford "UAHXP7SWMM"
|
|
713
|
-
if (/^ICOA-[A-Z]{2,3}-\d{1,6}$/i.test(input.trim())) {
|
|
714
|
-
processing = true;
|
|
715
|
-
try {
|
|
716
|
-
await program.parseAsync(['node', 'icoa', 'exam', 'token', input.trim()]);
|
|
717
|
-
}
|
|
718
|
-
catch { }
|
|
719
|
-
processing = false;
|
|
720
|
-
rl.prompt();
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
// New 10-char Crockford Base32 token (e.g., UAHXP7SWMM) — no hyphen
|
|
724
|
-
if (/^[A-Z]{2}[0-9A-HJKMNP-TV-Z]{8}$/i.test(input.trim())) {
|
|
725
|
-
processing = true;
|
|
726
|
-
try {
|
|
727
|
-
await program.parseAsync(['node', 'icoa', 'exam', 'token', input.trim().toUpperCase()]);
|
|
728
|
-
}
|
|
729
|
-
catch { }
|
|
730
|
-
processing = false;
|
|
731
|
-
rl.prompt();
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
// "exam UAHXP7SWMM" — 10-char token after exam prefix
|
|
735
|
-
const examTokenMatch = input.match(/^exam\s+([A-Z]{2}[0-9A-HJKMNP-TV-Z]{8})$/i);
|
|
736
|
-
if (examTokenMatch) {
|
|
737
|
-
processing = true;
|
|
738
|
-
try {
|
|
739
|
-
await program.parseAsync(['node', 'icoa', 'exam', 'token', examTokenMatch[1].toUpperCase()]);
|
|
740
|
-
}
|
|
741
|
-
catch { }
|
|
742
|
-
processing = false;
|
|
743
|
-
rl.prompt();
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
// "exam AU" / "exam PE" — shortcut to exam list <country>
|
|
747
|
-
const examCountryMatch = input.match(/^exam\s+([A-Z]{2,3})$/i);
|
|
748
|
-
if (examCountryMatch) {
|
|
749
|
-
processing = true;
|
|
750
|
-
try {
|
|
751
|
-
await program.parseAsync(['node', 'icoa', 'exam', 'list', examCountryMatch[1]]);
|
|
752
|
-
}
|
|
753
|
-
catch { }
|
|
754
|
-
processing = false;
|
|
755
|
-
rl.prompt();
|
|
756
|
-
return;
|
|
757
|
-
}
|
|
758
|
-
// Clear
|
|
759
|
-
if (input === 'clear' || input === 'cls') {
|
|
760
|
-
console.clear();
|
|
761
|
-
rl.prompt();
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
// Activate token
|
|
765
|
-
if (input.startsWith('activate ')) {
|
|
766
|
-
const token = input.slice(9).trim();
|
|
767
|
-
const result = activateToken(token);
|
|
768
|
-
if (result === 'ok') {
|
|
769
|
-
console.log(chalk.green(' Access granted! Token bound to this device.'));
|
|
770
|
-
}
|
|
771
|
-
else if (result === 'already_bound') {
|
|
772
|
-
console.log();
|
|
773
|
-
console.log(chalk.red(' Token already activated on a different device.'));
|
|
774
|
-
console.log(chalk.gray(' Each token binds to the first device that uses it. If you lost the device,'));
|
|
775
|
-
console.log(chalk.gray(' contact your proctor to have the token re-issued for a new device.'));
|
|
776
|
-
}
|
|
777
|
-
else {
|
|
778
|
-
console.log();
|
|
779
|
-
console.log(chalk.red(' Token not recognized.'));
|
|
780
|
-
console.log(chalk.gray(' Possible reasons:'));
|
|
781
|
-
console.log(chalk.white(' • ') + chalk.gray('Typo — tokens are case-insensitive, 10 chars, start with a 2-letter country code (e.g. ') + chalk.cyan('UAK7M2R9Q4') + chalk.gray(')'));
|
|
782
|
-
console.log(chalk.white(' • ') + chalk.gray('Expired — ask your proctor or organizer for a fresh token'));
|
|
783
|
-
console.log(chalk.white(' • ') + chalk.gray('Network — verify connection to ') + chalk.cyan('practice.icoa2026.au'));
|
|
784
|
-
console.log(chalk.gray(' Still stuck? type ') + chalk.cyan('help') + chalk.gray(' or try ') + chalk.cyan('exam demo') + chalk.gray(' for a free practice round.'));
|
|
785
|
-
}
|
|
786
|
-
console.log();
|
|
787
|
-
rl.prompt();
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
if (input === 'activate') {
|
|
791
|
-
console.log(chalk.gray(' Usage: ') + chalk.white('activate <token>'));
|
|
792
|
-
console.log();
|
|
793
|
-
rl.prompt();
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
// ─── Quick exam answer shortcuts ───
|
|
797
|
-
// "A" / "B" / "C" / "D" → answer current question (MCQ only)
|
|
798
|
-
// "2 C" / "5 A" → answer specific question (MCQ only)
|
|
799
|
-
// Practical questions (Q31-40) require flag format (ICOA{...}) — single
|
|
800
|
-
// letters on those would be silently accepted as the wrong flag answer,
|
|
801
|
-
// which is a footgun. Block and nudge the user toward the flag syntax.
|
|
802
|
-
const examState = getExamState();
|
|
803
|
-
if (examState) {
|
|
804
|
-
const upper = input.toUpperCase().trim();
|
|
805
|
-
// Helper: is question N practical (no A/B/C/D options)?
|
|
806
|
-
const isPracticalQ = (n) => {
|
|
807
|
-
const q = examState.questions.find((qq) => qq.number === n);
|
|
808
|
-
if (!q)
|
|
809
|
-
return false;
|
|
810
|
-
return q.type === 'ai4ctf' || q.type === 'ctf4ai' || (q.options && !q.options.A && !q.options.B);
|
|
811
|
-
};
|
|
812
|
-
// Helper: suggest the right chat entry for a practical question
|
|
813
|
-
const practicalGuidance = (n) => {
|
|
814
|
-
const isReal = examState.session.examId !== 'demo-free';
|
|
815
|
-
const cmd = isReal && n >= 39 ? 'ctf4ai' : isReal && n >= 31 ? 'ai4ctf' : null;
|
|
816
|
-
console.log();
|
|
817
|
-
console.log(chalk.yellow(` Q${n} is a practical question — letters (A/B/C/D) don't apply here.`));
|
|
818
|
-
if (cmd) {
|
|
819
|
-
console.log(chalk.white(' Enter the AI chat for this question: ') + chalk.bold.cyan(cmd));
|
|
820
|
-
console.log(chalk.gray(' Or submit a flag directly: ') + chalk.green(`exam answer ${n} ICOA{your_flag}`));
|
|
821
|
-
}
|
|
822
|
-
else {
|
|
823
|
-
console.log(chalk.gray(' Submit a flag: ') + chalk.green(`exam answer ${n} ICOA{your_flag}`));
|
|
824
|
-
}
|
|
825
|
-
console.log();
|
|
826
|
-
};
|
|
827
|
-
// Single letter: A, B, C, D → answer current question (only if MCQ)
|
|
828
|
-
if (/^[ABCD]$/.test(upper)) {
|
|
829
|
-
const currentQ = examState._lastQ || 1;
|
|
830
|
-
if (isPracticalQ(currentQ)) {
|
|
831
|
-
practicalGuidance(currentQ);
|
|
832
|
-
rl.prompt();
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
processing = true;
|
|
836
|
-
try {
|
|
837
|
-
await program.parseAsync(['node', 'icoa', 'exam', 'answer', String(currentQ), upper]);
|
|
838
|
-
}
|
|
839
|
-
catch { }
|
|
840
|
-
processing = false;
|
|
841
|
-
rl.prompt();
|
|
842
|
-
return;
|
|
843
|
-
}
|
|
844
|
-
// "N X" pattern: e.g. "2 C", "15 A" (MCQ only — same protection)
|
|
845
|
-
const match = upper.match(/^(\d+)\s+([ABCD])$/);
|
|
846
|
-
if (match) {
|
|
847
|
-
const targetQ = parseInt(match[1], 10);
|
|
848
|
-
if (isPracticalQ(targetQ)) {
|
|
849
|
-
practicalGuidance(targetQ);
|
|
850
|
-
rl.prompt();
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
processing = true;
|
|
854
|
-
try {
|
|
855
|
-
await program.parseAsync(['node', 'icoa', 'exam', 'answer', match[1], match[2]]);
|
|
856
|
-
}
|
|
857
|
-
catch { }
|
|
858
|
-
processing = false;
|
|
859
|
-
rl.prompt();
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
const cmd = input.split(/\s+/)[0].toLowerCase();
|
|
864
|
-
// ─── Mode-based command filtering ───
|
|
865
|
-
const selectionCommands = ['exam', 'demo', 'retry', 'nations', 'next', 'prev', 'continue', 'setup', 'lang', 'ref', 'ai4ctf', 'ctf4ai', 'mark', 'unmark', 'review', 'submit', 'env'];
|
|
866
|
-
const organizerCommands = ['join', 'exam', 'demo', 'retry', 'next', 'prev', 'logout', 'setup', 'lang', 'ref', 'ctf', 'mark', 'unmark', 'review', 'submit'];
|
|
867
|
-
// Selection mode allows shell commands prefixed with ! — contestants
|
|
868
|
-
// need !python3, !cat, !base64 etc. for practical questions (Q31-Q40).
|
|
869
|
-
// Also allow bare `python` / `python3` / `python3.12` since contestants
|
|
870
|
-
// naturally type that when they want interactive Python.
|
|
871
|
-
// BLOCKED_COMMANDS check downstream still rejects dangerous ones.
|
|
872
|
-
const isPythonCmd = /^python3?(\.\d+)?$/.test(cmd);
|
|
873
|
-
const isShellCommand = input.startsWith('!') || cmd.startsWith('!') || isPythonCmd;
|
|
874
|
-
if (mode === 'selection' && !isShellCommand && !selectionCommands.includes(cmd)) {
|
|
875
|
-
console.log(chalk.gray(' Not available in Selection mode.'));
|
|
876
|
-
if (examState) {
|
|
877
|
-
const currentQ = examState._lastQ || 1;
|
|
878
|
-
console.log(chalk.white(` Resume exam: exam q ${currentQ}`) + chalk.gray(' · ') + chalk.white('A/B/C/D') + chalk.gray(' to answer'));
|
|
879
|
-
}
|
|
880
|
-
else {
|
|
881
|
-
console.log(chalk.gray(' Try: demo · setup to switch mode'));
|
|
882
|
-
}
|
|
883
|
-
console.log();
|
|
884
|
-
rl.prompt();
|
|
885
|
-
return;
|
|
886
|
-
}
|
|
887
|
-
if (mode === 'organizer' && !organizerCommands.includes(cmd)) {
|
|
888
|
-
console.log(chalk.gray(' Not available in Organizer mode. Switch via: setup'));
|
|
889
|
-
console.log();
|
|
890
|
-
rl.prompt();
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
// Token check — only in olympiad mode
|
|
894
|
-
if (mode === 'olympiad' && (!isActivated() || !isDeviceMatch()) && !isFreeCommand(cmd)) {
|
|
895
|
-
console.log(chalk.yellow(' Restricted mode. ') + chalk.gray('Enter your access token:'));
|
|
896
|
-
console.log(chalk.white(' activate <token>'));
|
|
897
|
-
console.log();
|
|
898
|
-
console.log(chalk.gray(' Free commands: ') + chalk.white('ref [topic]') + chalk.gray(', ') + chalk.white('help') + chalk.gray(', ') + chalk.white('exit'));
|
|
899
|
-
console.log();
|
|
900
|
-
rl.prompt();
|
|
901
|
-
return;
|
|
902
|
-
}
|
|
903
|
-
// Check if it's a known ICOA command or a system command
|
|
904
|
-
const knownCommands = [
|
|
905
|
-
'join', 'activate', 'challenges', 'ch', 'open', 'submit', 'flag',
|
|
906
|
-
'scoreboard', 'sb', 'status', 'time', 'hint', 'hint-b', 'hint-c',
|
|
907
|
-
'hint-budget', 'ref', 'shell', 'files', 'connect', 'note',
|
|
908
|
-
'log', 'lang', 'setup', 'env', 'ai4ctf', 'model', 'ctf',
|
|
909
|
-
'exam', 'demo', 'retry', 'nations', 'next', 'prev', 'continue', 'logout', 'ctf4ai',
|
|
910
|
-
'mark', 'unmark', 'review', 'submit',
|
|
911
|
-
];
|
|
912
|
-
if (!knownCommands.includes(cmd)) {
|
|
913
|
-
// Block dangerous commands
|
|
914
|
-
if (BLOCKED_COMMANDS.has(cmd)) {
|
|
915
|
-
console.log(chalk.red(` Blocked: ${cmd} is not allowed during competition.`));
|
|
916
|
-
console.log();
|
|
917
|
-
rl.prompt();
|
|
918
|
-
return;
|
|
919
|
-
}
|
|
920
|
-
// Block path escape attempts
|
|
921
|
-
if (/(?:^|\s)(?:\/(?!home\/|Users\/|tmp\/)|\.\.\/|~\/)/.test(input) && !input.startsWith('cd ')) {
|
|
922
|
-
// Allow relative paths within workspace, block absolute paths outside
|
|
923
|
-
const hasAbsPath = /(?:^|\s)\/(?!home\/\w+\/icoa-workspace|Users\/\w+\/icoa-workspace|tmp\/)/.test(input);
|
|
924
|
-
const hasParentPath = /\.\./.test(input);
|
|
925
|
-
if (hasAbsPath || hasParentPath) {
|
|
926
|
-
console.log(chalk.red(' Blocked: access outside workspace is not allowed.'));
|
|
927
|
-
console.log(chalk.gray(` Workspace: ${WORKSPACE}`));
|
|
928
|
-
console.log();
|
|
929
|
-
rl.prompt();
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
// Force Python 3.12 — rewrite python/python3 to correct binary
|
|
934
|
-
// Strip optional ! prefix that contestants use to disambiguate shell
|
|
935
|
-
// commands from CLI commands (e.g. !python3 vs cli's "python" command).
|
|
936
|
-
let resolvedInput = input.startsWith('!') ? input.slice(1).trim() : input;
|
|
937
|
-
if (process.platform === 'darwin') {
|
|
938
|
-
const py12 = '/opt/homebrew/opt/python@3.12/bin/python3.12';
|
|
939
|
-
resolvedInput = resolvedInput
|
|
940
|
-
.replace(/^python3?\s/, `${py12} `)
|
|
941
|
-
.replace(/^(python3|python)$/, py12);
|
|
942
|
-
}
|
|
943
|
-
else if (process.platform === 'win32') {
|
|
944
|
-
// Windows: the binary is `python` (not `python3`). Python Launcher `py -3`
|
|
945
|
-
// is also common. Rewrite `python3 xyz` → `python xyz` or `py -3 xyz` so
|
|
946
|
-
// students who followed Unix-oriented practical-question hints don't get
|
|
947
|
-
// "'python3' is not recognized". Prefer `py -3` if the launcher exists
|
|
948
|
-
// (handles multi-version installs); fall back to plain `python`.
|
|
949
|
-
const pyCmd = (() => {
|
|
950
|
-
try {
|
|
951
|
-
execSyncFn('py -3 --version', { stdio: ['ignore', 'ignore', 'ignore'], timeout: 1500 });
|
|
952
|
-
return 'py -3';
|
|
953
|
-
}
|
|
954
|
-
catch { }
|
|
955
|
-
return 'python';
|
|
956
|
-
})();
|
|
957
|
-
resolvedInput = resolvedInput
|
|
958
|
-
.replace(/^python3?(\.\d+)?\s/, `${pyCmd} `)
|
|
959
|
-
.replace(/^python3?(\.\d+)?$/, pyCmd);
|
|
960
|
-
}
|
|
961
|
-
else {
|
|
962
|
-
// Linux/WSL: python → python3 (or python3.12 if available)
|
|
963
|
-
const py12 = (() => { try {
|
|
964
|
-
execSyncFn('which python3.12', { stdio: 'ignore' });
|
|
965
|
-
return 'python3.12';
|
|
966
|
-
}
|
|
967
|
-
catch {
|
|
968
|
-
return 'python3';
|
|
969
|
-
} })();
|
|
970
|
-
resolvedInput = resolvedInput
|
|
971
|
-
.replace(/^python\s/, `${py12} `)
|
|
972
|
-
.replace(/^python$/, py12);
|
|
973
|
-
}
|
|
974
|
-
// Ensure workspace directory
|
|
975
|
-
const cwd = ensureWorkspace();
|
|
976
|
-
// Interactive Python (no script args, no -c): print welcome banner and
|
|
977
|
-
// pre-import common CTF modules via a startup file. Makes entry painless
|
|
978
|
-
// for contestants — they land in a shell with base64/struct/hashlib/pwn
|
|
979
|
-
// already imported and see a 4-line cheat sheet of common one-liners.
|
|
980
|
-
const isInteractivePython = /^(\S*python3?(\.\d+)?)\s*$/.test(resolvedInput);
|
|
981
|
-
if (isInteractivePython) {
|
|
982
|
-
const startupPath = ensurePythonStartup();
|
|
983
|
-
resolvedInput = `PYTHONSTARTUP="${startupPath}" ${resolvedInput}`;
|
|
984
|
-
printPythonBanner();
|
|
985
|
-
}
|
|
986
|
-
// Route to Docker sandbox if available, otherwise system shell (in workspace)
|
|
987
|
-
processing = true;
|
|
988
|
-
try {
|
|
989
|
-
if (isDockerAvailable()) {
|
|
990
|
-
const ready = await ensureSandbox();
|
|
991
|
-
if (ready) {
|
|
992
|
-
await runInSandbox(resolvedInput, rl);
|
|
993
|
-
}
|
|
994
|
-
else {
|
|
995
|
-
await runSystemCommand(resolvedInput, rl, cwd);
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
else {
|
|
999
|
-
await runSystemCommand(resolvedInput, rl, cwd);
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
catch {
|
|
1003
|
-
console.log(chalk.yellow(` Command failed: ${cmd}`));
|
|
1004
|
-
}
|
|
1005
|
-
processing = false;
|
|
1006
|
-
console.log();
|
|
1007
|
-
rl.prompt();
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
processing = true;
|
|
1011
|
-
// During an exam, `submit` must mean "submit my exam", not "submit a CTF
|
|
1012
|
-
// flag". The `mapCommand` shortcut table always points `submit` at CTF,
|
|
1013
|
-
// which silently fails in Selection mode and leaves users stuck. Route
|
|
1014
|
-
// to exam submit here instead.
|
|
1015
|
-
const submitInExam = examState && (input.toLowerCase() === 'submit');
|
|
1016
|
-
const args = submitInExam ? ['exam', 'submit'] : mapCommand(input);
|
|
1017
|
-
// Pause REPL readline for commands that read stdin (join)
|
|
1018
|
-
const needsPause = args[0] === 'ctf' && args[1] === 'join';
|
|
1019
|
-
if (needsPause)
|
|
1020
|
-
rl.pause();
|
|
1021
|
-
process.exit = (() => {
|
|
1022
|
-
throw new Error(INTERCEPT);
|
|
1023
|
-
});
|
|
1024
|
-
try {
|
|
1025
|
-
await program.parseAsync(['node', 'icoa', ...args]);
|
|
1026
|
-
}
|
|
1027
|
-
catch (err) {
|
|
1028
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1029
|
-
if (msg === INTERCEPT) {
|
|
1030
|
-
// Command tried to exit — continue REPL
|
|
1031
|
-
}
|
|
1032
|
-
else if (msg.includes('commander.unknownCommand')) {
|
|
1033
|
-
// T4-12: typo suggestion via Levenshtein distance. Shorten the
|
|
1034
|
-
// "unknown command" frustration by pointing to the likely-intended
|
|
1035
|
-
// command when the user's input is within 2 edits of a real one.
|
|
1036
|
-
const { distance } = await import('fastest-levenshtein');
|
|
1037
|
-
const KNOWN_CMDS = [
|
|
1038
|
-
'ctf', 'hint', 'hint-b', 'hint-c', 'hint-budget', 'ref', 'shell',
|
|
1039
|
-
'files', 'connect', 'note', 'log', 'lang', 'setup', 'env',
|
|
1040
|
-
'ai4ctf', 'exam', 'ctf4ai', 'theme',
|
|
1041
|
-
'clear', 'cls', 'quit', 'exit', 'back', 'menu', 'help',
|
|
1042
|
-
'continue', 'activate', 'demo', 'challenges', 'status', 'scoreboard',
|
|
1043
|
-
'join', 'logout',
|
|
1044
|
-
];
|
|
1045
|
-
const firstWord = cmd.split(/\s+/)[0] || cmd;
|
|
1046
|
-
let best = { word: '', dist: Infinity };
|
|
1047
|
-
for (const known of KNOWN_CMDS) {
|
|
1048
|
-
const d = distance(firstWord.toLowerCase(), known);
|
|
1049
|
-
if (d < best.dist)
|
|
1050
|
-
best = { word: known, dist: d };
|
|
1051
|
-
}
|
|
1052
|
-
console.log(chalk.yellow(` Unknown command: ${cmd}.`));
|
|
1053
|
-
if (best.dist > 0 && best.dist <= 2) {
|
|
1054
|
-
console.log(chalk.gray(' Did you mean: ') + chalk.bold.cyan(best.word) + chalk.gray('?'));
|
|
1055
|
-
}
|
|
1056
|
-
console.log(chalk.gray(' Type ') + chalk.cyan('help') + chalk.gray(' for the full command list.'));
|
|
1057
|
-
}
|
|
1058
|
-
else if (msg.includes('commander.')) {
|
|
1059
|
-
// Internal Commander errors — ignore
|
|
1060
|
-
}
|
|
1061
|
-
else if (msg.includes('fetch failed') || msg.includes('ECONNREFUSED') || msg.includes('ETIMEDOUT')) {
|
|
1062
|
-
console.log(chalk.yellow(' Network error. Check your connection.'));
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
finally {
|
|
1066
|
-
process.exit = realExit;
|
|
1067
|
-
processing = false;
|
|
1068
|
-
if (needsPause)
|
|
1069
|
-
rl.resume();
|
|
1070
|
-
}
|
|
1071
|
-
// Switch prompt if entering chat/challenge mode
|
|
1072
|
-
if (isChatActive()) {
|
|
1073
|
-
rl.setPrompt(chalk.magenta('ai4ctf> '));
|
|
1074
|
-
}
|
|
1075
|
-
else if (isCtf4aiActive()) {
|
|
1076
|
-
rl.setPrompt(chalk.red('ctf4ai> '));
|
|
1077
|
-
}
|
|
1078
|
-
console.log();
|
|
1079
|
-
rl.prompt();
|
|
1080
|
-
});
|
|
1081
|
-
// SIGINT (Ctrl+C) — intercept gracefully so beginners don't lose confidence.
|
|
1082
|
-
// Without this listener, readline's default is to raise SIGINT which our
|
|
1083
|
-
// theme.ts handler converts to process.exit(130). Installing this listener
|
|
1084
|
-
// swallows that path and lets the user get oriented. If they want to exit,
|
|
1085
|
-
// they type `quit` or hit Ctrl+D (sends EOF → 'close' event below).
|
|
1086
|
-
rl.on('SIGINT', () => {
|
|
1087
|
-
console.log();
|
|
1088
|
-
if (isChatActive() || isCtf4aiActive()) {
|
|
1089
|
-
console.log(chalk.yellow(' Type ') + chalk.bold.cyan('exit') + chalk.yellow(' to leave chat, or Ctrl+D to close ICOA CLI.'));
|
|
1090
|
-
}
|
|
1091
|
-
else if (getExamState()) {
|
|
1092
|
-
console.log(chalk.yellow(' Exam paused. Your answers are auto-saved.'));
|
|
1093
|
-
console.log(chalk.white(' Resume: ') + chalk.cyan('exam q 1') +
|
|
1094
|
-
chalk.gray(' · Back to menu: ') + chalk.cyan('back') +
|
|
1095
|
-
chalk.gray(' · Close CLI: ') + chalk.cyan('quit'));
|
|
1096
|
-
}
|
|
1097
|
-
else {
|
|
1098
|
-
console.log(chalk.yellow(' Press Ctrl+D or type ') + chalk.bold.cyan('quit') + chalk.yellow(' to close. ') + chalk.bold.cyan('help') + chalk.yellow(' for commands.'));
|
|
1099
|
-
}
|
|
1100
|
-
console.log();
|
|
1101
|
-
rl.prompt();
|
|
1102
|
-
});
|
|
1103
|
-
rl.on('close', () => {
|
|
1104
|
-
stopLogSync();
|
|
1105
|
-
recordExit();
|
|
1106
|
-
resetTerminalTheme();
|
|
1107
|
-
realExit(0);
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
function runSystemCommand(input, rl, cwd) {
|
|
1111
|
-
return new Promise((resolve) => {
|
|
1112
|
-
// Fully release the TTY so interactive children (python3, bash, etc.)
|
|
1113
|
-
// get a clean terminal with proper echo. `rl.pause()` alone doesn't
|
|
1114
|
-
// reset raw mode or release stdin — readline keeps it in line-editing
|
|
1115
|
-
// mode where typed characters don't display until our REPL reads a line.
|
|
1116
|
-
const stdin = process.stdin;
|
|
1117
|
-
const wasRaw = stdin.isTTY ? !!stdin.isRaw : false;
|
|
1118
|
-
rl.pause();
|
|
1119
|
-
if (stdin.isTTY && typeof stdin.setRawMode === 'function') {
|
|
1120
|
-
try {
|
|
1121
|
-
stdin.setRawMode(false);
|
|
1122
|
-
}
|
|
1123
|
-
catch { }
|
|
1124
|
-
}
|
|
1125
|
-
const child = spawn(input, {
|
|
1126
|
-
shell: true,
|
|
1127
|
-
stdio: 'inherit',
|
|
1128
|
-
cwd: cwd || process.cwd(),
|
|
1129
|
-
});
|
|
1130
|
-
const restore = () => {
|
|
1131
|
-
if (stdin.isTTY && typeof stdin.setRawMode === 'function' && wasRaw) {
|
|
1132
|
-
try {
|
|
1133
|
-
stdin.setRawMode(true);
|
|
1134
|
-
}
|
|
1135
|
-
catch { }
|
|
1136
|
-
}
|
|
1137
|
-
rl.resume();
|
|
1138
|
-
resolve();
|
|
1139
|
-
};
|
|
1140
|
-
child.on('close', restore);
|
|
1141
|
-
child.on('error', restore);
|
|
1142
|
-
});
|
|
1143
|
-
}
|
|
1144
|
-
function mapCommand(input) {
|
|
1145
|
-
const parts = input.split(/\s+/);
|
|
1146
|
-
const cmd = parts[0].toLowerCase();
|
|
1147
|
-
const rest = parts.slice(1);
|
|
1148
|
-
const ctfShortcuts = {
|
|
1149
|
-
'demo': ['exam', 'demo'],
|
|
1150
|
-
'retry': ['exam', 'demo-retry'],
|
|
1151
|
-
'nations': ['exam', 'nations'],
|
|
1152
|
-
'next': ['exam', 'next'],
|
|
1153
|
-
'prev': ['exam', 'prev'],
|
|
1154
|
-
'mark': ['exam', 'mark', ...rest],
|
|
1155
|
-
'unmark': ['exam', 'unmark', ...rest],
|
|
1156
|
-
'review': ['exam', 'review'],
|
|
1157
|
-
'logout': ['ctf', 'logout'],
|
|
1158
|
-
'join': ['ctf', 'join', ...rest],
|
|
1159
|
-
'activate': ['ctf', 'activate', ...rest],
|
|
1160
|
-
'challenges': ['ctf', 'challenges'],
|
|
1161
|
-
'ch': ['ctf', 'challenges'],
|
|
1162
|
-
'open': ['ctf', 'open', ...rest],
|
|
1163
|
-
'submit': ['ctf', 'submit', ...rest],
|
|
1164
|
-
'flag': ['ctf', 'submit', ...rest],
|
|
1165
|
-
'scoreboard': ['ctf', 'scoreboard', ...rest],
|
|
1166
|
-
'sb': ['ctf', 'scoreboard', ...rest],
|
|
1167
|
-
'status': ['ctf', 'status'],
|
|
1168
|
-
'time': ['ctf', 'time'],
|
|
1169
|
-
};
|
|
1170
|
-
if (ctfShortcuts[cmd]) {
|
|
1171
|
-
return ctfShortcuts[cmd];
|
|
1172
|
-
}
|
|
1173
|
-
const directCommands = [
|
|
1174
|
-
'hint', 'hint-b', 'hint-c', 'hint-budget',
|
|
1175
|
-
'ref', 'shell', 'files', 'connect', 'note',
|
|
1176
|
-
'log', 'lang', 'setup', 'env', 'ai4ctf', 'model',
|
|
1177
|
-
'ctf', 'exam', 'ctf4ai',
|
|
1178
|
-
];
|
|
1179
|
-
if (directCommands.includes(cmd)) {
|
|
1180
|
-
return [cmd, ...rest];
|
|
1181
|
-
}
|
|
1182
|
-
return parts;
|
|
1183
|
-
}
|
|
1184
|
-
function printReplHelp(activated, mode = 'olympiad') {
|
|
1185
|
-
console.log();
|
|
1186
|
-
// ─── Selection / Organizer: lightweight help ───
|
|
1187
|
-
if (mode === 'selection' || mode === 'organizer') {
|
|
1188
|
-
console.log(chalk.bold.white(' Exam'));
|
|
1189
|
-
console.log(chalk.white(' join <url> ') + chalk.gray('Connect to exam server'));
|
|
1190
|
-
console.log(chalk.white(' exam list ') + chalk.gray('Available exams'));
|
|
1191
|
-
console.log(chalk.white(' exam start <id> ') + chalk.gray('Begin an exam'));
|
|
1192
|
-
console.log(chalk.white(' exam q [n] ') + chalk.gray('View questions'));
|
|
1193
|
-
console.log(chalk.white(' exam answer <n> <X> ') + chalk.gray('Answer question'));
|
|
1194
|
-
console.log(chalk.white(' exam review ') + chalk.gray('Review all answers'));
|
|
1195
|
-
console.log(chalk.white(' exam submit ') + chalk.gray('Submit for grading'));
|
|
1196
|
-
console.log(chalk.white(' exam result ') + chalk.gray('View your score'));
|
|
1197
|
-
console.log();
|
|
1198
|
-
console.log(chalk.bold.white(' System'));
|
|
1199
|
-
console.log(chalk.white(' ref [topic] ') + chalk.gray('Quick reference'));
|
|
1200
|
-
console.log(chalk.white(' setup ') + chalk.gray('Settings / switch mode'));
|
|
1201
|
-
console.log(chalk.white(' lang [code] ') + chalk.gray('Switch language'));
|
|
1202
|
-
console.log(chalk.white(' clear ') + chalk.gray('Clear screen'));
|
|
1203
|
-
console.log(chalk.white(' exit ') + chalk.gray('Quit'));
|
|
1204
|
-
console.log();
|
|
1205
|
-
return;
|
|
1206
|
-
}
|
|
1207
|
-
// ─── Olympiad: full help ───
|
|
1208
|
-
if (!activated) {
|
|
1209
|
-
console.log(chalk.bold.yellow(' Restricted Mode — activate with a token to unlock all commands'));
|
|
1210
|
-
console.log();
|
|
1211
|
-
console.log(chalk.white(' activate <token> ') + chalk.gray('Unlock full access'));
|
|
1212
|
-
console.log(chalk.white(' ref [topic] ') + chalk.gray('Quick reference'));
|
|
1213
|
-
console.log(chalk.white(' exit ') + chalk.gray('Quit'));
|
|
1214
|
-
console.log();
|
|
1215
|
-
return;
|
|
1216
|
-
}
|
|
1217
|
-
// How it works — workflow overview
|
|
1218
|
-
console.log(chalk.cyan(' ═══════════════════════════════════════════════'));
|
|
1219
|
-
console.log(chalk.bold.white(' How it works'));
|
|
1220
|
-
console.log();
|
|
1221
|
-
console.log(chalk.gray(' 1. Browse ') + chalk.white('challenges') + chalk.gray(' and pick one'));
|
|
1222
|
-
console.log(chalk.gray(' 2. ') + chalk.white('open <id>') + chalk.gray(' to read the challenge'));
|
|
1223
|
-
console.log(chalk.gray(' 3. Use ') + chalk.white('hint') + chalk.gray(' / ') + chalk.white('hint-b') + chalk.gray(' / ') + chalk.white('hint-c') + chalk.gray(' when stuck'));
|
|
1224
|
-
console.log(chalk.gray(' 4. ') + chalk.white('submit <id> icoa{flag}') + chalk.gray(' to score points'));
|
|
1225
|
-
console.log(chalk.gray(' 5. Check ') + chalk.white('scoreboard') + chalk.gray(' to track your rank'));
|
|
1226
|
-
console.log(chalk.cyan(' ═══════════════════════════════════════════════'));
|
|
1227
|
-
console.log();
|
|
1228
|
-
console.log(chalk.bold.white(' Competition'));
|
|
1229
|
-
console.log(chalk.white(' join <url> ') + chalk.gray('Connect to CTFd'));
|
|
1230
|
-
console.log(chalk.white(' challenges (ch) ') + chalk.gray('List challenges by category'));
|
|
1231
|
-
console.log(chalk.white(' open <id> ') + chalk.gray('Read challenge + get next steps'));
|
|
1232
|
-
console.log(chalk.white(' submit <id> <flag> ') + chalk.gray('Submit a flag'));
|
|
1233
|
-
console.log(chalk.white(' scoreboard (sb) ') + chalk.gray('Live rankings'));
|
|
1234
|
-
console.log(chalk.white(' status ') + chalk.gray('Your score, budget & timer'));
|
|
1235
|
-
console.log(chalk.white(' time ') + chalk.gray('Countdown timer'));
|
|
1236
|
-
console.log();
|
|
1237
|
-
console.log(chalk.bold.white(' AI Teammate') + chalk.gray(' — 3 levels, use wisely'));
|
|
1238
|
-
console.log(chalk.white(' hint "question" ') + chalk.gray('Level A — General guidance (50 uses)'));
|
|
1239
|
-
console.log(chalk.white(' hint-b "question" ') + chalk.gray('Level B — Deep analysis (10 uses)'));
|
|
1240
|
-
console.log(chalk.white(' hint-c "question" ') + chalk.gray('Level C — Critical assist (2 uses)'));
|
|
1241
|
-
console.log(chalk.white(' hint budget ') + chalk.gray('Check remaining uses'));
|
|
1242
|
-
console.log(chalk.white(' ai4ctf ') + chalk.gray('Free-chat with AI (no limit)'));
|
|
1243
|
-
console.log();
|
|
1244
|
-
console.log(chalk.bold.white(' Tools'));
|
|
1245
|
-
console.log(chalk.white(' ref [topic] ') + chalk.gray('Quick reference (linux, web, crypto...)'));
|
|
1246
|
-
console.log(chalk.white(' shell ') + chalk.gray('Docker sandbox'));
|
|
1247
|
-
console.log(chalk.white(' files <id> ') + chalk.gray('Download challenge files'));
|
|
1248
|
-
console.log(chalk.white(' connect <id> ') + chalk.gray('Connect to remote target'));
|
|
1249
|
-
console.log(chalk.white(' note [text] ') + chalk.gray('Personal notepad'));
|
|
1250
|
-
console.log(chalk.white(' log ') + chalk.gray('Session history'));
|
|
1251
|
-
console.log();
|
|
1252
|
-
console.log(chalk.bold.white(' System'));
|
|
1253
|
-
console.log(chalk.white(' setup ') + chalk.gray('Configure settings'));
|
|
1254
|
-
console.log(chalk.white(' lang [code] ') + chalk.gray('Switch language (15 supported)'));
|
|
1255
|
-
console.log(chalk.white(' logout ') + chalk.gray('Disconnect'));
|
|
1256
|
-
console.log(chalk.white(' clear ') + chalk.gray('Clear screen'));
|
|
1257
|
-
console.log(chalk.white(' exit ') + chalk.gray('Quit (session saved)'));
|
|
1258
|
-
console.log();
|
|
1259
|
-
}
|
|
1
|
+
import{createInterface as o}from"node:readline";import{spawn as e,execSync as t}from"node:child_process";import chalk from"chalk";import{isConnected as n,getConfig as l,saveConfig as s}from"./lib/config.js";import{isActivated as a,activateToken as r,isFreeCommand as i,isDeviceMatch as c,recordExit as g,recordResume as y,isFirstRunOrUpgrade as m,markVersionSeen as p}from"./lib/access.js";import{setReplMode as d}from"./lib/ui.js";import{isChatActive as h,handleChatMessage as u}from"./commands/ai4ctf.js";import{isCtf4aiActive as w,handleCtf4aiMessage as f}from"./commands/ctf4ai-demo.js";import{getExamState as b,getRealExamState as x,getDemoState as v}from"./lib/exam-state.js";import{getDemoStats as C}from"./lib/demo-stats.js";import{isExamSetupComplete as k}from"./lib/exam-setup.js";import{DEMO_PICK_SIZE as I,DEMO_POOL_SIZE as A}from"./lib/demo-exam.js";import{isNativeWindowsCmd as T}from"./lib/platform.js";import{resetTerminalTheme as S}from"./lib/theme.js";import{ensureSandbox as $,runInSandbox as O,isDockerAvailable as q}from"./lib/sandbox.js";import{logCommand as L}from"./lib/logger.js";import{startLogSync as j,stopLogSync as E}from"./lib/log-sync.js";import{existsSync as P,mkdirSync as R}from"node:fs";import{join as N}from"node:path";import{homedir as D}from"node:os";function F(){return x()?chalk.cyan("exam> "):v()?chalk.yellow("demo> "):chalk.green("icoa> ")}const M=N(D(),"icoa-workspace");function U(){return P(M)||R(M,{recursive:!0}),M}const z=new Set(["sudo","su","doas","pkexec","brew","apt","apt-get","yum","choco","npm","npx","pip","pip3","shutdown","reboot","halt","mkfs","fdisk","dd","iptables","ufw"]),B="__REPL_NO_EXIT__",W="2.5.1";function Q(){const o=C(),e=k(),n=`Free practice — ${I} questions (from pool of ${A})`,l=T();if(console.log(),console.log(" "+chalk.cyan.bold("[Selection Mode]")),console.log(),l)console.log(chalk.gray(" Platform: ")+chalk.white("Windows cmd.exe")+chalk.gray(" — routed to Paper C (MCQ-only, 45 min, 70 pts, zero extra tools)")),console.log();else if(o.attempts>0){const o=function(){const o=["python3.12 --version","/opt/homebrew/opt/python@3.12/bin/python3.12 --version","/usr/local/opt/python@3.12/bin/python3.12 --version","python3 --version","python --version","py -3.12 --version","py -3 --version"];let e="",n="missing";for(const l of o)try{const o=t(l,{encoding:"utf-8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).trim().replace("Python ",""),[s,a]=o.split(".").map(Number);if(3===s&&12===a)return{ok:!0,version:o,status:"ok"};e=o,n=3===s&&a>=10&&a<12?"old":3===s&&a>12?"new":"missing"}catch{}return{ok:"missing"!==n,version:e,status:n}}();"missing"===o.status?(console.log(chalk.yellow(" ⚠ Python not detected. For exam practical questions:")),console.log(chalk.gray(" → ")+chalk.bold.cyan("env python")+chalk.gray(" (platform install guide)")),console.log()):"new"===o.status&&(console.log(chalk.yellow(` ⚠ Python ${o.version} may lack CTF wheels. Python 3.12 recommended:`)),console.log(chalk.gray(" → ")+chalk.bold.cyan("env python")+chalk.gray(" (install guide)")),console.log())}if(0===o.attempts)console.log(chalk.white(" New here? Start with ")+chalk.bold.cyan("demo")+chalk.white(" — it takes a few minutes.")),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.bold.cyan(" demo")+chalk.gray(` ${n}`)),console.log(chalk.white(" lang")+chalk.gray(" List all supported languages")),console.log(chalk.white(" lang es")+chalk.gray(" Switch language (e.g. lang es, lang zh, lang fr)")),console.log(chalk.gray(" ─────────────────────────────────────────────"));else if(e||l){const e=1===o.attempts?"attempt":"attempts";o.attempts>0&&console.log(chalk.green(" ✓ Demo completed ")+chalk.gray(`(${o.attempts} ${e})`)),l||console.log(chalk.green(" ✓ Environment ready")),console.log(chalk.yellow(" → Enter your exam token to begin.")),console.log(chalk.gray(" (10-char code from your organizer, starts with your country code like ")+chalk.cyan("UA")+chalk.gray(" — case-insensitive)")),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.bold.yellow(" exam <token>")+chalk.gray(" Enter exam (primary action — use your organizer-issued token)")),console.log(chalk.gray(" format: ")+chalk.white("exam UAxxxxxxxx")+chalk.gray(" (2-letter country prefix + 8 chars)")),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.gray(" Other commands:")),console.log(chalk.white(" demo")+chalk.gray(` ${n}`)),l||console.log(chalk.white(" exam setup")+chalk.gray(" Re-verify tool environment")),console.log(chalk.white(" lang")+chalk.gray(" List all supported languages")),console.log(chalk.white(" lang es")+chalk.gray(" Switch language (e.g. lang es, lang zh, lang fr)")),console.log(chalk.gray(" ─────────────────────────────────────────────"))}else{const e=1===o.attempts?"attempt":"attempts";console.log(chalk.green(" ✓ Demo completed ")+chalk.gray(`(${o.attempts} ${e}${o.bestPercentage>0?` · best ${o.bestPercentage}%`:""})`)),console.log(chalk.yellow(" → Next: prepare your environment for the real exam.")),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.white(" demo")+chalk.gray(` ${n}`)),console.log(chalk.bold.yellow(" exam setup")+chalk.gray(" Install tools for national selection (~150MB)")),console.log(chalk.white(" lang")+chalk.gray(" List all supported languages")),console.log(chalk.white(" lang es")+chalk.gray(" Switch language (e.g. lang es, lang zh, lang fr)")),console.log(chalk.gray(" ─────────────────────────────────────────────"))}console.log(chalk.gray(" ")+chalk.gray("Tip: ")+chalk.cyan("help")+chalk.gray(" for commands · ")+chalk.cyan("Ctrl+C")+chalk.gray(" pauses · ")+chalk.cyan("quit")+chalk.gray(" closes")),console.log()}export async function startRepl(e,x){const v=l(),C=n(),k=process.exit.bind(process),I=a();if(v.demoCleanedForVersion!==W){try{const{existsSync:o,unlinkSync:e}=await import("node:fs"),{join:t}=await import("node:path"),{getIcoaDir:n}=await import("./lib/config.js"),l=t(n(),"demo-state.json");o(l)&&e(l)}catch{}s({demoCleanedForVersion:W})}const{select:A,confirm:T}=await import("@inquirer/prompts"),_=v.mode||"",V=[{name:` ${chalk.bold("National Selection")} ${chalk.gray("—")} ${chalk.gray("demo, exam (lightweight)")}`,value:"selection"},{name:` ${chalk.bold("International Olympiad")} ${chalk.gray("—")} ${chalk.gray("CTF × AI (~500MB, advanced)")}`,value:"olympiad"},{name:` ${chalk.bold("National/Regional Partner")} ${chalk.gray("—")} ${chalk.gray("organizer tools (tokens, competitions)")}`,value:"organizer"},{name:` ${chalk.gray("About ICOA")} ${chalk.gray("·")} ${chalk.gray("Info & contact")}`,value:"about"}];console.log(chalk.gray(" Use ")+chalk.yellow("↑")+chalk.gray(" or ")+chalk.yellow("↓")+chalk.gray(" to select, ")+chalk.yellow("Enter")+chalk.gray(" to confirm.")),console.log();let G="";for(;!G;){const o=await A({message:"Mode",choices:V,default:_||"selection"});"about"!==o?G=o:(console.clear(),console.log(),console.log(chalk.cyan(" ═══════════════════════════════════════════════════")),console.log(chalk.bold.yellow(" ICOA")+chalk.white(" — AI-Native CLI OS for Cyber & AI Security")),console.log(chalk.gray(" Olympiad & Competition · K-12 to University")),console.log(chalk.cyan(" ───────────────────────────────────────────────────")),console.log(),console.log(chalk.bold.white(" What Makes ICOA Different")),console.log(chalk.gray(" · AI-native AI teammate, AI adversary, AI translation")),console.log(chalk.gray(" · CLI OS Complete competition environment in terminal")),console.log(chalk.gray(" · 110 tools pwntools, z3, gdb, nmap, sleuthkit... pre-configured")),console.log(chalk.gray(" · Global scale 15,000+ concurrent exams · 15 languages")),console.log(),console.log(chalk.bold.white(" Competition Format")),console.log(" "+chalk.green.bold("AI4CTF")+chalk.gray(" [Day 1] AI as teammate — 5hr jeopardy CTF")),console.log(" "+chalk.red.bold("CTF4AI")+chalk.gray(" [Day 2] Challenge AI — adversarial ML, red-team")),console.log(),console.log(chalk.white(" Sydney, Australia")+chalk.gray(" · Jun 27 - Jul 2, 2026 · 40+ countries")),console.log(),console.log(chalk.bold.white(" Organized by")+chalk.gray(" ASRA (Australia) · ICO Foundation Inc")),console.log(chalk.bold.white(" Contact ")+chalk.cyan(" australia@icoa2026.au · accreditation@icoa2026.au")),console.log(chalk.bold.white(" Website ")+chalk.cyan.underline(" https://icoa2026.au")),console.log(chalk.cyan(" ═══════════════════════════════════════════════════")),console.log(),console.log(chalk.gray(" Press ")+chalk.yellow("Enter")+chalk.gray(" to return...")),await new Promise(o=>{const e=t=>{process.stdin.removeListener("data",e),process.stdin.isTTY&&process.stdin.setRawMode&&process.stdin.setRawMode(!1),process.stdin.pause(),o()};process.stdin.isTTY&&process.stdin.setRawMode&&process.stdin.setRawMode(!0),process.stdin.resume(),process.stdin.once("data",e)}),console.clear())}if("olympiad"===G&&"olympiad"!==_&&(console.log(),console.log(chalk.yellow(" This mode will download ~500MB of CTF tools and AI models.")),await T({message:"Continue?",default:!0})||(G="selection",console.log(chalk.gray(" Switched to National Selection mode.")))),G!==_&&s({mode:G}),console.log(),"olympiad"===G&&m(W)){p(W),console.log(chalk.gray(" Checking competition environment..."));const{execSync:o}=await import("node:child_process"),e=[{name:"pwntools",cmd:'python3 -c "import pwn"'},{name:"z3-solver",cmd:'python3 -c "import z3"'},{name:"numpy",cmd:'python3 -c "import numpy"'},{name:"requests",cmd:'python3 -c "import requests"'}];let t=0;for(const n of e)try{o(n.cmd,{stdio:"ignore"})}catch{t++}if(t>0){console.log(chalk.yellow(` ${t} core libraries missing.`));try{const{confirm:o}=await import("@inquirer/prompts");if(await o({message:" Install competition Python libraries now?",default:!0,theme:{prefix:"",style:{message:o=>chalk.green(o),defaultAnswer:o=>chalk.green(o)}}})){console.log();const{execSync:o}=await import("node:child_process");o("icoa env setup",{stdio:"inherit"})}}catch{console.log(chalk.gray(" Run ")+chalk.white("env setup")+chalk.gray(" later to install."))}console.log()}else console.log(chalk.green(" All core libraries ready.")),console.log()}if(x){const o=y();if(o){const e=Math.floor(o.awaySeconds/60),t=o.awaySeconds%60;console.log(chalk.yellow(` Session resumed. Away: ${e}m ${t}s | Total exits: ${o.exitCount}`)),console.log()}}"selection"===G?Q():"organizer"===G?(console.log(chalk.yellow.bold(" [National/Regional Partner]")),console.log(),console.log(chalk.bold.white(" ██╗ ██████╗ ██████╗ █████╗")),console.log(chalk.bold.white(" ██║██╔════╝██╔═══██╗██╔══██╗")),console.log(chalk.bold.white(" ██║██║ ██║ ██║███████║")),console.log(chalk.bold.white(" ██║██║ ██║ ██║██╔══██║")),console.log(chalk.bold.white(" ██║╚██████╗╚██████╔╝██║ ██║")),console.log(chalk.bold.white(" ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝")),console.log(),console.log(chalk.yellow(" International Cyber Olympiad in AI 2026")),console.log(chalk.bold.magenta(" The World's First AI-Native CLI Operating System")),console.log(chalk.bold.magenta(" for Cybersecurity & AI Security Competition")),console.log(chalk.bold.magenta(" and Olympiad for K-12")),console.log(chalk.gray(" Sydney, Australia · Jun 27 - Jul 2, 2026")),console.log(),console.log(chalk.white(" Vision")),console.log(chalk.gray(" Building a global pipeline for youth cyber & AI")),console.log(chalk.gray(" security talent through education and competition.")),console.log(),console.log(chalk.white(" Capacity")),console.log(chalk.gray(" 15,000+ concurrent online examinations")),console.log(chalk.gray(" National selection, training, and education support")),console.log(),console.log(chalk.white(" Olympic Spirit")),console.log(chalk.gray(" Excellence · Friendship · Respect")),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.white(" New country accreditation & support:")),console.log(chalk.cyan(" australia@icoa2026.au")),console.log(chalk.cyan(" accreditation@icoa2026.au")),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(),C?(console.log(chalk.green(` Logged in as ${v.userName}`)),console.log(chalk.white(" exam list")+chalk.gray(" Manage exams")),console.log(chalk.white(" logout")+chalk.gray(" Disconnect"))):console.log(chalk.white(" join <url>")+chalk.gray(" Connect to manage exams")),console.log()):I&&!c()?(console.log(chalk.red(" Token was activated on a different device.")),console.log(chalk.gray(" Contact organizer for assistance.")),console.log()):C?(console.log(chalk.green.bold(` Welcome back, ${v.userName}!`)),console.log(chalk.gray(` Connected to ${v.ctfdUrl}`)),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.white(" Ready to compete? Start here:")),console.log(),console.log(chalk.bold.cyan(" challenges")+chalk.gray(" Browse challenges by category")),console.log(chalk.white(" status")+chalk.gray(" Your score & hint budget")),console.log(chalk.white(" scoreboard")+chalk.gray(" Live rankings")),console.log(chalk.white(" help")+chalk.gray(" Full command list")),console.log(),console.log(chalk.gray(" Tool environment:")),console.log(chalk.white(" env")+chalk.gray(" See which of the 110 CTF tools are installed")),console.log(chalk.white(" env setup")+chalk.gray(" Install anything missing (~5 min, one-time)")),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.gray(" Tip: ")+chalk.cyan("help")+chalk.gray(" · ")+chalk.cyan("Ctrl+C")+chalk.gray(" pauses · ")+chalk.cyan("quit")+chalk.gray(" closes")),console.log()):I?(U(),console.log(chalk.green.bold(" Welcome, competitor!")),console.log(chalk.gray(` Workspace: ${M}`)),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.white(" Get started:")),console.log(),console.log(chalk.white(" Step 1 ")+chalk.bold.cyan("join <url>")+chalk.gray(" Connect to competition server")),console.log(chalk.white(" Step 2 ")+chalk.bold.cyan("challenges")+chalk.gray(" Browse & solve challenges")),console.log(chalk.white(" Step 3 ")+chalk.bold.cyan("hint")+chalk.gray(" Ask AI when stuck")),console.log(),console.log(chalk.gray(" Before Step 1 — make sure your tools are ready:")),console.log(chalk.white(" env")+chalk.gray(" See which of the 110 CTF tools are installed")),console.log(chalk.white(" env setup")+chalk.gray(" Install anything missing (~5 min, one-time)")),console.log(),console.log(chalk.gray(" Also: ")+chalk.white("help")+chalk.gray(" all commands")),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.gray(" Tip: ")+chalk.cyan("Ctrl+C")+chalk.gray(" pauses · ")+chalk.cyan("exit")+chalk.gray(" → menu · ")+chalk.cyan("quit")+chalk.gray(" closes CLI")),console.log()):(console.log(chalk.bold.white(" Welcome to ICOA CLI — International Olympiad")),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.white(" To begin, activate your competition token:")),console.log(),console.log(chalk.bold.cyan(" activate <token>")),console.log(),console.log(chalk.gray(" While waiting, explore:")),console.log(chalk.white(" ref linux")+chalk.gray(" Quick reference for Linux")),console.log(chalk.white(" ref web")+chalk.gray(" Quick reference for Web")),console.log(chalk.white(" env")+chalk.gray(" See which of the 110 CTF tools are installed")),console.log(chalk.white(" env setup")+chalk.gray(" Install anything missing (~5 min, one-time)")),console.log(chalk.white(" help")+chalk.gray(" All available commands")),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.gray(" Tip: ")+chalk.cyan("Ctrl+C")+chalk.gray(" pauses · ")+chalk.cyan("exit")+chalk.gray(" → menu · ")+chalk.cyan("quit")+chalk.gray(" closes CLI")),console.log()),e.exitOverride(),e.configureOutput({writeErr:()=>{},writeOut:o=>{console.log(o)}});const J=o({input:process.stdin,output:process.stdout,prompt:F(),terminal:!0});let K=!1;d(!0),j();const Z=J.prompt.bind(J);J.prompt=o=>{h()||w()||J.setPrompt(F()),Z(o)},J.prompt(),J.on("line",async o=>{if(K)return;const n=o.trim();if(!n)return J.setPrompt(h()?chalk.magenta("ai4ctf> "):F()),void J.prompt();if(h()){K=!0;const o=await u(n);return K=!1,"exit"===o&&J.setPrompt(F()),void J.prompt()}if(w()){K=!0;const o=await f(n);return K=!1,"exit"!==o&&"solved"!==o||J.setPrompt(F()),void J.prompt()}if(L(n),"exit"===n)return b()?(console.log(),console.log(chalk.yellow(" ⚠ An exam is in progress.")),console.log(chalk.white(" To return to menu without losing progress, type: ")+chalk.bold.cyan("back")),console.log(chalk.white(" To fully close ICOA CLI, type: ")+chalk.bold.cyan("quit")),console.log(chalk.gray(" Your progress is auto-saved either way.")),console.log(),void J.prompt()):(console.log(),console.log(chalk.gray(" ")+chalk.white("exit")+chalk.gray(" returns to the main menu. To fully close ICOA CLI, type ")+chalk.bold.cyan("quit")+chalk.gray(".")),"selection"===G&&Q(),void J.prompt());if("quit"===n||"q"===n||"quit confirm"===n){const o=b();return o&&"demo-free"!==o.session.examId&&"quit confirm"!==n?(console.log(),console.log(chalk.yellow(" ⚠ A real exam is in progress.")),console.log(chalk.gray(" Your answers are auto-saved on the server, but the exam timer keeps ticking")),console.log(chalk.gray(" on the server side even if you close the CLI.")),console.log(),console.log(chalk.white(" To leave the CLI but keep the exam alive, type: ")+chalk.bold.cyan("back")),console.log(chalk.gray(" (recommended — you can resume with ")+chalk.cyan("exam q 1")+chalk.gray(" after relaunching icoa)")),console.log(),console.log(chalk.white(" To really close ICOA CLI, type: ")+chalk.bold.cyan("quit confirm")),console.log(),void J.prompt()):(o&&"demo-free"===o.session.examId&&(console.log(),console.log(chalk.gray(" Demo paused. Resume with: ")+chalk.white("demo")+chalk.gray(" (fresh) or ")+chalk.white("exam q 1")+chalk.gray(" (continue)."))),E(),g(),console.log(chalk.gray(" Session saved. Use ")+chalk.white("icoa --resume")+chalk.gray(" to continue.")),S(),void k(0))}if("back"===n||"menu"===n){const o=b(),e=o&&"demo-free"!==o.session.examId,t=o&&"demo-free"===o.session.examId&&(()=>{const e=new Date(o.session.startedAt||0).getTime();return Date.now()-e<18e5})();if(e)console.log(),console.log(chalk.gray(" Exam paused. Your progress is saved.")),console.log(chalk.white(" Resume: exam q 1")+chalk.gray(" · ")+chalk.white("exam review")+chalk.gray(" · ")+chalk.white("exam submit")),console.log();else if(t){const e=Object.keys(o.answers).length,t=o.session.questionCount;console.log(),console.log(chalk.gray(` Demo paused (${e}/${t} answered). Resume with: `)+chalk.white("exam q 1")),console.log(chalk.gray(" Or type ")+chalk.white("demo")+chalk.gray(" to restart.")),console.log()}else{if(o&&"demo-free"===o.session.examId){const{clearExamState:o}=await import("./lib/exam-state.js");o("demo-free")}const e=l();fetch("https://practice.icoa2026.au/api/icoa/demo-stats",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"post-report-back",lang:e.language||"en",timestamp:(new Date).toISOString()}),signal:AbortSignal.timeout(5e3)}).catch(()=>{}),"selection"===G?Q():console.log(chalk.gray(" Already at main menu."))}return void J.prompt()}if("help"===n||"?"===n){if(b()){K=!0;try{await e.parseAsync(["node","icoa","exam","help"])}catch{}return K=!1,void J.prompt()}return function(o,e="olympiad"){console.log(),"selection"===e||"organizer"===e?(console.log(chalk.bold.white(" Exam")),console.log(chalk.white(" join <url> ")+chalk.gray("Connect to exam server")),console.log(chalk.white(" exam list ")+chalk.gray("Available exams")),console.log(chalk.white(" exam start <id> ")+chalk.gray("Begin an exam")),console.log(chalk.white(" exam q [n] ")+chalk.gray("View questions")),console.log(chalk.white(" exam answer <n> <X> ")+chalk.gray("Answer question")),console.log(chalk.white(" exam review ")+chalk.gray("Review all answers")),console.log(chalk.white(" exam submit ")+chalk.gray("Submit for grading")),console.log(chalk.white(" exam result ")+chalk.gray("View your score")),console.log(),console.log(chalk.bold.white(" System")),console.log(chalk.white(" ref [topic] ")+chalk.gray("Quick reference")),console.log(chalk.white(" setup ")+chalk.gray("Settings / switch mode")),console.log(chalk.white(" lang [code] ")+chalk.gray("Switch language")),console.log(chalk.white(" clear ")+chalk.gray("Clear screen")),console.log(chalk.white(" exit ")+chalk.gray("Quit")),console.log()):o?(console.log(chalk.cyan(" ═══════════════════════════════════════════════")),console.log(chalk.bold.white(" How it works")),console.log(),console.log(chalk.gray(" 1. Browse ")+chalk.white("challenges")+chalk.gray(" and pick one")),console.log(chalk.gray(" 2. ")+chalk.white("open <id>")+chalk.gray(" to read the challenge")),console.log(chalk.gray(" 3. Use ")+chalk.white("hint")+chalk.gray(" / ")+chalk.white("hint-b")+chalk.gray(" / ")+chalk.white("hint-c")+chalk.gray(" when stuck")),console.log(chalk.gray(" 4. ")+chalk.white("submit <id> icoa{flag}")+chalk.gray(" to score points")),console.log(chalk.gray(" 5. Check ")+chalk.white("scoreboard")+chalk.gray(" to track your rank")),console.log(chalk.cyan(" ═══════════════════════════════════════════════")),console.log(),console.log(chalk.bold.white(" Competition")),console.log(chalk.white(" join <url> ")+chalk.gray("Connect to CTFd")),console.log(chalk.white(" challenges (ch) ")+chalk.gray("List challenges by category")),console.log(chalk.white(" open <id> ")+chalk.gray("Read challenge + get next steps")),console.log(chalk.white(" submit <id> <flag> ")+chalk.gray("Submit a flag")),console.log(chalk.white(" scoreboard (sb) ")+chalk.gray("Live rankings")),console.log(chalk.white(" status ")+chalk.gray("Your score, budget & timer")),console.log(chalk.white(" time ")+chalk.gray("Countdown timer")),console.log(),console.log(chalk.bold.white(" AI Teammate")+chalk.gray(" — 3 levels, use wisely")),console.log(chalk.white(' hint "question" ')+chalk.gray("Level A — General guidance (50 uses)")),console.log(chalk.white(' hint-b "question" ')+chalk.gray("Level B — Deep analysis (10 uses)")),console.log(chalk.white(' hint-c "question" ')+chalk.gray("Level C — Critical assist (2 uses)")),console.log(chalk.white(" hint budget ")+chalk.gray("Check remaining uses")),console.log(chalk.white(" ai4ctf ")+chalk.gray("Free-chat with AI (no limit)")),console.log(),console.log(chalk.bold.white(" Tools")),console.log(chalk.white(" ref [topic] ")+chalk.gray("Quick reference (linux, web, crypto...)")),console.log(chalk.white(" shell ")+chalk.gray("Docker sandbox")),console.log(chalk.white(" files <id> ")+chalk.gray("Download challenge files")),console.log(chalk.white(" connect <id> ")+chalk.gray("Connect to remote target")),console.log(chalk.white(" note [text] ")+chalk.gray("Personal notepad")),console.log(chalk.white(" log ")+chalk.gray("Session history")),console.log(),console.log(chalk.bold.white(" System")),console.log(chalk.white(" setup ")+chalk.gray("Configure settings")),console.log(chalk.white(" lang [code] ")+chalk.gray("Switch language (15 supported)")),console.log(chalk.white(" logout ")+chalk.gray("Disconnect")),console.log(chalk.white(" clear ")+chalk.gray("Clear screen")),console.log(chalk.white(" exit ")+chalk.gray("Quit (session saved)")),console.log()):(console.log(chalk.bold.yellow(" Restricted Mode — activate with a token to unlock all commands")),console.log(),console.log(chalk.white(" activate <token> ")+chalk.gray("Unlock full access")),console.log(chalk.white(" ref [topic] ")+chalk.gray("Quick reference")),console.log(chalk.white(" exit ")+chalk.gray("Quit")),console.log())}(a(),G),void J.prompt()}if("more help"===n.toLowerCase()&&b()){K=!0;try{await e.parseAsync(["node","icoa","exam","more-help"])}catch{}return K=!1,void J.prompt()}if("continue"===n.toLowerCase())return console.log(),console.log(chalk.green.bold(" ═══ AI4CTF — AI as Your Teammate ═══")),console.log(),console.log(chalk.white(" In AI4CTF, you solve cybersecurity challenges")),console.log(chalk.white(" with AI by your side.")),console.log(),console.log(chalk.white(" In competition, you get AI help at 3 levels:")),console.log(chalk.yellow(" hint a")+chalk.gray(" General guidance (50 uses)")),console.log(chalk.yellow(" hint b")+chalk.gray(" Deep analysis (10 uses)")),console.log(chalk.yellow(" hint c")+chalk.gray(" Critical assist (2 uses)")),console.log(),console.log(chalk.white(" Try it now! Type: ")+chalk.bold.green("ai4ctf")),console.log(chalk.gray(' Chat freely with your AI teammate. Type "exit" when done.')),console.log(),console.log(chalk.gray(" After ai4ctf, try: ")+chalk.bold.red("ctf4ai")+chalk.gray(' — trick the AI into saying "koala"')),console.log(),void J.prompt();if(/^ICOA-[A-Z]{2,3}-\d{1,6}$/i.test(n.trim())){K=!0;try{await e.parseAsync(["node","icoa","exam","token",n.trim()])}catch{}return K=!1,void J.prompt()}if(/^[A-Z]{2}[0-9A-HJKMNP-TV-Z]{8}$/i.test(n.trim())){K=!0;try{await e.parseAsync(["node","icoa","exam","token",n.trim().toUpperCase()])}catch{}return K=!1,void J.prompt()}const s=n.match(/^exam\s+([A-Z]{2}[0-9A-HJKMNP-TV-Z]{8})$/i);if(s){K=!0;try{await e.parseAsync(["node","icoa","exam","token",s[1].toUpperCase()])}catch{}return K=!1,void J.prompt()}const y=n.match(/^exam\s+([A-Z]{2,3})$/i);if(y){K=!0;try{await e.parseAsync(["node","icoa","exam","list",y[1]])}catch{}return K=!1,void J.prompt()}if("clear"===n||"cls"===n)return console.clear(),void J.prompt();if(n.startsWith("activate ")){const o=n.slice(9).trim(),e=r(o);return"ok"===e?console.log(chalk.green(" Access granted! Token bound to this device.")):"already_bound"===e?(console.log(),console.log(chalk.red(" Token already activated on a different device.")),console.log(chalk.gray(" Each token binds to the first device that uses it. If you lost the device,")),console.log(chalk.gray(" contact your proctor to have the token re-issued for a new device."))):(console.log(),console.log(chalk.red(" Token not recognized.")),console.log(chalk.gray(" Possible reasons:")),console.log(chalk.white(" • ")+chalk.gray("Typo — tokens are case-insensitive, 10 chars, start with a 2-letter country code (e.g. ")+chalk.cyan("UAK7M2R9Q4")+chalk.gray(")")),console.log(chalk.white(" • ")+chalk.gray("Expired — ask your proctor or organizer for a fresh token")),console.log(chalk.white(" • ")+chalk.gray("Network — verify connection to ")+chalk.cyan("practice.icoa2026.au")),console.log(chalk.gray(" Still stuck? type ")+chalk.cyan("help")+chalk.gray(" or try ")+chalk.cyan("exam demo")+chalk.gray(" for a free practice round."))),console.log(),void J.prompt()}if("activate"===n)return console.log(chalk.gray(" Usage: ")+chalk.white("activate <token>")),console.log(),void J.prompt();const m=b();if(m){const o=n.toUpperCase().trim(),t=o=>{const e=m.questions.find(e=>e.number===o);return!!e&&("ai4ctf"===e.type||"ctf4ai"===e.type||e.options&&!e.options.A&&!e.options.B)},l=o=>{const e="demo-free"!==m.session.examId,t=e&&o>=39?"ctf4ai":e&&o>=31?"ai4ctf":null;console.log(),console.log(chalk.yellow(` Q${o} is a practical question — letters (A/B/C/D) don't apply here.`)),t?(console.log(chalk.white(" Enter the AI chat for this question: ")+chalk.bold.cyan(t)),console.log(chalk.gray(" Or submit a flag directly: ")+chalk.green(`exam answer ${o} ICOA{your_flag}`))):console.log(chalk.gray(" Submit a flag: ")+chalk.green(`exam answer ${o} ICOA{your_flag}`)),console.log()};if(/^[ABCD]$/.test(o)){const n=m._lastQ||1;if(t(n))return l(n),void J.prompt();K=!0;try{await e.parseAsync(["node","icoa","exam","answer",String(n),o])}catch{}return K=!1,void J.prompt()}const s=o.match(/^(\d+)\s+([ABCD])$/);if(s){const o=parseInt(s[1],10);if(t(o))return l(o),void J.prompt();K=!0;try{await e.parseAsync(["node","icoa","exam","answer",s[1],s[2]])}catch{}return K=!1,void J.prompt()}}const p=n.split(/\s+/)[0].toLowerCase(),d=/^python3?(\.\d+)?$/.test(p),x=n.startsWith("!")||p.startsWith("!")||d;if("selection"===G&&!x&&!["exam","demo","retry","nations","next","prev","continue","setup","lang","ref","ai4ctf","ctf4ai","mark","unmark","review","submit","env"].includes(p)){if(console.log(chalk.gray(" Not available in Selection mode.")),m){const o=m._lastQ||1;console.log(chalk.white(` Resume exam: exam q ${o}`)+chalk.gray(" · ")+chalk.white("A/B/C/D")+chalk.gray(" to answer"))}else console.log(chalk.gray(" Try: demo · setup to switch mode"));return console.log(),void J.prompt()}if("organizer"===G&&!["join","exam","demo","retry","next","prev","logout","setup","lang","ref","ctf","mark","unmark","review","submit"].includes(p))return console.log(chalk.gray(" Not available in Organizer mode. Switch via: setup")),console.log(),void J.prompt();if(!("olympiad"!==G||a()&&c()||i(p)))return console.log(chalk.yellow(" Restricted mode. ")+chalk.gray("Enter your access token:")),console.log(chalk.white(" activate <token>")),console.log(),console.log(chalk.gray(" Free commands: ")+chalk.white("ref [topic]")+chalk.gray(", ")+chalk.white("help")+chalk.gray(", ")+chalk.white("exit")),console.log(),void J.prompt();if(!["join","activate","challenges","ch","open","submit","flag","scoreboard","sb","status","time","hint","hint-b","hint-c","hint-budget","ref","shell","files","connect","note","log","lang","setup","env","ai4ctf","model","ctf","exam","demo","retry","nations","next","prev","continue","logout","ctf4ai","mark","unmark","review","submit"].includes(p)){if(z.has(p))return console.log(chalk.red(` Blocked: ${p} is not allowed during competition.`)),console.log(),void J.prompt();if(/(?:^|\s)(?:\/(?!home\/|Users\/|tmp\/)|\.\.\/|~\/)/.test(n)&&!n.startsWith("cd ")){const o=/(?:^|\s)\/(?!home\/\w+\/icoa-workspace|Users\/\w+\/icoa-workspace|tmp\/)/.test(n),e=/\.\./.test(n);if(o||e)return console.log(chalk.red(" Blocked: access outside workspace is not allowed.")),console.log(chalk.gray(` Workspace: ${M}`)),console.log(),void J.prompt()}let o=n.startsWith("!")?n.slice(1).trim():n;if("darwin"===process.platform){const e="/opt/homebrew/opt/python@3.12/bin/python3.12";o=o.replace(/^python3?\s/,`${e} `).replace(/^(python3|python)$/,e)}else if("win32"===process.platform){const e=(()=>{try{return t("py -3 --version",{stdio:["ignore","ignore","ignore"],timeout:1500}),"py -3"}catch{}return"python"})();o=o.replace(/^python3?(\.\d+)?\s/,`${e} `).replace(/^python3?(\.\d+)?$/,e)}else{const e=(()=>{try{return t("which python3.12",{stdio:"ignore"}),"python3.12"}catch{return"python3"}})();o=o.replace(/^python\s/,`${e} `).replace(/^python$/,e)}const e=U();/^(\S*python3?(\.\d+)?)\s*$/.test(o)&&(o=`PYTHONSTARTUP="${function(){const o=N(D(),".icoa");P(o)||R(o,{recursive:!0});const e=N(o,"python-startup.py");if(!P(e)){const{writeFileSync:o}=require("node:fs");o(e,"# ICOA exam interactive startup — auto-loaded by PYTHONSTARTUP\nimport base64, struct, hashlib, re, json, os, sys, binascii\ntry: import requests\nexcept ImportError: pass\ntry: from Crypto.Cipher import AES\nexcept ImportError: pass\ntry: from Crypto.Util.Padding import pad, unpad\nexcept ImportError: pass\ntry: from pwn import xor, p32, u32, p64, u64\nexcept ImportError: pass\ntry: import bs4\nexcept ImportError: pass\ntry: import numpy as np\nexcept ImportError: pass\n")}return e}()}" ${o}`,console.log(),console.log(chalk.cyan(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")),console.log(chalk.bold.white(" Python ready — ICOA exam toolkit pre-loaded")),console.log(chalk.cyan(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")),console.log(),console.log(chalk.white(" Already imported: ")+chalk.gray("base64, struct, hashlib, re, json, binascii")),console.log(chalk.white(" Also available: ")+chalk.gray("requests, bs4, numpy, AES, pad/unpad, xor, p32/u32/p64/u64")),console.log(),console.log(chalk.yellow(" Quick examples:")),console.log(chalk.gray(' base64.b64decode("aGVsbG8=") ')+chalk.gray("# decode base64")),console.log(chalk.gray(' bytes.fromhex("48656c6c6f") ')+chalk.gray("# hex → bytes")),console.log(chalk.gray(' "ICOA{x}".encode() ')+chalk.gray("# str → bytes")),console.log(chalk.gray(" [chr(c) for c in [73,67,79,65]] ")+chalk.gray("# ASCII codes")),console.log(chalk.gray(' xor(bytes.fromhex("0a2b"), b"IC") ')+chalk.gray("# pwntools XOR")),console.log(),console.log(chalk.gray(" Exit: ")+chalk.white("exit()")+chalk.gray(" or Ctrl-D")),console.log(chalk.cyan(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")),console.log()),K=!0;try{q()&&await $()?await O(o,J):await Y(o,J,e)}catch{console.log(chalk.yellow(` Command failed: ${p}`))}return K=!1,console.log(),void J.prompt()}K=!0;const v=m&&"submit"===n.toLowerCase()?["exam","submit"]:function(o){const e=o.split(/\s+/),t=e[0].toLowerCase(),n=e.slice(1),l={demo:["exam","demo"],retry:["exam","demo-retry"],nations:["exam","nations"],next:["exam","next"],prev:["exam","prev"],mark:["exam","mark",...n],unmark:["exam","unmark",...n],review:["exam","review"],logout:["ctf","logout"],join:["ctf","join",...n],activate:["ctf","activate",...n],challenges:["ctf","challenges"],ch:["ctf","challenges"],open:["ctf","open",...n],submit:["ctf","submit",...n],flag:["ctf","submit",...n],scoreboard:["ctf","scoreboard",...n],sb:["ctf","scoreboard",...n],status:["ctf","status"],time:["ctf","time"]};return l[t]?l[t]:["hint","hint-b","hint-c","hint-budget","ref","shell","files","connect","note","log","lang","setup","env","ai4ctf","model","ctf","exam","ctf4ai"].includes(t)?[t,...n]:e}(n),C="ctf"===v[0]&&"join"===v[1];C&&J.pause(),process.exit=()=>{throw new Error(B)};try{await e.parseAsync(["node","icoa",...v])}catch(o){const e=o instanceof Error?o.message:String(o);if(e===B);else if(e.includes("commander.unknownCommand")){const{distance:o}=await import("fastest-levenshtein"),e=["ctf","hint","hint-b","hint-c","hint-budget","ref","shell","files","connect","note","log","lang","setup","env","ai4ctf","exam","ctf4ai","theme","clear","cls","quit","exit","back","menu","help","continue","activate","demo","challenges","status","scoreboard","join","logout"],t=p.split(/\s+/)[0]||p;let n={word:"",dist:1/0};for(const l of e){const e=o(t.toLowerCase(),l);e<n.dist&&(n={word:l,dist:e})}console.log(chalk.yellow(` Unknown command: ${p}.`)),n.dist>0&&n.dist<=2&&console.log(chalk.gray(" Did you mean: ")+chalk.bold.cyan(n.word)+chalk.gray("?")),console.log(chalk.gray(" Type ")+chalk.cyan("help")+chalk.gray(" for the full command list."))}else e.includes("commander.")||(e.includes("fetch failed")||e.includes("ECONNREFUSED")||e.includes("ETIMEDOUT"))&&console.log(chalk.yellow(" Network error. Check your connection."))}finally{process.exit=k,K=!1,C&&J.resume()}h()?J.setPrompt(chalk.magenta("ai4ctf> ")):w()&&J.setPrompt(chalk.red("ctf4ai> ")),console.log(),J.prompt()}),J.on("SIGINT",()=>{if(console.log(),h()||w())console.log(chalk.yellow(" Ctrl+C did not close ICOA CLI — you are still in the AI chat.")),console.log(chalk.white(" Type ")+chalk.bold.cyan("exit")+chalk.white(" to leave the chat and return to the menu."));else if(b()){const o="demo-free"!==b().session.examId;console.log(chalk.yellow(" Ctrl+C did NOT close ICOA CLI.")),console.log(chalk.gray(` Your ${o?"exam":"demo"} is paused and every answer is auto-saved.`)),console.log(),console.log(chalk.white(" Resume: ")+chalk.cyan("exam q 1")+chalk.gray(" · Back to menu: ")+chalk.cyan("back")+chalk.gray(" · Close CLI: ")+chalk.cyan(o?"quit confirm":"quit"))}else console.log(chalk.yellow(" Ctrl+C did not close ICOA CLI — you are still at the ")+chalk.cyan("icoa>")+chalk.yellow(" prompt.")),console.log(chalk.gray(" Keep typing — ")+chalk.cyan("help")+chalk.gray(" lists commands. (Only ")+chalk.cyan("quit")+chalk.gray(" or Ctrl+D actually close the CLI.)"));console.log(),J.prompt()}),J.on("close",()=>{E(),g(),S(),k(0)})}function Y(o,t,n){return new Promise(l=>{const s=process.stdin,a=!!s.isTTY&&!!s.isRaw;if(t.pause(),s.isTTY&&"function"==typeof s.setRawMode)try{s.setRawMode(!1)}catch{}const r=e(o,{shell:!0,stdio:"inherit",cwd:n||process.cwd()}),i=()=>{if(s.isTTY&&"function"==typeof s.setRawMode&&a)try{s.setRawMode(!0)}catch{}t.resume(),l()};r.on("close",i),r.on("error",i)})}
|