icoa-cli 1.5.2 → 1.6.1

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/index.js CHANGED
@@ -14,40 +14,31 @@ import { registerSetupCommand } from './commands/setup.js';
14
14
  import { registerEnvCommand } from './commands/env.js';
15
15
  import { getConfig, saveConfig } from './lib/config.js';
16
16
  import { startRepl } from './repl.js';
17
- import { checkTerminal } from './lib/terminal.js';
18
17
  import { setTerminalTheme } from './lib/theme.js';
19
- // Banner: padded programmatically for perfect alignment
20
- const W = 58; // inner width between ║ ║
21
- function pad(text, visible) {
22
- return text + ' '.repeat(Math.max(0, W - visible));
23
- }
24
- const B = chalk.cyan('║');
25
- const TOP = chalk.cyan('╔' + '═'.repeat(W) + '╗');
26
- const BOT = chalk.cyan('╚' + '═'.repeat(W) + '╝');
27
- const EMPTY = B + ' '.repeat(W) + B;
18
+ const LINE = chalk.cyan(' ─────────────────────────────────────────────────────');
28
19
  const BANNER = `
29
- ${TOP}
30
- ${EMPTY}
31
- ${B}${pad(' ' + chalk.bold.white('██╗ ██████╗ ██████╗ █████╗'), 31)}${B}
32
- ${B}${pad(' ' + chalk.bold.white('██║██╔════╝██╔═══██╗██╔══██╗'), 32)}${B}
33
- ${B}${pad(' ' + chalk.bold.white('██║██║ ██║ ██║███████║'), 32)}${B}
34
- ${B}${pad(' ' + chalk.bold.white('██║██║ ██║ ██║██╔══██║'), 32)}${B}
35
- ${B}${pad(' ' + chalk.bold.white('██║╚██████╗╚██████╔╝██║ ██║'), 32)}${B}
36
- ${B}${pad(' ' + chalk.bold.white('╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝'), 33)}${B}
37
- ${EMPTY}
38
- ${B}${pad(' ' + chalk.yellow('International Cyber Olympiad in AI 2026'), 42)}${B}
39
- ${B}${pad(' ' + chalk.bold.magenta("The World's First AI Security Olympiad"), 41)}${B}
40
- ${EMPTY}
41
- ${B}${pad(' ' + chalk.green.bold('AI4CTF') + ' ' + chalk.gray('AI as your teammate'), 29)}${B}
42
- ${B}${pad(' ' + chalk.red.bold('CTF4AI') + ' ' + chalk.gray('Hack & evaluate AI systems'), 36)}${B}
43
- ${B}${pad(' ' + chalk.bold.white('AI is your ally. AI is your target.'), 38)}${B}
44
- ${EMPTY}
45
- ${B}${pad(' ' + chalk.white('Sydney, Australia') + ' ' + chalk.gray('Jun 27 - Jul 2, 2026'), 42)}${B}
46
- ${B}${pad(' ' + chalk.cyan.underline('https://icoa2026.au'), 22)}${B}
47
- ${EMPTY}
48
- ${B}${pad(' ' + chalk.gray('CLI-Native Competition Terminal v1.5.2'), 42)}${B}
49
- ${EMPTY}
50
- ${BOT}
20
+ ${LINE}
21
+
22
+ ${chalk.bold.white('██╗ ██████╗ ██████╗ █████╗')}
23
+ ${chalk.bold.white('██║██╔════╝██╔═══██╗██╔══██╗')}
24
+ ${chalk.bold.white('██║██║ ██║ ██║███████║')}
25
+ ${chalk.bold.white('██║██║ ██║ ██║██╔══██║')}
26
+ ${chalk.bold.white('██║╚██████╗╚██████╔╝██║ ██║')}
27
+ ${chalk.bold.white('╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝')}
28
+
29
+ ${chalk.yellow('International Cyber Olympiad in AI 2026')}
30
+ ${chalk.bold.magenta("The World's First AI Security Olympiad")}
31
+
32
+ ${chalk.green.bold('AI4CTF')}${chalk.gray('[Day 1]')} ${chalk.white('AI as your teammate')}
33
+ ${chalk.red.bold('CTF4AI')}${chalk.gray('[Day 2]')} ${chalk.white('Hack & evaluate AI systems')}
34
+ ${chalk.bold.yellow('AI is your ally. AI is your target.')}
35
+
36
+ ${chalk.white('Sydney, Australia')} ${chalk.gray('Jun 27 - Jul 2, 2026')}
37
+ ${chalk.cyan.underline('https://icoa2026.au')}
38
+
39
+ ${chalk.gray('CLI-Native Competition Terminal v1.6.1')}
40
+
41
+ ${LINE}
51
42
  `;
52
43
  // Global error handlers
53
44
  process.on('uncaughtException', (err) => {
@@ -73,7 +64,6 @@ program
73
64
  // Force hacker theme: black background + green text
74
65
  setTerminalTheme();
75
66
  console.log(BANNER);
76
- checkTerminal();
77
67
  // If running interactively (no extra args or --resume), start REPL
78
68
  if (process.argv.length <= 2 || opts.resume) {
79
69
  startRepl(program, !!opts.resume);
@@ -1,3 +1,5 @@
1
+ export declare function isFirstRunOrUpgrade(currentVersion: string): boolean;
2
+ export declare function markVersionSeen(version: string): void;
1
3
  export declare function validateToken(token: string): boolean;
2
4
  export declare function isActivated(): boolean;
3
5
  export type ActivateResult = 'ok' | 'invalid' | 'already_bound';
@@ -107,6 +107,14 @@ const TOKEN_HASHES = new Set([
107
107
  "cde02309ad295648d86fa4acefdf7224809abb2ab9b025f14ae41e021512c5ed",
108
108
  "39835a1337c2afd9a9690cb9946752899a110df312d9d3aa68e10f85fad49dea",
109
109
  ]);
110
+ // Check if first run or version upgrade
111
+ export function isFirstRunOrUpgrade(currentVersion) {
112
+ const config = getConfig();
113
+ return config.lastVersion !== currentVersion;
114
+ }
115
+ export function markVersionSeen(version) {
116
+ saveConfig({ lastVersion: version });
117
+ }
110
118
  // Free commands that don't require token
111
119
  const FREE_COMMANDS = new Set(['ref', 'help', 'exit', 'quit', 'q', 'clear', 'cls', 'activate']);
112
120
  function hashToken(token) {
package/dist/repl.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  import { Command } from 'commander';
2
- export declare function startRepl(program: Command, resumeMode: boolean): void;
2
+ export declare function startRepl(program: Command, resumeMode: boolean): Promise<void>;
package/dist/repl.js CHANGED
@@ -2,49 +2,103 @@ import { createInterface } from 'node:readline';
2
2
  import { spawn } from 'node:child_process';
3
3
  import chalk from 'chalk';
4
4
  import { isConnected, getConfig } from './lib/config.js';
5
- import { isActivated, activateToken, isFreeCommand, isDeviceMatch, recordExit, recordResume } from './lib/access.js';
5
+ import { isActivated, activateToken, isFreeCommand, isDeviceMatch, recordExit, recordResume, isFirstRunOrUpgrade, markVersionSeen } from './lib/access.js';
6
6
  import { resetTerminalTheme } from './lib/theme.js';
7
7
  import { ensureSandbox, runInSandbox, isDockerAvailable } from './lib/sandbox.js';
8
8
  const INTERCEPT = '__REPL_NO_EXIT__';
9
- export function startRepl(program, resumeMode) {
9
+ const VERSION = '1.6.1';
10
+ export async function startRepl(program, resumeMode) {
10
11
  const config = getConfig();
11
12
  const connected = isConnected();
12
13
  const realExit = process.exit.bind(process);
13
14
  const activated = isActivated();
15
+ // First run or upgrade: prompt to install env
16
+ if (isFirstRunOrUpgrade(VERSION)) {
17
+ markVersionSeen(VERSION);
18
+ console.log(chalk.gray(' Checking competition environment...'));
19
+ // Quick check: count missing Python libs
20
+ const { execSync } = await import('node:child_process');
21
+ const checks = [
22
+ { name: 'pwntools', cmd: 'python3 -c "import pwn"' },
23
+ { name: 'z3-solver', cmd: 'python3 -c "import z3"' },
24
+ { name: 'numpy', cmd: 'python3 -c "import numpy"' },
25
+ { name: 'requests', cmd: 'python3 -c "import requests"' },
26
+ ];
27
+ let missing = 0;
28
+ for (const c of checks) {
29
+ try {
30
+ execSync(c.cmd, { stdio: 'ignore' });
31
+ }
32
+ catch {
33
+ missing++;
34
+ }
35
+ }
36
+ if (missing > 0) {
37
+ console.log(chalk.yellow(` ${missing} core libraries missing.`));
38
+ try {
39
+ const { confirm } = await import('@inquirer/prompts');
40
+ const yes = await confirm({ message: ' Install competition Python libraries now?', default: true });
41
+ if (yes) {
42
+ console.log();
43
+ // Trigger env setup
44
+ const { execSync: ex } = await import('node:child_process');
45
+ ex('node ' + new URL('../index.js', import.meta.url).pathname + ' env setup', { stdio: 'inherit' });
46
+ }
47
+ }
48
+ catch {
49
+ console.log(chalk.gray(' Run ') + chalk.white('env setup') + chalk.gray(' later to install.'));
50
+ }
51
+ console.log();
52
+ }
53
+ else {
54
+ console.log(chalk.green(' All core libraries ready.'));
55
+ console.log();
56
+ }
57
+ }
14
58
  // Handle resume
15
59
  if (resumeMode) {
16
60
  const info = recordResume();
17
61
  if (info) {
18
62
  const mins = Math.floor(info.awaySeconds / 60);
19
63
  const secs = info.awaySeconds % 60;
20
- console.log(chalk.yellow(` Session resumed. Away: ${mins}m ${secs}s | Total exits: ${info.exitCount}`));
21
- }
22
- else {
23
- console.log(chalk.gray(' No previous session to resume.'));
64
+ console.log(chalk.yellow(` Session resumed. Away: ${mins}m ${secs}s | Total exits: ${info.exitCount}`));
65
+ console.log();
24
66
  }
25
- console.log();
26
67
  }
27
68
  // Device mismatch check
28
69
  if (activated && !isDeviceMatch()) {
29
- console.log(chalk.red(' Token was activated on a different device. Access denied.'));
30
- console.log(chalk.gray(' Contact organizer for assistance.'));
70
+ console.log(chalk.red(' Token was activated on a different device.'));
71
+ console.log(chalk.gray(' Contact organizer for assistance.'));
31
72
  console.log();
32
- // Fall through to restricted mode
33
73
  }
34
74
  else if (connected) {
35
- console.log(chalk.gray(' Connected to: ') + chalk.white(config.ctfdUrl));
36
- console.log(chalk.gray(' User: ') + chalk.white(config.userName));
75
+ console.log(chalk.green(` Welcome back, ${config.userName}!`));
76
+ console.log(chalk.gray(` Connected to ${config.ctfdUrl}`));
77
+ console.log();
37
78
  }
38
79
  else if (activated) {
39
- console.log(chalk.green(' Access granted. ') + chalk.gray('Type: ') + chalk.white('join <url>') + chalk.gray(' to connect.'));
80
+ console.log(chalk.green(' Welcome, competitor! Ready to hack.'));
81
+ console.log();
82
+ console.log(chalk.gray(' Quick Start'));
83
+ console.log(chalk.gray(' ─────────────'));
84
+ console.log(chalk.white(' join <url> ') + chalk.gray('Connect to competition'));
85
+ console.log(chalk.white(' challenges ') + chalk.gray('View challenges'));
86
+ console.log(chalk.white(' hint <question> ') + chalk.gray('Ask AI for help'));
87
+ console.log(chalk.white(' env ') + chalk.gray('Check your tools'));
88
+ console.log(chalk.white(' help ') + chalk.gray('All commands'));
89
+ console.log();
40
90
  }
41
91
  else {
42
- console.log(chalk.yellow(' Restricted mode. ') + chalk.gray('Type: ') + chalk.white('activate <token>') + chalk.gray(' to unlock.'));
43
- console.log(chalk.gray(' Available: ') + chalk.white('ref [topic]') + chalk.gray(', ') + chalk.white('help') + chalk.gray(', ') + chalk.white('exit'));
92
+ console.log(chalk.white(' Welcome to ICOA CLI!'));
93
+ console.log();
94
+ console.log(chalk.gray(' Quick Start'));
95
+ console.log(chalk.gray(' ─────────────'));
96
+ console.log(chalk.white(' activate <token> ') + chalk.gray('Unlock with your access token'));
97
+ console.log(chalk.white(' ref <topic> ') + chalk.gray('Browse tool references'));
98
+ console.log(chalk.white(' env ') + chalk.gray('Check your tools'));
99
+ console.log(chalk.white(' help ') + chalk.gray('All commands'));
100
+ console.log();
44
101
  }
45
- console.log();
46
- console.log(chalk.gray(' Type ') + chalk.white('help') + chalk.gray(' for commands, ') + chalk.white('exit') + chalk.gray(' to quit.'));
47
- console.log();
48
102
  program.exitOverride();
49
103
  program.configureOutput({
50
104
  writeErr: () => { },
@@ -100,6 +100,7 @@ export interface IcoaConfig {
100
100
  geminiModel: string;
101
101
  accessToken: string;
102
102
  deviceFingerprint: string;
103
+ lastVersion: string;
103
104
  }
104
105
  export type CompetitionState = 'pre_competition' | 'demo' | 'live' | 'finished' | 'unknown';
105
106
  export type HintLevel = 'A' | 'B' | 'C';
@@ -28,5 +28,6 @@ export const DEFAULT_CONFIG = {
28
28
  geminiModel: 'gemini-2.5-flash',
29
29
  accessToken: '',
30
30
  deviceFingerprint: '',
31
+ lastVersion: '',
31
32
  };
32
33
  export const SUPPORTED_LANGUAGES = ['en', 'zh', 'ja', 'ko', 'es'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "1.5.2",
3
+ "version": "1.6.1",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {