icoa-cli 2.8.0 → 2.9.1

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.
@@ -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,14 +88,25 @@ 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
- const prefix = selected ? chalk.green.bold(` ▸ ${key}.`) : chalk.gray(` ${key}.`);
83
- const text = selected ? chalk.green.bold(q.options[key]) : chalk.white(q.options[key]);
84
- console.log(`${prefix} ${text}`);
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
- 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') +
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 +
89
110
  (q.number > 1 ? chalk.gray(' · ') + chalk.white('prev') : '') +
90
111
  (q.number < total ? chalk.gray(' · ') + chalk.white('next') : ''));
91
112
  }
@@ -262,6 +283,140 @@ export function registerExamCommand(program) {
262
283
  const q = state.questions[prev - 1];
263
284
  printQuestion(q, state.answers[q.number]);
264
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
+ });
265
420
  // ─── exam answer <n> <choice> ───
266
421
  exam
267
422
  .command('answer <n> <choice>')
@@ -498,17 +653,18 @@ export function registerExamCommand(program) {
498
653
  console.log();
499
654
  printHeader('ICOA Demo Exam — Free Practice');
500
655
  console.log();
501
- console.log(chalk.white(' This is a free practice exam to help you prepare.'));
502
- console.log(chalk.white(' 30 questions · 30 minutes · English'));
503
- 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'));
504
658
  console.log();
505
659
  console.log(chalk.gray(' ┌─────────────────────────────────────────────────┐'));
506
- console.log(chalk.gray(' │') + chalk.yellow(' For real exams, your proctor will provide: ') + chalk.gray('│'));
507
- console.log(chalk.gray(' │') + chalk.white(' · Server URL ') + chalk.gray('│'));
508
- console.log(chalk.gray(' │') + chalk.white(' · Username & Password ') + chalk.gray('│'));
509
- console.log(chalk.gray(' │') + chalk.white(' · Exam ID ') + chalk.gray('│'));
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('│'));
510
664
  console.log(chalk.gray(' └─────────────────────────────────────────────────┘'));
511
665
  console.log();
666
+ console.log(chalk.gray(' For real exams, your proctor will provide credentials.'));
667
+ console.log();
512
668
  const proceed = await confirm({
513
669
  message: chalk.white('Start demo exam now?'),
514
670
  default: true,
package/dist/index.js CHANGED
@@ -17,6 +17,11 @@ import { registerExamCommand } from './commands/exam.js';
17
17
  import { getConfig, saveConfig } from './lib/config.js';
18
18
  import { startRepl } from './repl.js';
19
19
  import { setTerminalTheme } from './lib/theme.js';
20
+ import { readFileSync } from 'node:fs';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { dirname, join } from 'node:path';
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+ const PKG_VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')).version;
20
25
  const LINE = chalk.cyan(' ─────────────────────────────────────────────────────');
21
26
  const BANNER = `
22
27
  ${LINE}
@@ -38,7 +43,7 @@ ${LINE}
38
43
  ${chalk.white('Sydney, Australia')} ${chalk.gray('Jun 27 - Jul 2, 2026')}
39
44
  ${chalk.cyan.underline('https://icoa2026.au')}
40
45
 
41
- ${chalk.gray('CLI-Native Competition Terminal v2.7.0')}
46
+ ${chalk.gray(`CLI-Native Competition Terminal v${PKG_VERSION}`)}
42
47
 
43
48
  ${LINE}
44
49
  `;
package/dist/repl.js CHANGED
@@ -237,12 +237,35 @@ export async function startRepl(program, resumeMode) {
237
237
  realExit(0);
238
238
  return;
239
239
  }
240
- // Help
240
+ // Help — during exam, route to exam help; otherwise show REPL help
241
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
+ }
242
252
  printReplHelp(isActivated(), mode);
243
253
  rl.prompt();
244
254
  return;
245
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
+ }
246
269
  // Clear
247
270
  if (input === 'clear' || input === 'cls') {
248
271
  console.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.8.0",
3
+ "version": "2.9.1",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {