From ff778d27ef04bdb1f1887b94681d05eb20242969 Mon Sep 17 00:00:00 2001 From: Beckett Frey Date: Mon, 11 May 2026 12:28:57 -0500 Subject: [PATCH 1/7] Add Husky pre-commit and pre-push hooks Pre-commit runs lint-staged (ESLint + Prettier on staged files) and a project-wide typecheck. Pre-push runs full lint, typecheck, and next build so any pushed branch is guaranteed buildable. --- .husky/pre-commit | 2 + .husky/pre-push | 3 + .prettierignore | 8 + .prettierrc.json | 1 + package-lock.json | 489 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 19 +- 6 files changed, 520 insertions(+), 2 deletions(-) create mode 100755 .husky/pre-commit create mode 100755 .husky/pre-push create mode 100644 .prettierignore create mode 100644 .prettierrc.json diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..f3f510d --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +npx lint-staged +npm run typecheck diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..cc9739d --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,3 @@ +npm run lint +npm run typecheck +npm run build diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..20ed00c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +.next/ +out/ +build/ +node_modules/ +next-env.d.ts +public/ +tsconfig.tsbuildinfo +package-lock.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +{} diff --git a/package-lock.json b/package-lock.json index 845154e..8a823f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,9 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.0.1", + "husky": "^9.1.7", + "lint-staged": "^16.4.0", + "prettier": "^3.8.3", "tailwindcss": "^4", "typescript": "^5" } @@ -2154,6 +2157,35 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2571,6 +2603,39 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -2597,6 +2662,23 @@ "dev": true, "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2821,6 +2903,19 @@ "node": ">=10.13.0" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-abstract": { "version": "1.24.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", @@ -3445,6 +3540,13 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3647,6 +3749,19 @@ "node": ">=6.9.0" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3891,6 +4006,22 @@ "hermes-estree": "0.25.1" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4127,6 +4258,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -4788,6 +4935,61 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lint-staged": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", + "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.3", + "listr2": "^9.0.5", + "picomatch": "^4.0.3", + "string-argv": "^0.3.2", + "tinyexec": "^1.0.4", + "yaml": "^2.8.2" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4811,6 +5013,56 @@ "dev": true, "license": "MIT" }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4887,6 +5139,19 @@ "node": ">=8.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5169,6 +5434,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5345,6 +5626,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5501,6 +5798,23 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -5512,6 +5826,13 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5813,6 +6134,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5843,6 +6207,33 @@ "node": ">= 0.4" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -5956,6 +6347,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -6049,6 +6456,16 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -6501,6 +6918,62 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -6508,6 +6981,22 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 4d0ae5c..d07e84c 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,22 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "USE_FAKE_RELEASES=true next dev", + "dev": "USE_FAKE_RELEASES=false next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "typecheck": "tsc --noEmit", + "format": "prettier --write .", + "prepare": "husky" + }, + "lint-staged": { + "*.{ts,tsx,js,jsx,mjs}": [ + "eslint --fix", + "prettier --write" + ], + "*.{json,css,md}": [ + "prettier --write" + ] }, "dependencies": { "lucide-react": "^0.553.0", @@ -21,6 +33,9 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.0.1", + "husky": "^9.1.7", + "lint-staged": "^16.4.0", + "prettier": "^3.8.3", "tailwindcss": "^4", "typescript": "^5" } From b497921988fbed660782ea4582e0d4db147b598e Mon Sep 17 00:00:00 2001 From: Beckett Frey Date: Mon, 11 May 2026 13:54:57 -0500 Subject: [PATCH 2/7] Add CI workflow running lint, typecheck, and prettier check --- .github/workflows/ci.yml | 43 ++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 44 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f3fa4ee --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +jobs: + lint: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - run: npm run lint + + typecheck: + name: typecheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - run: npm run typecheck + + format: + name: format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - run: npm run format:check diff --git a/package.json b/package.json index d07e84c..e422712 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "eslint", "typecheck": "tsc --noEmit", "format": "prettier --write .", + "format:check": "prettier --check .", "prepare": "husky" }, "lint-staged": { From 0bcbe61cdac7a4843068aa2d50892c92e656baef Mon Sep 17 00:00:00 2001 From: Beckett Frey Date: Mon, 11 May 2026 13:55:19 -0500 Subject: [PATCH 3/7] Mark workflow lock files as generated and prefer ours on merge --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c1965c2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.github/workflows/*.lock.yml linguist-generated=true merge=ours \ No newline at end of file From 9f50ddd18a48ee4a44fee603e86207d05c45a34e Mon Sep 17 00:00:00 2001 From: Beckett Frey Date: Mon, 11 May 2026 14:19:45 -0500 Subject: [PATCH 4/7] Add contribution guide with guardrails documented within --- .github/CONTRIBUTING.md | 113 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 .github/CONTRIBUTING.md diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..85baacd --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,113 @@ +# Contributing + +> [!NOTE] +> This guide serves to track changes to CI/CD and inform contributors and new maintainers of those patterns so they aren't repeated or causing confusion during development. + +`main` is the code deployed to production, Vercel auto-deploys every commit to it. All human changes reach `main` through a pull request (no direct pushes). The one exception is the sync-docs automation bot, which pushes generated documentation commits (`sync: update documentation from voxkit-desktop`) directly to `main` when new application code is deployed from [voxkit-desktop](https://github.com/BrainBehaviorAnalyticsLab/voxkit-desktop). + +This contribution guide is split by role: + +- **[For developers](#for-developers)**: writing and shipping code. +- **[For repo & Vercel owners](#for-repo--vercel-owners)**: setting the guardrails that control inflow. + +--- + +## For developers + +- **Internal members** (collaborators with write access) create a branch in this repo. +- **External contributors** fork the repo, branch in the fork, and open a PR from the fork against `main`. + +### Contribution model (GitHub Flow) + +``` +branch-name ──PR──► main ──auto-deploy──► Vercel production +``` + +1. Branch off `main` (in this repo if internal, in your fork if external): `git checkout -b ` +2. Commit work locally, the pre-commit hook enforces lint + format + typecheck on every commit. +3. Push the branch; the pre-push hook enforces full lint + typecheck + `next build`, so a broken branch doesn't even reach GitHub. +4. Open a PR against `main`. Vercel posts a preview URL on the PR. +5. Merge via Squash and merge once review + checks pass. Keeps `main` history linear; 1 commit = 1 shipped change. +6. Delete the branch after merge. Vercel deploys the new `main` to production automatically. + +### Useful scripts + +| Command | What it does | +| ------------------- | --------------------------------------------- | +| `npm run dev` | Local dev server (`USE_FAKE_RELEASES=false`). | +| `npm run build` | Production build. | +| `npm run lint` | ESLint over the project. | +| `npm run typecheck` | `tsc --noEmit`. | +| `npm run format` | Prettier-format the whole project. | + +--- + +## For repo & Vercel owners + +You own the guardrails that make the developer workflow safe. The rules below are authoritative. + +### GitHub branch protection (`main`) + +We use **Rulesets**, not the legacy "Branch protection rules". Rulesets supersede branch protection and are required for app-based bypass (see [Automation bot bypass](#automation-bot-bypass) below). + +`Settings → Rules → Rulesets → New branch ruleset` + +Configure the ruleset: + +- **Name**: `main protection` (or similar). +- **Enforcement status**: `Active`. +- **Target branches**: `Include default branch` (or add `refs/heads/main` explicitly). +- **Bypass list**: add the **Repository admin** role so the someone can land emergency fixes or rollbacks if PR review is unavailable. The sync-docs bot is added in the next section. No other humans should be on this list. + +Branch rules to enable: + +- **Restrict deletions**: prevents `main` from being deleted. +- **Require linear history**: pairs with squash-merge to keep history flat. +- **Require a pull request before merging**: + - Required approvals: **1**. + - **Dismiss stale pull request approvals when new commits are pushed**: on. + - **Require approval of the most recent reviewable push**: on. +- **Require status checks to pass**: + - **Require branches to be up to date before merging**: on. + - Add each required check by name once the CI workflow exists (lint, typecheck, build, Vercel). +- **Block force pushes**: on. + +Anything not in the Bypass list is subject to all of the above. The Repository admin can bypass for genuine emergencies; routine work still goes through PRs. + +### Automation bot bypass + +The docs-sync workflow in [`voxkit-desktop`](https://github.com/BrainBehaviorAnalyticsLab/voxkit-desktop) pushes generated docs to `main` here using `secrets.PRIVATE_REPO_TOKEN`. The push is authenticated as the **owner of that PAT**, not as `github-actions[bot]`, so the PAT must be issued by the Repository admin (already in the Bypass list above). No separate bypass entry is needed. + +Also in `Settings → General → Pull Requests`: + +- **Allow squash merging**: on. +- **Allow merge commits**: off. +- **Allow rebase merging**: off. +- **Automatically delete head branches**: on. + +### Vercel project + +- **Production branch**: `main`. Every commit triggers a production deployment. +- **Preview deployments**: enabled for every PR and every non-`main` branch push. Vercel posts the preview URL as a PR check. +- **Environment variables**: managed in the Vercel dashboard. + - Production secrets must be scoped **Production only** so PR previews can't read them. + - Preview-safe variables can be scoped to Preview + Development. +- **GitHub integration**: the Vercel GitHub App must have access to this repo so it can post deployment statuses (these become required checks in branch protection). + +### Rollback + +Production is `main`. Two paths: + +1. **Preferred**: revert the bad commit on `main` via a PR; `git revert ` → PR → squash-merge. Vercel auto-redeploys. History stays linear and the revert is auditable. +2. **Emergency**: in the Vercel dashboard, promote a prior production deployment to current. Do this when a revert PR would be too slow. Immediately follow with a revert PR so `main` and production are back in sync. + +### Enforcement summary + +| Stage | Enforced by | Gate | Authoritative? | +| ----------------- | ------------------------ | ------------------------------------ | ------------------ | +| Each commit | Husky `pre-commit` | lint-staged + typecheck | No (`--no-verify`) | +| Each push | Husky `pre-push` | full lint + typecheck + `next build` | No (`--no-verify`) | +| Reaching `main` | GitHub branch protection | PR + approval + CI checks | **Yes** | +| Production deploy | Vercel | auto on `main` | **Yes** | + +The hooks make the common case fast and pleasant. Branch protection is what actually keeps `main` clean. From 400faa40e2065b1293b601c7d50eaf73f3655c86 Mon Sep 17 00:00:00 2001 From: Beckett Frey Date: Mon, 11 May 2026 14:21:33 -0500 Subject: [PATCH 5/7] Reformat files according to new convention (w/ prettier) --- app/api/releases/route.ts | 30 ++-- app/docs/page.tsx | 31 ++-- app/download/page.tsx | 8 +- app/features/page.tsx | 244 ++++++++++++++++++++----------- app/foundations/page.tsx | 113 ++++++++++----- app/globals.css | 37 +++-- app/help/[topic]/page.tsx | 263 ++++++++++++++++++++++------------ app/help/page.tsx | 127 +++++++++++----- app/layout.tsx | 5 +- app/page.tsx | 239 +++++++++++++++--------------- components/DecisionTree.tsx | 130 +++++++++-------- components/DownloadButton.tsx | 36 +++-- components/GridBackground.tsx | 28 +++- components/GridButton.tsx | 20 ++- components/HelpChat.tsx | 94 +++++++----- components/OSIcons.tsx | 29 +++- components/WaveSeparator.tsx | 63 +++++--- data/help-content.json | 6 +- layout/Footer.tsx | 112 +++++++++------ layout/Navbar.tsx | 154 ++++++++++---------- layout/index.tsx | 6 +- lib/fakeReleases.ts | 100 ++++++------- types/help.ts | 2 +- 23 files changed, 1142 insertions(+), 735 deletions(-) diff --git a/app/api/releases/route.ts b/app/api/releases/route.ts index 1598453..5ef663c 100644 --- a/app/api/releases/route.ts +++ b/app/api/releases/route.ts @@ -1,4 +1,4 @@ -import { NextResponse } from 'next/server'; +import { NextResponse } from "next/server"; import type { GitHubRelease, GitHubAsset, @@ -7,8 +7,8 @@ import type { OperatingSystem, ReleaseAsset, ReleasesAPIResponse, -} from '../../../types/releases'; -import { fakeGitHubReleases } from '../../../lib/fakeReleases'; +} from "../../../types/releases"; +import { fakeGitHubReleases } from "../../../lib/fakeReleases"; const OS_EXTENSIONS: Record = { macos: /\.(dmg|pkg)$/i, @@ -29,8 +29,8 @@ function parseVersion(tag: string): string | null { } function compareVersions(v1: string, v2: string): number { - const parts1 = v1.split('.').map(Number); - const parts2 = v2.split('.').map(Number); + const parts1 = v1.split(".").map(Number); + const parts2 = v2.split(".").map(Number); for (let i = 0; i < 3; i++) { if (parts1[i] > parts2[i]) return 1; if (parts1[i] < parts2[i]) return -1; @@ -50,7 +50,7 @@ export async function GET() { try { let releases: GitHubRelease[]; - if (process.env.USE_FAKE_RELEASES === 'true') { + if (process.env.USE_FAKE_RELEASES === "true") { releases = fakeGitHubReleases; } else { const owner = process.env.GITHUB_OWNER; @@ -58,25 +58,25 @@ export async function GET() { if (!owner || !repo) { return NextResponse.json( - { error: 'GITHUB_OWNER or GITHUB_REPO is not configured' }, - { status: 500 } + { error: "GITHUB_OWNER or GITHUB_REPO is not configured" }, + { status: 500 }, ); } const response = await fetch( `https://api.github.com/repos/${owner}/${repo}/releases`, { - headers: { Accept: 'application/vnd.github+json' }, + headers: { Accept: "application/vnd.github+json" }, next: { revalidate: 5000 }, - } + }, ); if (!response.ok) { const errorText = await response.text(); - console.error('GitHub API error:', response.status, errorText); + console.error("GitHub API error:", response.status, errorText); return NextResponse.json( { error: `Failed to fetch releases from GitHub: ${response.status}` }, - { status: response.status } + { status: response.status }, ); } @@ -121,10 +121,10 @@ export async function GET() { return NextResponse.json(apiResponse); } catch (error) { - console.error('Error fetching releases:', error); + console.error("Error fetching releases:", error); return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } + { error: "Internal server error" }, + { status: 500 }, ); } } diff --git a/app/docs/page.tsx b/app/docs/page.tsx index 4cab87e..f4db611 100644 --- a/app/docs/page.tsx +++ b/app/docs/page.tsx @@ -4,27 +4,24 @@ import { Footer, Navbar } from "../../layout"; export default function DocsPage() { return ( <> -
- +
+ - {/* Documentation iframe container */} -
-
- - {/* Iframe wrapper with styling */} -
-