licenseguard-cli 1.2.1 ā 2.0.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 +319 -157
- package/bin/licenseguard.js +54 -35
- package/lib/commands/init-fast.js +70 -3
- package/lib/commands/init.js +82 -4
- package/lib/scanner/compat-checker.js +114 -0
- package/lib/scanner/index.js +179 -0
- package/lib/scanner/progress.js +22 -0
- package/lib/utils/file-ops.js +18 -1
- package/package.json +20 -4
package/LICENSE
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2025
|
|
4
|
-
|
|
3
|
+
Copyright (c) 2025 v
|
|
4
|
+
|
|
5
5
|
|
|
6
6
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
7
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -1,66 +1,134 @@
|
|
|
1
1
|
# LicenseGuard
|
|
2
2
|
|
|
3
|
-
[](https://
|
|
4
|
-
[](https://github.com/your-username/licenseguard/actions)
|
|
5
|
-
[](https://codecov.io/gh/your-username/licenseguard)
|
|
3
|
+
[](https://www.npmjs.com/package/licenseguard-cli)
|
|
6
4
|
[](https://opensource.org/licenses/MIT)
|
|
7
5
|
|
|
8
|
-
|
|
6
|
+
> License setup & compliance guard for developers
|
|
9
7
|
|
|
10
|
-
LicenseGuard
|
|
8
|
+
LicenseGuard helps you set up open source licenses and protects your project from license conflicts. It scans your dependencies for incompatible licenses and automatically notifies developers about licensing requirements - works with any language (Node.js, Python, Rust, Go, etc.).
|
|
9
|
+
|
|
10
|
+
## Key Features
|
|
11
|
+
|
|
12
|
+
- **Dependency Scanning** - Scans npm dependencies for license conflicts during setup
|
|
13
|
+
- **Conflict Detection** - Detects incompatible licenses (e.g., GPL vs MIT) and blocks creation
|
|
14
|
+
- **SPDX Compatibility** - Industry-standard license compatibility checking
|
|
15
|
+
- **Scan Results** - Save scan results to `.licenseguardrc` for transparency
|
|
16
|
+
- **Automatic Notifications** - See license info immediately after `git clone`
|
|
17
|
+
- **Zero Effort** - Global hooks install once, work forever
|
|
18
|
+
- **Language Agnostic** - Works for Python, Rust, Go, Ruby, any project
|
|
19
|
+
- **Offline** - All license templates bundled, no internet required
|
|
20
|
+
|
|
21
|
+
---
|
|
11
22
|
|
|
12
23
|
## Quick Start
|
|
13
24
|
|
|
25
|
+
### For Developers (One-time Setup)
|
|
26
|
+
|
|
14
27
|
```bash
|
|
15
|
-
|
|
16
|
-
|
|
28
|
+
npm install -g licenseguard-cli
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
That's it! Now every time you clone a repo with `.licenseguardrc`, you'll see:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
git clone https://github.com/some/project
|
|
35
|
+
# š This project uses MIT License by ProjectOwner
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### For Project Owners
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cd your-project
|
|
42
|
+
licenseguard init
|
|
43
|
+
```
|
|
17
44
|
|
|
18
|
-
|
|
45
|
+
Follow the prompts, then commit:
|
|
19
46
|
|
|
20
|
-
|
|
47
|
+
```bash
|
|
48
|
+
git add LICENSE .licenseguardrc
|
|
49
|
+
git commit -m "Add license"
|
|
50
|
+
git push
|
|
21
51
|
```
|
|
22
52
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
53
|
+
Anyone who has LicenseGuard installed globally will now see your license info when they clone.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## How It Works
|
|
58
|
+
|
|
59
|
+
### Automatic Global Hooks
|
|
60
|
+
|
|
61
|
+
When you install LicenseGuard globally, it automatically:
|
|
62
|
+
|
|
63
|
+
1. Creates git template directory at `~/.git-templates/hooks/`
|
|
64
|
+
2. Installs self-contained hooks (only needs Node.js, not LicenseGuard)
|
|
65
|
+
3. Configures git: `git config --global init.templateDir ~/.git-templates`
|
|
66
|
+
|
|
67
|
+
Now **every** `git clone` or `git init` copies these hooks automatically.
|
|
27
68
|
|
|
28
|
-
|
|
69
|
+
The hooks check for `.licenseguardrc` and display license info if found:
|
|
29
70
|
|
|
30
|
-
### Using npx (Recommended)
|
|
31
71
|
```bash
|
|
32
|
-
|
|
72
|
+
git clone <any-repo>
|
|
73
|
+
# If .licenseguardrc exists:
|
|
74
|
+
# š This project uses MIT License by OwnerName
|
|
75
|
+
|
|
76
|
+
git checkout feature-branch
|
|
77
|
+
# š This project uses MIT License by OwnerName
|
|
78
|
+
|
|
79
|
+
git commit -m "changes"
|
|
80
|
+
# ā¹ļø Reminder: This project is licensed under MIT
|
|
33
81
|
```
|
|
34
82
|
|
|
35
|
-
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Installation Options
|
|
86
|
+
|
|
87
|
+
### Global (Recommended)
|
|
88
|
+
|
|
36
89
|
```bash
|
|
37
90
|
npm install -g licenseguard-cli
|
|
38
|
-
licenseguard --init
|
|
39
91
|
```
|
|
40
92
|
|
|
41
|
-
|
|
93
|
+
Enables automatic license notifications for all git operations.
|
|
94
|
+
|
|
95
|
+
### Using npx (No install)
|
|
96
|
+
|
|
42
97
|
```bash
|
|
43
|
-
|
|
44
|
-
npx licenseguard-cli --init
|
|
98
|
+
npx licenseguard-cli init
|
|
45
99
|
```
|
|
46
100
|
|
|
47
|
-
|
|
101
|
+
One-time use without global install (no automatic notifications).
|
|
48
102
|
|
|
49
|
-
###
|
|
103
|
+
### Local Development Dependency
|
|
50
104
|
|
|
51
105
|
```bash
|
|
52
|
-
|
|
106
|
+
npm install --save-dev licenseguard-cli
|
|
53
107
|
```
|
|
54
108
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
5. Optionally initializing git if not already a repo
|
|
61
|
-
6. Installing git hooks for license notifications
|
|
109
|
+
For use in npm scripts (see Advanced Usage).
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Commands
|
|
62
114
|
|
|
63
|
-
|
|
115
|
+
### `init` - Interactive Setup
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
licenseguard init
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Guides you through:
|
|
122
|
+
1. Selecting license type (MIT, Apache 2.0, GPL 3.0, etc.)
|
|
123
|
+
2. Copyright owner name
|
|
124
|
+
3. Copyright year (defaults to current)
|
|
125
|
+
4. Project URL (optional)
|
|
126
|
+
5. Dependency scanning for license conflicts
|
|
127
|
+
6. Option to save scan results
|
|
128
|
+
7. Git initialization (if needed)
|
|
129
|
+
8. Git hooks installation
|
|
130
|
+
|
|
131
|
+
Example (with clean dependencies):
|
|
64
132
|
```
|
|
65
133
|
š LicenseGuard - Interactive License Setup
|
|
66
134
|
|
|
@@ -69,46 +137,91 @@ This command guides you through:
|
|
|
69
137
|
? Copyright year: 2025
|
|
70
138
|
? Project URL (optional): https://github.com/you/project
|
|
71
139
|
|
|
140
|
+
š Scanning dependencies for license conflicts...
|
|
141
|
+
|
|
142
|
+
ā Scan complete - 150 dependencies checked
|
|
143
|
+
ā 150 compatible
|
|
144
|
+
ā 0 incompatible
|
|
145
|
+
ā 0 unknown
|
|
146
|
+
|
|
147
|
+
? Save scan results to .licenseguardrc? Yes
|
|
148
|
+
|
|
72
149
|
ā LICENSE file created
|
|
150
|
+
ā Scan results saved to .licenseguardrc
|
|
73
151
|
ā Configuration saved to .licenseguardrc
|
|
74
152
|
ā Git hooks installed
|
|
75
153
|
|
|
76
154
|
š Your project is now licensed under MIT
|
|
77
155
|
```
|
|
78
156
|
|
|
79
|
-
|
|
157
|
+
Example (with conflicts):
|
|
158
|
+
```
|
|
159
|
+
š Scanning dependencies for license conflicts...
|
|
160
|
+
|
|
161
|
+
ā Scan complete - 150 dependencies checked
|
|
162
|
+
ā 147 compatible
|
|
163
|
+
ā 2 incompatible
|
|
164
|
+
ā ļø 1 unknown
|
|
165
|
+
|
|
166
|
+
ā ļø CONFLICTS DETECTED:
|
|
167
|
+
|
|
168
|
+
ā some-gpl-lib@2.0.0 (GPL-3.0)
|
|
169
|
+
Conflict: Copyleft incompatible with MIT
|
|
170
|
+
Location: node_modules/some-gpl-lib/package.json
|
|
171
|
+
|
|
172
|
+
ā LICENSE NOT created due to license conflicts.
|
|
173
|
+
|
|
174
|
+
Fix conflicts or use --force to proceed anyway:
|
|
175
|
+
licenseguard init --force
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Flags:**
|
|
179
|
+
- `--force` - Create LICENSE despite conflicts (shows warnings)
|
|
180
|
+
- `--noscan` - Skip dependency scanning
|
|
181
|
+
|
|
182
|
+
### `init --fast` - Non-Interactive Setup
|
|
80
183
|
|
|
81
184
|
```bash
|
|
82
|
-
licenseguard --
|
|
185
|
+
licenseguard init --fast --license mit --owner "Your Name"
|
|
83
186
|
```
|
|
84
187
|
|
|
85
|
-
|
|
188
|
+
Perfect for CI/CD or scripting. Automatically scans dependencies and auto-saves clean results.
|
|
86
189
|
|
|
87
|
-
**
|
|
88
|
-
- `--
|
|
89
|
-
- `--
|
|
90
|
-
- `--
|
|
91
|
-
- `--
|
|
190
|
+
**Flags:**
|
|
191
|
+
- `--fast` - Enable non-interactive mode
|
|
192
|
+
- `--license <type>` (required) - License type
|
|
193
|
+
- `--owner <name>` (optional) - Auto-detects from git config
|
|
194
|
+
- `--year <year>` (optional) - Defaults to current year
|
|
195
|
+
- `--url <url>` (optional) - Auto-detects from git remote
|
|
196
|
+
- `--force` - Create LICENSE despite conflicts
|
|
197
|
+
- `--noscan` - Skip dependency scanning
|
|
92
198
|
|
|
93
|
-
**
|
|
199
|
+
**Auto-save behavior in fast mode:**
|
|
200
|
+
- Clean scan (no conflicts) ā Automatically saves `scanResult`
|
|
201
|
+
- Conflicts detected ā Does not save `scanResult`
|
|
202
|
+
|
|
203
|
+
Examples:
|
|
94
204
|
```bash
|
|
95
|
-
# Minimal
|
|
96
|
-
licenseguard --
|
|
205
|
+
# Minimal
|
|
206
|
+
licenseguard init --fast --license mit
|
|
97
207
|
|
|
98
|
-
#
|
|
99
|
-
licenseguard --
|
|
208
|
+
# Skip scanning
|
|
209
|
+
licenseguard init --fast --license mit --noscan
|
|
100
210
|
|
|
101
|
-
#
|
|
102
|
-
licenseguard --
|
|
211
|
+
# Force creation despite conflicts
|
|
212
|
+
licenseguard init --fast --license mit --force
|
|
213
|
+
|
|
214
|
+
# Full specification
|
|
215
|
+
licenseguard init --fast --license apache2_0 --owner "Apache Corp" --year 2025
|
|
103
216
|
```
|
|
104
217
|
|
|
105
|
-
### List Available Licenses
|
|
218
|
+
### `ls` - List Available Licenses
|
|
106
219
|
|
|
107
220
|
```bash
|
|
108
|
-
licenseguard
|
|
221
|
+
licenseguard ls
|
|
109
222
|
```
|
|
110
223
|
|
|
111
|
-
|
|
224
|
+
Output:
|
|
112
225
|
```
|
|
113
226
|
Available License Templates:
|
|
114
227
|
|
|
@@ -120,23 +233,36 @@ Available License Templates:
|
|
|
120
233
|
ā WTFPL - Do What The F*ck You Want To Public License (ultra-permissive)
|
|
121
234
|
```
|
|
122
235
|
|
|
123
|
-
|
|
236
|
+
### `setup` - Setup Hooks Only
|
|
124
237
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
| `apache2_0` | Apache 2.0 | Apache License 2.0 (permissive with patent grant) |
|
|
129
|
-
| `gpl3_0` | GPL 3.0 | GNU General Public License 3.0 (copyleft) |
|
|
130
|
-
| `bsd3clause` | BSD 3-Clause | BSD 3-Clause License (permissive with attribution) |
|
|
131
|
-
| `isc` | ISC | ISC License (simpler MIT alternative) |
|
|
132
|
-
| `wtfpl` | WTFPL | Do What The F*ck You Want To Public License (ultra-permissive) |
|
|
238
|
+
```bash
|
|
239
|
+
licenseguard setup
|
|
240
|
+
```
|
|
133
241
|
|
|
134
|
-
|
|
242
|
+
Reads existing `.licenseguardrc` and installs hooks. Used in npm prepare scripts.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Supported Licenses
|
|
247
|
+
|
|
248
|
+
| Key | Name | Description |
|
|
249
|
+
|-----|------|-------------|
|
|
250
|
+
| `mit` | MIT | Permissive, widely used |
|
|
251
|
+
| `apache2_0` | Apache 2.0 | Permissive with patent grant |
|
|
252
|
+
| `gpl3_0` | GPL 3.0 | Copyleft |
|
|
253
|
+
| `bsd3clause` | BSD 3-Clause | Permissive with attribution |
|
|
254
|
+
| `isc` | ISC | Simpler MIT alternative |
|
|
255
|
+
| `wtfpl` | WTFPL | Ultra-permissive |
|
|
256
|
+
|
|
257
|
+
Not sure which to choose? Visit [choosealicense.com](https://choosealicense.com).
|
|
258
|
+
|
|
259
|
+
---
|
|
135
260
|
|
|
136
261
|
## Configuration
|
|
137
262
|
|
|
138
|
-
LicenseGuard creates
|
|
263
|
+
LicenseGuard creates `.licenseguardrc` in your project root.
|
|
139
264
|
|
|
265
|
+
**Basic format:**
|
|
140
266
|
```json
|
|
141
267
|
{
|
|
142
268
|
"license": "mit",
|
|
@@ -146,148 +272,184 @@ LicenseGuard creates a `.licenseguardrc` file in your project root:
|
|
|
146
272
|
}
|
|
147
273
|
```
|
|
148
274
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
275
|
+
**With scan results (optional):**
|
|
276
|
+
```json
|
|
277
|
+
{
|
|
278
|
+
"license": "mit",
|
|
279
|
+
"owner": "Your Name",
|
|
280
|
+
"year": "2025",
|
|
281
|
+
"url": "https://github.com/you/project",
|
|
282
|
+
"scanResult": {
|
|
283
|
+
"timestamp": "2025-11-18T10:30:00.000Z",
|
|
284
|
+
"totalDependencies": 150,
|
|
285
|
+
"compatible": 150,
|
|
286
|
+
"incompatible": 0,
|
|
287
|
+
"unknown": 0,
|
|
288
|
+
"issues": []
|
|
289
|
+
}
|
|
290
|
+
}
|
|
159
291
|
```
|
|
160
292
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
293
|
+
**Why save scan results?**
|
|
294
|
+
- **Transparency badge** - Shows your project has validated license compliance
|
|
295
|
+
- **Trust signal** - Like CI badges or test coverage badges
|
|
296
|
+
- **Audit trail** - Documents when dependencies were last checked
|
|
297
|
+
- **Open source best practice** - Demonstrates license awareness
|
|
166
298
|
|
|
167
|
-
**
|
|
168
|
-
- Hooks are **educational only** - they never block git operations
|
|
169
|
-
- Hooks always exit with code 0 (success)
|
|
170
|
-
- If `.licenseguardrc` is missing, hooks silently exit
|
|
299
|
+
This file **must be committed** to your repository so others can see your license info.
|
|
171
300
|
|
|
172
|
-
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Advanced Usage
|
|
173
304
|
|
|
174
|
-
|
|
305
|
+
### For npm Projects (Alternative to Global Install)
|
|
175
306
|
|
|
176
|
-
|
|
307
|
+
If you can't rely on developers having LicenseGuard installed globally, use npm prepare script:
|
|
177
308
|
|
|
178
|
-
**Add to your package.json:**
|
|
179
309
|
```json
|
|
180
310
|
{
|
|
181
311
|
"devDependencies": {
|
|
182
|
-
"licenseguard-cli": "^
|
|
312
|
+
"licenseguard-cli": "^2.0.0"
|
|
183
313
|
},
|
|
184
314
|
"scripts": {
|
|
185
|
-
"prepare": "licenseguard
|
|
315
|
+
"prepare": "licenseguard setup || true"
|
|
186
316
|
}
|
|
187
317
|
}
|
|
188
318
|
```
|
|
189
319
|
|
|
190
|
-
|
|
191
|
-
```bash
|
|
192
|
-
git clone <your-repo> # Gets code + .licenseguardrc
|
|
193
|
-
npm install # AUTOMATICALLY:
|
|
194
|
-
# š "This project uses MIT License by Your Name"
|
|
195
|
-
# ā Git hooks installed
|
|
196
|
-
git checkout feature # Notification appears (hooks active)
|
|
197
|
-
git commit # Reminder appears (hooks active)
|
|
198
|
-
```
|
|
320
|
+
When developers run `npm install`, hooks are set up automatically.
|
|
199
321
|
|
|
200
|
-
|
|
201
|
-
- Reads `.licenseguardrc` and displays license notification
|
|
202
|
-
- Installs git hooks automatically
|
|
203
|
-
- Always exits 0 (never breaks `npm install`)
|
|
204
|
-
- Safe to run multiple times (idempotent)
|
|
322
|
+
### Existing Git Hooks
|
|
205
323
|
|
|
206
|
-
|
|
324
|
+
LicenseGuard **never overwrites** existing hooks. If conflicts exist:
|
|
207
325
|
|
|
208
|
-
|
|
209
|
-
-
|
|
210
|
-
- `.git/hooks/licenseguard-post-checkout`
|
|
326
|
+
- Creates `licenseguard-post-checkout` and `licenseguard-pre-commit`
|
|
327
|
+
- Shows warning with merge instructions
|
|
211
328
|
|
|
212
|
-
|
|
329
|
+
### Non-Git Projects
|
|
213
330
|
|
|
214
|
-
|
|
331
|
+
LicenseGuard works without git:
|
|
332
|
+
- `init` offers to run `git init`
|
|
333
|
+
- `init --fast` creates LICENSE file only
|
|
334
|
+
- Hooks are skipped with warning
|
|
215
335
|
|
|
216
|
-
|
|
217
|
-
- Interactive mode (`--init`) will offer to run `git init`
|
|
218
|
-
- Fast mode (`--init-fast`) will skip hooks and warn you
|
|
219
|
-
- `--setup` command will skip hooks with a warning
|
|
220
|
-
- LICENSE file is always created regardless of git status
|
|
336
|
+
---
|
|
221
337
|
|
|
222
338
|
## FAQ
|
|
223
339
|
|
|
224
|
-
###
|
|
225
|
-
Yes! LicenseGuard is completely offline. All license templates are bundled with the package.
|
|
340
|
+
### What licenses are checked during scanning?
|
|
226
341
|
|
|
227
|
-
|
|
228
|
-
|
|
342
|
+
Scans **npm dependencies only** (reads `package.json` and `node_modules`). It checks:
|
|
343
|
+
- SPDX license identifiers (MIT, Apache-2.0, GPL-3.0, BSD-3-Clause, ISC, etc.)
|
|
344
|
+
- License compatibility using industry-standard rules
|
|
345
|
+
- Copyleft vs permissive conflicts (e.g., GPL incompatible with MIT)
|
|
229
346
|
|
|
230
|
-
|
|
231
|
-
Yes! LicenseGuard is fully cross-platform and works on Linux, macOS, and Windows.
|
|
347
|
+
For non-JavaScript projects, use `--noscan` flag.
|
|
232
348
|
|
|
233
|
-
###
|
|
234
|
-
Interactive mode will ask if you want to overwrite it. Fast mode will overwrite without asking.
|
|
349
|
+
### How does SPDX compatibility work?
|
|
235
350
|
|
|
236
|
-
|
|
237
|
-
|
|
351
|
+
LicenseGuard uses [spdx-satisfies](https://www.npmjs.com/package/spdx-satisfies) for compatibility checking:
|
|
352
|
+
- **Permissive licenses** (MIT, Apache, BSD, ISC) - Compatible with most licenses
|
|
353
|
+
- **Copyleft licenses** (GPL-3.0) - Incompatible with permissive project licenses
|
|
354
|
+
- **Unknown licenses** - Generates warnings but doesn't block
|
|
355
|
+
- **Custom rules** - Fallback for non-SPDX licenses (WTFPL, proprietary)
|
|
238
356
|
|
|
239
|
-
###
|
|
240
|
-
The hooks are informational only and don't block anything. If you don't want them, simply delete the hook files from `.git/hooks/`.
|
|
357
|
+
### What is the scanResult for?
|
|
241
358
|
|
|
242
|
-
|
|
243
|
-
|
|
359
|
+
`scanResult` is optional transparency data you can commit to show:
|
|
360
|
+
1. Your project has validated license compliance
|
|
361
|
+
2. When dependencies were last scanned
|
|
362
|
+
3. What conflicts (if any) were detected
|
|
363
|
+
4. Trust signal for users and contributors (like CI badges)
|
|
244
364
|
|
|
245
|
-
|
|
365
|
+
You choose whether to save it after each scan. Clean scans default to YES, conflicts default to NO.
|
|
246
366
|
|
|
247
|
-
|
|
367
|
+
### Can I skip dependency scanning?
|
|
248
368
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
5. Open a Pull Request
|
|
369
|
+
Yes! Use the `--noscan` flag:
|
|
370
|
+
```bash
|
|
371
|
+
licenseguard init --noscan
|
|
372
|
+
```
|
|
254
373
|
|
|
255
|
-
|
|
374
|
+
This is useful for:
|
|
375
|
+
- Non-JavaScript projects
|
|
376
|
+
- Projects without dependencies
|
|
377
|
+
- When you want manual license management
|
|
256
378
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
379
|
+
### Does this work for non-JavaScript projects?
|
|
380
|
+
|
|
381
|
+
**Yes!** LicenseGuard works for any project:
|
|
382
|
+
- Python projects
|
|
383
|
+
- Rust/Cargo projects
|
|
384
|
+
- Go modules
|
|
385
|
+
- Ruby gems
|
|
386
|
+
- Any language
|
|
387
|
+
|
|
388
|
+
The hooks only need Node.js installed (which most developers have).
|
|
261
389
|
|
|
262
|
-
|
|
263
|
-
npm install
|
|
390
|
+
### Do my contributors need to install LicenseGuard?
|
|
264
391
|
|
|
265
|
-
|
|
266
|
-
npm test
|
|
392
|
+
For automatic notifications: **Yes**, they need `npm install -g licenseguard-cli` once.
|
|
267
393
|
|
|
268
|
-
|
|
269
|
-
npm run test:coverage
|
|
394
|
+
Alternative: Use npm prepare script (see Advanced Usage) - then only project owner installs.
|
|
270
395
|
|
|
271
|
-
|
|
272
|
-
|
|
396
|
+
### Does this work offline?
|
|
397
|
+
|
|
398
|
+
Yes! All license templates are bundled. No internet required.
|
|
273
399
|
|
|
274
|
-
|
|
275
|
-
npm run format
|
|
400
|
+
### Can I disable notifications?
|
|
276
401
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
licenseguard --init
|
|
402
|
+
Delete hooks from `.git/hooks/`:
|
|
403
|
+
```bash
|
|
404
|
+
rm .git/hooks/post-checkout .git/hooks/pre-commit
|
|
281
405
|
```
|
|
282
406
|
|
|
407
|
+
Or remove global hooks:
|
|
408
|
+
```bash
|
|
409
|
+
rm -rf ~/.git-templates/hooks/
|
|
410
|
+
git config --global --unset init.templateDir
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### What Node.js versions work?
|
|
414
|
+
|
|
415
|
+
Node.js 18.x or 20.x (LTS versions).
|
|
416
|
+
|
|
417
|
+
### Does it work on Windows?
|
|
418
|
+
|
|
419
|
+
Yes! Fully cross-platform (Linux, macOS, Windows).
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## Why LicenseGuard?
|
|
424
|
+
|
|
425
|
+
- **Not enforcing** - Unlike license scanners, we inform and educate
|
|
426
|
+
- **Zero friction** - One global install, automatic forever
|
|
427
|
+
- **Universal** - Works with any language/framework
|
|
428
|
+
- **Educational** - Raises awareness without blocking workflows
|
|
429
|
+
- **Open source** - MIT licensed, free forever
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Contributing
|
|
434
|
+
|
|
435
|
+
Contributions welcome!
|
|
436
|
+
|
|
437
|
+
1. Fork the repository
|
|
438
|
+
2. Create feature branch: `git checkout -b feature/amazing`
|
|
439
|
+
3. Commit changes: `git commit -m 'Add feature'`
|
|
440
|
+
4. Push: `git push origin feature/amazing`
|
|
441
|
+
5. Open Pull Request
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
283
445
|
## License
|
|
284
446
|
|
|
285
|
-
|
|
447
|
+
MIT License - see [LICENSE](LICENSE) file.
|
|
286
448
|
|
|
287
449
|
---
|
|
288
450
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
- [
|
|
292
|
-
- [
|
|
293
|
-
- [
|
|
451
|
+
## Links
|
|
452
|
+
|
|
453
|
+
- [npm Package](https://www.npmjs.com/package/licenseguard-cli)
|
|
454
|
+
- [Choose a License](https://choosealicense.com)
|
|
455
|
+
- [Open Source Initiative](https://opensource.org/licenses)
|
package/bin/licenseguard.js
CHANGED
|
@@ -1,51 +1,70 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { program } = require('commander')
|
|
4
|
+
const chalk = require('chalk')
|
|
5
|
+
const { version } = require('../package.json')
|
|
4
6
|
const { runList } = require('../lib/commands/list')
|
|
5
7
|
const { runInit } = require('../lib/commands/init')
|
|
6
8
|
const { runInitFast } = require('../lib/commands/init-fast')
|
|
7
9
|
const { setupCommand } = require('../lib/commands/setup')
|
|
8
10
|
|
|
9
11
|
program
|
|
10
|
-
.version(
|
|
11
|
-
.description('License setup & compliance
|
|
12
|
+
.version(version)
|
|
13
|
+
.description('License setup & compliance guard for developers')
|
|
12
14
|
|
|
15
|
+
// Init command with subcommand-specific options
|
|
13
16
|
program
|
|
14
|
-
.
|
|
15
|
-
.
|
|
16
|
-
.option('--
|
|
17
|
-
.option('--
|
|
18
|
-
.option('--
|
|
19
|
-
.option('--
|
|
20
|
-
.option('--
|
|
21
|
-
.option(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
} else if (options.initFast) {
|
|
35
|
-
runInitFast(options).catch((err) => {
|
|
36
|
-
console.error('Error:', err.message)
|
|
37
|
-
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('--fast', 'Non-interactive mode with auto-detection')
|
|
22
|
+
.option('--license <type>', 'License type (for --fast mode)')
|
|
23
|
+
.option('--owner <name>', 'Copyright owner name (for --fast mode)')
|
|
24
|
+
.option('--year <year>', 'Copyright year (for --fast mode)')
|
|
25
|
+
.option('--url <url>', 'Project URL (for --fast mode)')
|
|
26
|
+
.action(async (options) => {
|
|
27
|
+
try {
|
|
28
|
+
if (options.fast) {
|
|
29
|
+
await runInitFast(options)
|
|
30
|
+
} else {
|
|
31
|
+
await runInit(options)
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(chalk.red('ā Error:'), error.message)
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
38
37
|
})
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
|
|
39
|
+
// List command
|
|
40
|
+
program
|
|
41
|
+
.command('ls')
|
|
42
|
+
.description('List available license templates')
|
|
43
|
+
.action(async () => {
|
|
44
|
+
try {
|
|
45
|
+
await runList()
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(chalk.red('ā Error:'), error.message)
|
|
48
|
+
process.exit(1)
|
|
49
|
+
}
|
|
43
50
|
})
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
|
|
52
|
+
// Setup command (kept for npm prepare script compatibility)
|
|
53
|
+
program
|
|
54
|
+
.command('setup')
|
|
55
|
+
.description('Setup license notification and install git hooks (for npm prepare script)')
|
|
56
|
+
.action(async () => {
|
|
57
|
+
try {
|
|
58
|
+
await setupCommand()
|
|
59
|
+
} catch (error) {
|
|
60
|
+
// Even on error, don't exit 1 - npm prepare compatibility
|
|
61
|
+
console.error(chalk.yellow('ā ļø Setup warning:'), error.message)
|
|
62
|
+
}
|
|
48
63
|
})
|
|
49
|
-
|
|
64
|
+
|
|
65
|
+
program.parse(process.argv)
|
|
66
|
+
|
|
67
|
+
// Show help if no command provided
|
|
68
|
+
if (!process.argv.slice(2).length) {
|
|
50
69
|
program.help()
|
|
51
70
|
}
|
|
@@ -5,6 +5,7 @@ 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')
|
|
8
9
|
|
|
9
10
|
function getGitConfig(key) {
|
|
10
11
|
try {
|
|
@@ -63,19 +64,85 @@ async function runInitFast(options) {
|
|
|
63
64
|
if (url) console.log(chalk.gray(`URL: ${url}`))
|
|
64
65
|
console.log()
|
|
65
66
|
|
|
67
|
+
// Scanner integration (Story 2.3) - Fast mode
|
|
68
|
+
let scanResult = null
|
|
69
|
+
if (!options.noscan) {
|
|
70
|
+
// Convert internal license format to SPDX identifier
|
|
71
|
+
const licenseToSPDX = {
|
|
72
|
+
'mit': 'MIT',
|
|
73
|
+
'apache2_0': 'Apache-2.0',
|
|
74
|
+
'gpl3_0': 'GPL-3.0',
|
|
75
|
+
'bsd3clause': 'BSD-3-Clause',
|
|
76
|
+
'isc': 'ISC',
|
|
77
|
+
'wtfpl': 'WTFPL'
|
|
78
|
+
}
|
|
79
|
+
const spdxLicense = licenseToSPDX[flags.license] || flags.license.toUpperCase()
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
console.log(chalk.blue('š Scanning dependencies for license conflicts...\n'))
|
|
83
|
+
|
|
84
|
+
scanResult = await scanDependencies(spdxLicense)
|
|
85
|
+
const hasConflicts = displayConflictReport(scanResult, spdxLicense)
|
|
86
|
+
|
|
87
|
+
if (hasConflicts && !options.force) {
|
|
88
|
+
// Block LICENSE creation due to conflicts
|
|
89
|
+
console.error(chalk.red('\nā LICENSE NOT created due to license conflicts.'))
|
|
90
|
+
console.log(chalk.yellow('\nFix conflicts or use --force to proceed anyway:'))
|
|
91
|
+
console.log(chalk.blue(' licenseguard init --fast --force --license ' + flags.license + '\n'))
|
|
92
|
+
process.exit(1)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (hasConflicts && options.force) {
|
|
96
|
+
console.log(chalk.yellow('\nā ļø Creating LICENSE despite conflicts (--force mode)\n'))
|
|
97
|
+
}
|
|
98
|
+
} catch (scanError) {
|
|
99
|
+
// If scanning fails (not process.exit), warn but don't block
|
|
100
|
+
if (scanError.message !== 'process.exit called') {
|
|
101
|
+
console.log(chalk.yellow(`\nā ļø Dependency scanning failed: ${scanError.message}`))
|
|
102
|
+
console.log(chalk.yellow('Continuing with LICENSE creation...\n'))
|
|
103
|
+
} else {
|
|
104
|
+
// Re-throw process.exit errors
|
|
105
|
+
throw scanError
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
66
110
|
// Generate license text
|
|
67
111
|
const licenseContent = generateLicense(flags.license, owner, year, url)
|
|
68
112
|
|
|
69
113
|
// Write LICENSE file directly (no prompt in fast mode)
|
|
70
114
|
fs.writeFileSync('LICENSE', licenseContent, 'utf8')
|
|
71
115
|
|
|
72
|
-
//
|
|
73
|
-
|
|
116
|
+
// Auto-save scan results in fast mode (Story 2.4: AC #1, #2)
|
|
117
|
+
// Default: YES for clean scans, NO for conflicts
|
|
118
|
+
const configData = {
|
|
74
119
|
license: flags.license,
|
|
75
120
|
owner: owner,
|
|
76
121
|
year: year,
|
|
77
122
|
url: url,
|
|
78
|
-
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (scanResult) {
|
|
126
|
+
const hasConflicts = scanResult.incompatible > 0
|
|
127
|
+
const shouldSave = !hasConflicts // Auto-save if clean
|
|
128
|
+
|
|
129
|
+
if (shouldSave) {
|
|
130
|
+
configData.scanResult = scanResult
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Write config file
|
|
135
|
+
writeConfig(configData)
|
|
136
|
+
|
|
137
|
+
// Feedback for scan result save
|
|
138
|
+
if (scanResult) {
|
|
139
|
+
const hasConflicts = scanResult.incompatible > 0
|
|
140
|
+
if (!hasConflicts) {
|
|
141
|
+
console.log(chalk.green('ā Scan results saved to .licenseguardrc'))
|
|
142
|
+
} else {
|
|
143
|
+
console.log(chalk.gray('Scan results not saved (conflicts detected)'))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
79
146
|
|
|
80
147
|
// Success messages
|
|
81
148
|
console.log(chalk.green('ā LICENSE file created'))
|
package/lib/commands/init.js
CHANGED
|
@@ -3,8 +3,9 @@ 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')
|
|
6
7
|
|
|
7
|
-
async function runInit() {
|
|
8
|
+
async function runInit(options = {}) {
|
|
8
9
|
try {
|
|
9
10
|
console.log(chalk.blue('š LicenseGuard - Interactive License Setup\n'))
|
|
10
11
|
|
|
@@ -48,6 +49,49 @@ async function runInit() {
|
|
|
48
49
|
},
|
|
49
50
|
])
|
|
50
51
|
|
|
52
|
+
// Scanner integration (Story 2.3)
|
|
53
|
+
let scanResult = null
|
|
54
|
+
if (!options.noscan) {
|
|
55
|
+
// Convert internal license format to SPDX identifier
|
|
56
|
+
const licenseToSPDX = {
|
|
57
|
+
'mit': 'MIT',
|
|
58
|
+
'apache2_0': 'Apache-2.0',
|
|
59
|
+
'gpl3_0': 'GPL-3.0',
|
|
60
|
+
'bsd3clause': 'BSD-3-Clause',
|
|
61
|
+
'isc': 'ISC',
|
|
62
|
+
'wtfpl': 'WTFPL'
|
|
63
|
+
}
|
|
64
|
+
const spdxLicense = licenseToSPDX[answers.license] || answers.license.toUpperCase()
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
console.log(chalk.blue('\nš Scanning dependencies for license conflicts...\n'))
|
|
68
|
+
|
|
69
|
+
scanResult = await scanDependencies(spdxLicense)
|
|
70
|
+
const hasConflicts = displayConflictReport(scanResult, spdxLicense)
|
|
71
|
+
|
|
72
|
+
if (hasConflicts && !options.force) {
|
|
73
|
+
// Block LICENSE creation due to conflicts
|
|
74
|
+
console.error(chalk.red('\nā LICENSE NOT created due to license conflicts.'))
|
|
75
|
+
console.log(chalk.yellow('\nFix conflicts or use --force to proceed anyway:'))
|
|
76
|
+
console.log(chalk.blue(' licenseguard init --force\n'))
|
|
77
|
+
process.exit(1)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (hasConflicts && options.force) {
|
|
81
|
+
console.log(chalk.yellow('\nā ļø Creating LICENSE despite conflicts (--force mode)\n'))
|
|
82
|
+
}
|
|
83
|
+
} catch (scanError) {
|
|
84
|
+
// If scanning fails (not process.exit), warn but don't block
|
|
85
|
+
if (scanError.message !== 'process.exit called') {
|
|
86
|
+
console.log(chalk.yellow(`\nā ļø Dependency scanning failed: ${scanError.message}`))
|
|
87
|
+
console.log(chalk.yellow('Continuing with LICENSE creation...\n'))
|
|
88
|
+
} else {
|
|
89
|
+
// Re-throw process.exit errors
|
|
90
|
+
throw scanError
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
51
95
|
// Generate license text
|
|
52
96
|
const licenseContent = generateLicense(
|
|
53
97
|
answers.license,
|
|
@@ -64,13 +108,47 @@ async function runInit() {
|
|
|
64
108
|
process.exit(0)
|
|
65
109
|
}
|
|
66
110
|
|
|
67
|
-
//
|
|
68
|
-
|
|
111
|
+
// Prompt to save scan results (Story 2.4: AC #1, #2)
|
|
112
|
+
let saveScanResult = false
|
|
113
|
+
if (scanResult) {
|
|
114
|
+
// Determine default: YES for clean scans, NO for conflicts
|
|
115
|
+
const hasConflicts = scanResult.incompatible > 0
|
|
116
|
+
const defaultSave = !hasConflicts
|
|
117
|
+
|
|
118
|
+
const saveAnswer = await inquirer.prompt([
|
|
119
|
+
{
|
|
120
|
+
type: 'confirm',
|
|
121
|
+
name: 'saveScanResult',
|
|
122
|
+
message: 'Save scan results to .licenseguardrc?',
|
|
123
|
+
default: defaultSave,
|
|
124
|
+
},
|
|
125
|
+
])
|
|
126
|
+
|
|
127
|
+
saveScanResult = saveAnswer.saveScanResult
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Write config file (Story 2.4: AC #3)
|
|
131
|
+
const configData = {
|
|
69
132
|
license: answers.license,
|
|
70
133
|
owner: answers.owner,
|
|
71
134
|
year: answers.year,
|
|
72
135
|
url: answers.url,
|
|
73
|
-
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (saveScanResult && scanResult) {
|
|
139
|
+
configData.scanResult = scanResult
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
writeConfig(configData)
|
|
143
|
+
|
|
144
|
+
// Feedback for scan result save choice
|
|
145
|
+
if (scanResult) {
|
|
146
|
+
if (saveScanResult) {
|
|
147
|
+
console.log(chalk.green('ā Scan results saved to .licenseguardrc'))
|
|
148
|
+
} else {
|
|
149
|
+
console.log(chalk.gray('Scan results not saved'))
|
|
150
|
+
}
|
|
151
|
+
}
|
|
74
152
|
|
|
75
153
|
// Success messages
|
|
76
154
|
console.log(chalk.green('\nā LICENSE file created'))
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* License compatibility checker
|
|
3
|
+
* Uses SPDX libraries for standard licenses + custom rules for non-SPDX
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const satisfies = require('spdx-satisfies')
|
|
7
|
+
const parse = require('spdx-expression-parse')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Custom compatibility rules for non-SPDX licenses
|
|
11
|
+
* @type {Object}
|
|
12
|
+
*/
|
|
13
|
+
const CUSTOM_COMPAT = {
|
|
14
|
+
wtfpl: {
|
|
15
|
+
type: 'permissive',
|
|
16
|
+
compatibleWith: '*', // Compatible with everything
|
|
17
|
+
description: 'Do What The F*ck You Want To Public License'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Permissive licenses that are generally compatible with each other
|
|
23
|
+
*/
|
|
24
|
+
const PERMISSIVE_LICENSES = ['MIT', 'ISC', 'BSD-2-Clause', 'BSD-3-Clause', 'Apache-2.0', '0BSD']
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Copyleft licenses that have restrictions
|
|
28
|
+
*/
|
|
29
|
+
const COPYLEFT_LICENSES = ['GPL-2.0', 'GPL-3.0', 'AGPL-3.0', 'GPL-2.0-only', 'GPL-3.0-only', 'LGPL-2.1', 'LGPL-3.0']
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if two licenses are compatible
|
|
33
|
+
* @param {string} projectLicense - The project's license
|
|
34
|
+
* @param {string} depLicense - The dependency's license
|
|
35
|
+
* @returns {{compatible: boolean, reason: string}} Compatibility result
|
|
36
|
+
*/
|
|
37
|
+
function checkCompatibility(projectLicense, depLicense) {
|
|
38
|
+
// Handle unknown licenses
|
|
39
|
+
if (depLicense === 'UNKNOWN' || !depLicense) {
|
|
40
|
+
return {
|
|
41
|
+
compatible: false, // Warn but don't block (handled by caller)
|
|
42
|
+
reason: 'No license field found'
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Handle custom licenses (WTFPL, proprietary, etc.)
|
|
47
|
+
const depLowerCase = depLicense.toLowerCase()
|
|
48
|
+
if (CUSTOM_COMPAT[depLowerCase]) {
|
|
49
|
+
if (CUSTOM_COMPAT[depLowerCase].compatibleWith === '*') {
|
|
50
|
+
return { compatible: true, reason: 'Ultra-permissive license' }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Validate SPDX expressions
|
|
55
|
+
try {
|
|
56
|
+
parse(projectLicense)
|
|
57
|
+
parse(depLicense)
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// Invalid SPDX expression
|
|
60
|
+
return {
|
|
61
|
+
compatible: false,
|
|
62
|
+
reason: `Invalid SPDX expression: ${depLicense}`
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Same license is always compatible
|
|
67
|
+
if (projectLicense === depLicense) {
|
|
68
|
+
return { compatible: true, reason: 'Compatible' }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check if both are permissive licenses
|
|
72
|
+
const projectIsPermissive = PERMISSIVE_LICENSES.includes(projectLicense)
|
|
73
|
+
const depIsPermissive = PERMISSIVE_LICENSES.includes(depLicense)
|
|
74
|
+
|
|
75
|
+
// Permissive licenses are compatible with each other
|
|
76
|
+
if (projectIsPermissive && depIsPermissive) {
|
|
77
|
+
return { compatible: true, reason: 'Compatible' }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check for copyleft restrictions
|
|
81
|
+
const depIsCopyleft = COPYLEFT_LICENSES.some(lic => depLicense.includes(lic))
|
|
82
|
+
|
|
83
|
+
// Permissive project cannot use copyleft dependencies
|
|
84
|
+
if (projectIsPermissive && depIsCopyleft) {
|
|
85
|
+
return {
|
|
86
|
+
compatible: false,
|
|
87
|
+
reason: 'Copyleft incompatible with permissive license'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Copyleft project can use permissive dependencies
|
|
92
|
+
const projectIsCopyleft = COPYLEFT_LICENSES.some(lic => projectLicense.includes(lic))
|
|
93
|
+
if (projectIsCopyleft && depIsPermissive) {
|
|
94
|
+
return { compatible: true, reason: 'Compatible' }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// For complex expressions, try spdx-satisfies
|
|
98
|
+
try {
|
|
99
|
+
const isCompatible = satisfies(depLicense, projectLicense)
|
|
100
|
+
if (isCompatible) {
|
|
101
|
+
return { compatible: true, reason: 'Compatible' }
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// Fall through to default incompatible
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Default: incompatible
|
|
108
|
+
return {
|
|
109
|
+
compatible: false,
|
|
110
|
+
reason: `License ${depLicense} incompatible with ${projectLicense}`
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { checkCompatibility, CUSTOM_COMPAT }
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency License Scanner
|
|
3
|
+
* Scans node_modules for license conflicts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs')
|
|
7
|
+
const path = require('path')
|
|
8
|
+
const chalk = require('chalk')
|
|
9
|
+
const { checkCompatibility } = require('./compat-checker')
|
|
10
|
+
const { showProgress } = require('./progress')
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse package.json to get dependencies
|
|
14
|
+
* @returns {{deps: string[], packageJson: Object}} Dependency list and package.json
|
|
15
|
+
*/
|
|
16
|
+
function parsePackageJson() {
|
|
17
|
+
try {
|
|
18
|
+
const content = fs.readFileSync('package.json', 'utf8')
|
|
19
|
+
const packageJson = JSON.parse(content)
|
|
20
|
+
|
|
21
|
+
// Only scan production dependencies
|
|
22
|
+
const deps = Object.keys(packageJson.dependencies || {})
|
|
23
|
+
|
|
24
|
+
return { deps, packageJson }
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw new Error('Failed to read package.json: ' + error.message)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extract license info from a dependency
|
|
32
|
+
* @param {string} depName - Dependency name
|
|
33
|
+
* @returns {{name: string, version: string, license: string, path: string}} License info
|
|
34
|
+
*/
|
|
35
|
+
function extractLicense(depName) {
|
|
36
|
+
const depPath = path.join('node_modules', depName, 'package.json')
|
|
37
|
+
|
|
38
|
+
if (!fs.existsSync(depPath)) {
|
|
39
|
+
return {
|
|
40
|
+
name: depName,
|
|
41
|
+
version: 'unknown',
|
|
42
|
+
license: 'NOT_INSTALLED',
|
|
43
|
+
path: depPath
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const content = fs.readFileSync(depPath, 'utf8')
|
|
49
|
+
const depPackageJson = JSON.parse(content)
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
name: depName,
|
|
53
|
+
version: depPackageJson.version || 'unknown',
|
|
54
|
+
license: depPackageJson.license || 'UNKNOWN',
|
|
55
|
+
path: depPath
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return {
|
|
59
|
+
name: depName,
|
|
60
|
+
version: 'unknown',
|
|
61
|
+
license: 'PARSE_ERROR',
|
|
62
|
+
path: depPath
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Scan all dependencies for license compatibility
|
|
69
|
+
* @param {string} projectLicense - The project's license
|
|
70
|
+
* @returns {Promise<Object>} Scan results
|
|
71
|
+
*/
|
|
72
|
+
async function scanDependencies(projectLicense) {
|
|
73
|
+
// 1. Read project package.json
|
|
74
|
+
const { deps } = parsePackageJson()
|
|
75
|
+
|
|
76
|
+
const results = {
|
|
77
|
+
timestamp: new Date().toISOString(),
|
|
78
|
+
totalDependencies: deps.length,
|
|
79
|
+
compatible: 0,
|
|
80
|
+
incompatible: 0,
|
|
81
|
+
unknown: 0,
|
|
82
|
+
issues: []
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. Scan each dependency
|
|
86
|
+
for (let i = 0; i < deps.length; i++) {
|
|
87
|
+
showProgress(i + 1, deps.length)
|
|
88
|
+
|
|
89
|
+
const depName = deps[i]
|
|
90
|
+
const depInfo = extractLicense(depName)
|
|
91
|
+
|
|
92
|
+
// Skip if not installed
|
|
93
|
+
if (depInfo.license === 'NOT_INSTALLED') {
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Handle parse errors as unknown
|
|
98
|
+
if (depInfo.license === 'PARSE_ERROR') {
|
|
99
|
+
results.unknown++
|
|
100
|
+
results.issues.push({
|
|
101
|
+
package: `${depName}@${depInfo.version}`,
|
|
102
|
+
license: 'UNKNOWN',
|
|
103
|
+
type: 'warning',
|
|
104
|
+
reason: 'Failed to parse package.json',
|
|
105
|
+
location: depInfo.path
|
|
106
|
+
})
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 3. Check compatibility
|
|
111
|
+
const compatResult = checkCompatibility(projectLicense, depInfo.license)
|
|
112
|
+
|
|
113
|
+
if (!compatResult.compatible) {
|
|
114
|
+
const isUnknown = depInfo.license === 'UNKNOWN'
|
|
115
|
+
|
|
116
|
+
if (isUnknown) {
|
|
117
|
+
results.unknown++
|
|
118
|
+
} else {
|
|
119
|
+
results.incompatible++
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
results.issues.push({
|
|
123
|
+
package: `${depName}@${depInfo.version}`,
|
|
124
|
+
license: depInfo.license,
|
|
125
|
+
type: isUnknown ? 'warning' : 'conflict',
|
|
126
|
+
reason: compatResult.reason,
|
|
127
|
+
location: depInfo.path
|
|
128
|
+
})
|
|
129
|
+
} else {
|
|
130
|
+
results.compatible++
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return results
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Display conflict report to console
|
|
139
|
+
* @param {Object} scanResult - Scan results
|
|
140
|
+
* @param {string} projectLicense - The project's license
|
|
141
|
+
* @returns {boolean} True if conflicts found (incompatible licenses), false otherwise
|
|
142
|
+
*/
|
|
143
|
+
function displayConflictReport(scanResult, projectLicense) {
|
|
144
|
+
if (scanResult.incompatible === 0 && scanResult.unknown === 0) {
|
|
145
|
+
console.log(chalk.green(`\nā
All ${scanResult.totalDependencies} dependencies compatible with ${projectLicense.toUpperCase()}!`))
|
|
146
|
+
return false // No conflicts
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const issueCount = scanResult.incompatible + scanResult.unknown
|
|
150
|
+
const hasConflicts = scanResult.incompatible > 0
|
|
151
|
+
|
|
152
|
+
if (hasConflicts) {
|
|
153
|
+
console.log(chalk.red(`\nā ${issueCount} issue(s) found:\n`))
|
|
154
|
+
} else {
|
|
155
|
+
// Only warnings, no conflicts
|
|
156
|
+
console.log(chalk.yellow(`\nā ļø ${issueCount} warning(s) found:\n`))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const issue of scanResult.issues) {
|
|
160
|
+
if (issue.type === 'conflict') {
|
|
161
|
+
console.log(chalk.red(`ā ${issue.package} (${issue.license})`))
|
|
162
|
+
} else {
|
|
163
|
+
console.log(chalk.yellow(`ā ļø ${issue.package} (${issue.license})`))
|
|
164
|
+
}
|
|
165
|
+
console.log(chalk.gray(` ${issue.reason}`))
|
|
166
|
+
console.log(chalk.gray(` Location: ${issue.location}\n`))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Only return true if there are actual conflicts (incompatible licenses)
|
|
170
|
+
// Warnings (unknown licenses) should not block
|
|
171
|
+
return hasConflicts
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
scanDependencies,
|
|
176
|
+
parsePackageJson,
|
|
177
|
+
extractLicense,
|
|
178
|
+
displayConflictReport
|
|
179
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress indicator for dependency scanning
|
|
3
|
+
* Displays "Scanning dependencies... N/total" with in-place updates
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Show scanning progress indicator
|
|
8
|
+
* @param {number} current - Current dependency count
|
|
9
|
+
* @param {number} total - Total dependencies to scan
|
|
10
|
+
*/
|
|
11
|
+
function showProgress(current, total) {
|
|
12
|
+
// Clear current line and write progress
|
|
13
|
+
// \r returns to start of line without newline
|
|
14
|
+
process.stdout.write(`\rScanning dependencies... ${current}/${total}`)
|
|
15
|
+
|
|
16
|
+
// Newline when complete
|
|
17
|
+
if (current === total) {
|
|
18
|
+
process.stdout.write('\n')
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { showProgress }
|
package/lib/utils/file-ops.js
CHANGED
|
@@ -34,7 +34,24 @@ async function writeLicenseFile(content) {
|
|
|
34
34
|
|
|
35
35
|
function writeConfig(config) {
|
|
36
36
|
try {
|
|
37
|
-
|
|
37
|
+
let finalConfig = config
|
|
38
|
+
|
|
39
|
+
// If .licenseguardrc exists, preserve existing fields when updating
|
|
40
|
+
if (fs.existsSync('.licenseguardrc')) {
|
|
41
|
+
try {
|
|
42
|
+
const existingContent = fs.readFileSync('.licenseguardrc', 'utf8')
|
|
43
|
+
const existingConfig = JSON.parse(existingContent)
|
|
44
|
+
|
|
45
|
+
// Merge: new config overwrites matching fields (including scanResult)
|
|
46
|
+
// But existing fields not in new config are preserved
|
|
47
|
+
finalConfig = { ...existingConfig, ...config }
|
|
48
|
+
} catch (parseError) {
|
|
49
|
+
// If existing file is malformed, just use new config
|
|
50
|
+
console.log(chalk.yellow('ā ļø Existing .licenseguardrc malformed, overwriting'))
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const configContent = JSON.stringify(finalConfig, null, 2)
|
|
38
55
|
fs.writeFileSync('.licenseguardrc', configContent, 'utf8')
|
|
39
56
|
return true
|
|
40
57
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "licenseguard-cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "License setup & compliance
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "License setup & compliance guard for developers",
|
|
5
5
|
"bin": {
|
|
6
6
|
"licenseguard": "./bin/licenseguard.js"
|
|
7
7
|
},
|
|
@@ -9,15 +9,24 @@
|
|
|
9
9
|
"doc": "docs"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
+
"test": "jest",
|
|
13
|
+
"test:watch": "jest --watch",
|
|
14
|
+
"test:coverage": "jest --coverage",
|
|
15
|
+
"lint": "eslint .",
|
|
16
|
+
"format": "prettier --write .",
|
|
12
17
|
"postinstall": "node lib/postinstall.js"
|
|
13
18
|
},
|
|
14
19
|
"keywords": [
|
|
15
20
|
"license",
|
|
16
21
|
"compliance",
|
|
22
|
+
"scanning",
|
|
23
|
+
"spdx",
|
|
24
|
+
"dependencies",
|
|
17
25
|
"MIT",
|
|
18
26
|
"Apache",
|
|
19
27
|
"GPL",
|
|
20
|
-
"developer-tools"
|
|
28
|
+
"developer-tools",
|
|
29
|
+
"license-checker"
|
|
21
30
|
],
|
|
22
31
|
"author": "v",
|
|
23
32
|
"license": "MIT",
|
|
@@ -25,6 +34,13 @@
|
|
|
25
34
|
"chalk": "^4.1.2",
|
|
26
35
|
"commander": "^11.1.0",
|
|
27
36
|
"inquirer": "^8.2.5",
|
|
28
|
-
"
|
|
37
|
+
"spdx-expression-parse": "^4.0.0",
|
|
38
|
+
"spdx-satisfies": "^6.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"eslint": "^8.54.0",
|
|
42
|
+
"jest": "^29.7.0",
|
|
43
|
+
"mock-fs": "^5.2.0",
|
|
44
|
+
"prettier": "^3.1.0"
|
|
29
45
|
}
|
|
30
46
|
}
|