icoa-cli 2.19.355 → 2.19.356
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/ctf4ai-demo.js +1 -1
- package/dist/commands/ctf4vla.js +1 -1
- package/dist/commands/exam.js +1 -1
- package/dist/commands/ipynb.d.ts +10 -4
- package/dist/commands/ipynb.js +1 -1
- package/dist/commands/shell.d.ts +15 -0
- package/dist/commands/shell.js +1 -1
- package/dist/lib/editor-spawn.d.ts +23 -0
- package/dist/lib/editor-spawn.js +1 -0
- package/dist/lib/notebook-doc.d.ts +38 -0
- package/dist/lib/notebook-doc.js +1 -0
- package/dist/lib/sandbox.d.ts +18 -0
- package/dist/lib/sandbox.js +1 -1
- package/package.json +1 -1
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/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;
|
package/dist/commands/shell.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{execSync as o}from"node:child_process";import chalk from"chalk";import{printInfo as
|
|
1
|
+
import{execSync as o,spawnSync as e}from"node:child_process";import chalk from"chalk";import{printInfo as n,printSuccess as r}from"../lib/ui.js";import{logCommand as s}from"../lib/logger.js";import{getActiveCwd as t}from"../lib/exam-sandbox.js";export function resolveShellLaunch(o){return o.dockerAvailable?{mode:"docker",command:"docker"}:{mode:"host",command:"win32"===o.platform?o.comspecEnv||"cmd.exe":o.shellEnv||"/bin/bash"}}export{SANDBOX_IMAGES,resolveSandboxImage}from"../lib/sandbox.js";import{SANDBOX_IMAGES as i,resolveSandboxImage as l}from"../lib/sandbox.js";export function buildSandboxRunArgs(o){return["run","--rm","-it","--name",o.containerName,"--network=host","-v",`${o.cwd}:/work`,"-w","/work","-v","icoa-aienv:/root/.icoa",o.image,"/bin/bash"]}function a(){const e=t(),{command:s}=resolveShellLaunch({dockerAvailable:!1,platform:process.platform,shellEnv:process.env.SHELL,comspecEnv:process.env.COMSPEC});n("Docker not found — opening a host shell."),console.log(chalk.gray(" All 110 CTF tools run on your host. If any are missing, run ")+chalk.cyan("env setup")+chalk.gray(".")),console.log(chalk.gray(" Type 'exit' to return to ICOA CLI.")),console.log();try{o(s,{stdio:"inherit",cwd:e}),console.log(),r("Shell session ended.")}catch{console.log(),n("Shell session ended.")}}export function registerShellCommand(c){c.command("shell").description("Open an interactive CTF shell (Docker sandbox if available, else host)").action(async()=>{if(s("shell"),!function(){try{return o("docker info",{stdio:"ignore"}),!0}catch{return!1}}())return void a();const c=l(e=>{try{return o(`docker image inspect ${e}`,{stdio:"ignore"}),!0}catch{return!1}},e=>{n(`Pulling ${e}...`);try{return o(`docker pull ${e}`,{stdio:"inherit"}),!0}catch{return!1}});if(null===c)return n("Could not pull a sandbox image — using your host shell instead."),void a();const d=t();r("Launching sandbox..."),console.log(chalk.gray(" Your folder is mounted at ")+chalk.cyan("/work")+chalk.gray(" — files there persist on your host.")),c===i[0]?console.log(chalk.gray(" ML venv: ")+chalk.cyan("~/.icoa/aienv/bin/python")+chalk.gray(" (sklearn / jupyter / mujoco) — its pip installs persist across sessions.")):console.log(chalk.gray(" This image has no ML venv — get the newer one with ")+chalk.cyan(`docker pull ${i[0]}`)),console.log(chalk.gray(" Type 'exit' to return to ICOA CLI.")),console.log();const m=`icoa-sandbox-${Date.now()}`,u=e("docker",buildSandboxRunArgs({cwd:d,image:c,containerName:m}),{stdio:"inherit"});console.log(),u.error?n("Sandbox session ended."):r("Sandbox session ended.")})}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* editor-spawn — hand the terminal to the user's $EDITOR for one text
|
|
3
|
+
* fragment, then take it back. The `git commit` / `crontab -e` idiom: write a
|
|
4
|
+
* temp file, block on the editor, read the file back.
|
|
5
|
+
*
|
|
6
|
+
* BUG-008 note: this must NOT create any readline. `spawnSync` blocks the
|
|
7
|
+
* event loop, so the main REPL's readline cannot fire while the editor owns
|
|
8
|
+
* the TTY; we only save/drop/restore raw mode around the child (editors expect
|
|
9
|
+
* a cooked terminal and manage their own termios).
|
|
10
|
+
*/
|
|
11
|
+
export interface EditorCommand {
|
|
12
|
+
cmd: string;
|
|
13
|
+
args: string[];
|
|
14
|
+
}
|
|
15
|
+
/** $VISUAL → $EDITOR → nano (notepad on native Windows). Values like
|
|
16
|
+
* "code -w" split on whitespace into cmd + args. Pure for testability. */
|
|
17
|
+
export declare function resolveEditor(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): EditorCommand;
|
|
18
|
+
/**
|
|
19
|
+
* Open `initial` in the user's editor; return the edited text, or null if the
|
|
20
|
+
* editor failed to launch or exited non-zero (a deliberate cancel, vi `:cq`).
|
|
21
|
+
* Caller is expected to `rl.pause()` before and `rl.resume()` after.
|
|
22
|
+
*/
|
|
23
|
+
export declare function editTextInEditor(initial: string, suffix?: string): string | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{spawnSync as r}from"node:child_process";import{mkdtempSync as o,readFileSync as t,rmSync as s,writeFileSync as e}from"node:fs";import{tmpdir as n}from"node:os";import{join as i}from"node:path";export function resolveEditor(r=process.env,o=process.platform){const t=(r.VISUAL??"").trim()||(r.EDITOR??"").trim();if(t){const r=t.split(/\s+/);return{cmd:r[0],args:r.slice(1)}}return"win32"===o?{cmd:"notepad",args:[]}:{cmd:"nano",args:[]}}export function editTextInEditor(c,d=".py"){const l=o(i(n(),"icoa-cell-")),a=i(l,`cell${d}`);e(a,""===c||c.endsWith("\n")?c:`${c}\n`);const m=resolveEditor(),p=process.stdin,f=!!p.isTTY&&p.isRaw;try{p.isTTY&&p.setRawMode(!1);const o=r(m.cmd,[...m.args,a],{stdio:"inherit"});return o.error||0!==o.status?null:t(a,"utf8")}catch{return null}finally{p.isTTY&&f&&p.setRawMode(!0),s(l,{recursive:!0,force:!0})}}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* notebook-doc — the pure cell model behind `icoa ipynb`'s cell-stepper
|
|
3
|
+
* (cells / show / edit / run / save). nbformat v4 is plain JSON, so parse and
|
|
4
|
+
* serialize live here with zero dependencies; keeping the module pure (no I/O,
|
|
5
|
+
* no chalk) means the whole document lifecycle is unit-tested without a kernel.
|
|
6
|
+
*
|
|
7
|
+
* Round-trip safety: cells loaded from disk keep their original outputs in
|
|
8
|
+
* `rawOutputs` and are written back verbatim on save unless the cell was
|
|
9
|
+
* re-run this session (then the fresh FoldedCell result wins).
|
|
10
|
+
*/
|
|
11
|
+
import type { FoldedCell } from './kernel-protocol.js';
|
|
12
|
+
export type CellKind = 'code' | 'markdown';
|
|
13
|
+
export interface NbCell {
|
|
14
|
+
kind: CellKind;
|
|
15
|
+
source: string;
|
|
16
|
+
/** Result of running the cell THIS session; null = not run yet. */
|
|
17
|
+
result: FoldedCell | null;
|
|
18
|
+
/** nbformat outputs as loaded from disk (preserved on save if not re-run). */
|
|
19
|
+
rawOutputs?: unknown[];
|
|
20
|
+
rawExecCount?: number | null;
|
|
21
|
+
}
|
|
22
|
+
/** Parse a .ipynb (nbformat v4) JSON string. Raw cells are dropped. */
|
|
23
|
+
export declare function parseIpynb(json: string): NbCell[];
|
|
24
|
+
/**
|
|
25
|
+
* Split a Python script into cells on `# %%` markers (jupytext percent
|
|
26
|
+
* format; the arena starter uses the same markers). Marker lines are
|
|
27
|
+
* separators and are dropped. No markers → the whole file is one cell.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parsePercentPy(text: string): NbCell[];
|
|
30
|
+
/** Serialize cells to a Jupyter-openable nbformat v4.5 JSON string. */
|
|
31
|
+
export declare function serializeIpynb(cells: NbCell[]): string;
|
|
32
|
+
/** One-char run state for the `cells` listing. */
|
|
33
|
+
export declare function statusChar(cell: NbCell): string;
|
|
34
|
+
/** First non-blank line, trimmed and truncated to `width` (with ellipsis). */
|
|
35
|
+
export declare function cellPreview(cell: NbCell, width: number): string;
|
|
36
|
+
/** Parse `run` arguments: '3' → [3], '1-5' → [1..5], 'all' → [1..count].
|
|
37
|
+
* 1-based, validated against count; null = not a valid range. */
|
|
38
|
+
export declare function parseRunRange(arg: string, count: number): number[] | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function parseIpynb(t){let e;try{e=JSON.parse(t)}catch{throw new Error("not valid JSON")}const n=e?.cells;if(!Array.isArray(n))throw new Error("not a Jupyter notebook (no cells array)");const r=[];for(const t of n){if(!t||"object"!=typeof t)continue;const e=t.cell_type;if("code"!==e&&"markdown"!==e)continue;const n=t.source,o={kind:e,source:(Array.isArray(n)?n.join(""):"string"==typeof n?n:"").replace(/\s+$/,""),result:null};if("code"===e){const e=t.outputs;Array.isArray(e)&&e.length>0&&(o.rawOutputs=e);const n=t.execution_count;o.rawExecCount="number"==typeof n?n:null}r.push(o)}return r}export function parsePercentPy(t){const e=[];let n=[];const r=()=>{const t=n.join("\n").replace(/^\n+/,"").replace(/\s+$/,"");t&&e.push({kind:"code",source:t,result:null}),n=[]};for(const e of t.split("\n"))/^#\s*%%/.test(e)?r():n.push(e);return r(),e}function t(t){if(t.result){const e=[];for(const n of t.result.outputs)"stream"===n.kind?e.push({output_type:"stream",name:n.name,text:n.text}):"result"===n.kind?e.push({output_type:"execute_result",execution_count:t.result.execCount,data:n.data??{"text/plain":n.text},metadata:{}}):"display"===n.kind?e.push({output_type:"display_data",data:n.data,metadata:{}}):e.push({output_type:"error",ename:n.ename,evalue:n.evalue,traceback:n.traceback});return{outputs:e,execution_count:t.result.execCount}}return{outputs:t.rawOutputs??[],execution_count:t.rawExecCount??null}}export function serializeIpynb(e){const n={nbformat:4,nbformat_minor:5,metadata:{kernelspec:{display_name:"Python 3",language:"python",name:"python3"},language_info:{name:"python",version:"3.12"}},cells:e.map(e=>"markdown"===e.kind?{cell_type:"markdown",metadata:{},source:e.source}:{cell_type:"code",metadata:{},source:e.source,...t(e)})};return`${JSON.stringify(n,null,1)}\n`}export function statusChar(t){return"markdown"===t.kind?"md":null===t.result?"○":t.result.ok?"✓":"✗"}export function cellPreview(t,e){const n=(t.source.split("\n").find(t=>""!==t.trim())??"").trim();return n.length<=e?n:`${n.slice(0,Math.max(0,e-1))}…`}export function parseRunRange(t,e){const n=t.trim().toLowerCase();if(!n)return null;if("all"===n)return Array.from({length:e},(t,e)=>e+1);const r=n.match(/^(\d+)(?:-(\d+))?$/);if(!r)return null;const o=Number(r[1]),u=r[2]?Number(r[2]):o;return o<1||u<o||u>e?null:Array.from({length:u-o+1},(t,e)=>o+e)}
|
package/dist/lib/sandbox.d.ts
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox images, newest first. `2026.1` bakes the aienv ML venv (core+data+
|
|
3
|
+
* physics groups, mujoco==3.9.0 judge-parity pin) at ~/.icoa/aienv — the same
|
|
4
|
+
* path `aienv setup` provisions on a host — so arena / self_check / notebook
|
|
5
|
+
* flows work inside the container without teams self-installing an ML stack
|
|
6
|
+
* that drifts from the server judge (the 2026-07 finals feedback). The legacy
|
|
7
|
+
* `2026` tag stays as a fallback so the sandbox keeps working for users who
|
|
8
|
+
* only have (or can only reach) the old image.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SANDBOX_IMAGES: readonly ["icoa/sandbox:2026.1", "icoa/sandbox:2026"];
|
|
11
|
+
/**
|
|
12
|
+
* Pick the sandbox image: any LOCAL image first (offline-graceful — never block
|
|
13
|
+
* a session on a multi-GB pull when a working sandbox is already on disk), then
|
|
14
|
+
* pull newest-first. null → caller decides the fallback (host shell / local
|
|
15
|
+
* build). Probes are injected so the ladder stays pure + unit-tested (tests
|
|
16
|
+
* live in test/shell-sandbox-mounts.test.js via the shell.ts re-export).
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveSandboxImage(hasLocal: (image: string) => boolean, pull: (image: string) => boolean, images?: readonly string[]): string | null;
|
|
1
19
|
export declare function isDockerAvailable(): boolean;
|
|
2
20
|
export declare function ensureSandbox(): Promise<boolean>;
|
|
3
21
|
export declare function runInSandbox(command: string, rl: {
|
package/dist/lib/sandbox.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{execSync as o,spawn as e}from"node:child_process";import chalk from"chalk";const
|
|
1
|
+
import{execSync as o,spawn as e}from"node:child_process";import chalk from"chalk";export const SANDBOX_IMAGES=["icoa/sandbox:2026.1","icoa/sandbox:2026"];export function resolveSandboxImage(o,e,r=SANDBOX_IMAGES){for(const e of r)if(o(e))return e;for(const o of r)if(e(o))return o;return null}const r="icoa-sandbox";export function isDockerAvailable(){try{return o("docker info",{stdio:"ignore"}),!0}catch{return!1}}export async function ensureSandbox(){if(!isDockerAvailable())return console.log(chalk.yellow(" Docker not found. Install Docker Desktop to use sandbox tools.")),console.log(chalk.gray(" https://www.docker.com/products/docker-desktop")),!1;if(function(){try{return"true"===o(`docker inspect -f '{{.State.Running}}' ${r} 2>/dev/null`,{encoding:"utf-8"}).trim()}catch{return!1}}())return!0;try{return o(`docker start ${r}`,{stdio:"ignore"}),!0}catch{}let e=resolveSandboxImage(e=>{try{return o(`docker image inspect ${e}`,{stdio:"ignore"}),!0}catch{return!1}},e=>{console.log(chalk.gray(` Pulling ${e} (first time only)...`));try{return o(`docker pull ${e}`,{stdio:"inherit"}),!0}catch{return!1}});if(!e){console.log(chalk.gray(" Building sandbox from local Dockerfile.aienv..."));try{const r=new URL("../../docker",import.meta.url).pathname;o(`docker build -f ${r}/Dockerfile.aienv -t ${SANDBOX_IMAGES[0]} ${r}`,{stdio:"inherit"}),e=SANDBOX_IMAGES[0]}catch{return console.log(chalk.red(" Failed to set up sandbox.")),!1}}try{return o(`docker run -d --name ${r} -v icoa-challenges:/home/competitor/challenges --network bridge ${e} sleep infinity`,{stdio:"ignore"}),!0}catch{return console.log(chalk.red(" Failed to start sandbox container.")),!1}}export function runInSandbox(o,t){return new Promise(n=>{t.pause();const c=e("docker",["exec","-it",r,"bash","-c",o],{stdio:"inherit",shell:!0});c.on("close",()=>{t.resume(),n()}),c.on("error",()=>{t.resume(),n()})})}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "icoa-cli",
|
|
3
|
-
"version": "2.19.
|
|
3
|
+
"version": "2.19.356",
|
|
4
4
|
"description": "ICOA CLI — The world's first CLI-native cyber & AI security olympiad terminal: AI4CTF (Day 1), CTF4AI (Day 2), VLA4CTF (Pioneer Round — embodied AI)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|