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.
- package/dist/commands/ai4ctf.js +1 -1
- package/dist/commands/arena.js +1 -1
- package/dist/commands/ctf.js +787 -1
- package/dist/commands/ctf4ai-demo.js +1 -1
- package/dist/commands/ctf4vla.js +1 -1
- package/dist/commands/demo2.js +1502 -1
- package/dist/commands/exam.js +1 -1
- package/dist/commands/files.js +59 -1
- package/dist/commands/ipynb.d.ts +10 -4
- package/dist/commands/ipynb.js +1 -1
- package/dist/commands/lang.js +202 -1
- package/dist/commands/log.js +171 -1
- package/dist/commands/shell.d.ts +15 -0
- package/dist/commands/shell.js +151 -1
- package/dist/commands/sim.js +389 -1
- package/dist/index.js +355 -1
- package/dist/lib/access.js +184 -1
- package/dist/lib/aienv.js +205 -1
- package/dist/lib/arena-submit.js +21 -1
- package/dist/lib/banner.js +31 -1
- package/dist/lib/budget.js +6 -1
- package/dist/lib/challenge-dir.js +16 -1
- package/dist/lib/colors.js +17 -1
- package/dist/lib/comms.js +212 -1
- package/dist/lib/config.js +93 -1
- package/dist/lib/countdown.js +43 -1
- package/dist/lib/country-lang.js +39 -1
- package/dist/lib/ctfd-client.js +417 -1
- package/dist/lib/demo-exam.js +478 -1
- package/dist/lib/demo-flags.js +27 -1
- package/dist/lib/demo-stats.js +62 -1
- package/dist/lib/demo2-progress.js +102 -1
- package/dist/lib/docker-probe.d.ts +45 -0
- package/dist/lib/docker-probe.js +118 -0
- package/dist/lib/editor-spawn.d.ts +23 -0
- package/dist/lib/editor-spawn.js +53 -0
- package/dist/lib/exam-client.js +54 -1
- package/dist/lib/exam-sandbox.js +201 -1
- package/dist/lib/exam-setup.js +36 -1
- package/dist/lib/exam-state.js +273 -1
- package/dist/lib/gemini.js +247 -1
- package/dist/lib/i18n.js +302 -1
- package/dist/lib/integrity-snapshot.js +88 -1
- package/dist/lib/interactive-spawn.js +55 -1
- package/dist/lib/ipynb-input.js +65 -1
- package/dist/lib/kernel-protocol.js +88 -1
- package/dist/lib/kernel.js +146 -2
- package/dist/lib/learn-curricula.js +309 -1
- package/dist/lib/learn-i18n.js +184 -1
- package/dist/lib/learn-input.js +101 -1
- package/dist/lib/learn-render.js +863 -1
- package/dist/lib/learn-state.js +103 -1
- package/dist/lib/log-sync.js +155 -1
- package/dist/lib/logger.js +49 -1
- package/dist/lib/main-rl.js +7 -1
- package/dist/lib/menu-nav.js +105 -1
- package/dist/lib/notebook-doc.d.ts +38 -0
- package/dist/lib/notebook-doc.js +137 -0
- package/dist/lib/open-file.js +55 -1
- package/dist/lib/paper-upgrade.js +119 -1
- package/dist/lib/platform.js +99 -1
- package/dist/lib/render-card.js +112 -1
- package/dist/lib/repl-asker.js +67 -1
- package/dist/lib/sample-runner.js +227 -1
- package/dist/lib/sandbox.d.ts +25 -1
- package/dist/lib/sandbox.js +144 -1
- package/dist/lib/shell-split.js +69 -1
- package/dist/lib/sim-cooldown.js +75 -1
- package/dist/lib/theme.js +119 -1
- package/dist/lib/token-format.js +74 -1
- package/dist/lib/tool-man.js +418 -1
- package/dist/lib/toolset-hash.js +48 -1
- package/dist/lib/translation.js +80 -1
- package/dist/lib/translations-fetcher.js +95 -1
- package/dist/lib/ui.js +99 -1
- package/dist/lib/update-check.js +114 -1
- package/dist/lib/version.js +24 -1
- package/dist/postinstall.js +48 -1
- package/dist/repl.js +2391 -1
- package/dist/types/index.js +63 -1
- package/package.json +1 -1
|
@@ -1 +1,95 @@
|
|
|
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
|
|
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
|
+
}
|
package/dist/lib/update-check.js
CHANGED
|
@@ -1 +1,114 @@
|
|
|
1
|
-
import
|
|
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
|
+
}
|
package/dist/lib/version.js
CHANGED
|
@@ -1 +1,24 @@
|
|
|
1
|
-
import{readFileSync
|
|
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
|
+
}
|
package/dist/postinstall.js
CHANGED
|
@@ -1,2 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
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 {};
|