icoa-cli 2.19.100 → 2.19.101
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/commands/ctf.js
CHANGED
|
@@ -1,620 +1 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { CTFdClient } from '../lib/ctfd-client.js';
|
|
3
|
-
import { getConfig, saveConfig, getBudget } from '../lib/config.js';
|
|
4
|
-
import { logCommand, logSubmission } from '../lib/logger.js';
|
|
5
|
-
import { getTranslation } from '../lib/translation.js';
|
|
6
|
-
import { printSuccess, printError, printWarning, printInfo, printTable, printMarkdown, printHeader, printKeyValue, createSpinner, formatCountdown, } from '../lib/ui.js';
|
|
7
|
-
function requireConnection() {
|
|
8
|
-
const config = getConfig();
|
|
9
|
-
if (!config.ctfdUrl || (!config.token && !config.sessionCookie)) {
|
|
10
|
-
printError('Not connected to CTFd. Run: join <url>');
|
|
11
|
-
process.exit(1);
|
|
12
|
-
}
|
|
13
|
-
const session = config.sessionCookie || '';
|
|
14
|
-
const token = config.token && !config.token.includes('session=') ? config.token : '';
|
|
15
|
-
return { config, client: new CTFdClient(config.ctfdUrl, token, session || config.token) };
|
|
16
|
-
}
|
|
17
|
-
export function registerCtfCommands(program) {
|
|
18
|
-
const ctf = program.command('ctf').description('Competition commands');
|
|
19
|
-
// ─── icoa ctf join <url> ───
|
|
20
|
-
ctf
|
|
21
|
-
.command('join <url>')
|
|
22
|
-
.description('Connect to a CTFd instance')
|
|
23
|
-
.action(async (rawUrl) => {
|
|
24
|
-
// Auto-add https:// if missing
|
|
25
|
-
let url = rawUrl.trim().replace(/\/+$/, '');
|
|
26
|
-
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
27
|
-
url = 'https://' + url;
|
|
28
|
-
}
|
|
29
|
-
logCommand(`ctf join ${url}`);
|
|
30
|
-
console.log();
|
|
31
|
-
printInfo(`Connecting to ${chalk.bold(url)}`);
|
|
32
|
-
// Read username and password using raw stdin (avoids REPL readline conflict)
|
|
33
|
-
function rawInput(prompt, mask) {
|
|
34
|
-
return new Promise((resolve) => {
|
|
35
|
-
process.stdout.write(chalk.white(prompt));
|
|
36
|
-
if (process.stdin.isTTY)
|
|
37
|
-
process.stdin.setRawMode?.(true);
|
|
38
|
-
let buf = '';
|
|
39
|
-
const onData = (ch) => {
|
|
40
|
-
const c = ch.toString();
|
|
41
|
-
if (c === '\n' || c === '\r') {
|
|
42
|
-
if (process.stdin.isTTY)
|
|
43
|
-
process.stdin.setRawMode?.(false);
|
|
44
|
-
process.stdin.removeListener('data', onData);
|
|
45
|
-
process.stdout.write('\n');
|
|
46
|
-
resolve(buf);
|
|
47
|
-
}
|
|
48
|
-
else if (c === '\x7f' || c === '\b') {
|
|
49
|
-
if (buf.length > 0) {
|
|
50
|
-
buf = buf.slice(0, -1);
|
|
51
|
-
process.stdout.write('\b \b');
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
else if (c === '\x03') {
|
|
55
|
-
if (process.stdin.isTTY)
|
|
56
|
-
process.stdin.setRawMode?.(false);
|
|
57
|
-
process.stdin.removeListener('data', onData);
|
|
58
|
-
process.stdout.write('\n');
|
|
59
|
-
resolve('');
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
buf += c;
|
|
63
|
-
process.stdout.write(mask || c);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
process.stdin.on('data', onData);
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
const username = await rawInput(' Username: ');
|
|
70
|
-
const password = await rawInput(' Password: ', '*');
|
|
71
|
-
let token = '';
|
|
72
|
-
let sessionCookie = '';
|
|
73
|
-
let csrfNonce = '';
|
|
74
|
-
const spinner = createSpinner('Logging in...');
|
|
75
|
-
spinner.start();
|
|
76
|
-
try {
|
|
77
|
-
const client = new CTFdClient(url, '');
|
|
78
|
-
const result = await client.loginWithCredentials(username, password);
|
|
79
|
-
token = result.token;
|
|
80
|
-
sessionCookie = result.session;
|
|
81
|
-
csrfNonce = result.csrf;
|
|
82
|
-
if (token) {
|
|
83
|
-
spinner.succeed('Login successful — API token auto-generated');
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
spinner.succeed('Login successful (session mode)');
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
catch (err) {
|
|
90
|
-
spinner.fail('Login failed');
|
|
91
|
-
printError(err.message);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
// Test connection
|
|
95
|
-
const spinner2 = createSpinner('Testing connection...');
|
|
96
|
-
spinner2.start();
|
|
97
|
-
try {
|
|
98
|
-
const client = new CTFdClient(url, token, sessionCookie, csrfNonce);
|
|
99
|
-
const user = await client.testConnection();
|
|
100
|
-
spinner2.succeed('Connected successfully');
|
|
101
|
-
console.log();
|
|
102
|
-
printKeyValue('User', user.name);
|
|
103
|
-
printKeyValue('Score', String(user.score || 0));
|
|
104
|
-
if (user.team_id) {
|
|
105
|
-
printKeyValue('Team ID', String(user.team_id));
|
|
106
|
-
}
|
|
107
|
-
// Fetch competition metadata (times, mode)
|
|
108
|
-
try {
|
|
109
|
-
const meta = await client.getCompetitionMeta();
|
|
110
|
-
const configUpdate = {
|
|
111
|
-
ctfdUrl: url,
|
|
112
|
-
token: token || sessionCookie,
|
|
113
|
-
userId: user.id,
|
|
114
|
-
userName: user.name,
|
|
115
|
-
teamId: user.team_id,
|
|
116
|
-
sessionCookie: sessionCookie,
|
|
117
|
-
country: user.country || '',
|
|
118
|
-
};
|
|
119
|
-
if (meta.start) {
|
|
120
|
-
configUpdate.competitionStartsAt = new Date(meta.start * 1000).toISOString();
|
|
121
|
-
printKeyValue('Starts', new Date(meta.start * 1000).toLocaleString());
|
|
122
|
-
}
|
|
123
|
-
if (meta.end) {
|
|
124
|
-
configUpdate.competitionEndsAt = new Date(meta.end * 1000).toISOString();
|
|
125
|
-
printKeyValue('Ends', new Date(meta.end * 1000).toLocaleString());
|
|
126
|
-
}
|
|
127
|
-
// Determine competition state
|
|
128
|
-
const now = Date.now() / 1000;
|
|
129
|
-
if (meta.start && now < meta.start) {
|
|
130
|
-
configUpdate.competitionState = 'pre_competition';
|
|
131
|
-
}
|
|
132
|
-
else if (meta.end && now > meta.end) {
|
|
133
|
-
configUpdate.competitionState = 'finished';
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
configUpdate.competitionState = 'live';
|
|
137
|
-
}
|
|
138
|
-
printKeyValue('Status', configUpdate.competitionState);
|
|
139
|
-
saveConfig(configUpdate);
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
// Fallback: save without competition times
|
|
143
|
-
saveConfig({
|
|
144
|
-
ctfdUrl: url,
|
|
145
|
-
token: token,
|
|
146
|
-
userId: user.id,
|
|
147
|
-
userName: user.name,
|
|
148
|
-
teamId: user.team_id,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
console.log();
|
|
152
|
-
printSuccess('Connection saved. You are ready!');
|
|
153
|
-
console.log();
|
|
154
|
-
const currentMode = getConfig().mode || '';
|
|
155
|
-
if (currentMode === 'olympiad') {
|
|
156
|
-
// Olympiad: guided step-by-step walkthrough
|
|
157
|
-
console.log(chalk.cyan(' ─────────────────────────────────────────────'));
|
|
158
|
-
console.log(chalk.bold.white(' How to compete:'));
|
|
159
|
-
console.log();
|
|
160
|
-
console.log(chalk.white(' Step 1 ') + chalk.bold.cyan('challenges') + chalk.gray(' Browse all challenges'));
|
|
161
|
-
console.log(chalk.white(' Step 2 ') + chalk.bold.cyan('open <id>') + chalk.gray(' Read challenge details'));
|
|
162
|
-
console.log(chalk.white(' Step 3 ') + chalk.bold.cyan('hint "your question"') + chalk.gray(' Ask AI for help'));
|
|
163
|
-
console.log(chalk.white(' Step 4 ') + chalk.bold.cyan('submit <id> <flag>') + chalk.gray(' Submit your answer'));
|
|
164
|
-
console.log();
|
|
165
|
-
console.log(chalk.gray(' More:'));
|
|
166
|
-
console.log(chalk.white(' scoreboard') + chalk.gray(' Live rankings'));
|
|
167
|
-
console.log(chalk.white(' status') + chalk.gray(' Your score & hint budget'));
|
|
168
|
-
console.log(chalk.white(' ai4ctf') + chalk.gray(' Free-chat with AI teammate'));
|
|
169
|
-
console.log(chalk.cyan(' ─────────────────────────────────────────────'));
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
console.log(chalk.gray(' Next:'));
|
|
173
|
-
console.log(chalk.white(' exam list ') + chalk.gray('View available exams'));
|
|
174
|
-
console.log(chalk.white(' challenges ') + chalk.gray('View CTF challenges'));
|
|
175
|
-
console.log(chalk.white(' status ') + chalk.gray('Check score & budget'));
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
catch (err) {
|
|
179
|
-
if (sessionCookie) {
|
|
180
|
-
// Session login succeeded but API test failed — save anyway
|
|
181
|
-
spinner2.succeed('Connected (session mode — limited API)');
|
|
182
|
-
saveConfig({
|
|
183
|
-
ctfdUrl: url,
|
|
184
|
-
token: sessionCookie,
|
|
185
|
-
userName: username,
|
|
186
|
-
sessionCookie: sessionCookie,
|
|
187
|
-
});
|
|
188
|
-
console.log();
|
|
189
|
-
printKeyValue('User', username);
|
|
190
|
-
printWarning('API access limited. Some features may not work.');
|
|
191
|
-
printSuccess('Connection saved.');
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
spinner2.fail('Connection test failed');
|
|
195
|
-
printError(err.message);
|
|
196
|
-
saveConfig({ ctfdUrl: url });
|
|
197
|
-
console.log();
|
|
198
|
-
printInfo('Connection saved. Try: join ' + url);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
// ─── icoa ctf logout ───
|
|
203
|
-
ctf
|
|
204
|
-
.command('logout')
|
|
205
|
-
.description('Disconnect and clear credentials')
|
|
206
|
-
.action(() => {
|
|
207
|
-
logCommand('ctf logout');
|
|
208
|
-
saveConfig({
|
|
209
|
-
ctfdUrl: '',
|
|
210
|
-
token: '',
|
|
211
|
-
sessionCookie: '',
|
|
212
|
-
userId: null,
|
|
213
|
-
userName: '',
|
|
214
|
-
teamId: null,
|
|
215
|
-
country: '',
|
|
216
|
-
});
|
|
217
|
-
printSuccess('Logged out. Credentials cleared.');
|
|
218
|
-
console.log();
|
|
219
|
-
console.log(chalk.gray(' What now?'));
|
|
220
|
-
console.log(chalk.white(' join <url>') + chalk.gray(' Re-connect to competition'));
|
|
221
|
-
console.log(chalk.white(' setup') + chalk.gray(' Switch mode'));
|
|
222
|
-
console.log(chalk.white(' exit') + chalk.gray(' Quit ICOA CLI'));
|
|
223
|
-
});
|
|
224
|
-
// ─── icoa ctf activate <code> ───
|
|
225
|
-
ctf
|
|
226
|
-
.command('activate <code>')
|
|
227
|
-
.description('Validate competition code')
|
|
228
|
-
.action(async (code) => {
|
|
229
|
-
logCommand(`ctf activate ${code}`);
|
|
230
|
-
const { client } = requireConnection();
|
|
231
|
-
const spinner = createSpinner('Validating competition code...');
|
|
232
|
-
spinner.start();
|
|
233
|
-
try {
|
|
234
|
-
// Try custom ICOA endpoint first
|
|
235
|
-
const res = await fetch(getConfig().ctfdUrl + '/api/v1/icoa/activate', {
|
|
236
|
-
method: 'POST',
|
|
237
|
-
headers: {
|
|
238
|
-
Authorization: `Token ${getConfig().token}`,
|
|
239
|
-
'Content-Type': 'application/json',
|
|
240
|
-
},
|
|
241
|
-
body: JSON.stringify({ code }),
|
|
242
|
-
});
|
|
243
|
-
if (res.ok) {
|
|
244
|
-
const data = (await res.json());
|
|
245
|
-
spinner.succeed('Competition code validated');
|
|
246
|
-
saveConfig({
|
|
247
|
-
competitionCode: code,
|
|
248
|
-
teamId: data.data?.team_id || getConfig().teamId,
|
|
249
|
-
competitionState: 'live',
|
|
250
|
-
});
|
|
251
|
-
printSuccess(`Activated: ${code}`);
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
// Fallback: just store the code locally
|
|
255
|
-
spinner.succeed('Competition code saved');
|
|
256
|
-
saveConfig({ competitionCode: code });
|
|
257
|
-
printInfo('Code stored locally. Server validation will be attempted during competition.');
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
catch {
|
|
261
|
-
// Server doesn't have custom endpoint, store locally
|
|
262
|
-
spinner.succeed('Competition code saved');
|
|
263
|
-
saveConfig({ competitionCode: code });
|
|
264
|
-
printInfo('Code stored locally.');
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
// ─── icoa ctf challenges ───
|
|
268
|
-
ctf
|
|
269
|
-
.command('challenges')
|
|
270
|
-
.description('List all challenges')
|
|
271
|
-
.action(async () => {
|
|
272
|
-
logCommand('ctf challenges');
|
|
273
|
-
const { client } = requireConnection();
|
|
274
|
-
const spinner = createSpinner('Loading challenges...');
|
|
275
|
-
spinner.start();
|
|
276
|
-
try {
|
|
277
|
-
const challenges = await client.getChallenges();
|
|
278
|
-
spinner.stop();
|
|
279
|
-
if (!challenges || challenges.length === 0) {
|
|
280
|
-
printInfo('No challenges available yet.');
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
// Group by category
|
|
284
|
-
const byCategory = new Map();
|
|
285
|
-
for (const c of challenges) {
|
|
286
|
-
const cat = c.category || 'Uncategorized';
|
|
287
|
-
if (!byCategory.has(cat))
|
|
288
|
-
byCategory.set(cat, []);
|
|
289
|
-
byCategory.get(cat).push(c);
|
|
290
|
-
}
|
|
291
|
-
const solved = challenges.filter((c) => c.solved_by_me).length;
|
|
292
|
-
printHeader(`Challenges (${solved}/${challenges.length} solved)`);
|
|
293
|
-
// Category descriptions for beginners
|
|
294
|
-
const catDesc = {
|
|
295
|
-
'Web': 'Find vulnerabilities in websites',
|
|
296
|
-
'Crypto': 'Break ciphers & encryption',
|
|
297
|
-
'Reversing': 'Analyze compiled programs',
|
|
298
|
-
'Rev': 'Analyze compiled programs',
|
|
299
|
-
'Pwn': 'Exploit binary vulnerabilities',
|
|
300
|
-
'Forensics': 'Investigate digital evidence',
|
|
301
|
-
'Misc': 'Creative & mixed challenges',
|
|
302
|
-
'OSINT': 'Open-source intelligence gathering',
|
|
303
|
-
'AI': 'AI security & adversarial ML',
|
|
304
|
-
};
|
|
305
|
-
// Difficulty based on points
|
|
306
|
-
const difficulty = (pts) => {
|
|
307
|
-
if (pts <= 100)
|
|
308
|
-
return chalk.green('Easy');
|
|
309
|
-
if (pts <= 250)
|
|
310
|
-
return chalk.yellow('Medium');
|
|
311
|
-
if (pts <= 500)
|
|
312
|
-
return chalk.red('Hard');
|
|
313
|
-
return chalk.magenta('Expert');
|
|
314
|
-
};
|
|
315
|
-
// Sort categories and display
|
|
316
|
-
const rows = [...byCategory.entries()]
|
|
317
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
318
|
-
.flatMap(([category, challs]) => {
|
|
319
|
-
const desc = catDesc[category] || '';
|
|
320
|
-
const catLabel = desc
|
|
321
|
-
? chalk.cyan.bold(`── ${category} ──`) + ' ' + chalk.gray(desc)
|
|
322
|
-
: chalk.cyan.bold(`── ${category} ──`);
|
|
323
|
-
const catRow = [catLabel, '', '', '', ''];
|
|
324
|
-
const challRows = challs
|
|
325
|
-
.sort((a, b) => a.value - b.value)
|
|
326
|
-
.map((c) => [
|
|
327
|
-
String(c.id),
|
|
328
|
-
c.solved_by_me ? chalk.gray.strikethrough(c.name) : c.name,
|
|
329
|
-
c.solved_by_me ? chalk.gray(String(c.value)) : chalk.yellow(String(c.value)),
|
|
330
|
-
c.solved_by_me ? chalk.gray('--') : difficulty(c.value),
|
|
331
|
-
c.solved_by_me ? chalk.green('✓') : chalk.gray('○'),
|
|
332
|
-
]);
|
|
333
|
-
return [catRow, ...challRows];
|
|
334
|
-
});
|
|
335
|
-
printTable(['#', 'Name', 'Points', 'Difficulty', 'Solved'], rows);
|
|
336
|
-
console.log(chalk.gray(` ${challenges.length} challenges, ${solved} solved`));
|
|
337
|
-
if (solved === 0) {
|
|
338
|
-
console.log();
|
|
339
|
-
console.log(chalk.gray(' Tip: Start with ') + chalk.green('Easy') + chalk.gray(' challenges! Type ') + chalk.white('open <id>') + chalk.gray(' to read one.'));
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
catch (err) {
|
|
343
|
-
spinner.fail('Failed to load challenges');
|
|
344
|
-
printError(err.message);
|
|
345
|
-
}
|
|
346
|
-
});
|
|
347
|
-
// ─── icoa ctf open <id> ───
|
|
348
|
-
ctf
|
|
349
|
-
.command('open <id>')
|
|
350
|
-
.description('View challenge details')
|
|
351
|
-
.action(async (id) => {
|
|
352
|
-
logCommand(`ctf open ${id}`);
|
|
353
|
-
const { config, client } = requireConnection();
|
|
354
|
-
const spinner = createSpinner('Loading challenge...');
|
|
355
|
-
spinner.start();
|
|
356
|
-
try {
|
|
357
|
-
const challenge = await client.getChallenge(parseInt(id));
|
|
358
|
-
spinner.stop();
|
|
359
|
-
// Save current challenge context for hints
|
|
360
|
-
saveConfig({
|
|
361
|
-
currentChallengeId: challenge.id,
|
|
362
|
-
currentChallengeName: challenge.name,
|
|
363
|
-
currentChallengeCategory: challenge.category,
|
|
364
|
-
});
|
|
365
|
-
printHeader(`${challenge.name} [${challenge.category}]`);
|
|
366
|
-
printKeyValue('Points', String(challenge.value));
|
|
367
|
-
printKeyValue('Solves', String(challenge.solves));
|
|
368
|
-
if (challenge.connection_info) {
|
|
369
|
-
printKeyValue('Connection', challenge.connection_info);
|
|
370
|
-
}
|
|
371
|
-
console.log();
|
|
372
|
-
// Display description (with optional translation)
|
|
373
|
-
let description = challenge.description;
|
|
374
|
-
if (config.language !== 'en') {
|
|
375
|
-
try {
|
|
376
|
-
description = await getTranslation(description, challenge.id, config.language);
|
|
377
|
-
}
|
|
378
|
-
catch {
|
|
379
|
-
// Translation failed, use English
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
printMarkdown(description);
|
|
383
|
-
// Show files
|
|
384
|
-
if (challenge.files && challenge.files.length > 0) {
|
|
385
|
-
console.log();
|
|
386
|
-
printInfo(`Files (${challenge.files.length}): use ${chalk.white(`files ${id}`)} to download`);
|
|
387
|
-
}
|
|
388
|
-
// Show hints
|
|
389
|
-
if (challenge.hints && challenge.hints.length > 0) {
|
|
390
|
-
printInfo(`CTFd Hints available: ${challenge.hints.length}`);
|
|
391
|
-
}
|
|
392
|
-
// Show connection info
|
|
393
|
-
if (challenge.connection_info) {
|
|
394
|
-
console.log();
|
|
395
|
-
printInfo(`Quick connect: ${chalk.white(`connect ${id}`)}`);
|
|
396
|
-
}
|
|
397
|
-
// Show guided next actions
|
|
398
|
-
console.log();
|
|
399
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
400
|
-
console.log(chalk.bold.white(' What to do next:'));
|
|
401
|
-
console.log(chalk.white(` hint "how to start?"`) + chalk.gray(' Ask AI for guidance (Level A)'));
|
|
402
|
-
if (challenge.files && challenge.files.length > 0) {
|
|
403
|
-
console.log(chalk.white(` files ${id}`) + chalk.gray(' Download challenge files'));
|
|
404
|
-
}
|
|
405
|
-
if (challenge.connection_info) {
|
|
406
|
-
console.log(chalk.white(` connect ${id}`) + chalk.gray(' Connect to target'));
|
|
407
|
-
}
|
|
408
|
-
console.log(chalk.white(` submit ${id} icoa{flag}`) + chalk.gray(' Submit your answer'));
|
|
409
|
-
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
410
|
-
}
|
|
411
|
-
catch (err) {
|
|
412
|
-
spinner.fail('Failed to load challenge');
|
|
413
|
-
printError(err.message);
|
|
414
|
-
}
|
|
415
|
-
});
|
|
416
|
-
// ─── icoa ctf submit <id> <flag> ───
|
|
417
|
-
ctf
|
|
418
|
-
.command('submit <id> <flag>')
|
|
419
|
-
.description('Submit a flag')
|
|
420
|
-
.action(async (id, flag) => {
|
|
421
|
-
logCommand(`ctf submit ${id}`);
|
|
422
|
-
logSubmission(parseInt(id), flag);
|
|
423
|
-
const { client } = requireConnection();
|
|
424
|
-
const spinner = createSpinner('Submitting flag...');
|
|
425
|
-
spinner.start();
|
|
426
|
-
try {
|
|
427
|
-
const result = await client.submitFlag(parseInt(id), flag);
|
|
428
|
-
spinner.stop();
|
|
429
|
-
switch (result.status) {
|
|
430
|
-
case 'correct':
|
|
431
|
-
console.log();
|
|
432
|
-
console.log(chalk.green.bold(' 🎉 Correct! ') + chalk.white(result.message));
|
|
433
|
-
console.log();
|
|
434
|
-
break;
|
|
435
|
-
case 'incorrect':
|
|
436
|
-
printError('Incorrect. ' + result.message);
|
|
437
|
-
break;
|
|
438
|
-
case 'already_solved':
|
|
439
|
-
printWarning('Already solved.');
|
|
440
|
-
break;
|
|
441
|
-
case 'paused':
|
|
442
|
-
printWarning('Competition is paused.');
|
|
443
|
-
break;
|
|
444
|
-
case 'ratelimited':
|
|
445
|
-
printWarning('Rate limited. Please wait before trying again.');
|
|
446
|
-
break;
|
|
447
|
-
default:
|
|
448
|
-
printInfo(result.message || 'Unknown response');
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
catch (err) {
|
|
452
|
-
spinner.fail('Submission failed');
|
|
453
|
-
printError(err.message);
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
// ─── icoa ctf scoreboard ───
|
|
457
|
-
ctf
|
|
458
|
-
.command('scoreboard [top]')
|
|
459
|
-
.description('View scoreboard (optional: top N)')
|
|
460
|
-
.action(async (top) => {
|
|
461
|
-
logCommand('ctf scoreboard');
|
|
462
|
-
const { config, client } = requireConnection();
|
|
463
|
-
const spinner = createSpinner('Loading scoreboard...');
|
|
464
|
-
spinner.start();
|
|
465
|
-
try {
|
|
466
|
-
const scoreboard = await client.getScoreboard();
|
|
467
|
-
spinner.stop();
|
|
468
|
-
if (!scoreboard || scoreboard.length === 0) {
|
|
469
|
-
printInfo('Scoreboard is empty.');
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
printHeader('Scoreboard');
|
|
473
|
-
const rows = scoreboard.map((entry) => {
|
|
474
|
-
const isMyTeam = entry.account_id === config.teamId;
|
|
475
|
-
const row = [
|
|
476
|
-
String(entry.pos),
|
|
477
|
-
entry.name,
|
|
478
|
-
String(entry.score),
|
|
479
|
-
];
|
|
480
|
-
if (isMyTeam) {
|
|
481
|
-
return row.map((cell) => chalk.yellow.bold(cell));
|
|
482
|
-
}
|
|
483
|
-
return row;
|
|
484
|
-
});
|
|
485
|
-
printTable(['Rank', 'Team', 'Score'], rows);
|
|
486
|
-
}
|
|
487
|
-
catch (err) {
|
|
488
|
-
spinner.fail('Failed to load scoreboard');
|
|
489
|
-
printError(err.message);
|
|
490
|
-
}
|
|
491
|
-
});
|
|
492
|
-
// ─── icoa ctf status ───
|
|
493
|
-
ctf
|
|
494
|
-
.command('status')
|
|
495
|
-
.description('Show competition status')
|
|
496
|
-
.action(async () => {
|
|
497
|
-
logCommand('ctf status');
|
|
498
|
-
const { config, client } = requireConnection();
|
|
499
|
-
printHeader('Competition Status');
|
|
500
|
-
// Competition state
|
|
501
|
-
const stateColors = {
|
|
502
|
-
pre_competition: chalk.yellow,
|
|
503
|
-
demo: chalk.blue,
|
|
504
|
-
live: chalk.green,
|
|
505
|
-
finished: chalk.red,
|
|
506
|
-
unknown: chalk.gray,
|
|
507
|
-
};
|
|
508
|
-
const stateLabels = {
|
|
509
|
-
pre_competition: 'PRE-COMPETITION',
|
|
510
|
-
demo: 'DEMO',
|
|
511
|
-
live: 'LIVE',
|
|
512
|
-
finished: 'FINISHED',
|
|
513
|
-
unknown: 'UNKNOWN',
|
|
514
|
-
};
|
|
515
|
-
const state = config.competitionState || 'unknown';
|
|
516
|
-
const colorFn = stateColors[state] || chalk.gray;
|
|
517
|
-
console.log(` ${chalk.gray('State:')} ${chalk.bold(colorFn(stateLabels[state] || state))}`);
|
|
518
|
-
// User info
|
|
519
|
-
try {
|
|
520
|
-
const user = await client.testConnection();
|
|
521
|
-
printKeyValue('User', chalk.white.bold(user.name));
|
|
522
|
-
printKeyValue('Score', chalk.yellow.bold(String(user.score || 0)));
|
|
523
|
-
printKeyValue('Rank', chalk.cyan(user.place || 'N/A'));
|
|
524
|
-
}
|
|
525
|
-
catch {
|
|
526
|
-
printKeyValue('User', config.userName || 'Unknown');
|
|
527
|
-
}
|
|
528
|
-
// Hint budget with visual bars
|
|
529
|
-
console.log();
|
|
530
|
-
console.log(` ${chalk.gray('Hint Budget:')}`);
|
|
531
|
-
const budget = getBudget();
|
|
532
|
-
const bar = (used, total, color) => {
|
|
533
|
-
const width = 20;
|
|
534
|
-
const filled = Math.round((used / total) * width);
|
|
535
|
-
const empty = width - filled;
|
|
536
|
-
return color('█'.repeat(filled)) + chalk.gray('░'.repeat(empty)) + ` ${used}/${total}`;
|
|
537
|
-
};
|
|
538
|
-
console.log(` A ${bar(budget.a, 50, chalk.green)}`);
|
|
539
|
-
console.log(` B ${bar(budget.b, 10, chalk.yellow)}`);
|
|
540
|
-
console.log(` C ${bar(budget.c, 2, chalk.red)}`);
|
|
541
|
-
console.log(` ${chalk.gray('Tokens')} ${bar(budget.tokenCap - budget.tokensUsed, budget.tokenCap, chalk.cyan)}`);
|
|
542
|
-
// Time
|
|
543
|
-
if (config.competitionEndsAt) {
|
|
544
|
-
console.log();
|
|
545
|
-
const endsAt = new Date(config.competitionEndsAt);
|
|
546
|
-
const now = new Date();
|
|
547
|
-
if (now < endsAt) {
|
|
548
|
-
printKeyValue('Time Remaining', chalk.yellow.bold(formatCountdown(endsAt)));
|
|
549
|
-
}
|
|
550
|
-
else {
|
|
551
|
-
printKeyValue('Time', chalk.red('Competition ended'));
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
console.log();
|
|
555
|
-
});
|
|
556
|
-
// ─── icoa ctf time ───
|
|
557
|
-
ctf
|
|
558
|
-
.command('time')
|
|
559
|
-
.description('Show competition countdown')
|
|
560
|
-
.action(() => {
|
|
561
|
-
logCommand('ctf time');
|
|
562
|
-
const config = getConfig();
|
|
563
|
-
if (!config.competitionStartsAt && !config.competitionEndsAt) {
|
|
564
|
-
printInfo('Competition times not configured.');
|
|
565
|
-
printInfo('They will be set when the competition server provides timing information.');
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
const now = new Date();
|
|
569
|
-
const startsAt = config.competitionStartsAt ? new Date(config.competitionStartsAt) : null;
|
|
570
|
-
const endsAt = config.competitionEndsAt ? new Date(config.competitionEndsAt) : null;
|
|
571
|
-
printHeader('Competition Timer');
|
|
572
|
-
if (startsAt && now < startsAt) {
|
|
573
|
-
console.log(chalk.yellow(' Competition has not started yet.'));
|
|
574
|
-
console.log();
|
|
575
|
-
const update = () => {
|
|
576
|
-
process.stdout.write(`\r ${chalk.bold('Starts in:')} ${formatCountdown(startsAt)} `);
|
|
577
|
-
};
|
|
578
|
-
update();
|
|
579
|
-
const interval = setInterval(() => {
|
|
580
|
-
if (new Date() >= startsAt) {
|
|
581
|
-
clearInterval(interval);
|
|
582
|
-
console.log();
|
|
583
|
-
printSuccess('Competition has started! Good luck!');
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
update();
|
|
587
|
-
}, 1000);
|
|
588
|
-
process.on('SIGINT', () => {
|
|
589
|
-
clearInterval(interval);
|
|
590
|
-
console.log();
|
|
591
|
-
process.exit(0);
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
else if (endsAt && now < endsAt) {
|
|
595
|
-
console.log(chalk.green(' Competition is LIVE'));
|
|
596
|
-
console.log();
|
|
597
|
-
const update = () => {
|
|
598
|
-
process.stdout.write(`\r ${chalk.bold('Time remaining:')} ${formatCountdown(endsAt)} `);
|
|
599
|
-
};
|
|
600
|
-
update();
|
|
601
|
-
const interval = setInterval(() => {
|
|
602
|
-
if (new Date() >= endsAt) {
|
|
603
|
-
clearInterval(interval);
|
|
604
|
-
console.log();
|
|
605
|
-
printWarning('Competition has ended!');
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
update();
|
|
609
|
-
}, 1000);
|
|
610
|
-
process.on('SIGINT', () => {
|
|
611
|
-
clearInterval(interval);
|
|
612
|
-
console.log();
|
|
613
|
-
process.exit(0);
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
else {
|
|
617
|
-
console.log(chalk.red(' Competition has ended.'));
|
|
618
|
-
}
|
|
619
|
-
});
|
|
620
|
-
}
|
|
1
|
+
import chalk from"chalk";import{CTFdClient as e}from"../lib/ctfd-client.js";import{getConfig as o,saveConfig as t,getBudget as n}from"../lib/config.js";import{logCommand as s,logSubmission as i}from"../lib/logger.js";import{getTranslation as a}from"../lib/translation.js";import{printSuccess as c,printError as l,printWarning as r,printInfo as d,printTable as g,printMarkdown as m,printHeader as p,printKeyValue as u,createSpinner as h,formatCountdown as y}from"../lib/ui.js";function w(){const t=o();t.ctfdUrl&&(t.token||t.sessionCookie)||(l("Not connected to CTFd. Run: join <url>"),process.exit(1));const n=t.sessionCookie||"",s=t.token&&!t.token.includes("session=")?t.token:"";return{config:t,client:new e(t.ctfdUrl,s,n||t.token)}}export function registerCtfCommands(f){const b=f.command("ctf").description("Competition commands");b.command("join <url>").description("Connect to a CTFd instance").action(async n=>{let i=n.trim().replace(/\/+$/,"");function a(e,o){return new Promise(t=>{process.stdout.write(chalk.white(e)),process.stdin.isTTY&&process.stdin.setRawMode?.(!0);let n="";const s=e=>{const i=e.toString();"\n"===i||"\r"===i?(process.stdin.isTTY&&process.stdin.setRawMode?.(!1),process.stdin.removeListener("data",s),process.stdout.write("\n"),t(n)):""===i||"\b"===i?n.length>0&&(n=n.slice(0,-1),process.stdout.write("\b \b")):""===i?(process.stdin.isTTY&&process.stdin.setRawMode?.(!1),process.stdin.removeListener("data",s),process.stdout.write("\n"),t("")):(n+=i,process.stdout.write(o||i))};process.stdin.on("data",s)})}i.startsWith("http://")||i.startsWith("https://")||(i="https://"+i),s(`ctf join ${i}`),console.log(),d(`Connecting to ${chalk.bold(i)}`);const g=await a(" Username: "),m=await a(" Password: ","*");let p="",y="",w="";const f=h("Logging in...");f.start();try{const o=new e(i,""),t=await o.loginWithCredentials(g,m);p=t.token,y=t.session,w=t.csrf,p?f.succeed("Login successful — API token auto-generated"):f.succeed("Login successful (session mode)")}catch(e){return f.fail("Login failed"),void l(e.message)}const b=h("Testing connection...");b.start();try{const n=new e(i,p,y,w),s=await n.testConnection();b.succeed("Connected successfully"),console.log(),u("User",s.name),u("Score",String(s.score||0)),s.team_id&&u("Team ID",String(s.team_id));try{const e=await n.getCompetitionMeta(),o={ctfdUrl:i,token:p||y,userId:s.id,userName:s.name,teamId:s.team_id,sessionCookie:y,country:s.country||""};e.start&&(o.competitionStartsAt=new Date(1e3*e.start).toISOString(),u("Starts",new Date(1e3*e.start).toLocaleString())),e.end&&(o.competitionEndsAt=new Date(1e3*e.end).toISOString(),u("Ends",new Date(1e3*e.end).toLocaleString()));const a=Date.now()/1e3;e.start&&a<e.start?o.competitionState="pre_competition":e.end&&a>e.end?o.competitionState="finished":o.competitionState="live",u("Status",o.competitionState),t(o)}catch{t({ctfdUrl:i,token:p,userId:s.id,userName:s.name,teamId:s.team_id})}console.log(),c("Connection saved. You are ready!"),console.log(),"olympiad"===(o().mode||"")?(console.log(chalk.cyan(" ─────────────────────────────────────────────")),console.log(chalk.bold.white(" How to compete:")),console.log(),console.log(chalk.white(" Step 1 ")+chalk.bold.cyan("challenges")+chalk.gray(" Browse all challenges")),console.log(chalk.white(" Step 2 ")+chalk.bold.cyan("open <id>")+chalk.gray(" Read challenge details")),console.log(chalk.white(" Step 3 ")+chalk.bold.cyan('hint "your question"')+chalk.gray(" Ask AI for help")),console.log(chalk.white(" Step 4 ")+chalk.bold.cyan("submit <id> <flag>")+chalk.gray(" Submit your answer")),console.log(),console.log(chalk.gray(" More:")),console.log(chalk.white(" scoreboard")+chalk.gray(" Live rankings")),console.log(chalk.white(" status")+chalk.gray(" Your score & hint budget")),console.log(chalk.white(" ai4ctf")+chalk.gray(" Free-chat with AI teammate")),console.log(chalk.cyan(" ─────────────────────────────────────────────"))):(console.log(chalk.gray(" Next:")),console.log(chalk.white(" exam list ")+chalk.gray("View available exams")),console.log(chalk.white(" challenges ")+chalk.gray("View CTF challenges")),console.log(chalk.white(" status ")+chalk.gray("Check score & budget")))}catch(e){y?(b.succeed("Connected (session mode — limited API)"),t({ctfdUrl:i,token:y,userName:g,sessionCookie:y}),console.log(),u("User",g),r("API access limited. Some features may not work."),c("Connection saved.")):(b.fail("Connection test failed"),l(e.message),t({ctfdUrl:i}),console.log(),d("Connection saved. Try: join "+i))}}),b.command("logout").description("Disconnect and clear credentials").action(()=>{s("ctf logout"),t({ctfdUrl:"",token:"",sessionCookie:"",userId:null,userName:"",teamId:null,country:""}),c("Logged out. Credentials cleared."),console.log(),console.log(chalk.gray(" What now?")),console.log(chalk.white(" join <url>")+chalk.gray(" Re-connect to competition")),console.log(chalk.white(" setup")+chalk.gray(" Switch mode")),console.log(chalk.white(" exit")+chalk.gray(" Quit ICOA CLI"))}),b.command("activate <code>").description("Validate competition code").action(async e=>{s(`ctf activate ${e}`);const{client:n}=w(),i=h("Validating competition code...");i.start();try{const n=await fetch(o().ctfdUrl+"/api/v1/icoa/activate",{method:"POST",headers:{Authorization:`Token ${o().token}`,"Content-Type":"application/json"},body:JSON.stringify({code:e})});if(n.ok){const s=await n.json();i.succeed("Competition code validated"),t({competitionCode:e,teamId:s.data?.team_id||o().teamId,competitionState:"live"}),c(`Activated: ${e}`)}else i.succeed("Competition code saved"),t({competitionCode:e}),d("Code stored locally. Server validation will be attempted during competition.")}catch{i.succeed("Competition code saved"),t({competitionCode:e}),d("Code stored locally.")}}),b.command("challenges").description("List all challenges").action(async()=>{s("ctf challenges");const{client:e}=w(),o=h("Loading challenges...");o.start();try{const t=await e.getChallenges();if(o.stop(),!t||0===t.length)return void d("No challenges available yet.");const n=new Map;for(const e of t){const o=e.category||"Uncategorized";n.has(o)||n.set(o,[]),n.get(o).push(e)}const s=t.filter(e=>e.solved_by_me).length;p(`Challenges (${s}/${t.length} solved)`);const i={Web:"Find vulnerabilities in websites",Crypto:"Break ciphers & encryption",Reversing:"Analyze compiled programs",Rev:"Analyze compiled programs",Pwn:"Exploit binary vulnerabilities",Forensics:"Investigate digital evidence",Misc:"Creative & mixed challenges",OSINT:"Open-source intelligence gathering",AI:"AI security & adversarial ML"},a=e=>e<=100?chalk.green("Easy"):e<=250?chalk.yellow("Medium"):e<=500?chalk.red("Hard"):chalk.magenta("Expert"),c=[...n.entries()].sort(([e],[o])=>e.localeCompare(o)).flatMap(([e,o])=>{const t=i[e]||"";return[[t?chalk.cyan.bold(`── ${e} ──`)+" "+chalk.gray(t):chalk.cyan.bold(`── ${e} ──`),"","","",""],...o.sort((e,o)=>e.value-o.value).map(e=>[String(e.id),e.solved_by_me?chalk.gray.strikethrough(e.name):e.name,e.solved_by_me?chalk.gray(String(e.value)):chalk.yellow(String(e.value)),e.solved_by_me?chalk.gray("--"):a(e.value),e.solved_by_me?chalk.green("✓"):chalk.gray("○")])]});g(["#","Name","Points","Difficulty","Solved"],c),console.log(chalk.gray(` ${t.length} challenges, ${s} solved`)),0===s&&(console.log(),console.log(chalk.gray(" Tip: Start with ")+chalk.green("Easy")+chalk.gray(" challenges! Type ")+chalk.white("open <id>")+chalk.gray(" to read one.")))}catch(e){o.fail("Failed to load challenges"),l(e.message)}}),b.command("open <id>").description("View challenge details").action(async e=>{s(`ctf open ${e}`);const{config:o,client:n}=w(),i=h("Loading challenge...");i.start();try{const s=await n.getChallenge(parseInt(e));i.stop(),t({currentChallengeId:s.id,currentChallengeName:s.name,currentChallengeCategory:s.category}),p(`${s.name} [${s.category}]`),u("Points",String(s.value)),u("Solves",String(s.solves)),s.connection_info&&u("Connection",s.connection_info),console.log();let c=s.description;if("en"!==o.language)try{c=await a(c,s.id,o.language)}catch{}m(c),s.files&&s.files.length>0&&(console.log(),d(`Files (${s.files.length}): use ${chalk.white(`files ${e}`)} to download`)),s.hints&&s.hints.length>0&&d(`CTFd Hints available: ${s.hints.length}`),s.connection_info&&(console.log(),d(`Quick connect: ${chalk.white(`connect ${e}`)}`)),console.log(),console.log(chalk.gray(" ─────────────────────────────────────────────")),console.log(chalk.bold.white(" What to do next:")),console.log(chalk.white(' hint "how to start?"')+chalk.gray(" Ask AI for guidance (Level A)")),s.files&&s.files.length>0&&console.log(chalk.white(` files ${e}`)+chalk.gray(" Download challenge files")),s.connection_info&&console.log(chalk.white(` connect ${e}`)+chalk.gray(" Connect to target")),console.log(chalk.white(` submit ${e} icoa{flag}`)+chalk.gray(" Submit your answer")),console.log(chalk.gray(" ─────────────────────────────────────────────"))}catch(e){i.fail("Failed to load challenge"),l(e.message)}}),b.command("submit <id> <flag>").description("Submit a flag").action(async(e,o)=>{s(`ctf submit ${e}`),i(parseInt(e),o);const{client:t}=w(),n=h("Submitting flag...");n.start();try{const s=await t.submitFlag(parseInt(e),o);switch(n.stop(),s.status){case"correct":console.log(),console.log(chalk.green.bold(" 🎉 Correct! ")+chalk.white(s.message)),console.log();break;case"incorrect":l("Incorrect. "+s.message);break;case"already_solved":r("Already solved.");break;case"paused":r("Competition is paused.");break;case"ratelimited":r("Rate limited. Please wait before trying again.");break;default:d(s.message||"Unknown response")}}catch(e){n.fail("Submission failed"),l(e.message)}}),b.command("scoreboard [top]").description("View scoreboard (optional: top N)").action(async e=>{s("ctf scoreboard");const{config:o,client:t}=w(),n=h("Loading scoreboard...");n.start();try{const e=await t.getScoreboard();if(n.stop(),!e||0===e.length)return void d("Scoreboard is empty.");p("Scoreboard");const s=e.map(e=>{const t=e.account_id===o.teamId,n=[String(e.pos),e.name,String(e.score)];return t?n.map(e=>chalk.yellow.bold(e)):n});g(["Rank","Team","Score"],s)}catch(e){n.fail("Failed to load scoreboard"),l(e.message)}}),b.command("status").description("Show competition status").action(async()=>{s("ctf status");const{config:e,client:o}=w();p("Competition Status");const t={pre_competition:chalk.yellow,demo:chalk.blue,live:chalk.green,finished:chalk.red,unknown:chalk.gray},i=e.competitionState||"unknown",a=t[i]||chalk.gray;console.log(` ${chalk.gray("State:")} ${chalk.bold(a({pre_competition:"PRE-COMPETITION",demo:"DEMO",live:"LIVE",finished:"FINISHED",unknown:"UNKNOWN"}[i]||i))}`);try{const e=await o.testConnection();u("User",chalk.white.bold(e.name)),u("Score",chalk.yellow.bold(String(e.score||0))),u("Rank",chalk.cyan(e.place||"N/A"))}catch{u("User",e.userName||"Unknown")}console.log(),console.log(` ${chalk.gray("Hint Budget:")}`);const c=n(),l=(e,o,t)=>{const n=Math.round(e/o*20),s=20-n;return t("█".repeat(n))+chalk.gray("░".repeat(s))+` ${e}/${o}`};if(console.log(` A ${l(c.a,50,chalk.green)}`),console.log(` B ${l(c.b,10,chalk.yellow)}`),console.log(` C ${l(c.c,2,chalk.red)}`),console.log(` ${chalk.gray("Tokens")} ${l(c.tokenCap-c.tokensUsed,c.tokenCap,chalk.cyan)}`),e.competitionEndsAt){console.log();const o=new Date(e.competitionEndsAt);new Date<o?u("Time Remaining",chalk.yellow.bold(y(o))):u("Time",chalk.red("Competition ended"))}console.log()}),b.command("time").description("Show competition countdown").action(()=>{s("ctf time");const e=o();if(!e.competitionStartsAt&&!e.competitionEndsAt)return d("Competition times not configured."),void d("They will be set when the competition server provides timing information.");const t=new Date,n=e.competitionStartsAt?new Date(e.competitionStartsAt):null,i=e.competitionEndsAt?new Date(e.competitionEndsAt):null;if(p("Competition Timer"),n&&t<n){console.log(chalk.yellow(" Competition has not started yet.")),console.log();const e=()=>{process.stdout.write(`\r ${chalk.bold("Starts in:")} ${y(n)} `)};e();const o=setInterval(()=>{if(new Date>=n)return clearInterval(o),console.log(),void c("Competition has started! Good luck!");e()},1e3);process.on("SIGINT",()=>{clearInterval(o),console.log(),process.exit(0)})}else if(i&&t<i){console.log(chalk.green(" Competition is LIVE")),console.log();const e=()=>{process.stdout.write(`\r ${chalk.bold("Time remaining:")} ${y(i)} `)};e();const o=setInterval(()=>{if(new Date>=i)return clearInterval(o),console.log(),void r("Competition has ended!");e()},1e3);process.on("SIGINT",()=>{clearInterval(o),console.log(),process.exit(0)})}else console.log(chalk.red(" Competition has ended."))})}
|