icoa-cli 2.19.88 → 2.19.90

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.
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `theme` command — toggle terminal color scheme.
3
+ *
4
+ * Two variants:
5
+ * - `dark` (default): Darcula — #2B2B2B bg + #A9B7C6 fg, easy on the eyes.
6
+ * - `high-contrast`: pure black bg + pure white fg for low-vision users,
7
+ * projectors, and cheap LCDs where Darcula's grays disappear.
8
+ *
9
+ * Change applies on next `icoa` launch (the current session keeps its paint
10
+ * to avoid flicker while students are mid-question).
11
+ */
12
+ import { Command } from 'commander';
13
+ export declare function registerThemeCommand(program: Command): void;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * `theme` command — toggle terminal color scheme.
3
+ *
4
+ * Two variants:
5
+ * - `dark` (default): Darcula — #2B2B2B bg + #A9B7C6 fg, easy on the eyes.
6
+ * - `high-contrast`: pure black bg + pure white fg for low-vision users,
7
+ * projectors, and cheap LCDs where Darcula's grays disappear.
8
+ *
9
+ * Change applies on next `icoa` launch (the current session keeps its paint
10
+ * to avoid flicker while students are mid-question).
11
+ */
12
+ import chalk from 'chalk';
13
+ import { getConfig, saveConfig } from '../lib/config.js';
14
+ import { logCommand } from '../lib/logger.js';
15
+ const VALID = ['dark', 'high-contrast'];
16
+ export function registerThemeCommand(program) {
17
+ program
18
+ .command('theme [variant]')
19
+ .description('Switch color scheme (dark | high-contrast)')
20
+ .action((variant) => {
21
+ logCommand(`theme ${variant || ''}`);
22
+ const config = getConfig();
23
+ const current = config.themeVariant === 'high-contrast' ? 'high-contrast' : 'dark';
24
+ if (!variant) {
25
+ console.log();
26
+ console.log(chalk.gray(' Current theme: ') + chalk.white(current));
27
+ console.log();
28
+ console.log(chalk.gray(' Available themes:'));
29
+ console.log(' ' + chalk.white('dark ') + chalk.gray('Darcula — gray on dark gray (default)'));
30
+ console.log(' ' + chalk.white('high-contrast ') + chalk.gray('Pure white on pure black — low vision / projectors'));
31
+ console.log();
32
+ console.log(chalk.gray(' Usage: ') + chalk.cyan('theme <name>'));
33
+ console.log(chalk.gray(' Applies on next ') + chalk.cyan('icoa') + chalk.gray(' launch.'));
34
+ console.log();
35
+ return;
36
+ }
37
+ if (!VALID.includes(variant)) {
38
+ console.log();
39
+ console.log(chalk.red(` Unknown theme: ${variant}`));
40
+ console.log(chalk.gray(' Valid: ') + chalk.white(VALID.join(', ')));
41
+ console.log();
42
+ return;
43
+ }
44
+ saveConfig({ themeVariant: variant });
45
+ console.log();
46
+ console.log(chalk.green(` ✓ Theme set to: ${variant}`));
47
+ console.log(chalk.gray(' Restart ') + chalk.cyan('icoa') + chalk.gray(' to see the new colors.'));
48
+ console.log();
49
+ });
50
+ }
@@ -112,6 +112,9 @@ export async function runTutorial() {
112
112
  console.log(' ' + chalk.cyan('demo') + chalk.gray(' — free practice (10 questions, no timer)'));
113
113
  console.log(' ' + chalk.cyan('exam <token>') + chalk.gray(' — real exam (when you have an organizer-issued token)'));
114
114
  console.log(' ' + chalk.cyan('lang es') + chalk.gray(' — switch UI language (17 supported)'));
115
+ console.log(' ' + chalk.cyan('theme high-contrast') + chalk.gray(' — low-vision-friendly colors (restart after)'));
116
+ console.log();
117
+ console.log(chalk.gray(' Full beginner guide: ') + chalk.cyan.underline('https://github.com/newaipanda/ICOA_CLI/blob/main/docs/getting-started-k12.md'));
115
118
  console.log();
116
119
  }
117
120
  export function registerTutorialCommand(program) {
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ import { registerAi4ctfCommand } from './commands/ai4ctf.js';
16
16
  import { registerExamCommand } from './commands/exam.js';
17
17
  import { registerCtf4aiDemoCommand } from './commands/ctf4ai-demo.js';
18
18
  import { registerTutorialCommand } from './commands/tutorial.js';
19
+ import { registerThemeCommand } from './commands/theme.js';
19
20
  import { getConfig, saveConfig } from './lib/config.js';
20
21
  import { startRepl } from './repl.js';
21
22
  import { setTerminalTheme } from './lib/theme.js';
@@ -98,12 +99,17 @@ async function pauseWithSkip(ms) {
98
99
  const program = new Command();
99
100
  program
100
101
  .name('icoa')
101
- .version('1.2.0')
102
+ // T4-X2: version now reads live from package.json (PKG_VERSION).
103
+ // Was hardcoded to '1.2.0' which made `icoa --version` misreport on every
104
+ // release — bug since v1.2.0 era, finally fixed.
105
+ .version(PKG_VERSION)
102
106
  .description('ICOA CLI — CLI-Native CTF Competition Terminal')
103
107
  .option('--resume', 'Resume previous session')
104
108
  .action(async (opts) => {
105
- // Force hacker theme: black background + green text
106
- setTerminalTheme();
109
+ // Terminal theme Darcula by default, 'high-contrast' for low-vision users.
110
+ // Variant stored in config.themeVariant; toggled via `theme` REPL command.
111
+ const cfg = getConfig();
112
+ setTerminalTheme(cfg.themeVariant === 'high-contrast' ? 'high-contrast' : 'dark');
107
113
  checkForUpdates();
108
114
  // T2-7: UTF-8 locale sanity check. Non-UTF-8 terminals mangle the box-
109
115
  // drawing banner, the ✓/✗/⚠ glyphs used throughout the CLI, and any
@@ -144,6 +150,7 @@ registerAi4ctfCommand(program);
144
150
  registerExamCommand(program);
145
151
  registerCtf4aiDemoCommand(program);
146
152
  registerTutorialCommand(program);
153
+ registerThemeCommand(program);
147
154
  // Hidden command: switch AI model
148
155
  program
149
156
  .command('model', { hidden: true })
@@ -1,2 +1,3 @@
1
- export declare function setTerminalTheme(): void;
1
+ export type ThemeVariant = 'dark' | 'high-contrast';
2
+ export declare function setTerminalTheme(variant?: ThemeVariant): void;
2
3
  export declare function resetTerminalTheme(): void;
package/dist/lib/theme.js CHANGED
@@ -19,20 +19,35 @@
19
19
  //
20
20
  // Legacy cmd.exe (pre-Win10 1809) can't run Node 22 anyway, so no separate
21
21
  // fallback path is needed.
22
- const OSC_INIT = '\x1b]10;#A9B7C6\x07' + // default fg
22
+ const OSC_INIT_DARK = '\x1b]10;#A9B7C6\x07' + // default fg
23
23
  '\x1b]11;#2B2B2B\x07' + // default bg
24
24
  '\x1b]12;#A9B7C6\x07'; // cursor color
25
+ // High-contrast: pure black bg + pure white fg. For students with low vision
26
+ // or screens where Darcula's subtle grays wash out (e.g., projectors, cheap
27
+ // LCDs under fluorescent light). Still works with existing chalk colors —
28
+ // cyan/green/yellow/red all show up clearly against pure black.
29
+ const OSC_INIT_HC = '\x1b]10;#FFFFFF\x07' +
30
+ '\x1b]11;#000000\x07' +
31
+ '\x1b]12;#FFFFFF\x07';
25
32
  const OSC_RESET = '\x1b]110\x07' + // reset default fg
26
33
  '\x1b]111\x07' + // reset default bg
27
34
  '\x1b]112\x07'; // reset cursor color
28
- const SGR_INIT_TRUECOLOR = '\x1b[38;2;169;183;198m' + // fg #A9B7C6
35
+ const SGR_INIT_TRUECOLOR_DARK = '\x1b[38;2;169;183;198m' + // fg #A9B7C6
29
36
  '\x1b[48;2;43;43;43m' + // bg #2B2B2B
30
37
  '\x1b[2J' +
31
38
  '\x1b[H';
32
- const SGR_INIT_256 = '\x1b[38;5;250m' + // fg ≈ #BCBCBC
39
+ const SGR_INIT_256_DARK = '\x1b[38;5;250m' + // fg ≈ #BCBCBC
33
40
  '\x1b[48;5;235m' + // bg ≈ #262626
34
41
  '\x1b[2J' +
35
42
  '\x1b[H';
43
+ const SGR_INIT_TRUECOLOR_HC = '\x1b[38;2;255;255;255m' + // fg pure white
44
+ '\x1b[48;2;0;0;0m' + // bg pure black
45
+ '\x1b[2J' +
46
+ '\x1b[H';
47
+ const SGR_INIT_256_HC = '\x1b[38;5;231m' + // fg white (231 = pure white in 256)
48
+ '\x1b[48;5;16m' + // bg black (16 = pure black in 256)
49
+ '\x1b[2J' +
50
+ '\x1b[H';
36
51
  const SGR_RESET = '\x1b[0m\x1b[2J\x1b[H';
37
52
  function supportsAnsi() {
38
53
  if (!process.stdout.isTTY)
@@ -58,13 +73,16 @@ function isAppleTerminal() {
58
73
  return process.env.TERM_PROGRAM === 'Apple_Terminal';
59
74
  }
60
75
  let armed = false;
61
- export function setTerminalTheme() {
76
+ export function setTerminalTheme(variant = 'dark') {
62
77
  if (!supportsAnsi())
63
78
  return;
64
79
  if (isIcoaTerminal())
65
80
  return; // host is already Darcula; nothing to do
66
- const sgr = isAppleTerminal() ? SGR_INIT_256 : SGR_INIT_TRUECOLOR;
67
- process.stdout.write(OSC_INIT + sgr);
81
+ const osc = variant === 'high-contrast' ? OSC_INIT_HC : OSC_INIT_DARK;
82
+ const sgr = isAppleTerminal()
83
+ ? (variant === 'high-contrast' ? SGR_INIT_256_HC : SGR_INIT_256_DARK)
84
+ : (variant === 'high-contrast' ? SGR_INIT_TRUECOLOR_HC : SGR_INIT_TRUECOLOR_DARK);
85
+ process.stdout.write(osc + sgr);
68
86
  if (!armed) {
69
87
  armed = true;
70
88
  // Belt-and-braces cleanup on every exit path. Without these, Ctrl+C leaves
package/dist/repl.js CHANGED
@@ -426,6 +426,9 @@ export async function startRepl(program, resumeMode) {
426
426
  console.log(chalk.white(' scoreboard') + chalk.gray(' Live rankings'));
427
427
  console.log(chalk.white(' help') + chalk.gray(' Full command list'));
428
428
  console.log(chalk.gray(' ─────────────────────────────────────────────'));
429
+ // T4-X1: parity with Selection-mode footer — give Olympiad users the
430
+ // same beginner escape-hatch hint so newcomers don't get stuck.
431
+ console.log(chalk.gray(' Tip: ') + chalk.cyan('help') + chalk.gray(' · ') + chalk.cyan('tutorial') + chalk.gray(' 30-sec tour · ') + chalk.cyan('Ctrl+C') + chalk.gray(' pauses · ') + chalk.cyan('quit') + chalk.gray(' closes'));
429
432
  console.log();
430
433
  }
431
434
  else if (activated) {
@@ -440,8 +443,9 @@ export async function startRepl(program, resumeMode) {
440
443
  console.log(chalk.white(' Step 2 ') + chalk.bold.cyan('challenges') + chalk.gray(' Browse & solve challenges'));
441
444
  console.log(chalk.white(' Step 3 ') + chalk.bold.cyan('hint') + chalk.gray(' Ask AI when stuck'));
442
445
  console.log();
443
- console.log(chalk.gray(' Also: ') + chalk.white('env') + chalk.gray(' check tools ') + chalk.white('help') + chalk.gray(' all commands'));
446
+ console.log(chalk.gray(' Also: ') + chalk.white('env') + chalk.gray(' check tools ') + chalk.white('help') + chalk.gray(' all commands ') + chalk.white('tutorial') + chalk.gray(' 30-sec tour'));
444
447
  console.log(chalk.gray(' ─────────────────────────────────────────────'));
448
+ console.log(chalk.gray(' Tip: ') + chalk.cyan('Ctrl+C') + chalk.gray(' pauses · ') + chalk.cyan('exit') + chalk.gray(' → menu · ') + chalk.cyan('quit') + chalk.gray(' closes CLI'));
445
449
  console.log();
446
450
  }
447
451
  else {
@@ -457,7 +461,9 @@ export async function startRepl(program, resumeMode) {
457
461
  console.log(chalk.white(' ref web') + chalk.gray(' Quick reference for Web'));
458
462
  console.log(chalk.white(' env') + chalk.gray(' Check your tools'));
459
463
  console.log(chalk.white(' help') + chalk.gray(' All available commands'));
464
+ console.log(chalk.white(' tutorial') + chalk.gray(' 30-second walkthrough for first-timers'));
460
465
  console.log(chalk.gray(' ─────────────────────────────────────────────'));
466
+ console.log(chalk.gray(' Tip: ') + chalk.cyan('Ctrl+C') + chalk.gray(' pauses · ') + chalk.cyan('exit') + chalk.gray(' → menu · ') + chalk.cyan('quit') + chalk.gray(' closes CLI'));
461
467
  console.log();
462
468
  }
463
469
  }
@@ -971,7 +977,30 @@ export async function startRepl(program, resumeMode) {
971
977
  // Command tried to exit — continue REPL
972
978
  }
973
979
  else if (msg.includes('commander.unknownCommand')) {
974
- console.log(chalk.yellow(` Unknown command: ${cmd}. Type 'help' for commands.`));
980
+ // T4-12: typo suggestion via Levenshtein distance. Shorten the
981
+ // "unknown command" frustration by pointing to the likely-intended
982
+ // command when the user's input is within 2 edits of a real one.
983
+ const { distance } = await import('fastest-levenshtein');
984
+ const KNOWN_CMDS = [
985
+ 'ctf', 'hint', 'hint-b', 'hint-c', 'hint-budget', 'ref', 'shell',
986
+ 'files', 'connect', 'note', 'log', 'lang', 'setup', 'env',
987
+ 'ai4ctf', 'exam', 'ctf4ai', 'tutorial', 'theme',
988
+ 'clear', 'cls', 'quit', 'exit', 'back', 'menu', 'help',
989
+ 'continue', 'activate', 'demo', 'challenges', 'status', 'scoreboard',
990
+ 'join', 'logout',
991
+ ];
992
+ const firstWord = cmd.split(/\s+/)[0] || cmd;
993
+ let best = { word: '', dist: Infinity };
994
+ for (const known of KNOWN_CMDS) {
995
+ const d = distance(firstWord.toLowerCase(), known);
996
+ if (d < best.dist)
997
+ best = { word: known, dist: d };
998
+ }
999
+ console.log(chalk.yellow(` Unknown command: ${cmd}.`));
1000
+ if (best.dist > 0 && best.dist <= 2) {
1001
+ console.log(chalk.gray(' Did you mean: ') + chalk.bold.cyan(best.word) + chalk.gray('?'));
1002
+ }
1003
+ console.log(chalk.gray(' Type ') + chalk.cyan('help') + chalk.gray(' for the full command list.'));
975
1004
  }
976
1005
  else if (msg.includes('commander.')) {
977
1006
  // Internal Commander errors — ignore
@@ -106,6 +106,7 @@ export interface IcoaConfig {
106
106
  country: string;
107
107
  mode: IcoaMode | '';
108
108
  demoIntroSeen: boolean;
109
+ themeVariant?: 'dark' | 'high-contrast';
109
110
  }
110
111
  export type CompetitionState = 'pre_competition' | 'demo' | 'live' | 'finished' | 'unknown';
111
112
  export type IcoaMode = 'selection' | 'olympiad' | 'organizer';
@@ -33,5 +33,6 @@ export const DEFAULT_CONFIG = {
33
33
  country: '',
34
34
  mode: '',
35
35
  demoIntroSeen: false,
36
+ themeVariant: 'dark',
36
37
  };
37
38
  export const SUPPORTED_LANGUAGES = ['en', 'zh', 'ja', 'ko', 'es', 'ar', 'fr', 'pt', 'ru', 'hi', 'de', 'id', 'th', 'vi', 'tr', 'uk', 'ht'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.88",
3
+ "version": "2.19.90",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,7 @@
32
32
  "chalk": "^5.4.1",
33
33
  "cli-table3": "^0.6.5",
34
34
  "commander": "^13.1.0",
35
+ "fastest-levenshtein": "^1.0.16",
35
36
  "marked": "^15.0.7",
36
37
  "marked-terminal": "^7.3.0",
37
38
  "ora": "^8.2.0"