icoa-cli 2.19.52 → 2.19.53

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.
Files changed (2) hide show
  1. package/dist/repl.js +29 -7
  2. package/package.json +1 -1
package/dist/repl.js CHANGED
@@ -413,19 +413,43 @@ export async function startRepl(program, resumeMode) {
413
413
  realExit(0);
414
414
  return;
415
415
  }
416
- // "back" — return to main menu (clear exam state for demo, keep for real)
416
+ // "back" — return to main menu.
417
+ // Real exam: show "Exam paused", preserve state (server timer is ticking).
418
+ // Active demo: show pause message, keep state so `exam q N` can resume.
419
+ // Stale demo: auto-clear and show menu (demo from a previous session the
420
+ // user long abandoned — hanging state makes the menu lie).
421
+ // Nothing: show selection menu.
422
+ // A demo is "active" if `startedAt` is within the last 30 minutes. That window
423
+ // covers an intentional "back to check something and come right back" case but
424
+ // clears anything left over from a prior session.
417
425
  if (input === 'back') {
418
426
  const state = getExamState();
419
- if (state) {
427
+ const isRealExam = state && state.session.examId !== 'demo-free';
428
+ const isActiveDemo = state && state.session.examId === 'demo-free' && (() => {
429
+ const started = new Date(state.session.startedAt || 0).getTime();
430
+ return Date.now() - started < 30 * 60 * 1000;
431
+ })();
432
+ if (isRealExam) {
420
433
  console.log();
421
434
  console.log(chalk.gray(' Exam paused. Your progress is saved.'));
422
435
  console.log(chalk.white(' Resume: exam q 1') + chalk.gray(' · ') + chalk.white('exam review') + chalk.gray(' · ') + chalk.white('exam submit'));
423
436
  console.log();
424
437
  }
438
+ else if (isActiveDemo) {
439
+ const answered = Object.keys(state.answers).length;
440
+ const total = state.session.questionCount;
441
+ console.log();
442
+ console.log(chalk.gray(` Demo paused (${answered}/${total} answered). Resume with: `) + chalk.white(`exam q 1`));
443
+ console.log(chalk.gray(' Or type ') + chalk.white('demo') + chalk.gray(' to restart.'));
444
+ console.log();
445
+ }
425
446
  else {
426
- // No active examtypically this is the "post-report back" choice
427
- // after the user has finished the 3-stage demo flow. Report it as a
428
- // distinct event so the admin can count retry vs back decisions.
447
+ // Stale demo state from a past session clear it so the menu reflects
448
+ // reality. Demo is free-practice, user can always restart.
449
+ if (state && state.session.examId === 'demo-free') {
450
+ const { clearExamState } = await import('./lib/exam-state.js');
451
+ clearExamState('demo-free');
452
+ }
429
453
  const cfg = getConfig();
430
454
  fetch('https://practice.icoa2026.au/api/icoa/demo-stats', {
431
455
  method: 'POST',
@@ -437,8 +461,6 @@ export async function startRepl(program, resumeMode) {
437
461
  }),
438
462
  signal: AbortSignal.timeout(5000),
439
463
  }).catch(() => { });
440
- // Show the National Selection menu so the user always knows what
441
- // commands are available (demo / exam setup / exam <token>).
442
464
  if (mode === 'selection') {
443
465
  printSelectionMenu();
444
466
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.52",
3
+ "version": "2.19.53",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {