@wipcomputer/wip-license-guard 1.9.14 → 1.9.17

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 (5) hide show
  1. package/README.md +32 -0
  2. package/SKILL.md +23 -3
  3. package/cli.mjs +35 -38
  4. package/core.mjs +127 -6
  5. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,32 @@
1
+ ###### WIP Computer
2
+
3
+ # License Guard
4
+
5
+ Enforce licensing on every commit. Copyright, dual-license, CLA. Checked automatically.
6
+
7
+ ## What it does
8
+
9
+ - Ensures your own repos have correct copyright, license type, and LICENSE files
10
+ - Interactive first-run setup
11
+ - Toolbox-aware: checks every sub-tool
12
+ - Auto-fix mode repairs issues
13
+ - `readme-license` scans all your repos and applies a standard license block to every README in one command
14
+ - Removes duplicate license sections from sub-tool READMEs
15
+
16
+ ## Usage
17
+
18
+ ```bash
19
+ node tools/wip-license-guard/cli.mjs /path/to/repo
20
+ node tools/wip-license-guard/cli.mjs /path/to/repo --fix
21
+ ```
22
+
23
+ ## Requirements
24
+
25
+ - node (18+)
26
+ - git
27
+
28
+ ## Interfaces
29
+
30
+ - **CLI**: Command-line tool
31
+
32
+ ## Part of [AI DevOps Toolbox](https://github.com/wipcomputer/wip-ai-devops-toolbox)
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. Scans source files for correct copyright headers, verifies dual-license blocks (MIT + AGPL), and checks LICENSE files.
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 /path/to/repo # scan and report
44
- wip-license-guard /path/to/repo --fix # auto-fix missing headers
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, replaceReadmeLicenseSection, removeReadmeLicenseSection } from './core.mjs';
9
+ import { generateLicense, generateCLA, 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
 
@@ -38,29 +39,6 @@ const WIP_STANDARD = {
38
39
  attribution: 'Built by Parker Todd Brooks, Lēsa (OpenClaw, Claude Opus 4.6), Claude Code (Claude Opus 4.6).',
39
40
  };
40
41
 
41
- function generateCLA() {
42
- return `###### WIP Computer
43
-
44
- # Contributor License Agreement
45
-
46
- By submitting a pull request to this repository, you agree to the following:
47
-
48
- 1. **You grant WIP Computer, Inc. a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license** to use, reproduce, modify, distribute, sublicense, and otherwise exploit your contribution under any license, including commercial licenses.
49
-
50
- 2. **You retain copyright** to your contribution. This agreement does not transfer ownership. You can use your own code however you want.
51
-
52
- 3. **You confirm** that your contribution is your original work, or that you have the right to submit it under these terms.
53
-
54
- 4. **You understand** that your contribution may be used in both open source and commercial versions of this software.
55
-
56
- This is standard open source governance. Apache, Google, Meta, and Anthropic all use similar agreements. The goal is simple: keep the tools free for everyone while allowing WIP Computer, Inc. to offer commercial licenses to companies that need them.
57
-
58
- Using these tools to build your own software is always free. This agreement only matters if WIP Computer, Inc. needs to relicense the codebase commercially.
59
-
60
- If you have questions, open an issue or reach out.
61
- `;
62
- }
63
-
64
42
  async function init(repoPath) {
65
43
  const configPath = join(repoPath, '.license-guard.json');
66
44
 
@@ -74,12 +52,12 @@ async function init(repoPath) {
74
52
  ok(`Config saved to .license-guard.json`);
75
53
 
76
54
  const licensePath = join(repoPath, 'LICENSE');
77
- writeFileSync(licensePath, generateLicense(config));
55
+ writeFileSync(licensePath, generateLicense(config, repoPath));
78
56
  ok(`LICENSE file generated (dual MIT+AGPLv3)`);
79
57
 
80
58
  const claPath = join(repoPath, 'CLA.md');
81
59
  if (!existsSync(claPath)) {
82
- writeFileSync(claPath, generateCLA());
60
+ writeFileSync(claPath, generateCLA(repoPath));
83
61
  ok(`CLA.md generated`);
84
62
  } else {
85
63
  ok(`CLA.md already exists`);
@@ -132,14 +110,14 @@ async function init(repoPath) {
132
110
  ok(`Config saved to .license-guard.json`);
133
111
 
134
112
  const licensePath = join(repoPath, 'LICENSE');
135
- const licenseText = generateLicense(config);
113
+ const licenseText = generateLicense(config, repoPath);
136
114
  writeFileSync(licensePath, licenseText);
137
115
  ok(`LICENSE file generated`);
138
116
 
139
117
  // Generate CLA.md if it doesn't exist
140
118
  const claPath = join(repoPath, 'CLA.md');
141
119
  if (!existsSync(claPath)) {
142
- writeFileSync(claPath, generateCLA());
120
+ writeFileSync(claPath, generateCLA(repoPath));
143
121
  ok(`CLA.md generated`);
144
122
  }
145
123
 
@@ -173,7 +151,7 @@ async function check(repoPath) {
173
151
  warn('LICENSE file missing');
174
152
  issues++;
175
153
  if (FIX) {
176
- writeFileSync(licensePath, generateLicense(config));
154
+ writeFileSync(licensePath, generateLicense(config, repoPath));
177
155
  ok('LICENSE file created (--fix)');
178
156
  issues--;
179
157
  }
@@ -184,7 +162,7 @@ async function check(repoPath) {
184
162
  warn(`LICENSE copyright does not match "${config.copyright}"`);
185
163
  issues++;
186
164
  if (FIX) {
187
- writeFileSync(licensePath, generateLicense(config));
165
+ writeFileSync(licensePath, generateLicense(config, repoPath));
188
166
  ok('LICENSE file updated (--fix)');
189
167
  issues--;
190
168
  }
@@ -197,7 +175,7 @@ async function check(repoPath) {
197
175
  warn('LICENSE file is MIT-only but config says MIT+AGPL');
198
176
  issues++;
199
177
  if (FIX) {
200
- writeFileSync(licensePath, generateLicense(config));
178
+ writeFileSync(licensePath, generateLicense(config, repoPath));
201
179
  ok('LICENSE file updated to dual-license (--fix)');
202
180
  issues--;
203
181
  }
@@ -213,7 +191,7 @@ async function check(repoPath) {
213
191
  warn('CLA.md missing');
214
192
  issues++;
215
193
  if (FIX) {
216
- writeFileSync(claPath, generateCLA());
194
+ writeFileSync(claPath, generateCLA(repoPath));
217
195
  ok('CLA.md created (--fix)');
218
196
  issues--;
219
197
  }
@@ -288,7 +266,7 @@ async function check(repoPath) {
288
266
  warn(`tools/${entry.name}/LICENSE missing`);
289
267
  issues++;
290
268
  if (FIX) {
291
- writeFileSync(toolLicense, generateLicense(config));
269
+ writeFileSync(toolLicense, generateLicense(config, repoPath));
292
270
  ok(`tools/${entry.name}/LICENSE created (--fix)`);
293
271
  issues--;
294
272
  }
@@ -298,7 +276,7 @@ async function check(repoPath) {
298
276
  warn(`tools/${entry.name}/LICENSE wrong copyright`);
299
277
  issues++;
300
278
  if (FIX) {
301
- writeFileSync(toolLicense, generateLicense(config));
279
+ writeFileSync(toolLicense, generateLicense(config, repoPath));
302
280
  ok(`tools/${entry.name}/LICENSE updated (--fix)`);
303
281
  issues--;
304
282
  }
@@ -321,7 +299,8 @@ async function check(repoPath) {
321
299
  }
322
300
 
323
301
  async function readmeLicense(targetPath) {
324
- log(`\n wip-license-guard readme-license${FIX ? ' --fix' : ''}\n`);
302
+ const mode = FIX ? '--fix' : DRY_RUN ? '--dry-run' : '';
303
+ log(`\n wip-license-guard readme-license${mode ? ' ' + mode : ''}\n`);
325
304
 
326
305
  // Detect if targetPath is a single repo or a directory of repos
327
306
  const repos = [];
@@ -376,8 +355,16 @@ async function readmeLicense(targetPath) {
376
355
  } else if (content.includes('## License')) {
377
356
  warn(`${repoName}/README.md ... non-standard license section`);
378
357
  totalIssues++;
358
+ if (DRY_RUN) {
359
+ // Extract current license section for preview
360
+ const match = content.match(/## License[\s\S]*?(?=\n## [^#]|$)/);
361
+ if (match) {
362
+ log(` current: ${match[0].split('\n').slice(0, 3).join(' | ').substring(0, 80)}...`);
363
+ }
364
+ log(` would replace with: standard dual MIT/AGPLv3 block`);
365
+ }
379
366
  if (FIX) {
380
- const updated = replaceReadmeLicenseSection(content, config);
367
+ const updated = replaceReadmeLicenseSection(content, config, repoPath);
381
368
  writeFileSync(readmePath, updated);
382
369
  ok(`${repoName}/README.md ... updated to standard (--fix)`);
383
370
  totalIssues--;
@@ -385,8 +372,11 @@ async function readmeLicense(targetPath) {
385
372
  } else {
386
373
  warn(`${repoName}/README.md ... missing ## License`);
387
374
  totalIssues++;
375
+ if (DRY_RUN) {
376
+ log(` would add: standard dual MIT/AGPLv3 block at end of README`);
377
+ }
388
378
  if (FIX) {
389
- const updated = replaceReadmeLicenseSection(content, config);
379
+ const updated = replaceReadmeLicenseSection(content, config, repoPath);
390
380
  writeFileSync(readmePath, updated);
391
381
  ok(`${repoName}/README.md ... added standard block (--fix)`);
392
382
  totalIssues--;
@@ -410,6 +400,9 @@ async function readmeLicense(targetPath) {
410
400
  if (subContent.includes('## License')) {
411
401
  warn(`${repoName}/tools/${tool.name}/README.md ... has license section (should be removed)`);
412
402
  totalIssues++;
403
+ if (DRY_RUN) {
404
+ log(` would remove: ## License section from sub-tool README`);
405
+ }
413
406
  if (FIX) {
414
407
  const cleaned = removeReadmeLicenseSection(subContent);
415
408
  writeFileSync(subReadme, cleaned);
@@ -425,8 +418,11 @@ async function readmeLicense(targetPath) {
425
418
  log('');
426
419
  if (totalIssues === 0) {
427
420
  log(' All README license sections are correct.\n');
421
+ } else if (DRY_RUN) {
422
+ log(` ${totalIssues} issue(s) found. Dry run complete. No changes made.`);
423
+ log(` Run with --fix to apply changes.\n`);
428
424
  } else {
429
- log(` ${totalIssues} issue(s) found. Run with --fix to auto-repair.\n`);
425
+ log(` ${totalIssues} issue(s) found. Run with --dry-run to preview or --fix to apply.\n`);
430
426
  }
431
427
 
432
428
  return totalIssues;
@@ -453,6 +449,7 @@ if (command === 'init') {
453
449
  check [path] Audit repo against saved config. Exit 1 if issues found.
454
450
  check --fix [path] Auto-fix issues (update LICENSE files, wrong copyright).
455
451
  readme-license [path] Scan README license sections. Works on one repo or a directory of repos.
452
+ readme-license --dry-run Preview what would change. Shows current vs standard for each README.
456
453
  readme-license --fix Apply standard license block to all READMEs. Remove from sub-tools.
457
454
  help Show this help.
458
455
 
package/core.mjs CHANGED
@@ -1,12 +1,81 @@
1
1
  // wip-license-guard/core.mjs
2
2
  // License generation and validation logic.
3
+ // Reads templates from ai/wip-templates/readme/ when available.
4
+ // Falls back to hardcoded defaults for standalone use.
3
5
 
4
- export function generateLicense(config) {
6
+ import { existsSync, readFileSync } from 'node:fs';
7
+ import { join, dirname } from 'node:path';
8
+
9
+ // ── Template Resolution ─────────────────────────────────────────────
10
+
11
+ /**
12
+ * Find the templates directory. Checks:
13
+ * 1. WIP_TEMPLATES_DIR env var
14
+ * 2. Walk up from repoPath looking for ai/wip-templates/readme/
15
+ * 3. Walk up from this file's location (for toolbox-internal use)
16
+ * Returns null if not found.
17
+ */
18
+ function findTemplatesDir(repoPath) {
19
+ // 1. Env var
20
+ const envDir = process.env.WIP_TEMPLATES_DIR;
21
+ if (envDir && existsSync(join(envDir, 'LICENSE.md'))) return envDir;
22
+
23
+ // 2. Walk up from repoPath
24
+ if (repoPath) {
25
+ let dir = repoPath;
26
+ for (let i = 0; i < 10; i++) {
27
+ const candidate = join(dir, 'ai', 'wip-templates', 'readme');
28
+ if (existsSync(join(candidate, 'LICENSE.md'))) return candidate;
29
+ const parent = dirname(dir);
30
+ if (parent === dir) break;
31
+ dir = parent;
32
+ }
33
+ }
34
+
35
+ // 3. Walk up from this file (tools/wip-license-guard/ -> repo root)
36
+ const thisDir = dirname(new URL(import.meta.url).pathname);
37
+ let dir = thisDir;
38
+ for (let i = 0; i < 10; i++) {
39
+ const candidate = join(dir, 'ai', 'wip-templates', 'readme');
40
+ if (existsSync(join(candidate, 'LICENSE.md'))) return candidate;
41
+ const parent = dirname(dir);
42
+ if (parent === dir) break;
43
+ dir = parent;
44
+ }
45
+
46
+ return null;
47
+ }
48
+
49
+ /**
50
+ * Read a template file. Returns content or null.
51
+ */
52
+ function readTemplate(templatesDir, filename) {
53
+ if (!templatesDir) return null;
54
+ const path = join(templatesDir, filename);
55
+ if (!existsSync(path)) return null;
56
+ return readFileSync(path, 'utf8');
57
+ }
58
+
59
+ /**
60
+ * Extract the markdown format section from wip-lic-footer.md.
61
+ * The file has two sections: // PLAIN TXT and // MD FORMAT.
62
+ * Returns the MD FORMAT section, or the whole file if no marker found.
63
+ */
64
+ function extractMdFormat(content) {
65
+ const marker = '// MD FORMAT';
66
+ const idx = content.indexOf(marker);
67
+ if (idx === -1) return content;
68
+ return content.slice(idx + marker.length).trim();
69
+ }
70
+
71
+ // ── License Generation ──────────────────────────────────────────────
72
+
73
+ export function generateLicense(config, repoPath) {
5
74
  const { copyright, license, year } = config;
6
75
 
7
76
  if (license === 'MIT') return generateMIT(copyright, year);
8
77
  if (license === 'AGPL-3.0') return generateAGPL(copyright, year);
9
- if (license === 'MIT+AGPL') return generateDual(copyright, year);
78
+ if (license === 'MIT+AGPL') return generateDual(copyright, year, repoPath);
10
79
 
11
80
  return generateMIT(copyright, year);
12
81
  }
@@ -56,7 +125,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
56
125
  `;
57
126
  }
58
127
 
59
- function generateDual(copyright, year) {
128
+ function generateDual(copyright, year, repoPath) {
129
+ // Try template first
130
+ const templatesDir = findTemplatesDir(repoPath);
131
+ const template = readTemplate(templatesDir, 'LICENSE.md');
132
+ if (template) {
133
+ // Replace copyright year if template has a different one
134
+ return template.replace(/Copyright \(c\) \d{4}/, `Copyright (c) ${year}`);
135
+ }
136
+
137
+ // Hardcoded fallback
60
138
  return `Dual License: MIT + AGPLv3
61
139
 
62
140
  Copyright (c) ${year} ${copyright}
@@ -112,7 +190,40 @@ AGPLv3 for personal use is free. Commercial licenses available.
112
190
  `;
113
191
  }
114
192
 
115
- export function generateReadmeBlock(config) {
193
+ // ── CLA Generation ──────────────────────────────────────────────────
194
+
195
+ export function generateCLA(repoPath) {
196
+ // Try template first
197
+ const templatesDir = findTemplatesDir(repoPath);
198
+ const template = readTemplate(templatesDir, 'cla.md');
199
+ if (template) return template;
200
+
201
+ // Hardcoded fallback
202
+ return `###### WIP Computer
203
+
204
+ # Contributor License Agreement
205
+
206
+ By submitting a pull request to this repository, you agree to the following:
207
+
208
+ 1. **You grant WIP Computer, Inc. a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license** to use, reproduce, modify, distribute, sublicense, and otherwise exploit your contribution under any license, including commercial licenses.
209
+
210
+ 2. **You retain copyright** to your contribution. This agreement does not transfer ownership. You can use your own code however you want.
211
+
212
+ 3. **You confirm** that your contribution is your original work, or that you have the right to submit it under these terms.
213
+
214
+ 4. **You understand** that your contribution may be used in both open source and commercial versions of this software.
215
+
216
+ This is standard open source governance. Apache, Google, Meta, and Anthropic all use similar agreements. The goal is simple: keep the tools free for everyone while allowing WIP Computer, Inc. to offer commercial licenses to companies that need them.
217
+
218
+ Using these tools to build your own software is always free. This agreement only matters if WIP Computer, Inc. needs to relicense the codebase commercially.
219
+
220
+ If you have questions, open an issue or reach out.
221
+ `;
222
+ }
223
+
224
+ // ── README License Block ────────────────────────────────────────────
225
+
226
+ export function generateReadmeBlock(config, repoPath) {
116
227
  const { license, attribution } = config;
117
228
 
118
229
  if (license === 'MIT') {
@@ -129,6 +240,14 @@ AGPLv3. AGPLv3 for personal use is free.${attribution ? '\n\n' + attribution : '
129
240
  `;
130
241
  }
131
242
 
243
+ // MIT+AGPL: try template first
244
+ const templatesDir = findTemplatesDir(repoPath);
245
+ const footer = readTemplate(templatesDir, 'wip-lic-footer.md');
246
+ if (footer) {
247
+ return extractMdFormat(footer);
248
+ }
249
+
250
+ // Hardcoded fallback
132
251
  return `## License
133
252
 
134
253
  Dual-license model designed to keep tools free while preventing commercial resellers.
@@ -160,13 +279,15 @@ By submitting a PR, you agree to the [Contributor License Agreement](CLA.md).
160
279
  ${attribution ? '\n' + attribution : ''}`;
161
280
  }
162
281
 
282
+ // ── README License Section Replace/Remove ───────────────────────────
283
+
163
284
  /**
164
285
  * Replace ## License section in readme content.
165
286
  * If no ## License exists, appends the block at the end.
166
287
  * Returns the updated content.
167
288
  */
168
- export function replaceReadmeLicenseSection(content, config) {
169
- const block = generateReadmeBlock(config);
289
+ export function replaceReadmeLicenseSection(content, config, repoPath) {
290
+ const block = generateReadmeBlock(config, repoPath);
170
291
 
171
292
  // Match from "## License" to the next ## heading or end of file
172
293
  const licenseRegex = /## License[\s\S]*?(?=\n## [^#]|\n---\s*$|$)/;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-license-guard",
3
- "version": "1.9.14",
3
+ "version": "1.9.17",
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": {