@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.
- package/README.md +32 -0
- package/cli.mjs +13 -36
- package/core.mjs +127 -6
- 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
|
-
|
|
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
|
-
|
|
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