Browse Source

dev(tools): add license header adder thingy

KernelDeimos 10 months ago
parent
commit
67139ef91b

+ 1 - 0
addlicense.yml

@@ -0,0 +1 @@
+header: doc/license_header.txt

+ 448 - 7
package-lock.json

@@ -1024,6 +1024,102 @@
         "url": "https://github.com/sponsors/nzakas"
       }
     },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+      "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+      "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+      "license": "MIT"
+    },
+    "node_modules/@isaacs/cliui/node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "license": "MIT",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+      "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
     "node_modules/@istanbuljs/load-nyc-config": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -6021,6 +6117,10 @@
         "node": ">=14"
       }
     },
+    "node_modules/comment-parser": {
+      "resolved": "tools/comment-parser",
+      "link": true
+    },
     "node_modules/commondir": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@@ -6183,6 +6283,15 @@
       "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
       "license": "ISC"
     },
+    "node_modules/console-table-printer": {
+      "version": "2.12.1",
+      "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.12.1.tgz",
+      "integrity": "sha512-wKGOQRRvdnd89pCeH96e2Fn4wkbenSP6LMHfjfyNLMbGuHEFbMqQNuxXqd0oXG9caIOQ1FTvc5Uijp9/4jujnQ==",
+      "license": "MIT",
+      "dependencies": {
+        "simple-wcswidth": "^1.0.1"
+      }
+    },
     "node_modules/content-disposition": {
       "version": "0.5.4",
       "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -6327,7 +6436,6 @@
       "version": "7.0.3",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
       "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
-      "dev": true,
       "dependencies": {
         "path-key": "^3.1.0",
         "shebang-command": "^2.0.0",
@@ -6617,6 +6725,12 @@
         "node": ">=0.3.1"
       }
     },
+    "node_modules/diff-match-patch": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
+      "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",
+      "license": "Apache-2.0"
+    },
     "node_modules/diff3": {
       "version": "0.0.3",
       "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz",
@@ -6707,6 +6821,12 @@
         "url": "https://dotenvx.com"
       }
     },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+      "license": "MIT"
+    },
     "node_modules/ecdsa-sig-formatter": {
       "version": "1.0.11",
       "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -8001,6 +8121,10 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/google-license": {
+      "resolved": "tools/google-license",
+      "link": true
+    },
     "node_modules/gopd": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@@ -8682,8 +8806,7 @@
     "node_modules/isexe": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
-      "dev": true
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
     },
     "node_modules/isobject": {
       "version": "3.0.1",
@@ -8889,6 +9012,24 @@
         "node": ">=8"
       }
     },
+    "node_modules/jackspeak": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz",
+      "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
     "node_modules/jest-worker": {
       "version": "27.5.1",
       "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
@@ -8973,6 +9114,15 @@
       "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
       "license": "BSD-3-Clause"
     },
+    "node_modules/js-levenshtein": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
+      "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/js-parse-and-output": {
       "resolved": "experiments/js-parse-and-output",
       "link": true
@@ -9458,6 +9608,10 @@
         "decamelize": "^1.2.0"
       }
     },
+    "node_modules/license-headers": {
+      "resolved": "tools/license-headers",
+      "link": true
+    },
     "node_modules/load-bmfont": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.1.tgz",
@@ -10760,6 +10914,12 @@
         "node": ">=8"
       }
     },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
+      "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==",
+      "license": "BlueOak-1.0.0"
+    },
     "node_modules/pako": {
       "version": "1.0.11",
       "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
@@ -10855,7 +11015,6 @@
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
       "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
-      "dev": true,
       "engines": {
         "node": ">=8"
       }
@@ -10865,6 +11024,40 @@
       "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
       "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
     },
+    "node_modules/path-scurry": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
+      "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^11.0.0",
+        "minipass": "^7.1.2"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/path-scurry/node_modules/lru-cache": {
+      "version": "11.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz",
+      "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==",
+      "license": "ISC",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/path-scurry/node_modules/minipass": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
     "node_modules/path-to-regexp": {
       "version": "0.1.7",
       "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
@@ -11965,7 +12158,6 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
       "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
-      "dev": true,
       "dependencies": {
         "shebang-regex": "^3.0.0"
       },
@@ -11977,7 +12169,6 @@
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
       "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
-      "dev": true,
       "engines": {
         "node": ">=8"
       }
@@ -12116,6 +12307,12 @@
         "node": ">=10"
       }
     },
+    "node_modules/simple-wcswidth": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.0.1.tgz",
+      "integrity": "sha512-xMO/8eNREtaROt7tJvWJqHBDTMFN4eiQ5I4JRMuilwfnFcV5W9u7RUkueNkdw0jPqGMX36iCywelS5yilTuOxg==",
+      "license": "MIT"
+    },
     "node_modules/sinon": {
       "version": "15.2.0",
       "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz",
@@ -12435,6 +12632,21 @@
         "node": ">=8"
       }
     },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/strip-ansi": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -12446,6 +12658,19 @@
         "node": ">=8"
       }
     },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/strip-bom": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
@@ -13383,7 +13608,6 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
       "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
-      "dev": true,
       "dependencies": {
         "isexe": "^2.0.0"
       },
@@ -13525,6 +13749,24 @@
         "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
       }
     },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
     "node_modules/wrappy": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -13638,6 +13880,18 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/yaml": {
+      "version": "2.4.5",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz",
+      "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==",
+      "license": "ISC",
+      "bin": {
+        "yaml": "bin.mjs"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
     "node_modules/yargs": {
       "version": "17.7.2",
       "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
@@ -14213,9 +14467,196 @@
       "version": "1.0.0",
       "license": "AGPL-3.0-only"
     },
+    "tools/comment-parser": {
+      "version": "1.0.0",
+      "license": "AGPL-3.0-only",
+      "devDependencies": {
+        "chai": "^5.1.1"
+      }
+    },
+    "tools/comment-parser/node_modules/assertion-error": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+      "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "tools/comment-parser/node_modules/chai": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
+      "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "assertion-error": "^2.0.1",
+        "check-error": "^2.1.1",
+        "deep-eql": "^5.0.1",
+        "loupe": "^3.1.0",
+        "pathval": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "tools/comment-parser/node_modules/check-error": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
+      "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 16"
+      }
+    },
+    "tools/comment-parser/node_modules/deep-eql": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+      "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "tools/comment-parser/node_modules/loupe": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz",
+      "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "get-func-name": "^2.0.1"
+      }
+    },
+    "tools/comment-parser/node_modules/pathval": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
+      "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14.16"
+      }
+    },
     "tools/file-walker": {
       "version": "1.0.0",
       "license": "AGPL-3.0-only"
+    },
+    "tools/google-license": {
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "commander": "^12.1.0",
+        "glob": "^11.0.0",
+        "handlebars": "^4.7.8"
+      }
+    },
+    "tools/google-license/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "tools/google-license/node_modules/commander": {
+      "version": "12.1.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
+      "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "tools/google-license/node_modules/foreground-child": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz",
+      "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==",
+      "license": "ISC",
+      "dependencies": {
+        "cross-spawn": "^7.0.0",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "tools/google-license/node_modules/glob": {
+      "version": "11.0.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz",
+      "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==",
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^4.0.1",
+        "minimatch": "^10.0.0",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^2.0.0"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "tools/google-license/node_modules/minimatch": {
+      "version": "10.0.1",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
+      "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "tools/google-license/node_modules/minipass": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "tools/google-license/node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "tools/license-headers": {
+      "version": "1.0.0",
+      "license": "AGPL-3.0-only",
+      "dependencies": {
+        "console-table-printer": "^2.12.1",
+        "dedent": "^1.5.3",
+        "diff-match-patch": "^1.0.5",
+        "js-levenshtein": "^1.1.6",
+        "yaml": "^2.4.5"
+      }
     }
   }
 }

+ 345 - 0
tools/comment-parser/main.js

@@ -0,0 +1,345 @@
+const lib = {};
+lib.dedent_lines = lines => {
+    // If any lines are just spaces, remove the spaces
+    for ( let i=0 ; i < lines.length ; i++ ) {
+        if ( /^\s+$/.test(lines[i]) ) lines[i] = '';
+    }
+    
+    // Remove leading and trailing blanks
+    while ( lines[0] === '' ) lines.shift();
+    while ( lines[lines.length-1] === '' ) lines.pop();
+
+    let min_indent = Number.MAX_SAFE_INTEGER;
+    for ( let i=0 ; i < lines.length ; i++ ) {
+        if ( lines[i] === '' ) continue;
+        let n_spaces = 0;
+        for ( let j=0 ; j < lines[i].length ; j++ ) {
+            if ( lines[i][j] === ' ' ) n_spaces++;
+            else break;
+        }
+        if ( n_spaces < min_indent ) min_indent = n_spaces;
+    }
+    for ( let i=0 ; i < lines.length ; i++ ) {
+        if ( lines[i] === '' ) continue;
+        lines[i] = lines[i].slice(min_indent);
+    }
+};
+
+const StringStream = (str, { state_ } = {}) => {
+    const state = state_ ?? { pos: 0 };
+    return {
+        skip_whitespace () {
+            while ( /^\s/.test(str[state.pos]) ) state.pos++;
+        },
+        // INCOMPLETE: only handles single chars
+        skip_matching (items) {
+            while ( items.some(item => {
+                return str[state.pos] === item;
+            }) ) state.pos++;
+        },
+        fwd (amount) {
+            state.pos += amount ?? 1;
+        },
+        fork () {
+            return StringStream(str, { state_: { pos: state.pos } });
+        },
+        async get_pos () {
+            return state.pos;
+        },
+        async get_char () {
+            return str[state.pos];
+        },
+        async matches (re_or_lit) {
+            if ( re_or_lit instanceof RegExp ) {
+                const re = re_or_lit;
+                return re.test(str.slice(state.pos));
+            }
+            
+            const lit = re_or_lit;
+            return lit === str.slice(state.pos, state.pos + lit.length);
+        },
+        async get_until (re_or_lit) {
+            let index;
+            if ( re_or_lit instanceof RegExp ) {
+                const re = re_or_lit;
+                const result = re.exec(str.slice(state.pos));
+                if ( ! result ) return;
+                index = state.pos + result.index;
+            } else {
+                const lit = re_or_lit;
+                const ind = str.slice(state.pos).indexOf(lit);
+                // TODO: parser warnings?
+                if ( ind === -1 ) return;
+                index = state.pos + ind;
+            }
+            const start_pos = state.pos;
+            state.pos = index;
+            return str.slice(start_pos, index);
+        },
+        async debug () {
+            const l1 = str.length;
+            const l2 = str.length - state.pos;
+            const clean = s => s.replace(/\n/, '{LF}');
+            return `[stream : "${
+                clean(str.slice(0, Math.min(6, l1)))
+            }"... |${state.pos}| ..."${
+                clean(str.slice(state.pos, state.pos + Math.min(6, l2)))
+            }"]`
+        }
+    };
+};
+
+const LinesCommentParser = ({
+    prefix
+}) => {
+    return {
+        parse: async (stream) => {
+            stream.skip_whitespace();
+            const lines = [];
+            while ( await stream.matches(prefix) ) {
+                const line = await stream.get_until('\n');
+                if ( ! line ) return;
+                lines.push(line);
+                stream.fwd();
+                stream.skip_matching([' ', '\t']);
+                if ( await stream.get_char() === '\n' ){
+                    stream.fwd();
+                    break;
+                }
+                stream.skip_whitespace();
+            }
+            if ( lines.length === 0 ) return;
+            for ( let i=0 ; i < lines.length ; i++ ) {
+                lines[i] = lines[i].slice(prefix.length);
+            }
+            lib.dedent_lines(lines);
+            return {
+                lines,
+            };
+        }
+    };
+};
+
+const BlockCommentParser = ({
+    start,
+    end,
+    ignore_line_prefix,
+}) => {
+    return {
+        parse: async (stream) => {
+            stream.skip_whitespace();
+            stream.debug('starting at', await stream.debug())
+            if ( ! stream.matches(start) ) return;
+            stream.fwd(start.length);
+            const contents = await stream.get_until(end);
+            if ( ! contents ) return;
+            stream.fwd(end.length);
+            // console.log('ending at', await stream.debug())
+            const lines = contents.split('\n');
+            
+            // === Formatting Time! === //
+            
+            // Special case: remove the last '*' after '/**'
+            if ( lines[0].trim() === ignore_line_prefix ) {
+                lines.shift();
+            }
+            
+            // First dedent pass
+            lib.dedent_lines(lines);
+            
+            // If all the lines start with asterisks, remove
+            let allofem = true;
+            for ( let i=0 ; i < lines.length ; i++ ) {
+                if ( lines[i] === '' ) continue;
+                if ( ! lines[i].startsWith(ignore_line_prefix) ) {
+                    allofem = false;
+                    break
+                }
+            }
+            
+            if ( allofem ) {
+                for ( let i=0 ; i < lines.length ; i++ ) {
+                    if ( lines[i] === '' ) continue;
+                    lines[i] = lines[i].slice(ignore_line_prefix.length);
+                }
+                
+                // Second dedent pass
+                lib.dedent_lines(lines);
+            }
+            
+            return { lines };
+        }
+    };
+};
+
+const LinesCommentWriter = ({ prefix }) => {
+    return {
+        write: (lines) => {
+            lib.dedent_lines(lines);
+            for ( let i=0 ; i < lines.length ; i++ ) {
+                lines[i] = prefix + lines[i];
+            }
+            return lines.join('\n') + '\n';
+        }
+    };
+};
+
+const BlockCommentWriter = ({ start, end, prefix }) => {
+    return {
+        write: (lines) => {
+            lib.dedent_lines(lines);
+            for ( let i=0 ; i < lines.length ; i++ ) {
+                lines[i] = prefix + lines[i];
+            }
+            let s = start + '\n';
+            s += lines.join('\n') + '\n';
+            s += end + '\n';
+            return s;
+        }
+    };
+};
+
+const CommentParser = () => {
+    const registry_ = {
+        object: {
+            parsers: {
+                lines: LinesCommentParser,
+                block: BlockCommentParser,
+            },
+            writers: {
+                lines: LinesCommentWriter,
+                block: BlockCommentWriter,
+            },
+        },
+        data: {
+            extensions: {
+                js: 'javascript',
+                cjs: 'javascript',
+                mjs: 'javascript',
+            },
+            languages: {
+                javascript: {
+                    parsers: [
+                        ['lines', {
+                            prefix: '// ',
+                        }],
+                        ['block', {
+                            start: '/*',
+                            end: '*/',
+                            ignore_line_prefix: '*',
+                        }],
+                    ],
+                    writers: {
+                        lines: ['lines', {
+                            prefix: '//'
+                        }],
+                        block: ['block', {
+                            start: '/*',
+                            end: '*/',
+                            prefix: ' * ',
+                        }]
+                    },
+                }
+            },
+        }
+        
+    };
+    
+    const get_language_by_filename = ({ filename }) => {
+        const { language } = (({ filename }) => {
+            const { language_id } = (({ filename }) => {
+                const { extension } = (({ filename }) => {
+                    const components = ('' + filename).split('.');
+                    const extension = components[components.length - 1];
+                    return { extension };
+                })({ filename });
+                
+                const language_id = registry_.data.extensions[extension];
+                
+                if ( ! language_id ) {
+                    throw new Error(`unrecognized language id: ` +
+                        language_id);
+                }
+                return { language_id };
+            })({ filename });
+            
+            const language = registry_.data.languages[language_id];
+            return { language };
+        })({ filename });
+
+        if ( ! language ) {
+            // TODO: use strutil quot here
+            throw new Error(`unrecognized language: ${language}`)
+        }
+        
+        return { language };
+    }
+    
+    const supports = ({ filename }) => {
+        try {
+            get_language_by_filename({ filename });
+        } catch (e) {
+            return false;
+        }
+        return true;
+    };
+    
+    const extract_top_comments = async ({ filename, source }) => {
+        const { language } = get_language_by_filename({ filename });
+        
+        // TODO: registry has `data` and `object`...
+        //       ... maybe add `virt` (virtual), which will
+        //       behave in the way the above code is written.
+
+        const inst_ = spec => registry_.object.parsers[spec[0]](spec[1]);
+        
+        let ss = StringStream(source);
+        const results = [];
+        for (;;) {
+            let comment;
+            for ( let parser of language.parsers ) {
+                const parser_name = parser[0];
+                parser = inst_(parser);
+
+                const ss_ = ss.fork();
+                const start_pos = await ss_.get_pos();
+                comment = await parser.parse(ss_);
+                const end_pos = await ss_.get_pos();
+                if ( comment ) {
+                    ss = ss_;
+                    comment.type = parser_name;
+                    comment.range = [start_pos, end_pos];
+                    break;
+                }
+            }
+            if ( ! comment ) break;
+            results.push(comment);
+        }
+        
+        return results;
+    }
+    
+    const output_comment = ({ filename, style, text }) => {
+        const { language } = get_language_by_filename({ filename });
+        
+        const inst_ = spec => registry_.object.writers[spec[0]](spec[1]);
+        let writer = language.writers[style];
+        writer = inst_(writer);
+        const lines = text.split('\n');
+        const s = writer.write(lines);
+        return s;
+    }
+    
+    return {
+        supports,
+        extract_top_comments,
+        output_comment,
+    };
+};
+
+module.exports = {
+    StringStream,
+    LinesCommentParser,
+    BlockCommentParser,
+    CommentParser,
+};

+ 15 - 0
tools/comment-parser/package.json

@@ -0,0 +1,15 @@
+{
+  "name": "comment-parser",
+  "version": "1.0.0",
+  "main": "main.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "AGPL-3.0-only",
+  "description": "",
+  "devDependencies": {
+    "chai": "^5.1.1"
+  }
+}

+ 127 - 0
tools/comment-parser/test/test.js

@@ -0,0 +1,127 @@
+const {
+    StringStream,
+    LinesCommentParser,
+    BlockCommentParser,
+    CommentParser
+} = require('../main');
+
+const assert = async (label, fn) => {
+    if ( ! await fn() ) {
+        // TODO: strutil quot
+        throw new Error(`assert: '${label}' failed`)
+    }
+};
+
+describe('parsers', () => {
+    describe('lines-comment-parser', () => {
+        it ('basic test', async () => {
+            const parser = LinesCommentParser({ prefix: '//' });
+            let lines;
+            const ss = StringStream(`
+                // first line of  first block
+                // second line of first block
+                
+                // first line of second block
+                
+                function () {}
+            `);
+            const results = [];
+            for (;;) {
+                comment = await parser.parse(ss);
+                if ( ! comment ) break;
+                results.push(comment.lines);
+            }
+            console.log('results?', results);
+        })
+    })
+    describe('block-comment-parser', () => {
+        it ('basic test', async () => {
+            const parser = BlockCommentParser({
+                start: '/*',
+                end: '*/',
+                ignore_line_prefix: '*',
+            });
+            let lines;
+            const ss = StringStream(`
+                /*
+                First block
+                comment
+                */
+                /*
+                 * second block
+                 * comment
+                 */
+                
+                /**
+                 * third block
+                 * comment
+                 */
+                function () {}
+            `);
+            const results = [];
+            for (;;) {
+                comment = await parser.parse(ss);
+                if ( ! comment ) break;
+                results.push(comment.lines);
+            }
+            console.log('results?', results);
+        })
+        it ('doesn\'t return anything for line comments', async () => {
+            const parser = BlockCommentParser({
+                start: '/*',
+                end: '*/',
+                ignore_line_prefix: '*',
+            });
+            let lines;
+            const ss = StringStream(`
+                // this comment should not be parsed
+                // by the block comment parser
+                function () {}
+            `);
+            const results = [];
+            for (;;) {
+                comment = await parser.parse(ss);
+                if ( ! comment ) break;
+                results.push(comment.lines);
+            }
+            console.log('results?', results);
+        })
+    })
+    describe('extract_top_comments', () => {
+        it ('basic test', async () => {
+            const parser = CommentParser();
+            
+            const filename = 'test.js';
+            const source = `
+                // First lines comment
+                // second line of lines comment
+                
+                /*
+                First block comment
+                second line of block comment
+                */
+            `;
+        
+            const results = await parser.extract_top_comments({
+                filename,
+                source,
+            });
+            console.log('results?', results);
+        })
+    })
+    describe('StringStream', () => {
+        describe('fork', () => {
+            it('works', async () => {
+                const ss = StringStream('asdf');
+                const ss_ = ss.fork();
+                ss_.fwd();
+                await assert('fwd worked', async () => {
+                    return await ss_.get_char() === 's';
+                });
+                await assert('upstream state is same', async () => {
+                    return await ss.get_char() === 'a';
+                });
+            })
+        })
+    })
+});

+ 23 - 21
tools/file-walker/test.js

@@ -20,6 +20,19 @@ const fs = require('fs');
 const fsp = fs.promises;
 const path_ = require('path');
 
+const EXCLUDE_LISTS = {
+    NOT_SOURCE: [
+        /^\.git/,
+        /^volatile\//,
+        /^node_modules\//,
+        /\/node_modules$/,
+        /^node_modules$/,
+        /package-lock\.json/,
+        /src\/backend\/src\/public\/assets/,
+        /^src\/gui\/src\/lib/
+    ]
+};
+
 const hl_readdir = async path => {
     const names = await fs.promises.readdir(path);
     const entries = [];
@@ -130,15 +143,7 @@ const blame = async (path) => {
 const walk_test = async () => {
     // console.log(await hl_readdir('.'));
     for await ( const value of walk({
-        excludes: [
-            /^\.git/,
-            /^volatile\//,
-            /^node_modules\//,
-            /\/node_modules$/,
-            /^node_modules$/,
-            /package-lock\.json/,
-            /^src\/gui\/dist/,
-        ]
+        excludes: EXCLUDE_LISTS.NOT_SOURCE,
     }, '.') ) {
         if ( ! value.is_dir ) continue;
         console.log('value', value.path);
@@ -170,16 +175,7 @@ git blame parsing.
 const walk_and_blame = async () => {
     // console.log(await hl_readdir('.'));
     for await ( const value of walk({
-        excludes: [
-            /^\.git/,
-            /^volatile\//,
-            /^node_modules\//,
-            /\/node_modules$/,
-            /^node_modules$/,
-            /package-lock\.json/,
-            /src\/backend\/src\/public\/assets/,
-            /^src\/gui\/src\/lib/
-        ]
+        excludes: EXCLUDE_LISTS.NOT_SOURCE,
     }, '.') ) {
         if ( value.is_dir ) continue;
         console.log('value', value.path);
@@ -194,6 +190,12 @@ const walk_and_blame = async () => {
     console.log('AUTHORS', authors);
 }
 
-const main = walk_and_blame;
+if ( require.main === module ) {
+    const main = walk_and_blame;
+    main();
+}
 
-main();
+module.exports = {
+    walk,
+    EXCLUDE_LISTS,
+};

+ 356 - 0
tools/license-headers/main.js

@@ -0,0 +1,356 @@
+const levenshtein = require('js-levenshtein');
+const DiffMatchPatch = require('diff-match-patch');
+const dmp = new DiffMatchPatch();
+const dedent = require('dedent');
+
+const { walk, EXCLUDE_LISTS } = require('file-walker');
+const { CommentParser } = require('../comment-parser/main');
+
+const fs = require('fs');
+const path_ = require('path');
+
+const CompareFn = ({ header1, header2, distance_only = false }) => {
+    
+    // Calculate Levenshtein distance
+    const distance = levenshtein(header1, header2);
+    // console.log(`Levenshtein distance: ${distance}`);
+    
+    if ( distance_only ) return { distance };
+
+    // Generate diffs using diff-match-patch
+    const diffs = dmp.diff_main(header1, header2);
+    dmp.diff_cleanupSemantic(diffs);
+    
+    let term_diff = '';
+
+    // Manually format diffs for terminal display
+    diffs.forEach(([type, text]) => {
+        switch (type) {
+            case DiffMatchPatch.DIFF_INSERT:
+            term_diff += `\x1b[32m${text}\x1b[0m`;  // Green for insertions
+            break;
+            case DiffMatchPatch.DIFF_DELETE:
+            term_diff += `\x1b[31m${text}\x1b[0m`;  // Red for deletions
+            break;
+            case DiffMatchPatch.DIFF_EQUAL:
+            term_diff += text;  // No color for equal parts
+            break;
+        }
+    });
+    
+    return {
+        distance,
+        term_diff,
+    };
+}
+
+const LicenseChecker = ({
+    comment_parser,
+    desired_header,
+}) => {
+    const supports = ({ filename }) => {
+        return comment_parser.supports({ filename });
+    };
+    const compare = async ({ filename, source }) => {
+        const headers = await comment_parser.extract_top_comments(
+            { filename, source });
+        const headers_lines = headers.map(h => h.lines);
+            
+        if ( headers.length < 1 ) {
+            return {
+                has_header: false,
+            };
+        }
+        
+        // console.log('headers', headers);
+
+        let top = 0;
+        let bottom = 0;
+        let current_distance = Number.MAX_SAFE_INTEGER;
+        
+        // "wah"
+        for ( let i=1 ; i <= headers.length ; i++ ) {
+            const combined = headers_lines.slice(top, i).flat();
+            const combined_txt = combined.join('\n');
+            const { distance } =
+                CompareFn({
+                    header1: desired_header,
+                    header2: combined_txt,
+                    distance_only: true,
+                });
+            if ( distance < current_distance ) {
+                current_distance = distance;
+                bottom = i;
+            } else {
+                break;
+            }
+        }
+        // "woop"
+        for ( let i=1 ; i < headers.length ; i++ ) {
+            const combined = headers_lines.slice(i, bottom).flat();
+            const combined_txt = combined.join('\n');
+            const { distance } =
+                CompareFn({
+                    header1: desired_header,
+                    header2: combined_txt,
+                    distance_only: true,
+                });
+            if ( distance < current_distance ) {
+                current_distance = distance;
+                top = i;
+            } else {
+                break;
+            }
+        }
+
+        const combined = headers_lines.slice(top, bottom).flat();
+        const combined_txt = combined.join('\n');
+            
+        const diff_info = CompareFn({
+            header1: desired_header,
+            header2: combined_txt,
+        })
+        
+        diff_info.range = [
+            headers[top].range[0],
+            headers[bottom-1].range[1],
+        ];
+        
+        diff_info.has_header = true;
+            
+        return diff_info;
+    };
+    return {
+        compare,
+        supports,
+    };
+};
+
+const license_check_test = async ({ options }) => {
+    const comment_parser = CommentParser();
+    const license_checker = LicenseChecker({
+        comment_parser,
+        desired_header: fs.readFileSync(
+            path_.join(__dirname, '../../doc/license_header.txt'),
+            'utf-8',
+        ),
+    });
+    
+    const walk_iterator = walk({
+        excludes: EXCLUDE_LISTS.NOT_SOURCE,
+    }, path_.join(__dirname, '../..'));
+    for await ( const value of walk_iterator ) {
+        if ( value.is_dir ) continue;
+        if ( value.name !== 'dev-console-ui-utils.js' ) continue;
+        console.log(value.path);
+        const source = fs.readFileSync(value.path, 'utf-8');
+        const diff_info = await license_checker.compare({
+            filename: value.name,
+            source,
+        })
+        if ( diff_info ) {
+            process.stdout.write('\x1B[36;1m=======\x1B[0m\n');
+            process.stdout.write(diff_info.term_diff);
+            process.stdout.write('\n\x1B[36;1m=======\x1B[0m\n');
+            // console.log('headers', headers);
+        } else {
+            console.log('NO COMMENT');
+        }
+        
+        console.log('RANGE', diff_info.range)
+        
+        const new_comment = comment_parser.output_comment({
+            filename: value.name,
+            style: 'block',
+            text: 'some text\nto display'
+        });
+
+        console.log('NEW COMMENT?', new_comment);
+    }
+};
+
+const cmd_check_fn = async () => {
+    const comment_parser = CommentParser();
+    const license_checker = LicenseChecker({
+        comment_parser,
+        desired_header: fs.readFileSync(
+            path_.join(__dirname, '../../doc/license_header.txt'),
+            'utf-8',
+        ),
+    });
+    
+    const counts = {
+        ok: 0,
+        missing: 0,
+        conflict: 0,
+        error: 0,
+        unsupported: 0,
+    };
+    
+    const walk_iterator = walk({
+        excludes: EXCLUDE_LISTS.NOT_SOURCE,
+    }, path_.join(__dirname, '../..'));
+    for await ( const value of walk_iterator ) {
+        if ( value.is_dir ) continue;
+
+        process.stdout.write(value.path + ' ... ');
+
+        if ( ! license_checker.supports({ filename: value.name }) ) {
+            process.stdout.write(`\x1B[37;1mUNSUPPORTED\x1B[0m\n`);
+            counts.unsupported++;
+            continue;
+        }
+
+        const source = fs.readFileSync(value.path, 'utf-8');
+        const diff_info = await license_checker.compare({
+            filename: value.name,
+            source,
+        })
+        if ( ! diff_info ) {
+            counts.error++;
+            continue;
+        }
+        if ( ! diff_info.has_header ) {
+            counts.missing++;
+            process.stdout.write(`\x1B[33;1mMISSING\x1B[0m\n`);
+            continue;
+        }
+        if ( diff_info ) {
+            if ( diff_info.distance !== 0 ) {
+                counts.conflict++;
+                process.stdout.write(`\x1B[31;1mCONFLICT\x1B[0m\n`);
+            } else {
+                counts.ok++;
+                process.stdout.write(`\x1B[32;1mOK\x1B[0m\n`);
+            }
+        } else {
+            console.log('NO COMMENT');
+        }
+    }
+    
+    const { Table } = require('console-table-printer');
+    const t = new Table({
+        columns: [
+            {
+                title: 'License Header',
+                name: 'situation', alignment: 'left', color: 'white_bold' },
+            {
+                title: 'Number of Files',
+                name: 'count', alignment: 'right' },
+        ],
+        colorMap: {
+            green: '\x1B[32;1m',
+            yellow: '\x1B[33;1m',
+            red: '\x1B[31;1m',
+        }
+    });
+    
+    console.log('');
+    
+    if ( counts.error > 0 ) {
+        console.log(`\x1B[31;1mTHERE WERE SOME ERRORS!\x1B[0m`);
+        console.log('check the log above for the stack trace');
+        console.log('');
+        t.addRow({ situation: 'error', count: counts.error },
+            { color: 'red' });
+    }
+    
+    console.log(dedent(`
+        \x1B[31;1mAny text below is mostly lies!\x1B[0m
+        This tool is still being developed and most of what's
+        described is "the plan" rather than a thing that will
+        actually happen.
+        \x1B[31;1m^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\x1B[0m
+    `));
+
+    if ( counts.conflict ) {
+        console.log(dedent(`
+            \x1B[37;1mIt looks like you have some conflicts!\x1B[0m
+            Run the following command to update license headers:
+
+               \x1B[36;1maddlicense sync\x1B[0m
+               
+            This will begin an interactive license update.
+            Any time the license doesn't quite match you will
+            be given the option to replace it or skip the file.
+            \x1B[90mSee \`addlicense help sync\` for other options.\x1B[0m
+            
+            You will also be able to choose
+            "remember for headers matching this one"
+            if you know the same issue will come up later.
+        `));
+    } else if ( counts.missing ) {
+        console.log(dedent(`
+            \x1B[37;1mSome missing license headers!\x1B[0m
+            Run the following command to add the missing license headers:
+
+               \x1B[36;1maddlicense sync\x1B[0m
+        `));
+    } else {
+        console.log(dedent(`
+            \x1B[37;1mNo action to perform!\x1B[0m
+            Run the following command to do absolutely nothing:
+
+               \x1B[36;1maddlicense sync\x1B[0m
+        `));
+    }
+
+    console.log('');
+
+    t.addRow({ situation: 'ok', count: counts.ok },
+        { color: 'green' });
+    t.addRow({ situation: 'missing', count: counts.missing },
+        { color: 'yellow' });
+    t.addRow({ situation: 'conflict', count: counts.conflict },
+        { color: 'red' });
+    t.addRow({ situation: 'unsupported', count: counts.unsupported });
+    t.printTable();
+};
+
+const main = async () => {
+    const { program } = require('commander');
+    const helptext = dedent(`
+        Usage: usage text
+    `);
+    
+    const run_command = async ({ cmd, cmd_fn }) => {
+        const options = {
+            program: program.opts(),
+            command: cmd.opts(),
+        };
+        console.log('options', options);
+        
+        if ( ! fs.existsSync(options.program.config) ) {
+            // TODO: configuration wizard
+            fs.writeFileSync(options.program.config, '');
+        }
+        
+        await cmd_fn({ options });
+    };
+    
+    program
+        .name('addlicense')
+        .option('-c, --config', 'configuration file', 'addlicense.yml')
+        .addHelpText('before', helptext)
+        ;
+    const cmd_check = program.command('check')
+        .description('check license headers')
+        .option('-n, --non-interactive', 'disable prompting')
+        .action(() => {
+            run_command({ cmd: cmd_check, cmd_fn: cmd_check_fn });
+        })
+    const cmd_sync = program.command('sync')
+        .description('synchronize files with license header rules')
+        .option('-n, --non-interactive', 'disable prompting')
+        .action(() => {
+            console.log('called sync');
+            console.log(program.opts());
+            console.log(cmd_sync.opts());
+        })
+    program.parse(process.argv);
+        
+};
+
+if ( require.main === module ) {
+    main();
+}

+ 19 - 0
tools/license-headers/package.json

@@ -0,0 +1,19 @@
+{
+  "name": "license-headers",
+  "version": "1.0.0",
+  "main": "main.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "AGPL-3.0-only",
+  "description": "",
+  "dependencies": {
+    "console-table-printer": "^2.12.1",
+    "dedent": "^1.5.3",
+    "diff-match-patch": "^1.0.5",
+    "js-levenshtein": "^1.1.6",
+    "yaml": "^2.4.5"
+  }
+}