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.
Files changed (45) hide show
  1. package/dist/commands/ai4ctf.js +1 -700
  2. package/dist/commands/connect.js +1 -66
  3. package/dist/commands/ctf.js +1 -620
  4. package/dist/commands/ctf4ai-demo.js +1 -525
  5. package/dist/commands/env.js +1 -738
  6. package/dist/commands/exam.js +1 -2353
  7. package/dist/commands/files.js +1 -52
  8. package/dist/commands/hint.js +1 -119
  9. package/dist/commands/lang.js +1 -155
  10. package/dist/commands/log.js +1 -165
  11. package/dist/commands/note.js +1 -40
  12. package/dist/commands/ref.js +1 -68
  13. package/dist/commands/setup.js +1 -122
  14. package/dist/commands/shell.js +1 -55
  15. package/dist/commands/theme.js +1 -50
  16. package/dist/index.js +1 -225
  17. package/dist/lib/access.js +1 -246
  18. package/dist/lib/budget.js +1 -42
  19. package/dist/lib/colors.js +1 -21
  20. package/dist/lib/config.js +1 -60
  21. package/dist/lib/ctfd-client.js +1 -274
  22. package/dist/lib/demo-exam.js +1 -249
  23. package/dist/lib/demo-flags.js +1 -27
  24. package/dist/lib/demo-stats.js +1 -65
  25. package/dist/lib/exam-client.js +1 -57
  26. package/dist/lib/exam-setup.js +1 -23
  27. package/dist/lib/exam-state.js +1 -112
  28. package/dist/lib/gemini.js +1 -235
  29. package/dist/lib/i18n.js +1 -273
  30. package/dist/lib/log-sync.js +1 -110
  31. package/dist/lib/logger.js +1 -59
  32. package/dist/lib/paper-upgrade.js +1 -117
  33. package/dist/lib/platform.js +1 -86
  34. package/dist/lib/sandbox.js +1 -93
  35. package/dist/lib/terminal.js +1 -49
  36. package/dist/lib/theme.js +1 -108
  37. package/dist/lib/translation.js +1 -66
  38. package/dist/lib/ui.js +1 -80
  39. package/dist/lib/update-check.js +1 -102
  40. package/dist/postinstall.js +1 -48
  41. package/dist/repl.js +1 -1281
  42. package/dist/types/index.d.ts +1 -1
  43. package/dist/types/index.js +1 -38
  44. package/package.json +6 -2
  45. package/translations/sw/i18n-snippet.ts +1 -0
@@ -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."))})}