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/commands/files.js
CHANGED
|
@@ -1,59 +1 @@
|
|
|
1
|
-
import chalk from
|
|
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
|
-
}
|
|
1
|
+
import chalk from"chalk";import{CTFdClient as o}from"../lib/ctfd-client.js";import{getConfig as e,isConnected as i}from"../lib/config.js";import{challengeDownloadDir as l}from"../lib/challenge-dir.js";import{logCommand as t}from"../lib/logger.js";import{printError as n,createSpinner as s}from"../lib/ui.js";export function registerFilesCommand(c){c.command("files <id>").description("Download challenge files").action(async c=>{t(`files ${c}`);const a=e();if(!i())return void n("Not connected. Run: join <url>");const r=a.sessionCookie||"",f=a.token&&!a.token.includes("session=")?a.token:"",g=new o(a.ctfdUrl,f,r||a.token),d=l(c),h=s("Fetching challenge files...");h.start();try{const o=await g.getChallengeFiles(parseInt(c,10));if(!o||0===o.length)return void h.info("No files attached to this challenge.");h.text=`Downloading ${o.length} file(s)...`;const e=[];for(const i of o)try{const o=await g.downloadFile(i,d);e.push(o)}catch(o){h.warn(`Failed to download: ${i}`)}h.succeed(`Downloaded ${e.length} file(s)`),console.log(chalk.gray(` Location: ${d}`));for(const o of e)console.log(chalk.gray(` → ${o.split("/").pop()}`));const i=e.length?e[0].split("/").pop():"<file>";console.log(chalk.gray(` Run tools on them directly, e.g. ${chalk.white(`file ${c}/${i}`)}`)),console.log(chalk.gray(` Open an image/audio file with: ${chalk.white(`view ${c} <file>`)}`)),console.log()}catch(o){h.fail("Failed to download files"),n(o.message)}})}
|
package/dist/commands/lang.js
CHANGED
|
@@ -1,202 +1 @@
|
|
|
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
|
-
}
|
|
1
|
+
import chalk from"chalk";import{getConfig as e,saveConfig as o}from"../lib/config.js";import{COUNTRY_LANG as s}from"../lib/country-lang.js";import{getExamState as a,getRealExamState as t}from"../lib/exam-state.js";import{logCommand as n}from"../lib/logger.js";import{ensureLangCache as r}from"../lib/translations-fetcher.js";import{loadUiPack as i}from"../lib/i18n.js";import{printSuccess as l,printError as c,printInfo as g}from"../lib/ui.js";import{SUPPORTED_LANGUAGES as u}from"../types/index.js";const m={en:"English",zh:"中文 (Chinese)",ja:"日本語 (Japanese)",ko:"한국어 (Korean)",es:"Español (Spanish)",ar:"العربية (Arabic)",fr:"Français (French)",pt:"Português (Portuguese)",ru:"Русский (Russian)",hi:"हिन्दी (Hindi)",de:"Deutsch (German)",id:"Bahasa (Indonesian)",th:"ไทย (Thai)",vi:"Tiếng Việt (Vietnamese)",tr:"Türkçe (Turkish)",uk:"Українська (Ukrainian)",ht:"Kreyòl (Haitian Creole)",sw:"Kiswahili (Swahili)",uz:"Oʻzbek (Uzbek)",lo:"ລາວ (Lao)"};export function registerLangCommand(p){p.command("lang [code]").description("Switch display language").action(async p=>{if(n(`lang ${p||""}`),!p){const o=e();g(`Current language: ${chalk.white(m[o.language]||o.language)}`),console.log(),console.log(chalk.gray(" Supported languages:"));for(const e of u){const s=o.language===e?chalk.yellow(" ← current"):"";console.log(` ${chalk.white(e)} ${m[e]}${s}`)}return console.log(),console.log(chalk.gray(" Switch now: ")+chalk.cyan("lang <code>")+chalk.gray(" (e.g. ")+chalk.cyan("lang es")+chalk.gray(")")),console.log(chalk.gray(' No "back" needed — you are still at the ')+chalk.cyan("icoa>")+chalk.gray(" prompt.")),void console.log()}if(!u.includes(p)){c(`Unsupported language: ${p}`);const e=u.map(e=>`${e} (${m[e]?.split(" ")[0]||e})`).join(", ");return void g(`Supported: ${e}`)}const d=t(),y=d?.session?.token;if(y){const e=y.substring(0,2).toUpperCase(),o=s[e];if(o&&p!==o&&"en"!==p)return c("Language is locked during your exam."),g(`Your token (${e}xxx) supports two languages: ${chalk.cyan("en")} (English) or ${chalk.cyan(o)} (${m[o]||o}).`),void g("To change language, finish or quit the current exam first.")}o({language:p}),l(`Language set to: ${m[p]||p}`),await r(p),i(p);const h=a();if(h&&"demo-free"===h.session.examId)try{const{pickDemoQuestions:e,getLocalizedDemoSession:o,getLocalizedDemoQuestions:s,getLocalizedExplanations:a,DEMO_PICK_SIZE:t}=await import("../lib/demo-exam.js"),{saveExamState:n}=await import("../lib/exam-state.js");if(h.questions.every(e=>null!=e.sourceNumber&&Array.isArray(e.sourceOrder)&&4===e.sourceOrder.length)){const e=s(),t=a();h.questions=h.questions.map(o=>{const s=e.find(e=>e.number===o.sourceNumber);return s&&o.sourceOrder?{...o,text:s.text,category:s.category,options:{A:s.options[o.sourceOrder[0]],B:s.options[o.sourceOrder[1]],C:s.options[o.sourceOrder[2]],D:s.options[o.sourceOrder[3]]},explanation:t[o.sourceNumber]}:o}),h.session.examName=o().examName,n(h);const r=h._lastQ||1;console.log(),console.log(chalk.green(` Demo continues in ${m[p]||p}. Your progress is kept.`)),console.log(chalk.white(` Resume: exam q ${r}`))}else h.questions=e(t),h.answers={},h.session.examName=o().examName,h.session.startedAt=(new Date).toISOString(),h._lastQ=1,n(h),console.log(),console.log(chalk.green(` Demo restarted in ${m[p]||p}.`)),console.log(chalk.white(" Type: exam q 1"))}catch{console.log(chalk.gray(" Language changed. Type: demo"))}else if(h&&h.session.token)try{const{getConfig:e}=await import("../lib/config.js"),{saveExamState:o}=await import("../lib/exam-state.js"),{getDeviceFingerprint:s}=await import("../lib/access.js"),a=e().ctfdUrl||"https://practice.icoa2026.au",t=h.session.token,n=await fetch(`${a}/api/icoa/exam-token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:t,deviceHash:s(),lang:p}),signal:AbortSignal.timeout(1e4)});if(n.ok){const e=(await n.json()).data.questions;h.questions=e,o(h);const s=h._lastQ||1;console.log(),l(`Exam questions updated to ${m[p]||p}. Your answers are kept.`),console.log(chalk.white(` Resume: exam q ${s}`))}else if(429===n.status){const e=await n.json().catch(()=>null),o=e?.message?.match(/(\d+)\s*s/i),s=o?parseInt(o[1],10):60;console.log(),g(`Server rate-limited that token (wait ~${s}s).`),g(`Your exam session is intact — try ${chalk.cyan(`lang ${p}`)} again after the cooldown.`)}else{const{clearExamState:e}=await import("../lib/exam-state.js");e(),console.log(),console.log(chalk.green(" ✓ Old session record cleared — your scores are safe.")),console.log(chalk.gray(" To start (or resume) your exam, type your token:")),console.log(chalk.gray(" → ")+chalk.bold.cyan("exam <your-token>"))}}catch{console.log(chalk.yellow(" Could not reach server. Language changed for UI only."))}else if(h){const e=h._lastQ||1;console.log(),console.log(chalk.gray(` Exam in progress — resuming Q${e}:`)),console.log(chalk.white(` Type: exam q ${e}`))}})}
|
package/dist/commands/log.js
CHANGED
|
@@ -1,171 +1 @@
|
|
|
1
|
-
import chalk from
|
|
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
|
-
}
|
|
1
|
+
import chalk from"chalk";import{readFileSync as o,existsSync as t}from"node:fs";import{join as e}from"node:path";import{getSessionLog as n}from"../lib/logger.js";import{getIcoaDir as s,getConfig as i}from"../lib/config.js";import{printHeader as l,printInfo as a,printTable as r}from"../lib/ui.js";export function registerLogCommand(c){const g=c.command("log").description("Display session history").action(()=>{!function(){const o=n();if(0===o.length)return void a("No session log entries yet.");l("Session Log");const t=o.map(o=>{const t=o.timestamp.replace("T"," ").substring(0,19),e={A:chalk.green,B:chalk.yellow,C:chalk.red,command:chalk.blue,submit:chalk.magenta}[o.level]||chalk.gray,n=o.input.length>60?`${o.input.substring(0,57)}...`:o.input;return[chalk.gray(t),e(o.level.padEnd(7)),n]});r(["Time","Type","Content"],t),console.log(chalk.gray(` ${o.length} entries total`)),console.log(),console.log(chalk.gray(" You are at the ")+chalk.cyan("icoa>")+chalk.gray(" prompt. Also: ")+chalk.cyan("log stats")+chalk.gray(" · ")+chalk.cyan("log export")+chalk.gray(" · ")+chalk.cyan("help")+chalk.gray(" all commands.")),console.log()}()});g.command("export").description("Export full audit log for review").action(async()=>{await async function(){const l=i(),a=s(),r=(e(a,"session.log"),e(a,"session-state.json")),c=(e(a,"config.json"),(new Date).toISOString().replace(/[:.]/g,"-").substring(0,19)),g=`icoa-audit-${l.userName||"unknown"}-${c}.json`,m=e(process.cwd(),g),y={exportedAt:(new Date).toISOString(),version:"1.7.2",competitor:{userName:l.userName,userId:l.userId,teamName:l.teamName,teamId:l.teamId,sessionId:l.sessionId},connection:{ctfdUrl:l.ctfdUrl},session:t(r)?JSON.parse(o(r,"utf-8")):null,commands:n()},d=n(),u={};for(const o of d)u[o.level]=(u[o.level]||0)+1;y.summary={totalCommands:d.length,byType:u,firstEntry:d[0]?.timestamp||null,lastEntry:d[d.length-1]?.timestamp||null};const{writeFileSync:p}=await import("node:fs");p(m,JSON.stringify(y,null,2)),console.log(),console.log(chalk.green(" ✓ Audit log exported")),console.log(chalk.white(` ${m}`)),console.log(),console.log(chalk.gray(" Contents:")),console.log(chalk.gray(` Commands: ${d.length}`)),Object.entries(u).forEach(([o,t])=>{console.log(chalk.gray(` ${o}: ${t}`))}),console.log(),console.log(chalk.gray(" This file contains the complete session audit trail.")),console.log(chalk.gray(" Submit to organizers for post-competition verification.")),console.log()}()}),g.command("stats").description("Show session statistics").action(()=>{!function(){const i=n(),l=s(),a=e(l,"session-state.json");if(console.log(),console.log(chalk.bold.white(" Session Statistics")),console.log(chalk.gray(" ─────────────────────────────────────────────")),0===i.length)return console.log(chalk.gray(" No activity recorded yet.")),void console.log();const r=new Date(i[0].timestamp),c=new Date(i[i.length-1].timestamp),g=Math.round((c.getTime()-r.getTime())/6e4);console.log(chalk.gray(" First activity: ")+chalk.white(r.toLocaleString())),console.log(chalk.gray(" Last activity: ")+chalk.white(c.toLocaleString())),console.log(chalk.gray(" Duration: ")+chalk.white(`${g} min`)),console.log();const m={};for(const o of i)m[o.level]=(m[o.level]||0)+1;if(console.log(chalk.gray(" Total commands: ")+chalk.white(String(i.length))),m.command&&console.log(chalk.blue(" commands: ")+chalk.white(String(m.command))),m.A&&console.log(chalk.green(" hint A: ")+chalk.white(String(m.A))),m.B&&console.log(chalk.yellow(" hint B: ")+chalk.white(String(m.B))),m.C&&console.log(chalk.red(" hint C: ")+chalk.white(String(m.C))),m.submit&&console.log(chalk.magenta(" submissions: ")+chalk.white(String(m.submit))),t(a))try{const t=JSON.parse(o(a,"utf-8"));if(console.log(),console.log(chalk.gray(" Exit count: ")+chalk.white(String(t.exitCount||0))),t.totalAwaySeconds){const o=Math.round(t.totalAwaySeconds/60);console.log(chalk.gray(" Total away: ")+chalk.white(`${o} min`))}}catch{}console.log()}()})}
|
package/dist/commands/shell.js
CHANGED
|
@@ -1,151 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import { printInfo, printSuccess } from '../lib/ui.js';
|
|
4
|
-
import { logCommand } from '../lib/logger.js';
|
|
5
|
-
import { getActiveCwd } from '../lib/exam-sandbox.js';
|
|
6
|
-
import { isDockerRunning } from '../lib/docker-probe.js';
|
|
7
|
-
/**
|
|
8
|
-
* Decide which shell to launch. The Docker sandbox is the PREFERRED path (extra
|
|
9
|
-
* isolation), but it is OPTIONAL: the 110 CTF tools are installed on the HOST by
|
|
10
|
-
* `env setup`, and the `env` doctor explicitly marks docker "optional — not
|
|
11
|
-
* needed". So when Docker is absent we must NOT dead-end the user with an
|
|
12
|
-
* "install Docker" wall — we drop them into a host shell where every tool they
|
|
13
|
-
* need already lives. Pure + testable so the no-docker → host fallback is locked
|
|
14
|
-
* by a regression test.
|
|
15
|
-
*/
|
|
16
|
-
export function resolveShellLaunch(opts) {
|
|
17
|
-
if (opts.dockerAvailable) {
|
|
18
|
-
return { mode: 'docker', command: 'docker' };
|
|
19
|
-
}
|
|
20
|
-
const command = opts.platform === 'win32' ? opts.comspecEnv || 'cmd.exe' : opts.shellEnv || '/bin/bash';
|
|
21
|
-
return { mode: 'host', command };
|
|
22
|
-
}
|
|
23
|
-
// The image ladder is shared with the REPL's persistent-container path — one
|
|
24
|
-
// source of truth in lib/sandbox.ts; re-exported here for the unit tests.
|
|
25
|
-
export { SANDBOX_IMAGES, resolveSandboxImage } from '../lib/sandbox.js';
|
|
26
|
-
import { SANDBOX_IMAGES, resolveSandboxImage } from '../lib/sandbox.js';
|
|
27
|
-
/**
|
|
28
|
-
* docker-run args as an ARRAY (spawnSync — no shell, so a cwd with spaces
|
|
29
|
-
* survives untouched). The two mounts are the aienv × docker interlock:
|
|
30
|
-
* - cwd → /work: challenge files flow both ways instead of being invisible
|
|
31
|
-
* from inside the container (and work done in /work outlives --rm).
|
|
32
|
-
* - icoa-aienv named volume → /root/.icoa: docker's copy-up seeds it from the
|
|
33
|
-
* image-baked venv on first use, then in-container pip installs survive
|
|
34
|
-
* --rm across sessions. Reset anytime: `docker volume rm icoa-aienv`.
|
|
35
|
-
*/
|
|
36
|
-
export function buildSandboxRunArgs(opts) {
|
|
37
|
-
return [
|
|
38
|
-
'run',
|
|
39
|
-
'--rm',
|
|
40
|
-
'-it',
|
|
41
|
-
'--name',
|
|
42
|
-
opts.containerName,
|
|
43
|
-
'--network=host',
|
|
44
|
-
'-v',
|
|
45
|
-
`${opts.cwd}:/work`,
|
|
46
|
-
'-w',
|
|
47
|
-
'/work',
|
|
48
|
-
'-v',
|
|
49
|
-
'icoa-aienv:/root/.icoa',
|
|
50
|
-
opts.image,
|
|
51
|
-
'/bin/bash',
|
|
52
|
-
];
|
|
53
|
-
}
|
|
54
|
-
// Probe the daemon socket, not the `docker` CLI — checking must never wake a
|
|
55
|
-
// stopped Docker Desktop (the sandbox is optional; host shell is the fallback).
|
|
56
|
-
// See lib/docker-probe.ts.
|
|
57
|
-
function isDockerAvailable() {
|
|
58
|
-
return isDockerRunning();
|
|
59
|
-
}
|
|
60
|
-
function openHostShell() {
|
|
61
|
-
const cwd = getActiveCwd();
|
|
62
|
-
const { command } = resolveShellLaunch({
|
|
63
|
-
dockerAvailable: false,
|
|
64
|
-
platform: process.platform,
|
|
65
|
-
shellEnv: process.env.SHELL,
|
|
66
|
-
comspecEnv: process.env.COMSPEC,
|
|
67
|
-
});
|
|
68
|
-
printInfo('Docker not found — opening a host shell.');
|
|
69
|
-
console.log(chalk.gray(' All 110 CTF tools run on your host. If any are missing, run ') +
|
|
70
|
-
chalk.cyan('env setup') +
|
|
71
|
-
chalk.gray('.'));
|
|
72
|
-
console.log(chalk.gray(" Type 'exit' to return to ICOA CLI."));
|
|
73
|
-
console.log();
|
|
74
|
-
try {
|
|
75
|
-
execSync(command, { stdio: 'inherit', cwd });
|
|
76
|
-
console.log();
|
|
77
|
-
printSuccess('Shell session ended.');
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
// A non-zero exit (incl. the user typing `exit`) lands here — not an error.
|
|
81
|
-
console.log();
|
|
82
|
-
printInfo('Shell session ended.');
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
export function registerShellCommand(program) {
|
|
86
|
-
program
|
|
87
|
-
.command('shell')
|
|
88
|
-
.description('Open an interactive CTF shell (Docker sandbox if available, else host)')
|
|
89
|
-
.action(async () => {
|
|
90
|
-
logCommand('shell');
|
|
91
|
-
// Docker is the preferred (isolated) path but is OPTIONAL — fall back to
|
|
92
|
-
// the host shell instead of telling the user to install Docker.
|
|
93
|
-
if (!(await isDockerAvailable())) {
|
|
94
|
-
openHostShell();
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
const hasLocal = (image) => {
|
|
98
|
-
try {
|
|
99
|
-
execSync(`docker image inspect ${image}`, { stdio: 'ignore' });
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
const pull = (image) => {
|
|
107
|
-
printInfo(`Pulling ${image}...`);
|
|
108
|
-
try {
|
|
109
|
-
execSync(`docker pull ${image}`, { stdio: 'inherit' });
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
const image = resolveSandboxImage(hasLocal, pull);
|
|
117
|
-
if (image === null) {
|
|
118
|
-
// No image local or pullable — don't dead-end, the host has the tools.
|
|
119
|
-
printInfo('Could not pull a sandbox image — using your host shell instead.');
|
|
120
|
-
openHostShell();
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
const cwd = getActiveCwd();
|
|
124
|
-
printSuccess('Launching sandbox...');
|
|
125
|
-
console.log(chalk.gray(' Your folder is mounted at ') +
|
|
126
|
-
chalk.cyan('/work') +
|
|
127
|
-
chalk.gray(' — files there persist on your host.'));
|
|
128
|
-
if (image === SANDBOX_IMAGES[0]) {
|
|
129
|
-
console.log(chalk.gray(' ML venv: ') +
|
|
130
|
-
chalk.cyan('~/.icoa/aienv/bin/python') +
|
|
131
|
-
chalk.gray(' (sklearn / jupyter / mujoco) — its pip installs persist across sessions.'));
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
console.log(chalk.gray(' This image has no ML venv — get the newer one with ') +
|
|
135
|
-
chalk.cyan(`docker pull ${SANDBOX_IMAGES[0]}`));
|
|
136
|
-
}
|
|
137
|
-
console.log(chalk.gray(" Type 'exit' to return to ICOA CLI."));
|
|
138
|
-
console.log();
|
|
139
|
-
const containerName = `icoa-sandbox-${Date.now()}`;
|
|
140
|
-
const result = spawnSync('docker', buildSandboxRunArgs({ cwd, image, containerName }), {
|
|
141
|
-
stdio: 'inherit',
|
|
142
|
-
});
|
|
143
|
-
console.log();
|
|
144
|
-
if (result.error) {
|
|
145
|
-
printInfo('Sandbox session ended.');
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
printSuccess('Sandbox session ended.');
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
}
|
|
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 i}from"../lib/exam-sandbox.js";import{isDockerRunning as l}from"../lib/docker-probe.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 t,resolveSandboxImage as a}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 c(){const e=i(),{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(d){d.command("shell").description("Open an interactive CTF shell (Docker sandbox if available, else host)").action(async()=>{if(s("shell"),!await l())return void c();const d=a(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===d)return n("Could not pull a sandbox image — using your host shell instead."),void c();const m=i();r("Launching sandbox..."),console.log(chalk.gray(" Your folder is mounted at ")+chalk.cyan("/work")+chalk.gray(" — files there persist on your host.")),d===t[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 ${t[0]}`)),console.log(chalk.gray(" Type 'exit' to return to ICOA CLI.")),console.log();const u=`icoa-sandbox-${Date.now()}`,g=e("docker",buildSandboxRunArgs({cwd:m,image:d,containerName:u}),{stdio:"inherit"});console.log(),g.error?n("Sandbox session ended."):r("Sandbox session ended.")})}
|