icoa-cli 2.19.44 → 2.19.46
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 +7 -12
- package/dist/lib/exam-state.d.ts +3 -1
- package/dist/lib/exam-state.js +56 -5
- package/package.json +1 -1
package/dist/commands/exam.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
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');
|
package/dist/lib/exam-state.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/exam-state.js
CHANGED
|
@@ -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
|
-
|
|
58
|
+
const f = resolveStateFile(state.session.examId);
|
|
59
|
+
writeFileSync(f, JSON.stringify(state, null, 2));
|
|
20
60
|
}
|
|
21
|
-
export function clearExamState() {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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;
|