diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..42a54e7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,71 @@ +# AGENTS.md — shop (craftshop monorepo) + +## Project structure + +- `client/` — frontend (React + Vite + TypeScript + MUI), **FSD architecture**: `app/pages/widgets/features/entities/shared` +- `server/` — backend (Fastify + Prisma + SQLite) +- `shared/constants/` — JS + `.d.ts` files shared between client and server (order statuses, delivery carriers, payment methods, upload limits) + +## Developer commands + +### Client (`cd client`) + +| Command | What it does | +|---|---| +| `npm run dev` | Vite dev server on `:5173`, proxies `/api` and `/uploads` to `http://127.0.0.1:3333` | +| `npm run build` | Runs `tsc -b` first, then `vite build` | +| `npm run lint` | ESLint (flat config) | +| `npm run lint:fix` | ESLint with `--fix` | +| `npm run format` | Prettier write all | +| `npm run format:check` | Prettier check only | +| `npm test` | vitest run | +| `npm run test:watch` | vitest watch mode | + +### Server (`cd server`) + +| Command | What it does | +|---|---| +| `npm run dev` | `node --env-file=.dev_env --watch src/index.js` (requires Node 20.6+) | +| `npm run dev:classic` | `node --watch src/index.js` (loads `.env` via dotenv) | +| `npm test` | vitest run | +| `npm run db:reset:test` | Reset SQLite DB + re-run migrations + seed (uses `.dev_env`) | + +### Build order (when changing both packages) + +```bash +cd server && npm run db:migrate # if schema changed +cd server && npm test # server tests first +cd client && npm run lint && npm run format:check && npm test # then client +cd client && npm run build # full typecheck + build +``` + +## Conventions + +- **Language**: Отвечай пользователю **на русском**. +- **Single quotes**, no semicolons, trailing commas, 120 print width (Prettier + ESLint enforce). +- **FSD import boundaries** enforced by `eslint-plugin-boundaries`. Lower layers cannot import upper layers. If ESLint complains about an import, the architecture is wrong. +- **Aliases**: `@/` → `client/src/`, `@shared/` → `shared/` (configured in both vite.config.ts and tsconfig). +- **API requests**: Use `apiClient` (axios wrapper from `shared/api/`) with `@tanstack/react-query`. Invalidate queries after mutations. +- **UI**: Prefer MUI components over custom HTML/CSS. +- **`no-console`**: ESLint error; use `console.warn/error/info` only. +- **Admin access**: Only users with email matching `ADMIN_EMAIL` env var can access admin routes. Server auto-creates the admin user on startup. +- **Server helpers**: `slugify`, `parseMaterialsInput`, `mapProductForApi` are decorated on fastify instance, accessed via `request.server.*`. + +## Testing + +- **Client**: vitest + jsdom + @testing-library/react. Setup file: `client/src/testing/setup.ts`. +- **Server**: vitest with globals enabled. +- Test files live in `__tests__/` directories next to the code they test. + +## OAuth + +- VK callback: `{SERVER_PUBLIC_URL}/api/auth/oauth/vk/callback` +- Yandex callback: `{SERVER_PUBLIC_URL}/api/auth/oauth/yandex/callback` +- Required env vars: `VK_CLIENT_ID`, `VK_CLIENT_SECRET`, `YANDEX_CLIENT_ID`, `YANDEX_CLIENT_SECRET`, `SERVER_PUBLIC_URL`, `CLIENT_PUBLIC_URL` + +## Notable quirks + +- `.env` is gitignored. Use `.dev_env` in the server repo for local dev (it is committed). Copy `.env.example` to `.env` for custom config. +- Vite dev server (client) relies on backend running at `127.0.0.1:3333`. Start server first. +- Rich text rendering uses `shared/ui/RichTextMessageContent` (TipTap). Pass `tone="review"`, `tone="chat"`, or `tone="default"`. +- `db:reset:test` runs `prisma migrate reset --force`, which destroys all data. diff --git a/docs/deploy-changes.md b/docs/deploy-changes.md deleted file mode 100644 index 8dd5e1b..0000000 --- a/docs/deploy-changes.md +++ /dev/null @@ -1,104 +0,0 @@ -# Справка: как задеплоить изменения - -Как обновить уже работающий стенд (код на сервере + фронт в nginx) после правок в репозитории. - -## 1. Перед выкладкой (локально, по желанию) - -- **Фронт**: `cd client && npm run lint && npm run build` — убедиться, что сборка проходит. - -### Windows: `npm ci` / сборка падает с EPERM на `.node` - -Часто файл держит **запущенный `npm run dev`** (Vite), другой терминал с Node или антивирус. Сделайте: - -1. Остановить все dev-серверы и лишние процессы `node`. -2. Запускать деплой из **Git Bash** или через **`.\scripts\deploy-ssh.ps1`** (не вызывайте `deploy-ssh.sh` напрямую из PowerShell без `bash`). -3. Повторить `./scripts/deploy-ssh.sh` (скрипт перед `npm ci` удаляет каталоги `@unrs` и `@rolldown` в `client/node_modules`, если EPERM из‑за блокировок). -4. Если не помогло: вручную `cd client && npm run build`, затем **`./scripts/deploy-ssh.sh --frontend-only --skip-build`** (выложится уже готовый `client/dist`). - -- **Бэкенд**: при изменениях в `server/prisma` — миграции должны быть в репозитории; на сервере выполнится `prisma migrate deploy` (см. скрипт деплоя). -- **Общие константы**: каталог `shared/constants/` синхронизируется скриптом деплоя вместе с `server/` (автоматически в `deploy_backend`). - -## 2. Переменные окружения на сервере - -После обновления кода сверьте `server/.env` с актуальным **`server/.env.example`** на предмет новых переменных. - -Актуально для загрузок файлов (если нужны нестандартные лимиты): - -- `ADMIN_IMAGE_MAX_FILE_BYTES` (или устаревшее `PRODUCT_IMAGE_MAX_FILE_BYTES`) — одно изображение в админке: товары, галерея (по умолчанию 20 МБ). -- `OTHER_UPLOAD_MAX_FILE_BYTES` — отзывы, чек оплаты и т.п. (по умолчанию 2 МБ). -- `MAX_UPLOAD_BODY_BYTES` — весь POST multipart (по умолчанию рассчитывается от лимита фото товара × 10 + запас). - -**413 на проде, локально ок:** чаще всего **nginx** с лимитом по умолчанию 1 МБ. См. **[docs/nginx-upload-limit.md](nginx-upload-limit.md)** — добавьте `client_max_body_size` для `location /api/`. - -Общие для публикации: `JWT_SECRET`, `ADMIN_EMAIL`, `DATABASE_URL`, `CORS_ORIGIN`, при раздельных доменах — `VITE_*` на этапе сборки фронта, `SERVER_PUBLIC_URL` / `CLIENT_PUBLIC_URL` для OAuth. - -После правок `.env` **перезапустите API** (systemd или ваша команда). - -## 3. Автоматический деплой по SSH (рекомендуется) - -1. Скопируйте `scripts/deploy.env.example` в **`scripts/deploy.env`** и заполните: - - `DEPLOY_HOST`, `DEPLOY_USER`, `DEPLOY_PATH`, `DEPLOY_FRONTEND_DIST`; - - при необходимости `DEPLOY_SSH_IDENTITY`, `DEPLOY_RESTART_CMD` (например `systemctl restart craftshop-api`). -2. Из корня репозитория (нужен **bash**: Git Bash / WSL / Linux/macOS): - - ```bash - ./scripts/deploy-ssh.sh --all - ``` - - Варианты: - - - `--frontend-only` / `-f` — только сборка `client` и выкладка `dist` на сервер. - - `--backend-only` / `-b` — только `server` (rsync, `npm ci`, Prisma migrate), плюс `DEPLOY_RESTART_CMD` если задан. - - `--all` / `-a` — и фронт, и бэкенд (поведение по умолчанию). - -3. Убедитесь, что после бэкенд-части API перезапущен (`DEPLOY_RESTART_CMD` или вручную). - -Подробности опций: в начале файла **`scripts/deploy-ssh.sh`** (справка `usage`). - -## 4. Первичный деплой с нуля (LAN / один раз) - -Если сервер ещё не настроен, используйте сценарий первичной подготовки (bootstrap, systemd, первый выклад): - -- **`scripts/complete-lan-deploy.ps1`** (Windows) — нужны `scripts/deploy.env` и `scripts/craftshop-remote-lan.env`, см. комментарии в скрипте. - -Черновик архитектуры и требований к ВМ: **[docs/test-deploy-proxmox.md](test-deploy-proxmox.md)**. - -## 5. Ручное обновление (если без скрипта) - -**Бэкенд** (на сервере, в каталоге приложения, например `/opt/craftshop/server`): - -```bash -git pull -npm ci -npx prisma migrate deploy -# при необходимости: npx prisma db seed -``` - -Перезапуск процесса Node (пример): - -```bash -systemctl restart craftshop-api -``` - -**Фронт** (на машине разработчика): - -```bash -cd client -npm ci -npm run build -``` - -Содержимое **`client/dist/`** скопируйте в каталог статики nginx (как у вас настроено, часто совпадает с `DEPLOY_FRONTEND_DIST` из `deploy.env.example`). - -## 6. Что не потерять при деплое - -- Каталоги **`shared/`** и **`server/`** должны быть рядом на одном уровне (например, `/opt/craftshop/shared/constants/order-status.js` и `/opt/craftshop/server/src/lib/order-status.js`). Скрипт деплоя синхронизирует оба. -- Файл **SQLite** и каталог **`server/uploads/`** должны лежать на **персистентном диске** (не внутри временного слоя контейнера без тома). -- Nginx (или аналог): **`/api`** → прокси на Fastify, **`/uploads`** → те же файлы, что пишет сервер, либо прокси на `@fastify/static` (см. [test-deploy-proxmox.md](test-deploy-proxmox.md)). - -## 7. Проверка после выкладки - -- `GET https://<ваш-домен>/api/health` или `curl http://127.0.0.1:3333/health` на сервере. -- Открыть витрину, при необходимости войти в админку и проверить загрузку фото (лимиты см. выше). - -Дополнительно: общий обзор проекта и локальный запуск — **[README.md](../README.md)**. diff --git a/docs/nginx-upload-limit.md b/docs/nginx-upload-limit.md deleted file mode 100644 index 183ee2c..0000000 --- a/docs/nginx-upload-limit.md +++ /dev/null @@ -1,36 +0,0 @@ -# Лимит размера запроса (413) за reverse proxy - -Локально запросы идут напрямую в Node (Vite проксирует на Fastify) — тело до **~201 МБ** (см. `getMaxUploadBodyBytes()` в [`server/src/lib/upload-limits.js`](../server/src/lib/upload-limits.js)). - -На проде перед Node часто стоит **nginx** (или Caddy, Traefik). У **nginx** по умолчанию **`client_max_body_size 1m`** — загрузка картинки больше ~1 МБ даёт **413**, хотя на локалке всё работает. - -## Nginx - -В блоке `http`, `server` или в `location /api/` (где проксируется API) задайте лимит **не меньше** максимального тела одного запроса загрузки: - -- один файл до **20 МБ** — достаточно **`25m`**; -- до **10 файлов** за один `POST /api/admin/uploads` — теоретически до **~200 МБ** данных + multipart — разумно **`250m`** или **`300m`**. - -Пример для `location`, который проксирует API: - -```nginx -location /api/ { - client_max_body_size 250m; - proxy_pass http://127.0.0.1:3333; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; -} -``` - -После правки конфига: `sudo nginx -t && sudo systemctl reload nginx`. - -## Переменные Node (если 413 не от nginx) - -В `server/.env` не задавайте слишком маленькое **`MAX_UPLOAD_BODY_BYTES`** — иначе Fastify отрежет тело раньше прокси. По умолчанию считается как `getProductImageMaxFileBytes() * 10 + 1 МБ`. Если задали вручную — должно быть **≥ суммы ваших файлов** в одном multipart-запросе. - -## Caddy (кратко) - -В соответствующем site-блоке задайте лимит тела запроса по [документации Caddy v2](https://caddyserver.com/docs/caddyfile/directives/request_body) (например, `request_body` с `max_size`). diff --git a/docs/superpowers/plans/2026-05-14-auto-deploy.md b/docs/superpowers/plans/2026-05-14-auto-deploy.md new file mode 100644 index 0000000..2a17fc7 --- /dev/null +++ b/docs/superpowers/plans/2026-05-14-auto-deploy.md @@ -0,0 +1,546 @@ +# Auto-Deploy Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** One-command auto-deploy script that detects git changes and deploys only modified components. Clean up obsolete files. + +**Architecture:** One bash script (`deploy-auto.sh`) replaces the existing `deploy-ssh.sh`. It uses `git diff --name-only` against a saved commit hash (`.deployed-commit`) to decide what changed, then reuses existing rsync/tar transport logic. A markdown guide (`SERVER_SETUP.md`) replaces the bootstrap script. + +**Tech Stack:** Bash, git, rsync/tar, SSH, nginx, systemd, Node.js + +--- + +### Task 1: Delete obsolete files + +**Files:** +- Delete: `scripts/deploy-ssh.sh` +- Delete: `scripts/deploy-ssh.ps1` +- Delete: `scripts/deploy.env.example` +- Delete: `scripts/read-deploy-env.ps1` +- Delete: `scripts/register-ssh-key-for-root.ps1` +- Delete: `scripts/complete-lan-deploy.ps1` +- Delete: `scripts/craftshop-remote-lan.env` +- Delete: `scripts/server-bootstrap.sh` +- Delete: `docs/deploy-changes.md` +- Delete: `docs/test-deploy-proxmox.md` +- Delete: `docs/nginx-upload-limit.md` + +- [ ] **Step 1: Remove all obsolete files** + +```bash +git rm scripts/deploy-ssh.sh \ + scripts/deploy-ssh.ps1 \ + scripts/deploy.env.example \ + scripts/read-deploy-env.ps1 \ + scripts/register-ssh-key-for-root.ps1 \ + scripts/complete-lan-deploy.ps1 \ + scripts/craftshop-remote-lan.env \ + scripts/server-bootstrap.sh \ + docs/deploy-changes.md \ + docs/test-deploy-proxmox.md \ + docs/nginx-upload-limit.md +``` + +- [ ] **Step 2: Commit** + +```bash +git add -A && git commit -m "chore: remove obsolete deploy scripts and docs" +``` + +--- + +### Task 2: Add .deployed-commit to .gitignore + +**Files:** +- Modify: `.gitignore` + +- [ ] **Step 1: Add .deployed-commit to .gitignore** + +Add line at the end of `.gitignore`: + +``` +.deployed-commit +``` + +- [ ] **Step 2: Commit** + +```bash +git add .gitignore && git commit -m "chore: add .deployed-commit to gitignore" +``` + +--- + +### Task 3: Write deploy-auto.sh + +**Files:** +- Create: `scripts/deploy-auto.sh` + +- [ ] **Step 1: Create deploy-auto.sh with full implementation** + +```bash +#!/usr/bin/env bash +# Auto-deploy: детект изменений через git diff, сборка и деплой только изменённых компонентов. +# +# Зависимости: bash, git, ssh; rsync (Linux/macOS) или tar (Git Bash). +# Конфиг: scripts/deploy.env (скопируйте из deploy.env.example). +# +# Примеры: +# ./scripts/deploy-auto.sh # деплой изменений +# ./scripts/deploy-auto.sh --force # полный деплой всех компонентов +# ./scripts/deploy-auto.sh -f # только фронт +# ./scripts/deploy-auto.sh -b # только бэкенд + +set -euo pipefail + +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)" + +# --- Config --- +if [[ -f "$SCRIPT_DIR/deploy.env" ]]; then + set -a + 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:-systemctl restart craftshop-api}" +DEPLOY_SERVER_OWNER="${DEPLOY_SERVER_OWNER:-deploy}" +DEPLOY_SKIP_CHOWN="${DEPLOY_SKIP_CHOWN:-0}" + +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") + +# --- Flags --- +FORCE=false +TARGET="auto" + +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 + +# --- Helpers --- +remote_exec() { + "${SSH_BASE[@]}" "$@" +} + +should_use_tar_transport() { + case "$(uname -s 2>/dev/null)" in + MINGW*|MSYS*|CYGWIN_NT*) return 0 ;; + *) return 1 ;; + esac +} + +build_rsync_rsh() { + printf '%q ' ssh "${SSH_OPTS[@]}" -o StrictHostKeyChecking=accept-new +} + +# --- Diff detection --- +changed_client=false +changed_server=false + +if [[ "$TARGET" == "frontend" ]]; then + changed_client=true +elif [[ "$TARGET" == "backend" ]]; then + changed_server=true +elif [[ "$FORCE" == true ]]; then + changed_client=true + changed_server=true +else + DEPLOYED_FILE="$ROOT/.deployed-commit" + if [[ -f "$DEPLOYED_FILE" ]]; then + LAST_DEPLOYED=$(cat "$DEPLOYED_FILE") + else + LAST_DEPLOYED="HEAD~1" + fi + + echo ">>> Diff: $LAST_DEPLOYED..HEAD" + CHANGED_FILES=$(git -C "$ROOT" diff --name-only "$LAST_DEPLOYED" HEAD 2>/dev/null || true) + + if echo "$CHANGED_FILES" | grep -q "^client/"; then + changed_client=true + fi + if echo "$CHANGED_FILES" | grep -q "^server/"; then + changed_server=true + fi + if echo "$CHANGED_FILES" | grep -q "^shared/"; then + changed_client=true + changed_server=true + fi + + if [[ "$changed_client" == false && "$changed_server" == false ]]; then + echo ">>> Ничего не изменилось с последнего деплоя." + exit 0 + fi +fi + +echo ">>> Клиент: $changed_client, Сервер: $changed_server" + +# --- Deploy: Client --- +if [[ "$changed_client" == true ]]; then + echo ">>> Сборка клиента..." + (cd "$ROOT/client" && npm ci && npm run build) + + remote_exec mkdir -p "$DEPLOY_FRONTEND_DIST" + + if should_use_tar_transport; then + remote_exec "find ${DEPLOY_FRONTEND_DIST} -mindepth 1 -delete 2>/dev/null || true" + (cd "$ROOT/client/dist" && tar -czf - .) | \ + "${SSH_BASE[@]}" "mkdir -p ${DEPLOY_FRONTEND_DIST} && tar xzf - -C ${DEPLOY_FRONTEND_DIST}" + else + local rsh + rsh="$(build_rsync_rsh)" + rsync "${RSYNC_OPTS[@]}" -e "$rsh" \ + "$ROOT/client/dist/" "${REMOTE}:${DEPLOY_FRONTEND_DIST}/" + fi + + echo ">>> Клиент задеплоен" +fi + +# --- Deploy: Server --- +if [[ "$changed_server" == true ]]; then + remote_exec mkdir -p "$DEPLOY_PATH/server" "$DEPLOY_PATH/shared" + + if should_use_tar_transport; then + (cd "$ROOT/server" && 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" + + (cd "$ROOT/shared" && tar -czf - \ + --exclude=.git \ + .) | "${SSH_BASE[@]}" "mkdir -p ${DEPLOY_PATH}/shared && tar xzf - -C ${DEPLOY_PATH}/shared" + else + 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/" + + rsync "${RSYNC_OPTS[@]}" -e "$rsh" \ + --exclude .git \ + "$ROOT/shared/" "${REMOTE}:${DEPLOY_PATH}/shared/" + 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}" + remote_exec chown -R "${DEPLOY_SERVER_OWNER}:${DEPLOY_SERVER_OWNER}" "$DEPLOY_PATH/server" + remote_exec chown -R "${DEPLOY_SERVER_OWNER}:${DEPLOY_SERVER_OWNER}" "$DEPLOY_PATH/shared" + fi + + if [[ -n "${DEPLOY_RESTART_CMD}" ]]; then + echo ">>> Рестарт: $DEPLOY_RESTART_CMD" + remote_exec bash -lc "$DEPLOY_RESTART_CMD" + fi + + echo ">>> Сервер задеплоен" +fi + +# --- Save deployed commit --- +if [[ "$TARGET" == "auto" && "$FORCE" != true ]]; then + CURRENT_HEAD=$(git -C "$ROOT" rev-parse HEAD) + echo "$CURRENT_HEAD" > "$ROOT/.deployed-commit" + echo ">>> Сохранён коммит $CURRENT_HEAD" +fi + +echo ">>> Готово." +``` + +- [ ] **Step 2: Make executable** + +```bash +chmod +x scripts/deploy-auto.sh +``` + +- [ ] **Step 3: Commit** + +```bash +git add scripts/deploy-auto.sh && git commit -m "feat: add deploy-auto.sh with git diff detection" +``` + +--- + +### Task 4: Write SERVER_SETUP.md + +**Files:** +- Create: `scripts/SERVER_SETUP.md` + +- [ ] **Step 1: Create SERVER_SETUP.md** + +```markdown +# Первичная настройка LXC для Craftshop + +Выполнять от **root** на свежем Debian/Ubuntu LXC. + +--- + +## 1. Базовые пакеты и Node.js + +```bash +apt-get update -y +apt-get install -y ca-certificates curl gnupg curl git + +curl -fsSL https://deb.nodesource.com/setup_22.x | bash - +apt-get install -y nodejs + +node --version # >= 22 +npm --version +``` + +## 2. Пользователь и каталоги + +```bash +useradd --create-home --shell /bin/bash deploy +mkdir -p /opt/craftshop/server/uploads /opt/craftshop/www +chown -R deploy:deploy /opt/craftshop +chmod 755 /opt/craftshop /opt/craftshop/server /opt/craftshop/www +``` + +## 3. systemd unit + +```bash +cat >/etc/systemd/system/craftshop-api.service <<'UNIT' +[Unit] +Description=Craftshop API (Fastify) +After=network.target + +[Service] +Type=simple +User=deploy +Group=deploy +WorkingDirectory=/opt/craftshop/server +EnvironmentFile=-/opt/craftshop/server/.env +ExecStart=/usr/bin/node src/index.js +Restart=on-failure +RestartSec=5 +LimitNOFILE=65535 + +[Install] +WantedBy=multi-user.target +UNIT + +systemctl daemon-reload +systemctl enable craftshop-api.service +``` + +## 4. Nginx + +```bash +apt-get install -y nginx + +cat >/etc/nginx/sites-available/craftshop <<'NGX' +server { + listen 80 default_server; + listen [::]:80 default_server; + + server_name _; + + root /opt/craftshop/www; + index index.html; + + location /api/ { + client_max_body_size 250m; + proxy_pass http://127.0.0.1:3333; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /uploads/ { + proxy_pass http://127.0.0.1:3333; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location / { + try_files $uri $uri/ /index.html; + } +} +NGX + +rm -f /etc/nginx/sites-enabled/default +ln -sf /etc/nginx/sites-available/craftshop /etc/nginx/sites-enabled/craftshop +nginx -t && systemctl reload nginx +``` + +## 5. NetBird VPN + +Установи NetBird и подключись к сети: + +```bash +curl -fsSL https://pkgs.netbird.io/install.sh | sh +netbird up # потребуется SSO-логин +``` + +Проверь: `ip a` — должен появиться интерфейс `wt0` с IP из твоей NetBird-сети. + +## 6. VPS с Nginx Proxy Manager + +На VPS (где установлен NPM): + +1. Добавь DNS-запись (A) для домена, указывающую на IP VPS +2. В NPM → Proxy Hosts → Add: + - Domain: `craftshop.твой-домен` + - Forward Hostname: `` (IP интерфейса `wt0` на LXC) + - Forward Port: `80` + - SSL: запросить Let's Encrypt сертификат +3. Сохрани + +## 7. Переменные окружения + +```bash +cat >/opt/craftshop/server/.env <<'ENV' +DATABASE_URL="file:./prod.db" +PORT=3333 +JWT_SECRET=<сгенерируй случайную строку> +ADMIN_EMAIL=<твой email> +CORS_ORIGIN=https://craftshop.твой-домен +IS_DEFAULT_CODE_ENABLED=false +ENV +chown deploy:deploy /opt/craftshop/server/.env +chmod 600 /opt/craftshop/server/.env +``` + +Сгенерировать `JWT_SECRET`: + +```bash +node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" +``` + +## 8. Первый деплой + +На машине разработчика: + +```bash +./scripts/deploy-auto.sh --force +``` + +После деплоя: + +```bash +systemctl start craftshop-api +systemctl status craftshop-api +curl http://127.0.0.1:3333/health +``` + +Также проверь через внешний URL: `https://craftshop.твой-домен/api/health`. +``` + +- [ ] **Step 2: Commit** + +```bash +git add scripts/SERVER_SETUP.md && git commit -m "docs: add SERVER_SETUP.md for first-time LXC setup" +``` + +--- + +### Task 5: Update README.md + +**Files:** +- Modify: `README.md` + +- [ ] **Step 1: Remove old doc references and add deploy section** + +Удалить строки со ссылками на удалённые doc-файлы: +- `docs/test-deploy-proxmox.md` +- `docs/deploy-changes.md` +- `docs/nginx-upload-limit.md` + +Добавить в конец README.md: + +```markdown +## Деплой + +```bash +# Настроить scripts/deploy.env +cp scripts/deploy.env.example scripts/deploy.env +# Заполнить DEPLOY_HOST, DEPLOY_PATH + +# Первичная настройка LXC: см. scripts/SERVER_SETUP.md +# Деплой только изменившихся компонентов: +./scripts/deploy-auto.sh + +# Полный деплой (игнорировать diff): +./scripts/deploy-auto.sh --force +``` +``` + +- [ ] **Step 2: Commit** + +```bash +git add README.md && git commit -m "docs: update README with new deploy instructions" +``` + +--- + +## Self-review checklist + +1. **Spec coverage:** + - Удаление файлов → Task 1 ✓ + - .gitignore → Task 2 ✓ + - deploy-auto.sh с детектом diff → Task 3 ✓ + - Интерфейс команд (--force, -f, -b) → Task 3 ✓ + - Transport rsync/tar → Task 3 ✓ + - SERVER_SETUP.md → Task 4 ✓ + - README.md → Task 5 ✓ + - Все пункты spec покрыты ✓ + +2. **Placeholder scan:** полный код в каждом шаге, нет "TBD", "TODO", "implement later" ✓ +3. **Type consistency:** единый подход к переменным (DEPLOY_*), флагам, transport-функциям ✓ diff --git a/docs/superpowers/specs/2026-05-14-auto-deploy-design.md b/docs/superpowers/specs/2026-05-14-auto-deploy-design.md new file mode 100644 index 0000000..dd73d9e --- /dev/null +++ b/docs/superpowers/specs/2026-05-14-auto-deploy-design.md @@ -0,0 +1,95 @@ +# Auto-Deploy для Craftshop + +**Дата:** 2026-05-14 +**Статус:** Утверждён + +## Мотивация + +Заменить ручной SSH-деплой (deploy-ssh.sh) на скрипт с автоматическим детектом изменений через git diff. Деплоятся только те компоненты (client/server), которые реально изменились с последнего деплоя. Удалить устаревшие скрипты и документацию. + +## Состав репозитория после изменений + +### Новые файлы + +| Файл | Назначение | +|---|---| +| `scripts/deploy-auto.sh` | Основной скрипт — детект изменений, сборка, деплой | +| `scripts/SERVER_SETUP.md` | Инструкция с командами для первичной настройки LXC от root | + +### Изменённые файлы + +| Файл | Изменение | +|---|---| +| `.gitignore` | Добавить `.deployed-commit` | +| `README.md` | Убрать ссылки на удалённые doc'и, добавить секцию по деплою | + +### Удаляемые файлы + +| Файл | Причина | +|---|---| +| `scripts/deploy-ssh.sh` | Заменён на `deploy-auto.sh` | +| `scripts/deploy-ssh.ps1` | Не нужен | +| `scripts/deploy.env.example` | Есть `deploy.env` | +| `scripts/read-deploy-env.ps1` | Не нужен | +| `scripts/register-ssh-key-for-root.ps1` | Разовое действие | +| `scripts/complete-lan-deploy.ps1` | Устарел | +| `scripts/craftshop-remote-lan.env` | Устарел | +| `scripts/server-bootstrap.sh` | Заменён на `SERVER_SETUP.md` | +| `docs/deploy-changes.md` | Неактуально | +| `docs/test-deploy-proxmox.md` | Неактуально | +| `docs/nginx-upload-limit.md` | Неактуально | + +## deploy-auto.sh — алгоритм + +1. Загрузить `scripts/deploy.env` (без перезаписи уже экспортированных переменных) +2. Прочитать `.deployed-commit` (если нет файла — взять `HEAD~1`) +3. `git diff --name-only HEAD` → определить: + - `changed_client` — есть изменения в `client/` + - `changed_server` — есть изменения в `server/` + - `changed_shared` — есть изменения в `shared/` +4. Если `changed_shared` → `changed_server=true`, `changed_client=true` (shared используется обоими) +5. Если ни один флаг не true → `echo "nothing to deploy"` → exit 0 +6. **Клиент:** `cd client && npm ci && npm run build` → rsync/tar `client/dist/` → `$DEPLOY_PATH/www/` +7. **Сервер:** rsync/tar `server/` и `shared/` → `$DEPLOY_PATH/` → remote `npm ci && npx prisma generate && npx prisma migrate deploy` → remote `systemctl restart craftshop-api` +8. Если все шаги успешны → записать `HEAD` в `.deployed-commit` +9. Флаг `--force` — игнорировать diff и деплоить всё + +### Интерфейс команд + +| Команда | Что делает | +|---|---| +| `./scripts/deploy-auto.sh` | Деплой изменений | +| `./scripts/deploy-auto.sh --force` | Полный деплой всех компонентов | +| `./scripts/deploy-auto.sh --frontend-only` | Только клиент | +| `./scripts/deploy-auto.sh --backend-only` | Только сервер | + +### Поддерживаемые конфиги (deploy.env) + +Обязательные: `DEPLOY_HOST`, `DEPLOY_PATH` +Опциональные: `DEPLOY_USER` (root), `DEPLOY_SSH_IDENTITY`, `DEPLOY_RESTART_CMD`, `DEPLOY_FRONTEND_DIST` + +### Transport + +Переиспользуется логика из текущего `deploy-ssh.sh`: +- Linux/macOS → rsync +- Git Bash Windows → tar | ssh (из‑за проблем cwRsync) + +## SERVER_SETUP.md — структура + +Инструкция с блоками копируемых команд для последовательного выполнения от root: + +1. Установка Node.js 22.x (NodeSource) +2. Создание пользователя deploy +3. Создание директорий `/opt/craftshop/{server,www,uploads}` +4. systemd unit `craftshop-api` +5. Установка и настройка nginx (статика + proxy /api + /uploads) +6. NetBird VPN (подключение к сети) +7. Настройка `.env` +8. Запуск сервиса и проверка `GET /health` + +## Безопасность + +- `prisma migrate deploy` — не сбрасывает БД, только накатывает новые миграции +- `*.db` исключены из rsync +- `.env` и `.dev_env` исключены из rsync +- `.deployed-commit` — локальный файл, добавлен в .gitignore diff --git a/docs/test-deploy-proxmox.md b/docs/test-deploy-proxmox.md deleted file mode 100644 index 71a5f61..0000000 --- a/docs/test-deploy-proxmox.md +++ /dev/null @@ -1,78 +0,0 @@ -# Тестовый деплой на Proxmox (локально у вас в ЦОД) - -Цель — один недорогой стенд: витрина + API + SQLite, без отказоустойчивого кластера. - -## Серверов и ресурсов - -Рекомендация для **минимального тестового** инстанса на Proxmox: - -| Вариант | Ресурс | Комментарий | -|---------|--------|---------------| -| **1× LXC Debian 12 или Ubuntu 22.04** | CPU: **2 vCPU**, RAM: **1–2 GB**, Диск: **15–25 GB** (SSD/pool SSD) | Один контейнер: Node (API), при желании статика фронта с того же узла через nginx | - -Для сборки (`npm ci` + `vite build`) и запасов по памяти лучше **2 GB RAM**; на **1 GB** возможны OOM при параллельной сборке — собирать фронт можно на машине разработчика и копировать `client/dist`. - -Отделять приложение на **два LXC** (фронт / бэкенд) для теста обычно не нужно; имеет смысл только если хотите явно ограничить поверхность API. - -### Сеть и доступ - -- Одна виртуальная сеть VM/LAN вашего хостинга до Proxmox. -- Открыть снаружи (если нужен доступ из браузера): **80/tcp** и **443/tcp** на reverse proxy; прямой порт Fastify (**3333**) наружу не публиковать. -- Сервер приложения слушает **опционально** `127.0.0.1:3333`, наружу отдаёт **nginx** или **caddy**: - - - статика из `client/dist` (SPA `try_files`); - - - `location /api/` → proxy на `http://127.0.0.1:3333`; - - - `location /uploads/` → те же файлы с диска, что использует сервер (`server/uploads/` рядом с процессом) или через тот же origin с proxy на Fastify `@fastify/static`. - -## Очистка БД локально перед тестом - -В каталоге `server/` (нужны Node **20.6+** или **22+** для флага `--env-file`; иначе скопируйте переменные из `.dev_env` в `.env` и используйте обычные команды Prisma): - -```bash -npm run db:reset:test -``` - -Удалит данные SQLite, заново применит миграции и выполнит `prisma`-seed из `server/package.json`. - -## Программный стек на ВМ/LXC - -- **Node.js** LTS (20.6+, лучше 22; скрипты `dev`, `start:dev_env`, `db:*:test` читают `server/.dev_env` через `node --env-file=.dev_env`). -- Приложение ставится так: склонировать репозиторий, на сервере в `server/` — `npm ci`, `npm run db:migrate:test` (или `migrate deploy`), при необходимости `npm run db:seed:test`. -- Переменные окружения: скопировать `server/.dev_env` или `server/.env.example`, задать **сильный** `JWT_SECRET`, свой `ADMIN_EMAIL`, `DATABASE_URL=file:./dev.db` или путь под персистентный раздел (`/var/lib/craftshop/data.db`). -- Для прод-подобного теста: **`IS_DEFAULT_CODE_ENABLED=false`** (не оставлять общий код на поставку наружу). - -## Сервис (systemd, пример) - -Один юнит `craftshop-api.service`: - -- `WorkingDirectory=/opt/craftshop/server` -- `ExecStart=/usr/bin/node src/index.js` (перед этим экспортировать env через `EnvironmentFile=` к вашему `.env`) -- После деплоя: `systemctl daemon-reload && systemctl enable --now craftshop-api` - -Персистентность: файл SQLite и каталог **`uploads/`** должны жить на диске, который не теряется при пересборке образа контейнера. - -## Контрольный чеклист перед «тестом в бою» - -1. Прогон миграций на чистую БД: `npm run db:reset:test` (только в **закрытом** окружении — стирает данные). -2. Сборка фронта и выставление `VITE_API_URL` (если API не на том же origin, что SPA). -3. `CORS_ORIGIN` — URL публичного фронта. -4. Выключить `DEFAULT_CODE` на внешнем стенде. - -## Краткая схема - -```mermaid -flowchart LR - User[Браузер] - Px[nginx на LXC] - Api[Fastify Node :3333] - Db[(SQLite файл)] - Up[диск uploads] - - User --> Px - Px -->|"/api"| Api - Px -->|"статика /"| Ui[client dist] - Api --> Db - Api --> Up -``` diff --git a/opencode.jsonc b/opencode.jsonc index e0ed0db..a208eff 100644 --- a/opencode.jsonc +++ b/opencode.jsonc @@ -1,5 +1,6 @@ { "$schema": "https://opencode.ai/config.json", + "plugin": ["superpowers@git+https://github.com/obra/superpowers.git"], "mcp": { "context7": { "type": "remote", diff --git a/scripts/complete-lan-deploy.ps1 b/scripts/complete-lan-deploy.ps1 deleted file mode 100644 index 123a76e..0000000 --- a/scripts/complete-lan-deploy.ps1 +++ /dev/null @@ -1,64 +0,0 @@ -# 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 deleted file mode 100644 index 9aeb312..0000000 --- a/scripts/deploy-ssh.ps1 +++ /dev/null @@ -1,82 +0,0 @@ -# Вызывает 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). - -Если npm ci падает с EPERM на .node (Windows): остановите Vite/Node, затем снова .\scripts\deploy-ssh.ps1 -Или: cd client; npm run build; затем .\scripts\deploy-ssh.ps1 -SkipBuild (только выкладка dist). -"@ | 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 deleted file mode 100644 index 8ea53c3..0000000 --- a/scripts/deploy-ssh.sh +++ /dev/null @@ -1,261 +0,0 @@ -#!/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" - remote_exec mkdir -p "$DEPLOY_PATH/shared" - - if should_use_tar_transport; then - echo ">>> Бэкенд (server): 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" - - echo ">>> Бэкенд (shared): tar|ssh → $REMOTE:$DEPLOY_PATH/shared/" - ( - cd "$ROOT/shared" || exit 1 - tar -czf - \ - --exclude=.git \ - . - ) | "${SSH_BASE[@]}" "mkdir -p ${DEPLOY_PATH}/shared && tar xzf - -C ${DEPLOY_PATH}/shared" - fi - else - echo ">>> Бэкенд (server): 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/" - - echo ">>> Бэкенд (shared): rsync → $REMOTE:$DEPLOY_PATH/shared/" - rsync "${RSYNC_OPTS[@]}" \ - -e "$rsh" \ - --exclude .git \ - "${ROOT}/shared/" "${REMOTE}:${DEPLOY_PATH}/shared/" - 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" - remote_exec chown -R "${DEPLOY_SERVER_OWNER}:${DEPLOY_SERVER_OWNER}" "$DEPLOY_PATH/shared" - fi - if [[ -n "${DEPLOY_RESTART_CMD}" ]]; then - echo ">>> Рестарт: $DEPLOY_RESTART_CMD" - remote_exec bash -lc "$DEPLOY_RESTART_CMD" - elif [[ -z "$DRY_RUN" ]]; then - echo "" >&2 - echo "ВНИМАНИЕ: код сервера обновлён, но процесс Node не перезапущен." >&2 - echo " Без рестарта новые маршруты (и правки API) не появятся." >&2 - echo " Задайте в deploy.env: DEPLOY_RESTART_CMD='systemctl restart <ваш-сервис-api>'" >&2 - echo "" >&2 - 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: снимаем блокировки нативных .node (@unrs, @rolldown, .resolver-binding-*)" - echo ">>> Подсказка: остановите «npm run dev» / dev-серверы и IDE, если EPERM останется." - rm -rf "$ROOT/client/node_modules/@unrs" "$ROOT/client/node_modules/@rolldown" 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) || { - echo "" >&2 - echo "Сборка фронта не удалась. На Windows часто EPERM на .node — закройте процессы Node (dev-сервер), повторите." >&2 - echo "Или соберите фронт вручную (cd client && npm run build), затем: $0 --frontend-only --skip-build" >&2 - exit 1 - } - 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 deleted file mode 100644 index 76dd97e..0000000 --- a/scripts/deploy.env.example +++ /dev/null @@ -1,19 +0,0 @@ -# Скопируйте в 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 deleted file mode 100644 index 8d2cd2c..0000000 --- a/scripts/read-deploy-env.ps1 +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 0be12db..0000000 --- a/scripts/register-ssh-key-for-root.ps1 +++ /dev/null @@ -1,33 +0,0 @@ -# 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 deleted file mode 100644 index f7b400c..0000000 --- a/scripts/server-bootstrap.sh +++ /dev/null @@ -1,203 +0,0 @@ -#!/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"