@wipcomputer/wip-license-guard 1.9.13 → 1.9.15
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/SKILL.md +23 -3
- package/cli.mjs +139 -1
- package/core.mjs +46 -2
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -29,17 +29,37 @@ metadata:
|
|
|
29
29
|
|
|
30
30
|
# wip-license-guard
|
|
31
31
|
|
|
32
|
-
License compliance for your own repos.
|
|
32
|
+
License compliance for your own repos. Ensures correct copyright, dual-license blocks, LICENSE files, and README license sections.
|
|
33
33
|
|
|
34
34
|
## When to Use This Skill
|
|
35
35
|
|
|
36
36
|
- Before a release, to verify all files have correct license headers
|
|
37
37
|
- After adding new source files to a repo
|
|
38
38
|
- To enforce the MIT/AGPL dual-license pattern
|
|
39
|
+
- To standardize README license sections across all your repos
|
|
39
40
|
|
|
40
41
|
## CLI
|
|
41
42
|
|
|
42
43
|
```bash
|
|
43
|
-
wip-license-guard
|
|
44
|
-
wip-license-guard
|
|
44
|
+
wip-license-guard check [path] # audit repo against config
|
|
45
|
+
wip-license-guard check --fix [path] # auto-fix LICENSE, CLA, copyright issues
|
|
46
|
+
wip-license-guard init [path] # interactive setup
|
|
47
|
+
wip-license-guard init --from-standard # apply WIP Computer defaults (no prompts)
|
|
48
|
+
wip-license-guard readme-license [path] # audit README license sections
|
|
49
|
+
wip-license-guard readme-license --dry-run # preview what would change
|
|
50
|
+
wip-license-guard readme-license --fix # apply standard block to all READMEs
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### readme-license
|
|
54
|
+
|
|
55
|
+
Scans all repos for README license sections. Three modes:
|
|
56
|
+
|
|
57
|
+
- **No flags**: audit only. Reports non-standard, missing, and sub-tool READMEs that shouldn't have license sections.
|
|
58
|
+
- **--dry-run**: preview. Shows what each README has now and what would change. No files touched.
|
|
59
|
+
- **--fix**: apply. Replaces non-standard sections with the standard dual MIT/AGPLv3 block. Removes license sections from sub-tool READMEs.
|
|
60
|
+
|
|
61
|
+
Works on a single repo or a directory of repos:
|
|
62
|
+
```bash
|
|
63
|
+
wip-license-guard readme-license /path/to/one-repo
|
|
64
|
+
wip-license-guard readme-license /path/to/directory-of-repos
|
|
45
65
|
```
|
package/cli.mjs
CHANGED
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
import { existsSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { createInterface } from 'node:readline';
|
|
9
|
-
import { generateLicense, generateReadmeBlock } from './core.mjs';
|
|
9
|
+
import { generateLicense, generateReadmeBlock, replaceReadmeLicenseSection, removeReadmeLicenseSection } from './core.mjs';
|
|
10
10
|
|
|
11
11
|
const args = process.argv.slice(2);
|
|
12
12
|
const HELP_FLAGS = ['--help', '-h', 'help'];
|
|
13
13
|
const command = HELP_FLAGS.some(f => args.includes(f)) ? 'help' : (args.find(a => !a.startsWith('--')) || 'check');
|
|
14
14
|
const target = args.find((a, i) => i > 0 && !a.startsWith('--')) || '.';
|
|
15
15
|
const FIX = args.includes('--fix');
|
|
16
|
+
const DRY_RUN = args.includes('--dry-run');
|
|
16
17
|
const QUIET = args.includes('--quiet');
|
|
17
18
|
const FROM_STANDARD = args.includes('--from-standard');
|
|
18
19
|
|
|
@@ -320,6 +321,136 @@ async function check(repoPath) {
|
|
|
320
321
|
return issues;
|
|
321
322
|
}
|
|
322
323
|
|
|
324
|
+
async function readmeLicense(targetPath) {
|
|
325
|
+
const mode = FIX ? '--fix' : DRY_RUN ? '--dry-run' : '';
|
|
326
|
+
log(`\n wip-license-guard readme-license${mode ? ' ' + mode : ''}\n`);
|
|
327
|
+
|
|
328
|
+
// Detect if targetPath is a single repo or a directory of repos
|
|
329
|
+
const repos = [];
|
|
330
|
+
const configPath = join(targetPath, '.license-guard.json');
|
|
331
|
+
if (existsSync(configPath)) {
|
|
332
|
+
// Single repo
|
|
333
|
+
repos.push(targetPath);
|
|
334
|
+
} else {
|
|
335
|
+
// Directory of repos (or nested categories like ldm-os/components/)
|
|
336
|
+
const scanDir = (dir) => {
|
|
337
|
+
try {
|
|
338
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
339
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '_trash') continue;
|
|
340
|
+
const sub = join(dir, entry.name);
|
|
341
|
+
if (existsSync(join(sub, '.license-guard.json'))) {
|
|
342
|
+
repos.push(sub);
|
|
343
|
+
} else if (existsSync(join(sub, 'package.json')) || existsSync(join(sub, 'README.md'))) {
|
|
344
|
+
repos.push(sub);
|
|
345
|
+
} else {
|
|
346
|
+
scanDir(sub); // recurse into category folders
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
} catch {}
|
|
350
|
+
};
|
|
351
|
+
scanDir(targetPath);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (repos.length === 0) {
|
|
355
|
+
warn('No repos found. Point at a repo or a directory containing repos.');
|
|
356
|
+
return 1;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
log(` Found ${repos.length} repo(s)\n`);
|
|
360
|
+
|
|
361
|
+
let totalIssues = 0;
|
|
362
|
+
|
|
363
|
+
for (const repoPath of repos) {
|
|
364
|
+
const repoName = repoPath.split('/').pop();
|
|
365
|
+
const repoConfig = join(repoPath, '.license-guard.json');
|
|
366
|
+
const config = existsSync(repoConfig)
|
|
367
|
+
? JSON.parse(readFileSync(repoConfig, 'utf8'))
|
|
368
|
+
: WIP_STANDARD;
|
|
369
|
+
|
|
370
|
+
// 1. Check main README
|
|
371
|
+
const readmePath = join(repoPath, 'README.md');
|
|
372
|
+
if (existsSync(readmePath)) {
|
|
373
|
+
const content = readFileSync(readmePath, 'utf8');
|
|
374
|
+
const expected = generateReadmeBlock(config);
|
|
375
|
+
|
|
376
|
+
if (content.includes('### Can I use this?') && content.includes('Dual-license model')) {
|
|
377
|
+
ok(`${repoName}/README.md ... standard license block`);
|
|
378
|
+
} else if (content.includes('## License')) {
|
|
379
|
+
warn(`${repoName}/README.md ... non-standard license section`);
|
|
380
|
+
totalIssues++;
|
|
381
|
+
if (DRY_RUN) {
|
|
382
|
+
// Extract current license section for preview
|
|
383
|
+
const match = content.match(/## License[\s\S]*?(?=\n## [^#]|$)/);
|
|
384
|
+
if (match) {
|
|
385
|
+
log(` current: ${match[0].split('\n').slice(0, 3).join(' | ').substring(0, 80)}...`);
|
|
386
|
+
}
|
|
387
|
+
log(` would replace with: standard dual MIT/AGPLv3 block`);
|
|
388
|
+
}
|
|
389
|
+
if (FIX) {
|
|
390
|
+
const updated = replaceReadmeLicenseSection(content, config);
|
|
391
|
+
writeFileSync(readmePath, updated);
|
|
392
|
+
ok(`${repoName}/README.md ... updated to standard (--fix)`);
|
|
393
|
+
totalIssues--;
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
396
|
+
warn(`${repoName}/README.md ... missing ## License`);
|
|
397
|
+
totalIssues++;
|
|
398
|
+
if (DRY_RUN) {
|
|
399
|
+
log(` would add: standard dual MIT/AGPLv3 block at end of README`);
|
|
400
|
+
}
|
|
401
|
+
if (FIX) {
|
|
402
|
+
const updated = replaceReadmeLicenseSection(content, config);
|
|
403
|
+
writeFileSync(readmePath, updated);
|
|
404
|
+
ok(`${repoName}/README.md ... added standard block (--fix)`);
|
|
405
|
+
totalIssues--;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
warn(`${repoName}/README.md ... not found`);
|
|
410
|
+
totalIssues++;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 2. Check sub-tool READMEs (should NOT have license sections)
|
|
414
|
+
const toolsDir = join(repoPath, 'tools');
|
|
415
|
+
if (existsSync(toolsDir)) {
|
|
416
|
+
try {
|
|
417
|
+
for (const tool of readdirSync(toolsDir, { withFileTypes: true })) {
|
|
418
|
+
if (!tool.isDirectory()) continue;
|
|
419
|
+
const subReadme = join(toolsDir, tool.name, 'README.md');
|
|
420
|
+
if (!existsSync(subReadme)) continue;
|
|
421
|
+
|
|
422
|
+
const subContent = readFileSync(subReadme, 'utf8');
|
|
423
|
+
if (subContent.includes('## License')) {
|
|
424
|
+
warn(`${repoName}/tools/${tool.name}/README.md ... has license section (should be removed)`);
|
|
425
|
+
totalIssues++;
|
|
426
|
+
if (DRY_RUN) {
|
|
427
|
+
log(` would remove: ## License section from sub-tool README`);
|
|
428
|
+
}
|
|
429
|
+
if (FIX) {
|
|
430
|
+
const cleaned = removeReadmeLicenseSection(subContent);
|
|
431
|
+
writeFileSync(subReadme, cleaned);
|
|
432
|
+
ok(`${repoName}/tools/${tool.name}/README.md ... license section removed (--fix)`);
|
|
433
|
+
totalIssues--;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
} catch {}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
log('');
|
|
442
|
+
if (totalIssues === 0) {
|
|
443
|
+
log(' All README license sections are correct.\n');
|
|
444
|
+
} else if (DRY_RUN) {
|
|
445
|
+
log(` ${totalIssues} issue(s) found. Dry run complete. No changes made.`);
|
|
446
|
+
log(` Run with --fix to apply changes.\n`);
|
|
447
|
+
} else {
|
|
448
|
+
log(` ${totalIssues} issue(s) found. Run with --dry-run to preview or --fix to apply.\n`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return totalIssues;
|
|
452
|
+
}
|
|
453
|
+
|
|
323
454
|
// Main
|
|
324
455
|
if (command === 'init') {
|
|
325
456
|
await init(target === 'init' ? '.' : target);
|
|
@@ -327,6 +458,10 @@ if (command === 'init') {
|
|
|
327
458
|
const repoPath = (target === 'check') ? '.' : target;
|
|
328
459
|
const issues = await check(repoPath);
|
|
329
460
|
process.exit(issues > 0 ? 1 : 0);
|
|
461
|
+
} else if (command === 'readme-license') {
|
|
462
|
+
const repoPath = (target === 'readme-license') ? '.' : target;
|
|
463
|
+
const issues = await readmeLicense(repoPath);
|
|
464
|
+
process.exit(issues > 0 ? 1 : 0);
|
|
330
465
|
} else if (command === '--help' || command === '-h' || command === 'help') {
|
|
331
466
|
console.log(`
|
|
332
467
|
wip-license-guard
|
|
@@ -336,6 +471,9 @@ if (command === 'init') {
|
|
|
336
471
|
init --from-standard Apply WIP Computer defaults (MIT+AGPL, CLA, attribution).
|
|
337
472
|
check [path] Audit repo against saved config. Exit 1 if issues found.
|
|
338
473
|
check --fix [path] Auto-fix issues (update LICENSE files, wrong copyright).
|
|
474
|
+
readme-license [path] Scan README license sections. Works on one repo or a directory of repos.
|
|
475
|
+
readme-license --dry-run Preview what would change. Shows current vs standard for each README.
|
|
476
|
+
readme-license --fix Apply standard license block to all READMEs. Remove from sub-tools.
|
|
339
477
|
help Show this help.
|
|
340
478
|
|
|
341
479
|
On first run, if no config exists, check will offer to run init.
|
package/core.mjs
CHANGED
|
@@ -131,15 +131,59 @@ AGPLv3. AGPLv3 for personal use is free.${attribution ? '\n\n' + attribution : '
|
|
|
131
131
|
|
|
132
132
|
return `## License
|
|
133
133
|
|
|
134
|
+
Dual-license model designed to keep tools free while preventing commercial resellers.
|
|
135
|
+
|
|
134
136
|
\`\`\`
|
|
135
137
|
MIT All CLI tools, MCP servers, skills, and hooks (use anywhere, no restrictions).
|
|
136
138
|
AGPLv3 Commercial redistribution, marketplace listings, or bundling into paid services.
|
|
137
139
|
\`\`\`
|
|
138
140
|
|
|
139
|
-
Dual-license model designed to keep tools free while preventing commercial resellers.
|
|
140
|
-
|
|
141
141
|
AGPLv3 for personal use is free. Commercial licenses available.
|
|
142
142
|
|
|
143
|
+
### Can I use this?
|
|
144
|
+
|
|
145
|
+
**Yes, freely:**
|
|
146
|
+
- Use any tool locally or on your own servers
|
|
147
|
+
- Modify the code for your own projects
|
|
148
|
+
- Include in your internal CI/CD pipelines
|
|
149
|
+
- Fork it and send us feedback via PRs (we'd love that)
|
|
150
|
+
|
|
151
|
+
**Need a commercial license:**
|
|
152
|
+
- Bundle into a product you sell
|
|
153
|
+
- List on a marketplace (VS Code, JetBrains, etc.)
|
|
154
|
+
- Offer as part of a hosted/SaaS platform
|
|
155
|
+
- Redistribute commercially
|
|
156
|
+
|
|
143
157
|
Using these tools to build your own software is fine. Reselling the tools themselves is what requires a commercial license.
|
|
158
|
+
|
|
159
|
+
By submitting a PR, you agree to the [Contributor License Agreement](CLA.md).
|
|
144
160
|
${attribution ? '\n' + attribution : ''}`;
|
|
145
161
|
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Replace ## License section in readme content.
|
|
165
|
+
* If no ## License exists, appends the block at the end.
|
|
166
|
+
* Returns the updated content.
|
|
167
|
+
*/
|
|
168
|
+
export function replaceReadmeLicenseSection(content, config) {
|
|
169
|
+
const block = generateReadmeBlock(config);
|
|
170
|
+
|
|
171
|
+
// Match from "## License" to the next ## heading or end of file
|
|
172
|
+
const licenseRegex = /## License[\s\S]*?(?=\n## [^#]|\n---\s*$|$)/;
|
|
173
|
+
if (licenseRegex.test(content)) {
|
|
174
|
+
return content.replace(licenseRegex, block.trimEnd());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// No license section found, append
|
|
178
|
+
return content.trimEnd() + '\n\n' + block;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Remove ## License section from content (for sub-tool READMEs).
|
|
183
|
+
* Returns the updated content.
|
|
184
|
+
*/
|
|
185
|
+
export function removeReadmeLicenseSection(content) {
|
|
186
|
+
// Match from "## License" to the next ## heading or end of file
|
|
187
|
+
const licenseRegex = /\n## License[\s\S]*?(?=\n## [^#]|$)/;
|
|
188
|
+
return content.replace(licenseRegex, '').trimEnd() + '\n';
|
|
189
|
+
}
|
package/package.json
CHANGED