icoa-cli 2.8.0 → 2.9.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.
- package/dist/commands/exam.js +168 -12
- package/dist/index.js +6 -1
- package/dist/repl.js +24 -1
- package/package.json +1 -1
package/dist/commands/exam.js
CHANGED
|
@@ -62,10 +62,20 @@ function printQuestionProgress(current, total, answered) {
|
|
|
62
62
|
console.log();
|
|
63
63
|
console.log(` ${bar} ${chalk.white.bold(`${current}`)}${chalk.gray(`/${total}`)} ${chalk.gray(`(${answered} answered)`)} ${chalk.gray(`${pct}%`)}`);
|
|
64
64
|
}
|
|
65
|
+
function getHelpState(state) {
|
|
66
|
+
return {
|
|
67
|
+
used: state._helpUsed || 0,
|
|
68
|
+
max: state._helpMax || 5,
|
|
69
|
+
perQ: state._helpPerQ || {},
|
|
70
|
+
eliminated: state._eliminated || {},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
65
73
|
function printQuestion(q, answer) {
|
|
66
74
|
const state = getExamState();
|
|
67
75
|
const total = Number(state?.session.questionCount || 30);
|
|
68
76
|
const answered = Object.keys(state?.answers || {}).length;
|
|
77
|
+
const help = getHelpState(state);
|
|
78
|
+
const eliminated = help.eliminated[q.number] || [];
|
|
69
79
|
// Progress bar
|
|
70
80
|
printQuestionProgress(q.number, total, answered);
|
|
71
81
|
// Easter egg
|
|
@@ -78,14 +88,25 @@ function printQuestion(q, answer) {
|
|
|
78
88
|
console.log(` ${q.text}`);
|
|
79
89
|
console.log();
|
|
80
90
|
for (const key of ['A', 'B', 'C', 'D']) {
|
|
91
|
+
const isEliminated = eliminated.includes(key);
|
|
81
92
|
const selected = answer === key;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
if (isEliminated) {
|
|
94
|
+
console.log(chalk.gray.strikethrough(` ${key}. ${q.options[key]}`));
|
|
95
|
+
}
|
|
96
|
+
else if (selected) {
|
|
97
|
+
console.log(chalk.green.bold(` ▸ ${key}. ${q.options[key]}`));
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
console.log(chalk.gray(` ${key}.`) + ' ' + chalk.white(q.options[key]));
|
|
101
|
+
}
|
|
85
102
|
}
|
|
86
103
|
console.log();
|
|
87
|
-
// Navigation hint
|
|
88
|
-
|
|
104
|
+
// Navigation + help hint
|
|
105
|
+
const remaining = help.max - help.used;
|
|
106
|
+
const helpHint = remaining > 0
|
|
107
|
+
? chalk.yellow('help') + chalk.gray(` (${remaining}/${help.max})`)
|
|
108
|
+
: (help.max < 8 ? chalk.gray('help 0/5 — type ') + chalk.yellow('more help') : chalk.gray('help 0/8'));
|
|
109
|
+
console.log(chalk.gray(' Type ') + chalk.white('A') + chalk.gray('/') + chalk.white('B') + chalk.gray('/') + chalk.white('C') + chalk.gray('/') + chalk.white('D') + chalk.gray(' to answer · ') + helpHint +
|
|
89
110
|
(q.number > 1 ? chalk.gray(' · ') + chalk.white('prev') : '') +
|
|
90
111
|
(q.number < total ? chalk.gray(' · ') + chalk.white('next') : ''));
|
|
91
112
|
}
|
|
@@ -262,6 +283,140 @@ export function registerExamCommand(program) {
|
|
|
262
283
|
const q = state.questions[prev - 1];
|
|
263
284
|
printQuestion(q, state.answers[q.number]);
|
|
264
285
|
});
|
|
286
|
+
// ─── exam help ───
|
|
287
|
+
exam
|
|
288
|
+
.command('help')
|
|
289
|
+
.description('Eliminate one wrong option (limited uses)')
|
|
290
|
+
.action(() => {
|
|
291
|
+
logCommand('exam help');
|
|
292
|
+
const state = getExamState();
|
|
293
|
+
if (!state) {
|
|
294
|
+
printError('No exam in progress.');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const help = getHelpState(state);
|
|
298
|
+
const currentQ = state._lastQ || 1;
|
|
299
|
+
const qHelps = help.perQ[currentQ] || 0;
|
|
300
|
+
const eliminated = help.eliminated[currentQ] || [];
|
|
301
|
+
const q = state.questions.find((qq) => qq.number === currentQ);
|
|
302
|
+
if (!q)
|
|
303
|
+
return;
|
|
304
|
+
// Find the correct answer for demo
|
|
305
|
+
let correctAnswer = null;
|
|
306
|
+
if (state.session.examId === 'demo-free') {
|
|
307
|
+
import('../lib/demo-exam.js').then(({ DEMO_ANSWERS }) => {
|
|
308
|
+
correctAnswer = DEMO_ANSWERS[currentQ];
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
// Check: already used 2 helps on this question
|
|
312
|
+
if (qHelps >= 2) {
|
|
313
|
+
console.log();
|
|
314
|
+
console.log(chalk.yellow(' 🙈 I can\'t help you more on this one!'));
|
|
315
|
+
console.log(chalk.gray(' You already have a 50/50 — trust your instinct!'));
|
|
316
|
+
console.log();
|
|
317
|
+
printQuestion(q, state.answers[q.number]);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// Check: only 1 option left (shouldn't happen with max 2 helps, but safety)
|
|
321
|
+
const remaining = ['A', 'B', 'C', 'D'].filter((k) => !eliminated.includes(k));
|
|
322
|
+
if (remaining.length <= 2) {
|
|
323
|
+
console.log();
|
|
324
|
+
console.log(chalk.yellow(' 😄 Only 2 options left — you got this!'));
|
|
325
|
+
console.log();
|
|
326
|
+
printQuestion(q, state.answers[q.number]);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
// Check budget
|
|
330
|
+
if (help.used >= help.max) {
|
|
331
|
+
if (help.max < 8) {
|
|
332
|
+
console.log();
|
|
333
|
+
console.log(chalk.yellow(` Help used: ${help.used}/${help.max}`));
|
|
334
|
+
console.log(chalk.white(' Need more? Type: ') + chalk.bold.yellow('more help') + chalk.gray(' (+3 bonus uses)'));
|
|
335
|
+
console.log();
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
console.log();
|
|
339
|
+
console.log(chalk.gray(` All help used (${help.used}/${help.max}). You\'re on your own! 💪`));
|
|
340
|
+
console.log();
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
// Eliminate one wrong option (async for demo answers)
|
|
345
|
+
const doEliminate = (correct) => {
|
|
346
|
+
const wrongRemaining = remaining.filter((k) => k !== correct && !eliminated.includes(k));
|
|
347
|
+
if (wrongRemaining.length === 0)
|
|
348
|
+
return;
|
|
349
|
+
// Pick random wrong option to eliminate
|
|
350
|
+
const toRemove = wrongRemaining[Math.floor(Math.random() * wrongRemaining.length)];
|
|
351
|
+
eliminated.push(toRemove);
|
|
352
|
+
// Update state
|
|
353
|
+
if (!help.eliminated[currentQ])
|
|
354
|
+
help.eliminated[currentQ] = [];
|
|
355
|
+
help.eliminated[currentQ] = eliminated;
|
|
356
|
+
help.perQ[currentQ] = qHelps + 1;
|
|
357
|
+
help.used++;
|
|
358
|
+
state._helpUsed = help.used;
|
|
359
|
+
state._helpMax = help.max;
|
|
360
|
+
state._helpPerQ = help.perQ;
|
|
361
|
+
state._eliminated = help.eliminated;
|
|
362
|
+
saveExamState(state);
|
|
363
|
+
console.log();
|
|
364
|
+
console.log(chalk.yellow(` 💡 Option ${toRemove} eliminated!`) + chalk.gray(` (help ${help.used}/${help.max})`));
|
|
365
|
+
printQuestion(q, state.answers[q.number]);
|
|
366
|
+
};
|
|
367
|
+
if (state.session.examId === 'demo-free') {
|
|
368
|
+
import('../lib/demo-exam.js').then(({ DEMO_ANSWERS }) => {
|
|
369
|
+
doEliminate(DEMO_ANSWERS[currentQ]);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
// For server exams, we don't know the answer — eliminate random non-selected option
|
|
374
|
+
const userAnswer = state.answers[currentQ];
|
|
375
|
+
const candidates = remaining.filter((k) => k !== userAnswer);
|
|
376
|
+
const toRemove = candidates[Math.floor(Math.random() * candidates.length)];
|
|
377
|
+
eliminated.push(toRemove);
|
|
378
|
+
if (!help.eliminated[currentQ])
|
|
379
|
+
help.eliminated[currentQ] = [];
|
|
380
|
+
help.eliminated[currentQ] = eliminated;
|
|
381
|
+
help.perQ[currentQ] = qHelps + 1;
|
|
382
|
+
help.used++;
|
|
383
|
+
state._helpUsed = help.used;
|
|
384
|
+
state._helpMax = help.max;
|
|
385
|
+
state._helpPerQ = help.perQ;
|
|
386
|
+
state._eliminated = help.eliminated;
|
|
387
|
+
saveExamState(state);
|
|
388
|
+
console.log();
|
|
389
|
+
console.log(chalk.yellow(` 💡 Option ${toRemove} eliminated!`) + chalk.gray(` (help ${help.used}/${help.max})`));
|
|
390
|
+
printQuestion(q, state.answers[q.number]);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
// ─── exam more-help ───
|
|
394
|
+
exam
|
|
395
|
+
.command('more-help')
|
|
396
|
+
.description('Unlock 3 bonus help uses')
|
|
397
|
+
.action(() => {
|
|
398
|
+
logCommand('exam more-help');
|
|
399
|
+
const state = getExamState();
|
|
400
|
+
if (!state) {
|
|
401
|
+
printError('No exam in progress.');
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const help = getHelpState(state);
|
|
405
|
+
if (help.max >= 8) {
|
|
406
|
+
console.log(chalk.gray(' Bonus help already unlocked.'));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (help.used < help.max) {
|
|
410
|
+
console.log(chalk.gray(` You still have ${help.max - help.used} helps remaining. Use them first!`));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
state._helpMax = 8;
|
|
414
|
+
saveExamState(state);
|
|
415
|
+
console.log();
|
|
416
|
+
console.log(chalk.green(' 🎁 +3 bonus help unlocked! (3/8 remaining)'));
|
|
417
|
+
console.log(chalk.gray(' Type "help" on any question to use.'));
|
|
418
|
+
console.log();
|
|
419
|
+
});
|
|
265
420
|
// ─── exam answer <n> <choice> ───
|
|
266
421
|
exam
|
|
267
422
|
.command('answer <n> <choice>')
|
|
@@ -498,17 +653,18 @@ export function registerExamCommand(program) {
|
|
|
498
653
|
console.log();
|
|
499
654
|
printHeader('ICOA Demo Exam — Free Practice');
|
|
500
655
|
console.log();
|
|
501
|
-
console.log(chalk.white('
|
|
502
|
-
console.log(chalk.white(' 30 questions · 30 minutes ·
|
|
503
|
-
console.log(chalk.white(' No account or login required.'));
|
|
656
|
+
console.log(chalk.white(' Free practice exam · No account needed'));
|
|
657
|
+
console.log(chalk.white(' 30 questions · 30 minutes · Single choice'));
|
|
504
658
|
console.log();
|
|
505
659
|
console.log(chalk.gray(' ┌─────────────────────────────────────────────────┐'));
|
|
506
|
-
console.log(chalk.gray(' │') + chalk.
|
|
507
|
-
console.log(chalk.gray(' │') + chalk.white('
|
|
508
|
-
console.log(chalk.gray(' │') + chalk.white('
|
|
509
|
-
console.log(chalk.gray(' │') + chalk.white('
|
|
660
|
+
console.log(chalk.gray(' │') + chalk.white(' Each question has 4 options (A/B/C/D) ') + chalk.gray('│'));
|
|
661
|
+
console.log(chalk.gray(' │') + chalk.white(' Type a letter to answer the current question ') + chalk.gray('│'));
|
|
662
|
+
console.log(chalk.gray(' │') + chalk.white(' Type ') + chalk.yellow('help') + chalk.white(' to eliminate a wrong option (5 uses) ') + chalk.gray('│'));
|
|
663
|
+
console.log(chalk.gray(' │') + chalk.white(' Type ') + chalk.yellow('next') + chalk.white('/') + chalk.yellow('prev') + chalk.white(' to navigate between questions ') + chalk.gray('│'));
|
|
510
664
|
console.log(chalk.gray(' └─────────────────────────────────────────────────┘'));
|
|
511
665
|
console.log();
|
|
666
|
+
console.log(chalk.gray(' For real exams, your proctor will provide credentials.'));
|
|
667
|
+
console.log();
|
|
512
668
|
const proceed = await confirm({
|
|
513
669
|
message: chalk.white('Start demo exam now?'),
|
|
514
670
|
default: true,
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,11 @@ import { registerExamCommand } from './commands/exam.js';
|
|
|
17
17
|
import { getConfig, saveConfig } from './lib/config.js';
|
|
18
18
|
import { startRepl } from './repl.js';
|
|
19
19
|
import { setTerminalTheme } from './lib/theme.js';
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const PKG_VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')).version;
|
|
20
25
|
const LINE = chalk.cyan(' ─────────────────────────────────────────────────────');
|
|
21
26
|
const BANNER = `
|
|
22
27
|
${LINE}
|
|
@@ -38,7 +43,7 @@ ${LINE}
|
|
|
38
43
|
${chalk.white('Sydney, Australia')} ${chalk.gray('Jun 27 - Jul 2, 2026')}
|
|
39
44
|
${chalk.cyan.underline('https://icoa2026.au')}
|
|
40
45
|
|
|
41
|
-
${chalk.gray(
|
|
46
|
+
${chalk.gray(`CLI-Native Competition Terminal v${PKG_VERSION}`)}
|
|
42
47
|
|
|
43
48
|
${LINE}
|
|
44
49
|
`;
|
package/dist/repl.js
CHANGED
|
@@ -237,12 +237,35 @@ export async function startRepl(program, resumeMode) {
|
|
|
237
237
|
realExit(0);
|
|
238
238
|
return;
|
|
239
239
|
}
|
|
240
|
-
// Help
|
|
240
|
+
// Help — during exam, route to exam help; otherwise show REPL help
|
|
241
241
|
if (input === 'help' || input === '?') {
|
|
242
|
+
if (getExamState()) {
|
|
243
|
+
processing = true;
|
|
244
|
+
try {
|
|
245
|
+
await program.parseAsync(['node', 'icoa', 'exam', 'help']);
|
|
246
|
+
}
|
|
247
|
+
catch { }
|
|
248
|
+
processing = false;
|
|
249
|
+
rl.prompt();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
242
252
|
printReplHelp(isActivated(), mode);
|
|
243
253
|
rl.prompt();
|
|
244
254
|
return;
|
|
245
255
|
}
|
|
256
|
+
// "more help" — during exam, unlock bonus helps
|
|
257
|
+
if (input.toLowerCase() === 'more help') {
|
|
258
|
+
if (getExamState()) {
|
|
259
|
+
processing = true;
|
|
260
|
+
try {
|
|
261
|
+
await program.parseAsync(['node', 'icoa', 'exam', 'more-help']);
|
|
262
|
+
}
|
|
263
|
+
catch { }
|
|
264
|
+
processing = false;
|
|
265
|
+
rl.prompt();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
246
269
|
// Clear
|
|
247
270
|
if (input === 'clear' || input === 'cls') {
|
|
248
271
|
console.clear();
|