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.
@@ -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 helper for developers')
13
+ .description('License setup & compliance guard for developers')
13
14
 
15
+ // Init command with subcommand-specific options
14
16
  program
15
- .option('--init', 'Interactive license setup wizard')
16
- .option('--init-fast', 'Non-interactive fast setup')
17
- .option('--license <type>', 'License type (for --init-fast)')
18
- .option('--owner <name>', 'Copyright owner name (for --init-fast)')
19
- .option('--year <year>', 'Copyright year (for --init-fast)')
20
- .option('--url <url>', 'Project URL (for --init-fast)')
21
- .option('--ls', 'List available license templates')
22
- .option(
23
- '--setup',
24
- 'Setup license notification and install git hooks (for npm prepare script)'
25
- )
26
- .parse(process.argv)
27
-
28
- const options = program.opts()
29
-
30
- if (options.init) {
31
- runInit().catch((err) => {
32
- console.error('Error:', err.message)
33
- process.exit(1)
34
- })
35
- } else if (options.initFast) {
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
- } else if (options.ls) {
41
- runList().catch((err) => {
42
- console.error('Error:', err.message)
43
- process.exit(1)
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
- } else if (options.setup) {
46
- setupCommand().catch((err) => {
47
- // Even on error, don't exit 1 - npm prepare compatibility
48
- console.error('Setup warning:', err.message)
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
- } else {
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
- // Write config file
73
- writeConfig({
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'))
@@ -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
- // Write config file
68
- writeConfig({
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 }