icoa-cli 2.19.98 → 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
@@ -454,6 +454,10 @@ export async function startRepl(program, resumeMode) {
454
454
  console.log(chalk.white(' status') + chalk.gray(' Your score & hint budget'));
455
455
  console.log(chalk.white(' scoreboard') + chalk.gray(' Live rankings'));
456
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)'));
457
461
  console.log(chalk.gray(' ─────────────────────────────────────────────'));
458
462
  console.log(chalk.gray(' Tip: ') + chalk.cyan('help') + chalk.gray(' · ') + chalk.cyan('Ctrl+C') + chalk.gray(' pauses · ') + chalk.cyan('quit') + chalk.gray(' closes'));
459
463
  console.log();
@@ -470,7 +474,11 @@ export async function startRepl(program, resumeMode) {
470
474
  console.log(chalk.white(' Step 2 ') + chalk.bold.cyan('challenges') + chalk.gray(' Browse & solve challenges'));
471
475
  console.log(chalk.white(' Step 3 ') + chalk.bold.cyan('hint') + chalk.gray(' Ask AI when stuck'));
472
476
  console.log();
473
- console.log(chalk.gray(' Also: ') + chalk.white('env') + chalk.gray(' check tools ') + chalk.white('help') + chalk.gray(' all commands'));
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'));
474
482
  console.log(chalk.gray(' ─────────────────────────────────────────────'));
475
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'));
476
484
  console.log();
@@ -486,7 +494,8 @@ export async function startRepl(program, resumeMode) {
486
494
  console.log(chalk.gray(' While waiting, explore:'));
487
495
  console.log(chalk.white(' ref linux') + chalk.gray(' Quick reference for Linux'));
488
496
  console.log(chalk.white(' ref web') + chalk.gray(' Quick reference for Web'));
489
- console.log(chalk.white(' env') + chalk.gray(' Check your tools'));
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)'));
490
499
  console.log(chalk.white(' help') + chalk.gray(' All available commands'));
491
500
  console.log(chalk.gray(' ─────────────────────────────────────────────'));
492
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'));
@@ -575,13 +584,30 @@ export async function startRepl(program, resumeMode) {
575
584
  rl.prompt();
576
585
  return;
577
586
  }
578
- // Explicit quit — `quit` or `q` always closes the CLI.
579
- if (input === 'quit' || input === 'q') {
580
- 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.'));
581
599
  console.log();
582
- console.log(chalk.yellow(' An exam is in progress progress is auto-saved.'));
583
- console.log(chalk.gray(' Closing anyway. Resume with: ') + chalk.white('icoa --resume'));
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)'));
584
602
  console.log();
603
+ console.log(chalk.white(' To really close ICOA CLI, type: ') + chalk.bold.cyan('quit confirm'));
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).'));
585
611
  }
586
612
  stopLogSync();
587
613
  recordExit();
@@ -1077,16 +1103,21 @@ export async function startRepl(program, resumeMode) {
1077
1103
  rl.on('SIGINT', () => {
1078
1104
  console.log();
1079
1105
  if (isChatActive() || isCtf4aiActive()) {
1080
- 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.'));
1081
1108
  }
1082
1109
  else if (getExamState()) {
1083
- 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();
1084
1114
  console.log(chalk.white(' Resume: ') + chalk.cyan('exam q 1') +
1085
1115
  chalk.gray(' · Back to menu: ') + chalk.cyan('back') +
1086
- chalk.gray(' · Close CLI: ') + chalk.cyan('quit'));
1116
+ chalk.gray(' · Close CLI: ') + chalk.cyan(isReal ? 'quit confirm' : 'quit'));
1087
1117
  }
1088
1118
  else {
1089
- 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.)'));
1090
1121
  }
1091
1122
  console.log();
1092
1123
  rl.prompt();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.98",
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": {