icoa-cli 1.7.3 → 1.8.0
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/index.js +1 -1
- package/dist/lib/log-sync.d.ts +2 -0
- package/dist/lib/log-sync.js +92 -0
- package/dist/repl.js +6 -1
- package/package.json +1 -1
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.
|
|
39
|
+
${chalk.gray('CLI-Native Competition Terminal v1.8.0')}
|
|
40
40
|
|
|
41
41
|
${LINE}
|
|
42
42
|
`;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { getIcoaDir, getConfig } from './config.js';
|
|
4
|
+
const SYNC_INTERVAL = 30_000; // 30 seconds
|
|
5
|
+
const SYNC_STATE_FILE = () => join(getIcoaDir(), 'sync-state.json');
|
|
6
|
+
let syncTimer = null;
|
|
7
|
+
function getSyncState() {
|
|
8
|
+
const file = SYNC_STATE_FILE();
|
|
9
|
+
if (!existsSync(file))
|
|
10
|
+
return { lastSyncedLine: 0, lastSyncAt: null, syncCount: 0, failCount: 0 };
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(readFileSync(file, 'utf-8'));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return { lastSyncedLine: 0, lastSyncAt: null, syncCount: 0, failCount: 0 };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function saveSyncState(state) {
|
|
19
|
+
writeFileSync(SYNC_STATE_FILE(), JSON.stringify(state, null, 2));
|
|
20
|
+
}
|
|
21
|
+
async function syncLogs() {
|
|
22
|
+
const config = getConfig();
|
|
23
|
+
if (!config.ctfdUrl)
|
|
24
|
+
return; // Not connected, skip
|
|
25
|
+
const logPath = join(getIcoaDir(), 'session.log');
|
|
26
|
+
if (!existsSync(logPath))
|
|
27
|
+
return;
|
|
28
|
+
const state = getSyncState();
|
|
29
|
+
const allLines = readFileSync(logPath, 'utf-8').trim().split('\n').filter(Boolean);
|
|
30
|
+
// Only send new lines since last sync
|
|
31
|
+
const newLines = allLines.slice(state.lastSyncedLine);
|
|
32
|
+
if (newLines.length === 0)
|
|
33
|
+
return;
|
|
34
|
+
const payload = {
|
|
35
|
+
userId: config.userId,
|
|
36
|
+
userName: config.userName,
|
|
37
|
+
teamId: config.teamId,
|
|
38
|
+
sessionId: config.sessionId,
|
|
39
|
+
deviceFingerprint: config.deviceFingerprint,
|
|
40
|
+
timestamp: new Date().toISOString(),
|
|
41
|
+
entries: newLines.map(line => {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(line);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return { raw: line };
|
|
47
|
+
}
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
try {
|
|
51
|
+
// POST to CTFd server audit endpoint
|
|
52
|
+
const url = new URL('/api/v1/audit/logs', config.ctfdUrl).href;
|
|
53
|
+
const res = await fetch(url, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
'Authorization': `Token ${config.token}`,
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify(payload),
|
|
60
|
+
signal: AbortSignal.timeout(10_000), // 10s timeout
|
|
61
|
+
});
|
|
62
|
+
if (res.ok) {
|
|
63
|
+
state.lastSyncedLine = allLines.length;
|
|
64
|
+
state.lastSyncAt = new Date().toISOString();
|
|
65
|
+
state.syncCount++;
|
|
66
|
+
saveSyncState(state);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
state.failCount++;
|
|
70
|
+
saveSyncState(state);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Silent fail — network issues shouldn't break the CLI
|
|
75
|
+
state.failCount++;
|
|
76
|
+
saveSyncState(state);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function startLogSync() {
|
|
80
|
+
// Initial sync after 5 seconds
|
|
81
|
+
setTimeout(() => syncLogs(), 5_000);
|
|
82
|
+
// Then every 30 seconds
|
|
83
|
+
syncTimer = setInterval(() => syncLogs(), SYNC_INTERVAL);
|
|
84
|
+
}
|
|
85
|
+
export function stopLogSync() {
|
|
86
|
+
if (syncTimer) {
|
|
87
|
+
clearInterval(syncTimer);
|
|
88
|
+
syncTimer = null;
|
|
89
|
+
}
|
|
90
|
+
// Final sync before exit
|
|
91
|
+
syncLogs();
|
|
92
|
+
}
|
package/dist/repl.js
CHANGED
|
@@ -6,8 +6,9 @@ import { isActivated, activateToken, isFreeCommand, isDeviceMatch, recordExit, r
|
|
|
6
6
|
import { resetTerminalTheme } from './lib/theme.js';
|
|
7
7
|
import { ensureSandbox, runInSandbox, isDockerAvailable } from './lib/sandbox.js';
|
|
8
8
|
import { logCommand } from './lib/logger.js';
|
|
9
|
+
import { startLogSync, stopLogSync } from './lib/log-sync.js';
|
|
9
10
|
const INTERCEPT = '__REPL_NO_EXIT__';
|
|
10
|
-
const VERSION = '1.
|
|
11
|
+
const VERSION = '1.8.0';
|
|
11
12
|
export async function startRepl(program, resumeMode) {
|
|
12
13
|
const config = getConfig();
|
|
13
14
|
const connected = isConnected();
|
|
@@ -116,6 +117,8 @@ export async function startRepl(program, resumeMode) {
|
|
|
116
117
|
terminal: true,
|
|
117
118
|
});
|
|
118
119
|
let processing = false;
|
|
120
|
+
// Start background log sync (every 30s to server)
|
|
121
|
+
startLogSync();
|
|
119
122
|
rl.prompt();
|
|
120
123
|
rl.on('line', async (line) => {
|
|
121
124
|
if (processing)
|
|
@@ -129,6 +132,7 @@ export async function startRepl(program, resumeMode) {
|
|
|
129
132
|
logCommand(input);
|
|
130
133
|
// Exit — record, reset terminal colors, and quit
|
|
131
134
|
if (input === 'exit' || input === 'quit' || input === 'q') {
|
|
135
|
+
stopLogSync();
|
|
132
136
|
recordExit();
|
|
133
137
|
console.log(chalk.gray(' Session saved. Use ') + chalk.white('icoa --resume') + chalk.gray(' to continue.'));
|
|
134
138
|
resetTerminalTheme();
|
|
@@ -241,6 +245,7 @@ export async function startRepl(program, resumeMode) {
|
|
|
241
245
|
rl.prompt();
|
|
242
246
|
});
|
|
243
247
|
rl.on('close', () => {
|
|
248
|
+
stopLogSync();
|
|
244
249
|
recordExit();
|
|
245
250
|
resetTerminalTheme();
|
|
246
251
|
realExit(0);
|