driftdetect 0.6.1 → 0.7.1

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.
@@ -0,0 +1,659 @@
1
+ /**
2
+ * Constants Command - drift constants
3
+ *
4
+ * Analyze constants, enums, and exported values across the codebase.
5
+ * Detects hardcoded secrets, inconsistent values, and magic numbers.
6
+ *
7
+ * @requirements Constant & Enum Extraction Feature
8
+ */
9
+ import { Command } from 'commander';
10
+ import * as fs from 'node:fs/promises';
11
+ import * as path from 'node:path';
12
+ import chalk from 'chalk';
13
+ import { ConstantStore, ConstantSecurityScanner, ConsistencyAnalyzer, } from 'driftdetect-core';
14
+ /** Directory name for drift configuration */
15
+ const DRIFT_DIR = '.drift';
16
+ /** Directory name for constants data */
17
+ const CONSTANTS_DIR = 'constants';
18
+ /**
19
+ * Check if constants data exists
20
+ */
21
+ async function constantsDataExists(rootDir) {
22
+ try {
23
+ await fs.access(path.join(rootDir, DRIFT_DIR, CONSTANTS_DIR));
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ /**
31
+ * Show helpful message when constants data not initialized
32
+ */
33
+ function showNotInitializedMessage() {
34
+ console.log();
35
+ console.log(chalk.yellow('⚠️ No constant data discovered yet.'));
36
+ console.log();
37
+ console.log(chalk.gray('Constant tracking extracts constants, enums, and exported values.'));
38
+ console.log(chalk.gray('Run a scan to discover constants:'));
39
+ console.log();
40
+ console.log(chalk.cyan(' drift scan'));
41
+ console.log();
42
+ }
43
+ /**
44
+ * Get color for severity level
45
+ */
46
+ function getSeverityColor(severity) {
47
+ switch (severity) {
48
+ case 'critical':
49
+ return chalk.red;
50
+ case 'high':
51
+ return chalk.redBright;
52
+ case 'medium':
53
+ return chalk.yellow;
54
+ case 'low':
55
+ return chalk.blue;
56
+ default:
57
+ return chalk.gray;
58
+ }
59
+ }
60
+ /**
61
+ * Get color for category
62
+ */
63
+ function getCategoryColor(category) {
64
+ switch (category) {
65
+ case 'security':
66
+ return chalk.red;
67
+ case 'api':
68
+ return chalk.blue;
69
+ case 'config':
70
+ return chalk.green;
71
+ case 'error':
72
+ return chalk.yellow;
73
+ case 'feature_flag':
74
+ return chalk.magenta;
75
+ case 'limit':
76
+ return chalk.cyan;
77
+ default:
78
+ return chalk.gray;
79
+ }
80
+ }
81
+ /**
82
+ * Overview action - default view showing summary
83
+ */
84
+ async function overviewAction(options) {
85
+ const rootDir = process.cwd();
86
+ const format = options.format ?? 'text';
87
+ if (!(await constantsDataExists(rootDir))) {
88
+ if (format === 'json') {
89
+ console.log(JSON.stringify({ error: 'No constants data found' }));
90
+ }
91
+ else {
92
+ showNotInitializedMessage();
93
+ }
94
+ return;
95
+ }
96
+ const store = new ConstantStore({ rootDir });
97
+ try {
98
+ const stats = await store.getStats();
99
+ const index = await store.getIndex();
100
+ // JSON output
101
+ if (format === 'json') {
102
+ console.log(JSON.stringify({
103
+ totalConstants: stats.totalConstants,
104
+ totalEnums: stats.totalEnums,
105
+ byLanguage: stats.byLanguage,
106
+ byCategory: stats.byCategory,
107
+ issues: stats.issues,
108
+ lastScanAt: index.generatedAt,
109
+ }, null, 2));
110
+ return;
111
+ }
112
+ // Text output
113
+ console.log();
114
+ console.log(chalk.bold('📊 Constants Overview'));
115
+ console.log(chalk.gray('─'.repeat(60)));
116
+ console.log();
117
+ console.log(`Total Constants: ${chalk.cyan(stats.totalConstants)}`);
118
+ console.log(`Total Enums: ${chalk.cyan(stats.totalEnums)}`);
119
+ console.log();
120
+ // By language
121
+ if (Object.keys(stats.byLanguage).length > 0) {
122
+ console.log(chalk.bold('By Language:'));
123
+ for (const [lang, count] of Object.entries(stats.byLanguage)) {
124
+ console.log(` ${chalk.white(lang.padEnd(12))} ${chalk.cyan(count)}`);
125
+ }
126
+ console.log();
127
+ }
128
+ // By category
129
+ if (Object.keys(stats.byCategory).length > 0) {
130
+ console.log(chalk.bold('By Category:'));
131
+ for (const [cat, count] of Object.entries(stats.byCategory)) {
132
+ const color = getCategoryColor(cat);
133
+ console.log(` ${color(cat.padEnd(16))} ${chalk.cyan(count)}`);
134
+ }
135
+ console.log();
136
+ }
137
+ // Issues
138
+ const totalIssues = stats.issues.magicValues + stats.issues.deadConstants +
139
+ stats.issues.potentialSecrets + stats.issues.inconsistentValues;
140
+ if (totalIssues > 0) {
141
+ console.log(chalk.bold('Issues:'));
142
+ if (stats.issues.potentialSecrets > 0) {
143
+ console.log(` ${chalk.red('●')} Potential Secrets: ${chalk.red(stats.issues.potentialSecrets)}`);
144
+ }
145
+ if (stats.issues.inconsistentValues > 0) {
146
+ console.log(` ${chalk.yellow('●')} Inconsistent Values: ${chalk.yellow(stats.issues.inconsistentValues)}`);
147
+ }
148
+ if (stats.issues.deadConstants > 0) {
149
+ console.log(` ${chalk.gray('●')} Dead Constants: ${chalk.gray(stats.issues.deadConstants)}`);
150
+ }
151
+ if (stats.issues.magicValues > 0) {
152
+ console.log(` ${chalk.blue('●')} Magic Values: ${chalk.blue(stats.issues.magicValues)}`);
153
+ }
154
+ console.log();
155
+ }
156
+ // Quick actions
157
+ console.log(chalk.gray("Run 'drift constants list' to browse constants"));
158
+ if (stats.issues.potentialSecrets > 0) {
159
+ console.log(chalk.yellow("Run 'drift constants secrets' to review potential secrets"));
160
+ }
161
+ console.log();
162
+ }
163
+ catch {
164
+ if (format === 'json') {
165
+ console.log(JSON.stringify({ error: 'Failed to load constants data' }));
166
+ }
167
+ else {
168
+ showNotInitializedMessage();
169
+ }
170
+ }
171
+ }
172
+ /**
173
+ * List action - list all constants with filtering
174
+ */
175
+ async function listAction(options) {
176
+ const rootDir = process.cwd();
177
+ const format = options.format ?? 'text';
178
+ const limit = options.limit ?? 50;
179
+ if (!(await constantsDataExists(rootDir))) {
180
+ if (format === 'json') {
181
+ console.log(JSON.stringify({ error: 'No constants data found', constants: [] }));
182
+ }
183
+ else {
184
+ showNotInitializedMessage();
185
+ }
186
+ return;
187
+ }
188
+ const store = new ConstantStore({ rootDir });
189
+ let constants = await store.getAllConstants();
190
+ let enums = await store.getAllEnums();
191
+ // Apply filters
192
+ if (options.category) {
193
+ constants = constants.filter(c => c.category === options.category);
194
+ }
195
+ if (options.language) {
196
+ constants = constants.filter(c => c.language === options.language);
197
+ enums = enums.filter(e => e.language === options.language);
198
+ }
199
+ if (options.file) {
200
+ constants = constants.filter(c => c.file.includes(options.file));
201
+ enums = enums.filter(e => e.file.includes(options.file));
202
+ }
203
+ if (options.search) {
204
+ const searchLower = options.search.toLowerCase();
205
+ constants = constants.filter(c => c.name.toLowerCase().includes(searchLower) ||
206
+ c.qualifiedName.toLowerCase().includes(searchLower));
207
+ enums = enums.filter(e => e.name.toLowerCase().includes(searchLower));
208
+ }
209
+ if (options.exported !== undefined) {
210
+ constants = constants.filter(c => c.isExported === options.exported);
211
+ enums = enums.filter(e => e.isExported === options.exported);
212
+ }
213
+ // Limit results
214
+ const paginatedConstants = constants.slice(0, limit);
215
+ const paginatedEnums = enums.slice(0, Math.max(0, limit - paginatedConstants.length));
216
+ // JSON output
217
+ if (format === 'json') {
218
+ console.log(JSON.stringify({
219
+ constants: paginatedConstants.map(c => ({
220
+ id: c.id,
221
+ name: c.name,
222
+ qualifiedName: c.qualifiedName,
223
+ file: c.file,
224
+ line: c.line,
225
+ language: c.language,
226
+ kind: c.kind,
227
+ category: c.category,
228
+ value: c.value,
229
+ isExported: c.isExported,
230
+ })),
231
+ enums: paginatedEnums.map(e => ({
232
+ id: e.id,
233
+ name: e.name,
234
+ file: e.file,
235
+ line: e.line,
236
+ memberCount: e.members.length,
237
+ })),
238
+ total: constants.length + enums.length,
239
+ }, null, 2));
240
+ return;
241
+ }
242
+ // CSV output
243
+ if (format === 'csv') {
244
+ console.log('id,name,file,line,language,kind,category,value,exported');
245
+ for (const c of paginatedConstants) {
246
+ const value = c.value !== undefined ? String(c.value).replace(/,/g, ';') : '';
247
+ console.log(`${c.id},${c.name},${c.file},${c.line},${c.language},${c.kind},${c.category},"${value}",${c.isExported}`);
248
+ }
249
+ return;
250
+ }
251
+ // Text output
252
+ console.log();
253
+ console.log(chalk.bold('📋 Constants'));
254
+ console.log(chalk.gray('─'.repeat(80)));
255
+ console.log();
256
+ if (paginatedConstants.length === 0 && paginatedEnums.length === 0) {
257
+ console.log(chalk.gray(' No constants found matching filters.'));
258
+ console.log();
259
+ return;
260
+ }
261
+ // Constants
262
+ for (const c of paginatedConstants) {
263
+ const categoryColor = getCategoryColor(c.category);
264
+ const name = chalk.white(c.name.padEnd(30));
265
+ const category = categoryColor(c.category.padEnd(12));
266
+ const exported = c.isExported ? chalk.green('✓') : chalk.gray('·');
267
+ const value = c.value !== undefined
268
+ ? chalk.gray(String(c.value).slice(0, 30) + (String(c.value).length > 30 ? '...' : ''))
269
+ : '';
270
+ console.log(` ${exported} ${name} ${category} ${value}`);
271
+ console.log(chalk.gray(` ${c.file}:${c.line}`));
272
+ }
273
+ // Enums
274
+ if (paginatedEnums.length > 0) {
275
+ console.log();
276
+ console.log(chalk.bold('📦 Enums'));
277
+ console.log();
278
+ for (const e of paginatedEnums) {
279
+ const name = chalk.white(e.name.padEnd(30));
280
+ const members = chalk.cyan(`${e.members.length} members`);
281
+ console.log(` ${name} ${members}`);
282
+ console.log(chalk.gray(` ${e.file}:${e.line}`));
283
+ }
284
+ }
285
+ console.log();
286
+ console.log(chalk.gray(`Showing ${paginatedConstants.length + paginatedEnums.length} of ${constants.length + enums.length} items`));
287
+ console.log();
288
+ }
289
+ /**
290
+ * Get action - show details for a specific constant
291
+ */
292
+ async function getAction(nameOrId, options) {
293
+ const rootDir = process.cwd();
294
+ const format = options.format ?? 'text';
295
+ if (!(await constantsDataExists(rootDir))) {
296
+ if (format === 'json') {
297
+ console.log(JSON.stringify({ error: 'No constants data found' }));
298
+ }
299
+ else {
300
+ showNotInitializedMessage();
301
+ }
302
+ return;
303
+ }
304
+ const store = new ConstantStore({ rootDir });
305
+ // Try to find by ID first, then by name
306
+ let constant = await store.getConstantById(nameOrId);
307
+ if (!constant) {
308
+ const results = await store.searchByName(nameOrId);
309
+ constant = results[0] ?? null;
310
+ }
311
+ let enumDef = null;
312
+ if (!constant) {
313
+ enumDef = await store.getEnumById(nameOrId);
314
+ if (!enumDef) {
315
+ const enums = await store.getAllEnums();
316
+ enumDef = enums.find(e => e.name === nameOrId) ?? null;
317
+ }
318
+ }
319
+ if (!constant && !enumDef) {
320
+ if (format === 'json') {
321
+ console.log(JSON.stringify({ error: `Constant or enum not found: ${nameOrId}` }));
322
+ }
323
+ else {
324
+ console.log();
325
+ console.log(chalk.red(`Constant or enum '${nameOrId}' not found.`));
326
+ console.log(chalk.gray("Run 'drift constants list' to see available constants."));
327
+ console.log();
328
+ }
329
+ return;
330
+ }
331
+ // JSON output
332
+ if (format === 'json') {
333
+ console.log(JSON.stringify({
334
+ constant: constant ?? undefined,
335
+ enum: enumDef ?? undefined,
336
+ }, null, 2));
337
+ return;
338
+ }
339
+ // Text output
340
+ console.log();
341
+ if (constant) {
342
+ console.log(chalk.bold(`📌 Constant: ${constant.name}`));
343
+ console.log(chalk.gray('─'.repeat(60)));
344
+ console.log();
345
+ console.log(`Qualified Name: ${chalk.cyan(constant.qualifiedName)}`);
346
+ console.log(`File: ${chalk.white(constant.file)}:${constant.line}`);
347
+ console.log(`Language: ${chalk.white(constant.language)}`);
348
+ console.log(`Kind: ${chalk.white(constant.kind)}`);
349
+ console.log(`Category: ${getCategoryColor(constant.category)(constant.category)}`);
350
+ console.log(`Exported: ${constant.isExported ? chalk.green('yes') : chalk.gray('no')}`);
351
+ if (constant.value !== undefined) {
352
+ console.log();
353
+ console.log(chalk.bold('Value:'));
354
+ console.log(` ${chalk.cyan(String(constant.value))}`);
355
+ }
356
+ if (constant.docComment) {
357
+ console.log();
358
+ console.log(chalk.bold('Documentation:'));
359
+ console.log(` ${chalk.gray(constant.docComment)}`);
360
+ }
361
+ }
362
+ if (enumDef) {
363
+ console.log(chalk.bold(`📦 Enum: ${enumDef.name}`));
364
+ console.log(chalk.gray('─'.repeat(60)));
365
+ console.log();
366
+ console.log(`File: ${chalk.white(enumDef.file)}:${enumDef.line}`);
367
+ console.log(`Language: ${chalk.white(enumDef.language)}`);
368
+ console.log(`Exported: ${enumDef.isExported ? chalk.green('yes') : chalk.gray('no')}`);
369
+ console.log(`Members: ${chalk.cyan(enumDef.members.length)}`);
370
+ if (enumDef.members.length > 0) {
371
+ console.log();
372
+ console.log(chalk.bold('Members:'));
373
+ for (const member of enumDef.members.slice(0, 20)) {
374
+ const value = member.value !== undefined ? ` = ${chalk.cyan(String(member.value))}` : '';
375
+ console.log(` ${chalk.white(member.name)}${value}`);
376
+ }
377
+ if (enumDef.members.length > 20) {
378
+ console.log(chalk.gray(` ... and ${enumDef.members.length - 20} more`));
379
+ }
380
+ }
381
+ }
382
+ console.log();
383
+ }
384
+ /**
385
+ * Secrets action - show potential hardcoded secrets
386
+ */
387
+ async function secretsAction(options) {
388
+ const rootDir = process.cwd();
389
+ const format = options.format ?? 'text';
390
+ const limit = options.limit ?? 50;
391
+ const minSeverity = options.severity;
392
+ if (!(await constantsDataExists(rootDir))) {
393
+ if (format === 'json') {
394
+ console.log(JSON.stringify({ error: 'No constants data found', secrets: [] }));
395
+ }
396
+ else {
397
+ showNotInitializedMessage();
398
+ }
399
+ return;
400
+ }
401
+ const store = new ConstantStore({ rootDir });
402
+ const scanner = new ConstantSecurityScanner();
403
+ const constants = await store.getAllConstants();
404
+ const result = scanner.scan(constants);
405
+ // Filter by severity
406
+ let secrets = result.secrets;
407
+ if (minSeverity) {
408
+ const severityOrder = ['info', 'low', 'medium', 'high', 'critical'];
409
+ const minIndex = severityOrder.indexOf(minSeverity);
410
+ secrets = secrets.filter(s => severityOrder.indexOf(s.severity) >= minIndex);
411
+ }
412
+ const paginatedSecrets = secrets.slice(0, limit);
413
+ // JSON output
414
+ if (format === 'json') {
415
+ console.log(JSON.stringify({
416
+ secrets: paginatedSecrets,
417
+ total: secrets.length,
418
+ bySeverity: {
419
+ critical: secrets.filter(s => s.severity === 'critical').length,
420
+ high: secrets.filter(s => s.severity === 'high').length,
421
+ medium: secrets.filter(s => s.severity === 'medium').length,
422
+ low: secrets.filter(s => s.severity === 'low').length,
423
+ info: secrets.filter(s => s.severity === 'info').length,
424
+ },
425
+ }, null, 2));
426
+ return;
427
+ }
428
+ // Text output
429
+ console.log();
430
+ console.log(chalk.bold('🔐 Potential Hardcoded Secrets'));
431
+ console.log(chalk.gray('─'.repeat(60)));
432
+ console.log();
433
+ if (secrets.length === 0) {
434
+ console.log(chalk.green(' ✓ No hardcoded secrets detected.'));
435
+ console.log();
436
+ return;
437
+ }
438
+ // Summary
439
+ const critical = secrets.filter(s => s.severity === 'critical').length;
440
+ const high = secrets.filter(s => s.severity === 'high').length;
441
+ if (critical > 0) {
442
+ console.log(chalk.red(` 🔴 ${critical} CRITICAL secrets found!`));
443
+ }
444
+ if (high > 0) {
445
+ console.log(chalk.redBright(` 🟠 ${high} HIGH severity secrets found`));
446
+ }
447
+ console.log();
448
+ // List secrets
449
+ for (const secret of paginatedSecrets) {
450
+ const severityColor = getSeverityColor(secret.severity);
451
+ const severity = severityColor(secret.severity.toUpperCase().padEnd(8));
452
+ const name = chalk.white(secret.name.padEnd(30));
453
+ console.log(` ${severity} ${name}`);
454
+ console.log(chalk.gray(` ${secret.file}:${secret.line}`));
455
+ console.log(chalk.gray(` Type: ${secret.secretType}`));
456
+ console.log();
457
+ }
458
+ console.log(chalk.gray(`Showing ${paginatedSecrets.length} of ${secrets.length} potential secrets`));
459
+ console.log();
460
+ console.log(chalk.yellow('⚠️ Move secrets to environment variables or a secrets manager'));
461
+ console.log();
462
+ }
463
+ /**
464
+ * Inconsistent action - show constants with inconsistent values
465
+ */
466
+ async function inconsistentAction(options) {
467
+ const rootDir = process.cwd();
468
+ const format = options.format ?? 'text';
469
+ const limit = options.limit ?? 50;
470
+ if (!(await constantsDataExists(rootDir))) {
471
+ if (format === 'json') {
472
+ console.log(JSON.stringify({ error: 'No constants data found', inconsistencies: [] }));
473
+ }
474
+ else {
475
+ showNotInitializedMessage();
476
+ }
477
+ return;
478
+ }
479
+ const store = new ConstantStore({ rootDir });
480
+ const analyzer = new ConsistencyAnalyzer();
481
+ const constants = await store.getAllConstants();
482
+ const result = analyzer.analyze(constants);
483
+ const paginatedInconsistencies = result.inconsistencies.slice(0, limit);
484
+ // JSON output
485
+ if (format === 'json') {
486
+ console.log(JSON.stringify({
487
+ inconsistencies: paginatedInconsistencies,
488
+ total: result.inconsistencies.length,
489
+ }, null, 2));
490
+ return;
491
+ }
492
+ // Text output
493
+ console.log();
494
+ console.log(chalk.bold('⚡ Inconsistent Constants'));
495
+ console.log(chalk.gray('─'.repeat(60)));
496
+ console.log();
497
+ if (result.inconsistencies.length === 0) {
498
+ console.log(chalk.green(' ✓ No inconsistent constants found.'));
499
+ console.log();
500
+ return;
501
+ }
502
+ for (const inc of paginatedInconsistencies) {
503
+ console.log(chalk.yellow(` ${inc.name}`));
504
+ console.log(chalk.gray(` Found ${inc.instances.length} different values:`));
505
+ for (const inst of inc.instances.slice(0, 5)) {
506
+ console.log(` ${chalk.cyan(String(inst.value))} in ${chalk.gray(inst.file)}:${inst.line}`);
507
+ }
508
+ if (inc.instances.length > 5) {
509
+ console.log(chalk.gray(` ... and ${inc.instances.length - 5} more`));
510
+ }
511
+ console.log();
512
+ }
513
+ console.log(chalk.gray(`Showing ${paginatedInconsistencies.length} of ${result.inconsistencies.length} inconsistencies`));
514
+ console.log();
515
+ }
516
+ /**
517
+ * Dead action - show potentially unused constants
518
+ */
519
+ async function deadAction(options) {
520
+ const rootDir = process.cwd();
521
+ const format = options.format ?? 'text';
522
+ const limit = options.limit ?? 50;
523
+ if (!(await constantsDataExists(rootDir))) {
524
+ if (format === 'json') {
525
+ console.log(JSON.stringify({ error: 'No constants data found', dead: [] }));
526
+ }
527
+ else {
528
+ showNotInitializedMessage();
529
+ }
530
+ return;
531
+ }
532
+ const store = new ConstantStore({ rootDir });
533
+ // Get constants that are not exported and uncategorized (likely unused)
534
+ const constants = await store.getAllConstants();
535
+ const potentiallyDead = constants
536
+ .filter(c => !c.isExported && c.category === 'uncategorized')
537
+ .slice(0, limit);
538
+ // JSON output
539
+ if (format === 'json') {
540
+ console.log(JSON.stringify({
541
+ dead: potentiallyDead.map(c => ({
542
+ id: c.id,
543
+ name: c.name,
544
+ file: c.file,
545
+ line: c.line,
546
+ confidence: 0.5,
547
+ reason: 'not_exported_uncategorized',
548
+ })),
549
+ total: potentiallyDead.length,
550
+ }, null, 2));
551
+ return;
552
+ }
553
+ // Text output
554
+ console.log();
555
+ console.log(chalk.bold('💀 Potentially Unused Constants'));
556
+ console.log(chalk.gray('─'.repeat(60)));
557
+ console.log();
558
+ if (potentiallyDead.length === 0) {
559
+ console.log(chalk.green(' ✓ No obvious dead constants found.'));
560
+ console.log();
561
+ return;
562
+ }
563
+ console.log(chalk.gray(' Note: Full dead code detection requires reference analysis.'));
564
+ console.log();
565
+ for (const c of potentiallyDead) {
566
+ console.log(` ${chalk.gray('●')} ${chalk.white(c.name)}`);
567
+ console.log(chalk.gray(` ${c.file}:${c.line}`));
568
+ }
569
+ console.log();
570
+ console.log(chalk.gray(`Found ${potentiallyDead.length} potentially unused constants`));
571
+ console.log();
572
+ }
573
+ /**
574
+ * Export action - export constants to file
575
+ */
576
+ async function exportAction(outputPath, options) {
577
+ const rootDir = process.cwd();
578
+ const format = options.format ?? 'json';
579
+ if (!(await constantsDataExists(rootDir))) {
580
+ console.log(chalk.red('No constants data found. Run drift scan first.'));
581
+ process.exit(1);
582
+ }
583
+ const store = new ConstantStore({ rootDir });
584
+ let constants = await store.getAllConstants();
585
+ const enums = await store.getAllEnums();
586
+ // Apply filters
587
+ if (options.category) {
588
+ constants = constants.filter(c => c.category === options.category);
589
+ }
590
+ if (options.language) {
591
+ constants = constants.filter(c => c.language === options.language);
592
+ }
593
+ let content;
594
+ if (format === 'csv') {
595
+ const lines = ['id,name,qualifiedName,file,line,language,kind,category,value,exported'];
596
+ for (const c of constants) {
597
+ const value = c.value !== undefined ? String(c.value).replace(/"/g, '""') : '';
598
+ lines.push(`"${c.id}","${c.name}","${c.qualifiedName}","${c.file}",${c.line},"${c.language}","${c.kind}","${c.category}","${value}",${c.isExported}`);
599
+ }
600
+ content = lines.join('\n');
601
+ }
602
+ else {
603
+ content = JSON.stringify({ constants, enums }, null, 2);
604
+ }
605
+ await fs.writeFile(outputPath, content, 'utf-8');
606
+ console.log(chalk.green(`✓ Exported ${constants.length} constants to ${outputPath}`));
607
+ }
608
+ /**
609
+ * Create the constants command with subcommands
610
+ */
611
+ export const constantsCommand = new Command('constants')
612
+ .description('Analyze constants, enums, and exported values')
613
+ .option('-f, --format <format>', 'Output format (text, json)', 'text')
614
+ .option('--verbose', 'Enable verbose output')
615
+ .action(overviewAction);
616
+ // Subcommands
617
+ constantsCommand
618
+ .command('list')
619
+ .description('List all constants with filtering')
620
+ .option('-f, --format <format>', 'Output format (text, json, csv)', 'text')
621
+ .option('-c, --category <category>', 'Filter by category')
622
+ .option('-l, --language <language>', 'Filter by language')
623
+ .option('--file <path>', 'Filter by file path')
624
+ .option('-s, --search <query>', 'Search by name')
625
+ .option('--exported', 'Show only exported constants')
626
+ .option('--limit <n>', 'Limit results', parseInt)
627
+ .action(listAction);
628
+ constantsCommand
629
+ .command('get <nameOrId>')
630
+ .description('Show details for a specific constant or enum')
631
+ .option('-f, --format <format>', 'Output format (text, json)', 'text')
632
+ .action(getAction);
633
+ constantsCommand
634
+ .command('secrets')
635
+ .description('Show potential hardcoded secrets')
636
+ .option('-f, --format <format>', 'Output format (text, json)', 'text')
637
+ .option('--severity <level>', 'Minimum severity (info, low, medium, high, critical)')
638
+ .option('--limit <n>', 'Limit results', parseInt)
639
+ .action(secretsAction);
640
+ constantsCommand
641
+ .command('inconsistent')
642
+ .description('Show constants with inconsistent values across files')
643
+ .option('-f, --format <format>', 'Output format (text, json)', 'text')
644
+ .option('--limit <n>', 'Limit results', parseInt)
645
+ .action(inconsistentAction);
646
+ constantsCommand
647
+ .command('dead')
648
+ .description('Show potentially unused constants')
649
+ .option('-f, --format <format>', 'Output format (text, json)', 'text')
650
+ .option('--limit <n>', 'Limit results', parseInt)
651
+ .action(deadAction);
652
+ constantsCommand
653
+ .command('export <output>')
654
+ .description('Export constants to file')
655
+ .option('-f, --format <format>', 'Output format (json, csv)', 'json')
656
+ .option('-c, --category <category>', 'Filter by category')
657
+ .option('-l, --language <language>', 'Filter by language')
658
+ .action(exportAction);
659
+ //# sourceMappingURL=constants.js.map