licenseguard-cli 2.1.0 → 2.1.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,210 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [2.1.1] - 2025-11-25
9
+
10
+ ### Added
11
+ - **Color-coded License Output** - Visual safety hierarchy for quick risk assessment
12
+ - Green (🟢): Permissive licenses (MIT, Apache-2.0, BSD-*, ISC)
13
+ - Yellow (āš ļø): Weak copyleft (MPL-2.0, LGPL-*)
14
+ - Red (āŒ): Strong copyleft (GPL-*, AGPL-*)
15
+ - Gray (ā”): Unknown licenses (requires manual review)
16
+ - Emojis as secondary indicators for accessibility (colorblind-friendly)
17
+ - Works in both light and dark terminal themes
18
+ - **Update Notifier** - Automatic update notifications
19
+ - Checks npm registry once per 24 hours
20
+ - Displays banner when newer version available
21
+ - Non-blocking (doesn't slow down CLI startup)
22
+ - Fails silently if network unavailable
23
+ - Cache stored in OS temp directory
24
+
25
+ ### Changed
26
+ - All license output now color-coded in `init` command
27
+ - Conflict reports now show visual safety indicators
28
+
29
+ ## [2.1.0] - 2025-11-23
30
+
31
+ ### Added
32
+
33
+ #### Multi-Ecosystem Dependency Scanning
34
+ - **C/C++ (Conan) Support**
35
+ - Scans Conan 2.x and 1.x projects
36
+ - Auto-detects `conanfile.txt` and `conanfile.py`
37
+ - Tested with Facebook's folly library (23 dependencies, found 3 real GPL conflicts)
38
+ - Prevents GPL contamination in MIT/Apache projects
39
+ - **Rust (Cargo) Support**
40
+ - Scans Cargo projects via `cargo metadata --format-version 1`
41
+ - Auto-detects `Cargo.toml`
42
+ - 100% test coverage
43
+ - **Python (pip/pipenv/poetry) Support**
44
+ - **Native Python scanner** using `importlib.metadata` (IPC Bridge approach)
45
+ - **98.6% detection rate** (342/347 packages) vs 9.2% with pip show parsing
46
+ - Auto-detects `requirements.txt`, `Pipfile`, `pyproject.toml`
47
+ - Priority: poetry > pipenv > pip
48
+ - 37 license normalizations for Python ecosystem quirks
49
+ - Batch optimization (30x faster than individual calls)
50
+ - **Go (modules) Support**
51
+ - Scans Go modules with streaming NDJSON for large projects
52
+ - Dynamic cache detection via `go env GOMODCACHE`
53
+ - Auto-detects `go.mod`
54
+ - Jaccard Index matching for LICENSE files (no package metadata fallback)
55
+
56
+ - **Authoritative Source Citations (--explain)**
57
+ - Added `--explain` flag to `init` and `scan` commands
58
+ - Shows citations from FSF, OSI, and Mozilla for compatibility decisions
59
+ - Provides direct URLs to license text and compatibility matrices
60
+ - Helps developers verify "Why is this a conflict?" with legal backing
61
+
62
+ #### Advanced License Detection
63
+ - **Jaccard Index License Detector** (5-layer multi-strategy detection)
64
+ - Layer 1: SPDX-License-Identifier headers (fastest)
65
+ - Layer 2: License header/title detection (for full license texts)
66
+ - Layer 3: Dual-license pattern detection
67
+ - Layer 4: Key phrase patterns (distinctive phrases)
68
+ - Layer 5: Jaccard similarity matching (edge cases)
69
+ - Reduced Go scan warnings from 27 to 7
70
+ - Handles BSD-2-Clause vs BSD-3-Clause differentiation
71
+ - Universal detector usable across all ecosystems
72
+ - **GPL Contamination Prevention**
73
+ - Detects copyleft licenses in transitive dependency trees
74
+ - Real-world validation: Found 3 GPL conflicts in folly's 23-dependency tree
75
+ - Business value: Prevents license violations before production
76
+
77
+ ### Changed
78
+
79
+ #### Architecture
80
+ - **Plugin Architecture** - Refactored from monolithic to pluggable ecosystem plugins
81
+ - `lib/scanner/plugins/node.js` - Node.js scanner (extracted from monolithic v2.0)
82
+ - `lib/scanner/plugins/cpp.js` - C/C++ Conan scanner
83
+ - `lib/scanner/plugins/rust.js` - Rust Cargo scanner
84
+ - `lib/scanner/plugins/python.js` - Python scanner with IPC bridge
85
+ - `lib/scanner/plugins/go.js` - Go modules scanner
86
+ - **Auto-detection** - Scanner now auto-detects project type (Node > C++ > Rust > Python > Go priority)
87
+ - **Node.js Scanner** - Backward compatible, all Epic 2 tests still passing
88
+
89
+ ### Technical
90
+
91
+ #### New Files
92
+ - `lib/scanner/plugins/cpp.js` - Conan plugin (87% coverage)
93
+ - `lib/scanner/plugins/rust.js` - Cargo plugin (100% coverage)
94
+ - `lib/scanner/plugins/python.js` - Python plugin with IPC bridge (98% coverage)
95
+ - `lib/scanner/plugins/python-license-scanner.py` - Native Python scanner script
96
+ - `lib/scanner/plugins/go.js` - Go modules plugin (92% coverage)
97
+ - `lib/scanner/plugins/node.js` - Refactored Node.js scanner (100% coverage)
98
+ - `lib/scanner/license-detector.js` - Jaccard Index multi-strategy detector (85% coverage)
99
+
100
+ #### Test Growth
101
+ - **635 tests** (was 132 in v2.0.0) - **+503 tests, +381% growth**
102
+ - **19 test suites** (was ~10 in v2.0.0)
103
+ - **0 regressions** - All Epic 2 tests passing
104
+ - **Coverage:**
105
+ - Plugins: 94.37% (target: >80%)
106
+ - License detector: 85.41% (target: >80%)
107
+ - Overall: 84.46% statements, 75.95% branches
108
+
109
+ #### Dependencies
110
+ - No new npm dependencies added (uses child_process for ecosystem tools)
111
+
112
+ #### Performance
113
+ - Node.js: <1s for 1500 packages
114
+ - Python: ~1-2s for 347 packages (IPC Bridge overhead)
115
+ - C++: <1s for 23 packages (Conan metadata parsing)
116
+ - Rust: <1s for typical project
117
+ - Go: <1s for typical project
118
+
119
+ ### Breaking Changes
120
+ None - Fully backward compatible with v2.0.0
121
+
122
+ ### Known Limitations
123
+ - Mixed-language projects not yet supported (auto-detection picks first match)
124
+ - Python requires Python 3.7+ installed
125
+ - C++ requires Conan 1.x or 2.x installed
126
+ - Rust requires Cargo installed
127
+ - Go requires Go installed
128
+
129
+ ---
130
+
131
+ **Epic 3 Completed:** Multi-Ecosystem Scanner Support
132
+ **Stories Completed:** 3.0 (Plugin Architecture), 3.1 (C++), 3.2 (Rust), 3.3 (Python), 3.4 (Go), plus 2 hotfixes (compat-checker, license-detector)
133
+
134
+ ## [2.0.0] - 2025-11-18
135
+
136
+ ### BREAKING CHANGES
137
+
138
+ #### CLI Architecture Migration
139
+ - **Migrated from flag-based to subcommand-based routing**
140
+ - Old: `licenseguard --init` → New: `licenseguard init`
141
+ - Old: `licenseguard --ls` → New: `licenseguard ls`
142
+ - Old: `licenseguard --setup` → New: `licenseguard setup`
143
+ - **Rationale:** Subcommands provide better CLI semantics and enable future extensibility
144
+ - **Migration:** Update all scripts and documentation to use new syntax
145
+ - **Backward compatibility:** Use `--noscan` flag for v1.x behavior without dependency scanning
146
+
147
+ ### Added
148
+
149
+ #### License Compliance Guard
150
+ - **Dependency license scanning during init**
151
+ - Scans all npm dependencies for license conflicts
152
+ - Reads `package.json` and `node_modules/*/package.json`
153
+ - Displays scan summary with compatible/incompatible/unknown counts
154
+ - **SPDX license compatibility checking**
155
+ - Uses `spdx-satisfies` for industry-standard compatibility rules
156
+ - Checks copyleft vs permissive conflicts (e.g., GPL-3.0 incompatible with MIT)
157
+ - Supports complex license expressions (e.g., "MIT OR Apache-2.0")
158
+ - **Conflict detection with blocking**
159
+ - LICENSE creation blocked if incompatible licenses detected
160
+ - Exits with code 1 and error message
161
+ - Shows detailed conflict report with package names, licenses, and reasons
162
+ - **scanResult persistence to .licenseguardrc**
163
+ - Optional field to save scan results for transparency
164
+ - Includes timestamp, counts, and issues array
165
+ - Acts as compliance badge (like CI or coverage badges)
166
+ - Prompt with smart defaults: YES for clean scans, NO for conflicts
167
+ - **--force flag to override blocking**
168
+ - Creates LICENSE despite conflicts when user explicitly accepts risks
169
+ - Shows warnings but allows proceeding
170
+ - Useful for false positives or acceptable conflicts
171
+ - **--noscan flag for v1.x compatibility**
172
+ - Skips dependency scanning entirely
173
+ - Maintains v1.x behavior for non-JavaScript projects
174
+ - No scanResult generated or saved
175
+
176
+ ### Changed
177
+
178
+ - **Help text improved** - Now shows subcommands with descriptions
179
+ - **Error messages enhanced** - More actionable feedback for common issues
180
+ - **CLI routing refactored** - Cleaner subcommand architecture
181
+ - **Init command enhanced** - Integrated scanning after license selection, before file creation
182
+ - **Init-fast command enhanced** - Auto-saves clean scan results, skips saving on conflicts
183
+ - **Configuration format extended** - `.licenseguardrc` now supports optional `scanResult` field
184
+
185
+ ### Technical
186
+
187
+ #### New Dependencies
188
+ - `spdx-satisfies@5.x` - SPDX license compatibility checking
189
+ - `spdx-expression-parse@4.x` - Parse SPDX license expressions
190
+
191
+ #### New Modules
192
+ - `lib/scanner/index.js` - Dependency scanner with conflict detection
193
+ - `lib/compat/rules.js` - License compatibility rules engine
194
+
195
+ #### Test Coverage
196
+ - Added scanner unit tests
197
+ - Added file-ops scanResult handling tests
198
+ - Maintained 86%+ coverage target
199
+
200
+ ## [1.1.0] - 2025-11-17
201
+
202
+ ### Added
203
+ - Initial public release (Epic 1)
204
+ - Interactive license setup (`init` command)
205
+ - Fast mode for CI/CD (`init --fast`)
206
+ - 6 embedded license templates (MIT, Apache 2.0, GPL 3.0, BSD 3-Clause, ISC, WTFPL)
207
+ - Git hooks for license notifications (post-checkout, pre-commit)
208
+ - Global hooks installation via npm postinstall
209
+ - `.licenseguardrc` configuration file
210
+ - Cross-platform support (Linux, macOS, Windows)
package/README.md CHANGED
@@ -269,6 +269,72 @@ Reads existing `.licenseguardrc` and installs hooks. Used in npm prepare scripts
269
269
 
270
270
  ---
271
271
 
272
+ ## Color-coded Output
273
+
274
+ LicenseGuard uses visual safety hierarchy with color-coding to help you quickly identify dangerous licenses:
275
+
276
+ ### Safety Level Legend
277
+
278
+ | Color | Emoji | License Type | Examples |
279
+ |-------|-------|--------------|----------|
280
+ | 🟢 **Green** | Safe | Permissive licenses | MIT, Apache-2.0, BSD-*, ISC, 0BSD, Unlicense |
281
+ | āš ļø **Yellow** | Caution | Weak copyleft | MPL-2.0, LGPL-*, EPL-* |
282
+ | āŒ **Red** | Dangerous | Strong copyleft | GPL-*, AGPL-* |
283
+ | ā” **Gray** | Unknown | Requires manual review | UNKNOWN, unrecognized licenses |
284
+
285
+ ### How It Works
286
+
287
+ Licenses are automatically color-coded in all commands (`init`, `scan`) based on their safety level:
288
+
289
+ - **Green licenses** (🟢) are permissive and safe to use - they allow you to do almost anything
290
+ - **Yellow licenses** (āš ļø) require caution - they have some copyleft restrictions
291
+ - **Red licenses** (āŒ) are strong copyleft - they may require your project to use the same license
292
+ - **Gray licenses** (ā”) are unknown or unrecognized - manual review required
293
+
294
+ **Example Output:**
295
+ ```bash
296
+ licenseguard scan
297
+
298
+ āŒ 2 issue(s) found:
299
+
300
+ āŒ some-gpl-lib@2.0.0 (āŒ GPL-3.0)
301
+ Conflict: Copyleft incompatible with MIT
302
+ Location: node_modules/some-gpl-lib/package.json
303
+
304
+ āš ļø unknown-lib@1.0.0 (ā” UNKNOWN)
305
+ No license field found
306
+ Location: node_modules/unknown-lib/package.json
307
+ ```
308
+
309
+ ### Accessibility
310
+
311
+ Colors work in both light and dark terminal themes, and emojis are used as secondary indicators for colorblind users:
312
+ - Green = 🟢 (green circle)
313
+ - Yellow = āš ļø (warning triangle)
314
+ - Red = āŒ (red X)
315
+ - Gray = ā” (question mark)
316
+
317
+ ---
318
+
319
+ ## Update Notifications
320
+
321
+ LicenseGuard automatically checks for updates once per day (cached for 24 hours). When a new version is available, you'll see:
322
+
323
+ ```bash
324
+ ╔═══════════════════════════════════════════════╗
325
+ ā•‘ Update available: 2.1.0 → 2.1.1 ā•‘
326
+ ā•‘ Run: npm install -g licenseguard-cli@latest ā•‘
327
+ ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•
328
+ ```
329
+
330
+ **Update check behavior:**
331
+ - Checks npm registry once per 24 hours
332
+ - Non-blocking (doesn't slow down CLI startup)
333
+ - Fails silently if network unavailable
334
+ - Result cached in OS temp directory
335
+
336
+ ---
337
+
272
338
  ## Supported Licenses
273
339
 
274
340
  | Key | Name | Description |
@@ -6,7 +6,14 @@ const { version } = require('../package.json')
6
6
  const { runList } = require('../lib/commands/list')
7
7
  const { runInit } = require('../lib/commands/init')
8
8
  const { runInitFast } = require('../lib/commands/init-fast')
9
+ const { runScan } = require('../lib/commands/scan')
9
10
  const { setupCommand } = require('../lib/commands/setup')
11
+ const { checkForUpdates } = require('../lib/utils/update-notifier')
12
+
13
+ // Check for updates (non-blocking, silent failure)
14
+ checkForUpdates(version).catch(() => {
15
+ // Silent failure - don't block user
16
+ })
10
17
 
11
18
  program
12
19
  .version(version)
@@ -37,6 +44,24 @@ program
37
44
  }
38
45
  })
39
46
 
47
+ // Scan command
48
+ program
49
+ .command('scan')
50
+ .description('Scan dependencies for license conflicts')
51
+ .option('--license <type>', 'Specify project license (if not auto-detected)')
52
+ .option('--allow', 'Allow conflicts (exit 0 even if conflicts found)')
53
+ .option('--fail-on-unknown', 'Fail if unknown licenses detected')
54
+ .option('--explain', 'Show authoritative source citations for license compatibility decisions')
55
+ .option('--cwd <path>', 'Working directory to scan')
56
+ .action(async (options) => {
57
+ try {
58
+ await runScan(options)
59
+ } catch (error) {
60
+ console.error(chalk.red('āœ— Error:'), error.message)
61
+ process.exit(1)
62
+ }
63
+ })
64
+
40
65
  // List command
41
66
  program
42
67
  .command('ls')
@@ -5,7 +5,6 @@
5
5
 
6
6
  const chalk = require('chalk')
7
7
  const fs = require('fs')
8
- const path = require('path')
9
8
  const { scanDependencies, displayConflictReport } = require('../scanner')
10
9
  const { detectLicenseFromText } = require('../scanner/license-detector')
11
10
 
@@ -41,7 +40,9 @@ function detectProjectLicense() {
41
40
  try {
42
41
  const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
43
42
  if (pkg.license) return pkg.license
44
- } catch (e) {}
43
+ } catch (e) {
44
+ // Ignore malformed package.json
45
+ }
45
46
  }
46
47
 
47
48
  if (fs.existsSync('Cargo.toml')) {
@@ -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
+ }
@@ -8,6 +8,9 @@ const chalk = require('chalk')
8
8
  // Import explainCompatibility for --explain flag support
9
9
  const { explainCompatibility } = require('./compat-checker')
10
10
 
11
+ // Import color mapper for license color-coding
12
+ const { colorizeWithEmoji } = require('./color-mapper')
13
+
11
14
  // Import ecosystem plugins
12
15
  const nodePlugin = require('./plugins/node')
13
16
  const cppPlugin = require('./plugins/cpp')
@@ -83,10 +86,13 @@ function displayConflictReport(scanResult, projectLicense, options = {}) {
83
86
  }
84
87
 
85
88
  for (const issue of scanResult.issues) {
89
+ // Color-code the license based on safety level
90
+ const coloredLicense = colorizeWithEmoji(issue.license, issue.license)
91
+
86
92
  if (issue.type === 'conflict') {
87
- console.log(chalk.red(`āŒ ${issue.package} (${issue.license})`))
93
+ console.log(chalk.red(`āŒ ${issue.package} (${coloredLicense})`))
88
94
  } else {
89
- console.log(chalk.yellow(`āš ļø ${issue.package} (${issue.license})`))
95
+ console.log(chalk.yellow(`āš ļø ${issue.package} (${coloredLicense})`))
90
96
  }
91
97
  console.log(chalk.gray(` ${issue.reason}`))
92
98
  console.log(chalk.gray(` Location: ${issue.location}`))
@@ -42,7 +42,7 @@ const LICENSE_HEADER_PATTERNS = {
42
42
  /\(mit\)/i
43
43
  ],
44
44
  'BSD-3-Clause': [
45
- /bsd\s+3[- ]clause\s+(license|\"new\")/i,
45
+ /bsd\s+3[- ]clause\s+(license|"new")/i,
46
46
  /\b3[- ]clause\s+bsd\b/i
47
47
  ],
48
48
  'BSD-2-Clause': [
@@ -192,7 +192,6 @@ function getGoModulesSync() {
192
192
 
193
193
  // Parse NDJSON (newline-delimited JSON)
194
194
  const modules = []
195
- let buffer = ''
196
195
  let braceCount = 0
197
196
  let startIdx = 0
198
197
  let inString = false
@@ -167,7 +167,7 @@ function parsePyprojectToml() {
167
167
  * @param {string} manager - Package manager (for error messages)
168
168
  * @returns {Object} Map of { packageName: licenseString }
169
169
  */
170
- function getLicensesBatch(packageNames, manager) {
170
+ function getLicensesBatch(packageNames, _manager) {
171
171
  if (packageNames.length === 0) return {}
172
172
 
173
173
  const CHUNK_SIZE = 100 // Process in chunks to avoid stdin size limits
@@ -0,0 +1,141 @@
1
+ const https = require('https')
2
+ const fs = require('fs')
3
+ const path = require('path')
4
+ const os = require('os')
5
+ const chalk = require('chalk')
6
+
7
+ const CACHE_FILE = path.join(os.tmpdir(), 'licenseguard-update-check')
8
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours
9
+ const FETCH_TIMEOUT_MS = 5000 // 5 seconds
10
+
11
+ /**
12
+ * Fetch package version info from npm registry
13
+ * @param {string} packageName - Package name to check
14
+ * @returns {Promise<Object>} - Package metadata from npm registry
15
+ */
16
+ function fetchNpmVersion(packageName) {
17
+ return new Promise((resolve, reject) => {
18
+ const options = {
19
+ hostname: 'registry.npmjs.org',
20
+ path: `/${packageName}`,
21
+ method: 'GET',
22
+ timeout: FETCH_TIMEOUT_MS
23
+ }
24
+
25
+ const req = https.request(options, (res) => {
26
+ let data = ''
27
+
28
+ res.on('data', (chunk) => {
29
+ data += chunk
30
+ })
31
+
32
+ res.on('end', () => {
33
+ try {
34
+ const parsed = JSON.parse(data)
35
+ resolve(parsed)
36
+ } catch (error) {
37
+ reject(new Error('Failed to parse npm registry response'))
38
+ }
39
+ })
40
+ })
41
+
42
+ req.on('error', (error) => {
43
+ reject(error)
44
+ })
45
+
46
+ req.on('timeout', () => {
47
+ req.destroy()
48
+ reject(new Error('npm registry request timeout'))
49
+ })
50
+
51
+ req.end()
52
+ })
53
+ }
54
+
55
+ /**
56
+ * Check if cached result is still valid
57
+ * @returns {boolean} - True if cache exists and is fresh
58
+ */
59
+ function isCacheValid() {
60
+ if (!fs.existsSync(CACHE_FILE)) {
61
+ return false
62
+ }
63
+
64
+ try {
65
+ const stat = fs.statSync(CACHE_FILE)
66
+ const age = Date.now() - stat.mtimeMs
67
+ return age < CACHE_TTL_MS
68
+ } catch (error) {
69
+ return false
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Compare two semantic version strings
75
+ * @param {string} current - Current version (e.g., "2.1.0")
76
+ * @param {string} latest - Latest version (e.g., "2.2.0")
77
+ * @returns {boolean} - True if latest is newer than current
78
+ */
79
+ function isNewerVersion(current, latest) {
80
+ const currentParts = current.split('.').map(Number)
81
+ const latestParts = latest.split('.').map(Number)
82
+
83
+ for (let i = 0; i < 3; i++) {
84
+ if (latestParts[i] > currentParts[i]) return true
85
+ if (latestParts[i] < currentParts[i]) return false
86
+ }
87
+
88
+ return false // Versions are equal
89
+ }
90
+
91
+ /**
92
+ * Display update banner in terminal
93
+ * @param {string} current - Current version
94
+ * @param {string} latest - Latest available version
95
+ */
96
+ function displayUpdateBanner(current, latest) {
97
+ console.log(chalk.yellow('\n╔═══════════════════════════════════════════════╗'))
98
+ console.log(chalk.yellow(`ā•‘ Update available: ${current} → ${latest} ā•‘`))
99
+ console.log(chalk.yellow('ā•‘ Run: npm install -g licenseguard-cli@latest ā•‘'))
100
+ console.log(chalk.yellow('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n'))
101
+ }
102
+
103
+ /**
104
+ * Check for updates from npm registry (with caching)
105
+ * Non-blocking, silent failure on errors
106
+ * @param {string} currentVersion - Current version from package.json
107
+ * @returns {Promise<void>}
108
+ */
109
+ async function checkForUpdates(currentVersion) {
110
+ try {
111
+ // Check cache validity
112
+ if (isCacheValid()) {
113
+ return // Skip check - cache is fresh
114
+ }
115
+
116
+ // Fetch latest version from npm registry
117
+ const data = await fetchNpmVersion('licenseguard-cli')
118
+ const latestVersion = data['dist-tags'].latest
119
+
120
+ // Cache result
121
+ fs.writeFileSync(CACHE_FILE, latestVersion, 'utf8')
122
+
123
+ // Compare versions and display banner if update available
124
+ if (isNewerVersion(currentVersion, latestVersion)) {
125
+ displayUpdateBanner(currentVersion, latestVersion)
126
+ }
127
+ } catch (error) {
128
+ // Silent failure - don't block user or pollute output
129
+ // Network issues, timeouts, parsing errors all handled gracefully
130
+ }
131
+ }
132
+
133
+ module.exports = {
134
+ checkForUpdates,
135
+ displayUpdateBanner,
136
+ isNewerVersion,
137
+ fetchNpmVersion,
138
+ isCacheValid,
139
+ CACHE_FILE,
140
+ CACHE_TTL_MS
141
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "licenseguard-cli",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "License setup & compliance guard for developers",
5
5
  "bin": {
6
6
  "licenseguard": "bin/licenseguard.js"