icoa-cli 2.19.99 → 2.19.100

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.
@@ -506,8 +506,9 @@ function showStatus() {
506
506
  console.log(chalk.gray(' ─────────────────────────────────────────────'));
507
507
  console.log(` ${chalk.green(`✓ ${installed}/${total}`)} ${missing > 0 ? chalk.red(`✗ ${missing} missing`) : chalk.green('All 110 ready!')}`);
508
508
  if (missing > 0) {
509
- console.log(chalk.gray(' Install everything: ') + chalk.white('env setup'));
509
+ console.log(chalk.gray(' Install everything: ') + chalk.white('env setup') + chalk.gray(' (~5 min, one-time)'));
510
510
  }
511
+ console.log(chalk.gray(' You are back at the ') + chalk.cyan('icoa>') + chalk.gray(' prompt. ') + chalk.cyan('help') + chalk.gray(' for all commands.'));
511
512
  console.log();
512
513
  }
513
514
  async function installAll() {
@@ -39,8 +39,8 @@ export function registerLangCommand(program) {
39
39
  console.log(` ${chalk.white(lang)} ${LANG_NAMES[lang]}${current}`);
40
40
  }
41
41
  console.log();
42
- console.log(chalk.gray(' Usage: lang <code>'));
43
- console.log(chalk.gray(' Example: lang es'));
42
+ console.log(chalk.gray(' Switch now: ') + chalk.cyan('lang <code>') + chalk.gray(' (e.g. ') + chalk.cyan('lang es') + chalk.gray(')'));
43
+ console.log(chalk.gray(' No "back" needed — you are still at the ') + chalk.cyan('icoa>') + chalk.gray(' prompt.'));
44
44
  console.log();
45
45
  return;
46
46
  }
@@ -53,6 +53,8 @@ function showLog() {
53
53
  printTable(['Time', 'Type', 'Content'], rows);
54
54
  console.log(chalk.gray(` ${entries.length} entries total`));
55
55
  console.log();
56
+ console.log(chalk.gray(' You are at the ') + chalk.cyan('icoa>') + chalk.gray(' prompt. Also: ') + chalk.cyan('log stats') + chalk.gray(' · ') + chalk.cyan('log export') + chalk.gray(' · ') + chalk.cyan('help') + chalk.gray(' all commands.'));
57
+ console.log();
56
58
  }
57
59
  async function exportLog() {
58
60
  const config = getConfig();
@@ -14,12 +14,20 @@ export function registerNoteCommand(program) {
14
14
  if (!words || words.length === 0) {
15
15
  // Display existing notes
16
16
  if (!existsSync(NOTES_FILE)) {
17
- printInfo('No notes yet. Add one with: icoa note "your note here"');
17
+ printInfo('No notes yet. Add one with: note "your note here"');
18
+ console.log();
19
+ console.log(chalk.gray(' You are at the ') + chalk.cyan('icoa>') + chalk.gray(' prompt. Type another command or ') + chalk.cyan('help') + chalk.gray(' for the list.'));
20
+ console.log();
18
21
  return;
19
22
  }
20
23
  const content = readFileSync(NOTES_FILE, 'utf-8');
21
24
  printHeader('Notes');
22
25
  console.log(content);
26
+ console.log();
27
+ console.log(chalk.gray(' ─────────────────────────────────────────────'));
28
+ console.log(chalk.gray(' End of notes. You are back at the ') + chalk.cyan('icoa>') + chalk.gray(' prompt.'));
29
+ console.log(chalk.gray(' Add one: ') + chalk.cyan('note "your text"') + chalk.gray(' · ') + chalk.cyan('help') + chalk.gray(' for all commands'));
30
+ console.log();
23
31
  return;
24
32
  }
25
33
  // Add new note
@@ -44,8 +44,8 @@ export function registerRefCommand(program) {
44
44
  const rows = files.map((f) => [chalk.white(f)]);
45
45
  printTable(['Topic'], rows);
46
46
  console.log();
47
- console.log(chalk.gray(` Usage: ref <topic>`));
48
- console.log(chalk.gray(` Example: ref python`));
47
+ console.log(chalk.gray(' Open one: ') + chalk.cyan('ref <topic>') + chalk.gray(' (e.g. ') + chalk.cyan('ref python') + chalk.gray(')'));
48
+ console.log(chalk.gray(' No "back" needed — ref just prints and returns to the ') + chalk.cyan('icoa>') + chalk.gray(' prompt.'));
49
49
  console.log();
50
50
  return;
51
51
  }
@@ -59,5 +59,10 @@ export function registerRefCommand(program) {
59
59
  const content = readFileSync(filePath, 'utf-8');
60
60
  printHeader(`Reference: ${topic}`);
61
61
  console.log(content);
62
+ console.log();
63
+ console.log(chalk.gray(' ─────────────────────────────────────────────'));
64
+ console.log(chalk.gray(' End of ') + chalk.cyan(`ref ${topic}`) + chalk.gray('. You are back at the ') + chalk.cyan('icoa>') + chalk.gray(' prompt.'));
65
+ console.log(chalk.gray(' Next: ') + chalk.cyan('ref') + chalk.gray(' list topics · ') + chalk.cyan('ref <topic>') + chalk.gray(' open another · ') + chalk.cyan('help') + chalk.gray(' all commands'));
66
+ console.log();
62
67
  });
63
68
  }
@@ -13,6 +13,8 @@ export function registerSetupCommand(program) {
13
13
  const config = getConfig();
14
14
  printHeader('ICOA CLI Setup');
15
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();
16
18
  // Show current configuration
17
19
  printKeyValue('CTFd URL', config.ctfdUrl || chalk.gray('Not configured'));
18
20
  printKeyValue('CTFd Token', config.token ? chalk.green('Configured') : chalk.gray('Not configured'));
@@ -21,83 +23,100 @@ export function registerSetupCommand(program) {
21
23
  printKeyValue('Mode', config.mode || chalk.gray('Not set'));
22
24
  printKeyValue('Session ID', config.sessionId.substring(0, 8) + '...');
23
25
  console.log();
24
- const action = await select({
25
- message: 'What would you like to configure?',
26
- choices: [
27
- { name: 'Switch Mode', value: 'mode' },
28
- { name: 'Gemini API Key', value: 'gemini' },
29
- { name: 'CTFd Connection', value: 'ctfd' },
30
- { name: 'Reset Hint Budget', value: 'budget' },
31
- { name: 'View All Settings', value: 'view' },
32
- { name: 'Exit', value: 'exit' },
33
- ],
34
- });
35
- switch (action) {
36
- case 'mode': {
37
- const newMode = await select({
38
- message: 'Select mode:',
39
- choices: [
40
- { name: 'National Selection — demo, exam, lightweight', value: 'selection' },
41
- { name: 'International OlympiadFull CTF with AI assistance', value: 'olympiad' },
42
- { name: 'National/Regional PartnerOrganizer management', value: 'organizer' },
43
- ],
44
- });
45
- saveConfig({ mode: newMode });
46
- printSuccess(`Mode switched to: ${newMode}. Restart ICOA CLI to apply.`);
47
- break;
48
- }
49
- case 'gemini': {
50
- console.log();
51
- printInfo('Get your API key from: https://aistudio.google.com/apikey');
52
- console.log();
53
- const key = await input({ message: 'Enter Gemini API Key:' });
54
- if (key.trim()) {
55
- setApiKey(key.trim());
56
- printSuccess('Gemini API key saved.');
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 Selectiondemo, exam, lightweight', value: 'selection' },
44
+ { name: 'International OlympiadFull 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;
57
51
  }
58
- else {
59
- printError('No key provided.');
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;
60
65
  }
61
- break;
62
- }
63
- case 'ctfd': {
64
- printInfo('Use "join <url>" to connect to a CTFd instance.');
65
- break;
66
- }
67
- case 'budget': {
68
- const { confirm } = await import('@inquirer/prompts');
69
- const proceed = await confirm({
70
- message: 'Reset hint budget to defaults (A:50, B:10, C:2)? This cannot be undone.',
71
- default: false,
72
- });
73
- if (proceed) {
74
- const { saveBudget } = await import('../lib/config.js');
75
- const { DEFAULT_BUDGET } = await import('../types/index.js');
76
- saveBudget({ ...DEFAULT_BUDGET });
77
- printSuccess('Hint budget reset to defaults.');
66
+ case 'ctfd': {
67
+ printInfo('Use "join <url>" to connect to a CTFd instance.');
68
+ break;
78
69
  }
79
- break;
80
- }
81
- case 'view': {
82
- console.log();
83
- printHeader('Full Configuration');
84
- const full = getConfig();
85
- for (const [key, value] of Object.entries(full)) {
86
- if (key === 'token' && value) {
87
- printKeyValue(key, value.toString().substring(0, 8) + '...');
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.');
88
81
  }
89
- else if (key === 'geminiApiKey' && value) {
90
- printKeyValue(key, value.toString().substring(0, 8) + '...');
91
- }
92
- else {
93
- printKeyValue(key, String(value ?? 'null'));
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
+ }
94
98
  }
99
+ console.log();
100
+ break;
95
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.'));
96
116
  console.log();
97
- break;
117
+ return;
98
118
  }
99
- case 'exit':
100
- break;
119
+ throw err;
101
120
  }
102
121
  });
103
122
  }
@@ -29,8 +29,8 @@ export function registerThemeCommand(program) {
29
29
  console.log(' ' + chalk.white('dark ') + chalk.gray('Darcula — gray on dark gray (default)'));
30
30
  console.log(' ' + chalk.white('high-contrast ') + chalk.gray('Pure white on pure black — low vision / projectors'));
31
31
  console.log();
32
- console.log(chalk.gray(' Usage: ') + chalk.cyan('theme <name>'));
33
- console.log(chalk.gray(' Applies on next ') + chalk.cyan('icoa') + chalk.gray(' launch.'));
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
34
  console.log();
35
35
  return;
36
36
  }
package/dist/repl.js CHANGED
@@ -584,13 +584,30 @@ export async function startRepl(program, resumeMode) {
584
584
  rl.prompt();
585
585
  return;
586
586
  }
587
- // Explicit quit — `quit` or `q` always closes the CLI.
588
- if (input === 'quit' || input === 'q') {
589
- if (getExamState()) {
587
+ // Explicit quit — `quit` / `q` close the CLI. During a live **real** exam
588
+ // (token-gated, timed, graded) we require `quit confirm` as a second step
589
+ // to prevent accidental loss of a 90-minute session to a single keystroke.
590
+ // Demo has no time pressure and no scoring, so demo quit stays one-step.
591
+ if (input === 'quit' || input === 'q' || input === 'quit confirm') {
592
+ const state = getExamState();
593
+ const isRealExam = state && state.session.examId !== 'demo-free';
594
+ if (isRealExam && input !== 'quit confirm') {
595
+ console.log();
596
+ console.log(chalk.yellow(' ⚠ A real exam is in progress.'));
597
+ console.log(chalk.gray(' Your answers are auto-saved on the server, but the exam timer keeps ticking'));
598
+ console.log(chalk.gray(' on the server side even if you close the CLI.'));
599
+ console.log();
600
+ console.log(chalk.white(' To leave the CLI but keep the exam alive, type: ') + chalk.bold.cyan('back'));
601
+ console.log(chalk.gray(' (recommended — you can resume with ') + chalk.cyan('exam q 1') + chalk.gray(' after relaunching icoa)'));
590
602
  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'));
603
+ console.log(chalk.white(' To really close ICOA CLI, type: ') + chalk.bold.cyan('quit confirm'));
593
604
  console.log();
605
+ rl.prompt();
606
+ return;
607
+ }
608
+ if (state && state.session.examId === 'demo-free') {
609
+ console.log();
610
+ console.log(chalk.gray(' Demo paused. Resume with: ') + chalk.white('demo') + chalk.gray(' (fresh) or ') + chalk.white('exam q 1') + chalk.gray(' (continue).'));
594
611
  }
595
612
  stopLogSync();
596
613
  recordExit();
@@ -1086,16 +1103,21 @@ export async function startRepl(program, resumeMode) {
1086
1103
  rl.on('SIGINT', () => {
1087
1104
  console.log();
1088
1105
  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.'));
1106
+ console.log(chalk.yellow(' Ctrl+C did not close ICOA CLI you are still in the AI chat.'));
1107
+ console.log(chalk.white(' Type ') + chalk.bold.cyan('exit') + chalk.white(' to leave the chat and return to the menu.'));
1090
1108
  }
1091
1109
  else if (getExamState()) {
1092
- console.log(chalk.yellow(' Exam paused. Your answers are auto-saved.'));
1110
+ const isReal = getExamState().session.examId !== 'demo-free';
1111
+ console.log(chalk.yellow(' Ctrl+C did NOT close ICOA CLI.'));
1112
+ console.log(chalk.gray(` Your ${isReal ? 'exam' : 'demo'} is paused and every answer is auto-saved.`));
1113
+ console.log();
1093
1114
  console.log(chalk.white(' Resume: ') + chalk.cyan('exam q 1') +
1094
1115
  chalk.gray(' · Back to menu: ') + chalk.cyan('back') +
1095
- chalk.gray(' · Close CLI: ') + chalk.cyan('quit'));
1116
+ chalk.gray(' · Close CLI: ') + chalk.cyan(isReal ? 'quit confirm' : 'quit'));
1096
1117
  }
1097
1118
  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.'));
1119
+ console.log(chalk.yellow(' Ctrl+C did not close ICOA CLI you are still at the ') + chalk.cyan('icoa>') + chalk.yellow(' prompt.'));
1120
+ 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.)'));
1099
1121
  }
1100
1122
  console.log();
1101
1123
  rl.prompt();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.99",
3
+ "version": "2.19.100",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {