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
package/dist/commands/files.js
CHANGED
|
@@ -1 +1,59 @@
|
|
|
1
|
-
import chalk from
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { CTFdClient } from '../lib/ctfd-client.js';
|
|
3
|
+
import { getConfig, isConnected } from '../lib/config.js';
|
|
4
|
+
import { challengeDownloadDir } from '../lib/challenge-dir.js';
|
|
5
|
+
import { logCommand } from '../lib/logger.js';
|
|
6
|
+
import { printError, createSpinner } from '../lib/ui.js';
|
|
7
|
+
export function registerFilesCommand(program) {
|
|
8
|
+
program
|
|
9
|
+
.command('files <id>')
|
|
10
|
+
.description('Download challenge files')
|
|
11
|
+
.action(async (id) => {
|
|
12
|
+
logCommand(`files ${id}`);
|
|
13
|
+
const config = getConfig();
|
|
14
|
+
if (!isConnected()) {
|
|
15
|
+
printError('Not connected. Run: join <url>');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
// Session-mode logins store the cookie string in config.token; passing it
|
|
19
|
+
// as the API token sends `Authorization: Token session=…` → CTFd serves the
|
|
20
|
+
// login page (HTML) and JSON parsing fails. Split token vs session cookie.
|
|
21
|
+
const session = config.sessionCookie || '';
|
|
22
|
+
const token = config.token && !config.token.includes('session=') ? config.token : '';
|
|
23
|
+
const client = new CTFdClient(config.ctfdUrl, token, session || config.token);
|
|
24
|
+
const destDir = challengeDownloadDir(id);
|
|
25
|
+
const spinner = createSpinner('Fetching challenge files...');
|
|
26
|
+
spinner.start();
|
|
27
|
+
try {
|
|
28
|
+
const files = await client.getChallengeFiles(parseInt(id, 10));
|
|
29
|
+
if (!files || files.length === 0) {
|
|
30
|
+
spinner.info('No files attached to this challenge.');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
spinner.text = `Downloading ${files.length} file(s)...`;
|
|
34
|
+
const downloaded = [];
|
|
35
|
+
for (const filePath of files) {
|
|
36
|
+
try {
|
|
37
|
+
const dest = await client.downloadFile(filePath, destDir);
|
|
38
|
+
downloaded.push(dest);
|
|
39
|
+
}
|
|
40
|
+
catch (_err) {
|
|
41
|
+
spinner.warn(`Failed to download: ${filePath}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
spinner.succeed(`Downloaded ${downloaded.length} file(s)`);
|
|
45
|
+
console.log(chalk.gray(` Location: ${destDir}`));
|
|
46
|
+
for (const f of downloaded) {
|
|
47
|
+
console.log(chalk.gray(` → ${f.split('/').pop()}`));
|
|
48
|
+
}
|
|
49
|
+
const firstName = downloaded.length ? downloaded[0].split('/').pop() : '<file>';
|
|
50
|
+
console.log(chalk.gray(` Run tools on them directly, e.g. ${chalk.white(`file ${id}/${firstName}`)}`));
|
|
51
|
+
console.log(chalk.gray(` Open an image/audio file with: ${chalk.white(`view ${id} <file>`)}`));
|
|
52
|
+
console.log();
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
spinner.fail('Failed to download files');
|
|
56
|
+
printError(err.message);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
package/dist/commands/ipynb.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `icoa ipynb` — an in-terminal Python notebook (Phase 1 UI of the CLI
|
|
3
|
-
* arena, see `project_cli_notebook_arena_plan`). The free, ungraded
|
|
4
|
-
* "a free Colab in your terminal" — and the cell surface the scored
|
|
5
|
-
* will later wrap.
|
|
2
|
+
* `icoa ipynb [file]` — an in-terminal Python notebook (Phase 1 UI of the CLI
|
|
3
|
+
* notebook arena, see `project_cli_notebook_arena_plan`). The free, ungraded
|
|
4
|
+
* engine — "a free Colab in your terminal" — and the cell surface the scored
|
|
5
|
+
* `arena` will later wrap.
|
|
6
|
+
*
|
|
7
|
+
* Cell-stepper: the session is a real document. Typed blocks append as cells;
|
|
8
|
+
* `cells / show / edit / add / del / run / save` step through them, `edit N`
|
|
9
|
+
* hands the terminal to $EDITOR (nano/vi) for that one cell, `save` writes a
|
|
10
|
+
* Jupyter-openable .ipynb. Opening a `.py` splits it on `# %%` markers (the
|
|
11
|
+
* arena starter ships with them).
|
|
6
12
|
*
|
|
7
13
|
* Runs against the aienv venv kernel via NotebookKernel (Node ↔ Python bridge).
|
|
8
14
|
* Lives in the main REPL using the BUG-008 listener-swap (no second readline).
|
package/dist/commands/ipynb.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{existsSync as e,mkdirSync as o,writeFileSync as n}from"node:fs";import{homedir as t}from"node:os";import{createInterface as r}from"node:readline";import{join as l}from"node:path";import chalk from"chalk";import{aienvPaths as a}from"../lib/aienv.js";import{shouldExecuteCell as i}from"../lib/ipynb-input.js";import{NotebookKernel as s}from"../lib/kernel.js";import{getMainRl as c}from"../lib/main-rl.js";import{openFile as g}from"../lib/open-file.js";const y=new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`,"g");export function registerIpynbCommand(o){o.command("ipynb").description("Open an in-terminal Python notebook (AI/ML — needs `aienv setup`)").action(async()=>{await async function(){const o=a(t()),n=l(t(),".icoa","ipynb-out");if(p(),!e(o.python))return console.log(chalk.yellow(" The AI/ML environment is not set up yet.")),console.log(chalk.gray(" Run ")+chalk.bold.cyan("aienv setup")+chalk.gray(" first (one-time, ~300 MB).")),void console.log();const f=new s({venvPython:o.python,venvRoot:o.root});process.stdout.write(chalk.gray(" Starting Python kernel…"));try{await f.start()}catch{return console.log(chalk.red(" failed.")),console.log(chalk.gray(" Check the environment: ")+chalk.cyan("aienv status")),void console.log()}console.log(chalk.green(" ready.")),console.log(chalk.gray(" Type Python and press Enter. Blocks (def/for/…) continue until a blank line.")),console.log(chalk.gray(" ")+chalk.cyan("help")+chalk.gray(" · ")+chalk.cyan("clear")+chalk.gray(" · ")+chalk.cyan("restart")+chalk.gray(" (fresh kernel) · ")+chalk.cyan("quit")),console.log();const m=c(),h=null!==m,v=h?m.listeners("line").slice():[];h&&m.removeAllListeners("line");const w=h?m:r({input:process.stdin,output:process.stdout,terminal:!0}),b=[];let k=!1;const x=()=>{w.setPrompt(chalk.bold.cyan(b.length?"icoa ipynb ...> ":"icoa ipynb> ")),w.prompt()};let P=()=>{};const $=new Promise(e=>{P=e}),C=async e=>{if(k)return;const o=e.trim();if(0===b.length){const e=o.toLowerCase();if("quit"===e||"exit"===e||"back"===e||"q"===e)return void await(async()=>{if(w.removeAllListeners("line"),console.log(),console.log(chalk.gray(" Notebook closed. Kernel stopped.")),console.log(),await f.shutdown().catch(()=>{}),h){w.setPrompt(chalk.bold.cyan("icoa> "));for(const e of v)w.on("line",e);w.prompt(),P()}else P(),w.close()})();if("clear"===e||"cls"===e)return console.clear(),p(),void x();if("restart"===e)return await(async()=>{process.stdout.write(chalk.gray(" Restarting kernel…")),await f.shutdown().catch(()=>{});try{await f.start(),console.log(chalk.green(" fresh kernel ready.")+chalk.gray(" (all variables cleared)"))}catch{console.log(chalk.red(" restart failed.")+chalk.gray(" Leave and re-enter the notebook."))}})(),void x();if("help"===e||"?"===e)return console.log(),console.log(chalk.bold.white(" Notebook commands")),console.log(chalk.cyan(" help")+chalk.gray(" show this")),console.log(chalk.cyan(" clear")+chalk.gray(" clear the screen")),console.log(chalk.cyan(" restart")+chalk.gray(" restart the kernel (wipes all variables)")),console.log(chalk.cyan(" open")+chalk.gray(" reopen the last figure in your image viewer")),console.log(chalk.cyan(" quit")+chalk.gray(" leave the notebook")),console.log(chalk.gray(" Everything else runs as Python in the persistent kernel.")),console.log(),void x();if("open"===e)return u&&g(u)?console.log(chalk.gray(" reopening ")+chalk.cyan(u)):console.log(chalk.gray(" no figure yet — plot something (e.g. matplotlib) first.")),void x();if(""===o)return void x()}if(b.push(e),!i(b))return void x();const t=b.join("\n").replace(/\s+$/,"");if(b.length=0,""!==t.trim()){k=!0;try{!function(e,o){for(const n of e.outputs)if("stream"===n.kind){const e=n.text.replace(/\n$/,"");if(0===e.length)continue;console.log("stderr"===n.name?chalk.yellow(e):chalk.white(e))}else if("result"===n.kind)console.log(chalk.cyan(` Out[${e.execCount??"*"}]: `)+chalk.white(n.text));else if("error"===n.kind){console.log(chalk.bold.red(` ${n.ename}: ${n.evalue}`));for(const e of n.traceback)console.log(chalk.gray(` ${e.replace(y,"")}`))}else"display"===n.kind&&d(n.data,o)}(await f.execute(t),n)}catch(e){console.log(chalk.red(" Kernel error: ")+chalk.gray(e instanceof Error?e.message:String(e)))}k=!1,x()}else x()};w.on("line",e=>{C(e)}),h||w.on("close",async()=>{await f.shutdown().catch(()=>{}),process.exit(0)}),x(),await $}()})}function p(){console.log(),console.log(chalk.bold.white(" ICOA Notebook ")+chalk.gray("· in-terminal Python · AI/ML notebook arena")),console.log(chalk.gray(" ─────────────────────────────────────────────"))}let f=0,u=null;function d(e,t){if(e["image/png"])try{o(t,{recursive:!0}),f+=1;const r=l(t,`figure-${f}.png`);return n(r,Buffer.from(e["image/png"],"base64")),u=r,console.log(chalk.magenta(" 🖼 figure saved → ")+chalk.cyan(r)),void(g(r)?console.log(chalk.gray(" opening in your image viewer… (type ")+chalk.cyan("open")+chalk.gray(" to reopen)")):console.log(chalk.gray(" open it with your file browser (inline images need a graphical viewer).")))}catch{}e["text/plain"]&&console.log(chalk.white(` ${e["text/plain"]}`))}
|
|
1
|
+
import{existsSync as e,mkdirSync as o,readFileSync as n,writeFileSync as l}from"node:fs";import{homedir as t}from"node:os";import{createInterface as r}from"node:readline";import{isAbsolute as s,join as c,resolve as a}from"node:path";import chalk from"chalk";import{aienvPaths as i}from"../lib/aienv.js";import{editTextInEditor as g}from"../lib/editor-spawn.js";import{shouldExecuteCell as y}from"../lib/ipynb-input.js";import{NotebookKernel as d}from"../lib/kernel.js";import{getMainRl as u}from"../lib/main-rl.js";import{cellPreview as p,parseIpynb as h,parsePercentPy as f,parseRunRange as m,serializeIpynb as w,statusChar as v}from"../lib/notebook-doc.js";import{openFile as b}from"../lib/open-file.js";const k=new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`,"g");export function registerIpynbCommand(o){o.command("ipynb").argument("[file]","notebook (.ipynb) or script (.py, split on # %%) to open").description("Open an in-terminal Python notebook (AI/ML — needs `aienv setup`)").action(async o=>{await async function(o){const k=i(t()),x=c(t(),".icoa","ipynb-out");if($(),!e(k.python))return console.log(chalk.yellow(" The AI/ML environment is not set up yet.")),console.log(chalk.gray(" Run ")+chalk.bold.cyan("aienv setup")+chalk.gray(" first (one-time, ~300 MB).")),void console.log();const P=o?function(o){const l=s(o)?o:a(process.cwd(),o);if(!e(l))return{doc:{cells:[],path:l,dirty:!1},note:chalk.gray(" new notebook — will be created on ")+chalk.cyan("save")};try{const e=n(l,"utf8"),o=l.endsWith(".ipynb")?h(e):f(e);return{doc:{cells:o,path:l,dirty:!1},note:chalk.gray(" loaded ")+chalk.cyan(l)+chalk.gray(` — ${o.length} cell${1===o.length?"":"s"}. Type `)+chalk.cyan("cells")+chalk.gray(" to list them.")}}catch(e){return{doc:{cells:[],path:null,dirty:!1},note:chalk.yellow(` could not open ${l}: ${e instanceof Error?e.message:String(e)}`)+chalk.gray(" — starting an empty notebook.")}}}(o):null,C=P?P.doc:{cells:[],path:null,dirty:!1},I=new d({venvPython:k.python,venvRoot:k.root});process.stdout.write(chalk.gray(" Starting Python kernel…"));try{await I.start()}catch{return console.log(chalk.red(" failed.")),console.log(chalk.gray(" Check the environment: ")+chalk.cyan("aienv status")),void console.log()}console.log(chalk.green(" ready.")),P&&console.log(P.note),console.log(chalk.gray(" Type Python and press Enter. Blocks (def/for/…) continue until a blank line.")),console.log(chalk.gray(" Cells: ")+chalk.cyan("cells")+chalk.gray(" list · ")+chalk.cyan("edit N")+chalk.gray(" edit in $EDITOR · ")+chalk.cyan("run N")+chalk.gray(" / ")+chalk.cyan("run all")+chalk.gray(" · ")+chalk.cyan("save")+chalk.gray(" · ")+chalk.cyan("help")+chalk.gray(" · ")+chalk.cyan("quit")),console.log();const j=u(),S=null!==j,q=S?j.listeners("line").slice():[];S&&j.removeAllListeners("line");const L=S?j:r({input:process.stdin,output:process.stdout,terminal:!0}),O=[];let R=!1,A=!1;const T=()=>{L.setPrompt(chalk.bold.cyan(O.length?"icoa ipynb ...> ":"icoa ipynb> ")),L.prompt()};let M=()=>{};const B=new Promise(e=>{M=e}),D=e=>{const o=Number(e.trim());return!Number.isInteger(o)||o<1||o>C.cells.length?(console.log(0===C.cells.length?chalk.yellow(" no cells yet — type Python or use ")+chalk.cyan("add"):chalk.yellow(` no cell ${e.trim()||"?"} — pick 1-${C.cells.length} (see `)+chalk.cyan("cells")+chalk.yellow(")")),null):{cell:C.cells[o-1],idx:o}},K=async e=>{if(R)return void(e.trim()&&console.log(chalk.gray(" (busy — wait for the current cell to finish, then retype)")));const o=e.trim();if(0===O.length){const e=o.split(/\s+/),n=(e[0]??"").toLowerCase(),t=o.slice(e[0]?.length??0).trim();if("quit"!==n&&"exit"!==n&&"q"!==n&&(A=!1),"quit"===n||"exit"===n||"back"===n||"q"===n)return C.dirty&&!A?(A=!0,console.log(chalk.yellow(" unsaved changes — ")+chalk.cyan("save")+chalk.yellow(" first, or type ")+chalk.cyan("quit")+chalk.yellow(" again to discard.")),void T()):void await(async()=>{if(L.removeAllListeners("line"),console.log(),console.log(chalk.gray(" Notebook closed. Kernel stopped.")),console.log(),await I.shutdown().catch(()=>{}),S){L.setPrompt(chalk.bold.cyan("icoa> "));for(const e of q)L.on("line",e);L.prompt(),M()}else M(),L.close()})();if("clear"===n||"cls"===n)return console.clear(),$(),void T();if("restart"===n)return await(async()=>{process.stdout.write(chalk.gray(" Restarting kernel…")),await I.shutdown().catch(()=>{});try{await I.start(),console.log(chalk.green(" fresh kernel ready.")+chalk.gray(" (all variables cleared)"))}catch{console.log(chalk.red(" restart failed.")+chalk.gray(" Leave and re-enter the notebook."))}})(),void T();if("help"===n||"?"===n)return console.log(),console.log(chalk.bold.white(" Notebook commands")),console.log(chalk.cyan(" cells")+chalk.gray(" list cells (○ not run · ✓ ok · ✗ error)")),console.log(chalk.cyan(" show N")+chalk.gray(" print cell N source + outputs")),console.log(chalk.cyan(" edit N")+chalk.gray(" edit cell N in your editor ($EDITOR, default nano)")),console.log(chalk.cyan(" add")+chalk.gray(" write a new cell in your editor")),console.log(chalk.cyan(" del N")+chalk.gray(" delete cell N")),console.log(chalk.cyan(" run N")+chalk.gray(" run cell N (also: run 2-5, run all — stops on error)")),console.log(chalk.cyan(" save [file]")+chalk.gray(" save as a real .ipynb")),console.log(chalk.cyan(" restart")+chalk.gray(" restart the kernel (wipes all variables)")),console.log(chalk.cyan(" open")+chalk.gray(" reopen the last figure in your image viewer")),console.log(chalk.cyan(" clear")+chalk.gray(" clear the screen · ")+chalk.cyan("quit")+chalk.gray(" leave")),console.log(chalk.gray(" Everything else runs as Python in the persistent kernel (and is appended as a cell).")),console.log(),void T();if("open"===n)return N&&b(N)?console.log(chalk.gray(" reopening ")+chalk.cyan(N)):console.log(chalk.gray(" no figure yet — plot something (e.g. matplotlib) first.")),void T();if("cells"===n||"ls"===n)return(()=>{if(0===C.cells.length)return void console.log(chalk.gray(" no cells yet — type Python, or ")+chalk.cyan("add")+chalk.gray(" to write one in your editor."));console.log();const e=Math.max(20,(process.stdout.columns??80)-12);C.cells.forEach((o,n)=>{const l=v(o),t="✓"===l?chalk.green(l.padEnd(2)):"✗"===l?chalk.red(l.padEnd(2)):chalk.gray(l.padEnd(2));console.log(`${chalk.cyan(String(n+1).padStart(4))} ${t} ${chalk.white(p(o,e))}`)}),console.log(),console.log(chalk.gray(" ")+chalk.cyan("show N")+chalk.gray(" · ")+chalk.cyan("edit N")+chalk.gray(" · ")+chalk.cyan("run N")+chalk.gray(" · ")+chalk.cyan("run all"))})(),void T();if("show"===n)return(e=>{const o=D(e);if(o){console.log(),console.log(chalk.gray(` ── cell ${o.idx} ${"markdown"===o.cell.kind?"(markdown) ":""}──`));for(const e of o.cell.source.split("\n"))console.log(chalk.white(` ${e}`));o.cell.result&&(console.log(chalk.gray(" ── output ──")),E(o.cell.result,x)),console.log()}})(t),void T();if("edit"===n)return(e=>{const o=D(e);if(!o)return;L.pause();const n=g(o.cell.source,"markdown"===o.cell.kind?".md":".py");if(L.resume(),null===n)return void console.log(chalk.yellow(" editor cancelled or failed — cell unchanged.")+chalk.gray(" (set $EDITOR to choose your editor)"));const l=n.replace(/\s+$/,"");l!==o.cell.source?(o.cell.source=l,o.cell.result=null,o.cell.rawOutputs=void 0,o.cell.rawExecCount=void 0,C.dirty=!0,console.log(chalk.green(` ✓ cell ${o.idx} updated`)+chalk.gray(" — ")+chalk.cyan(`run ${o.idx}`)+chalk.gray(" to execute."))):console.log(chalk.gray(" no changes."))})(t),void T();if("add"===n)return(()=>{L.pause();const e=g("");L.resume();const o=null===e?"":e.replace(/\s+$/,"");o?(C.cells.push({kind:"code",source:o,result:null}),C.dirty=!0,console.log(chalk.green(` ✓ added cell ${C.cells.length}`)+chalk.gray(" — ")+chalk.cyan(`run ${C.cells.length}`)+chalk.gray(" to execute."))):console.log(chalk.gray(" nothing added."))})(),void T();if("del"===n||"delete"===n)return(e=>{const o=D(e);o&&(C.cells.splice(o.idx-1,1),C.dirty=!0,console.log(chalk.gray(` deleted cell ${o.idx} — ${C.cells.length} left.`)))})(t),void T();if("run"===n)return R=!0,await(async e=>{const o=m(e,C.cells.length);if(o)for(const e of o){const n=C.cells[e-1];if("markdown"!==n.kind){console.log(chalk.gray(` ── run cell ${e}: `)+chalk.white(p(n,50))+chalk.gray(" ──"));try{n.result=await I.execute(n.source)}catch(e){return void console.log(chalk.red(" Kernel error: ")+chalk.gray(e instanceof Error?e.message:String(e)))}if(C.dirty=!0,E(n.result,x),!n.result.ok)return void(e!==o[o.length-1]&&console.log(chalk.yellow(` ✗ cell ${e} raised — stopping here.`)+chalk.gray(" Fix it (")+chalk.cyan(`edit ${e}`)+chalk.gray(") and re-run.")))}else{if(o.length>1)continue;console.log(chalk.gray(` cell ${e} is markdown — nothing to run.`))}}else console.log(0===C.cells.length?chalk.yellow(" no cells yet — type Python or use ")+chalk.cyan("add"):chalk.yellow(" usage: ")+chalk.cyan("run 3")+chalk.yellow(" · ")+chalk.cyan("run 1-5")+chalk.yellow(" · ")+chalk.cyan("run all"))})(t),R=!1,void T();if("save"===n)return(e=>{if(0===C.cells.length)return void console.log(chalk.yellow(" nothing to save — the notebook is empty."));let o;o=e.trim()?s(e.trim())?e.trim():a(process.cwd(),e.trim()):C.path?C.path.endsWith(".ipynb")?C.path:`${C.path.replace(/\.[^./]+$/,"")}.ipynb`:a(process.cwd(),"notebook.ipynb"),o.endsWith(".ipynb")||(o+=".ipynb");try{l(o,w(C.cells)),C.path=o,C.dirty=!1,console.log(chalk.green(" ✓ saved ")+chalk.cyan(o)+chalk.gray(` (${C.cells.length} cells)`))}catch(e){console.log(chalk.red(" save failed: ")+chalk.gray(e instanceof Error?e.message:String(e)))}})(t),void T();if(""===o)return void T()}if(O.push(e),!y(O))return void T();const n=O.join("\n").replace(/\s+$/,"");if(O.length=0,""!==n.trim()){R=!0;try{const e=await I.execute(n);C.cells.push({kind:"code",source:n,result:e}),C.dirty=!0,E(e,x)}catch(e){console.log(chalk.red(" Kernel error: ")+chalk.gray(e instanceof Error?e.message:String(e)))}R=!1,T()}else T()};L.on("line",e=>{K(e)}),S||L.on("close",async()=>{await I.shutdown().catch(()=>{}),process.exit(0)}),T(),await B}(o)})}function $(){console.log(),console.log(chalk.bold.white(" ICOA Notebook ")+chalk.gray("· in-terminal Python · AI/ML notebook arena")),console.log(chalk.gray(" ─────────────────────────────────────────────"))}let x=0,N=null;function E(e,o){for(const n of e.outputs)if("stream"===n.kind){const e=n.text.replace(/\n$/,"");if(0===e.length)continue;console.log("stderr"===n.name?chalk.yellow(e):chalk.white(e))}else if("result"===n.kind)console.log(chalk.cyan(` Out[${e.execCount??"*"}]: `)+chalk.white(n.text));else if("error"===n.kind){console.log(chalk.bold.red(` ${n.ename}: ${n.evalue}`));for(const e of n.traceback)console.log(chalk.gray(` ${e.replace(k,"")}`))}else"display"===n.kind&&P(n.data,o)}function P(e,n){if(e["image/png"])try{o(n,{recursive:!0}),x+=1;const t=c(n,`figure-${x}.png`);return l(t,Buffer.from(e["image/png"],"base64")),N=t,console.log(chalk.magenta(" 🖼 figure saved → ")+chalk.cyan(t)),void(b(t)?console.log(chalk.gray(" opening in your image viewer… (type ")+chalk.cyan("open")+chalk.gray(" to reopen)")):console.log(chalk.gray(" open it with your file browser (inline images need a graphical viewer).")))}catch{}e["text/plain"]&&console.log(chalk.white(` ${e["text/plain"]}`))}
|
package/dist/commands/lang.js
CHANGED
|
@@ -1 +1,202 @@
|
|
|
1
|
-
import chalk from
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getConfig, saveConfig } from '../lib/config.js';
|
|
3
|
+
import { COUNTRY_LANG } from '../lib/country-lang.js';
|
|
4
|
+
import { getExamState, getRealExamState } from '../lib/exam-state.js';
|
|
5
|
+
import { logCommand } from '../lib/logger.js';
|
|
6
|
+
import { ensureLangCache } from '../lib/translations-fetcher.js';
|
|
7
|
+
import { loadUiPack } from '../lib/i18n.js';
|
|
8
|
+
import { printSuccess, printError, printInfo } from '../lib/ui.js';
|
|
9
|
+
import { SUPPORTED_LANGUAGES } from '../types/index.js';
|
|
10
|
+
const LANG_NAMES = {
|
|
11
|
+
en: 'English',
|
|
12
|
+
zh: '中文 (Chinese)',
|
|
13
|
+
ja: '日本語 (Japanese)',
|
|
14
|
+
ko: '한국어 (Korean)',
|
|
15
|
+
es: 'Español (Spanish)',
|
|
16
|
+
ar: 'العربية (Arabic)',
|
|
17
|
+
fr: 'Français (French)',
|
|
18
|
+
pt: 'Português (Portuguese)',
|
|
19
|
+
ru: 'Русский (Russian)',
|
|
20
|
+
hi: 'हिन्दी (Hindi)',
|
|
21
|
+
de: 'Deutsch (German)',
|
|
22
|
+
id: 'Bahasa (Indonesian)',
|
|
23
|
+
th: 'ไทย (Thai)',
|
|
24
|
+
vi: 'Tiếng Việt (Vietnamese)',
|
|
25
|
+
tr: 'Türkçe (Turkish)',
|
|
26
|
+
uk: 'Українська (Ukrainian)',
|
|
27
|
+
ht: 'Kreyòl (Haitian Creole)',
|
|
28
|
+
sw: 'Kiswahili (Swahili)',
|
|
29
|
+
uz: 'Oʻzbek (Uzbek)',
|
|
30
|
+
lo: 'ລາວ (Lao)',
|
|
31
|
+
};
|
|
32
|
+
export function registerLangCommand(program) {
|
|
33
|
+
program
|
|
34
|
+
.command('lang [code]')
|
|
35
|
+
.description('Switch display language')
|
|
36
|
+
.action(async (code) => {
|
|
37
|
+
logCommand(`lang ${code || ''}`);
|
|
38
|
+
if (!code) {
|
|
39
|
+
const config = getConfig();
|
|
40
|
+
printInfo(`Current language: ${chalk.white(LANG_NAMES[config.language] || config.language)}`);
|
|
41
|
+
console.log();
|
|
42
|
+
console.log(chalk.gray(' Supported languages:'));
|
|
43
|
+
for (const lang of SUPPORTED_LANGUAGES) {
|
|
44
|
+
const current = config.language === lang ? chalk.yellow(' ← current') : '';
|
|
45
|
+
console.log(` ${chalk.white(lang)} ${LANG_NAMES[lang]}${current}`);
|
|
46
|
+
}
|
|
47
|
+
console.log();
|
|
48
|
+
console.log(chalk.gray(' Switch now: ') +
|
|
49
|
+
chalk.cyan('lang <code>') +
|
|
50
|
+
chalk.gray(' (e.g. ') +
|
|
51
|
+
chalk.cyan('lang es') +
|
|
52
|
+
chalk.gray(')'));
|
|
53
|
+
console.log(chalk.gray(' No "back" needed — you are still at the ') + chalk.cyan('icoa>') + chalk.gray(' prompt.'));
|
|
54
|
+
console.log();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!SUPPORTED_LANGUAGES.includes(code)) {
|
|
58
|
+
printError(`Unsupported language: ${code}`);
|
|
59
|
+
const supported = SUPPORTED_LANGUAGES.map((l) => `${l} (${LANG_NAMES[l]?.split(' ')[0] || l})`).join(', ');
|
|
60
|
+
printInfo(`Supported: ${supported}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Country lock: during an active real exam, only allow English or the
|
|
64
|
+
// language auto-detected from the token's country prefix. Demo exams
|
|
65
|
+
// are not affected (getRealExamState returns null for demos).
|
|
66
|
+
const realExam = getRealExamState();
|
|
67
|
+
const examToken = realExam?.session?.token;
|
|
68
|
+
if (examToken) {
|
|
69
|
+
const prefix = examToken.substring(0, 2).toUpperCase();
|
|
70
|
+
const expectedLang = COUNTRY_LANG[prefix];
|
|
71
|
+
if (expectedLang && code !== expectedLang && code !== 'en') {
|
|
72
|
+
printError(`Language is locked during your exam.`);
|
|
73
|
+
printInfo(`Your token (${prefix}xxx) supports two languages: ` +
|
|
74
|
+
`${chalk.cyan('en')} (English) or ${chalk.cyan(expectedLang)} (${LANG_NAMES[expectedLang] || expectedLang}).`);
|
|
75
|
+
printInfo(`To change language, finish or quit the current exam first.`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
saveConfig({ language: code });
|
|
80
|
+
printSuccess(`Language set to: ${LANG_NAMES[code] || code}`);
|
|
81
|
+
// Fetch the translation pack on first switch to this language (no-op
|
|
82
|
+
// when already cached or when code === 'en'). Non-fatal if it fails;
|
|
83
|
+
// per-question lookups will fall back to the on-the-fly translator.
|
|
84
|
+
await ensureLangCache(code);
|
|
85
|
+
// Apply the freshly-fetched UI pack immediately (the pack's ui.json now
|
|
86
|
+
// drives interface strings via t(); see src/lib/i18n.ts lazy-load).
|
|
87
|
+
loadUiPack(code);
|
|
88
|
+
// If demo in progress, re-translate each drawn question in place using
|
|
89
|
+
// sourceNumber + sourceOrder so the user's answers and option positions are
|
|
90
|
+
// preserved. If an older state lacks these fields (pre-v2.19.22), fall back
|
|
91
|
+
// to restart-with-fresh-pick so nothing crashes.
|
|
92
|
+
const state = getExamState();
|
|
93
|
+
if (state && state.session.examId === 'demo-free') {
|
|
94
|
+
try {
|
|
95
|
+
const { pickDemoQuestions, getLocalizedDemoSession, getLocalizedDemoQuestions, getLocalizedExplanations, DEMO_PICK_SIZE, } = await import('../lib/demo-exam.js');
|
|
96
|
+
const { saveExamState } = await import('../lib/exam-state.js');
|
|
97
|
+
const canRetranslate = state.questions.every((q) => q.sourceNumber != null && Array.isArray(q.sourceOrder) && q.sourceOrder.length === 4);
|
|
98
|
+
if (canRetranslate) {
|
|
99
|
+
const pool = getLocalizedDemoQuestions();
|
|
100
|
+
const explanations = getLocalizedExplanations();
|
|
101
|
+
state.questions = state.questions.map((q) => {
|
|
102
|
+
const src = pool.find((p) => p.number === q.sourceNumber);
|
|
103
|
+
if (!src || !q.sourceOrder)
|
|
104
|
+
return q;
|
|
105
|
+
return {
|
|
106
|
+
...q,
|
|
107
|
+
text: src.text,
|
|
108
|
+
category: src.category,
|
|
109
|
+
options: {
|
|
110
|
+
A: src.options[q.sourceOrder[0]],
|
|
111
|
+
B: src.options[q.sourceOrder[1]],
|
|
112
|
+
C: src.options[q.sourceOrder[2]],
|
|
113
|
+
D: src.options[q.sourceOrder[3]],
|
|
114
|
+
},
|
|
115
|
+
explanation: explanations[q.sourceNumber],
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
state.session.examName = getLocalizedDemoSession().examName;
|
|
119
|
+
saveExamState(state);
|
|
120
|
+
const currentQ = state._lastQ || 1;
|
|
121
|
+
console.log();
|
|
122
|
+
console.log(chalk.green(` Demo continues in ${LANG_NAMES[code] || code}. Your progress is kept.`));
|
|
123
|
+
console.log(chalk.white(` Resume: exam q ${currentQ}`));
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// Legacy state from before v2.19.22 — safely reset
|
|
127
|
+
state.questions = pickDemoQuestions(DEMO_PICK_SIZE);
|
|
128
|
+
state.answers = {};
|
|
129
|
+
state.session.examName = getLocalizedDemoSession().examName;
|
|
130
|
+
state.session.startedAt = new Date().toISOString();
|
|
131
|
+
state._lastQ = 1;
|
|
132
|
+
saveExamState(state);
|
|
133
|
+
console.log();
|
|
134
|
+
console.log(chalk.green(` Demo restarted in ${LANG_NAMES[code] || code}.`));
|
|
135
|
+
console.log(chalk.white(' Type: exam q 1'));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
console.log(chalk.gray(' Language changed. Type: demo'));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else if (state && state.session.token) {
|
|
143
|
+
// Real exam with token: re-fetch questions in new language from server
|
|
144
|
+
try {
|
|
145
|
+
const { getConfig } = await import('../lib/config.js');
|
|
146
|
+
const { saveExamState } = await import('../lib/exam-state.js');
|
|
147
|
+
const { getDeviceFingerprint } = await import('../lib/access.js');
|
|
148
|
+
const config = getConfig();
|
|
149
|
+
const serverUrl = config.ctfdUrl || 'https://practice.icoa2026.au';
|
|
150
|
+
const token = state.session.token;
|
|
151
|
+
const res = await fetch(`${serverUrl}/api/icoa/exam-token`, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: { 'Content-Type': 'application/json' },
|
|
154
|
+
body: JSON.stringify({ token, deviceHash: getDeviceFingerprint(), lang: code }),
|
|
155
|
+
signal: AbortSignal.timeout(10000),
|
|
156
|
+
});
|
|
157
|
+
if (res.ok) {
|
|
158
|
+
const json = (await res.json());
|
|
159
|
+
const newQuestions = json.data.questions;
|
|
160
|
+
// Keep answers, interactions, aiUsage — only update question text
|
|
161
|
+
state.questions = newQuestions;
|
|
162
|
+
saveExamState(state);
|
|
163
|
+
const currentQ = state._lastQ || 1;
|
|
164
|
+
console.log();
|
|
165
|
+
printSuccess(`Exam questions updated to ${LANG_NAMES[code] || code}. Your answers are kept.`);
|
|
166
|
+
console.log(chalk.white(` Resume: exam q ${currentQ}`));
|
|
167
|
+
}
|
|
168
|
+
else if (res.status === 429) {
|
|
169
|
+
// V8 anti-bruteforce cooldown — token is alive, just throttled.
|
|
170
|
+
// Do NOT clear local state, or contestants lose their real session
|
|
171
|
+
// because the server briefly rate-limited a language switch.
|
|
172
|
+
const errBody = (await res.json().catch(() => null));
|
|
173
|
+
const m = errBody?.message?.match(/(\d+)\s*s/i);
|
|
174
|
+
const waitS = m ? parseInt(m[1], 10) : 60;
|
|
175
|
+
console.log();
|
|
176
|
+
printInfo(`Server rate-limited that token (wait ~${waitS}s).`);
|
|
177
|
+
printInfo(`Your exam session is intact — try ${chalk.cyan(`lang ${code}`)} again after the cooldown.`);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
// Other non-200: token revoked / submitted / window closed. Auto-
|
|
181
|
+
// clear the dead local session so contestants never need to learn
|
|
182
|
+
// the hidden `exam reset` command — they just retype their token.
|
|
183
|
+
const { clearExamState } = await import('../lib/exam-state.js');
|
|
184
|
+
clearExamState();
|
|
185
|
+
console.log();
|
|
186
|
+
console.log(chalk.green(' ✓ Old session record cleared — your scores are safe.'));
|
|
187
|
+
console.log(chalk.gray(' To start (or resume) your exam, type your token:'));
|
|
188
|
+
console.log(chalk.gray(' → ') + chalk.bold.cyan('exam <your-token>'));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
console.log(chalk.yellow(' Could not reach server. Language changed for UI only.'));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else if (state) {
|
|
196
|
+
const currentQ = state._lastQ || 1;
|
|
197
|
+
console.log();
|
|
198
|
+
console.log(chalk.gray(` Exam in progress — resuming Q${currentQ}:`));
|
|
199
|
+
console.log(chalk.white(` Type: exam q ${currentQ}`));
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
package/dist/commands/log.js
CHANGED
|
@@ -1 +1,171 @@
|
|
|
1
|
-
import chalk from
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { getSessionLog } from '../lib/logger.js';
|
|
5
|
+
import { getIcoaDir, getConfig } from '../lib/config.js';
|
|
6
|
+
import { printHeader, printInfo, printTable } from '../lib/ui.js';
|
|
7
|
+
export function registerLogCommand(program) {
|
|
8
|
+
const logCmd = program
|
|
9
|
+
.command('log')
|
|
10
|
+
.description('Display session history')
|
|
11
|
+
.action(() => {
|
|
12
|
+
showLog();
|
|
13
|
+
});
|
|
14
|
+
// icoa log export — export full audit log for post-competition review
|
|
15
|
+
logCmd
|
|
16
|
+
.command('export')
|
|
17
|
+
.description('Export full audit log for review')
|
|
18
|
+
.action(async () => {
|
|
19
|
+
await exportLog();
|
|
20
|
+
});
|
|
21
|
+
// icoa log stats — show summary statistics
|
|
22
|
+
logCmd
|
|
23
|
+
.command('stats')
|
|
24
|
+
.description('Show session statistics')
|
|
25
|
+
.action(() => {
|
|
26
|
+
showStats();
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function showLog() {
|
|
30
|
+
const entries = getSessionLog();
|
|
31
|
+
if (entries.length === 0) {
|
|
32
|
+
printInfo('No session log entries yet.');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
printHeader('Session Log');
|
|
36
|
+
const rows = entries.map((entry) => {
|
|
37
|
+
const time = entry.timestamp.replace('T', ' ').substring(0, 19);
|
|
38
|
+
const levelColor = {
|
|
39
|
+
A: chalk.green,
|
|
40
|
+
B: chalk.yellow,
|
|
41
|
+
C: chalk.red,
|
|
42
|
+
command: chalk.blue,
|
|
43
|
+
submit: chalk.magenta,
|
|
44
|
+
};
|
|
45
|
+
const colorFn = levelColor[entry.level] || chalk.gray;
|
|
46
|
+
const input = entry.input.length > 60 ? `${entry.input.substring(0, 57)}...` : entry.input;
|
|
47
|
+
return [chalk.gray(time), colorFn(entry.level.padEnd(7)), input];
|
|
48
|
+
});
|
|
49
|
+
printTable(['Time', 'Type', 'Content'], rows);
|
|
50
|
+
console.log(chalk.gray(` ${entries.length} entries total`));
|
|
51
|
+
console.log();
|
|
52
|
+
console.log(chalk.gray(' You are at the ') +
|
|
53
|
+
chalk.cyan('icoa>') +
|
|
54
|
+
chalk.gray(' prompt. Also: ') +
|
|
55
|
+
chalk.cyan('log stats') +
|
|
56
|
+
chalk.gray(' · ') +
|
|
57
|
+
chalk.cyan('log export') +
|
|
58
|
+
chalk.gray(' · ') +
|
|
59
|
+
chalk.cyan('help') +
|
|
60
|
+
chalk.gray(' all commands.'));
|
|
61
|
+
console.log();
|
|
62
|
+
}
|
|
63
|
+
async function exportLog() {
|
|
64
|
+
const config = getConfig();
|
|
65
|
+
const icoaDir = getIcoaDir();
|
|
66
|
+
const _logFile = join(icoaDir, 'session.log');
|
|
67
|
+
const sessionFile = join(icoaDir, 'session-state.json');
|
|
68
|
+
const _configFile = join(icoaDir, 'config.json');
|
|
69
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
|
70
|
+
const userName = config.userName || 'unknown';
|
|
71
|
+
const exportName = `icoa-audit-${userName}-${timestamp}.json`;
|
|
72
|
+
const exportPath = join(process.cwd(), exportName);
|
|
73
|
+
// Gather all audit data
|
|
74
|
+
const audit = {
|
|
75
|
+
exportedAt: new Date().toISOString(),
|
|
76
|
+
version: '1.7.2',
|
|
77
|
+
competitor: {
|
|
78
|
+
userName: config.userName,
|
|
79
|
+
userId: config.userId,
|
|
80
|
+
teamName: config.teamName,
|
|
81
|
+
teamId: config.teamId,
|
|
82
|
+
sessionId: config.sessionId,
|
|
83
|
+
},
|
|
84
|
+
connection: {
|
|
85
|
+
ctfdUrl: config.ctfdUrl,
|
|
86
|
+
},
|
|
87
|
+
session: existsSync(sessionFile) ? JSON.parse(readFileSync(sessionFile, 'utf-8')) : null,
|
|
88
|
+
commands: getSessionLog(),
|
|
89
|
+
};
|
|
90
|
+
// Count by type
|
|
91
|
+
const entries = getSessionLog();
|
|
92
|
+
const counts = {};
|
|
93
|
+
for (const e of entries) {
|
|
94
|
+
counts[e.level] = (counts[e.level] || 0) + 1;
|
|
95
|
+
}
|
|
96
|
+
audit.summary = {
|
|
97
|
+
totalCommands: entries.length,
|
|
98
|
+
byType: counts,
|
|
99
|
+
firstEntry: entries[0]?.timestamp || null,
|
|
100
|
+
lastEntry: entries[entries.length - 1]?.timestamp || null,
|
|
101
|
+
};
|
|
102
|
+
// Write export
|
|
103
|
+
const { writeFileSync } = await import('node:fs');
|
|
104
|
+
writeFileSync(exportPath, JSON.stringify(audit, null, 2));
|
|
105
|
+
console.log();
|
|
106
|
+
console.log(chalk.green(` ✓ Audit log exported`));
|
|
107
|
+
console.log(chalk.white(` ${exportPath}`));
|
|
108
|
+
console.log();
|
|
109
|
+
console.log(chalk.gray(' Contents:'));
|
|
110
|
+
console.log(chalk.gray(` Commands: ${entries.length}`));
|
|
111
|
+
Object.entries(counts).forEach(([type, count]) => {
|
|
112
|
+
console.log(chalk.gray(` ${type}: ${count}`));
|
|
113
|
+
});
|
|
114
|
+
console.log();
|
|
115
|
+
console.log(chalk.gray(' This file contains the complete session audit trail.'));
|
|
116
|
+
console.log(chalk.gray(' Submit to organizers for post-competition verification.'));
|
|
117
|
+
console.log();
|
|
118
|
+
}
|
|
119
|
+
function showStats() {
|
|
120
|
+
const entries = getSessionLog();
|
|
121
|
+
const icoaDir = getIcoaDir();
|
|
122
|
+
const sessionFile = join(icoaDir, 'session-state.json');
|
|
123
|
+
console.log();
|
|
124
|
+
console.log(chalk.bold.white(' Session Statistics'));
|
|
125
|
+
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
126
|
+
if (entries.length === 0) {
|
|
127
|
+
console.log(chalk.gray(' No activity recorded yet.'));
|
|
128
|
+
console.log();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Time range
|
|
132
|
+
const first = new Date(entries[0].timestamp);
|
|
133
|
+
const last = new Date(entries[entries.length - 1].timestamp);
|
|
134
|
+
const durationMin = Math.round((last.getTime() - first.getTime()) / 60000);
|
|
135
|
+
console.log(chalk.gray(' First activity: ') + chalk.white(first.toLocaleString()));
|
|
136
|
+
console.log(chalk.gray(' Last activity: ') + chalk.white(last.toLocaleString()));
|
|
137
|
+
console.log(chalk.gray(' Duration: ') + chalk.white(`${durationMin} min`));
|
|
138
|
+
console.log();
|
|
139
|
+
// Count by type
|
|
140
|
+
const counts = {};
|
|
141
|
+
for (const e of entries) {
|
|
142
|
+
counts[e.level] = (counts[e.level] || 0) + 1;
|
|
143
|
+
}
|
|
144
|
+
console.log(chalk.gray(' Total commands: ') + chalk.white(String(entries.length)));
|
|
145
|
+
if (counts.command)
|
|
146
|
+
console.log(chalk.blue(' commands: ') + chalk.white(String(counts.command)));
|
|
147
|
+
if (counts.A)
|
|
148
|
+
console.log(chalk.green(' hint A: ') + chalk.white(String(counts.A)));
|
|
149
|
+
if (counts.B)
|
|
150
|
+
console.log(chalk.yellow(' hint B: ') + chalk.white(String(counts.B)));
|
|
151
|
+
if (counts.C)
|
|
152
|
+
console.log(chalk.red(' hint C: ') + chalk.white(String(counts.C)));
|
|
153
|
+
if (counts.submit)
|
|
154
|
+
console.log(chalk.magenta(' submissions: ') + chalk.white(String(counts.submit)));
|
|
155
|
+
// Exit info
|
|
156
|
+
if (existsSync(sessionFile)) {
|
|
157
|
+
try {
|
|
158
|
+
const session = JSON.parse(readFileSync(sessionFile, 'utf-8'));
|
|
159
|
+
console.log();
|
|
160
|
+
console.log(chalk.gray(' Exit count: ') + chalk.white(String(session.exitCount || 0)));
|
|
161
|
+
if (session.totalAwaySeconds) {
|
|
162
|
+
const awayMin = Math.round(session.totalAwaySeconds / 60);
|
|
163
|
+
console.log(chalk.gray(' Total away: ') + chalk.white(`${awayMin} min`));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
/* ignore */
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
console.log();
|
|
171
|
+
}
|
package/dist/commands/shell.d.ts
CHANGED
|
@@ -17,4 +17,19 @@ export declare function resolveShellLaunch(opts: {
|
|
|
17
17
|
mode: 'docker' | 'host';
|
|
18
18
|
command: string;
|
|
19
19
|
};
|
|
20
|
+
export { SANDBOX_IMAGES, resolveSandboxImage } from '../lib/sandbox.js';
|
|
21
|
+
/**
|
|
22
|
+
* docker-run args as an ARRAY (spawnSync — no shell, so a cwd with spaces
|
|
23
|
+
* survives untouched). The two mounts are the aienv × docker interlock:
|
|
24
|
+
* - cwd → /work: challenge files flow both ways instead of being invisible
|
|
25
|
+
* from inside the container (and work done in /work outlives --rm).
|
|
26
|
+
* - icoa-aienv named volume → /root/.icoa: docker's copy-up seeds it from the
|
|
27
|
+
* image-baked venv on first use, then in-container pip installs survive
|
|
28
|
+
* --rm across sessions. Reset anytime: `docker volume rm icoa-aienv`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildSandboxRunArgs(opts: {
|
|
31
|
+
cwd: string;
|
|
32
|
+
image: string;
|
|
33
|
+
containerName: string;
|
|
34
|
+
}): string[];
|
|
20
35
|
export declare function registerShellCommand(program: Command): void;
|