icoa-cli 1.7.1 → 1.7.3
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/env.js +33 -3
- package/dist/commands/log.js +154 -27
- package/dist/index.js +1 -1
- package/dist/repl.js +6 -3
- package/package.json +1 -1
package/dist/commands/env.js
CHANGED
|
@@ -263,10 +263,40 @@ async function installAll() {
|
|
|
263
263
|
console.log();
|
|
264
264
|
const os = platform();
|
|
265
265
|
const pipFlag = os === 'darwin' || os === 'linux' ? '--break-system-packages' : '';
|
|
266
|
-
// Python version
|
|
266
|
+
// Python version check — install 3.12 if needed
|
|
267
267
|
const pyVer = getPythonMajorMinor();
|
|
268
|
-
if (pyVer
|
|
269
|
-
console.log(chalk.yellow(`
|
|
268
|
+
if (pyVer !== '3.12') {
|
|
269
|
+
console.log(chalk.yellow(` Python ${pyVer || 'not found'} — installing Python 3.12...`));
|
|
270
|
+
try {
|
|
271
|
+
if (os === 'darwin') {
|
|
272
|
+
execSync('brew install python@3.12', { stdio: 'inherit' });
|
|
273
|
+
// Link python3.12 as default python3
|
|
274
|
+
try {
|
|
275
|
+
execSync('brew link --overwrite python@3.12', { stdio: 'ignore' });
|
|
276
|
+
}
|
|
277
|
+
catch { /* ok */ }
|
|
278
|
+
console.log(chalk.green(' ✓ Python 3.12 installed'));
|
|
279
|
+
console.log(chalk.gray(' Use: /opt/homebrew/opt/python@3.12/bin/python3.12'));
|
|
280
|
+
console.log(chalk.gray(' Or: brew link --overwrite python@3.12'));
|
|
281
|
+
}
|
|
282
|
+
else if (os === 'linux') {
|
|
283
|
+
execSync('sudo apt-get update && sudo apt-get install -y python3.12 python3.12-venv python3.12-dev', { stdio: 'inherit' });
|
|
284
|
+
console.log(chalk.green(' ✓ Python 3.12 installed'));
|
|
285
|
+
console.log(chalk.gray(' Set default: sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1'));
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
execSync('choco install -y python312', { stdio: 'inherit' });
|
|
289
|
+
console.log(chalk.green(' ✓ Python 3.12 installed'));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
console.log(chalk.red(' ✗ Failed to install Python 3.12'));
|
|
294
|
+
console.log(chalk.gray(` Manual install: ${os === 'darwin' ? 'brew install python@3.12' : os === 'linux' ? 'sudo apt install python3.12' : 'choco install python312'}`));
|
|
295
|
+
}
|
|
296
|
+
console.log();
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
console.log(chalk.green(` ✓ Python 3.12 (${getVersion('python3')})`));
|
|
270
300
|
console.log();
|
|
271
301
|
}
|
|
272
302
|
// Install system tools via brew (macOS) / apt (Linux) / choco (Windows)
|
package/dist/commands/log.js
CHANGED
|
@@ -1,36 +1,163 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
import { getSessionLog } from '../lib/logger.js';
|
|
5
|
+
import { getIcoaDir, getConfig } from '../lib/config.js';
|
|
3
6
|
import { printHeader, printInfo, printTable } from '../lib/ui.js';
|
|
4
7
|
export function registerLogCommand(program) {
|
|
5
|
-
program
|
|
8
|
+
const logCmd = program
|
|
6
9
|
.command('log')
|
|
7
10
|
.description('Display session history')
|
|
8
11
|
.action(() => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 [
|
|
48
|
+
chalk.gray(time),
|
|
49
|
+
colorFn(entry.level.padEnd(7)),
|
|
50
|
+
input,
|
|
51
|
+
];
|
|
52
|
+
});
|
|
53
|
+
printTable(['Time', 'Type', 'Content'], rows);
|
|
54
|
+
console.log(chalk.gray(` ${entries.length} entries total`));
|
|
55
|
+
console.log();
|
|
56
|
+
}
|
|
57
|
+
async function exportLog() {
|
|
58
|
+
const config = getConfig();
|
|
59
|
+
const icoaDir = getIcoaDir();
|
|
60
|
+
const logFile = join(icoaDir, 'session.log');
|
|
61
|
+
const sessionFile = join(icoaDir, 'session-state.json');
|
|
62
|
+
const configFile = join(icoaDir, 'config.json');
|
|
63
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
|
64
|
+
const userName = config.userName || 'unknown';
|
|
65
|
+
const exportName = `icoa-audit-${userName}-${timestamp}.json`;
|
|
66
|
+
const exportPath = join(process.cwd(), exportName);
|
|
67
|
+
// Gather all audit data
|
|
68
|
+
const audit = {
|
|
69
|
+
exportedAt: new Date().toISOString(),
|
|
70
|
+
version: '1.7.2',
|
|
71
|
+
competitor: {
|
|
72
|
+
userName: config.userName,
|
|
73
|
+
userId: config.userId,
|
|
74
|
+
teamName: config.teamName,
|
|
75
|
+
teamId: config.teamId,
|
|
76
|
+
sessionId: config.sessionId,
|
|
77
|
+
},
|
|
78
|
+
connection: {
|
|
79
|
+
ctfdUrl: config.ctfdUrl,
|
|
80
|
+
},
|
|
81
|
+
session: existsSync(sessionFile) ? JSON.parse(readFileSync(sessionFile, 'utf-8')) : null,
|
|
82
|
+
commands: getSessionLog(),
|
|
83
|
+
};
|
|
84
|
+
// Count by type
|
|
85
|
+
const entries = getSessionLog();
|
|
86
|
+
const counts = {};
|
|
87
|
+
for (const e of entries) {
|
|
88
|
+
counts[e.level] = (counts[e.level] || 0) + 1;
|
|
89
|
+
}
|
|
90
|
+
audit.summary = {
|
|
91
|
+
totalCommands: entries.length,
|
|
92
|
+
byType: counts,
|
|
93
|
+
firstEntry: entries[0]?.timestamp || null,
|
|
94
|
+
lastEntry: entries[entries.length - 1]?.timestamp || null,
|
|
95
|
+
};
|
|
96
|
+
// Write export
|
|
97
|
+
const { writeFileSync } = await import('node:fs');
|
|
98
|
+
writeFileSync(exportPath, JSON.stringify(audit, null, 2));
|
|
99
|
+
console.log();
|
|
100
|
+
console.log(chalk.green(` ✓ Audit log exported`));
|
|
101
|
+
console.log(chalk.white(` ${exportPath}`));
|
|
102
|
+
console.log();
|
|
103
|
+
console.log(chalk.gray(' Contents:'));
|
|
104
|
+
console.log(chalk.gray(` Commands: ${entries.length}`));
|
|
105
|
+
Object.entries(counts).forEach(([type, count]) => {
|
|
106
|
+
console.log(chalk.gray(` ${type}: ${count}`));
|
|
35
107
|
});
|
|
108
|
+
console.log();
|
|
109
|
+
console.log(chalk.gray(' This file contains the complete session audit trail.'));
|
|
110
|
+
console.log(chalk.gray(' Submit to organizers for post-competition verification.'));
|
|
111
|
+
console.log();
|
|
112
|
+
}
|
|
113
|
+
function showStats() {
|
|
114
|
+
const entries = getSessionLog();
|
|
115
|
+
const icoaDir = getIcoaDir();
|
|
116
|
+
const sessionFile = join(icoaDir, 'session-state.json');
|
|
117
|
+
console.log();
|
|
118
|
+
console.log(chalk.bold.white(' Session Statistics'));
|
|
119
|
+
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
120
|
+
if (entries.length === 0) {
|
|
121
|
+
console.log(chalk.gray(' No activity recorded yet.'));
|
|
122
|
+
console.log();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// Time range
|
|
126
|
+
const first = new Date(entries[0].timestamp);
|
|
127
|
+
const last = new Date(entries[entries.length - 1].timestamp);
|
|
128
|
+
const durationMin = Math.round((last.getTime() - first.getTime()) / 60000);
|
|
129
|
+
console.log(chalk.gray(' First activity: ') + chalk.white(first.toLocaleString()));
|
|
130
|
+
console.log(chalk.gray(' Last activity: ') + chalk.white(last.toLocaleString()));
|
|
131
|
+
console.log(chalk.gray(' Duration: ') + chalk.white(`${durationMin} min`));
|
|
132
|
+
console.log();
|
|
133
|
+
// Count by type
|
|
134
|
+
const counts = {};
|
|
135
|
+
for (const e of entries) {
|
|
136
|
+
counts[e.level] = (counts[e.level] || 0) + 1;
|
|
137
|
+
}
|
|
138
|
+
console.log(chalk.gray(' Total commands: ') + chalk.white(String(entries.length)));
|
|
139
|
+
if (counts['command'])
|
|
140
|
+
console.log(chalk.blue(' commands: ') + chalk.white(String(counts['command'])));
|
|
141
|
+
if (counts['A'])
|
|
142
|
+
console.log(chalk.green(' hint A: ') + chalk.white(String(counts['A'])));
|
|
143
|
+
if (counts['B'])
|
|
144
|
+
console.log(chalk.yellow(' hint B: ') + chalk.white(String(counts['B'])));
|
|
145
|
+
if (counts['C'])
|
|
146
|
+
console.log(chalk.red(' hint C: ') + chalk.white(String(counts['C'])));
|
|
147
|
+
if (counts['submit'])
|
|
148
|
+
console.log(chalk.magenta(' submissions: ') + chalk.white(String(counts['submit'])));
|
|
149
|
+
// Exit info
|
|
150
|
+
if (existsSync(sessionFile)) {
|
|
151
|
+
try {
|
|
152
|
+
const session = JSON.parse(readFileSync(sessionFile, 'utf-8'));
|
|
153
|
+
console.log();
|
|
154
|
+
console.log(chalk.gray(' Exit count: ') + chalk.white(String(session.exitCount || 0)));
|
|
155
|
+
if (session.totalAwaySeconds) {
|
|
156
|
+
const awayMin = Math.round(session.totalAwaySeconds / 60);
|
|
157
|
+
console.log(chalk.gray(' Total away: ') + chalk.white(`${awayMin} min`));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch { /* ignore */ }
|
|
161
|
+
}
|
|
162
|
+
console.log();
|
|
36
163
|
}
|
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ ${LINE}
|
|
|
36
36
|
${chalk.white('Sydney, Australia')} ${chalk.gray('Jun 27 - Jul 2, 2026')}
|
|
37
37
|
${chalk.cyan.underline('https://icoa2026.au')}
|
|
38
38
|
|
|
39
|
-
${chalk.gray('CLI-Native Competition Terminal v1.7.
|
|
39
|
+
${chalk.gray('CLI-Native Competition Terminal v1.7.3')}
|
|
40
40
|
|
|
41
41
|
${LINE}
|
|
42
42
|
`;
|
package/dist/repl.js
CHANGED
|
@@ -5,8 +5,9 @@ import { isConnected, getConfig } from './lib/config.js';
|
|
|
5
5
|
import { isActivated, activateToken, isFreeCommand, isDeviceMatch, recordExit, recordResume, isFirstRunOrUpgrade, markVersionSeen } from './lib/access.js';
|
|
6
6
|
import { resetTerminalTheme } from './lib/theme.js';
|
|
7
7
|
import { ensureSandbox, runInSandbox, isDockerAvailable } from './lib/sandbox.js';
|
|
8
|
+
import { logCommand } from './lib/logger.js';
|
|
8
9
|
const INTERCEPT = '__REPL_NO_EXIT__';
|
|
9
|
-
const VERSION = '1.7.
|
|
10
|
+
const VERSION = '1.7.3';
|
|
10
11
|
export async function startRepl(program, resumeMode) {
|
|
11
12
|
const config = getConfig();
|
|
12
13
|
const connected = isConnected();
|
|
@@ -124,6 +125,8 @@ export async function startRepl(program, resumeMode) {
|
|
|
124
125
|
rl.prompt();
|
|
125
126
|
return;
|
|
126
127
|
}
|
|
128
|
+
// Log ALL commands for audit trail
|
|
129
|
+
logCommand(input);
|
|
127
130
|
// Exit — record, reset terminal colors, and quit
|
|
128
131
|
if (input === 'exit' || input === 'quit' || input === 'q') {
|
|
129
132
|
recordExit();
|
|
@@ -182,8 +185,8 @@ export async function startRepl(program, resumeMode) {
|
|
|
182
185
|
const knownCommands = [
|
|
183
186
|
'join', 'activate', 'challenges', 'ch', 'open', 'submit', 'flag',
|
|
184
187
|
'scoreboard', 'sb', 'status', 'time', 'hint', 'hint-b', 'hint-c',
|
|
185
|
-
'hint-budget', 'ref', 'shell', 'files', 'connect', 'note',
|
|
186
|
-
'lang', 'setup', 'env', 'model', 'ctf',
|
|
188
|
+
'hint-budget', 'ref', 'shell', 'files', 'connect', 'note',
|
|
189
|
+
'log', 'lang', 'setup', 'env', 'model', 'ctf',
|
|
187
190
|
];
|
|
188
191
|
if (!knownCommands.includes(cmd)) {
|
|
189
192
|
// Route to Docker sandbox if available, otherwise system shell
|