Compare commits
31 Commits
update-ver
...
fix/253
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75d52c771f | ||
|
|
c868b008c5 | ||
|
|
7f1ef91db9 | ||
|
|
2ee5f73117 | ||
|
|
881456e74e | ||
|
|
650afd5872 | ||
|
|
f0dc393358 | ||
|
|
db72fea774 | ||
|
|
24896f9c0d | ||
|
|
bf434e3b39 | ||
|
|
7699858a35 | ||
|
|
68b6fc81bd | ||
|
|
16947cedf5 | ||
|
|
eca0cb57f9 | ||
|
|
011cbd23b2 | ||
|
|
19e18b4ebf | ||
|
|
ffef6313d4 | ||
|
|
7b4daf8754 | ||
|
|
fdeda6c77a | ||
|
|
5acaf144fb | ||
|
|
926032e83b | ||
|
|
8fc9b27f55 | ||
|
|
9a8cff3227 | ||
|
|
e40bd1f6a3 | ||
|
|
6c982050da | ||
|
|
b40f5b788c | ||
|
|
962a3b9908 | ||
|
|
a1fe386efd | ||
|
|
aa76e17e25 | ||
|
|
2c3fdf5544 | ||
|
|
74dd29b87b |
@@ -27,3 +27,12 @@ AUTH_ENABLED=false
|
||||
AUTH_SETUP_COMPLETED=false
|
||||
JWT_SECRET=
|
||||
DATABASE_URL="file:/opt/ProxmoxVE-Local/data/settings.db"
|
||||
AUTO_SYNC_ENABLED=false
|
||||
SYNC_INTERVAL_TYPE=
|
||||
SYNC_INTERVAL_PREDEFINED=
|
||||
AUTO_DOWNLOAD_NEW=
|
||||
AUTO_UPDATE_EXISTING=
|
||||
NOTIFICATION_ENABLED=
|
||||
APPRISE_URLS=
|
||||
LAST_AUTO_SYNC=
|
||||
SYNC_INTERVAL_CRON=
|
||||
180
package-lock.json
generated
180
package-lock.json
generated
@@ -8,7 +8,7 @@
|
||||
"name": "pve-scripts-local",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.17.1",
|
||||
"@prisma/client": "^6.18.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@t3-oss/env-nextjs": "^0.13.8",
|
||||
@@ -29,7 +29,7 @@
|
||||
"cron-validator": "^1.2.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^0.546.0",
|
||||
"lucide-react": "^0.548.0",
|
||||
"next": "^15.5.6",
|
||||
"node-cron": "^3.0.3",
|
||||
"node-pty": "^1.0.0",
|
||||
@@ -48,7 +48,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@tailwindcss/postcss": "^4.1.15",
|
||||
"@tailwindcss/postcss": "^4.1.16",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@@ -59,7 +59,7 @@
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"eslint": "^9.38.0",
|
||||
@@ -2042,9 +2042,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "6.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.17.1.tgz",
|
||||
"integrity": "sha512-zL58jbLzYamjnNnmNA51IOZdbk5ci03KviXCuB0Tydc9btH2kDWsi1pQm2VecviRTM7jGia0OPPkgpGnT3nKvw==",
|
||||
"version": "6.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.18.0.tgz",
|
||||
"integrity": "sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -2660,9 +2660,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.38",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz",
|
||||
"integrity": "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==",
|
||||
"version": "1.0.0-beta.43",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz",
|
||||
"integrity": "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -3053,56 +3053,49 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.15.tgz",
|
||||
"integrity": "sha512-HF4+7QxATZWY3Jr8OlZrBSXmwT3Watj0OogeDvdUY/ByXJHQ+LBtqA2brDb3sBxYslIFx6UP94BJ4X6a4L9Bmw==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz",
|
||||
"integrity": "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"enhanced-resolve": "^5.18.3",
|
||||
"jiti": "^2.6.0",
|
||||
"jiti": "^2.6.1",
|
||||
"lightningcss": "1.30.2",
|
||||
"magic-string": "^0.30.19",
|
||||
"source-map-js": "^1.2.1",
|
||||
"tailwindcss": "4.1.15"
|
||||
"tailwindcss": "4.1.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/node_modules/tailwindcss": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.15.tgz",
|
||||
"integrity": "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.15.tgz",
|
||||
"integrity": "sha512-krhX+UOOgnsUuks2SR7hFafXmLQrKxB4YyRTERuCE59JlYL+FawgaAlSkOYmDRJdf1Q+IFNDMl9iRnBW7QBDfQ==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz",
|
||||
"integrity": "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.15",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.15",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.15",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.15",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.15",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.15",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.15",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.15",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.15",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.15",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.15",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.15"
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.16",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.16",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.16",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.16",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.16",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.16",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.16",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.16",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.16",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.16",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.15.tgz",
|
||||
"integrity": "sha512-TkUkUgAw8At4cBjCeVCRMc/guVLKOU1D+sBPrHt5uVcGhlbVKxrCaCW9OKUIBv1oWkjh4GbunD/u/Mf0ql6kEA==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.16.tgz",
|
||||
"integrity": "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3117,9 +3110,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.15.tgz",
|
||||
"integrity": "sha512-xt5XEJpn2piMSfvd1UFN6jrWXyaKCwikP4Pidcf+yfHTSzSpYhG3dcMktjNkQO3JiLCp+0bG0HoWGvz97K162w==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz",
|
||||
"integrity": "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3134,9 +3127,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.15.tgz",
|
||||
"integrity": "sha512-TnWaxP6Bx2CojZEXAV2M01Yl13nYPpp0EtGpUrY+LMciKfIXiLL2r/SiSRpagE5Fp2gX+rflp/Os1VJDAyqymg==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.16.tgz",
|
||||
"integrity": "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3151,9 +3144,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.15.tgz",
|
||||
"integrity": "sha512-quISQDWqiB6Cqhjc3iWptXVZHNVENsWoI77L1qgGEHNIdLDLFnw3/AfY7DidAiiCIkGX/MjIdB3bbBZR/G2aJg==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.16.tgz",
|
||||
"integrity": "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3168,9 +3161,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.15.tgz",
|
||||
"integrity": "sha512-ObG76+vPlab65xzVUQbExmDU9FIeYLQ5k2LrQdR2Ud6hboR+ZobXpDoKEYXf/uOezOfIYmy2Ta3w0ejkTg9yxg==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.16.tgz",
|
||||
"integrity": "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -3185,9 +3178,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.15.tgz",
|
||||
"integrity": "sha512-4WbBacRmk43pkb8/xts3wnOZMDKsPFyEH/oisCm2q3aLZND25ufvJKcDUpAu0cS+CBOL05dYa8D4U5OWECuH/Q==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.16.tgz",
|
||||
"integrity": "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3202,9 +3195,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.15.tgz",
|
||||
"integrity": "sha512-AbvmEiteEj1nf42nE8skdHv73NoR+EwXVSgPY6l39X12Ex8pzOwwfi3Kc8GAmjsnsaDEbk+aj9NyL3UeyHcTLg==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.16.tgz",
|
||||
"integrity": "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3219,9 +3212,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.15.tgz",
|
||||
"integrity": "sha512-+rzMVlvVgrXtFiS+ES78yWgKqpThgV19ISKD58Ck+YO5pO5KjyxLt7AWKsWMbY0R9yBDC82w6QVGz837AKQcHg==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.16.tgz",
|
||||
"integrity": "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3236,9 +3229,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.15.tgz",
|
||||
"integrity": "sha512-fPdEy7a8eQN9qOIK3Em9D3TO1z41JScJn8yxl/76mp4sAXFDfV4YXxsiptJcOwy6bGR+70ZSwFIZhTXzQeqwQg==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.16.tgz",
|
||||
"integrity": "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3253,9 +3246,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.15.tgz",
|
||||
"integrity": "sha512-sJ4yd6iXXdlgIMfIBXuVGp/NvmviEoMVWMOAGxtxhzLPp9LOj5k0pMEMZdjeMCl4C6Up+RM8T3Zgk+BMQ0bGcQ==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.16.tgz",
|
||||
"integrity": "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==",
|
||||
"bundleDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
"@emnapi/core",
|
||||
@@ -3343,9 +3336,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.15.tgz",
|
||||
"integrity": "sha512-sJGE5faXnNQ1iXeqmRin7Ds/ru2fgCiaQZQQz3ZGIDtvbkeV85rAZ0QJFMDg0FrqsffZG96H1U9AQlNBRLsHVg==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz",
|
||||
"integrity": "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3360,9 +3353,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.15.tgz",
|
||||
"integrity": "sha512-NLeHE7jUV6HcFKS504bpOohyi01zPXi2PXmjFfkzTph8xRxDdxkRsXm/xDO5uV5K3brrE1cCwbUYmFUSHR3u1w==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.16.tgz",
|
||||
"integrity": "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3377,26 +3370,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.15.tgz",
|
||||
"integrity": "sha512-IZh8IT76KujRz6d15wZw4eoeViT4TqmzVWNNfpuNCTKiaZUwgr5vtPqO4HjuYDyx3MgGR5qgPt1HMzTeLJyA3g==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.16.tgz",
|
||||
"integrity": "sha512-Qn3SFGPXYQMKR/UtqS+dqvPrzEeBZHrFA92maT4zijCVggdsXnDBMsPFJo1eArX3J+O+Gi+8pV4PkqjLCNBk3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@tailwindcss/node": "4.1.15",
|
||||
"@tailwindcss/oxide": "4.1.15",
|
||||
"@tailwindcss/node": "4.1.16",
|
||||
"@tailwindcss/oxide": "4.1.16",
|
||||
"postcss": "^8.4.41",
|
||||
"tailwindcss": "4.1.15"
|
||||
"tailwindcss": "4.1.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss/node_modules/tailwindcss": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.15.tgz",
|
||||
"integrity": "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tailwindcss/typography": {
|
||||
"version": "0.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
|
||||
@@ -4384,18 +4370,18 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz",
|
||||
"integrity": "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.0.tgz",
|
||||
"integrity": "sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.28.4",
|
||||
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
||||
"@rolldown/pluginutils": "1.0.0-beta.38",
|
||||
"@rolldown/pluginutils": "1.0.0-beta.43",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"react-refresh": "^0.17.0"
|
||||
"react-refresh": "^0.18.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
@@ -8675,9 +8661,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.546.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.546.0.tgz",
|
||||
"integrity": "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==",
|
||||
"version": "0.548.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.548.0.tgz",
|
||||
"integrity": "sha512-63b16z63jM9yc1MwxajHeuu0FRZFsDtljtDjYm26Kd86UQ5HQzu9ksEtoUUw4RBuewodw/tGFmvipePvRsKeDA==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
@@ -10645,9 +10631,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
|
||||
"integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.17.1",
|
||||
"@prisma/client": "^6.18.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@t3-oss/env-nextjs": "^0.13.8",
|
||||
@@ -43,7 +43,7 @@
|
||||
"cron-validator": "^1.2.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^0.546.0",
|
||||
"lucide-react": "^0.548.0",
|
||||
"next": "^15.5.6",
|
||||
"node-cron": "^3.0.3",
|
||||
"node-pty": "^1.0.0",
|
||||
@@ -62,7 +62,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@tailwindcss/postcss": "^4.1.15",
|
||||
"@tailwindcss/postcss": "^4.1.16",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@@ -73,7 +73,7 @@
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"eslint": "^9.38.0",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"documentation": "https://docs.bunkerweb.io/latest/",
|
||||
"website": "https://www.bunkerweb.io/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/bunkerweb.webp",
|
||||
"config_path": "/opt/bunkerweb/variables.env",
|
||||
"config_path": "/etc/bunkerweb/variables.env",
|
||||
"description": "BunkerWeb is a security-focused web server that enhances web application protection. It guards against common web vulnerabilities like SQL injection, XSS, and CSRF. It features simple setup and configuration using a YAML file, customizable security rules, and provides detailed logs for traffic monitoring and threat detection.",
|
||||
"install_methods": [
|
||||
{
|
||||
|
||||
48
scripts/json/execute.json
Normal file
48
scripts/json/execute.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "PVE LXC Execute Command",
|
||||
"slug": "lxc-execute",
|
||||
"categories": [
|
||||
1
|
||||
],
|
||||
"date_created": "2025-09-18",
|
||||
"type": "pve",
|
||||
"updateable": false,
|
||||
"privileged": false,
|
||||
"interface_port": null,
|
||||
"documentation": null,
|
||||
"website": null,
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/proxmox.webp",
|
||||
"config_path": "",
|
||||
"description": "This script allows administrators to execute a custom command inside one or multiple LXC containers on a Proxmox VE node. Containers can be selectively excluded via an interactive checklist. If a container is stopped, the script will automatically start it, run the command, and then shut it down again. Only Debian and Ubuntu based containers are supported.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "tools/pve/execute.sh",
|
||||
"resources": {
|
||||
"cpu": null,
|
||||
"ram": null,
|
||||
"hdd": null,
|
||||
"os": null,
|
||||
"version": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Execute within the Proxmox shell.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Non-Debian/Ubuntu containers will be skipped automatically.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Stopped containers will be started temporarily to run the command, then shut down again.",
|
||||
"type": "warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
"ram": 2048,
|
||||
"hdd": 10,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"documentation": "https://github.com/HydroshieldMKII/Guardian/blob/main/README.md",
|
||||
"config_path": "/opt/guardian/.env",
|
||||
"website": "https://github.com/HydroshieldMKII/Guardian",
|
||||
"logo": null,
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/guardian-plex.webp",
|
||||
"description": "Guardian is a lightweight companion app for Plex that lets you monitor, approve or block devices in real time. It helps you enforce per-user or global policies, stop unwanted sessions automatically and grant temporary access - all through a simple web interface.",
|
||||
"install_methods": [
|
||||
{
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 8,
|
||||
"hdd": 16,
|
||||
"os": "ubuntu",
|
||||
"version": "24.04"
|
||||
}
|
||||
|
||||
40
scripts/json/jotty.json
Normal file
40
scripts/json/jotty.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "jotty",
|
||||
"slug": "jotty",
|
||||
"categories": [
|
||||
12
|
||||
],
|
||||
"date_created": "2025-10-21",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 3000,
|
||||
"documentation": "https://github.com/fccview/jotty/blob/main/README.md",
|
||||
"website": "https://github.com/fccview/jotty",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/jotty.webp",
|
||||
"config_path": "/opt/jotty/.env",
|
||||
"description": "A simple, self-hosted app for your checklists and notes. Tired of bloated, cloud-based to-do apps? jotty is a lightweight alternative for managing your personal checklists and notes. It's built with Next.js 14, is easy to deploy, and keeps all your data on your own server.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/jotty.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 3072,
|
||||
"hdd": 6,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "jotty was previously named rwMarkable",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
"ram": 2048,
|
||||
"hdd": 8,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,186 +1,186 @@
|
||||
{
|
||||
"categories": [
|
||||
{
|
||||
"name": "Proxmox & Virtualization",
|
||||
"id": 1,
|
||||
"sort_order": 1.0,
|
||||
"description": "Tools and scripts to manage Proxmox VE and virtualization platforms effectively.",
|
||||
"icon": "server"
|
||||
},
|
||||
{
|
||||
"name": "Operating Systems",
|
||||
"id": 2,
|
||||
"sort_order": 2.0,
|
||||
"description": "Scripts for deploying and managing various operating systems.",
|
||||
"icon": "monitor"
|
||||
},
|
||||
{
|
||||
"name": "Containers & Docker",
|
||||
"id": 3,
|
||||
"sort_order": 3.0,
|
||||
"description": "Solutions for containerization using Docker and related technologies.",
|
||||
"icon": "box"
|
||||
},
|
||||
{
|
||||
"name": "Network & Firewall",
|
||||
"id": 4,
|
||||
"sort_order": 4.0,
|
||||
"description": "Enhance network security and configure firewalls with ease.",
|
||||
"icon": "shield"
|
||||
},
|
||||
{
|
||||
"name": "Adblock & DNS",
|
||||
"id": 5,
|
||||
"sort_order": 5.0,
|
||||
"description": "Optimize your network with DNS and ad-blocking solutions.",
|
||||
"icon": "ban"
|
||||
},
|
||||
{
|
||||
"name": "Authentication & Security",
|
||||
"id": 6,
|
||||
"sort_order": 6.0,
|
||||
"description": "Secure your infrastructure with authentication and security tools.",
|
||||
"icon": "lock"
|
||||
},
|
||||
{
|
||||
"name": "Backup & Recovery",
|
||||
"id": 7,
|
||||
"sort_order": 7.0,
|
||||
"description": "Reliable backup and recovery scripts to protect your data.",
|
||||
"icon": "archive"
|
||||
},
|
||||
{
|
||||
"name": "Databases",
|
||||
"id": 8,
|
||||
"sort_order": 8.0,
|
||||
"description": "Deploy and manage robust database systems with ease.",
|
||||
"icon": "database"
|
||||
},
|
||||
{
|
||||
"name": "Monitoring & Analytics",
|
||||
"id": 9,
|
||||
"sort_order": 9.0,
|
||||
"description": "Monitor system performance and analyze data seamlessly.",
|
||||
"icon": "bar-chart"
|
||||
},
|
||||
{
|
||||
"name": "Dashboards & Frontends",
|
||||
"id": 10,
|
||||
"sort_order": 10.0,
|
||||
"description": "Create interactive dashboards and user-friendly frontends.",
|
||||
"icon": "layout"
|
||||
},
|
||||
{
|
||||
"name": "Files & Downloads",
|
||||
"id": 11,
|
||||
"sort_order": 11.0,
|
||||
"description": "Manage file sharing and downloading solutions efficiently.",
|
||||
"icon": "download"
|
||||
},
|
||||
{
|
||||
"name": "Documents & Notes",
|
||||
"id": 12,
|
||||
"sort_order": 12.0,
|
||||
"description": "Organize and manage documents and note-taking tools.",
|
||||
"icon": "file-text"
|
||||
},
|
||||
{
|
||||
"name": "Media & Streaming",
|
||||
"id": 13,
|
||||
"sort_order": 13.0,
|
||||
"description": "Stream and manage media effortlessly across devices.",
|
||||
"icon": "play"
|
||||
},
|
||||
{
|
||||
"name": "*Arr Suite",
|
||||
"id": 14,
|
||||
"sort_order": 14.0,
|
||||
"description": "Automated media management with the popular *Arr suite tools.",
|
||||
"icon": "tv"
|
||||
},
|
||||
{
|
||||
"name": "NVR & Cameras",
|
||||
"id": 15,
|
||||
"sort_order": 15.0,
|
||||
"description": "Manage network video recorders and camera setups.",
|
||||
"icon": "camera"
|
||||
},
|
||||
{
|
||||
"name": "IoT & Smart Home",
|
||||
"id": 16,
|
||||
"sort_order": 16.0,
|
||||
"description": "Control and automate IoT devices and smart home systems.",
|
||||
"icon": "home"
|
||||
},
|
||||
{
|
||||
"name": "ZigBee, Z-Wave & Matter",
|
||||
"id": 17,
|
||||
"sort_order": 17.0,
|
||||
"description": "Solutions for ZigBee, Z-Wave, and Matter-based device management.",
|
||||
"icon": "radio"
|
||||
},
|
||||
{
|
||||
"name": "MQTT & Messaging",
|
||||
"id": 18,
|
||||
"sort_order": 18.0,
|
||||
"description": "Set up reliable messaging and MQTT-based communication systems.",
|
||||
"icon": "message-circle"
|
||||
},
|
||||
{
|
||||
"name": "Automation & Scheduling",
|
||||
"id": 19,
|
||||
"sort_order": 19.0,
|
||||
"description": "Automate tasks and manage scheduling with powerful tools.",
|
||||
"icon": "clock"
|
||||
},
|
||||
{
|
||||
"name": "AI / Coding & Dev-Tools",
|
||||
"id": 20,
|
||||
"sort_order": 20.0,
|
||||
"description": "Leverage AI and developer tools for smarter coding workflows.",
|
||||
"icon": "code"
|
||||
},
|
||||
{
|
||||
"name": "Webservers & Proxies",
|
||||
"id": 21,
|
||||
"sort_order": 21.0,
|
||||
"description": "Deploy and configure web servers and proxy solutions.",
|
||||
"icon": "globe"
|
||||
},
|
||||
{
|
||||
"name": "Bots & ChatOps",
|
||||
"id": 22,
|
||||
"sort_order": 22.0,
|
||||
"description": "Enhance collaboration with bots and ChatOps integrations.",
|
||||
"icon": "bot"
|
||||
},
|
||||
{
|
||||
"name": "Finance & Budgeting",
|
||||
"id": 23,
|
||||
"sort_order": 23.0,
|
||||
"description": "Track expenses and manage budgets efficiently.",
|
||||
"icon": "dollar-sign"
|
||||
},
|
||||
{
|
||||
"name": "Gaming & Leisure",
|
||||
"id": 24,
|
||||
"sort_order": 24.0,
|
||||
"description": "Scripts for gaming servers and leisure-related tools.",
|
||||
"icon": "gamepad-2"
|
||||
},
|
||||
{
|
||||
"name": "Business & ERP",
|
||||
"id": 25,
|
||||
"sort_order": 25.0,
|
||||
"description": "Streamline business operations with ERP and management tools.",
|
||||
"icon": "building"
|
||||
},
|
||||
{
|
||||
"name": "Miscellaneous",
|
||||
"id": 0,
|
||||
"sort_order": 99.0,
|
||||
"description": "General scripts and tools that don't fit into other categories.",
|
||||
"icon": "more-horizontal"
|
||||
}
|
||||
]
|
||||
}
|
||||
"categories": [
|
||||
{
|
||||
"name": "Proxmox & Virtualization",
|
||||
"id": 1,
|
||||
"sort_order": 1.0,
|
||||
"description": "Tools and scripts to manage Proxmox VE and virtualization platforms effectively.",
|
||||
"icon": "server"
|
||||
},
|
||||
{
|
||||
"name": "Operating Systems",
|
||||
"id": 2,
|
||||
"sort_order": 2.0,
|
||||
"description": "Scripts for deploying and managing various operating systems.",
|
||||
"icon": "monitor"
|
||||
},
|
||||
{
|
||||
"name": "Containers & Docker",
|
||||
"id": 3,
|
||||
"sort_order": 3.0,
|
||||
"description": "Solutions for containerization using Docker and related technologies.",
|
||||
"icon": "box"
|
||||
},
|
||||
{
|
||||
"name": "Network & Firewall",
|
||||
"id": 4,
|
||||
"sort_order": 4.0,
|
||||
"description": "Enhance network security and configure firewalls with ease.",
|
||||
"icon": "shield"
|
||||
},
|
||||
{
|
||||
"name": "Adblock & DNS",
|
||||
"id": 5,
|
||||
"sort_order": 5.0,
|
||||
"description": "Optimize your network with DNS and ad-blocking solutions.",
|
||||
"icon": "ban"
|
||||
},
|
||||
{
|
||||
"name": "Authentication & Security",
|
||||
"id": 6,
|
||||
"sort_order": 6.0,
|
||||
"description": "Secure your infrastructure with authentication and security tools.",
|
||||
"icon": "lock"
|
||||
},
|
||||
{
|
||||
"name": "Backup & Recovery",
|
||||
"id": 7,
|
||||
"sort_order": 7.0,
|
||||
"description": "Reliable backup and recovery scripts to protect your data.",
|
||||
"icon": "archive"
|
||||
},
|
||||
{
|
||||
"name": "Databases",
|
||||
"id": 8,
|
||||
"sort_order": 8.0,
|
||||
"description": "Deploy and manage robust database systems with ease.",
|
||||
"icon": "database"
|
||||
},
|
||||
{
|
||||
"name": "Monitoring & Analytics",
|
||||
"id": 9,
|
||||
"sort_order": 9.0,
|
||||
"description": "Monitor system performance and analyze data seamlessly.",
|
||||
"icon": "bar-chart"
|
||||
},
|
||||
{
|
||||
"name": "Dashboards & Frontends",
|
||||
"id": 10,
|
||||
"sort_order": 10.0,
|
||||
"description": "Create interactive dashboards and user-friendly frontends.",
|
||||
"icon": "layout"
|
||||
},
|
||||
{
|
||||
"name": "Files & Downloads",
|
||||
"id": 11,
|
||||
"sort_order": 11.0,
|
||||
"description": "Manage file sharing and downloading solutions efficiently.",
|
||||
"icon": "download"
|
||||
},
|
||||
{
|
||||
"name": "Documents & Notes",
|
||||
"id": 12,
|
||||
"sort_order": 12.0,
|
||||
"description": "Organize and manage documents and note-taking tools.",
|
||||
"icon": "file-text"
|
||||
},
|
||||
{
|
||||
"name": "Media & Streaming",
|
||||
"id": 13,
|
||||
"sort_order": 13.0,
|
||||
"description": "Stream and manage media effortlessly across devices.",
|
||||
"icon": "play"
|
||||
},
|
||||
{
|
||||
"name": "*Arr Suite",
|
||||
"id": 14,
|
||||
"sort_order": 14.0,
|
||||
"description": "Automated media management with the popular *Arr suite tools.",
|
||||
"icon": "tv"
|
||||
},
|
||||
{
|
||||
"name": "NVR & Cameras",
|
||||
"id": 15,
|
||||
"sort_order": 15.0,
|
||||
"description": "Manage network video recorders and camera setups.",
|
||||
"icon": "camera"
|
||||
},
|
||||
{
|
||||
"name": "IoT & Smart Home",
|
||||
"id": 16,
|
||||
"sort_order": 16.0,
|
||||
"description": "Control and automate IoT devices and smart home systems.",
|
||||
"icon": "home"
|
||||
},
|
||||
{
|
||||
"name": "ZigBee, Z-Wave & Matter",
|
||||
"id": 17,
|
||||
"sort_order": 17.0,
|
||||
"description": "Solutions for ZigBee, Z-Wave, and Matter-based device management.",
|
||||
"icon": "radio"
|
||||
},
|
||||
{
|
||||
"name": "MQTT & Messaging",
|
||||
"id": 18,
|
||||
"sort_order": 18.0,
|
||||
"description": "Set up reliable messaging and MQTT-based communication systems.",
|
||||
"icon": "message-circle"
|
||||
},
|
||||
{
|
||||
"name": "Automation & Scheduling",
|
||||
"id": 19,
|
||||
"sort_order": 19.0,
|
||||
"description": "Automate tasks and manage scheduling with powerful tools.",
|
||||
"icon": "clock"
|
||||
},
|
||||
{
|
||||
"name": "AI / Coding & Dev-Tools",
|
||||
"id": 20,
|
||||
"sort_order": 20.0,
|
||||
"description": "Leverage AI and developer tools for smarter coding workflows.",
|
||||
"icon": "code"
|
||||
},
|
||||
{
|
||||
"name": "Webservers & Proxies",
|
||||
"id": 21,
|
||||
"sort_order": 21.0,
|
||||
"description": "Deploy and configure web servers and proxy solutions.",
|
||||
"icon": "globe"
|
||||
},
|
||||
{
|
||||
"name": "Bots & ChatOps",
|
||||
"id": 22,
|
||||
"sort_order": 22.0,
|
||||
"description": "Enhance collaboration with bots and ChatOps integrations.",
|
||||
"icon": "bot"
|
||||
},
|
||||
{
|
||||
"name": "Finance & Budgeting",
|
||||
"id": 23,
|
||||
"sort_order": 23.0,
|
||||
"description": "Track expenses and manage budgets efficiently.",
|
||||
"icon": "dollar-sign"
|
||||
},
|
||||
{
|
||||
"name": "Gaming & Leisure",
|
||||
"id": 24,
|
||||
"sort_order": 24.0,
|
||||
"description": "Scripts for gaming servers and leisure-related tools.",
|
||||
"icon": "gamepad-2"
|
||||
},
|
||||
{
|
||||
"name": "Business & ERP",
|
||||
"id": 25,
|
||||
"sort_order": 25.0,
|
||||
"description": "Streamline business operations with ERP and management tools.",
|
||||
"icon": "building"
|
||||
},
|
||||
{
|
||||
"name": "Miscellaneous",
|
||||
"id": 0,
|
||||
"sort_order": 99.0,
|
||||
"description": "General scripts and tools that don't fit into other categories.",
|
||||
"icon": "more-horizontal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"ram": 1024,
|
||||
"hdd": 4,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"ram": 2048,
|
||||
"hdd": 8,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
"version": "12"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"ram": 2048,
|
||||
"hdd": 6,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"ram": 1024,
|
||||
"hdd": 4,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
40
scripts/json/open-archiver.json
Normal file
40
scripts/json/open-archiver.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "Open-Archiver",
|
||||
"slug": "open-archiver",
|
||||
"categories": [
|
||||
7
|
||||
],
|
||||
"date_created": "2025-10-18",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 3000,
|
||||
"documentation": "https://docs.openarchiver.com/",
|
||||
"config_path": "/opt/openarchiver/.env",
|
||||
"website": "https://openarchiver.com/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/open-archiver.webp",
|
||||
"description": "Open Archiver is a secure, self-hosted email archiving solution, and it's completely open source. Get an email archiver that enables full-text search across email and attachments. Create a permanent, searchable, and compliant mail archive from Google Workspace, Microsoft 35, and any IMAP server.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/open-archiver.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 3072,
|
||||
"hdd": 8,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Data directory is: `/opt/openarchiver-data`. If you have a lot of email, you might consider mounting external storage to this directory.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
"ram": 8192,
|
||||
"hdd": 25,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
"type": "default",
|
||||
"script": "ct/paperless-ai.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"cpu": 4,
|
||||
"ram": 4096,
|
||||
"hdd": 20,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"ram": 1024,
|
||||
"hdd": 4,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"ram": 512,
|
||||
"hdd": 2,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@
|
||||
"ram": 2048,
|
||||
"hdd": 5,
|
||||
"os": "Debian",
|
||||
"version": "12"
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -356,7 +356,7 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr
|
||||
}
|
||||
}, [selectedCategory]);
|
||||
|
||||
const handleCardClick = (scriptCard: { slug: string }) => {
|
||||
const handleCardClick = (scriptCard: ScriptCardType) => {
|
||||
// All scripts are GitHub scripts, open modal
|
||||
setSelectedSlug(scriptCard.slug);
|
||||
setIsModalOpen(true);
|
||||
|
||||
@@ -46,6 +46,8 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
|
||||
const [appriseUrls, setAppriseUrls] = useState<string[]>([]);
|
||||
const [appriseUrlsText, setAppriseUrlsText] = useState('');
|
||||
const [lastAutoSync, setLastAutoSync] = useState('');
|
||||
const [lastAutoSyncError, setLastAutoSyncError] = useState<string | null>(null);
|
||||
const [lastAutoSyncErrorTime, setLastAutoSyncErrorTime] = useState<string | null>(null);
|
||||
const [cronValidationError, setCronValidationError] = useState('');
|
||||
|
||||
// Load existing settings when modal opens
|
||||
@@ -311,6 +313,8 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
|
||||
setAppriseUrls(settings.appriseUrls ?? []);
|
||||
setAppriseUrlsText((settings.appriseUrls ?? []).join('\n'));
|
||||
setLastAutoSync(settings.lastAutoSync ?? '');
|
||||
setLastAutoSyncError(settings.lastAutoSyncError ?? null);
|
||||
setLastAutoSyncErrorTime(settings.lastAutoSyncErrorTime ?? null);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -323,30 +327,6 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
|
||||
setMessage(null);
|
||||
|
||||
try {
|
||||
// Validate cron expression if custom
|
||||
if (syncIntervalType === 'custom' && syncIntervalCron) {
|
||||
const response = await fetch('/api/settings/auto-sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
autoSyncEnabled,
|
||||
syncIntervalType,
|
||||
syncIntervalPredefined,
|
||||
syncIntervalCron,
|
||||
autoDownloadNew,
|
||||
autoUpdateExisting,
|
||||
notificationEnabled,
|
||||
appriseUrls: appriseUrls
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
setMessage({ type: 'error', text: errorData.error ?? 'Failed to save auto-sync settings' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch('/api/settings/auto-sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -361,7 +341,7 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
|
||||
appriseUrls: appriseUrls
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
setMessage({ type: 'success', text: 'Auto-sync settings saved successfully!' });
|
||||
setTimeout(() => setMessage(null), 3000);
|
||||
@@ -824,7 +804,41 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
|
||||
</div>
|
||||
<Toggle
|
||||
checked={autoSyncEnabled}
|
||||
onCheckedChange={setAutoSyncEnabled}
|
||||
onCheckedChange={async (checked) => {
|
||||
setAutoSyncEnabled(checked);
|
||||
|
||||
// Auto-save when toggle changes
|
||||
try {
|
||||
// If syncIntervalType is custom but no cron expression, fallback to predefined
|
||||
const effectiveSyncIntervalType = (syncIntervalType === 'custom' && !syncIntervalCron)
|
||||
? 'predefined'
|
||||
: syncIntervalType;
|
||||
|
||||
const response = await fetch('/api/settings/auto-sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
autoSyncEnabled: checked,
|
||||
syncIntervalType: effectiveSyncIntervalType,
|
||||
syncIntervalPredefined: effectiveSyncIntervalType === 'predefined' ? syncIntervalPredefined : undefined,
|
||||
syncIntervalCron: effectiveSyncIntervalType === 'custom' ? syncIntervalCron : undefined,
|
||||
autoDownloadNew,
|
||||
autoUpdateExisting,
|
||||
notificationEnabled,
|
||||
appriseUrls: appriseUrls
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Update local state to reflect the effective sync interval type
|
||||
if (effectiveSyncIntervalType !== syncIntervalType) {
|
||||
setSyncIntervalType(effectiveSyncIntervalType);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving auto-sync toggle:', error);
|
||||
}
|
||||
}}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</div>
|
||||
@@ -1016,6 +1030,25 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
|
||||
</div>
|
||||
)}
|
||||
|
||||
{lastAutoSyncError && (
|
||||
<div className="p-3 bg-error/10 text-error-foreground border border-error/20 rounded-md">
|
||||
<div className="flex items-start gap-2">
|
||||
<svg className="w-4 h-4 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Last sync error:</p>
|
||||
<p className="text-sm mt-1">{lastAutoSyncError}</p>
|
||||
{lastAutoSyncErrorTime && (
|
||||
<p className="text-xs mt-1 opacity-75">
|
||||
{new Date(lastAutoSyncErrorTime).toLocaleString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={triggerManualSync}
|
||||
|
||||
@@ -535,7 +535,30 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
};
|
||||
|
||||
const handleDownloadAllFiltered = () => {
|
||||
const slugsToDownload = filteredScripts.map(script => script.slug).filter(Boolean);
|
||||
|
||||
let scriptsToDownload: ScriptCardType[] = filteredScripts;
|
||||
|
||||
if (!hasActiveFilters) {
|
||||
|
||||
const scriptMap = new Map<string, ScriptCardType>();
|
||||
|
||||
filteredScripts.forEach(script => {
|
||||
if (script?.slug) {
|
||||
scriptMap.set(script.slug, script);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
newestScripts.forEach(script => {
|
||||
if (script?.slug && !scriptMap.has(script.slug)) {
|
||||
scriptMap.set(script.slug, script);
|
||||
}
|
||||
});
|
||||
|
||||
scriptsToDownload = Array.from(scriptMap.values());
|
||||
}
|
||||
|
||||
const slugsToDownload = scriptsToDownload.map(script => script.slug).filter(Boolean);
|
||||
if (slugsToDownload.length > 0) {
|
||||
void downloadScriptsIndividually(slugsToDownload);
|
||||
}
|
||||
@@ -574,7 +597,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
}, []);
|
||||
|
||||
|
||||
const handleCardClick = (scriptCard: { slug: string }) => {
|
||||
const handleCardClick = (scriptCard: ScriptCardType) => {
|
||||
// All scripts are GitHub scripts, open modal
|
||||
setSelectedSlug(scriptCard.slug);
|
||||
setIsModalOpen(true);
|
||||
|
||||
@@ -64,14 +64,12 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Validate custom cron expression
|
||||
if (settings.syncIntervalType === 'custom') {
|
||||
if (!settings.syncIntervalCron || typeof settings.syncIntervalCron !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Custom cron expression is required when syncIntervalType is "custom"' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!isValidCron(settings.syncIntervalCron, { seconds: false })) {
|
||||
if (!settings.syncIntervalCron || typeof settings.syncIntervalCron !== 'string' || settings.syncIntervalCron.trim() === '') {
|
||||
// Fallback to predefined if custom is selected but no cron expression
|
||||
settings.syncIntervalType = 'predefined';
|
||||
settings.syncIntervalPredefined = settings.syncIntervalPredefined || '1hour';
|
||||
settings.syncIntervalCron = '';
|
||||
} else if (!isValidCron(settings.syncIntervalCron, { seconds: false })) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid cron expression' },
|
||||
{ status: 400 }
|
||||
@@ -138,7 +136,9 @@ export async function POST(request: NextRequest) {
|
||||
'AUTO_UPDATE_EXISTING': settings.autoUpdateExisting ? 'true' : 'false',
|
||||
'NOTIFICATION_ENABLED': settings.notificationEnabled ? 'true' : 'false',
|
||||
'APPRISE_URLS': Array.isArray(settings.appriseUrls) ? JSON.stringify(settings.appriseUrls) : (settings.appriseUrls || '[]'),
|
||||
'LAST_AUTO_SYNC': settings.lastAutoSync || ''
|
||||
'LAST_AUTO_SYNC': settings.lastAutoSync || '',
|
||||
'LAST_AUTO_SYNC_ERROR': settings.lastAutoSyncError || '',
|
||||
'LAST_AUTO_SYNC_ERROR_TIME': settings.lastAutoSyncErrorTime || ''
|
||||
};
|
||||
|
||||
// Update or add each setting
|
||||
@@ -160,15 +160,28 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Reschedule auto-sync service with new settings
|
||||
try {
|
||||
const { AutoSyncService } = await import('../../../../server/services/autoSyncService.js');
|
||||
const autoSyncService = new AutoSyncService();
|
||||
const { getAutoSyncService, setAutoSyncService } = await import('../../../../server/lib/autoSyncInit.js');
|
||||
let autoSyncService = getAutoSyncService();
|
||||
|
||||
// If no global instance exists, create one
|
||||
if (!autoSyncService) {
|
||||
const { AutoSyncService } = await import('../../../../server/services/autoSyncService.js');
|
||||
autoSyncService = new AutoSyncService();
|
||||
setAutoSyncService(autoSyncService);
|
||||
}
|
||||
|
||||
// Update the global service instance with new settings
|
||||
autoSyncService.saveSettings(settings);
|
||||
|
||||
if (settings.autoSyncEnabled) {
|
||||
autoSyncService.scheduleAutoSync();
|
||||
console.log('Auto-sync rescheduled with new settings');
|
||||
} else {
|
||||
autoSyncService.stopAutoSync();
|
||||
console.log('Auto-sync stopped');
|
||||
// Ensure the service is completely stopped and won't restart
|
||||
autoSyncService.isRunning = false;
|
||||
// Also stop the global service instance if it exists
|
||||
const { stopAutoSync: stopGlobalAutoSync } = await import('../../../../server/lib/autoSyncInit.js');
|
||||
stopGlobalAutoSync();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error rescheduling auto-sync service:', error);
|
||||
@@ -204,7 +217,9 @@ export async function GET() {
|
||||
autoUpdateExisting: false,
|
||||
notificationEnabled: false,
|
||||
appriseUrls: [],
|
||||
lastAutoSync: ''
|
||||
lastAutoSync: '',
|
||||
lastAutoSyncError: null,
|
||||
lastAutoSyncErrorTime: null
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -228,7 +243,9 @@ export async function GET() {
|
||||
return [];
|
||||
}
|
||||
})(),
|
||||
lastAutoSync: getEnvValue(envContent, 'LAST_AUTO_SYNC') || ''
|
||||
lastAutoSync: getEnvValue(envContent, 'LAST_AUTO_SYNC') || '',
|
||||
lastAutoSyncError: getEnvValue(envContent, 'LAST_AUTO_SYNC_ERROR') || null,
|
||||
lastAutoSyncErrorTime: getEnvValue(envContent, 'LAST_AUTO_SYNC_ERROR_TIME') || null
|
||||
};
|
||||
|
||||
return NextResponse.json({ settings });
|
||||
|
||||
@@ -115,6 +115,18 @@ export const scriptsRouter = createTRPCRouter({
|
||||
.input(z.object({ slug: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
try {
|
||||
console.log('getScriptBySlug called with slug:', input.slug);
|
||||
console.log('githubJsonService methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(githubJsonService)));
|
||||
console.log('githubJsonService.getScriptBySlug type:', typeof githubJsonService.getScriptBySlug);
|
||||
|
||||
if (typeof githubJsonService.getScriptBySlug !== 'function') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'getScriptBySlug method is not available on githubJsonService',
|
||||
script: null
|
||||
};
|
||||
}
|
||||
|
||||
const script = await githubJsonService.getScriptBySlug(input.slug);
|
||||
if (!script) {
|
||||
return {
|
||||
@@ -125,6 +137,7 @@ export const scriptsRouter = createTRPCRouter({
|
||||
}
|
||||
return { success: true, script };
|
||||
} catch (error) {
|
||||
console.error('Error in getScriptBySlug:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch script',
|
||||
@@ -490,14 +503,29 @@ export const scriptsRouter = createTRPCRouter({
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const autoSyncService = new AutoSyncService();
|
||||
// Use the global auto-sync service instance
|
||||
const { getAutoSyncService, setAutoSyncService } = await import('~/server/lib/autoSyncInit');
|
||||
let autoSyncService = getAutoSyncService();
|
||||
|
||||
// If no global instance exists, create one
|
||||
if (!autoSyncService) {
|
||||
const { AutoSyncService } = await import('~/server/services/autoSyncService');
|
||||
autoSyncService = new AutoSyncService();
|
||||
setAutoSyncService(autoSyncService);
|
||||
}
|
||||
|
||||
// Save settings to both .env file and service instance
|
||||
autoSyncService.saveSettings(input);
|
||||
|
||||
// Reschedule auto-sync if enabled
|
||||
if (input.autoSyncEnabled) {
|
||||
autoSyncService.scheduleAutoSync();
|
||||
console.log('Auto-sync rescheduled with new settings');
|
||||
} else {
|
||||
autoSyncService.stopAutoSync();
|
||||
// Ensure the service is completely stopped and won't restart
|
||||
autoSyncService.isRunning = false;
|
||||
console.log('Auto-sync stopped');
|
||||
}
|
||||
|
||||
return { success: true, message: 'Auto-sync settings saved successfully' };
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
import { AutoSyncService } from '../services/autoSyncService.js';
|
||||
|
||||
let autoSyncService = null;
|
||||
let isInitialized = false;
|
||||
|
||||
/**
|
||||
* Initialize auto-sync service and schedule cron job if enabled
|
||||
*/
|
||||
export function initializeAutoSync() {
|
||||
if (isInitialized) {
|
||||
console.log('Auto-sync service already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Initializing auto-sync service...');
|
||||
autoSyncService = new AutoSyncService();
|
||||
isInitialized = true;
|
||||
console.log('AutoSyncService instance created');
|
||||
|
||||
// Load settings and schedule if enabled
|
||||
const settings = autoSyncService.loadSettings();
|
||||
console.log('Settings loaded:', settings);
|
||||
|
||||
if (settings.autoSyncEnabled) {
|
||||
console.log('Auto-sync is enabled, scheduling cron job...');
|
||||
autoSyncService.scheduleAutoSync();
|
||||
console.log('Cron job scheduled');
|
||||
} else {
|
||||
console.log('Auto-sync is disabled');
|
||||
}
|
||||
@@ -23,6 +33,7 @@ export function initializeAutoSync() {
|
||||
console.log('Auto-sync service initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize auto-sync service:', error);
|
||||
console.error('Error stack:', error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +46,7 @@ export function stopAutoSync() {
|
||||
console.log('Stopping auto-sync service...');
|
||||
autoSyncService.stopAutoSync();
|
||||
autoSyncService = null;
|
||||
isInitialized = false;
|
||||
console.log('Auto-sync service stopped');
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -49,6 +61,13 @@ export function getAutoSyncService() {
|
||||
return autoSyncService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the auto-sync service instance (for external management)
|
||||
*/
|
||||
export function setAutoSyncService(service) {
|
||||
autoSyncService = service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Graceful shutdown handler
|
||||
*/
|
||||
|
||||
@@ -49,6 +49,13 @@ export function getAutoSyncService(): AutoSyncService | null {
|
||||
return autoSyncService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the auto-sync service instance (for external management)
|
||||
*/
|
||||
export function setAutoSyncService(service: AutoSyncService | null): void {
|
||||
autoSyncService = service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Graceful shutdown handler
|
||||
*/
|
||||
|
||||
@@ -25,6 +25,25 @@ export class ScriptManager {
|
||||
// Initialize lazily to avoid accessing env vars during module load
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely handle file modification time, providing fallback for invalid dates
|
||||
* @param mtime - The file modification time from fs.stat
|
||||
* @returns Date - Valid date or current date as fallback
|
||||
*/
|
||||
private safeMtime(mtime: Date): Date {
|
||||
try {
|
||||
// Check if the date is valid
|
||||
if (!mtime || isNaN(mtime.getTime())) {
|
||||
console.warn('Invalid mtime detected, using current time as fallback');
|
||||
return new Date();
|
||||
}
|
||||
return mtime;
|
||||
} catch (error) {
|
||||
console.warn('Error processing mtime:', error);
|
||||
return new Date();
|
||||
}
|
||||
}
|
||||
|
||||
private initializeConfig() {
|
||||
if (this.scriptsDir === null) {
|
||||
// Handle both absolute and relative paths for testing
|
||||
@@ -63,7 +82,7 @@ export class ScriptManager {
|
||||
path: filePath,
|
||||
extension,
|
||||
size: stats.size,
|
||||
lastModified: stats.mtime,
|
||||
lastModified: this.safeMtime(stats.mtime),
|
||||
executable
|
||||
});
|
||||
}
|
||||
@@ -125,7 +144,7 @@ export class ScriptManager {
|
||||
path: filePath,
|
||||
extension,
|
||||
size: stats.size,
|
||||
lastModified: stats.mtime,
|
||||
lastModified: this.safeMtime(stats.mtime),
|
||||
executable,
|
||||
logo,
|
||||
slug
|
||||
@@ -212,7 +231,7 @@ export class ScriptManager {
|
||||
path: filePath,
|
||||
extension,
|
||||
size: stats.size,
|
||||
lastModified: stats.mtime,
|
||||
lastModified: this.safeMtime(stats.mtime),
|
||||
executable,
|
||||
logo,
|
||||
slug
|
||||
|
||||
@@ -6,12 +6,34 @@ import { readFile, writeFile, readFileSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import cronValidator from 'cron-validator';
|
||||
|
||||
// Global lock to prevent multiple autosync instances from running simultaneously
|
||||
let globalAutoSyncLock = false;
|
||||
|
||||
export class AutoSyncService {
|
||||
constructor() {
|
||||
this.cronJob = null;
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely convert a date to ISO string, handling invalid dates
|
||||
* @param {Date} date - The date to convert
|
||||
* @returns {string} - ISO string or fallback timestamp
|
||||
*/
|
||||
safeToISOString(date) {
|
||||
try {
|
||||
// Check if the date is valid
|
||||
if (!date || isNaN(date.getTime())) {
|
||||
console.warn('Invalid date provided to safeToISOString, using current time as fallback');
|
||||
return new Date().toISOString();
|
||||
}
|
||||
return date.toISOString();
|
||||
} catch (error) {
|
||||
console.warn('Error converting date to ISO string:', error instanceof Error ? error.message : String(error));
|
||||
return new Date().toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load auto-sync settings from .env file
|
||||
*/
|
||||
@@ -20,6 +42,19 @@ export class AutoSyncService {
|
||||
const envPath = join(process.cwd(), '.env');
|
||||
const envContent = readFileSync(envPath, 'utf8');
|
||||
|
||||
/** @type {{
|
||||
* autoSyncEnabled: boolean;
|
||||
* syncIntervalType: string;
|
||||
* syncIntervalPredefined?: string;
|
||||
* syncIntervalCron?: string;
|
||||
* autoDownloadNew: boolean;
|
||||
* autoUpdateExisting: boolean;
|
||||
* notificationEnabled: boolean;
|
||||
* appriseUrls?: string[];
|
||||
* lastAutoSync?: string;
|
||||
* lastAutoSyncError?: string;
|
||||
* lastAutoSyncErrorTime?: string;
|
||||
* }} */
|
||||
const settings = {
|
||||
autoSyncEnabled: false,
|
||||
syncIntervalType: 'predefined',
|
||||
@@ -29,7 +64,9 @@ export class AutoSyncService {
|
||||
autoUpdateExisting: false,
|
||||
notificationEnabled: false,
|
||||
appriseUrls: [],
|
||||
lastAutoSync: ''
|
||||
lastAutoSync: '',
|
||||
lastAutoSyncError: '',
|
||||
lastAutoSyncErrorTime: ''
|
||||
};
|
||||
const lines = envContent.split('\n');
|
||||
|
||||
@@ -74,6 +111,12 @@ export class AutoSyncService {
|
||||
case 'LAST_AUTO_SYNC':
|
||||
settings.lastAutoSync = value;
|
||||
break;
|
||||
case 'LAST_AUTO_SYNC_ERROR':
|
||||
settings.lastAutoSyncError = value;
|
||||
break;
|
||||
case 'LAST_AUTO_SYNC_ERROR_TIME':
|
||||
settings.lastAutoSyncErrorTime = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,7 +133,9 @@ export class AutoSyncService {
|
||||
autoUpdateExisting: false,
|
||||
notificationEnabled: false,
|
||||
appriseUrls: [],
|
||||
lastAutoSync: ''
|
||||
lastAutoSync: '',
|
||||
lastAutoSyncError: '',
|
||||
lastAutoSyncErrorTime: ''
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -107,6 +152,8 @@ export class AutoSyncService {
|
||||
* @param {boolean} settings.notificationEnabled
|
||||
* @param {Array<string>} [settings.appriseUrls]
|
||||
* @param {string} [settings.lastAutoSync]
|
||||
* @param {string} [settings.lastAutoSyncError]
|
||||
* @param {string} [settings.lastAutoSyncErrorTime]
|
||||
*/
|
||||
saveSettings(settings) {
|
||||
try {
|
||||
@@ -130,19 +177,37 @@ export class AutoSyncService {
|
||||
'AUTO_UPDATE_EXISTING': settings.autoUpdateExisting.toString(),
|
||||
'NOTIFICATION_ENABLED': settings.notificationEnabled.toString(),
|
||||
'APPRISE_URLS': JSON.stringify(settings.appriseUrls || []),
|
||||
'LAST_AUTO_SYNC': settings.lastAutoSync || ''
|
||||
'LAST_AUTO_SYNC': settings.lastAutoSync || '',
|
||||
'LAST_AUTO_SYNC_ERROR': settings.lastAutoSyncError || '',
|
||||
'LAST_AUTO_SYNC_ERROR_TIME': settings.lastAutoSyncErrorTime || ''
|
||||
};
|
||||
|
||||
const existingKeys = new Set();
|
||||
|
||||
for (const line of lines) {
|
||||
const [key] = line.split('=');
|
||||
const trimmedKey = key?.trim();
|
||||
if (trimmedKey && trimmedKey in settingsMap) {
|
||||
// @ts-ignore - Dynamic key access is safe here
|
||||
newLines.push(`${trimmedKey}=${settingsMap[trimmedKey]}`);
|
||||
existingKeys.add(trimmedKey);
|
||||
} else if (trimmedKey && !(trimmedKey in settingsMap)) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
// Skip empty lines and comments
|
||||
if (!trimmedLine || trimmedLine.startsWith('#')) {
|
||||
newLines.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
const equalIndex = trimmedLine.indexOf('=');
|
||||
if (equalIndex === -1) {
|
||||
// Line doesn't contain '=', keep as is
|
||||
newLines.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = trimmedLine.substring(0, equalIndex).trim();
|
||||
if (key && key in settingsMap) {
|
||||
// Replace existing setting
|
||||
// @ts-ignore - Dynamic property access is safe here
|
||||
newLines.push(`${key}=${settingsMap[key]}`);
|
||||
existingKeys.add(key);
|
||||
} else {
|
||||
// Keep other settings as is
|
||||
newLines.push(line);
|
||||
}
|
||||
}
|
||||
@@ -170,6 +235,14 @@ export class AutoSyncService {
|
||||
|
||||
const settings = this.loadSettings();
|
||||
if (!settings.autoSyncEnabled) {
|
||||
console.log('Auto-sync is disabled, not scheduling cron job');
|
||||
this.isRunning = false; // Ensure we're completely stopped
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there's already a global autosync running
|
||||
if (globalAutoSyncLock) {
|
||||
console.log('Auto-sync is already running globally, not scheduling new cron job');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -200,8 +273,28 @@ export class AutoSyncService {
|
||||
console.log(`Scheduling auto-sync with cron expression: ${cronExpression}`);
|
||||
|
||||
this.cronJob = cron.schedule(cronExpression, async () => {
|
||||
// Check global lock first
|
||||
if (globalAutoSyncLock) {
|
||||
console.log('Auto-sync already running globally, skipping cron execution...');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isRunning) {
|
||||
console.log('Auto-sync already running, skipping...');
|
||||
console.log('Auto-sync already running locally, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
// Double-check that autosync is still enabled before executing
|
||||
const currentSettings = this.loadSettings();
|
||||
if (!currentSettings.autoSyncEnabled) {
|
||||
console.log('Auto-sync has been disabled, stopping and destroying cron job');
|
||||
this.stopAutoSync();
|
||||
return;
|
||||
}
|
||||
|
||||
// Additional check: if cronJob is null, it means it was stopped
|
||||
if (!this.cronJob) {
|
||||
console.log('Cron job was stopped, skipping execution');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -221,8 +314,13 @@ export class AutoSyncService {
|
||||
stopAutoSync() {
|
||||
if (this.cronJob) {
|
||||
this.cronJob.stop();
|
||||
this.cronJob.destroy();
|
||||
this.cronJob = null;
|
||||
console.log('Auto-sync cron job stopped');
|
||||
this.isRunning = false;
|
||||
console.log('Auto-sync cron job stopped and destroyed');
|
||||
} else {
|
||||
console.log('No active cron job to stop');
|
||||
this.isRunning = false; // Ensure isRunning is false even if no cron job
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,11 +328,19 @@ export class AutoSyncService {
|
||||
* Execute auto-sync process
|
||||
*/
|
||||
async executeAutoSync() {
|
||||
if (this.isRunning) {
|
||||
console.log('Auto-sync already running, skipping...');
|
||||
return { success: false, message: 'Auto-sync already running' };
|
||||
// Check global lock first
|
||||
if (globalAutoSyncLock) {
|
||||
console.log('Auto-sync already running globally, skipping...');
|
||||
return { success: false, message: 'Auto-sync already running globally' };
|
||||
}
|
||||
|
||||
if (this.isRunning) {
|
||||
console.log('Auto-sync already running locally, skipping...');
|
||||
return { success: false, message: 'Auto-sync already running locally' };
|
||||
}
|
||||
|
||||
// Set global lock
|
||||
globalAutoSyncLock = true;
|
||||
this.isRunning = true;
|
||||
const startTime = new Date();
|
||||
|
||||
@@ -251,56 +357,120 @@ export class AutoSyncService {
|
||||
|
||||
const results = {
|
||||
jsonSync: syncResult,
|
||||
newScripts: [],
|
||||
updatedScripts: [],
|
||||
errors: []
|
||||
newScripts: /** @type {any[]} */ ([]),
|
||||
updatedScripts: /** @type {any[]} */ ([]),
|
||||
errors: /** @type {string[]} */ ([])
|
||||
};
|
||||
|
||||
// Step 2: Auto-download/update scripts if enabled
|
||||
const settings = this.loadSettings();
|
||||
|
||||
if (settings.autoDownloadNew || settings.autoUpdateExisting) {
|
||||
console.log('Processing synced JSON files for script downloads...');
|
||||
|
||||
// Only process scripts for files that were actually synced
|
||||
// @ts-ignore - syncedFiles exists in the JavaScript version
|
||||
if (syncResult.syncedFiles && syncResult.syncedFiles.length > 0) {
|
||||
// @ts-ignore - syncedFiles exists in the JavaScript version
|
||||
console.log(`Processing ${syncResult.syncedFiles.length} synced JSON files for new scripts...`);
|
||||
console.log(`Processing ${syncResult.syncedFiles.length} synced JSON files for script downloads...`);
|
||||
|
||||
// Get all scripts from synced files
|
||||
// @ts-ignore - syncedFiles exists in the JavaScript version
|
||||
const allSyncedScripts = await githubJsonService.getScriptsForFiles(syncResult.syncedFiles);
|
||||
// Get scripts only for the synced files
|
||||
const localScriptsService = await import('./localScripts.js');
|
||||
const syncedScripts = [];
|
||||
|
||||
// Initialize script downloader service
|
||||
// @ts-ignore - initializeConfig is public in the JS version
|
||||
scriptDownloaderService.initializeConfig();
|
||||
|
||||
// Filter to only truly NEW scripts (not previously downloaded)
|
||||
const newScripts = [];
|
||||
for (const script of allSyncedScripts) {
|
||||
const isDownloaded = await scriptDownloaderService.isScriptDownloaded(script);
|
||||
if (!isDownloaded) {
|
||||
newScripts.push(script);
|
||||
for (const filename of syncResult.syncedFiles) {
|
||||
try {
|
||||
// Extract slug from filename (remove .json extension)
|
||||
const slug = filename.replace('.json', '');
|
||||
const script = await localScriptsService.localScriptsService.getScriptBySlug(slug);
|
||||
if (script) {
|
||||
syncedScripts.push(script);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Error loading script from ${filename}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Found ${newScripts.length} new scripts out of ${allSyncedScripts.length} total scripts`);
|
||||
console.log(`Found ${syncedScripts.length} scripts from synced JSON files`);
|
||||
|
||||
if (settings.autoDownloadNew && newScripts.length > 0) {
|
||||
console.log(`Auto-downloading ${newScripts.length} new scripts...`);
|
||||
const downloadResult = await scriptDownloaderService.autoDownloadNewScripts(newScripts);
|
||||
// @ts-ignore - Type assertion needed for dynamic assignment
|
||||
results.newScripts = downloadResult.downloaded;
|
||||
// @ts-ignore - Type assertion needed for dynamic assignment
|
||||
results.errors.push(...downloadResult.errors);
|
||||
// Filter to only truly NEW scripts (not previously downloaded)
|
||||
const newScripts = [];
|
||||
const existingScripts = [];
|
||||
|
||||
for (const script of syncedScripts) {
|
||||
try {
|
||||
// Validate script object
|
||||
if (!script || !script.slug) {
|
||||
console.warn('Invalid script object found, skipping:', script);
|
||||
continue;
|
||||
}
|
||||
|
||||
const isDownloaded = await scriptDownloaderService.isScriptDownloaded(script);
|
||||
if (!isDownloaded) {
|
||||
newScripts.push(script);
|
||||
} else {
|
||||
existingScripts.push(script);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Error checking script ${script?.slug || 'unknown'}:`, error);
|
||||
// Treat as new script if we can't check
|
||||
if (script && script.slug) {
|
||||
newScripts.push(script);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.autoUpdateExisting) {
|
||||
console.log('Auto-updating existing scripts from synced files...');
|
||||
const updateResult = await scriptDownloaderService.autoUpdateExistingScripts(allSyncedScripts);
|
||||
// @ts-ignore - Type assertion needed for dynamic assignment
|
||||
results.updatedScripts = updateResult.updated;
|
||||
// @ts-ignore - Type assertion needed for dynamic assignment
|
||||
results.errors.push(...updateResult.errors);
|
||||
console.log(`Found ${newScripts.length} new scripts and ${existingScripts.length} existing scripts from synced files`);
|
||||
|
||||
// Download new scripts
|
||||
if (settings.autoDownloadNew && newScripts.length > 0) {
|
||||
console.log(`Auto-downloading ${newScripts.length} new scripts...`);
|
||||
const downloaded = [];
|
||||
const errors = [];
|
||||
|
||||
for (const script of newScripts) {
|
||||
try {
|
||||
const result = await scriptDownloaderService.loadScript(script);
|
||||
if (result.success) {
|
||||
downloaded.push(script); // Store full script object for category grouping
|
||||
console.log(`Downloaded script: ${script.name || script.slug}`);
|
||||
} else {
|
||||
errors.push(`${script.name || script.slug}: ${result.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
errors.push(`${script.name || script.slug}: ${errorMsg}`);
|
||||
console.error(`Failed to download script ${script.slug}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
results.newScripts = downloaded;
|
||||
results.errors.push(...errors);
|
||||
}
|
||||
|
||||
// Update existing scripts
|
||||
if (settings.autoUpdateExisting && existingScripts.length > 0) {
|
||||
console.log(`Auto-updating ${existingScripts.length} existing scripts...`);
|
||||
const updated = [];
|
||||
const errors = [];
|
||||
|
||||
for (const script of existingScripts) {
|
||||
try {
|
||||
// Always update existing scripts when auto-update is enabled
|
||||
const result = await scriptDownloaderService.loadScript(script);
|
||||
if (result.success) {
|
||||
updated.push(script); // Store full script object for category grouping
|
||||
console.log(`Updated script: ${script.name || script.slug}`);
|
||||
} else {
|
||||
errors.push(`${script.name || script.slug}: ${result.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
errors.push(`${script.name || script.slug}: ${errorMsg}`);
|
||||
console.error(`Failed to update script ${script.slug}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
results.updatedScripts = updated;
|
||||
results.errors.push(...errors);
|
||||
}
|
||||
} else {
|
||||
console.log('No JSON files were synced, skipping script download/update');
|
||||
@@ -310,14 +480,19 @@ export class AutoSyncService {
|
||||
}
|
||||
|
||||
// Step 3: Send notifications if enabled
|
||||
if (settings.notificationEnabled && settings.appriseUrls?.length > 0) {
|
||||
console.log('Sending notifications...');
|
||||
if (settings.notificationEnabled && settings.appriseUrls && settings.appriseUrls.length > 0) {
|
||||
console.log('Sending success notifications...');
|
||||
await this.sendSyncNotification(results);
|
||||
console.log('Success notifications sent');
|
||||
}
|
||||
|
||||
// Step 4: Update last sync time
|
||||
const lastSyncTime = new Date().toISOString();
|
||||
const updatedSettings = { ...settings, lastAutoSync: lastSyncTime };
|
||||
// Step 4: Update last sync time and clear any previous errors
|
||||
const lastSyncTime = this.safeToISOString(new Date());
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
lastAutoSync: lastSyncTime,
|
||||
lastAutoSyncError: '' // Clear any previous errors on successful sync
|
||||
};
|
||||
this.saveSettings(updatedSettings);
|
||||
|
||||
const duration = new Date().getTime() - startTime.getTime();
|
||||
@@ -333,27 +508,51 @@ export class AutoSyncService {
|
||||
} catch (error) {
|
||||
console.error('Auto-sync execution failed:', error);
|
||||
|
||||
// Check if it's a rate limit error
|
||||
const isRateLimitError = error instanceof Error && error.name === 'RateLimitError';
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
// Send error notification if enabled
|
||||
const settings = this.loadSettings();
|
||||
if (settings.notificationEnabled && settings.appriseUrls?.length > 0) {
|
||||
if (settings.notificationEnabled && settings.appriseUrls && settings.appriseUrls.length > 0) {
|
||||
try {
|
||||
const notificationTitle = isRateLimitError ? 'Auto-Sync Rate Limited' : 'Auto-Sync Failed';
|
||||
const notificationMessage = isRateLimitError
|
||||
? `GitHub API rate limit exceeded. Please set a GITHUB_TOKEN in your .env file for higher rate limits. Error: ${errorMessage}`
|
||||
: `Auto-sync failed with error: ${errorMessage}`;
|
||||
|
||||
await appriseService.sendNotification(
|
||||
'Auto-Sync Failed',
|
||||
`Auto-sync failed with error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
settings.appriseUrls
|
||||
notificationTitle,
|
||||
notificationMessage,
|
||||
settings.appriseUrls || []
|
||||
);
|
||||
} catch (notifError) {
|
||||
console.error('Failed to send error notification:', notifError);
|
||||
}
|
||||
}
|
||||
|
||||
// Store the error in settings for UI display
|
||||
const errorSettings = this.loadSettings();
|
||||
const errorToStore = isRateLimitError
|
||||
? `GitHub API rate limit exceeded. Please set a GITHUB_TOKEN in your .env file for higher rate limits.`
|
||||
: errorMessage;
|
||||
|
||||
const updatedErrorSettings = {
|
||||
...errorSettings,
|
||||
lastAutoSyncError: errorToStore,
|
||||
lastAutoSyncErrorTime: this.safeToISOString(new Date())
|
||||
};
|
||||
this.saveSettings(updatedErrorSettings);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
message: errorToStore,
|
||||
error: errorMessage,
|
||||
isRateLimitError
|
||||
};
|
||||
} finally {
|
||||
this.isRunning = false;
|
||||
globalAutoSyncLock = false; // Release global lock
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,6 +583,12 @@ export class AutoSyncService {
|
||||
const grouped = new Map();
|
||||
|
||||
scripts.forEach(script => {
|
||||
// Validate script object
|
||||
if (!script || !script.name) {
|
||||
console.warn('Invalid script object in groupScriptsByCategory, skipping:', script);
|
||||
return;
|
||||
}
|
||||
|
||||
const scriptCategories = script.categories || [0]; // Default to Miscellaneous (id: 0)
|
||||
scriptCategories.forEach((/** @type {number} */ catId) => {
|
||||
const categoryName = categoryMap.get(catId) || 'Miscellaneous';
|
||||
@@ -415,7 +620,18 @@ export class AutoSyncService {
|
||||
// @ts-ignore - Dynamic property access
|
||||
if (results.jsonSync) {
|
||||
// @ts-ignore - Dynamic property access
|
||||
body += `JSON Files: ${results.jsonSync.syncedCount} synced, ${results.jsonSync.skippedCount} up-to-date\n`;
|
||||
const syncedCount = results.jsonSync.count || 0;
|
||||
// @ts-ignore - Dynamic property access
|
||||
const syncedFiles = results.jsonSync.syncedFiles || [];
|
||||
|
||||
// Calculate up-to-date count (total files - synced files)
|
||||
// We can't easily get total file count from the sync result, so just show synced count
|
||||
if (syncedCount > 0) {
|
||||
body += `JSON Files: ${syncedCount} synced\n`;
|
||||
} else {
|
||||
body += `JSON Files: All up-to-date\n`;
|
||||
}
|
||||
|
||||
// @ts-ignore - Dynamic property access
|
||||
if (results.jsonSync.errors?.length > 0) {
|
||||
// @ts-ignore - Dynamic property access
|
||||
|
||||
@@ -29,14 +29,24 @@ export class GitHubService {
|
||||
}
|
||||
|
||||
private async fetchFromGitHub<T>(endpoint: string): Promise<T> {
|
||||
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'PVEScripts-Local/1.0',
|
||||
},
|
||||
});
|
||||
const headers: HeadersInit = {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'PVEScripts-Local/1.0',
|
||||
};
|
||||
|
||||
// Add GitHub token authentication if available
|
||||
if (env.GITHUB_TOKEN) {
|
||||
headers.Authorization = `token ${env.GITHUB_TOKEN}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}${endpoint}`, { headers });
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
const error = new Error(`GitHub API rate limit exceeded. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`);
|
||||
error.name = 'RateLimitError';
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,276 +1,6 @@
|
||||
import { writeFile, mkdir } from 'fs/promises';
|
||||
import { readFileSync, readdirSync, statSync, utimesSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { Buffer } from 'buffer';
|
||||
// JavaScript wrapper for githubJsonService.ts
|
||||
// This allows the JavaScript autoSyncService.js to import the TypeScript service
|
||||
|
||||
export class GitHubJsonService {
|
||||
constructor() {
|
||||
this.baseUrl = null;
|
||||
this.repoUrl = null;
|
||||
this.branch = null;
|
||||
this.jsonFolder = null;
|
||||
this.localJsonDirectory = null;
|
||||
this.scriptCache = new Map();
|
||||
}
|
||||
import { githubJsonService } from './githubJsonService.ts';
|
||||
|
||||
initializeConfig() {
|
||||
if (this.repoUrl === null) {
|
||||
// Get environment variables
|
||||
this.repoUrl = process.env.REPO_URL || "";
|
||||
this.branch = process.env.REPO_BRANCH || "main";
|
||||
this.jsonFolder = process.env.JSON_FOLDER || "scripts";
|
||||
this.localJsonDirectory = join(process.cwd(), 'scripts', 'json');
|
||||
|
||||
// Only validate GitHub URL if it's provided
|
||||
if (this.repoUrl) {
|
||||
// Extract owner and repo from the URL
|
||||
const urlMatch = /github\.com\/([^\/]+)\/([^\/]+)/.exec(this.repoUrl);
|
||||
if (!urlMatch) {
|
||||
throw new Error(`Invalid GitHub repository URL: ${this.repoUrl}`);
|
||||
}
|
||||
|
||||
const [, owner, repo] = urlMatch;
|
||||
this.baseUrl = `https://api.github.com/repos/${owner}/${repo}`;
|
||||
} else {
|
||||
// Set a dummy base URL if no REPO_URL is provided
|
||||
this.baseUrl = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fetchFromGitHub(endpoint) {
|
||||
this.initializeConfig();
|
||||
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'PVEScripts-Local/1.0',
|
||||
...(process.env.GITHUB_TOKEN && { 'Authorization': `token ${process.env.GITHUB_TOKEN}` })
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async syncJsonFiles() {
|
||||
try {
|
||||
this.initializeConfig();
|
||||
|
||||
if (!this.baseUrl) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'No GitHub repository configured'
|
||||
};
|
||||
}
|
||||
|
||||
console.log('Starting fast incremental JSON sync...');
|
||||
|
||||
// Ensure local directory exists
|
||||
await mkdir(this.localJsonDirectory, { recursive: true });
|
||||
|
||||
// Step 1: Get file list from GitHub (single API call)
|
||||
console.log('Fetching file list from GitHub...');
|
||||
const files = await this.fetchFromGitHub(`/contents/${this.jsonFolder}?ref=${this.branch}`);
|
||||
|
||||
if (!Array.isArray(files)) {
|
||||
throw new Error('Invalid response from GitHub API');
|
||||
}
|
||||
|
||||
const jsonFiles = files.filter(file => file.name.endsWith('.json'));
|
||||
console.log(`Found ${jsonFiles.length} JSON files in repository`);
|
||||
|
||||
// Step 2: Get local file list (fast local operation)
|
||||
const localFiles = new Map();
|
||||
try {
|
||||
console.log(`Looking for local files in: ${this.localJsonDirectory}`);
|
||||
const localFileList = readdirSync(this.localJsonDirectory);
|
||||
console.log(`Found ${localFileList.length} files in local directory`);
|
||||
for (const fileName of localFileList) {
|
||||
if (fileName.endsWith('.json')) {
|
||||
const filePath = join(this.localJsonDirectory, fileName);
|
||||
const stats = statSync(filePath);
|
||||
localFiles.set(fileName, {
|
||||
mtime: stats.mtime,
|
||||
size: stats.size
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error reading local directory:', error.message);
|
||||
console.log('Directory path:', this.localJsonDirectory);
|
||||
console.log('No local files found, will download all');
|
||||
}
|
||||
|
||||
console.log(`Found ${localFiles.size} local JSON files`);
|
||||
|
||||
// Step 3: Compare and identify files that need syncing
|
||||
const filesToSync = [];
|
||||
let skippedCount = 0;
|
||||
|
||||
for (const file of jsonFiles) {
|
||||
const localFile = localFiles.get(file.name);
|
||||
|
||||
if (!localFile) {
|
||||
// File doesn't exist locally
|
||||
filesToSync.push(file);
|
||||
console.log(`Missing: ${file.name}`);
|
||||
} else {
|
||||
// Compare modification times and sizes
|
||||
const localMtime = new Date(localFile.mtime);
|
||||
const remoteMtime = new Date(file.updated_at);
|
||||
const localSize = localFile.size;
|
||||
const remoteSize = file.size;
|
||||
|
||||
// Sync if remote is newer OR sizes are different (content changed)
|
||||
if (localMtime < remoteMtime || localSize !== remoteSize) {
|
||||
filesToSync.push(file);
|
||||
console.log(`Changed: ${file.name} (${localMtime.toISOString()} -> ${remoteMtime.toISOString()})`);
|
||||
} else {
|
||||
skippedCount++;
|
||||
console.log(`Up-to-date: ${file.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Files to sync: ${filesToSync.length}, Up-to-date: ${skippedCount}`);
|
||||
|
||||
// Step 4: Download only the files that need syncing
|
||||
let syncedCount = 0;
|
||||
const errors = [];
|
||||
const syncedFiles = [];
|
||||
|
||||
// Process files in batches to avoid overwhelming the API
|
||||
const batchSize = 10;
|
||||
for (let i = 0; i < filesToSync.length; i += batchSize) {
|
||||
const batch = filesToSync.slice(i, i + batchSize);
|
||||
|
||||
// Process batch in parallel
|
||||
const promises = batch.map(async (file) => {
|
||||
try {
|
||||
const content = await this.fetchFromGitHub(`/contents/${file.path}?ref=${this.branch}`);
|
||||
|
||||
if (content.content) {
|
||||
// Decode base64 content
|
||||
const fileContent = Buffer.from(content.content, 'base64').toString('utf-8');
|
||||
|
||||
// Write to local file
|
||||
const localPath = join(this.localJsonDirectory, file.name);
|
||||
await writeFile(localPath, fileContent, 'utf-8');
|
||||
|
||||
// Update file modification time to match remote
|
||||
const remoteMtime = new Date(file.updated_at);
|
||||
utimesSync(localPath, remoteMtime, remoteMtime);
|
||||
|
||||
syncedCount++;
|
||||
syncedFiles.push(file.name);
|
||||
console.log(`Synced: ${file.name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to sync ${file.name}:`, error.message);
|
||||
errors.push(`${file.name}: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// Small delay between batches to be nice to the API
|
||||
if (i + batchSize < filesToSync.length) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`JSON sync completed. Synced ${syncedCount} files, skipped ${skippedCount} files.`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully synced ${syncedCount} JSON files (${skippedCount} up-to-date)`,
|
||||
syncedCount,
|
||||
skippedCount,
|
||||
syncedFiles,
|
||||
errors
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('JSON sync failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: error.message,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getAllScripts() {
|
||||
try {
|
||||
this.initializeConfig();
|
||||
|
||||
if (!this.localJsonDirectory) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const scripts = [];
|
||||
|
||||
// Read all JSON files from local directory
|
||||
const files = readdirSync(this.localJsonDirectory);
|
||||
const jsonFiles = files.filter(file => file.endsWith('.json'));
|
||||
|
||||
for (const file of jsonFiles) {
|
||||
try {
|
||||
const filePath = join(this.localJsonDirectory, file);
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const script = JSON.parse(content);
|
||||
|
||||
if (script && typeof script === 'object') {
|
||||
scripts.push(script);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to parse ${file}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return scripts;
|
||||
} catch (error) {
|
||||
console.error('Failed to get all scripts:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scripts only for specific JSON files that were synced
|
||||
*/
|
||||
async getScriptsForFiles(syncedFiles) {
|
||||
try {
|
||||
this.initializeConfig();
|
||||
|
||||
if (!this.localJsonDirectory || !syncedFiles || syncedFiles.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const scripts = [];
|
||||
|
||||
for (const fileName of syncedFiles) {
|
||||
try {
|
||||
const filePath = join(this.localJsonDirectory, fileName);
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const script = JSON.parse(content);
|
||||
|
||||
if (script && typeof script === 'object') {
|
||||
scripts.push(script);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to parse ${fileName}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return scripts;
|
||||
} catch (error) {
|
||||
console.error('Failed to get scripts for synced files:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const githubJsonService = new GitHubJsonService();
|
||||
export { githubJsonService };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { writeFile, mkdir } from 'fs/promises';
|
||||
import { writeFile, mkdir, readdir } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { env } from '~/env.js';
|
||||
import type { Script, ScriptCard, GitHubFile } from '~/types/script';
|
||||
import { env } from '../../env.js';
|
||||
import type { Script, ScriptCard, GitHubFile } from '../../types/script';
|
||||
|
||||
export class GitHubJsonService {
|
||||
private baseUrl: string | null = null;
|
||||
@@ -41,14 +41,25 @@ export class GitHubJsonService {
|
||||
|
||||
private async fetchFromGitHub<T>(endpoint: string): Promise<T> {
|
||||
this.initializeConfig();
|
||||
const response = await fetch(`${this.baseUrl!}${endpoint}`, {
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'PVEScripts-Local/1.0',
|
||||
},
|
||||
});
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'PVEScripts-Local/1.0',
|
||||
};
|
||||
|
||||
// Add GitHub token authentication if available
|
||||
if (env.GITHUB_TOKEN) {
|
||||
headers.Authorization = `token ${env.GITHUB_TOKEN}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl!}${endpoint}`, { headers });
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
const error = new Error(`GitHub API rate limit exceeded. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`);
|
||||
error.name = 'RateLimitError';
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
@@ -59,8 +70,22 @@ export class GitHubJsonService {
|
||||
this.initializeConfig();
|
||||
const rawUrl = `https://raw.githubusercontent.com/${this.extractRepoPath()}/${this.branch!}/${filePath}`;
|
||||
|
||||
const response = await fetch(rawUrl);
|
||||
const headers: HeadersInit = {
|
||||
'User-Agent': 'PVEScripts-Local/1.0',
|
||||
};
|
||||
|
||||
// Add GitHub token authentication if available (for raw files, use token in URL or header)
|
||||
if (env.GITHUB_TOKEN) {
|
||||
headers.Authorization = `token ${env.GITHUB_TOKEN}`;
|
||||
}
|
||||
|
||||
const response = await fetch(rawUrl, { headers });
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
const error = new Error(`GitHub rate limit exceeded while downloading ${filePath}. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`);
|
||||
error.name = 'RateLimitError';
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
@@ -185,48 +210,90 @@ export class GitHubJsonService {
|
||||
}
|
||||
}
|
||||
|
||||
async syncJsonFiles(): Promise<{ success: boolean; message: string; count: number }> {
|
||||
async syncJsonFiles(): Promise<{ success: boolean; message: string; count: number; syncedFiles: string[] }> {
|
||||
try {
|
||||
// Get all scripts from GitHub (1 API call + raw downloads)
|
||||
const scripts = await this.getAllScripts();
|
||||
console.log('Starting fast incremental JSON sync...');
|
||||
|
||||
// Save scripts to local directory
|
||||
await this.saveScriptsLocally(scripts);
|
||||
// Get file list from GitHub
|
||||
console.log('Fetching file list from GitHub...');
|
||||
const githubFiles = await this.getJsonFiles();
|
||||
console.log(`Found ${githubFiles.length} JSON files in repository`);
|
||||
|
||||
// Get local files
|
||||
const localFiles = await this.getLocalJsonFiles();
|
||||
console.log(`Found ${localFiles.length} files in local directory`);
|
||||
console.log(`Found ${localFiles.filter(f => f.endsWith('.json')).length} local JSON files`);
|
||||
|
||||
// Compare and find files that need syncing
|
||||
const filesToSync = this.findFilesToSync(githubFiles, localFiles);
|
||||
console.log(`Found ${filesToSync.length} files that need syncing`);
|
||||
|
||||
if (filesToSync.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
message: 'All JSON files are up to date',
|
||||
count: 0,
|
||||
syncedFiles: []
|
||||
};
|
||||
}
|
||||
|
||||
// Download and save only the files that need syncing
|
||||
const syncedFiles = await this.syncSpecificFiles(filesToSync);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully synced ${scripts.length} scripts from GitHub using 1 API call + raw downloads`,
|
||||
count: scripts.length
|
||||
message: `Successfully synced ${syncedFiles.length} JSON files from GitHub`,
|
||||
count: syncedFiles.length,
|
||||
syncedFiles
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error syncing JSON files:', error);
|
||||
console.error('JSON sync failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to sync JSON files: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
count: 0
|
||||
count: 0,
|
||||
syncedFiles: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async saveScriptsLocally(scripts: Script[]): Promise<void> {
|
||||
private async getLocalJsonFiles(): Promise<string[]> {
|
||||
this.initializeConfig();
|
||||
try {
|
||||
// Ensure the directory exists
|
||||
await mkdir(this.localJsonDirectory!, { recursive: true });
|
||||
|
||||
// Save each script as a JSON file
|
||||
for (const script of scripts) {
|
||||
const filename = `${script.slug}.json`;
|
||||
const filePath = join(this.localJsonDirectory!, filename);
|
||||
const content = JSON.stringify(script, null, 2);
|
||||
await writeFile(filePath, content, 'utf-8');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error saving scripts locally:', error);
|
||||
throw new Error('Failed to save scripts locally');
|
||||
const files = await readdir(this.localJsonDirectory!);
|
||||
return files.filter(f => f.endsWith('.json'));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private findFilesToSync(githubFiles: GitHubFile[], localFiles: string[]): GitHubFile[] {
|
||||
const localFileSet = new Set(localFiles);
|
||||
// Return only files that don't exist locally
|
||||
return githubFiles.filter(ghFile => !localFileSet.has(ghFile.name));
|
||||
}
|
||||
|
||||
private async syncSpecificFiles(filesToSync: GitHubFile[]): Promise<string[]> {
|
||||
this.initializeConfig();
|
||||
const syncedFiles: string[] = [];
|
||||
|
||||
await mkdir(this.localJsonDirectory!, { recursive: true });
|
||||
|
||||
for (const file of filesToSync) {
|
||||
try {
|
||||
const script = await this.downloadJsonFile(file.path);
|
||||
const filename = `${script.slug}.json`;
|
||||
const filePath = join(this.localJsonDirectory!, filename);
|
||||
await writeFile(filePath, JSON.stringify(script, null, 2), 'utf-8');
|
||||
syncedFiles.push(filename);
|
||||
} catch (error) {
|
||||
console.error(`Failed to sync ${file.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return syncedFiles;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
|
||||
6
src/server/services/localScripts.js
Normal file
6
src/server/services/localScripts.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// JavaScript wrapper for localScripts.ts
|
||||
// This allows the JavaScript autoSyncService.js to import the TypeScript service
|
||||
|
||||
import { localScriptsService } from './localScripts.ts';
|
||||
|
||||
export { localScriptsService };
|
||||
@@ -1,14 +1,18 @@
|
||||
import { writeFile, readFile, mkdir } from 'fs/promises';
|
||||
// Real JavaScript implementation for script downloading
|
||||
import { join } from 'path';
|
||||
import { writeFile, mkdir, access, readFile } from 'fs/promises';
|
||||
|
||||
export class ScriptDownloaderService {
|
||||
constructor() {
|
||||
this.scriptsDirectory = null;
|
||||
this.repoUrl = null;
|
||||
}
|
||||
|
||||
initializeConfig() {
|
||||
if (this.scriptsDirectory === null) {
|
||||
this.scriptsDirectory = join(process.cwd(), 'scripts');
|
||||
// Get REPO_URL from environment or use default
|
||||
this.repoUrl = process.env.REPO_URL || 'https://github.com/community-scripts/ProxmoxVE';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,19 +27,35 @@ export class ScriptDownloaderService {
|
||||
}
|
||||
|
||||
async downloadFileFromGitHub(filePath) {
|
||||
// This is a simplified version - in a real implementation,
|
||||
// you would fetch the file content from GitHub
|
||||
// For now, we'll return a placeholder
|
||||
return `#!/bin/bash
|
||||
# Downloaded script: ${filePath}
|
||||
# This is a placeholder - implement actual GitHub file download
|
||||
echo "Script downloaded: ${filePath}"
|
||||
`;
|
||||
this.initializeConfig();
|
||||
if (!this.repoUrl) {
|
||||
throw new Error('REPO_URL environment variable is not set');
|
||||
}
|
||||
|
||||
// Extract repo path from URL
|
||||
const match = /github\.com\/([^\/]+)\/([^\/]+)/.exec(this.repoUrl);
|
||||
if (!match) {
|
||||
throw new Error('Invalid GitHub repository URL');
|
||||
}
|
||||
const [, owner, repo] = match;
|
||||
|
||||
const url = `https://raw.githubusercontent.com/${owner}/${repo}/main/${filePath}`;
|
||||
|
||||
console.log(`Downloading from GitHub: ${url}`);
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.text();
|
||||
}
|
||||
|
||||
modifyScriptContent(content) {
|
||||
// Modify script content for CT scripts if needed
|
||||
return content;
|
||||
// Replace the build.func source line
|
||||
const oldPattern = /source <\(curl -fsSL https:\/\/raw\.githubusercontent\.com\/community-scripts\/ProxmoxVE\/main\/misc\/build\.func\)/g;
|
||||
const newPattern = 'SCRIPT_DIR="$(dirname "$0")" \nsource "$SCRIPT_DIR/../core/build.func"';
|
||||
|
||||
return content.replace(oldPattern, newPattern);
|
||||
}
|
||||
|
||||
async loadScript(script) {
|
||||
@@ -57,6 +77,7 @@ echo "Script downloaded: ${filePath}"
|
||||
|
||||
if (fileName) {
|
||||
// Download from GitHub
|
||||
console.log(`Downloading script file: ${scriptPath}`);
|
||||
const content = await this.downloadFileFromGitHub(scriptPath);
|
||||
|
||||
// Determine target directory based on script path
|
||||
@@ -91,16 +112,6 @@ echo "Script downloaded: ${filePath}"
|
||||
await this.ensureDirectoryExists(join(this.scriptsDirectory, finalTargetDir));
|
||||
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
|
||||
await writeFile(filePath, content, 'utf-8');
|
||||
} else if (scriptPath.startsWith('vw/')) {
|
||||
targetDir = 'vw';
|
||||
// Preserve subdirectory structure for VW scripts
|
||||
const subPath = scriptPath.replace('vw/', '');
|
||||
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||
// Ensure the subdirectory exists
|
||||
await this.ensureDirectoryExists(join(this.scriptsDirectory, finalTargetDir));
|
||||
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
|
||||
await writeFile(filePath, content, 'utf-8');
|
||||
} else {
|
||||
// Handle other script types (fallback to ct directory)
|
||||
targetDir = 'ct';
|
||||
@@ -111,6 +122,7 @@ echo "Script downloaded: ${filePath}"
|
||||
}
|
||||
|
||||
files.push(`${finalTargetDir}/${fileName}`);
|
||||
console.log(`Successfully downloaded: ${finalTargetDir}/${fileName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,12 +133,15 @@ echo "Script downloaded: ${filePath}"
|
||||
if (hasCtScript) {
|
||||
const installScriptName = `${script.slug}-install.sh`;
|
||||
try {
|
||||
console.log(`Downloading install script: install/${installScriptName}`);
|
||||
const installContent = await this.downloadFileFromGitHub(`install/${installScriptName}`);
|
||||
const localInstallPath = join(this.scriptsDirectory, 'install', installScriptName);
|
||||
await writeFile(localInstallPath, installContent, 'utf-8');
|
||||
files.push(`install/${installScriptName}`);
|
||||
} catch {
|
||||
console.log(`Successfully downloaded: install/${installScriptName}`);
|
||||
} catch (error) {
|
||||
// Install script might not exist, that's okay
|
||||
console.log(`Install script not found: install/${installScriptName}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,78 +160,6 @@ echo "Script downloaded: ${filePath}"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-download new scripts that haven't been downloaded yet
|
||||
*/
|
||||
async autoDownloadNewScripts(allScripts) {
|
||||
this.initializeConfig();
|
||||
const downloaded = [];
|
||||
const errors = [];
|
||||
|
||||
for (const script of allScripts) {
|
||||
try {
|
||||
// Check if script is already downloaded
|
||||
const isDownloaded = await this.isScriptDownloaded(script);
|
||||
|
||||
if (!isDownloaded) {
|
||||
const result = await this.loadScript(script);
|
||||
if (result.success) {
|
||||
downloaded.push(script); // Return full script object instead of just name
|
||||
console.log(`Auto-downloaded new script: ${script.name || script.slug}`);
|
||||
} else {
|
||||
errors.push(`${script.name || script.slug}: ${result.message}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `${script.name || script.slug}: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||
errors.push(errorMsg);
|
||||
console.error(`Failed to auto-download script ${script.slug}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return { downloaded, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-update existing scripts to newer versions
|
||||
*/
|
||||
async autoUpdateExistingScripts(allScripts) {
|
||||
this.initializeConfig();
|
||||
const updated = [];
|
||||
const errors = [];
|
||||
|
||||
for (const script of allScripts) {
|
||||
try {
|
||||
// Check if script is downloaded
|
||||
const isDownloaded = await this.isScriptDownloaded(script);
|
||||
|
||||
if (isDownloaded) {
|
||||
// Check if update is needed by comparing content
|
||||
const needsUpdate = await this.scriptNeedsUpdate(script);
|
||||
|
||||
if (needsUpdate) {
|
||||
const result = await this.loadScript(script);
|
||||
if (result.success) {
|
||||
updated.push(script); // Return full script object instead of just name
|
||||
console.log(`Auto-updated script: ${script.name || script.slug}`);
|
||||
} else {
|
||||
errors.push(`${script.name || script.slug}: ${result.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `${script.name || script.slug}: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||
errors.push(errorMsg);
|
||||
console.error(`Failed to auto-update script ${script.slug}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return { updated, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a script is already downloaded
|
||||
*/
|
||||
async isScriptDownloaded(script) {
|
||||
if (!script.install_methods?.length) return false;
|
||||
|
||||
@@ -248,12 +191,6 @@ echo "Script downloaded: ${filePath}"
|
||||
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
|
||||
} else if (scriptPath.startsWith('vw/')) {
|
||||
targetDir = 'vw';
|
||||
const subPath = scriptPath.replace('vw/', '');
|
||||
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
|
||||
} else {
|
||||
targetDir = 'ct';
|
||||
finalTargetDir = targetDir;
|
||||
@@ -261,7 +198,7 @@ echo "Script downloaded: ${filePath}"
|
||||
}
|
||||
|
||||
try {
|
||||
await readFile(filePath, 'utf8');
|
||||
await import('fs/promises').then(fs => fs.readFile(filePath, 'utf8'));
|
||||
// File exists, continue checking other methods
|
||||
} catch {
|
||||
// File doesn't exist, script is not fully downloaded
|
||||
@@ -275,71 +212,206 @@ echo "Script downloaded: ${filePath}"
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a script needs updating by comparing local and remote content
|
||||
*/
|
||||
async scriptNeedsUpdate(script) {
|
||||
if (!script.install_methods?.length) return false;
|
||||
async checkScriptExists(script) {
|
||||
this.initializeConfig();
|
||||
const files = [];
|
||||
let ctExists = false;
|
||||
let installExists = false;
|
||||
|
||||
for (const method of script.install_methods) {
|
||||
if (method.script) {
|
||||
const scriptPath = method.script;
|
||||
const fileName = scriptPath.split('/').pop();
|
||||
|
||||
if (fileName) {
|
||||
// Determine target directory based on script path
|
||||
let targetDir;
|
||||
let finalTargetDir;
|
||||
let filePath;
|
||||
|
||||
if (scriptPath.startsWith('ct/')) {
|
||||
targetDir = 'ct';
|
||||
finalTargetDir = targetDir;
|
||||
filePath = join(this.scriptsDirectory, targetDir, fileName);
|
||||
} else if (scriptPath.startsWith('tools/')) {
|
||||
targetDir = 'tools';
|
||||
const subPath = scriptPath.replace('tools/', '');
|
||||
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
|
||||
} else if (scriptPath.startsWith('vm/')) {
|
||||
targetDir = 'vm';
|
||||
const subPath = scriptPath.replace('vm/', '');
|
||||
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
|
||||
} else if (scriptPath.startsWith('vw/')) {
|
||||
targetDir = 'vw';
|
||||
const subPath = scriptPath.replace('vw/', '');
|
||||
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
|
||||
} else {
|
||||
targetDir = 'ct';
|
||||
finalTargetDir = targetDir;
|
||||
filePath = join(this.scriptsDirectory, targetDir, fileName);
|
||||
}
|
||||
|
||||
try {
|
||||
// Read local content
|
||||
const localContent = await readFile(filePath, 'utf8');
|
||||
try {
|
||||
// Check scripts based on their install methods
|
||||
if (script.install_methods?.length) {
|
||||
for (const method of script.install_methods) {
|
||||
if (method.script) {
|
||||
const scriptPath = method.script;
|
||||
const fileName = scriptPath.split('/').pop();
|
||||
|
||||
// Download remote content
|
||||
const remoteContent = await this.downloadFileFromGitHub(scriptPath);
|
||||
|
||||
// Compare content (simple string comparison for now)
|
||||
// In a more sophisticated implementation, you might want to compare
|
||||
// file modification times or use content hashing
|
||||
return localContent !== remoteContent;
|
||||
} catch {
|
||||
// If we can't read local or download remote, assume update needed
|
||||
return true;
|
||||
if (fileName) {
|
||||
let targetDir;
|
||||
let finalTargetDir;
|
||||
let filePath;
|
||||
|
||||
if (scriptPath.startsWith('ct/')) {
|
||||
targetDir = 'ct';
|
||||
finalTargetDir = targetDir;
|
||||
filePath = join(this.scriptsDirectory, targetDir, fileName);
|
||||
} else if (scriptPath.startsWith('tools/')) {
|
||||
targetDir = 'tools';
|
||||
// Preserve subdirectory structure for tools scripts
|
||||
const subPath = scriptPath.replace('tools/', '');
|
||||
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
|
||||
} else if (scriptPath.startsWith('vm/')) {
|
||||
targetDir = 'vm';
|
||||
// Preserve subdirectory structure for VM scripts
|
||||
const subPath = scriptPath.replace('vm/', '');
|
||||
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||
filePath = join(this.scriptsDirectory, finalTargetDir, fileName);
|
||||
} else {
|
||||
targetDir = 'ct'; // Default fallback
|
||||
finalTargetDir = targetDir;
|
||||
filePath = join(this.scriptsDirectory, targetDir, fileName);
|
||||
}
|
||||
|
||||
try {
|
||||
await access(filePath);
|
||||
files.push(`${finalTargetDir}/${fileName}`);
|
||||
|
||||
// Set ctExists for all script types (CT, tools, vm) for UI consistency
|
||||
if (scriptPath.startsWith('ct/') || scriptPath.startsWith('tools/') || scriptPath.startsWith('vm/')) {
|
||||
ctExists = true;
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
// Check for install script for CT scripts
|
||||
const hasCtScript = script.install_methods?.some(method => method.script?.startsWith('ct/'));
|
||||
if (hasCtScript) {
|
||||
const installScriptName = `${script.slug}-install.sh`;
|
||||
const installPath = join(this.scriptsDirectory, 'install', installScriptName);
|
||||
|
||||
try {
|
||||
await access(installPath);
|
||||
files.push(`install/${installScriptName}`);
|
||||
installExists = true;
|
||||
} catch {
|
||||
// Install script doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
return { ctExists, installExists, files };
|
||||
} catch (error) {
|
||||
console.error('Error checking script existence:', error);
|
||||
return { ctExists: false, installExists: false, files: [] };
|
||||
}
|
||||
}
|
||||
|
||||
async compareScriptContent(script) {
|
||||
this.initializeConfig();
|
||||
const differences = [];
|
||||
let hasDifferences = false;
|
||||
|
||||
try {
|
||||
// First check if any local files exist
|
||||
const localFilesExist = await this.checkScriptExists(script);
|
||||
if (!localFilesExist.ctExists && !localFilesExist.installExists) {
|
||||
// No local files exist, so no comparison needed
|
||||
return { hasDifferences: false, differences: [] };
|
||||
}
|
||||
|
||||
// If we have local files, proceed with comparison
|
||||
// Use Promise.all to run comparisons in parallel
|
||||
const comparisonPromises = [];
|
||||
|
||||
// Compare scripts only if they exist locally
|
||||
if (localFilesExist.ctExists && script.install_methods?.length) {
|
||||
for (const method of script.install_methods) {
|
||||
if (method.script) {
|
||||
const scriptPath = method.script;
|
||||
const fileName = scriptPath.split('/').pop();
|
||||
|
||||
if (fileName) {
|
||||
let targetDir;
|
||||
let finalTargetDir;
|
||||
|
||||
if (scriptPath.startsWith('ct/')) {
|
||||
targetDir = 'ct';
|
||||
finalTargetDir = targetDir;
|
||||
} else if (scriptPath.startsWith('tools/')) {
|
||||
targetDir = 'tools';
|
||||
// Preserve subdirectory structure for tools scripts
|
||||
const subPath = scriptPath.replace('tools/', '');
|
||||
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||
} else if (scriptPath.startsWith('vm/')) {
|
||||
targetDir = 'vm';
|
||||
// Preserve subdirectory structure for VM scripts
|
||||
const subPath = scriptPath.replace('vm/', '');
|
||||
const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : '';
|
||||
finalTargetDir = subDir ? join(targetDir, subDir) : targetDir;
|
||||
} else {
|
||||
continue; // Skip unknown script types
|
||||
}
|
||||
|
||||
comparisonPromises.push(
|
||||
this.compareSingleFile(scriptPath, `${finalTargetDir}/${fileName}`)
|
||||
.then(result => {
|
||||
if (result.hasDifferences) {
|
||||
hasDifferences = true;
|
||||
differences.push(result.filePath);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Don't add to differences if there's an error reading files
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compare install script only if it exists locally
|
||||
if (localFilesExist.installExists) {
|
||||
const installScriptName = `${script.slug}-install.sh`;
|
||||
const installScriptPath = `install/${installScriptName}`;
|
||||
|
||||
comparisonPromises.push(
|
||||
this.compareSingleFile(installScriptPath, installScriptPath)
|
||||
.then(result => {
|
||||
if (result.hasDifferences) {
|
||||
hasDifferences = true;
|
||||
differences.push(result.filePath);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Don't add to differences if there's an error reading files
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Wait for all comparisons to complete
|
||||
await Promise.all(comparisonPromises);
|
||||
|
||||
return { hasDifferences, differences };
|
||||
} catch (error) {
|
||||
console.error('Error comparing script content:', error);
|
||||
return { hasDifferences: false, differences: [] };
|
||||
}
|
||||
}
|
||||
|
||||
async compareSingleFile(remotePath, filePath) {
|
||||
try {
|
||||
const localPath = join(this.scriptsDirectory, filePath);
|
||||
|
||||
// Read local content
|
||||
const localContent = await readFile(localPath, 'utf-8');
|
||||
|
||||
// Download remote content
|
||||
const remoteContent = await this.downloadFileFromGitHub(remotePath);
|
||||
|
||||
// Apply modification only for CT scripts, not for other script types
|
||||
let modifiedRemoteContent;
|
||||
if (remotePath.startsWith('ct/')) {
|
||||
modifiedRemoteContent = this.modifyScriptContent(remoteContent);
|
||||
} else {
|
||||
modifiedRemoteContent = remoteContent; // Don't modify tools or vm scripts
|
||||
}
|
||||
|
||||
// Compare content
|
||||
const hasDifferences = localContent !== modifiedRemoteContent;
|
||||
|
||||
return { hasDifferences, filePath };
|
||||
} catch (error) {
|
||||
console.error(`Error comparing file ${filePath}:`, error);
|
||||
return { hasDifferences: false, filePath };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user