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.
Files changed (45) hide show
  1. package/dist/commands/ai4ctf.js +1 -700
  2. package/dist/commands/connect.js +1 -66
  3. package/dist/commands/ctf.js +1 -620
  4. package/dist/commands/ctf4ai-demo.js +1 -525
  5. package/dist/commands/env.js +1 -737
  6. package/dist/commands/exam.js +1 -2353
  7. package/dist/commands/files.js +1 -52
  8. package/dist/commands/hint.js +1 -119
  9. package/dist/commands/lang.js +1 -155
  10. package/dist/commands/log.js +1 -163
  11. package/dist/commands/note.js +1 -32
  12. package/dist/commands/ref.js +1 -63
  13. package/dist/commands/setup.js +1 -103
  14. package/dist/commands/shell.js +1 -55
  15. package/dist/commands/theme.js +1 -50
  16. package/dist/index.js +1 -225
  17. package/dist/lib/access.js +1 -246
  18. package/dist/lib/budget.js +1 -42
  19. package/dist/lib/colors.js +1 -21
  20. package/dist/lib/config.js +1 -60
  21. package/dist/lib/ctfd-client.js +1 -274
  22. package/dist/lib/demo-exam.js +1 -249
  23. package/dist/lib/demo-flags.js +1 -27
  24. package/dist/lib/demo-stats.js +1 -65
  25. package/dist/lib/exam-client.js +1 -57
  26. package/dist/lib/exam-setup.js +1 -23
  27. package/dist/lib/exam-state.js +1 -112
  28. package/dist/lib/gemini.js +1 -235
  29. package/dist/lib/i18n.js +1 -273
  30. package/dist/lib/log-sync.js +1 -110
  31. package/dist/lib/logger.js +1 -59
  32. package/dist/lib/paper-upgrade.js +1 -117
  33. package/dist/lib/platform.js +1 -86
  34. package/dist/lib/sandbox.js +1 -93
  35. package/dist/lib/terminal.js +1 -49
  36. package/dist/lib/theme.js +1 -108
  37. package/dist/lib/translation.js +1 -66
  38. package/dist/lib/ui.js +1 -80
  39. package/dist/lib/update-check.js +1 -102
  40. package/dist/postinstall.js +1 -48
  41. package/dist/repl.js +1 -1259
  42. package/dist/types/index.d.ts +1 -1
  43. package/dist/types/index.js +1 -38
  44. package/package.json +6 -2
  45. 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)})}