licenseguard-cli 2.1.0 → 2.2.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.
@@ -6,8 +6,12 @@
6
6
  const chalk = require('chalk')
7
7
  const fs = require('fs')
8
8
  const path = require('path')
9
+ const inquirer = require('inquirer')
10
+ const { runInit } = require('./init')
9
11
  const { scanDependencies, displayConflictReport } = require('../scanner')
10
12
  const { detectLicenseFromText } = require('../scanner/license-detector')
13
+ const { calculateCoverage, displayCoverage } = require('../scanner/coverage-reporter')
14
+ const { generateHTML, writeHTMLFile } = require('../formatters/html-generator')
11
15
 
12
16
  /**
13
17
  * Try to detect project license from local files
@@ -41,7 +45,9 @@ function detectProjectLicense() {
41
45
  try {
42
46
  const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
43
47
  if (pkg.license) return pkg.license
44
- } catch (e) {}
48
+ } catch (e) {
49
+ // Ignore malformed package.json
50
+ }
45
51
  }
46
52
 
47
53
  if (fs.existsSync('Cargo.toml')) {
@@ -51,6 +57,188 @@ function detectProjectLicense() {
51
57
  return null
52
58
  }
53
59
 
60
+ /**
61
+ * Check if license string is valid (not null, empty, or UNLICENSED)
62
+ * @param {string|null} license - License to validate
63
+ * @returns {boolean} True if valid license
64
+ */
65
+ function isValidLicense(license) {
66
+ return !!(license && license !== 'UNLICENSED' && license.trim() !== '')
67
+ }
68
+
69
+ /**
70
+ * Detect license from Node.js package.json
71
+ * @returns {string|null} Detected license or null
72
+ */
73
+ function detectNodeLicense() {
74
+ try {
75
+ if (!fs.existsSync('package.json')) return null
76
+
77
+ const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
78
+ return pkg.license || null
79
+ } catch (error) {
80
+ return null
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Detect license from Rust Cargo.toml
86
+ * @returns {string|null} Detected license or null
87
+ */
88
+ function detectRustLicense() {
89
+ try {
90
+ if (!fs.existsSync('Cargo.toml')) return null
91
+
92
+ const content = fs.readFileSync('Cargo.toml', 'utf8')
93
+ const match = content.match(/license\s*=\s*"([^"]+)"/)
94
+ return match ? match[1] : null
95
+ } catch (error) {
96
+ return null
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Detect license from Python pyproject.toml or setup.py
102
+ * @returns {string|null} Detected license or null
103
+ */
104
+ function detectPythonLicense() {
105
+ // Try pyproject.toml first
106
+ try {
107
+ if (fs.existsSync('pyproject.toml')) {
108
+ const content = fs.readFileSync('pyproject.toml', 'utf8')
109
+ // Try different patterns for pyproject.toml
110
+ let match = content.match(/license\s*=\s*["{]([^"'}]+)["}]/)
111
+ if (!match) {
112
+ // Try project.license pattern
113
+ match = content.match(/project\.license\s*=\s*["{]([^"'}]+)["}]/)
114
+ }
115
+ if (match) return match[1]
116
+ }
117
+ } catch (error) {
118
+ // Continue to setup.py fallback
119
+ }
120
+
121
+ // Fallback to setup.py
122
+ try {
123
+ if (fs.existsSync('setup.py')) {
124
+ const content = fs.readFileSync('setup.py', 'utf8')
125
+ const match = content.match(/license\s*=\s*["']([^"']+)["']/)
126
+ if (match) return match[1]
127
+ }
128
+ } catch (error) {
129
+ // Ignore
130
+ }
131
+
132
+ return null
133
+ }
134
+
135
+ /**
136
+ * Detect license from Go go.mod (less standardized)
137
+ * @returns {string|null} Detected license or null
138
+ */
139
+ function detectGoLicense() {
140
+ try {
141
+ // Check go.mod for license comment
142
+ if (fs.existsSync('go.mod')) {
143
+ const content = fs.readFileSync('go.mod', 'utf8')
144
+ const match = content.match(/\/\/\s*license:\s*([^\n]+)/i)
145
+ if (match) return match[1].trim()
146
+ }
147
+
148
+ // Check common source files for license headers
149
+ const sourceFiles = ['main.go', 'LICENSE', 'LICENSE.txt']
150
+ for (const file of sourceFiles) {
151
+ if (fs.existsSync(file)) {
152
+ const content = fs.readFileSync(file, 'utf8')
153
+ // Look for SPDX-License-Identifier
154
+ const match = content.match(/SPDX-License-Identifier:\s*([^\n]+)/i)
155
+ if (match) return match[1].trim()
156
+ }
157
+ }
158
+ } catch (error) {
159
+ // Ignore
160
+ }
161
+
162
+ return null
163
+ }
164
+
165
+ /**
166
+ * Detect license from C++ conanfile.txt or CMakeLists.txt
167
+ * @returns {string|null} Detected license or null
168
+ */
169
+ function detectCppLicense() {
170
+ try {
171
+ // Try conanfile.txt
172
+ if (fs.existsSync('conanfile.txt')) {
173
+ const content = fs.readFileSync('conanfile.txt', 'utf8')
174
+ const match = content.match(/license\s*=\s*([^\n]+)/)
175
+ if (match) return match[1].trim()
176
+ }
177
+
178
+ // Try CMakeLists.txt
179
+ if (fs.existsSync('CMakeLists.txt')) {
180
+ const content = fs.readFileSync('CMakeLists.txt', 'utf8')
181
+ // Look for license comment
182
+ const match = content.match(/#\s*license:\s*([^\n]+)/i)
183
+ if (match) return match[1].trim()
184
+ }
185
+ } catch (error) {
186
+ // Ignore
187
+ }
188
+
189
+ return null
190
+ }
191
+
192
+ /**
193
+ * Auto-detect project license from package manager files
194
+ * Tries multiple ecosystems in priority order
195
+ * @returns {string|null} Detected license or null
196
+ */
197
+ function autoDetectLicense() {
198
+ const strategies = [
199
+ { name: 'package.json', detector: detectNodeLicense },
200
+ { name: 'Cargo.toml', detector: detectRustLicense },
201
+ { name: 'pyproject.toml/setup.py', detector: detectPythonLicense },
202
+ { name: 'go.mod', detector: detectGoLicense },
203
+ { name: 'conanfile.txt/CMakeLists.txt', detector: detectCppLicense }
204
+ ]
205
+
206
+ for (const strategy of strategies) {
207
+ const license = strategy.detector()
208
+ if (isValidLicense(license)) {
209
+ return license
210
+ }
211
+ }
212
+
213
+ return null // Could not auto-detect
214
+ }
215
+
216
+ /**
217
+ * Check if running in interactive mode (TTY and not CI)
218
+ * @returns {boolean} True if interactive mode
219
+ */
220
+ function isInteractive() {
221
+ return process.stdin.isTTY && !process.env.CI
222
+ }
223
+
224
+ /**
225
+ * Get project name from package.json or current directory
226
+ * @returns {string} Project name
227
+ */
228
+ function getProjectName() {
229
+ try {
230
+ const pkgPath = path.join(process.cwd(), 'package.json')
231
+ if (fs.existsSync(pkgPath)) {
232
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
233
+ return pkg.name || path.basename(process.cwd())
234
+ }
235
+ } catch (error) {
236
+ // Ignore errors, fall back to directory name
237
+ }
238
+
239
+ return path.basename(process.cwd())
240
+ }
241
+
54
242
  /**
55
243
  * Run the scan command
56
244
  * @param {Object} options - Command options
@@ -67,10 +255,59 @@ async function runScan(options) {
67
255
 
68
256
  console.log(chalk.blue(`šŸ“‚ Scanning in: ${process.cwd()}`))
69
257
 
70
- // 2. Determine Project License
258
+ // 2. Check for config file existence
259
+ const configPath = '.licenseguardrc'
260
+ const configExists = fs.existsSync(configPath)
261
+
262
+ // 3. Determine Project License
71
263
  let projectLicense = options.license
72
264
 
73
- if (!projectLicense) {
265
+ if (!projectLicense && !configExists) {
266
+ // Config missing - determine mode (interactive vs CI/CD)
267
+ const interactive = isInteractive()
268
+
269
+ if (interactive) {
270
+ // Interactive mode: Prompt user to run init
271
+ console.log(chalk.yellow('āš ļø Configuration not found.\n'))
272
+ console.log('LicenseGuard needs to know your project\'s license to detect conflicts.')
273
+ console.log(chalk.gray('(e.g. MIT projects can\'t use GPL libraries)\n'))
274
+
275
+ const answer = await inquirer.prompt([{
276
+ type: 'confirm',
277
+ name: 'initNow',
278
+ message: 'Would you like to initialize configuration now?',
279
+ default: true
280
+ }])
281
+
282
+ if (answer.initNow) {
283
+ // Run init interactively
284
+ await runInit(options)
285
+ console.log(chalk.blue('\nšŸ” Continuing with scan...\n'))
286
+ // Config now exists, reload it
287
+ if (fs.existsSync(configPath)) {
288
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'))
289
+ projectLicense = config.license
290
+ }
291
+ } else {
292
+ console.log(chalk.yellow('Okay, scanning aborted. Run licenseguard init when ready.'))
293
+ process.exit(0)
294
+ }
295
+ } else {
296
+ // CI/CD mode: Auto-detect license
297
+ projectLicense = autoDetectLicense()
298
+
299
+ if (!projectLicense) {
300
+ console.error(chalk.red('āœ— Error: No configuration found and license could not be auto-detected.'))
301
+ console.log(chalk.yellow('\nFor CI/CD usage, commit .licenseguardrc to your repository:'))
302
+ console.log(chalk.blue(' 1. Run: licenseguard init'))
303
+ console.log(chalk.blue(' 2. Commit: git add .licenseguardrc && git commit\n'))
304
+ process.exit(1)
305
+ }
306
+
307
+ console.log(chalk.blue(`ā„¹ļø Auto-detected project license: ${projectLicense} (from package manager)\n`))
308
+ }
309
+ } else if (!projectLicense) {
310
+ // Config exists, try to load it
74
311
  projectLicense = detectProjectLicense()
75
312
  if (projectLicense) {
76
313
  console.log(chalk.blue(`ā„¹ļø Detected project license: ${projectLicense}`))
@@ -91,10 +328,21 @@ async function runScan(options) {
91
328
  const results = await scanDependencies(projectLicense)
92
329
 
93
330
  // 4. Display Report
94
- const hasConflicts = displayConflictReport(results, projectLicense, {
331
+ const hasConflicts = await displayConflictReport(results, projectLicense, {
95
332
  explain: options.explain
96
333
  })
97
334
 
335
+ // 4.5. Display Coverage Report
336
+ const coverage = calculateCoverage(results)
337
+ displayCoverage(coverage)
338
+
339
+ // 4.6. Generate HTML Attribution (if requested)
340
+ if (options.format === 'html') {
341
+ const projectName = getProjectName()
342
+ const html = generateHTML(results, projectName)
343
+ writeHTMLFile(html, './CREDITS.html')
344
+ }
345
+
98
346
  // 5. Handle Exit Code
99
347
  if (hasConflicts && !options.allow) {
100
348
  console.log(chalk.red('\nāŒ Scan failed: License conflicts detected.'))
@@ -118,4 +366,15 @@ async function runScan(options) {
118
366
  }
119
367
  }
120
368
 
121
- module.exports = { runScan }
369
+ module.exports = {
370
+ runScan,
371
+ autoDetectLicense,
372
+ detectNodeLicense,
373
+ detectRustLicense,
374
+ detectPythonLicense,
375
+ detectGoLicense,
376
+ detectCppLicense,
377
+ isValidLicense,
378
+ isInteractive,
379
+ getProjectName
380
+ }
@@ -0,0 +1,232 @@
1
+ const fs = require('fs')
2
+ const chalk = require('chalk')
3
+
4
+ /**
5
+ * Generates mobile-optimized HTML attribution page
6
+ * @param {Object} scanResults - Scan results from scanner
7
+ * @param {Array} scanResults.packages - Array of package objects
8
+ * @param {string} projectName - Project name for title
9
+ * @returns {string} HTML content ready to write
10
+ * @example
11
+ * const html = generateHTML(scanResults, 'MyApp')
12
+ * fs.writeFileSync('CREDITS.html', html)
13
+ */
14
+ function generateHTML(scanResults, projectName) {
15
+ const template = `<!DOCTYPE html>
16
+ <html lang="en">
17
+ <head>
18
+ <meta charset="UTF-8">
19
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
20
+ <title>Open Source Licenses - ${escapeHtml(projectName)}</title>
21
+ <style>
22
+ body {
23
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
24
+ line-height: 1.6;
25
+ max-width: 800px;
26
+ margin: 0 auto;
27
+ padding: 20px;
28
+ background: #f5f5f5;
29
+ color: #333;
30
+ }
31
+ h1 {
32
+ color: #333;
33
+ border-bottom: 2px solid #007AFF;
34
+ padding-bottom: 10px;
35
+ }
36
+ .package {
37
+ background: white;
38
+ padding: 15px;
39
+ margin: 10px 0;
40
+ border-radius: 8px;
41
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
42
+ }
43
+ .package-name {
44
+ font-weight: bold;
45
+ color: #007AFF;
46
+ font-size: 1.1em;
47
+ }
48
+ .license {
49
+ color: #666;
50
+ font-size: 0.9em;
51
+ margin-top: 5px;
52
+ }
53
+ .license-text {
54
+ background: #f9f9f9;
55
+ padding: 10px;
56
+ margin-top: 10px;
57
+ border-left: 3px solid #007AFF;
58
+ font-size: 0.85em;
59
+ white-space: pre-wrap;
60
+ display: none;
61
+ font-family: monospace;
62
+ }
63
+ .show-license {
64
+ cursor: pointer;
65
+ color: #007AFF;
66
+ text-decoration: underline;
67
+ font-size: 0.9em;
68
+ margin-top: 10px;
69
+ display: inline-block;
70
+ min-height: 44px;
71
+ min-width: 44px;
72
+ padding: 12px;
73
+ line-height: 20px;
74
+ }
75
+ .footer {
76
+ text-align: center;
77
+ margin-top: 40px;
78
+ color: #999;
79
+ font-size: 0.85em;
80
+ }
81
+ .footer a {
82
+ color: #007AFF;
83
+ text-decoration: none;
84
+ }
85
+ .footer a:hover {
86
+ text-decoration: underline;
87
+ }
88
+ @media (max-width: 768px) {
89
+ body {
90
+ padding: 10px;
91
+ }
92
+ .package {
93
+ padding: 12px;
94
+ }
95
+ }
96
+ @media (prefers-color-scheme: dark) {
97
+ body {
98
+ background: #1c1c1e;
99
+ color: #f5f5f5;
100
+ }
101
+ .package {
102
+ background: #2c2c2e;
103
+ }
104
+ .license-text {
105
+ background: #3a3a3c;
106
+ }
107
+ h1 {
108
+ color: #f5f5f5;
109
+ }
110
+ }
111
+ </style>
112
+ </head>
113
+ <body>
114
+ <h1>Open Source Licenses</h1>
115
+ <p>This application uses the following open source libraries:</p>
116
+
117
+ ${generatePackageCards(scanResults.packages)}
118
+
119
+ <div class="footer">
120
+ <p>Generated by <a href="https://www.npmjs.com/package/licenseguard-cli">LicenseGuard</a> v${getVersion()}</p>
121
+ </div>
122
+
123
+ <script>
124
+ // Progressive enhancement: show/hide license text
125
+ document.querySelectorAll('.show-license').forEach(link => {
126
+ link.addEventListener('click', function() {
127
+ const licenseText = this.nextElementSibling
128
+ if (licenseText.style.display === 'none' || !licenseText.style.display) {
129
+ licenseText.style.display = 'block'
130
+ this.textContent = 'Hide license text'
131
+ } else {
132
+ licenseText.style.display = 'none'
133
+ this.textContent = 'Show license text'
134
+ }
135
+ })
136
+ })
137
+ </script>
138
+ </body>
139
+ </html>`
140
+
141
+ return template
142
+ }
143
+
144
+ /**
145
+ * Generates HTML package cards for all packages
146
+ * @param {Array} packages - Array of package objects
147
+ * @returns {string} HTML string with all package cards
148
+ */
149
+ function generatePackageCards(packages) {
150
+ if (!packages || packages.length === 0) {
151
+ return '<div class="package"><p>No packages found</p></div>'
152
+ }
153
+
154
+ // Sort alphabetically by package name
155
+ const sortedPackages = [...packages].sort((a, b) =>
156
+ a.name.localeCompare(b.name)
157
+ )
158
+
159
+ return sortedPackages.map(pkg => {
160
+ const licenseTextHtml = pkg.licenseText
161
+ ? `
162
+ <div class="show-license">Show license text</div>
163
+ <div class="license-text">${escapeHtml(pkg.licenseText)}</div>
164
+ `
165
+ : ''
166
+
167
+ return `
168
+ <div class="package">
169
+ <div class="package-name">${escapeHtml(pkg.name)} v${escapeHtml(pkg.version)}</div>
170
+ <div class="license">License: ${escapeHtml(pkg.license)}</div>${licenseTextHtml}
171
+ </div>
172
+ `.trim()
173
+ }).join('\n')
174
+ }
175
+
176
+ /**
177
+ * Escapes HTML special characters to prevent XSS
178
+ * @param {string} text - Text to escape
179
+ * @returns {string} Escaped text safe for HTML
180
+ */
181
+ function escapeHtml(text) {
182
+ if (!text) return ''
183
+
184
+ return String(text)
185
+ .replace(/&/g, '&amp;')
186
+ .replace(/</g, '&lt;')
187
+ .replace(/>/g, '&gt;')
188
+ .replace(/"/g, '&quot;')
189
+ .replace(/'/g, '&#39;')
190
+ }
191
+
192
+ /**
193
+ * Gets LicenseGuard version from package.json
194
+ * @returns {string} Version string
195
+ */
196
+ function getVersion() {
197
+ try {
198
+ const packageJson = require('../../package.json')
199
+ return packageJson.version
200
+ } catch (error) {
201
+ return '2.2.0'
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Writes HTML file to disk with error handling
207
+ * @param {string} html - HTML content to write
208
+ * @param {string} outputPath - Path to write file
209
+ */
210
+ function writeHTMLFile(html, outputPath) {
211
+ try {
212
+ fs.writeFileSync(outputPath, html, 'utf8')
213
+ console.log(chalk.green(`āœ“ Attribution page saved to: ${outputPath}`))
214
+ } catch (error) {
215
+ if (error.code === 'EACCES') {
216
+ console.error(chalk.red('āœ— Error: Could not write CREDITS.html (permission denied)'))
217
+ console.log(chalk.yellow('Try running with elevated permissions or choose different output directory'))
218
+ } else if (error.code === 'ENOSPC') {
219
+ console.error(chalk.red('āœ— Error: Could not write CREDITS.html (disk full)'))
220
+ } else {
221
+ console.error(chalk.red(`āœ— Error: Could not write CREDITS.html (${error.message})`))
222
+ }
223
+ process.exit(1)
224
+ }
225
+ }
226
+
227
+ module.exports = {
228
+ generateHTML,
229
+ escapeHtml,
230
+ generatePackageCards,
231
+ writeHTMLFile
232
+ }
@@ -0,0 +1,87 @@
1
+ const chalk = require('chalk')
2
+
3
+ // License color mapping based on safety level
4
+ const LICENSE_COLORS = {
5
+ // Green: Permissive licenses
6
+ 'MIT': 'green',
7
+ 'Apache-2.0': 'green',
8
+ 'BSD-2-Clause': 'green',
9
+ 'BSD-3-Clause': 'green',
10
+ 'ISC': 'green',
11
+ '0BSD': 'green',
12
+ 'Unlicense': 'green',
13
+
14
+ // Yellow: Weak copyleft
15
+ 'MPL-2.0': 'yellow',
16
+ 'LGPL-2.1': 'yellow',
17
+ 'LGPL-3.0': 'yellow',
18
+ 'EPL-1.0': 'yellow',
19
+ 'EPL-2.0': 'yellow',
20
+
21
+ // Red: Strong copyleft
22
+ 'GPL-2.0': 'red',
23
+ 'GPL-3.0': 'red',
24
+ 'AGPL-3.0': 'red',
25
+
26
+ // Gray: Unknown
27
+ 'UNKNOWN': 'gray'
28
+ }
29
+
30
+ // Emoji indicators for accessibility
31
+ const EMOJI_INDICATORS = {
32
+ green: '🟢',
33
+ yellow: 'āš ļø',
34
+ red: 'āŒ',
35
+ gray: 'ā”'
36
+ }
37
+
38
+ /**
39
+ * Get color for a given license
40
+ * @param {string} license - License identifier (e.g., "MIT", "GPL-3.0")
41
+ * @returns {string} - Color name (green/yellow/red/gray)
42
+ */
43
+ function getColor(license) {
44
+ return LICENSE_COLORS[license] || 'gray'
45
+ }
46
+
47
+ /**
48
+ * Get emoji indicator for a given license
49
+ * @param {string} license - License identifier
50
+ * @returns {string} - Emoji indicator
51
+ */
52
+ function getEmoji(license) {
53
+ const color = getColor(license)
54
+ return EMOJI_INDICATORS[color]
55
+ }
56
+
57
+ /**
58
+ * Colorize text based on license safety level
59
+ * @param {string} license - License identifier
60
+ * @param {string} text - Text to colorize
61
+ * @returns {string} - Colorized text with chalk
62
+ */
63
+ function colorize(license, text) {
64
+ const color = getColor(license)
65
+ return chalk[color](text)
66
+ }
67
+
68
+ /**
69
+ * Colorize text with emoji indicator
70
+ * @param {string} license - License identifier
71
+ * @param {string} text - Text to colorize
72
+ * @returns {string} - Colorized text with emoji prefix
73
+ */
74
+ function colorizeWithEmoji(license, text) {
75
+ const emoji = getEmoji(license)
76
+ const coloredText = colorize(license, text)
77
+ return `${emoji} ${coloredText}`
78
+ }
79
+
80
+ module.exports = {
81
+ getColor,
82
+ getEmoji,
83
+ colorize,
84
+ colorizeWithEmoji,
85
+ LICENSE_COLORS,
86
+ EMOJI_INDICATORS
87
+ }
@@ -0,0 +1,84 @@
1
+ const chalk = require('chalk')
2
+
3
+ /**
4
+ * Calculate coverage statistics from scan results
5
+ * @param {Object} scanResults - Scan results object
6
+ * @returns {Object} Coverage statistics { total, identified, unknown, percentage }
7
+ */
8
+ function calculateCoverage(scanResults) {
9
+ // Handle cases where scanResults might be undefined or null
10
+ if (!scanResults) {
11
+ return {
12
+ total: 0,
13
+ identified: 0,
14
+ unknown: 0,
15
+ percentage: 0.0,
16
+ }
17
+ }
18
+
19
+ // Support both old format (packages array) and new format (totalDependencies)
20
+ let totalPackages = 0
21
+ let unknownPackages = 0
22
+
23
+ if (scanResults.packages) {
24
+ // Old format: packages array
25
+ totalPackages = scanResults.packages.length
26
+ unknownPackages = scanResults.packages.filter(p => p.license === 'UNKNOWN').length
27
+ } else if (scanResults.totalDependencies !== undefined) {
28
+ // New format: totalDependencies, compatible, incompatible, unknown
29
+ totalPackages = scanResults.totalDependencies
30
+ unknownPackages = scanResults.unknown || 0
31
+ } else {
32
+ // Unknown format
33
+ return {
34
+ total: 0,
35
+ identified: 0,
36
+ unknown: 0,
37
+ percentage: 0.0,
38
+ }
39
+ }
40
+
41
+ const identifiedPackages = totalPackages - unknownPackages
42
+ const percentage = totalPackages > 0
43
+ ? ((identifiedPackages / totalPackages) * 100).toFixed(1)
44
+ : '0.0'
45
+
46
+ return {
47
+ total: totalPackages,
48
+ identified: identifiedPackages,
49
+ unknown: unknownPackages,
50
+ percentage: parseFloat(percentage),
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Get emoji indicator based on coverage percentage
56
+ * @param {number} percentage - Coverage percentage
57
+ * @returns {string} Emoji indicator (āœ…/āš ļø/āŒ)
58
+ */
59
+ function getCoverageEmoji(percentage) {
60
+ if (percentage >= 90) return 'āœ…'
61
+ if (percentage >= 70) return 'āš ļø'
62
+ return 'āŒ'
63
+ }
64
+
65
+ /**
66
+ * Display coverage report to console
67
+ * @param {Object} coverage - Coverage object from calculateCoverage
68
+ */
69
+ function displayCoverage(coverage) {
70
+ console.log(chalk.blue('\nšŸ“Š Scan Coverage:'))
71
+
72
+ const emoji = getCoverageEmoji(coverage.percentage)
73
+ console.log(`${emoji} ${coverage.identified}/${coverage.total} packages identified (${coverage.percentage}%)`)
74
+
75
+ if (coverage.unknown > 0) {
76
+ console.log(chalk.yellow(`āš ļø ${coverage.unknown} packages with unknown licenses`))
77
+ }
78
+
79
+ if (coverage.percentage < 80) {
80
+ console.log(chalk.yellow('\nšŸ’” Tip: Some packages may need manual LICENSE file inspection'))
81
+ }
82
+ }
83
+
84
+ module.exports = { calculateCoverage, displayCoverage, getCoverageEmoji }