licenseguard-cli 1.2.2 → 2.1.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/LICENSE +2 -2
- package/README.md +355 -157
- package/bin/licenseguard.js +53 -34
- package/lib/commands/init-fast.js +62 -3
- package/lib/commands/init.js +74 -4
- package/lib/commands/scan.js +121 -0
- package/lib/scanner/compat-checker.js +433 -0
- package/lib/scanner/index.js +131 -0
- package/lib/scanner/license-compatibility-matrix.json +338 -0
- package/lib/scanner/license-detector.js +847 -0
- package/lib/scanner/license-normalizer.js +357 -0
- package/lib/scanner/plugins/cpp.js +267 -0
- package/lib/scanner/plugins/go.js +421 -0
- package/lib/scanner/plugins/node.js +149 -0
- package/lib/scanner/plugins/python-license-scanner.py +173 -0
- package/lib/scanner/plugins/python.js +336 -0
- package/lib/scanner/plugins/rust.js +196 -0
- package/lib/scanner/progress.js +22 -0
- package/lib/utils/file-ops.js +18 -1
- package/lib/utils/license-mapper.js +28 -0
- package/package.json +21 -5
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* License compatibility checker
|
|
3
|
+
* Uses SPDX libraries for standard licenses + custom rules for non-SPDX
|
|
4
|
+
* Enhanced with authoritative compatibility matrix (FSF, Mozilla, Apache sources)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const parse = require('spdx-expression-parse')
|
|
8
|
+
const { normalize, areSameLicense } = require('./license-normalizer')
|
|
9
|
+
const COMPAT_MATRIX = require('./license-compatibility-matrix.json')
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Custom compatibility rules for non-SPDX licenses
|
|
13
|
+
* @type {Object}
|
|
14
|
+
*/
|
|
15
|
+
const CUSTOM_COMPAT = {
|
|
16
|
+
wtfpl: {
|
|
17
|
+
type: 'permissive',
|
|
18
|
+
compatibleWith: '*', // Compatible with everything
|
|
19
|
+
description: 'Do What The F*ck You Want To Public License'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Known copyleft license patterns that impose restrictions
|
|
25
|
+
* These require derivative works to use the same license
|
|
26
|
+
*/
|
|
27
|
+
const COPYLEFT_PATTERNS = [
|
|
28
|
+
'GPL', // GNU General Public License (GPL-2.0, GPL-3.0, etc.)
|
|
29
|
+
'AGPL', // GNU Affero General Public License
|
|
30
|
+
'LGPL', // GNU Lesser General Public License
|
|
31
|
+
'MPL', // Mozilla Public License
|
|
32
|
+
'EPL', // Eclipse Public License
|
|
33
|
+
'EUPL', // European Union Public License
|
|
34
|
+
'CDDL', // Common Development and Distribution License
|
|
35
|
+
'CPL', // Common Public License
|
|
36
|
+
'APSL', // Apple Public Source License
|
|
37
|
+
'OSL', // Open Software License
|
|
38
|
+
'QPL', // Q Public License
|
|
39
|
+
'RPSL', // RealNetworks Public Source License
|
|
40
|
+
'SISSL', // Sun Industry Standards Source License
|
|
41
|
+
'SPL', // Sun Public License
|
|
42
|
+
'Watcom' // Sybase Open Watcom Public License
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Permissive license exceptions that might contain misleading patterns
|
|
47
|
+
* These are ultra-permissive and should ALWAYS be allowed
|
|
48
|
+
*/
|
|
49
|
+
const PERMISSIVE_EXCEPTIONS = [
|
|
50
|
+
'WTFPL', // Do What The F*ck You Want To Public License
|
|
51
|
+
'Unlicense', // Public domain dedication
|
|
52
|
+
'CC0', // Creative Commons Zero (public domain)
|
|
53
|
+
'0BSD' // BSD Zero Clause (public domain equivalent)
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Detect if a license is copyleft (requires derivative works to use same license)
|
|
58
|
+
* Uses smart pattern matching instead of hardcoded whitelist
|
|
59
|
+
*
|
|
60
|
+
* @param {string} license - SPDX license identifier
|
|
61
|
+
* @returns {boolean} True if copyleft, false if permissive
|
|
62
|
+
*
|
|
63
|
+
* Algorithm:
|
|
64
|
+
* 1. Check if license is ultra-permissive exception → return false
|
|
65
|
+
* 2. Check if license contains copyleft pattern → return true
|
|
66
|
+
* 3. Default to permissive (safe assumption for ~95% of licenses)
|
|
67
|
+
*/
|
|
68
|
+
function isCopyleft(license) {
|
|
69
|
+
if (!license) return false
|
|
70
|
+
|
|
71
|
+
const upper = license.toUpperCase()
|
|
72
|
+
|
|
73
|
+
// Tier 1: Ultra-permissive exceptions (always allow)
|
|
74
|
+
if (PERMISSIVE_EXCEPTIONS.some(p => upper.includes(p.toUpperCase()))) {
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Tier 2: Known copyleft patterns (block these)
|
|
79
|
+
if (COPYLEFT_PATTERNS.some(pattern => upper.includes(pattern))) {
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Tier 3: Default to permissive (safe assumption)
|
|
84
|
+
// Most licenses are permissive (MIT-like, BSD-like, Apache-like)
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check license compatibility using authoritative compatibility matrix
|
|
90
|
+
* Handles SPDX normalization, upgrade paths, and explicit compatibility rules
|
|
91
|
+
*
|
|
92
|
+
* @param {string} projectLicense - The project's license (will be normalized)
|
|
93
|
+
* @param {string} depLicense - The dependency's license (will be normalized)
|
|
94
|
+
* @returns {{compatible: boolean, reason: string, severity: string, source: object|null}} Enhanced compatibility result
|
|
95
|
+
*
|
|
96
|
+
* Algorithm:
|
|
97
|
+
* 1. Normalize both licenses to canonical SPDX form
|
|
98
|
+
* 2. Check if same license (compatible)
|
|
99
|
+
* 3. Lookup project license in matrix
|
|
100
|
+
* 4. Check explicit compatibility/incompatibility rules
|
|
101
|
+
* 5. Check upgrade paths (LGPL→GPL, MPL→GPL)
|
|
102
|
+
* 6. Expand wildcards (*permissive*, *copyleft*)
|
|
103
|
+
* 7. Return result with severity and source citations
|
|
104
|
+
*/
|
|
105
|
+
function checkWithMatrix(projectLicense, depLicense) {
|
|
106
|
+
// Normalize licenses to canonical SPDX form
|
|
107
|
+
const normalizedProject = normalize(projectLicense)
|
|
108
|
+
const normalizedDep = normalize(depLicense)
|
|
109
|
+
|
|
110
|
+
// Same license is always compatible
|
|
111
|
+
if (areSameLicense(normalizedProject, normalizedDep)) {
|
|
112
|
+
return {
|
|
113
|
+
compatible: true,
|
|
114
|
+
reason: `Same license (${normalizedDep})`,
|
|
115
|
+
severity: 'PASS',
|
|
116
|
+
source: null
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Lookup project license in matrix
|
|
121
|
+
const projectEntry = COMPAT_MATRIX.licenses[normalizedProject]
|
|
122
|
+
|
|
123
|
+
if (!projectEntry) {
|
|
124
|
+
// Project license not in matrix - fall back to conservative check
|
|
125
|
+
return {
|
|
126
|
+
compatible: true, // Conservative: allow unknown combinations with warning
|
|
127
|
+
reason: `Unknown project license (${normalizedProject}) - unable to verify compatibility`,
|
|
128
|
+
severity: 'WARNING',
|
|
129
|
+
source: null
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check explicit incompatibility first
|
|
134
|
+
if (projectEntry.incompatible_with) {
|
|
135
|
+
// Direct match
|
|
136
|
+
if (projectEntry.incompatible_with.includes(normalizedDep)) {
|
|
137
|
+
return {
|
|
138
|
+
compatible: false,
|
|
139
|
+
reason: `${normalizedDep} explicitly incompatible with ${normalizedProject}`,
|
|
140
|
+
severity: 'ERROR',
|
|
141
|
+
source: projectEntry.sources
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Wildcard match (*copyleft*, *permissive*)
|
|
146
|
+
for (const incompatRule of projectEntry.incompatible_with) {
|
|
147
|
+
if (incompatRule.startsWith('*') && incompatRule.endsWith('*')) {
|
|
148
|
+
const wildcardKey = incompatRule
|
|
149
|
+
if (COMPAT_MATRIX.wildcards[wildcardKey] && COMPAT_MATRIX.wildcards[wildcardKey].includes(normalizedDep)) {
|
|
150
|
+
return {
|
|
151
|
+
compatible: false,
|
|
152
|
+
reason: `${normalizedDep} (${wildcardKey.replace(/\*/g, '')}) incompatible with ${normalizedProject}`,
|
|
153
|
+
severity: 'ERROR',
|
|
154
|
+
source: projectEntry.sources
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check explicit compatibility
|
|
162
|
+
if (projectEntry.compatible_with) {
|
|
163
|
+
// Direct match
|
|
164
|
+
if (projectEntry.compatible_with.includes(normalizedDep)) {
|
|
165
|
+
return {
|
|
166
|
+
compatible: true,
|
|
167
|
+
reason: `${normalizedDep} explicitly compatible with ${normalizedProject}`,
|
|
168
|
+
severity: 'PASS',
|
|
169
|
+
source: projectEntry.sources
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Upgrade path (LGPL→GPL, MPL→GPL)
|
|
174
|
+
if (projectEntry.can_upgrade_from && projectEntry.can_upgrade_from.includes(normalizedDep)) {
|
|
175
|
+
return {
|
|
176
|
+
compatible: true,
|
|
177
|
+
reason: `${normalizedDep} can upgrade to ${normalizedProject} (Section 3 upgrade path)`,
|
|
178
|
+
severity: 'PASS',
|
|
179
|
+
source: projectEntry.sources
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Wildcard match (*permissive*, *copyleft*, *)
|
|
184
|
+
for (const compatRule of projectEntry.compatible_with) {
|
|
185
|
+
if (compatRule === '*') {
|
|
186
|
+
// Universal compatibility (public domain)
|
|
187
|
+
return {
|
|
188
|
+
compatible: true,
|
|
189
|
+
reason: `${normalizedDep} compatible with ${normalizedProject} (public domain)`,
|
|
190
|
+
severity: 'PASS',
|
|
191
|
+
source: projectEntry.sources
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (compatRule.startsWith('*') && compatRule.endsWith('*')) {
|
|
196
|
+
const wildcardKey = compatRule
|
|
197
|
+
if (COMPAT_MATRIX.wildcards[wildcardKey] && COMPAT_MATRIX.wildcards[wildcardKey].includes(normalizedDep)) {
|
|
198
|
+
return {
|
|
199
|
+
compatible: true,
|
|
200
|
+
reason: `${normalizedDep} (${wildcardKey.replace(/\*/g, '')}) compatible with ${normalizedProject}`,
|
|
201
|
+
severity: 'PASS',
|
|
202
|
+
source: projectEntry.sources
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// No explicit rule found - conservative default
|
|
210
|
+
return {
|
|
211
|
+
compatible: true, // Conservative: allow with warning
|
|
212
|
+
reason: `No explicit compatibility rule for ${normalizedDep} + ${normalizedProject} - verify manually`,
|
|
213
|
+
severity: 'WARNING',
|
|
214
|
+
source: null
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Check compatibility of a single (atomic) license identifier
|
|
220
|
+
* Uses authoritative compatibility matrix for copyleft combinations
|
|
221
|
+
*
|
|
222
|
+
* @param {string} projectLicense - The project's license
|
|
223
|
+
* @param {string} depLicense - Single license identifier (not expression)
|
|
224
|
+
* @returns {{compatible: boolean, reason: string}} Compatibility result
|
|
225
|
+
*/
|
|
226
|
+
function checkSingleCompatibility(projectLicense, depLicense) {
|
|
227
|
+
// Normalize licenses first for accurate same-license detection
|
|
228
|
+
const normalizedProject = normalize(projectLicense)
|
|
229
|
+
const normalizedDep = normalize(depLicense)
|
|
230
|
+
|
|
231
|
+
// Same license is always compatible (handles GPL-3.0 vs GPL-3.0-only)
|
|
232
|
+
if (areSameLicense(normalizedProject, normalizedDep)) {
|
|
233
|
+
return { compatible: true, reason: `Same license (${normalizedDep})` }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Smart copyleft detection
|
|
237
|
+
const projectIsCopyleft = isCopyleft(normalizedProject)
|
|
238
|
+
const depIsCopyleft = isCopyleft(normalizedDep)
|
|
239
|
+
|
|
240
|
+
// Case 1: Permissive project + copyleft dependency = INCOMPATIBLE
|
|
241
|
+
if (!projectIsCopyleft && depIsCopyleft) {
|
|
242
|
+
return {
|
|
243
|
+
compatible: false,
|
|
244
|
+
reason: `Copyleft license ${normalizedDep} incompatible with permissive ${normalizedProject}`
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Case 2: Copyleft project + permissive dependency
|
|
249
|
+
// IMPORTANT: Still check matrix because some permissive licenses have
|
|
250
|
+
// specific incompatibilities (e.g., Apache-2.0 + GPL-2.0 patent conflict)
|
|
251
|
+
if (projectIsCopyleft && !depIsCopyleft) {
|
|
252
|
+
const matrixResult = checkWithMatrix(normalizedProject, normalizedDep)
|
|
253
|
+
// If matrix says incompatible, trust it (handles Apache-2.0 + GPL-2.0)
|
|
254
|
+
if (!matrixResult.compatible) {
|
|
255
|
+
return {
|
|
256
|
+
compatible: false,
|
|
257
|
+
reason: matrixResult.reason
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Otherwise, permissive dep is compatible with copyleft project
|
|
261
|
+
return {
|
|
262
|
+
compatible: true,
|
|
263
|
+
reason: 'Permissive dependency compatible with copyleft project'
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Case 3: Both permissive = COMPATIBLE
|
|
268
|
+
if (!projectIsCopyleft && !depIsCopyleft) {
|
|
269
|
+
return {
|
|
270
|
+
compatible: true,
|
|
271
|
+
reason: 'Both permissive licenses'
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Case 4: Both copyleft - USE MATRIX INSTEAD OF NAIVE LOGIC
|
|
276
|
+
// CRITICAL FIX: This replaces the naive "different copyleft = incompatible" logic
|
|
277
|
+
if (projectIsCopyleft && depIsCopyleft) {
|
|
278
|
+
const matrixResult = checkWithMatrix(normalizedProject, normalizedDep)
|
|
279
|
+
// Return simplified result (remove severity/source for backward compatibility)
|
|
280
|
+
return {
|
|
281
|
+
compatible: matrixResult.compatible,
|
|
282
|
+
reason: matrixResult.reason
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Fallback
|
|
287
|
+
return { compatible: true, reason: 'No known incompatibility' }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Recursively evaluate SPDX expression AST for compatibility
|
|
292
|
+
* Handles OR (disjunctive) and AND (conjunctive) expressions
|
|
293
|
+
*
|
|
294
|
+
* @param {string} projectLicense - The project's license
|
|
295
|
+
* @param {Object} licenseNode - AST node from spdx-expression-parse
|
|
296
|
+
* @returns {{compatible: boolean, reason: string}} Compatibility result
|
|
297
|
+
*/
|
|
298
|
+
function isCompatibleRecursive(projectLicense, licenseNode) {
|
|
299
|
+
// Base Case: Leaf node with 'license' property
|
|
300
|
+
if (licenseNode.license) {
|
|
301
|
+
return checkSingleCompatibility(projectLicense, licenseNode.license)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Handle "OR" (Disjunctive) - User can choose either
|
|
305
|
+
if (licenseNode.conjunction === 'or') {
|
|
306
|
+
const left = isCompatibleRecursive(projectLicense, licenseNode.left)
|
|
307
|
+
const right = isCompatibleRecursive(projectLicense, licenseNode.right)
|
|
308
|
+
|
|
309
|
+
// If either is compatible, the whole expression is compatible
|
|
310
|
+
if (left.compatible) return left
|
|
311
|
+
if (right.compatible) return right
|
|
312
|
+
|
|
313
|
+
// Both incompatible - return left's reason
|
|
314
|
+
return { compatible: false, reason: left.reason }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Handle "AND" (Conjunctive) - Must satisfy both
|
|
318
|
+
if (licenseNode.conjunction === 'and') {
|
|
319
|
+
const left = isCompatibleRecursive(projectLicense, licenseNode.left)
|
|
320
|
+
const right = isCompatibleRecursive(projectLicense, licenseNode.right)
|
|
321
|
+
|
|
322
|
+
// Both must be compatible
|
|
323
|
+
if (!left.compatible) {
|
|
324
|
+
return { compatible: false, reason: `Part of AND expression failed: ${left.reason}` }
|
|
325
|
+
}
|
|
326
|
+
if (!right.compatible) {
|
|
327
|
+
return { compatible: false, reason: `Part of AND expression failed: ${right.reason}` }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return { compatible: true, reason: 'All licenses in AND expression are compatible' }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Unknown structure
|
|
334
|
+
return { compatible: false, reason: 'Unknown license expression structure' }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Check if two licenses are compatible
|
|
339
|
+
* Handles complex SPDX expressions with OR/AND
|
|
340
|
+
*
|
|
341
|
+
* @param {string} projectLicense - The project's license
|
|
342
|
+
* @param {string} depLicense - The dependency's license (can be SPDX expression)
|
|
343
|
+
* @returns {{compatible: boolean, reason: string}} Compatibility result
|
|
344
|
+
*/
|
|
345
|
+
function checkCompatibility(projectLicense, depLicense) {
|
|
346
|
+
// Handle unknown licenses
|
|
347
|
+
if (depLicense === 'UNKNOWN' || !depLicense) {
|
|
348
|
+
return {
|
|
349
|
+
compatible: false,
|
|
350
|
+
reason: 'No license field found'
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Handle custom licenses (WTFPL, etc.)
|
|
355
|
+
const depLowerCase = depLicense.toLowerCase()
|
|
356
|
+
if (CUSTOM_COMPAT[depLowerCase]) {
|
|
357
|
+
if (CUSTOM_COMPAT[depLowerCase].compatibleWith === '*') {
|
|
358
|
+
return { compatible: true, reason: 'Ultra-permissive license' }
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Parse SPDX expression into AST
|
|
363
|
+
let ast
|
|
364
|
+
try {
|
|
365
|
+
ast = parse(depLicense)
|
|
366
|
+
} catch (error) {
|
|
367
|
+
return {
|
|
368
|
+
compatible: false,
|
|
369
|
+
reason: `Invalid SPDX expression: ${depLicense}`
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Evaluate AST recursively
|
|
374
|
+
return isCompatibleRecursive(projectLicense, ast)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Format compatibility result with source citations for --explain flag
|
|
379
|
+
* Shows authoritative sources (FSF, Mozilla, Apache) and URLs
|
|
380
|
+
*
|
|
381
|
+
* @param {string} projectLicense - The project's license
|
|
382
|
+
* @param {string} depLicense - The dependency's license
|
|
383
|
+
* @returns {string} Formatted explanation with sources
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* explainCompatibility('GPL-3.0', 'LGPL-2.1-or-later')
|
|
387
|
+
* // Returns:
|
|
388
|
+
* // "✅ Compatible: LGPL-2.1-or-later can upgrade to GPL-3.0-only (Section 3 upgrade path)
|
|
389
|
+
* //
|
|
390
|
+
* // Source: LGPL Section 3: Can upgrade to corresponding GPL version
|
|
391
|
+
* // URL: https://www.gnu.org/licenses/lgpl-3.0.html#section3"
|
|
392
|
+
*/
|
|
393
|
+
function explainCompatibility(projectLicense, depLicense) {
|
|
394
|
+
const result = checkWithMatrix(projectLicense, depLicense)
|
|
395
|
+
|
|
396
|
+
let explanation = ''
|
|
397
|
+
|
|
398
|
+
// Status emoji
|
|
399
|
+
if (result.compatible && result.severity === 'PASS') {
|
|
400
|
+
explanation += '✅ Compatible: '
|
|
401
|
+
} else if (result.compatible && result.severity === 'WARNING') {
|
|
402
|
+
explanation += '⚠️ Warning: '
|
|
403
|
+
} else {
|
|
404
|
+
explanation += '❌ Incompatible: '
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Reason
|
|
408
|
+
explanation += result.reason
|
|
409
|
+
|
|
410
|
+
// Source citations (if available)
|
|
411
|
+
if (result.source && result.source.citation) {
|
|
412
|
+
explanation += '\n\n'
|
|
413
|
+
explanation += `📚 Source: ${result.source.citation}`
|
|
414
|
+
if (result.source.url) {
|
|
415
|
+
explanation += `\n🔗 URL: ${result.source.url}`
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return explanation
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
module.exports = {
|
|
423
|
+
checkCompatibility,
|
|
424
|
+
checkSingleCompatibility,
|
|
425
|
+
isCompatibleRecursive,
|
|
426
|
+
isCopyleft,
|
|
427
|
+
checkWithMatrix,
|
|
428
|
+
explainCompatibility,
|
|
429
|
+
CUSTOM_COMPAT,
|
|
430
|
+
COPYLEFT_PATTERNS,
|
|
431
|
+
PERMISSIVE_EXCEPTIONS,
|
|
432
|
+
COMPAT_MATRIX
|
|
433
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency License Scanner - Orchestrator
|
|
3
|
+
* Auto-detects project type and delegates to appropriate plugin
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require('chalk')
|
|
7
|
+
|
|
8
|
+
// Import explainCompatibility for --explain flag support
|
|
9
|
+
const { explainCompatibility } = require('./compat-checker')
|
|
10
|
+
|
|
11
|
+
// Import ecosystem plugins
|
|
12
|
+
const nodePlugin = require('./plugins/node')
|
|
13
|
+
const cppPlugin = require('./plugins/cpp')
|
|
14
|
+
const rustPlugin = require('./plugins/rust')
|
|
15
|
+
const pythonPlugin = require('./plugins/python')
|
|
16
|
+
const goPlugin = require('./plugins/go')
|
|
17
|
+
|
|
18
|
+
// Plugin registry with priority ordering
|
|
19
|
+
// Priority: node > cpp > rust > python > go
|
|
20
|
+
const plugins = {
|
|
21
|
+
node: nodePlugin,
|
|
22
|
+
cpp: cppPlugin,
|
|
23
|
+
rust: rustPlugin,
|
|
24
|
+
python: pythonPlugin,
|
|
25
|
+
go: goPlugin
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Plugin detection order (first match wins)
|
|
29
|
+
const pluginOrder = ['node', 'cpp', 'rust', 'python', 'go']
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Auto-detect project type and get the appropriate plugin
|
|
33
|
+
* @returns {{name: string, plugin: Object}|null} Detected plugin or null
|
|
34
|
+
*/
|
|
35
|
+
function detectPlugin() {
|
|
36
|
+
for (const name of pluginOrder) {
|
|
37
|
+
const plugin = plugins[name]
|
|
38
|
+
if (plugin && plugin.detect()) {
|
|
39
|
+
return { name, plugin }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Scan all dependencies for license compatibility
|
|
47
|
+
* Auto-detects project type and delegates to appropriate plugin
|
|
48
|
+
* @param {string} projectLicense - The project's license
|
|
49
|
+
* @returns {Promise<Object>} Scan results
|
|
50
|
+
*/
|
|
51
|
+
async function scanDependencies(projectLicense) {
|
|
52
|
+
const detected = detectPlugin()
|
|
53
|
+
|
|
54
|
+
if (!detected) {
|
|
55
|
+
throw new Error('No supported package manager detected')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return await detected.plugin.scanDependencies(projectLicense)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Display conflict report to console
|
|
63
|
+
* @param {Object} scanResult - Scan results
|
|
64
|
+
* @param {string} projectLicense - The project's license
|
|
65
|
+
* @param {Object} options - Display options
|
|
66
|
+
* @param {boolean} options.explain - Show authoritative source citations
|
|
67
|
+
* @returns {boolean} True if conflicts found (incompatible licenses), false otherwise
|
|
68
|
+
*/
|
|
69
|
+
function displayConflictReport(scanResult, projectLicense, options = {}) {
|
|
70
|
+
if (scanResult.incompatible === 0 && scanResult.unknown === 0) {
|
|
71
|
+
console.log(chalk.green(`\n✅ All ${scanResult.totalDependencies} dependencies compatible with ${projectLicense.toUpperCase()}!`))
|
|
72
|
+
return false // No conflicts
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const issueCount = scanResult.incompatible + scanResult.unknown
|
|
76
|
+
const hasConflicts = scanResult.incompatible > 0
|
|
77
|
+
|
|
78
|
+
if (hasConflicts) {
|
|
79
|
+
console.log(chalk.red(`\n❌ ${issueCount} issue(s) found:\n`))
|
|
80
|
+
} else {
|
|
81
|
+
// Only warnings, no conflicts
|
|
82
|
+
console.log(chalk.yellow(`\n⚠️ ${issueCount} warning(s) found:\n`))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const issue of scanResult.issues) {
|
|
86
|
+
if (issue.type === 'conflict') {
|
|
87
|
+
console.log(chalk.red(`❌ ${issue.package} (${issue.license})`))
|
|
88
|
+
} else {
|
|
89
|
+
console.log(chalk.yellow(`⚠️ ${issue.package} (${issue.license})`))
|
|
90
|
+
}
|
|
91
|
+
console.log(chalk.gray(` ${issue.reason}`))
|
|
92
|
+
console.log(chalk.gray(` Location: ${issue.location}`))
|
|
93
|
+
|
|
94
|
+
// Show authoritative source citations when --explain is used
|
|
95
|
+
if (options.explain && issue.license && issue.license !== 'UNKNOWN') {
|
|
96
|
+
const explanation = explainCompatibility(projectLicense, issue.license)
|
|
97
|
+
// Extract source citation from explanation (after "📚 Source:")
|
|
98
|
+
const sourceMatch = explanation.match(/📚 Source: (.+?)(?:\n|$)/)
|
|
99
|
+
const urlMatch = explanation.match(/🔗 URL: (.+?)(?:\n|$)/)
|
|
100
|
+
|
|
101
|
+
if (sourceMatch || urlMatch) {
|
|
102
|
+
console.log(chalk.blue(' ────────────────────────'))
|
|
103
|
+
if (sourceMatch) {
|
|
104
|
+
console.log(chalk.blue(` 📚 ${sourceMatch[1]}`))
|
|
105
|
+
}
|
|
106
|
+
if (urlMatch) {
|
|
107
|
+
console.log(chalk.blue(` 🔗 ${urlMatch[1]}`))
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log() // Blank line between issues
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Only return true if there are actual conflicts (incompatible licenses)
|
|
116
|
+
// Warnings (unknown licenses) should not block
|
|
117
|
+
return hasConflicts
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Re-export plugin functions for backward compatibility
|
|
121
|
+
// These are deprecated but kept for BC
|
|
122
|
+
const { parsePackageJson, extractLicense } = nodePlugin
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
scanDependencies,
|
|
126
|
+
parsePackageJson,
|
|
127
|
+
extractLicense,
|
|
128
|
+
displayConflictReport,
|
|
129
|
+
// Additional exports for testing
|
|
130
|
+
detectPlugin
|
|
131
|
+
}
|