icoa-cli 2.19.54 → 2.19.56
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 +46 -8
- package/package.json +1 -1
package/dist/commands/exam.js
CHANGED
|
@@ -1453,12 +1453,30 @@ export function registerExamCommand(program) {
|
|
|
1453
1453
|
.description('Enter exam with access token (no login needed)')
|
|
1454
1454
|
.action(async (code) => {
|
|
1455
1455
|
logCommand(`exam token ${code}`);
|
|
1456
|
-
const { getRealExamState } = await import('../lib/exam-state.js');
|
|
1456
|
+
const { getRealExamState, saveExamState: saveExamStateFn } = await import('../lib/exam-state.js');
|
|
1457
1457
|
const existing = getRealExamState();
|
|
1458
1458
|
if (existing) {
|
|
1459
|
+
const existingToken = existing.session.token;
|
|
1460
|
+
// Migration path: pre-v2.19.55 sessions didn't persist the token on
|
|
1461
|
+
// state. Those sessions can't use `lang` or submit via the new token
|
|
1462
|
+
// path. If the user re-types the matching token, attach it so the
|
|
1463
|
+
// session can reach the server again. Device binding on the server
|
|
1464
|
+
// means only the original token holder can do this.
|
|
1465
|
+
if (!existingToken && code) {
|
|
1466
|
+
existing.session.token = code.trim();
|
|
1467
|
+
saveExamStateFn(existing);
|
|
1468
|
+
console.log();
|
|
1469
|
+
console.log(chalk.green(' ✓ Token re-attached to your in-progress exam.'));
|
|
1470
|
+
console.log(chalk.gray(' You can now use: ') + chalk.white('lang <code>') + chalk.gray(' · ') + chalk.white('exam submit'));
|
|
1471
|
+
console.log();
|
|
1472
|
+
console.log(chalk.bold.white(' Continue this exam:'));
|
|
1473
|
+
console.log(chalk.gray(' → ') + chalk.bold.cyan('exam q 1') + chalk.gray(' resume at any question'));
|
|
1474
|
+
console.log(chalk.gray(' → ') + chalk.bold.cyan('exam review') + chalk.gray(' see progress + flagged items'));
|
|
1475
|
+
console.log();
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1459
1478
|
const answered = Object.keys(existing.answers).length;
|
|
1460
1479
|
const total = existing.session.questionCount;
|
|
1461
|
-
const existingToken = existing.session.token;
|
|
1462
1480
|
const deadline = getExamDeadline();
|
|
1463
1481
|
const remainingMin = deadline ? Math.max(0, Math.round((deadline.getTime() - Date.now()) / 60000)) : null;
|
|
1464
1482
|
console.log();
|
|
@@ -1560,17 +1578,37 @@ export function registerExamCommand(program) {
|
|
|
1560
1578
|
console.log(chalk.gray(' • You may exit and resume with the same token'));
|
|
1561
1579
|
console.log();
|
|
1562
1580
|
// ── Wait for confirmation ──
|
|
1563
|
-
|
|
1564
|
-
|
|
1581
|
+
// Raw stdin read (not readline.createInterface) — a second readline on
|
|
1582
|
+
// process.stdin fights the parent REPL's readline and leaves stdin in
|
|
1583
|
+
// a half-detached state, so subsequent commands silently get no input.
|
|
1565
1584
|
await new Promise((resolve) => {
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1585
|
+
process.stdout.write(chalk.bold.yellow(' Press Enter to start the exam timer... '));
|
|
1586
|
+
const wasRaw = process.stdin.isTTY ? process.stdin.isRaw : false;
|
|
1587
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
1588
|
+
process.stdin.setRawMode(false);
|
|
1589
|
+
}
|
|
1590
|
+
const onData = (chunk) => {
|
|
1591
|
+
const s = chunk.toString();
|
|
1592
|
+
if (s.includes('\n') || s.includes('\r')) {
|
|
1593
|
+
process.stdin.removeListener('data', onData);
|
|
1594
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
1595
|
+
process.stdin.setRawMode(wasRaw);
|
|
1596
|
+
}
|
|
1597
|
+
process.stdout.write('\n');
|
|
1598
|
+
resolve();
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
process.stdin.on('data', onData);
|
|
1602
|
+
process.stdin.resume();
|
|
1570
1603
|
});
|
|
1571
1604
|
// ── Timer starts NOW ──
|
|
1572
1605
|
const confirmedAt = new Date().toISOString();
|
|
1573
1606
|
session.confirmedAt = confirmedAt;
|
|
1607
|
+
// Store the token on session so lang switching, exam submit, and
|
|
1608
|
+
// resume can reach the server without re-prompting. Device binding
|
|
1609
|
+
// means the token alone is useless on another machine, so keeping
|
|
1610
|
+
// it in exam-state.json is safe.
|
|
1611
|
+
session.token = code.trim();
|
|
1574
1612
|
saveExamState({ session, questions, answers: {}, interactions: [], aiUsage: { ai4ctf: 0, ctf4ai: 0 } });
|
|
1575
1613
|
console.log();
|
|
1576
1614
|
printSuccess('Exam started! Timer is running.');
|