icoa-cli 2.19.100 → 2.19.102
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 -700
- package/dist/commands/connect.js +1 -66
- package/dist/commands/ctf.js +1 -620
- package/dist/commands/ctf4ai-demo.js +1 -525
- package/dist/commands/env.js +1 -738
- package/dist/commands/exam.js +1 -2353
- package/dist/commands/files.js +1 -52
- package/dist/commands/hint.js +1 -119
- package/dist/commands/lang.js +1 -155
- package/dist/commands/log.js +1 -165
- package/dist/commands/note.js +1 -40
- package/dist/commands/ref.js +1 -68
- package/dist/commands/setup.js +1 -122
- package/dist/commands/shell.js +1 -55
- package/dist/commands/theme.js +1 -50
- package/dist/index.js +1 -225
- package/dist/lib/access.js +1 -246
- package/dist/lib/budget.js +1 -42
- package/dist/lib/colors.js +1 -21
- package/dist/lib/config.js +1 -60
- package/dist/lib/ctfd-client.js +1 -274
- package/dist/lib/demo-exam.js +1 -249
- package/dist/lib/demo-flags.js +1 -27
- package/dist/lib/demo-stats.js +1 -65
- package/dist/lib/exam-client.js +1 -57
- package/dist/lib/exam-setup.js +1 -23
- package/dist/lib/exam-state.js +1 -112
- package/dist/lib/gemini.js +1 -235
- package/dist/lib/i18n.js +1 -273
- package/dist/lib/log-sync.js +1 -110
- package/dist/lib/logger.js +1 -59
- package/dist/lib/paper-upgrade.js +1 -117
- package/dist/lib/platform.js +1 -86
- package/dist/lib/sandbox.js +1 -93
- package/dist/lib/terminal.js +1 -49
- package/dist/lib/theme.js +1 -108
- package/dist/lib/translation.js +1 -66
- package/dist/lib/ui.js +1 -80
- package/dist/lib/update-check.js +1 -102
- package/dist/postinstall.js +1 -48
- package/dist/repl.js +1 -1281
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +1 -38
- package/package.json +6 -2
- package/translations/sw/i18n-snippet.ts +1 -0
package/dist/lib/log-sync.js
CHANGED
|
@@ -1,110 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { getIcoaDir, getConfig } from './config.js';
|
|
4
|
-
import { getDeviceFingerprint } from './access.js';
|
|
5
|
-
// Every 30s, post new lines from ~/.icoa/session.log to the server audit
|
|
6
|
-
// endpoint. Two auth modes:
|
|
7
|
-
//
|
|
8
|
-
// 1. Logged-in user (CTFd token present) → Authorization: Token <token>,
|
|
9
|
-
// stored on the server as /opt/CTFd/audit/user-<userId>.jsonl
|
|
10
|
-
// 2. Anonymous demo user (no token) → X-Device-Fingerprint: <hash>,
|
|
11
|
-
// stored as /opt/CTFd/audit/demo-<fingerprint>.jsonl
|
|
12
|
-
//
|
|
13
|
-
// The demo path is what lets us observe the full prompt/command history of
|
|
14
|
-
// users who never log in (the main demo audience).
|
|
15
|
-
const SYNC_INTERVAL = 30_000;
|
|
16
|
-
const DEFAULT_SERVER = 'https://practice.icoa2026.au';
|
|
17
|
-
const SYNC_STATE_FILE = () => join(getIcoaDir(), 'sync-state.json');
|
|
18
|
-
let syncTimer = null;
|
|
19
|
-
function getSyncState() {
|
|
20
|
-
const file = SYNC_STATE_FILE();
|
|
21
|
-
if (!existsSync(file))
|
|
22
|
-
return { lastSyncedLine: 0, lastSyncAt: null, syncCount: 0, failCount: 0 };
|
|
23
|
-
try {
|
|
24
|
-
return JSON.parse(readFileSync(file, 'utf-8'));
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return { lastSyncedLine: 0, lastSyncAt: null, syncCount: 0, failCount: 0 };
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
function saveSyncState(state) {
|
|
31
|
-
writeFileSync(SYNC_STATE_FILE(), JSON.stringify(state, null, 2));
|
|
32
|
-
}
|
|
33
|
-
async function syncLogs() {
|
|
34
|
-
const config = getConfig();
|
|
35
|
-
const serverUrl = config.ctfdUrl || DEFAULT_SERVER;
|
|
36
|
-
const logPath = join(getIcoaDir(), 'session.log');
|
|
37
|
-
if (!existsSync(logPath))
|
|
38
|
-
return;
|
|
39
|
-
const state = getSyncState();
|
|
40
|
-
const allLines = readFileSync(logPath, 'utf-8').trim().split('\n').filter(Boolean);
|
|
41
|
-
const newLines = allLines.slice(state.lastSyncedLine);
|
|
42
|
-
if (newLines.length === 0)
|
|
43
|
-
return;
|
|
44
|
-
// Pick auth mode: token for logged-in users, device fingerprint for anon.
|
|
45
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
46
|
-
let identity;
|
|
47
|
-
if (config.token) {
|
|
48
|
-
headers['Authorization'] = `Token ${config.token}`;
|
|
49
|
-
identity = `user:${config.userId ?? 'unknown'}`;
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
const fp = config.deviceFingerprint || getDeviceFingerprint();
|
|
53
|
-
headers['X-Device-Fingerprint'] = fp;
|
|
54
|
-
identity = `demo:${fp.slice(0, 12)}`;
|
|
55
|
-
}
|
|
56
|
-
const payload = {
|
|
57
|
-
identity,
|
|
58
|
-
userId: config.userId,
|
|
59
|
-
userName: config.userName,
|
|
60
|
-
teamId: config.teamId,
|
|
61
|
-
sessionId: config.sessionId,
|
|
62
|
-
deviceFingerprint: config.deviceFingerprint || getDeviceFingerprint(),
|
|
63
|
-
lang: config.language || 'en',
|
|
64
|
-
timestamp: new Date().toISOString(),
|
|
65
|
-
entries: newLines.map((line) => {
|
|
66
|
-
try {
|
|
67
|
-
return JSON.parse(line);
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return { raw: line };
|
|
71
|
-
}
|
|
72
|
-
}),
|
|
73
|
-
};
|
|
74
|
-
try {
|
|
75
|
-
const url = new URL('/api/icoa/audit', serverUrl).href;
|
|
76
|
-
const res = await fetch(url, {
|
|
77
|
-
method: 'POST',
|
|
78
|
-
headers,
|
|
79
|
-
body: JSON.stringify(payload),
|
|
80
|
-
signal: AbortSignal.timeout(10_000),
|
|
81
|
-
});
|
|
82
|
-
if (res.ok) {
|
|
83
|
-
state.lastSyncedLine = allLines.length;
|
|
84
|
-
state.lastSyncAt = new Date().toISOString();
|
|
85
|
-
state.syncCount++;
|
|
86
|
-
saveSyncState(state);
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
state.failCount++;
|
|
90
|
-
saveSyncState(state);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
// Silent fail — network issues shouldn't break the CLI
|
|
95
|
-
state.failCount++;
|
|
96
|
-
saveSyncState(state);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
export function startLogSync() {
|
|
100
|
-
setTimeout(() => syncLogs(), 5_000);
|
|
101
|
-
syncTimer = setInterval(() => syncLogs(), SYNC_INTERVAL);
|
|
102
|
-
}
|
|
103
|
-
export function stopLogSync() {
|
|
104
|
-
if (syncTimer) {
|
|
105
|
-
clearInterval(syncTimer);
|
|
106
|
-
syncTimer = null;
|
|
107
|
-
}
|
|
108
|
-
// Final sync before exit
|
|
109
|
-
syncLogs();
|
|
110
|
-
}
|
|
1
|
+
import{readFileSync as t,existsSync as n,writeFileSync as e}from"node:fs";import{join as i}from"node:path";import{getIcoaDir as o,getConfig as r}from"./config.js";import{getDeviceFingerprint as s}from"./access.js";const a=()=>i(o(),"sync-state.json");let c=null;function l(t){e(a(),JSON.stringify(t,null,2))}async function u(){const e=r(),c=e.ctfdUrl||"https://practice.icoa2026.au",u=i(o(),"session.log");if(!n(u))return;const f=function(){const e=a();if(!n(e))return{lastSyncedLine:0,lastSyncAt:null,syncCount:0,failCount:0};try{return JSON.parse(t(e,"utf-8"))}catch{return{lastSyncedLine:0,lastSyncAt:null,syncCount:0,failCount:0}}}(),d=t(u,"utf-8").trim().split("\n").filter(Boolean),p=d.slice(f.lastSyncedLine);if(0===p.length)return;const y={"Content-Type":"application/json"};let m;if(e.token)y.Authorization=`Token ${e.token}`,m=`user:${e.userId??"unknown"}`;else{const t=e.deviceFingerprint||s();y["X-Device-Fingerprint"]=t,m=`demo:${t.slice(0,12)}`}const g={identity:m,userId:e.userId,userName:e.userName,teamId:e.teamId,sessionId:e.sessionId,deviceFingerprint:e.deviceFingerprint||s(),lang:e.language||"en",timestamp:(new Date).toISOString(),entries:p.map(t=>{try{return JSON.parse(t)}catch{return{raw:t}}})};try{const t=new URL("/api/icoa/audit",c).href;(await fetch(t,{method:"POST",headers:y,body:JSON.stringify(g),signal:AbortSignal.timeout(1e4)})).ok?(f.lastSyncedLine=d.length,f.lastSyncAt=(new Date).toISOString(),f.syncCount++,l(f)):(f.failCount++,l(f))}catch{f.failCount++,l(f)}}export function startLogSync(){setTimeout(()=>u(),5e3),c=setInterval(()=>u(),3e4)}export function stopLogSync(){c&&(clearInterval(c),c=null),u()}
|
package/dist/lib/logger.js
CHANGED
|
@@ -1,59 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { getConfig, getIcoaDir } from './config.js';
|
|
4
|
-
function getLogPath() {
|
|
5
|
-
return join(getIcoaDir(), 'session.log');
|
|
6
|
-
}
|
|
7
|
-
export function logEntry(entry) {
|
|
8
|
-
try {
|
|
9
|
-
appendFileSync(getLogPath(), JSON.stringify(entry) + '\n');
|
|
10
|
-
}
|
|
11
|
-
catch {
|
|
12
|
-
// Silently fail — logging should never break the CLI
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
export function logCommand(command) {
|
|
16
|
-
const config = getConfig();
|
|
17
|
-
logEntry({
|
|
18
|
-
timestamp: new Date().toISOString(),
|
|
19
|
-
level: 'command',
|
|
20
|
-
input: command,
|
|
21
|
-
sessionId: config.sessionId,
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
export function logHint(level, question, challengeId) {
|
|
25
|
-
const config = getConfig();
|
|
26
|
-
logEntry({
|
|
27
|
-
timestamp: new Date().toISOString(),
|
|
28
|
-
level: level,
|
|
29
|
-
input: question,
|
|
30
|
-
challengeId,
|
|
31
|
-
sessionId: config.sessionId,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
export function logSubmission(challengeId, flag) {
|
|
35
|
-
const config = getConfig();
|
|
36
|
-
logEntry({
|
|
37
|
-
timestamp: new Date().toISOString(),
|
|
38
|
-
level: 'submit',
|
|
39
|
-
input: flag,
|
|
40
|
-
challengeId,
|
|
41
|
-
sessionId: config.sessionId,
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
export function getSessionLog() {
|
|
45
|
-
const logPath = getLogPath();
|
|
46
|
-
if (!existsSync(logPath))
|
|
47
|
-
return [];
|
|
48
|
-
try {
|
|
49
|
-
const raw = readFileSync(logPath, 'utf-8');
|
|
50
|
-
return raw
|
|
51
|
-
.trim()
|
|
52
|
-
.split('\n')
|
|
53
|
-
.filter(Boolean)
|
|
54
|
-
.map((line) => JSON.parse(line));
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return [];
|
|
58
|
-
}
|
|
59
|
-
}
|
|
1
|
+
import{appendFileSync as t,readFileSync as n,existsSync as o}from"node:fs";import{join as e}from"node:path";import{getConfig as s,getIcoaDir as i}from"./config.js";function r(){return e(i(),"session.log")}export function logEntry(n){try{t(r(),JSON.stringify(n)+"\n")}catch{}}export function logCommand(t){const n=s();logEntry({timestamp:(new Date).toISOString(),level:"command",input:t,sessionId:n.sessionId})}export function logHint(t,n,o){const e=s();logEntry({timestamp:(new Date).toISOString(),level:t,input:n,challengeId:o,sessionId:e.sessionId})}export function logSubmission(t,n){const o=s();logEntry({timestamp:(new Date).toISOString(),level:"submit",input:n,challengeId:t,sessionId:o.sessionId})}export function getSessionLog(){const t=r();if(!o(t))return[];try{return n(t,"utf-8").trim().split("\n").filter(Boolean).map(t=>JSON.parse(t))}catch{return[]}}
|
|
@@ -1,117 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Paper C → Paper B upgrade prompt.
|
|
3
|
-
*
|
|
4
|
-
* Shown after a successful Paper C submission. Paper C is the K-12 entry
|
|
5
|
-
* funnel (MCQ only, no tools). Paper B is the next step — adds AI4CTF
|
|
6
|
-
* and CTF4AI practical challenges for 150-point total. The upgrade path
|
|
7
|
-
* depends on which platform the student is on.
|
|
8
|
-
*
|
|
9
|
-
* Policy:
|
|
10
|
-
* - Only triggered after Paper C submission (examId ends in `-c` or
|
|
11
|
-
* starts with a country code + `-2026-c`).
|
|
12
|
-
* - Only triggered if student passed. Students who failed C should
|
|
13
|
-
* retry C before being pushed toward a harder paper.
|
|
14
|
-
* - Always points at https://icoa2026.au/selectionguide/ for detailed
|
|
15
|
-
* platform-specific install instructions, so the CLI copy stays
|
|
16
|
-
* concise and the canonical guide lives in one place.
|
|
17
|
-
* - Never prescribes a token — students must request one from their
|
|
18
|
-
* organizer. This prevents the CLI from implying self-service token
|
|
19
|
-
* issuance.
|
|
20
|
-
*/
|
|
21
|
-
import chalk from 'chalk';
|
|
22
|
-
import { isNativeWindowsCmd, isInWSL, hasPython } from './platform.js';
|
|
23
|
-
/**
|
|
24
|
-
* True when the just-submitted exam is a C paper (MCQ entry funnel).
|
|
25
|
-
* Matches `ua-2026-c`, `pe-2026-c`, `cn-2026-c`, etc. Case-insensitive.
|
|
26
|
-
*/
|
|
27
|
-
export function isCPaper(examId) {
|
|
28
|
-
if (!examId)
|
|
29
|
-
return false;
|
|
30
|
-
return /-2026-c$/i.test(examId.trim());
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Renders the C→B upgrade prompt to stdout. No-op if conditions aren't met.
|
|
34
|
-
*
|
|
35
|
-
* @param examId The submitted exam ID (from state.session.examId before clear)
|
|
36
|
-
* @param passed Whether the student met the passing bar
|
|
37
|
-
*/
|
|
38
|
-
export function showCToBUpgradePrompt(examId, passed) {
|
|
39
|
-
if (!isCPaper(examId))
|
|
40
|
-
return;
|
|
41
|
-
if (!passed)
|
|
42
|
-
return;
|
|
43
|
-
const onCmd = isNativeWindowsCmd();
|
|
44
|
-
const onWSL = isInWSL();
|
|
45
|
-
const pyReady = hasPython();
|
|
46
|
-
console.log(chalk.cyan(' ─────────────────────────────────────────────'));
|
|
47
|
-
console.log();
|
|
48
|
-
console.log(chalk.bold.white(' ★ Ready for more? Paper B — K-12 with AI'));
|
|
49
|
-
console.log();
|
|
50
|
-
console.log(chalk.gray(' Paper B adds ') + chalk.white('AI4CTF') + chalk.gray(' (chat with AI to find hidden flags) and ')
|
|
51
|
-
+ chalk.white('CTF4AI') + chalk.gray(' (break AI security).'));
|
|
52
|
-
console.log(chalk.gray(' 150 points total · 90 minutes · same command interface you already know.'));
|
|
53
|
-
console.log();
|
|
54
|
-
if (onCmd) {
|
|
55
|
-
// Windows cmd / PowerShell — needs WSL2 path for full Paper B
|
|
56
|
-
console.log(chalk.bold.white(' To take Paper B on Windows, install WSL2 + Ubuntu 22:'));
|
|
57
|
-
console.log();
|
|
58
|
-
console.log(chalk.white(' 1. Open PowerShell as Administrator'));
|
|
59
|
-
console.log(chalk.gray(' (right-click PowerShell → "Run as administrator")'));
|
|
60
|
-
console.log(chalk.white(' 2. Run: ') + chalk.cyan('wsl --install -d Ubuntu-22.04'));
|
|
61
|
-
console.log(chalk.white(' 3. Reboot when prompted, create a Linux username + password'));
|
|
62
|
-
console.log(chalk.white(' 4. Inside Ubuntu, install Node.js 22 and this CLI:'));
|
|
63
|
-
console.log(chalk.gray(' ') + chalk.cyan('curl -fsSL https://deb.nodesource.com/setup_22.x | sudo bash -'));
|
|
64
|
-
console.log(chalk.gray(' ') + chalk.cyan('sudo apt install -y nodejs'));
|
|
65
|
-
console.log(chalk.gray(' ') + chalk.cyan('sudo npm install -g icoa-cli'));
|
|
66
|
-
console.log();
|
|
67
|
-
console.log(chalk.gray(' Setup takes 30-60 min the first time. You only do this once.'));
|
|
68
|
-
}
|
|
69
|
-
else if (onWSL) {
|
|
70
|
-
// Already on WSL2 — just need Python for practicals
|
|
71
|
-
console.log(chalk.bold.white(' You are on WSL2 — your setup is almost ready:'));
|
|
72
|
-
console.log();
|
|
73
|
-
if (!pyReady) {
|
|
74
|
-
console.log(chalk.white(' Install Python 3 (for Paper B practical questions):'));
|
|
75
|
-
console.log(chalk.gray(' ') + chalk.cyan('sudo apt install -y python3 python3-pip'));
|
|
76
|
-
console.log();
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
console.log(chalk.green(' ✓ Python 3 already installed. You are ready.'));
|
|
80
|
-
console.log();
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
else if (process.platform === 'darwin') {
|
|
84
|
-
// macOS
|
|
85
|
-
console.log(chalk.bold.white(' On macOS, install Python 3 for Paper B practicals:'));
|
|
86
|
-
console.log();
|
|
87
|
-
console.log(chalk.white(' With Homebrew:'));
|
|
88
|
-
console.log(chalk.gray(' ') + chalk.cyan('brew install python@3.12'));
|
|
89
|
-
console.log();
|
|
90
|
-
console.log(chalk.white(' Without Homebrew: download from ') + chalk.cyan.underline('https://python.org'));
|
|
91
|
-
if (pyReady) {
|
|
92
|
-
console.log();
|
|
93
|
-
console.log(chalk.green(' ✓ Python 3 already detected. You are ready.'));
|
|
94
|
-
}
|
|
95
|
-
console.log();
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
// Linux native (not WSL)
|
|
99
|
-
console.log(chalk.bold.white(' On Linux, install Python 3 for Paper B practicals:'));
|
|
100
|
-
console.log();
|
|
101
|
-
console.log(chalk.white(' Ubuntu / Debian:'));
|
|
102
|
-
console.log(chalk.gray(' ') + chalk.cyan('sudo apt install -y python3 python3-pip'));
|
|
103
|
-
console.log(chalk.white(' Fedora / RHEL:'));
|
|
104
|
-
console.log(chalk.gray(' ') + chalk.cyan('sudo dnf install -y python3 python3-pip'));
|
|
105
|
-
if (pyReady) {
|
|
106
|
-
console.log();
|
|
107
|
-
console.log(chalk.green(' ✓ Python 3 already detected. You are ready.'));
|
|
108
|
-
}
|
|
109
|
-
console.log();
|
|
110
|
-
}
|
|
111
|
-
console.log(chalk.bold.white(' Then get a Paper B token from your organizer.'));
|
|
112
|
-
console.log(chalk.gray(' Each token is one-shot and bound to one device.'));
|
|
113
|
-
console.log();
|
|
114
|
-
console.log(chalk.gray(' Detailed step-by-step guide: ')
|
|
115
|
-
+ chalk.cyan.underline('https://icoa2026.au/selectionguide/'));
|
|
116
|
-
console.log();
|
|
117
|
-
}
|
|
1
|
+
import chalk from"chalk";import{isNativeWindowsCmd as o,isInWSL as e,hasPython as l}from"./platform.js";export function isCPaper(o){return!!o&&/-2026-c$/i.test(o.trim())}export function showCToBUpgradePrompt(n,t){if(!isCPaper(n))return;if(!t)return;const s=o(),a=e(),r=l();console.log(chalk.cyan(" ─────────────────────────────────────────────")),console.log(),console.log(chalk.bold.white(" ★ Ready for more? Paper B — K-12 with AI")),console.log(),console.log(chalk.gray(" Paper B adds ")+chalk.white("AI4CTF")+chalk.gray(" (chat with AI to find hidden flags) and ")+chalk.white("CTF4AI")+chalk.gray(" (break AI security).")),console.log(chalk.gray(" 150 points total · 90 minutes · same command interface you already know.")),console.log(),s?(console.log(chalk.bold.white(" To take Paper B on Windows, install WSL2 + Ubuntu 22:")),console.log(),console.log(chalk.white(" 1. Open PowerShell as Administrator")),console.log(chalk.gray(' (right-click PowerShell → "Run as administrator")')),console.log(chalk.white(" 2. Run: ")+chalk.cyan("wsl --install -d Ubuntu-22.04")),console.log(chalk.white(" 3. Reboot when prompted, create a Linux username + password")),console.log(chalk.white(" 4. Inside Ubuntu, install Node.js 22 and this CLI:")),console.log(chalk.gray(" ")+chalk.cyan("curl -fsSL https://deb.nodesource.com/setup_22.x | sudo bash -")),console.log(chalk.gray(" ")+chalk.cyan("sudo apt install -y nodejs")),console.log(chalk.gray(" ")+chalk.cyan("sudo npm install -g icoa-cli")),console.log(),console.log(chalk.gray(" Setup takes 30-60 min the first time. You only do this once."))):a?(console.log(chalk.bold.white(" You are on WSL2 — your setup is almost ready:")),console.log(),r?(console.log(chalk.green(" ✓ Python 3 already installed. You are ready.")),console.log()):(console.log(chalk.white(" Install Python 3 (for Paper B practical questions):")),console.log(chalk.gray(" ")+chalk.cyan("sudo apt install -y python3 python3-pip")),console.log())):"darwin"===process.platform?(console.log(chalk.bold.white(" On macOS, install Python 3 for Paper B practicals:")),console.log(),console.log(chalk.white(" With Homebrew:")),console.log(chalk.gray(" ")+chalk.cyan("brew install python@3.12")),console.log(),console.log(chalk.white(" Without Homebrew: download from ")+chalk.cyan.underline("https://python.org")),r&&(console.log(),console.log(chalk.green(" ✓ Python 3 already detected. You are ready."))),console.log()):(console.log(chalk.bold.white(" On Linux, install Python 3 for Paper B practicals:")),console.log(),console.log(chalk.white(" Ubuntu / Debian:")),console.log(chalk.gray(" ")+chalk.cyan("sudo apt install -y python3 python3-pip")),console.log(chalk.white(" Fedora / RHEL:")),console.log(chalk.gray(" ")+chalk.cyan("sudo dnf install -y python3 python3-pip")),r&&(console.log(),console.log(chalk.green(" ✓ Python 3 already detected. You are ready."))),console.log()),console.log(chalk.bold.white(" Then get a Paper B token from your organizer.")),console.log(chalk.gray(" Each token is one-shot and bound to one device.")),console.log(),console.log(chalk.gray(" Detailed step-by-step guide: ")+chalk.cyan.underline("https://icoa2026.au/selectionguide/")),console.log()}
|
package/dist/lib/platform.js
CHANGED
|
@@ -1,86 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Platform detection for Windows cmd.exe K-12 entry path.
|
|
3
|
-
*
|
|
4
|
-
* Context: Windows middle-school students (12-14 y/o) are the expected largest
|
|
5
|
-
* cohort for ICOA 2026. Observed in-field: 4-5 students blocked by WSL install
|
|
6
|
-
* complexity. Their viable path is Windows native cmd + Node.js, which handles
|
|
7
|
-
* MCQ + AI chat perfectly but not Q31-36 (Unix grep / strings / pwntools).
|
|
8
|
-
*
|
|
9
|
-
* Decision (v2.19.97): C paper (ua-2026-c) is the cmd entry funnel — 30 MCQ
|
|
10
|
-
* only, 45 min, 70 pts, no tools. Non-cmd users get B paper (full 150 pts).
|
|
11
|
-
*
|
|
12
|
-
* This module is the single source of truth for "should we show exam setup?"
|
|
13
|
-
* and "should we route this user to C paper?". Used by src/repl.ts menu
|
|
14
|
-
* rendering and src/commands/exam.ts pre-token guards.
|
|
15
|
-
*/
|
|
16
|
-
import { execFileSync } from 'node:child_process';
|
|
17
|
-
/**
|
|
18
|
-
* Heuristic: true when running inside a WSL distribution. Node.js reports
|
|
19
|
-
* `process.platform === 'linux'` in that case, but WSL sets specific env vars.
|
|
20
|
-
*/
|
|
21
|
-
export function isInWSL() {
|
|
22
|
-
return !!(process.env.WSL_DISTRO_NAME || process.env.WSLENV);
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* True when the user is on Windows native (cmd.exe or PowerShell), NOT a
|
|
26
|
-
* WSL distribution.
|
|
27
|
-
*/
|
|
28
|
-
export function isNativeWindowsCmd() {
|
|
29
|
-
return process.platform === 'win32' && !isInWSL();
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Probe whether any Python 3 binary is reachable. Silent — no stderr leaks
|
|
33
|
-
* to the user. Used to decide whether Q31-35 practical questions are even
|
|
34
|
-
* theoretically doable on this machine.
|
|
35
|
-
*
|
|
36
|
-
* Uses execFileSync (no shell) so probe failures don't leak cmd.exe's
|
|
37
|
-
* "not recognized as internal or external command" messages to stdout.
|
|
38
|
-
*/
|
|
39
|
-
export function hasPython() {
|
|
40
|
-
const probes = [
|
|
41
|
-
['python3', ['--version']],
|
|
42
|
-
['python', ['--version']],
|
|
43
|
-
['py', ['-3', '--version']],
|
|
44
|
-
];
|
|
45
|
-
for (const [bin, args] of probes) {
|
|
46
|
-
try {
|
|
47
|
-
execFileSync(bin, args, {
|
|
48
|
-
encoding: 'utf-8',
|
|
49
|
-
timeout: 1500,
|
|
50
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
51
|
-
});
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
catch { /* try next */ }
|
|
55
|
-
}
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
export function getPlatformInfo() {
|
|
59
|
-
return {
|
|
60
|
-
platform: process.platform,
|
|
61
|
-
isWindowsNativeCmd: isNativeWindowsCmd(),
|
|
62
|
-
isInWSL: isInWSL(),
|
|
63
|
-
hasPython: hasPython(),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Should the Selection-mode menu show the `exam setup` row?
|
|
68
|
-
*
|
|
69
|
-
* Windows native cmd: NO. Setup installs pip packages for Q31-35 practical
|
|
70
|
-
* which don't apply on the C-paper (cmd-recommended) path. Showing setup
|
|
71
|
-
* sends them chasing Python installs that may fail for 12-year-olds.
|
|
72
|
-
*
|
|
73
|
-
* Everyone else (macOS, Linux, WSL): YES. They can reach B-paper and setup
|
|
74
|
-
* is a legit pre-step.
|
|
75
|
-
*/
|
|
76
|
-
export function shouldShowExamSetup() {
|
|
77
|
-
return !isNativeWindowsCmd();
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Is this user recommended to take the C paper (MCQ-only) rather than B?
|
|
81
|
-
* Currently: Windows native cmd users. Future: might expand based on age
|
|
82
|
-
* or partner-country policy.
|
|
83
|
-
*/
|
|
84
|
-
export function shouldRecommendCPaper() {
|
|
85
|
-
return isNativeWindowsCmd();
|
|
86
|
-
}
|
|
1
|
+
import{execFileSync as o}from"node:child_process";export function isInWSL(){return!(!process.env.WSL_DISTRO_NAME&&!process.env.WSLENV)}export function isNativeWindowsCmd(){return"win32"===process.platform&&!isInWSL()}export function hasPython(){const n=[["python3",["--version"]],["python",["--version"]],["py",["-3","--version"]]];for(const[t,e]of n)try{return o(t,e,{encoding:"utf-8",timeout:1500,stdio:["ignore","pipe","ignore"]}),!0}catch{}return!1}export function getPlatformInfo(){return{platform:process.platform,isWindowsNativeCmd:isNativeWindowsCmd(),isInWSL:isInWSL(),hasPython:hasPython()}}export function shouldShowExamSetup(){return!isNativeWindowsCmd()}export function shouldRecommendCPaper(){return isNativeWindowsCmd()}
|
package/dist/lib/sandbox.js
CHANGED
|
@@ -1,93 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
const IMAGE = 'icoa/sandbox:2026';
|
|
4
|
-
const CONTAINER = 'icoa-sandbox';
|
|
5
|
-
export function isDockerAvailable() {
|
|
6
|
-
try {
|
|
7
|
-
execSync('docker info', { stdio: 'ignore' });
|
|
8
|
-
return true;
|
|
9
|
-
}
|
|
10
|
-
catch {
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
export function isSandboxRunning() {
|
|
15
|
-
try {
|
|
16
|
-
const out = execSync(`docker inspect -f '{{.State.Running}}' ${CONTAINER} 2>/dev/null`, {
|
|
17
|
-
encoding: 'utf-8',
|
|
18
|
-
});
|
|
19
|
-
return out.trim() === 'true';
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
export async function ensureSandbox() {
|
|
26
|
-
if (!isDockerAvailable()) {
|
|
27
|
-
console.log(chalk.yellow(' Docker not found. Install Docker Desktop to use sandbox tools.'));
|
|
28
|
-
console.log(chalk.gray(' https://www.docker.com/products/docker-desktop'));
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
if (isSandboxRunning())
|
|
32
|
-
return true;
|
|
33
|
-
// Check if container exists but stopped
|
|
34
|
-
try {
|
|
35
|
-
execSync(`docker start ${CONTAINER}`, { stdio: 'ignore' });
|
|
36
|
-
return true;
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
// Container doesn't exist, create it
|
|
40
|
-
}
|
|
41
|
-
// Check if image exists
|
|
42
|
-
try {
|
|
43
|
-
execSync(`docker image inspect ${IMAGE}`, { stdio: 'ignore' });
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
console.log(chalk.gray(' Pulling sandbox image (first time only)...'));
|
|
47
|
-
try {
|
|
48
|
-
execSync(`docker pull ${IMAGE}`, { stdio: 'inherit' });
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
// Image not on registry yet, try building locally
|
|
52
|
-
console.log(chalk.gray(' Building sandbox from local Dockerfile...'));
|
|
53
|
-
try {
|
|
54
|
-
const dockerDir = new URL('../../docker', import.meta.url).pathname;
|
|
55
|
-
execSync(`docker build -t ${IMAGE} ${dockerDir}`, { stdio: 'inherit' });
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
console.log(chalk.red(' Failed to set up sandbox.'));
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
// Create and start container
|
|
64
|
-
try {
|
|
65
|
-
execSync(`docker run -d --name ${CONTAINER} ` +
|
|
66
|
-
`-v icoa-challenges:/home/competitor/challenges ` +
|
|
67
|
-
`--network host ` +
|
|
68
|
-
`${IMAGE} sleep infinity`, { stdio: 'ignore' });
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
console.log(chalk.red(' Failed to start sandbox container.'));
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
export function runInSandbox(command, rl) {
|
|
77
|
-
return new Promise((resolve) => {
|
|
78
|
-
rl.pause();
|
|
79
|
-
const opts = {
|
|
80
|
-
stdio: 'inherit',
|
|
81
|
-
shell: true,
|
|
82
|
-
};
|
|
83
|
-
const child = spawn('docker', ['exec', '-it', CONTAINER, 'bash', '-c', command], opts);
|
|
84
|
-
child.on('close', () => {
|
|
85
|
-
rl.resume();
|
|
86
|
-
resolve();
|
|
87
|
-
});
|
|
88
|
-
child.on('error', () => {
|
|
89
|
-
rl.resume();
|
|
90
|
-
resolve();
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
}
|
|
1
|
+
import{execSync as o,spawn as e}from"node:child_process";import chalk from"chalk";const r="icoa/sandbox:2026",n="icoa-sandbox";export function isDockerAvailable(){try{return o("docker info",{stdio:"ignore"}),!0}catch{return!1}}export function isSandboxRunning(){try{return"true"===o(`docker inspect -f '{{.State.Running}}' ${n} 2>/dev/null`,{encoding:"utf-8"}).trim()}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(isSandboxRunning())return!0;try{return o(`docker start ${n}`,{stdio:"ignore"}),!0}catch{}try{o(`docker image inspect ${r}`,{stdio:"ignore"})}catch{console.log(chalk.gray(" Pulling sandbox image (first time only)..."));try{o(`docker pull ${r}`,{stdio:"inherit"})}catch{console.log(chalk.gray(" Building sandbox from local Dockerfile..."));try{const e=new URL("../../docker",import.meta.url).pathname;o(`docker build -t ${r} ${e}`,{stdio:"inherit"})}catch{return console.log(chalk.red(" Failed to set up sandbox.")),!1}}}try{return o(`docker run -d --name ${n} -v icoa-challenges:/home/competitor/challenges --network host ${r} 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(t=>{r.pause();const c=e("docker",["exec","-it",n,"bash","-c",o],{stdio:"inherit",shell:!0});c.on("close",()=>{r.resume(),t()}),c.on("error",()=>{r.resume(),t()})})}
|
package/dist/lib/terminal.js
CHANGED
|
@@ -1,49 +1 @@
|
|
|
1
|
-
import chalk from
|
|
2
|
-
export function detectTerminal() {
|
|
3
|
-
const termProgram = process.env.TERM_PROGRAM || '';
|
|
4
|
-
const platform = process.platform;
|
|
5
|
-
// ICOA Terminal (custom bundled app) — bypass platform-specific checks
|
|
6
|
-
if (process.env.ICOA_TERMINAL === '1') {
|
|
7
|
-
return { name: 'ICOA Terminal', allowed: true };
|
|
8
|
-
}
|
|
9
|
-
// macOS: only Apple Terminal
|
|
10
|
-
if (platform === 'darwin') {
|
|
11
|
-
if (termProgram === 'Apple_Terminal') {
|
|
12
|
-
return { name: 'macOS Terminal', allowed: true };
|
|
13
|
-
}
|
|
14
|
-
return { name: termProgram || 'Unknown macOS terminal', allowed: false };
|
|
15
|
-
}
|
|
16
|
-
// Linux: GNOME Terminal, or basic xterm/linux console
|
|
17
|
-
if (platform === 'linux') {
|
|
18
|
-
const isGnome = termProgram === 'gnome-terminal' ||
|
|
19
|
-
process.env.GNOME_TERMINAL_SERVICE !== undefined;
|
|
20
|
-
if (isGnome) {
|
|
21
|
-
return { name: 'GNOME Terminal', allowed: true };
|
|
22
|
-
}
|
|
23
|
-
// Also allow basic linux console (no TERM_PROGRAM, e.g. TTY)
|
|
24
|
-
if (!termProgram && (process.env.TERM === 'linux' || process.env.TERM === 'xterm')) {
|
|
25
|
-
return { name: 'Linux Console', allowed: true };
|
|
26
|
-
}
|
|
27
|
-
return { name: termProgram || 'Unknown Linux terminal', allowed: false };
|
|
28
|
-
}
|
|
29
|
-
// Windows: PowerShell
|
|
30
|
-
if (platform === 'win32') {
|
|
31
|
-
const isPowerShell = !!process.env.PSModulePath;
|
|
32
|
-
if (isPowerShell) {
|
|
33
|
-
return { name: 'PowerShell', allowed: true };
|
|
34
|
-
}
|
|
35
|
-
return { name: 'Windows CMD or other', allowed: false };
|
|
36
|
-
}
|
|
37
|
-
return { name: 'Unknown', allowed: false };
|
|
38
|
-
}
|
|
39
|
-
export function checkTerminal() {
|
|
40
|
-
const info = detectTerminal();
|
|
41
|
-
if (!info.allowed) {
|
|
42
|
-
console.log(chalk.yellow(` ⚠ Unsupported terminal: ${info.name}`));
|
|
43
|
-
console.log(chalk.gray(' Competition requires:'));
|
|
44
|
-
console.log(chalk.gray(' macOS → Terminal.app (system default)'));
|
|
45
|
-
console.log(chalk.gray(' Linux → GNOME Terminal (system default)'));
|
|
46
|
-
console.log(chalk.gray(' Windows → PowerShell (system default)'));
|
|
47
|
-
console.log();
|
|
48
|
-
}
|
|
49
|
-
}
|
|
1
|
+
import chalk from"chalk";export function detectTerminal(){const e=process.env.TERM_PROGRAM||"",n=process.platform;return"1"===process.env.ICOA_TERMINAL?{name:"ICOA Terminal",allowed:!0}:"darwin"===n?"Apple_Terminal"===e?{name:"macOS Terminal",allowed:!0}:{name:e||"Unknown macOS terminal",allowed:!1}:"linux"===n?"gnome-terminal"===e||void 0!==process.env.GNOME_TERMINAL_SERVICE?{name:"GNOME Terminal",allowed:!0}:e||"linux"!==process.env.TERM&&"xterm"!==process.env.TERM?{name:e||"Unknown Linux terminal",allowed:!1}:{name:"Linux Console",allowed:!0}:"win32"===n?process.env.PSModulePath?{name:"PowerShell",allowed:!0}:{name:"Windows CMD or other",allowed:!1}:{name:"Unknown",allowed:!1}}export function checkTerminal(){const e=detectTerminal();e.allowed||(console.log(chalk.yellow(` ⚠ Unsupported terminal: ${e.name}`)),console.log(chalk.gray(" Competition requires:")),console.log(chalk.gray(" macOS → Terminal.app (system default)")),console.log(chalk.gray(" Linux → GNOME Terminal (system default)")),console.log(chalk.gray(" Windows → PowerShell (system default)")),console.log())}
|
package/dist/lib/theme.js
CHANGED
|
@@ -1,108 +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' +
|
|
30
|
-
'\x1b]11;#000000\x07' +
|
|
31
|
-
'\x1b]12;#FFFFFF\x07';
|
|
32
|
-
const OSC_RESET = '\x1b]110\x07' + // reset default fg
|
|
33
|
-
'\x1b]111\x07' + // reset default bg
|
|
34
|
-
'\x1b]112\x07'; // reset cursor color
|
|
35
|
-
const SGR_INIT_TRUECOLOR_DARK = '\x1b[38;2;169;183;198m' + // fg #A9B7C6
|
|
36
|
-
'\x1b[48;2;43;43;43m' + // bg #2B2B2B
|
|
37
|
-
'\x1b[2J' +
|
|
38
|
-
'\x1b[H';
|
|
39
|
-
const SGR_INIT_256_DARK = '\x1b[38;5;250m' + // fg ≈ #BCBCBC
|
|
40
|
-
'\x1b[48;5;235m' + // bg ≈ #262626
|
|
41
|
-
'\x1b[2J' +
|
|
42
|
-
'\x1b[H';
|
|
43
|
-
const SGR_INIT_TRUECOLOR_HC = '\x1b[38;2;255;255;255m' + // fg pure white
|
|
44
|
-
'\x1b[48;2;0;0;0m' + // bg pure black
|
|
45
|
-
'\x1b[2J' +
|
|
46
|
-
'\x1b[H';
|
|
47
|
-
const SGR_INIT_256_HC = '\x1b[38;5;231m' + // fg white (231 = pure white in 256)
|
|
48
|
-
'\x1b[48;5;16m' + // bg black (16 = pure black in 256)
|
|
49
|
-
'\x1b[2J' +
|
|
50
|
-
'\x1b[H';
|
|
51
|
-
const SGR_RESET = '\x1b[0m\x1b[2J\x1b[H';
|
|
52
|
-
function supportsAnsi() {
|
|
53
|
-
if (!process.stdout.isTTY)
|
|
54
|
-
return false;
|
|
55
|
-
const depth = process.stdout.getColorDepth?.();
|
|
56
|
-
if (typeof depth === 'number')
|
|
57
|
-
return depth >= 8;
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
// When icoa-cli runs inside the ICOA Terminal (Tauri + xterm.js), the host is
|
|
61
|
-
// already pre-themed to the exact Darcula palette we'd be setting. Every OSC
|
|
62
|
-
// and SGR we'd emit is a no-op in terms of color, but the \x1b[2J inside our
|
|
63
|
-
// init/reset sequences would clear the grid visibly. Skip the paint entirely
|
|
64
|
-
// in that environment so the banner simply appears in the shell cursor
|
|
65
|
-
// position and scrollback is preserved on exit.
|
|
66
|
-
function isIcoaTerminal() {
|
|
67
|
-
return process.env.ICOA_TERMINAL === '1';
|
|
68
|
-
}
|
|
69
|
-
// macOS Terminal.app does not implement SGR truecolor (\x1b[38;2;… / \x1b[48;2;…)
|
|
70
|
-
// and mis-parses those sequences as 16-color codes, producing e.g. a yellow bg.
|
|
71
|
-
// Detect it and fall back to 256-color SGR which Terminal.app handles correctly.
|
|
72
|
-
function isAppleTerminal() {
|
|
73
|
-
return process.env.TERM_PROGRAM === 'Apple_Terminal';
|
|
74
|
-
}
|
|
75
|
-
let armed = false;
|
|
76
|
-
export function setTerminalTheme(variant = 'dark') {
|
|
77
|
-
if (!supportsAnsi())
|
|
78
|
-
return;
|
|
79
|
-
if (isIcoaTerminal())
|
|
80
|
-
return; // host is already Darcula; nothing to do
|
|
81
|
-
const osc = variant === 'high-contrast' ? OSC_INIT_HC : OSC_INIT_DARK;
|
|
82
|
-
const sgr = isAppleTerminal()
|
|
83
|
-
? (variant === 'high-contrast' ? SGR_INIT_256_HC : SGR_INIT_256_DARK)
|
|
84
|
-
: (variant === 'high-contrast' ? SGR_INIT_TRUECOLOR_HC : SGR_INIT_TRUECOLOR_DARK);
|
|
85
|
-
process.stdout.write(osc + sgr);
|
|
86
|
-
if (!armed) {
|
|
87
|
-
armed = true;
|
|
88
|
-
// Belt-and-braces cleanup on every exit path. Without these, Ctrl+C leaves
|
|
89
|
-
// the user's shell stuck with our SGR state.
|
|
90
|
-
const cleanup = () => {
|
|
91
|
-
try {
|
|
92
|
-
process.stdout.write(OSC_RESET + SGR_RESET);
|
|
93
|
-
}
|
|
94
|
-
catch { }
|
|
95
|
-
};
|
|
96
|
-
process.on('exit', cleanup);
|
|
97
|
-
process.on('SIGINT', () => { cleanup(); process.exit(130); });
|
|
98
|
-
process.on('SIGTERM', () => { cleanup(); process.exit(143); });
|
|
99
|
-
process.on('SIGHUP', () => { cleanup(); process.exit(129); });
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
export function resetTerminalTheme() {
|
|
103
|
-
if (!supportsAnsi())
|
|
104
|
-
return;
|
|
105
|
-
if (isIcoaTerminal())
|
|
106
|
-
return; // nothing to undo
|
|
107
|
-
process.stdout.write(OSC_RESET + SGR_RESET);
|
|
108
|
-
}
|
|
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))}
|