@wipcomputer/wip-license-guard 1.9.15 → 1.9.18

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 (4) hide show
  1. package/README.md +32 -0
  2. package/cli.mjs +13 -36
  3. package/core.mjs +127 -6
  4. 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/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, 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'];
@@ -39,29 +39,6 @@ const WIP_STANDARD = {
39
39
  attribution: 'Built by Parker Todd Brooks, Lēsa (OpenClaw, Claude Opus 4.6), Claude Code (Claude Opus 4.6).',
40
40
  };
41
41
 
42
- function generateCLA() {
43
- return `###### WIP Computer
44
-
45
- # Contributor License Agreement
46
-
47
- By submitting a pull request to this repository, you agree to the following:
48
-
49
- 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.
50
-
51
- 2. **You retain copyright** to your contribution. This agreement does not transfer ownership. You can use your own code however you want.
52
-
53
- 3. **You confirm** that your contribution is your original work, or that you have the right to submit it under these terms.
54
-
55
- 4. **You understand** that your contribution may be used in both open source and commercial versions of this software.
56
-
57
- 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.
58
-
59
- 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.
60
-
61
- If you have questions, open an issue or reach out.
62
- `;
63
- }
64
-
65
42
  async function init(repoPath) {
66
43
  const configPath = join(repoPath, '.license-guard.json');
67
44
 
@@ -75,12 +52,12 @@ async function init(repoPath) {
75
52
  ok(`Config saved to .license-guard.json`);
76
53
 
77
54
  const licensePath = join(repoPath, 'LICENSE');
78
- writeFileSync(licensePath, generateLicense(config));
55
+ writeFileSync(licensePath, generateLicense(config, repoPath));
79
56
  ok(`LICENSE file generated (dual MIT+AGPLv3)`);
80
57
 
81
58
  const claPath = join(repoPath, 'CLA.md');
82
59
  if (!existsSync(claPath)) {
83
- writeFileSync(claPath, generateCLA());
60
+ writeFileSync(claPath, generateCLA(repoPath));
84
61
  ok(`CLA.md generated`);
85
62
  } else {
86
63
  ok(`CLA.md already exists`);
@@ -133,14 +110,14 @@ async function init(repoPath) {
133
110
  ok(`Config saved to .license-guard.json`);
134
111
 
135
112
  const licensePath = join(repoPath, 'LICENSE');
136
- const licenseText = generateLicense(config);
113
+ const licenseText = generateLicense(config, repoPath);
137
114
  writeFileSync(licensePath, licenseText);
138
115
  ok(`LICENSE file generated`);
139
116
 
140
117
  // Generate CLA.md if it doesn't exist
141
118
  const claPath = join(repoPath, 'CLA.md');
142
119
  if (!existsSync(claPath)) {
143
- writeFileSync(claPath, generateCLA());
120
+ writeFileSync(claPath, generateCLA(repoPath));
144
121
  ok(`CLA.md generated`);
145
122
  }
146
123
 
@@ -174,7 +151,7 @@ async function check(repoPath) {
174
151
  warn('LICENSE file missing');
175
152
  issues++;
176
153
  if (FIX) {
177
- writeFileSync(licensePath, generateLicense(config));
154
+ writeFileSync(licensePath, generateLicense(config, repoPath));
178
155
  ok('LICENSE file created (--fix)');
179
156
  issues--;
180
157
  }
@@ -185,7 +162,7 @@ async function check(repoPath) {
185
162
  warn(`LICENSE copyright does not match "${config.copyright}"`);
186
163
  issues++;
187
164
  if (FIX) {
188
- writeFileSync(licensePath, generateLicense(config));
165
+ writeFileSync(licensePath, generateLicense(config, repoPath));
189
166
  ok('LICENSE file updated (--fix)');
190
167
  issues--;
191
168
  }
@@ -198,7 +175,7 @@ async function check(repoPath) {
198
175
  warn('LICENSE file is MIT-only but config says MIT+AGPL');
199
176
  issues++;
200
177
  if (FIX) {
201
- writeFileSync(licensePath, generateLicense(config));
178
+ writeFileSync(licensePath, generateLicense(config, repoPath));
202
179
  ok('LICENSE file updated to dual-license (--fix)');
203
180
  issues--;
204
181
  }
@@ -214,7 +191,7 @@ async function check(repoPath) {
214
191
  warn('CLA.md missing');
215
192
  issues++;
216
193
  if (FIX) {
217
- writeFileSync(claPath, generateCLA());
194
+ writeFileSync(claPath, generateCLA(repoPath));
218
195
  ok('CLA.md created (--fix)');
219
196
  issues--;
220
197
  }
@@ -289,7 +266,7 @@ async function check(repoPath) {
289
266
  warn(`tools/${entry.name}/LICENSE missing`);
290
267
  issues++;
291
268
  if (FIX) {
292
- writeFileSync(toolLicense, generateLicense(config));
269
+ writeFileSync(toolLicense, generateLicense(config, repoPath));
293
270
  ok(`tools/${entry.name}/LICENSE created (--fix)`);
294
271
  issues--;
295
272
  }
@@ -299,7 +276,7 @@ async function check(repoPath) {
299
276
  warn(`tools/${entry.name}/LICENSE wrong copyright`);
300
277
  issues++;
301
278
  if (FIX) {
302
- writeFileSync(toolLicense, generateLicense(config));
279
+ writeFileSync(toolLicense, generateLicense(config, repoPath));
303
280
  ok(`tools/${entry.name}/LICENSE updated (--fix)`);
304
281
  issues--;
305
282
  }
@@ -387,7 +364,7 @@ async function readmeLicense(targetPath) {
387
364
  log(` would replace with: standard dual MIT/AGPLv3 block`);
388
365
  }
389
366
  if (FIX) {
390
- const updated = replaceReadmeLicenseSection(content, config);
367
+ const updated = replaceReadmeLicenseSection(content, config, repoPath);
391
368
  writeFileSync(readmePath, updated);
392
369
  ok(`${repoName}/README.md ... updated to standard (--fix)`);
393
370
  totalIssues--;
@@ -399,7 +376,7 @@ async function readmeLicense(targetPath) {
399
376
  log(` would add: standard dual MIT/AGPLv3 block at end of README`);
400
377
  }
401
378
  if (FIX) {
402
- const updated = replaceReadmeLicenseSection(content, config);
379
+ const updated = replaceReadmeLicenseSection(content, config, repoPath);
403
380
  writeFileSync(readmePath, updated);
404
381
  ok(`${repoName}/README.md ... added standard block (--fix)`);
405
382
  totalIssues--;
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.15",
3
+ "version": "1.9.18",
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": {