Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
b0e7e94a23 chore: add VERSION v0.4.0 2025-10-13 14:37:11 +00:00
21 changed files with 343 additions and 690 deletions

View File

@@ -1,6 +1,6 @@
# Template for release drafts
name-template: 'v$NEXT_PATCH_VERSION' # You can switch to $NEXT_MINOR_VERSION or $NEXT_MAJOR_VERSION
tag-template: 'v$NEXT_PATCH_VERSION'
name-template: 'v$NEXT_MINOR_VERSION' # You can switch to $NEXT_MINOR_VERSION or $NEXT_MAJOR_VERSION
tag-template: 'v$NEXT_MINOR_VERSION'
# Exclude PRs with this label from release notes
exclude-labels:

242
package-lock.json generated
View File

@@ -10,7 +10,7 @@
"dependencies": {
"@radix-ui/react-slot": "^1.2.3",
"@t3-oss/env-nextjs": "^0.13.8",
"@tanstack/react-query": "^5.90.3",
"@tanstack/react-query": "^5.87.4",
"@trpc/client": "^11.6.0",
"@trpc/react-query": "^11.6.0",
"@trpc/server": "^11.6.0",
@@ -25,7 +25,7 @@
"clsx": "^2.1.1",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.545.0",
"next": "^15.5.5",
"next": "^15.5.3",
"node-pty": "^1.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -47,9 +47,9 @@
"@types/bcryptjs": "^3.0.0",
"@types/better-sqlite3": "^7.6.8",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^24.7.2",
"@types/node": "^24.7.1",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.2.2",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^5.0.2",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
@@ -61,7 +61,7 @@
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^4.1.14",
"typescript": "^5.8.2",
"typescript-eslint": "^8.46.1",
"typescript-eslint": "^8.27.0",
"vitest": "^3.2.4"
}
},
@@ -1797,9 +1797,9 @@
}
},
"node_modules/@next/env": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.5.tgz",
"integrity": "sha512-2Zhvss36s/yL+YSxD5ZL5dz5pI6ki1OLxYlh6O77VJ68sBnlUrl5YqhBgCy7FkdMsp9RBeGFwpuDCdpJOqdKeQ==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.4.tgz",
"integrity": "sha512-27SQhYp5QryzIT5uO8hq99C69eLQ7qkzkDPsk3N+GuS2XgOgoYEeOav7Pf8Tn4drECOVDsDg8oj+/DVy8qQL2A==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -1813,9 +1813,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.5.tgz",
"integrity": "sha512-lYExGHuFIHeOxf40mRLWoA84iY2sLELB23BV5FIDHhdJkN1LpRTPc1MDOawgTo5ifbM5dvAwnGuHyNm60G1+jw==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.4.tgz",
"integrity": "sha512-nopqz+Ov6uvorej8ndRX6HlxCYWCO3AHLfKK2TYvxoSB2scETOcfm/HSS3piPqc3A+MUgyHoqE6je4wnkjfrOA==",
"cpu": [
"arm64"
],
@@ -1829,9 +1829,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.5.tgz",
"integrity": "sha512-cacs/WQqa96IhqUm+7CY+z/0j9sW6X80KE07v3IAJuv+z0UNvJtKSlT/T1w1SpaQRa9l0wCYYZlRZUhUOvEVmg==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.4.tgz",
"integrity": "sha512-QOTCFq8b09ghfjRJKfb68kU9k2K+2wsC4A67psOiMn849K9ZXgCSRQr0oVHfmKnoqCbEmQWG1f2h1T2vtJJ9mA==",
"cpu": [
"x64"
],
@@ -1845,9 +1845,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.5.tgz",
"integrity": "sha512-tLd90SvkRFik6LSfuYjcJEmwqcNEnVYVOyKTacSazya/SLlSwy/VYKsDE4GIzOBd+h3gW+FXqShc2XBavccHCg==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.4.tgz",
"integrity": "sha512-eRD5zkts6jS3VfE/J0Kt1VxdFqTnMc3QgO5lFE5GKN3KDI/uUpSyK3CjQHmfEkYR4wCOl0R0XrsjpxfWEA++XA==",
"cpu": [
"arm64"
],
@@ -1861,9 +1861,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.5.tgz",
"integrity": "sha512-ekV76G2R/l3nkvylkfy9jBSYHeB4QcJ7LdDseT6INnn1p51bmDS1eGoSoq+RxfQ7B1wt+Qa0pIl5aqcx0GLpbw==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.4.tgz",
"integrity": "sha512-TOK7iTxmXFc45UrtKqWdZ1shfxuL4tnVAOuuJK4S88rX3oyVV4ZkLjtMT85wQkfBrOOvU55aLty+MV8xmcJR8A==",
"cpu": [
"arm64"
],
@@ -1877,9 +1877,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.5.tgz",
"integrity": "sha512-tI+sBu+3FmWtqlqD4xKJcj3KJtqbniLombKTE7/UWyyoHmOyAo3aZ7QcEHIOgInXOG1nt0rwh0KGmNbvSB0Djg==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.4.tgz",
"integrity": "sha512-7HKolaj+481FSW/5lL0BcTkA4Ueam9SPYWyN/ib/WGAFZf0DGAN8frNpNZYFHtM4ZstrHZS3LY3vrwlIQfsiMA==",
"cpu": [
"x64"
],
@@ -1893,9 +1893,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.5.tgz",
"integrity": "sha512-kDRh+epN/ulroNJLr+toDjN+/JClY5L+OAWjOrrKCI0qcKvTw9GBx7CU/rdA2bgi4WpZN3l0rf/3+b8rduEwrQ==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.4.tgz",
"integrity": "sha512-nlQQ6nfgN0nCO/KuyEUwwOdwQIGjOs4WNMjEUtpIQJPR2NUfmGpW2wkJln1d4nJ7oUzd1g4GivH5GoEPBgfsdw==",
"cpu": [
"x64"
],
@@ -1909,9 +1909,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.5.tgz",
"integrity": "sha512-GDgdNPFFqiKjTrmfw01sMMRWhVN5wOCmFzPloxa7ksDfX6TZt62tAK986f0ZYqWpvDFqeBCLAzmgTURvtQBdgw==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.4.tgz",
"integrity": "sha512-PcR2bN7FlM32XM6eumklmyWLLbu2vs+D7nJX8OAIoWy69Kef8mfiN4e8TUv2KohprwifdpFKPzIP1njuCjD0YA==",
"cpu": [
"arm64"
],
@@ -1925,9 +1925,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.5.tgz",
"integrity": "sha512-5kE3oRJxc7M8RmcTANP8RGoJkaYlwIiDD92gSwCjJY0+j8w8Sl1lvxgQ3bxfHY2KkHFai9tpy/Qx1saWV8eaJQ==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.4.tgz",
"integrity": "sha512-1ur2tSHZj8Px/KMAthmuI9FMp/YFusMMGoRNJaRZMOlSkgvLjzosSdQI0cJAKogdHl3qXUQKL9MGaYvKwA7DXg==",
"cpu": [
"x64"
],
@@ -2769,9 +2769,9 @@
}
},
"node_modules/@tanstack/query-core": {
"version": "5.90.3",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.3.tgz",
"integrity": "sha512-HtPOnCwmx4dd35PfXU8jjkhwYrsHfuqgC8RCJIwWglmhIUIlzPP0ZcEkDAc+UtAWCiLm7T8rxeEfHZlz3hYMCA==",
"version": "5.90.2",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz",
"integrity": "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2779,12 +2779,12 @@
}
},
"node_modules/@tanstack/react-query": {
"version": "5.90.3",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.3.tgz",
"integrity": "sha512-i/LRL6DtuhG6bjGzavIMIVuKKPWx2AnEBIsBfuMm3YoHne0a20nWmsatOCBcVSaT0/8/5YFjNkebHAPLVUSi0Q==",
"version": "5.90.2",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.2.tgz",
"integrity": "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.90.3"
"@tanstack/query-core": "5.90.2"
},
"funding": {
"type": "github",
@@ -3077,9 +3077,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.7.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz",
"integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==",
"version": "24.7.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.1.tgz",
"integrity": "sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.14.0"
@@ -3101,9 +3101,9 @@
}
},
"node_modules/@types/react-dom": {
"version": "19.2.2",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"version": "19.2.1",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz",
"integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -3135,17 +3135,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz",
"integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==",
"version": "8.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz",
"integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.46.1",
"@typescript-eslint/type-utils": "8.46.1",
"@typescript-eslint/utils": "8.46.1",
"@typescript-eslint/visitor-keys": "8.46.1",
"@typescript-eslint/scope-manager": "8.46.0",
"@typescript-eslint/type-utils": "8.46.0",
"@typescript-eslint/utils": "8.46.0",
"@typescript-eslint/visitor-keys": "8.46.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@@ -3159,7 +3159,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.46.1",
"@typescript-eslint/parser": "^8.46.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
@@ -3175,16 +3175,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz",
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==",
"version": "8.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz",
"integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.1",
"@typescript-eslint/types": "8.46.1",
"@typescript-eslint/typescript-estree": "8.46.1",
"@typescript-eslint/visitor-keys": "8.46.1",
"@typescript-eslint/scope-manager": "8.46.0",
"@typescript-eslint/types": "8.46.0",
"@typescript-eslint/typescript-estree": "8.46.0",
"@typescript-eslint/visitor-keys": "8.46.0",
"debug": "^4.3.4"
},
"engines": {
@@ -3200,14 +3200,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz",
"integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==",
"version": "8.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.0.tgz",
"integrity": "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.46.1",
"@typescript-eslint/types": "^8.46.1",
"@typescript-eslint/tsconfig-utils": "^8.46.0",
"@typescript-eslint/types": "^8.46.0",
"debug": "^4.3.4"
},
"engines": {
@@ -3222,14 +3222,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz",
"integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==",
"version": "8.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz",
"integrity": "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.46.1",
"@typescript-eslint/visitor-keys": "8.46.1"
"@typescript-eslint/types": "8.46.0",
"@typescript-eslint/visitor-keys": "8.46.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3240,9 +3240,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz",
"integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==",
"version": "8.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz",
"integrity": "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3257,15 +3257,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz",
"integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==",
"version": "8.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.0.tgz",
"integrity": "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.46.1",
"@typescript-eslint/typescript-estree": "8.46.1",
"@typescript-eslint/utils": "8.46.1",
"@typescript-eslint/types": "8.46.0",
"@typescript-eslint/typescript-estree": "8.46.0",
"@typescript-eslint/utils": "8.46.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@@ -3282,9 +3282,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz",
"integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==",
"version": "8.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz",
"integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3296,16 +3296,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz",
"integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==",
"version": "8.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz",
"integrity": "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.46.1",
"@typescript-eslint/tsconfig-utils": "8.46.1",
"@typescript-eslint/types": "8.46.1",
"@typescript-eslint/visitor-keys": "8.46.1",
"@typescript-eslint/project-service": "8.46.0",
"@typescript-eslint/tsconfig-utils": "8.46.0",
"@typescript-eslint/types": "8.46.0",
"@typescript-eslint/visitor-keys": "8.46.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -3381,9 +3381,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -3394,16 +3394,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz",
"integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==",
"version": "8.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.0.tgz",
"integrity": "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.46.1",
"@typescript-eslint/types": "8.46.1",
"@typescript-eslint/typescript-estree": "8.46.1"
"@typescript-eslint/scope-manager": "8.46.0",
"@typescript-eslint/types": "8.46.0",
"@typescript-eslint/typescript-estree": "8.46.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3418,13 +3418,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz",
"integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==",
"version": "8.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz",
"integrity": "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.46.1",
"@typescript-eslint/types": "8.46.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -7941,12 +7941,12 @@
"license": "MIT"
},
"node_modules/next": {
"version": "15.5.5",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.5.tgz",
"integrity": "sha512-OQVdBPtpBfq7HxFN0kOVb7rXXOSIkt5lTzDJDGRBcOyVvNRIWFauMqi1gIHd1pszq1542vMOGY0HP4CaiALfkA==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.4.tgz",
"integrity": "sha512-xH4Yjhb82sFYQfY3vbkJfgSDgXvBB6a8xPs9i35k6oZJRoQRihZH+4s9Yo2qsWpzBmZ3lPXaJ2KPXLfkvW4LnA==",
"license": "MIT",
"dependencies": {
"@next/env": "15.5.5",
"@next/env": "15.5.4",
"@swc/helpers": "0.5.15",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
@@ -7959,14 +7959,14 @@
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "15.5.5",
"@next/swc-darwin-x64": "15.5.5",
"@next/swc-linux-arm64-gnu": "15.5.5",
"@next/swc-linux-arm64-musl": "15.5.5",
"@next/swc-linux-x64-gnu": "15.5.5",
"@next/swc-linux-x64-musl": "15.5.5",
"@next/swc-win32-arm64-msvc": "15.5.5",
"@next/swc-win32-x64-msvc": "15.5.5",
"@next/swc-darwin-arm64": "15.5.4",
"@next/swc-darwin-x64": "15.5.4",
"@next/swc-linux-arm64-gnu": "15.5.4",
"@next/swc-linux-arm64-musl": "15.5.4",
"@next/swc-linux-x64-gnu": "15.5.4",
"@next/swc-linux-x64-musl": "15.5.4",
"@next/swc-win32-arm64-msvc": "15.5.4",
"@next/swc-win32-x64-msvc": "15.5.4",
"sharp": "^0.34.3"
},
"peerDependencies": {
@@ -10430,16 +10430,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.1.tgz",
"integrity": "sha512-VHgijW803JafdSsDO8I761r3SHrgk4T00IdyQ+/UsthtgPRsBWQLqoSxOolxTpxRKi1kGXK0bSz4CoAc9ObqJA==",
"version": "8.46.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.0.tgz",
"integrity": "sha512-6+ZrB6y2bT2DX3K+Qd9vn7OFOJR+xSLDj+Aw/N3zBwUt27uTw2sw2TE2+UcY1RiyBZkaGbTkVg9SSdPNUG6aUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.46.1",
"@typescript-eslint/parser": "8.46.1",
"@typescript-eslint/typescript-estree": "8.46.1",
"@typescript-eslint/utils": "8.46.1"
"@typescript-eslint/eslint-plugin": "8.46.0",
"@typescript-eslint/parser": "8.46.0",
"@typescript-eslint/typescript-estree": "8.46.0",
"@typescript-eslint/utils": "8.46.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View File

@@ -24,7 +24,7 @@
"dependencies": {
"@radix-ui/react-slot": "^1.2.3",
"@t3-oss/env-nextjs": "^0.13.8",
"@tanstack/react-query": "^5.90.3",
"@tanstack/react-query": "^5.87.4",
"@trpc/client": "^11.6.0",
"@trpc/react-query": "^11.6.0",
"@trpc/server": "^11.6.0",
@@ -39,7 +39,7 @@
"clsx": "^2.1.1",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.545.0",
"next": "^15.5.5",
"next": "^15.5.3",
"node-pty": "^1.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -61,9 +61,9 @@
"@types/bcryptjs": "^3.0.0",
"@types/better-sqlite3": "^7.6.8",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^24.7.2",
"@types/node": "^24.7.1",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.2.2",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^5.0.2",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
@@ -75,7 +75,7 @@
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^4.1.14",
"typescript": "^5.8.2",
"typescript-eslint": "^8.46.1",
"typescript-eslint": "^8.27.0",
"vitest": "^3.2.4"
},
"ct3aMetadata": {

41
scripts/json/openwrt.json Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "OpenWrt",
"slug": "openwrt",
"categories": [
4,
2
],
"date_created": "2024-05-02",
"type": "vm",
"updateable": true,
"privileged": false,
"interface_port": null,
"documentation": "https://openwrt.org/docs/start",
"website": "https://openwrt.org/",
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/openwrt.webp",
"config_path": "",
"description": "OpenWrt is a powerful open-source firmware that can transform a wide range of networking devices into highly customizable and feature-rich routers, providing users with greater control and flexibility over their network infrastructure.",
"install_methods": [
{
"type": "default",
"script": "vm/openwrt.sh",
"resources": {
"cpu": 1,
"ram": 256,
"hdd": 0.5,
"os": null,
"version": null
}
}
],
"default_credentials": {
"username": null,
"password": null
},
"notes": [
{
"text": "If you use VLANs (default LAN is set to VLAN 999), make sure the Proxmox Linux Bridge is configured as VLAN-aware, otherwise the VM may fail to start.",
"type": "info"
}
]
}

35
scripts/json/petio.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "Petio",
"slug": "petio",
"categories": [
13
],
"date_created": "2024-06-12",
"type": "ct",
"updateable": true,
"privileged": false,
"interface_port": 7777,
"documentation": "https://docs.petio.tv/",
"website": "https://petio.tv/",
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/petio.webp",
"config_path": "",
"description": "Petio is a third party companion app available to Plex server owners to allow their users to request, review and discover content.",
"install_methods": [
{
"type": "default",
"script": "ct/petio.sh",
"resources": {
"cpu": 2,
"ram": 1024,
"hdd": 4,
"os": "ubuntu",
"version": "20.04"
}
}
],
"default_credentials": {
"username": null,
"password": null
},
"notes": []
}

144
server.js
View File

@@ -51,7 +51,6 @@ const handle = app.getRequestHandler();
* @property {string} [mode]
* @property {ServerInfo} [server]
* @property {boolean} [isUpdate]
* @property {boolean} [isShell]
* @property {string} [containerId]
*/
@@ -208,15 +207,13 @@ class ScriptExecutionHandler {
* @param {WebSocketMessage} message
*/
async handleMessage(ws, message) {
const { action, scriptPath, executionId, input, mode, server, isUpdate, isShell, containerId } = message;
const { action, scriptPath, executionId, input, mode, server, isUpdate, containerId } = message;
switch (action) {
case 'start':
if (scriptPath && executionId) {
if (isUpdate && containerId) {
await this.startUpdateExecution(ws, containerId, executionId, mode, server);
} else if (isShell && containerId) {
await this.startShellExecution(ws, containerId, executionId, mode, server);
} else {
await this.startScriptExecution(ws, scriptPath, executionId, mode, server);
}
@@ -712,145 +709,6 @@ class ScriptExecutionHandler {
});
}
}
/**
* Start shell execution
* @param {ExtendedWebSocket} ws
* @param {string} containerId
* @param {string} executionId
* @param {string} mode
* @param {ServerInfo|null} server
*/
async startShellExecution(ws, containerId, executionId, mode = 'local', server = null) {
try {
// Send start message
this.sendMessage(ws, {
type: 'start',
data: `Starting shell session for container ${containerId}...`,
timestamp: Date.now()
});
if (mode === 'ssh' && server) {
await this.startSSHShellExecution(ws, containerId, executionId, server);
} else {
await this.startLocalShellExecution(ws, containerId, executionId);
}
} catch (error) {
this.sendMessage(ws, {
type: 'error',
data: `Failed to start shell: ${error instanceof Error ? error.message : String(error)}`,
timestamp: Date.now()
});
}
}
/**
* Start local shell execution
* @param {ExtendedWebSocket} ws
* @param {string} containerId
* @param {string} executionId
*/
async startLocalShellExecution(ws, containerId, executionId) {
const { spawn } = await import('node-pty');
// Create a shell process that will run pct enter
const childProcess = spawn('bash', ['-c', `pct enter ${containerId}`], {
name: 'xterm-color',
cols: 80,
rows: 24,
cwd: process.cwd(),
env: process.env
});
// Store the execution
this.activeExecutions.set(executionId, {
process: childProcess,
ws
});
// Handle pty data
childProcess.onData((data) => {
this.sendMessage(ws, {
type: 'output',
data: data.toString(),
timestamp: Date.now()
});
});
// Note: No automatic command is sent - user can type commands interactively
// Handle process exit
childProcess.onExit((e) => {
this.sendMessage(ws, {
type: 'end',
data: `Shell session ended with exit code: ${e.exitCode}`,
timestamp: Date.now()
});
this.activeExecutions.delete(executionId);
});
}
/**
* Start SSH shell execution
* @param {ExtendedWebSocket} ws
* @param {string} containerId
* @param {string} executionId
* @param {ServerInfo} server
*/
async startSSHShellExecution(ws, containerId, executionId, server) {
const sshService = getSSHExecutionService();
try {
const execution = await sshService.executeCommand(
server,
`pct enter ${containerId}`,
/** @param {string} data */
(data) => {
this.sendMessage(ws, {
type: 'output',
data: data,
timestamp: Date.now()
});
},
/** @param {string} error */
(error) => {
this.sendMessage(ws, {
type: 'error',
data: error,
timestamp: Date.now()
});
},
/** @param {number} code */
(code) => {
this.sendMessage(ws, {
type: 'end',
data: `Shell session ended with exit code: ${code}`,
timestamp: Date.now()
});
this.activeExecutions.delete(executionId);
}
);
// Store the execution
this.activeExecutions.set(executionId, {
process: /** @type {any} */ (execution).process,
ws
});
// Note: No automatic command is sent - user can type commands interactively
} catch (error) {
this.sendMessage(ws, {
type: 'error',
data: `SSH shell execution failed: ${error instanceof Error ? error.message : String(error)}`,
timestamp: Date.now()
});
}
}
}
// TerminalHandler removed - not used by current application

View File

@@ -385,7 +385,7 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr
);
}
if (!downloadedScripts?.length) {
if (!downloadedScripts || downloadedScripts.length === 0) {
return (
<div className="text-center py-12">
<div className="text-muted-foreground">

View File

@@ -12,10 +12,10 @@ export function Footer({ onOpenReleaseNotes }: FooterProps) {
const { data: versionData } = api.version.getCurrentVersion.useQuery();
return (
<footer className="sticky bottom-0 mt-auto border-t border-border bg-muted/30 py-3 backdrop-blur-sm">
<footer className="sticky bottom-0 mt-auto border-t border-border bg-muted/30 py-6 backdrop-blur-sm">
<div className="container mx-auto px-4">
<div className="flex flex-col sm:flex-row items-center justify-between gap-2 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-4">
<span>© 2024 PVE Scripts Local</span>
{versionData?.success && versionData.version && (
<Button
@@ -29,7 +29,7 @@ export function Footer({ onOpenReleaseNotes }: FooterProps) {
)}
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="sm"

View File

@@ -20,10 +20,6 @@ interface InstalledScript {
server_ip: string | null;
server_user: string | null;
server_password: string | null;
server_auth_type: string | null;
server_ssh_key: string | null;
server_ssh_key_passphrase: string | null;
server_ssh_port: number | null;
server_color: string | null;
installation_date: string;
status: 'in_progress' | 'success' | 'failed';
@@ -39,7 +35,6 @@ export function InstalledScriptsTab() {
const [sortField, setSortField] = useState<'script_name' | 'container_id' | 'server_name' | 'status' | 'installation_date'>('server_name');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
const [updatingScript, setUpdatingScript] = useState<{ id: number; containerId: string; server?: any } | null>(null);
const [openingShell, setOpeningShell] = useState<{ id: number; containerId: string; server?: any } | null>(null);
const [editingScriptId, setEditingScriptId] = useState<number | null>(null);
const [editFormData, setEditFormData] = useState<{ script_name: string; container_id: string }>({ script_name: '', container_id: '' });
const [showAddForm, setShowAddForm] = useState(false);
@@ -64,7 +59,6 @@ export function InstalledScriptsTab() {
} | null>(null);
const [controllingScriptId, setControllingScriptId] = useState<number | null>(null);
const scriptsRef = useRef<InstalledScript[]>([]);
const statusCheckTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Error modal state
const [errorModal, setErrorModal] = useState<{
@@ -318,34 +312,17 @@ export function InstalledScriptsTab() {
// Function to fetch container statuses - simplified to just check all servers
const fetchContainerStatuses = useCallback(() => {
console.log('fetchContainerStatuses called, isPending:', containerStatusMutation.isPending);
const currentScripts = scriptsRef.current;
// Prevent multiple simultaneous status checks
if (containerStatusMutation.isPending) {
console.log('Status check already pending, skipping');
return;
}
// Clear any existing timeout
if (statusCheckTimeoutRef.current) {
clearTimeout(statusCheckTimeoutRef.current);
}
// Debounce status checks by 500ms
statusCheckTimeoutRef.current = setTimeout(() => {
const currentScripts = scriptsRef.current;
// Get unique server IDs from scripts
const serverIds = [...new Set(currentScripts
.filter(script => script.server_id)
.map(script => script.server_id!))];
// Get unique server IDs from scripts
const serverIds = [...new Set(currentScripts
.filter(script => script.server_id)
.map(script => script.server_id!))];
console.log('Executing status check for server IDs:', serverIds);
if (serverIds.length > 0) {
containerStatusMutation.mutate({ serverIds });
}
}, 500);
}, [containerStatusMutation]);
if (serverIds.length > 0) {
containerStatusMutation.mutate({ serverIds });
}
}, []); // Empty dependency array to prevent infinite loops
// Run cleanup when component mounts and scripts are loaded (only once)
useEffect(() => {
@@ -356,22 +333,17 @@ export function InstalledScriptsTab() {
}, [scripts.length, serversData?.servers, cleanupMutation]);
// Note: Individual status fetching removed - using bulk fetchContainerStatuses instead
// Trigger status check when tab becomes active (component mounts)
useEffect(() => {
if (scripts.length > 0) {
console.log('Status check triggered - scripts length:', scripts.length);
fetchContainerStatuses();
}
}, [scripts.length, fetchContainerStatuses]);
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (statusCheckTimeoutRef.current) {
clearTimeout(statusCheckTimeoutRef.current);
}
};
}, []);
}, [scripts.length]); // Only depend on scripts.length to prevent infinite loops
// Update scripts with container statuses
const scriptsWithStatus = scripts.map(script => ({
...script,
container_status: script.container_id ? containerStatuses.get(script.id) ?? 'unknown' : undefined
@@ -531,17 +503,13 @@ export function InstalledScriptsTab() {
onConfirm: () => {
// Get server info if it's SSH mode
let server = null;
if (script.server_id && script.server_user) {
if (script.server_id && script.server_user && script.server_password) {
server = {
id: script.server_id,
name: script.server_name,
ip: script.server_ip,
user: script.server_user,
password: script.server_password,
auth_type: script.server_auth_type ?? 'password',
ssh_key: script.server_ssh_key,
ssh_key_passphrase: script.server_ssh_key_passphrase,
ssh_port: script.server_ssh_port ?? 22
password: script.server_password
};
}
@@ -559,91 +527,6 @@ export function InstalledScriptsTab() {
setUpdatingScript(null);
};
const handleOpenShell = (script: InstalledScript) => {
if (!script.container_id) {
setErrorModal({
isOpen: true,
title: 'Shell Access Failed',
message: 'No Container ID available for this script',
details: 'This script does not have a valid container ID and cannot be accessed via shell.'
});
return;
}
// Get server info if it's SSH mode
let server = null;
if (script.server_id && script.server_user) {
server = {
id: script.server_id,
name: script.server_name,
ip: script.server_ip,
user: script.server_user,
password: script.server_password,
auth_type: script.server_auth_type ?? 'password',
ssh_key: script.server_ssh_key,
ssh_key_passphrase: script.server_ssh_key_passphrase,
ssh_port: script.server_ssh_port ?? 22
};
}
setOpeningShell({
id: script.id,
containerId: script.container_id,
server: server
});
};
const handleCloseShellTerminal = () => {
setOpeningShell(null);
};
// Auto-scroll to terminals when they open
useEffect(() => {
if (openingShell) {
// Small delay to ensure the terminal is rendered
setTimeout(() => {
const terminalElement = document.querySelector('[data-terminal="shell"]');
if (terminalElement) {
// Scroll to the terminal with smooth animation
terminalElement.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'nearest'
});
// Add a subtle highlight effect
terminalElement.classList.add('animate-pulse');
setTimeout(() => {
terminalElement.classList.remove('animate-pulse');
}, 2000);
}
}, 200);
}
}, [openingShell]);
useEffect(() => {
if (updatingScript) {
// Small delay to ensure the terminal is rendered
setTimeout(() => {
const terminalElement = document.querySelector('[data-terminal="update"]');
if (terminalElement) {
// Scroll to the terminal with smooth animation
terminalElement.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'nearest'
});
// Add a subtle highlight effect
terminalElement.classList.add('animate-pulse');
setTimeout(() => {
terminalElement.classList.remove('animate-pulse');
}, 2000);
}
}, 200);
}
}, [updatingScript]);
const handleEditScript = (script: InstalledScript) => {
setEditingScriptId(script.id);
setEditFormData({
@@ -756,7 +639,7 @@ export function InstalledScriptsTab() {
<div className="space-y-6">
{/* Update Terminal */}
{updatingScript && (
<div className="mb-8" data-terminal="update">
<div className="mb-8">
<Terminal
scriptPath={`update-${updatingScript.containerId}`}
onClose={handleCloseUpdateTerminal}
@@ -768,20 +651,6 @@ export function InstalledScriptsTab() {
</div>
)}
{/* Shell Terminal */}
{openingShell && (
<div className="mb-8" data-terminal="shell">
<Terminal
scriptPath={`shell-${openingShell.containerId}`}
onClose={handleCloseShellTerminal}
mode={openingShell.server ? 'ssh' : 'local'}
server={openingShell.server}
isShell={true}
containerId={openingShell.containerId}
/>
</div>
)}
{/* Header with Stats */}
<div className="bg-card rounded-lg shadow p-6">
<h2 className="text-2xl font-bold text-foreground mb-4">Installed Scripts</h2>
@@ -1103,7 +972,6 @@ export function InstalledScriptsTab() {
onSave={handleSaveEdit}
onCancel={handleCancelEdit}
onUpdate={() => handleUpdateScript(script)}
onShell={() => handleOpenShell(script)}
onDelete={() => handleDeleteScript(Number(script.id))}
isUpdating={updateScriptMutation.isPending}
isDeleting={deleteScriptMutation.isPending}
@@ -1199,14 +1067,15 @@ export function InstalledScriptsTab() {
>
<td className="px-6 py-4 whitespace-nowrap">
{editingScriptId === script.id ? (
<div className="flex items-center min-h-[2.5rem]">
<div className="space-y-2">
<input
type="text"
value={editFormData.script_name}
onChange={(e) => handleInputChange('script_name', e.target.value)}
className="w-full px-3 py-2 text-sm font-medium border border-input rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
className="w-full px-2 py-1 text-sm border border-border rounded bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="Script name"
/>
<div className="text-xs text-muted-foreground">{script.script_path}</div>
</div>
) : (
<div>
@@ -1217,15 +1086,13 @@ export function InstalledScriptsTab() {
</td>
<td className="px-6 py-4 whitespace-nowrap">
{editingScriptId === script.id ? (
<div className="flex items-center min-h-[2.5rem]">
<input
type="text"
value={editFormData.container_id}
onChange={(e) => handleInputChange('container_id', e.target.value)}
className="w-full px-3 py-2 text-sm font-mono border border-input rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
placeholder="Container ID"
/>
</div>
<input
type="text"
value={editFormData.container_id}
onChange={(e) => handleInputChange('container_id', e.target.value)}
className="w-full px-2 py-1 text-sm font-mono border border-border rounded bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="Container ID"
/>
) : (
script.container_id ? (
<div className="flex items-center space-x-2">
@@ -1312,17 +1179,6 @@ export function InstalledScriptsTab() {
Update
</Button>
)}
{/* Shell button - only show for SSH scripts with container_id */}
{script.container_id && script.execution_mode === 'ssh' && (
<Button
onClick={() => handleOpenShell(script)}
variant="secondary"
size="sm"
disabled={containerStatuses.get(script.id) === 'stopped'}
>
Shell
</Button>
)}
{/* Container Control Buttons - only show for SSH scripts with container_id */}
{script.container_id && script.execution_mode === 'ssh' && (
<>
@@ -1331,10 +1187,6 @@ export function InstalledScriptsTab() {
disabled={controllingScriptId === script.id || (containerStatuses.get(script.id) ?? 'unknown') === 'unknown'}
variant={(containerStatuses.get(script.id) ?? 'unknown') === 'running' ? 'destructive' : 'default'}
size="sm"
className={(containerStatuses.get(script.id) ?? 'unknown') === 'running'
? "bg-red-600 hover:bg-red-700 text-white border border-red-500 hover:border-red-400 hover:scale-105 hover:shadow-lg hover:shadow-red-500/25 transition-all duration-200 disabled:hover:scale-100"
: "bg-green-600 hover:bg-green-700 text-white border border-green-500 hover:border-green-400 hover:scale-105 hover:shadow-lg hover:shadow-green-500/25 transition-all duration-200 disabled:hover:scale-100"
}
>
{controllingScriptId === script.id ? 'Working...' : (containerStatuses.get(script.id) ?? 'unknown') === 'running' ? 'Stop' : 'Start'}
</Button>
@@ -1343,7 +1195,6 @@ export function InstalledScriptsTab() {
disabled={controllingScriptId === script.id}
variant="destructive"
size="sm"
className="bg-red-800 hover:bg-red-900 text-white border border-red-600 hover:border-red-500 hover:scale-105 hover:shadow-lg hover:shadow-red-600/30 transition-all duration-200 disabled:hover:scale-100"
>
{controllingScriptId === script.id ? 'Working...' : 'Destroy'}
</Button>

View File

@@ -93,22 +93,9 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK
);
if (keyLine) {
let keyType = 'Unknown';
// Check for traditional PEM format keys
if (keyLine.includes('RSA')) {
keyType = 'RSA';
} else if (keyLine.includes('ED25519')) {
keyType = 'ED25519';
} else if (keyLine.includes('ECDSA')) {
keyType = 'ECDSA';
} else if (keyLine.includes('OPENSSH PRIVATE KEY')) {
// For OpenSSH format keys, try to detect type from the key content
// This is a heuristic - OpenSSH ED25519 keys typically start with specific patterns
// We'll default to "OpenSSH" for now since we can't reliably detect the type
keyType = 'OpenSSH';
}
const keyType = keyLine.includes('RSA') ? 'RSA' :
keyLine.includes('ED25519') ? 'ED25519' :
keyLine.includes('ECDSA') ? 'ECDSA' : 'Unknown';
return `${keyType} key (${keyContent.length} characters)`;
}
@@ -155,7 +142,7 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK
<input
ref={fileInputRef}
type="file"
accept=".pem,.key,.id_rsa,.id_ed25519,.id_ecdsa,ed25519,id_rsa,id_ed25519,id_ecdsa,*"
accept=".pem,.key,.id_rsa,.id_ed25519,.id_ecdsa"
onChange={handleFileSelect}
className="hidden"
disabled={disabled}
@@ -166,7 +153,7 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK
Drag and drop your SSH private key here, or click to browse
</p>
<p className="text-xs text-muted-foreground">
Supported formats: RSA, ED25519, ECDSA (.pem, .key, .id_rsa, ed25519, etc.)
Supported formats: RSA, ED25519, ECDSA (.pem, .key, .id_rsa, etc.)
</p>
</div>
</div>

View File

@@ -14,10 +14,6 @@ interface InstalledScript {
server_ip: string | null;
server_user: string | null;
server_password: string | null;
server_auth_type: string | null;
server_ssh_key: string | null;
server_ssh_key_passphrase: string | null;
server_ssh_port: number | null;
server_color: string | null;
installation_date: string;
status: 'in_progress' | 'success' | 'failed';
@@ -35,7 +31,6 @@ interface ScriptInstallationCardProps {
onSave: () => void;
onCancel: () => void;
onUpdate: () => void;
onShell: () => void;
onDelete: () => void;
isUpdating: boolean;
isDeleting: boolean;
@@ -55,7 +50,6 @@ export function ScriptInstallationCard({
onSave,
onCancel,
onUpdate,
onShell,
onDelete,
isUpdating,
isDeleting,
@@ -209,18 +203,6 @@ export function ScriptInstallationCard({
Update
</Button>
)}
{/* Shell button - only show for SSH scripts with container_id */}
{script.container_id && script.execution_mode === 'ssh' && (
<Button
onClick={onShell}
variant="secondary"
size="sm"
className="flex-1 min-w-0"
disabled={containerStatus === 'stopped'}
>
Shell
</Button>
)}
{/* Container Control Buttons - only show for SSH scripts with container_id */}
{script.container_id && script.execution_mode === 'ssh' && (
<>

View File

@@ -571,7 +571,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
);
}
if (!scriptsWithStatus?.length) {
if (!scriptsWithStatus || scriptsWithStatus.length === 0) {
return (
<div className="text-center py-12">
<div className="text-muted-foreground">
@@ -626,13 +626,11 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
<Button
onClick={handleBatchDownload}
disabled={loadSingleScriptMutation.isPending}
variant="outline"
size="sm"
className="bg-blue-500/10 hover:bg-blue-500/20 border-blue-500/30 text-blue-300 hover:text-blue-200 hover:border-blue-400/50"
className="bg-blue-600 hover:bg-blue-700 text-white"
>
{loadSingleScriptMutation.isPending ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current mr-2"></div>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Downloading...
</>
) : (
@@ -644,7 +642,6 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
onClick={handleDownloadAllFiltered}
disabled={filteredScripts.length === 0 || loadSingleScriptMutation.isPending}
variant="outline"
size="sm"
>
{loadSingleScriptMutation.isPending ? (
<>
@@ -660,8 +657,8 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
{selectedSlugs.size > 0 && (
<Button
onClick={clearSelection}
variant="outline"
size="default"
variant="ghost"
size="sm"
>
Clear Selection
</Button>
@@ -670,8 +667,8 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
{filteredScripts.length > 0 && (
<Button
onClick={selectAllVisible}
variant="outline"
size="default"
variant="ghost"
size="sm"
>
Select All Visible
</Button>

View File

@@ -4,7 +4,6 @@ import { useState } from 'react';
import type { Server, CreateServerData } from '../../types/server';
import { ServerForm } from './ServerForm';
import { Button } from './ui/button';
import { ConfirmationModal } from './ConfirmationModal';
interface ServerListProps {
servers: Server[];
@@ -16,14 +15,6 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) {
const [editingId, setEditingId] = useState<number | null>(null);
const [testingConnections, setTestingConnections] = useState<Set<number>>(new Set());
const [connectionResults, setConnectionResults] = useState<Map<number, { success: boolean; message: string }>>(new Map());
const [confirmationModal, setConfirmationModal] = useState<{
isOpen: boolean;
variant: 'danger';
title: string;
message: string;
confirmText: string;
onConfirm: () => void;
} | null>(null);
const handleEdit = (server: Server) => {
setEditingId(server.id);
@@ -41,20 +32,9 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) {
};
const handleDelete = (id: number) => {
const server = servers.find(s => s.id === id);
if (!server) return;
setConfirmationModal({
isOpen: true,
variant: 'danger',
title: 'Delete Server',
message: `This will permanently delete the server configuration "${server.name}" (${server.ip}) and all associated installed scripts. This action cannot be undone!`,
confirmText: server.name,
onConfirm: () => {
onDelete(id);
setConfirmationModal(null);
}
});
if (window.confirm('Are you sure you want to delete this server configuration?')) {
onDelete(id);
}
};
const handleTestConnection = async (server: Server) => {
@@ -248,21 +228,6 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) {
)}
</div>
))}
{/* Confirmation Modal */}
{confirmationModal && (
<ConfirmationModal
isOpen={confirmationModal.isOpen}
onClose={() => setConfirmationModal(null)}
onConfirm={confirmationModal.onConfirm}
title={confirmationModal.title}
message={confirmationModal.message}
variant={confirmationModal.variant}
confirmText={confirmationModal.confirmText}
confirmButtonText="Delete Server"
cancelButtonText="Cancel"
/>
)}
</div>
);
}

View File

@@ -11,7 +11,6 @@ interface TerminalProps {
mode?: 'local' | 'ssh';
server?: any;
isUpdate?: boolean;
isShell?: boolean;
containerId?: string;
}
@@ -21,7 +20,7 @@ interface TerminalMessage {
timestamp: number;
}
export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate = false, isShell = false, containerId }: TerminalProps) {
export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate = false, containerId }: TerminalProps) {
const [isConnected, setIsConnected] = useState(false);
const [isRunning, setIsRunning] = useState(false);
const [isClient, setIsClient] = useState(false);
@@ -333,7 +332,6 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
mode,
server,
isUpdate,
isShell,
containerId
};
ws.send(JSON.stringify(message));
@@ -374,7 +372,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
wsRef.current.close();
}
};
}, [scriptPath, mode, server, isUpdate, isShell, containerId, isMobile]); // eslint-disable-line react-hooks/exhaustive-deps
}, [scriptPath, mode, server, isUpdate, containerId, isMobile]); // eslint-disable-line react-hooks/exhaustive-deps
const startScript = () => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN && !isRunning) {
@@ -390,7 +388,6 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
mode,
server,
isUpdate,
isShell,
containerId
}));
}

View File

@@ -173,9 +173,6 @@ export async function DELETE(
);
}
// Delete all installed scripts associated with this server
db.deleteInstalledScriptsByServer(id);
const result = db.deleteServer(id);
return NextResponse.json(

View File

@@ -20,13 +20,7 @@ import { api } from '~/trpc/react';
export default function Home() {
const [runningScript, setRunningScript] = useState<{ path: string; name: string; mode?: 'local' | 'ssh'; server?: any } | null>(null);
const [activeTab, setActiveTab] = useState<'scripts' | 'downloaded' | 'installed'>(() => {
if (typeof window !== 'undefined') {
const savedTab = localStorage.getItem('activeTab') as 'scripts' | 'downloaded' | 'installed';
return savedTab || 'scripts';
}
return 'scripts';
});
const [activeTab, setActiveTab] = useState<'scripts' | 'downloaded' | 'installed'>('scripts');
const [releaseNotesOpen, setReleaseNotesOpen] = useState(false);
const [highlightVersion, setHighlightVersion] = useState<string | undefined>(undefined);
const terminalRef = useRef<HTMLDivElement>(null);
@@ -37,13 +31,6 @@ export default function Home() {
const { data: installedScriptsData } = api.installedScripts.getAllInstalledScripts.useQuery();
const { data: versionData } = api.version.getCurrentVersion.useQuery();
// Save active tab to localStorage whenever it changes
useEffect(() => {
if (typeof window !== 'undefined') {
localStorage.setItem('activeTab', activeTab);
}
}, [activeTab]);
// Auto-show release notes modal after update
useEffect(() => {
if (versionData?.success && versionData.version) {
@@ -70,42 +57,15 @@ export default function Home() {
// Calculate script counts
const scriptCounts = {
available: (() => {
if (!scriptCardsData?.success) return 0;
// Deduplicate scripts using Map by slug (same logic as ScriptsGrid.tsx)
const scriptMap = new Map<string, any>();
scriptCardsData.cards?.forEach(script => {
if (script?.name && script?.slug) {
// Use slug as unique identifier, only keep first occurrence
if (!scriptMap.has(script.slug)) {
scriptMap.set(script.slug, script);
}
}
});
return scriptMap.size;
})(),
available: scriptCardsData?.success ? scriptCardsData.cards?.length ?? 0 : 0,
downloaded: (() => {
if (!scriptCardsData?.success || !localScriptsData?.scripts) return 0;
// First deduplicate GitHub scripts using Map by slug
const scriptMap = new Map<string, any>();
scriptCardsData.cards?.forEach(script => {
if (script?.name && script?.slug) {
if (!scriptMap.has(script.slug)) {
scriptMap.set(script.slug, script);
}
}
});
const deduplicatedGithubScripts = Array.from(scriptMap.values());
// Count scripts that are both in GitHub data and have local versions
const githubScripts = scriptCardsData.cards ?? [];
const localScripts = localScriptsData.scripts ?? [];
// Count scripts that are both in deduplicated GitHub data and have local versions
return deduplicatedGithubScripts.filter(script => {
return githubScripts.filter(script => {
if (!script?.name) return false;
return localScripts.some(local => {
if (!local?.name) return false;
@@ -171,58 +131,64 @@ export default function Home() {
{/* Tab Navigation */}
<div className="mb-6 sm:mb-8">
<div className="border-b border-border">
<nav className="-mb-px flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-1">
<Button
variant="ghost"
size="null"
onClick={() => setActiveTab('scripts')}
className={`px-3 py-2 text-sm flex items-center justify-center sm:justify-start gap-2 w-full sm:w-auto ${
activeTab === 'scripts'
? 'bg-accent text-accent-foreground rounded-t-md rounded-b-none'
: 'hover:bg-accent hover:text-accent-foreground hover:rounded-t-md hover:rounded-b-none'
}`}>
<Package className="h-4 w-4" />
<span className="hidden sm:inline">Available Scripts</span>
<span className="sm:hidden">Available</span>
<span className="ml-1 px-2 py-0.5 text-xs bg-muted text-muted-foreground rounded-full">
{scriptCounts.available}
</span>
<nav className="-mb-px flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2 lg:space-x-8">
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="null"
onClick={() => setActiveTab('scripts')}
className={`px-3 py-2 text-sm flex items-center justify-center sm:justify-start gap-2 w-full sm:w-auto ${
activeTab === 'scripts'
? 'bg-accent text-accent-foreground rounded-t-md rounded-b-none'
: 'hover:bg-accent hover:text-accent-foreground hover:rounded-t-md hover:rounded-b-none'
}`}>
<Package className="h-4 w-4" />
<span className="hidden sm:inline">Available Scripts</span>
<span className="sm:hidden">Available</span>
<span className="ml-1 px-2 py-0.5 text-xs bg-muted text-muted-foreground rounded-full">
{scriptCounts.available}
</span>
</Button>
<ContextualHelpIcon section="available-scripts" tooltip="Help with Available Scripts" />
</Button>
<Button
variant="ghost"
size="null"
onClick={() => setActiveTab('downloaded')}
className={`px-3 py-2 text-sm flex items-center justify-center sm:justify-start gap-2 w-full sm:w-auto ${
activeTab === 'downloaded'
? 'bg-accent text-accent-foreground rounded-t-md rounded-b-none'
: 'hover:bg-accent hover:text-accent-foreground hover:rounded-t-md hover:rounded-b-none'
}`}>
<HardDrive className="h-4 w-4" />
<span className="hidden sm:inline">Downloaded Scripts</span>
<span className="sm:hidden">Downloaded</span>
<span className="ml-1 px-2 py-0.5 text-xs bg-muted text-muted-foreground rounded-full">
{scriptCounts.downloaded}
</span>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="null"
onClick={() => setActiveTab('downloaded')}
className={`px-3 py-2 text-sm flex items-center justify-center sm:justify-start gap-2 w-full sm:w-auto ${
activeTab === 'downloaded'
? 'bg-accent text-accent-foreground rounded-t-md rounded-b-none'
: 'hover:bg-accent hover:text-accent-foreground hover:rounded-t-md hover:rounded-b-none'
}`}>
<HardDrive className="h-4 w-4" />
<span className="hidden sm:inline">Downloaded Scripts</span>
<span className="sm:hidden">Downloaded</span>
<span className="ml-1 px-2 py-0.5 text-xs bg-muted text-muted-foreground rounded-full">
{scriptCounts.downloaded}
</span>
</Button>
<ContextualHelpIcon section="downloaded-scripts" tooltip="Help with Downloaded Scripts" />
</Button>
<Button
variant="ghost"
size="null"
onClick={() => setActiveTab('installed')}
className={`px-3 py-2 text-sm flex items-center justify-center sm:justify-start gap-2 w-full sm:w-auto ${
activeTab === 'installed'
? 'bg-accent text-accent-foreground rounded-t-md rounded-b-none'
: 'hover:bg-accent hover:text-accent-foreground hover:rounded-t-md hover:rounded-b-none'
}`}>
<FolderOpen className="h-4 w-4" />
<span className="hidden sm:inline">Installed Scripts</span>
<span className="sm:hidden">Installed</span>
<span className="ml-1 px-2 py-0.5 text-xs bg-muted text-muted-foreground rounded-full">
{scriptCounts.installed}
</span>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="null"
onClick={() => setActiveTab('installed')}
className={`px-3 py-2 text-sm flex items-center justify-center sm:justify-start gap-2 w-full sm:w-auto ${
activeTab === 'installed'
? 'bg-accent text-accent-foreground rounded-t-md rounded-b-none'
: 'hover:bg-accent hover:text-accent-foreground hover:rounded-t-md hover:rounded-b-none'
}`}>
<FolderOpen className="h-4 w-4" />
<span className="hidden sm:inline">Installed Scripts</span>
<span className="sm:hidden">Installed</span>
<span className="ml-1 px-2 py-0.5 text-xs bg-muted text-muted-foreground rounded-full">
{scriptCounts.installed}
</span>
</Button>
<ContextualHelpIcon section="installed-scripts" tooltip="Help with Installed Scripts" />
</Button>
</div>
</nav>
</div>
</div>

View File

@@ -3,7 +3,7 @@
* to ensure optimal readability based on luminance
*/
export function getContrastColor(hexColor: string): 'black' | 'white' {
if (!hexColor?.length || hexColor.length !== 7 || !hexColor.startsWith('#')) {
if (!hexColor || hexColor.length !== 7 || !hexColor.startsWith('#')) {
return 'black'; // Default to black for invalid colors
}

View File

@@ -551,31 +551,23 @@ export const installedScriptsRouter = createTRPCRouter({
const listCommand = 'pct list';
let listOutput = '';
// Add timeout to prevent hanging connections
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('SSH command timeout after 30 seconds')), 30000);
await new Promise<void>((resolve, reject) => {
void sshExecutionService.executeCommand(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
server as any,
listCommand,
(data: string) => {
listOutput += data;
},
(error: string) => {
console.error(`pct list error on server ${(server as any).name}:`, error);
reject(new Error(error));
},
(_exitCode: number) => {
resolve();
}
);
});
await Promise.race([
new Promise<void>((resolve, reject) => {
void sshExecutionService.executeCommand(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
server as any,
listCommand,
(data: string) => {
listOutput += data;
},
(error: string) => {
console.error(`pct list error on server ${(server as any).name}:`, error);
reject(new Error(error));
},
(_exitCode: number) => {
resolve();
}
);
}),
timeoutPromise
]);
// Parse pct list output
const lines = listOutput.split('\n').filter(line => line.trim());
@@ -914,8 +906,9 @@ export const installedScriptsRouter = createTRPCRouter({
);
});
}
} catch {
} catch (_error) {
// If status check fails, continue with destroy attempt
// The destroy command will handle the error appropriately
}
// Execute destroy command

View File

@@ -90,7 +90,7 @@ class DatabaseService {
installation_date DATETIME DEFAULT CURRENT_TIMESTAMP,
status TEXT NOT NULL CHECK(status IN ('in_progress', 'success', 'failed')),
output_log TEXT,
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE SET NULL
)
`);
@@ -180,10 +180,6 @@ class DatabaseService {
s.ip as server_ip,
s.user as server_user,
s.password as server_password,
s.auth_type as server_auth_type,
s.ssh_key as server_ssh_key,
s.ssh_key_passphrase as server_ssh_key_passphrase,
s.ssh_port as server_ssh_port,
s.color as server_color
FROM installed_scripts inst
LEFT JOIN servers s ON inst.server_id = s.id
@@ -276,14 +272,6 @@ class DatabaseService {
return stmt.run(id);
}
/**
* @param {number} server_id
*/
deleteInstalledScriptsByServer(server_id) {
const stmt = this.db.prepare('DELETE FROM installed_scripts WHERE server_id = ?');
return stmt.run(server_id);
}
close() {
this.db.close();
}

View File

@@ -32,9 +32,7 @@ class SSHExecutionService {
const tempDir = mkdtempSync(join(tmpdir(), 'ssh-key-'));
const tempKeyPath = join(tempDir, 'private_key');
// Normalize the key: trim any trailing whitespace and ensure exactly one newline at the end
const normalizedKey = ssh_key.trimEnd() + '\n';
writeFileSync(tempKeyPath, normalizedKey);
writeFileSync(tempKeyPath, ssh_key);
chmodSync(tempKeyPath, 0o600); // Set proper permissions
return tempKeyPath;
@@ -297,7 +295,7 @@ class SSHExecutionService {
try {
unlinkSync(tempKeyPath);
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
rmdirSync(tempDir);
unlinkSync(tempDir);
} catch (cleanupError) {
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
}
@@ -316,7 +314,7 @@ class SSHExecutionService {
try {
unlinkSync(tempKeyPath);
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
rmdirSync(tempDir);
unlinkSync(tempDir);
} catch (cleanupError) {
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
}
@@ -330,7 +328,7 @@ class SSHExecutionService {
try {
unlinkSync(tempKeyPath);
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
rmdirSync(tempDir);
unlinkSync(tempDir);
} catch (cleanupError) {
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
}
@@ -385,7 +383,7 @@ class SSHExecutionService {
try {
unlinkSync(tempKeyPath);
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
rmdirSync(tempDir);
unlinkSync(tempDir);
} catch (cleanupError) {
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
}
@@ -416,7 +414,7 @@ class SSHExecutionService {
try {
unlinkSync(tempKeyPath);
const tempDir = tempKeyPath.substring(0, tempKeyPath.lastIndexOf('/'));
rmdirSync(tempDir);
unlinkSync(tempDir);
} catch (cleanupError) {
console.warn('Failed to clean up temporary SSH key file:', cleanupError);
}

View File

@@ -557,9 +557,7 @@ expect {
tempKeyPath = join(tempDir, 'private_key');
// Write the private key to temporary file
// Normalize the key: trim any trailing whitespace and ensure exactly one newline at the end
const normalizedKey = ssh_key.trimEnd() + '\n';
writeFileSync(tempKeyPath, normalizedKey);
writeFileSync(tempKeyPath, ssh_key);
chmodSync(tempKeyPath, 0o600); // Set proper permissions
// Build SSH command