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/.github/ISSUE_TEMPLATE/bug_report.md +28 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
- package/.github/ROADMAP_ISSUES.md +169 -0
- package/LICENSE +21 -0
- package/README.md +501 -0
- package/bin/apigraveyard.js +686 -0
- package/hooks/pre-commit +203 -0
- package/package.json +34 -0
- package/scripts/install-hooks.js +182 -0
- package/src/database.js +518 -0
- package/src/display.js +534 -0
- package/src/scanner.js +294 -0
- package/src/tester.js +578 -0
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
|
+
};
|