icoa-cli 2.7.2 → 2.9.0
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/exam.js +172 -18
- package/dist/repl.js +56 -1
- package/package.json +1 -1
package/dist/commands/exam.js
CHANGED
|
@@ -62,10 +62,20 @@ function printQuestionProgress(current, total, answered) {
|
|
|
62
62
|
console.log();
|
|
63
63
|
console.log(` ${bar} ${chalk.white.bold(`${current}`)}${chalk.gray(`/${total}`)} ${chalk.gray(`(${answered} answered)`)} ${chalk.gray(`${pct}%`)}`);
|
|
64
64
|
}
|
|
65
|
+
function getHelpState(state) {
|
|
66
|
+
return {
|
|
67
|
+
used: state._helpUsed || 0,
|
|
68
|
+
max: state._helpMax || 5,
|
|
69
|
+
perQ: state._helpPerQ || {},
|
|
70
|
+
eliminated: state._eliminated || {},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
65
73
|
function printQuestion(q, answer) {
|
|
66
74
|
const state = getExamState();
|
|
67
75
|
const total = Number(state?.session.questionCount || 30);
|
|
68
76
|
const answered = Object.keys(state?.answers || {}).length;
|
|
77
|
+
const help = getHelpState(state);
|
|
78
|
+
const eliminated = help.eliminated[q.number] || [];
|
|
69
79
|
// Progress bar
|
|
70
80
|
printQuestionProgress(q.number, total, answered);
|
|
71
81
|
// Easter egg
|
|
@@ -78,20 +88,27 @@ function printQuestion(q, answer) {
|
|
|
78
88
|
console.log(` ${q.text}`);
|
|
79
89
|
console.log();
|
|
80
90
|
for (const key of ['A', 'B', 'C', 'D']) {
|
|
91
|
+
const isEliminated = eliminated.includes(key);
|
|
81
92
|
const selected = answer === key;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
if (isEliminated) {
|
|
94
|
+
console.log(chalk.gray.strikethrough(` ${key}. ${q.options[key]}`));
|
|
95
|
+
}
|
|
96
|
+
else if (selected) {
|
|
97
|
+
console.log(chalk.green.bold(` ▸ ${key}. ${q.options[key]}`));
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
console.log(chalk.gray(` ${key}.`) + ' ' + chalk.white(q.options[key]));
|
|
101
|
+
}
|
|
85
102
|
}
|
|
86
103
|
console.log();
|
|
87
|
-
// Navigation hint
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
// Navigation + help hint
|
|
105
|
+
const remaining = help.max - help.used;
|
|
106
|
+
const helpHint = remaining > 0
|
|
107
|
+
? chalk.yellow('help') + chalk.gray(` (${remaining}/${help.max})`)
|
|
108
|
+
: (help.max < 8 ? chalk.gray('help 0/5 — type ') + chalk.yellow('more help') : chalk.gray('help 0/8'));
|
|
109
|
+
console.log(chalk.gray(' Type ') + chalk.white('A') + chalk.gray('/') + chalk.white('B') + chalk.gray('/') + chalk.white('C') + chalk.gray('/') + chalk.white('D') + chalk.gray(' to answer · ') + helpHint +
|
|
110
|
+
(q.number > 1 ? chalk.gray(' · ') + chalk.white('prev') : '') +
|
|
111
|
+
(q.number < total ? chalk.gray(' · ') + chalk.white('next') : ''));
|
|
95
112
|
}
|
|
96
113
|
export function registerExamCommand(program) {
|
|
97
114
|
const exam = program.command('exam').description('National selection exam');
|
|
@@ -210,6 +227,8 @@ export function registerExamCommand(program) {
|
|
|
210
227
|
printError(`Question ${n} not found (1-${state.questions.length}).`);
|
|
211
228
|
return;
|
|
212
229
|
}
|
|
230
|
+
state._lastQ = num;
|
|
231
|
+
saveExamState(state);
|
|
213
232
|
printQuestion(q, state.answers[num]);
|
|
214
233
|
}
|
|
215
234
|
else {
|
|
@@ -264,6 +283,140 @@ export function registerExamCommand(program) {
|
|
|
264
283
|
const q = state.questions[prev - 1];
|
|
265
284
|
printQuestion(q, state.answers[q.number]);
|
|
266
285
|
});
|
|
286
|
+
// ─── exam help ───
|
|
287
|
+
exam
|
|
288
|
+
.command('help')
|
|
289
|
+
.description('Eliminate one wrong option (limited uses)')
|
|
290
|
+
.action(() => {
|
|
291
|
+
logCommand('exam help');
|
|
292
|
+
const state = getExamState();
|
|
293
|
+
if (!state) {
|
|
294
|
+
printError('No exam in progress.');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const help = getHelpState(state);
|
|
298
|
+
const currentQ = state._lastQ || 1;
|
|
299
|
+
const qHelps = help.perQ[currentQ] || 0;
|
|
300
|
+
const eliminated = help.eliminated[currentQ] || [];
|
|
301
|
+
const q = state.questions.find((qq) => qq.number === currentQ);
|
|
302
|
+
if (!q)
|
|
303
|
+
return;
|
|
304
|
+
// Find the correct answer for demo
|
|
305
|
+
let correctAnswer = null;
|
|
306
|
+
if (state.session.examId === 'demo-free') {
|
|
307
|
+
import('../lib/demo-exam.js').then(({ DEMO_ANSWERS }) => {
|
|
308
|
+
correctAnswer = DEMO_ANSWERS[currentQ];
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
// Check: already used 2 helps on this question
|
|
312
|
+
if (qHelps >= 2) {
|
|
313
|
+
console.log();
|
|
314
|
+
console.log(chalk.yellow(' 🙈 I can\'t help you more on this one!'));
|
|
315
|
+
console.log(chalk.gray(' You already have a 50/50 — trust your instinct!'));
|
|
316
|
+
console.log();
|
|
317
|
+
printQuestion(q, state.answers[q.number]);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// Check: only 1 option left (shouldn't happen with max 2 helps, but safety)
|
|
321
|
+
const remaining = ['A', 'B', 'C', 'D'].filter((k) => !eliminated.includes(k));
|
|
322
|
+
if (remaining.length <= 2) {
|
|
323
|
+
console.log();
|
|
324
|
+
console.log(chalk.yellow(' 😄 Only 2 options left — you got this!'));
|
|
325
|
+
console.log();
|
|
326
|
+
printQuestion(q, state.answers[q.number]);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
// Check budget
|
|
330
|
+
if (help.used >= help.max) {
|
|
331
|
+
if (help.max < 8) {
|
|
332
|
+
console.log();
|
|
333
|
+
console.log(chalk.yellow(` Help used: ${help.used}/${help.max}`));
|
|
334
|
+
console.log(chalk.white(' Need more? Type: ') + chalk.bold.yellow('more help') + chalk.gray(' (+3 bonus uses)'));
|
|
335
|
+
console.log();
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
console.log();
|
|
339
|
+
console.log(chalk.gray(` All help used (${help.used}/${help.max}). You\'re on your own! 💪`));
|
|
340
|
+
console.log();
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
// Eliminate one wrong option (async for demo answers)
|
|
345
|
+
const doEliminate = (correct) => {
|
|
346
|
+
const wrongRemaining = remaining.filter((k) => k !== correct && !eliminated.includes(k));
|
|
347
|
+
if (wrongRemaining.length === 0)
|
|
348
|
+
return;
|
|
349
|
+
// Pick random wrong option to eliminate
|
|
350
|
+
const toRemove = wrongRemaining[Math.floor(Math.random() * wrongRemaining.length)];
|
|
351
|
+
eliminated.push(toRemove);
|
|
352
|
+
// Update state
|
|
353
|
+
if (!help.eliminated[currentQ])
|
|
354
|
+
help.eliminated[currentQ] = [];
|
|
355
|
+
help.eliminated[currentQ] = eliminated;
|
|
356
|
+
help.perQ[currentQ] = qHelps + 1;
|
|
357
|
+
help.used++;
|
|
358
|
+
state._helpUsed = help.used;
|
|
359
|
+
state._helpMax = help.max;
|
|
360
|
+
state._helpPerQ = help.perQ;
|
|
361
|
+
state._eliminated = help.eliminated;
|
|
362
|
+
saveExamState(state);
|
|
363
|
+
console.log();
|
|
364
|
+
console.log(chalk.yellow(` 💡 Option ${toRemove} eliminated!`) + chalk.gray(` (help ${help.used}/${help.max})`));
|
|
365
|
+
printQuestion(q, state.answers[q.number]);
|
|
366
|
+
};
|
|
367
|
+
if (state.session.examId === 'demo-free') {
|
|
368
|
+
import('../lib/demo-exam.js').then(({ DEMO_ANSWERS }) => {
|
|
369
|
+
doEliminate(DEMO_ANSWERS[currentQ]);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
// For server exams, we don't know the answer — eliminate random non-selected option
|
|
374
|
+
const userAnswer = state.answers[currentQ];
|
|
375
|
+
const candidates = remaining.filter((k) => k !== userAnswer);
|
|
376
|
+
const toRemove = candidates[Math.floor(Math.random() * candidates.length)];
|
|
377
|
+
eliminated.push(toRemove);
|
|
378
|
+
if (!help.eliminated[currentQ])
|
|
379
|
+
help.eliminated[currentQ] = [];
|
|
380
|
+
help.eliminated[currentQ] = eliminated;
|
|
381
|
+
help.perQ[currentQ] = qHelps + 1;
|
|
382
|
+
help.used++;
|
|
383
|
+
state._helpUsed = help.used;
|
|
384
|
+
state._helpMax = help.max;
|
|
385
|
+
state._helpPerQ = help.perQ;
|
|
386
|
+
state._eliminated = help.eliminated;
|
|
387
|
+
saveExamState(state);
|
|
388
|
+
console.log();
|
|
389
|
+
console.log(chalk.yellow(` 💡 Option ${toRemove} eliminated!`) + chalk.gray(` (help ${help.used}/${help.max})`));
|
|
390
|
+
printQuestion(q, state.answers[q.number]);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
// ─── exam more-help ───
|
|
394
|
+
exam
|
|
395
|
+
.command('more-help')
|
|
396
|
+
.description('Unlock 3 bonus help uses')
|
|
397
|
+
.action(() => {
|
|
398
|
+
logCommand('exam more-help');
|
|
399
|
+
const state = getExamState();
|
|
400
|
+
if (!state) {
|
|
401
|
+
printError('No exam in progress.');
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const help = getHelpState(state);
|
|
405
|
+
if (help.max >= 8) {
|
|
406
|
+
console.log(chalk.gray(' Bonus help already unlocked.'));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (help.used < help.max) {
|
|
410
|
+
console.log(chalk.gray(` You still have ${help.max - help.used} helps remaining. Use them first!`));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
state._helpMax = 8;
|
|
414
|
+
saveExamState(state);
|
|
415
|
+
console.log();
|
|
416
|
+
console.log(chalk.green(' 🎁 +3 bonus help unlocked! (3/8 remaining)'));
|
|
417
|
+
console.log(chalk.gray(' Type "help" on any question to use.'));
|
|
418
|
+
console.log();
|
|
419
|
+
});
|
|
267
420
|
// ─── exam answer <n> <choice> ───
|
|
268
421
|
exam
|
|
269
422
|
.command('answer <n> <choice>')
|
|
@@ -500,17 +653,18 @@ export function registerExamCommand(program) {
|
|
|
500
653
|
console.log();
|
|
501
654
|
printHeader('ICOA Demo Exam — Free Practice');
|
|
502
655
|
console.log();
|
|
503
|
-
console.log(chalk.white('
|
|
504
|
-
console.log(chalk.white(' 30 questions · 30 minutes ·
|
|
505
|
-
console.log(chalk.white(' No account or login required.'));
|
|
656
|
+
console.log(chalk.white(' Free practice exam · No account needed'));
|
|
657
|
+
console.log(chalk.white(' 30 questions · 30 minutes · Single choice'));
|
|
506
658
|
console.log();
|
|
507
659
|
console.log(chalk.gray(' ┌─────────────────────────────────────────────────┐'));
|
|
508
|
-
console.log(chalk.gray(' │') + chalk.
|
|
509
|
-
console.log(chalk.gray(' │') + chalk.white('
|
|
510
|
-
console.log(chalk.gray(' │') + chalk.white('
|
|
511
|
-
console.log(chalk.gray(' │') + chalk.white('
|
|
660
|
+
console.log(chalk.gray(' │') + chalk.white(' Each question has 4 options (A/B/C/D) ') + chalk.gray('│'));
|
|
661
|
+
console.log(chalk.gray(' │') + chalk.white(' Type a letter to answer the current question ') + chalk.gray('│'));
|
|
662
|
+
console.log(chalk.gray(' │') + chalk.white(' Type ') + chalk.yellow('help') + chalk.white(' to eliminate a wrong option (5 uses) ') + chalk.gray('│'));
|
|
663
|
+
console.log(chalk.gray(' │') + chalk.white(' Type ') + chalk.yellow('next') + chalk.white('/') + chalk.yellow('prev') + chalk.white(' to navigate between questions ') + chalk.gray('│'));
|
|
512
664
|
console.log(chalk.gray(' └─────────────────────────────────────────────────┘'));
|
|
513
665
|
console.log();
|
|
666
|
+
console.log(chalk.gray(' For real exams, your proctor will provide credentials.'));
|
|
667
|
+
console.log();
|
|
514
668
|
const proceed = await confirm({
|
|
515
669
|
message: chalk.white('Start demo exam now?'),
|
|
516
670
|
default: true,
|
package/dist/repl.js
CHANGED
|
@@ -5,6 +5,7 @@ import { isConnected, getConfig, saveConfig } from './lib/config.js';
|
|
|
5
5
|
import { isActivated, activateToken, isFreeCommand, isDeviceMatch, recordExit, recordResume, isFirstRunOrUpgrade, markVersionSeen } from './lib/access.js';
|
|
6
6
|
import { setReplMode } from './lib/ui.js';
|
|
7
7
|
import { isChatActive, handleChatMessage } from './commands/ai4ctf.js';
|
|
8
|
+
import { getExamState } from './lib/exam-state.js';
|
|
8
9
|
import { resetTerminalTheme } from './lib/theme.js';
|
|
9
10
|
import { ensureSandbox, runInSandbox, isDockerAvailable } from './lib/sandbox.js';
|
|
10
11
|
import { logCommand } from './lib/logger.js';
|
|
@@ -236,12 +237,35 @@ export async function startRepl(program, resumeMode) {
|
|
|
236
237
|
realExit(0);
|
|
237
238
|
return;
|
|
238
239
|
}
|
|
239
|
-
// Help
|
|
240
|
+
// Help — during exam, route to exam help; otherwise show REPL help
|
|
240
241
|
if (input === 'help' || input === '?') {
|
|
242
|
+
if (getExamState()) {
|
|
243
|
+
processing = true;
|
|
244
|
+
try {
|
|
245
|
+
await program.parseAsync(['node', 'icoa', 'exam', 'help']);
|
|
246
|
+
}
|
|
247
|
+
catch { }
|
|
248
|
+
processing = false;
|
|
249
|
+
rl.prompt();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
241
252
|
printReplHelp(isActivated(), mode);
|
|
242
253
|
rl.prompt();
|
|
243
254
|
return;
|
|
244
255
|
}
|
|
256
|
+
// "more help" — during exam, unlock bonus helps
|
|
257
|
+
if (input.toLowerCase() === 'more help') {
|
|
258
|
+
if (getExamState()) {
|
|
259
|
+
processing = true;
|
|
260
|
+
try {
|
|
261
|
+
await program.parseAsync(['node', 'icoa', 'exam', 'more-help']);
|
|
262
|
+
}
|
|
263
|
+
catch { }
|
|
264
|
+
processing = false;
|
|
265
|
+
rl.prompt();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
245
269
|
// Clear
|
|
246
270
|
if (input === 'clear' || input === 'cls') {
|
|
247
271
|
console.clear();
|
|
@@ -271,6 +295,37 @@ export async function startRepl(program, resumeMode) {
|
|
|
271
295
|
rl.prompt();
|
|
272
296
|
return;
|
|
273
297
|
}
|
|
298
|
+
// ─── Quick exam answer shortcuts ───
|
|
299
|
+
// "A" / "B" / "C" / "D" → answer current question
|
|
300
|
+
// "2 C" / "5 A" → answer specific question
|
|
301
|
+
const examState = getExamState();
|
|
302
|
+
if (examState) {
|
|
303
|
+
const upper = input.toUpperCase().trim();
|
|
304
|
+
// Single letter: A, B, C, D → answer current question
|
|
305
|
+
if (/^[ABCD]$/.test(upper)) {
|
|
306
|
+
const currentQ = examState._lastQ || 1;
|
|
307
|
+
processing = true;
|
|
308
|
+
try {
|
|
309
|
+
await program.parseAsync(['node', 'icoa', 'exam', 'answer', String(currentQ), upper]);
|
|
310
|
+
}
|
|
311
|
+
catch { }
|
|
312
|
+
processing = false;
|
|
313
|
+
rl.prompt();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
// "N X" pattern: e.g. "2 C", "15 A"
|
|
317
|
+
const match = upper.match(/^(\d+)\s+([ABCD])$/);
|
|
318
|
+
if (match) {
|
|
319
|
+
processing = true;
|
|
320
|
+
try {
|
|
321
|
+
await program.parseAsync(['node', 'icoa', 'exam', 'answer', match[1], match[2]]);
|
|
322
|
+
}
|
|
323
|
+
catch { }
|
|
324
|
+
processing = false;
|
|
325
|
+
rl.prompt();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
274
329
|
const cmd = input.split(/\s+/)[0].toLowerCase();
|
|
275
330
|
// ─── Mode-based command filtering ───
|
|
276
331
|
const selectionCommands = ['join', 'exam', 'demo', 'next', 'prev', 'setup', 'lang', 'ref', 'ctf'];
|