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
package/bin/licenseguard.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { program } = require('commander')
|
|
4
|
+
const chalk = require('chalk')
|
|
4
5
|
const { version } = require('../package.json')
|
|
5
6
|
const { runList } = require('../lib/commands/list')
|
|
6
7
|
const { runInit } = require('../lib/commands/init')
|
|
@@ -9,44 +10,62 @@ const { setupCommand } = require('../lib/commands/setup')
|
|
|
9
10
|
|
|
10
11
|
program
|
|
11
12
|
.version(version)
|
|
12
|
-
.description('License setup & compliance
|
|
13
|
+
.description('License setup & compliance guard for developers')
|
|
13
14
|
|
|
15
|
+
// Init command with subcommand-specific options
|
|
14
16
|
program
|
|
15
|
-
.
|
|
16
|
-
.
|
|
17
|
-
.option('--
|
|
18
|
-
.option('--
|
|
19
|
-
.option('--
|
|
20
|
-
.option('--
|
|
21
|
-
.option('--
|
|
22
|
-
.option(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
runInitFast(options).catch((err) => {
|
|
37
|
-
console.error('Error:', err.message)
|
|
38
|
-
process.exit(1)
|
|
17
|
+
.command('init')
|
|
18
|
+
.description('Interactive license setup with dependency scanning')
|
|
19
|
+
.option('--force', 'Create LICENSE despite conflicts')
|
|
20
|
+
.option('--noscan', 'Skip dependency scanning')
|
|
21
|
+
.option('--explain', 'Show authoritative source citations for license compatibility decisions')
|
|
22
|
+
.option('--fast', 'Non-interactive mode with auto-detection')
|
|
23
|
+
.option('--license <type>', 'License type (for --fast mode)')
|
|
24
|
+
.option('--owner <name>', 'Copyright owner name (for --fast mode)')
|
|
25
|
+
.option('--year <year>', 'Copyright year (for --fast mode)')
|
|
26
|
+
.option('--url <url>', 'Project URL (for --fast mode)')
|
|
27
|
+
.action(async (options) => {
|
|
28
|
+
try {
|
|
29
|
+
if (options.fast) {
|
|
30
|
+
await runInitFast(options)
|
|
31
|
+
} else {
|
|
32
|
+
await runInit(options)
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error(chalk.red('✗ Error:'), error.message)
|
|
36
|
+
process.exit(1)
|
|
37
|
+
}
|
|
39
38
|
})
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
|
|
40
|
+
// List command
|
|
41
|
+
program
|
|
42
|
+
.command('ls')
|
|
43
|
+
.description('List available license templates')
|
|
44
|
+
.action(async () => {
|
|
45
|
+
try {
|
|
46
|
+
await runList()
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error(chalk.red('✗ Error:'), error.message)
|
|
49
|
+
process.exit(1)
|
|
50
|
+
}
|
|
44
51
|
})
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
|
|
53
|
+
// Setup command (kept for npm prepare script compatibility)
|
|
54
|
+
program
|
|
55
|
+
.command('setup')
|
|
56
|
+
.description('Setup license notification and install git hooks (for npm prepare script)')
|
|
57
|
+
.action(async () => {
|
|
58
|
+
try {
|
|
59
|
+
await setupCommand()
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// Even on error, don't exit 1 - npm prepare compatibility
|
|
62
|
+
console.error(chalk.yellow('⚠️ Setup warning:'), error.message)
|
|
63
|
+
}
|
|
49
64
|
})
|
|
50
|
-
|
|
65
|
+
|
|
66
|
+
program.parse(process.argv)
|
|
67
|
+
|
|
68
|
+
// Show help if no command provided
|
|
69
|
+
if (!process.argv.slice(2).length) {
|
|
51
70
|
program.help()
|
|
52
71
|
}
|
|
@@ -5,6 +5,8 @@ const chalk = require('chalk')
|
|
|
5
5
|
const { generateLicense, LICENSE_TEMPLATES } = require('../templates')
|
|
6
6
|
const { writeConfig } = require('../utils/file-ops')
|
|
7
7
|
const { isGitRepo, installHooks } = require('../utils/git-helpers')
|
|
8
|
+
const { scanDependencies, displayConflictReport } = require('../scanner')
|
|
9
|
+
const { toSPDX } = require('../utils/license-mapper')
|
|
8
10
|
|
|
9
11
|
function getGitConfig(key) {
|
|
10
12
|
try {
|
|
@@ -63,19 +65,76 @@ async function runInitFast(options) {
|
|
|
63
65
|
if (url) console.log(chalk.gray(`URL: ${url}`))
|
|
64
66
|
console.log()
|
|
65
67
|
|
|
68
|
+
// Scanner integration (Story 2.3) - Fast mode
|
|
69
|
+
let scanResult = null
|
|
70
|
+
if (!options.noscan) {
|
|
71
|
+
const spdxLicense = toSPDX(flags.license)
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
console.log(chalk.blue('🔍 Scanning dependencies for license conflicts...\n'))
|
|
75
|
+
|
|
76
|
+
scanResult = await scanDependencies(spdxLicense)
|
|
77
|
+
const hasConflicts = displayConflictReport(scanResult, spdxLicense)
|
|
78
|
+
|
|
79
|
+
if (hasConflicts && !options.force) {
|
|
80
|
+
// Block LICENSE creation due to conflicts
|
|
81
|
+
console.error(chalk.red('\n✗ LICENSE NOT created due to license conflicts.'))
|
|
82
|
+
console.log(chalk.yellow('\nFix conflicts or use --force to proceed anyway:'))
|
|
83
|
+
console.log(chalk.blue(' licenseguard init --fast --force --license ' + flags.license + '\n'))
|
|
84
|
+
process.exit(1)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (hasConflicts && options.force) {
|
|
88
|
+
console.log(chalk.yellow('\n⚠️ Creating LICENSE despite conflicts (--force mode)\n'))
|
|
89
|
+
}
|
|
90
|
+
} catch (scanError) {
|
|
91
|
+
// If scanning fails (not process.exit), warn but don't block
|
|
92
|
+
if (scanError.message !== 'process.exit called') {
|
|
93
|
+
console.log(chalk.yellow(`\n⚠️ Dependency scanning failed: ${scanError.message}`))
|
|
94
|
+
console.log(chalk.yellow('Continuing with LICENSE creation...\n'))
|
|
95
|
+
} else {
|
|
96
|
+
// Re-throw process.exit errors
|
|
97
|
+
throw scanError
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
66
102
|
// Generate license text
|
|
67
103
|
const licenseContent = generateLicense(flags.license, owner, year, url)
|
|
68
104
|
|
|
69
105
|
// Write LICENSE file directly (no prompt in fast mode)
|
|
70
106
|
fs.writeFileSync('LICENSE', licenseContent, 'utf8')
|
|
71
107
|
|
|
72
|
-
//
|
|
73
|
-
|
|
108
|
+
// Auto-save scan results in fast mode (Story 2.4: AC #1, #2)
|
|
109
|
+
// Default: YES for clean scans, NO for conflicts
|
|
110
|
+
const configData = {
|
|
74
111
|
license: flags.license,
|
|
75
112
|
owner: owner,
|
|
76
113
|
year: year,
|
|
77
114
|
url: url,
|
|
78
|
-
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (scanResult) {
|
|
118
|
+
const hasConflicts = scanResult.incompatible > 0
|
|
119
|
+
const shouldSave = !hasConflicts // Auto-save if clean
|
|
120
|
+
|
|
121
|
+
if (shouldSave) {
|
|
122
|
+
configData.scanResult = scanResult
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Write config file
|
|
127
|
+
writeConfig(configData)
|
|
128
|
+
|
|
129
|
+
// Feedback for scan result save
|
|
130
|
+
if (scanResult) {
|
|
131
|
+
const hasConflicts = scanResult.incompatible > 0
|
|
132
|
+
if (!hasConflicts) {
|
|
133
|
+
console.log(chalk.green('✓ Scan results saved to .licenseguardrc'))
|
|
134
|
+
} else {
|
|
135
|
+
console.log(chalk.gray('Scan results not saved (conflicts detected)'))
|
|
136
|
+
}
|
|
137
|
+
}
|
|
79
138
|
|
|
80
139
|
// Success messages
|
|
81
140
|
console.log(chalk.green('✓ LICENSE file created'))
|
package/lib/commands/init.js
CHANGED
|
@@ -3,8 +3,10 @@ const chalk = require('chalk')
|
|
|
3
3
|
const { generateLicense } = require('../templates')
|
|
4
4
|
const { writeLicenseFile, writeConfig } = require('../utils/file-ops')
|
|
5
5
|
const { isGitRepo, initGitRepo, installHooks } = require('../utils/git-helpers')
|
|
6
|
+
const { scanDependencies, displayConflictReport } = require('../scanner')
|
|
7
|
+
const { toSPDX } = require('../utils/license-mapper')
|
|
6
8
|
|
|
7
|
-
async function runInit() {
|
|
9
|
+
async function runInit(options = {}) {
|
|
8
10
|
try {
|
|
9
11
|
console.log(chalk.blue('📜 LicenseGuard - Interactive License Setup\n'))
|
|
10
12
|
|
|
@@ -48,6 +50,40 @@ async function runInit() {
|
|
|
48
50
|
},
|
|
49
51
|
])
|
|
50
52
|
|
|
53
|
+
// Scanner integration (Story 2.3)
|
|
54
|
+
let scanResult = null
|
|
55
|
+
if (!options.noscan) {
|
|
56
|
+
const spdxLicense = toSPDX(answers.license)
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
console.log(chalk.blue('\n🔍 Scanning dependencies for license conflicts...\n'))
|
|
60
|
+
|
|
61
|
+
scanResult = await scanDependencies(spdxLicense)
|
|
62
|
+
const hasConflicts = displayConflictReport(scanResult, spdxLicense, { explain: options.explain })
|
|
63
|
+
|
|
64
|
+
if (hasConflicts && !options.force) {
|
|
65
|
+
// Block LICENSE creation due to conflicts
|
|
66
|
+
console.error(chalk.red('\n✗ LICENSE NOT created due to license conflicts.'))
|
|
67
|
+
console.log(chalk.yellow('\nFix conflicts or use --force to proceed anyway:'))
|
|
68
|
+
console.log(chalk.blue(' licenseguard init --force\n'))
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (hasConflicts && options.force) {
|
|
73
|
+
console.log(chalk.yellow('\n⚠️ Creating LICENSE despite conflicts (--force mode)\n'))
|
|
74
|
+
}
|
|
75
|
+
} catch (scanError) {
|
|
76
|
+
// If scanning fails (not process.exit), warn but don't block
|
|
77
|
+
if (scanError.message !== 'process.exit called') {
|
|
78
|
+
console.log(chalk.yellow(`\n⚠️ Dependency scanning failed: ${scanError.message}`))
|
|
79
|
+
console.log(chalk.yellow('Continuing with LICENSE creation...\n'))
|
|
80
|
+
} else {
|
|
81
|
+
// Re-throw process.exit errors
|
|
82
|
+
throw scanError
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
51
87
|
// Generate license text
|
|
52
88
|
const licenseContent = generateLicense(
|
|
53
89
|
answers.license,
|
|
@@ -64,13 +100,47 @@ async function runInit() {
|
|
|
64
100
|
process.exit(0)
|
|
65
101
|
}
|
|
66
102
|
|
|
67
|
-
//
|
|
68
|
-
|
|
103
|
+
// Prompt to save scan results (Story 2.4: AC #1, #2)
|
|
104
|
+
let saveScanResult = false
|
|
105
|
+
if (scanResult) {
|
|
106
|
+
// Determine default: YES for clean scans, NO for conflicts
|
|
107
|
+
const hasConflicts = scanResult.incompatible > 0
|
|
108
|
+
const defaultSave = !hasConflicts
|
|
109
|
+
|
|
110
|
+
const saveAnswer = await inquirer.prompt([
|
|
111
|
+
{
|
|
112
|
+
type: 'confirm',
|
|
113
|
+
name: 'saveScanResult',
|
|
114
|
+
message: 'Save scan results to .licenseguardrc?',
|
|
115
|
+
default: defaultSave,
|
|
116
|
+
},
|
|
117
|
+
])
|
|
118
|
+
|
|
119
|
+
saveScanResult = saveAnswer.saveScanResult
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Write config file (Story 2.4: AC #3)
|
|
123
|
+
const configData = {
|
|
69
124
|
license: answers.license,
|
|
70
125
|
owner: answers.owner,
|
|
71
126
|
year: answers.year,
|
|
72
127
|
url: answers.url,
|
|
73
|
-
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (saveScanResult && scanResult) {
|
|
131
|
+
configData.scanResult = scanResult
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
writeConfig(configData)
|
|
135
|
+
|
|
136
|
+
// Feedback for scan result save choice
|
|
137
|
+
if (scanResult) {
|
|
138
|
+
if (saveScanResult) {
|
|
139
|
+
console.log(chalk.green('✓ Scan results saved to .licenseguardrc'))
|
|
140
|
+
} else {
|
|
141
|
+
console.log(chalk.gray('Scan results not saved'))
|
|
142
|
+
}
|
|
143
|
+
}
|
|
74
144
|
|
|
75
145
|
// Success messages
|
|
76
146
|
console.log(chalk.green('\n✓ LICENSE file created'))
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan Command Handler
|
|
3
|
+
* Runs dependency scanning without modifying project files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require('chalk')
|
|
7
|
+
const fs = require('fs')
|
|
8
|
+
const path = require('path')
|
|
9
|
+
const { scanDependencies, displayConflictReport } = require('../scanner')
|
|
10
|
+
const { detectLicenseFromText } = require('../scanner/license-detector')
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Try to detect project license from local files
|
|
14
|
+
* @returns {string|null} Detected license or null
|
|
15
|
+
*/
|
|
16
|
+
function detectProjectLicense() {
|
|
17
|
+
// 1. Try .licenseguardrc
|
|
18
|
+
if (fs.existsSync('.licenseguardrc')) {
|
|
19
|
+
try {
|
|
20
|
+
const config = JSON.parse(fs.readFileSync('.licenseguardrc', 'utf8'))
|
|
21
|
+
if (config.license) return config.license
|
|
22
|
+
} catch (e) {
|
|
23
|
+
// Ignore config error
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 2. Try LICENSE file
|
|
28
|
+
const licenseFiles = ['LICENSE', 'LICENSE.txt', 'LICENSE.md', 'COPYING']
|
|
29
|
+
for (const file of licenseFiles) {
|
|
30
|
+
if (fs.existsSync(file)) {
|
|
31
|
+
const content = fs.readFileSync(file, 'utf8')
|
|
32
|
+
const detected = detectLicenseFromText(content)
|
|
33
|
+
if (detected && detected !== 'UNKNOWN') {
|
|
34
|
+
return detected
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 3. Try package managers (basic check)
|
|
40
|
+
if (fs.existsSync('package.json')) {
|
|
41
|
+
try {
|
|
42
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
|
43
|
+
if (pkg.license) return pkg.license
|
|
44
|
+
} catch (e) {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (fs.existsSync('Cargo.toml')) {
|
|
48
|
+
// TODO: Parse Cargo.toml for license
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Run the scan command
|
|
56
|
+
* @param {Object} options - Command options
|
|
57
|
+
*/
|
|
58
|
+
async function runScan(options) {
|
|
59
|
+
// 1. Handle CWD
|
|
60
|
+
if (options.cwd) {
|
|
61
|
+
try {
|
|
62
|
+
process.chdir(options.cwd)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error(`Failed to change directory to ${options.cwd}: ${error.message}`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(chalk.blue(`📂 Scanning in: ${process.cwd()}`))
|
|
69
|
+
|
|
70
|
+
// 2. Determine Project License
|
|
71
|
+
let projectLicense = options.license
|
|
72
|
+
|
|
73
|
+
if (!projectLicense) {
|
|
74
|
+
projectLicense = detectProjectLicense()
|
|
75
|
+
if (projectLicense) {
|
|
76
|
+
console.log(chalk.blue(`ℹ️ Detected project license: ${projectLicense}`))
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!projectLicense) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
'Could not determine project license.\n' +
|
|
83
|
+
'Please create a LICENSE file, use "licenseguard init", or specify --license <type>'
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 3. Run Scan
|
|
88
|
+
console.log(chalk.gray('🔍 Scanning dependencies...'))
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const results = await scanDependencies(projectLicense)
|
|
92
|
+
|
|
93
|
+
// 4. Display Report
|
|
94
|
+
const hasConflicts = displayConflictReport(results, projectLicense, {
|
|
95
|
+
explain: options.explain
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// 5. Handle Exit Code
|
|
99
|
+
if (hasConflicts && !options.allow) {
|
|
100
|
+
console.log(chalk.red('\n❌ Scan failed: License conflicts detected.'))
|
|
101
|
+
process.exit(1)
|
|
102
|
+
} else if (hasConflicts && options.allow) {
|
|
103
|
+
console.log(chalk.yellow('\n⚠️ Scan passed (conflicts allowed via flag).'))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Handle unknown licenses if fail-on-unknown flag is set
|
|
107
|
+
if (options.failOnUnknown && results.unknown > 0) {
|
|
108
|
+
console.log(chalk.red('\n❌ Scan failed: Unknown licenses detected (--fail-on-unknown).'))
|
|
109
|
+
process.exit(1)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
} catch (error) {
|
|
113
|
+
// Check for specific plugin errors
|
|
114
|
+
if (error.message.includes('No supported package manager')) {
|
|
115
|
+
throw new Error('No supported package manager found (package.json, go.mod, Cargo.toml, etc.)')
|
|
116
|
+
}
|
|
117
|
+
throw error
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { runScan }
|