icoa-cli 2.19.21 → 2.19.23
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/lang.js +45 -15
- package/dist/lib/colors.d.ts +15 -0
- package/dist/lib/colors.js +21 -0
- package/dist/lib/demo-exam.js +21 -11
- package/dist/lib/theme.js +57 -47
- package/dist/lib/ui.js +10 -5
- package/dist/types/index.d.ts +2 -0
- package/package.json +1 -1
package/dist/commands/lang.js
CHANGED
|
@@ -51,25 +51,55 @@ export function registerLangCommand(program) {
|
|
|
51
51
|
}
|
|
52
52
|
saveConfig({ language: code });
|
|
53
53
|
printSuccess(`Language set to: ${LANG_NAMES[code] || code}`);
|
|
54
|
-
// If demo
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
54
|
+
// If demo in progress, re-translate each drawn question in place using
|
|
55
|
+
// sourceNumber + sourceOrder so the user's answers and option positions are
|
|
56
|
+
// preserved. If an older state lacks these fields (pre-v2.19.22), fall back
|
|
57
|
+
// to restart-with-fresh-pick so nothing crashes.
|
|
58
58
|
const state = getExamState();
|
|
59
59
|
if (state && state.session.examId === 'demo-free') {
|
|
60
60
|
try {
|
|
61
|
-
const { pickDemoQuestions, getLocalizedDemoSession, DEMO_PICK_SIZE } = await import('../lib/demo-exam.js');
|
|
62
|
-
const freshQuestions = pickDemoQuestions(DEMO_PICK_SIZE);
|
|
63
|
-
state.questions = freshQuestions;
|
|
64
|
-
state.answers = {};
|
|
65
|
-
state.session.examName = getLocalizedDemoSession().examName;
|
|
66
|
-
state.session.startedAt = new Date().toISOString();
|
|
67
|
-
state._lastQ = 1;
|
|
61
|
+
const { pickDemoQuestions, getLocalizedDemoSession, getLocalizedDemoQuestions, getLocalizedExplanations, DEMO_PICK_SIZE, } = await import('../lib/demo-exam.js');
|
|
68
62
|
const { saveExamState } = await import('../lib/exam-state.js');
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
const canRetranslate = state.questions.every((q) => q.sourceNumber != null && Array.isArray(q.sourceOrder) && q.sourceOrder.length === 4);
|
|
64
|
+
if (canRetranslate) {
|
|
65
|
+
const pool = getLocalizedDemoQuestions();
|
|
66
|
+
const explanations = getLocalizedExplanations();
|
|
67
|
+
state.questions = state.questions.map((q) => {
|
|
68
|
+
const src = pool.find((p) => p.number === q.sourceNumber);
|
|
69
|
+
if (!src || !q.sourceOrder)
|
|
70
|
+
return q;
|
|
71
|
+
return {
|
|
72
|
+
...q,
|
|
73
|
+
text: src.text,
|
|
74
|
+
category: src.category,
|
|
75
|
+
options: {
|
|
76
|
+
A: src.options[q.sourceOrder[0]],
|
|
77
|
+
B: src.options[q.sourceOrder[1]],
|
|
78
|
+
C: src.options[q.sourceOrder[2]],
|
|
79
|
+
D: src.options[q.sourceOrder[3]],
|
|
80
|
+
},
|
|
81
|
+
explanation: explanations[q.sourceNumber],
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
state.session.examName = getLocalizedDemoSession().examName;
|
|
85
|
+
saveExamState(state);
|
|
86
|
+
const currentQ = state._lastQ || 1;
|
|
87
|
+
console.log();
|
|
88
|
+
console.log(chalk.green(` Demo continues in ${LANG_NAMES[code] || code}. Your progress is kept.`));
|
|
89
|
+
console.log(chalk.white(` Resume: exam q ${currentQ}`));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// Legacy state from before v2.19.22 — safely reset
|
|
93
|
+
state.questions = pickDemoQuestions(DEMO_PICK_SIZE);
|
|
94
|
+
state.answers = {};
|
|
95
|
+
state.session.examName = getLocalizedDemoSession().examName;
|
|
96
|
+
state.session.startedAt = new Date().toISOString();
|
|
97
|
+
state._lastQ = 1;
|
|
98
|
+
saveExamState(state);
|
|
99
|
+
console.log();
|
|
100
|
+
console.log(chalk.green(` Demo restarted in ${LANG_NAMES[code] || code}.`));
|
|
101
|
+
console.log(chalk.white(' Type: exam q 1'));
|
|
102
|
+
}
|
|
73
103
|
}
|
|
74
104
|
catch {
|
|
75
105
|
console.log(chalk.gray(' Language changed. Type: demo'));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const c: {
|
|
2
|
+
fg: (s: string) => string;
|
|
3
|
+
muted: (s: string) => string;
|
|
4
|
+
red: (s: string) => string;
|
|
5
|
+
green: (s: string) => string;
|
|
6
|
+
yellow: (s: string) => string;
|
|
7
|
+
blue: (s: string) => string;
|
|
8
|
+
cyan: (s: string) => string;
|
|
9
|
+
orange: (s: string) => string;
|
|
10
|
+
white: (s: string) => string;
|
|
11
|
+
};
|
|
12
|
+
export declare const DARCULA_BG_HEX = "#2B2B2B";
|
|
13
|
+
export declare const DARCULA_FG_HEX = "#A9B7C6";
|
|
14
|
+
export declare const DARCULA_BG_RGB: readonly [43, 43, 43];
|
|
15
|
+
export declare const DARCULA_FG_RGB: readonly [169, 183, 198];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Darcula palette — from ICOA Terminal xterm.js theme spec.
|
|
2
|
+
// Use c.* helpers when you need brand-accurate truecolor (e.g. the orange
|
|
3
|
+
// accent #CC7832). For generic success/error/warning, chalk.green/.red/.yellow
|
|
4
|
+
// remain fine — they render as the terminal's Darcula 16-color when using an
|
|
5
|
+
// ICOA theme and as close-enough defaults elsewhere.
|
|
6
|
+
const tc = (r, g, b) => (s) => `\x1b[38;2;${r};${g};${b}m${s}\x1b[39m`;
|
|
7
|
+
export const c = {
|
|
8
|
+
fg: tc(169, 183, 198), // #A9B7C6 body text
|
|
9
|
+
muted: tc(85, 85, 85), // #555555 comments / secondary
|
|
10
|
+
red: tc(255, 107, 104), // #FF6B68 error
|
|
11
|
+
green: tc(168, 192, 35), // #A8C023 success
|
|
12
|
+
yellow: tc(214, 191, 85), // #D6BF55 warning
|
|
13
|
+
blue: tc(126, 174, 241), // #7EAEF1 link
|
|
14
|
+
cyan: tc(40, 123, 222), // #287BDE command / path
|
|
15
|
+
orange: tc(204, 120, 50), // #CC7832 brand accent
|
|
16
|
+
white: tc(255, 255, 255), // #FFFFFF highlight
|
|
17
|
+
};
|
|
18
|
+
export const DARCULA_BG_HEX = '#2B2B2B';
|
|
19
|
+
export const DARCULA_FG_HEX = '#A9B7C6';
|
|
20
|
+
export const DARCULA_BG_RGB = [43, 43, 43];
|
|
21
|
+
export const DARCULA_FG_RGB = [169, 183, 198];
|
package/dist/lib/demo-exam.js
CHANGED
|
@@ -127,16 +127,24 @@ function shuffle(arr) {
|
|
|
127
127
|
}
|
|
128
128
|
return out;
|
|
129
129
|
}
|
|
130
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* Shuffle the four options within a single question; recompute the answer letter
|
|
132
|
+
* and record `sourceOrder` so the shuffle can be replayed against a different
|
|
133
|
+
* translation of the same question (used by lang-switch to keep answers).
|
|
134
|
+
*/
|
|
131
135
|
function shuffleQuestionOptions(q) {
|
|
132
136
|
if (!q.answer)
|
|
133
137
|
return q;
|
|
134
|
-
const correctText = q.options[q.answer];
|
|
135
138
|
const keys = ['A', 'B', 'C', 'D'];
|
|
136
|
-
const
|
|
137
|
-
const newOptions = {
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
const sourceOrder = shuffle(keys);
|
|
140
|
+
const newOptions = {
|
|
141
|
+
A: q.options[sourceOrder[0]],
|
|
142
|
+
B: q.options[sourceOrder[1]],
|
|
143
|
+
C: q.options[sourceOrder[2]],
|
|
144
|
+
D: q.options[sourceOrder[3]],
|
|
145
|
+
};
|
|
146
|
+
const newAnswer = keys[sourceOrder.indexOf(q.answer)];
|
|
147
|
+
return { ...q, options: newOptions, answer: newAnswer, sourceOrder };
|
|
140
148
|
}
|
|
141
149
|
/**
|
|
142
150
|
* Pick `n` random questions from the pool, shuffle each question's options,
|
|
@@ -152,12 +160,14 @@ export function pickDemoQuestions(n = DEMO_PICK_SIZE) {
|
|
|
152
160
|
answer: DEMO_ANSWERS[q.number],
|
|
153
161
|
explanation: explanations[q.number],
|
|
154
162
|
}));
|
|
155
|
-
// Shuffle pool, pick n, shuffle each question's options, renumber 1..n
|
|
163
|
+
// Shuffle pool, pick n, shuffle each question's options, renumber 1..n.
|
|
164
|
+
// Keep the original pool number as `sourceNumber` so lang-switch can re-translate
|
|
165
|
+
// this exact question without losing the user's answer.
|
|
156
166
|
const picked = shuffle(enriched).slice(0, n);
|
|
157
|
-
return picked.map((q, i) =>
|
|
158
|
-
|
|
159
|
-
number: i + 1
|
|
160
|
-
})
|
|
167
|
+
return picked.map((q, i) => {
|
|
168
|
+
const shuffled = shuffleQuestionOptions(q);
|
|
169
|
+
return { ...shuffled, sourceNumber: q.number, number: i + 1 };
|
|
170
|
+
});
|
|
161
171
|
}
|
|
162
172
|
/**
|
|
163
173
|
* Get localized explanations for demo questions.
|
package/dist/lib/theme.js
CHANGED
|
@@ -1,52 +1,62 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Unified Darcula terminal theme — works identically on macOS Terminal.app,
|
|
2
|
+
// iTerm2, GNOME Terminal, Konsole, Windows Terminal (cmd/PowerShell/WSL).
|
|
3
|
+
//
|
|
4
|
+
// Two mechanisms are combined so every modern terminal gets the best it can:
|
|
5
|
+
//
|
|
6
|
+
// 1. OSC 10/11/12 sets the terminal's *default* fg/bg/cursor colors.
|
|
7
|
+
// Honored by iTerm2, GNOME Terminal, Konsole, Windows Terminal → lossless
|
|
8
|
+
// background, no scrollback or resize artifacts. Ignored by macOS
|
|
9
|
+
// Terminal.app.
|
|
10
|
+
//
|
|
11
|
+
// 2. SGR 38/48 + \x1b[2J paints the visible grid cells with Darcula colors.
|
|
12
|
+
// Works everywhere including Terminal.app. The known limits on Terminal.app
|
|
13
|
+
// (resize edge, scrollback of pre-launch history, copy-paste carrying bg)
|
|
14
|
+
// are protocol-level and accepted; fix path is importing a .terminal profile.
|
|
15
|
+
//
|
|
16
|
+
// Legacy cmd.exe (pre-Win10 1809) can't run Node 22 anyway, so no separate
|
|
17
|
+
// fallback path is needed.
|
|
18
|
+
const OSC_INIT = '\x1b]10;#A9B7C6\x07' + // default fg
|
|
19
|
+
'\x1b]11;#2B2B2B\x07' + // default bg
|
|
20
|
+
'\x1b]12;#A9B7C6\x07'; // cursor color
|
|
21
|
+
const OSC_RESET = '\x1b]110\x07' + // reset default fg
|
|
22
|
+
'\x1b]111\x07' + // reset default bg
|
|
23
|
+
'\x1b]112\x07'; // reset cursor color
|
|
24
|
+
const SGR_INIT = '\x1b[38;2;169;183;198m' + // fg #A9B7C6
|
|
25
|
+
'\x1b[48;2;43;43;43m' + // bg #2B2B2B
|
|
26
|
+
'\x1b[2J' + // paint grid with current bg
|
|
27
|
+
'\x1b[H'; // cursor home
|
|
28
|
+
const SGR_RESET = '\x1b[0m\x1b[2J\x1b[H';
|
|
29
|
+
function supportsAnsi() {
|
|
30
|
+
if (!process.stdout.isTTY)
|
|
31
|
+
return false;
|
|
32
|
+
const depth = process.stdout.getColorDepth?.();
|
|
33
|
+
if (typeof depth === 'number')
|
|
34
|
+
return depth >= 8;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
let armed = false;
|
|
3
38
|
export function setTerminalTheme() {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
// Linux / Windows: ANSI escape codes
|
|
25
|
-
process.stdout.write('\x1b[0m\x1b[40m\x1b[38;2;0;255;65m\x1b[2J\x1b[H');
|
|
39
|
+
if (!supportsAnsi())
|
|
40
|
+
return;
|
|
41
|
+
process.stdout.write(OSC_INIT + SGR_INIT);
|
|
42
|
+
if (!armed) {
|
|
43
|
+
armed = true;
|
|
44
|
+
// Belt-and-braces cleanup on every exit path. Without these, Ctrl+C leaves
|
|
45
|
+
// the user's shell stuck with our SGR state.
|
|
46
|
+
const cleanup = () => {
|
|
47
|
+
try {
|
|
48
|
+
process.stdout.write(OSC_RESET + SGR_RESET);
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
};
|
|
52
|
+
process.on('exit', cleanup);
|
|
53
|
+
process.on('SIGINT', () => { cleanup(); process.exit(130); });
|
|
54
|
+
process.on('SIGTERM', () => { cleanup(); process.exit(143); });
|
|
55
|
+
process.on('SIGHUP', () => { cleanup(); process.exit(129); });
|
|
26
56
|
}
|
|
27
|
-
// Clear screen
|
|
28
|
-
process.stdout.write('\x1b[2J\x1b[H');
|
|
29
57
|
}
|
|
30
58
|
export function resetTerminalTheme() {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// Reset to macOS Terminal.app default profile colors
|
|
35
|
-
execSync(`osascript -e '
|
|
36
|
-
tell application "Terminal"
|
|
37
|
-
set bg to {65535, 65535, 65535}
|
|
38
|
-
set fg to {0, 0, 0}
|
|
39
|
-
set background color of selected tab of front window to bg
|
|
40
|
-
set normal text color of selected tab of front window to fg
|
|
41
|
-
set cursor color of selected tab of front window to fg
|
|
42
|
-
end tell
|
|
43
|
-
'`, { stdio: 'ignore' });
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
process.stdout.write('\x1b[0m\x1b[2J\x1b[H');
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
process.stdout.write('\x1b[0m\x1b[2J\x1b[H');
|
|
51
|
-
}
|
|
59
|
+
if (!supportsAnsi())
|
|
60
|
+
return;
|
|
61
|
+
process.stdout.write(OSC_RESET + SGR_RESET);
|
|
52
62
|
}
|
package/dist/lib/ui.js
CHANGED
|
@@ -3,18 +3,23 @@ import Table from 'cli-table3';
|
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import { Marked } from 'marked';
|
|
5
5
|
import { markedTerminal } from 'marked-terminal';
|
|
6
|
+
import { c } from './colors.js';
|
|
6
7
|
const marked = new Marked(markedTerminal());
|
|
8
|
+
// Wrap the message body in explicit Darcula fg (#A9B7C6). Without this,
|
|
9
|
+
// chalk.green('✓ ') emits \x1b[39m at the end and the unstyled msg falls
|
|
10
|
+
// back to the terminal's profile fg — which is black on macOS Terminal.app
|
|
11
|
+
// default, invisible on our forced #2B2B2B background.
|
|
7
12
|
export function printSuccess(msg) {
|
|
8
|
-
console.log(chalk.green('✓ ') + msg);
|
|
13
|
+
console.log(chalk.green('✓ ') + c.fg(msg));
|
|
9
14
|
}
|
|
10
15
|
export function printError(msg) {
|
|
11
|
-
console.log(chalk.red('✗ ') + msg);
|
|
16
|
+
console.log(chalk.red('✗ ') + c.fg(msg));
|
|
12
17
|
}
|
|
13
18
|
export function printWarning(msg) {
|
|
14
|
-
console.log(chalk.yellow('⚠ ') + msg);
|
|
19
|
+
console.log(chalk.yellow('⚠ ') + c.fg(msg));
|
|
15
20
|
}
|
|
16
21
|
export function printInfo(msg) {
|
|
17
|
-
console.log(chalk.blue('ℹ ') + msg);
|
|
22
|
+
console.log(chalk.blue('ℹ ') + c.fg(msg));
|
|
18
23
|
}
|
|
19
24
|
export function printTable(headers, rows) {
|
|
20
25
|
const table = new Table({
|
|
@@ -71,5 +76,5 @@ export function printHeader(title) {
|
|
|
71
76
|
console.log(chalk.cyan(' ' + '─'.repeat(title.length + 4)));
|
|
72
77
|
}
|
|
73
78
|
export function printKeyValue(key, value) {
|
|
74
|
-
console.log(` ${chalk.gray(key + ':')} ${value}`);
|
|
79
|
+
console.log(` ${chalk.gray(key + ':')} ${c.fg(value)}`);
|
|
75
80
|
}
|
package/dist/types/index.d.ts
CHANGED