icoa-cli 2.19.78 → 2.19.80

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.
@@ -1891,22 +1891,33 @@ export function registerExamCommand(program) {
1891
1891
  console.log();
1892
1892
  return;
1893
1893
  }
1894
- // Skip if already set up
1894
+ // Skip if already set up — but only if MOST packages actually installed.
1895
+ // If previous run had 0 or very few successes, retry instead of saying
1896
+ // "already set up" (which is misleading and strands the user).
1895
1897
  const existingSetup = getExamSetup();
1896
1898
  if (existingSetup) {
1897
- console.log();
1898
- console.log(chalk.green(' ✓ ') + chalk.green('Environment already set up'));
1899
- console.log(chalk.gray(` Completed: ${existingSetup.completedAt.split('T')[0]}`));
1900
- console.log(chalk.gray(` Python: ${existingSetup.pythonVersion}`));
1901
- console.log(chalk.gray(` Packages: ${existingSetup.installedPackages.length} installed`));
1902
- if (existingSetup.failedPackages.length > 0) {
1903
- console.log(chalk.yellow(` Failed: ${existingSetup.failedPackages.join(', ')}`));
1899
+ const installedCount = existingSetup.installedPackages.length;
1900
+ const totalAttempted = installedCount + existingSetup.failedPackages.length;
1901
+ const successRate = totalAttempted > 0 ? installedCount / totalAttempted : 0;
1902
+ if (installedCount >= 8 || successRate >= 0.7) {
1903
+ console.log();
1904
+ console.log(chalk.green(' ✓ ') + chalk.green('Environment already set up'));
1905
+ console.log(chalk.gray(` Completed: ${existingSetup.completedAt.split('T')[0]}`));
1906
+ console.log(chalk.gray(` Python: ${existingSetup.pythonVersion}`));
1907
+ console.log(chalk.gray(` Packages: ${installedCount} installed`));
1908
+ if (existingSetup.failedPackages.length > 0) {
1909
+ console.log(chalk.yellow(` Failed: ${existingSetup.failedPackages.join(', ')}`));
1910
+ }
1911
+ console.log();
1912
+ console.log(chalk.white(' Next step: ') + chalk.bold.cyan('exam <token>'));
1913
+ console.log(chalk.gray(' Or type ') + chalk.bold.cyan('back') + chalk.gray(' to return to the main menu.'));
1914
+ console.log();
1915
+ return;
1904
1916
  }
1917
+ // Prior setup mostly failed — retry
1905
1918
  console.log();
1906
- console.log(chalk.white(' Next step: ') + chalk.bold.cyan('exam <token>'));
1907
- console.log(chalk.gray(' Or type ') + chalk.bold.cyan('back') + chalk.gray(' to return to the main menu.'));
1919
+ console.log(chalk.yellow(` Previous setup had only ${installedCount}/${totalAttempted} packages. Retrying...`));
1908
1920
  console.log();
1909
- return;
1910
1921
  }
1911
1922
  console.log();
1912
1923
  printHeader('Exam Environment Setup');
@@ -1914,7 +1925,7 @@ export function registerExamCommand(program) {
1914
1925
  console.log(chalk.white(' Installing Python packages for practical questions.'));
1915
1926
  console.log(chalk.gray(' Expected: 1-2 minutes · ~150MB disk'));
1916
1927
  console.log();
1917
- // Step 1: Check Python 3.12+
1928
+ // Step 1: Check Python 3.10+ (all packages support 3.10, 3.12 recommended)
1918
1929
  const printPythonInstallGuide = (reason, currentVersion) => {
1919
1930
  const platform = process.platform;
1920
1931
  console.log();
@@ -1922,10 +1933,10 @@ export function registerExamCommand(program) {
1922
1933
  printError('Python 3 not found.');
1923
1934
  }
1924
1935
  else {
1925
- printError(`Python ${currentVersion} found, but 3.12+ required for the exam.`);
1936
+ printError(`Python ${currentVersion} found, but 3.10+ required for the exam.`);
1926
1937
  }
1927
1938
  console.log();
1928
- console.log(chalk.bold.white(' How to install Python 3.12:'));
1939
+ console.log(chalk.bold.white(' How to install Python 3.10+ (3.12 recommended):'));
1929
1940
  console.log();
1930
1941
  if (platform === 'darwin') {
1931
1942
  console.log(chalk.yellow(' macOS (Homebrew, recommended):'));
@@ -1967,11 +1978,16 @@ export function registerExamCommand(program) {
1967
1978
  const raw = execSync(`${pythonBin} --version`, { encoding: 'utf-8', timeout: 5000 }).trim();
1968
1979
  pythonVersion = raw.replace('Python ', '');
1969
1980
  const parts = pythonVersion.split('.').map(Number);
1970
- if (parts[0] < 3 || (parts[0] === 3 && parts[1] < 12)) {
1981
+ if (parts[0] < 3 || (parts[0] === 3 && parts[1] < 10)) {
1971
1982
  printPythonInstallGuide('too_old', pythonVersion);
1972
1983
  return;
1973
1984
  }
1974
- console.log(chalk.green(` ✓ Python ${pythonVersion}`));
1985
+ if (parts[0] === 3 && parts[1] < 12) {
1986
+ console.log(chalk.green(` ✓ Python ${pythonVersion}`) + chalk.gray(' (works, but 3.12 recommended)'));
1987
+ }
1988
+ else {
1989
+ console.log(chalk.green(` ✓ Python ${pythonVersion}`));
1990
+ }
1975
1991
  }
1976
1992
  catch {
1977
1993
  printPythonInstallGuide('missing');
@@ -2006,11 +2022,39 @@ export function registerExamCommand(program) {
2006
2022
  console.log();
2007
2023
  console.log(chalk.white(` Installing ${PACKAGES.length} packages...`));
2008
2024
  console.log();
2025
+ // Detect PEP 668 "externally-managed-environment" — modern Ubuntu/Debian/Kali
2026
+ // block system-wide pip installs. Try a harmless install to detect.
2027
+ let pipExtraFlags = '';
2028
+ try {
2029
+ execSync(`${pythonBin} -m pip install --dry-run pip 2>&1`, { encoding: 'utf-8', timeout: 10000 });
2030
+ }
2031
+ catch (e) {
2032
+ const msg = String(e?.stdout || e?.stderr || e?.message || '');
2033
+ if (/externally-managed-environment|break-system-packages/i.test(msg)) {
2034
+ // Pick the safer option: --user (installs to ~/.local, no sudo needed)
2035
+ pipExtraFlags = '--user --break-system-packages';
2036
+ console.log(chalk.yellow(' ℹ PEP 668 detected — using --user install (no sudo needed)'));
2037
+ console.log();
2038
+ }
2039
+ }
2040
+ // Python 3.13 warning: pwntools may fail to build
2041
+ const pyParts = pythonVersion.split('.').map(Number);
2042
+ if (pyParts[0] === 3 && pyParts[1] >= 13) {
2043
+ console.log(chalk.yellow(` ⚠ Python ${pythonVersion} detected`));
2044
+ console.log(chalk.gray(' Some packages (pwntools, scapy) may not have wheels yet for 3.13.'));
2045
+ console.log(chalk.gray(' Python 3.12 is recommended. Install:'));
2046
+ if (process.platform === 'linux') {
2047
+ console.log(chalk.green(' sudo apt install python3.12 python3.12-venv'));
2048
+ console.log(chalk.green(' sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1'));
2049
+ }
2050
+ console.log();
2051
+ }
2009
2052
  const installed = [];
2010
2053
  const failed = [];
2054
+ let firstError = '';
2011
2055
  for (const pkg of PACKAGES) {
2012
2056
  try {
2013
- execSync(`${pythonBin} -m pip install "${pkg}" --quiet 2>/dev/null`, {
2057
+ execSync(`${pythonBin} -m pip install "${pkg}" ${pipExtraFlags} --quiet 2>&1`, {
2014
2058
  encoding: 'utf-8', timeout: 180000, stdio: 'pipe',
2015
2059
  });
2016
2060
  const importName = IMPORT_MAP[pkg] || pkg;
@@ -2028,24 +2072,50 @@ export function registerExamCommand(program) {
2028
2072
  console.log(chalk.green(` ✓ ${pkg}`) + (ver ? chalk.gray(` (${ver})`) : ''));
2029
2073
  installed.push(ver ? `${pkg}==${ver}` : pkg);
2030
2074
  }
2031
- catch {
2032
- console.log(chalk.red(` ✗ ${pkg}`));
2033
- failed.push(pkg);
2075
+ catch (e) {
2076
+ const raw = String(e?.stdout || e?.stderr || e?.message || '').slice(-400);
2077
+ const short = raw.split('\n').filter((l) => l.trim()).slice(-2).join(' | ').slice(0, 120);
2078
+ console.log(chalk.red(` ✗ ${pkg}`) + chalk.gray(` ${short}`));
2079
+ failed.push({ pkg, error: short });
2080
+ if (!firstError)
2081
+ firstError = raw;
2034
2082
  }
2035
2083
  }
2084
+ // If wholesale failure, show the first full error so user can debug
2085
+ if (installed.length === 0 && firstError) {
2086
+ console.log();
2087
+ console.log(chalk.yellow(' First error detail (for debugging):'));
2088
+ console.log(chalk.gray(' ' + firstError.split('\n').slice(-6).join('\n ')));
2089
+ }
2036
2090
  // Save state
2037
2091
  saveExamSetup({
2038
2092
  completedAt: new Date().toISOString(),
2039
2093
  pythonVersion,
2040
2094
  installedPackages: installed,
2041
- failedPackages: failed,
2095
+ failedPackages: failed.map((f) => f.pkg),
2042
2096
  });
2043
2097
  console.log();
2044
2098
  if (failed.length === 0) {
2045
2099
  printSuccess(`Environment ready! All ${PACKAGES.length} packages installed.`);
2046
2100
  }
2101
+ else if (installed.length === 0) {
2102
+ printError(`Setup failed — 0 of ${PACKAGES.length} packages installed.`);
2103
+ console.log();
2104
+ console.log(chalk.yellow(' Troubleshooting:'));
2105
+ if (pyParts[0] === 3 && pyParts[1] >= 13) {
2106
+ console.log(chalk.gray(' Python 3.13 is too new — most CTF libraries lack wheels.'));
2107
+ console.log(chalk.gray(' Install Python 3.12 and re-run exam setup.'));
2108
+ }
2109
+ else {
2110
+ console.log(chalk.gray(' • Check you have internet (pip.pypa.io must be reachable)'));
2111
+ console.log(chalk.gray(' • Try: ') + chalk.cyan(`${pythonBin} -m pip install pwntools`) + chalk.gray(' — see full error'));
2112
+ console.log(chalk.gray(' • On Kali/Ubuntu 22+: might need python3.12-dev + build-essential'));
2113
+ }
2114
+ console.log();
2115
+ console.log(chalk.white(' Rerun when fixed: ') + chalk.bold.cyan('exam setup'));
2116
+ }
2047
2117
  else {
2048
- printWarning(`${installed.length}/${PACKAGES.length} packages installed. ${failed.length} failed: ${failed.join(', ')}`);
2118
+ printWarning(`${installed.length}/${PACKAGES.length} packages installed. ${failed.length} failed: ${failed.map((f) => f.pkg).join(', ')}`);
2049
2119
  }
2050
2120
  // Python usage tutorial for beginners
2051
2121
  console.log();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.78",
3
+ "version": "2.19.80",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {