icoa-cli 2.19.52 → 2.19.54
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 +30 -2
- package/dist/repl.js +29 -7
- package/package.json +1 -1
package/dist/commands/exam.js
CHANGED
|
@@ -1456,8 +1456,33 @@ export function registerExamCommand(program) {
|
|
|
1456
1456
|
const { getRealExamState } = await import('../lib/exam-state.js');
|
|
1457
1457
|
const existing = getRealExamState();
|
|
1458
1458
|
if (existing) {
|
|
1459
|
-
|
|
1460
|
-
|
|
1459
|
+
const answered = Object.keys(existing.answers).length;
|
|
1460
|
+
const total = existing.session.questionCount;
|
|
1461
|
+
const existingToken = existing.session.token;
|
|
1462
|
+
const deadline = getExamDeadline();
|
|
1463
|
+
const remainingMin = deadline ? Math.max(0, Math.round((deadline.getTime() - Date.now()) / 60000)) : null;
|
|
1464
|
+
console.log();
|
|
1465
|
+
console.log(chalk.yellow(` ⚠ An exam is already in progress on this device.`));
|
|
1466
|
+
console.log();
|
|
1467
|
+
console.log(chalk.gray(' Exam: ') + chalk.white(existing.session.examName));
|
|
1468
|
+
if (existingToken) {
|
|
1469
|
+
console.log(chalk.gray(' Token: ') + chalk.white(existingToken) + (existingToken === code ? chalk.green(' (same as you just typed)') : chalk.yellow(' (different from the one you typed)')));
|
|
1470
|
+
}
|
|
1471
|
+
console.log(chalk.gray(' Answers: ') + chalk.white(`${answered}/${total}`));
|
|
1472
|
+
if (remainingMin !== null) {
|
|
1473
|
+
console.log(chalk.gray(' Time: ') + chalk.white(`${remainingMin} min remaining`));
|
|
1474
|
+
}
|
|
1475
|
+
console.log();
|
|
1476
|
+
console.log(chalk.bold.white(' To continue this exam:'));
|
|
1477
|
+
console.log(chalk.gray(' → ') + chalk.bold.cyan('exam q 1') + chalk.gray(' resume at any question'));
|
|
1478
|
+
console.log(chalk.gray(' → ') + chalk.bold.cyan('exam review') + chalk.gray(' see progress + flagged items'));
|
|
1479
|
+
console.log(chalk.gray(' → ') + chalk.bold.cyan('exam submit') + chalk.gray(' finish and submit'));
|
|
1480
|
+
if (existingToken && existingToken !== code) {
|
|
1481
|
+
console.log();
|
|
1482
|
+
console.log(chalk.yellow(' Note: you typed a different token. Each exam token is bound to'));
|
|
1483
|
+
console.log(chalk.yellow(' one device + one session. You cannot switch tokens mid-exam.'));
|
|
1484
|
+
}
|
|
1485
|
+
console.log();
|
|
1461
1486
|
return;
|
|
1462
1487
|
}
|
|
1463
1488
|
// Gate: require exam setup
|
|
@@ -1465,6 +1490,7 @@ export function registerExamCommand(program) {
|
|
|
1465
1490
|
console.log();
|
|
1466
1491
|
printWarning('Pre-exam setup required before entering a token.');
|
|
1467
1492
|
console.log(chalk.gray(' → ') + chalk.bold.cyan('exam setup'));
|
|
1493
|
+
console.log(chalk.gray(' Or type ') + chalk.bold.cyan('back') + chalk.gray(' to return to the main menu.'));
|
|
1468
1494
|
console.log();
|
|
1469
1495
|
return;
|
|
1470
1496
|
}
|
|
@@ -1721,6 +1747,7 @@ export function registerExamCommand(program) {
|
|
|
1721
1747
|
}
|
|
1722
1748
|
console.log();
|
|
1723
1749
|
console.log(chalk.white(' Next step: ') + chalk.bold.cyan('exam <token>'));
|
|
1750
|
+
console.log(chalk.gray(' Or type ') + chalk.bold.cyan('back') + chalk.gray(' to return to the main menu.'));
|
|
1724
1751
|
console.log();
|
|
1725
1752
|
return;
|
|
1726
1753
|
}
|
|
@@ -1857,6 +1884,7 @@ export function registerExamCommand(program) {
|
|
|
1857
1884
|
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
1858
1885
|
console.log();
|
|
1859
1886
|
console.log(chalk.white(' Next step: ') + chalk.bold.cyan('exam <token>'));
|
|
1887
|
+
console.log(chalk.gray(' Or type ') + chalk.bold.cyan('back') + chalk.gray(' to return to the main menu.'));
|
|
1860
1888
|
console.log();
|
|
1861
1889
|
});
|
|
1862
1890
|
// Default action: progressive onboarding dashboard
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
427
|
-
//
|
|
428
|
-
|
|
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
|
}
|