icoa-cli 2.6.1 โ†’ 2.7.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.
@@ -44,11 +44,37 @@ function printTimeRemaining() {
44
44
  }
45
45
  }
46
46
  }
47
+ // Australian easter eggs every 5 questions
48
+ const EASTER_EGGS = {
49
+ 5: { emoji: '๐Ÿ›๏ธ', text: 'Sydney Opera House โ€” You\'re doing great!' },
50
+ 10: { emoji: '๐ŸŒ‰', text: 'Sydney Harbour Bridge โ€” Keep going!' },
51
+ 15: { emoji: '๐Ÿจ', text: 'Halfway there! A koala cheers you on!' },
52
+ 20: { emoji: '๐Ÿฆ˜', text: 'A kangaroo hops by โ€” almost there!' },
53
+ 25: { emoji: '๐Ÿ–๏ธ', text: 'Bondi Beach โ€” 5 more to go!' },
54
+ 30: { emoji: '๐ŸŽ‰', text: 'G\'day mate! All questions done!' },
55
+ };
56
+ function printQuestionProgress(current, total, answered) {
57
+ const width = 30;
58
+ const filled = Math.round((current / total) * width);
59
+ const empty = width - filled;
60
+ const bar = chalk.cyan('โ”'.repeat(filled)) + chalk.gray('โ”€'.repeat(empty));
61
+ const pct = Math.round((current / total) * 100);
62
+ console.log();
63
+ console.log(` ${bar} ${chalk.white.bold(`${current}`)}${chalk.gray(`/${total}`)} ${chalk.gray(`(${answered} answered)`)} ${chalk.gray(`${pct}%`)}`);
64
+ }
47
65
  function printQuestion(q, answer) {
48
66
  const state = getExamState();
49
- const total = state?.session.questionCount || '?';
67
+ const total = Number(state?.session.questionCount || 30);
68
+ const answered = Object.keys(state?.answers || {}).length;
69
+ // Progress bar
70
+ printQuestionProgress(q.number, total, answered);
71
+ // Easter egg
72
+ const egg = EASTER_EGGS[q.number];
73
+ if (egg && q.number <= total) {
74
+ console.log(chalk.yellow(` ${egg.emoji} ${egg.text}`));
75
+ }
50
76
  console.log();
51
- console.log(chalk.bold.white(` Q${q.number}/${total}`) + (q.category ? chalk.gray(` [${q.category}]`) : ''));
77
+ console.log(chalk.bold.white(` Q${q.number}`) + (q.category ? chalk.gray(` [${q.category}]`) : ''));
52
78
  console.log(` ${q.text}`);
53
79
  console.log();
54
80
  for (const key of ['A', 'B', 'C', 'D']) {
@@ -58,6 +84,14 @@ function printQuestion(q, answer) {
58
84
  console.log(`${prefix} ${text}`);
59
85
  }
60
86
  console.log();
87
+ // Navigation hint
88
+ const nav = [];
89
+ if (q.number > 1)
90
+ nav.push('exam prev');
91
+ if (q.number < total)
92
+ nav.push('exam next');
93
+ nav.push(`exam answer ${q.number} <A-D>`);
94
+ console.log(chalk.gray(` ${nav.join(' ยท ')}`));
61
95
  }
62
96
  export function registerExamCommand(program) {
63
97
  const exam = program.command('exam').description('National selection exam');
@@ -192,6 +226,43 @@ export function registerExamCommand(program) {
192
226
  console.log();
193
227
  }
194
228
  });
229
+ // โ”€โ”€โ”€ exam next โ”€โ”€โ”€
230
+ exam
231
+ .command('next')
232
+ .description('Go to next question')
233
+ .action(() => {
234
+ logCommand('exam next');
235
+ const state = getExamState();
236
+ if (!state) {
237
+ printError('No exam in progress.');
238
+ return;
239
+ }
240
+ const current = state.questions.findIndex((q) => !state.answers[q.number]);
241
+ const lastViewed = state._lastQ || 1;
242
+ const next = Math.min(lastViewed + 1, state.questions.length);
243
+ state._lastQ = next;
244
+ saveExamState(state);
245
+ const q = state.questions[next - 1];
246
+ printQuestion(q, state.answers[q.number]);
247
+ });
248
+ // โ”€โ”€โ”€ exam prev โ”€โ”€โ”€
249
+ exam
250
+ .command('prev')
251
+ .description('Go to previous question')
252
+ .action(() => {
253
+ logCommand('exam prev');
254
+ const state = getExamState();
255
+ if (!state) {
256
+ printError('No exam in progress.');
257
+ return;
258
+ }
259
+ const lastViewed = state._lastQ || 1;
260
+ const prev = Math.max(lastViewed - 1, 1);
261
+ state._lastQ = prev;
262
+ saveExamState(state);
263
+ const q = state.questions[prev - 1];
264
+ printQuestion(q, state.answers[q.number]);
265
+ });
195
266
  // โ”€โ”€โ”€ exam answer <n> <choice> โ”€โ”€โ”€
196
267
  exam
197
268
  .command('answer <n> <choice>')
@@ -217,15 +288,20 @@ export function registerExamCommand(program) {
217
288
  return;
218
289
  }
219
290
  state.answers[num] = c;
291
+ state._lastQ = num;
220
292
  saveExamState(state);
221
293
  const answered = Object.keys(state.answers).length;
222
294
  const total = state.session.questionCount;
223
295
  printSuccess(`Q${num}: ${c} saved (${answered}/${total} answered)`);
296
+ // Auto-show next question
224
297
  if (num < state.questions.length) {
225
- console.log(chalk.gray(` Next: exam q ${num + 1}`));
298
+ const nextQ = state.questions[num]; // 0-indexed: questions[num] = question num+1
299
+ printQuestion(nextQ, state.answers[nextQ.number]);
226
300
  }
227
301
  else if (answered === total) {
228
- console.log(chalk.gray(' All answered! Use: exam review'));
302
+ console.log();
303
+ console.log(chalk.green.bold(' ๐ŸŽ‰ All questions answered!'));
304
+ console.log(chalk.white(' Use: exam review ยท exam submit'));
229
305
  }
230
306
  });
231
307
  // โ”€โ”€โ”€ exam review โ”€โ”€โ”€
@@ -313,10 +389,22 @@ export function registerExamCommand(program) {
313
389
  clearExamState();
314
390
  const percentage = Math.round(score / total * 100);
315
391
  console.log();
316
- printHeader('Demo Exam Result');
317
- printKeyValue('Score', chalk.bold(`${score}/${total}`));
318
- printKeyValue('Percentage', chalk.bold(`${percentage}%`));
319
- printKeyValue('Status', percentage >= 60 ? chalk.green.bold('PASSED') : chalk.red.bold('NOT PASSED'));
392
+ console.log(chalk.cyan(' โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'));
393
+ console.log();
394
+ console.log(chalk.bold.white(' โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—'));
395
+ console.log(chalk.bold.white(' โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—'));
396
+ console.log(chalk.bold.white(' โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘'));
397
+ console.log(chalk.bold.white(' โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘'));
398
+ console.log(chalk.bold.white(' โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘'));
399
+ console.log(chalk.bold.white(' โ•šโ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ• โ•šโ•โ•'));
400
+ console.log();
401
+ console.log(chalk.bold(` Score: ${score}/${total} (${percentage}%)`));
402
+ console.log(chalk.bold(` ${percentage >= 60 ? chalk.green('โœ“ PASSED') : chalk.red('โœ— NOT PASSED')}`));
403
+ console.log();
404
+ console.log(chalk.yellow(' International Cyber Olympiad in AI 2026'));
405
+ console.log(chalk.gray(' Sydney, Australia ยท Jun 27 - Jul 2, 2026'));
406
+ console.log();
407
+ console.log(chalk.cyan(' โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'));
320
408
  console.log();
321
409
  console.log(chalk.gray(' This was a free practice exam.'));
322
410
  console.log(chalk.gray(' For the real exam, contact your national organizer.'));
package/dist/index.js CHANGED
@@ -38,7 +38,7 @@ ${LINE}
38
38
  ${chalk.white('Sydney, Australia')} ${chalk.gray('Jun 27 - Jul 2, 2026')}
39
39
  ${chalk.cyan.underline('https://icoa2026.au')}
40
40
 
41
- ${chalk.gray('CLI-Native Competition Terminal v2.6.1')}
41
+ ${chalk.gray('CLI-Native Competition Terminal v2.7.0')}
42
42
 
43
43
  ${LINE}
44
44
  `;
package/dist/repl.js CHANGED
@@ -43,7 +43,7 @@ export async function startRepl(program, resumeMode) {
43
43
  { name: ` ${chalk.bold('National/Regional Partner')} ${chalk.gray('ยท')} ${chalk.gray('Organizer management')}`, value: 'organizer' },
44
44
  ];
45
45
  const defaultIndex = savedMode ? choices.findIndex((c) => c.value === savedMode) : 0;
46
- console.log(chalk.gray(' Use โ†‘โ†“ to select, Enter to confirm.'));
46
+ console.log(chalk.gray(' Use ') + chalk.yellow('โ†‘') + chalk.gray(' or ') + chalk.yellow('โ†“') + chalk.gray(' to select, ') + chalk.yellow('Enter') + chalk.gray(' to confirm.'));
47
47
  console.log();
48
48
  let mode = await selectMode({
49
49
  message: 'Mode',
@@ -273,8 +273,8 @@ export async function startRepl(program, resumeMode) {
273
273
  }
274
274
  const cmd = input.split(/\s+/)[0].toLowerCase();
275
275
  // โ”€โ”€โ”€ Mode-based command filtering โ”€โ”€โ”€
276
- const selectionCommands = ['join', 'exam', 'demo', 'setup', 'lang', 'ref', 'ctf'];
277
- const organizerCommands = ['join', 'exam', 'demo', 'setup', 'lang', 'ref', 'ctf'];
276
+ const selectionCommands = ['join', 'exam', 'demo', 'next', 'prev', 'setup', 'lang', 'ref', 'ctf'];
277
+ const organizerCommands = ['join', 'exam', 'demo', 'next', 'prev', 'setup', 'lang', 'ref', 'ctf'];
278
278
  if (mode === 'selection' && !selectionCommands.includes(cmd)) {
279
279
  console.log(chalk.gray(' Not available in Selection mode. Switch via: setup'));
280
280
  console.log();
@@ -303,7 +303,7 @@ export async function startRepl(program, resumeMode) {
303
303
  'scoreboard', 'sb', 'status', 'time', 'hint', 'hint-b', 'hint-c',
304
304
  'hint-budget', 'ref', 'shell', 'files', 'connect', 'note',
305
305
  'log', 'lang', 'setup', 'env', 'ai4ctf', 'model', 'ctf',
306
- 'exam', 'demo',
306
+ 'exam', 'demo', 'next', 'prev',
307
307
  ];
308
308
  if (!knownCommands.includes(cmd)) {
309
309
  // Block dangerous commands
@@ -438,6 +438,8 @@ function mapCommand(input) {
438
438
  const rest = parts.slice(1);
439
439
  const ctfShortcuts = {
440
440
  'demo': ['exam', 'demo'],
441
+ 'next': ['exam', 'next'],
442
+ 'prev': ['exam', 'prev'],
441
443
  'join': ['ctf', 'join', ...rest],
442
444
  'activate': ['ctf', 'activate', ...rest],
443
445
  'challenges': ['ctf', 'challenges'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.6.1",
3
+ "version": "2.7.1",
4
4
  "description": "ICOA CLI โ€” The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {