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.
- package/dist/commands/env.js +2 -1
- package/dist/commands/lang.js +2 -2
- package/dist/commands/log.js +2 -0
- package/dist/commands/note.js +9 -1
- package/dist/commands/ref.js +7 -2
- package/dist/commands/setup.js +88 -69
- package/dist/commands/theme.js +2 -2
- package/dist/repl.js +31 -9
- package/package.json +1 -1
package/dist/commands/env.js
CHANGED
|
@@ -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() {
|
package/dist/commands/lang.js
CHANGED
|
@@ -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('
|
|
43
|
-
console.log(chalk.gray('
|
|
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
|
}
|
package/dist/commands/log.js
CHANGED
|
@@ -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();
|
package/dist/commands/note.js
CHANGED
|
@@ -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:
|
|
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
|
package/dist/commands/ref.js
CHANGED
|
@@ -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(
|
|
48
|
-
console.log(chalk.gray(
|
|
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
|
}
|
package/dist/commands/setup.js
CHANGED
|
@@ -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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 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;
|
|
57
51
|
}
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
117
|
+
return;
|
|
98
118
|
}
|
|
99
|
-
|
|
100
|
-
break;
|
|
119
|
+
throw err;
|
|
101
120
|
}
|
|
102
121
|
});
|
|
103
122
|
}
|
package/dist/commands/theme.js
CHANGED
|
@@ -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('
|
|
33
|
-
console.log(chalk.gray('
|
|
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`
|
|
588
|
-
|
|
589
|
-
|
|
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.
|
|
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('
|
|
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
|
-
|
|
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('
|
|
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();
|