icoa-cli 2.12.3 → 2.13.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.
@@ -663,6 +663,66 @@ export function registerExamCommand(program) {
663
663
  printError(err.message);
664
664
  }
665
665
  });
666
+ // ─── exam token <code> (Selection mode: no login needed) ───
667
+ exam
668
+ .command('token <code>')
669
+ .description('Enter exam with access token (no login needed)')
670
+ .action(async (code) => {
671
+ logCommand(`exam token ${code}`);
672
+ const existing = getExamState();
673
+ if (existing) {
674
+ printWarning(`Exam "${existing.session.examName}" is already in progress.`);
675
+ printInfo('Use "exam review" or "exam submit" first.');
676
+ return;
677
+ }
678
+ const config = getConfig();
679
+ const serverUrl = config.ctfdUrl || 'https://practice.icoa2026.au';
680
+ console.log();
681
+ drawProgress(0, 'Validating token...');
682
+ try {
683
+ const res = await fetch(`${serverUrl}:9090/api/icoa/exam-token`, {
684
+ method: 'POST',
685
+ headers: { 'Content-Type': 'application/json' },
686
+ body: JSON.stringify({ token: code.trim() }),
687
+ signal: AbortSignal.timeout(10000),
688
+ });
689
+ if (!res.ok) {
690
+ drawProgress(0, '');
691
+ console.log();
692
+ const err = await res.json().catch(() => ({ message: 'Invalid token' }));
693
+ printError(err.message || 'Invalid exam token');
694
+ printInfo('Check your token or contact your proctor.');
695
+ return;
696
+ }
697
+ drawProgress(30, 'Loading exam...');
698
+ const json = await res.json();
699
+ const { session, questions } = json.data;
700
+ drawProgress(60, 'Preparing questions...');
701
+ await sleep(200);
702
+ drawProgress(90, 'Starting timer...');
703
+ await sleep(150);
704
+ saveExamState({ session, questions, answers: {} });
705
+ drawProgress(100, 'Ready!');
706
+ console.log();
707
+ console.log();
708
+ printHeader(session.examName);
709
+ printKeyValue('Questions', String(session.questionCount));
710
+ printKeyValue('Duration', `${session.durationMinutes} minutes`);
711
+ printTimeRemaining();
712
+ if (questions.length > 0) {
713
+ printQuestion(questions[0]);
714
+ }
715
+ }
716
+ catch (err) {
717
+ console.log();
718
+ if (err.name === 'TimeoutError') {
719
+ printError('Server not reachable. Check your internet connection.');
720
+ }
721
+ else {
722
+ printError(err.message || 'Failed to start exam');
723
+ }
724
+ }
725
+ });
666
726
  // ─── exam demo ───
667
727
  exam
668
728
  .command('demo')
package/dist/repl.js CHANGED
@@ -180,6 +180,9 @@ export async function startRepl(program, resumeMode) {
180
180
  console.log(chalk.gray(' ─────────────────────────────────────────────'));
181
181
  console.log(chalk.bold.cyan(' demo') + chalk.gray(' Free practice exam (30 questions)'));
182
182
  console.log(chalk.gray(' No account needed. Try it now!'));
183
+ console.log();
184
+ console.log(chalk.white(' exam <token>') + chalk.gray(' Enter exam with access token'));
185
+ console.log(chalk.gray(' Provided by your proctor.'));
183
186
  console.log(chalk.gray(' ─────────────────────────────────────────────'));
184
187
  console.log();
185
188
  }
@@ -233,6 +236,7 @@ export async function startRepl(program, resumeMode) {
233
236
  else if (connected) {
234
237
  console.log(chalk.green(` Welcome back, ${config.userName}!`));
235
238
  console.log(chalk.gray(` Connected to ${config.ctfdUrl}`));
239
+ console.log(chalk.gray(' logout to disconnect'));
236
240
  console.log();
237
241
  }
238
242
  else if (activated) {
@@ -337,6 +341,17 @@ export async function startRepl(program, resumeMode) {
337
341
  return;
338
342
  }
339
343
  }
344
+ // ICOA exam token detection (e.g., "ICOA-PE-001")
345
+ if (/^ICOA-[A-Z]{2,3}-\d{1,6}$/i.test(input.trim())) {
346
+ processing = true;
347
+ try {
348
+ await program.parseAsync(['node', 'icoa', 'exam', 'token', input.trim()]);
349
+ }
350
+ catch { }
351
+ processing = false;
352
+ rl.prompt();
353
+ return;
354
+ }
340
355
  // Clear
341
356
  if (input === 'clear' || input === 'cls') {
342
357
  console.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.12.3",
3
+ "version": "2.13.1",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {