@wipcomputer/wip-ai-devops-toolbox 1.9.20

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 (146) hide show
  1. package/.license-guard.json +7 -0
  2. package/.publish-skill.json +4 -0
  3. package/CHANGELOG.md +1120 -0
  4. package/CLA.md +19 -0
  5. package/DEV-GUIDE-GENERAL-PUBLIC.md +882 -0
  6. package/LICENSE +52 -0
  7. package/README.md +238 -0
  8. package/SKILL.md +728 -0
  9. package/TECHNICAL.md +282 -0
  10. package/UNIVERSAL-INTERFACE.md +180 -0
  11. package/_trash/RELEASE-NOTES-v1-8-0.md +29 -0
  12. package/_trash/RELEASE-NOTES-v1-8-1.md +7 -0
  13. package/_trash/RELEASE-NOTES-v1-8-2.md +7 -0
  14. package/_trash/RELEASE-NOTES-v1-9-0.md +37 -0
  15. package/_trash/RELEASE-NOTES-v1-9-1.md +38 -0
  16. package/_trash/RELEASE-NOTES-v1-9-10.md +40 -0
  17. package/_trash/RELEASE-NOTES-v1-9-2.md +40 -0
  18. package/_trash/RELEASE-NOTES-v1-9-6.md +72 -0
  19. package/_trash/RELEASE-NOTES-v1-9-7.md +23 -0
  20. package/_trash/RELEASE-NOTES-v1-9-9.md +75 -0
  21. package/_trash/guide 2/DEV-GUIDE.md +487 -0
  22. package/_trash/guide 2/scripts/deploy-public.sh +152 -0
  23. package/package.json +27 -0
  24. package/scripts/SKILL-deploy-public.md +61 -0
  25. package/scripts/SKILL-post-merge-rename.md +47 -0
  26. package/scripts/deploy-public.sh +264 -0
  27. package/scripts/post-merge-rename.sh +205 -0
  28. package/scripts/publish-skill.sh +134 -0
  29. package/tools/deploy-public/LICENSE +52 -0
  30. package/tools/deploy-public/README.md +31 -0
  31. package/tools/deploy-public/SKILL.md +71 -0
  32. package/tools/deploy-public/deploy-public.sh +264 -0
  33. package/tools/deploy-public/package.json +9 -0
  34. package/tools/ldm-jobs/LICENSE +52 -0
  35. package/tools/ldm-jobs/README.md +46 -0
  36. package/tools/ldm-jobs/backup.sh +16 -0
  37. package/tools/ldm-jobs/branch-protect.sh +39 -0
  38. package/tools/ldm-jobs/crystal-capture.sh +19 -0
  39. package/tools/ldm-jobs/setup-shell.sh +27 -0
  40. package/tools/ldm-jobs/visibility-audit.sh +27 -0
  41. package/tools/post-merge-rename/LICENSE +52 -0
  42. package/tools/post-merge-rename/README.md +29 -0
  43. package/tools/post-merge-rename/SKILL.md +57 -0
  44. package/tools/post-merge-rename/package.json +9 -0
  45. package/tools/post-merge-rename/post-merge-rename.sh +122 -0
  46. package/tools/wip-branch-guard/INSTALL.md +41 -0
  47. package/tools/wip-branch-guard/guard.mjs +259 -0
  48. package/tools/wip-branch-guard/package.json +11 -0
  49. package/tools/wip-file-guard/CHANGELOG.md +6 -0
  50. package/tools/wip-file-guard/LICENSE +52 -0
  51. package/tools/wip-file-guard/README.md +113 -0
  52. package/tools/wip-file-guard/REFERENCE.md +86 -0
  53. package/tools/wip-file-guard/SKILL.md +105 -0
  54. package/tools/wip-file-guard/guard.mjs +128 -0
  55. package/tools/wip-file-guard/openclaw.plugin.json +8 -0
  56. package/tools/wip-file-guard/package.json +27 -0
  57. package/tools/wip-file-guard/test.sh +119 -0
  58. package/tools/wip-license-guard/LICENSE +52 -0
  59. package/tools/wip-license-guard/README.md +32 -0
  60. package/tools/wip-license-guard/SKILL.md +65 -0
  61. package/tools/wip-license-guard/cli.mjs +464 -0
  62. package/tools/wip-license-guard/core.mjs +310 -0
  63. package/tools/wip-license-guard/hook.mjs +146 -0
  64. package/tools/wip-license-guard/package.json +15 -0
  65. package/tools/wip-license-hook/CHANGELOG.md +17 -0
  66. package/tools/wip-license-hook/LICENSE +52 -0
  67. package/tools/wip-license-hook/README.md +200 -0
  68. package/tools/wip-license-hook/SKILL.md +111 -0
  69. package/tools/wip-license-hook/dist/cli/index.d.ts +15 -0
  70. package/tools/wip-license-hook/dist/cli/index.js +170 -0
  71. package/tools/wip-license-hook/dist/cli/index.js.map +1 -0
  72. package/tools/wip-license-hook/dist/core/detector.d.ts +12 -0
  73. package/tools/wip-license-hook/dist/core/detector.js +104 -0
  74. package/tools/wip-license-hook/dist/core/detector.js.map +1 -0
  75. package/tools/wip-license-hook/dist/core/index.d.ts +4 -0
  76. package/tools/wip-license-hook/dist/core/index.js +5 -0
  77. package/tools/wip-license-hook/dist/core/index.js.map +1 -0
  78. package/tools/wip-license-hook/dist/core/ledger.d.ts +49 -0
  79. package/tools/wip-license-hook/dist/core/ledger.js +72 -0
  80. package/tools/wip-license-hook/dist/core/ledger.js.map +1 -0
  81. package/tools/wip-license-hook/dist/core/reporter.d.ts +14 -0
  82. package/tools/wip-license-hook/dist/core/reporter.js +227 -0
  83. package/tools/wip-license-hook/dist/core/reporter.js.map +1 -0
  84. package/tools/wip-license-hook/dist/core/scanner.d.ts +39 -0
  85. package/tools/wip-license-hook/dist/core/scanner.js +325 -0
  86. package/tools/wip-license-hook/dist/core/scanner.js.map +1 -0
  87. package/tools/wip-license-hook/hooks/pre-pull.sh +55 -0
  88. package/tools/wip-license-hook/hooks/pre-push.sh +51 -0
  89. package/tools/wip-license-hook/mcp-server.mjs +119 -0
  90. package/tools/wip-license-hook/package-lock.json +54 -0
  91. package/tools/wip-license-hook/package.json +43 -0
  92. package/tools/wip-license-hook/src/cli/index.ts +189 -0
  93. package/tools/wip-license-hook/src/core/detector.ts +130 -0
  94. package/tools/wip-license-hook/src/core/index.ts +4 -0
  95. package/tools/wip-license-hook/src/core/ledger.ts +116 -0
  96. package/tools/wip-license-hook/src/core/reporter.ts +255 -0
  97. package/tools/wip-license-hook/src/core/scanner.ts +367 -0
  98. package/tools/wip-license-hook/tsconfig.json +16 -0
  99. package/tools/wip-readme-format/README.md +49 -0
  100. package/tools/wip-readme-format/SKILL.md +84 -0
  101. package/tools/wip-readme-format/format.mjs +570 -0
  102. package/tools/wip-readme-format/package.json +15 -0
  103. package/tools/wip-release/CHANGELOG.md +42 -0
  104. package/tools/wip-release/LICENSE +52 -0
  105. package/tools/wip-release/README.md +45 -0
  106. package/tools/wip-release/REFERENCE.md +100 -0
  107. package/tools/wip-release/SKILL.md +139 -0
  108. package/tools/wip-release/cli.js +161 -0
  109. package/tools/wip-release/core.mjs +1174 -0
  110. package/tools/wip-release/mcp-server.mjs +109 -0
  111. package/tools/wip-release/package.json +36 -0
  112. package/tools/wip-repo-init/README.md +38 -0
  113. package/tools/wip-repo-init/SKILL.md +77 -0
  114. package/tools/wip-repo-init/init.mjs +142 -0
  115. package/tools/wip-repo-init/package.json +11 -0
  116. package/tools/wip-repo-permissions-hook/LICENSE +52 -0
  117. package/tools/wip-repo-permissions-hook/README.md +86 -0
  118. package/tools/wip-repo-permissions-hook/SKILL.md +73 -0
  119. package/tools/wip-repo-permissions-hook/cli.js +83 -0
  120. package/tools/wip-repo-permissions-hook/core.mjs +122 -0
  121. package/tools/wip-repo-permissions-hook/guard.mjs +64 -0
  122. package/tools/wip-repo-permissions-hook/mcp-server.mjs +92 -0
  123. package/tools/wip-repo-permissions-hook/openclaw.plugin.json +8 -0
  124. package/tools/wip-repo-permissions-hook/package.json +31 -0
  125. package/tools/wip-repos/LICENSE +52 -0
  126. package/tools/wip-repos/README.md +77 -0
  127. package/tools/wip-repos/SKILL.md +80 -0
  128. package/tools/wip-repos/cli.mjs +176 -0
  129. package/tools/wip-repos/core.mjs +290 -0
  130. package/tools/wip-repos/mcp-server.mjs +157 -0
  131. package/tools/wip-repos/package.json +34 -0
  132. package/tools/wip-universal-installer/CHANGELOG.md +57 -0
  133. package/tools/wip-universal-installer/LICENSE +52 -0
  134. package/tools/wip-universal-installer/README.md +81 -0
  135. package/tools/wip-universal-installer/REFERENCE.md +122 -0
  136. package/tools/wip-universal-installer/SKILL.md +87 -0
  137. package/tools/wip-universal-installer/SPEC.md +180 -0
  138. package/tools/wip-universal-installer/detect.mjs +130 -0
  139. package/tools/wip-universal-installer/examples/minimal/README.md +20 -0
  140. package/tools/wip-universal-installer/examples/minimal/SKILL.md +28 -0
  141. package/tools/wip-universal-installer/examples/minimal/cli.mjs +4 -0
  142. package/tools/wip-universal-installer/examples/minimal/core.mjs +8 -0
  143. package/tools/wip-universal-installer/examples/minimal/mcp-server.mjs +27 -0
  144. package/tools/wip-universal-installer/examples/minimal/package.json +12 -0
  145. package/tools/wip-universal-installer/install.js +930 -0
  146. package/tools/wip-universal-installer/package.json +36 -0
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * wip-license-hook CLI
4
+ *
5
+ * Commands:
6
+ * init Initialize ledger for current project
7
+ * scan Scan all deps, update ledger
8
+ * check <dep> Check specific dependency
9
+ * gate Pre-merge license gate (for hooks)
10
+ * report Print ledger report
11
+ * dashboard Generate static dashboard HTML
12
+ * alert Print current alerts
13
+ * install Install git hooks in current repo
14
+ */
15
+
16
+ import { resolve } from "node:path";
17
+ import { existsSync, writeFileSync, mkdirSync, copyFileSync } from "node:fs";
18
+ import {
19
+ readLedger, writeLedger, createEmptyLedger, findEntry,
20
+ scanAndUpdate, gateCheck,
21
+ formatScanReport, formatGateOutput, formatLedgerReport,
22
+ writeDashboard, generateBadgeUrl,
23
+ } from "../core/index.js";
24
+
25
+ const args = process.argv.slice(2);
26
+ const command = args[0];
27
+ const flags = new Set(args.filter((a) => a.startsWith("--")));
28
+ const positional = args.filter((a) => !a.startsWith("--")).slice(1);
29
+
30
+ const repoRoot = resolve(process.cwd());
31
+ const offline = flags.has("--offline");
32
+ const verbose = flags.has("--verbose");
33
+
34
+ function help(): void {
35
+ console.log(`
36
+ wip-license-hook — License rug-pull detection
37
+
38
+ Usage: wip-license-hook <command> [options]
39
+
40
+ Commands:
41
+ init Initialize LICENSE-LEDGER.json
42
+ scan Scan all dependencies, update ledger
43
+ check <name> Check a specific dependency
44
+ gate Pre-merge license gate (exit 1 if changed)
45
+ report Print ledger status
46
+ dashboard Generate static HTML dashboard
47
+ alert Show current alerts
48
+ install Install git hooks in .git/hooks/
49
+ badge Print shields.io badge URL
50
+
51
+ Options:
52
+ --offline Skip network calls (use cached data only)
53
+ --verbose Verbose output
54
+ --help Show this help
55
+ `);
56
+ }
57
+
58
+ async function main(): Promise<void> {
59
+ if (!command || command === "--help" || flags.has("--help")) {
60
+ help();
61
+ return;
62
+ }
63
+
64
+ switch (command) {
65
+ case "init": {
66
+ const ledgerPath = resolve(repoRoot, "LICENSE-LEDGER.json");
67
+ if (existsSync(ledgerPath)) {
68
+ console.log("⚠️ LICENSE-LEDGER.json already exists. Use 'scan' to update.");
69
+ return;
70
+ }
71
+ writeLedger(repoRoot, createEmptyLedger());
72
+ mkdirSync(resolve(repoRoot, "ledger/snapshots"), { recursive: true });
73
+ console.log("✅ Initialized LICENSE-LEDGER.json and ledger/snapshots/");
74
+ console.log(" Run 'wip-license-hook scan' to populate.");
75
+ break;
76
+ }
77
+
78
+ case "scan": {
79
+ console.log(offline ? "📡 Scanning (offline mode)..." : "📡 Scanning dependencies...");
80
+ const results = scanAndUpdate({ repoRoot, offline, verbose });
81
+ console.log(formatScanReport(results));
82
+ break;
83
+ }
84
+
85
+ case "check": {
86
+ const name = positional[0];
87
+ if (!name) {
88
+ console.error("Usage: wip-license-hook check <dependency-name>");
89
+ process.exit(1);
90
+ }
91
+ const ledger = readLedger(repoRoot);
92
+ const entry = findEntry(ledger, name);
93
+ if (!entry) {
94
+ console.log(`❓ ${name} not found in ledger. Run 'scan' first.`);
95
+ process.exit(1);
96
+ }
97
+ console.log(`\n ${entry.status === "clean" ? "✅" : "🚫"} ${entry.name}`);
98
+ console.log(` Type: ${entry.type}`);
99
+ console.log(` Source: ${entry.source}`);
100
+ console.log(` Adopted: ${entry.license_at_adoption} on ${entry.adopted_date}`);
101
+ console.log(` Current: ${entry.license_current} (checked ${entry.last_checked})`);
102
+ console.log(` Status: ${entry.status}\n`);
103
+ break;
104
+ }
105
+
106
+ case "gate": {
107
+ if (offline) {
108
+ console.log("⚠️ Offline mode — skipping license gate (cannot verify upstream).");
109
+ console.log(" Your push will proceed, but licenses were NOT checked.");
110
+ process.exit(0);
111
+ }
112
+ const { safe, alerts } = gateCheck(repoRoot, offline);
113
+ console.log(formatGateOutput(safe, alerts));
114
+ if (!safe) process.exit(1);
115
+ break;
116
+ }
117
+
118
+ case "report": {
119
+ const ledger = readLedger(repoRoot);
120
+ console.log(formatLedgerReport(ledger));
121
+ break;
122
+ }
123
+
124
+ case "dashboard": {
125
+ const outPath = writeDashboard(repoRoot);
126
+ console.log(`✅ Dashboard written to ${outPath}`);
127
+ break;
128
+ }
129
+
130
+ case "alert": {
131
+ const ledger = readLedger(repoRoot);
132
+ if (ledger.alerts.length === 0) {
133
+ console.log("✅ No active alerts.");
134
+ } else {
135
+ console.log(`\n⚠️ ${ledger.alerts.length} alert(s):\n`);
136
+ for (const a of ledger.alerts) {
137
+ console.log(` ${a.message}`);
138
+ console.log(` Detected: ${a.detected}\n`);
139
+ }
140
+ }
141
+ break;
142
+ }
143
+
144
+ case "install": {
145
+ const hooksDir = resolve(repoRoot, ".git/hooks");
146
+ if (!existsSync(resolve(repoRoot, ".git"))) {
147
+ console.error("❌ Not a git repository. Run from a git repo root.");
148
+ process.exit(1);
149
+ }
150
+ mkdirSync(hooksDir, { recursive: true });
151
+
152
+ // Find hooks relative to the package
153
+ const pkgRoot = resolve(new URL(".", import.meta.url).pathname, "../../..");
154
+ const prePullSrc = resolve(pkgRoot, "hooks/pre-pull.sh");
155
+ const preCommitSrc = resolve(pkgRoot, "hooks/pre-push.sh");
156
+
157
+ for (const [src, dest] of [
158
+ [prePullSrc, resolve(hooksDir, "pre-merge-commit")],
159
+ [preCommitSrc, resolve(hooksDir, "pre-push")],
160
+ ]) {
161
+ if (existsSync(src)) {
162
+ copyFileSync(src, dest);
163
+ const { chmodSync } = await import("node:fs");
164
+ chmodSync(dest, 0o755);
165
+ console.log(`✅ Installed ${dest}`);
166
+ } else {
167
+ console.log(`⚠️ Hook source not found: ${src}`);
168
+ }
169
+ }
170
+ break;
171
+ }
172
+
173
+ case "badge": {
174
+ const ledger = readLedger(repoRoot);
175
+ console.log(generateBadgeUrl(ledger));
176
+ break;
177
+ }
178
+
179
+ default:
180
+ console.error(`Unknown command: ${command}`);
181
+ help();
182
+ process.exit(1);
183
+ }
184
+ }
185
+
186
+ main().catch((err) => {
187
+ console.error("Fatal error:", err.message);
188
+ process.exit(1);
189
+ });
@@ -0,0 +1,130 @@
1
+ /**
2
+ * License text fingerprinting — identifies license type from file content.
3
+ */
4
+
5
+ export type LicenseId =
6
+ | "MIT"
7
+ | "Apache-2.0"
8
+ | "BSD-2-Clause"
9
+ | "BSD-3-Clause"
10
+ | "GPL-2.0"
11
+ | "GPL-3.0"
12
+ | "LGPL-2.1"
13
+ | "LGPL-3.0"
14
+ | "MPL-2.0"
15
+ | "ISC"
16
+ | "BSL-1.1"
17
+ | "SSPL-1.0"
18
+ | "Unlicense"
19
+ | "AGPL-3.0"
20
+ | "UNKNOWN";
21
+
22
+ interface Fingerprint {
23
+ id: LicenseId;
24
+ markers: string[]; // All must match (case-insensitive)
25
+ antiMarkers?: string[]; // None must match
26
+ }
27
+
28
+ const FINGERPRINTS: Fingerprint[] = [
29
+ {
30
+ id: "MIT",
31
+ markers: ["permission is hereby granted, free of charge", "the software is provided \"as is\""],
32
+ antiMarkers: ["apache", "gnu general public"],
33
+ },
34
+ {
35
+ id: "Apache-2.0",
36
+ markers: ["apache license", "version 2.0"],
37
+ },
38
+ {
39
+ id: "GPL-3.0",
40
+ markers: ["gnu general public license", "version 3"],
41
+ antiMarkers: ["lesser general public"],
42
+ },
43
+ {
44
+ id: "GPL-2.0",
45
+ markers: ["gnu general public license", "version 2"],
46
+ antiMarkers: ["lesser general public", "version 3"],
47
+ },
48
+ {
49
+ id: "LGPL-3.0",
50
+ markers: ["gnu lesser general public license", "version 3"],
51
+ },
52
+ {
53
+ id: "LGPL-2.1",
54
+ markers: ["gnu lesser general public license", "version 2.1"],
55
+ },
56
+ {
57
+ id: "AGPL-3.0",
58
+ markers: ["gnu affero general public license", "version 3"],
59
+ },
60
+ {
61
+ id: "BSD-3-Clause",
62
+ markers: ["redistribution and use in source and binary forms", "neither the name"],
63
+ },
64
+ {
65
+ id: "BSD-2-Clause",
66
+ markers: ["redistribution and use in source and binary forms"],
67
+ antiMarkers: ["neither the name"],
68
+ },
69
+ {
70
+ id: "MPL-2.0",
71
+ markers: ["mozilla public license", "version 2.0"],
72
+ },
73
+ {
74
+ id: "ISC",
75
+ markers: ["isc license", "permission to use, copy, modify"],
76
+ },
77
+ {
78
+ id: "BSL-1.1",
79
+ markers: ["business source license", "1.1"],
80
+ },
81
+ {
82
+ id: "SSPL-1.0",
83
+ markers: ["server side public license"],
84
+ },
85
+ {
86
+ id: "Unlicense",
87
+ markers: ["this is free and unencumbered software released into the public domain"],
88
+ },
89
+ ];
90
+
91
+ /**
92
+ * Detect license type from raw license file text.
93
+ */
94
+ export function detectLicenseFromText(text: string): LicenseId {
95
+ const lower = text.toLowerCase();
96
+
97
+ for (const fp of FINGERPRINTS) {
98
+ const allMatch = fp.markers.every((m) => lower.includes(m));
99
+ const anyAnti = fp.antiMarkers?.some((m) => lower.includes(m)) ?? false;
100
+ if (allMatch && !anyAnti) return fp.id;
101
+ }
102
+
103
+ return "UNKNOWN";
104
+ }
105
+
106
+ /**
107
+ * Normalize a license SPDX identifier from package metadata.
108
+ */
109
+ export function normalizeSpdx(raw: string): LicenseId {
110
+ const map: Record<string, LicenseId> = {
111
+ mit: "MIT",
112
+ "apache-2.0": "Apache-2.0",
113
+ apache2: "Apache-2.0",
114
+ "bsd-2-clause": "BSD-2-Clause",
115
+ "bsd-3-clause": "BSD-3-Clause",
116
+ "gpl-2.0": "GPL-2.0",
117
+ "gpl-3.0": "GPL-3.0",
118
+ "gpl-3.0-only": "GPL-3.0",
119
+ "lgpl-2.1": "LGPL-2.1",
120
+ "lgpl-3.0": "LGPL-3.0",
121
+ "mpl-2.0": "MPL-2.0",
122
+ isc: "ISC",
123
+ "bsl-1.1": "BSL-1.1",
124
+ "sspl-1.0": "SSPL-1.0",
125
+ unlicense: "Unlicense",
126
+ "agpl-3.0": "AGPL-3.0",
127
+ "agpl-3.0-only": "AGPL-3.0",
128
+ };
129
+ return map[raw.toLowerCase().trim()] ?? "UNKNOWN";
130
+ }
@@ -0,0 +1,4 @@
1
+ export { detectLicenseFromText, normalizeSpdx, type LicenseId } from "./detector.js";
2
+ export { readLedger, writeLedger, createEmptyLedger, findEntry, upsertEntry, archiveSnapshot, hasLicenseChanged, addAlert, type Ledger, type LedgerEntry, type Alert, type DependencyType, type DependencyStatus } from "./ledger.js";
3
+ export { scanAll, scanAndUpdate, gateCheck, findLicenseFile, readLicenseFromDir, type ScanResult } from "./scanner.js";
4
+ export { formatScanReport, formatGateOutput, formatLedgerReport, generateDashboardHtml, generateBadgeUrl, writeDashboard } from "./reporter.js";
@@ -0,0 +1,116 @@
1
+ /**
2
+ * License ledger — read/write/compare LICENSE-LEDGER.json + snapshot archiving.
3
+ */
4
+
5
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from "node:fs";
6
+ import { join, dirname } from "node:path";
7
+ import type { LicenseId } from "./detector.js";
8
+
9
+ export type DependencyStatus = "clean" | "changed" | "removed" | "unknown";
10
+ export type DependencyType = "fork" | "npm" | "pip" | "cargo" | "go";
11
+
12
+ export interface LedgerEntry {
13
+ name: string;
14
+ source: string;
15
+ type: DependencyType;
16
+ license_at_adoption: LicenseId;
17
+ license_current: LicenseId;
18
+ adopted_date: string; // ISO date
19
+ last_checked: string; // ISO date
20
+ commit_at_adoption?: string;
21
+ status: DependencyStatus;
22
+ }
23
+
24
+ export interface Alert {
25
+ dependency: string;
26
+ from: LicenseId;
27
+ to: LicenseId;
28
+ detected: string; // ISO datetime
29
+ message: string;
30
+ }
31
+
32
+ export interface Ledger {
33
+ version: 1;
34
+ dependencies: LedgerEntry[];
35
+ last_full_scan: string | null;
36
+ alerts: Alert[];
37
+ }
38
+
39
+ const LEDGER_FILE = "LICENSE-LEDGER.json";
40
+ const SNAPSHOT_DIR = "ledger/snapshots";
41
+
42
+ export function ledgerPath(repoRoot: string): string {
43
+ return join(repoRoot, LEDGER_FILE);
44
+ }
45
+
46
+ export function createEmptyLedger(): Ledger {
47
+ return {
48
+ version: 1,
49
+ dependencies: [],
50
+ last_full_scan: null,
51
+ alerts: [],
52
+ };
53
+ }
54
+
55
+ export function readLedger(repoRoot: string): Ledger {
56
+ const p = ledgerPath(repoRoot);
57
+ if (!existsSync(p)) return createEmptyLedger();
58
+ return JSON.parse(readFileSync(p, "utf-8"));
59
+ }
60
+
61
+ export function writeLedger(repoRoot: string, ledger: Ledger): void {
62
+ const p = ledgerPath(repoRoot);
63
+ writeFileSync(p, JSON.stringify(ledger, null, 2) + "\n", "utf-8");
64
+ }
65
+
66
+ export function findEntry(ledger: Ledger, name: string): LedgerEntry | undefined {
67
+ return ledger.dependencies.find((d) => d.name === name);
68
+ }
69
+
70
+ export function upsertEntry(ledger: Ledger, entry: LedgerEntry): void {
71
+ const idx = ledger.dependencies.findIndex((d) => d.name === entry.name);
72
+ if (idx >= 0) {
73
+ ledger.dependencies[idx] = entry;
74
+ } else {
75
+ ledger.dependencies.push(entry);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Archive a LICENSE file snapshot for a dependency.
81
+ */
82
+ export function archiveSnapshot(
83
+ repoRoot: string,
84
+ depName: string,
85
+ licenseContent: string,
86
+ date?: string
87
+ ): string {
88
+ const d = date ?? new Date().toISOString().slice(0, 10);
89
+ const dir = join(repoRoot, SNAPSHOT_DIR, depName);
90
+ mkdirSync(dir, { recursive: true });
91
+ const filename = `LICENSE-${d}.txt`;
92
+ const p = join(dir, filename);
93
+ writeFileSync(p, licenseContent, "utf-8");
94
+ return p;
95
+ }
96
+
97
+ /**
98
+ * Compare an entry's current license against its adoption license.
99
+ * Returns true if changed.
100
+ */
101
+ export function hasLicenseChanged(entry: LedgerEntry): boolean {
102
+ return entry.license_at_adoption !== entry.license_current;
103
+ }
104
+
105
+ /**
106
+ * Add an alert to the ledger.
107
+ */
108
+ export function addAlert(ledger: Ledger, dep: string, from: LicenseId, to: LicenseId): void {
109
+ ledger.alerts.push({
110
+ dependency: dep,
111
+ from,
112
+ to,
113
+ detected: new Date().toISOString(),
114
+ message: `⚠️ License changed: ${dep} went from ${from} → ${to}`,
115
+ });
116
+ }
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Reporter — generate reports, alerts, and static dashboard HTML.
3
+ */
4
+
5
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { readLedger, type Ledger, type LedgerEntry, type Alert } from "./ledger.js";
8
+ import type { ScanResult } from "./scanner.js";
9
+
10
+ // ─── Console reports ───
11
+
12
+ export function formatScanReport(results: ScanResult[]): string {
13
+ const lines: string[] = [
14
+ "",
15
+ "╔══════════════════════════════════════════════════╗",
16
+ "║ wip-license-hook — Scan Report ║",
17
+ "╚══════════════════════════════════════════════════╝",
18
+ "",
19
+ ];
20
+
21
+ const changed = results.filter((r) => r.wasChanged);
22
+ const newDeps = results.filter((r) => r.isNew);
23
+ const clean = results.filter((r) => !r.wasChanged && !r.isNew);
24
+
25
+ if (changed.length > 0) {
26
+ lines.push("🚨 LICENSE CHANGES DETECTED:");
27
+ lines.push("─".repeat(50));
28
+ for (const r of changed) {
29
+ lines.push(` 🚫 ${r.name} (${r.type})`);
30
+ lines.push(` License changed → now: ${r.detectedLicense}`);
31
+ lines.push(` Source: ${r.source}`);
32
+ }
33
+ lines.push("");
34
+ }
35
+
36
+ if (newDeps.length > 0) {
37
+ lines.push(`📦 New dependencies found: ${newDeps.length}`);
38
+ for (const r of newDeps) {
39
+ lines.push(` ➕ ${r.name} (${r.type}) — ${r.detectedLicense}`);
40
+ }
41
+ lines.push("");
42
+ }
43
+
44
+ if (clean.length > 0) {
45
+ lines.push(`✅ Clean dependencies: ${clean.length}`);
46
+ for (const r of clean) {
47
+ lines.push(` ✓ ${r.name} — ${r.detectedLicense}`);
48
+ }
49
+ lines.push("");
50
+ }
51
+
52
+ lines.push(`Total scanned: ${results.length}`);
53
+ lines.push("");
54
+
55
+ return lines.join("\n");
56
+ }
57
+
58
+ export function formatGateOutput(safe: boolean, alerts: string[]): string {
59
+ const lines: string[] = [];
60
+
61
+ if (safe) {
62
+ lines.push("");
63
+ lines.push("╔══════════════════════════════════════════════════╗");
64
+ lines.push("║ ✅ LICENSE CHECK PASSED — All licenses clean ║");
65
+ lines.push("╚══════════════════════════════════════════════════╝");
66
+ lines.push("");
67
+ } else {
68
+ lines.push("");
69
+ lines.push("╔══════════════════════════════════════════════════╗");
70
+ lines.push("║ 🚫 LICENSE CHECK FAILED — Changes detected! ║");
71
+ lines.push("╚══════════════════════════════════════════════════╝");
72
+ lines.push("");
73
+ for (const alert of alerts) {
74
+ lines.push(` ${alert}`);
75
+ }
76
+ lines.push("");
77
+ lines.push(" Action required: Review license changes before proceeding.");
78
+ lines.push(" Run: wip-license-hook scan --verbose for details.");
79
+ lines.push("");
80
+ }
81
+
82
+ return lines.join("\n");
83
+ }
84
+
85
+ export function formatLedgerReport(ledger: Ledger): string {
86
+ const lines: string[] = [
87
+ "",
88
+ "╔══════════════════════════════════════════════════╗",
89
+ "║ wip-license-hook — Ledger Status ║",
90
+ "╚══════════════════════════════════════════════════╝",
91
+ "",
92
+ `Last full scan: ${ledger.last_full_scan ?? "never"}`,
93
+ `Total dependencies: ${ledger.dependencies.length}`,
94
+ `Active alerts: ${ledger.alerts.length}`,
95
+ "",
96
+ ];
97
+
98
+ const statusIcon: Record<string, string> = {
99
+ clean: "✅",
100
+ changed: "🚫",
101
+ removed: "❓",
102
+ unknown: "❔",
103
+ };
104
+
105
+ for (const dep of ledger.dependencies) {
106
+ const icon = statusIcon[dep.status] ?? "❔";
107
+ lines.push(` ${icon} ${dep.name} (${dep.type})`);
108
+ lines.push(` Adopted: ${dep.license_at_adoption} on ${dep.adopted_date}`);
109
+ lines.push(` Current: ${dep.license_current} (checked ${dep.last_checked})`);
110
+ lines.push(` Source: ${dep.source}`);
111
+ }
112
+
113
+ if (ledger.alerts.length > 0) {
114
+ lines.push("");
115
+ lines.push("⚠️ ALERTS:");
116
+ lines.push("─".repeat(50));
117
+ for (const a of ledger.alerts) {
118
+ lines.push(` ${a.message}`);
119
+ lines.push(` Detected: ${a.detected}`);
120
+ }
121
+ }
122
+
123
+ lines.push("");
124
+ return lines.join("\n");
125
+ }
126
+
127
+ // ─── Dashboard HTML generation ───
128
+
129
+ export function generateDashboardHtml(ledger: Ledger): string {
130
+ const rows = ledger.dependencies
131
+ .map((d) => {
132
+ const statusClass = d.status === "clean" ? "clean" : d.status === "changed" ? "changed" : "unknown";
133
+ const statusEmoji = d.status === "clean" ? "✅" : d.status === "changed" ? "🚫" : "❔";
134
+ return `<tr class="${statusClass}">
135
+ <td>${statusEmoji} ${escHtml(d.name)}</td>
136
+ <td>${escHtml(d.type)}</td>
137
+ <td>${escHtml(d.license_at_adoption)}</td>
138
+ <td>${escHtml(d.license_current)}</td>
139
+ <td>${escHtml(d.adopted_date)}</td>
140
+ <td>${escHtml(d.last_checked)}</td>
141
+ <td>${escHtml(d.status)}</td>
142
+ </tr>`;
143
+ })
144
+ .join("\n");
145
+
146
+ const alertRows = ledger.alerts
147
+ .map(
148
+ (a) => `<tr>
149
+ <td>⚠️ ${escHtml(a.dependency)}</td>
150
+ <td>${escHtml(a.from)} → ${escHtml(a.to)}</td>
151
+ <td>${escHtml(a.detected)}</td>
152
+ <td>${escHtml(a.message)}</td>
153
+ </tr>`
154
+ )
155
+ .join("\n");
156
+
157
+ return `<!DOCTYPE html>
158
+ <html lang="en">
159
+ <head>
160
+ <meta charset="UTF-8">
161
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
162
+ <title>License Compliance Dashboard — wip-license-hook</title>
163
+ <style>
164
+ * { margin: 0; padding: 0; box-sizing: border-box; }
165
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0d1117; color: #c9d1d9; padding: 2rem; }
166
+ h1 { color: #58a6ff; margin-bottom: 0.5rem; }
167
+ .subtitle { color: #8b949e; margin-bottom: 2rem; }
168
+ .stats { display: flex; gap: 2rem; margin-bottom: 2rem; }
169
+ .stat { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1rem 1.5rem; }
170
+ .stat-value { font-size: 2rem; font-weight: bold; }
171
+ .stat-label { color: #8b949e; font-size: 0.875rem; }
172
+ .stat-clean .stat-value { color: #3fb950; }
173
+ .stat-changed .stat-value { color: #f85149; }
174
+ .stat-total .stat-value { color: #58a6ff; }
175
+ table { width: 100%; border-collapse: collapse; background: #161b22; border: 1px solid #30363d; border-radius: 8px; overflow: hidden; margin-bottom: 2rem; }
176
+ th { background: #21262d; text-align: left; padding: 0.75rem 1rem; color: #8b949e; font-weight: 600; border-bottom: 1px solid #30363d; }
177
+ td { padding: 0.75rem 1rem; border-bottom: 1px solid #30363d; }
178
+ tr.changed td { background: #f8514922; }
179
+ tr.clean td { }
180
+ .footer { color: #8b949e; font-size: 0.8rem; margin-top: 2rem; }
181
+ h2 { color: #58a6ff; margin: 1.5rem 0 1rem; }
182
+ </style>
183
+ </head>
184
+ <body>
185
+ <h1>🔒 License Compliance Dashboard</h1>
186
+ <p class="subtitle">Generated by wip-license-hook — ${new Date().toISOString()}</p>
187
+
188
+ <div class="stats">
189
+ <div class="stat stat-total">
190
+ <div class="stat-value">${ledger.dependencies.length}</div>
191
+ <div class="stat-label">Total Dependencies</div>
192
+ </div>
193
+ <div class="stat stat-clean">
194
+ <div class="stat-value">${ledger.dependencies.filter((d) => d.status === "clean").length}</div>
195
+ <div class="stat-label">Clean</div>
196
+ </div>
197
+ <div class="stat stat-changed">
198
+ <div class="stat-value">${ledger.dependencies.filter((d) => d.status === "changed").length}</div>
199
+ <div class="stat-label">Changed</div>
200
+ </div>
201
+ </div>
202
+
203
+ <h2>Dependencies</h2>
204
+ <table>
205
+ <thead>
206
+ <tr><th>Name</th><th>Type</th><th>License (Adopted)</th><th>License (Current)</th><th>Adopted</th><th>Last Checked</th><th>Status</th></tr>
207
+ </thead>
208
+ <tbody>${rows}</tbody>
209
+ </table>
210
+
211
+ ${
212
+ ledger.alerts.length > 0
213
+ ? `<h2>⚠️ Alerts</h2>
214
+ <table>
215
+ <thead><tr><th>Dependency</th><th>Change</th><th>Detected</th><th>Message</th></tr></thead>
216
+ <tbody>${alertRows}</tbody>
217
+ </table>`
218
+ : ""
219
+ }
220
+
221
+ <div class="footer">
222
+ <p>Last full scan: ${ledger.last_full_scan ?? "never"}</p>
223
+ <p>Powered by <a href="https://github.com/wipcomputer/wip-license-hook" style="color: #58a6ff;">wip-license-hook</a></p>
224
+ </div>
225
+ </body>
226
+ </html>`;
227
+ }
228
+
229
+ function escHtml(s: string): string {
230
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
231
+ }
232
+
233
+ // ─── Badge generation (shields.io style) ───
234
+
235
+ export function generateBadgeUrl(ledger: Ledger): string {
236
+ const changed = ledger.dependencies.filter((d) => d.status === "changed").length;
237
+ const total = ledger.dependencies.length;
238
+ const color = changed === 0 ? "brightgreen" : "red";
239
+ const label = "license%20compliance";
240
+ const message = changed === 0 ? `${total}%20clean` : `${changed}%20changed`;
241
+ return `https://img.shields.io/badge/${label}-${message}-${color}`;
242
+ }
243
+
244
+ /**
245
+ * Write the dashboard HTML to disk.
246
+ */
247
+ export function writeDashboard(repoRoot: string, ledger?: Ledger): string {
248
+ const l = ledger ?? readLedger(repoRoot);
249
+ const html = generateDashboardHtml(l);
250
+ const dir = join(repoRoot, "dashboard");
251
+ mkdirSync(dir, { recursive: true });
252
+ const outPath = join(dir, "index.html");
253
+ writeFileSync(outPath, html, "utf-8");
254
+ return outPath;
255
+ }