icoa-cli 1.0.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.
Files changed (77) hide show
  1. package/dist/commands/connect.d.ts +2 -0
  2. package/dist/commands/connect.js +66 -0
  3. package/dist/commands/ctf.d.ts +2 -0
  4. package/dist/commands/ctf.js +472 -0
  5. package/dist/commands/files.d.ts +2 -0
  6. package/dist/commands/files.js +52 -0
  7. package/dist/commands/hint.d.ts +2 -0
  8. package/dist/commands/hint.js +107 -0
  9. package/dist/commands/lang.d.ts +2 -0
  10. package/dist/commands/lang.js +42 -0
  11. package/dist/commands/log.d.ts +2 -0
  12. package/dist/commands/log.js +36 -0
  13. package/dist/commands/note.d.ts +2 -0
  14. package/dist/commands/note.js +32 -0
  15. package/dist/commands/ref.d.ts +2 -0
  16. package/dist/commands/ref.js +63 -0
  17. package/dist/commands/setup.d.ts +2 -0
  18. package/dist/commands/setup.js +88 -0
  19. package/dist/commands/shell.d.ts +2 -0
  20. package/dist/commands/shell.js +55 -0
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.js +78 -0
  23. package/dist/lib/budget.d.ts +8 -0
  24. package/dist/lib/budget.js +29 -0
  25. package/dist/lib/config.d.ts +7 -0
  26. package/dist/lib/config.js +60 -0
  27. package/dist/lib/ctfd-client.d.ts +22 -0
  28. package/dist/lib/ctfd-client.js +161 -0
  29. package/dist/lib/gemini.d.ts +7 -0
  30. package/dist/lib/gemini.js +108 -0
  31. package/dist/lib/logger.d.ts +6 -0
  32. package/dist/lib/logger.js +59 -0
  33. package/dist/lib/translation.d.ts +1 -0
  34. package/dist/lib/translation.js +40 -0
  35. package/dist/lib/ui.d.ts +10 -0
  36. package/dist/lib/ui.js +59 -0
  37. package/dist/types/index.d.ts +125 -0
  38. package/dist/types/index.js +29 -0
  39. package/package.json +43 -0
  40. package/refs/ROPgadget.txt +67 -0
  41. package/refs/base64.txt +63 -0
  42. package/refs/bash.txt +79 -0
  43. package/refs/binwalk.txt +43 -0
  44. package/refs/bs4.txt +61 -0
  45. package/refs/checksec.txt +57 -0
  46. package/refs/curl.txt +73 -0
  47. package/refs/cyberchef.txt +78 -0
  48. package/refs/exiftool.txt +50 -0
  49. package/refs/ffuf.txt +73 -0
  50. package/refs/gcc.txt +66 -0
  51. package/refs/gdb.txt +83 -0
  52. package/refs/hashcat.txt +64 -0
  53. package/refs/hint.txt +42 -0
  54. package/refs/icoa.txt +36 -0
  55. package/refs/john.txt +74 -0
  56. package/refs/linux.txt +58 -0
  57. package/refs/nc.txt +64 -0
  58. package/refs/nmap.txt +57 -0
  59. package/refs/numpy.txt +59 -0
  60. package/refs/openssl.txt +75 -0
  61. package/refs/pillow.txt +67 -0
  62. package/refs/pwntools.txt +79 -0
  63. package/refs/pycrypto.txt +77 -0
  64. package/refs/python.txt +94 -0
  65. package/refs/r2.txt +85 -0
  66. package/refs/regex.txt +73 -0
  67. package/refs/requests.txt +83 -0
  68. package/refs/rules.txt +28 -0
  69. package/refs/scapy.txt +80 -0
  70. package/refs/sqlmap.txt +69 -0
  71. package/refs/steghide.txt +71 -0
  72. package/refs/struct.txt +61 -0
  73. package/refs/sympy.txt +77 -0
  74. package/refs/tshark.txt +65 -0
  75. package/refs/vim.txt +74 -0
  76. package/refs/volatility.txt +41 -0
  77. package/refs/z3.txt +78 -0
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerConnectCommand(program: Command): void;
@@ -0,0 +1,66 @@
1
+ import { execSync } from 'node:child_process';
2
+ import chalk from 'chalk';
3
+ import { CTFdClient } from '../lib/ctfd-client.js';
4
+ import { getConfig, isConnected } from '../lib/config.js';
5
+ import { logCommand } from '../lib/logger.js';
6
+ import { printError, printInfo, printSuccess, createSpinner } from '../lib/ui.js';
7
+ export function registerConnectCommand(program) {
8
+ program
9
+ .command('connect <id>')
10
+ .description('Connect to challenge remote target')
11
+ .action(async (id) => {
12
+ logCommand(`connect ${id}`);
13
+ const config = getConfig();
14
+ if (!isConnected()) {
15
+ printError('Not connected. Run: icoa ctf join <url>');
16
+ return;
17
+ }
18
+ const client = new CTFdClient(config.ctfdUrl, config.token);
19
+ const spinner = createSpinner('Fetching connection info...');
20
+ spinner.start();
21
+ try {
22
+ const challenge = await client.getChallenge(parseInt(id));
23
+ spinner.stop();
24
+ if (!challenge.connection_info) {
25
+ printInfo('This challenge has no connection information.');
26
+ return;
27
+ }
28
+ const connInfo = challenge.connection_info.trim();
29
+ printInfo(`Connection: ${chalk.white(connInfo)}`);
30
+ console.log();
31
+ // Parse connection info and execute
32
+ if (connInfo.match(/^nc\s+/i) || connInfo.match(/^ncat\s+/i)) {
33
+ // netcat connection
34
+ printSuccess('Connecting...');
35
+ execSync(connInfo, { stdio: 'inherit' });
36
+ }
37
+ else if (connInfo.match(/^ssh\s+/i)) {
38
+ // SSH connection
39
+ printSuccess('Connecting via SSH...');
40
+ execSync(connInfo, { stdio: 'inherit' });
41
+ }
42
+ else if (connInfo.includes(':')) {
43
+ // host:port format
44
+ const [host, port] = connInfo.split(':');
45
+ printSuccess(`Connecting to ${host}:${port}...`);
46
+ execSync(`nc ${host} ${port}`, { stdio: 'inherit' });
47
+ }
48
+ else {
49
+ printInfo('Could not auto-detect connection type.');
50
+ printInfo(`Connection info: ${connInfo}`);
51
+ printInfo('Connect manually using the information above.');
52
+ }
53
+ }
54
+ catch (err) {
55
+ if (err.status !== undefined) {
56
+ // Process exited (normal for nc/ssh)
57
+ console.log();
58
+ printInfo('Connection closed.');
59
+ }
60
+ else {
61
+ spinner.fail('Failed to connect');
62
+ printError(err.message);
63
+ }
64
+ }
65
+ });
66
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerCtfCommands(program: Command): void;
@@ -0,0 +1,472 @@
1
+ import chalk from 'chalk';
2
+ import { input, select } from '@inquirer/prompts';
3
+ import { CTFdClient } from '../lib/ctfd-client.js';
4
+ import { getConfig, saveConfig, getBudget } from '../lib/config.js';
5
+ import { logCommand, logSubmission } from '../lib/logger.js';
6
+ import { getTranslation } from '../lib/translation.js';
7
+ import { printSuccess, printError, printWarning, printInfo, printTable, printMarkdown, printHeader, printKeyValue, createSpinner, formatCountdown, } from '../lib/ui.js';
8
+ function requireConnection() {
9
+ const config = getConfig();
10
+ if (!config.ctfdUrl || !config.token) {
11
+ printError('Not connected to CTFd. Run: icoa ctf join <url>');
12
+ process.exit(1);
13
+ }
14
+ return { config, client: new CTFdClient(config.ctfdUrl, config.token) };
15
+ }
16
+ export function registerCtfCommands(program) {
17
+ const ctf = program.command('ctf').description('Competition commands');
18
+ // ─── icoa ctf join <url> ───
19
+ ctf
20
+ .command('join <url>')
21
+ .description('Connect to a CTFd instance')
22
+ .action(async (url) => {
23
+ logCommand(`ctf join ${url}`);
24
+ console.log();
25
+ printInfo(`Connecting to ${chalk.bold(url)}`);
26
+ const authMethod = await select({
27
+ message: 'Authentication method:',
28
+ choices: [
29
+ { name: 'Access Token (from CTFd Settings)', value: 'token' },
30
+ { name: 'Username & Password', value: 'login' },
31
+ ],
32
+ });
33
+ let token;
34
+ if (authMethod === 'token') {
35
+ token = await input({ message: 'Enter your CTFd Access Token:' });
36
+ }
37
+ else {
38
+ const username = await input({ message: 'Username:' });
39
+ const password = await input({ message: 'Password:' });
40
+ const spinner = createSpinner('Logging in...');
41
+ spinner.start();
42
+ try {
43
+ const client = new CTFdClient(url, '');
44
+ token = await client.loginWithCredentials(username, password);
45
+ spinner.succeed('Login successful');
46
+ }
47
+ catch (err) {
48
+ spinner.fail('Login failed');
49
+ printError(err.message);
50
+ return;
51
+ }
52
+ }
53
+ // Test connection
54
+ const spinner = createSpinner('Testing connection...');
55
+ spinner.start();
56
+ try {
57
+ const client = new CTFdClient(url, token);
58
+ const user = await client.testConnection();
59
+ spinner.succeed('Connected successfully');
60
+ console.log();
61
+ printKeyValue('User', user.name);
62
+ printKeyValue('Score', String(user.score || 0));
63
+ if (user.team_id) {
64
+ printKeyValue('Team ID', String(user.team_id));
65
+ }
66
+ // Fetch competition metadata (times, mode)
67
+ try {
68
+ const meta = await client.getCompetitionMeta();
69
+ const configUpdate = {
70
+ ctfdUrl: url,
71
+ token: token,
72
+ userId: user.id,
73
+ userName: user.name,
74
+ teamId: user.team_id,
75
+ };
76
+ if (meta.start) {
77
+ configUpdate.competitionStartsAt = new Date(meta.start * 1000).toISOString();
78
+ printKeyValue('Starts', new Date(meta.start * 1000).toLocaleString());
79
+ }
80
+ if (meta.end) {
81
+ configUpdate.competitionEndsAt = new Date(meta.end * 1000).toISOString();
82
+ printKeyValue('Ends', new Date(meta.end * 1000).toLocaleString());
83
+ }
84
+ // Determine competition state
85
+ const now = Date.now() / 1000;
86
+ if (meta.start && now < meta.start) {
87
+ configUpdate.competitionState = 'pre_competition';
88
+ }
89
+ else if (meta.end && now > meta.end) {
90
+ configUpdate.competitionState = 'finished';
91
+ }
92
+ else {
93
+ configUpdate.competitionState = 'live';
94
+ }
95
+ printKeyValue('Status', configUpdate.competitionState);
96
+ saveConfig(configUpdate);
97
+ }
98
+ catch {
99
+ // Fallback: save without competition times
100
+ saveConfig({
101
+ ctfdUrl: url,
102
+ token: token,
103
+ userId: user.id,
104
+ userName: user.name,
105
+ teamId: user.team_id,
106
+ });
107
+ }
108
+ console.log();
109
+ printSuccess('Connection saved. You are ready to compete!');
110
+ }
111
+ catch (err) {
112
+ spinner.fail('Connection failed');
113
+ printError(err.message);
114
+ }
115
+ });
116
+ // ─── icoa ctf activate <code> ───
117
+ ctf
118
+ .command('activate <code>')
119
+ .description('Validate competition code')
120
+ .action(async (code) => {
121
+ logCommand(`ctf activate ${code}`);
122
+ const { client } = requireConnection();
123
+ const spinner = createSpinner('Validating competition code...');
124
+ spinner.start();
125
+ try {
126
+ // Try custom ICOA endpoint first
127
+ const res = await fetch(getConfig().ctfdUrl + '/api/v1/icoa/activate', {
128
+ method: 'POST',
129
+ headers: {
130
+ Authorization: `Token ${getConfig().token}`,
131
+ 'Content-Type': 'application/json',
132
+ },
133
+ body: JSON.stringify({ code }),
134
+ });
135
+ if (res.ok) {
136
+ const data = (await res.json());
137
+ spinner.succeed('Competition code validated');
138
+ saveConfig({
139
+ competitionCode: code,
140
+ teamId: data.data?.team_id || getConfig().teamId,
141
+ competitionState: 'live',
142
+ });
143
+ printSuccess(`Activated: ${code}`);
144
+ }
145
+ else {
146
+ // Fallback: just store the code locally
147
+ spinner.succeed('Competition code saved');
148
+ saveConfig({ competitionCode: code });
149
+ printInfo('Code stored locally. Server validation will be attempted during competition.');
150
+ }
151
+ }
152
+ catch {
153
+ // Server doesn't have custom endpoint, store locally
154
+ spinner.succeed('Competition code saved');
155
+ saveConfig({ competitionCode: code });
156
+ printInfo('Code stored locally.');
157
+ }
158
+ });
159
+ // ─── icoa ctf challenges ───
160
+ ctf
161
+ .command('challenges')
162
+ .description('List all challenges')
163
+ .action(async () => {
164
+ logCommand('ctf challenges');
165
+ const { client } = requireConnection();
166
+ const spinner = createSpinner('Loading challenges...');
167
+ spinner.start();
168
+ try {
169
+ const challenges = await client.getChallenges();
170
+ spinner.stop();
171
+ if (!challenges || challenges.length === 0) {
172
+ printInfo('No challenges available yet.');
173
+ return;
174
+ }
175
+ // Group by category
176
+ const byCategory = new Map();
177
+ for (const c of challenges) {
178
+ const cat = c.category || 'Uncategorized';
179
+ if (!byCategory.has(cat))
180
+ byCategory.set(cat, []);
181
+ byCategory.get(cat).push(c);
182
+ }
183
+ const solved = challenges.filter((c) => c.solved_by_me).length;
184
+ printHeader(`Challenges (${solved}/${challenges.length} solved)`);
185
+ // Sort categories and display
186
+ const rows = [...byCategory.entries()]
187
+ .sort(([a], [b]) => a.localeCompare(b))
188
+ .flatMap(([category, challs]) => {
189
+ const catRow = [chalk.cyan.bold(`── ${category} ──`), '', '', '', ''];
190
+ const challRows = challs
191
+ .sort((a, b) => a.value - b.value)
192
+ .map((c) => [
193
+ String(c.id),
194
+ c.solved_by_me ? chalk.gray.strikethrough(c.name) : c.name,
195
+ chalk.gray(c.category),
196
+ c.solved_by_me ? chalk.gray(String(c.value)) : chalk.yellow(String(c.value)),
197
+ c.solved_by_me ? chalk.green('✓') : chalk.gray('○'),
198
+ ]);
199
+ return [catRow, ...challRows];
200
+ });
201
+ printTable(['#', 'Name', 'Category', 'Points', 'Solved'], rows);
202
+ console.log(chalk.gray(` ${challenges.length} challenges, ${solved} solved`));
203
+ }
204
+ catch (err) {
205
+ spinner.fail('Failed to load challenges');
206
+ printError(err.message);
207
+ }
208
+ });
209
+ // ─── icoa ctf open <id> ───
210
+ ctf
211
+ .command('open <id>')
212
+ .description('View challenge details')
213
+ .action(async (id) => {
214
+ logCommand(`ctf open ${id}`);
215
+ const { config, client } = requireConnection();
216
+ const spinner = createSpinner('Loading challenge...');
217
+ spinner.start();
218
+ try {
219
+ const challenge = await client.getChallenge(parseInt(id));
220
+ spinner.stop();
221
+ // Save current challenge context for hints
222
+ saveConfig({
223
+ currentChallengeId: challenge.id,
224
+ currentChallengeName: challenge.name,
225
+ currentChallengeCategory: challenge.category,
226
+ });
227
+ printHeader(`${challenge.name} [${challenge.category}]`);
228
+ printKeyValue('Points', String(challenge.value));
229
+ printKeyValue('Solves', String(challenge.solves));
230
+ if (challenge.connection_info) {
231
+ printKeyValue('Connection', challenge.connection_info);
232
+ }
233
+ console.log();
234
+ // Display description (with optional translation)
235
+ let description = challenge.description;
236
+ if (config.language !== 'en') {
237
+ try {
238
+ description = await getTranslation(description, challenge.id, config.language);
239
+ }
240
+ catch {
241
+ // Translation failed, use English
242
+ }
243
+ }
244
+ printMarkdown(description);
245
+ // Show files
246
+ if (challenge.files && challenge.files.length > 0) {
247
+ console.log();
248
+ printInfo(`Files (${challenge.files.length}): use ${chalk.white(`icoa files ${id}`)} to download`);
249
+ }
250
+ // Show hints
251
+ if (challenge.hints && challenge.hints.length > 0) {
252
+ printInfo(`CTFd Hints available: ${challenge.hints.length}`);
253
+ }
254
+ // Show connection info
255
+ if (challenge.connection_info) {
256
+ console.log();
257
+ printInfo(`Quick connect: ${chalk.white(`icoa connect ${id}`)}`);
258
+ }
259
+ // Show helpful next actions
260
+ console.log();
261
+ console.log(chalk.gray(` Next: icoa hint "how to approach this?" | icoa ctf submit ${id} "icoa{flag}"`));
262
+ }
263
+ catch (err) {
264
+ spinner.fail('Failed to load challenge');
265
+ printError(err.message);
266
+ }
267
+ });
268
+ // ─── icoa ctf submit <id> <flag> ───
269
+ ctf
270
+ .command('submit <id> <flag>')
271
+ .description('Submit a flag')
272
+ .action(async (id, flag) => {
273
+ logCommand(`ctf submit ${id}`);
274
+ logSubmission(parseInt(id), flag);
275
+ const { client } = requireConnection();
276
+ const spinner = createSpinner('Submitting flag...');
277
+ spinner.start();
278
+ try {
279
+ const result = await client.submitFlag(parseInt(id), flag);
280
+ spinner.stop();
281
+ switch (result.status) {
282
+ case 'correct':
283
+ console.log();
284
+ console.log(chalk.green.bold(' 🎉 Correct! ') + chalk.white(result.message));
285
+ console.log();
286
+ break;
287
+ case 'incorrect':
288
+ printError('Incorrect. ' + result.message);
289
+ break;
290
+ case 'already_solved':
291
+ printWarning('Already solved.');
292
+ break;
293
+ case 'paused':
294
+ printWarning('Competition is paused.');
295
+ break;
296
+ case 'ratelimited':
297
+ printWarning('Rate limited. Please wait before trying again.');
298
+ break;
299
+ default:
300
+ printInfo(result.message || 'Unknown response');
301
+ }
302
+ }
303
+ catch (err) {
304
+ spinner.fail('Submission failed');
305
+ printError(err.message);
306
+ }
307
+ });
308
+ // ─── icoa ctf scoreboard ───
309
+ ctf
310
+ .command('scoreboard [top]')
311
+ .description('View scoreboard (optional: top N)')
312
+ .action(async (top) => {
313
+ logCommand('ctf scoreboard');
314
+ const { config, client } = requireConnection();
315
+ const spinner = createSpinner('Loading scoreboard...');
316
+ spinner.start();
317
+ try {
318
+ const scoreboard = await client.getScoreboard();
319
+ spinner.stop();
320
+ if (!scoreboard || scoreboard.length === 0) {
321
+ printInfo('Scoreboard is empty.');
322
+ return;
323
+ }
324
+ printHeader('Scoreboard');
325
+ const rows = scoreboard.map((entry) => {
326
+ const isMyTeam = entry.account_id === config.teamId;
327
+ const row = [
328
+ String(entry.pos),
329
+ entry.name,
330
+ String(entry.score),
331
+ ];
332
+ if (isMyTeam) {
333
+ return row.map((cell) => chalk.yellow.bold(cell));
334
+ }
335
+ return row;
336
+ });
337
+ printTable(['Rank', 'Team', 'Score'], rows);
338
+ }
339
+ catch (err) {
340
+ spinner.fail('Failed to load scoreboard');
341
+ printError(err.message);
342
+ }
343
+ });
344
+ // ─── icoa ctf status ───
345
+ ctf
346
+ .command('status')
347
+ .description('Show competition status')
348
+ .action(async () => {
349
+ logCommand('ctf status');
350
+ const { config, client } = requireConnection();
351
+ printHeader('Competition Status');
352
+ // Competition state
353
+ const stateColors = {
354
+ pre_competition: chalk.yellow,
355
+ demo: chalk.blue,
356
+ live: chalk.green,
357
+ finished: chalk.red,
358
+ unknown: chalk.gray,
359
+ };
360
+ const stateLabels = {
361
+ pre_competition: 'PRE-COMPETITION',
362
+ demo: 'DEMO',
363
+ live: 'LIVE',
364
+ finished: 'FINISHED',
365
+ unknown: 'UNKNOWN',
366
+ };
367
+ const state = config.competitionState || 'unknown';
368
+ const colorFn = stateColors[state] || chalk.gray;
369
+ console.log(` ${chalk.gray('State:')} ${chalk.bold(colorFn(stateLabels[state] || state))}`);
370
+ // User info
371
+ try {
372
+ const user = await client.testConnection();
373
+ printKeyValue('User', chalk.white.bold(user.name));
374
+ printKeyValue('Score', chalk.yellow.bold(String(user.score || 0)));
375
+ printKeyValue('Rank', chalk.cyan(user.place || 'N/A'));
376
+ }
377
+ catch {
378
+ printKeyValue('User', config.userName || 'Unknown');
379
+ }
380
+ // Hint budget with visual bars
381
+ console.log();
382
+ console.log(` ${chalk.gray('Hint Budget:')}`);
383
+ const budget = getBudget();
384
+ const bar = (used, total, color) => {
385
+ const width = 20;
386
+ const filled = Math.round((used / total) * width);
387
+ const empty = width - filled;
388
+ return color('█'.repeat(filled)) + chalk.gray('░'.repeat(empty)) + ` ${used}/${total}`;
389
+ };
390
+ console.log(` A ${bar(budget.a, 50, chalk.green)}`);
391
+ console.log(` B ${bar(budget.b, 10, chalk.yellow)}`);
392
+ console.log(` C ${bar(budget.c, 2, chalk.red)}`);
393
+ console.log(` ${chalk.gray('Tokens')} ${bar(budget.tokenCap - budget.tokensUsed, budget.tokenCap, chalk.cyan)}`);
394
+ // Time
395
+ if (config.competitionEndsAt) {
396
+ console.log();
397
+ const endsAt = new Date(config.competitionEndsAt);
398
+ const now = new Date();
399
+ if (now < endsAt) {
400
+ printKeyValue('Time Remaining', chalk.yellow.bold(formatCountdown(endsAt)));
401
+ }
402
+ else {
403
+ printKeyValue('Time', chalk.red('Competition ended'));
404
+ }
405
+ }
406
+ console.log();
407
+ });
408
+ // ─── icoa ctf time ───
409
+ ctf
410
+ .command('time')
411
+ .description('Show competition countdown')
412
+ .action(() => {
413
+ logCommand('ctf time');
414
+ const config = getConfig();
415
+ if (!config.competitionStartsAt && !config.competitionEndsAt) {
416
+ printInfo('Competition times not configured.');
417
+ printInfo('They will be set when the competition server provides timing information.');
418
+ return;
419
+ }
420
+ const now = new Date();
421
+ const startsAt = config.competitionStartsAt ? new Date(config.competitionStartsAt) : null;
422
+ const endsAt = config.competitionEndsAt ? new Date(config.competitionEndsAt) : null;
423
+ printHeader('Competition Timer');
424
+ if (startsAt && now < startsAt) {
425
+ console.log(chalk.yellow(' Competition has not started yet.'));
426
+ console.log();
427
+ const update = () => {
428
+ process.stdout.write(`\r ${chalk.bold('Starts in:')} ${formatCountdown(startsAt)} `);
429
+ };
430
+ update();
431
+ const interval = setInterval(() => {
432
+ if (new Date() >= startsAt) {
433
+ clearInterval(interval);
434
+ console.log();
435
+ printSuccess('Competition has started! Good luck!');
436
+ return;
437
+ }
438
+ update();
439
+ }, 1000);
440
+ process.on('SIGINT', () => {
441
+ clearInterval(interval);
442
+ console.log();
443
+ process.exit(0);
444
+ });
445
+ }
446
+ else if (endsAt && now < endsAt) {
447
+ console.log(chalk.green(' Competition is LIVE'));
448
+ console.log();
449
+ const update = () => {
450
+ process.stdout.write(`\r ${chalk.bold('Time remaining:')} ${formatCountdown(endsAt)} `);
451
+ };
452
+ update();
453
+ const interval = setInterval(() => {
454
+ if (new Date() >= endsAt) {
455
+ clearInterval(interval);
456
+ console.log();
457
+ printWarning('Competition has ended!');
458
+ return;
459
+ }
460
+ update();
461
+ }, 1000);
462
+ process.on('SIGINT', () => {
463
+ clearInterval(interval);
464
+ console.log();
465
+ process.exit(0);
466
+ });
467
+ }
468
+ else {
469
+ console.log(chalk.red(' Competition has ended.'));
470
+ }
471
+ });
472
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerFilesCommand(program: Command): void;
@@ -0,0 +1,52 @@
1
+ import { join } from 'node:path';
2
+ import { homedir } from 'node:os';
3
+ import chalk from 'chalk';
4
+ import { CTFdClient } from '../lib/ctfd-client.js';
5
+ import { getConfig, isConnected } from '../lib/config.js';
6
+ import { logCommand } from '../lib/logger.js';
7
+ import { printError, createSpinner } from '../lib/ui.js';
8
+ export function registerFilesCommand(program) {
9
+ program
10
+ .command('files <id>')
11
+ .description('Download challenge files')
12
+ .action(async (id) => {
13
+ logCommand(`files ${id}`);
14
+ const config = getConfig();
15
+ if (!isConnected()) {
16
+ printError('Not connected. Run: icoa ctf join <url>');
17
+ return;
18
+ }
19
+ const client = new CTFdClient(config.ctfdUrl, config.token);
20
+ const destDir = join(homedir(), 'icoa-challenges', id);
21
+ const spinner = createSpinner('Fetching challenge files...');
22
+ spinner.start();
23
+ try {
24
+ const files = await client.getChallengeFiles(parseInt(id));
25
+ if (!files || files.length === 0) {
26
+ spinner.info('No files attached to this challenge.');
27
+ return;
28
+ }
29
+ spinner.text = `Downloading ${files.length} file(s)...`;
30
+ const downloaded = [];
31
+ for (const filePath of files) {
32
+ try {
33
+ const dest = await client.downloadFile(filePath, destDir);
34
+ downloaded.push(dest);
35
+ }
36
+ catch (err) {
37
+ spinner.warn(`Failed to download: ${filePath}`);
38
+ }
39
+ }
40
+ spinner.succeed(`Downloaded ${downloaded.length} file(s)`);
41
+ console.log(chalk.gray(` Location: ${destDir}`));
42
+ for (const f of downloaded) {
43
+ console.log(chalk.gray(` → ${f.split('/').pop()}`));
44
+ }
45
+ console.log();
46
+ }
47
+ catch (err) {
48
+ spinner.fail('Failed to download files');
49
+ printError(err.message);
50
+ }
51
+ });
52
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerHintCommands(program: Command): void;