@wipcomputer/wip-license-guard 1.0.0

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 (6) hide show
  1. package/LICENSE +52 -0
  2. package/SKILL.md +45 -0
  3. package/cli.mjs +349 -0
  4. package/core.mjs +145 -0
  5. package/hook.mjs +146 -0
  6. package/package.json +15 -0
package/LICENSE ADDED
@@ -0,0 +1,52 @@
1
+ Dual License: MIT + AGPLv3
2
+
3
+ Copyright (c) 2026 WIP Computer, Inc.
4
+
5
+
6
+ 1. MIT License (local and personal use)
7
+ ---------------------------------------
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+
28
+ 2. GNU Affero General Public License v3.0 (commercial and cloud use)
29
+ --------------------------------------------------------------------
30
+
31
+ If you run this software as part of a hosted service, cloud platform,
32
+ marketplace listing, or any network-accessible offering for commercial
33
+ purposes, the AGPLv3 terms apply. You must either:
34
+
35
+ a) Release your complete source code under AGPLv3, or
36
+ b) Obtain a commercial license.
37
+
38
+ This program is free software: you can redistribute it and/or modify
39
+ it under the terms of the GNU Affero General Public License as published
40
+ by the Free Software Foundation, either version 3 of the License, or
41
+ (at your option) any later version.
42
+
43
+ This program is distributed in the hope that it will be useful,
44
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
45
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
46
+ GNU Affero General Public License for more details.
47
+
48
+ You should have received a copy of the GNU Affero General Public License
49
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
50
+
51
+
52
+ AGPLv3 for personal use is free. Commercial licenses available.
package/SKILL.md ADDED
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: wip-license-guard
3
+ description: License compliance for your own repos. Ensures correct copyright headers, dual-license blocks, and LICENSE files across all source files.
4
+ license: MIT
5
+ interface: [cli, skill]
6
+ metadata:
7
+ display-name: "License Guard"
8
+ version: "1.0.0"
9
+ homepage: "https://github.com/wipcomputer/wip-ai-devops-toolbox"
10
+ author: "Parker Todd Brooks"
11
+ category: dev-tools
12
+ capabilities:
13
+ - copyright-enforcement
14
+ - license-compliance
15
+ - license-file-check
16
+ requires:
17
+ bins: [node, git]
18
+ openclaw:
19
+ requires:
20
+ bins: [node, git]
21
+ install:
22
+ - id: node
23
+ kind: node
24
+ package: "@wipcomputer/wip-license-guard"
25
+ bins: [wip-license-guard]
26
+ label: "Install via npm"
27
+ emoji: "📜"
28
+ ---
29
+
30
+ # wip-license-guard
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.
33
+
34
+ ## When to Use This Skill
35
+
36
+ - Before a release, to verify all files have correct license headers
37
+ - After adding new source files to a repo
38
+ - To enforce the MIT/AGPL dual-license pattern
39
+
40
+ ## CLI
41
+
42
+ ```bash
43
+ wip-license-guard /path/to/repo # scan and report
44
+ wip-license-guard /path/to/repo --fix # auto-fix missing headers
45
+ ```
package/cli.mjs ADDED
@@ -0,0 +1,349 @@
1
+ #!/usr/bin/env node
2
+ // wip-license-guard
3
+ // License compliance for your own repos.
4
+ // Ensures correct copyright, dual-license blocks, and LICENSE files.
5
+
6
+ import { existsSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { createInterface } from 'node:readline';
9
+ import { generateLicense, generateReadmeBlock } from './core.mjs';
10
+
11
+ const args = process.argv.slice(2);
12
+ const HELP_FLAGS = ['--help', '-h', 'help'];
13
+ const command = HELP_FLAGS.some(f => args.includes(f)) ? 'help' : (args.find(a => !a.startsWith('--')) || 'check');
14
+ const target = args.find((a, i) => i > 0 && !a.startsWith('--')) || '.';
15
+ const FIX = args.includes('--fix');
16
+ const QUIET = args.includes('--quiet');
17
+ const FROM_STANDARD = args.includes('--from-standard');
18
+
19
+ function log(msg) { if (!QUIET) console.log(msg); }
20
+ function ok(msg) { if (!QUIET) console.log(` \u2713 ${msg}`); }
21
+ function warn(msg) { console.log(` \u2717 ${msg}`); }
22
+
23
+ function ask(question) {
24
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
25
+ return new Promise(resolve => {
26
+ rl.question(question, answer => {
27
+ rl.close();
28
+ resolve(answer.trim());
29
+ });
30
+ });
31
+ }
32
+
33
+ // WIP Computer standard defaults
34
+ const WIP_STANDARD = {
35
+ copyright: 'WIP Computer, Inc.',
36
+ license: 'MIT+AGPL',
37
+ year: String(new Date().getFullYear()),
38
+ attribution: 'Built by Parker Todd Brooks, Lēsa (OpenClaw, Claude Opus 4.6), Claude Code (Claude Opus 4.6).',
39
+ };
40
+
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
+ async function init(repoPath) {
65
+ const configPath = join(repoPath, '.license-guard.json');
66
+
67
+ // --from-standard: apply WIP Computer defaults without prompting
68
+ if (FROM_STANDARD) {
69
+ log('\n wip-license-guard init --from-standard\n');
70
+
71
+ const config = { ...WIP_STANDARD, created: new Date().toISOString() };
72
+
73
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
74
+ ok(`Config saved to .license-guard.json`);
75
+
76
+ const licensePath = join(repoPath, 'LICENSE');
77
+ writeFileSync(licensePath, generateLicense(config));
78
+ ok(`LICENSE file generated (dual MIT+AGPLv3)`);
79
+
80
+ const claPath = join(repoPath, 'CLA.md');
81
+ if (!existsSync(claPath)) {
82
+ writeFileSync(claPath, generateCLA());
83
+ ok(`CLA.md generated`);
84
+ } else {
85
+ ok(`CLA.md already exists`);
86
+ }
87
+
88
+ log(`\n Standard: ${config.copyright}, ${config.license}, ${config.year}`);
89
+ log(` Done. Run \`wip-license-guard check\` to audit.\n`);
90
+ return config;
91
+ }
92
+
93
+ if (existsSync(configPath)) {
94
+ const existing = JSON.parse(readFileSync(configPath, 'utf8'));
95
+ log(`\nLicense guard already configured:`);
96
+ log(` Copyright: ${existing.copyright}`);
97
+ log(` License: ${existing.license}`);
98
+ log(` Year: ${existing.year}`);
99
+ const update = await ask('\nUpdate? (y/N) ');
100
+ if (update.toLowerCase() !== 'y') {
101
+ log('Keeping existing config.');
102
+ return existing;
103
+ }
104
+ }
105
+
106
+ log('\n wip-license-guard init\n');
107
+
108
+ const copyright = await ask(' Copyright holder (e.g. WIP Computer, Inc.): ');
109
+ if (!copyright) {
110
+ console.error('Copyright holder is required.');
111
+ process.exit(1);
112
+ }
113
+
114
+ log('\n License types:');
115
+ log(' 1. MIT only');
116
+ log(' 2. AGPLv3 only');
117
+ log(' 3. MIT + AGPLv3 dual-license (recommended for WIP repos)');
118
+ const licenseChoice = await ask('\n Choose (1/2/3): ');
119
+
120
+ const licenseMap = { '1': 'MIT', '2': 'AGPL-3.0', '3': 'MIT+AGPL' };
121
+ const license = licenseMap[licenseChoice] || 'MIT+AGPL';
122
+
123
+ const currentYear = new Date().getFullYear();
124
+ const yearInput = await ask(` Copyright year (${currentYear}): `);
125
+ const year = yearInput || String(currentYear);
126
+
127
+ const attribution = await ask(' Attribution (e.g. Built by Parker Todd Brooks, ...): ');
128
+
129
+ const config = { copyright, license, year, attribution, created: new Date().toISOString() };
130
+
131
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
132
+ ok(`Config saved to .license-guard.json`);
133
+
134
+ const licensePath = join(repoPath, 'LICENSE');
135
+ const licenseText = generateLicense(config);
136
+ writeFileSync(licensePath, licenseText);
137
+ ok(`LICENSE file generated`);
138
+
139
+ // Generate CLA.md if it doesn't exist
140
+ const claPath = join(repoPath, 'CLA.md');
141
+ if (!existsSync(claPath)) {
142
+ writeFileSync(claPath, generateCLA());
143
+ ok(`CLA.md generated`);
144
+ }
145
+
146
+ log(`\nDone. Run \`wip-license-guard check\` to audit.`);
147
+ return config;
148
+ }
149
+
150
+ async function check(repoPath) {
151
+ const configPath = join(repoPath, '.license-guard.json');
152
+
153
+ if (!existsSync(configPath)) {
154
+ log('\n No .license-guard.json found.');
155
+ const doInit = await ask(' Initialize license guard? (Y/n) ');
156
+ if (doInit.toLowerCase() !== 'n') {
157
+ await init(repoPath);
158
+ return 0;
159
+ }
160
+ process.exit(1);
161
+ }
162
+
163
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
164
+ let issues = 0;
165
+
166
+ log(`\n wip-license-guard check\n`);
167
+ log(` Copyright: ${config.copyright}`);
168
+ log(` License: ${config.license}\n`);
169
+
170
+ // Check LICENSE file
171
+ const licensePath = join(repoPath, 'LICENSE');
172
+ if (!existsSync(licensePath)) {
173
+ warn('LICENSE file missing');
174
+ issues++;
175
+ if (FIX) {
176
+ writeFileSync(licensePath, generateLicense(config));
177
+ ok('LICENSE file created (--fix)');
178
+ issues--;
179
+ }
180
+ } else {
181
+ const licenseText = readFileSync(licensePath, 'utf8');
182
+
183
+ if (!licenseText.includes(config.copyright)) {
184
+ warn(`LICENSE copyright does not match "${config.copyright}"`);
185
+ issues++;
186
+ if (FIX) {
187
+ writeFileSync(licensePath, generateLicense(config));
188
+ ok('LICENSE file updated (--fix)');
189
+ issues--;
190
+ }
191
+ } else {
192
+ ok('LICENSE copyright correct');
193
+ }
194
+
195
+ if (config.license === 'MIT+AGPL') {
196
+ if (!licenseText.includes('AGPL') && !licenseText.includes('GNU Affero')) {
197
+ warn('LICENSE file is MIT-only but config says MIT+AGPL');
198
+ issues++;
199
+ if (FIX) {
200
+ writeFileSync(licensePath, generateLicense(config));
201
+ ok('LICENSE file updated to dual-license (--fix)');
202
+ issues--;
203
+ }
204
+ } else {
205
+ ok('LICENSE includes AGPLv3 terms');
206
+ }
207
+ }
208
+ }
209
+
210
+ // Check CLA.md
211
+ const claPath = join(repoPath, 'CLA.md');
212
+ if (!existsSync(claPath)) {
213
+ warn('CLA.md missing');
214
+ issues++;
215
+ if (FIX) {
216
+ writeFileSync(claPath, generateCLA());
217
+ ok('CLA.md created (--fix)');
218
+ issues--;
219
+ }
220
+ } else {
221
+ ok('CLA.md exists');
222
+ }
223
+
224
+ // Check README (license + structure standard)
225
+ const readmePath = join(repoPath, 'README.md');
226
+ if (existsSync(readmePath)) {
227
+ const readme = readFileSync(readmePath, 'utf8');
228
+
229
+ // License checks
230
+ if (!readme.includes('## License')) {
231
+ warn('README.md missing ## License section');
232
+ issues++;
233
+ } else {
234
+ ok('README.md has License section');
235
+ }
236
+
237
+ if (config.license === 'MIT+AGPL' && !readme.includes('AGPL')) {
238
+ warn('README.md License section missing AGPL reference');
239
+ issues++;
240
+ } else if (config.license === 'MIT+AGPL') {
241
+ ok('README.md references AGPL');
242
+ }
243
+
244
+ // README structure standard checks
245
+ if (!readme.match(/^#\s+\S/m)) {
246
+ warn('README.md missing # title');
247
+ issues++;
248
+ } else {
249
+ ok('README.md has title');
250
+ }
251
+
252
+ if (config.attribution && !readme.includes(config.attribution.split(',')[0])) {
253
+ warn('README.md missing attribution');
254
+ issues++;
255
+ } else if (config.attribution) {
256
+ ok('README.md has attribution');
257
+ }
258
+
259
+ // Warn if README contains content that belongs in TECHNICAL.md
260
+ const technicalPatterns = [
261
+ /## (Architecture|API|Config|Configuration|Build|Development Setup|Quick Start)/i,
262
+ /```json\s*\n\s*\{[\s\S]*?"command"/, // MCP config blocks
263
+ /npm install -g /, // install commands belong in TECHNICAL.md
264
+ ];
265
+ for (const pattern of technicalPatterns) {
266
+ if (pattern.test(readme)) {
267
+ warn('README.md contains technical content (move to TECHNICAL.md)');
268
+ issues++;
269
+ break;
270
+ }
271
+ }
272
+ } else {
273
+ warn('README.md not found');
274
+ issues++;
275
+ }
276
+
277
+ // Check sub-tools (toolbox mode)
278
+ const toolsDir = join(repoPath, 'tools');
279
+ if (existsSync(toolsDir)) {
280
+ try {
281
+ const entries = readdirSync(toolsDir, { withFileTypes: true });
282
+ for (const entry of entries) {
283
+ if (!entry.isDirectory()) continue;
284
+ const toolPath = join(toolsDir, entry.name);
285
+ const toolLicense = join(toolPath, 'LICENSE');
286
+
287
+ if (!existsSync(toolLicense)) {
288
+ warn(`tools/${entry.name}/LICENSE missing`);
289
+ issues++;
290
+ if (FIX) {
291
+ writeFileSync(toolLicense, generateLicense(config));
292
+ ok(`tools/${entry.name}/LICENSE created (--fix)`);
293
+ issues--;
294
+ }
295
+ } else {
296
+ const text = readFileSync(toolLicense, 'utf8');
297
+ if (!text.includes(config.copyright)) {
298
+ warn(`tools/${entry.name}/LICENSE wrong copyright`);
299
+ issues++;
300
+ if (FIX) {
301
+ writeFileSync(toolLicense, generateLicense(config));
302
+ ok(`tools/${entry.name}/LICENSE updated (--fix)`);
303
+ issues--;
304
+ }
305
+ } else {
306
+ ok(`tools/${entry.name}/LICENSE correct`);
307
+ }
308
+ }
309
+ }
310
+ } catch {}
311
+ }
312
+
313
+ log('');
314
+ if (issues === 0) {
315
+ log(' All checks passed.\n');
316
+ } else {
317
+ log(` ${issues} issue(s) found. Run with --fix to auto-repair.\n`);
318
+ }
319
+
320
+ return issues;
321
+ }
322
+
323
+ // Main
324
+ if (command === 'init') {
325
+ await init(target === 'init' ? '.' : target);
326
+ } else if (command === 'check') {
327
+ const repoPath = (target === 'check') ? '.' : target;
328
+ const issues = await check(repoPath);
329
+ process.exit(issues > 0 ? 1 : 0);
330
+ } else if (command === '--help' || command === '-h' || command === 'help') {
331
+ console.log(`
332
+ wip-license-guard
333
+
334
+ Commands:
335
+ init [path] Interactive setup. Asks license type, copyright, year.
336
+ init --from-standard Apply WIP Computer defaults (MIT+AGPL, CLA, attribution).
337
+ check [path] Audit repo against saved config. Exit 1 if issues found.
338
+ check --fix [path] Auto-fix issues (update LICENSE files, wrong copyright).
339
+ help Show this help.
340
+
341
+ On first run, if no config exists, check will offer to run init.
342
+ Use --from-standard for new WIP Computer repos (no prompts, just works).
343
+
344
+ Config file: .license-guard.json (commit this to your repo)
345
+ `);
346
+ } else {
347
+ console.error(`Unknown command: ${command}. Run wip-license-guard help.`);
348
+ process.exit(1);
349
+ }
package/core.mjs ADDED
@@ -0,0 +1,145 @@
1
+ // wip-license-guard/core.mjs
2
+ // License generation and validation logic.
3
+
4
+ export function generateLicense(config) {
5
+ const { copyright, license, year } = config;
6
+
7
+ if (license === 'MIT') return generateMIT(copyright, year);
8
+ if (license === 'AGPL-3.0') return generateAGPL(copyright, year);
9
+ if (license === 'MIT+AGPL') return generateDual(copyright, year);
10
+
11
+ return generateMIT(copyright, year);
12
+ }
13
+
14
+ function generateMIT(copyright, year) {
15
+ return `MIT License
16
+
17
+ Copyright (c) ${year} ${copyright}
18
+
19
+ Permission is hereby granted, free of charge, to any person obtaining a copy
20
+ of this software and associated documentation files (the "Software"), to deal
21
+ in the Software without restriction, including without limitation the rights
22
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
23
+ copies of the Software, and to permit persons to whom the Software is
24
+ furnished to do so, subject to the following conditions:
25
+
26
+ The above copyright notice and this permission notice shall be included in all
27
+ copies or substantial portions of the Software.
28
+
29
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
35
+ SOFTWARE.
36
+ `;
37
+ }
38
+
39
+ function generateAGPL(copyright, year) {
40
+ return `GNU Affero General Public License v3.0
41
+
42
+ Copyright (c) ${year} ${copyright}
43
+
44
+ This program is free software: you can redistribute it and/or modify
45
+ it under the terms of the GNU Affero General Public License as published
46
+ by the Free Software Foundation, either version 3 of the License, or
47
+ (at your option) any later version.
48
+
49
+ This program is distributed in the hope that it will be useful,
50
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
51
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
52
+ GNU Affero General Public License for more details.
53
+
54
+ You should have received a copy of the GNU Affero General Public License
55
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
56
+ `;
57
+ }
58
+
59
+ function generateDual(copyright, year) {
60
+ return `Dual License: MIT + AGPLv3
61
+
62
+ Copyright (c) ${year} ${copyright}
63
+
64
+
65
+ 1. MIT License (local and personal use)
66
+ ---------------------------------------
67
+
68
+ Permission is hereby granted, free of charge, to any person obtaining a copy
69
+ of this software and associated documentation files (the "Software"), to deal
70
+ in the Software without restriction, including without limitation the rights
71
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
72
+ copies of the Software, and to permit persons to whom the Software is
73
+ furnished to do so, subject to the following conditions:
74
+
75
+ The above copyright notice and this permission notice shall be included in all
76
+ copies or substantial portions of the Software.
77
+
78
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
79
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
80
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
81
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
82
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
83
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
84
+ SOFTWARE.
85
+
86
+
87
+ 2. GNU Affero General Public License v3.0 (commercial and cloud use)
88
+ --------------------------------------------------------------------
89
+
90
+ If you run this software as part of a hosted service, cloud platform,
91
+ marketplace listing, or any network-accessible offering for commercial
92
+ purposes, the AGPLv3 terms apply. You must either:
93
+
94
+ a) Release your complete source code under AGPLv3, or
95
+ b) Obtain a commercial license.
96
+
97
+ This program is free software: you can redistribute it and/or modify
98
+ it under the terms of the GNU Affero General Public License as published
99
+ by the Free Software Foundation, either version 3 of the License, or
100
+ (at your option) any later version.
101
+
102
+ This program is distributed in the hope that it will be useful,
103
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
104
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
105
+ GNU Affero General Public License for more details.
106
+
107
+ You should have received a copy of the GNU Affero General Public License
108
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
109
+
110
+
111
+ AGPLv3 for personal use is free. Commercial licenses available.
112
+ `;
113
+ }
114
+
115
+ export function generateReadmeBlock(config) {
116
+ const { license, attribution } = config;
117
+
118
+ if (license === 'MIT') {
119
+ return `## License
120
+
121
+ MIT.${attribution ? ' ' + attribution : ''}
122
+ `;
123
+ }
124
+
125
+ if (license === 'AGPL-3.0') {
126
+ return `## License
127
+
128
+ AGPLv3. AGPLv3 for personal use is free.${attribution ? '\n\n' + attribution : ''}
129
+ `;
130
+ }
131
+
132
+ return `## License
133
+
134
+ \`\`\`
135
+ MIT All CLI tools, MCP servers, skills, and hooks (use anywhere, no restrictions).
136
+ AGPLv3 Commercial redistribution, marketplace listings, or bundling into paid services.
137
+ \`\`\`
138
+
139
+ Dual-license model designed to keep tools free while preventing commercial resellers.
140
+
141
+ AGPLv3 for personal use is free. Commercial licenses available.
142
+
143
+ Using these tools to build your own software is fine. Reselling the tools themselves is what requires a commercial license.
144
+ ${attribution ? '\n' + attribution : ''}`;
145
+ }
package/hook.mjs ADDED
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ // wip-license-guard/hook.mjs
3
+ // PreToolUse hook for Claude Code.
4
+ // Blocks commits/pushes when license compliance fails.
5
+ // Checks: LICENSE file, copyright, CLA.md, README license section.
6
+
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+
10
+ function deny(reason) {
11
+ const output = {
12
+ hookSpecificOutput: {
13
+ hookEventName: 'PreToolUse',
14
+ permissionDecision: 'deny',
15
+ permissionDecisionReason: reason,
16
+ },
17
+ };
18
+ process.stdout.write(JSON.stringify(output));
19
+ }
20
+
21
+ function findRepoRoot(startPath) {
22
+ let dir = startPath;
23
+ for (let i = 0; i < 20; i++) {
24
+ if (existsSync(join(dir, '.git'))) return dir;
25
+ const parent = join(dir, '..');
26
+ if (parent === dir) break;
27
+ dir = parent;
28
+ }
29
+ return null;
30
+ }
31
+
32
+ function checkLicenseCompliance(repoPath) {
33
+ const issues = [];
34
+ const configPath = join(repoPath, '.license-guard.json');
35
+
36
+ // No config means license-guard hasn't been set up. Don't block.
37
+ if (!existsSync(configPath)) return [];
38
+
39
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
40
+
41
+ // 1. LICENSE file must exist
42
+ const licensePath = join(repoPath, 'LICENSE');
43
+ if (!existsSync(licensePath)) {
44
+ issues.push('LICENSE file is missing');
45
+ } else {
46
+ const licenseText = readFileSync(licensePath, 'utf8');
47
+
48
+ // 2. Copyright must match
49
+ if (!licenseText.includes(config.copyright)) {
50
+ issues.push(`LICENSE copyright does not match "${config.copyright}"`);
51
+ }
52
+
53
+ // 3. Dual-license must include AGPL
54
+ if (config.license === 'MIT+AGPL' && !licenseText.includes('AGPL') && !licenseText.includes('GNU Affero')) {
55
+ issues.push('LICENSE is MIT-only but config requires MIT+AGPL');
56
+ }
57
+ }
58
+
59
+ // 4. CLA.md should exist for repos with contributors
60
+ if (!existsSync(join(repoPath, 'CLA.md'))) {
61
+ issues.push('CLA.md is missing');
62
+ }
63
+
64
+ // 5. README must have license section
65
+ const readmePath = join(repoPath, 'README.md');
66
+ if (existsSync(readmePath)) {
67
+ const readme = readFileSync(readmePath, 'utf8');
68
+ if (!readme.includes('## License')) {
69
+ issues.push('README.md missing ## License section');
70
+ }
71
+ if (config.license === 'MIT+AGPL' && !readme.includes('AGPL')) {
72
+ issues.push('README.md License section missing AGPL reference');
73
+ }
74
+ }
75
+
76
+ return issues;
77
+ }
78
+
79
+ // CLI mode: node hook.mjs --check [path]
80
+ if (process.argv.includes('--check')) {
81
+ const path = process.argv[process.argv.indexOf('--check') + 1] || '.';
82
+ const issues = checkLicenseCompliance(path);
83
+ if (issues.length === 0) {
84
+ console.log(' All license checks passed.');
85
+ process.exit(0);
86
+ } else {
87
+ console.log(' License compliance issues:');
88
+ for (const issue of issues) console.log(` - ${issue}`);
89
+ console.log('\n Run `wip-license-guard check --fix` to auto-repair.');
90
+ process.exit(1);
91
+ }
92
+ }
93
+
94
+ async function main() {
95
+ let raw = '';
96
+ for await (const chunk of process.stdin) {
97
+ raw += chunk;
98
+ }
99
+
100
+ let input;
101
+ try {
102
+ input = JSON.parse(raw);
103
+ } catch {
104
+ process.exit(0);
105
+ }
106
+
107
+ const toolName = input.tool_name || '';
108
+
109
+ // Only check on Bash tool calls that look like git commit or git push
110
+ if (toolName !== 'Bash') {
111
+ process.exit(0);
112
+ }
113
+
114
+ const command = input.tool_input?.command || '';
115
+
116
+ // Check if this is a git commit or git push
117
+ const isCommit = /\bgit\s+commit\b/.test(command);
118
+ const isPush = /\bgit\s+push\b/.test(command);
119
+
120
+ if (!isCommit && !isPush) {
121
+ process.exit(0);
122
+ }
123
+
124
+ // Find repo root from cwd
125
+ const cwd = input.tool_input?.cwd || process.cwd();
126
+ const repoRoot = findRepoRoot(cwd);
127
+
128
+ if (!repoRoot) {
129
+ process.exit(0);
130
+ }
131
+
132
+ const issues = checkLicenseCompliance(repoRoot);
133
+
134
+ if (issues.length > 0) {
135
+ const issueList = issues.map(i => ` - ${i}`).join('\n');
136
+ deny(
137
+ `BLOCKED: License compliance check failed.\n${issueList}\n\nFix these issues before committing. Run \`wip-license-guard check --fix\` to auto-repair.`
138
+ );
139
+ process.exit(0);
140
+ }
141
+
142
+ // All good
143
+ process.exit(0);
144
+ }
145
+
146
+ main().catch(() => process.exit(0));
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@wipcomputer/wip-license-guard",
3
+ "version": "1.0.0",
4
+ "description": "License compliance for your own repos. Ensures correct copyright, dual-license blocks, and LICENSE files.",
5
+ "type": "module",
6
+ "bin": {
7
+ "wip-license-guard": "./cli.mjs"
8
+ },
9
+ "license": "MIT AND AGPL-3.0-or-later",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/wipcomputer/wip-ai-devops-toolbox.git",
13
+ "directory": "tools/wip-license-guard"
14
+ }
15
+ }