apigraveyard 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.
package/src/display.js ADDED
@@ -0,0 +1,534 @@
1
+ /**
2
+ * Display Module
3
+ * Terminal UI display functions for APIgraveyard
4
+ * Handles colored output, tables, and formatted messages
5
+ */
6
+
7
+ import chalk from 'chalk';
8
+ import Table from 'cli-table3';
9
+ import ora from 'ora';
10
+
11
+ /**
12
+ * Status emoji mappings
13
+ * @constant {Object.<string, string>}
14
+ */
15
+ const STATUS_EMOJI = {
16
+ VALID: '✅',
17
+ INVALID: '❌',
18
+ EXPIRED: '⏰',
19
+ RATE_LIMITED: '⚠️',
20
+ ERROR: '💥',
21
+ UNTESTED: '❓'
22
+ };
23
+
24
+ /**
25
+ * Status color functions
26
+ * @constant {Object.<string, Function>}
27
+ */
28
+ const STATUS_COLORS = {
29
+ VALID: chalk.green,
30
+ INVALID: chalk.red,
31
+ EXPIRED: chalk.red,
32
+ RATE_LIMITED: chalk.yellow,
33
+ ERROR: chalk.gray,
34
+ UNTESTED: chalk.yellow
35
+ };
36
+
37
+ /**
38
+ * Display the APIgraveyard ASCII banner
39
+ *
40
+ * @example
41
+ * showBanner();
42
+ */
43
+ export function showBanner() {
44
+ const banner = `
45
+ ${chalk.gray(' ___ ____ ____ __')}
46
+ ${chalk.gray(' / | / __ \\/ _/___ __________ __ _____ __ ______ __________/ /')}
47
+ ${chalk.gray(' / /| | / /_/ // // __ `/ ___/ __ `/ | / / _ \\/ / / / __ `/ ___/ __ /')}
48
+ ${chalk.gray(' / ___ |/ ____// // /_/ / / / /_/ /| |/ / __/ /_/ / /_/ / / / /_/ /')}
49
+ ${chalk.gray('/_/ |_/_/ /___/\\__, /_/ \\__,_/ |___/\\___/\\__, /\\__,_/_/ \\__,_/')}
50
+ ${chalk.gray(' /____/ /____/')}
51
+ ${chalk.dim(' 🪦 RIP APIs 🪦')}
52
+ `;
53
+ console.log(banner);
54
+ }
55
+
56
+ /**
57
+ * Creates a loading spinner with custom text
58
+ *
59
+ * @param {string} text - Text to display next to spinner
60
+ * @returns {import('ora').Ora} - Ora spinner instance
61
+ *
62
+ * @example
63
+ * const spinner = createSpinner('Scanning files...');
64
+ * spinner.start();
65
+ * // ... do work
66
+ * spinner.succeed('Scan complete!');
67
+ */
68
+ export function createSpinner(text) {
69
+ return ora({
70
+ text,
71
+ spinner: 'dots'
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Formats a timestamp for display
77
+ *
78
+ * @param {string|Date} timestamp - ISO timestamp or Date object
79
+ * @returns {string} - Formatted date string
80
+ */
81
+ function formatTimestamp(timestamp) {
82
+ if (!timestamp) return chalk.dim('Never');
83
+ const date = new Date(timestamp);
84
+ return date.toLocaleString();
85
+ }
86
+
87
+ /**
88
+ * Gets colored status text with emoji
89
+ *
90
+ * @param {string} status - Status string (VALID, INVALID, etc.)
91
+ * @returns {string} - Colored status with emoji
92
+ */
93
+ function getStatusDisplay(status) {
94
+ const emoji = STATUS_EMOJI[status] || STATUS_EMOJI.UNTESTED;
95
+ const colorFn = STATUS_COLORS[status] || STATUS_COLORS.UNTESTED;
96
+ return `${emoji} ${colorFn(status || 'UNTESTED')}`;
97
+ }
98
+
99
+ /**
100
+ * Truncates a string to specified length with ellipsis
101
+ *
102
+ * @param {string} str - String to truncate
103
+ * @param {number} maxLength - Maximum length
104
+ * @returns {string} - Truncated string
105
+ */
106
+ function truncate(str, maxLength) {
107
+ if (!str || str.length <= maxLength) return str || '';
108
+ return str.substring(0, maxLength - 3) + '...';
109
+ }
110
+
111
+ /**
112
+ * Displays scan results in a formatted table
113
+ * Shows all found API keys with their locations and status
114
+ *
115
+ * @param {Object} scanResults - Results from scanner.scanDirectory()
116
+ * @param {number} scanResults.totalFiles - Number of files scanned
117
+ * @param {Array} scanResults.keysFound - Array of found keys
118
+ *
119
+ * @example
120
+ * const results = await scanDirectory('./src');
121
+ * displayScanResults(results);
122
+ */
123
+ export function displayScanResults(scanResults) {
124
+ const { totalFiles, keysFound } = scanResults;
125
+
126
+ console.log('\n' + chalk.bold.cyan('📊 Scan Results'));
127
+ console.log(chalk.dim('─'.repeat(60)));
128
+
129
+ if (keysFound.length === 0) {
130
+ console.log(chalk.green('\n✅ No API keys found in the scanned files.\n'));
131
+ console.log(chalk.dim(`Scanned ${totalFiles} files.`));
132
+ return;
133
+ }
134
+
135
+ // Create table
136
+ const table = new Table({
137
+ head: [
138
+ chalk.cyan.bold('Service'),
139
+ chalk.cyan.bold('Key'),
140
+ chalk.cyan.bold('File'),
141
+ chalk.cyan.bold('Line'),
142
+ chalk.cyan.bold('Status')
143
+ ],
144
+ colWidths: [15, 25, 30, 8, 15],
145
+ style: {
146
+ head: [],
147
+ border: ['gray']
148
+ },
149
+ wordWrap: true
150
+ });
151
+
152
+ // Add rows
153
+ keysFound.forEach(key => {
154
+ const status = key.status || 'UNTESTED';
155
+ table.push([
156
+ chalk.white(key.service),
157
+ chalk.yellow(key.key),
158
+ chalk.dim(truncate(key.filePath, 28)),
159
+ chalk.dim(key.lineNumber.toString()),
160
+ getStatusDisplay(status)
161
+ ]);
162
+ });
163
+
164
+ console.log(table.toString());
165
+
166
+ // Summary section
167
+ console.log('\n' + chalk.bold.cyan('📈 Summary'));
168
+ console.log(chalk.dim('─'.repeat(60)));
169
+
170
+ // Service breakdown
171
+ const serviceCount = {};
172
+ keysFound.forEach(key => {
173
+ serviceCount[key.service] = (serviceCount[key.service] || 0) + 1;
174
+ });
175
+
176
+ console.log(chalk.bold(`\nTotal keys found: ${chalk.yellow(keysFound.length)}`));
177
+ console.log(chalk.bold(`Files scanned: ${chalk.blue(totalFiles)}`));
178
+
179
+ console.log(chalk.bold('\nBy service:'));
180
+ Object.entries(serviceCount)
181
+ .sort((a, b) => b[1] - a[1])
182
+ .forEach(([service, count]) => {
183
+ console.log(` ${chalk.white(service)}: ${chalk.yellow(count)}`);
184
+ });
185
+
186
+ console.log('');
187
+ }
188
+
189
+ /**
190
+ * Displays test results in a formatted table
191
+ * Shows validation status for each tested key
192
+ *
193
+ * @param {Array} testResults - Results from tester.testKeys()
194
+ *
195
+ * @example
196
+ * const results = await testKeys(keysFound);
197
+ * displayTestResults(results);
198
+ */
199
+ export function displayTestResults(testResults) {
200
+ console.log('\n' + chalk.bold.cyan('🧪 Test Results'));
201
+ console.log(chalk.dim('─'.repeat(70)));
202
+
203
+ if (testResults.length === 0) {
204
+ console.log(chalk.yellow('\n⚠️ No keys to test.\n'));
205
+ return;
206
+ }
207
+
208
+ // Create table
209
+ const table = new Table({
210
+ head: [
211
+ chalk.cyan.bold('Service'),
212
+ chalk.cyan.bold('Key'),
213
+ chalk.cyan.bold('Status'),
214
+ chalk.cyan.bold('Details'),
215
+ chalk.cyan.bold('Last Tested')
216
+ ],
217
+ colWidths: [15, 22, 18, 20, 20],
218
+ style: {
219
+ head: [],
220
+ border: ['gray']
221
+ },
222
+ wordWrap: true
223
+ });
224
+
225
+ // Add rows
226
+ testResults.forEach(result => {
227
+ const quotaInfo = getQuotaDisplay(result.details);
228
+ const testedAt = formatTimestamp(result.details?.testedAt);
229
+
230
+ table.push([
231
+ chalk.white(result.service),
232
+ chalk.yellow(result.key),
233
+ getStatusDisplay(result.status),
234
+ chalk.dim(quotaInfo),
235
+ chalk.dim(truncate(testedAt, 18))
236
+ ]);
237
+ });
238
+
239
+ console.log(table.toString());
240
+
241
+ // Summary
242
+ const statusCounts = {};
243
+ testResults.forEach(r => {
244
+ statusCounts[r.status] = (statusCounts[r.status] || 0) + 1;
245
+ });
246
+
247
+ console.log('\n' + chalk.bold('Summary:'));
248
+ Object.entries(statusCounts).forEach(([status, count]) => {
249
+ const emoji = STATUS_EMOJI[status] || '❓';
250
+ const colorFn = STATUS_COLORS[status] || chalk.white;
251
+ console.log(` ${emoji} ${colorFn(status)}: ${count}`);
252
+ });
253
+
254
+ console.log('');
255
+ }
256
+
257
+ /**
258
+ * Gets quota/details display string from result details
259
+ *
260
+ * @param {Object} details - Test result details
261
+ * @returns {string} - Formatted quota info
262
+ */
263
+ function getQuotaDisplay(details) {
264
+ if (!details) return '-';
265
+
266
+ if (details.modelsCount !== undefined) {
267
+ return `${details.modelsCount} models`;
268
+ }
269
+ if (details.username) {
270
+ return `@${details.username}`;
271
+ }
272
+ if (details.rateLimit?.remaining !== undefined) {
273
+ return `Rate: ${details.rateLimit.remaining}`;
274
+ }
275
+ if (details.livemode !== undefined) {
276
+ return details.livemode ? 'Live mode' : 'Test mode';
277
+ }
278
+ if (details.note) {
279
+ return truncate(details.note, 18);
280
+ }
281
+
282
+ return '-';
283
+ }
284
+
285
+ /**
286
+ * Displays a list of tracked projects
287
+ * Shows project name, path, key count, and last scanned time
288
+ *
289
+ * @param {Array} projects - Array of project objects from database
290
+ *
291
+ * @example
292
+ * const projects = await getAllProjects();
293
+ * displayProjectList(projects);
294
+ */
295
+ export function displayProjectList(projects) {
296
+ console.log('\n' + chalk.bold.cyan('📁 Tracked Projects'));
297
+ console.log(chalk.dim('─'.repeat(70)));
298
+
299
+ if (projects.length === 0) {
300
+ console.log(chalk.yellow('\n⚠️ No projects tracked yet.'));
301
+ console.log(chalk.dim('Run "apigraveyard scan <directory>" to scan a project.\n'));
302
+ return;
303
+ }
304
+
305
+ projects.forEach((project, index) => {
306
+ const keyCount = project.keys?.length || 0;
307
+ const validKeys = project.keys?.filter(k => k.status === 'VALID').length || 0;
308
+ const scannedAt = formatTimestamp(project.scannedAt);
309
+
310
+ console.log('');
311
+ console.log(
312
+ chalk.bold.white(`${index + 1}. ${project.name}`) +
313
+ chalk.dim(` (${truncate(project.path, 40)})`)
314
+ );
315
+ console.log(
316
+ chalk.dim(' ') +
317
+ chalk.yellow(`🔑 ${keyCount} keys`) +
318
+ (validKeys > 0 ? chalk.green(` (${validKeys} valid)`) : '') +
319
+ chalk.dim(` • Last scanned: ${scannedAt}`)
320
+ );
321
+ });
322
+
323
+ console.log('\n' + chalk.dim('─'.repeat(70)));
324
+ console.log(chalk.dim(`Total: ${projects.length} project(s)\n`));
325
+ }
326
+
327
+ /**
328
+ * Displays detailed information about a single API key
329
+ *
330
+ * @param {Object} keyObject - Key object with details
331
+ * @param {string} keyObject.service - Service name
332
+ * @param {string} keyObject.key - Masked key
333
+ * @param {string} keyObject.fullKey - Full key (will be masked in display)
334
+ * @param {string} keyObject.status - Validation status
335
+ * @param {string} keyObject.filePath - File where key was found
336
+ * @param {number} keyObject.lineNumber - Line number in file
337
+ * @param {string} keyObject.lastTested - Last test timestamp
338
+ * @param {Object} keyObject.quotaInfo - Quota/rate limit info
339
+ *
340
+ * @example
341
+ * displayKeyDetails(project.keys[0]);
342
+ */
343
+ export function displayKeyDetails(keyObject) {
344
+ const status = keyObject.status || 'UNTESTED';
345
+ const colorFn = STATUS_COLORS[status] || chalk.white;
346
+
347
+ console.log('\n' + chalk.bold.cyan('🔑 Key Details'));
348
+ console.log(chalk.dim('─'.repeat(50)));
349
+
350
+ console.log(`
351
+ ${chalk.bold('Service:')} ${chalk.white(keyObject.service)}
352
+ ${chalk.bold('Key:')} ${chalk.yellow(keyObject.key)}
353
+ ${chalk.bold('Status:')} ${getStatusDisplay(status)}
354
+ ${chalk.bold('File:')} ${chalk.dim(keyObject.filePath)}
355
+ ${chalk.bold('Line:')} ${chalk.dim(keyObject.lineNumber)}:${chalk.dim(keyObject.column || 1)}
356
+ ${chalk.bold('Last Tested:')} ${formatTimestamp(keyObject.lastTested)}
357
+ `);
358
+
359
+ // Show quota info if available
360
+ if (keyObject.quotaInfo && Object.keys(keyObject.quotaInfo).length > 0) {
361
+ console.log(chalk.bold(' Quota/Details:'));
362
+ const quota = keyObject.quotaInfo;
363
+
364
+ if (quota.modelsCount !== undefined) {
365
+ console.log(` ${chalk.dim('Models available:')} ${quota.modelsCount}`);
366
+ }
367
+ if (quota.username) {
368
+ console.log(` ${chalk.dim('Username:')} @${quota.username}`);
369
+ }
370
+ if (quota.rateLimit) {
371
+ console.log(` ${chalk.dim('Rate limit remaining:')} ${quota.rateLimit.remaining}/${quota.rateLimit.limit}`);
372
+ }
373
+ if (quota.livemode !== undefined) {
374
+ console.log(` ${chalk.dim('Mode:')} ${quota.livemode ? 'Live' : 'Test'}`);
375
+ }
376
+ console.log('');
377
+ }
378
+
379
+ // Show error if present
380
+ if (keyObject.lastError) {
381
+ console.log(chalk.red(` ❌ Error: ${keyObject.lastError}`));
382
+ console.log('');
383
+ }
384
+
385
+ console.log(chalk.dim('─'.repeat(50)) + '\n');
386
+ }
387
+
388
+ /**
389
+ * Displays a warning message with yellow styling and box
390
+ *
391
+ * @param {string} message - Warning message to display
392
+ *
393
+ * @example
394
+ * showWarning('Some API keys may have been exposed!');
395
+ */
396
+ export function showWarning(message) {
397
+ const lines = message.split('\n');
398
+ const maxLength = Math.max(...lines.map(l => l.length), 40);
399
+ const border = '─'.repeat(maxLength + 4);
400
+
401
+ console.log('');
402
+ console.log(chalk.yellow(`┌${border}┐`));
403
+ console.log(chalk.yellow(`│ ⚠️ ${chalk.bold('WARNING')}${' '.repeat(maxLength - 7)} │`));
404
+ console.log(chalk.yellow(`├${border}┤`));
405
+ lines.forEach(line => {
406
+ const padding = ' '.repeat(maxLength - line.length);
407
+ console.log(chalk.yellow(`│ ${line}${padding} │`));
408
+ });
409
+ console.log(chalk.yellow(`└${border}┘`));
410
+ console.log('');
411
+ }
412
+
413
+ /**
414
+ * Displays an error message with red styling and box
415
+ *
416
+ * @param {string} message - Error message to display
417
+ *
418
+ * @example
419
+ * showError('Failed to read configuration file');
420
+ */
421
+ export function showError(message) {
422
+ const lines = message.split('\n');
423
+ const maxLength = Math.max(...lines.map(l => l.length), 40);
424
+ const border = '─'.repeat(maxLength + 4);
425
+
426
+ console.log('');
427
+ console.log(chalk.red(`┌${border}┐`));
428
+ console.log(chalk.red(`│ ❌ ${chalk.bold('ERROR')}${' '.repeat(maxLength - 5)} │`));
429
+ console.log(chalk.red(`├${border}┤`));
430
+ lines.forEach(line => {
431
+ const padding = ' '.repeat(maxLength - line.length);
432
+ console.log(chalk.red(`│ ${line}${padding} │`));
433
+ });
434
+ console.log(chalk.red(`└${border}┘`));
435
+ console.log('');
436
+ }
437
+
438
+ /**
439
+ * Displays a success message with green styling
440
+ *
441
+ * @param {string} message - Success message to display
442
+ *
443
+ * @example
444
+ * showSuccess('All keys validated successfully!');
445
+ */
446
+ export function showSuccess(message) {
447
+ console.log(chalk.green(`\n✅ ${message}\n`));
448
+ }
449
+
450
+ /**
451
+ * Displays an info message with blue styling
452
+ *
453
+ * @param {string} message - Info message to display
454
+ *
455
+ * @example
456
+ * showInfo('Scanning directory...');
457
+ */
458
+ export function showInfo(message) {
459
+ console.log(chalk.blue(`ℹ️ ${message}`));
460
+ }
461
+
462
+ /**
463
+ * Displays database statistics
464
+ *
465
+ * @param {Object} stats - Statistics from database.getDatabaseStats()
466
+ *
467
+ * @example
468
+ * const stats = await getDatabaseStats();
469
+ * displayStats(stats);
470
+ */
471
+ export function displayStats(stats) {
472
+ console.log('\n' + chalk.bold.cyan('📊 Database Statistics'));
473
+ console.log(chalk.dim('─'.repeat(40)));
474
+
475
+ console.log(`
476
+ ${chalk.bold('Database Version:')} ${chalk.white(stats.version)}
477
+ ${chalk.bold('Database Path:')} ${chalk.dim(stats.dbPath)}
478
+
479
+ ${chalk.bold('Projects:')} ${chalk.yellow(stats.totalProjects)}
480
+ ${chalk.bold('Total Keys:')} ${chalk.yellow(stats.totalKeys)}
481
+
482
+ ${chalk.bold('Status Breakdown:')}
483
+ ${chalk.green('✅ Valid:')} ${stats.validKeys}
484
+ ${chalk.red('❌ Invalid:')} ${stats.invalidKeys}
485
+ ${chalk.yellow('❓ Untested:')} ${stats.untestedKeys}
486
+ ${chalk.gray('🚫 Banned:')} ${stats.bannedKeys}
487
+
488
+ ${chalk.bold('Created:')} ${formatTimestamp(stats.createdAt)}
489
+ ${chalk.bold('Last Updated:')} ${formatTimestamp(stats.updatedAt)}
490
+ `);
491
+
492
+ console.log(chalk.dim('─'.repeat(40)) + '\n');
493
+ }
494
+
495
+ /**
496
+ * Displays help for a specific command
497
+ *
498
+ * @param {string} command - Command name
499
+ * @param {string} description - Command description
500
+ * @param {Array<{flag: string, desc: string}>} options - Command options
501
+ *
502
+ * @example
503
+ * displayHelp('scan', 'Scan directory for API keys', [
504
+ * { flag: '-r, --recursive', desc: 'Scan recursively' }
505
+ * ]);
506
+ */
507
+ export function displayHelp(command, description, options = []) {
508
+ console.log('\n' + chalk.bold.cyan(`📖 Help: ${command}`));
509
+ console.log(chalk.dim('─'.repeat(50)));
510
+ console.log(`\n${description}\n`);
511
+
512
+ if (options.length > 0) {
513
+ console.log(chalk.bold('Options:'));
514
+ options.forEach(opt => {
515
+ console.log(` ${chalk.yellow(opt.flag.padEnd(20))} ${chalk.dim(opt.desc)}`);
516
+ });
517
+ console.log('');
518
+ }
519
+ }
520
+
521
+ export default {
522
+ showBanner,
523
+ createSpinner,
524
+ displayScanResults,
525
+ displayTestResults,
526
+ displayProjectList,
527
+ displayKeyDetails,
528
+ showWarning,
529
+ showError,
530
+ showSuccess,
531
+ showInfo,
532
+ displayStats,
533
+ displayHelp
534
+ };