icoa-cli 2.19.100 → 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 -738
  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 -165
  11. package/dist/commands/note.js +1 -40
  12. package/dist/commands/ref.js +1 -68
  13. package/dist/commands/setup.js +1 -122
  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 -1281
  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
@@ -1,122 +1 @@
1
- import chalk from 'chalk';
2
- import { input, select } from '@inquirer/prompts';
3
- import { getConfig, saveConfig } from '../lib/config.js';
4
- import { setApiKey } from '../lib/gemini.js';
5
- import { printSuccess, printError, printInfo, printHeader, printKeyValue } from '../lib/ui.js';
6
- import { logCommand } from '../lib/logger.js';
7
- export function registerSetupCommand(program) {
8
- program
9
- .command('setup')
10
- .description('Configure ICOA CLI settings')
11
- .action(async () => {
12
- logCommand('setup');
13
- const config = getConfig();
14
- printHeader('ICOA CLI Setup');
15
- console.log();
16
- console.log(chalk.gray(' Tip: press ') + chalk.cyan('Ctrl+C') + chalk.gray(' any time to cancel setup and return to the ') + chalk.cyan('icoa>') + chalk.gray(' prompt — nothing is saved until you confirm.'));
17
- console.log();
18
- // Show current configuration
19
- printKeyValue('CTFd URL', config.ctfdUrl || chalk.gray('Not configured'));
20
- printKeyValue('CTFd Token', config.token ? chalk.green('Configured') : chalk.gray('Not configured'));
21
- printKeyValue('Gemini API Key', config.geminiApiKey || process.env.GEMINI_API_KEY ? chalk.green('Configured') : chalk.gray('Not configured'));
22
- printKeyValue('Language', config.language);
23
- printKeyValue('Mode', config.mode || chalk.gray('Not set'));
24
- printKeyValue('Session ID', config.sessionId.substring(0, 8) + '...');
25
- console.log();
26
- try {
27
- const action = await select({
28
- message: 'What would you like to configure?',
29
- choices: [
30
- { name: 'Switch Mode', value: 'mode' },
31
- { name: 'Gemini API Key', value: 'gemini' },
32
- { name: 'CTFd Connection', value: 'ctfd' },
33
- { name: 'Reset Hint Budget', value: 'budget' },
34
- { name: 'View All Settings', value: 'view' },
35
- { name: 'Exit', value: 'exit' },
36
- ],
37
- });
38
- switch (action) {
39
- case 'mode': {
40
- const newMode = await select({
41
- message: 'Select mode:',
42
- choices: [
43
- { name: 'National Selection — demo, exam, lightweight', value: 'selection' },
44
- { name: 'International Olympiad — Full CTF with AI assistance', value: 'olympiad' },
45
- { name: 'National/Regional Partner — Organizer management', value: 'organizer' },
46
- ],
47
- });
48
- saveConfig({ mode: newMode });
49
- printSuccess(`Mode switched to: ${newMode}. Restart ICOA CLI to apply.`);
50
- break;
51
- }
52
- case 'gemini': {
53
- console.log();
54
- printInfo('Get your API key from: https://aistudio.google.com/apikey');
55
- console.log();
56
- const key = await input({ message: 'Enter Gemini API Key:' });
57
- if (key.trim()) {
58
- setApiKey(key.trim());
59
- printSuccess('Gemini API key saved.');
60
- }
61
- else {
62
- printError('No key provided.');
63
- }
64
- break;
65
- }
66
- case 'ctfd': {
67
- printInfo('Use "join <url>" to connect to a CTFd instance.');
68
- break;
69
- }
70
- case 'budget': {
71
- const { confirm } = await import('@inquirer/prompts');
72
- const proceed = await confirm({
73
- message: 'Reset hint budget to defaults (A:50, B:10, C:2)? This cannot be undone.',
74
- default: false,
75
- });
76
- if (proceed) {
77
- const { saveBudget } = await import('../lib/config.js');
78
- const { DEFAULT_BUDGET } = await import('../types/index.js');
79
- saveBudget({ ...DEFAULT_BUDGET });
80
- printSuccess('Hint budget reset to defaults.');
81
- }
82
- break;
83
- }
84
- case 'view': {
85
- console.log();
86
- printHeader('Full Configuration');
87
- const full = getConfig();
88
- for (const [key, value] of Object.entries(full)) {
89
- if (key === 'token' && value) {
90
- printKeyValue(key, value.toString().substring(0, 8) + '...');
91
- }
92
- else if (key === 'geminiApiKey' && value) {
93
- printKeyValue(key, value.toString().substring(0, 8) + '...');
94
- }
95
- else {
96
- printKeyValue(key, String(value ?? 'null'));
97
- }
98
- }
99
- console.log();
100
- break;
101
- }
102
- case 'exit':
103
- break;
104
- }
105
- console.log();
106
- console.log(chalk.gray(' Setup complete. You are back at the ') + chalk.cyan('icoa>') + chalk.gray(' prompt.'));
107
- console.log();
108
- }
109
- catch (err) {
110
- // @inquirer/prompts throws ExitPromptError on Ctrl+C. Swallow it cleanly
111
- // so users don't see a scary stack trace — setup is entirely reversible,
112
- // and every save in this wizard is behind an explicit confirm step.
113
- if (err?.name === 'ExitPromptError') {
114
- console.log();
115
- console.log(chalk.gray(' Setup cancelled. You are back at the ') + chalk.cyan('icoa>') + chalk.gray(' prompt. Nothing was saved.'));
116
- console.log();
117
- return;
118
- }
119
- throw err;
120
- }
121
- });
122
- }
1
+ import chalk from"chalk";import{input as e,select as o}from"@inquirer/prompts";import{getConfig as t,saveConfig as i}from"../lib/config.js";import{setApiKey as n}from"../lib/gemini.js";import{printSuccess as a,printError as r,printInfo as s,printHeader as c,printKeyValue as l}from"../lib/ui.js";import{logCommand as g}from"../lib/logger.js";export function registerSetupCommand(m){m.command("setup").description("Configure ICOA CLI settings").action(async()=>{g("setup");const m=t();c("ICOA CLI Setup"),console.log(),console.log(chalk.gray(" Tip: press ")+chalk.cyan("Ctrl+C")+chalk.gray(" any time to cancel setup and return to the ")+chalk.cyan("icoa>")+chalk.gray(" prompt — nothing is saved until you confirm.")),console.log(),l("CTFd URL",m.ctfdUrl||chalk.gray("Not configured")),l("CTFd Token",m.token?chalk.green("Configured"):chalk.gray("Not configured")),l("Gemini API Key",m.geminiApiKey||process.env.GEMINI_API_KEY?chalk.green("Configured"):chalk.gray("Not configured")),l("Language",m.language),l("Mode",m.mode||chalk.gray("Not set")),l("Session ID",m.sessionId.substring(0,8)+"..."),console.log();try{switch(await o({message:"What would you like to configure?",choices:[{name:"Switch Mode",value:"mode"},{name:"Gemini API Key",value:"gemini"},{name:"CTFd Connection",value:"ctfd"},{name:"Reset Hint Budget",value:"budget"},{name:"View All Settings",value:"view"},{name:"Exit",value:"exit"}]})){case"mode":{const e=await o({message:"Select mode:",choices:[{name:"National Selection — demo, exam, lightweight",value:"selection"},{name:"International Olympiad — Full CTF with AI assistance",value:"olympiad"},{name:"National/Regional Partner — Organizer management",value:"organizer"}]});i({mode:e}),a(`Mode switched to: ${e}. Restart ICOA CLI to apply.`);break}case"gemini":{console.log(),s("Get your API key from: https://aistudio.google.com/apikey"),console.log();const o=await e({message:"Enter Gemini API Key:"});o.trim()?(n(o.trim()),a("Gemini API key saved.")):r("No key provided.");break}case"ctfd":s('Use "join <url>" to connect to a CTFd instance.');break;case"budget":{const{confirm:e}=await import("@inquirer/prompts");if(await e({message:"Reset hint budget to defaults (A:50, B:10, C:2)? This cannot be undone.",default:!1})){const{saveBudget:e}=await import("../lib/config.js"),{DEFAULT_BUDGET:o}=await import("../types/index.js");e({...o}),a("Hint budget reset to defaults.")}break}case"view":{console.log(),c("Full Configuration");const e=t();for(const[o,t]of Object.entries(e))l(o,"token"===o&&t||"geminiApiKey"===o&&t?t.toString().substring(0,8)+"...":String(t??"null"));console.log();break}}console.log(),console.log(chalk.gray(" Setup complete. You are back at the ")+chalk.cyan("icoa>")+chalk.gray(" prompt.")),console.log()}catch(e){if("ExitPromptError"===e?.name)return console.log(),console.log(chalk.gray(" Setup cancelled. You are back at the ")+chalk.cyan("icoa>")+chalk.gray(" prompt. Nothing was saved.")),void console.log();throw e}})}
@@ -1,55 +1 @@
1
- import { execSync } from 'node:child_process';
2
- import chalk from 'chalk';
3
- import { printError, printInfo, printSuccess } from '../lib/ui.js';
4
- import { logCommand } from '../lib/logger.js';
5
- export function registerShellCommand(program) {
6
- program
7
- .command('shell')
8
- .description('Open interactive shell in Docker sandbox')
9
- .action(async () => {
10
- logCommand('shell');
11
- // Check if Docker is available
12
- try {
13
- execSync('docker info', { stdio: 'ignore' });
14
- }
15
- catch {
16
- printError('Docker is not available.');
17
- console.log();
18
- console.log(chalk.gray(' Docker is required for the ICOA sandbox environment.'));
19
- console.log(chalk.gray(' Install Docker from: https://docs.docker.com/get-docker/'));
20
- console.log();
21
- return;
22
- }
23
- const imageName = 'icoa/sandbox:2026';
24
- const containerName = `icoa-sandbox-${Date.now()}`;
25
- // Check if image exists
26
- try {
27
- execSync(`docker image inspect ${imageName}`, { stdio: 'ignore' });
28
- }
29
- catch {
30
- printInfo(`Sandbox image not found. Pulling ${imageName}...`);
31
- try {
32
- execSync(`docker pull ${imageName}`, { stdio: 'inherit' });
33
- }
34
- catch {
35
- printError(`Failed to pull ${imageName}.`);
36
- printInfo('The sandbox image may not be published yet.');
37
- printInfo('You can build it locally: cd docker && docker build -t icoa/sandbox:2026 .');
38
- return;
39
- }
40
- }
41
- printSuccess('Launching sandbox...');
42
- console.log(chalk.gray(" Type 'exit' to return to ICOA CLI."));
43
- console.log();
44
- try {
45
- execSync(`docker run --rm -it --name ${containerName} --network=host ${imageName} /bin/bash`, { stdio: 'inherit' });
46
- console.log();
47
- printSuccess('Sandbox session ended.');
48
- }
49
- catch {
50
- // User likely just exited
51
- console.log();
52
- printInfo('Sandbox session ended.');
53
- }
54
- });
55
- }
1
+ import{execSync as o}from"node:child_process";import chalk from"chalk";import{printError as e,printInfo as n,printSuccess as i}from"../lib/ui.js";import{logCommand as r}from"../lib/logger.js";export function registerShellCommand(t){t.command("shell").description("Open interactive shell in Docker sandbox").action(async()=>{r("shell");try{o("docker info",{stdio:"ignore"})}catch{return e("Docker is not available."),console.log(),console.log(chalk.gray(" Docker is required for the ICOA sandbox environment.")),console.log(chalk.gray(" Install Docker from: https://docs.docker.com/get-docker/")),void console.log()}const t="icoa/sandbox:2026",c=`icoa-sandbox-${Date.now()}`;try{o(`docker image inspect ${t}`,{stdio:"ignore"})}catch{n(`Sandbox image not found. Pulling ${t}...`);try{o(`docker pull ${t}`,{stdio:"inherit"})}catch{return e(`Failed to pull ${t}.`),n("The sandbox image may not be published yet."),void n("You can build it locally: cd docker && docker build -t icoa/sandbox:2026 .")}}i("Launching sandbox..."),console.log(chalk.gray(" Type 'exit' to return to ICOA CLI.")),console.log();try{o(`docker run --rm -it --name ${c} --network=host ${t} /bin/bash`,{stdio:"inherit"}),console.log(),i("Sandbox session ended.")}catch{console.log(),n("Sandbox session ended.")}})}
@@ -1,50 +1 @@
1
- /**
2
- * `theme` command — toggle terminal color scheme.
3
- *
4
- * Two variants:
5
- * - `dark` (default): Darcula — #2B2B2B bg + #A9B7C6 fg, easy on the eyes.
6
- * - `high-contrast`: pure black bg + pure white fg for low-vision users,
7
- * projectors, and cheap LCDs where Darcula's grays disappear.
8
- *
9
- * Change applies on next `icoa` launch (the current session keeps its paint
10
- * to avoid flicker while students are mid-question).
11
- */
12
- import chalk from 'chalk';
13
- import { getConfig, saveConfig } from '../lib/config.js';
14
- import { logCommand } from '../lib/logger.js';
15
- const VALID = ['dark', 'high-contrast'];
16
- export function registerThemeCommand(program) {
17
- program
18
- .command('theme [variant]')
19
- .description('Switch color scheme (dark | high-contrast)')
20
- .action((variant) => {
21
- logCommand(`theme ${variant || ''}`);
22
- const config = getConfig();
23
- const current = config.themeVariant === 'high-contrast' ? 'high-contrast' : 'dark';
24
- if (!variant) {
25
- console.log();
26
- console.log(chalk.gray(' Current theme: ') + chalk.white(current));
27
- console.log();
28
- console.log(chalk.gray(' Available themes:'));
29
- console.log(' ' + chalk.white('dark ') + chalk.gray('Darcula — gray on dark gray (default)'));
30
- console.log(' ' + chalk.white('high-contrast ') + chalk.gray('Pure white on pure black — low vision / projectors'));
31
- console.log();
32
- console.log(chalk.gray(' Switch: ') + chalk.cyan('theme <name>') + chalk.gray(' (applies on next ') + chalk.cyan('icoa') + chalk.gray(' launch)'));
33
- console.log(chalk.gray(' No "back" needed — you are still at the ') + chalk.cyan('icoa>') + chalk.gray(' prompt.'));
34
- console.log();
35
- return;
36
- }
37
- if (!VALID.includes(variant)) {
38
- console.log();
39
- console.log(chalk.red(` Unknown theme: ${variant}`));
40
- console.log(chalk.gray(' Valid: ') + chalk.white(VALID.join(', ')));
41
- console.log();
42
- return;
43
- }
44
- saveConfig({ themeVariant: variant });
45
- console.log();
46
- console.log(chalk.green(` ✓ Theme set to: ${variant}`));
47
- console.log(chalk.gray(' Restart ') + chalk.cyan('icoa') + chalk.gray(' to see the new colors.'));
48
- console.log();
49
- });
50
- }
1
+ import chalk from"chalk";import{getConfig as o,saveConfig as e}from"../lib/config.js";import{logCommand as n}from"../lib/logger.js";const a=["dark","high-contrast"];export function registerThemeCommand(l){l.command("theme [variant]").description("Switch color scheme (dark | high-contrast)").action(l=>{n(`theme ${l||""}`);const r="high-contrast"===o().themeVariant?"high-contrast":"dark";return l?a.includes(l)?(e({themeVariant:l}),console.log(),console.log(chalk.green(` ✓ Theme set to: ${l}`)),console.log(chalk.gray(" Restart ")+chalk.cyan("icoa")+chalk.gray(" to see the new colors.")),void console.log()):(console.log(),console.log(chalk.red(` Unknown theme: ${l}`)),console.log(chalk.gray(" Valid: ")+chalk.white(a.join(", "))),void console.log()):(console.log(),console.log(chalk.gray(" Current theme: ")+chalk.white(r)),console.log(),console.log(chalk.gray(" Available themes:")),console.log(" "+chalk.white("dark ")+chalk.gray("Darcula — gray on dark gray (default)")),console.log(" "+chalk.white("high-contrast ")+chalk.gray("Pure white on pure black — low vision / projectors")),console.log(),console.log(chalk.gray(" Switch: ")+chalk.cyan("theme <name>")+chalk.gray(" (applies on next ")+chalk.cyan("icoa")+chalk.gray(" launch)")),console.log(chalk.gray(' No "back" needed — you are still at the ')+chalk.cyan("icoa>")+chalk.gray(" prompt.")),void console.log())})}
package/dist/index.js CHANGED
@@ -1,226 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import chalk from 'chalk';
4
- import { registerCtfCommands } from './commands/ctf.js';
5
- import { registerHintCommands } from './commands/hint.js';
6
- import { registerRefCommand } from './commands/ref.js';
7
- import { registerShellCommand } from './commands/shell.js';
8
- import { registerFilesCommand } from './commands/files.js';
9
- import { registerConnectCommand } from './commands/connect.js';
10
- import { registerNoteCommand } from './commands/note.js';
11
- import { registerLogCommand } from './commands/log.js';
12
- import { registerLangCommand } from './commands/lang.js';
13
- import { registerSetupCommand } from './commands/setup.js';
14
- import { registerEnvCommand } from './commands/env.js';
15
- import { registerAi4ctfCommand } from './commands/ai4ctf.js';
16
- import { registerExamCommand } from './commands/exam.js';
17
- import { registerCtf4aiDemoCommand } from './commands/ctf4ai-demo.js';
18
- import { registerThemeCommand } from './commands/theme.js';
19
- import { getConfig, saveConfig } from './lib/config.js';
20
- import { startRepl } from './repl.js';
21
- import { setTerminalTheme } from './lib/theme.js';
22
- import { checkForUpdates } from './lib/update-check.js';
23
- import { readFileSync } from 'node:fs';
24
- import { fileURLToPath } from 'node:url';
25
- import { dirname, join } from 'node:path';
26
- const __dirname = dirname(fileURLToPath(import.meta.url));
27
- const PKG_VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')).version;
28
- const LINE = chalk.cyan(' ─────────────────────────────────────────────────────');
29
- const BANNER = `
30
- ${LINE}
31
-
32
- ${chalk.bold.white('██╗ ██████╗ ██████╗ █████╗')}
33
- ${chalk.bold.white('██║██╔════╝██╔═══██╗██╔══██╗')}
34
- ${chalk.bold.white('██║██║ ██║ ██║███████║')}
35
- ${chalk.bold.white('██║██║ ██║ ██║██╔══██║')}
36
- ${chalk.bold.white('██║╚██████╗╚██████╔╝██║ ██║')}
37
- ${chalk.bold.white('╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝')}
38
-
39
- ${chalk.yellow('International Cyber Olympiad in AI 2026')}
40
- ${chalk.bold.magenta("The World's First AI-Native CLI Operating System")}
41
- ${chalk.bold.magenta("for Cybersecurity & AI Security Competition")}
42
- ${chalk.bold.magenta("and Olympiad for K-12")}
43
-
44
- ${chalk.green.bold('AI4CTF')}${chalk.gray('[Day 1]')} ${chalk.white('AI as your teammate')}
45
- ${chalk.red.bold('CTF4AI')}${chalk.gray('[Day 2]')} ${chalk.white('Challenge & evaluate AI systems')}
46
- ${chalk.bold.yellow('AI is your ally. AI is your target.')}
47
-
48
- ${chalk.white('Sydney, Australia')} ${chalk.gray('Jun 27 - Jul 2, 2026')}
49
- ${chalk.cyan.underline('https://icoa2026.au')}
50
-
51
- ${chalk.gray(`CLI-Native Competition Terminal v${PKG_VERSION}`)}
52
-
53
- ${LINE}
54
- `;
55
- // Global error handlers
56
- process.on('uncaughtException', (err) => {
57
- if (err.message === '__REPL_NO_EXIT__')
58
- return;
59
- console.error(chalk.red('Error:'), err.message);
60
- process.exit(1);
61
- });
62
- process.on('unhandledRejection', (reason) => {
63
- const msg = reason instanceof Error ? reason.message : String(reason);
64
- if (msg === '__REPL_NO_EXIT__')
65
- return;
66
- console.error(chalk.red('Error:'), msg);
67
- process.exit(1);
68
- });
69
- // Pause for ms, skippable by any key press (so users can read the banner)
70
- async function pauseWithSkip(ms) {
71
- const stdin = process.stdin;
72
- if (!stdin.isTTY || typeof stdin.setRawMode !== 'function') {
73
- await new Promise((r) => setTimeout(r, ms));
74
- return;
75
- }
76
- return new Promise((resolve) => {
77
- let done = false;
78
- const finish = () => {
79
- if (done)
80
- return;
81
- done = true;
82
- stdin.removeListener('data', onKey);
83
- try {
84
- stdin.setRawMode(false);
85
- }
86
- catch { }
87
- stdin.pause();
88
- resolve();
89
- };
90
- const onKey = () => finish();
91
- stdin.setRawMode(true);
92
- stdin.resume();
93
- stdin.once('data', onKey);
94
- console.log(chalk.gray(' (press any key to continue...)'));
95
- setTimeout(finish, ms);
96
- });
97
- }
98
- const program = new Command();
99
- program
100
- .name('icoa')
101
- // T4-X2: version now reads live from package.json (PKG_VERSION).
102
- // Was hardcoded to '1.2.0' which made `icoa --version` misreport on every
103
- // release — bug since v1.2.0 era, finally fixed.
104
- .version(PKG_VERSION)
105
- .description('ICOA CLI — CLI-Native CTF Competition Terminal')
106
- .option('--resume', 'Resume previous session')
107
- .action(async (opts) => {
108
- // Terminal theme — Darcula by default, 'high-contrast' for low-vision users.
109
- // Variant stored in config.themeVariant; toggled via `theme` REPL command.
110
- const cfg = getConfig();
111
- setTerminalTheme(cfg.themeVariant === 'high-contrast' ? 'high-contrast' : 'dark');
112
- checkForUpdates();
113
- // T2-7: UTF-8 locale sanity check. Non-UTF-8 terminals mangle the box-
114
- // drawing banner, the ✓/✗/⚠ glyphs used throughout the CLI, and any
115
- // Cyrillic/CJK/Arabic/Devanagari/etc. translation text. Warn once at
116
- // startup (not blocking) so students see a concrete fix hint instead of
117
- // wondering why "哪个命令..." appears as "????..." on their machine.
118
- const envLang = process.env.LANG || process.env.LC_ALL || process.env.LC_CTYPE || '';
119
- if (!/UTF-?8/i.test(envLang)) {
120
- if (process.platform === 'win32') {
121
- // Windows cmd.exe default code page (437 US, 936 CN, etc.) mangles
122
- // non-Latin script. Fix is `chcp 65001`, not Unix-style `export LANG`.
123
- // Skip the warning if the code page is already 65001 (PowerShell +
124
- // Windows Terminal often set it automatically).
125
- let codepage = '';
126
- try {
127
- const { execFileSync } = await import('node:child_process');
128
- codepage = execFileSync('chcp.com', [], {
129
- encoding: 'utf-8',
130
- timeout: 1500,
131
- stdio: ['ignore', 'pipe', 'ignore'],
132
- }).trim();
133
- }
134
- catch { /* chcp.com unavailable or timed out */ }
135
- if (!codepage.includes('65001')) {
136
- console.log(chalk.yellow('⚠ Windows terminal is not using UTF-8 (current: ' + (codepage || 'unknown') + ').'));
137
- console.log(chalk.gray(' Non-English text (Ukrainian, Chinese, Japanese, etc.) may show as "?" or garbled glyphs.'));
138
- console.log(chalk.gray(' Fix (run before ') + chalk.cyan('icoa') + chalk.gray('): ') + chalk.cyan('chcp 65001'));
139
- console.log(chalk.gray(' Or stay in English inside the CLI: ') + chalk.cyan('lang en'));
140
- console.log();
141
- }
142
- }
143
- else {
144
- console.log(chalk.yellow('⚠ Your terminal locale is not UTF-8 (LANG=' + (envLang || '(unset)') + ').'));
145
- console.log(chalk.gray(' Non-English text and box characters may display as "?" or garbled glyphs.'));
146
- console.log(chalk.gray(' Fix: ') + chalk.cyan('export LANG=en_US.UTF-8') + chalk.gray(' (or your locale, e.g. ') + chalk.cyan('zh_CN.UTF-8') + chalk.gray(', ') + chalk.cyan('uk_UA.UTF-8') + chalk.gray(')'));
147
- console.log();
148
- }
149
- }
150
- console.log(BANNER);
151
- // If running interactively (no extra args or --resume), start REPL
152
- if (process.argv.length <= 2 || opts.resume) {
153
- // Brief pause so users can read the banner + logo before mode selector appears
154
- // Skip on --resume (user is coming back, doesn't need the intro)
155
- if (!opts.resume) {
156
- await pauseWithSkip(3000);
157
- }
158
- startRepl(program, !!opts.resume);
159
- return;
160
- }
161
- });
162
- registerCtfCommands(program);
163
- registerHintCommands(program);
164
- registerRefCommand(program);
165
- registerShellCommand(program);
166
- registerFilesCommand(program);
167
- registerConnectCommand(program);
168
- registerNoteCommand(program);
169
- registerLogCommand(program);
170
- registerLangCommand(program);
171
- registerSetupCommand(program);
172
- registerEnvCommand(program);
173
- registerAi4ctfCommand(program);
174
- registerExamCommand(program);
175
- registerCtf4aiDemoCommand(program);
176
- registerThemeCommand(program);
177
- // Hidden command: switch AI model
178
- program
179
- .command('model', { hidden: true })
180
- .argument('[name]', 'model name to switch to')
181
- .action((name) => {
182
- const config = getConfig();
183
- const current = config.geminiModel || 'gemini-2.5-flash';
184
- if (!name) {
185
- console.log();
186
- console.log(chalk.gray(' Current model: ') + chalk.white(current));
187
- console.log();
188
- console.log(chalk.gray(' Available models:'));
189
- console.log(chalk.bold.white(' Gemini 3.x (Latest)'));
190
- console.log(chalk.white(' model gemini-3.1-pro-preview ') + chalk.gray('Most powerful, paid'));
191
- console.log(chalk.white(' model gemini-3-flash-preview ') + chalk.gray('Fast, free tier'));
192
- console.log(chalk.bold.white(' Gemini 2.5 (Stable)'));
193
- console.log(chalk.white(' model gemini-2.5-flash ') + chalk.gray('Fast, free tier (default)'));
194
- console.log(chalk.white(' model gemini-2.5-pro ') + chalk.gray('Strong reasoning, paid'));
195
- console.log(chalk.bold.white(' Open Source'));
196
- console.log(chalk.white(' model gemma-4-31b-it ') + chalk.gray('Free, open-source'));
197
- console.log(chalk.white(' model <any-model-id> ') + chalk.gray('Custom model'));
198
- console.log();
199
- console.log(chalk.gray(' Translation uses gemini-3.1-pro-preview for best quality.'));
200
- console.log();
201
- }
202
- else {
203
- saveConfig({ geminiModel: name });
204
- console.log();
205
- console.log(chalk.green(' Model switched: ') + chalk.gray(current) + chalk.white(' -> ') + chalk.bold.white(name));
206
- console.log();
207
- }
208
- });
209
- // Proctor-only escape hatch: `ICOA_RESET_STATE=1 icoa` wipes any abandoned
210
- // exam state *before* any subcommand or REPL runs. Fires for ALL invocations
211
- // (including `icoa exam <token>`) so proctors have a one-liner recovery that
212
- // works regardless of which subcommand the student would run next.
213
- // The token itself is untouched server-side.
214
- if (process.env.ICOA_RESET_STATE === '1') {
215
- try {
216
- const { clearExamState } = await import('./lib/exam-state.js');
217
- clearExamState();
218
- console.log(chalk.yellow('⚠ ICOA_RESET_STATE=1 — local exam state wiped.'));
219
- console.log(chalk.gray(' (Token NOT revoked server-side. Re-enter a fresh token with `exam <token>`.)'));
220
- console.log();
221
- }
222
- catch (e) {
223
- console.log(chalk.red('⚠ ICOA_RESET_STATE: could not clear state — ') + chalk.gray(String(e)));
224
- }
225
- }
226
- program.parse();
2
+ import{Command as o}from"commander";import chalk from"chalk";import{registerCtfCommands as e}from"./commands/ctf.js";import{registerHintCommands as n}from"./commands/hint.js";import{registerRefCommand as r}from"./commands/ref.js";import{registerShellCommand as s}from"./commands/shell.js";import{registerFilesCommand as t}from"./commands/files.js";import{registerConnectCommand as i}from"./commands/connect.js";import{registerNoteCommand as a}from"./commands/note.js";import{registerLogCommand as l}from"./commands/log.js";import{registerLangCommand as m}from"./commands/lang.js";import{registerSetupCommand as c}from"./commands/setup.js";import{registerEnvCommand as g}from"./commands/env.js";import{registerAi4ctfCommand as d}from"./commands/ai4ctf.js";import{registerExamCommand as p}from"./commands/exam.js";import{registerCtf4aiDemoCommand as y}from"./commands/ctf4ai-demo.js";import{registerThemeCommand as h}from"./commands/theme.js";import{getConfig as f,saveConfig as u}from"./lib/config.js";import{startRepl as w}from"./repl.js";import{setTerminalTheme as T}from"./lib/theme.js";import{checkForUpdates as b}from"./lib/update-check.js";import{readFileSync as C}from"node:fs";import{fileURLToPath as $}from"node:url";import{dirname as _,join as A}from"node:path";const j=_($(import.meta.url)),v=JSON.parse(C(A(j,"..","package.json"),"utf-8")).version,E=chalk.cyan(" ─────────────────────────────────────────────────────"),I=`\n${E}\n\n ${chalk.bold.white("██╗ ██████╗ ██████╗ █████╗")}\n ${chalk.bold.white("██║██╔════╝██╔═══██╗██╔══██╗")}\n ${chalk.bold.white("██║██║ ██║ ██║███████║")}\n ${chalk.bold.white("██║██║ ██║ ██║██╔══██║")}\n ${chalk.bold.white("██║╚██████╗╚██████╔╝██║ ██║")}\n ${chalk.bold.white("╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝")}\n\n ${chalk.yellow("International Cyber Olympiad in AI 2026")}\n ${chalk.bold.magenta("The World's First AI-Native CLI Operating System")}\n ${chalk.bold.magenta("for Cybersecurity & AI Security Competition")}\n ${chalk.bold.magenta("and Olympiad for K-12")}\n\n ${chalk.green.bold("AI4CTF")}${chalk.gray("[Day 1]")} ${chalk.white("AI as your teammate")}\n ${chalk.red.bold("CTF4AI")}${chalk.gray("[Day 2]")} ${chalk.white("Challenge & evaluate AI systems")}\n ${chalk.bold.yellow("AI is your ally. AI is your target.")}\n\n ${chalk.white("Sydney, Australia")} ${chalk.gray("Jun 27 - Jul 2, 2026")}\n ${chalk.cyan.underline("https://icoa2026.au")}\n\n ${chalk.gray(`CLI-Native Competition Terminal v${v}`)}\n\n${E}\n`;process.on("uncaughtException",o=>{"__REPL_NO_EXIT__"!==o.message&&(console.error(chalk.red("Error:"),o.message),process.exit(1))}),process.on("unhandledRejection",o=>{const e=o instanceof Error?o.message:String(o);"__REPL_NO_EXIT__"!==e&&(console.error(chalk.red("Error:"),e),process.exit(1))});const S=new o;if(S.name("icoa").version(v).description("ICOA CLI — CLI-Native CTF Competition Terminal").option("--resume","Resume previous session").action(async o=>{const e=f();T("high-contrast"===e.themeVariant?"high-contrast":"dark"),b();const n=process.env.LANG||process.env.LC_ALL||process.env.LC_CTYPE||"";if(!/UTF-?8/i.test(n))if("win32"===process.platform){let o="";try{const{execFileSync:e}=await import("node:child_process");o=e("chcp.com",[],{encoding:"utf-8",timeout:1500,stdio:["ignore","pipe","ignore"]}).trim()}catch{}o.includes("65001")||(console.log(chalk.yellow("⚠ Windows terminal is not using UTF-8 (current: "+(o||"unknown")+").")),console.log(chalk.gray(' Non-English text (Ukrainian, Chinese, Japanese, etc.) may show as "?" or garbled glyphs.')),console.log(chalk.gray(" Fix (run before ")+chalk.cyan("icoa")+chalk.gray("): ")+chalk.cyan("chcp 65001")),console.log(chalk.gray(" Or stay in English inside the CLI: ")+chalk.cyan("lang en")),console.log())}else console.log(chalk.yellow("⚠ Your terminal locale is not UTF-8 (LANG="+(n||"(unset)")+").")),console.log(chalk.gray(' Non-English text and box characters may display as "?" or garbled glyphs.')),console.log(chalk.gray(" Fix: ")+chalk.cyan("export LANG=en_US.UTF-8")+chalk.gray(" (or your locale, e.g. ")+chalk.cyan("zh_CN.UTF-8")+chalk.gray(", ")+chalk.cyan("uk_UA.UTF-8")+chalk.gray(")")),console.log();if(console.log(I),process.argv.length<=2||o.resume)return o.resume||await async function(){const o=process.stdin;if(o.isTTY&&"function"==typeof o.setRawMode)return new Promise(e=>{let n=!1;const r=()=>{if(!n){n=!0,o.removeListener("data",s);try{o.setRawMode(!1)}catch{}o.pause(),e()}},s=()=>r();o.setRawMode(!0),o.resume(),o.once("data",s),console.log(chalk.gray(" (press any key to continue...)")),setTimeout(r,3e3)});await new Promise(o=>setTimeout(o,3e3))}(),void w(S,!!o.resume)}),e(S),n(S),r(S),s(S),t(S),i(S),a(S),l(S),m(S),c(S),g(S),d(S),p(S),y(S),h(S),S.command("model",{hidden:!0}).argument("[name]","model name to switch to").action(o=>{const e=f().geminiModel||"gemini-2.5-flash";o?(u({geminiModel:o}),console.log(),console.log(chalk.green(" Model switched: ")+chalk.gray(e)+chalk.white(" -> ")+chalk.bold.white(o)),console.log()):(console.log(),console.log(chalk.gray(" Current model: ")+chalk.white(e)),console.log(),console.log(chalk.gray(" Available models:")),console.log(chalk.bold.white(" Gemini 3.x (Latest)")),console.log(chalk.white(" model gemini-3.1-pro-preview ")+chalk.gray("Most powerful, paid")),console.log(chalk.white(" model gemini-3-flash-preview ")+chalk.gray("Fast, free tier")),console.log(chalk.bold.white(" Gemini 2.5 (Stable)")),console.log(chalk.white(" model gemini-2.5-flash ")+chalk.gray("Fast, free tier (default)")),console.log(chalk.white(" model gemini-2.5-pro ")+chalk.gray("Strong reasoning, paid")),console.log(chalk.bold.white(" Open Source")),console.log(chalk.white(" model gemma-4-31b-it ")+chalk.gray("Free, open-source")),console.log(chalk.white(" model <any-model-id> ")+chalk.gray("Custom model")),console.log(),console.log(chalk.gray(" Translation uses gemini-3.1-pro-preview for best quality.")),console.log())}),"1"===process.env.ICOA_RESET_STATE)try{const{clearExamState:o}=await import("./lib/exam-state.js");o(),console.log(chalk.yellow("⚠ ICOA_RESET_STATE=1 — local exam state wiped.")),console.log(chalk.gray(" (Token NOT revoked server-side. Re-enter a fresh token with `exam <token>`.)")),console.log()}catch(o){console.log(chalk.red("⚠ ICOA_RESET_STATE: could not clear state — ")+chalk.gray(String(o)))}S.parse();