icoa-cli 2.19.95 → 2.19.97
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 +10 -1
- package/dist/index.js +29 -4
- package/dist/lib/platform.d.ts +58 -0
- package/dist/lib/platform.js +86 -0
- package/dist/repl.js +57 -13
- package/package.json +1 -1
package/dist/commands/exam.js
CHANGED
|
@@ -1658,7 +1658,16 @@ export function registerExamCommand(program) {
|
|
|
1658
1658
|
return;
|
|
1659
1659
|
}
|
|
1660
1660
|
// Gate: require exam setup
|
|
1661
|
-
|
|
1661
|
+
// v2.19.97 — Windows cmd users bypass the exam-setup gate entirely.
|
|
1662
|
+
// Their recommended path is C paper (MCQ only, no Python / no Unix tools),
|
|
1663
|
+
// so forcing them through `exam setup` (which installs pip packages) would
|
|
1664
|
+
// be both unnecessary and likely to fail. Server enforces token→exam_id
|
|
1665
|
+
// binding (1:1 FK), so if they use a B/A token on cmd they'll still get
|
|
1666
|
+
// routed to the correct exam; but the missing tools may cost them points
|
|
1667
|
+
// on Q31-36. That's the trade-off they opted into.
|
|
1668
|
+
const { isNativeWindowsCmd: _isNativeWindowsCmd } = await import('../lib/platform.js');
|
|
1669
|
+
const cmdPath = _isNativeWindowsCmd();
|
|
1670
|
+
if (!cmdPath && !isExamSetupComplete()) {
|
|
1662
1671
|
console.log();
|
|
1663
1672
|
printWarning('Pre-exam setup required before entering a token.');
|
|
1664
1673
|
console.log(chalk.gray(' → ') + chalk.bold.cyan('exam setup'));
|
package/dist/index.js
CHANGED
|
@@ -117,10 +117,35 @@ program
|
|
|
117
117
|
// wondering why "哪个命令..." appears as "????..." on their machine.
|
|
118
118
|
const envLang = process.env.LANG || process.env.LC_ALL || process.env.LC_CTYPE || '';
|
|
119
119
|
if (!/UTF-?8/i.test(envLang)) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
if (process.platform === 'win32') {
|
|
121
|
+
// Windows cmd.exe default code page (437 US, 936 CN, etc.) mangles
|
|
122
|
+
// non-Latin script. Fix is `chcp 65001`, not Unix-style `export LANG`.
|
|
123
|
+
// Skip the warning if the code page is already 65001 (PowerShell +
|
|
124
|
+
// Windows Terminal often set it automatically).
|
|
125
|
+
let codepage = '';
|
|
126
|
+
try {
|
|
127
|
+
const { execFileSync } = await import('node:child_process');
|
|
128
|
+
codepage = execFileSync('chcp.com', [], {
|
|
129
|
+
encoding: 'utf-8',
|
|
130
|
+
timeout: 1500,
|
|
131
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
132
|
+
}).trim();
|
|
133
|
+
}
|
|
134
|
+
catch { /* chcp.com unavailable or timed out */ }
|
|
135
|
+
if (!codepage.includes('65001')) {
|
|
136
|
+
console.log(chalk.yellow('⚠ Windows terminal is not using UTF-8 (current: ' + (codepage || 'unknown') + ').'));
|
|
137
|
+
console.log(chalk.gray(' Non-English text (Ukrainian, Chinese, Japanese, etc.) may show as "?" or garbled glyphs.'));
|
|
138
|
+
console.log(chalk.gray(' Fix (run before ') + chalk.cyan('icoa') + chalk.gray('): ') + chalk.cyan('chcp 65001'));
|
|
139
|
+
console.log(chalk.gray(' Or stay in English inside the CLI: ') + chalk.cyan('lang en'));
|
|
140
|
+
console.log();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.log(chalk.yellow('⚠ Your terminal locale is not UTF-8 (LANG=' + (envLang || '(unset)') + ').'));
|
|
145
|
+
console.log(chalk.gray(' Non-English text and box characters may display as "?" or garbled glyphs.'));
|
|
146
|
+
console.log(chalk.gray(' Fix: ') + chalk.cyan('export LANG=en_US.UTF-8') + chalk.gray(' (or your locale, e.g. ') + chalk.cyan('zh_CN.UTF-8') + chalk.gray(', ') + chalk.cyan('uk_UA.UTF-8') + chalk.gray(')'));
|
|
147
|
+
console.log();
|
|
148
|
+
}
|
|
124
149
|
}
|
|
125
150
|
console.log(BANNER);
|
|
126
151
|
// If running interactively (no extra args or --resume), start REPL
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform detection for Windows cmd.exe K-12 entry path.
|
|
3
|
+
*
|
|
4
|
+
* Context: Windows middle-school students (12-14 y/o) are the expected largest
|
|
5
|
+
* cohort for ICOA 2026. Observed in-field: 4-5 students blocked by WSL install
|
|
6
|
+
* complexity. Their viable path is Windows native cmd + Node.js, which handles
|
|
7
|
+
* MCQ + AI chat perfectly but not Q31-36 (Unix grep / strings / pwntools).
|
|
8
|
+
*
|
|
9
|
+
* Decision (v2.19.97): C paper (ua-2026-c) is the cmd entry funnel — 30 MCQ
|
|
10
|
+
* only, 45 min, 70 pts, no tools. Non-cmd users get B paper (full 150 pts).
|
|
11
|
+
*
|
|
12
|
+
* This module is the single source of truth for "should we show exam setup?"
|
|
13
|
+
* and "should we route this user to C paper?". Used by src/repl.ts menu
|
|
14
|
+
* rendering and src/commands/exam.ts pre-token guards.
|
|
15
|
+
*/
|
|
16
|
+
export interface PlatformInfo {
|
|
17
|
+
platform: NodeJS.Platform;
|
|
18
|
+
isWindowsNativeCmd: boolean;
|
|
19
|
+
isInWSL: boolean;
|
|
20
|
+
hasPython: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Heuristic: true when running inside a WSL distribution. Node.js reports
|
|
24
|
+
* `process.platform === 'linux'` in that case, but WSL sets specific env vars.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isInWSL(): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* True when the user is on Windows native (cmd.exe or PowerShell), NOT a
|
|
29
|
+
* WSL distribution.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isNativeWindowsCmd(): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Probe whether any Python 3 binary is reachable. Silent — no stderr leaks
|
|
34
|
+
* to the user. Used to decide whether Q31-35 practical questions are even
|
|
35
|
+
* theoretically doable on this machine.
|
|
36
|
+
*
|
|
37
|
+
* Uses execFileSync (no shell) so probe failures don't leak cmd.exe's
|
|
38
|
+
* "not recognized as internal or external command" messages to stdout.
|
|
39
|
+
*/
|
|
40
|
+
export declare function hasPython(): boolean;
|
|
41
|
+
export declare function getPlatformInfo(): PlatformInfo;
|
|
42
|
+
/**
|
|
43
|
+
* Should the Selection-mode menu show the `exam setup` row?
|
|
44
|
+
*
|
|
45
|
+
* Windows native cmd: NO. Setup installs pip packages for Q31-35 practical
|
|
46
|
+
* which don't apply on the C-paper (cmd-recommended) path. Showing setup
|
|
47
|
+
* sends them chasing Python installs that may fail for 12-year-olds.
|
|
48
|
+
*
|
|
49
|
+
* Everyone else (macOS, Linux, WSL): YES. They can reach B-paper and setup
|
|
50
|
+
* is a legit pre-step.
|
|
51
|
+
*/
|
|
52
|
+
export declare function shouldShowExamSetup(): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Is this user recommended to take the C paper (MCQ-only) rather than B?
|
|
55
|
+
* Currently: Windows native cmd users. Future: might expand based on age
|
|
56
|
+
* or partner-country policy.
|
|
57
|
+
*/
|
|
58
|
+
export declare function shouldRecommendCPaper(): boolean;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform detection for Windows cmd.exe K-12 entry path.
|
|
3
|
+
*
|
|
4
|
+
* Context: Windows middle-school students (12-14 y/o) are the expected largest
|
|
5
|
+
* cohort for ICOA 2026. Observed in-field: 4-5 students blocked by WSL install
|
|
6
|
+
* complexity. Their viable path is Windows native cmd + Node.js, which handles
|
|
7
|
+
* MCQ + AI chat perfectly but not Q31-36 (Unix grep / strings / pwntools).
|
|
8
|
+
*
|
|
9
|
+
* Decision (v2.19.97): C paper (ua-2026-c) is the cmd entry funnel — 30 MCQ
|
|
10
|
+
* only, 45 min, 70 pts, no tools. Non-cmd users get B paper (full 150 pts).
|
|
11
|
+
*
|
|
12
|
+
* This module is the single source of truth for "should we show exam setup?"
|
|
13
|
+
* and "should we route this user to C paper?". Used by src/repl.ts menu
|
|
14
|
+
* rendering and src/commands/exam.ts pre-token guards.
|
|
15
|
+
*/
|
|
16
|
+
import { execFileSync } from 'node:child_process';
|
|
17
|
+
/**
|
|
18
|
+
* Heuristic: true when running inside a WSL distribution. Node.js reports
|
|
19
|
+
* `process.platform === 'linux'` in that case, but WSL sets specific env vars.
|
|
20
|
+
*/
|
|
21
|
+
export function isInWSL() {
|
|
22
|
+
return !!(process.env.WSL_DISTRO_NAME || process.env.WSLENV);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* True when the user is on Windows native (cmd.exe or PowerShell), NOT a
|
|
26
|
+
* WSL distribution.
|
|
27
|
+
*/
|
|
28
|
+
export function isNativeWindowsCmd() {
|
|
29
|
+
return process.platform === 'win32' && !isInWSL();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Probe whether any Python 3 binary is reachable. Silent — no stderr leaks
|
|
33
|
+
* to the user. Used to decide whether Q31-35 practical questions are even
|
|
34
|
+
* theoretically doable on this machine.
|
|
35
|
+
*
|
|
36
|
+
* Uses execFileSync (no shell) so probe failures don't leak cmd.exe's
|
|
37
|
+
* "not recognized as internal or external command" messages to stdout.
|
|
38
|
+
*/
|
|
39
|
+
export function hasPython() {
|
|
40
|
+
const probes = [
|
|
41
|
+
['python3', ['--version']],
|
|
42
|
+
['python', ['--version']],
|
|
43
|
+
['py', ['-3', '--version']],
|
|
44
|
+
];
|
|
45
|
+
for (const [bin, args] of probes) {
|
|
46
|
+
try {
|
|
47
|
+
execFileSync(bin, args, {
|
|
48
|
+
encoding: 'utf-8',
|
|
49
|
+
timeout: 1500,
|
|
50
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
51
|
+
});
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch { /* try next */ }
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
export function getPlatformInfo() {
|
|
59
|
+
return {
|
|
60
|
+
platform: process.platform,
|
|
61
|
+
isWindowsNativeCmd: isNativeWindowsCmd(),
|
|
62
|
+
isInWSL: isInWSL(),
|
|
63
|
+
hasPython: hasPython(),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Should the Selection-mode menu show the `exam setup` row?
|
|
68
|
+
*
|
|
69
|
+
* Windows native cmd: NO. Setup installs pip packages for Q31-35 practical
|
|
70
|
+
* which don't apply on the C-paper (cmd-recommended) path. Showing setup
|
|
71
|
+
* sends them chasing Python installs that may fail for 12-year-olds.
|
|
72
|
+
*
|
|
73
|
+
* Everyone else (macOS, Linux, WSL): YES. They can reach B-paper and setup
|
|
74
|
+
* is a legit pre-step.
|
|
75
|
+
*/
|
|
76
|
+
export function shouldShowExamSetup() {
|
|
77
|
+
return !isNativeWindowsCmd();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Is this user recommended to take the C paper (MCQ-only) rather than B?
|
|
81
|
+
* Currently: Windows native cmd users. Future: might expand based on age
|
|
82
|
+
* or partner-country policy.
|
|
83
|
+
*/
|
|
84
|
+
export function shouldRecommendCPaper() {
|
|
85
|
+
return isNativeWindowsCmd();
|
|
86
|
+
}
|
package/dist/repl.js
CHANGED
|
@@ -10,6 +10,7 @@ import { getExamState, getRealExamState, getDemoState } from './lib/exam-state.j
|
|
|
10
10
|
import { getDemoStats } from './lib/demo-stats.js';
|
|
11
11
|
import { isExamSetupComplete } from './lib/exam-setup.js';
|
|
12
12
|
import { DEMO_PICK_SIZE, DEMO_POOL_SIZE } from './lib/demo-exam.js';
|
|
13
|
+
import { isNativeWindowsCmd } from './lib/platform.js';
|
|
13
14
|
import { resetTerminalTheme } from './lib/theme.js';
|
|
14
15
|
import { ensureSandbox, runInSandbox, isDockerAvailable } from './lib/sandbox.js';
|
|
15
16
|
import { logCommand } from './lib/logger.js';
|
|
@@ -106,17 +107,29 @@ function checkPython() {
|
|
|
106
107
|
// Probe python3.12 first — macOS Homebrew installs python@3.12 alongside a
|
|
107
108
|
// newer default python3, and we don't want to flag a 3.12-ready machine just
|
|
108
109
|
// because the default alias moved to 3.13.
|
|
110
|
+
// On Windows (cmd/PowerShell), the binary is `python` not `python3`, and the
|
|
111
|
+
// Python launcher `py -3` is also common. Include these so Windows K-12
|
|
112
|
+
// students don't get a spurious "Python missing" warning when Python IS
|
|
113
|
+
// installed. We also stdio: ignore stderr so cmd.exe's "not recognized as
|
|
114
|
+
// internal or external command" error doesn't leak to the user.
|
|
109
115
|
const probes = [
|
|
110
116
|
'python3.12 --version',
|
|
111
117
|
'/opt/homebrew/opt/python@3.12/bin/python3.12 --version',
|
|
112
118
|
'/usr/local/opt/python@3.12/bin/python3.12 --version',
|
|
113
119
|
'python3 --version',
|
|
120
|
+
'python --version', // Windows default name
|
|
121
|
+
'py -3.12 --version', // Windows Python Launcher
|
|
122
|
+
'py -3 --version', // Windows Python Launcher (any 3.x)
|
|
114
123
|
];
|
|
115
124
|
let lastVersion = '';
|
|
116
125
|
let lastStatus = 'missing';
|
|
117
126
|
for (const cmd of probes) {
|
|
118
127
|
try {
|
|
119
|
-
const out = execSyncFn(cmd, {
|
|
128
|
+
const out = execSyncFn(cmd, {
|
|
129
|
+
encoding: 'utf-8',
|
|
130
|
+
timeout: 2000,
|
|
131
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
132
|
+
}).trim();
|
|
120
133
|
const version = out.replace('Python ', '');
|
|
121
134
|
const [maj, min] = version.split('.').map(Number);
|
|
122
135
|
if (maj === 3 && min === 12)
|
|
@@ -137,13 +150,21 @@ function printSelectionMenu() {
|
|
|
137
150
|
const stats = getDemoStats();
|
|
138
151
|
const setupDone = isExamSetupComplete();
|
|
139
152
|
const demoLine = `Free practice — ${DEMO_PICK_SIZE} questions (from pool of ${DEMO_POOL_SIZE})`;
|
|
153
|
+
// v2.19.97 — Windows cmd K-12 entry path. Skip setup prompts + Python
|
|
154
|
+
// warnings for cmd users; they're routed to C paper (MCQ-only) which
|
|
155
|
+
// needs zero external tools.
|
|
156
|
+
const cmdMode = isNativeWindowsCmd();
|
|
140
157
|
console.log();
|
|
141
158
|
console.log(' ' + chalk.cyan.bold('[Selection Mode]'));
|
|
142
159
|
console.log();
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
160
|
+
if (cmdMode) {
|
|
161
|
+
console.log(chalk.gray(' Platform: ') + chalk.white('Windows cmd.exe') + chalk.gray(' — routed to Paper C (MCQ-only, 45 min, 70 pts, zero extra tools)'));
|
|
162
|
+
console.log();
|
|
163
|
+
}
|
|
164
|
+
else if (stats.attempts > 0) {
|
|
165
|
+
// Proactive Python check only matters for non-cmd users heading toward
|
|
166
|
+
// exam setup. Missing or version >= 3.13 shows yellow note pointing at
|
|
167
|
+
// `env python`. Quiet when 3.10-3.12 are installed.
|
|
147
168
|
const py = checkPython();
|
|
148
169
|
if (py.status === 'missing') {
|
|
149
170
|
console.log(chalk.yellow(' ⚠ Python not detected. For exam practical questions:'));
|
|
@@ -166,8 +187,9 @@ function printSelectionMenu() {
|
|
|
166
187
|
console.log(chalk.white(' lang es') + chalk.gray(' Switch language (e.g. lang es, lang zh, lang fr)'));
|
|
167
188
|
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
168
189
|
}
|
|
169
|
-
else if (!setupDone) {
|
|
190
|
+
else if (!setupDone && !cmdMode) {
|
|
170
191
|
// State 1: demo done at least once, but exam environment not installed.
|
|
192
|
+
// Windows cmd users skip this state entirely — they don't need setup.
|
|
171
193
|
const plural = stats.attempts === 1 ? 'attempt' : 'attempts';
|
|
172
194
|
console.log(chalk.green(' ✓ Demo completed ') + chalk.gray(`(${stats.attempts} ${plural}${stats.bestPercentage > 0 ? ` · best ${stats.bestPercentage}%` : ''})`));
|
|
173
195
|
console.log(chalk.yellow(' → Next: prepare your environment for the real exam.'));
|
|
@@ -180,13 +202,15 @@ function printSelectionMenu() {
|
|
|
180
202
|
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
181
203
|
}
|
|
182
204
|
else {
|
|
183
|
-
// State 2:
|
|
184
|
-
//
|
|
185
|
-
// already done demo; without a concrete example they stare at "<token>"
|
|
186
|
-
// and don't know what to type or where the token comes from.
|
|
205
|
+
// State 2: ready to enter exam. For cmd users this is reached immediately
|
|
206
|
+
// (no setup gate); for others it requires exam setup completion.
|
|
187
207
|
const plural = stats.attempts === 1 ? 'attempt' : 'attempts';
|
|
188
|
-
|
|
189
|
-
|
|
208
|
+
if (stats.attempts > 0) {
|
|
209
|
+
console.log(chalk.green(' ✓ Demo completed ') + chalk.gray(`(${stats.attempts} ${plural})`));
|
|
210
|
+
}
|
|
211
|
+
if (!cmdMode) {
|
|
212
|
+
console.log(chalk.green(' ✓ Environment ready'));
|
|
213
|
+
}
|
|
190
214
|
console.log(chalk.yellow(' → Enter your exam token to begin.'));
|
|
191
215
|
console.log(chalk.gray(' (10-char code from your organizer, starts with your country code like ') + chalk.cyan('UA') + chalk.gray(' — case-insensitive)'));
|
|
192
216
|
console.log();
|
|
@@ -196,7 +220,9 @@ function printSelectionMenu() {
|
|
|
196
220
|
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
197
221
|
console.log(chalk.gray(' Other commands:'));
|
|
198
222
|
console.log(chalk.white(' demo') + chalk.gray(` ${demoLine}`));
|
|
199
|
-
|
|
223
|
+
if (!cmdMode) {
|
|
224
|
+
console.log(chalk.white(' exam setup') + chalk.gray(' Re-verify tool environment'));
|
|
225
|
+
}
|
|
200
226
|
console.log(chalk.white(' lang') + chalk.gray(' List all supported languages'));
|
|
201
227
|
console.log(chalk.white(' lang es') + chalk.gray(' Switch language (e.g. lang es, lang zh, lang fr)'));
|
|
202
228
|
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
@@ -905,6 +931,24 @@ export async function startRepl(program, resumeMode) {
|
|
|
905
931
|
.replace(/^python3?\s/, `${py12} `)
|
|
906
932
|
.replace(/^(python3|python)$/, py12);
|
|
907
933
|
}
|
|
934
|
+
else if (process.platform === 'win32') {
|
|
935
|
+
// Windows: the binary is `python` (not `python3`). Python Launcher `py -3`
|
|
936
|
+
// is also common. Rewrite `python3 xyz` → `python xyz` or `py -3 xyz` so
|
|
937
|
+
// students who followed Unix-oriented practical-question hints don't get
|
|
938
|
+
// "'python3' is not recognized". Prefer `py -3` if the launcher exists
|
|
939
|
+
// (handles multi-version installs); fall back to plain `python`.
|
|
940
|
+
const pyCmd = (() => {
|
|
941
|
+
try {
|
|
942
|
+
execSyncFn('py -3 --version', { stdio: ['ignore', 'ignore', 'ignore'], timeout: 1500 });
|
|
943
|
+
return 'py -3';
|
|
944
|
+
}
|
|
945
|
+
catch { }
|
|
946
|
+
return 'python';
|
|
947
|
+
})();
|
|
948
|
+
resolvedInput = resolvedInput
|
|
949
|
+
.replace(/^python3?(\.\d+)?\s/, `${pyCmd} `)
|
|
950
|
+
.replace(/^python3?(\.\d+)?$/, pyCmd);
|
|
951
|
+
}
|
|
908
952
|
else {
|
|
909
953
|
// Linux/WSL: python → python3 (or python3.12 if available)
|
|
910
954
|
const py12 = (() => { try {
|