icoa-cli 2.19.589 → 2.19.591

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.
@@ -1 +1 @@
1
- import{execFile as e,execFileSync as o}from"node:child_process";import{lookup as t}from"node:dns/promises";import r from"node:net";import n from"node:tls";import chalk from"chalk";import{getConfig as s,isConnected as i}from"../lib/config.js";import{getCliVersion as a}from"../lib/version.js";import{logCommand as l}from"../lib/logger.js";import{resolveHostPython as c,REQUIRED_PY as u}from"../lib/aienv.js";import{enableSystemTrust as p,systemTrustActive as h,transportAdvice as d,transportCauseCode as m}from"../lib/tls-trust.js";import{httpFetch as f}from"../lib/http.js";function w(e,o,t,r){const n="ok"===e?chalk.green(" ✓"):"warn"===e?chalk.yellow(" ⚠"):chalk.red(" ✗"),s=chalk.white(o.padEnd(16)),i="ok"===e?chalk.gray(t):("warn"===e?chalk.yellow:chalk.red)(t);console.log(`${n} ${s} ${i}`),r&&"ok"!==e&&console.log(chalk.gray(` → ${r}`))}function y(e,t){try{return o(e,t,{stdio:["ignore","pipe","ignore"],timeout:3e3}).toString().trim()}catch{return null}}async function g(e,o){try{const t=await f(e,{headers:{"User-Agent":"icoa-cli",...o??{}},timeoutMs:6e3});return{status:t.status,body:await t.text()}}catch{return null}}const k="practice.icoa2026.au";function $(e){return new Promise(o=>{const t=n.connect({host:e,port:443,servername:e,rejectUnauthorized:!1,timeout:8e3},()=>{const e=t.getPeerCertificate(),r=e?.issuer&&(e.issuer.O||e.issuer.CN)||"unknown";o({authorized:t.authorized,issuer:String(r),error:t.authorized?"":t.authorizationError||""}),t.destroy()});t.setTimeout(8e3,()=>{t.destroy(),o({failure:Object.assign(new Error("TLS connect timed out"),{code:"UND_ERR_CONNECT_TIMEOUT"})})}),t.on("error",e=>o({failure:e}))})}let v=!1;function T(e,o=5e3){return new Promise(t=>{const n=r.connect({host:e,port:443,timeout:o});n.once("connect",()=>{n.destroy(),t("ok")}),n.once("timeout",()=>{n.destroy(),t("ETIMEDOUT")}),n.once("error",e=>t(e.code||e.message))})}export function registerDoctorCommand(e){e.command("doctor").description("Run a read-only self-check: version, Node, Python, tools, server, network, terminal").action(runDoctor)}export async function runDoctor(){l("doctor"),console.log(),console.log(chalk.bold.white(" ICOA Doctor")+chalk.gray(" — read-only self-check (nothing is installed or changed)")),console.log(chalk.gray(" ─────────────────────────────────────────────────────"));const o=[];o.push(await async function(){const e=a(),o=await g("https://registry.npmjs.org/icoa-cli/latest");if(!o||200!==o.status)return w("warn","CLI version",`v${e} (could not reach npm to compare)`,"check your network"),"warn";let t="";try{t=JSON.parse(o.body).version??""}catch{}return t&&t!==e?(w("warn","CLI version",`v${e} — latest is v${t}`,"npm install -g icoa-cli@latest"),"warn"):(w("ok","CLI version",`v${e} (latest)`),"ok")}()),o.push(Number(process.versions.node.split(".")[0])>=22?(w("ok","Node.js",`v${process.versions.node}`),"ok"):(w("fail","Node.js",`v${process.versions.node} — need 22+`,"icoa env setup (installs the pinned Node 22.x)"),"fail")),o.push(function(){const e=c(e=>y(e,["--version"]));return e?(w("ok","Python 3.12",`${e.cmd} (${e.version.major}.${e.version.minor})`),"ok"):(w("fail","Python 3.12",`no ${u.major}.${u.minor}.x interpreter found`,"python setup (one command, ~20 s, no sudo) · or brew install python@3.12 / apt install python3.12"),"fail")}()),o.push(function(){const e=["git","base64"].filter(e=>{return o=e,!(null!==y("win32"===process.platform?"where":"which",[o]));var o}),o=y("docker",["--version"]);return 0===e.length?(w("ok","Core tools","git, base64 present"+(o?" · docker (sandbox on)":" · docker absent (host-shell fallback)")),"ok"):(w("warn","Core tools",`missing: ${e.join(", ")}`,"icoa env setup (installs the CTF toolkit)"),"warn")}()),o.push(await async function(){const e=s(),o=i(),t=(e.ctfdUrl||"https://practice.icoa2026.au").replace(/\/$/,""),r=await g(`${t}/api/v1/users/me`,e.token?{Authorization:`Token ${e.token}`}:void 0);return r?o?200===r.status?(w("ok","Server",`${t} · signed in as ${e.userName||"you"}`),"ok"):401===r.status||403===r.status?(w("fail","Server",`${t} rejected your session (HTTP ${r.status})`,"your token expired — join <url> again"),"fail"):(w("warn","Server",`${t} returned HTTP ${r.status}`,"retry shortly, or re-join"),"warn"):(w("warn","Server",`${t} reachable — not signed in`,"join <url> to sign in with your account"),"warn"):(w("fail","Server",`${t} unreachable`,"see the HTTPS (443) line below — it names the fault"),v=!0,"fail")}()),o.push(await async function(){let o=[];try{o=await t(k,{all:!0}),w("ok","DNS",`${k} → ${o.map(e=>e.address).join(", ")}`)}catch(e){return w("fail","DNS",`cannot resolve ${k} (${m(e)||"lookup failed"})`,"set DNS to 1.1.1.1 or 8.8.8.8, or use a phone hotspot"),v=!0,"fail"}const r=await $(k);if("failure"in r){const t=m(r.failure);return w("fail","HTTPS (443)","cannot open a connection"+(t?` (${t})`:"")),v=!0,await async function(o){const[t,r,n]=await Promise.all([Promise.all(o.map(async e=>({...e,result:await T(e.address)}))),(s=`https://${k}/`,new Promise(o=>{e("curl",["-sS","-o","win32"===process.platform?"NUL":"/dev/null","-w","%{http_code}","--max-time","8",s],{timeout:1e4},(e,t,r)=>{if(e&&"ENOENT"===e.code)return o("absent");const n=String(t||"").trim();if(/^[1-5]\d\d$/.test(n))return o("ok");const s=String(r||"").trim().split("\n")[0]||(e?e.message:"no response");o(s.replace(/^curl:\s*\(\d+\)\s*/,""))})})),T("registry.npmjs.org")]);var s;const i=t.map(e=>{return`${o=e.family,6===o?"IPv6":"IPv4"} ${e.address} → ${e.result}`;var o}).join(" · "),a=t.some(e=>4===e.family&&"ok"===e.result),l=t.some(e=>6===e.family&&"ok"===e.result),c=a||l;let u,p;console.log(chalk.gray(` route: ${i}`)),console.log(chalk.gray(` curl → ${"ok"===r?"reaches us":"absent"===r?"not installed":r} · other HTTPS sites (registry.npmjs.org) → ${"ok"===n?"ok":n}`)),c?(u=a?"the connection works now — the failure above was momentary":"IPv4 is dead on this network, IPv6 works",p=a?"just retry":"retry — icoa falls back to IPv6 by itself; if it keeps failing, use a phone hotspot"):"ok"===r?(u="this computer reaches us (curl works) but Node does not",p="a proxy setting Node cannot use — check ~/.curlrc, HTTPS_PROXY, and any VPN app; a phone hotspot bypasses all of them"):"ok"===n?(u="this computer reaches other HTTPS sites, but practice.icoa2026.au is dropped for everything except the browser",p='a VPN / web-filter app here tunnels only the browser. Quit it fully or use a phone hotspot. Tell the coach: "104.64.209.70 TCP 443 is dropped on this network"'):(u="nothing outside the browser can open HTTPS on this network",p="this network only allows browser traffic through its proxy — use a phone hotspot"),console.log(chalk.yellow(` ⇒ ${u}`)),console.log(chalk.gray(` → ${p}`))}(o),"fail"}if(r.authorized)w("ok","HTTPS (443)",`certificate valid · issued by ${r.issuer}`);else{const e=h()||p()?await $(k):null;if(null===e||"failure"in e||!e.authorized)return w("fail","HTTPS (443)",`re-signed as "${r.issuer}" and your OS does not trust it either (${r.error})`,"ask the network admin to exempt practice.icoa2026.au from SSL inspection, or use a phone hotspot"),"fail";w("warn","HTTPS (443)",`this network re-signs HTTPS as "${r.issuer}" (SSL inspection)`,"handled automatically — icoa now uses the certificates your OS trusts. Nothing to do.")}try{const e=await f(`https://${k}/api/icoa/learn/validate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:"DOCTOR00",device_fingerprint:"doctor-probe"}),timeoutMs:8e3});return 429===e.status?(w("warn","Learn API","rate-limited (HTTP 429)","too many attempts from this machine — wait a minute and retry"),"warn"):(w("ok","Learn API",`answered (HTTP ${e.status}) — token entry will work here`),r.authorized?"ok":"warn")}catch(e){const o=m(e);return w("fail","Learn API","no answer"+(o?` (${o})`:""),d(e)[1]||d(e)[0]||"retry, or use a phone hotspot"),"fail"}}()),o.push(function(){const e=["HTTPS_PROXY","https_proxy","HTTP_PROXY","http_proxy"].filter(e=>(process.env[e]||"").trim());return 0===e.length?"ok":(w("warn","Proxy",`${e[0]} is set — Node ignores it, so icoa connects directly`,"if direct connections are blocked here, ask the admin to allow practice.icoa2026.au on TCP 443"),"warn")}()),o.push(function(){const e=(process.env.LANG||process.env.LC_ALL||process.env.LC_CTYPE||"").toLowerCase(),o=e.includes("utf-8")||e.includes("utf8"),t=process.stdout.columns||0;return o?t&&t<60?(w("warn","Terminal",`${t} cols — narrow (some charts fall back to text)`,"widen the window to ≥60 columns"),"warn"):(w("ok","Terminal",`UTF-8 · ${t||"?"} cols`),"ok"):(w("warn","Terminal",`locale "${e||"unset"}" is not UTF-8 (charts/QR degrade to ASCII)`,"export LANG=en_US.UTF-8"),"warn")}()),console.log(chalk.gray(" ─────────────────────────────────────────────────────"));const r=o.filter(e=>"fail"===e).length,n=o.filter(e=>"warn"===e).length;0===r&&0===n?console.log(chalk.green(" All clear — you are ready to compete.")):0===r?console.log(chalk.yellow(` ${n} warning${n>1?"s":""} — usable, but see the → fixes above.`)):console.log(chalk.red(` ${r} blocker${r>1?"s":""}`)+(n?chalk.yellow(` + ${n} warning${n>1?"s":""}`):"")+(v?chalk.gray(" — this computer cannot reach the ICOA server. Follow the ⇒ / → lines above; installing anything will not help."):chalk.gray(" — fix the ✗ items first. Most are solved by ")+chalk.cyan("icoa env setup"))),console.log()}
1
+ import{execFile as e,execFileSync as o}from"node:child_process";import{lookup as t}from"node:dns/promises";import r from"node:net";import n from"node:tls";import chalk from"chalk";import{getConfig as s,isConnected as i}from"../lib/config.js";import{getCliVersion as a}from"../lib/version.js";import{logCommand as c}from"../lib/logger.js";import{REQUIRED_PY as l}from"../lib/aienv.js";import{probePython as u}from"../lib/python-setup.js";import{enableSystemTrust as h,systemTrustActive as d,transportAdvice as p,transportCauseCode as m}from"../lib/tls-trust.js";import{httpFetch as f}from"../lib/http.js";function w(e,o,t,r){const n="ok"===e?chalk.green(" ✓"):"warn"===e?chalk.yellow(" ⚠"):chalk.red(" ✗"),s=chalk.white(o.padEnd(16)),i="ok"===e?chalk.gray(t):("warn"===e?chalk.yellow:chalk.red)(t);console.log(`${n} ${s} ${i}`),r&&"ok"!==e&&console.log(chalk.gray(` → ${r}`))}function y(e,t){try{return o(e,t,{stdio:["ignore","pipe","ignore"],timeout:3e3}).toString().trim()}catch{return null}}async function g(e,o){try{const t=await f(e,{headers:{"User-Agent":"icoa-cli",...o??{}},timeoutMs:6e3});return{status:t.status,body:await t.text()}}catch{return null}}const k="practice.icoa2026.au";function $(e){return new Promise(o=>{const t=n.connect({host:e,port:443,servername:e,rejectUnauthorized:!1,timeout:8e3},()=>{const e=t.getPeerCertificate(),r=e?.issuer&&(e.issuer.O||e.issuer.CN)||"unknown";o({authorized:t.authorized,issuer:String(r),error:t.authorized?"":t.authorizationError||""}),t.destroy()});t.setTimeout(8e3,()=>{t.destroy(),o({failure:Object.assign(new Error("TLS connect timed out"),{code:"UND_ERR_CONNECT_TIMEOUT"})})}),t.on("error",e=>o({failure:e}))})}let v=!1;function T(e,o=5e3){return new Promise(t=>{const n=r.connect({host:e,port:443,timeout:o});n.once("connect",()=>{n.destroy(),t("ok")}),n.once("timeout",()=>{n.destroy(),t("ETIMEDOUT")}),n.once("error",e=>t(e.code||e.message))})}export function registerDoctorCommand(e){e.command("doctor").description("Run a read-only self-check: version, Node, Python, tools, server, network, terminal").action(runDoctor)}export async function runDoctor(){c("doctor"),console.log(),console.log(chalk.bold.white(" ICOA Doctor")+chalk.gray(" — read-only self-check (nothing is installed or changed)")),console.log(chalk.gray(" ─────────────────────────────────────────────────────"));const o=[];o.push(await async function(){const e=a(),o=await g("https://registry.npmjs.org/icoa-cli/latest");if(!o||200!==o.status)return w("warn","CLI version",`v${e} (could not reach npm to compare)`,"check your network"),"warn";let t="";try{t=JSON.parse(o.body).version??""}catch{}return t&&t!==e?(w("warn","CLI version",`v${e} — latest is v${t}`,"npm install -g icoa-cli@latest"),"warn"):(w("ok","CLI version",`v${e} (latest)`),"ok")}()),o.push(Number(process.versions.node.split(".")[0])>=22?(w("ok","Node.js",`v${process.versions.node}`),"ok"):(w("fail","Node.js",`v${process.versions.node} — need 22+`,"icoa env setup (installs the pinned Node 22.x)"),"fail")),o.push(function(){const e=u(!0);if(e.ok){const o="aienv"===e.source?"aienv venv":"icoa"===e.source?"installed by icoa":"already on this machine";return w("ok",`Python ${l.major}.${l.minor}`,`${e.version} · ${o} · ${e.cmd}`),"ok"}return w("fail",`Python ${l.major}.${l.minor}`,e.otherVersion?`only Python ${e.otherVersion} here — need a ${l.major}.${l.minor}.x alongside it`:`no ${l.major}.${l.minor}.x interpreter found`,"python setup (one command, ~20 s, no sudo — reading cards and answering never needs it)"),"fail"}()),o.push(function(){const e=["git","base64"].filter(e=>{return o=e,!(null!==y("win32"===process.platform?"where":"which",[o]));var o}),o=y("docker",["--version"]);return 0===e.length?(w("ok","Core tools","git, base64 present"+(o?" · docker (sandbox on)":" · docker absent (host-shell fallback)")),"ok"):(w("warn","Core tools",`missing: ${e.join(", ")}`,"icoa env setup (installs the CTF toolkit)"),"warn")}()),o.push(await async function(){const e=s(),o=i(),t=(e.ctfdUrl||"https://practice.icoa2026.au").replace(/\/$/,""),r=await g(`${t}/api/v1/users/me`,e.token?{Authorization:`Token ${e.token}`}:void 0);return r?o?200===r.status?(w("ok","Server",`${t} · signed in as ${e.userName||"you"}`),"ok"):401===r.status||403===r.status?(w("fail","Server",`${t} rejected your session (HTTP ${r.status})`,"your token expired — join <url> again"),"fail"):(w("warn","Server",`${t} returned HTTP ${r.status}`,"retry shortly, or re-join"),"warn"):(w("warn","Server",`${t} reachable — not signed in`,"join <url> to sign in with your account"),"warn"):(w("fail","Server",`${t} unreachable`,"see the HTTPS (443) line below — it names the fault"),v=!0,"fail")}()),o.push(await async function(){let o=[];try{o=await t(k,{all:!0}),w("ok","DNS",`${k} → ${o.map(e=>e.address).join(", ")}`)}catch(e){return w("fail","DNS",`cannot resolve ${k} (${m(e)||"lookup failed"})`,"set DNS to 1.1.1.1 or 8.8.8.8, or use a phone hotspot"),v=!0,"fail"}const r=await $(k);if("failure"in r){const t=m(r.failure);return w("fail","HTTPS (443)","cannot open a connection"+(t?` (${t})`:"")),v=!0,await async function(o){const[t,r,n]=await Promise.all([Promise.all(o.map(async e=>({...e,result:await T(e.address)}))),(s=`https://${k}/`,new Promise(o=>{e("curl",["-sS","-o","win32"===process.platform?"NUL":"/dev/null","-w","%{http_code}","--max-time","8",s],{timeout:1e4},(e,t,r)=>{if(e&&"ENOENT"===e.code)return o("absent");const n=String(t||"").trim();if(/^[1-5]\d\d$/.test(n))return o("ok");const s=String(r||"").trim().split("\n")[0]||(e?e.message:"no response");o(s.replace(/^curl:\s*\(\d+\)\s*/,""))})})),T("registry.npmjs.org")]);var s;const i=t.map(e=>{return`${o=e.family,6===o?"IPv6":"IPv4"} ${e.address} → ${e.result}`;var o}).join(" · "),a=t.some(e=>4===e.family&&"ok"===e.result),c=t.some(e=>6===e.family&&"ok"===e.result),l=a||c;let u,h;console.log(chalk.gray(` route: ${i}`)),console.log(chalk.gray(` curl → ${"ok"===r?"reaches us":"absent"===r?"not installed":r} · other HTTPS sites (registry.npmjs.org) → ${"ok"===n?"ok":n}`)),l?(u=a?"the connection works now — the failure above was momentary":"IPv4 is dead on this network, IPv6 works",h=a?"just retry":"retry — icoa falls back to IPv6 by itself; if it keeps failing, use a phone hotspot"):"ok"===r?(u="this computer reaches us (curl works) but Node does not",h="a proxy setting Node cannot use — check ~/.curlrc, HTTPS_PROXY, and any VPN app; a phone hotspot bypasses all of them"):"ok"===n?(u="this computer reaches other HTTPS sites, but practice.icoa2026.au is dropped for everything except the browser",h='a VPN / web-filter app here tunnels only the browser. Quit it fully or use a phone hotspot. Tell the coach: "104.64.209.70 TCP 443 is dropped on this network"'):(u="nothing outside the browser can open HTTPS on this network",h="this network only allows browser traffic through its proxy — use a phone hotspot"),console.log(chalk.yellow(` ⇒ ${u}`)),console.log(chalk.gray(` → ${h}`))}(o),"fail"}if(r.authorized)w("ok","HTTPS (443)",`certificate valid · issued by ${r.issuer}`);else{const e=d()||h()?await $(k):null;if(null===e||"failure"in e||!e.authorized)return w("fail","HTTPS (443)",`re-signed as "${r.issuer}" and your OS does not trust it either (${r.error})`,"ask the network admin to exempt practice.icoa2026.au from SSL inspection, or use a phone hotspot"),"fail";w("warn","HTTPS (443)",`this network re-signs HTTPS as "${r.issuer}" (SSL inspection)`,"handled automatically — icoa now uses the certificates your OS trusts. Nothing to do.")}try{const e=await f(`https://${k}/api/icoa/learn/validate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:"DOCTOR00",device_fingerprint:"doctor-probe"}),timeoutMs:8e3});return 429===e.status?(w("warn","Learn API","rate-limited (HTTP 429)","too many attempts from this machine — wait a minute and retry"),"warn"):(w("ok","Learn API",`answered (HTTP ${e.status}) — token entry will work here`),r.authorized?"ok":"warn")}catch(e){const o=m(e);return w("fail","Learn API","no answer"+(o?` (${o})`:""),p(e)[1]||p(e)[0]||"retry, or use a phone hotspot"),"fail"}}()),o.push(function(){const e=["HTTPS_PROXY","https_proxy","HTTP_PROXY","http_proxy"].filter(e=>(process.env[e]||"").trim());return 0===e.length?"ok":(w("warn","Proxy",`${e[0]} is set — Node ignores it, so icoa connects directly`,"if direct connections are blocked here, ask the admin to allow practice.icoa2026.au on TCP 443"),"warn")}()),o.push(function(){const e=(process.env.LANG||process.env.LC_ALL||process.env.LC_CTYPE||"").toLowerCase(),o=e.includes("utf-8")||e.includes("utf8"),t=process.stdout.columns||0;return o?t&&t<60?(w("warn","Terminal",`${t} cols — narrow (some charts fall back to text)`,"widen the window to ≥60 columns"),"warn"):(w("ok","Terminal",`UTF-8 · ${t||"?"} cols`),"ok"):(w("warn","Terminal",`locale "${e||"unset"}" is not UTF-8 (charts/QR degrade to ASCII)`,"export LANG=en_US.UTF-8"),"warn")}()),console.log(chalk.gray(" ─────────────────────────────────────────────────────"));const r=o.filter(e=>"fail"===e).length,n=o.filter(e=>"warn"===e).length;0===r&&0===n?console.log(chalk.green(" All clear — you are ready to compete.")):0===r?console.log(chalk.yellow(` ${n} warning${n>1?"s":""} — usable, but see the → fixes above.`)):console.log(chalk.red(` ${r} blocker${r>1?"s":""}`)+(n?chalk.yellow(` + ${n} warning${n>1?"s":""}`):"")+(v?chalk.gray(" — this computer cannot reach the ICOA server. Follow the ⇒ / → lines above; installing anything will not help."):chalk.gray(" — fix the ✗ items first; the → line under each one is the exact command."))),console.log()}