icoa-cli 2.19.44 → 2.19.45

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.
@@ -1130,7 +1130,8 @@ export function registerExamCommand(program) {
1130
1130
  .description('Enter exam with access token (no login needed)')
1131
1131
  .action(async (code) => {
1132
1132
  logCommand(`exam token ${code}`);
1133
- const existing = getExamState();
1133
+ const { getRealExamState } = await import('../lib/exam-state.js');
1134
+ const existing = getRealExamState();
1134
1135
  if (existing) {
1135
1136
  printWarning(`Exam "${existing.session.examName}" is already in progress.`);
1136
1137
  printInfo('Use "exam review" or "exam submit" first.');
@@ -1255,17 +1256,11 @@ export function registerExamCommand(program) {
1255
1256
  }
1256
1257
  const DEMO_QUESTIONS = pickDemoQuestions(DEMO_PICK_SIZE);
1257
1258
  const DEMO_SESSION = getLocalizedDemoSession();
1258
- const existing = getExamState();
1259
- if (existing) {
1260
- if (existing.session.examId === 'demo-free') {
1261
- // Demo: always restart fresh
1262
- clearExamState();
1263
- }
1264
- else {
1265
- printWarning(`Exam "${existing.session.examName}" is in progress.`);
1266
- printInfo('Submit it first: exam submit');
1267
- return;
1268
- }
1259
+ // Demo uses separate state file — doesn't conflict with real exam
1260
+ const { getDemoState, clearExamState: clearState } = await import('../lib/exam-state.js');
1261
+ const existingDemo = getDemoState();
1262
+ if (existingDemo) {
1263
+ clearState('demo-free');
1269
1264
  }
1270
1265
  console.log();
1271
1266
  printHeader('ICOA Demo Exam — Free Practice');
@@ -1,7 +1,9 @@
1
1
  import type { ExamState } from '../types/index.js';
2
2
  export declare function getExamState(): ExamState | null;
3
+ export declare function getDemoState(): ExamState | null;
4
+ export declare function getRealExamState(): ExamState | null;
3
5
  export declare function saveExamState(state: ExamState): void;
4
- export declare function clearExamState(): void;
6
+ export declare function clearExamState(examId?: string): void;
5
7
  export declare function isExamActive(): boolean;
6
8
  export declare function getExamDeadline(): Date | null;
7
9
  export declare function addInteraction(interaction: import('../types/index.js').ExamInteraction): void;
@@ -1,10 +1,49 @@
1
1
  import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { getIcoaDir } from './config.js';
4
+ // Demo and real exam use separate state files so they don't block each other
4
5
  function stateFile() {
5
6
  return join(getIcoaDir(), 'exam-state.json');
6
7
  }
8
+ function demoStateFile() {
9
+ return join(getIcoaDir(), 'demo-state.json');
10
+ }
11
+ // Internal: pick the right file based on examId
12
+ function resolveStateFile(examId) {
13
+ return examId === 'demo-free' ? demoStateFile() : stateFile();
14
+ }
7
15
  export function getExamState() {
16
+ // Check both files, return the most recently active one
17
+ const files = [stateFile(), demoStateFile()];
18
+ let latest = null;
19
+ let latestTime = 0;
20
+ for (const f of files) {
21
+ if (!existsSync(f))
22
+ continue;
23
+ try {
24
+ const state = JSON.parse(readFileSync(f, 'utf-8'));
25
+ const t = new Date(state.session.confirmedAt || state.session.startedAt).getTime();
26
+ if (t > latestTime) {
27
+ latest = state;
28
+ latestTime = t;
29
+ }
30
+ }
31
+ catch { }
32
+ }
33
+ return latest;
34
+ }
35
+ export function getDemoState() {
36
+ const f = demoStateFile();
37
+ if (!existsSync(f))
38
+ return null;
39
+ try {
40
+ return JSON.parse(readFileSync(f, 'utf-8'));
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ export function getRealExamState() {
8
47
  const f = stateFile();
9
48
  if (!existsSync(f))
10
49
  return null;
@@ -16,12 +55,24 @@ export function getExamState() {
16
55
  }
17
56
  }
18
57
  export function saveExamState(state) {
19
- writeFileSync(stateFile(), JSON.stringify(state, null, 2));
58
+ const f = resolveStateFile(state.session.examId);
59
+ writeFileSync(f, JSON.stringify(state, null, 2));
20
60
  }
21
- export function clearExamState() {
22
- const f = stateFile();
23
- if (existsSync(f))
24
- unlinkSync(f);
61
+ export function clearExamState(examId) {
62
+ if (examId) {
63
+ const f = resolveStateFile(examId);
64
+ if (existsSync(f))
65
+ unlinkSync(f);
66
+ }
67
+ else {
68
+ // Clear the currently active exam
69
+ const state = getExamState();
70
+ if (state) {
71
+ const f = resolveStateFile(state.session.examId);
72
+ if (existsSync(f))
73
+ unlinkSync(f);
74
+ }
75
+ }
25
76
  }
26
77
  export function isExamActive() {
27
78
  return getExamState() !== null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.44",
3
+ "version": "2.19.45",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {