devcompass 2.1.0 → 2.3.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.
@@ -1,3 +1,4 @@
1
+ // src/commands/analyze.js
1
2
  const chalk = require('chalk');
2
3
  const ora = require('ora');
3
4
  const path = require('path');
@@ -7,6 +8,9 @@ const { findUnusedDeps } = require('../analyzers/unused-deps');
7
8
  const { findOutdatedDeps } = require('../analyzers/outdated');
8
9
  const { calculateScore } = require('../analyzers/scoring');
9
10
  const { checkEcosystemAlerts } = require('../alerts');
11
+ const { checkSecurity, calculateSecurityPenalty } = require('../analyzers/security');
12
+ const { analyzeBundleSizes, findHeavyPackages } = require('../analyzers/bundle-size');
13
+ const { checkLicenses, findProblematicLicenses } = require('../analyzers/licenses');
10
14
  const {
11
15
  formatAlerts,
12
16
  getSeverityDisplay,
@@ -18,18 +22,40 @@ const {
18
22
  logDivider,
19
23
  getScoreColor
20
24
  } = require('../utils/logger');
25
+ const { loadConfig, filterAlerts } = require('../config/loader');
26
+ const { getCached, setCache } = require('../cache/manager');
27
+ const { formatAsJson } = require('../utils/json-formatter');
28
+ const { handleCiMode } = require('../utils/ci-handler');
21
29
  const packageJson = require('../../package.json');
22
30
 
23
31
  async function analyze(options) {
24
32
  const projectPath = options.path || process.cwd();
25
33
 
26
- console.log('\n');
27
- log(chalk.cyan.bold(`🔍 DevCompass v${packageJson.version}`) + ' - Analyzing your project...\n');
34
+ // Load config
35
+ const config = loadConfig(projectPath);
28
36
 
29
- const spinner = ora({
30
- text: 'Loading project...',
31
- color: 'cyan'
32
- }).start();
37
+ // Handle output modes
38
+ const outputMode = options.json ? 'json' : (options.ci ? 'ci' : (options.silent ? 'silent' : 'normal'));
39
+
40
+ // Only show header for normal and CI modes
41
+ if (outputMode !== 'silent' && outputMode !== 'json') {
42
+ console.log('\n');
43
+ log(chalk.cyan.bold(`🔍 DevCompass v${packageJson.version}`) + ' - Analyzing your project...\n');
44
+ }
45
+
46
+ // Create spinner (disabled for json/silent modes to prevent EPIPE errors)
47
+ const spinner = (outputMode === 'json' || outputMode === 'silent')
48
+ ? {
49
+ start: function() { return this; },
50
+ succeed: function() {},
51
+ fail: function(msg) { if (msg) console.error(msg); },
52
+ text: '',
53
+ set text(val) {}
54
+ }
55
+ : ora({
56
+ text: 'Loading project...',
57
+ color: 'cyan'
58
+ }).start();
33
59
 
34
60
  try {
35
61
  const packageJsonPath = path.join(projectPath, 'package.json');
@@ -63,46 +89,168 @@ async function analyze(options) {
63
89
  process.exit(0);
64
90
  }
65
91
 
66
- // Check for ecosystem alerts
92
+ // Check for ecosystem alerts (with cache)
67
93
  spinner.text = 'Checking ecosystem alerts...';
68
94
  let alerts = [];
69
- try {
70
- alerts = await checkEcosystemAlerts(projectPath, dependencies);
71
- } catch (error) {
72
- console.log(chalk.yellow('\n⚠️ Could not check ecosystem alerts'));
73
- console.log(chalk.gray(` Error: ${error.message}\n`));
95
+
96
+ if (config.cache) {
97
+ alerts = getCached(projectPath, 'alerts');
74
98
  }
75
99
 
100
+ if (!alerts) {
101
+ try {
102
+ alerts = await checkEcosystemAlerts(projectPath, dependencies);
103
+ if (config.cache) {
104
+ setCache(projectPath, 'alerts', alerts);
105
+ }
106
+ } catch (error) {
107
+ if (outputMode !== 'silent') {
108
+ console.log(chalk.yellow('\n⚠️ Could not check ecosystem alerts'));
109
+ console.log(chalk.gray(` Error: ${error.message}\n`));
110
+ }
111
+ alerts = [];
112
+ }
113
+ }
114
+
115
+ // Filter alerts based on config
116
+ alerts = filterAlerts(alerts, config);
117
+
118
+ // Unused dependencies
76
119
  spinner.text = 'Detecting unused dependencies...';
77
120
  let unusedDeps = [];
78
- try {
79
- unusedDeps = await findUnusedDeps(projectPath, dependencies);
80
- } catch (error) {
81
- console.log(chalk.yellow('\n⚠️ Could not detect unused dependencies'));
82
- console.log(chalk.gray(` Error: ${error.message}\n`));
121
+
122
+ if (config.cache) {
123
+ unusedDeps = getCached(projectPath, 'unused');
124
+ }
125
+
126
+ if (!unusedDeps) {
127
+ try {
128
+ unusedDeps = await findUnusedDeps(projectPath, dependencies);
129
+ if (config.cache) {
130
+ setCache(projectPath, 'unused', unusedDeps);
131
+ }
132
+ } catch (error) {
133
+ if (outputMode !== 'silent') {
134
+ console.log(chalk.yellow('\n⚠️ Could not detect unused dependencies'));
135
+ console.log(chalk.gray(` Error: ${error.message}\n`));
136
+ }
137
+ unusedDeps = [];
138
+ }
83
139
  }
84
140
 
141
+ // Outdated packages
85
142
  spinner.text = 'Checking for outdated packages...';
86
143
  let outdatedDeps = [];
87
- try {
88
- outdatedDeps = await findOutdatedDeps(projectPath, dependencies);
89
- } catch (error) {
90
- console.log(chalk.yellow('\n⚠️ Could not check for outdated packages'));
91
- console.log(chalk.gray(` Error: ${error.message}\n`));
144
+
145
+ if (config.cache) {
146
+ outdatedDeps = getCached(projectPath, 'outdated');
147
+ }
148
+
149
+ if (!outdatedDeps) {
150
+ try {
151
+ outdatedDeps = await findOutdatedDeps(projectPath, dependencies);
152
+ if (config.cache) {
153
+ setCache(projectPath, 'outdated', outdatedDeps);
154
+ }
155
+ } catch (error) {
156
+ if (outputMode !== 'silent') {
157
+ console.log(chalk.yellow('\n⚠️ Could not check for outdated packages'));
158
+ console.log(chalk.gray(` Error: ${error.message}\n`));
159
+ }
160
+ outdatedDeps = [];
161
+ }
162
+ }
163
+
164
+ // Check security vulnerabilities (NEW)
165
+ spinner.text = 'Checking security vulnerabilities...';
166
+ let securityData = { vulnerabilities: [], metadata: { total: 0, critical: 0, high: 0, moderate: 0, low: 0 } };
167
+
168
+ if (config.cache) {
169
+ const cached = getCached(projectPath, 'security');
170
+ if (cached) securityData = cached;
171
+ }
172
+
173
+ if (securityData.metadata.total === 0) {
174
+ try {
175
+ securityData = await checkSecurity(projectPath);
176
+ if (config.cache) {
177
+ setCache(projectPath, 'security', securityData);
178
+ }
179
+ } catch (error) {
180
+ if (outputMode !== 'silent') {
181
+ console.log(chalk.yellow('\n⚠️ Could not check security vulnerabilities'));
182
+ console.log(chalk.gray(` Error: ${error.message}\n`));
183
+ }
184
+ }
185
+ }
186
+
187
+ // Analyze bundle sizes (NEW)
188
+ spinner.text = 'Analyzing bundle sizes...';
189
+ let bundleSizes = [];
190
+
191
+ if (config.cache) {
192
+ const cached = getCached(projectPath, 'bundleSizes');
193
+ if (cached) bundleSizes = cached;
194
+ }
195
+
196
+ if (bundleSizes.length === 0) {
197
+ try {
198
+ bundleSizes = await analyzeBundleSizes(projectPath, dependencies);
199
+ if (config.cache && bundleSizes.length > 0) {
200
+ setCache(projectPath, 'bundleSizes', bundleSizes);
201
+ }
202
+ } catch (error) {
203
+ // Bundle size analysis is optional
204
+ }
205
+ }
206
+
207
+ // Check licenses (NEW)
208
+ spinner.text = 'Checking licenses...';
209
+ let licenses = [];
210
+
211
+ if (config.cache) {
212
+ const cached = getCached(projectPath, 'licenses');
213
+ if (cached) licenses = cached;
92
214
  }
93
215
 
216
+ if (licenses.length === 0) {
217
+ try {
218
+ licenses = await checkLicenses(projectPath, dependencies);
219
+ if (config.cache && licenses.length > 0) {
220
+ setCache(projectPath, 'licenses', licenses);
221
+ }
222
+ } catch (error) {
223
+ // License checking is optional
224
+ }
225
+ }
226
+
227
+ // Calculate score (UPDATED)
94
228
  const alertPenalty = calculateAlertPenalty(alerts);
229
+ const securityPenalty = calculateSecurityPenalty(securityData.metadata);
230
+
95
231
  const score = calculateScore(
96
232
  totalDeps,
97
233
  unusedDeps.length,
98
234
  outdatedDeps.length,
99
235
  alerts.length,
100
- alertPenalty
236
+ alertPenalty,
237
+ securityPenalty
101
238
  );
102
239
 
103
240
  spinner.succeed(chalk.green(`Scanned ${totalDeps} dependencies in project`));
104
241
 
105
- displayResults(alerts, unusedDeps, outdatedDeps, score, totalDeps);
242
+ // Handle different output modes
243
+ if (outputMode === 'json') {
244
+ const jsonOutput = formatAsJson(alerts, unusedDeps, outdatedDeps, score, totalDeps, securityData, bundleSizes, licenses);
245
+ console.log(jsonOutput);
246
+ } else if (outputMode === 'ci') {
247
+ displayResults(alerts, unusedDeps, outdatedDeps, score, totalDeps, securityData, bundleSizes, licenses);
248
+ handleCiMode(score, config, alerts, unusedDeps);
249
+ } else if (outputMode === 'silent') {
250
+ // Silent mode - no output
251
+ } else {
252
+ displayResults(alerts, unusedDeps, outdatedDeps, score, totalDeps, securityData, bundleSizes, licenses);
253
+ }
106
254
 
107
255
  } catch (error) {
108
256
  spinner.fail(chalk.red('Analysis failed'));
@@ -114,10 +262,40 @@ async function analyze(options) {
114
262
  }
115
263
  }
116
264
 
117
- function displayResults(alerts, unusedDeps, outdatedDeps, score, totalDeps) {
265
+ function displayResults(alerts, unusedDeps, outdatedDeps, score, totalDeps, securityData, bundleSizes, licenses) {
118
266
  logDivider();
119
267
 
120
- // ECOSYSTEM ALERTS (NEW SECTION)
268
+ // SECURITY VULNERABILITIES (NEW SECTION)
269
+ if (securityData.metadata.total > 0) {
270
+ const criticalCount = securityData.metadata.critical;
271
+ const highCount = securityData.metadata.high;
272
+ const moderateCount = securityData.metadata.moderate;
273
+ const lowCount = securityData.metadata.low;
274
+
275
+ logSection('🔐 SECURITY VULNERABILITIES', securityData.metadata.total);
276
+
277
+ if (criticalCount > 0) {
278
+ log(chalk.red.bold(`\n 🔴 CRITICAL: ${criticalCount}`));
279
+ }
280
+ if (highCount > 0) {
281
+ log(chalk.red(` 🟠 HIGH: ${highCount}`));
282
+ }
283
+ if (moderateCount > 0) {
284
+ log(chalk.yellow(` 🟡 MODERATE: ${moderateCount}`));
285
+ }
286
+ if (lowCount > 0) {
287
+ log(chalk.gray(` ⚪ LOW: ${lowCount}`));
288
+ }
289
+
290
+ log(chalk.cyan('\n Run') + chalk.bold(' npm audit fix ') + chalk.cyan('to fix vulnerabilities\n'));
291
+ } else {
292
+ logSection('✅ SECURITY VULNERABILITIES');
293
+ log(chalk.green(' No vulnerabilities detected!\n'));
294
+ }
295
+
296
+ logDivider();
297
+
298
+ // ECOSYSTEM ALERTS
121
299
  if (alerts.length > 0) {
122
300
  logSection('🚨 ECOSYSTEM ALERTS', alerts.length);
123
301
 
@@ -198,6 +376,46 @@ function displayResults(alerts, unusedDeps, outdatedDeps, score, totalDeps) {
198
376
 
199
377
  logDivider();
200
378
 
379
+ // BUNDLE SIZE (NEW SECTION)
380
+ const heavyPackages = findHeavyPackages(bundleSizes);
381
+ if (heavyPackages.length > 0) {
382
+ logSection('📦 HEAVY PACKAGES', heavyPackages.length);
383
+
384
+ log(chalk.gray(' Packages larger than 1MB:\n'));
385
+
386
+ heavyPackages.slice(0, 10).forEach(pkg => {
387
+ const nameCol = pkg.name.padEnd(25);
388
+ const size = pkg.size > 5120
389
+ ? chalk.red(pkg.sizeFormatted)
390
+ : chalk.yellow(pkg.sizeFormatted);
391
+
392
+ log(` ${nameCol} ${size}`);
393
+ });
394
+
395
+ log('');
396
+ logDivider();
397
+ }
398
+
399
+ // LICENSE WARNINGS (NEW SECTION - ALWAYS SHOW)
400
+ const problematicLicenses = findProblematicLicenses(licenses);
401
+ if (problematicLicenses.length > 0) {
402
+ logSection('⚖️ LICENSE WARNINGS', problematicLicenses.length);
403
+
404
+ problematicLicenses.forEach(pkg => {
405
+ const type = pkg.type === 'restrictive'
406
+ ? chalk.red('Restrictive')
407
+ : chalk.yellow('Unknown');
408
+ log(` ${chalk.bold(pkg.package)} - ${type} (${pkg.license})`);
409
+ });
410
+
411
+ log(chalk.gray('\n Note: Restrictive licenses may require legal review\n'));
412
+ } else {
413
+ logSection('✅ LICENSE COMPLIANCE');
414
+ log(chalk.green(' All licenses are permissive!\n'));
415
+ }
416
+
417
+ logDivider();
418
+
201
419
  // PROJECT HEALTH
202
420
  logSection('📊 PROJECT HEALTH');
203
421
 
@@ -205,6 +423,10 @@ function displayResults(alerts, unusedDeps, outdatedDeps, score, totalDeps) {
205
423
  log(` Overall Score: ${scoreColor(score.total + '/10')}`);
206
424
  log(` Total Dependencies: ${chalk.cyan(totalDeps)}`);
207
425
 
426
+ if (securityData.metadata.total > 0) {
427
+ log(` Security Vulnerabilities: ${chalk.red(securityData.metadata.total)}`);
428
+ }
429
+
208
430
  if (alerts.length > 0) {
209
431
  log(` Ecosystem Alerts: ${chalk.red(alerts.length)}`);
210
432
  }
@@ -215,16 +437,23 @@ function displayResults(alerts, unusedDeps, outdatedDeps, score, totalDeps) {
215
437
  logDivider();
216
438
 
217
439
  // QUICK WINS
218
- displayQuickWins(alerts, unusedDeps, outdatedDeps, score, totalDeps);
440
+ displayQuickWins(alerts, unusedDeps, outdatedDeps, score, totalDeps, securityData);
219
441
  }
220
442
 
221
- function displayQuickWins(alerts, unusedDeps, outdatedDeps, score, totalDeps) {
443
+ function displayQuickWins(alerts, unusedDeps, outdatedDeps, score, totalDeps, securityData) {
222
444
  const hasCriticalAlerts = alerts.some(a => a.severity === 'critical' || a.severity === 'high');
445
+ const hasCriticalSecurity = securityData.metadata.critical > 0 || securityData.metadata.high > 0;
223
446
 
224
- if (hasCriticalAlerts || unusedDeps.length > 0) {
447
+ if (hasCriticalSecurity || hasCriticalAlerts || unusedDeps.length > 0) {
225
448
  logSection('💡 QUICK WINS');
226
449
 
227
- // Fix critical alerts first
450
+ // Fix security vulnerabilities first
451
+ if (hasCriticalSecurity) {
452
+ log(' 🔐 Fix security vulnerabilities:\n');
453
+ log(chalk.cyan(` npm audit fix\n`));
454
+ }
455
+
456
+ // Fix critical alerts
228
457
  if (hasCriticalAlerts) {
229
458
  const criticalAlerts = alerts.filter(a => a.severity === 'critical' || a.severity === 'high');
230
459
 
@@ -257,13 +486,18 @@ function displayQuickWins(alerts, unusedDeps, outdatedDeps, score, totalDeps) {
257
486
  0,
258
487
  outdatedDeps.length,
259
488
  alerts.length - alerts.filter(a => a.severity === 'critical' || a.severity === 'high').length,
260
- alertPenalty
489
+ alertPenalty,
490
+ 0 // Assume security issues fixed
261
491
  );
262
492
 
263
493
  log(' Expected impact:');
264
494
 
495
+ if (hasCriticalSecurity) {
496
+ log(` ${chalk.green('✓')} Resolve security vulnerabilities`);
497
+ }
498
+
265
499
  if (hasCriticalAlerts) {
266
- log(` ${chalk.green('✓')} Resolve critical security/stability issues`);
500
+ log(` ${chalk.green('✓')} Resolve critical stability issues`);
267
501
  }
268
502
 
269
503
  if (unusedDeps.length > 0) {
@@ -274,6 +508,8 @@ function displayQuickWins(alerts, unusedDeps, outdatedDeps, score, totalDeps) {
274
508
  const improvedScoreColor = getScoreColor(improvedScore.total);
275
509
  log(` ${chalk.green('✓')} Improve health score → ${improvedScoreColor(improvedScore.total + '/10')}\n`);
276
510
 
511
+ log(chalk.cyan('💡 TIP: Run') + chalk.bold(' devcompass fix ') + chalk.cyan('to apply these fixes automatically!\n'));
512
+
277
513
  logDivider();
278
514
  }
279
515
  }
@@ -1,3 +1,4 @@
1
+ // src/commands/fix.js
1
2
  const chalk = require('chalk');
2
3
  const ora = require('ora');
3
4
  const { execSync } = require('child_process');
@@ -1,3 +1,4 @@
1
+ // src/config/loader.js
1
2
  const fs = require('fs');
2
3
  const path = require('path');
3
4
 
@@ -20,10 +21,52 @@ function loadConfig(projectPath) {
20
21
  function getDefaultConfig() {
21
22
  return {
22
23
  ignore: [],
23
- ignoreSeverity: [],
24
+ ignoreSeverity: [], // e.g., ["low", "medium"]
25
+ minSeverity: null, // e.g., "medium" - only show medium+ alerts
24
26
  minScore: 0,
25
- cache: true
27
+ cache: true,
28
+ outputMode: 'normal' // normal, json, silent, ci
26
29
  };
27
30
  }
28
31
 
29
- module.exports = { loadConfig };
32
+ /**
33
+ * Check if alert should be ignored based on config
34
+ */
35
+ function shouldIgnoreAlert(alert, config) {
36
+ // Check if package is in ignore list
37
+ if (config.ignore.includes(alert.package)) {
38
+ return true;
39
+ }
40
+
41
+ // Check if severity is ignored
42
+ if (config.ignoreSeverity.includes(alert.severity)) {
43
+ return true;
44
+ }
45
+
46
+ // Check minimum severity level
47
+ if (config.minSeverity) {
48
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
49
+ const minLevel = severityOrder[config.minSeverity];
50
+ const alertLevel = severityOrder[alert.severity];
51
+
52
+ if (alertLevel > minLevel) {
53
+ return true; // Alert is below minimum severity
54
+ }
55
+ }
56
+
57
+ return false;
58
+ }
59
+
60
+ /**
61
+ * Filter alerts based on config
62
+ */
63
+ function filterAlerts(alerts, config) {
64
+ return alerts.filter(alert => !shouldIgnoreAlert(alert, config));
65
+ }
66
+
67
+ module.exports = {
68
+ loadConfig,
69
+ getDefaultConfig,
70
+ shouldIgnoreAlert,
71
+ filterAlerts
72
+ };
@@ -0,0 +1,33 @@
1
+ // src/utils/ci-handler.js
2
+ const chalk = require('chalk');
3
+
4
+ /**
5
+ * Handle CI mode - exit with error code if score below threshold
6
+ */
7
+ function handleCiMode(score, config, alerts, unusedDeps) {
8
+ const minScore = config.minScore || 7;
9
+
10
+ if (score.total < minScore) {
11
+ console.log(chalk.red(`\n❌ CI CHECK FAILED`));
12
+ console.log(chalk.red(`Health score ${score.total}/10 is below minimum ${minScore}/10\n`));
13
+
14
+ // Show critical issues
15
+ const criticalAlerts = alerts.filter(a => a.severity === 'critical' || a.severity === 'high');
16
+
17
+ if (criticalAlerts.length > 0) {
18
+ console.log(chalk.red(`Critical issues: ${criticalAlerts.length}`));
19
+ }
20
+
21
+ if (unusedDeps.length > 0) {
22
+ console.log(chalk.yellow(`Unused dependencies: ${unusedDeps.length}`));
23
+ }
24
+
25
+ process.exit(1); // Fail CI
26
+ } else {
27
+ console.log(chalk.green(`\n✅ CI CHECK PASSED`));
28
+ console.log(chalk.green(`Health score ${score.total}/10 meets minimum ${minScore}/10\n`));
29
+ process.exit(0);
30
+ }
31
+ }
32
+
33
+ module.exports = { handleCiMode };
@@ -0,0 +1,78 @@
1
+ // src/utils/json-formatter.js
2
+
3
+ /**
4
+ * Format analysis results as JSON
5
+ */
6
+ function formatAsJson(alerts, unusedDeps, outdatedDeps, score, totalDeps, securityData, bundleSizes, licenses) {
7
+ const problematicLicenses = licenses.filter(l => l.type === 'restrictive' || l.type === 'unknown');
8
+ const heavyPackages = bundleSizes.filter(p => p.size > 1024);
9
+
10
+ return JSON.stringify({
11
+ version: require('../../package.json').version,
12
+ timestamp: new Date().toISOString(),
13
+ summary: {
14
+ healthScore: score.total,
15
+ totalDependencies: totalDeps,
16
+ securityVulnerabilities: securityData.metadata.total,
17
+ ecosystemAlerts: alerts.length,
18
+ unusedDependencies: unusedDeps.length,
19
+ outdatedPackages: outdatedDeps.length,
20
+ heavyPackages: heavyPackages.length,
21
+ licenseWarnings: problematicLicenses.length
22
+ },
23
+ security: {
24
+ total: securityData.metadata.total,
25
+ critical: securityData.metadata.critical,
26
+ high: securityData.metadata.high,
27
+ moderate: securityData.metadata.moderate,
28
+ low: securityData.metadata.low,
29
+ vulnerabilities: securityData.vulnerabilities.map(v => ({
30
+ package: v.package,
31
+ severity: v.severity,
32
+ title: v.title,
33
+ cve: v.cve,
34
+ fixAvailable: v.fixAvailable
35
+ }))
36
+ },
37
+ ecosystemAlerts: alerts.map(alert => ({
38
+ package: alert.package,
39
+ version: alert.version,
40
+ severity: alert.severity,
41
+ title: alert.title,
42
+ affected: alert.affected,
43
+ fix: alert.fix,
44
+ source: alert.source,
45
+ reported: alert.reported
46
+ })),
47
+ unusedDependencies: unusedDeps.map(dep => ({
48
+ name: dep.name
49
+ })),
50
+ outdatedPackages: outdatedDeps.map(dep => ({
51
+ name: dep.name,
52
+ current: dep.current,
53
+ latest: dep.latest,
54
+ updateType: dep.versionsBehind
55
+ })),
56
+ bundleAnalysis: {
57
+ heavyPackages: heavyPackages.map(pkg => ({
58
+ name: pkg.name,
59
+ size: pkg.sizeFormatted
60
+ }))
61
+ },
62
+ licenses: {
63
+ warnings: problematicLicenses.map(pkg => ({
64
+ package: pkg.package,
65
+ license: pkg.license,
66
+ type: pkg.type
67
+ }))
68
+ },
69
+ scoreBreakdown: {
70
+ unusedPenalty: score.breakdown.unusedPenalty,
71
+ outdatedPenalty: score.breakdown.outdatedPenalty,
72
+ alertsPenalty: score.breakdown.alertsPenalty,
73
+ securityPenalty: score.breakdown.securityPenalty
74
+ }
75
+ }, null, 2);
76
+ }
77
+
78
+ module.exports = { formatAsJson };
@@ -1,3 +1,4 @@
1
+ // src/utils/logger.js
1
2
  const chalk = require('chalk');
2
3
 
3
4
  function log(message) {