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.
@@ -1658,7 +1658,16 @@ export function registerExamCommand(program) {
1658
1658
  return;
1659
1659
  }
1660
1660
  // Gate: require exam setup
1661
- if (!isExamSetupComplete()) {
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
- console.log(chalk.yellow('⚠ Your terminal locale is not UTF-8 (LANG=' + (envLang || '(unset)') + ').'));
121
- console.log(chalk.gray(' Non-English text and box characters may display as "?" or garbled glyphs.'));
122
- 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(')'));
123
- console.log();
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, { encoding: 'utf-8', timeout: 2000 }).trim();
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
- // Proactive Python check: only warn when it actually matters (user past demo
144
- // and heading toward exam setup). Missing or version >= 3.13 shows yellow
145
- // note pointing at `env python`. Quiet when 3.10-3.12 are installed.
146
- if (stats.attempts > 0) {
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: fully prepared. Exam entry is the primary CTA list it FIRST
184
- // and make the token format + origin obvious. Students landing here have
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
- console.log(chalk.green(' ✓ Demo completed ') + chalk.gray(`(${stats.attempts} ${plural})`));
189
- console.log(chalk.green(' ✓ Environment ready'));
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
- console.log(chalk.white(' exam setup') + chalk.gray(' Re-verify tool environment'));
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.95",
3
+ "version": "2.19.97",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {