icoa-cli 2.19.74 → 2.19.76

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.
Files changed (2) hide show
  1. package/dist/repl.js +81 -6
  2. package/package.json +1 -1
package/dist/repl.js CHANGED
@@ -17,6 +17,55 @@ import { startLogSync, stopLogSync } from './lib/log-sync.js';
17
17
  import { existsSync, mkdirSync } from 'node:fs';
18
18
  import { join } from 'node:path';
19
19
  import { homedir } from 'node:os';
20
+ // Pre-import commonly-used CTF modules on interactive Python startup.
21
+ // Failures are silent — selectors import only what's available so a partial
22
+ // exam setup doesn't crash the shell. Creates ~/.icoa/python-startup.py once.
23
+ const PYTHON_STARTUP_CONTENT = `# ICOA exam interactive startup — auto-loaded by PYTHONSTARTUP
24
+ import base64, struct, hashlib, re, json, os, sys, binascii
25
+ try: import requests
26
+ except ImportError: pass
27
+ try: from Crypto.Cipher import AES
28
+ except ImportError: pass
29
+ try: from Crypto.Util.Padding import pad, unpad
30
+ except ImportError: pass
31
+ try: from pwn import xor, p32, u32, p64, u64
32
+ except ImportError: pass
33
+ try: import bs4
34
+ except ImportError: pass
35
+ try: import numpy as np
36
+ except ImportError: pass
37
+ `;
38
+ function ensurePythonStartup() {
39
+ const icoaDir = join(homedir(), '.icoa');
40
+ if (!existsSync(icoaDir))
41
+ mkdirSync(icoaDir, { recursive: true });
42
+ const path = join(icoaDir, 'python-startup.py');
43
+ if (!existsSync(path)) {
44
+ const { writeFileSync } = require('node:fs');
45
+ writeFileSync(path, PYTHON_STARTUP_CONTENT);
46
+ }
47
+ return path;
48
+ }
49
+ function printPythonBanner() {
50
+ console.log();
51
+ console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
52
+ console.log(chalk.bold.white(' Python ready — ICOA exam toolkit pre-loaded'));
53
+ console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
54
+ console.log();
55
+ console.log(chalk.white(' Already imported: ') + chalk.gray('base64, struct, hashlib, re, json, binascii'));
56
+ console.log(chalk.white(' Also available: ') + chalk.gray('requests, bs4, numpy, AES, pad/unpad, xor, p32/u32/p64/u64'));
57
+ console.log();
58
+ console.log(chalk.yellow(' Quick examples:'));
59
+ console.log(chalk.gray(' base64.b64decode("aGVsbG8=") ') + chalk.gray('# decode base64'));
60
+ console.log(chalk.gray(' bytes.fromhex("48656c6c6f") ') + chalk.gray('# hex → bytes'));
61
+ console.log(chalk.gray(' "ICOA{x}".encode() ') + chalk.gray('# str → bytes'));
62
+ console.log(chalk.gray(' [chr(c) for c in [73,67,79,65]] ') + chalk.gray('# ASCII codes'));
63
+ console.log(chalk.gray(' xor(bytes.fromhex("0a2b"), b"IC") ') + chalk.gray('# pwntools XOR'));
64
+ console.log();
65
+ console.log(chalk.gray(' Exit: ') + chalk.white('exit()') + chalk.gray(' or Ctrl-D'));
66
+ console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
67
+ console.log();
68
+ }
20
69
  // Compute the REPL prompt based on current state.
21
70
  // Real exam wins over demo (matches getExamState() priority). Chat modes have
22
71
  // their own prompts and are handled separately.
@@ -792,6 +841,16 @@ export async function startRepl(program, resumeMode) {
792
841
  }
793
842
  // Ensure workspace directory
794
843
  const cwd = ensureWorkspace();
844
+ // Interactive Python (no script args, no -c): print welcome banner and
845
+ // pre-import common CTF modules via a startup file. Makes entry painless
846
+ // for contestants — they land in a shell with base64/struct/hashlib/pwn
847
+ // already imported and see a 4-line cheat sheet of common one-liners.
848
+ const isInteractivePython = /^(\S*python3?(\.\d+)?)\s*$/.test(resolvedInput);
849
+ if (isInteractivePython) {
850
+ const startupPath = ensurePythonStartup();
851
+ resolvedInput = `PYTHONSTARTUP="${startupPath}" ${resolvedInput}`;
852
+ printPythonBanner();
853
+ }
795
854
  // Route to Docker sandbox if available, otherwise system shell (in workspace)
796
855
  processing = true;
797
856
  try {
@@ -873,20 +932,36 @@ export async function startRepl(program, resumeMode) {
873
932
  }
874
933
  function runSystemCommand(input, rl, cwd) {
875
934
  return new Promise((resolve) => {
935
+ // Fully release the TTY so interactive children (python3, bash, etc.)
936
+ // get a clean terminal with proper echo. `rl.pause()` alone doesn't
937
+ // reset raw mode or release stdin — readline keeps it in line-editing
938
+ // mode where typed characters don't display until our REPL reads a line.
939
+ const stdin = process.stdin;
940
+ const wasRaw = stdin.isTTY ? !!stdin.isRaw : false;
876
941
  rl.pause();
942
+ if (stdin.isTTY && typeof stdin.setRawMode === 'function') {
943
+ try {
944
+ stdin.setRawMode(false);
945
+ }
946
+ catch { }
947
+ }
877
948
  const child = spawn(input, {
878
949
  shell: true,
879
950
  stdio: 'inherit',
880
951
  cwd: cwd || process.cwd(),
881
952
  });
882
- child.on('close', () => {
883
- rl.resume();
884
- resolve();
885
- });
886
- child.on('error', () => {
953
+ const restore = () => {
954
+ if (stdin.isTTY && typeof stdin.setRawMode === 'function' && wasRaw) {
955
+ try {
956
+ stdin.setRawMode(true);
957
+ }
958
+ catch { }
959
+ }
887
960
  rl.resume();
888
961
  resolve();
889
- });
962
+ };
963
+ child.on('close', restore);
964
+ child.on('error', restore);
890
965
  });
891
966
  }
892
967
  function mapCommand(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.74",
3
+ "version": "2.19.76",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {