icoa-cli 2.19.355 → 2.19.357

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 (81) hide show
  1. package/dist/commands/ai4ctf.js +1 -1
  2. package/dist/commands/arena.js +1 -1
  3. package/dist/commands/ctf.js +787 -1
  4. package/dist/commands/ctf4ai-demo.js +1 -1
  5. package/dist/commands/ctf4vla.js +1 -1
  6. package/dist/commands/demo2.js +1502 -1
  7. package/dist/commands/exam.js +1 -1
  8. package/dist/commands/files.js +59 -1
  9. package/dist/commands/ipynb.d.ts +10 -4
  10. package/dist/commands/ipynb.js +1 -1
  11. package/dist/commands/lang.js +202 -1
  12. package/dist/commands/log.js +171 -1
  13. package/dist/commands/shell.d.ts +15 -0
  14. package/dist/commands/shell.js +151 -1
  15. package/dist/commands/sim.js +389 -1
  16. package/dist/index.js +355 -1
  17. package/dist/lib/access.js +184 -1
  18. package/dist/lib/aienv.js +205 -1
  19. package/dist/lib/arena-submit.js +21 -1
  20. package/dist/lib/banner.js +31 -1
  21. package/dist/lib/budget.js +6 -1
  22. package/dist/lib/challenge-dir.js +16 -1
  23. package/dist/lib/colors.js +17 -1
  24. package/dist/lib/comms.js +212 -1
  25. package/dist/lib/config.js +93 -1
  26. package/dist/lib/countdown.js +43 -1
  27. package/dist/lib/country-lang.js +39 -1
  28. package/dist/lib/ctfd-client.js +417 -1
  29. package/dist/lib/demo-exam.js +478 -1
  30. package/dist/lib/demo-flags.js +27 -1
  31. package/dist/lib/demo-stats.js +62 -1
  32. package/dist/lib/demo2-progress.js +102 -1
  33. package/dist/lib/docker-probe.d.ts +45 -0
  34. package/dist/lib/docker-probe.js +118 -0
  35. package/dist/lib/editor-spawn.d.ts +23 -0
  36. package/dist/lib/editor-spawn.js +53 -0
  37. package/dist/lib/exam-client.js +54 -1
  38. package/dist/lib/exam-sandbox.js +201 -1
  39. package/dist/lib/exam-setup.js +36 -1
  40. package/dist/lib/exam-state.js +273 -1
  41. package/dist/lib/gemini.js +247 -1
  42. package/dist/lib/i18n.js +302 -1
  43. package/dist/lib/integrity-snapshot.js +88 -1
  44. package/dist/lib/interactive-spawn.js +55 -1
  45. package/dist/lib/ipynb-input.js +65 -1
  46. package/dist/lib/kernel-protocol.js +88 -1
  47. package/dist/lib/kernel.js +146 -2
  48. package/dist/lib/learn-curricula.js +309 -1
  49. package/dist/lib/learn-i18n.js +184 -1
  50. package/dist/lib/learn-input.js +101 -1
  51. package/dist/lib/learn-render.js +863 -1
  52. package/dist/lib/learn-state.js +103 -1
  53. package/dist/lib/log-sync.js +155 -1
  54. package/dist/lib/logger.js +49 -1
  55. package/dist/lib/main-rl.js +7 -1
  56. package/dist/lib/menu-nav.js +105 -1
  57. package/dist/lib/notebook-doc.d.ts +38 -0
  58. package/dist/lib/notebook-doc.js +137 -0
  59. package/dist/lib/open-file.js +55 -1
  60. package/dist/lib/paper-upgrade.js +119 -1
  61. package/dist/lib/platform.js +99 -1
  62. package/dist/lib/render-card.js +112 -1
  63. package/dist/lib/repl-asker.js +67 -1
  64. package/dist/lib/sample-runner.js +227 -1
  65. package/dist/lib/sandbox.d.ts +25 -1
  66. package/dist/lib/sandbox.js +144 -1
  67. package/dist/lib/shell-split.js +69 -1
  68. package/dist/lib/sim-cooldown.js +75 -1
  69. package/dist/lib/theme.js +119 -1
  70. package/dist/lib/token-format.js +74 -1
  71. package/dist/lib/tool-man.js +418 -1
  72. package/dist/lib/toolset-hash.js +48 -1
  73. package/dist/lib/translation.js +80 -1
  74. package/dist/lib/translations-fetcher.js +95 -1
  75. package/dist/lib/ui.js +99 -1
  76. package/dist/lib/update-check.js +114 -1
  77. package/dist/lib/version.js +24 -1
  78. package/dist/postinstall.js +48 -1
  79. package/dist/repl.js +2391 -1
  80. package/dist/types/index.js +63 -1
  81. package/package.json +1 -1
@@ -1 +1,95 @@
1
- import chalk from"chalk";import{spawn as t}from"node:child_process";import{existsSync as n,mkdirSync as r,writeFileSync as a}from"node:fs";import{join as e}from"node:path";import{getIcoaDir as o}from"./config.js";const s=["https://practice.icoa2026.au/assets/translations","https://au.icoa2026.au/assets/translations"];export function getLangCacheDir(t){return e(o(),"translations",t)}export function isLangCached(t){if("en"===t)return!0;const r=getLangCacheDir(t);return n(e(r,".cached"))}export async function ensureLangCache(n,o={}){if("en"===n)return!0;if(isLangCached(n))return!0;const i=getLangCacheDir(n);r(i,{recursive:!0}),o.silent||console.log(chalk.gray(` Fetching ${n} translation pack ...`));let c=null;for(const r of s)try{const s=`${r}/${n}.tar.gz`,l=await fetch(s);if(!l.ok){c=`HTTP ${l.status}`;continue}const f=e(i,`${n}.tar.gz`);return a(f,Buffer.from(await l.arrayBuffer())),await new Promise((n,r)=>{const a=t("tar",["-xzf",f,"-C",i],{stdio:"ignore"});a.on("close",t=>0===t?n():r(new Error(`tar exit ${t}`))),a.on("error",r)}),a(e(i,".cached"),(new Date).toISOString()),o.silent||console.log(chalk.green(` ✓ ${n} translations cached at ~/.icoa/translations/${n}/`)),!0}catch(t){c=t instanceof Error?t.message:String(t)}return o.silent||console.log(chalk.yellow(` Translation pack not available (${c??"all mirrors unreachable"}). The interface stays in English; per-question translations fall back to the on-the-fly translator.`)),!1}
1
+ /**
2
+ * Lazy translation-pack fetcher.
3
+ *
4
+ * v2.19.197 moved per-language translation files (translations/<lang>/*.json)
5
+ * out of the npm package, saving ~280 KB. They live on icoa2026.au and get
6
+ * fetched + extracted under ~/.icoa/translations/<lang>/ on first lang switch.
7
+ *
8
+ * - English is bundled in source (no fetch needed)
9
+ * - All other supported langs ship as <lang>.tar.gz on icoa2026.au
10
+ * - Per-lang tarballs are 4–17 KB compressed; single HTTP round-trip
11
+ * - A `.cached` sentinel file marks completion (idempotent re-runs)
12
+ * - Extraction shells out to `tar -xzf` — present on macOS, Linux, WSL2
13
+ */
14
+ import chalk from 'chalk';
15
+ import { spawn } from 'node:child_process';
16
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { getIcoaDir } from './config.js';
19
+ // Multi-source fallback: try each mirror in order before giving up to the
20
+ // on-the-fly translator. Two boxes on different IPs = real redundancy, so a
21
+ // single host being down (or its asset path not yet provisioned) no longer
22
+ // drops non-EN users to live Gemini. Same single git source builds both packs
23
+ // (scripts/build-translation-packs.sh --push sha256-gates them identical), so
24
+ // whichever mirror answers first serves byte-identical content.
25
+ //
26
+ // Hosts are the two boxes we hold SSH creds for → fully automatable publish
27
+ // (the retired icoa2026.au website had no SSH access, only a deploy webhook):
28
+ // primary = practice (public, always-on, NOT frozen, where learn already points)
29
+ // backup = mel/au (competition box). Safe as a backup because (1) the client
30
+ // only reaches it if practice is down, and (2) packs cache once (.cached) at
31
+ // language-switch time — BEFORE the exam — so exam-time fetch traffic ≈ 0,
32
+ // neutralising the single-venue-IP fail2ban risk.
33
+ const TRANSLATIONS_BASE_URLS = [
34
+ 'https://practice.icoa2026.au/assets/translations',
35
+ 'https://au.icoa2026.au/assets/translations',
36
+ ];
37
+ export function getLangCacheDir(lang) {
38
+ return join(getIcoaDir(), 'translations', lang);
39
+ }
40
+ export function isLangCached(lang) {
41
+ if (lang === 'en')
42
+ return true;
43
+ const dir = getLangCacheDir(lang);
44
+ // RULE: the menu / UI chrome stays English-only — non-EN `ui.json` packs are
45
+ // intentionally NOT served from the asset host, so t() falls back to EN per key
46
+ // for chrome. Lazy-load (the `lang` command) only translates question + demo
47
+ // content. So the cache is valid on the `.cached` sentinel alone; do NOT also
48
+ // require ui.json here — it will never be present, which would force a full
49
+ // pack re-download on every boot for non-EN users.
50
+ return existsSync(join(dir, '.cached'));
51
+ }
52
+ export async function ensureLangCache(lang, opts = {}) {
53
+ if (lang === 'en')
54
+ return true;
55
+ if (isLangCached(lang))
56
+ return true;
57
+ const cacheDir = getLangCacheDir(lang);
58
+ mkdirSync(cacheDir, { recursive: true });
59
+ if (!opts.silent) {
60
+ console.log(chalk.gray(` Fetching ${lang} translation pack ...`));
61
+ }
62
+ // Try each mirror in order; the first that returns the tarball wins.
63
+ let lastErr = null;
64
+ for (const base of TRANSLATIONS_BASE_URLS) {
65
+ try {
66
+ const url = `${base}/${lang}.tar.gz`;
67
+ const res = await fetch(url);
68
+ if (!res.ok) {
69
+ lastErr = `HTTP ${res.status}`;
70
+ continue; // try next mirror
71
+ }
72
+ const tgzPath = join(cacheDir, `${lang}.tar.gz`);
73
+ writeFileSync(tgzPath, Buffer.from(await res.arrayBuffer()));
74
+ await new Promise((resolve, reject) => {
75
+ const proc = spawn('tar', ['-xzf', tgzPath, '-C', cacheDir], { stdio: 'ignore' });
76
+ proc.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`tar exit ${code}`))));
77
+ proc.on('error', reject);
78
+ });
79
+ writeFileSync(join(cacheDir, '.cached'), new Date().toISOString());
80
+ if (!opts.silent) {
81
+ console.log(chalk.green(` ✓ ${lang} translations cached at ~/.icoa/translations/${lang}/`));
82
+ }
83
+ return true;
84
+ }
85
+ catch (e) {
86
+ lastErr = e instanceof Error ? e.message : String(e);
87
+ // try next mirror
88
+ }
89
+ }
90
+ // All mirrors exhausted → fall back to the on-the-fly translator.
91
+ if (!opts.silent) {
92
+ console.log(chalk.yellow(` Translation pack not available (${lastErr ?? 'all mirrors unreachable'}). The interface stays in English; per-question translations fall back to the on-the-fly translator.`));
93
+ }
94
+ return false;
95
+ }
package/dist/lib/ui.js CHANGED
@@ -1 +1,99 @@
1
- import chalk from"chalk";import o from"cli-table3";import ora from"ora";import{Marked as t}from"marked";import{markedTerminal as n}from"marked-terminal";import{c as e}from"./colors.js";const r=new t(n());export function printSuccess(o){console.log(chalk.green("✓ ")+e.fg(o))}export function printError(o){console.log(chalk.red("✗ ")+e.fg(o))}export function printWarning(o){console.log(chalk.yellow("⚠ ")+e.fg(o))}export function printInfo(o){console.log(chalk.blue("ℹ ")+e.fg(o))}export function printTable(t,n){const e=new o({head:t.map(o=>chalk.cyan.bold(o)),style:{head:[],border:[]}});for(const o of n)e.push(o);console.log(e.toString())}export function printMarkdown(o){const t=r.parse(o);"string"==typeof t&&console.log(t)}let l=!1;export function setReplMode(o){l=o}export function createSpinner(o){if(l){const t={text:o,start:()=>(console.log(chalk.cyan(` ${o}`)),t),stop:()=>t,succeed:o=>(console.log(chalk.green(` ✓ ${o}`)),t),fail:o=>(console.log(chalk.red(` ✗ ${o}`)),t),info:o=>(console.log(chalk.blue(` ℹ ${o}`)),t),warn:o=>(console.log(chalk.yellow(` ⚠ ${o}`)),t)};return t}return ora({text:o,color:"cyan"})}export function formatCountdown(o){const t=new Date,n=o.getTime()-t.getTime();if(n<=0)return"00:00:00";const e=Math.floor(n/36e5),r=Math.floor(n%36e5/6e4),l=Math.floor(n%6e4/1e3);return[e.toString().padStart(2,"0"),r.toString().padStart(2,"0"),l.toString().padStart(2,"0")].join(":")}export function printHeader(o){console.log(),console.log(chalk.cyan.bold(` ${o}`)),console.log(chalk.cyan(` ${"─".repeat(o.length+4)}`))}export function printKeyValue(o,t){console.log(` ${chalk.gray(`${o}:`)} ${e.fg(t)}`)}
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import ora from 'ora';
4
+ import { Marked } from 'marked';
5
+ import { markedTerminal } from 'marked-terminal';
6
+ import { c } from './colors.js';
7
+ const marked = new Marked(markedTerminal());
8
+ // Wrap the message body in explicit Darcula fg (#A9B7C6). Without this,
9
+ // chalk.green('✓ ') emits \x1b[39m at the end and the unstyled msg falls
10
+ // back to the terminal's profile fg — which is black on macOS Terminal.app
11
+ // default, invisible on our forced #2B2B2B background.
12
+ export function printSuccess(msg) {
13
+ console.log(chalk.green('✓ ') + c.fg(msg));
14
+ }
15
+ export function printError(msg) {
16
+ console.log(chalk.red('✗ ') + c.fg(msg));
17
+ }
18
+ export function printWarning(msg) {
19
+ console.log(chalk.yellow('⚠ ') + c.fg(msg));
20
+ }
21
+ export function printInfo(msg) {
22
+ console.log(chalk.blue('ℹ ') + c.fg(msg));
23
+ }
24
+ export function printTable(headers, rows) {
25
+ const table = new Table({
26
+ head: headers.map((h) => chalk.cyan.bold(h)),
27
+ style: { head: [], border: [] },
28
+ });
29
+ for (const row of rows) {
30
+ table.push(row);
31
+ }
32
+ console.log(table.toString());
33
+ }
34
+ export function printMarkdown(text) {
35
+ const rendered = marked.parse(text);
36
+ if (typeof rendered === 'string') {
37
+ console.log(rendered);
38
+ }
39
+ }
40
+ // In REPL mode, spinners conflict with readline — use simple log instead
41
+ let replMode = false;
42
+ export function setReplMode(enabled) {
43
+ replMode = enabled;
44
+ }
45
+ export function createSpinner(text) {
46
+ if (replMode) {
47
+ // Fake spinner that just logs — no terminal cursor manipulation
48
+ const fake = {
49
+ text,
50
+ start() {
51
+ console.log(chalk.cyan(` ${text}`));
52
+ return fake;
53
+ },
54
+ stop() {
55
+ return fake;
56
+ },
57
+ succeed(msg) {
58
+ console.log(chalk.green(` ✓ ${msg}`));
59
+ return fake;
60
+ },
61
+ fail(msg) {
62
+ console.log(chalk.red(` ✗ ${msg}`));
63
+ return fake;
64
+ },
65
+ info(msg) {
66
+ console.log(chalk.blue(` ℹ ${msg}`));
67
+ return fake;
68
+ },
69
+ warn(msg) {
70
+ console.log(chalk.yellow(` ⚠ ${msg}`));
71
+ return fake;
72
+ },
73
+ };
74
+ return fake;
75
+ }
76
+ return ora({ text, color: 'cyan' });
77
+ }
78
+ export function formatCountdown(targetDate) {
79
+ const now = new Date();
80
+ const diff = targetDate.getTime() - now.getTime();
81
+ if (diff <= 0)
82
+ return '00:00:00';
83
+ const hours = Math.floor(diff / (1000 * 60 * 60));
84
+ const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
85
+ const seconds = Math.floor((diff % (1000 * 60)) / 1000);
86
+ return [
87
+ hours.toString().padStart(2, '0'),
88
+ minutes.toString().padStart(2, '0'),
89
+ seconds.toString().padStart(2, '0'),
90
+ ].join(':');
91
+ }
92
+ export function printHeader(title) {
93
+ console.log();
94
+ console.log(chalk.cyan.bold(` ${title}`));
95
+ console.log(chalk.cyan(` ${'─'.repeat(title.length + 4)}`));
96
+ }
97
+ export function printKeyValue(key, value) {
98
+ console.log(` ${chalk.gray(`${key}:`)} ${c.fg(value)}`);
99
+ }
@@ -1 +1,114 @@
1
- import{readFileSync as o,writeFileSync as t,existsSync as e}from"node:fs";import{join as n,dirname as s,sep as l}from"node:path";import{fileURLToPath as r}from"node:url";import{homedir as i,platform as a}from"node:os";import chalk from"chalk";import{getIcoaDir as c}from"./config.js";const m=s(r(import.meta.url)),p=m,u=p.startsWith(i()+l)||p.includes(`${l}.nvm${l}`)||p.includes(`${l}.fnm${l}`)||p.includes(`${l}.volta${l}`),g="win32"===a()||"darwin"===a()||u?"npm install -g icoa-cli@latest":"sudo npm install -g icoa-cli@latest";function f(o,t){const e=function(o,t){const e=o.split(".").map(Number),n=t.split(".").map(Number);return e[0]!==n[0]||e[1]!==n[1]?-1:(e[2]??0)-(n[2]??0)}(t,o),n=e>0?chalk.gray(` (${e} version${1===e?"":"s"} behind)`):"",s=chalk.yellow(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");console.log(),console.log(s),console.log(chalk.bold.yellow(" ⬆ New version available!")),console.log(),console.log(chalk.white(" Current: ")+chalk.gray(`v${o}`)),console.log(chalk.white(" Latest: ")+chalk.bold.green(`v${t}`)+n),console.log(),console.log(chalk.white(" Update: ")+chalk.bold.cyan(g));const l="darwin"===a()?"Cmd+C":"win32"===a()?"Ctrl+C":"Ctrl+Shift+C";console.log(chalk.gray(` ↑ select & ${l} to copy — typos break it`)),console.log(s),console.log()}function d(o,t){const e=o.split(".").map(Number),n=t.split(".").map(Number);for(let o=0;o<Math.max(e.length,n.length);o++){const t=e[o]??0,s=n[o]??0;if(t>s)return!0;if(t<s)return!1}return!1}export function checkForUpdates(){setTimeout(async()=>{try{const s=n(m,"..","..","package.json"),l=JSON.parse(o(s,"utf-8")).version,r=n(c(),"update-check.json");if(e(r))try{const t=JSON.parse(o(r,"utf-8"));if(Date.now()-t.lastCheck<216e5)return void(t.latestVersion&&d(t.latestVersion,l)&&f(l,t.latestVersion))}catch{}const i=new AbortController,a=setTimeout(()=>i.abort(),5e3),p=await fetch("https://registry.npmjs.org/icoa-cli/latest",{signal:i.signal,headers:{Accept:"application/json"}});if(clearTimeout(a),!p.ok)return;const u=(await p.json()).version;if(!u)return;const g={lastCheck:Date.now(),latestVersion:u};t(r,JSON.stringify(g,null,2)),d(u,l)&&f(l,u)}catch{}},0)}
1
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
2
+ import { join, dirname, sep } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { homedir, platform } from 'node:os';
5
+ import chalk from 'chalk';
6
+ import { getIcoaDir } from './config.js';
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ // WSL2 + Linux nvm / fnm / volta installs put icoa under $HOME and DO NOT
9
+ // want sudo (sudo runs root's npm — usually missing — or installs to a
10
+ // different prefix the user's PATH never resolves, leaving the banner stuck
11
+ // at "N versions behind" forever). Detect user-scope by checking whether the
12
+ // install path is under $HOME (or one of the well-known per-user node version
13
+ // manager directories). macOS Homebrew lives under /opt/homebrew which is
14
+ // writable without sudo by design, so the explicit darwin branch stays.
15
+ const installPath = __dirname;
16
+ const isUserScopedInstall = installPath.startsWith(homedir() + sep) ||
17
+ installPath.includes(`${sep}.nvm${sep}`) ||
18
+ installPath.includes(`${sep}.fnm${sep}`) ||
19
+ installPath.includes(`${sep}.volta${sep}`);
20
+ const installCmd = platform() === 'win32' || platform() === 'darwin' || isUserScopedInstall
21
+ ? 'npm install -g icoa-cli@latest'
22
+ : 'sudo npm install -g icoa-cli@latest';
23
+ const SIX_HOURS_MS = 6 * 60 * 60 * 1000;
24
+ function versionsBehind(latest, current) {
25
+ // Count PATCH-level diff only (e.g. 2.19.40 vs 2.19.46 = 6)
26
+ const l = latest.split('.').map(Number);
27
+ const c = current.split('.').map(Number);
28
+ if (l[0] !== c[0] || l[1] !== c[1])
29
+ return -1; // major/minor diff
30
+ return (l[2] ?? 0) - (c[2] ?? 0);
31
+ }
32
+ function printUpdateBanner(current, latest) {
33
+ const behind = versionsBehind(latest, current);
34
+ const behindStr = behind > 0 ? chalk.gray(` (${behind} version${behind === 1 ? '' : 's'} behind)`) : '';
35
+ const line = chalk.yellow(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
36
+ console.log();
37
+ console.log(line);
38
+ console.log(chalk.bold.yellow(' ⬆ New version available!'));
39
+ console.log();
40
+ console.log(chalk.white(' Current: ') + chalk.gray(`v${current}`));
41
+ console.log(chalk.white(' Latest: ') + chalk.bold.green(`v${latest}`) + behindStr);
42
+ console.log();
43
+ console.log(chalk.white(' Update: ') + chalk.bold.cyan(installCmd));
44
+ const copyKey = platform() === 'darwin' ? 'Cmd+C' : platform() === 'win32' ? 'Ctrl+C' : 'Ctrl+Shift+C';
45
+ console.log(chalk.gray(` ↑ select & ${copyKey} to copy — typos break it`));
46
+ console.log(line);
47
+ console.log();
48
+ }
49
+ function isNewer(latest, current) {
50
+ const l = latest.split('.').map(Number);
51
+ const c = current.split('.').map(Number);
52
+ for (let i = 0; i < Math.max(l.length, c.length); i++) {
53
+ const lv = l[i] ?? 0;
54
+ const cv = c[i] ?? 0;
55
+ if (lv > cv)
56
+ return true;
57
+ if (lv < cv)
58
+ return false;
59
+ }
60
+ return false;
61
+ }
62
+ export function checkForUpdates() {
63
+ // Fire-and-forget async check
64
+ setTimeout(async () => {
65
+ try {
66
+ const pkgPath = join(__dirname, '..', '..', 'package.json');
67
+ const current = JSON.parse(readFileSync(pkgPath, 'utf-8')).version;
68
+ const cacheFile = join(getIcoaDir(), 'update-check.json');
69
+ // Check if we already checked recently
70
+ if (existsSync(cacheFile)) {
71
+ try {
72
+ const cache = JSON.parse(readFileSync(cacheFile, 'utf-8'));
73
+ if (Date.now() - cache.lastCheck < SIX_HOURS_MS) {
74
+ // Still within cooldown — but show banner if cached version is newer
75
+ if (cache.latestVersion && isNewer(cache.latestVersion, current)) {
76
+ printUpdateBanner(current, cache.latestVersion);
77
+ }
78
+ return;
79
+ }
80
+ }
81
+ catch {
82
+ // Corrupted cache, continue with fresh check
83
+ }
84
+ }
85
+ // Fetch latest version from npm with 5-second timeout
86
+ const controller = new AbortController();
87
+ const timeout = setTimeout(() => controller.abort(), 5000);
88
+ const res = await fetch('https://registry.npmjs.org/icoa-cli/latest', {
89
+ signal: controller.signal,
90
+ headers: { Accept: 'application/json' },
91
+ });
92
+ clearTimeout(timeout);
93
+ if (!res.ok)
94
+ return;
95
+ const data = (await res.json());
96
+ const latest = data.version;
97
+ if (!latest)
98
+ return;
99
+ // Save cache
100
+ const cache = {
101
+ lastCheck: Date.now(),
102
+ latestVersion: latest,
103
+ };
104
+ writeFileSync(cacheFile, JSON.stringify(cache, null, 2));
105
+ // Print banner if newer
106
+ if (isNewer(latest, current)) {
107
+ printUpdateBanner(current, latest);
108
+ }
109
+ }
110
+ catch {
111
+ // Completely silent on any failure
112
+ }
113
+ }, 0);
114
+ }
@@ -1 +1,24 @@
1
- import{readFileSync as n}from"node:fs";import{dirname as o,join as r}from"node:path";import{fileURLToPath as t}from"node:url";const e=o(t(import.meta.url));let i=null;export function getCliVersion(){if(i)return i;try{const o=JSON.parse(n(r(e,"..","..","package.json"),"utf-8"));i=o.version||"unknown"}catch{i="unknown"}return i}
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const __dirname_version = dirname(fileURLToPath(import.meta.url));
5
+ let _cached = null;
6
+ /**
7
+ * Returns the running CLI's package.json version, cached after first call.
8
+ * Used by interaction events + log-sync payload so future forensic audits
9
+ * can pinpoint which CLI version produced any given event. Added in
10
+ * v2.19.185 in response to the Paper E dispatcher bug, where we couldn't
11
+ * tell from the data which contestants ran pre-fix vs post-fix code.
12
+ */
13
+ export function getCliVersion() {
14
+ if (_cached)
15
+ return _cached;
16
+ try {
17
+ const pkg = JSON.parse(readFileSync(join(__dirname_version, '..', '..', 'package.json'), 'utf-8'));
18
+ _cached = pkg.version || 'unknown';
19
+ }
20
+ catch {
21
+ _cached = 'unknown';
22
+ }
23
+ return _cached;
24
+ }
@@ -1,2 +1,49 @@
1
1
  #!/usr/bin/env node
2
- const o=["Initializing ICOA CLI...","Loading competition modules...","Configuring exam system...","Setting up references...","Finalizing installation..."];function l(o,l){const e=Math.round(o/100*30),n=30-e,m=`${"█".repeat(e)}${"░".repeat(n)}`,t=`${String(o).padStart(3)}%`;process.stdout.write(`\r ${m} ${t} ${l}`)}(async function(){console.log(),console.log(" ██╗ ██████╗ ██████╗ █████╗"),console.log(" ██║██╔════╝██╔═══██╗██╔══██╗"),console.log(" ██║██║ ██║ ██║███████║"),console.log(" ██║██║ ██║ ██║██╔══██║"),console.log(" ██║╚██████╗╚██████╔╝██║ ██║"),console.log(" ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝"),console.log();for(let e=0;e<o.length;e++){const n=Math.round(e/o.length*100),m=Math.round((e+1)/o.length*100);for(let t=n;t<=m;t++)l(t,o[e]),await new Promise(o=>setTimeout(o,15))}console.log(),console.log(),console.log(" ✓ ICOA CLI installed successfully!"),console.log(),console.log(" Get started:"),console.log(" icoa Launch and select your mode"),console.log(" icoa --help Show all commands"),console.log()})().catch(()=>{});export{};
2
+ /**
3
+ * Post-install script: shows a progress bar during ICOA CLI setup.
4
+ * Runs automatically after `npm install -g icoa-cli`.
5
+ */
6
+ const steps = [
7
+ 'Initializing ICOA CLI...',
8
+ 'Loading competition modules...',
9
+ 'Configuring exam system...',
10
+ 'Setting up references...',
11
+ 'Finalizing installation...',
12
+ ];
13
+ const BAR_WIDTH = 30;
14
+ const TOTAL = 100;
15
+ function drawBar(percent, label) {
16
+ const filled = Math.round((percent / TOTAL) * BAR_WIDTH);
17
+ const empty = BAR_WIDTH - filled;
18
+ const bar = `\x1b[32m${'█'.repeat(filled)}\x1b[90m${'░'.repeat(empty)}\x1b[0m`;
19
+ const pct = `${String(percent).padStart(3)}%`;
20
+ process.stdout.write(`\r ${bar} ${pct} \x1b[90m${label}\x1b[0m`);
21
+ }
22
+ async function run() {
23
+ console.log();
24
+ console.log(' \x1b[1m\x1b[37m██╗ ██████╗ ██████╗ █████╗\x1b[0m');
25
+ console.log(' \x1b[1m\x1b[37m██║██╔════╝██╔═══██╗██╔══██╗\x1b[0m');
26
+ console.log(' \x1b[1m\x1b[37m██║██║ ██║ ██║███████║\x1b[0m');
27
+ console.log(' \x1b[1m\x1b[37m██║██║ ██║ ██║██╔══██║\x1b[0m');
28
+ console.log(' \x1b[1m\x1b[37m██║╚██████╗╚██████╔╝██║ ██║\x1b[0m');
29
+ console.log(' \x1b[1m\x1b[37m╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝\x1b[0m');
30
+ console.log();
31
+ for (let i = 0; i < steps.length; i++) {
32
+ const startPct = Math.round((i / steps.length) * TOTAL);
33
+ const endPct = Math.round(((i + 1) / steps.length) * TOTAL);
34
+ for (let p = startPct; p <= endPct; p++) {
35
+ drawBar(p, steps[i]);
36
+ await new Promise((r) => setTimeout(r, 15));
37
+ }
38
+ }
39
+ console.log();
40
+ console.log();
41
+ console.log(' \x1b[32m✓\x1b[0m ICOA CLI installed successfully!');
42
+ console.log();
43
+ console.log(' \x1b[90mGet started:\x1b[0m');
44
+ console.log(' \x1b[1m\x1b[37micoa\x1b[0m \x1b[90mLaunch and select your mode\x1b[0m');
45
+ console.log(' \x1b[1m\x1b[37micoa --help\x1b[0m \x1b[90mShow all commands\x1b[0m');
46
+ console.log();
47
+ }
48
+ run().catch(() => { });
49
+ export {};