icoa-cli 2.19.78 → 2.19.79

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');
@@ -2006,11 +2017,39 @@ export function registerExamCommand(program) {
2006
2017
  console.log();
2007
2018
  console.log(chalk.white(` Installing ${PACKAGES.length} packages...`));
2008
2019
  console.log();
2020
+ // Detect PEP 668 "externally-managed-environment" — modern Ubuntu/Debian/Kali
2021
+ // block system-wide pip installs. Try a harmless install to detect.
2022
+ let pipExtraFlags = '';
2023
+ try {
2024
+ execSync(`${pythonBin} -m pip install --dry-run pip 2>&1`, { encoding: 'utf-8', timeout: 10000 });
2025
+ }
2026
+ catch (e) {
2027
+ const msg = String(e?.stdout || e?.stderr || e?.message || '');
2028
+ if (/externally-managed-environment|break-system-packages/i.test(msg)) {
2029
+ // Pick the safer option: --user (installs to ~/.local, no sudo needed)
2030
+ pipExtraFlags = '--user --break-system-packages';
2031
+ console.log(chalk.yellow(' ℹ PEP 668 detected — using --user install (no sudo needed)'));
2032
+ console.log();
2033
+ }
2034
+ }
2035
+ // Python 3.13 warning: pwntools may fail to build
2036
+ const pyParts = pythonVersion.split('.').map(Number);
2037
+ if (pyParts[0] === 3 && pyParts[1] >= 13) {
2038
+ console.log(chalk.yellow(` ⚠ Python ${pythonVersion} detected`));
2039
+ console.log(chalk.gray(' Some packages (pwntools, scapy) may not have wheels yet for 3.13.'));
2040
+ console.log(chalk.gray(' Python 3.12 is recommended. Install:'));
2041
+ if (process.platform === 'linux') {
2042
+ console.log(chalk.green(' sudo apt install python3.12 python3.12-venv'));
2043
+ console.log(chalk.green(' sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1'));
2044
+ }
2045
+ console.log();
2046
+ }
2009
2047
  const installed = [];
2010
2048
  const failed = [];
2049
+ let firstError = '';
2011
2050
  for (const pkg of PACKAGES) {
2012
2051
  try {
2013
- execSync(`${pythonBin} -m pip install "${pkg}" --quiet 2>/dev/null`, {
2052
+ execSync(`${pythonBin} -m pip install "${pkg}" ${pipExtraFlags} --quiet 2>&1`, {
2014
2053
  encoding: 'utf-8', timeout: 180000, stdio: 'pipe',
2015
2054
  });
2016
2055
  const importName = IMPORT_MAP[pkg] || pkg;
@@ -2028,24 +2067,50 @@ export function registerExamCommand(program) {
2028
2067
  console.log(chalk.green(` ✓ ${pkg}`) + (ver ? chalk.gray(` (${ver})`) : ''));
2029
2068
  installed.push(ver ? `${pkg}==${ver}` : pkg);
2030
2069
  }
2031
- catch {
2032
- console.log(chalk.red(` ✗ ${pkg}`));
2033
- failed.push(pkg);
2070
+ catch (e) {
2071
+ const raw = String(e?.stdout || e?.stderr || e?.message || '').slice(-400);
2072
+ const short = raw.split('\n').filter((l) => l.trim()).slice(-2).join(' | ').slice(0, 120);
2073
+ console.log(chalk.red(` ✗ ${pkg}`) + chalk.gray(` ${short}`));
2074
+ failed.push({ pkg, error: short });
2075
+ if (!firstError)
2076
+ firstError = raw;
2034
2077
  }
2035
2078
  }
2079
+ // If wholesale failure, show the first full error so user can debug
2080
+ if (installed.length === 0 && firstError) {
2081
+ console.log();
2082
+ console.log(chalk.yellow(' First error detail (for debugging):'));
2083
+ console.log(chalk.gray(' ' + firstError.split('\n').slice(-6).join('\n ')));
2084
+ }
2036
2085
  // Save state
2037
2086
  saveExamSetup({
2038
2087
  completedAt: new Date().toISOString(),
2039
2088
  pythonVersion,
2040
2089
  installedPackages: installed,
2041
- failedPackages: failed,
2090
+ failedPackages: failed.map((f) => f.pkg),
2042
2091
  });
2043
2092
  console.log();
2044
2093
  if (failed.length === 0) {
2045
2094
  printSuccess(`Environment ready! All ${PACKAGES.length} packages installed.`);
2046
2095
  }
2096
+ else if (installed.length === 0) {
2097
+ printError(`Setup failed — 0 of ${PACKAGES.length} packages installed.`);
2098
+ console.log();
2099
+ console.log(chalk.yellow(' Troubleshooting:'));
2100
+ if (pyParts[0] === 3 && pyParts[1] >= 13) {
2101
+ console.log(chalk.gray(' Python 3.13 is too new — most CTF libraries lack wheels.'));
2102
+ console.log(chalk.gray(' Install Python 3.12 and re-run exam setup.'));
2103
+ }
2104
+ else {
2105
+ console.log(chalk.gray(' • Check you have internet (pip.pypa.io must be reachable)'));
2106
+ console.log(chalk.gray(' • Try: ') + chalk.cyan(`${pythonBin} -m pip install pwntools`) + chalk.gray(' — see full error'));
2107
+ console.log(chalk.gray(' • On Kali/Ubuntu 22+: might need python3.12-dev + build-essential'));
2108
+ }
2109
+ console.log();
2110
+ console.log(chalk.white(' Rerun when fixed: ') + chalk.bold.cyan('exam setup'));
2111
+ }
2047
2112
  else {
2048
- printWarning(`${installed.length}/${PACKAGES.length} packages installed. ${failed.length} failed: ${failed.join(', ')}`);
2113
+ printWarning(`${installed.length}/${PACKAGES.length} packages installed. ${failed.length} failed: ${failed.map((f) => f.pkg).join(', ')}`);
2049
2114
  }
2050
2115
  // Python usage tutorial for beginners
2051
2116
  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.79",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {