icoa-cli 2.19.357 → 2.19.359
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/ctf.js +1 -787
- package/dist/commands/ctf4ai-demo.js +1 -1
- package/dist/commands/ctf4vla.js +1 -1
- package/dist/commands/demo2.js +1 -1502
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +1 -0
- package/dist/commands/exam.js +1 -1
- package/dist/commands/files.js +1 -59
- package/dist/commands/lang.js +1 -202
- package/dist/commands/log.js +1 -171
- package/dist/commands/shell.js +1 -151
- package/dist/commands/sim.js +1 -389
- package/dist/index.js +1 -355
- package/dist/lib/access.js +1 -184
- package/dist/lib/aienv.js +1 -205
- package/dist/lib/arena-submit.js +1 -21
- package/dist/lib/banner.js +1 -31
- package/dist/lib/budget.js +1 -6
- package/dist/lib/challenge-dir.js +1 -16
- package/dist/lib/colors.js +1 -17
- package/dist/lib/comms.js +1 -212
- package/dist/lib/config.js +1 -93
- package/dist/lib/countdown.js +1 -43
- package/dist/lib/country-lang.js +1 -39
- package/dist/lib/ctfd-client.js +1 -417
- package/dist/lib/demo-exam.js +1 -478
- package/dist/lib/demo-flags.js +1 -27
- package/dist/lib/demo-stats.js +1 -62
- package/dist/lib/demo2-progress.js +1 -102
- package/dist/lib/docker-probe.js +1 -118
- package/dist/lib/editor-spawn.js +1 -53
- package/dist/lib/exam-client.js +1 -54
- package/dist/lib/exam-sandbox.js +1 -201
- package/dist/lib/exam-setup.js +1 -36
- package/dist/lib/exam-state.js +1 -273
- package/dist/lib/gemini.js +1 -247
- package/dist/lib/i18n.js +1 -302
- package/dist/lib/integrity-snapshot.js +1 -88
- package/dist/lib/interactive-spawn.js +1 -55
- package/dist/lib/ipynb-input.js +1 -65
- package/dist/lib/kernel-protocol.js +1 -88
- package/dist/lib/kernel.js +2 -146
- package/dist/lib/learn-curricula.js +1 -309
- package/dist/lib/learn-i18n.js +1 -184
- package/dist/lib/learn-input.js +1 -101
- package/dist/lib/learn-render.js +1 -863
- package/dist/lib/learn-state.js +1 -103
- package/dist/lib/log-sync.js +1 -155
- package/dist/lib/logger.js +1 -49
- package/dist/lib/main-rl.js +1 -7
- package/dist/lib/menu-nav.js +1 -105
- package/dist/lib/notebook-doc.js +1 -137
- package/dist/lib/open-file.js +1 -55
- package/dist/lib/paper-upgrade.js +1 -119
- package/dist/lib/platform.js +1 -99
- package/dist/lib/render-card.js +1 -112
- package/dist/lib/repl-asker.js +1 -67
- package/dist/lib/sample-runner.js +1 -227
- package/dist/lib/sandbox.js +1 -144
- package/dist/lib/shell-split.js +1 -69
- package/dist/lib/sim-cooldown.js +1 -75
- package/dist/lib/theme.js +1 -119
- package/dist/lib/token-format.js +1 -74
- package/dist/lib/tool-man.js +1 -418
- package/dist/lib/toolset-hash.js +1 -48
- package/dist/lib/translation.js +1 -80
- package/dist/lib/translations-fetcher.js +1 -95
- package/dist/lib/ui.js +1 -99
- package/dist/lib/update-check.js +1 -114
- package/dist/lib/version.js +1 -24
- package/dist/postinstall.js +1 -48
- package/dist/repl.js +1 -2391
- package/dist/types/index.js +1 -63
- package/package.json +1 -1
package/dist/lib/sandbox.js
CHANGED
|
@@ -1,144 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import { isDockerRunning } from './docker-probe.js';
|
|
4
|
-
/**
|
|
5
|
-
* Sandbox images, newest first. `2026.1` bakes the aienv ML venv (core+data+
|
|
6
|
-
* physics groups, mujoco==3.9.0 judge-parity pin) at ~/.icoa/aienv — the same
|
|
7
|
-
* path `aienv setup` provisions on a host — so arena / self_check / notebook
|
|
8
|
-
* flows work inside the container without teams self-installing an ML stack
|
|
9
|
-
* that drifts from the server judge (the 2026-07 finals feedback). The legacy
|
|
10
|
-
* `2026` tag stays as a fallback so the sandbox keeps working for users who
|
|
11
|
-
* only have (or can only reach) the old image.
|
|
12
|
-
*/
|
|
13
|
-
export const SANDBOX_IMAGES = ['icoa/sandbox:2026.1', 'icoa/sandbox:2026'];
|
|
14
|
-
/**
|
|
15
|
-
* Pick the sandbox image: any LOCAL image first (offline-graceful — never block
|
|
16
|
-
* a session on a multi-GB pull when a working sandbox is already on disk), then
|
|
17
|
-
* pull newest-first. null → caller decides the fallback (host shell / local
|
|
18
|
-
* build). Probes are injected so the ladder stays pure + unit-tested (tests
|
|
19
|
-
* live in test/shell-sandbox-mounts.test.js via the shell.ts re-export).
|
|
20
|
-
*/
|
|
21
|
-
export function resolveSandboxImage(hasLocal, pull, images = SANDBOX_IMAGES) {
|
|
22
|
-
for (const image of images)
|
|
23
|
-
if (hasLocal(image))
|
|
24
|
-
return image;
|
|
25
|
-
for (const image of images)
|
|
26
|
-
if (pull(image))
|
|
27
|
-
return image;
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
const CONTAINER = 'icoa-sandbox';
|
|
31
|
-
/**
|
|
32
|
-
* Is Docker usable right now? Probes the daemon socket directly (never the
|
|
33
|
-
* `docker` CLI) so a stopped Docker Desktop is NOT auto-started — the sandbox
|
|
34
|
-
* is optional and the host shell is the fallback, so merely *checking* must not
|
|
35
|
-
* wake Docker. See lib/docker-probe.ts.
|
|
36
|
-
*/
|
|
37
|
-
export async function isDockerAvailable() {
|
|
38
|
-
return isDockerRunning();
|
|
39
|
-
}
|
|
40
|
-
function isSandboxRunning() {
|
|
41
|
-
try {
|
|
42
|
-
const out = execSync(`docker inspect -f '{{.State.Running}}' ${CONTAINER} 2>/dev/null`, {
|
|
43
|
-
encoding: 'utf-8',
|
|
44
|
-
});
|
|
45
|
-
return out.trim() === 'true';
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
export async function ensureSandbox() {
|
|
52
|
-
if (!(await isDockerAvailable())) {
|
|
53
|
-
console.log(chalk.yellow(' Docker not found. Install Docker Desktop to use sandbox tools.'));
|
|
54
|
-
console.log(chalk.gray(' https://www.docker.com/products/docker-desktop'));
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
if (isSandboxRunning())
|
|
58
|
-
return true;
|
|
59
|
-
// Check if container exists but stopped
|
|
60
|
-
try {
|
|
61
|
-
execSync(`docker start ${CONTAINER}`, { stdio: 'ignore' });
|
|
62
|
-
return true;
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
// Container doesn't exist, create it
|
|
66
|
-
}
|
|
67
|
-
// Resolve an image via the ladder: local-first, then pull newest-first.
|
|
68
|
-
const hasLocal = (image) => {
|
|
69
|
-
try {
|
|
70
|
-
execSync(`docker image inspect ${image}`, { stdio: 'ignore' });
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
const pull = (image) => {
|
|
78
|
-
console.log(chalk.gray(` Pulling ${image} (first time only)...`));
|
|
79
|
-
try {
|
|
80
|
-
execSync(`docker pull ${image}`, { stdio: 'inherit' });
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
let image = resolveSandboxImage(hasLocal, pull);
|
|
88
|
-
if (!image) {
|
|
89
|
-
// Nothing pullable — build the derived 2026.1 locally (Dockerfile.aienv
|
|
90
|
-
// pulls the frozen CTF base `2026` itself as its FROM). We never rebuild
|
|
91
|
-
// the base from scratch: its 27 exact-pinned libs are integrity-frozen.
|
|
92
|
-
console.log(chalk.gray(' Building sandbox from local Dockerfile.aienv...'));
|
|
93
|
-
try {
|
|
94
|
-
const dockerDir = new URL('../../docker', import.meta.url).pathname;
|
|
95
|
-
execSync(`docker build -f ${dockerDir}/Dockerfile.aienv -t ${SANDBOX_IMAGES[0]} ${dockerDir}`, {
|
|
96
|
-
stdio: 'inherit',
|
|
97
|
-
});
|
|
98
|
-
image = SANDBOX_IMAGES[0];
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
console.log(chalk.red(' Failed to set up sandbox.'));
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
// Create and start container
|
|
106
|
-
try {
|
|
107
|
-
execSync(`docker run -d --name ${CONTAINER} ` +
|
|
108
|
-
`-v icoa-challenges:/home/competitor/challenges ` +
|
|
109
|
-
// Default bridge network, NOT `--network host`. Host networking shares
|
|
110
|
-
// the contestant's network namespace, so sandboxed solve-code could reach
|
|
111
|
-
// services bound to the host's 127.0.0.1 (internal eval/admin endpoints).
|
|
112
|
-
// Bridge still allows outbound to remote challenge targets + internet via
|
|
113
|
-
// NAT — only host-localhost is cut off, which no legitimate practical
|
|
114
|
-
// needs. See B9 in docs/POST_COMPETITION_AUDIT_STANDARD.md §10.
|
|
115
|
-
// KNOWN FOLLOW-UP (B9, deferred): the single shared `icoa-challenges`
|
|
116
|
-
// volume lets one challenge's code read another's files. Per-task
|
|
117
|
-
// ephemeral volumes need reworking the persistent-container model.
|
|
118
|
-
`--network bridge ` +
|
|
119
|
-
`${image} sleep infinity`, { stdio: 'ignore' });
|
|
120
|
-
return true;
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
console.log(chalk.red(' Failed to start sandbox container.'));
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
export function runInSandbox(command, rl) {
|
|
128
|
-
return new Promise((resolve) => {
|
|
129
|
-
rl.pause();
|
|
130
|
-
const opts = {
|
|
131
|
-
stdio: 'inherit',
|
|
132
|
-
shell: true,
|
|
133
|
-
};
|
|
134
|
-
const child = spawn('docker', ['exec', '-it', CONTAINER, 'bash', '-c', command], opts);
|
|
135
|
-
child.on('close', () => {
|
|
136
|
-
rl.resume();
|
|
137
|
-
resolve();
|
|
138
|
-
});
|
|
139
|
-
child.on('error', () => {
|
|
140
|
-
rl.resume();
|
|
141
|
-
resolve();
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
}
|
|
1
|
+
import{execSync as o,spawn as e}from"node:child_process";import chalk from"chalk";import{isDockerRunning as r}from"./docker-probe.js";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 t="icoa-sandbox";export async function isDockerAvailable(){return r()}export async function ensureSandbox(){if(!await 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}}' ${t} 2>/dev/null`,{encoding:"utf-8"}).trim()}catch{return!1}}())return!0;try{return o(`docker start ${t}`,{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 ${t} -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,r){return new Promise(n=>{r.pause();const c=e("docker",["exec","-it",t,"bash","-c",o],{stdio:"inherit",shell:!0});c.on("close",()=>{r.resume(),n()}),c.on("error",()=>{r.resume(),n()})})}
|
package/dist/lib/shell-split.js
CHANGED
|
@@ -1,69 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* shellSplit — a minimal, quote-aware tokenizer for REPL commands that forward
|
|
3
|
-
* raw args straight to a child process (currently `aienv run` / `aienv python`).
|
|
4
|
-
*
|
|
5
|
-
* The icoa REPL is NOT a shell: the default command split on /\s+/ turns quotes
|
|
6
|
-
* into literal characters and tears spaced args apart, so `aienv run "my file.py"`
|
|
7
|
-
* and `aienv python -c "print(1 + 1)"` reach python mangled. This honours
|
|
8
|
-
* single/double quotes (and backslash escapes inside double quotes / outside
|
|
9
|
-
* quotes) the way a real shell would tokenize them. (Direct-CLI invocation never
|
|
10
|
-
* needs this — there the system shell does the tokenization for us.)
|
|
11
|
-
*
|
|
12
|
-
* Scope is deliberately small: word-splitting + quote removal + backslash
|
|
13
|
-
* escapes. No globbing, no variable/`$()` expansion, no `~` expansion — the
|
|
14
|
-
* child process / caller handles paths, and we explicitly do not want shell
|
|
15
|
-
* metacharacter semantics in the REPL.
|
|
16
|
-
*/
|
|
17
|
-
export function shellSplit(input) {
|
|
18
|
-
const tokens = [];
|
|
19
|
-
let cur = '';
|
|
20
|
-
let inSingle = false;
|
|
21
|
-
let inDouble = false;
|
|
22
|
-
let hasTok = false; // so an explicit "" / '' still yields an empty-string arg
|
|
23
|
-
for (let i = 0; i < input.length; i++) {
|
|
24
|
-
const c = input[i];
|
|
25
|
-
if (inSingle) {
|
|
26
|
-
// Inside single quotes everything is literal until the closing quote.
|
|
27
|
-
if (c === "'")
|
|
28
|
-
inSingle = false;
|
|
29
|
-
else
|
|
30
|
-
cur += c;
|
|
31
|
-
hasTok = true;
|
|
32
|
-
}
|
|
33
|
-
else if (inDouble) {
|
|
34
|
-
if (c === '"')
|
|
35
|
-
inDouble = false;
|
|
36
|
-
else if (c === '\\' && (input[i + 1] === '"' || input[i + 1] === '\\'))
|
|
37
|
-
cur += input[++i];
|
|
38
|
-
else
|
|
39
|
-
cur += c;
|
|
40
|
-
hasTok = true;
|
|
41
|
-
}
|
|
42
|
-
else if (c === "'") {
|
|
43
|
-
inSingle = true;
|
|
44
|
-
hasTok = true;
|
|
45
|
-
}
|
|
46
|
-
else if (c === '"') {
|
|
47
|
-
inDouble = true;
|
|
48
|
-
hasTok = true;
|
|
49
|
-
}
|
|
50
|
-
else if (c === '\\' && i + 1 < input.length) {
|
|
51
|
-
cur += input[++i];
|
|
52
|
-
hasTok = true;
|
|
53
|
-
}
|
|
54
|
-
else if (/\s/.test(c)) {
|
|
55
|
-
if (hasTok) {
|
|
56
|
-
tokens.push(cur);
|
|
57
|
-
cur = '';
|
|
58
|
-
hasTok = false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
cur += c;
|
|
63
|
-
hasTok = true;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (hasTok)
|
|
67
|
-
tokens.push(cur);
|
|
68
|
-
return tokens;
|
|
69
|
-
}
|
|
1
|
+
export function shellSplit(t){const e=[];let l="",n=!1,s=!1,h=!1;for(let o=0;o<t.length;o++){const p=t[o];n?("'"===p?n=!1:l+=p,h=!0):s?('"'===p?s=!1:"\\"!==p||'"'!==t[o+1]&&"\\"!==t[o+1]?l+=p:l+=t[++o],h=!0):"'"===p?(n=!0,h=!0):'"'===p?(s=!0,h=!0):"\\"===p&&o+1<t.length?(l+=t[++o],h=!0):/\s/.test(p)?h&&(e.push(l),l="",h=!1):(l+=p,h=!0)}return h&&e.push(l),e}
|
package/dist/lib/sim-cooldown.js
CHANGED
|
@@ -1,75 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Shared 60s cooldown for any client-side caller of the MuJoCo sim
|
|
3
|
-
* endpoint (`/api/ai/vla/41/sim`). Both `icoa sim <scenario>` and
|
|
4
|
-
* `icoa demo2`'s parameter mode hit the same render pipeline; the
|
|
5
|
-
* cooldown lives in a file so they share one budget.
|
|
6
|
-
*
|
|
7
|
-
* Why: render is CPU-heavy shared infrastructure. 60s/student keeps
|
|
8
|
-
* concurrent load survivable.
|
|
9
|
-
*
|
|
10
|
-
* Two additional fields piggy-back on the same file so demo2 can detect
|
|
11
|
-
* what a user has already experienced (bundled dance / interactive arm).
|
|
12
|
-
* These do NOT gate anything — they are continuity signals only.
|
|
13
|
-
*/
|
|
14
|
-
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
-
import { join } from 'node:path';
|
|
16
|
-
import { homedir } from 'node:os';
|
|
17
|
-
export const COOLDOWN_SECONDS = 60;
|
|
18
|
-
const COOLDOWN_FILE = join(homedir(), '.icoa', 'sim-cooldown.json');
|
|
19
|
-
function readFile() {
|
|
20
|
-
try {
|
|
21
|
-
const raw = readFileSync(COOLDOWN_FILE, 'utf-8');
|
|
22
|
-
const j = JSON.parse(raw);
|
|
23
|
-
return typeof j === 'object' && j !== null ? j : {};
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return {};
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
function writeMerge(patch) {
|
|
30
|
-
try {
|
|
31
|
-
mkdirSync(join(homedir(), '.icoa'), { recursive: true });
|
|
32
|
-
const merged = { ...readFile(), ...patch };
|
|
33
|
-
writeFileSync(COOLDOWN_FILE, JSON.stringify(merged));
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
// Non-fatal: cooldown is convenience, not security
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
export function lastSimAt() {
|
|
40
|
-
const f = readFile();
|
|
41
|
-
return typeof f.lastCallAt === 'number' ? f.lastCallAt : 0;
|
|
42
|
-
}
|
|
43
|
-
export function markSimAt(t) {
|
|
44
|
-
writeMerge({ lastCallAt: t });
|
|
45
|
-
}
|
|
46
|
-
/** Seconds remaining (0 if clear). */
|
|
47
|
-
export function cooldownRemaining() {
|
|
48
|
-
const since = (Date.now() - lastSimAt()) / 1000;
|
|
49
|
-
return Math.max(0, Math.ceil(COOLDOWN_SECONDS - since));
|
|
50
|
-
}
|
|
51
|
-
// ─── Continuity signals (not used for gating) ────────────────────────────
|
|
52
|
-
export function lastBundledAt() {
|
|
53
|
-
const f = readFile();
|
|
54
|
-
return typeof f.lastBundledAt === 'number' ? f.lastBundledAt : 0;
|
|
55
|
-
}
|
|
56
|
-
export function markBundledAt(t) {
|
|
57
|
-
writeMerge({ lastBundledAt: t });
|
|
58
|
-
}
|
|
59
|
-
export function lastArmAt() {
|
|
60
|
-
const f = readFile();
|
|
61
|
-
return typeof f.lastArmAt === 'number' ? f.lastArmAt : 0;
|
|
62
|
-
}
|
|
63
|
-
export function markArmAt(t) {
|
|
64
|
-
writeMerge({ lastArmAt: t });
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Has the user seen any sim render — bundled, server, or arm — at least once?
|
|
68
|
-
* Used by demo2 + boot screen to switch from "first time" to "continue" copy.
|
|
69
|
-
*/
|
|
70
|
-
export function hasAnySimHistory() {
|
|
71
|
-
const f = readFile();
|
|
72
|
-
return ((typeof f.lastBundledAt === 'number' && f.lastBundledAt > 0) ||
|
|
73
|
-
(typeof f.lastCallAt === 'number' && f.lastCallAt > 0) ||
|
|
74
|
-
(typeof f.lastArmAt === 'number' && f.lastArmAt > 0));
|
|
75
|
-
}
|
|
1
|
+
import{mkdirSync as t,readFileSync as n,writeFileSync as o}from"node:fs";import{join as r}from"node:path";import{homedir as e}from"node:os";export const COOLDOWN_SECONDS=60;const l=r(e(),".icoa","sim-cooldown.json");function a(){try{const t=n(l,"utf-8"),o=JSON.parse(t);return"object"==typeof o&&null!==o?o:{}}catch{return{}}}function s(n){try{t(r(e(),".icoa"),{recursive:!0});const s={...a(),...n};o(l,JSON.stringify(s))}catch{}}export function lastSimAt(){const t=a();return"number"==typeof t.lastCallAt?t.lastCallAt:0}export function markSimAt(t){s({lastCallAt:t})}export function cooldownRemaining(){const t=(Date.now()-lastSimAt())/1e3;return Math.max(0,Math.ceil(60-t))}export function lastBundledAt(){const t=a();return"number"==typeof t.lastBundledAt?t.lastBundledAt:0}export function markBundledAt(t){s({lastBundledAt:t})}export function lastArmAt(){const t=a();return"number"==typeof t.lastArmAt?t.lastArmAt:0}export function markArmAt(t){s({lastArmAt:t})}export function hasAnySimHistory(){const t=a();return"number"==typeof t.lastBundledAt&&t.lastBundledAt>0||"number"==typeof t.lastCallAt&&t.lastCallAt>0||"number"==typeof t.lastArmAt&&t.lastArmAt>0}
|
package/dist/lib/theme.js
CHANGED
|
@@ -1,119 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// GNOME Terminal, Konsole, Windows Terminal (cmd/PowerShell/WSL).
|
|
3
|
-
//
|
|
4
|
-
// Three mechanisms are combined so every modern terminal gets the best it can:
|
|
5
|
-
//
|
|
6
|
-
// 1. OSC 10/11/12 sets the terminal's *default* fg/bg/cursor colors.
|
|
7
|
-
// Honored by iTerm2, GNOME Terminal, Konsole, Windows Terminal → lossless
|
|
8
|
-
// background, no scrollback or resize artifacts. Ignored by Terminal.app.
|
|
9
|
-
//
|
|
10
|
-
// 2. SGR 38;2/48;2 + \x1b[2J paints with 24-bit truecolor Darcula on every
|
|
11
|
-
// terminal that supports truecolor. This is the default path.
|
|
12
|
-
//
|
|
13
|
-
// 3. SGR 38;5/48;5 + \x1b[2J paints with 256-color approximation on macOS
|
|
14
|
-
// Terminal.app. Terminal.app does NOT support truecolor SGR and mis-parses
|
|
15
|
-
// `\x1b[48;2;43;43;43m` as a sequence of 16-color codes — the trailing
|
|
16
|
-
// `43` becomes ANSI "bg yellow", which is why v2.19.23/24 rendered with a
|
|
17
|
-
// yellow background there. Color 235 ≈ #262626 (dark gray, ~#2B2B2B) and
|
|
18
|
-
// color 250 ≈ #BCBCBC (light gray, ~#A9B7C6) are close enough.
|
|
19
|
-
//
|
|
20
|
-
// Legacy cmd.exe (pre-Win10 1809) can't run Node 22 anyway, so no separate
|
|
21
|
-
// fallback path is needed.
|
|
22
|
-
const OSC_INIT_DARK = '\x1b]10;#A9B7C6\x07' + // default fg
|
|
23
|
-
'\x1b]11;#2B2B2B\x07' + // default bg
|
|
24
|
-
'\x1b]12;#A9B7C6\x07'; // cursor color
|
|
25
|
-
// High-contrast: pure black bg + pure white fg. For students with low vision
|
|
26
|
-
// or screens where Darcula's subtle grays wash out (e.g., projectors, cheap
|
|
27
|
-
// LCDs under fluorescent light). Still works with existing chalk colors —
|
|
28
|
-
// cyan/green/yellow/red all show up clearly against pure black.
|
|
29
|
-
const OSC_INIT_HC = '\x1b]10;#FFFFFF\x07' + '\x1b]11;#000000\x07' + '\x1b]12;#FFFFFF\x07';
|
|
30
|
-
const OSC_RESET = '\x1b]110\x07' + // reset default fg
|
|
31
|
-
'\x1b]111\x07' + // reset default bg
|
|
32
|
-
'\x1b]112\x07'; // reset cursor color
|
|
33
|
-
const SGR_INIT_TRUECOLOR_DARK = '\x1b[38;2;169;183;198m' + // fg #A9B7C6
|
|
34
|
-
'\x1b[48;2;43;43;43m' + // bg #2B2B2B
|
|
35
|
-
'\x1b[2J' +
|
|
36
|
-
'\x1b[H';
|
|
37
|
-
const SGR_INIT_256_DARK = '\x1b[38;5;250m' + // fg ≈ #BCBCBC
|
|
38
|
-
'\x1b[48;5;235m' + // bg ≈ #262626
|
|
39
|
-
'\x1b[2J' +
|
|
40
|
-
'\x1b[H';
|
|
41
|
-
const SGR_INIT_TRUECOLOR_HC = '\x1b[38;2;255;255;255m' + // fg pure white
|
|
42
|
-
'\x1b[48;2;0;0;0m' + // bg pure black
|
|
43
|
-
'\x1b[2J' +
|
|
44
|
-
'\x1b[H';
|
|
45
|
-
const SGR_INIT_256_HC = '\x1b[38;5;231m' + // fg white (231 = pure white in 256)
|
|
46
|
-
'\x1b[48;5;16m' + // bg black (16 = pure black in 256)
|
|
47
|
-
'\x1b[2J' +
|
|
48
|
-
'\x1b[H';
|
|
49
|
-
const SGR_RESET = '\x1b[0m\x1b[2J\x1b[H';
|
|
50
|
-
function supportsAnsi() {
|
|
51
|
-
if (!process.stdout.isTTY)
|
|
52
|
-
return false;
|
|
53
|
-
const depth = process.stdout.getColorDepth?.();
|
|
54
|
-
if (typeof depth === 'number')
|
|
55
|
-
return depth >= 8;
|
|
56
|
-
return true;
|
|
57
|
-
}
|
|
58
|
-
// When icoa-cli runs inside the ICOA Terminal (Tauri + xterm.js), the host is
|
|
59
|
-
// already pre-themed to the exact Darcula palette we'd be setting. Every OSC
|
|
60
|
-
// and SGR we'd emit is a no-op in terms of color, but the \x1b[2J inside our
|
|
61
|
-
// init/reset sequences would clear the grid visibly. Skip the paint entirely
|
|
62
|
-
// in that environment so the banner simply appears in the shell cursor
|
|
63
|
-
// position and scrollback is preserved on exit.
|
|
64
|
-
function isIcoaTerminal() {
|
|
65
|
-
return process.env.ICOA_TERMINAL === '1';
|
|
66
|
-
}
|
|
67
|
-
// macOS Terminal.app does not implement SGR truecolor (\x1b[38;2;… / \x1b[48;2;…)
|
|
68
|
-
// and mis-parses those sequences as 16-color codes, producing e.g. a yellow bg.
|
|
69
|
-
// Detect it and fall back to 256-color SGR which Terminal.app handles correctly.
|
|
70
|
-
function isAppleTerminal() {
|
|
71
|
-
return process.env.TERM_PROGRAM === 'Apple_Terminal';
|
|
72
|
-
}
|
|
73
|
-
let armed = false;
|
|
74
|
-
export function setTerminalTheme(variant = 'dark') {
|
|
75
|
-
if (!supportsAnsi())
|
|
76
|
-
return;
|
|
77
|
-
if (isIcoaTerminal())
|
|
78
|
-
return; // host is already Darcula; nothing to do
|
|
79
|
-
const osc = variant === 'high-contrast' ? OSC_INIT_HC : OSC_INIT_DARK;
|
|
80
|
-
const sgr = isAppleTerminal()
|
|
81
|
-
? variant === 'high-contrast'
|
|
82
|
-
? SGR_INIT_256_HC
|
|
83
|
-
: SGR_INIT_256_DARK
|
|
84
|
-
: variant === 'high-contrast'
|
|
85
|
-
? SGR_INIT_TRUECOLOR_HC
|
|
86
|
-
: SGR_INIT_TRUECOLOR_DARK;
|
|
87
|
-
process.stdout.write(osc + sgr);
|
|
88
|
-
if (!armed) {
|
|
89
|
-
armed = true;
|
|
90
|
-
// Belt-and-braces cleanup on every exit path. Without these, Ctrl+C leaves
|
|
91
|
-
// the user's shell stuck with our SGR state.
|
|
92
|
-
const cleanup = () => {
|
|
93
|
-
try {
|
|
94
|
-
process.stdout.write(OSC_RESET + SGR_RESET);
|
|
95
|
-
}
|
|
96
|
-
catch { }
|
|
97
|
-
};
|
|
98
|
-
process.on('exit', cleanup);
|
|
99
|
-
process.on('SIGINT', () => {
|
|
100
|
-
cleanup();
|
|
101
|
-
process.exit(130);
|
|
102
|
-
});
|
|
103
|
-
process.on('SIGTERM', () => {
|
|
104
|
-
cleanup();
|
|
105
|
-
process.exit(143);
|
|
106
|
-
});
|
|
107
|
-
process.on('SIGHUP', () => {
|
|
108
|
-
cleanup();
|
|
109
|
-
process.exit(129);
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
export function resetTerminalTheme() {
|
|
114
|
-
if (!supportsAnsi())
|
|
115
|
-
return;
|
|
116
|
-
if (isIcoaTerminal())
|
|
117
|
-
return; // nothing to undo
|
|
118
|
-
process.stdout.write(OSC_RESET + SGR_RESET);
|
|
119
|
-
}
|
|
1
|
+
const e="]110]111]112",t="[0m[2J[H";function s(){if(!process.stdout.isTTY)return!1;const e=process.stdout.getColorDepth?.();return"number"!=typeof e||e>=8}function r(){return"1"===process.env.ICOA_TERMINAL}let o=!1;export function setTerminalTheme(n="dark"){if(!s())return;if(r())return;const c="high-contrast"===n?"]10;#FFFFFF]11;#000000]12;#FFFFFF":"]10;#A9B7C6]11;#2B2B2B]12;#A9B7C6",i="Apple_Terminal"===process.env.TERM_PROGRAM?"high-contrast"===n?"[38;5;231m[48;5;16m[2J[H":"[38;5;250m[48;5;235m[2J[H":"high-contrast"===n?"[38;2;255;255;255m[48;2;0;0;0m[2J[H":"[38;2;169;183;198m[48;2;43;43;43m[2J[H";if(process.stdout.write(c+i),!o){o=!0;const s=()=>{try{process.stdout.write(e+t)}catch{}};process.on("exit",s),process.on("SIGINT",()=>{s(),process.exit(130)}),process.on("SIGTERM",()=>{s(),process.exit(143)}),process.on("SIGHUP",()=>{s(),process.exit(129)})}}export function resetTerminalTheme(){s()&&(r()||process.stdout.write(e+t))}
|
package/dist/lib/token-format.js
CHANGED
|
@@ -1,74 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Client mirror of panda/token_alphabet.py — the confusable-free token rule.
|
|
3
|
-
*
|
|
4
|
-
* Tokens are <2-char prefix> + <7 random Crockford chars> + <1 checksum char>.
|
|
5
|
-
* Crockford Base32 excludes I, L, O, U (look like 1, 1, 0, V) so a token copied
|
|
6
|
-
* off a PDF or dictated aloud can't be mistranscribed.
|
|
7
|
-
*
|
|
8
|
-
* Two helpers used at token entry:
|
|
9
|
-
* - normalizeTokenBody(): fix look-alikes a human typed (O→0, I/L→1, U→V) in
|
|
10
|
-
* the BODY only (prefix is a fixed track/country code, may contain I/O).
|
|
11
|
-
* - validTokenChecksum(): verify the trailing Crockford checksum.
|
|
12
|
-
*
|
|
13
|
-
* IMPORTANT backward-compat note: tokens issued before 2026-06-07 used the full
|
|
14
|
-
* 36-char alphabet and have NO valid checksum (and ~61% of learn tokens contain
|
|
15
|
-
* a real I/L/O/U in the body). So on the LEARN path normalization must be a
|
|
16
|
-
* FALLBACK (try literal first) and checksum must NEVER hard-block — the server
|
|
17
|
-
* stays authoritative (BUG-001). Exam tokens have always been Crockford+checksum
|
|
18
|
-
* so both helpers are safe to apply directly there.
|
|
19
|
-
*/
|
|
20
|
-
export const CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
21
|
-
const CROCKFORD_SET = new Set(CROCKFORD);
|
|
22
|
-
const CROCKFORD_INDEX = {};
|
|
23
|
-
for (let i = 0; i < CROCKFORD.length; i++)
|
|
24
|
-
CROCKFORD_INDEX[CROCKFORD[i]] = i;
|
|
25
|
-
export const PREFIX_LEN = 2;
|
|
26
|
-
export const BODY_RANDOM_LEN = 7;
|
|
27
|
-
export const TOKEN_LEN = PREFIX_LEN + BODY_RANDOM_LEN + 1; // 10
|
|
28
|
-
// Look-alikes a human types → their Crockford canonical char.
|
|
29
|
-
const CONFUSABLE_MAP = { O: '0', I: '1', L: '1', U: 'V' };
|
|
30
|
-
/** Replace confusable letters in a string (uppercased first). */
|
|
31
|
-
export function normalizeConfusables(s) {
|
|
32
|
-
let out = '';
|
|
33
|
-
for (const ch of s.toUpperCase())
|
|
34
|
-
out += CONFUSABLE_MAP[ch] ?? ch;
|
|
35
|
-
return out;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Normalize a token's BODY only — the 2-char prefix is a fixed track/country
|
|
39
|
-
* code (e.g. EI / IO legitimately contain I/O) and must NOT be transformed.
|
|
40
|
-
* Returns the trimmed, upper-cased token with body confusables canonicalized.
|
|
41
|
-
*/
|
|
42
|
-
export function normalizeTokenBody(token) {
|
|
43
|
-
const t = token.trim().toUpperCase();
|
|
44
|
-
if (t.length <= PREFIX_LEN)
|
|
45
|
-
return t;
|
|
46
|
-
return t.slice(0, PREFIX_LEN) + normalizeConfusables(t.slice(PREFIX_LEN));
|
|
47
|
-
}
|
|
48
|
-
/** 1-char Crockford mod-32 checksum over the payload chars. */
|
|
49
|
-
export function checksumChar(payload) {
|
|
50
|
-
let total = 0;
|
|
51
|
-
for (const ch of payload)
|
|
52
|
-
total += CROCKFORD_INDEX[ch] ?? 0;
|
|
53
|
-
return CROCKFORD[total % 32];
|
|
54
|
-
}
|
|
55
|
-
/** True iff `body` is 7 Crockford chars + 1 correct checksum char. */
|
|
56
|
-
export function validBody(body) {
|
|
57
|
-
if (body.length !== BODY_RANDOM_LEN + 1)
|
|
58
|
-
return false;
|
|
59
|
-
for (const ch of body)
|
|
60
|
-
if (!CROCKFORD_SET.has(ch))
|
|
61
|
-
return false;
|
|
62
|
-
return checksumChar(body.slice(0, BODY_RANDOM_LEN)) === body[BODY_RANDOM_LEN];
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Whole-token checksum check for a 10-char `<prefix><body>` token. Returns
|
|
66
|
-
* false for old (pre-checksum) tokens — only meaningful as a *soft* signal on
|
|
67
|
-
* the learn path; authoritative only for exam tokens.
|
|
68
|
-
*/
|
|
69
|
-
export function validTokenChecksum(token) {
|
|
70
|
-
const t = token.trim().toUpperCase();
|
|
71
|
-
if (t.length !== TOKEN_LEN)
|
|
72
|
-
return false;
|
|
73
|
-
return validBody(t.slice(PREFIX_LEN));
|
|
74
|
-
}
|
|
1
|
+
export const CROCKFORD="0123456789ABCDEFGHJKMNPQRSTVWXYZ";const o=new Set(CROCKFORD),e={};for(let n=0;n<32;n++)e[CROCKFORD[n]]=n;export const PREFIX_LEN=2;export const BODY_RANDOM_LEN=7;export const TOKEN_LEN=10;const t={O:"0",I:"1",L:"1",U:"V"};export function normalizeConfusables(o){let e="";for(const n of o.toUpperCase())e+=t[n]??n;return e}export function normalizeTokenBody(o){const e=o.trim().toUpperCase();return e.length<=2?e:e.slice(0,2)+normalizeConfusables(e.slice(2))}export function checksumChar(o){let t=0;for(const n of o)t+=e[n]??0;return CROCKFORD[t%32]}export function validBody(e){if(8!==e.length)return!1;for(const t of e)if(!o.has(t))return!1;return checksumChar(e.slice(0,7))===e[7]}export function validTokenChecksum(o){const e=o.trim().toUpperCase();return 10===e.length&&validBody(e.slice(2))}
|