From e67d8bdc0a30d400b6c8952b55595e804459eadf Mon Sep 17 00:00:00 2001 From: "@kirill.komarov" Date: Sun, 10 May 2026 16:49:55 +0500 Subject: [PATCH] deploy --- .gitignore | 2 + client/package-lock.json | 396 ++++++++++++++++++-------- client/package.json | 4 +- scripts/complete-lan-deploy.ps1 | 64 +++++ scripts/deploy-ssh.ps1 | 79 +++++ scripts/deploy-ssh.sh | 235 +++++++++++++++ scripts/deploy.env.example | 19 ++ scripts/read-deploy-env.ps1 | 14 + scripts/register-ssh-key-for-root.ps1 | 33 +++ scripts/server-bootstrap.sh | 202 +++++++++++++ 10 files changed, 935 insertions(+), 113 deletions(-) create mode 100644 scripts/complete-lan-deploy.ps1 create mode 100644 scripts/deploy-ssh.ps1 create mode 100644 scripts/deploy-ssh.sh create mode 100644 scripts/deploy.env.example create mode 100644 scripts/read-deploy-env.ps1 create mode 100644 scripts/register-ssh-key-for-root.ps1 create mode 100644 scripts/server-bootstrap.sh diff --git a/.gitignore b/.gitignore index c98bb59..237e884 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,7 @@ node_modules dist *.log .env +scripts/deploy.env +scripts/craftshop-remote-lan.env server/prisma/dev.db server/prisma/dev.db-journal diff --git a/client/package-lock.json b/client/package-lock.json index d52ddd3..37286ae 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -28,12 +28,12 @@ "swiper": "^12.1.3" }, "devDependencies": { - "@eslint/js": "^10.0.1", + "@eslint/js": "^9.39.4", "@types/node": "^24.12.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", - "eslint": "^10.2.1", + "eslint": "^9.39.4", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-boundaries": "^6.0.2", @@ -80,6 +80,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -313,29 +314,6 @@ "node": ">=18.18" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -405,6 +383,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -448,6 +427,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -536,89 +516,180 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.5", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^10.2.4" + "minimatch": "^3.1.5" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", - "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.2.1" + "@eslint/core": "^0.17.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } } }, "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", - "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.2.1", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@floating-ui/core": { @@ -631,17 +702,6 @@ "@floating-ui/utils": "^0.2.11" } }, - "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" - } - }, "node_modules/@floating-ui/utils": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", @@ -907,6 +967,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.0.0.tgz", "integrity": "sha512-+VP/oQCDhDR87NQQgXnNBG8dwy6GNuQLnenS1pZvkbn2dKFSxRSRMybTpH9xUxXP+316mlYDy5CSbYtusnCWtw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.29.2", "@mui/core-downloads-tracker": "^9.0.0", @@ -1453,6 +1514,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.22.5.tgz", "integrity": "sha512-L1lhWz6ujGny8LduTJ7MBWYhzigwOvfUJUrJ7IzOJSuy3+OAzisdGDD1GV7LEO/hU0Hr2Mkm1wajRIHExvS9HQ==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1675,6 +1737,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.22.5.tgz", "integrity": "sha512-cVO3ZHCgxAWZ4zrFSs81FO2nyCk1wb2EHkpLpW98FzbJLkN9rDkazhW99P3HRWy/CvUldOT+8ecI1YrQtBojMg==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1793,6 +1856,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.22.5.tgz", "integrity": "sha512-Ifg4MzKCj3uRqe3ieTwYnomu2y4p7EXr2avVSKZYfh12i2dyWe2Gkn1KuZDREANVE+gHqFlQjJRYzhJFwzSCrg==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1807,6 +1871,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.22.5.tgz", "integrity": "sha512-Cr9Mv4igxvI2tKMiahw48sZxva3PfDzypErH8IB82N+9qa9n9ygVMt0BOaDg53hLKxEEVeYr2S/wCcJIVFgBTw==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-commands": "^1.6.2", @@ -1900,17 +1965,10 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -1933,6 +1991,7 @@ "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1953,8 +2012,8 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1963,8 +2022,8 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2038,6 +2097,7 @@ "integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/types": "8.59.1", @@ -2200,6 +2260,7 @@ "integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.1", @@ -2610,6 +2671,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2660,6 +2722,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -2980,6 +3049,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -3430,6 +3500,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=11.0.0" } @@ -3679,30 +3750,34 @@ } }, "node_modules/eslint": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz", - "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.5.5", - "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.1", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", + "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -3712,7 +3787,8 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -3720,7 +3796,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://eslint.org/donate" @@ -3740,6 +3816,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -3887,6 +3964,7 @@ "integrity": "sha512-rM9K8UBHcWKpzQzStn1YRN2T5NvdeIfSVoKu/lKF41znQXHAUcBbYXe5wd6GNjZjTrP7viQ49n1D83x/2gYgIw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@package-json/types": "^0.0.12", "@typescript-eslint/types": "^8.56.0", @@ -4143,19 +4221,17 @@ } }, "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -4174,19 +4250,76 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.16.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -5334,6 +5467,19 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -5747,6 +5893,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -5774,6 +5927,7 @@ "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.24.0.tgz", "integrity": "sha512-ALyFxgtd5R+65UqZ/++lOqwWcC0SNho9c27fYSyLmG7AfnAul2o46F05aDJGPbFU57wos9dgcIySHs0Xe6ia3A==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@mapbox/jsonlint-lines-primitives": "^2.0.2", "@mapbox/point-geometry": "^1.1.0", @@ -6242,6 +6396,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6310,6 +6465,7 @@ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -6515,6 +6671,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6524,6 +6681,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -7245,6 +7403,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -7474,6 +7645,7 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7667,6 +7839,7 @@ "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -7893,6 +8066,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/client/package.json b/client/package.json index 68d9e96..788dd25 100644 --- a/client/package.json +++ b/client/package.json @@ -33,12 +33,12 @@ "swiper": "^12.1.3" }, "devDependencies": { - "@eslint/js": "^10.0.1", + "@eslint/js": "^9.39.4", "@types/node": "^24.12.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", - "eslint": "^10.2.1", + "eslint": "^9.39.4", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-boundaries": "^6.0.2", diff --git a/scripts/complete-lan-deploy.ps1 b/scripts/complete-lan-deploy.ps1 new file mode 100644 index 0000000..123a76e --- /dev/null +++ b/scripts/complete-lan-deploy.ps1 @@ -0,0 +1,64 @@ +# First-time LAN deploy: bootstrap, craftshop-remote-lan.env, deploy-ssh.ps1 -All, systemd. +# Prerequisites: SSH to root works with key (see register-ssh-key-for-root.ps1). + +$ErrorActionPreference = "Stop" +$scriptsDir = $PSScriptRoot +$repoRoot = (Resolve-Path (Join-Path $scriptsDir "..")).Path +$deployEnv = Join-Path $scriptsDir "deploy.env" + +if (-not (Test-Path $deployEnv)) { + Write-Error "Missing scripts/deploy.env - copy deploy.env.example and set DEPLOY_HOST." +} + +. "$PSScriptRoot\read-deploy-env.ps1" +Import-DeployDotEnv $deployEnv + +$deployHost = [Environment]::GetEnvironmentVariable("DEPLOY_HOST", "Process") +$user = [Environment]::GetEnvironmentVariable("DEPLOY_USER", "Process") +if ([string]::IsNullOrWhiteSpace($user)) { $user = "root" } +if ([string]::IsNullOrWhiteSpace($deployHost)) { + Write-Error "DEPLOY_HOST is missing in scripts/deploy.env." +} + +$remote = "${user}@${deployHost}" +$bootstrap = Join-Path $scriptsDir "server-bootstrap.sh" +$lanEnv = Join-Path $scriptsDir "craftshop-remote-lan.env" + +if (-not (Test-Path $bootstrap)) { + Write-Error "Bootstrap script not found: $bootstrap" +} + +if (-not (Test-Path $lanEnv)) { + Write-Error "Missing scripts/craftshop-remote-lan.env (gitignored). Create it or copy from server/.env.example." +} + +ssh -o BatchMode=yes -o ConnectTimeout=8 $remote "echo ok" 2>$null +if ($LASTEXITCODE -ne 0) { + Write-Host "Passwordless SSH to $remote failed. Run .\scripts\register-ssh-key-for-root.ps1 first." + exit 1 +} + +Write-Host ">>> scp bootstrap" +scp -o StrictHostKeyChecking=accept-new $bootstrap "${remote}:/root/server-bootstrap.sh" + +Write-Host ">>> run bootstrap on server" +ssh $remote "bash /root/server-bootstrap.sh" + +Write-Host ">>> scp server .env" +scp -o StrictHostKeyChecking=accept-new $lanEnv "${remote}:/opt/craftshop/server/.env" + +Write-Host ">>> chmod .env (owner = same as /opt/craftshop/server, deploy or root)" +ssh $remote "chown --reference=/opt/craftshop/server /opt/craftshop/server/.env || chown root:root /opt/craftshop/server/.env; chmod 600 /opt/craftshop/server/.env" + +Set-Location $repoRoot +Write-Host ">>> deploy-ssh.ps1 -All" +& (Join-Path $scriptsDir "deploy-ssh.ps1") -All +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +Write-Host ">>> systemd enable craftshop-api" +ssh $remote "systemctl enable --now craftshop-api" + +Write-Host ">>> health check" +ssh $remote "curl -sS http://127.0.0.1:3333/health" + +Write-Host "Done. Open http://${deployHost}/" diff --git a/scripts/deploy-ssh.ps1 b/scripts/deploy-ssh.ps1 new file mode 100644 index 0000000..fb3ac66 --- /dev/null +++ b/scripts/deploy-ssh.ps1 @@ -0,0 +1,79 @@ +# Вызывает deploy-ssh.sh через bash (Git for Windows или bash в PATH). +# Запускайте из корня репозитория или откуда угодно — скрипт перейдёт в root. + +param( + [switch]$FrontendOnly, + [switch]$BackendOnly, + [switch]$All, + [switch]$DryRun, + [switch]$SkipBuild, + [switch]$Help +) + +$ErrorActionPreference = "Stop" +$scriptsDir = $PSScriptRoot +$repoRoot = (Resolve-Path (Join-Path $scriptsDir "..")).Path + +$gitUsrRs = "C:\Program Files\Git\usr\bin\rsync.exe" +$chocoRsDir = "C:\ProgramData\chocolatey\bin" +$chocoRs = Join-Path $chocoRsDir "rsync.exe" +if (Test-Path -LiteralPath $gitUsrRs) { + $env:Path = "C:\Program Files\Git\usr\bin;$env:Path" +} elseif (Test-Path -LiteralPath $chocoRs) { + $env:Path = "$chocoRsDir;$env:Path" +} + +function Show-Help { + @" +Использование (рядом с репозиторием или из любого каталога): + .\scripts\deploy-ssh.ps1 [-FrontendOnly] [-BackendOnly] [-All] + .\scripts\deploy-ssh.ps1 -DryRun -BackendOnly + +Конфиг: scripts/deploy.env (скопируйте из deploy.env.example). + +Нужны: bash (Git for Windows) и rsync в PATH. rsync без Git: установите пакет (например, choco install rsync). +"@ | Write-Host +} + +if ($Help) { Show-Help; exit 0 } + +$bash = Get-Command bash -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source +if (-not $bash) { + $git = "C:\Program Files\Git\bin\bash.exe" + if (Test-Path $git) { $bash = $git } +} +if (-not $bash) { + Write-Error "Не найден bash. Установите Git for Windows и добавьте Git\usr\bin в PATH (rsync)." +} + +if (-not (Get-Command rsync -ErrorAction SilentlyContinue)) { + Write-Error "Не найден rsync. Установите: choco install rsync -y либо добавьте C:\Program Files\Git\usr\bin в PATH." +} + +$argsToSh = [System.Collections.ArrayList]@() +if ($FrontendOnly) { [void]$argsToSh.Add("--frontend-only") } +elseif ($BackendOnly) { [void]$argsToSh.Add("--backend-only") } +else { [void]$argsToSh.Add("--all") } + +if ($DryRun) { [void]$argsToSh.Add("--dry-run") } +if ($SkipBuild) { [void]$argsToSh.Add("--skip-build") } + +function ConvertTo-MsysPath { + param([string]$Path) + $full = if (Test-Path $Path) { (Resolve-Path -LiteralPath $Path).Path } else { $Path } + if ($full -match '^([A-Za-z]):[\\/](.*)$') { + return "/" + $Matches[1].ToLower() + "/" + ($Matches[2] -replace '\\', '/') + } + return ($full -replace '\\', '/') +} + +$sh = Join-Path $scriptsDir "deploy-ssh.sh" +$shUnix = ConvertTo-MsysPath $sh +Push-Location $repoRoot +try { + & $bash $shUnix @argsToSh + exit $LASTEXITCODE +} +finally { + Pop-Location +} diff --git a/scripts/deploy-ssh.sh b/scripts/deploy-ssh.sh new file mode 100644 index 0000000..f83cb07 --- /dev/null +++ b/scripts/deploy-ssh.sh @@ -0,0 +1,235 @@ +#!/usr/bin/env bash +# Деплой по SSH: отдельно фронт (сборка + rsync dist), бэк (rsync + npm ci + prisma migrate). +# +# Требования: bash, ssh; на Linux/macOS используется rsync, на Git Bash/WIN — tar|ssh (обход проблем cwRsync с путами). +# +# Конфиг: переменные окружения или файл scripts/deploy.env (скопируйте из scripts/deploy.env.example). +# +# Примеры: +# ./scripts/deploy-ssh.sh --backend-only +# ./scripts/deploy-ssh.sh --frontend-only +# ./scripts/deploy-ssh.sh --all +# DEPLOY_HOST=10.0.0.5 ./scripts/deploy-ssh.sh -b + +set -euo pipefail + +# Git Bash вызывает Win32 ssh.exe: аргументы вроде /opt/... иначе подменяются → «mkdir: missing operand». +case "$(uname -s 2>/dev/null)" in + MINGW* | MSYS*) export MSYS2_ARG_CONV_EXCL="*" ;; +esac + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Загрузка deploy.env без перезаписи уже экспортированных переменных +if [[ -f "$SCRIPT_DIR/deploy.env" ]]; then + set -a + # shellcheck source=/dev/null + source "$SCRIPT_DIR/deploy.env" + set +a +fi + +DEPLOY_HOST="${DEPLOY_HOST:-}" +DEPLOY_USER="${DEPLOY_USER:-root}" +DEPLOY_PATH="${DEPLOY_PATH:-/opt/craftshop}" +DEPLOY_FRONTEND_DIST="${DEPLOY_FRONTEND_DIST:-$DEPLOY_PATH/www}" +DEPLOY_SSH_IDENTITY="${DEPLOY_SSH_IDENTITY:-}" +DEPLOY_RESTART_CMD="${DEPLOY_RESTART_CMD:-}" # от root: systemctl restart craftshop-api; от непривилегированного: sudo … +# При SSH от root файлы после rsync оказываются root:root; если API в systemd под другим пользователем (bootstrap: deploy), нужен chown: +DEPLOY_SERVER_OWNER="${DEPLOY_SERVER_OWNER:-deploy}" +DEPLOY_SKIP_CHOWN="${DEPLOY_SKIP_CHOWN:-0}" # 1 — не вызывать chown (например API тоже под root) + +RSYNC_OPTS=(-az --delete --human-readable --progress) +SSH_OPTS=() +if [[ -n "${DEPLOY_SSH_IDENTITY}" ]]; then + SSH_OPTS+=(-i "$DEPLOY_SSH_IDENTITY") +fi + +REMOTE="${DEPLOY_USER}@${DEPLOY_HOST}" +SSH_BASE=(ssh "${SSH_OPTS[@]}" -o StrictHostKeyChecking=accept-new "$REMOTE") + +should_use_tar_transport() { + case "$(uname -s 2>/dev/null)" in + MINGW*|MSYS*|CYGWIN_NT*) return 0 ;; + *) return 1 ;; + esac +} + +usage() { + cat <&2; usage >&2; exit 1 ;; + esac + shift +done + +if [[ -z "$DEPLOY_HOST" ]]; then + echo "Укажите DEPLOY_HOST (или добавьте в scripts/deploy.env)" >&2 + exit 1 +fi + +remote_exec() { + "${SSH_BASE[@]}" "$@" +} + +if [[ -n "$DRY_RUN" ]]; then + RSYNC_OPTS+=(--dry-run) +fi + +# Строка для rsync -e (одна подкоманда; пути без пробелов в -i надёжнее) +build_rsync_rsh() { + printf '%q ' ssh "${SSH_OPTS[@]}" -o StrictHostKeyChecking=accept-new +} + +deploy_backend() { + remote_exec mkdir -p "$DEPLOY_PATH/server" + + if should_use_tar_transport; then + echo ">>> Бэкенд: tar|ssh → $REMOTE:$DEPLOY_PATH/server/" + if [[ -n "$DRY_RUN" ]]; then + echo "(dry-run) без передачи tar" + else + ( + cd "$ROOT/server" || exit 1 + tar -czf - \ + --exclude=node_modules \ + --exclude=uploads \ + --exclude=.git \ + --exclude='*.db' \ + --exclude=.env \ + --exclude=.dev_env \ + . + ) | "${SSH_BASE[@]}" "mkdir -p ${DEPLOY_PATH}/server && tar xzf - -C ${DEPLOY_PATH}/server" + fi + else + echo ">>> Бэкенд: rsync → $REMOTE:$DEPLOY_PATH/server/" + local rsh + rsh="$(build_rsync_rsh)" + + rsync "${RSYNC_OPTS[@]}" \ + -e "$rsh" \ + --exclude node_modules \ + --exclude uploads \ + --exclude .git \ + --exclude '*.db' \ + --exclude .env \ + --exclude .dev_env \ + "${ROOT}/server/" "${REMOTE}:${DEPLOY_PATH}/server/" + fi + + if [[ -n "$DRY_RUN" ]]; then + echo "(dry-run) пропуск удалённых команд npm/prisma" + return 0 + fi + + echo ">>> Бэкенд: npm ci, prisma generate, migrate deploy на сервере" + remote_exec bash -lc "set -e + cd \"$DEPLOY_PATH/server\" + npm ci + npx prisma generate + npx prisma migrate deploy + " + if [[ "${DEPLOY_USER}" == "root" && "${DEPLOY_SKIP_CHOWN}" != "1" ]]; then + echo ">>> Права на серверный каталог: chown ${DEPLOY_SERVER_OWNER} (деплой от root)" + remote_exec chown -R "${DEPLOY_SERVER_OWNER}:${DEPLOY_SERVER_OWNER}" "$DEPLOY_PATH/server" + fi + if [[ -n "${DEPLOY_RESTART_CMD}" ]]; then + echo ">>> Рестарт: $DEPLOY_RESTART_CMD" + remote_exec bash -lc "$DEPLOY_RESTART_CMD" + elif [[ -z "$DRY_RUN" ]]; then + echo "(подсказка) задайте DEPLOY_RESTART_CMD, если нужен перезапуск сервиса" + fi +} + +deploy_frontend() { + if [[ -z "$SKIP_BUILD" ]]; then + echo ">>> Фронт: npm ci и npm run build (локально)" + # Windows: ESLint/typescript-eslint тянут @unrs/*.node — npm ci часто получает EPERM unlink, если файл держит Node/IDE или остался мусор .resolver-binding-* после сбоя. + if should_use_tar_transport; then + echo ">>> (Windows/Git Bash) перед npm ci: удалить client/node_modules/@unrs и мусор .resolver-binding-* (EPERM unlink)" + rm -rf "$ROOT/client/node_modules/@unrs" 2>/dev/null || true + ( + cd "$ROOT/client/node_modules" 2>/dev/null || exit 0 + shopt -s nullglob + for x in ./.resolver-binding-*; do + [[ -d "$x" ]] && rm -rf "$x" + done + ) + fi + (cd "$ROOT/client" && npm ci && npm run build) + else + echo ">>> Фронт: сборка пропущена (--skip-build)" + if [[ ! -d "$ROOT/client/dist" ]]; then + echo "Нет $ROOT/client/dist — выполните сборку без --skip-build" >&2 + exit 1 + fi + fi + + remote_exec mkdir -p "$DEPLOY_FRONTEND_DIST" + + if should_use_tar_transport; then + echo ">>> Фронт: tar|ssh dist → $REMOTE:$DEPLOY_FRONTEND_DIST/" + if [[ -n "$DRY_RUN" ]]; then + echo "(dry-run) без передачи tar (www)" + else + remote_exec "mkdir -p ${DEPLOY_FRONTEND_DIST} && find ${DEPLOY_FRONTEND_DIST} -mindepth 1 -delete 2>/dev/null || true" + ( + cd "$ROOT/client/dist" || exit 1 + tar -czf - . + ) | "${SSH_BASE[@]}" "mkdir -p ${DEPLOY_FRONTEND_DIST} && tar xzf - -C ${DEPLOY_FRONTEND_DIST}" + fi + else + echo ">>> Фронт: rsync dist → $REMOTE:$DEPLOY_FRONTEND_DIST/" + local rsh + rsh="$(build_rsync_rsh)" + rsync "${RSYNC_OPTS[@]}" \ + -e "$rsh" \ + "${ROOT}/client/dist/" "${REMOTE}:${DEPLOY_FRONTEND_DIST}/" + fi + + echo ">>> Фронт готов (проверьте nginx/root на путь $DEPLOY_FRONTEND_DIST)" +} + +case "$TARGET" in + backend) deploy_backend ;; + frontend) deploy_frontend ;; + all) + deploy_backend + deploy_frontend + ;; + *) echo "internal: bad TARGET=$TARGET" >&2; exit 1 ;; + esac + +echo "Готово." diff --git a/scripts/deploy.env.example b/scripts/deploy.env.example new file mode 100644 index 0000000..76dd97e --- /dev/null +++ b/scripts/deploy.env.example @@ -0,0 +1,19 @@ +# Скопируйте в deploy.env рядом с deploy-ssh.sh и подставьте значения. + +DEPLOY_HOST=192.168.1.88 +DEPLOY_USER=root +DEPLOY_PATH=/opt/craftshop + +# Куда выкладывается `client/dist/` (совпадайте с root в nginx для SPA + try_files). +DEPLOY_FRONTEND_DIST=/opt/craftshop/www + +# Опционально: ssh -i +# DEPLOY_SSH_IDENTITY=C:/Users/Me/.ssh/id_ed25519 + +# Если API под пользователем deploy — оставьте DEPLOY_SKIP_CHOWN=0 (дефолт chown deploy). +# Если на сервере CRAFTSHOP_USER=root (systemd под root): DEPLOY_SKIP_CHOWN=1 и DEPLOY_SERVER_OWNER=root +DEPLOY_SKIP_CHOWN=0 +# DEPLOY_SERVER_OWNER=deploy + +# После обновления кода API (под root без sudo) +DEPLOY_RESTART_CMD='systemctl restart craftshop-api' diff --git a/scripts/read-deploy-env.ps1 b/scripts/read-deploy-env.ps1 new file mode 100644 index 0000000..8d2cd2c --- /dev/null +++ b/scripts/read-deploy-env.ps1 @@ -0,0 +1,14 @@ +function Import-DeployDotEnv { + param([string]$Path) + if (-not (Test-Path $Path)) { return } + Get-Content $Path | ForEach-Object { + if ($_ -match '^\s*#' -or $_ -match '^\s*$') { return } + if ($_ -match '^([A-Za-z_][A-Za-z0-9_]*)=(.*)$') { + $name = $Matches[1]; $raw = $Matches[2].Trim() + $v = $raw + if ($raw.StartsWith("'") -and $raw.EndsWith("'")) { $v = $raw.Trim("'") } + elseif ($raw.StartsWith('"') -and $raw.EndsWith('"')) { $v = $raw.Trim('"') } + [Environment]::SetEnvironmentVariable($name, $v, "Process") + } + } +} diff --git a/scripts/register-ssh-key-for-root.ps1 b/scripts/register-ssh-key-for-root.ps1 new file mode 100644 index 0000000..0be12db --- /dev/null +++ b/scripts/register-ssh-key-for-root.ps1 @@ -0,0 +1,33 @@ +# Appends local id_ed25519.pub to root authorized_keys on the server (uses scripts/deploy.env). +# Run from repo root: .\scripts\register-ssh-key-for-root.ps1 + +$ErrorActionPreference = "Stop" +$scriptsDir = $PSScriptRoot +$deployEnv = Join-Path $scriptsDir "deploy.env" + +if (-not (Test-Path $deployEnv)) { + Write-Error "Missing scripts/deploy.env. Copy from deploy.env.example and set DEPLOY_HOST." +} + +. "$PSScriptRoot\read-deploy-env.ps1" +Import-DeployDotEnv $deployEnv + +$deployHost = [Environment]::GetEnvironmentVariable("DEPLOY_HOST", "Process") +$user = [Environment]::GetEnvironmentVariable("DEPLOY_USER", "Process") +if ([string]::IsNullOrWhiteSpace($user)) { $user = "root" } + +if ([string]::IsNullOrWhiteSpace($deployHost)) { + Write-Error "DEPLOY_HOST is not set in scripts/deploy.env." +} + +$keyPub = Join-Path $env:USERPROFILE ".ssh\id_ed25519.pub" +if (-not (Test-Path $keyPub)) { + Write-Error "Public key not found: $keyPub" +} + +$remote = "${user}@${deployHost}" +Write-Host "Adding key to $remote (from $keyPub). Enter password if SSH asks." +$bashCmd = "umask 077; mkdir -p .ssh && touch .ssh/authorized_keys && chmod 700 .ssh && cat >> .ssh/authorized_keys && chmod 600 .ssh/authorized_keys" +Get-Content -Raw $keyPub | ssh -o StrictHostKeyChecking=accept-new $remote $bashCmd + +Write-Host "Done. Verify: ssh $remote echo ssh-ok" diff --git a/scripts/server-bootstrap.sh b/scripts/server-bootstrap.sh new file mode 100644 index 0000000..a927ec0 --- /dev/null +++ b/scripts/server-bootstrap.sh @@ -0,0 +1,202 @@ +#!/usr/bin/env bash +# +# Этап A: первичная настройка свежего Debian/Ubuntu (LXC/VM) под Craftshop. +# Запускать НА СЕРВЕРЕ от root один раз: +# curl -fsSL … | bash или scp + bash server-bootstrap.sh +# +# По умолчанию: ставит Node 22.x (NodeSource), nginx, пользователя deploy, каталоги +# /opt/craftshop/server и /opt/craftshop/www, systemd craftshop-api, сайт nginx. +# +# После выполнения: +# 1) Положите .env в /opt/craftshop/server/ (DATABASE_URL, JWT_SECRET, ADMIN_EMAIL, CORS_ORIGIN, PORT=3333) +# 2) DEPLOY_RESTART_CMD при SSH от root: «systemctl restart craftshop-api» (без sudo) +# 3) Запустите deploy-ssh.sh с вашей машины (по умолчанию пользователь SSH — root, см. scripts/deploy.env.example) +# +set -euo pipefail + +if [[ "$(id -u)" != "0" ]]; then + echo "Запускайте от root: sudo $0" >&2 + exit 1 +fi + +CRAFTSHOP_ROOT="${CRAFTSHOP_ROOT:-/opt/craftshop}" +CRAFTSHOP_USER="${CRAFTSHOP_USER:-deploy}" +CRAFTSHOP_SERVER_NAME="${CRAFTSHOP_SERVER_NAME:-_}" +CRAFTSHOP_NODE_MAJOR="${CRAFTSHOP_NODE_MAJOR:-22}" +SKIP_NODE_INSTALL="${SKIP_NODE_INSTALL:-0}" +SKIP_NGINX="${SKIP_NGINX:-0}" +SKIP_SYSTEMD="${SKIP_SYSTEMD:-0}" + +if [[ -f /etc/craftshop-bootstrap.env ]]; then + set -a + # shellcheck source=/dev/null + source /etc/craftshop-bootstrap.env + set +a +fi + +detect_os() { + if [[ -f /etc/os-release ]]; then + # shellcheck source=/dev/null + source /etc/os-release + echo "${ID:-unknown}" + return 0 + fi + echo unknown +} + +OS="$(detect_os)" +if [[ "$OS" != "debian" && "$OS" != "ubuntu" ]]; then + echo "Ожидался Debian или Ubuntu (получено: $OS). Прервите выполнение, если образ другой." >&2 +fi + +echo ">>> apt-get update" +export DEBIAN_FRONTEND=noninteractive +apt-get update -y + +echo ">>> Базовые пакеты" +apt-get install -y ca-certificates curl gnupg + +if [[ "$SKIP_NODE_INSTALL" != "1" ]]; then + if command -v node >/dev/null 2>&1; then + ver="$(node -p "parseInt(process.versions.node,10)" 2>/dev/null || echo 0)" + if [[ "${ver:-0}" -ge 20 ]]; then + echo ">>> Node уже есть (версия $(node --version)), пропуск NodeSource" + else + echo ">>> Node слишком старый, ставим ${CRAFTSHOP_NODE_MAJOR}.x через NodeSource" + curl -fsSL "https://deb.nodesource.com/setup_${CRAFTSHOP_NODE_MAJOR}.x" | bash - + apt-get install -y nodejs + fi + else + echo ">>> Установка Node.js ${CRAFTSHOP_NODE_MAJOR}.x через NodeSource" + curl -fsSL "https://deb.nodesource.com/setup_${CRAFTSHOP_NODE_MAJOR}.x" | bash - + apt-get install -y nodejs + fi +else + echo ">>> SKIP_NODE_INSTALL=1 — убедитесь, что node >= 20.6 есть в системе" + command -v node >/dev/null 2>&1 || { + echo "node не найден" >&2 + exit 1 + } +fi + +echo ">>> Пользователь $CRAFTSHOP_USER" +if ! id -u "$CRAFTSHOP_USER" >/dev/null 2>&1; then + useradd --create-home --shell /bin/bash "$CRAFTSHOP_USER" +fi + +echo ">>> Каталоги $CRAFTSHOP_ROOT/{server,www,uploads}" +mkdir -p "$CRAFTSHOP_ROOT/server/uploads" "$CRAFTSHOP_ROOT/www" +chown -R "$CRAFTSHOP_USER:$CRAFTSHOP_USER" "$CRAFTSHOP_ROOT" +chmod 755 "$CRAFTSHOP_ROOT" "$CRAFTSHOP_ROOT/server" "$CRAFTSHOP_ROOT/www" + +if [[ "$SKIP_SYSTEMD" != "1" ]]; then + echo ">>> systemd craftshop-api" + cat >/etc/systemd/system/craftshop-api.service <"$NGINX_SITE" <>> nginx включён на порту 80 (server_name=$CRAFTSHOP_SERVER_NAME)" +fi + +cat >"$CRAFTSHOP_ROOT/server/README-PLACE-ENV.txt" < + ADMIN_EMAIL=<ваш email админа> + CORS_ORIGIN=http:// + IS_DEFAULT_CODE_ENABLED=false + +Базу и код вы зальёте скриптом deploy-ssh.sh с машины разработчика: + первый запуск после деплоя: systemctl start craftshop-api (от root; иначе sudo) +TXT +chown "$CRAFTSHOP_USER:$CRAFTSHOP_USER" "$CRAFTSHOP_ROOT/server/README-PLACE-ENV.txt" + +if [[ -n "${CRAFTSHOP_AUTHORIZED_KEY:-}" ]]; then + uhome="$(getent passwd "$CRAFTSHOP_USER" | cut -d: -f6)" + install -d -m 700 -o "$CRAFTSHOP_USER" -g "$CRAFTSHOP_USER" "$uhome/.ssh" + afile="$uhome/.ssh/authorized_keys" + if [[ ! -f "$afile" ]]; then + install -m 600 -o "$CRAFTSHOP_USER" -g "$CRAFTSHOP_USER" /dev/null "$afile" + fi + if ! grep -qFx "$CRAFTSHOP_AUTHORIZED_KEY" "$afile" 2>/dev/null; then + printf '%s\n' "$CRAFTSHOP_AUTHORIZED_KEY" >>"$afile" + fi + echo ">>> Добавлен SSH-ключ в $afile" +fi + +echo "" +echo "Готово Этап A." +echo "- Каталог: $CRAFTSHOP_ROOT" +echo "- Пользователь: $CRAFTSHOP_USER" +echo "- Положите $CRAFTSHOP_ROOT/server/.env и выполните деплой кода (--backend-only), затем: systemctl start craftshop-api"