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 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.3')}
39
+ ${chalk.gray('CLI-Native Competition Terminal v1.8.0')}
40
40
 
41
41
  ${LINE}
42
42
  `;
@@ -0,0 +1,2 @@
1
+ export declare function startLogSync(): void;
2
+ export declare function stopLogSync(): void;
@@ -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.7.3';
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "1.7.3",
3
+ "version": "1.8.0",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {