@wipcomputer/wip-license-guard 1.9.13 → 1.9.14

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.
Files changed (3) hide show
  1. package/cli.mjs +119 -1
  2. package/core.mjs +46 -2
  3. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -6,7 +6,7 @@
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'];
@@ -320,6 +320,118 @@ async function check(repoPath) {
320
320
  return issues;
321
321
  }
322
322
 
323
+ async function readmeLicense(targetPath) {
324
+ log(`\n wip-license-guard readme-license${FIX ? ' --fix' : ''}\n`);
325
+
326
+ // Detect if targetPath is a single repo or a directory of repos
327
+ const repos = [];
328
+ const configPath = join(targetPath, '.license-guard.json');
329
+ if (existsSync(configPath)) {
330
+ // Single repo
331
+ repos.push(targetPath);
332
+ } else {
333
+ // Directory of repos (or nested categories like ldm-os/components/)
334
+ const scanDir = (dir) => {
335
+ try {
336
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
337
+ if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '_trash') continue;
338
+ const sub = join(dir, entry.name);
339
+ if (existsSync(join(sub, '.license-guard.json'))) {
340
+ repos.push(sub);
341
+ } else if (existsSync(join(sub, 'package.json')) || existsSync(join(sub, 'README.md'))) {
342
+ repos.push(sub);
343
+ } else {
344
+ scanDir(sub); // recurse into category folders
345
+ }
346
+ }
347
+ } catch {}
348
+ };
349
+ scanDir(targetPath);
350
+ }
351
+
352
+ if (repos.length === 0) {
353
+ warn('No repos found. Point at a repo or a directory containing repos.');
354
+ return 1;
355
+ }
356
+
357
+ log(` Found ${repos.length} repo(s)\n`);
358
+
359
+ let totalIssues = 0;
360
+
361
+ for (const repoPath of repos) {
362
+ const repoName = repoPath.split('/').pop();
363
+ const repoConfig = join(repoPath, '.license-guard.json');
364
+ const config = existsSync(repoConfig)
365
+ ? JSON.parse(readFileSync(repoConfig, 'utf8'))
366
+ : WIP_STANDARD;
367
+
368
+ // 1. Check main README
369
+ const readmePath = join(repoPath, 'README.md');
370
+ if (existsSync(readmePath)) {
371
+ const content = readFileSync(readmePath, 'utf8');
372
+ const expected = generateReadmeBlock(config);
373
+
374
+ if (content.includes('### Can I use this?') && content.includes('Dual-license model')) {
375
+ ok(`${repoName}/README.md ... standard license block`);
376
+ } else if (content.includes('## License')) {
377
+ warn(`${repoName}/README.md ... non-standard license section`);
378
+ totalIssues++;
379
+ if (FIX) {
380
+ const updated = replaceReadmeLicenseSection(content, config);
381
+ writeFileSync(readmePath, updated);
382
+ ok(`${repoName}/README.md ... updated to standard (--fix)`);
383
+ totalIssues--;
384
+ }
385
+ } else {
386
+ warn(`${repoName}/README.md ... missing ## License`);
387
+ totalIssues++;
388
+ if (FIX) {
389
+ const updated = replaceReadmeLicenseSection(content, config);
390
+ writeFileSync(readmePath, updated);
391
+ ok(`${repoName}/README.md ... added standard block (--fix)`);
392
+ totalIssues--;
393
+ }
394
+ }
395
+ } else {
396
+ warn(`${repoName}/README.md ... not found`);
397
+ totalIssues++;
398
+ }
399
+
400
+ // 2. Check sub-tool READMEs (should NOT have license sections)
401
+ const toolsDir = join(repoPath, 'tools');
402
+ if (existsSync(toolsDir)) {
403
+ try {
404
+ for (const tool of readdirSync(toolsDir, { withFileTypes: true })) {
405
+ if (!tool.isDirectory()) continue;
406
+ const subReadme = join(toolsDir, tool.name, 'README.md');
407
+ if (!existsSync(subReadme)) continue;
408
+
409
+ const subContent = readFileSync(subReadme, 'utf8');
410
+ if (subContent.includes('## License')) {
411
+ warn(`${repoName}/tools/${tool.name}/README.md ... has license section (should be removed)`);
412
+ totalIssues++;
413
+ if (FIX) {
414
+ const cleaned = removeReadmeLicenseSection(subContent);
415
+ writeFileSync(subReadme, cleaned);
416
+ ok(`${repoName}/tools/${tool.name}/README.md ... license section removed (--fix)`);
417
+ totalIssues--;
418
+ }
419
+ }
420
+ }
421
+ } catch {}
422
+ }
423
+ }
424
+
425
+ log('');
426
+ if (totalIssues === 0) {
427
+ log(' All README license sections are correct.\n');
428
+ } else {
429
+ log(` ${totalIssues} issue(s) found. Run with --fix to auto-repair.\n`);
430
+ }
431
+
432
+ return totalIssues;
433
+ }
434
+
323
435
  // Main
324
436
  if (command === 'init') {
325
437
  await init(target === 'init' ? '.' : target);
@@ -327,6 +439,10 @@ if (command === 'init') {
327
439
  const repoPath = (target === 'check') ? '.' : target;
328
440
  const issues = await check(repoPath);
329
441
  process.exit(issues > 0 ? 1 : 0);
442
+ } else if (command === 'readme-license') {
443
+ const repoPath = (target === 'readme-license') ? '.' : target;
444
+ const issues = await readmeLicense(repoPath);
445
+ process.exit(issues > 0 ? 1 : 0);
330
446
  } else if (command === '--help' || command === '-h' || command === 'help') {
331
447
  console.log(`
332
448
  wip-license-guard
@@ -336,6 +452,8 @@ if (command === 'init') {
336
452
  init --from-standard Apply WIP Computer defaults (MIT+AGPL, CLA, attribution).
337
453
  check [path] Audit repo against saved config. Exit 1 if issues found.
338
454
  check --fix [path] Auto-fix issues (update LICENSE files, wrong copyright).
455
+ readme-license [path] Scan README license sections. Works on one repo or a directory of repos.
456
+ readme-license --fix Apply standard license block to all READMEs. Remove from sub-tools.
339
457
  help Show this help.
340
458
 
341
459
  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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-license-guard",
3
- "version": "1.9.13",
3
+ "version": "1.9.14",
4
4
  "description": "License compliance for your own repos. Ensures correct copyright, dual-license blocks, and LICENSE files.",
5
5
  "type": "module",
6
6
  "bin": {