From 513a88826d33dd9126ee6b58fea305223bcbe94c Mon Sep 17 00:00:00 2001 From: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:59:37 -0500 Subject: [PATCH 01/12] Update Docker image name for GitHub registry --- .github/workflows/docker-image.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index bab6cd4f..d2e1e90c 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -84,13 +84,12 @@ jobs: fi echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV - # Determine registry and image name if [ "${{ github.event.inputs.registry }}" == "dockerhub" ]; then echo "REGISTRY=docker.io" >> $GITHUB_ENV echo "IMAGE_NAME=bugattiguy527/termix" >> $GITHUB_ENV else echo "REGISTRY=ghcr.io" >> $GITHUB_ENV - echo "IMAGE_NAME=$REPO_OWNER/termix" >> $GITHUB_ENV + echo "IMAGE_NAME=LukeGus/termix" >> $GITHUB_ENV fi - name: Build and Push Multi-Arch Docker Image From 2450ae732ecfdef2feb73e3ec9f0d5797e8f1cc5 Mon Sep 17 00:00:00 2001 From: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Date: Mon, 20 Oct 2025 13:04:59 -0500 Subject: [PATCH 02/12] Fix image name casing in Docker workflow --- .github/workflows/docker-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index d2e1e90c..8b58b145 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -89,7 +89,7 @@ jobs: echo "IMAGE_NAME=bugattiguy527/termix" >> $GITHUB_ENV else echo "REGISTRY=ghcr.io" >> $GITHUB_ENV - echo "IMAGE_NAME=LukeGus/termix" >> $GITHUB_ENV + echo "IMAGE_NAME=lukegus/termix" >> $GITHUB_ENV fi - name: Build and Push Multi-Arch Docker Image From 8c867d3b16f2ac827a9de911f121075a1de72925 Mon Sep 17 00:00:00 2001 From: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Date: Mon, 20 Oct 2025 15:13:40 -0500 Subject: [PATCH 03/12] Remove untagged image cleanup step from workflow Removed the step to delete untagged image versions from the workflow. --- .github/workflows/docker-image.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 8b58b145..5bfc6c70 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -113,15 +113,6 @@ jobs: rm -rf /tmp/.buildx-cache mv /tmp/.buildx-cache-new /tmp/.buildx-cache - - name: Delete all untagged image versions - if: success() && github.event.inputs.registry != 'dockerhub' - uses: quartx-analytics/ghcr-cleaner@v1 - with: - owner-type: user - token: ${{ secrets.GHCR_TOKEN }} - repository-owner: ${{ github.repository_owner }} - delete-untagged: true - - name: Cleanup Docker Images Locally if: always() run: | From 9dd79929e88df81ce4af392ce84fa38d235d463c Mon Sep 17 00:00:00 2001 From: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Date: Mon, 20 Oct 2025 15:22:41 -0500 Subject: [PATCH 04/12] Change Docker login to use GHCR credentials Updated Docker login credentials for GitHub Container Registry. --- .github/workflows/docker-image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 5bfc6c70..7a2af48e 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -58,8 +58,8 @@ jobs: uses: docker/login-action@v3 with: registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + username: lukegus + password: ${{ secrets.GHCR_TOKEN }} - name: Login to Docker Hub if: github.event.inputs.registry == 'dockerhub' From 300e0a263f89c9590e957d724d63f04d86b56399 Mon Sep 17 00:00:00 2001 From: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Date: Mon, 20 Oct 2025 15:35:52 -0500 Subject: [PATCH 05/12] Remove cache moving step from Docker workflow Removed the step to move the build cache in the Docker workflow. --- .github/workflows/docker-image.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 7a2af48e..aa42b2e6 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -108,11 +108,6 @@ jobs: BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 outputs: type=registry,compression=zstd,compression-level=19 - - name: Move cache - run: | - rm -rf /tmp/.buildx-cache - mv /tmp/.buildx-cache-new /tmp/.buildx-cache - - name: Cleanup Docker Images Locally if: always() run: | From ad1864f062e57651b04bd332827cad4d706a0071 Mon Sep 17 00:00:00 2001 From: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:22:11 -0500 Subject: [PATCH 06/12] Refactor Docker image workflow for versioning and builds --- .github/workflows/docker-image.yml | 113 ++++++++++------------------- 1 file changed, 39 insertions(+), 74 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index aa42b2e6..a616abc4 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -3,103 +3,50 @@ name: Build and Push Docker Image on: workflow_dispatch: inputs: - tag_name: - description: "Custom tag name for the Docker image" - required: false - default: "" - registry: - description: "Docker registry to push to" + version: + description: "Version to build, e.g. 1.8.0" required: true - default: "ghcr" - type: choice - options: - - "ghcr" - - "dockerhub" + production: + description: "Set true for prod build, false for dev build" + required: true + default: false jobs: build: runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - - name: Checkout repository - uses: actions/checkout@v5 + - uses: actions/checkout@v5 with: fetch-depth: 1 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-qemu-action@v3 with: - platforms: arm64 + platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 - - name: Setup Blacksmith Builder - uses: useblacksmith/setup-docker-builder@v1 + - uses: useblacksmith/setup-docker-builder@v1 - - name: Cache npm dependencies - uses: actions/cache@v4 - with: - path: | - ~/.npm - node_modules - */*/node_modules - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - name: Cache Docker layers - uses: actions/cache@v4 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.ref_name }}-${{ hashFiles('docker/Dockerfile') }} - restore-keys: | - ${{ runner.os }}-buildx-${{ github.ref_name }}- - ${{ runner.os }}-buildx- - - - name: Login to GitHub Container Registry - if: github.event.inputs.registry != 'dockerhub' - uses: docker/login-action@v3 + - uses: docker/login-action@v3 with: registry: ghcr.io username: lukegus password: ${{ secrets.GHCR_TOKEN }} - - name: Login to Docker Hub - if: github.event.inputs.registry == 'dockerhub' - uses: docker/login-action@v3 + - uses: docker/login-action@v3 + if: ${{ github.event.inputs.production == 'true' }} with: username: bugattiguy527 password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Determine Docker image tag - run: | - REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') - echo "REPO_OWNER=$REPO_OWNER" >> $GITHUB_ENV - - if [ "${{ github.event.inputs.tag_name }}" != "" ]; then - IMAGE_TAG="${{ github.event.inputs.tag_name }}" - elif [ "${{ github.ref }}" == "refs/heads/main" ]; then - IMAGE_TAG="latest" - elif [ "${{ github.ref }}" == "refs/heads/development" ]; then - IMAGE_TAG="development-latest" - else - IMAGE_TAG="${{ github.ref_name }}" - fi - echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV - - if [ "${{ github.event.inputs.registry }}" == "dockerhub" ]; then - echo "REGISTRY=docker.io" >> $GITHUB_ENV - echo "IMAGE_NAME=bugattiguy527/termix" >> $GITHUB_ENV - else - echo "REGISTRY=ghcr.io" >> $GITHUB_ENV - echo "IMAGE_NAME=lukegus/termix" >> $GITHUB_ENV - fi - - - name: Build and Push Multi-Arch Docker Image + - name: Build and Push to GHCR uses: useblacksmith/build-push-action@v2 with: context: . file: ./docker/Dockerfile push: true - platforms: linux/amd64,linux/arm64 - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 + tags: | + ${{ github.event.inputs.production == 'true' && format('ghcr.io/lukegus/termix:release-{0}', github.event.inputs.version) || format('ghcr.io/lukegus/termix:dev-{0}', github.event.inputs.version) }} + ${{ github.event.inputs.production == 'true' && 'ghcr.io/lukegus/termix:latest' || '' }} labels: | org.opencontainers.image.source=https://github.com/${{ github.repository }} org.opencontainers.image.revision=${{ github.sha }} @@ -108,8 +55,26 @@ jobs: BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 outputs: type=registry,compression=zstd,compression-level=19 - - name: Cleanup Docker Images Locally - if: always() - run: | + - name: Build and Push to Docker Hub + if: ${{ github.event.inputs.production == 'true' }} + uses: useblacksmith/build-push-action@v2 + with: + context: . + file: ./docker/Dockerfile + push: true + platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 + tags: | + docker.io/bugattiguy527/termix:release-${{ github.event.inputs.version }} + docker.io/bugattiguy527/termix:latest + labels: | + org.opencontainers.image.source=https://github.com/${{ github.repository }} + org.opencontainers.image.revision=${{ github.sha }} + build-args: | + BUILDKIT_INLINE_CACHE=1 + BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 + outputs: type=registry,compression=zstd,compression-level=19 + + - run: | docker image prune -af docker system prune -af --volumes + if: always() From 40ac75de813abcf31e04390d72c762a1597826d4 Mon Sep 17 00:00:00 2001 From: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Date: Mon, 20 Oct 2025 17:14:19 -0500 Subject: [PATCH 07/12] Update docker-image.yml --- .github/workflows/docker-image.yml | 86 +++++++++++++++++------------- 1 file changed, 50 insertions(+), 36 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index a616abc4..80ab5da6 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -4,77 +4,91 @@ on: workflow_dispatch: inputs: version: - description: "Version to build, e.g. 1.8.0" + description: "Version to build (e.g., 1.8.0)" required: true production: - description: "Set true for prod build, false for dev build" + description: "Is this a production build?" required: true default: false + type: boolean jobs: build: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - name: Checkout repository + uses: actions/checkout@v5 with: fetch-depth: 1 - - uses: docker/setup-qemu-action@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 with: - platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 + platforms: linux/amd64,linux/arm64,linux/arm/v7 - - uses: useblacksmith/setup-docker-builder@v1 + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - name: Determine tags + id: tags + run: | + VERSION=${{ github.event.inputs.version }} + PROD=${{ github.event.inputs.production }} + + TAGS=() + ALL_TAGS=() + + if [ "$PROD" = "true" ]; then + # Production build → push release + latest to both GHCR and Docker Hub + TAGS+=("release-$VERSION" "latest") + for tag in "${TAGS[@]}"; do + ALL_TAGS+=("ghcr.io/lukegus/termix:$tag") + ALL_TAGS+=("docker.io/bugattiguy527/termix:$tag") + done + else + # Dev build → push only dev-x.x.x to GHCR + TAGS+=("dev-$VERSION") + for tag in "${TAGS[@]}"; do + ALL_TAGS+=("ghcr.io/lukegus/termix:$tag") + done + fi + + echo "ALL_TAGS=${ALL_TAGS[*]}" >> $GITHUB_ENV + echo "All tags to build:" + printf '%s\n' "${ALL_TAGS[@]}" + + - name: Login to GHCR + uses: docker/login-action@v3 with: registry: ghcr.io username: lukegus password: ${{ secrets.GHCR_TOKEN }} - - uses: docker/login-action@v3 + - name: Login to Docker Hub (prod only) if: ${{ github.event.inputs.production == 'true' }} + uses: docker/login-action@v3 with: username: bugattiguy527 password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and Push to GHCR - uses: useblacksmith/build-push-action@v2 + - name: Build and push multi-arch image + uses: docker/build-push-action@v5 with: context: . file: ./docker/Dockerfile push: true - platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 - tags: | - ${{ github.event.inputs.production == 'true' && format('ghcr.io/lukegus/termix:release-{0}', github.event.inputs.version) || format('ghcr.io/lukegus/termix:dev-{0}', github.event.inputs.version) }} - ${{ github.event.inputs.production == 'true' && 'ghcr.io/lukegus/termix:latest' || '' }} - labels: | - org.opencontainers.image.source=https://github.com/${{ github.repository }} - org.opencontainers.image.revision=${{ github.sha }} + platforms: linux/amd64,linux/arm64,linux/arm/v7 + tags: ${{ env.ALL_TAGS }} build-args: | BUILDKIT_INLINE_CACHE=1 BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 - outputs: type=registry,compression=zstd,compression-level=19 - - - name: Build and Push to Docker Hub - if: ${{ github.event.inputs.production == 'true' }} - uses: useblacksmith/build-push-action@v2 - with: - context: . - file: ./docker/Dockerfile - push: true - platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 - tags: | - docker.io/bugattiguy527/termix:release-${{ github.event.inputs.version }} - docker.io/bugattiguy527/termix:latest labels: | org.opencontainers.image.source=https://github.com/${{ github.repository }} org.opencontainers.image.revision=${{ github.sha }} - build-args: | - BUILDKIT_INLINE_CACHE=1 - BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 outputs: type=registry,compression=zstd,compression-level=19 - - run: | + - name: Cleanup Docker + if: always() + run: | docker image prune -af docker system prune -af --volumes - if: always() From b0e49ffb4f07abf526acb33fb00da4dda044dcd7 Mon Sep 17 00:00:00 2001 From: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Date: Wed, 29 Oct 2025 19:19:02 -0500 Subject: [PATCH 08/12] Update print statement from 'Hello' to 'Goodbye' --- .github/workflows/docker-image.yml | 1199 ++++++++++++++++++++++++++-- 1 file changed, 1135 insertions(+), 64 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 80ab5da6..b50a8905 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -1,94 +1,1165 @@ -name: Build and Push Docker Image +name: Build and Push Electron App on: workflow_dispatch: inputs: - version: - description: "Version to build (e.g., 1.8.0)" + build_type: + description: "Platform to build for" required: true - production: - description: "Is this a production build?" + default: "all" + type: choice + options: + - all + - windows + - linux + - macos + artifact_destination: + description: "What to do with the built app" required: true - default: false - type: boolean + default: "file" + type: choice + options: + - none + - file + - release + - submit jobs: - build: - runs-on: ubuntu-latest + build-windows: + runs-on: windows-latest + if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'windows' || github.event.inputs.build_type == '' + steps: - name: Checkout repository uses: actions/checkout@v5 with: fetch-depth: 1 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + - name: Setup Node.js + uses: actions/setup-node@v4 with: - platforms: linux/amd64,linux/arm64,linux/arm/v7 + node-version: "20" + cache: "npm" - - name: Setup Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Determine tags - id: tags + - name: Install dependencies run: | - VERSION=${{ github.event.inputs.version }} - PROD=${{ github.event.inputs.production }} + # Retry npm ci up to 3 times on failure + $maxAttempts = 3 + $attempt = 1 + while ($attempt -le $maxAttempts) { + try { + npm ci + break + } catch { + if ($attempt -eq $maxAttempts) { + Write-Error "npm ci failed after $maxAttempts attempts" + exit 1 + } + Write-Host "npm ci attempt $attempt failed, retrying in 10 seconds..." + Start-Sleep -Seconds 10 + $attempt++ + } + } - TAGS=() - ALL_TAGS=() + - name: Get version + id: package-version + run: | + $VERSION = (Get-Content package.json | ConvertFrom-Json).version + echo "version=$VERSION" >> $env:GITHUB_OUTPUT + echo "Building version: $VERSION" - if [ "$PROD" = "true" ]; then - # Production build → push release + latest to both GHCR and Docker Hub - TAGS+=("release-$VERSION" "latest") - for tag in "${TAGS[@]}"; do - ALL_TAGS+=("ghcr.io/lukegus/termix:$tag") - ALL_TAGS+=("docker.io/bugattiguy527/termix:$tag") - done - else - # Dev build → push only dev-x.x.x to GHCR - TAGS+=("dev-$VERSION") - for tag in "${TAGS[@]}"; do - ALL_TAGS+=("ghcr.io/lukegus/termix:$tag") - done + - name: Build Windows (All Architectures) + run: npm run build && npx electron-builder --win --x64 --ia32 + + - name: List release files + run: | + echo "Contents of release directory:" + dir release + + - name: Upload Windows x64 NSIS Installer + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_x64_*_nsis.exe') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_windows_x64_nsis + path: release/*_x64_*_nsis.exe + retention-days: 30 + + - name: Upload Windows ia32 NSIS Installer + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_ia32_*_nsis.exe') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_windows_ia32_nsis + path: release/*_ia32_*_nsis.exe + retention-days: 30 + + - name: Upload Windows x64 MSI Installer + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_x64_*_msi.msi') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_windows_x64_msi + path: release/*_x64_*_msi.msi + retention-days: 30 + + - name: Upload Windows ia32 MSI Installer + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_ia32_*_msi.msi') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_windows_ia32_msi + path: release/*_ia32_*_msi.msi + retention-days: 30 + + - name: Create Windows x64 Portable zip + if: hashFiles('release/win-unpacked/*') != '' + run: | + $VERSION = "${{ steps.package-version.outputs.version }}" + Compress-Archive -Path "release\win-unpacked\*" -DestinationPath "termix_windows_x64_${VERSION}_portable.zip" + + - name: Create Windows ia32 Portable zip + if: hashFiles('release/win-ia32-unpacked/*') != '' + run: | + $VERSION = "${{ steps.package-version.outputs.version }}" + Compress-Archive -Path "release\win-ia32-unpacked\*" -DestinationPath "termix_windows_ia32_${VERSION}_portable.zip" + + - name: Upload Windows x64 Portable + uses: actions/upload-artifact@v4 + if: hashFiles('termix_windows_x64_*_portable.zip') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_windows_x64_portable + path: termix_windows_x64_*_portable.zip + retention-days: 30 + + - name: Upload Windows ia32 Portable + uses: actions/upload-artifact@v4 + if: hashFiles('termix_windows_ia32_*_portable.zip') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_windows_ia32_portable + path: termix_windows_ia32_*_portable.zip + retention-days: 30 + + build-linux: + runs-on: blacksmith-4vcpu-ubuntu-2404 + if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'linux' || github.event.inputs.build_type == '' + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies + run: | + rm -f package-lock.json + # Retry npm install up to 3 times on failure + for i in 1 2 3; do + if npm install; then + break + else + if [ $i -eq 3 ]; then + echo "npm install failed after 3 attempts" + exit 1 + fi + echo "npm install attempt $i failed, retrying in 10 seconds..." + sleep 10 + fi + done + npm install --force @rollup/rollup-linux-x64-gnu + npm install --force @rollup/rollup-linux-arm64-gnu + npm install --force @rollup/rollup-linux-arm-gnueabihf + + - name: Build Linux (All Architectures) + run: npm run build && npx electron-builder --linux --x64 --arm64 --armv7l + + - name: Rename tar.gz files to match convention + run: | + VERSION=$(node -p "require('./package.json').version") + cd release + + # Rename x64 tar.gz if it exists + if [ -f "termix-${VERSION}-x64.tar.gz" ]; then + mv "termix-${VERSION}-x64.tar.gz" "termix_linux_x64_${VERSION}_portable.tar.gz" + echo "Renamed x64 tar.gz" fi - echo "ALL_TAGS=${ALL_TAGS[*]}" >> $GITHUB_ENV - echo "All tags to build:" - printf '%s\n' "${ALL_TAGS[@]}" + # Rename arm64 tar.gz if it exists + if [ -f "termix-${VERSION}-arm64.tar.gz" ]; then + mv "termix-${VERSION}-arm64.tar.gz" "termix_linux_arm64_${VERSION}_portable.tar.gz" + echo "Renamed arm64 tar.gz" + fi - - name: Login to GHCR - uses: docker/login-action@v3 + # Rename armv7l tar.gz if it exists + if [ -f "termix-${VERSION}-armv7l.tar.gz" ]; then + mv "termix-${VERSION}-armv7l.tar.gz" "termix_linux_armv7l_${VERSION}_portable.tar.gz" + echo "Renamed armv7l tar.gz" + fi + + cd .. + + - name: List release files + run: | + echo "Contents of release directory:" + ls -la release/ + + - name: Upload Linux x64 AppImage + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_x64_*_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none' with: - registry: ghcr.io - username: lukegus - password: ${{ secrets.GHCR_TOKEN }} + name: termix_linux_x64_appimage + path: release/*_x64_*_appimage.AppImage + retention-days: 30 - - name: Login to Docker Hub (prod only) - if: ${{ github.event.inputs.production == 'true' }} - uses: docker/login-action@v3 + - name: Upload Linux arm64 AppImage + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_arm64_*_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none' with: - username: bugattiguy527 - password: ${{ secrets.DOCKERHUB_TOKEN }} + name: termix_linux_arm64_appimage + path: release/*_arm64_*_appimage.AppImage + retention-days: 30 - - name: Build and push multi-arch image - uses: docker/build-push-action@v5 + - name: Upload Linux x64 DEB + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_x64_*_deb.deb') != '' && github.event.inputs.artifact_destination != 'none' with: - context: . - file: ./docker/Dockerfile - push: true - platforms: linux/amd64,linux/arm64,linux/arm/v7 - tags: ${{ env.ALL_TAGS }} - build-args: | - BUILDKIT_INLINE_CACHE=1 - BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 - labels: | - org.opencontainers.image.source=https://github.com/${{ github.repository }} - org.opencontainers.image.revision=${{ github.sha }} - outputs: type=registry,compression=zstd,compression-level=19 + name: termix_linux_x64_deb + path: release/*_x64_*_deb.deb + retention-days: 30 - - name: Cleanup Docker + - name: Upload Linux arm64 DEB + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_arm64_*_deb.deb') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_linux_arm64_deb + path: release/*_arm64_*_deb.deb + retention-days: 30 + + - name: Upload Linux armv7l DEB + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_armv7l_*_deb.deb') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_linux_armv7l_deb + path: release/*_armv7l_*_deb.deb + retention-days: 30 + + - name: Upload Linux x64 tar.gz + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_x64_*_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_linux_x64_portable + path: release/*_x64_*_portable.tar.gz + retention-days: 30 + + - name: Upload Linux arm64 tar.gz + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_arm64_*_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_linux_arm64_portable + path: release/*_arm64_*_portable.tar.gz + retention-days: 30 + + - name: Upload Linux armv7l tar.gz + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_armv7l_*_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_linux_armv7l_portable + path: release/*_armv7l_*_portable.tar.gz + retention-days: 30 + + build-macos: + runs-on: macos-latest + if: github.event.inputs.build_type == 'macos' || github.event.inputs.build_type == 'all' + needs: [] + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies + run: | + # Retry npm ci up to 3 times on failure + for i in 1 2 3; do + if npm ci; then + break + else + if [ $i -eq 3 ]; then + echo "npm ci failed after 3 attempts" + exit 1 + fi + echo "npm ci attempt $i failed, retrying in 10 seconds..." + sleep 10 + fi + done + npm install --force @rollup/rollup-darwin-arm64 + npm install dmg-license + + - name: Check for Code Signing Certificates + id: check_certs + run: | + if [ -n "${{ secrets.MAC_BUILD_CERTIFICATE_BASE64 }}" ] && [ -n "${{ secrets.MAC_P12_PASSWORD }}" ]; then + echo "has_certs=true" >> $GITHUB_OUTPUT + else + echo "has_certs=false" >> $GITHUB_OUTPUT + echo "⚠️ Code signing certificates not configured. MAS build will be unsigned." + fi + + - name: Import Code Signing Certificates + if: steps.check_certs.outputs.has_certs == 'true' + env: + MAC_BUILD_CERTIFICATE_BASE64: ${{ secrets.MAC_BUILD_CERTIFICATE_BASE64 }} + MAC_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.MAC_INSTALLER_CERTIFICATE_BASE64 }} + MAC_P12_PASSWORD: ${{ secrets.MAC_P12_PASSWORD }} + MAC_KEYCHAIN_PASSWORD: ${{ secrets.MAC_KEYCHAIN_PASSWORD }} + run: | + APP_CERT_PATH=$RUNNER_TEMP/app_certificate.p12 + INSTALLER_CERT_PATH=$RUNNER_TEMP/installer_certificate.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + + # Decode certificates + echo -n "$MAC_BUILD_CERTIFICATE_BASE64" | base64 --decode -o $APP_CERT_PATH + + if [ -n "$MAC_INSTALLER_CERTIFICATE_BASE64" ]; then + echo "Decoding installer certificate..." + echo -n "$MAC_INSTALLER_CERTIFICATE_BASE64" | base64 --decode -o $INSTALLER_CERT_PATH + else + echo "⚠️ MAC_INSTALLER_CERTIFICATE_BASE64 is empty" + fi + + # Create and configure keychain + security create-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # Import application certificate + echo "Importing application certificate..." + security import $APP_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + + # Import installer certificate if it exists + if [ -f "$INSTALLER_CERT_PATH" ]; then + echo "Importing installer certificate..." + security import $INSTALLER_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + else + echo "⚠️ Installer certificate file not found, skipping import" + fi + + security list-keychain -d user -s $KEYCHAIN_PATH + + echo "Imported certificates:" + security find-identity -v -p codesigning $KEYCHAIN_PATH + + - name: Build macOS App Store Package + if: steps.check_certs.outputs.has_certs == 'true' + env: + ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true + run: | + # Get current version for display + CURRENT_VERSION=$(node -p "require('./package.json').version") + BUILD_VERSION="${{ github.run_number }}" + + echo "✅ Package version: $CURRENT_VERSION (unchanged)" + echo "✅ Build number for Apple: $BUILD_VERSION" + + # Build MAS with custom buildVersion + npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION" + + - name: Clean up MAS keychain before DMG build + if: steps.check_certs.outputs.has_certs == 'true' + run: | + security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true + echo "Cleaned up MAS keychain" + + - name: Check for Developer ID Certificates + id: check_dev_id_certs + run: | + if [ -n "${{ secrets.DEVELOPER_ID_CERTIFICATE_BASE64 }}" ] && [ -n "${{ secrets.DEVELOPER_ID_P12_PASSWORD }}" ]; then + echo "has_dev_id_certs=true" >> $GITHUB_OUTPUT + echo "✅ Developer ID certificates configured for DMG signing" + else + echo "has_dev_id_certs=false" >> $GITHUB_OUTPUT + echo "⚠️ Developer ID certificates not configured. DMG will be unsigned." + echo "Add DEVELOPER_ID_CERTIFICATE_BASE64 and DEVELOPER_ID_P12_PASSWORD secrets to enable DMG signing." + fi + + - name: Import Developer ID Certificates + if: steps.check_dev_id_certs.outputs.has_dev_id_certs == 'true' + env: + DEVELOPER_ID_CERTIFICATE_BASE64: ${{ secrets.DEVELOPER_ID_CERTIFICATE_BASE64 }} + DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64 }} + DEVELOPER_ID_P12_PASSWORD: ${{ secrets.DEVELOPER_ID_P12_PASSWORD }} + MAC_KEYCHAIN_PASSWORD: ${{ secrets.MAC_KEYCHAIN_PASSWORD }} + run: | + DEV_CERT_PATH=$RUNNER_TEMP/dev_certificate.p12 + DEV_INSTALLER_CERT_PATH=$RUNNER_TEMP/dev_installer_certificate.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/dev-signing.keychain-db + + # Decode Developer ID certificate + echo -n "$DEVELOPER_ID_CERTIFICATE_BASE64" | base64 --decode -o $DEV_CERT_PATH + + if [ -n "$DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64" ]; then + echo "Decoding Developer ID installer certificate..." + echo -n "$DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64" | base64 --decode -o $DEV_INSTALLER_CERT_PATH + else + echo "⚠️ DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64 is empty (optional)" + fi + + # Create and configure keychain + security create-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # Import Developer ID Application certificate + echo "Importing Developer ID Application certificate..." + security import $DEV_CERT_PATH -P "$DEVELOPER_ID_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + + # Import Developer ID Installer certificate if it exists + if [ -f "$DEV_INSTALLER_CERT_PATH" ]; then + echo "Importing Developer ID Installer certificate..." + security import $DEV_INSTALLER_CERT_PATH -P "$DEVELOPER_ID_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + fi + + security list-keychain -d user -s $KEYCHAIN_PATH + + echo "Imported Developer ID certificates:" + security find-identity -v -p codesigning $KEYCHAIN_PATH + + - name: Build macOS DMG + env: + ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + # Build DMG without running npm run build again (already built above or skip if no certs) + if [ "${{ steps.check_certs.outputs.has_certs }}" == "true" ]; then + # Frontend already built, just package DMG + npx electron-builder --mac dmg --universal --x64 --arm64 + else + # No certs, need to build frontend first + npm run build && npx electron-builder --mac dmg --universal --x64 --arm64 + fi + + - name: List release directory + if: steps.check_certs.outputs.has_certs == 'true' + run: | + echo "Contents of release directory:" + ls -R release/ || echo "Release directory not found" + + - name: Upload macOS MAS PKG + if: steps.check_certs.outputs.has_certs == 'true' && hashFiles('release/*_*_*_mas.pkg') != '' && (github.event.inputs.artifact_destination == 'file' || github.event.inputs.artifact_destination == 'release' || github.event.inputs.artifact_destination == 'submit') + uses: actions/upload-artifact@v4 + with: + name: termix_macos_mas + path: release/*_*_*_mas.pkg + retention-days: 30 + if-no-files-found: warn + + - name: Upload macOS Universal DMG + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_universal_*_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_macos_universal_dmg + path: release/*_universal_*_dmg.dmg + retention-days: 30 + + - name: Upload macOS x64 DMG + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_x64_*_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_macos_x64_dmg + path: release/*_x64_*_dmg.dmg + retention-days: 30 + + - name: Upload macOS arm64 DMG + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_arm64_*_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_macos_arm64_dmg + path: release/*_arm64_*_dmg.dmg + retention-days: 30 + + - name: Check for App Store Connect API credentials + if: steps.check_certs.outputs.has_certs == 'true' + id: check_asc_creds + run: | + if [ -n "${{ secrets.APPLE_KEY_ID }}" ] && [ -n "${{ secrets.APPLE_ISSUER_ID }}" ] && [ -n "${{ secrets.APPLE_KEY_CONTENT }}" ]; then + echo "has_credentials=true" >> $GITHUB_OUTPUT + if [ "${{ github.event.inputs.artifact_destination }}" == "submit" ]; then + echo "✅ App Store Connect API credentials found. Will deploy to TestFlight." + else + echo "ℹ️ App Store Connect API credentials found, but store submission is disabled." + fi + else + echo "has_credentials=false" >> $GITHUB_OUTPUT + echo "⚠️ App Store Connect API credentials not configured. Skipping deployment." + echo "Add APPLE_KEY_ID, APPLE_ISSUER_ID, and APPLE_KEY_CONTENT secrets to enable automatic deployment." + fi + + - name: Setup Ruby for Fastlane + if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit' + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2' + bundler-cache: false + + - name: Install Fastlane + if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit' + run: | + gem install fastlane -N + fastlane --version + + - name: Deploy to App Store Connect (TestFlight) + if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit' + run: | + PKG_FILE=$(find release -name "*.pkg" -type f | head -n 1) + if [ -z "$PKG_FILE" ]; then + echo "Error: No .pkg file found in release directory" + exit 1 + fi + echo "Found package: $PKG_FILE" + + # Create API key file + mkdir -p ~/private_keys + echo "${{ secrets.APPLE_KEY_CONTENT }}" | base64 --decode > ~/private_keys/AuthKey_${{ secrets.APPLE_KEY_ID }}.p8 + + # Upload to App Store Connect using xcrun altool + xcrun altool --upload-app -f "$PKG_FILE" \ + --type macos \ + --apiKey "${{ secrets.APPLE_KEY_ID }}" \ + --apiIssuer "${{ secrets.APPLE_ISSUER_ID }}" + + echo "✅ Upload complete! Build will appear in App Store Connect after processing (10-30 minutes)" + continue-on-error: true + + - name: Clean up keychains if: always() run: | - docker image prune -af - docker system prune -af --volumes + security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true + security delete-keychain $RUNNER_TEMP/dev-signing.keychain-db || true + + submit-to-chocolatey: + runs-on: windows-latest + if: github.event.inputs.artifact_destination == 'submit' + needs: [build-windows] + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Get version from package.json + id: package-version + run: | + $VERSION = (Get-Content package.json | ConvertFrom-Json).version + echo "version=$VERSION" >> $env:GITHUB_OUTPUT + echo "Building Chocolatey package for version: $VERSION" + + - name: Download Windows x64 MSI artifact + uses: actions/download-artifact@v4 + with: + name: termix_windows_x64_msi + path: artifact + + - name: Get MSI file info + id: msi-info + run: | + $VERSION = "${{ steps.package-version.outputs.version }}" + $MSI_FILE = Get-ChildItem -Path artifact -Filter "*.msi" | Select-Object -First 1 + $MSI_NAME = $MSI_FILE.Name + $CHECKSUM = (Get-FileHash -Path $MSI_FILE.FullName -Algorithm SHA256).Hash + + echo "msi_name=$MSI_NAME" >> $env:GITHUB_OUTPUT + echo "checksum=$CHECKSUM" >> $env:GITHUB_OUTPUT + echo "MSI File: $MSI_NAME" + echo "SHA256: $CHECKSUM" + + - name: Prepare Chocolatey package + run: | + $VERSION = "${{ steps.package-version.outputs.version }}" + $CHECKSUM = "${{ steps.msi-info.outputs.checksum }}" + $MSI_NAME = "${{ steps.msi-info.outputs.msi_name }}" + + # Construct the download URL with the actual release tag format + $DOWNLOAD_URL = "https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$MSI_NAME" + + # Copy chocolatey files to build directory + New-Item -ItemType Directory -Force -Path "choco-build" + Copy-Item -Path "chocolatey\*" -Destination "choco-build" -Recurse -Force + + # Update chocolateyinstall.ps1 with actual values + $installScript = Get-Content "choco-build\tools\chocolateyinstall.ps1" -Raw -Encoding UTF8 + $installScript = $installScript -replace 'DOWNLOAD_URL_PLACEHOLDER', $DOWNLOAD_URL + $installScript = $installScript -replace 'CHECKSUM_PLACEHOLDER', $CHECKSUM + [System.IO.File]::WriteAllText("$PWD\choco-build\tools\chocolateyinstall.ps1", $installScript, [System.Text.UTF8Encoding]::new($false)) + + # Update nuspec with version (preserve UTF-8 encoding without BOM) + $nuspec = Get-Content "choco-build\termix-ssh.nuspec" -Raw -Encoding UTF8 + $nuspec = $nuspec -replace 'VERSION_PLACEHOLDER', $VERSION + [System.IO.File]::WriteAllText("$PWD\choco-build\termix-ssh.nuspec", $nuspec, [System.Text.UTF8Encoding]::new($false)) + + echo "Chocolatey package prepared for version $VERSION" + echo "Download URL: $DOWNLOAD_URL" + + # Verify the nuspec is valid + echo "" + echo "Verifying nuspec content:" + Get-Content "choco-build\termix-ssh.nuspec" -Head 10 + echo "" + + - name: Install Chocolatey + run: | + Set-ExecutionPolicy Bypass -Scope Process -Force + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 + iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) + + - name: Pack Chocolatey package + run: | + cd choco-build + echo "Packing Chocolatey package..." + choco pack termix-ssh.nuspec + + if ($LASTEXITCODE -ne 0) { + echo "❌ Failed to pack Chocolatey package" + exit 1 + } + + echo "" + echo "✅ Package created successfully" + echo "Package contents:" + Get-ChildItem *.nupkg | ForEach-Object { echo $_.Name } + + - name: Check for Chocolatey API Key + id: check_choco_key + run: | + if ("${{ secrets.CHOCOLATEY_API_KEY }}" -ne "") { + echo "has_key=true" >> $env:GITHUB_OUTPUT + echo "✅ Chocolatey API key found. Will push to Chocolatey." + } else { + echo "has_key=false" >> $env:GITHUB_OUTPUT + echo "⚠️ Chocolatey API key not configured. Package will be created but not pushed." + echo "Add CHOCOLATEY_API_KEY secret to enable automatic submission." + } + + - name: Push to Chocolatey + if: steps.check_choco_key.outputs.has_key == 'true' + run: | + $VERSION = "${{ steps.package-version.outputs.version }}" + cd choco-build + choco apikey --key "${{ secrets.CHOCOLATEY_API_KEY }}" --source https://push.chocolatey.org/ + + try { + choco push "termix-ssh.$VERSION.nupkg" --source https://push.chocolatey.org/ + if ($LASTEXITCODE -eq 0) { + echo "" + echo "✅ Package pushed to Chocolatey successfully!" + echo "View at: https://community.chocolatey.org/packages/termix-ssh/$VERSION" + } else { + throw "Chocolatey push failed with exit code $LASTEXITCODE" + } + } catch { + echo "" + echo "❌ Failed to push to Chocolatey" + echo "" + echo "Common reasons:" + echo "1. Package ID 'termix-ssh' is already owned by another user" + echo "2. You need to register/claim the package ID first" + echo "3. API key doesn't have push permissions" + echo "" + echo "Solutions:" + echo "1. Check if package exists: https://community.chocolatey.org/packages/termix-ssh" + echo "2. If it exists and is yours, contact Chocolatey support to claim it" + echo "3. Register a new package ID at: https://community.chocolatey.org/" + echo "" + echo "The package artifact has been saved for manual submission." + echo "" + exit 1 + } + + - name: Upload Chocolatey package as artifact + uses: actions/upload-artifact@v4 + with: + name: chocolatey-package + path: choco-build/*.nupkg + retention-days: 30 + + submit-to-flatpak: + runs-on: ubuntu-latest + if: github.event.inputs.artifact_destination == 'submit' + needs: [build-linux] + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Get version from package.json + id: package-version + run: | + VERSION=$(node -p "require('./package.json').version") + RELEASE_DATE=$(date +%Y-%m-%d) + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "release_date=$RELEASE_DATE" >> $GITHUB_OUTPUT + echo "Building Flatpak submission for version: $VERSION" + + - name: Download Linux x64 AppImage artifact + uses: actions/download-artifact@v4 + with: + name: termix_linux_x64_appimage + path: artifact-x64 + + - name: Download Linux arm64 AppImage artifact + uses: actions/download-artifact@v4 + with: + name: termix_linux_arm64_appimage + path: artifact-arm64 + + - name: Get AppImage file info + id: appimage-info + run: | + VERSION="${{ steps.package-version.outputs.version }}" + + # x64 AppImage + APPIMAGE_X64_FILE=$(find artifact-x64 -name "*.AppImage" -type f | head -n 1) + APPIMAGE_X64_NAME=$(basename "$APPIMAGE_X64_FILE") + CHECKSUM_X64=$(sha256sum "$APPIMAGE_X64_FILE" | awk '{print $1}') + + # arm64 AppImage + APPIMAGE_ARM64_FILE=$(find artifact-arm64 -name "*.AppImage" -type f | head -n 1) + APPIMAGE_ARM64_NAME=$(basename "$APPIMAGE_ARM64_FILE") + CHECKSUM_ARM64=$(sha256sum "$APPIMAGE_ARM64_FILE" | awk '{print $1}') + + echo "appimage_x64_name=$APPIMAGE_X64_NAME" >> $GITHUB_OUTPUT + echo "checksum_x64=$CHECKSUM_X64" >> $GITHUB_OUTPUT + echo "appimage_arm64_name=$APPIMAGE_ARM64_NAME" >> $GITHUB_OUTPUT + echo "checksum_arm64=$CHECKSUM_ARM64" >> $GITHUB_OUTPUT + + echo "x64 AppImage: $APPIMAGE_X64_NAME" + echo "x64 SHA256: $CHECKSUM_X64" + echo "arm64 AppImage: $APPIMAGE_ARM64_NAME" + echo "arm64 SHA256: $CHECKSUM_ARM64" + + - name: Install ImageMagick for icon generation + run: | + sudo apt-get update + sudo apt-get install -y imagemagick + + - name: Prepare Flatpak submission files + run: | + VERSION="${{ steps.package-version.outputs.version }}" + CHECKSUM_X64="${{ steps.appimage-info.outputs.checksum_x64 }}" + CHECKSUM_ARM64="${{ steps.appimage-info.outputs.checksum_arm64 }}" + RELEASE_DATE="${{ steps.package-version.outputs.release_date }}" + APPIMAGE_X64_NAME="${{ steps.appimage-info.outputs.appimage_x64_name }}" + APPIMAGE_ARM64_NAME="${{ steps.appimage-info.outputs.appimage_arm64_name }}" + + # Create submission directory + mkdir -p flatpak-submission + + # Copy Flatpak files to submission directory + cp flatpak/com.karmaa.termix.yml flatpak-submission/ + cp flatpak/com.karmaa.termix.desktop flatpak-submission/ + cp flatpak/com.karmaa.termix.metainfo.xml flatpak-submission/ + cp flatpak/flathub.json flatpak-submission/ + + # Copy and prepare icons + cp public/icon.svg flatpak-submission/com.karmaa.termix.svg + convert public/icon.png -resize 256x256 flatpak-submission/icon-256.png + convert public/icon.png -resize 128x128 flatpak-submission/icon-128.png + + # Update manifest with version and checksums + sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak-submission/com.karmaa.termix.yml + sed -i "s/CHECKSUM_X64_PLACEHOLDER/$CHECKSUM_X64/g" flatpak-submission/com.karmaa.termix.yml + sed -i "s/CHECKSUM_ARM64_PLACEHOLDER/$CHECKSUM_ARM64/g" flatpak-submission/com.karmaa.termix.yml + + # Update metainfo with version and date + sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak-submission/com.karmaa.termix.metainfo.xml + sed -i "s/DATE_PLACEHOLDER/$RELEASE_DATE/g" flatpak-submission/com.karmaa.termix.metainfo.xml + + echo "✅ Flatpak submission files prepared for version $VERSION" + echo "x64 Download URL: https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_X64_NAME" + echo "arm64 Download URL: https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_ARM64_NAME" + + - name: Create submission instructions + run: | + cat > flatpak-submission/SUBMISSION_INSTRUCTIONS.md << 'EOF' + # Flathub Submission Instructions for Termix + + ## Automatic Submission (Recommended) + + All files needed for Flathub submission are in this artifact. Follow these steps: + + 1. **Fork the Flathub repository**: + - Go to https://github.com/flathub/flathub + - Click "Fork" button + + 2. **Clone your fork**: + ```bash + git clone https://github.com/YOUR-USERNAME/flathub.git + cd flathub + git checkout -b com.karmaa.termix + ``` + + 3. **Copy all files from this artifact** to the root of your flathub fork + + 4. **Commit and push**: + ```bash + git add . + git commit -m "Add Termix ${{ steps.package-version.outputs.version }}" + git push origin com.karmaa.termix + ``` + + 5. **Create Pull Request**: + - Go to https://github.com/YOUR-USERNAME/flathub + - Click "Compare & pull request" + - Submit PR to flathub/flathub + + ## Files in this submission: + + - `com.karmaa.termix.yml` - Flatpak manifest + - `com.karmaa.termix.desktop` - Desktop entry + - `com.karmaa.termix.metainfo.xml` - AppStream metadata + - `flathub.json` - Flathub configuration + - `com.karmaa.termix.svg` - SVG icon + - `icon-256.png` - 256x256 icon + - `icon-128.png` - 128x128 icon + + ## Version Information: + + - Version: ${{ steps.package-version.outputs.version }} + - Release Date: ${{ steps.package-version.outputs.release_date }} + - x64 AppImage SHA256: ${{ steps.appimage-info.outputs.checksum_x64 }} + - arm64 AppImage SHA256: ${{ steps.appimage-info.outputs.checksum_arm64 }} + + ## After Submission: + + 1. Flathub maintainers will review your submission (usually 1-5 days) + 2. They may request changes - be responsive to feedback + 3. Once approved, Termix will be available via: `flatpak install flathub com.karmaa.termix` + + ## Resources: + + - [Flathub Submission Guidelines](https://docs.flathub.org/docs/for-app-authors/submission) + - [Flatpak Documentation](https://docs.flatpak.org/) + EOF + + echo "✅ Created submission instructions" + + - name: List submission files + run: | + echo "Flatpak submission files:" + ls -la flatpak-submission/ + + - name: Upload Flatpak submission as artifact + uses: actions/upload-artifact@v4 + with: + name: flatpak-submission + path: flatpak-submission/* + retention-days: 30 + + - name: Display next steps + run: | + echo "" + echo "🎉 Flatpak submission files ready!" + echo "" + echo "📦 Download the 'flatpak-submission' artifact and follow SUBMISSION_INSTRUCTIONS.md" + echo "" + echo "Quick summary:" + echo "1. Fork https://github.com/flathub/flathub" + echo "2. Copy artifact files to your fork" + echo "3. Create PR to flathub/flathub" + echo "" + + submit-to-homebrew: + runs-on: macos-latest + if: github.event.inputs.artifact_destination == 'submit' + needs: [build-macos] + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Get version from package.json + id: package-version + run: | + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Building Homebrew Cask for version: $VERSION" + + - name: Download macOS Universal DMG artifact + uses: actions/download-artifact@v4 + with: + name: termix_macos_universal_dmg + path: artifact + + - name: Get DMG file info + id: dmg-info + run: | + VERSION="${{ steps.package-version.outputs.version }}" + DMG_FILE=$(find artifact -name "*.dmg" -type f | head -n 1) + DMG_NAME=$(basename "$DMG_FILE") + CHECKSUM=$(shasum -a 256 "$DMG_FILE" | awk '{print $1}') + + echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT + echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT + echo "DMG File: $DMG_NAME" + echo "SHA256: $CHECKSUM" + + - name: Prepare Homebrew submission files + run: | + VERSION="${{ steps.package-version.outputs.version }}" + CHECKSUM="${{ steps.dmg-info.outputs.checksum }}" + DMG_NAME="${{ steps.dmg-info.outputs.dmg_name }}" + + # Create submission directory + mkdir -p homebrew-submission/Casks/t + + # Copy Homebrew cask file + cp homebrew/termix.rb homebrew-submission/Casks/t/termix.rb + cp homebrew/README.md homebrew-submission/ + + # Update cask with version and checksum + sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" homebrew-submission/Casks/t/termix.rb + sed -i '' "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" homebrew-submission/Casks/t/termix.rb + + echo "✅ Homebrew Cask prepared for version $VERSION" + echo "Download URL: https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$DMG_NAME" + + - name: Verify Cask syntax + run: | + # Install Homebrew if not present (should be on macos-latest) + if ! command -v brew &> /dev/null; then + echo "Installing Homebrew..." + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + fi + + # Basic syntax check + ruby -c homebrew-submission/Casks/t/termix.rb + echo "✅ Cask syntax is valid" + + - name: Create submission instructions + run: | + cat > homebrew-submission/SUBMISSION_INSTRUCTIONS.md << 'EOF' + # Homebrew Cask Submission Instructions for Termix + + ## Option 1: Submit to Official Homebrew Cask (Recommended) + + ### Prerequisites + - macOS with Homebrew installed + - GitHub account + + ### Steps + + 1. **Fork the Homebrew Cask repository**: + - Go to https://github.com/Homebrew/homebrew-cask + - Click "Fork" button + + 2. **Clone your fork**: + ```bash + git clone https://github.com/YOUR-USERNAME/homebrew-cask.git + cd homebrew-cask + git checkout -b termix + ``` + + 3. **Copy the cask file**: + - Copy `Casks/t/termix.rb` from this artifact to your fork at `Casks/t/termix.rb` + - Note: Casks are organized by first letter in subdirectories + + 4. **Test the cask locally**: + ```bash + brew install --cask ./Casks/t/termix.rb + brew uninstall --cask termix + ``` + + 5. **Run audit checks**: + ```bash + brew audit --cask --online ./Casks/t/termix.rb + brew style ./Casks/t/termix.rb + ``` + + 6. **Commit and push**: + ```bash + git add Casks/t/termix.rb + git commit -m "Add Termix ${{ steps.package-version.outputs.version }}" + git push origin termix + ``` + + 7. **Create Pull Request**: + - Go to https://github.com/YOUR-USERNAME/homebrew-cask + - Click "Compare & pull request" + - Fill in the PR template + - Submit to Homebrew/homebrew-cask + + ### PR Requirements + + Your PR should include: + - Clear commit message: "Add Termix X.Y.Z" or "Update Termix to X.Y.Z" + - All audit checks passing + - Working download URL + - Valid SHA256 checksum + + ## Option 2: Create Your Own Tap (Alternative) + + If you want more control and faster updates: + + 1. **Create a tap repository**: + - Create repo: `Termix-SSH/homebrew-termix` + - Add `Casks/termix.rb` to the repo + + 2. **Users install with**: + ```bash + brew tap termix-ssh/termix + brew install --cask termix + ``` + + ### Advantages of Custom Tap + - No approval process + - Instant updates + - Full control + - Can include beta versions + + ### Disadvantages + - Less discoverable + - Users must add tap first + - You maintain it yourself + + ## Files in this submission: + + - `Casks/t/termix.rb` - Homebrew Cask formula + - `README.md` - Detailed documentation + - `SUBMISSION_INSTRUCTIONS.md` - This file + + ## Version Information: + + - Version: ${{ steps.package-version.outputs.version }} + - DMG SHA256: ${{ steps.dmg-info.outputs.checksum }} + - DMG URL: https://github.com/Termix-SSH/Termix/releases/download/release-${{ steps.package-version.outputs.version }}-tag/${{ steps.dmg-info.outputs.dmg_name }} + + ## After Submission: + + ### Official Homebrew Cask: + 1. Maintainers will review (usually 24-48 hours) + 2. May request changes or fixes + 3. Once merged, users can install with: `brew install --cask termix` + 4. Homebrew bot will auto-update for future releases + + ### Custom Tap: + 1. Push to your tap repository + 2. Immediately available to users + 3. Update the cask file for each new release + + ## Resources: + + - [Homebrew Cask Documentation](https://docs.brew.sh/Cask-Cookbook) + - [Acceptable Casks](https://docs.brew.sh/Acceptable-Casks) + - [How to Open a PR](https://docs.brew.sh/How-To-Open-a-Homebrew-Pull-Request) + EOF + + echo "✅ Created submission instructions" + + - name: List submission files + run: | + echo "Homebrew submission files:" + find homebrew-submission -type f + + - name: Upload Homebrew submission as artifact + uses: actions/upload-artifact@v4 + with: + name: homebrew-submission + path: homebrew-submission/* + retention-days: 30 + + - name: Display next steps + run: | + echo "" + echo "🍺 Homebrew Cask ready!" + echo "" + echo "📦 Download the 'homebrew-submission' artifact and follow SUBMISSION_INSTRUCTIONS.md" + echo "" + echo "Quick summary:" + echo "Option 1 (Recommended): Fork https://github.com/Homebrew/homebrew-cask and submit PR" + echo "Option 2 (Alternative): Create your own tap at Termix-SSH/homebrew-termix" + echo "" + + upload-to-release: + runs-on: blacksmith-4vcpu-ubuntu-2404 + if: github.event.inputs.artifact_destination == 'release' + needs: [build-windows, build-linux, build-macos] + permissions: + contents: write + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Get latest release + id: get_release + run: | + echo "Fetching latest release from ${{ github.repository }}..." + LATEST_RELEASE=$(gh release list --repo ${{ github.repository }} --limit 1 --json tagName,name,isLatest -q '.[0]') + + if [ -z "$LATEST_RELEASE" ]; then + echo "ERROR: No releases found in ${{ github.repository }}" + exit 1 + fi + + RELEASE_TAG=$(echo "$LATEST_RELEASE" | jq -r '.tagName') + RELEASE_NAME=$(echo "$LATEST_RELEASE" | jq -r '.name') + + echo "tag=$RELEASE_TAG" >> $GITHUB_OUTPUT + echo "name=$RELEASE_NAME" >> $GITHUB_OUTPUT + echo "Latest release: $RELEASE_NAME ($RELEASE_TAG)" + env: + GH_TOKEN: ${{ github.token }} + + - name: Display artifact structure + run: | + echo "Artifact structure:" + ls -R artifacts/ + + - name: Upload artifacts to latest release + run: | + RELEASE_TAG="${{ steps.get_release.outputs.tag }}" + echo "Uploading artifacts to release: $RELEASE_TAG" + echo "" + + cd artifacts + for dir in */; do + echo "Processing directory: $dir" + cd "$dir" + for file in *; do + if [ -f "$file" ]; then + echo "Uploading: $file" + gh release upload "$RELEASE_TAG" "$file" --repo ${{ github.repository }} --clobber + echo "✓ $file uploaded successfully" + fi + done + cd .. + done + + echo "" + echo "All artifacts uploaded to: https://github.com/${{ github.repository }}/releases/tag/$RELEASE_TAG" + env: + GH_TOKEN: ${{ github.token }} From df195693131db15f2e5d36245710bc4a04ac4da7 Mon Sep 17 00:00:00 2001 From: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Date: Wed, 29 Oct 2025 19:19:47 -0500 Subject: [PATCH 09/12] Update docker build --- .github/workflows/docker-image.yml | 1197 ++-------------------------- 1 file changed, 63 insertions(+), 1134 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index b50a8905..80ab5da6 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -1,1165 +1,94 @@ -name: Build and Push Electron App +name: Build and Push Docker Image on: workflow_dispatch: inputs: - build_type: - description: "Platform to build for" + version: + description: "Version to build (e.g., 1.8.0)" required: true - default: "all" - type: choice - options: - - all - - windows - - linux - - macos - artifact_destination: - description: "What to do with the built app" + production: + description: "Is this a production build?" required: true - default: "file" - type: choice - options: - - none - - file - - release - - submit + default: false + type: boolean jobs: - build-windows: - runs-on: windows-latest - if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'windows' || github.event.inputs.build_type == '' - + build: + runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v5 with: fetch-depth: 1 - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 with: - node-version: "20" - cache: "npm" + platforms: linux/amd64,linux/arm64,linux/arm/v7 - - name: Install dependencies + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Determine tags + id: tags run: | - # Retry npm ci up to 3 times on failure - $maxAttempts = 3 - $attempt = 1 - while ($attempt -le $maxAttempts) { - try { - npm ci - break - } catch { - if ($attempt -eq $maxAttempts) { - Write-Error "npm ci failed after $maxAttempts attempts" - exit 1 - } - Write-Host "npm ci attempt $attempt failed, retrying in 10 seconds..." - Start-Sleep -Seconds 10 - $attempt++ - } - } + VERSION=${{ github.event.inputs.version }} + PROD=${{ github.event.inputs.production }} - - name: Get version - id: package-version - run: | - $VERSION = (Get-Content package.json | ConvertFrom-Json).version - echo "version=$VERSION" >> $env:GITHUB_OUTPUT - echo "Building version: $VERSION" + TAGS=() + ALL_TAGS=() - - name: Build Windows (All Architectures) - run: npm run build && npx electron-builder --win --x64 --ia32 - - - name: List release files - run: | - echo "Contents of release directory:" - dir release - - - name: Upload Windows x64 NSIS Installer - uses: actions/upload-artifact@v4 - if: hashFiles('release/*_x64_*_nsis.exe') != '' && github.event.inputs.artifact_destination != 'none' - with: - name: termix_windows_x64_nsis - path: release/*_x64_*_nsis.exe - retention-days: 30 - - - name: Upload Windows ia32 NSIS Installer - uses: actions/upload-artifact@v4 - if: hashFiles('release/*_ia32_*_nsis.exe') != '' && github.event.inputs.artifact_destination != 'none' - with: - name: termix_windows_ia32_nsis - path: release/*_ia32_*_nsis.exe - retention-days: 30 - - - name: Upload Windows x64 MSI Installer - uses: actions/upload-artifact@v4 - if: hashFiles('release/*_x64_*_msi.msi') != '' && github.event.inputs.artifact_destination != 'none' - with: - name: termix_windows_x64_msi - path: release/*_x64_*_msi.msi - retention-days: 30 - - - name: Upload Windows ia32 MSI Installer - uses: actions/upload-artifact@v4 - if: hashFiles('release/*_ia32_*_msi.msi') != '' && github.event.inputs.artifact_destination != 'none' - with: - name: termix_windows_ia32_msi - path: release/*_ia32_*_msi.msi - retention-days: 30 - - - name: Create Windows x64 Portable zip - if: hashFiles('release/win-unpacked/*') != '' - run: | - $VERSION = "${{ steps.package-version.outputs.version }}" - Compress-Archive -Path "release\win-unpacked\*" -DestinationPath "termix_windows_x64_${VERSION}_portable.zip" - - - name: Create Windows ia32 Portable zip - if: hashFiles('release/win-ia32-unpacked/*') != '' - run: | - $VERSION = "${{ steps.package-version.outputs.version }}" - Compress-Archive -Path "release\win-ia32-unpacked\*" -DestinationPath "termix_windows_ia32_${VERSION}_portable.zip" - - - name: Upload Windows x64 Portable - uses: actions/upload-artifact@v4 - if: hashFiles('termix_windows_x64_*_portable.zip') != '' && github.event.inputs.artifact_destination != 'none' - with: - name: termix_windows_x64_portable - path: termix_windows_x64_*_portable.zip - retention-days: 30 - - - name: Upload Windows ia32 Portable - uses: actions/upload-artifact@v4 - if: hashFiles('termix_windows_ia32_*_portable.zip') != '' && github.event.inputs.artifact_destination != 'none' - with: - name: termix_windows_ia32_portable - path: termix_windows_ia32_*_portable.zip - retention-days: 30 - - build-linux: - runs-on: blacksmith-4vcpu-ubuntu-2404 - if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'linux' || github.event.inputs.build_type == '' - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 1 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "npm" - - - name: Install dependencies - run: | - rm -f package-lock.json - # Retry npm install up to 3 times on failure - for i in 1 2 3; do - if npm install; then - break - else - if [ $i -eq 3 ]; then - echo "npm install failed after 3 attempts" - exit 1 - fi - echo "npm install attempt $i failed, retrying in 10 seconds..." - sleep 10 - fi - done - npm install --force @rollup/rollup-linux-x64-gnu - npm install --force @rollup/rollup-linux-arm64-gnu - npm install --force @rollup/rollup-linux-arm-gnueabihf - - - name: Build Linux (All Architectures) - run: npm run build && npx electron-builder --linux --x64 --arm64 --armv7l - - - name: Rename tar.gz files to match convention - run: | - VERSION=$(node -p "require('./package.json').version") - cd release - - # Rename x64 tar.gz if it exists - if [ -f "termix-${VERSION}-x64.tar.gz" ]; then - mv "termix-${VERSION}-x64.tar.gz" "termix_linux_x64_${VERSION}_portable.tar.gz" - echo "Renamed x64 tar.gz" - fi - - # Rename arm64 tar.gz if it exists - if [ -f "termix-${VERSION}-arm64.tar.gz" ]; then - mv "termix-${VERSION}-arm64.tar.gz" "termix_linux_arm64_${VERSION}_portable.tar.gz" - echo "Renamed arm64 tar.gz" - fi - - # Rename armv7l tar.gz if it exists - if [ -f "termix-${VERSION}-armv7l.tar.gz" ]; then - mv "termix-${VERSION}-armv7l.tar.gz" "termix_linux_armv7l_${VERSION}_portable.tar.gz" - echo "Renamed armv7l tar.gz" - fi - - cd .. - - - name: List release files - run: | - echo "Contents of release directory:" - ls -la release/ - - - name: Upload Linux x64 AppImage - uses: actions/upload-artifact@v4 - if: hashFiles('release/*_x64_*_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none' - with: - name: termix_linux_x64_appimage - path: release/*_x64_*_appimage.AppImage - retention-days: 30 - - - name: Upload Linux arm64 AppImage - uses: actions/upload-artifact@v4 - if: hashFiles('release/*_arm64_*_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none' - with: - name: termix_linux_arm64_appimage - path: release/*_arm64_*_appimage.AppImage - retention-days: 30 - - - name: Upload Linux x64 DEB - uses: actions/upload-artifact@v4 - if: hashFiles('release/*_x64_*_deb.deb') != '' && github.event.inputs.artifact_destination != 'none' - with: - name: termix_linux_x64_deb - path: release/*_x64_*_deb.deb - retention-days: 30 - - - name: Upload Linux arm64 DEB - uses: actions/upload-artifact@v4 - if: hashFiles('release/*_arm64_*_deb.deb') != '' && github.event.inputs.artifact_destination != 'none' - with: - name: termix_linux_arm64_deb - path: release/*_arm64_*_deb.deb - retention-days: 30 - - - name: Upload Linux armv7l DEB - uses: actions/upload-artifact@v4 - if: hashFiles('release/*_armv7l_*_deb.deb') != '' && github.event.inputs.artifact_destination != 'none' - with: - name: termix_linux_armv7l_deb - path: release/*_armv7l_*_deb.deb - retention-days: 30 - - - name: Upload Linux x64 tar.gz - uses: actions/upload-artifact@v4 - if: hashFiles('release/*_x64_*_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none' - with: - name: termix_linux_x64_portable - path: release/*_x64_*_portable.tar.gz - retention-days: 30 - - - name: Upload Linux arm64 tar.gz - uses: actions/upload-artifact@v4 - if: hashFiles('release/*_arm64_*_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none' - with: - name: termix_linux_arm64_portable - path: release/*_arm64_*_portable.tar.gz - retention-days: 30 - - - name: Upload Linux armv7l tar.gz - uses: actions/upload-artifact@v4 - if: hashFiles('release/*_armv7l_*_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none' - with: - name: termix_linux_armv7l_portable - path: release/*_armv7l_*_portable.tar.gz - retention-days: 30 - - build-macos: - runs-on: macos-latest - if: github.event.inputs.build_type == 'macos' || github.event.inputs.build_type == 'all' - needs: [] - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 1 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "npm" - - - name: Install dependencies - run: | - # Retry npm ci up to 3 times on failure - for i in 1 2 3; do - if npm ci; then - break - else - if [ $i -eq 3 ]; then - echo "npm ci failed after 3 attempts" - exit 1 - fi - echo "npm ci attempt $i failed, retrying in 10 seconds..." - sleep 10 - fi - done - npm install --force @rollup/rollup-darwin-arm64 - npm install dmg-license - - - name: Check for Code Signing Certificates - id: check_certs - run: | - if [ -n "${{ secrets.MAC_BUILD_CERTIFICATE_BASE64 }}" ] && [ -n "${{ secrets.MAC_P12_PASSWORD }}" ]; then - echo "has_certs=true" >> $GITHUB_OUTPUT + if [ "$PROD" = "true" ]; then + # Production build → push release + latest to both GHCR and Docker Hub + TAGS+=("release-$VERSION" "latest") + for tag in "${TAGS[@]}"; do + ALL_TAGS+=("ghcr.io/lukegus/termix:$tag") + ALL_TAGS+=("docker.io/bugattiguy527/termix:$tag") + done else - echo "has_certs=false" >> $GITHUB_OUTPUT - echo "⚠️ Code signing certificates not configured. MAS build will be unsigned." + # Dev build → push only dev-x.x.x to GHCR + TAGS+=("dev-$VERSION") + for tag in "${TAGS[@]}"; do + ALL_TAGS+=("ghcr.io/lukegus/termix:$tag") + done fi - - name: Import Code Signing Certificates - if: steps.check_certs.outputs.has_certs == 'true' - env: - MAC_BUILD_CERTIFICATE_BASE64: ${{ secrets.MAC_BUILD_CERTIFICATE_BASE64 }} - MAC_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.MAC_INSTALLER_CERTIFICATE_BASE64 }} - MAC_P12_PASSWORD: ${{ secrets.MAC_P12_PASSWORD }} - MAC_KEYCHAIN_PASSWORD: ${{ secrets.MAC_KEYCHAIN_PASSWORD }} - run: | - APP_CERT_PATH=$RUNNER_TEMP/app_certificate.p12 - INSTALLER_CERT_PATH=$RUNNER_TEMP/installer_certificate.p12 - KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + echo "ALL_TAGS=${ALL_TAGS[*]}" >> $GITHUB_ENV + echo "All tags to build:" + printf '%s\n' "${ALL_TAGS[@]}" - # Decode certificates - echo -n "$MAC_BUILD_CERTIFICATE_BASE64" | base64 --decode -o $APP_CERT_PATH - - if [ -n "$MAC_INSTALLER_CERTIFICATE_BASE64" ]; then - echo "Decoding installer certificate..." - echo -n "$MAC_INSTALLER_CERTIFICATE_BASE64" | base64 --decode -o $INSTALLER_CERT_PATH - else - echo "⚠️ MAC_INSTALLER_CERTIFICATE_BASE64 is empty" - fi - - # Create and configure keychain - security create-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - security set-keychain-settings -lut 21600 $KEYCHAIN_PATH - security unlock-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - - # Import application certificate - echo "Importing application certificate..." - security import $APP_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH - - # Import installer certificate if it exists - if [ -f "$INSTALLER_CERT_PATH" ]; then - echo "Importing installer certificate..." - security import $INSTALLER_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH - else - echo "⚠️ Installer certificate file not found, skipping import" - fi - - security list-keychain -d user -s $KEYCHAIN_PATH - - echo "Imported certificates:" - security find-identity -v -p codesigning $KEYCHAIN_PATH - - - name: Build macOS App Store Package - if: steps.check_certs.outputs.has_certs == 'true' - env: - ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true - run: | - # Get current version for display - CURRENT_VERSION=$(node -p "require('./package.json').version") - BUILD_VERSION="${{ github.run_number }}" - - echo "✅ Package version: $CURRENT_VERSION (unchanged)" - echo "✅ Build number for Apple: $BUILD_VERSION" - - # Build MAS with custom buildVersion - npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION" - - - name: Clean up MAS keychain before DMG build - if: steps.check_certs.outputs.has_certs == 'true' - run: | - security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true - echo "Cleaned up MAS keychain" - - - name: Check for Developer ID Certificates - id: check_dev_id_certs - run: | - if [ -n "${{ secrets.DEVELOPER_ID_CERTIFICATE_BASE64 }}" ] && [ -n "${{ secrets.DEVELOPER_ID_P12_PASSWORD }}" ]; then - echo "has_dev_id_certs=true" >> $GITHUB_OUTPUT - echo "✅ Developer ID certificates configured for DMG signing" - else - echo "has_dev_id_certs=false" >> $GITHUB_OUTPUT - echo "⚠️ Developer ID certificates not configured. DMG will be unsigned." - echo "Add DEVELOPER_ID_CERTIFICATE_BASE64 and DEVELOPER_ID_P12_PASSWORD secrets to enable DMG signing." - fi - - - name: Import Developer ID Certificates - if: steps.check_dev_id_certs.outputs.has_dev_id_certs == 'true' - env: - DEVELOPER_ID_CERTIFICATE_BASE64: ${{ secrets.DEVELOPER_ID_CERTIFICATE_BASE64 }} - DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64 }} - DEVELOPER_ID_P12_PASSWORD: ${{ secrets.DEVELOPER_ID_P12_PASSWORD }} - MAC_KEYCHAIN_PASSWORD: ${{ secrets.MAC_KEYCHAIN_PASSWORD }} - run: | - DEV_CERT_PATH=$RUNNER_TEMP/dev_certificate.p12 - DEV_INSTALLER_CERT_PATH=$RUNNER_TEMP/dev_installer_certificate.p12 - KEYCHAIN_PATH=$RUNNER_TEMP/dev-signing.keychain-db - - # Decode Developer ID certificate - echo -n "$DEVELOPER_ID_CERTIFICATE_BASE64" | base64 --decode -o $DEV_CERT_PATH - - if [ -n "$DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64" ]; then - echo "Decoding Developer ID installer certificate..." - echo -n "$DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64" | base64 --decode -o $DEV_INSTALLER_CERT_PATH - else - echo "⚠️ DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64 is empty (optional)" - fi - - # Create and configure keychain - security create-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - security set-keychain-settings -lut 21600 $KEYCHAIN_PATH - security unlock-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - - # Import Developer ID Application certificate - echo "Importing Developer ID Application certificate..." - security import $DEV_CERT_PATH -P "$DEVELOPER_ID_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH - - # Import Developer ID Installer certificate if it exists - if [ -f "$DEV_INSTALLER_CERT_PATH" ]; then - echo "Importing Developer ID Installer certificate..." - security import $DEV_INSTALLER_CERT_PATH -P "$DEVELOPER_ID_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH - fi - - security list-keychain -d user -s $KEYCHAIN_PATH - - echo "Imported Developer ID certificates:" - security find-identity -v -p codesigning $KEYCHAIN_PATH - - - name: Build macOS DMG - env: - ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - run: | - # Build DMG without running npm run build again (already built above or skip if no certs) - if [ "${{ steps.check_certs.outputs.has_certs }}" == "true" ]; then - # Frontend already built, just package DMG - npx electron-builder --mac dmg --universal --x64 --arm64 - else - # No certs, need to build frontend first - npm run build && npx electron-builder --mac dmg --universal --x64 --arm64 - fi - - - name: List release directory - if: steps.check_certs.outputs.has_certs == 'true' - run: | - echo "Contents of release directory:" - ls -R release/ || echo "Release directory not found" - - - name: Upload macOS MAS PKG - if: steps.check_certs.outputs.has_certs == 'true' && hashFiles('release/*_*_*_mas.pkg') != '' && (github.event.inputs.artifact_destination == 'file' || github.event.inputs.artifact_destination == 'release' || github.event.inputs.artifact_destination == 'submit') - uses: actions/upload-artifact@v4 + - name: Login to GHCR + uses: docker/login-action@v3 with: - name: termix_macos_mas - path: release/*_*_*_mas.pkg - retention-days: 30 - if-no-files-found: warn + registry: ghcr.io + username: lukegus + password: ${{ secrets.GHCR_TOKEN }} - - name: Upload macOS Universal DMG - uses: actions/upload-artifact@v4 - if: hashFiles('release/*_universal_*_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none' + - name: Login to Docker Hub (prod only) + if: ${{ github.event.inputs.production == 'true' }} + uses: docker/login-action@v3 with: - name: termix_macos_universal_dmg - path: release/*_universal_*_dmg.dmg - retention-days: 30 + username: bugattiguy527 + password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Upload macOS x64 DMG - uses: actions/upload-artifact@v4 - if: hashFiles('release/*_x64_*_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none' + - name: Build and push multi-arch image + uses: docker/build-push-action@v5 with: - name: termix_macos_x64_dmg - path: release/*_x64_*_dmg.dmg - retention-days: 30 + context: . + file: ./docker/Dockerfile + push: true + platforms: linux/amd64,linux/arm64,linux/arm/v7 + tags: ${{ env.ALL_TAGS }} + build-args: | + BUILDKIT_INLINE_CACHE=1 + BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 + labels: | + org.opencontainers.image.source=https://github.com/${{ github.repository }} + org.opencontainers.image.revision=${{ github.sha }} + outputs: type=registry,compression=zstd,compression-level=19 - - name: Upload macOS arm64 DMG - uses: actions/upload-artifact@v4 - if: hashFiles('release/*_arm64_*_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none' - with: - name: termix_macos_arm64_dmg - path: release/*_arm64_*_dmg.dmg - retention-days: 30 - - - name: Check for App Store Connect API credentials - if: steps.check_certs.outputs.has_certs == 'true' - id: check_asc_creds - run: | - if [ -n "${{ secrets.APPLE_KEY_ID }}" ] && [ -n "${{ secrets.APPLE_ISSUER_ID }}" ] && [ -n "${{ secrets.APPLE_KEY_CONTENT }}" ]; then - echo "has_credentials=true" >> $GITHUB_OUTPUT - if [ "${{ github.event.inputs.artifact_destination }}" == "submit" ]; then - echo "✅ App Store Connect API credentials found. Will deploy to TestFlight." - else - echo "ℹ️ App Store Connect API credentials found, but store submission is disabled." - fi - else - echo "has_credentials=false" >> $GITHUB_OUTPUT - echo "⚠️ App Store Connect API credentials not configured. Skipping deployment." - echo "Add APPLE_KEY_ID, APPLE_ISSUER_ID, and APPLE_KEY_CONTENT secrets to enable automatic deployment." - fi - - - name: Setup Ruby for Fastlane - if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit' - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.2' - bundler-cache: false - - - name: Install Fastlane - if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit' - run: | - gem install fastlane -N - fastlane --version - - - name: Deploy to App Store Connect (TestFlight) - if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit' - run: | - PKG_FILE=$(find release -name "*.pkg" -type f | head -n 1) - if [ -z "$PKG_FILE" ]; then - echo "Error: No .pkg file found in release directory" - exit 1 - fi - echo "Found package: $PKG_FILE" - - # Create API key file - mkdir -p ~/private_keys - echo "${{ secrets.APPLE_KEY_CONTENT }}" | base64 --decode > ~/private_keys/AuthKey_${{ secrets.APPLE_KEY_ID }}.p8 - - # Upload to App Store Connect using xcrun altool - xcrun altool --upload-app -f "$PKG_FILE" \ - --type macos \ - --apiKey "${{ secrets.APPLE_KEY_ID }}" \ - --apiIssuer "${{ secrets.APPLE_ISSUER_ID }}" - - echo "✅ Upload complete! Build will appear in App Store Connect after processing (10-30 minutes)" - continue-on-error: true - - - name: Clean up keychains + - name: Cleanup Docker if: always() run: | - security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true - security delete-keychain $RUNNER_TEMP/dev-signing.keychain-db || true - - submit-to-chocolatey: - runs-on: windows-latest - if: github.event.inputs.artifact_destination == 'submit' - needs: [build-windows] - permissions: - contents: read - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 1 - - - name: Get version from package.json - id: package-version - run: | - $VERSION = (Get-Content package.json | ConvertFrom-Json).version - echo "version=$VERSION" >> $env:GITHUB_OUTPUT - echo "Building Chocolatey package for version: $VERSION" - - - name: Download Windows x64 MSI artifact - uses: actions/download-artifact@v4 - with: - name: termix_windows_x64_msi - path: artifact - - - name: Get MSI file info - id: msi-info - run: | - $VERSION = "${{ steps.package-version.outputs.version }}" - $MSI_FILE = Get-ChildItem -Path artifact -Filter "*.msi" | Select-Object -First 1 - $MSI_NAME = $MSI_FILE.Name - $CHECKSUM = (Get-FileHash -Path $MSI_FILE.FullName -Algorithm SHA256).Hash - - echo "msi_name=$MSI_NAME" >> $env:GITHUB_OUTPUT - echo "checksum=$CHECKSUM" >> $env:GITHUB_OUTPUT - echo "MSI File: $MSI_NAME" - echo "SHA256: $CHECKSUM" - - - name: Prepare Chocolatey package - run: | - $VERSION = "${{ steps.package-version.outputs.version }}" - $CHECKSUM = "${{ steps.msi-info.outputs.checksum }}" - $MSI_NAME = "${{ steps.msi-info.outputs.msi_name }}" - - # Construct the download URL with the actual release tag format - $DOWNLOAD_URL = "https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$MSI_NAME" - - # Copy chocolatey files to build directory - New-Item -ItemType Directory -Force -Path "choco-build" - Copy-Item -Path "chocolatey\*" -Destination "choco-build" -Recurse -Force - - # Update chocolateyinstall.ps1 with actual values - $installScript = Get-Content "choco-build\tools\chocolateyinstall.ps1" -Raw -Encoding UTF8 - $installScript = $installScript -replace 'DOWNLOAD_URL_PLACEHOLDER', $DOWNLOAD_URL - $installScript = $installScript -replace 'CHECKSUM_PLACEHOLDER', $CHECKSUM - [System.IO.File]::WriteAllText("$PWD\choco-build\tools\chocolateyinstall.ps1", $installScript, [System.Text.UTF8Encoding]::new($false)) - - # Update nuspec with version (preserve UTF-8 encoding without BOM) - $nuspec = Get-Content "choco-build\termix-ssh.nuspec" -Raw -Encoding UTF8 - $nuspec = $nuspec -replace 'VERSION_PLACEHOLDER', $VERSION - [System.IO.File]::WriteAllText("$PWD\choco-build\termix-ssh.nuspec", $nuspec, [System.Text.UTF8Encoding]::new($false)) - - echo "Chocolatey package prepared for version $VERSION" - echo "Download URL: $DOWNLOAD_URL" - - # Verify the nuspec is valid - echo "" - echo "Verifying nuspec content:" - Get-Content "choco-build\termix-ssh.nuspec" -Head 10 - echo "" - - - name: Install Chocolatey - run: | - Set-ExecutionPolicy Bypass -Scope Process -Force - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 - iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) - - - name: Pack Chocolatey package - run: | - cd choco-build - echo "Packing Chocolatey package..." - choco pack termix-ssh.nuspec - - if ($LASTEXITCODE -ne 0) { - echo "❌ Failed to pack Chocolatey package" - exit 1 - } - - echo "" - echo "✅ Package created successfully" - echo "Package contents:" - Get-ChildItem *.nupkg | ForEach-Object { echo $_.Name } - - - name: Check for Chocolatey API Key - id: check_choco_key - run: | - if ("${{ secrets.CHOCOLATEY_API_KEY }}" -ne "") { - echo "has_key=true" >> $env:GITHUB_OUTPUT - echo "✅ Chocolatey API key found. Will push to Chocolatey." - } else { - echo "has_key=false" >> $env:GITHUB_OUTPUT - echo "⚠️ Chocolatey API key not configured. Package will be created but not pushed." - echo "Add CHOCOLATEY_API_KEY secret to enable automatic submission." - } - - - name: Push to Chocolatey - if: steps.check_choco_key.outputs.has_key == 'true' - run: | - $VERSION = "${{ steps.package-version.outputs.version }}" - cd choco-build - choco apikey --key "${{ secrets.CHOCOLATEY_API_KEY }}" --source https://push.chocolatey.org/ - - try { - choco push "termix-ssh.$VERSION.nupkg" --source https://push.chocolatey.org/ - if ($LASTEXITCODE -eq 0) { - echo "" - echo "✅ Package pushed to Chocolatey successfully!" - echo "View at: https://community.chocolatey.org/packages/termix-ssh/$VERSION" - } else { - throw "Chocolatey push failed with exit code $LASTEXITCODE" - } - } catch { - echo "" - echo "❌ Failed to push to Chocolatey" - echo "" - echo "Common reasons:" - echo "1. Package ID 'termix-ssh' is already owned by another user" - echo "2. You need to register/claim the package ID first" - echo "3. API key doesn't have push permissions" - echo "" - echo "Solutions:" - echo "1. Check if package exists: https://community.chocolatey.org/packages/termix-ssh" - echo "2. If it exists and is yours, contact Chocolatey support to claim it" - echo "3. Register a new package ID at: https://community.chocolatey.org/" - echo "" - echo "The package artifact has been saved for manual submission." - echo "" - exit 1 - } - - - name: Upload Chocolatey package as artifact - uses: actions/upload-artifact@v4 - with: - name: chocolatey-package - path: choco-build/*.nupkg - retention-days: 30 - - submit-to-flatpak: - runs-on: ubuntu-latest - if: github.event.inputs.artifact_destination == 'submit' - needs: [build-linux] - permissions: - contents: read - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 1 - - - name: Get version from package.json - id: package-version - run: | - VERSION=$(node -p "require('./package.json').version") - RELEASE_DATE=$(date +%Y-%m-%d) - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "release_date=$RELEASE_DATE" >> $GITHUB_OUTPUT - echo "Building Flatpak submission for version: $VERSION" - - - name: Download Linux x64 AppImage artifact - uses: actions/download-artifact@v4 - with: - name: termix_linux_x64_appimage - path: artifact-x64 - - - name: Download Linux arm64 AppImage artifact - uses: actions/download-artifact@v4 - with: - name: termix_linux_arm64_appimage - path: artifact-arm64 - - - name: Get AppImage file info - id: appimage-info - run: | - VERSION="${{ steps.package-version.outputs.version }}" - - # x64 AppImage - APPIMAGE_X64_FILE=$(find artifact-x64 -name "*.AppImage" -type f | head -n 1) - APPIMAGE_X64_NAME=$(basename "$APPIMAGE_X64_FILE") - CHECKSUM_X64=$(sha256sum "$APPIMAGE_X64_FILE" | awk '{print $1}') - - # arm64 AppImage - APPIMAGE_ARM64_FILE=$(find artifact-arm64 -name "*.AppImage" -type f | head -n 1) - APPIMAGE_ARM64_NAME=$(basename "$APPIMAGE_ARM64_FILE") - CHECKSUM_ARM64=$(sha256sum "$APPIMAGE_ARM64_FILE" | awk '{print $1}') - - echo "appimage_x64_name=$APPIMAGE_X64_NAME" >> $GITHUB_OUTPUT - echo "checksum_x64=$CHECKSUM_X64" >> $GITHUB_OUTPUT - echo "appimage_arm64_name=$APPIMAGE_ARM64_NAME" >> $GITHUB_OUTPUT - echo "checksum_arm64=$CHECKSUM_ARM64" >> $GITHUB_OUTPUT - - echo "x64 AppImage: $APPIMAGE_X64_NAME" - echo "x64 SHA256: $CHECKSUM_X64" - echo "arm64 AppImage: $APPIMAGE_ARM64_NAME" - echo "arm64 SHA256: $CHECKSUM_ARM64" - - - name: Install ImageMagick for icon generation - run: | - sudo apt-get update - sudo apt-get install -y imagemagick - - - name: Prepare Flatpak submission files - run: | - VERSION="${{ steps.package-version.outputs.version }}" - CHECKSUM_X64="${{ steps.appimage-info.outputs.checksum_x64 }}" - CHECKSUM_ARM64="${{ steps.appimage-info.outputs.checksum_arm64 }}" - RELEASE_DATE="${{ steps.package-version.outputs.release_date }}" - APPIMAGE_X64_NAME="${{ steps.appimage-info.outputs.appimage_x64_name }}" - APPIMAGE_ARM64_NAME="${{ steps.appimage-info.outputs.appimage_arm64_name }}" - - # Create submission directory - mkdir -p flatpak-submission - - # Copy Flatpak files to submission directory - cp flatpak/com.karmaa.termix.yml flatpak-submission/ - cp flatpak/com.karmaa.termix.desktop flatpak-submission/ - cp flatpak/com.karmaa.termix.metainfo.xml flatpak-submission/ - cp flatpak/flathub.json flatpak-submission/ - - # Copy and prepare icons - cp public/icon.svg flatpak-submission/com.karmaa.termix.svg - convert public/icon.png -resize 256x256 flatpak-submission/icon-256.png - convert public/icon.png -resize 128x128 flatpak-submission/icon-128.png - - # Update manifest with version and checksums - sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak-submission/com.karmaa.termix.yml - sed -i "s/CHECKSUM_X64_PLACEHOLDER/$CHECKSUM_X64/g" flatpak-submission/com.karmaa.termix.yml - sed -i "s/CHECKSUM_ARM64_PLACEHOLDER/$CHECKSUM_ARM64/g" flatpak-submission/com.karmaa.termix.yml - - # Update metainfo with version and date - sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak-submission/com.karmaa.termix.metainfo.xml - sed -i "s/DATE_PLACEHOLDER/$RELEASE_DATE/g" flatpak-submission/com.karmaa.termix.metainfo.xml - - echo "✅ Flatpak submission files prepared for version $VERSION" - echo "x64 Download URL: https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_X64_NAME" - echo "arm64 Download URL: https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_ARM64_NAME" - - - name: Create submission instructions - run: | - cat > flatpak-submission/SUBMISSION_INSTRUCTIONS.md << 'EOF' - # Flathub Submission Instructions for Termix - - ## Automatic Submission (Recommended) - - All files needed for Flathub submission are in this artifact. Follow these steps: - - 1. **Fork the Flathub repository**: - - Go to https://github.com/flathub/flathub - - Click "Fork" button - - 2. **Clone your fork**: - ```bash - git clone https://github.com/YOUR-USERNAME/flathub.git - cd flathub - git checkout -b com.karmaa.termix - ``` - - 3. **Copy all files from this artifact** to the root of your flathub fork - - 4. **Commit and push**: - ```bash - git add . - git commit -m "Add Termix ${{ steps.package-version.outputs.version }}" - git push origin com.karmaa.termix - ``` - - 5. **Create Pull Request**: - - Go to https://github.com/YOUR-USERNAME/flathub - - Click "Compare & pull request" - - Submit PR to flathub/flathub - - ## Files in this submission: - - - `com.karmaa.termix.yml` - Flatpak manifest - - `com.karmaa.termix.desktop` - Desktop entry - - `com.karmaa.termix.metainfo.xml` - AppStream metadata - - `flathub.json` - Flathub configuration - - `com.karmaa.termix.svg` - SVG icon - - `icon-256.png` - 256x256 icon - - `icon-128.png` - 128x128 icon - - ## Version Information: - - - Version: ${{ steps.package-version.outputs.version }} - - Release Date: ${{ steps.package-version.outputs.release_date }} - - x64 AppImage SHA256: ${{ steps.appimage-info.outputs.checksum_x64 }} - - arm64 AppImage SHA256: ${{ steps.appimage-info.outputs.checksum_arm64 }} - - ## After Submission: - - 1. Flathub maintainers will review your submission (usually 1-5 days) - 2. They may request changes - be responsive to feedback - 3. Once approved, Termix will be available via: `flatpak install flathub com.karmaa.termix` - - ## Resources: - - - [Flathub Submission Guidelines](https://docs.flathub.org/docs/for-app-authors/submission) - - [Flatpak Documentation](https://docs.flatpak.org/) - EOF - - echo "✅ Created submission instructions" - - - name: List submission files - run: | - echo "Flatpak submission files:" - ls -la flatpak-submission/ - - - name: Upload Flatpak submission as artifact - uses: actions/upload-artifact@v4 - with: - name: flatpak-submission - path: flatpak-submission/* - retention-days: 30 - - - name: Display next steps - run: | - echo "" - echo "🎉 Flatpak submission files ready!" - echo "" - echo "📦 Download the 'flatpak-submission' artifact and follow SUBMISSION_INSTRUCTIONS.md" - echo "" - echo "Quick summary:" - echo "1. Fork https://github.com/flathub/flathub" - echo "2. Copy artifact files to your fork" - echo "3. Create PR to flathub/flathub" - echo "" - - submit-to-homebrew: - runs-on: macos-latest - if: github.event.inputs.artifact_destination == 'submit' - needs: [build-macos] - permissions: - contents: read - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 1 - - - name: Get version from package.json - id: package-version - run: | - VERSION=$(node -p "require('./package.json').version") - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Building Homebrew Cask for version: $VERSION" - - - name: Download macOS Universal DMG artifact - uses: actions/download-artifact@v4 - with: - name: termix_macos_universal_dmg - path: artifact - - - name: Get DMG file info - id: dmg-info - run: | - VERSION="${{ steps.package-version.outputs.version }}" - DMG_FILE=$(find artifact -name "*.dmg" -type f | head -n 1) - DMG_NAME=$(basename "$DMG_FILE") - CHECKSUM=$(shasum -a 256 "$DMG_FILE" | awk '{print $1}') - - echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT - echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT - echo "DMG File: $DMG_NAME" - echo "SHA256: $CHECKSUM" - - - name: Prepare Homebrew submission files - run: | - VERSION="${{ steps.package-version.outputs.version }}" - CHECKSUM="${{ steps.dmg-info.outputs.checksum }}" - DMG_NAME="${{ steps.dmg-info.outputs.dmg_name }}" - - # Create submission directory - mkdir -p homebrew-submission/Casks/t - - # Copy Homebrew cask file - cp homebrew/termix.rb homebrew-submission/Casks/t/termix.rb - cp homebrew/README.md homebrew-submission/ - - # Update cask with version and checksum - sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" homebrew-submission/Casks/t/termix.rb - sed -i '' "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" homebrew-submission/Casks/t/termix.rb - - echo "✅ Homebrew Cask prepared for version $VERSION" - echo "Download URL: https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$DMG_NAME" - - - name: Verify Cask syntax - run: | - # Install Homebrew if not present (should be on macos-latest) - if ! command -v brew &> /dev/null; then - echo "Installing Homebrew..." - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - fi - - # Basic syntax check - ruby -c homebrew-submission/Casks/t/termix.rb - echo "✅ Cask syntax is valid" - - - name: Create submission instructions - run: | - cat > homebrew-submission/SUBMISSION_INSTRUCTIONS.md << 'EOF' - # Homebrew Cask Submission Instructions for Termix - - ## Option 1: Submit to Official Homebrew Cask (Recommended) - - ### Prerequisites - - macOS with Homebrew installed - - GitHub account - - ### Steps - - 1. **Fork the Homebrew Cask repository**: - - Go to https://github.com/Homebrew/homebrew-cask - - Click "Fork" button - - 2. **Clone your fork**: - ```bash - git clone https://github.com/YOUR-USERNAME/homebrew-cask.git - cd homebrew-cask - git checkout -b termix - ``` - - 3. **Copy the cask file**: - - Copy `Casks/t/termix.rb` from this artifact to your fork at `Casks/t/termix.rb` - - Note: Casks are organized by first letter in subdirectories - - 4. **Test the cask locally**: - ```bash - brew install --cask ./Casks/t/termix.rb - brew uninstall --cask termix - ``` - - 5. **Run audit checks**: - ```bash - brew audit --cask --online ./Casks/t/termix.rb - brew style ./Casks/t/termix.rb - ``` - - 6. **Commit and push**: - ```bash - git add Casks/t/termix.rb - git commit -m "Add Termix ${{ steps.package-version.outputs.version }}" - git push origin termix - ``` - - 7. **Create Pull Request**: - - Go to https://github.com/YOUR-USERNAME/homebrew-cask - - Click "Compare & pull request" - - Fill in the PR template - - Submit to Homebrew/homebrew-cask - - ### PR Requirements - - Your PR should include: - - Clear commit message: "Add Termix X.Y.Z" or "Update Termix to X.Y.Z" - - All audit checks passing - - Working download URL - - Valid SHA256 checksum - - ## Option 2: Create Your Own Tap (Alternative) - - If you want more control and faster updates: - - 1. **Create a tap repository**: - - Create repo: `Termix-SSH/homebrew-termix` - - Add `Casks/termix.rb` to the repo - - 2. **Users install with**: - ```bash - brew tap termix-ssh/termix - brew install --cask termix - ``` - - ### Advantages of Custom Tap - - No approval process - - Instant updates - - Full control - - Can include beta versions - - ### Disadvantages - - Less discoverable - - Users must add tap first - - You maintain it yourself - - ## Files in this submission: - - - `Casks/t/termix.rb` - Homebrew Cask formula - - `README.md` - Detailed documentation - - `SUBMISSION_INSTRUCTIONS.md` - This file - - ## Version Information: - - - Version: ${{ steps.package-version.outputs.version }} - - DMG SHA256: ${{ steps.dmg-info.outputs.checksum }} - - DMG URL: https://github.com/Termix-SSH/Termix/releases/download/release-${{ steps.package-version.outputs.version }}-tag/${{ steps.dmg-info.outputs.dmg_name }} - - ## After Submission: - - ### Official Homebrew Cask: - 1. Maintainers will review (usually 24-48 hours) - 2. May request changes or fixes - 3. Once merged, users can install with: `brew install --cask termix` - 4. Homebrew bot will auto-update for future releases - - ### Custom Tap: - 1. Push to your tap repository - 2. Immediately available to users - 3. Update the cask file for each new release - - ## Resources: - - - [Homebrew Cask Documentation](https://docs.brew.sh/Cask-Cookbook) - - [Acceptable Casks](https://docs.brew.sh/Acceptable-Casks) - - [How to Open a PR](https://docs.brew.sh/How-To-Open-a-Homebrew-Pull-Request) - EOF - - echo "✅ Created submission instructions" - - - name: List submission files - run: | - echo "Homebrew submission files:" - find homebrew-submission -type f - - - name: Upload Homebrew submission as artifact - uses: actions/upload-artifact@v4 - with: - name: homebrew-submission - path: homebrew-submission/* - retention-days: 30 - - - name: Display next steps - run: | - echo "" - echo "🍺 Homebrew Cask ready!" - echo "" - echo "📦 Download the 'homebrew-submission' artifact and follow SUBMISSION_INSTRUCTIONS.md" - echo "" - echo "Quick summary:" - echo "Option 1 (Recommended): Fork https://github.com/Homebrew/homebrew-cask and submit PR" - echo "Option 2 (Alternative): Create your own tap at Termix-SSH/homebrew-termix" - echo "" - - upload-to-release: - runs-on: blacksmith-4vcpu-ubuntu-2404 - if: github.event.inputs.artifact_destination == 'release' - needs: [build-windows, build-linux, build-macos] - permissions: - contents: write - - steps: - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - - name: Get latest release - id: get_release - run: | - echo "Fetching latest release from ${{ github.repository }}..." - LATEST_RELEASE=$(gh release list --repo ${{ github.repository }} --limit 1 --json tagName,name,isLatest -q '.[0]') - - if [ -z "$LATEST_RELEASE" ]; then - echo "ERROR: No releases found in ${{ github.repository }}" - exit 1 - fi - - RELEASE_TAG=$(echo "$LATEST_RELEASE" | jq -r '.tagName') - RELEASE_NAME=$(echo "$LATEST_RELEASE" | jq -r '.name') - - echo "tag=$RELEASE_TAG" >> $GITHUB_OUTPUT - echo "name=$RELEASE_NAME" >> $GITHUB_OUTPUT - echo "Latest release: $RELEASE_NAME ($RELEASE_TAG)" - env: - GH_TOKEN: ${{ github.token }} - - - name: Display artifact structure - run: | - echo "Artifact structure:" - ls -R artifacts/ - - - name: Upload artifacts to latest release - run: | - RELEASE_TAG="${{ steps.get_release.outputs.tag }}" - echo "Uploading artifacts to release: $RELEASE_TAG" - echo "" - - cd artifacts - for dir in */; do - echo "Processing directory: $dir" - cd "$dir" - for file in *; do - if [ -f "$file" ]; then - echo "Uploading: $file" - gh release upload "$RELEASE_TAG" "$file" --repo ${{ github.repository }} --clobber - echo "✓ $file uploaded successfully" - fi - done - cd .. - done - - echo "" - echo "All artifacts uploaded to: https://github.com/${{ github.repository }}/releases/tag/$RELEASE_TAG" - env: - GH_TOKEN: ${{ github.token }} + docker image prune -af + docker system prune -af --volumes From 41add20e0a33c24e641bebe725cde5ac888b8d20 Mon Sep 17 00:00:00 2001 From: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Date: Wed, 29 Oct 2025 19:20:15 -0500 Subject: [PATCH 10/12] Rename docker-image.yml to docker.yml --- .github/workflows/{docker-image.yml => docker.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{docker-image.yml => docker.yml} (100%) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker.yml similarity index 100% rename from .github/workflows/docker-image.yml rename to .github/workflows/docker.yml From dc29646a3996363e5e70d34ebf1ddc91801ae43a Mon Sep 17 00:00:00 2001 From: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Date: Wed, 29 Oct 2025 19:20:38 -0500 Subject: [PATCH 11/12] Rename electron-build.yml to electron.yml --- .github/workflows/electron-build.yml | 93 -- .github/workflows/electron.yml | 1165 ++++++++++++++++++++++++++ 2 files changed, 1165 insertions(+), 93 deletions(-) delete mode 100644 .github/workflows/electron-build.yml create mode 100644 .github/workflows/electron.yml diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml deleted file mode 100644 index 63de08f7..00000000 --- a/.github/workflows/electron-build.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: Build Electron App - -on: - workflow_dispatch: - inputs: - build_type: - description: "Build type to run" - required: true - default: "all" - type: choice - options: - - all - - windows - - linux - -jobs: - build-windows: - runs-on: windows-latest - if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'windows' || github.event.inputs.build_type == '' - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 1 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "npm" - - - name: Install dependencies - run: npm ci - - - name: Build Windows Portable - run: npm run build:win-portable - - - name: Build Windows Installer - run: npm run build:win-installer - - - name: Create Windows Portable zip - run: | - Compress-Archive -Path "release/win-unpacked/*" -DestinationPath "Termix-Windows-Portable.zip" - - - name: Upload Windows Portable Artifact - uses: actions/upload-artifact@v4 - with: - name: Termix-Windows-Portable - path: Termix-Windows-Portable.zip - retention-days: 30 - - - name: Upload Windows Installer Artifact - uses: actions/upload-artifact@v4 - with: - name: Termix-Windows-Installer - path: release/*.exe - retention-days: 30 - - build-linux: - runs-on: blacksmith-4vcpu-ubuntu-2404 - if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'linux' || github.event.inputs.build_type == '' - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 1 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "npm" - - - name: Install dependencies - run: npm ci - - - name: Build Linux Portable - run: npm run build:linux-portable - - - name: Create Linux Portable zip - run: | - cd release/linux-unpacked - zip -r ../../Termix-Linux-Portable.zip * - cd ../.. - - - name: Upload Linux Portable Artifact - uses: actions/upload-artifact@v4 - with: - name: Termix-Linux-Portable - path: Termix-Linux-Portable.zip - retention-days: 30 diff --git a/.github/workflows/electron.yml b/.github/workflows/electron.yml new file mode 100644 index 00000000..b50a8905 --- /dev/null +++ b/.github/workflows/electron.yml @@ -0,0 +1,1165 @@ +name: Build and Push Electron App + +on: + workflow_dispatch: + inputs: + build_type: + description: "Platform to build for" + required: true + default: "all" + type: choice + options: + - all + - windows + - linux + - macos + artifact_destination: + description: "What to do with the built app" + required: true + default: "file" + type: choice + options: + - none + - file + - release + - submit + +jobs: + build-windows: + runs-on: windows-latest + if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'windows' || github.event.inputs.build_type == '' + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies + run: | + # Retry npm ci up to 3 times on failure + $maxAttempts = 3 + $attempt = 1 + while ($attempt -le $maxAttempts) { + try { + npm ci + break + } catch { + if ($attempt -eq $maxAttempts) { + Write-Error "npm ci failed after $maxAttempts attempts" + exit 1 + } + Write-Host "npm ci attempt $attempt failed, retrying in 10 seconds..." + Start-Sleep -Seconds 10 + $attempt++ + } + } + + - name: Get version + id: package-version + run: | + $VERSION = (Get-Content package.json | ConvertFrom-Json).version + echo "version=$VERSION" >> $env:GITHUB_OUTPUT + echo "Building version: $VERSION" + + - name: Build Windows (All Architectures) + run: npm run build && npx electron-builder --win --x64 --ia32 + + - name: List release files + run: | + echo "Contents of release directory:" + dir release + + - name: Upload Windows x64 NSIS Installer + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_x64_*_nsis.exe') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_windows_x64_nsis + path: release/*_x64_*_nsis.exe + retention-days: 30 + + - name: Upload Windows ia32 NSIS Installer + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_ia32_*_nsis.exe') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_windows_ia32_nsis + path: release/*_ia32_*_nsis.exe + retention-days: 30 + + - name: Upload Windows x64 MSI Installer + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_x64_*_msi.msi') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_windows_x64_msi + path: release/*_x64_*_msi.msi + retention-days: 30 + + - name: Upload Windows ia32 MSI Installer + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_ia32_*_msi.msi') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_windows_ia32_msi + path: release/*_ia32_*_msi.msi + retention-days: 30 + + - name: Create Windows x64 Portable zip + if: hashFiles('release/win-unpacked/*') != '' + run: | + $VERSION = "${{ steps.package-version.outputs.version }}" + Compress-Archive -Path "release\win-unpacked\*" -DestinationPath "termix_windows_x64_${VERSION}_portable.zip" + + - name: Create Windows ia32 Portable zip + if: hashFiles('release/win-ia32-unpacked/*') != '' + run: | + $VERSION = "${{ steps.package-version.outputs.version }}" + Compress-Archive -Path "release\win-ia32-unpacked\*" -DestinationPath "termix_windows_ia32_${VERSION}_portable.zip" + + - name: Upload Windows x64 Portable + uses: actions/upload-artifact@v4 + if: hashFiles('termix_windows_x64_*_portable.zip') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_windows_x64_portable + path: termix_windows_x64_*_portable.zip + retention-days: 30 + + - name: Upload Windows ia32 Portable + uses: actions/upload-artifact@v4 + if: hashFiles('termix_windows_ia32_*_portable.zip') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_windows_ia32_portable + path: termix_windows_ia32_*_portable.zip + retention-days: 30 + + build-linux: + runs-on: blacksmith-4vcpu-ubuntu-2404 + if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'linux' || github.event.inputs.build_type == '' + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies + run: | + rm -f package-lock.json + # Retry npm install up to 3 times on failure + for i in 1 2 3; do + if npm install; then + break + else + if [ $i -eq 3 ]; then + echo "npm install failed after 3 attempts" + exit 1 + fi + echo "npm install attempt $i failed, retrying in 10 seconds..." + sleep 10 + fi + done + npm install --force @rollup/rollup-linux-x64-gnu + npm install --force @rollup/rollup-linux-arm64-gnu + npm install --force @rollup/rollup-linux-arm-gnueabihf + + - name: Build Linux (All Architectures) + run: npm run build && npx electron-builder --linux --x64 --arm64 --armv7l + + - name: Rename tar.gz files to match convention + run: | + VERSION=$(node -p "require('./package.json').version") + cd release + + # Rename x64 tar.gz if it exists + if [ -f "termix-${VERSION}-x64.tar.gz" ]; then + mv "termix-${VERSION}-x64.tar.gz" "termix_linux_x64_${VERSION}_portable.tar.gz" + echo "Renamed x64 tar.gz" + fi + + # Rename arm64 tar.gz if it exists + if [ -f "termix-${VERSION}-arm64.tar.gz" ]; then + mv "termix-${VERSION}-arm64.tar.gz" "termix_linux_arm64_${VERSION}_portable.tar.gz" + echo "Renamed arm64 tar.gz" + fi + + # Rename armv7l tar.gz if it exists + if [ -f "termix-${VERSION}-armv7l.tar.gz" ]; then + mv "termix-${VERSION}-armv7l.tar.gz" "termix_linux_armv7l_${VERSION}_portable.tar.gz" + echo "Renamed armv7l tar.gz" + fi + + cd .. + + - name: List release files + run: | + echo "Contents of release directory:" + ls -la release/ + + - name: Upload Linux x64 AppImage + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_x64_*_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_linux_x64_appimage + path: release/*_x64_*_appimage.AppImage + retention-days: 30 + + - name: Upload Linux arm64 AppImage + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_arm64_*_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_linux_arm64_appimage + path: release/*_arm64_*_appimage.AppImage + retention-days: 30 + + - name: Upload Linux x64 DEB + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_x64_*_deb.deb') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_linux_x64_deb + path: release/*_x64_*_deb.deb + retention-days: 30 + + - name: Upload Linux arm64 DEB + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_arm64_*_deb.deb') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_linux_arm64_deb + path: release/*_arm64_*_deb.deb + retention-days: 30 + + - name: Upload Linux armv7l DEB + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_armv7l_*_deb.deb') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_linux_armv7l_deb + path: release/*_armv7l_*_deb.deb + retention-days: 30 + + - name: Upload Linux x64 tar.gz + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_x64_*_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_linux_x64_portable + path: release/*_x64_*_portable.tar.gz + retention-days: 30 + + - name: Upload Linux arm64 tar.gz + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_arm64_*_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_linux_arm64_portable + path: release/*_arm64_*_portable.tar.gz + retention-days: 30 + + - name: Upload Linux armv7l tar.gz + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_armv7l_*_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_linux_armv7l_portable + path: release/*_armv7l_*_portable.tar.gz + retention-days: 30 + + build-macos: + runs-on: macos-latest + if: github.event.inputs.build_type == 'macos' || github.event.inputs.build_type == 'all' + needs: [] + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies + run: | + # Retry npm ci up to 3 times on failure + for i in 1 2 3; do + if npm ci; then + break + else + if [ $i -eq 3 ]; then + echo "npm ci failed after 3 attempts" + exit 1 + fi + echo "npm ci attempt $i failed, retrying in 10 seconds..." + sleep 10 + fi + done + npm install --force @rollup/rollup-darwin-arm64 + npm install dmg-license + + - name: Check for Code Signing Certificates + id: check_certs + run: | + if [ -n "${{ secrets.MAC_BUILD_CERTIFICATE_BASE64 }}" ] && [ -n "${{ secrets.MAC_P12_PASSWORD }}" ]; then + echo "has_certs=true" >> $GITHUB_OUTPUT + else + echo "has_certs=false" >> $GITHUB_OUTPUT + echo "⚠️ Code signing certificates not configured. MAS build will be unsigned." + fi + + - name: Import Code Signing Certificates + if: steps.check_certs.outputs.has_certs == 'true' + env: + MAC_BUILD_CERTIFICATE_BASE64: ${{ secrets.MAC_BUILD_CERTIFICATE_BASE64 }} + MAC_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.MAC_INSTALLER_CERTIFICATE_BASE64 }} + MAC_P12_PASSWORD: ${{ secrets.MAC_P12_PASSWORD }} + MAC_KEYCHAIN_PASSWORD: ${{ secrets.MAC_KEYCHAIN_PASSWORD }} + run: | + APP_CERT_PATH=$RUNNER_TEMP/app_certificate.p12 + INSTALLER_CERT_PATH=$RUNNER_TEMP/installer_certificate.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + + # Decode certificates + echo -n "$MAC_BUILD_CERTIFICATE_BASE64" | base64 --decode -o $APP_CERT_PATH + + if [ -n "$MAC_INSTALLER_CERTIFICATE_BASE64" ]; then + echo "Decoding installer certificate..." + echo -n "$MAC_INSTALLER_CERTIFICATE_BASE64" | base64 --decode -o $INSTALLER_CERT_PATH + else + echo "⚠️ MAC_INSTALLER_CERTIFICATE_BASE64 is empty" + fi + + # Create and configure keychain + security create-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # Import application certificate + echo "Importing application certificate..." + security import $APP_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + + # Import installer certificate if it exists + if [ -f "$INSTALLER_CERT_PATH" ]; then + echo "Importing installer certificate..." + security import $INSTALLER_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + else + echo "⚠️ Installer certificate file not found, skipping import" + fi + + security list-keychain -d user -s $KEYCHAIN_PATH + + echo "Imported certificates:" + security find-identity -v -p codesigning $KEYCHAIN_PATH + + - name: Build macOS App Store Package + if: steps.check_certs.outputs.has_certs == 'true' + env: + ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true + run: | + # Get current version for display + CURRENT_VERSION=$(node -p "require('./package.json').version") + BUILD_VERSION="${{ github.run_number }}" + + echo "✅ Package version: $CURRENT_VERSION (unchanged)" + echo "✅ Build number for Apple: $BUILD_VERSION" + + # Build MAS with custom buildVersion + npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION" + + - name: Clean up MAS keychain before DMG build + if: steps.check_certs.outputs.has_certs == 'true' + run: | + security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true + echo "Cleaned up MAS keychain" + + - name: Check for Developer ID Certificates + id: check_dev_id_certs + run: | + if [ -n "${{ secrets.DEVELOPER_ID_CERTIFICATE_BASE64 }}" ] && [ -n "${{ secrets.DEVELOPER_ID_P12_PASSWORD }}" ]; then + echo "has_dev_id_certs=true" >> $GITHUB_OUTPUT + echo "✅ Developer ID certificates configured for DMG signing" + else + echo "has_dev_id_certs=false" >> $GITHUB_OUTPUT + echo "⚠️ Developer ID certificates not configured. DMG will be unsigned." + echo "Add DEVELOPER_ID_CERTIFICATE_BASE64 and DEVELOPER_ID_P12_PASSWORD secrets to enable DMG signing." + fi + + - name: Import Developer ID Certificates + if: steps.check_dev_id_certs.outputs.has_dev_id_certs == 'true' + env: + DEVELOPER_ID_CERTIFICATE_BASE64: ${{ secrets.DEVELOPER_ID_CERTIFICATE_BASE64 }} + DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64 }} + DEVELOPER_ID_P12_PASSWORD: ${{ secrets.DEVELOPER_ID_P12_PASSWORD }} + MAC_KEYCHAIN_PASSWORD: ${{ secrets.MAC_KEYCHAIN_PASSWORD }} + run: | + DEV_CERT_PATH=$RUNNER_TEMP/dev_certificate.p12 + DEV_INSTALLER_CERT_PATH=$RUNNER_TEMP/dev_installer_certificate.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/dev-signing.keychain-db + + # Decode Developer ID certificate + echo -n "$DEVELOPER_ID_CERTIFICATE_BASE64" | base64 --decode -o $DEV_CERT_PATH + + if [ -n "$DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64" ]; then + echo "Decoding Developer ID installer certificate..." + echo -n "$DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64" | base64 --decode -o $DEV_INSTALLER_CERT_PATH + else + echo "⚠️ DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64 is empty (optional)" + fi + + # Create and configure keychain + security create-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # Import Developer ID Application certificate + echo "Importing Developer ID Application certificate..." + security import $DEV_CERT_PATH -P "$DEVELOPER_ID_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + + # Import Developer ID Installer certificate if it exists + if [ -f "$DEV_INSTALLER_CERT_PATH" ]; then + echo "Importing Developer ID Installer certificate..." + security import $DEV_INSTALLER_CERT_PATH -P "$DEVELOPER_ID_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + fi + + security list-keychain -d user -s $KEYCHAIN_PATH + + echo "Imported Developer ID certificates:" + security find-identity -v -p codesigning $KEYCHAIN_PATH + + - name: Build macOS DMG + env: + ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + # Build DMG without running npm run build again (already built above or skip if no certs) + if [ "${{ steps.check_certs.outputs.has_certs }}" == "true" ]; then + # Frontend already built, just package DMG + npx electron-builder --mac dmg --universal --x64 --arm64 + else + # No certs, need to build frontend first + npm run build && npx electron-builder --mac dmg --universal --x64 --arm64 + fi + + - name: List release directory + if: steps.check_certs.outputs.has_certs == 'true' + run: | + echo "Contents of release directory:" + ls -R release/ || echo "Release directory not found" + + - name: Upload macOS MAS PKG + if: steps.check_certs.outputs.has_certs == 'true' && hashFiles('release/*_*_*_mas.pkg') != '' && (github.event.inputs.artifact_destination == 'file' || github.event.inputs.artifact_destination == 'release' || github.event.inputs.artifact_destination == 'submit') + uses: actions/upload-artifact@v4 + with: + name: termix_macos_mas + path: release/*_*_*_mas.pkg + retention-days: 30 + if-no-files-found: warn + + - name: Upload macOS Universal DMG + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_universal_*_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_macos_universal_dmg + path: release/*_universal_*_dmg.dmg + retention-days: 30 + + - name: Upload macOS x64 DMG + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_x64_*_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_macos_x64_dmg + path: release/*_x64_*_dmg.dmg + retention-days: 30 + + - name: Upload macOS arm64 DMG + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_arm64_*_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_macos_arm64_dmg + path: release/*_arm64_*_dmg.dmg + retention-days: 30 + + - name: Check for App Store Connect API credentials + if: steps.check_certs.outputs.has_certs == 'true' + id: check_asc_creds + run: | + if [ -n "${{ secrets.APPLE_KEY_ID }}" ] && [ -n "${{ secrets.APPLE_ISSUER_ID }}" ] && [ -n "${{ secrets.APPLE_KEY_CONTENT }}" ]; then + echo "has_credentials=true" >> $GITHUB_OUTPUT + if [ "${{ github.event.inputs.artifact_destination }}" == "submit" ]; then + echo "✅ App Store Connect API credentials found. Will deploy to TestFlight." + else + echo "ℹ️ App Store Connect API credentials found, but store submission is disabled." + fi + else + echo "has_credentials=false" >> $GITHUB_OUTPUT + echo "⚠️ App Store Connect API credentials not configured. Skipping deployment." + echo "Add APPLE_KEY_ID, APPLE_ISSUER_ID, and APPLE_KEY_CONTENT secrets to enable automatic deployment." + fi + + - name: Setup Ruby for Fastlane + if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit' + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2' + bundler-cache: false + + - name: Install Fastlane + if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit' + run: | + gem install fastlane -N + fastlane --version + + - name: Deploy to App Store Connect (TestFlight) + if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit' + run: | + PKG_FILE=$(find release -name "*.pkg" -type f | head -n 1) + if [ -z "$PKG_FILE" ]; then + echo "Error: No .pkg file found in release directory" + exit 1 + fi + echo "Found package: $PKG_FILE" + + # Create API key file + mkdir -p ~/private_keys + echo "${{ secrets.APPLE_KEY_CONTENT }}" | base64 --decode > ~/private_keys/AuthKey_${{ secrets.APPLE_KEY_ID }}.p8 + + # Upload to App Store Connect using xcrun altool + xcrun altool --upload-app -f "$PKG_FILE" \ + --type macos \ + --apiKey "${{ secrets.APPLE_KEY_ID }}" \ + --apiIssuer "${{ secrets.APPLE_ISSUER_ID }}" + + echo "✅ Upload complete! Build will appear in App Store Connect after processing (10-30 minutes)" + continue-on-error: true + + - name: Clean up keychains + if: always() + run: | + security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true + security delete-keychain $RUNNER_TEMP/dev-signing.keychain-db || true + + submit-to-chocolatey: + runs-on: windows-latest + if: github.event.inputs.artifact_destination == 'submit' + needs: [build-windows] + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Get version from package.json + id: package-version + run: | + $VERSION = (Get-Content package.json | ConvertFrom-Json).version + echo "version=$VERSION" >> $env:GITHUB_OUTPUT + echo "Building Chocolatey package for version: $VERSION" + + - name: Download Windows x64 MSI artifact + uses: actions/download-artifact@v4 + with: + name: termix_windows_x64_msi + path: artifact + + - name: Get MSI file info + id: msi-info + run: | + $VERSION = "${{ steps.package-version.outputs.version }}" + $MSI_FILE = Get-ChildItem -Path artifact -Filter "*.msi" | Select-Object -First 1 + $MSI_NAME = $MSI_FILE.Name + $CHECKSUM = (Get-FileHash -Path $MSI_FILE.FullName -Algorithm SHA256).Hash + + echo "msi_name=$MSI_NAME" >> $env:GITHUB_OUTPUT + echo "checksum=$CHECKSUM" >> $env:GITHUB_OUTPUT + echo "MSI File: $MSI_NAME" + echo "SHA256: $CHECKSUM" + + - name: Prepare Chocolatey package + run: | + $VERSION = "${{ steps.package-version.outputs.version }}" + $CHECKSUM = "${{ steps.msi-info.outputs.checksum }}" + $MSI_NAME = "${{ steps.msi-info.outputs.msi_name }}" + + # Construct the download URL with the actual release tag format + $DOWNLOAD_URL = "https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$MSI_NAME" + + # Copy chocolatey files to build directory + New-Item -ItemType Directory -Force -Path "choco-build" + Copy-Item -Path "chocolatey\*" -Destination "choco-build" -Recurse -Force + + # Update chocolateyinstall.ps1 with actual values + $installScript = Get-Content "choco-build\tools\chocolateyinstall.ps1" -Raw -Encoding UTF8 + $installScript = $installScript -replace 'DOWNLOAD_URL_PLACEHOLDER', $DOWNLOAD_URL + $installScript = $installScript -replace 'CHECKSUM_PLACEHOLDER', $CHECKSUM + [System.IO.File]::WriteAllText("$PWD\choco-build\tools\chocolateyinstall.ps1", $installScript, [System.Text.UTF8Encoding]::new($false)) + + # Update nuspec with version (preserve UTF-8 encoding without BOM) + $nuspec = Get-Content "choco-build\termix-ssh.nuspec" -Raw -Encoding UTF8 + $nuspec = $nuspec -replace 'VERSION_PLACEHOLDER', $VERSION + [System.IO.File]::WriteAllText("$PWD\choco-build\termix-ssh.nuspec", $nuspec, [System.Text.UTF8Encoding]::new($false)) + + echo "Chocolatey package prepared for version $VERSION" + echo "Download URL: $DOWNLOAD_URL" + + # Verify the nuspec is valid + echo "" + echo "Verifying nuspec content:" + Get-Content "choco-build\termix-ssh.nuspec" -Head 10 + echo "" + + - name: Install Chocolatey + run: | + Set-ExecutionPolicy Bypass -Scope Process -Force + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 + iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) + + - name: Pack Chocolatey package + run: | + cd choco-build + echo "Packing Chocolatey package..." + choco pack termix-ssh.nuspec + + if ($LASTEXITCODE -ne 0) { + echo "❌ Failed to pack Chocolatey package" + exit 1 + } + + echo "" + echo "✅ Package created successfully" + echo "Package contents:" + Get-ChildItem *.nupkg | ForEach-Object { echo $_.Name } + + - name: Check for Chocolatey API Key + id: check_choco_key + run: | + if ("${{ secrets.CHOCOLATEY_API_KEY }}" -ne "") { + echo "has_key=true" >> $env:GITHUB_OUTPUT + echo "✅ Chocolatey API key found. Will push to Chocolatey." + } else { + echo "has_key=false" >> $env:GITHUB_OUTPUT + echo "⚠️ Chocolatey API key not configured. Package will be created but not pushed." + echo "Add CHOCOLATEY_API_KEY secret to enable automatic submission." + } + + - name: Push to Chocolatey + if: steps.check_choco_key.outputs.has_key == 'true' + run: | + $VERSION = "${{ steps.package-version.outputs.version }}" + cd choco-build + choco apikey --key "${{ secrets.CHOCOLATEY_API_KEY }}" --source https://push.chocolatey.org/ + + try { + choco push "termix-ssh.$VERSION.nupkg" --source https://push.chocolatey.org/ + if ($LASTEXITCODE -eq 0) { + echo "" + echo "✅ Package pushed to Chocolatey successfully!" + echo "View at: https://community.chocolatey.org/packages/termix-ssh/$VERSION" + } else { + throw "Chocolatey push failed with exit code $LASTEXITCODE" + } + } catch { + echo "" + echo "❌ Failed to push to Chocolatey" + echo "" + echo "Common reasons:" + echo "1. Package ID 'termix-ssh' is already owned by another user" + echo "2. You need to register/claim the package ID first" + echo "3. API key doesn't have push permissions" + echo "" + echo "Solutions:" + echo "1. Check if package exists: https://community.chocolatey.org/packages/termix-ssh" + echo "2. If it exists and is yours, contact Chocolatey support to claim it" + echo "3. Register a new package ID at: https://community.chocolatey.org/" + echo "" + echo "The package artifact has been saved for manual submission." + echo "" + exit 1 + } + + - name: Upload Chocolatey package as artifact + uses: actions/upload-artifact@v4 + with: + name: chocolatey-package + path: choco-build/*.nupkg + retention-days: 30 + + submit-to-flatpak: + runs-on: ubuntu-latest + if: github.event.inputs.artifact_destination == 'submit' + needs: [build-linux] + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Get version from package.json + id: package-version + run: | + VERSION=$(node -p "require('./package.json').version") + RELEASE_DATE=$(date +%Y-%m-%d) + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "release_date=$RELEASE_DATE" >> $GITHUB_OUTPUT + echo "Building Flatpak submission for version: $VERSION" + + - name: Download Linux x64 AppImage artifact + uses: actions/download-artifact@v4 + with: + name: termix_linux_x64_appimage + path: artifact-x64 + + - name: Download Linux arm64 AppImage artifact + uses: actions/download-artifact@v4 + with: + name: termix_linux_arm64_appimage + path: artifact-arm64 + + - name: Get AppImage file info + id: appimage-info + run: | + VERSION="${{ steps.package-version.outputs.version }}" + + # x64 AppImage + APPIMAGE_X64_FILE=$(find artifact-x64 -name "*.AppImage" -type f | head -n 1) + APPIMAGE_X64_NAME=$(basename "$APPIMAGE_X64_FILE") + CHECKSUM_X64=$(sha256sum "$APPIMAGE_X64_FILE" | awk '{print $1}') + + # arm64 AppImage + APPIMAGE_ARM64_FILE=$(find artifact-arm64 -name "*.AppImage" -type f | head -n 1) + APPIMAGE_ARM64_NAME=$(basename "$APPIMAGE_ARM64_FILE") + CHECKSUM_ARM64=$(sha256sum "$APPIMAGE_ARM64_FILE" | awk '{print $1}') + + echo "appimage_x64_name=$APPIMAGE_X64_NAME" >> $GITHUB_OUTPUT + echo "checksum_x64=$CHECKSUM_X64" >> $GITHUB_OUTPUT + echo "appimage_arm64_name=$APPIMAGE_ARM64_NAME" >> $GITHUB_OUTPUT + echo "checksum_arm64=$CHECKSUM_ARM64" >> $GITHUB_OUTPUT + + echo "x64 AppImage: $APPIMAGE_X64_NAME" + echo "x64 SHA256: $CHECKSUM_X64" + echo "arm64 AppImage: $APPIMAGE_ARM64_NAME" + echo "arm64 SHA256: $CHECKSUM_ARM64" + + - name: Install ImageMagick for icon generation + run: | + sudo apt-get update + sudo apt-get install -y imagemagick + + - name: Prepare Flatpak submission files + run: | + VERSION="${{ steps.package-version.outputs.version }}" + CHECKSUM_X64="${{ steps.appimage-info.outputs.checksum_x64 }}" + CHECKSUM_ARM64="${{ steps.appimage-info.outputs.checksum_arm64 }}" + RELEASE_DATE="${{ steps.package-version.outputs.release_date }}" + APPIMAGE_X64_NAME="${{ steps.appimage-info.outputs.appimage_x64_name }}" + APPIMAGE_ARM64_NAME="${{ steps.appimage-info.outputs.appimage_arm64_name }}" + + # Create submission directory + mkdir -p flatpak-submission + + # Copy Flatpak files to submission directory + cp flatpak/com.karmaa.termix.yml flatpak-submission/ + cp flatpak/com.karmaa.termix.desktop flatpak-submission/ + cp flatpak/com.karmaa.termix.metainfo.xml flatpak-submission/ + cp flatpak/flathub.json flatpak-submission/ + + # Copy and prepare icons + cp public/icon.svg flatpak-submission/com.karmaa.termix.svg + convert public/icon.png -resize 256x256 flatpak-submission/icon-256.png + convert public/icon.png -resize 128x128 flatpak-submission/icon-128.png + + # Update manifest with version and checksums + sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak-submission/com.karmaa.termix.yml + sed -i "s/CHECKSUM_X64_PLACEHOLDER/$CHECKSUM_X64/g" flatpak-submission/com.karmaa.termix.yml + sed -i "s/CHECKSUM_ARM64_PLACEHOLDER/$CHECKSUM_ARM64/g" flatpak-submission/com.karmaa.termix.yml + + # Update metainfo with version and date + sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak-submission/com.karmaa.termix.metainfo.xml + sed -i "s/DATE_PLACEHOLDER/$RELEASE_DATE/g" flatpak-submission/com.karmaa.termix.metainfo.xml + + echo "✅ Flatpak submission files prepared for version $VERSION" + echo "x64 Download URL: https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_X64_NAME" + echo "arm64 Download URL: https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_ARM64_NAME" + + - name: Create submission instructions + run: | + cat > flatpak-submission/SUBMISSION_INSTRUCTIONS.md << 'EOF' + # Flathub Submission Instructions for Termix + + ## Automatic Submission (Recommended) + + All files needed for Flathub submission are in this artifact. Follow these steps: + + 1. **Fork the Flathub repository**: + - Go to https://github.com/flathub/flathub + - Click "Fork" button + + 2. **Clone your fork**: + ```bash + git clone https://github.com/YOUR-USERNAME/flathub.git + cd flathub + git checkout -b com.karmaa.termix + ``` + + 3. **Copy all files from this artifact** to the root of your flathub fork + + 4. **Commit and push**: + ```bash + git add . + git commit -m "Add Termix ${{ steps.package-version.outputs.version }}" + git push origin com.karmaa.termix + ``` + + 5. **Create Pull Request**: + - Go to https://github.com/YOUR-USERNAME/flathub + - Click "Compare & pull request" + - Submit PR to flathub/flathub + + ## Files in this submission: + + - `com.karmaa.termix.yml` - Flatpak manifest + - `com.karmaa.termix.desktop` - Desktop entry + - `com.karmaa.termix.metainfo.xml` - AppStream metadata + - `flathub.json` - Flathub configuration + - `com.karmaa.termix.svg` - SVG icon + - `icon-256.png` - 256x256 icon + - `icon-128.png` - 128x128 icon + + ## Version Information: + + - Version: ${{ steps.package-version.outputs.version }} + - Release Date: ${{ steps.package-version.outputs.release_date }} + - x64 AppImage SHA256: ${{ steps.appimage-info.outputs.checksum_x64 }} + - arm64 AppImage SHA256: ${{ steps.appimage-info.outputs.checksum_arm64 }} + + ## After Submission: + + 1. Flathub maintainers will review your submission (usually 1-5 days) + 2. They may request changes - be responsive to feedback + 3. Once approved, Termix will be available via: `flatpak install flathub com.karmaa.termix` + + ## Resources: + + - [Flathub Submission Guidelines](https://docs.flathub.org/docs/for-app-authors/submission) + - [Flatpak Documentation](https://docs.flatpak.org/) + EOF + + echo "✅ Created submission instructions" + + - name: List submission files + run: | + echo "Flatpak submission files:" + ls -la flatpak-submission/ + + - name: Upload Flatpak submission as artifact + uses: actions/upload-artifact@v4 + with: + name: flatpak-submission + path: flatpak-submission/* + retention-days: 30 + + - name: Display next steps + run: | + echo "" + echo "🎉 Flatpak submission files ready!" + echo "" + echo "📦 Download the 'flatpak-submission' artifact and follow SUBMISSION_INSTRUCTIONS.md" + echo "" + echo "Quick summary:" + echo "1. Fork https://github.com/flathub/flathub" + echo "2. Copy artifact files to your fork" + echo "3. Create PR to flathub/flathub" + echo "" + + submit-to-homebrew: + runs-on: macos-latest + if: github.event.inputs.artifact_destination == 'submit' + needs: [build-macos] + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Get version from package.json + id: package-version + run: | + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Building Homebrew Cask for version: $VERSION" + + - name: Download macOS Universal DMG artifact + uses: actions/download-artifact@v4 + with: + name: termix_macos_universal_dmg + path: artifact + + - name: Get DMG file info + id: dmg-info + run: | + VERSION="${{ steps.package-version.outputs.version }}" + DMG_FILE=$(find artifact -name "*.dmg" -type f | head -n 1) + DMG_NAME=$(basename "$DMG_FILE") + CHECKSUM=$(shasum -a 256 "$DMG_FILE" | awk '{print $1}') + + echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT + echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT + echo "DMG File: $DMG_NAME" + echo "SHA256: $CHECKSUM" + + - name: Prepare Homebrew submission files + run: | + VERSION="${{ steps.package-version.outputs.version }}" + CHECKSUM="${{ steps.dmg-info.outputs.checksum }}" + DMG_NAME="${{ steps.dmg-info.outputs.dmg_name }}" + + # Create submission directory + mkdir -p homebrew-submission/Casks/t + + # Copy Homebrew cask file + cp homebrew/termix.rb homebrew-submission/Casks/t/termix.rb + cp homebrew/README.md homebrew-submission/ + + # Update cask with version and checksum + sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" homebrew-submission/Casks/t/termix.rb + sed -i '' "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" homebrew-submission/Casks/t/termix.rb + + echo "✅ Homebrew Cask prepared for version $VERSION" + echo "Download URL: https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$DMG_NAME" + + - name: Verify Cask syntax + run: | + # Install Homebrew if not present (should be on macos-latest) + if ! command -v brew &> /dev/null; then + echo "Installing Homebrew..." + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + fi + + # Basic syntax check + ruby -c homebrew-submission/Casks/t/termix.rb + echo "✅ Cask syntax is valid" + + - name: Create submission instructions + run: | + cat > homebrew-submission/SUBMISSION_INSTRUCTIONS.md << 'EOF' + # Homebrew Cask Submission Instructions for Termix + + ## Option 1: Submit to Official Homebrew Cask (Recommended) + + ### Prerequisites + - macOS with Homebrew installed + - GitHub account + + ### Steps + + 1. **Fork the Homebrew Cask repository**: + - Go to https://github.com/Homebrew/homebrew-cask + - Click "Fork" button + + 2. **Clone your fork**: + ```bash + git clone https://github.com/YOUR-USERNAME/homebrew-cask.git + cd homebrew-cask + git checkout -b termix + ``` + + 3. **Copy the cask file**: + - Copy `Casks/t/termix.rb` from this artifact to your fork at `Casks/t/termix.rb` + - Note: Casks are organized by first letter in subdirectories + + 4. **Test the cask locally**: + ```bash + brew install --cask ./Casks/t/termix.rb + brew uninstall --cask termix + ``` + + 5. **Run audit checks**: + ```bash + brew audit --cask --online ./Casks/t/termix.rb + brew style ./Casks/t/termix.rb + ``` + + 6. **Commit and push**: + ```bash + git add Casks/t/termix.rb + git commit -m "Add Termix ${{ steps.package-version.outputs.version }}" + git push origin termix + ``` + + 7. **Create Pull Request**: + - Go to https://github.com/YOUR-USERNAME/homebrew-cask + - Click "Compare & pull request" + - Fill in the PR template + - Submit to Homebrew/homebrew-cask + + ### PR Requirements + + Your PR should include: + - Clear commit message: "Add Termix X.Y.Z" or "Update Termix to X.Y.Z" + - All audit checks passing + - Working download URL + - Valid SHA256 checksum + + ## Option 2: Create Your Own Tap (Alternative) + + If you want more control and faster updates: + + 1. **Create a tap repository**: + - Create repo: `Termix-SSH/homebrew-termix` + - Add `Casks/termix.rb` to the repo + + 2. **Users install with**: + ```bash + brew tap termix-ssh/termix + brew install --cask termix + ``` + + ### Advantages of Custom Tap + - No approval process + - Instant updates + - Full control + - Can include beta versions + + ### Disadvantages + - Less discoverable + - Users must add tap first + - You maintain it yourself + + ## Files in this submission: + + - `Casks/t/termix.rb` - Homebrew Cask formula + - `README.md` - Detailed documentation + - `SUBMISSION_INSTRUCTIONS.md` - This file + + ## Version Information: + + - Version: ${{ steps.package-version.outputs.version }} + - DMG SHA256: ${{ steps.dmg-info.outputs.checksum }} + - DMG URL: https://github.com/Termix-SSH/Termix/releases/download/release-${{ steps.package-version.outputs.version }}-tag/${{ steps.dmg-info.outputs.dmg_name }} + + ## After Submission: + + ### Official Homebrew Cask: + 1. Maintainers will review (usually 24-48 hours) + 2. May request changes or fixes + 3. Once merged, users can install with: `brew install --cask termix` + 4. Homebrew bot will auto-update for future releases + + ### Custom Tap: + 1. Push to your tap repository + 2. Immediately available to users + 3. Update the cask file for each new release + + ## Resources: + + - [Homebrew Cask Documentation](https://docs.brew.sh/Cask-Cookbook) + - [Acceptable Casks](https://docs.brew.sh/Acceptable-Casks) + - [How to Open a PR](https://docs.brew.sh/How-To-Open-a-Homebrew-Pull-Request) + EOF + + echo "✅ Created submission instructions" + + - name: List submission files + run: | + echo "Homebrew submission files:" + find homebrew-submission -type f + + - name: Upload Homebrew submission as artifact + uses: actions/upload-artifact@v4 + with: + name: homebrew-submission + path: homebrew-submission/* + retention-days: 30 + + - name: Display next steps + run: | + echo "" + echo "🍺 Homebrew Cask ready!" + echo "" + echo "📦 Download the 'homebrew-submission' artifact and follow SUBMISSION_INSTRUCTIONS.md" + echo "" + echo "Quick summary:" + echo "Option 1 (Recommended): Fork https://github.com/Homebrew/homebrew-cask and submit PR" + echo "Option 2 (Alternative): Create your own tap at Termix-SSH/homebrew-termix" + echo "" + + upload-to-release: + runs-on: blacksmith-4vcpu-ubuntu-2404 + if: github.event.inputs.artifact_destination == 'release' + needs: [build-windows, build-linux, build-macos] + permissions: + contents: write + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Get latest release + id: get_release + run: | + echo "Fetching latest release from ${{ github.repository }}..." + LATEST_RELEASE=$(gh release list --repo ${{ github.repository }} --limit 1 --json tagName,name,isLatest -q '.[0]') + + if [ -z "$LATEST_RELEASE" ]; then + echo "ERROR: No releases found in ${{ github.repository }}" + exit 1 + fi + + RELEASE_TAG=$(echo "$LATEST_RELEASE" | jq -r '.tagName') + RELEASE_NAME=$(echo "$LATEST_RELEASE" | jq -r '.name') + + echo "tag=$RELEASE_TAG" >> $GITHUB_OUTPUT + echo "name=$RELEASE_NAME" >> $GITHUB_OUTPUT + echo "Latest release: $RELEASE_NAME ($RELEASE_TAG)" + env: + GH_TOKEN: ${{ github.token }} + + - name: Display artifact structure + run: | + echo "Artifact structure:" + ls -R artifacts/ + + - name: Upload artifacts to latest release + run: | + RELEASE_TAG="${{ steps.get_release.outputs.tag }}" + echo "Uploading artifacts to release: $RELEASE_TAG" + echo "" + + cd artifacts + for dir in */; do + echo "Processing directory: $dir" + cd "$dir" + for file in *; do + if [ -f "$file" ]; then + echo "Uploading: $file" + gh release upload "$RELEASE_TAG" "$file" --repo ${{ github.repository }} --clobber + echo "✓ $file uploaded successfully" + fi + done + cd .. + done + + echo "" + echo "All artifacts uploaded to: https://github.com/${{ github.repository }}/releases/tag/$RELEASE_TAG" + env: + GH_TOKEN: ${{ github.token }} From ce865daedf3665b6817bdf722e841670c19b1e9c Mon Sep 17 00:00:00 2001 From: root Date: Mon, 3 Nov 2025 16:28:02 +0000 Subject: [PATCH 12/12] feat: add Russian translation and readme --- README-RU.md | 137 +++ src/locales/ru/translation.json | 1591 +++++++++++++++++++++++++++++++ 2 files changed, 1728 insertions(+) create mode 100644 README-RU.md create mode 100644 src/locales/ru/translation.json diff --git a/README-RU.md b/README-RU.md new file mode 100644 index 00000000..6aabbdd6 --- /dev/null +++ b/README-RU.md @@ -0,0 +1,137 @@ +# Статистика репозитория + +

+ English English | + 中文 中文 | + Русский Русский +

+ +![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars) +![GitHub forks](https://img.shields.io/github/forks/Termix-SSH/Termix?style=flat&label=Forks) +![GitHub Release](https://img.shields.io/github/v/release/Termix-SSH/Termix?style=flat&label=Release) +Discord + +

+ Достижение дня +
+ Достигнуто 1 сентября 2025 года +

+ +#### Лучшие технологии + +[![React Badge](https://img.shields.io/badge/-React-61DBFB?style=flat-square&labelColor=black&logo=react&logoColor=61DBFB)](#) +[![TypeScript Badge](https://img.shields.io/badge/-TypeScript-3178C6?style=flat-square&labelColor=black&logo=typescript&logoColor=3178C6)](#) +[![Node.js Badge](https://img.shields.io/badge/-Node.js-3C873A?style=flat-square&labelColor=black&logo=node.js&logoColor=3C873A)](#) +[![Vite Badge](https://img.shields.io/badge/-Vite-646CFF?style=flat-square&labelColor=black&logo=vite&logoColor=646CFF)](#) +[![Tailwind CSS Badge](https://img.shields.io/badge/-TailwindCSS-38B2AC?style=flat-square&labelColor=black&logo=tailwindcss&logoColor=38B2AC)](#) +[![Docker Badge](https://img.shields.io/badge/-Docker-2496ED?style=flat-square&labelColor=black&logo=docker&logoColor=2496ED)](#) +[![SQLite Badge](https://img.shields.io/badge/-SQLite-003B57?style=flat-square&labelColor=black&logo=sqlite&logoColor=003B57)](#) +[![Radix UI Badge](https://img.shields.io/badge/-Radix%20UI-161618?style=flat-square&labelColor=black&logo=radixui&logoColor=161618)](#) + +
+

+ + Баннер Termix +

+ +Если хотите, вы можете поддержать проект здесь!\ +[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus) + +# Обзор + +

+ + Баннер Termix +

+ +Termix — это открытая, бесплатная и самохостинговая универсальная платформа для управления серверами. Она предоставляет веб-решение +для управления вашими серверами и инфраструктурой через единый интуитивно понятный интерфейс. Termix предлагает доступ к SSH-терминалу, +возможности SSH-туннелирования и удаленное управление файлами, а в будущем появится еще больше инструментов. + +# Особенности + +- **Доступ к SSH-терминалу** — полнофункциональный терминал с поддержкой разделения экрана (до 4 панелей) и системой вкладок +- **Управление SSH-туннелями** — создание и управление SSH-туннелями с автоматической переподключением и мониторингом работоспособности +- **Удаленный файловый менеджер** — управление файлами непосредственно на удаленных серверах с поддержкой просмотра и редактирования кода, изображений, аудио и видео. Беспроблемная загрузка, скачивание, переименование, удаление и перемещение файлов. +- **Менеджер хостов SSH** — сохраняйте, систематизируйте и управляйте своими SSH-соединениями с помощью тегов и папок, а также легко сохраняйте повторно используемую информацию для входа в систему, имея возможность автоматизировать развертывание SSH-ключей +- **Статистика сервера** — просмотр использования ЦП, памяти и жесткого диска на любом SSH-сервере +- **Аутентификация пользователей** - Безопасное управление пользователями с помощью элементов управления администратора и поддержки OIDC и 2FA (TOTP). +- **Шифрование базы данных** - Файлы базы данных SQLite шифруются в режиме ожидания с помощью автоматического шифрования/дешифрования. +- **Экспорт/импорт данных** - Экспорт и импорт SSH-хостов, учетных данных и данных файлового менеджера с инкрементной синхронизацией. +- **Автоматическая настройка SSL** - Встроенная генерация и управление SSL-сертификатами с перенаправлением HTTPS. +- **Современный интерфейс** - Чистый интерфейс, удобный для настольных компьютеров и мобильных устройств, созданный с помощью React, Tailwind CSS и Shadcn +- **Языки** - Встроенная поддержка английского, китайского и немецкого языков +- **Поддержка платформ** - Доступно в виде веб-приложения, настольного приложения (Windows и Linux) и специального мобильного приложения для iOS и Android. Планируется поддержка macOS и iPadOS. + +# Планируемые функции + +Смотрите [Projects](https://github.com/orgs/Termix-SSH/projects/2) для ознакомления со всеми запланированными функциями. Если вы хотите внести свой вклад смотрите [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md). + +# Установка + +Поддерживаемые устройства: + +- Веб-сайт (любой современный браузер, такой как Google, Safari и Firefox) +- Windows (приложение) +- Linux (приложение) +- iOS (приложение) +- Android (приложение) +- iPadOS и macOS находятся в стадии разработки + +Посетите Termix [Docs](https://docs.termix.site/install) для получения дополнительной информации об установке Termix на всех платформах. В противном случае, +просмотрите пример файла Docker Compose здесь: + +```yaml +services: + termix: + image: ghcr.io/lukegus/termix:latest + container_name: termix + restart: unless-stopped + ports: + - '8080:8080' + volumes: + - termix-data:/app/data + environment: + PORT: '8080' + +volumes: + termix-data: + driver: local +``` + +# Поддержка + +Если вам нужна помощь или вы хотите запросить функцию в Termix, посетите [Issues](https://github.com/Termix-SSH/Support/issues), войдите в систему и нажмите `New Issue`. +Пожалуйста, опишите свою проблему как можно подробнее, желательно на английском языке. Вы также можете присоединиться к [Discord](https://discord.gg/jVQGdvHDrf) серверу и посетите канал +службы поддержки, однако время отклика может быть более длительным. + +# Внешний вид + +

+ Termix Demo 1 + Termix Demo 2 +

+ +

+ Termix Demo 3 + Termix Demo 4 +

+ +

+ Termix Demo 5 + Termix Demo 6 +

+ +

+ Termix Demo 7 +

+ +

+ +

+ +# Лицензия + +Распространяется по лицензии Apache версии 2.0. Дополнительную информацию смотрите в файле LICENSE. diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json new file mode 100644 index 00000000..606e6d8d --- /dev/null +++ b/src/locales/ru/translation.json @@ -0,0 +1,1591 @@ +{ + "credentials": { + "credentialsViewer": "Просмотр учетных данных", + "manageYourSSHCredentials": "Безопасное управление вашими SSH-учетными данными", + "addCredential": "Добавить учетные данные", + "createCredential": "Создать учетные данные", + "editCredential": "Редактировать учетные данные", + "viewCredential": "Просмотреть учетные данные", + "duplicateCredential": "Дублировать учетные данные", + "deleteCredential": "Удалить учетные данные", + "updateCredential": "Обновить учетные данные", + "credentialName": "Название учетных данных", + "credentialDescription": "Описание", + "username": "Имя пользователя", + "searchCredentials": "Поиск учетных данных...", + "selectFolder": "Выбрать папку", + "selectAuthType": "Выбрать тип аутентификации", + "allFolders": "Все папки", + "allAuthTypes": "Все типы аутентификации", + "uncategorized": "Без категории", + "totalCredentials": "Всего", + "keyBased": "На основе ключа", + "passwordBased": "На основе пароля", + "folders": "Папки", + "noCredentialsMatchFilters": "Нет учетных данных, соответствующих вашим фильтрам", + "noCredentialsYet": "Учетные данные еще не созданы", + "createFirstCredential": "Создайте свои первые учетные данные", + "failedToFetchCredentials": "Не удалось загрузить учетные данные", + "credentialDeletedSuccessfully": "Учетные данные успешно удалены", + "failedToDeleteCredential": "Не удалось удалить учетные данные", + "confirmDeleteCredential": "Вы уверены, что хотите удалить учетные данные \"{{name}}\"?", + "credentialCreatedSuccessfully": "Учетные данные успешно созданы", + "credentialUpdatedSuccessfully": "Учетные данные успешно обновлены", + "failedToSaveCredential": "Не удалось сохранить учетные данные", + "failedToFetchCredentialDetails": "Не удалось загрузить детали учетных данных", + "failedToFetchHostsUsing": "Не удалось загрузить хосты, использующие эти учетные данные", + "loadingCredentials": "Загрузка учетных данных...", + "retry": "Повторить", + "noCredentials": "Нет учетных данных", + "noCredentialsMessage": "Вы еще не добавили учетные данные. Нажмите \"Добавить учетные данные\", чтобы начать.", + "sshCredentials": "SSH-учетные данные", + "credentialsCount": "{{count}} учетных данных", + "refresh": "Обновить", + "passwordRequired": "Требуется пароль", + "sshKeyRequired": "Требуется SSH-ключ", + "credentialAddedSuccessfully": "Учетные данные \"{{name}}\" успешно добавлены", + "general": "Общее", + "description": "Описание", + "folder": "Папка", + "tags": "Теги", + "addTagsSpaceToAdd": "Добавить теги (нажмите пробел для добавления)", + "password": "Пароль", + "key": "Ключ", + "sshPrivateKey": "Приватный SSH-ключ", + "upload": "Загрузить", + "updateKey": "Обновить ключ", + "keyPassword": "Пароль ключа (опционально)", + "keyType": "Тип ключа", + "keyTypeRSA": "RSA", + "keyTypeECDSA": "ECDSA", + "keyTypeEd25519": "Ed25519", + "updateCredential": "Обновить учетные данные", + "basicInfo": "Основная информация", + "authentication": "Аутентификация", + "organization": "Организация", + "basicInformation": "Основная информация", + "basicInformationDescription": "Введите основную информацию для этих учетных данных", + "authenticationMethod": "Метод аутентификации", + "authenticationMethodDescription": "Выберите способ аутентификации на SSH-серверах", + "organizationDescription": "Организуйте ваши учетные данные с помощью папок и тегов", + "enterCredentialName": "Введите название учетных данных", + "enterCredentialDescription": "Введите описание (опционально)", + "enterUsername": "Введите имя пользователя", + "nameIsRequired": "Требуется название учетных данных", + "usernameIsRequired": "Требуется имя пользователя", + "authenticationType": "Тип аутентификации", + "passwordAuthDescription": "Использовать аутентификацию по паролю", + "sshKeyAuthDescription": "Использовать аутентификацию по SSH-ключу", + "passwordIsRequired": "Требуется пароль", + "sshKeyIsRequired": "Требуется SSH-ключ", + "sshKeyType": "Тип SSH-ключа", + "privateKey": "Приватный ключ", + "enterPassword": "Введите пароль", + "enterPrivateKey": "Введите приватный ключ", + "keyPassphrase": "Парольная фраза ключа", + "enterKeyPassphrase": "Введите парольную фразу ключа (опционально)", + "keyPassphraseOptional": "Опционально: оставьте пустым, если у ключа нет парольной фразы", + "leaveEmptyToKeepCurrent": "Оставьте пустым, чтобы сохранить текущее значение", + "uploadKeyFile": "Загрузить файл ключа", + "generateKeyPairButton": "Сгенерировать пару ключей", + "generateKeyPair": "Сгенерировать пару ключей", + "generateKeyPairDescription": "Сгенерировать новую пару SSH-ключей. Если вы хотите защитить ключ парольной фразой, сначала введите ее в поле Пароль ключа ниже.", + "deploySSHKey": "Развернуть SSH-ключ", + "deploySSHKeyDescription": "Развернуть публичный ключ на целевом сервере", + "sourceCredential": "Исходные учетные данные", + "targetHost": "Целевой хост", + "deploymentProcess": "Процесс развертывания", + "deploymentProcessDescription": "Это безопасно добавит публичный ключ в файл ~/.ssh/authorized_keys целевого хоста без перезаписи существующих ключей. Операция обратима.", + "chooseHostToDeploy": "Выберите хост для развертывания...", + "deploying": "Развертывание...", + "name": "Имя", + "noHostsAvailable": "Нет доступных хостов", + "noHostsMatchSearch": "Нет хостов, соответствующих вашему запросу", + "sshKeyGenerationNotImplemented": "Функция генерации SSH-ключей скоро будет доступна", + "connectionTestingNotImplemented": "Функция тестирования подключения скоро будет доступна", + "testConnection": "Тестировать подключение", + "selectOrCreateFolder": "Выбрать или создать папку", + "noFolder": "Без папки", + "orCreateNewFolder": "Или создать новую папку", + "addTag": "Добавить тег", + "saving": "Сохранение...", + "overview": "Обзор", + "security": "Безопасность", + "usage": "Использование", + "securityDetails": "Детали безопасности", + "securityDetailsDescription": "Просмотр зашифрованной информации учетных данных", + "credentialSecured": "Учетные данные защищены", + "credentialSecuredDescription": "Все конфиденциальные данные зашифрованы с помощью AES-256", + "passwordAuthentication": "Аутентификация по паролю", + "keyAuthentication": "Аутентификация по ключу", + "keyType": "Тип ключа", + "securityReminder": "Напоминание о безопасности", + "securityReminderText": "Никогда не передавайте ваши учетные данные. Все данные зашифрованы при хранении.", + "hostsUsingCredential": "Хосты, использующие эти учетные данные", + "noHostsUsingCredential": "В настоящее время эти учетные данные не используются ни на одном хосте", + "timesUsed": "Количество использований", + "lastUsed": "Последнее использование", + "connectedHosts": "Подключенные хосты", + "created": "Создано", + "lastModified": "Последнее изменение", + "usageStatistics": "Статистика использования", + "copiedToClipboard": "{{field}} скопировано в буфер обмена", + "failedToCopy": "Не удалось скопировать в буфер обмена", + "sshKey": "SSH-ключ", + "createCredentialDescription": "Создать новые SSH-учетные данные для безопасного доступа", + "editCredentialDescription": "Обновить информацию об учетных данных", + "listView": "Список", + "folderView": "Папки", + "unknownCredential": "Неизвестно", + "confirmRemoveFromFolder": "Вы уверены, что хотите удалить \"{{name}}\" из папки \"{{folder}}\"? Учетные данные будут перемещены в \"Без категории\".", + "removedFromFolder": "Учетные данные \"{{name}}\" успешно удалены из папки", + "failedToRemoveFromFolder": "Не удалось удалить учетные данные из папки", + "folderRenamed": "Папка \"{{oldName}}\" успешно переименована в \"{{newName}}\"", + "failedToRenameFolder": "Не удалось переименовать папку", + "movedToFolder": "Учетные данные \"{{name}}\" успешно перемещены в \"{{folder}}\"", + "failedToMoveToFolder": "Не удалось переместить учетные данные в папку", + "sshPublicKey": "Публичный SSH-ключ", + "publicKeyNote": "Публичный ключ опционален, но рекомендуется для проверки ключа", + "publicKeyUploaded": "Публичный ключ загружен", + "uploadPublicKey": "Загрузить публичный ключ", + "uploadPrivateKeyFile": "Загрузить файл приватного ключа", + "uploadPublicKeyFile": "Загрузить файл публичного ключа", + "privateKeyRequiredForGeneration": "Для генерации публичного ключа требуется приватный ключ", + "failedToGeneratePublicKey": "Не удалось сгенерировать публичный ключ", + "generatePublicKey": "Сгенерировать из приватного ключа", + "publicKeyGeneratedSuccessfully": "Публичный ключ успешно сгенерирован", + "detectedKeyType": "Обнаруженный тип ключа", + "detectingKeyType": "определение...", + "optional": "Опционально", + "generateKeyPairNew": "Сгенерировать новую пару ключей", + "generateEd25519": "Сгенерировать Ed25519", + "generateECDSA": "Сгенерировать ECDSA", + "generateRSA": "Сгенерировать RSA", + "keyPairGeneratedSuccessfully": "Пара ключей {{keyType}} успешно сгенерирована", + "failedToGenerateKeyPair": "Не удалось сгенерировать пару ключей", + "generateKeyPairNote": "Сгенерировать новую пару SSH-ключей напрямую. Это заменит любые существующие ключи в форме.", + "invalidKey": "Неверный ключ", + "detectionError": "Ошибка определения", + "unknown": "Неизвестно" + }, + "dragIndicator": { + "error": "Ошибка: {{error}}", + "dragging": "Перетаскивание {{fileName}}", + "preparing": "Подготовка {{fileName}}", + "readySingle": "Готово к скачиванию {{fileName}}", + "readyMultiple": "Готово к скачиванию {{count}} файлов", + "batchDrag": "Перетащите {{count}} файлов на рабочий стол", + "dragToDesktop": "Перетащите на рабочий стол", + "canDragAnywhere": "Вы можете перетаскивать файлы в любое место на рабочем столе" + }, + "sshTools": { + "title": "SSH-инструменты", + "closeTools": "Закрыть SSH-инструменты", + "keyRecording": "Запись клавиш", + "startKeyRecording": "Начать запись клавиш", + "stopKeyRecording": "Остановить запись клавиш", + "selectTerminals": "Выберите терминалы:", + "typeCommands": "Введите команды (поддерживаются все клавиши):", + "commandsWillBeSent": "Команды будут отправлены в {{count}} выбранных терминалов.", + "settings": "Настройки", + "enableRightClickCopyPaste": "Включить копирование/вставку по правому клику", + "shareIdeas": "Есть идеи, что должно быть следующим для SSH-инструментов? Поделитесь ими на" + }, + "snippets": { + "title": "Сниппеты", + "new": "Новый сниппет", + "create": "Создать сниппет", + "edit": "Редактировать сниппет", + "run": "Выполнить", + "empty": "Сниппетов пока нет", + "emptyHint": "Создайте сниппет для сохранения часто используемых команд", + "name": "Название", + "description": "Описание", + "content": "Команда", + "namePlaceholder": "например, Перезапуск Nginx", + "descriptionPlaceholder": "Опциональное описание", + "contentPlaceholder": "например, sudo systemctl restart nginx", + "nameRequired": "Требуется название", + "contentRequired": "Требуется команда", + "createDescription": "Создать новый сниппет команды для быстрого выполнения", + "editDescription": "Редактировать этот сниппет команды", + "deleteConfirmTitle": "Удалить сниппет", + "deleteConfirmDescription": "Вы уверены, что хотите удалить \"{{name}}\"?", + "createSuccess": "Сниппет успешно создан", + "updateSuccess": "Сниппет успешно обновлен", + "deleteSuccess": "Сниппет успешно удален", + "createFailed": "Не удалось создать сниппет", + "updateFailed": "Не удалось обновить сниппет", + "deleteFailed": "Не удалось удалить сниппет", + "failedToFetch": "Не удалось загрузить сниппеты", + "executeSuccess": "Выполняется: {{name}}", + "copySuccess": "Сниппет \"{{name}}\" скопирован в буфер обмена", + "runTooltip": "Выполнить этот сниппет в терминале", + "copyTooltip": "Скопировать сниппет в буфер обмена", + "editTooltip": "Редактировать этот сниппет", + "deleteTooltip": "Удалить этот сниппет" + }, + "homepage": { + "loggedInTitle": "Вы вошли в систему!", + "loggedInMessage": "Вы вошли в систему! Используйте боковую панель для доступа ко всем доступным инструментам. Чтобы начать, создайте SSH-хост в разделе SSH-менеджера. После создания вы можете подключиться к этому хосту, используя другие приложения на боковой панели.", + "failedToLoadAlerts": "Не удалось загрузить оповещения", + "failedToDismissAlert": "Не удалось закрыть оповещение" + }, + "serverConfig": { + "title": "Конфигурация сервера", + "description": "Настройте URL сервера Termix для подключения к вашим серверным службам", + "serverUrl": "URL сервера", + "enterServerUrl": "Пожалуйста, введите URL сервера", + "testConnectionFirst": "Пожалуйста, сначала проверьте подключение", + "connectionSuccess": "Подключение успешно!", + "connectionFailed": "Подключение не удалось", + "connectionError": "Произошла ошибка подключения", + "connected": "Подключено", + "disconnected": "Отключено", + "configSaved": "Конфигурация успешно сохранена", + "saveFailed": "Не удалось сохранить конфигурацию", + "saveError": "Ошибка сохранения конфигурации", + "saving": "Сохранение...", + "saveConfig": "Сохранить конфигурацию", + "helpText": "Введите URL, где работает ваш сервер Termix (например, http://localhost:30001 или https://your-server.com)" + }, + "versionCheck": { + "error": "Ошибка проверки версии", + "checkFailed": "Не удалось проверить наличие обновлений", + "upToDate": "Приложение обновлено", + "currentVersion": "Вы используете версию {{version}}", + "updateAvailable": "Доступно обновление", + "newVersionAvailable": "Доступна новая версия! Вы используете {{current}}, но доступна {{latest}}.", + "releasedOn": "Выпущена {{date}}", + "downloadUpdate": "Скачать обновление", + "dismiss": "Закрыть", + "checking": "Проверка обновлений...", + "checkUpdates": "Проверить обновления", + "checkingUpdates": "Проверка обновлений...", + "refresh": "Обновить", + "updateRequired": "Требуется обновление", + "updateDismissed": "Уведомление об обновлении закрыто", + "noUpdatesFound": "Обновления не найдены" + }, + "common": { + "close": "Закрыть", + "minimize": "Свернуть", + "online": "В сети", + "offline": "Не в сети", + "continue": "Продолжить", + "maintenance": "Обслуживание", + "degraded": "Снижена производительность", + "discord": "Discord", + "error": "Ошибка", + "warning": "Предупреждение", + "info": "Информация", + "success": "Успех", + "loading": "Загрузка", + "required": "Обязательно", + "optional": "Опционально", + "clear": "Очистить", + "toggleSidebar": "Переключить боковую панель", + "sidebar": "Боковая панель", + "home": "Главная", + "expired": "Истек", + "expiresToday": "Истекает сегодня", + "expiresTomorrow": "Истекает завтра", + "expiresInDays": "Истекает через {{days}} дней", + "updateAvailable": "Доступно обновление", + "sshPath": "SSH-путь", + "localPath": "Локальный путь", + "loading": "Загрузка...", + "noAuthCredentials": "Нет учетных данных аутентификации для этого SSH-хоста", + "noReleases": "Нет выпусков", + "updatesAndReleases": "Обновления и выпуски", + "newVersionAvailable": "Доступна новая версия ({{version}}).", + "failedToFetchUpdateInfo": "Не удалось загрузить информацию об обновлениях", + "preRelease": "Предварительный выпуск", + "loginFailed": "Ошибка входа", + "noReleasesFound": "Выпуски не найдены.", + "yourBackupCodes": "Ваши резервные коды", + "sendResetCode": "Отправить код сброса", + "verifyCode": "Проверить код", + "resetPassword": "Сбросить пароль", + "resetCode": "Код сброса", + "newPassword": "Новый пароль", + "sshPath": "SSH-путь", + "localPath": "Локальный путь", + "folder": "Папка", + "file": "Файл", + "renamedSuccessfully": "успешно переименован", + "deletedSuccessfully": "успешно удален", + "noAuthCredentials": "Нет учетных данных аутентификации для этого SSH-хоста", + "noTunnelConnections": "Нет настроенных туннельных подключений", + "sshTools": "SSH-инструменты", + "english": "Английский", + "russia": "Русский", + "chinese": "Китайский", + "german": "Немецкий", + "cancel": "Отмена", + "username": "Имя пользователя", + "name": "Имя", + "login": "Войти", + "logout": "Выйти", + "register": "Зарегистрироваться", + "username": "Имя пользователя", + "password": "Пароль", + "version": "Версия", + "confirmPassword": "Подтвердите пароль", + "back": "Назад", + "email": "Email", + "submit": "Отправить", + "cancel": "Отмена", + "change": "Изменить", + "save": "Сохранить", + "delete": "Удалить", + "edit": "Редактировать", + "add": "Добавить", + "search": "Поиск", + "loading": "Загрузка...", + "error": "Ошибка", + "success": "Успех", + "warning": "Предупреждение", + "info": "Информация", + "confirm": "Подтвердить", + "yes": "Да", + "no": "Нет", + "ok": "OK", + "close": "Закрыть", + "enabled": "Включено", + "disabled": "Отключено", + "important": "Важно", + "notEnabled": "Не включено", + "settingUp": "Настройка...", + "back": "Назад", + "next": "Далее", + "previous": "Назад", + "refresh": "Обновить", + "settings": "Настройки", + "profile": "Профиль", + "help": "Помощь", + "about": "О программе", + "language": "Язык", + "autoDetect": "Автоопределение", + "changeAccountPassword": "Изменить пароль вашей учетной записи", + "passwordResetTitle": "Сброс пароля", + "passwordResetDescription": "Вы собираетесь сбросить пароль. Это приведет к выходу из всех активных сеансов.", + "enterSixDigitCode": "Введите 6-значный код из логов docker-контейнера для пользователя:", + "enterNewPassword": "Введите новый пароль для пользователя:", + "passwordsDoNotMatch": "Пароли не совпадают", + "passwordMinLength": "Пароль должен содержать не менее 6 символов", + "passwordResetSuccess": "Пароль успешно сброшен! Теперь вы можете войти с новым паролем.", + "failedToInitiatePasswordReset": "Не удалось инициировать сброс пароля", + "failedToVerifyResetCode": "Не удалось проверить код сброса", + "failedToCompletePasswordReset": "Не удалось завершить сброс пароля", + "documentation": "Документация", + "retry": "Повторить", + "checking": "Проверка...", + "checkingDatabase": "Проверка подключения к базе данных..." + }, + "nav": { + "home": "Главная", + "hosts": "Хосты", + "credentials": "Учетные данные", + "terminal": "Терминал", + "tunnels": "Туннели", + "fileManager": "Файловый менеджер", + "serverStats": "Статистика сервера", + "admin": "Администрирование", + "userProfile": "Профиль пользователя", + "tools": "Инструменты", + "snippets": "Сниппеты", + "newTab": "Новая вкладка", + "splitScreen": "Разделить экран", + "closeTab": "Закрыть вкладку", + "sshManager": "SSH-менеджер", + "hostManager": "Менеджер хостов", + "cannotSplitTab": "Невозможно разделить эту вкладку", + "tabNavigation": "Навигация по вкладкам" + }, + "admin": { + "title": "Настройки администратора", + "oidc": "OIDC", + "users": "Пользователи", + "userManagement": "Управление пользователями", + "makeAdmin": "Сделать администратором", + "removeAdmin": "Убрать администратора", + "deleteUser": "Удалить пользователя", + "allowRegistration": "Разрешить регистрацию", + "oidcSettings": "Настройки OIDC", + "clientId": "Client ID", + "clientSecret": "Client Secret", + "issuerUrl": "Issuer URL", + "authorizationUrl": "Authorization URL", + "tokenUrl": "Token URL", + "updateSettings": "Обновить настройки", + "confirmDelete": "Вы уверены, что хотите удалить этого пользователя?", + "confirmMakeAdmin": "Вы уверены, что хотите сделать этого пользователя администратором?", + "confirmRemoveAdmin": "Вы уверены, что хотите убрать права администратора у этого пользователя?", + "externalAuthentication": "Внешняя аутентификация (OIDC)", + "configureExternalProvider": "Настройте внешнего провайдера идентификации для аутентификации OIDC/OAuth2.", + "userIdentifierPath": "Путь к идентификатору пользователя", + "displayNamePath": "Путь к отображаемому имени", + "scopes": "Области действия", + "saving": "Сохранение...", + "saveConfiguration": "Сохранить конфигурацию", + "reset": "Сбросить", + "success": "Успех", + "loading": "Загрузка...", + "refresh": "Обновить", + "loadingUsers": "Загрузка пользователей...", + "username": "Имя пользователя", + "type": "Тип", + "actions": "Действия", + "external": "Внешний", + "local": "Локальный", + "adminManagement": "Управление администраторами", + "makeUserAdmin": "Сделать пользователя администратором", + "adding": "Добавление...", + "currentAdmins": "Текущие администраторы", + "adminBadge": "Администратор", + "removeAdminButton": "Убрать администратора", + "general": "Общее", + "userRegistration": "Регистрация пользователей", + "allowNewAccountRegistration": "Разрешить регистрацию новых учетных записей", + "allowPasswordLogin": "Разрешить вход по имени пользователя/паролю", + "missingRequiredFields": "Отсутствуют обязательные поля: {{fields}}", + "oidcConfigurationUpdated": "Конфигурация OIDC успешно обновлена!", + "failedToFetchOidcConfig": "Не удалось загрузить конфигурацию OIDC", + "failedToFetchRegistrationStatus": "Не удалось загрузить статус регистрации", + "failedToFetchPasswordLoginStatus": "Не удалось загрузить статус входа по паролю", + "failedToFetchUsers": "Не удалось загрузить пользователей", + "oidcConfigurationDisabled": "Конфигурация OIDC успешно отключена!", + "failedToUpdateOidcConfig": "Не удалось обновить конфигурацию OIDC", + "failedToDisableOidcConfig": "Не удалось отключить конфигурацию OIDC", + "enterUsernameToMakeAdmin": "Введите имя пользователя, чтобы сделать администратором", + "userIsNowAdmin": "Пользователь {{username}} теперь администратор", + "failedToMakeUserAdmin": "Не удалось сделать пользователя администратором", + "removeAdminStatus": "Убрать статус администратора у {{username}}?", + "adminStatusRemoved": "Статус администратора убран у {{username}}", + "failedToRemoveAdminStatus": "Не удалось убрать статус администратора", + "deleteUser": "Удалить пользователя {{username}}? Это нельзя отменить.", + "userDeletedSuccessfully": "Пользователь {{username}} успешно удален", + "failedToDeleteUser": "Не удалось удалить пользователя", + "overrideUserInfoUrl": "Переопределить User Info URL (не требуется)", + "databaseSecurity": "Безопасность базы данных", + "encryptionStatus": "Статус шифрования", + "encryptionEnabled": "Шифрование включено", + "enabled": "Включено", + "disabled": "Отключено", + "keyId": "ID ключа", + "created": "Создано", + "migrationStatus": "Статус миграции", + "migrationCompleted": "Миграция завершена", + "migrationRequired": "Требуется миграция", + "deviceProtectedMasterKey": "Мастер-ключ, защищенный средой", + "legacyKeyStorage": "Устаревшее хранилище ключей", + "masterKeyEncryptedWithDeviceFingerprint": "Мастер-ключ зашифрован с помощью отпечатка среды (защита KEK активна)", + "keyNotProtectedByDeviceBinding": "Ключ не защищен привязкой к среде (рекомендуется обновление)", + "valid": "Действителен", + "initializeDatabaseEncryption": "Инициализировать шифрование базы данных", + "enableAes256EncryptionWithDeviceBinding": "Включить AES-256 шифрование с защитой мастер-ключа, привязанного к среде. Это создает безопасность корпоративного уровня для SSH-ключей, паролей и токенов аутентификации.", + "featuresEnabled": "Включенные функции:", + "aes256GcmAuthenticatedEncryption": "Аутентифицированное шифрование AES-256-GCM", + "deviceFingerprintMasterKeyProtection": "Защита мастер-ключа отпечатком среды (KEK)", + "pbkdf2KeyDerivation": "Производство ключей PBKDF2 с 100K итерациями", + "automaticKeyManagement": "Автоматическое управление ключами и их ротация", + "initializing": "Инициализация...", + "initializeEnterpriseEncryption": "Инициализировать корпоративное шифрование", + "migrateExistingData": "Мигрировать существующие данные", + "encryptExistingUnprotectedData": "Зашифровать существующие незащищенные данные в вашей базе данных. Этот процесс безопасен и создает автоматические резервные копии.", + "testMigrationDryRun": "Проверить совместимость шифрования", + "migrating": "Миграция...", + "migrateData": "Мигрировать данные", + "securityInformation": "Информация о безопасности", + "sshPrivateKeysEncryptedWithAes256": "SSH-приватные ключи и пароли зашифрованы с помощью AES-256-GCM", + "userAuthTokensProtected": "Токены аутентификации пользователей и секреты 2FA защищены", + "masterKeysProtectedByDeviceFingerprint": "Мастер-ключи шифрования защищены отпечатком устройства (KEK)", + "keysBoundToServerInstance": "Ключи привязаны к текущей серверной среде (мигрируемы через переменные окружения)", + "pbkdf2HkdfKeyDerivation": "Производство ключей PBKDF2 + HKDF с 100K итерациями", + "backwardCompatibleMigration": "Все данные остаются обратно совместимыми во время миграции", + "enterpriseGradeSecurityActive": "Безопасность корпоративного уровня активна", + "masterKeysProtectedByDeviceBinding": "Ваши мастер-ключи шифрования защищены отпечатком среды. Это использует имя хоста сервера, пути и другую информацию о среде для генерации ключей защиты. Для миграции серверов установите переменную окружения DB_ENCRYPTION_KEY на новом сервере.", + "important": "Важно", + "keepEncryptionKeysSecure": "Обеспечьте безопасность данных: регулярно создавайте резервные копии файлов базы данных и конфигурации сервера. Для миграции на новый сервер установите переменную окружения DB_ENCRYPTION_KEY в новой среде или сохраните то же имя хоста и структуру каталогов.", + "loadingEncryptionStatus": "Загрузка статуса шифрования...", + "testMigrationDescription": "Проверить, что существующие данные могут быть безопасно мигрированы в зашифрованный формат без фактического изменения каких-либо данных", + "serverMigrationGuide": "Руководство по миграции сервера", + "migrationInstructions": "Для миграции зашифрованных данных на новый сервер: 1) Создайте резервную копию файлов базы данных, 2) Установите переменную окружения DB_ENCRYPTION_KEY=\"your-key\" на новом сервере, 3) Восстановите файлы базы данных", + "environmentProtection": "Защита среды", + "environmentProtectionDesc": "Защищает ключи шифрования на основе информации о серверной среде (имя хоста, пути и т.д.), мигрируемы через переменные окружения", + "verificationCompleted": "Проверка совместимости завершена - данные не изменялись", + "verificationInProgress": "Проверка завершена", + "dataMigrationCompleted": "Миграция данных успешно завершена!", + "migrationCompleted": "Миграция завершена", + "verificationFailed": "Проверка совместимости не удалась", + "migrationFailed": "Миграция не удалась", + "runningVerification": "Выполняется проверка совместимости...", + "startingMigration": "Начало миграции...", + "hardwareFingerprintSecurity": "Безопасность отпечатка оборудования", + "hardwareBoundEncryption": "Активно шифрование, привязанное к оборудованию", + "masterKeysNowProtectedByHardwareFingerprint": "Мастер-ключи теперь защищены реальным отпечатком оборудования вместо переменных окружения", + "cpuSerialNumberDetection": "Обнаружение серийного номера CPU", + "motherboardUuidIdentification": "Идентификация UUID материнской платы", + "diskSerialNumberVerification": "Проверка серийного номера диска", + "biosSerialNumberCheck": "Проверка серийного номера BIOS", + "stableMacAddressFiltering": "Фильтрация стабильных MAC-адресов", + "databaseFileEncryption": "Шифрование файлов базы данных", + "dualLayerProtection": "Активна двухуровневая защита", + "bothFieldAndFileEncryptionActive": "Теперь активны как полевое, так и файловое шифрование для максимальной безопасности", + "fieldLevelAes256Encryption": "Полевое шифрование AES-256 для конфиденциальных данных", + "fileLevelDatabaseEncryption": "Файловое шифрование базы данных с привязкой к оборудованию", + "hardwareBoundFileKeys": "Ключи шифрования файлов, привязанные к оборудованию", + "automaticEncryptedBackups": "Автоматическое создание зашифрованных резервных копий", + "createEncryptedBackup": "Создать зашифрованную резервную копию", + "creatingBackup": "Создание резервной копии...", + "backupCreated": "Резервная копия создана", + "encryptedBackupCreatedSuccessfully": "Зашифрованная резервная копия успешно создана", + "backupCreationFailed": "Не удалось создать резервную копию", + "databaseMigration": "Миграция базы данных", + "exportForMigration": "Экспорт для миграции", + "exportDatabaseForHardwareMigration": "Экспортировать базу данных как файл SQLite с расшифрованными данными для миграции на новое оборудование", + "exportDatabase": "Экспортировать базу данных SQLite", + "exporting": "Экспорт...", + "exportCreated": "Экспорт SQLite создан", + "exportContainsDecryptedData": "Экспорт SQLite содержит расшифрованные данные - храните безопасно!", + "databaseExportedSuccessfully": "База данных SQLite успешно экспортирована", + "databaseExportFailed": "Не удалось экспортировать базу данных SQLite", + "importFromMigration": "Импорт из миграции", + "importDatabaseFromAnotherSystem": "Импортировать базу данных SQLite из другой системы или оборудования", + "importDatabase": "Импортировать базу данных SQLite", + "importing": "Импорт...", + "selectedFile": "Выбранный файл SQLite", + "importWillReplaceExistingData": "Импорт SQLite заменит существующие данные - рекомендуется резервное копирование!", + "pleaseSelectImportFile": "Пожалуйста, выберите файл для импорта SQLite", + "databaseImportedSuccessfully": "База данных SQLite успешно импортирована", + "databaseImportFailed": "Не удалось импортировать базу данных SQLite", + "manageEncryptionAndBackups": "Управление ключами шифрования, безопасностью базы данных и операциями резервного копирования", + "activeSecurityFeatures": "Текущие активные меры безопасности и защиты", + "deviceBindingTechnology": "Продвинутая технология защиты ключей на основе оборудования", + "backupAndRecovery": "Безопасное создание резервных копий и восстановление базы данных", + "crossSystemDataTransfer": "Экспорт и импорт баз данных между разными системами", + "noMigrationNeeded": "Миграция не требуется", + "encryptionKey": "Ключ шифрования", + "keyProtection": "Защита ключа", + "active": "Активно", + "legacy": "Устаревшее", + "dataStatus": "Статус данных", + "encrypted": "Зашифровано", + "needsMigration": "Требуется миграция", + "ready": "Готово", + "initializeEncryption": "Инициализировать шифрование", + "initialize": "Инициализировать", + "test": "Тест", + "migrate": "Мигрировать", + "backup": "Резервная копия", + "createBackup": "Создать резервную копию", + "exportImport": "Экспорт/Импорт", + "export": "Экспорт", + "import": "Импорт", + "passwordRequired": "Требуется пароль", + "confirmExport": "Подтвердить экспорт", + "exportDescription": "Экспортировать SSH-хосты и учетные данные как файл SQLite", + "importDescription": "Импортировать файл SQLite с инкрементным слиянием (пропускает дубликаты)", + "criticalWarning": "Критическое предупреждение", + "cannotDisablePasswordLoginWithoutOIDC": "Невозможно отключить вход по паролю без настройки OIDC! Вы должны настроить аутентификацию OIDC перед отключением входа по паролю, иначе вы потеряете доступ к Termix.", + "confirmDisablePasswordLogin": "Вы уверены, что хотите отключить вход по паролю? Убедитесь, что OIDC правильно настроен и работает, прежде чем продолжить, иначе вы потеряете доступ к вашему экземпляру Termix.", + "passwordLoginDisabled": "Вход по паролю успешно отключен", + "passwordLoginAndRegistrationDisabled": "Вход по паролю и регистрация новых учетных записей успешно отключены", + "requiresPasswordLogin": "Требуется включенный вход по паролю", + "passwordLoginDisabledWarning": "Вход по паролю отключен. Убедитесь, что OIDC правильно настроен, иначе вы не сможете войти в Termix.", + "oidcRequiredWarning": "КРИТИЧЕСКИ: Вход по паролю отключен. Если вы сбросите или неправильно настроите OIDC, вы потеряете весь доступ к Termix и заблокируете свой экземпляр. Продолжайте только если вы абсолютно уверены.", + "confirmDisableOIDCWarning": "ПРЕДУПРЕЖДЕНИЕ: Вы собираетесь отключить OIDC, пока вход по паролю также отключен. Это заблокирует ваш экземпляр Termix, и вы потеряете весь доступ. Вы абсолютно уверены, что хотите продолжить?" + }, + "hosts": { + "title": "Менеджер хостов", + "sshHosts": "SSH-хосты", + "noHosts": "Нет SSH-хостов", + "noHostsMessage": "Вы еще не добавили SSH-хосты. Нажмите \"Добавить хост\", чтобы начать.", + "loadingHosts": "Загрузка хостов...", + "failedToLoadHosts": "Не удалось загрузить хосты", + "retry": "Повторить", + "refresh": "Обновить", + "hostsCount": "{{count}} хостов", + "importJson": "Импорт JSON", + "importing": "Импорт...", + "importJsonTitle": "Импорт SSH-хостов из JSON", + "importJsonDesc": "Загрузите JSON-файл для массового импорта нескольких SSH-хостов (макс. 100).", + "downloadSample": "Скачать образец", + "formatGuide": "Руководство по формату", + "exportCredentialWarning": "Предупреждение: Хост \"{{name}}\" использует аутентификацию по учетным данным. Экспортируемый файл не будет включать данные учетных данных, и их нужно будет вручную перенастроить после импорта. Вы хотите продолжить?", + "exportSensitiveDataWarning": "Предупреждение: Хост \"{{name}}\" содержит конфиденциальные данные аутентификации (пароль/SSH-ключ). Экспортируемый файл будет включать эти данные в открытом виде. Пожалуйста, храните файл в безопасности и удалите его после использования. Вы хотите продолжить?", + "uncategorized": "Без категории", + "confirmDelete": "Вы уверены, что хотите удалить \"{{name}}\"?", + "failedToDeleteHost": "Не удалось удалить хост", + "failedToExportHost": "Не удалось экспортировать хост. Пожалуйста, убедитесь, что вы вошли в систему и имеете доступ к данным хоста.", + "jsonMustContainHosts": "JSON должен содержать массив \"hosts\" или быть массивом хостов", + "noHostsInJson": "В JSON-файле не найдено хостов", + "maxHostsAllowed": "Разрешено максимум 100 хостов за импорт", + "importCompleted": "Импорт завершен: {{success}} успешно, {{failed}} не удалось", + "importFailed": "Импорт не удался", + "importError": "Ошибка импорта", + "failedToImportJson": "Не удалось импортировать JSON-файл", + "connectionDetails": "Детали подключения", + "organization": "Организация", + "ipAddress": "IP-адрес", + "port": "Порт", + "name": "Имя", + "username": "Имя пользователя", + "folder": "Папка", + "tags": "Теги", + "pin": "Закрепить", + "passwordRequired": "Пароль требуется при использовании аутентификации по паролю", + "sshKeyRequired": "Приватный SSH-ключ требуется при использовании аутентификации по ключу", + "keyTypeRequired": "Тип ключа требуется при использовании аутентификации по ключу", + "mustSelectValidSshConfig": "Необходимо выбрать допустимую SSH-конфигурацию из списка", + "addHost": "Добавить хост", + "editHost": "Редактировать хост", + "cloneHost": "Клонировать хост", + "updateHost": "Обновить хост", + "hostUpdatedSuccessfully": "Хост \"{{name}}\" успешно обновлен!", + "hostAddedSuccessfully": "Хост \"{{name}}\" успешно добавлен!", + "hostDeletedSuccessfully": "Хост \"{{name}}\" успешно удален!", + "failedToSaveHost": "Не удалось сохранить хост. Пожалуйста, попробуйте снова.", + "enableTerminal": "Включить терминал", + "enableTerminalDesc": "Включить/отключить видимость хоста во вкладке Терминал", + "enableTunnel": "Включить туннель", + "enableTunnelDesc": "Включить/отключить видимость хоста во вкладке Туннель", + "enableFileManager": "Включить файловый менеджер", + "enableFileManagerDesc": "Включить/отключить видимость хоста во вкладке Файловый менеджер", + "defaultPath": "Путь по умолчанию", + "defaultPathDesc": "Каталог по умолчанию при открытии файлового менеджера для этого хоста", + "tunnelConnections": "Туннельные подключения", + "connection": "Подключение", + "remove": "Удалить", + "sourcePort": "Исходный порт", + "sourcePortDesc": " (Источник относится к Текущим деталям подключения во вкладке Общее)", + "endpointPort": "Порт конечной точки", + "endpointSshConfig": "SSH-конфигурация конечной точки", + "tunnelForwardDescription": "Этот туннель будет перенаправлять трафик с порта {{sourcePort}} на исходной машине (текущие детали подключения во вкладке общее) на порт {{endpointPort}} на машине конечной точки.", + "maxRetries": "Макс. попыток", + "maxRetriesDescription": "Максимальное количество попыток повторного подключения туннеля.", + "retryInterval": "Интервал повтора (секунды)", + "retryIntervalDescription": "Время ожидания между попытками повторного подключения.", + "autoStartContainer": "Автозапуск при запуске контейнера", + "autoStartDesc": "Автоматически запускать этот туннель при запуске контейнера", + "addConnection": "Добавить туннельное подключение", + "sshpassRequired": "Требуется Sshpass для аутентификации по паролю", + "sshpassRequiredDesc": "Для аутентификации по паролю в туннелях, sshpass должен быть установлен в системе.", + "otherInstallMethods": "Другие способы установки:", + "debianUbuntuEquivalent": "(Debian/Ubuntu) или эквивалент для вашей ОС.", + "or": "или", + "centosRhelFedora": "CentOS/RHEL/Fedora", + "macos": "macOS", + "windows": "Windows", + "sshServerConfigRequired": "Требуется конфигурация SSH-сервера", + "sshServerConfigDesc": "Для туннельных подключений SSH-сервер должен быть настроен для разрешения переадресации портов:", + "gatewayPortsYes": "для привязки удаленных портов ко всем интерфейсам", + "allowTcpForwardingYes": "для включения переадресации портов", + "permitRootLoginYes": "если используется пользователь root для туннелирования", + "editSshConfig": "Отредактируйте /etc/ssh/sshd_config и перезапустите SSH: sudo systemctl restart sshd", + "upload": "Загрузить", + "authentication": "Аутентификация", + "password": "Пароль", + "key": "Ключ", + "credential": "Учетные данные", + "none": "Нет", + "selectCredential": "Выбрать учетные данные", + "selectCredentialPlaceholder": "Выберите учетные данные...", + "credentialRequired": "Учетные данные требуются при использовании аутентификации по учетным данным", + "credentialDescription": "Выбор учетных данных перезапишет текущее имя пользователя и будет использовать детали аутентификации учетных данных.", + "sshPrivateKey": "Приватный SSH-ключ", + "keyPassword": "Пароль ключа", + "keyType": "Тип ключа", + "autoDetect": "Автоопределение", + "rsa": "RSA", + "ed25519": "ED25519", + "ecdsaNistP256": "ECDSA NIST P-256", + "ecdsaNistP384": "ECDSA NIST P-384", + "ecdsaNistP521": "ECDSA NIST P-521", + "dsa": "DSA", + "rsaSha2256": "RSA SHA2-256", + "rsaSha2512": "RSA SHA2-512", + "uploadFile": "Загрузить файл", + "pasteKey": "Вставить ключ", + "updateKey": "Обновить ключ", + "existingKey": "Существующий ключ (нажмите для изменения)", + "existingCredential": "Существующие учетные данные (нажмите для изменения)", + "addTagsSpaceToAdd": "добавить теги (пробел для добавления)", + "terminalBadge": "Терминал", + "tunnelBadge": "Туннель", + "fileManagerBadge": "Файловый менеджер", + "general": "Общее", + "terminal": "Терминал", + "tunnel": "Туннель", + "fileManager": "Файловый менеджер", + "serverStats": "Статистика сервера", + "hostViewer": "Просмотрщик хостов", + "enableServerStats": "Включить статистику сервера", + "enableServerStatsDesc": "Включить/отключить сбор статистики сервера для этого хоста", + "displayItems": "Элементы отображения", + "displayItemsDesc": "Выберите, какие метрики отображать на странице статистики сервера", + "enableCpu": "Использование CPU", + "enableMemory": "Использование памяти", + "enableDisk": "Использование диска", + "enableNetwork": "Сетевая статистика (Скоро)", + "enableProcesses": "Количество процессов (Скоро)", + "enableUptime": "Время работы (Скоро)", + "enableHostname": "Имя хоста (Скоро)", + "enableOs": "Операционная система (Скоро)", + "customCommands": "Пользовательские команды (Скоро)", + "customCommandsDesc": "Определите пользовательские команды выключения и перезагрузки для этого сервера", + "shutdownCommand": "Команда выключения", + "rebootCommand": "Команда перезагрузки", + "confirmRemoveFromFolder": "Вы уверены, что хотите удалить \"{{name}}\" из папки \"{{folder}}\"? Хост будет перемещен в \"Без папки\".", + "removedFromFolder": "Хост \"{{name}}\" успешно удален из папки", + "failedToRemoveFromFolder": "Не удалось удалить хост из папки", + "folderRenamed": "Папка \"{{oldName}}\" успешно переименована в \"{{newName}}\"", + "failedToRenameFolder": "Не удалось переименовать папку", + "movedToFolder": "Хост \"{{name}}\" успешно перемещен в \"{{folder}}\"", + "failedToMoveToFolder": "Не удалось переместить хост в папку", + "statistics": "Статистика", + "enabledWidgets": "Включенные виджеты", + "enabledWidgetsDesc": "Выберите, какие виджеты статистики отображать для этого хоста", + "monitoringConfiguration": "Конфигурация мониторинга", + "monitoringConfigurationDesc": "Настройте, как часто проверяются статистика и статус сервера", + "statusCheckEnabled": "Включить мониторинг статуса", + "statusCheckEnabledDesc": "Проверять, находится ли сервер в сети или вне сети", + "statusCheckInterval": "Интервал проверки статуса", + "statusCheckIntervalDesc": "Как часто проверять, находится ли хост в сети (5с - 1ч)", + "metricsEnabled": "Включить мониторинг метрик", + "metricsEnabledDesc": "Собирать статистику CPU, RAM, диска и другую системную статистику", + "metricsInterval": "Интервал сбора метрик", + "metricsIntervalDesc": "Как часто собирать статистику сервера (5с - 1ч)", + "intervalSeconds": "секунд", + "intervalMinutes": "минут", + "intervalValidation": "Интервалы мониторинга должны быть между 5 секундами и 1 часом (3600 секунд)", + "monitoringDisabled": "Мониторинг сервера отключен для этого хоста", + "enableMonitoring": "Включите мониторинг в Менеджере хостов → вкладка Статистика", + "monitoringDisabledBadge": "Мониторинг выключен", + "statusMonitoring": "Статус", + "metricsMonitoring": "Метрики", + "terminalCustomizationNotice": "Примечание: Настройки терминала работают только на рабочем столе (веб-сайт и Electron-приложение). Мобильные приложения и мобильный веб-сайт используют системные настройки терминала по умолчанию.", + "noneAuthTitle": "Интерактивная аутентификация по клавиатуре", + "noneAuthDescription": "Этот метод аутентификации будет использовать интерактивную аутентификацию по клавиатуре при подключении к SSH-серверу.", + "noneAuthDetails": "Интерактивная аутентификация по клавиатуре позволяет серверу запрашивать у вас учетные данные во время подключения. Это полезно для серверов, которые требуют многофакторную аутентификацию или динамический ввод пароля." + }, + "terminal": { + "title": "Терминал", + "connect": "Подключиться к хосту", + "disconnect": "Отключиться", + "clear": "Очистить", + "copy": "Копировать", + "paste": "Вставить", + "find": "Найти", + "fullscreen": "Полный экран", + "splitHorizontal": "Разделить горизонтально", + "splitVertical": "Разделить вертикально", + "closePanel": "Закрыть панель", + "reconnect": "Переподключиться", + "sessionEnded": "Сеанс завершен", + "connectionLost": "Подключение потеряно", + "error": "ОШИБКА: {{message}}", + "disconnected": "Отключено", + "connectionClosed": "Подключение закрыто", + "connectionError": "Ошибка подключения: {{message}}", + "connected": "Подключено", + "sshConnected": "SSH-подключение установлено", + "authError": "Ошибка аутентификации: {{message}}", + "unknownError": "Произошла неизвестная ошибка", + "messageParseError": "Не удалось разобрать сообщение сервера", + "websocketError": "Ошибка подключения WebSocket", + "connecting": "Подключение...", + "reconnecting": "Переподключение... ({{attempt}}/{{max}})", + "reconnected": "Успешно переподключено", + "maxReconnectAttemptsReached": "Достигнуто максимальное количество попыток переподключения", + "connectionTimeout": "Таймаут подключения", + "terminalTitle": "Терминал - {{host}}", + "terminalWithPath": "Терминал - {{host}}:{{path}}", + "runTitle": "Выполнение {{command}} - {{host}}", + "totpRequired": "Требуется двухфакторная аутентификация", + "totpCodeLabel": "Код проверки", + "totpPlaceholder": "000000", + "totpVerify": "Проверить" + }, + "fileManager": { + "title": "Файловый менеджер", + "file": "Файл", + "folder": "Папка", + "connectToSsh": "Подключитесь к SSH для использования файловых операций", + "uploadFile": "Загрузить файл", + "downloadFile": "Скачать", + "edit": "Редактировать", + "preview": "Просмотр", + "previous": "Предыдущий", + "next": "Следующий", + "pageXOfY": "Страница {{current}} из {{total}}", + "zoomOut": "Уменьшить", + "zoomIn": "Увеличить", + "newFile": "Новый файл", + "newFolder": "Новая папка", + "rename": "Переименовать", + "renameItem": "Переименовать элемент", + "deleteItem": "Удалить элемент", + "currentPath": "Текущий путь", + "uploadFileTitle": "Загрузить файл", + "maxFileSize": "Макс: 1GB (JSON) / 5GB (Binary) - Поддерживаются большие файлы", + "removeFile": "Удалить файл", + "clickToSelectFile": "Нажмите для выбора файла", + "chooseFile": "Выбрать файл", + "uploading": "Загрузка...", + "downloading": "Скачивание...", + "uploadingFile": "Загрузка {{name}}...", + "uploadingLargeFile": "Загрузка большого файла {{name}} ({{size}})...", + "downloadingFile": "Скачивание {{name}}...", + "creatingFile": "Создание {{name}}...", + "creatingFolder": "Создание {{name}}...", + "deletingItem": "Удаление {{type}} {{name}}...", + "renamingItem": "Переименование {{type}} {{oldName}} в {{newName}}...", + "createNewFile": "Создать новый файл", + "fileName": "Имя файла", + "creating": "Создание...", + "createFile": "Создать файл", + "createNewFolder": "Создать новую папку", + "folderName": "Имя папки", + "createFolder": "Создать папку", + "warningCannotUndo": "Предупреждение: Это действие нельзя отменить", + "itemPath": "Путь к элементу", + "thisIsDirectory": "Это каталог (будет удален рекурсивно)", + "deleting": "Удаление...", + "currentPathLabel": "Текущий путь", + "newName": "Новое имя", + "thisIsDirectoryRename": "Это каталог", + "renaming": "Переименование...", + "fileUploadedSuccessfully": "Файл \"{{name}}\" успешно загружен", + "failedToUploadFile": "Не удалось загрузить файл", + "fileDownloadedSuccessfully": "Файл \"{{name}}\" успешно скачан", + "failedToDownloadFile": "Не удалось скачать файл", + "noFileContent": "Содержимое файла не получено", + "filePath": "Путь к файлу", + "fileCreatedSuccessfully": "Файл \"{{name}}\" успешно создан", + "failedToCreateFile": "Не удалось создать файл", + "folderCreatedSuccessfully": "Папка \"{{name}}\" успешно создана", + "failedToCreateFolder": "Не удалось создать папку", + "failedToCreateItem": "Не удалось создать элемент", + "operationFailed": "Операция {{operation}} не удалась для {{name}}: {{error}}", + "failedToResolveSymlink": "Не удалось разрешить символическую ссылку", + "itemDeletedSuccessfully": "{{type}} успешно удален", + "itemsDeletedSuccessfully": "{{count}} элементов успешно удалено", + "failedToDeleteItems": "Не удалось удалить элементы", + "dragFilesToUpload": "Перетащите файлы сюда для загрузки", + "emptyFolder": "Эта папка пуста", + "itemCount": "{{count}} элементов", + "selectedCount": "{{count}} выбрано", + "searchFiles": "Поиск файлов...", + "upload": "Загрузить", + "selectHostToStart": "Выберите хост для начала управления файлами", + "failedToConnect": "Не удалось подключиться к SSH", + "failedToLoadDirectory": "Не удалось загрузить каталог", + "noSSHConnection": "Нет доступного SSH-подключения", + "enterFolderName": "Введите имя папки:", + "enterFileName": "Введите имя файла:", + "copy": "Копировать", + "cut": "Вырезать", + "paste": "Вставить", + "delete": "Удалить", + "properties": "Свойства", + "preview": "Просмотр", + "refresh": "Обновить", + "downloadFiles": "Скачать {{count}} файлов в браузер", + "copyFiles": "Копировать {{count}} элементов", + "cutFiles": "Вырезать {{count}} элементов", + "deleteFiles": "Удалить {{count}} элементов", + "filesCopiedToClipboard": "{{count}} элементов скопировано в буфер обмена", + "filesCutToClipboard": "{{count}} элементов вырезано в буфер обмена", + "movedItems": "Перемещено {{count}} элементов", + "failedToDeleteItem": "Не удалось удалить элемент", + "itemRenamedSuccessfully": "{{type}} успешно переименован", + "failedToRenameItem": "Не удалось переименовать элемент", + "upload": "Загрузить", + "download": "Скачать", + "newFile": "Новый файл", + "newFolder": "Новая папка", + "rename": "Переименовать", + "delete": "Удалить", + "permissions": "Права доступа", + "size": "Размер", + "modified": "Изменен", + "path": "Путь", + "fileName": "Имя файла", + "folderName": "Имя папки", + "confirmDelete": "Вы уверены, что хотите удалить {{name}}?", + "uploadSuccess": "Файл успешно загружен", + "uploadFailed": "Не удалось загрузить файл", + "downloadSuccess": "Файл успешно скачан", + "downloadFailed": "Не удалось скачать файл", + "permissionDenied": "Доступ запрещен", + "checkDockerLogs": "Проверьте логи Docker для получения подробной информации об ошибке", + "internalServerError": "Произошла внутренняя ошибка сервера", + "serverError": "Ошибка сервера", + "error": "Ошибка", + "requestFailed": "Запрос завершился с кодом состояния", + "unknownFileError": "неизвестно", + "cannotReadFile": "Невозможно прочитать файл", + "noSshSessionId": "Нет доступного ID SSH-сессии", + "noFilePath": "Нет доступного пути к файлу", + "noCurrentHost": "Нет текущего хоста", + "fileSavedSuccessfully": "Файл успешно сохранен", + "saveTimeout": "Операция сохранения превысила время ожидания. Файл мог быть успешно сохранен, но операция заняла слишком много времени для завершения. Проверьте логи Docker для подтверждения.", + "failedToSaveFile": "Не удалось сохранить файл", + "folder": "Папка", + "file": "Файл", + "deletedSuccessfully": "успешно удален", + "failedToDeleteItem": "Не удалось удалить элемент", + "connectToServer": "Подключиться к серверу", + "selectServerToEdit": "Выберите сервер на боковой панели, чтобы начать редактирование файлов", + "fileOperations": "Файловые операции", + "confirmDeleteMessage": "Вы уверены, что хотите удалить {{name}}?", + "confirmDeleteSingleItem": "Вы уверены, что хотите окончательно удалить \"{{name}}\"?", + "confirmDeleteMultipleItems": "Вы уверены, что хотите окончательно удалить {{count}} элементов?", + "confirmDeleteMultipleItemsWithFolders": "Вы уверены, что хотите окончательно удалить {{count}} элементов? Это включает папки и их содержимое.", + "confirmDeleteFolder": "Вы уверены, что хотите окончательно удалить папку \"{{name}}\" и все ее содержимое?", + "deleteDirectoryWarning": "Это удалит папку и все ее содержимое.", + "actionCannotBeUndone": "Это действие нельзя отменить.", + "permanentDeleteWarning": "Это действие нельзя отменить. Элемент(ы) будут окончательно удалены с сервера.", + "recent": "Недавние", + "pinned": "Закрепленные", + "folderShortcuts": "Ярлыки папок", + "noRecentFiles": "Нет недавних файлов.", + "noPinnedFiles": "Нет закрепленных файлов.", + "enterFolderPath": "Введите путь к папке", + "noShortcuts": "Нет ярлыков.", + "searchFilesAndFolders": "Поиск файлов и папок...", + "noFilesOrFoldersFound": "Файлы или папки не найдены.", + "failedToConnectSSH": "Не удалось подключиться к SSH", + "failedToReconnectSSH": "Не удалось переподключить SSH-сессию", + "failedToListFiles": "Не удалось получить список файлов", + "fetchHomeDataTimeout": "Получение домашних данных превысило время ожидания", + "sshStatusCheckTimeout": "Проверка статуса SSH превысила время ожидания", + "sshReconnectionTimeout": "Переподключение SSH превысило время ожидания", + "saveOperationTimeout": "Операция сохранения превысила время ожидания", + "cannotSaveFile": "Невозможно сохранить файл", + "dragSystemFilesToUpload": "Перетащите системные файлы сюда для загрузки", + "dragFilesToWindowToDownload": "Перетащите файлы за пределы окна для скачивания", + "openTerminalHere": "Открыть терминал здесь", + "run": "Выполнить", + "saveToSystem": "Сохранить как...", + "selectLocationToSave": "Выберите место для сохранения", + "openTerminalInFolder": "Открыть терминал в этой папке", + "openTerminalInFileLocation": "Открыть терминал в расположении файла", + "terminalWithPath": "Терминал - {{host}}:{{path}}", + "runningFile": "Выполнение - {{file}}", + "onlyRunExecutableFiles": "Можно выполнять только исполняемые файлы", + "noHostSelected": "Хост не выбран", + "starred": "Избранное", + "shortcuts": "Ярлыки", + "directories": "Каталоги", + "removedFromRecentFiles": "Удалено \"{{name}}\" из недавних файлов", + "removeFailed": "Удаление не удалось", + "unpinnedSuccessfully": "Откреплено \"{{name}}\" успешно", + "unpinFailed": "Открепление не удалось", + "removedShortcut": "Удален ярлык \"{{name}}\"", + "removeShortcutFailed": "Не удалось удалить ярлык", + "clearedAllRecentFiles": "Все недавние файлы очищены", + "clearFailed": "Очистка не удалась", + "removeFromRecentFiles": "Удалить из недавних файлов", + "clearAllRecentFiles": "Очистить все недавние файлы", + "unpinFile": "Открепить файл", + "removeShortcut": "Удалить ярлык", + "saveFilesToSystem": "Сохранить {{count}} файлов как...", + "saveToSystem": "Сохранить как...", + "pinFile": "Закрепить файл", + "addToShortcuts": "Добавить в ярлыки", + "selectLocationToSave": "Выберите место для сохранения", + "downloadToDefaultLocation": "Скачать в место по умолчанию", + "pasteFailed": "Вставка не удалась", + "noUndoableActions": "Нет действий для отмены", + "undoCopySuccess": "Операция копирования отменена: Удалено {{count}} скопированных файлов", + "undoCopyFailedDelete": "Отмена не удалась: Не удалось удалить скопированные файлы", + "undoCopyFailedNoInfo": "Отмена не удалась: Не удалось найти информацию о скопированных файлах", + "undoMoveSuccess": "Операция перемещения отменена: Перемещено {{count}} файлов обратно в исходное расположение", + "undoMoveFailedMove": "Отмена не удалась: Не удалось переместить файлы обратно", + "undoMoveFailedNoInfo": "Отмена не удалась: Не удалось найти информацию о перемещенных файлах", + "undoDeleteNotSupported": "Операцию удаления нельзя отменить: Файлы окончательно удалены с сервера", + "undoTypeNotSupported": "Неподдерживаемый тип операции отмены", + "undoOperationFailed": "Операция отмены не удалась", + "unknownError": "Неизвестная ошибка", + "enterPath": "Введите путь...", + "editPath": "Редактировать путь", + "confirm": "Подтвердить", + "cancel": "Отмена", + "folderName": "Имя папки", + "find": "Найти...", + "replaceWith": "Заменить на...", + "replace": "Заменить", + "replaceAll": "Заменить все", + "downloadInstead": "Скачать вместо этого", + "keyboardShortcuts": "Горячие клавиши", + "searchAndReplace": "Поиск и замена", + "editing": "Редактирование", + "navigation": "Навигация", + "code": "Код", + "search": "Поиск", + "findNext": "Найти следующее", + "findPrevious": "Найти предыдущее", + "save": "Сохранить", + "selectAll": "Выделить все", + "undo": "Отменить", + "redo": "Повторить", + "goToLine": "Перейти к строке", + "moveLineUp": "Переместить строку вверх", + "moveLineDown": "Переместить строку вниз", + "toggleComment": "Закомментировать/раскомментировать", + "indent": "Увеличить отступ", + "outdent": "Уменьшить отступ", + "autoComplete": "Автозавершение", + "imageLoadError": "Не удалось загрузить изображение", + "zoomIn": "Увеличить", + "zoomOut": "Уменьшить", + "rotate": "Повернуть", + "originalSize": "Оригинальный размер", + "startTyping": "Начните печатать...", + "unknownSize": "Неизвестный размер", + "fileIsEmpty": "Файл пуст", + "modified": "Изменен", + "largeFileWarning": "Предупреждение о большом файле", + "largeFileWarningDesc": "Этот файл имеет размер {{size}}, что может вызвать проблемы с производительностью при открытии как текста.", + "fileNotFoundAndRemoved": "Файл \"{{name}}\" не найден и был удален из недавних/закрепленных файлов", + "failedToLoadFile": "Не удалось загрузить файл: {{error}}", + "serverErrorOccurred": "Произошла ошибка сервера. Пожалуйста, попробуйте позже.", + "fileSavedSuccessfully": "Файл успешно сохранен", + "autoSaveFailed": "Автосохранение не удалось", + "fileAutoSaved": "Файл автосохранен", + "fileDownloadedSuccessfully": "Файл успешно скачан", + "moveFileFailed": "Не удалось переместить {{name}}", + "moveOperationFailed": "Операция перемещения не удалась", + "canOnlyCompareFiles": "Можно сравнивать только два файла", + "comparingFiles": "Сравнение файлов: {{file1}} и {{file2}}", + "dragFailed": "Операция перетаскивания не удалась", + "filePinnedSuccessfully": "Файл \"{{name}}\" успешно закреплен", + "pinFileFailed": "Не удалось закрепить файл", + "fileUnpinnedSuccessfully": "Файл \"{{name}}\" успешно откреплен", + "unpinFileFailed": "Не удалось открепить файл", + "shortcutAddedSuccessfully": "Ярлык папки \"{{name}}\" успешно добавлен", + "addShortcutFailed": "Не удалось добавить ярлык", + "operationCompletedSuccessfully": "{{operation}} {{count}} элементов успешно завершено", + "operationCompleted": "{{operation}} {{count}} элементов", + "downloadFileSuccess": "Файл {{name}} успешно скачан", + "downloadFileFailed": "Скачивание не удалось", + "moveTo": "Переместить в {{name}}", + "diffCompareWith": "Сравнить различия с {{name}}", + "dragOutsideToDownload": "Перетащите за пределы окна для скачивания ({{count}} файлов)", + "newFolderDefault": "НоваяПапка", + "newFileDefault": "НовыйФайл.txt", + "successfullyMovedItems": "Успешно перемещено {{count}} элементов в {{target}}", + "move": "Переместить", + "searchInFile": "Поиск в файле (Ctrl+F)", + "showKeyboardShortcuts": "Показать горячие клавиши", + "startWritingMarkdown": "Начните писать ваш markdown-контент...", + "loadingFileComparison": "Загрузка сравнения файлов...", + "reload": "Перезагрузить", + "compare": "Сравнить", + "sideBySide": "Рядом", + "inline": "Встроенное", + "fileComparison": "Сравнение файлов: {{file1}} vs {{file2}}", + "fileTooLarge": "Файл слишком большой: {{error}}", + "sshConnectionFailed": "SSH-подключение не удалось. Пожалуйста, проверьте ваше подключение к {{name}} ({{ip}}:{{port}})", + "loadFileFailed": "Не удалось загрузить файл: {{error}}", + "connectedSuccessfully": "Успешно подключено", + "totpVerificationFailed": "Проверка TOTP не удалась" + }, + "tunnels": { + "title": "SSH-туннели", + "noSshTunnels": "Нет SSH-туннелей", + "createFirstTunnelMessage": "Вы еще не создали SSH-туннели. Настройте туннельные подключения в Менеджере хостов, чтобы начать.", + "connected": "Подключено", + "disconnected": "Отключено", + "connecting": "Подключение...", + "disconnecting": "Отключение...", + "unknownTunnelStatus": "Неизвестно", + "unknown": "Неизвестно", + "error": "Ошибка", + "failed": "Не удалось", + "retrying": "Повторная попытка", + "waiting": "Ожидание", + "waitingForRetry": "Ожидание повторной попытки", + "retryingConnection": "Повторное подключение", + "canceling": "Отмена...", + "connect": "Подключить", + "disconnect": "Отключить", + "cancel": "Отмена", + "port": "Порт", + "attempt": "Попытка {{current}} из {{max}}", + "nextRetryIn": "Следующая попытка через {{seconds}} секунд", + "checkDockerLogs": "Проверьте ваши логи Docker для выяснения причины ошибки, присоединяйтесь к", + "noTunnelConnections": "Нет настроенных туннельных подключений", + "tunnelConnections": "Туннельные подключения", + "addTunnel": "Добавить туннель", + "editTunnel": "Редактировать туннель", + "deleteTunnel": "Удалить туннель", + "tunnelName": "Имя туннеля", + "localPort": "Локальный порт", + "remoteHost": "Удаленный хост", + "remotePort": "Удаленный порт", + "autoStart": "Автозапуск", + "status": "Статус", + "active": "Активно", + "inactive": "Неактивно", + "start": "Запустить", + "stop": "Остановить", + "restart": "Перезапустить", + "connectionType": "Тип подключения", + "local": "Локальный", + "remote": "Удаленный", + "dynamic": "Динамический", + "noSshTunnels": "Нет SSH-туннелей", + "createFirstTunnelMessage": "Создайте ваш первый SSH-туннель, чтобы начать. Используйте SSH-менеджер для добавления хостов с туннельными подключениями.", + "unknownConnectionStatus": "Неизвестно", + "connected": "Подключено", + "connecting": "Подключение...", + "disconnecting": "Отключение...", + "disconnected": "Отключено", + "portMapping": "Порт {{sourcePort}} → {{endpointHost}}:{{endpointPort}}", + "disconnect": "Отключить", + "connect": "Подключить", + "canceling": "Отмена...", + "endpointHostNotFound": "Хост конечной точки не найден", + "discord": "Discord", + "githubIssue": "Проблема на GitHub", + "forHelp": "для помощи" + }, + "serverStats": { + "title": "Статистика сервера", + "cpu": "CPU", + "memory": "Память", + "disk": "Диск", + "network": "Сеть", + "uptime": "Время работы", + "loadAverage": "Средняя загрузка", + "processes": "Процессы", + "connections": "Подключения", + "usage": "Использование", + "available": "Доступно", + "total": "Всего", + "free": "Свободно", + "used": "Использовано", + "percentage": "Процент", + "refreshStatusAndMetrics": "Обновить статус и метрики", + "refreshStatus": "Обновить статус", + "fileManagerAlreadyOpen": "Файловый менеджер уже открыт для этого хоста", + "openFileManager": "Открыть файловый менеджер", + "cpuCores_one": "{{count}} CPU", + "cpuCores_other": "{{count}} CPU", + "naCpus": "N/A CPU", + "loadAverage": "Средняя: {{avg1}}, {{avg5}}, {{avg15}}", + "loadAverageNA": "Средняя: N/A", + "cpuUsage": "Использование CPU", + "memoryUsage": "Использование памяти", + "diskUsage": "Использование диска", + "rootStorageSpace": "Место в корневом хранилище", + "of": "из", + "feedbackMessage": "Есть идеи, что должно быть следующим для управления сервером? Поделитесь ими на", + "failedToFetchHostConfig": "Не удалось загрузить конфигурацию хоста", + "failedToFetchStatus": "Не удалось загрузить статус сервера", + "failedToFetchMetrics": "Не удалось загрузить метрики сервера", + "failedToFetchHomeData": "Не удалось загрузить домашние данные", + "loadingMetrics": "Загрузка метрик...", + "refreshing": "Обновление...", + "serverOffline": "Сервер не в сети", + "cannotFetchMetrics": "Невозможно получить метрики с отключенного сервера", + "totpRequired": "Требуется TOTP-аутентификация", + "totpUnavailable": "Статистика сервера недоступна для серверов с включенным TOTP", + "load": "Загрузка", + "free": "Свободно", + "available": "Доступно", + "editLayout": "Редактировать макет", + "cancelEdit": "Отмена", + "addWidget": "Добавить виджет", + "saveLayout": "Сохранить макет", + "unsavedChanges": "Несохраненные изменения", + "layoutSaved": "Макет успешно сохранен", + "failedToSaveLayout": "Не удалось сохранить макет", + "systemInfo": "Системная информация", + "hostname": "Имя хоста", + "operatingSystem": "Операционная система", + "kernel": "Ядро", + "totalUptime": "Общее время работы", + "seconds": "секунд", + "networkInterfaces": "Сетевые интерфейсы", + "noInterfacesFound": "Сетевые интерфейсы не найдены", + "totalProcesses": "Всего процессов", + "running": "Запущено", + "noProcessesFound": "Процессы не найдены" + }, + "auth": { + "loginTitle": "Вход в Termix", + "registerTitle": "Создать учетную запись", + "loginButton": "Войти", + "registerButton": "Зарегистрироваться", + "forgotPassword": "Забыли пароль?", + "rememberMe": "Запомнить меня", + "noAccount": "Нет учетной записи?", + "hasAccount": "Уже есть учетная запись?", + "loginSuccess": "Вход выполнен успешно", + "loginFailed": "Ошибка входа", + "registerSuccess": "Регистрация успешна", + "registerFailed": "Ошибка регистрации", + "logoutSuccess": "Выход выполнен успешно", + "invalidCredentials": "Неверное имя пользователя или пароль", + "accountCreated": "Учетная запись успешно создана", + "passwordReset": "Ссылка для сброса пароля отправлена", + "twoFactorAuth": "Двухфакторная аутентификация", + "enterCode": "Введите код проверки", + "backupCode": "Или используйте резервный код", + "verifyCode": "Проверить код", + "enableTwoFactor": "Включить двухфакторную аутентификацию", + "disableTwoFactor": "Отключить двухфакторную аутентификацию", + "scanQRCode": "Отсканируйте этот QR-код вашим приложением-аутентификатором", + "backupCodes": "Резервные коды", + "saveBackupCodes": "Сохраните эти резервные коды в безопасном месте", + "twoFactorEnabledSuccess": "Двухфакторная аутентификация успешно включена!", + "twoFactorDisabled": "Двухфакторная аутентификация отключена", + "newBackupCodesGenerated": "Сгенерированы новые резервные коды", + "backupCodesDownloaded": "Резервные коды скачаны", + "pleaseEnterSixDigitCode": "Пожалуйста, введите 6-значный код", + "invalidVerificationCode": "Неверный код проверки", + "failedToDisableTotp": "Не удалось отключить TOTP", + "failedToGenerateBackupCodes": "Не удалось сгенерировать резервные коды", + "enterPassword": "Введите ваш пароль", + "lockedOidcAuth": "Заблокировано (OIDC Auth)", + "twoFactorTitle": "Двухфакторная аутентификация", + "twoFactorProtected": "Ваша учетная запись защищена двухфакторной аутентификацией", + "twoFactorActive": "Двухфакторная аутентификация в настоящее время активна на вашей учетной записи", + "disable2FA": "Отключить 2FA", + "disableTwoFactorWarning": "Отключение двухфакторной аутентификации сделает вашу учетную запись менее защищенной", + "passwordOrTotpCode": "Пароль или TOTP-код", + "or": "Или", + "generateNewBackupCodesText": "Сгенерируйте новые резервные коды, если вы потеряли существующие", + "generateNewBackupCodes": "Сгенерировать новые резервные коды", + "yourBackupCodes": "Ваши резервные коды", + "download": "Скачать", + "setupTwoFactorTitle": "Настройка двухфакторной аутентификации", + "step1ScanQR": "Шаг 1: Отсканируйте QR-код вашим приложением-аутентификатором", + "manualEntryCode": "Код для ручного ввода", + "cannotScanQRText": "Если вы не можете отсканировать QR-код, введите этот код вручную в вашем приложении-аутентификаторе", + "nextVerifyCode": "Далее: Проверить код", + "verifyAuthenticator": "Проверьте ваш аутентификатор", + "step2EnterCode": "Шаг 2: Введите 6-значный код из вашего приложения-аутентификатора", + "verificationCode": "Код проверки", + "back": "Назад", + "verifyAndEnable": "Проверить и включить", + "saveBackupCodesTitle": "Сохраните ваши резервные коды", + "step3StoreCodesSecurely": "Шаг 3: Сохраните эти коды в безопасном месте", + "importantBackupCodesText": "Сохраните эти резервные коды в безопасном месте. Вы можете использовать их для доступа к вашей учетной записи, если потеряете ваше устройство аутентификации.", + "completeSetup": "Завершить настройку", + "notEnabledText": "Двухфакторная аутентификация добавляет дополнительный уровень безопасности, требуя код из вашего приложения-аутентификатора при входе.", + "enableTwoFactorButton": "Включить двухфакторную аутентификацию", + "addExtraSecurityLayer": "Добавьте дополнительный уровень безопасности к вашей учетной записи", + "firstUser": "Первый пользователь", + "firstUserMessage": "Вы первый пользователь и будете сделаны администратором. Вы можете просмотреть настройки администратора в выпадающем меню пользователя на боковой панели. Если вы считаете, что это ошибка, проверьте логи docker или создайте проблему на GitHub.", + "external": "Внешний", + "loginWithExternal": "Войти через внешнего провайдера", + "loginWithExternalDesc": "Войти с использованием настроенного внешнего провайдера идентификации", + "externalNotSupportedInElectron": "Внешняя аутентификация пока не поддерживается в Electron-приложении. Пожалуйста, используйте веб-версию для входа через OIDC.", + "resetPasswordButton": "Сбросить пароль", + "sendResetCode": "Отправить код сброса", + "resetCodeDesc": "Введите ваше имя пользователя для получения кода сброса пароля. Код будет записан в логи docker-контейнера.", + "resetCode": "Код сброса", + "verifyCodeButton": "Проверить код", + "enterResetCode": "Введите 6-значный код из логов docker-контейнера для пользователя:", + "goToLogin": "Перейти ко входу", + "newPassword": "Новый пароль", + "confirmNewPassword": "Подтвердите пароль", + "enterNewPassword": "Введите ваш новый пароль для пользователя:", + "signUp": "Зарегистрироваться", + "dataLossWarning": "Сброс пароля этим способом удалит все ваши сохраненные SSH-хосты, учетные данные и другие зашифрованные данные. Это действие нельзя отменить. Используйте это только если вы забыли пароль и не вошли в систему.", + "authenticationDisabled": "Аутентификация отключена", + "authenticationDisabledDesc": "Все методы аутентификации в настоящее время отключены. Пожалуйста, свяжитесь с вашим администратором." + }, + "errors": { + "notFound": "Страница не найдена", + "unauthorized": "Неавторизованный доступ", + "forbidden": "Доступ запрещен", + "serverError": "Ошибка сервера", + "networkError": "Сетевая ошибка", + "databaseConnection": "Не удалось подключиться к базе данных.", + "unknownError": "Неизвестная ошибка", + "loginFailed": "Ошибка входа", + "failedPasswordReset": "Не удалось инициировать сброс пароля", + "failedVerifyCode": "Не удалось проверить код сброса", + "failedCompleteReset": "Не удалось завершить сброс пароля", + "invalidTotpCode": "Неверный TOTP-код", + "failedOidcLogin": "Не удалось начать вход через OIDC", + "failedUserInfo": "Не удалось получить информацию о пользователе после входа через OIDC", + "oidcAuthFailed": "OIDC-аутентификация не удалась", + "noTokenReceived": "Токен не получен при входе", + "invalidAuthUrl": "Получен неверный URL авторизации от бэкенда", + "invalidInput": "Неверный ввод", + "requiredField": "Это поле обязательно", + "minLength": "Минимальная длина {{min}}", + "maxLength": "Максимальная длина {{max}}", + "invalidEmail": "Неверный адрес email", + "passwordMismatch": "Пароли не совпадают", + "passwordLoginDisabled": "Вход по имени пользователя/паролю в настоящее время отключен", + "weakPassword": "Пароль слишком слабый", + "usernameExists": "Имя пользователя уже существует", + "emailExists": "Email уже существует", + "loadFailed": "Не удалось загрузить данные", + "saveError": "Не удалось сохранить", + "sessionExpired": "Сеанс истек - пожалуйста, войдите снова" + }, + "messages": { + "saveSuccess": "Успешно сохранено", + "saveError": "Не удалось сохранить", + "deleteSuccess": "Успешно удалено", + "deleteError": "Не удалось удалить", + "updateSuccess": "Успешно обновлено", + "updateError": "Не удалось обновить", + "copySuccess": "Скопировано в буфер обмена", + "copyError": "Не удалось скопировать", + "copiedToClipboard": "{{item}} скопировано в буфер обмена", + "connectionEstablished": "Подключение установлено", + "connectionClosed": "Подключение закрыто", + "reconnecting": "Переподключение...", + "processing": "Обработка...", + "pleaseWait": "Пожалуйста, подождите...", + "registrationDisabled": "Регистрация новых учетных записей в настоящее время отключена администратором. Пожалуйста, войдите или свяжитесь с администратором.", + "databaseConnected": "Подключение к базе данных успешно установлено", + "databaseConnectionFailed": "Не удалось подключиться к серверу базы данных", + "checkServerConnection": "Пожалуйста, проверьте ваше подключение к серверу и попробуйте снова", + "resetCodeSent": "Код сброса отправлен в логи Docker", + "codeVerified": "Код успешно проверен", + "passwordResetSuccess": "Пароль успешно сброшен", + "loginSuccess": "Вход выполнен успешно", + "registrationSuccess": "Регистрация успешна" + }, + "profile": { + "title": "Профиль пользователя", + "description": "Управление настройками учетной записи и безопасностью", + "security": "Безопасность", + "changePassword": "Изменить пароль", + "twoFactorAuth": "Двухфакторная аутентификация", + "accountInfo": "Информация об учетной записи", + "role": "Роль", + "admin": "Администратор", + "user": "Пользователь", + "authMethod": "Метод аутентификации", + "local": "Локальный", + "external": "Внешний (OIDC)", + "selectPreferredLanguage": "Выберите предпочитаемый язык интерфейса", + "currentPassword": "Текущий пароль", + "passwordChangedSuccess": "Пароль успешно изменен! Пожалуйста, войдите снова.", + "failedToChangePassword": "Не удалось изменить пароль. Пожалуйста, проверьте ваш текущий пароль и попробуйте снова." + }, + "user": { + "failedToLoadVersionInfo": "Не удалось загрузить информацию о версии" + }, + "placeholders": { + "enterCode": "000000", + "ipAddress": "127.0.0.1", + "port": "22", + "maxRetries": "3", + "retryInterval": "10", + "language": "Язык", + "username": "имя пользователя", + "hostname": "имя хоста", + "folder": "папка", + "password": "пароль", + "keyPassword": "пароль ключа", + "pastePrivateKey": "Вставьте ваш приватный ключ здесь...", + "pastePublicKey": "Вставьте ваш публичный ключ здесь...", + "credentialName": "Мой SSH-сервер", + "description": "Описание SSH-учетных данных", + "searchCredentials": "Поиск учетных данных по имени, имени пользователя или тегам...", + "sshConfig": "конфигурация ssh конечной точки", + "homePath": "/home", + "clientId": "your-client-id", + "clientSecret": "your-client-secret", + "authUrl": "https://your-provider.com/application/o/authorize/", + "redirectUrl": "https://your-provider.com/application/o/termix/", + "tokenUrl": "https://your-provider.com/application/o/token/", + "userIdField": "sub", + "usernameField": "name", + "scopes": "openid email profile", + "userinfoUrl": "https://your-provider.com/application/o/userinfo/", + "enterUsername": "Введите имя пользователя, чтобы сделать администратором", + "searchHosts": "Поиск хостов по имени, имени пользователя, IP, папке, тегам...", + "enterPassword": "Введите ваш пароль", + "totpCode": "6-значный TOTP-код", + "searchHostsAny": "Поиск хостов по любой информации...", + "confirmPassword": "Введите ваш пароль для подтверждения", + "typeHere": "Введите здесь", + "fileName": "Введите имя файла (например, example.txt)", + "folderName": "Введите имя папки", + "fullPath": "Введите полный путь к элементу", + "currentPath": "Введите текущий путь к элементу", + "newName": "Введите новое имя" + }, + "leftSidebar": { + "failedToLoadHosts": "Не удалось загрузить хосты", + "noFolder": "Без папки", + "passwordRequired": "Требуется пароль", + "failedToDeleteAccount": "Не удалось удалить учетную запись", + "failedToMakeUserAdmin": "Не удалось сделать пользователя администратором", + "userIsNowAdmin": "Пользователь {{username}} теперь администратор", + "removeAdminConfirm": "Вы уверены, что хотите убрать статус администратора у {{username}}?", + "deleteUserConfirm": "Вы уверены, что хотите удалить пользователя {{username}}? Это действие нельзя отменить.", + "deleteAccount": "Удалить учетную запись", + "closeDeleteAccount": "Закрыть удаление учетной записи", + "deleteAccountWarning": "Это действие нельзя отменить. Это окончательно удалит вашу учетную запись и все связанные данные.", + "deleteAccountWarningDetails": "Удаление вашей учетной записи удалит все ваши данные, включая SSH-хосты, конфигурации и настройки. Это действие необратимо.", + "cannotDeleteAccount": "Невозможно удалить учетную запись", + "lastAdminWarning": "Вы последний пользователь-администратор. Вы не можете удалить свою учетную запись, так как это оставит систему без администраторов. Пожалуйста, сначала сделайте другого пользователя администратором или свяжитесь с поддержкой системы.", + "confirmPassword": "Подтвердите пароль", + "deleting": "Удаление...", + "cancel": "Отмена" + }, + "interface": { + "sidebar": "Боковая панель", + "toggleSidebar": "Переключить боковую панель", + "close": "Закрыть", + "online": "В сети", + "offline": "Не в сети", + "maintenance": "Обслуживание", + "degraded": "Снижена производительность", + "noTunnelConnections": "Нет настроенных туннельных подключений", + "discord": "Discord", + "connectToSshForOperations": "Подключитесь к SSH для использования файловых операций", + "uploadFile": "Загрузить файл", + "newFile": "Новый файл", + "newFolder": "Новая папка", + "rename": "Переименовать", + "deleteItem": "Удалить элемент", + "createNewFile": "Создать новый файл", + "createNewFolder": "Создать новую папку", + "deleteItem": "Удалить элемент", + "renameItem": "Переименовать элемент", + "clickToSelectFile": "Нажмите для выбора файла", + "noSshHosts": "Нет SSH-хостов", + "sshHosts": "SSH-хосты", + "importSshHosts": "Импорт SSH-хостов из JSON", + "clientId": "Client ID", + "clientSecret": "Client Secret", + "error": "Ошибка", + "warning": "Предупреждение", + "deleteAccount": "Удалить учетную запись", + "closeDeleteAccount": "Закрыть удаление учетной записи", + "cannotDeleteAccount": "Невозможно удалить учетную запись", + "confirmPassword": "Подтвердите пароль", + "deleting": "Удаление...", + "externalAuth": "Внешняя аутентификация (OIDC)", + "configureExternalProvider": "Настройте внешнего провайдера идентификации для", + "waitingForRetry": "Ожидание повторной попытки", + "retryingConnection": "Повторное подключение", + "resetSplitSizes": "Сбросить размеры разделения", + "sshManagerAlreadyOpen": "SSH-менеджер уже открыт", + "disabledDuringSplitScreen": "Отключено во время разделенного экрана", + "unknown": "Неизвестно", + "connected": "Подключено", + "disconnected": "Отключено", + "maxRetriesExhausted": "Исчерпаны максимальные попытки", + "endpointHostNotFound": "Хост конечной точки не найден", + "administrator": "Администратор", + "user": "Пользователь", + "external": "Внешний", + "local": "Локальный", + "saving": "Сохранение...", + "saveConfiguration": "Сохранить конфигурацию", + "loading": "Загрузка...", + "refresh": "Обновить", + "adding": "Добавление...", + "makeAdmin": "Сделать администратором", + "verifying": "Проверка...", + "verifyAndEnable": "Проверить и включить", + "secretKey": "Секретный ключ", + "totpQrCode": "TOTP QR-код", + "passwordRequired": "Пароль требуется при использовании аутентификации по паролю", + "sshKeyRequired": "Приватный SSH-ключ требуется при использовании аутентификации по ключу", + "keyTypeRequired": "Тип ключа требуется при использовании аутентификации по ключу", + "validSshConfigRequired": "Необходимо выбрать допустимую SSH-конфигурацию из списка", + "updateHost": "Обновить хост", + "addHost": "Добавить хост", + "editHost": "Редактировать хост", + "pinConnection": "Закрепить подключение", + "authentication": "Аутентификация", + "password": "Пароль", + "key": "Ключ", + "sshPrivateKey": "Приватный SSH-ключ", + "keyPassword": "Пароль ключа", + "keyType": "Тип ключа", + "enableTerminal": "Включить терминал", + "enableTunnel": "Включить туннель", + "enableFileManager": "Включить файловый менеджер", + "defaultPath": "Путь по умолчанию", + "tunnelConnections": "Туннельные подключения", + "maxRetries": "Макс. попыток", + "upload": "Загрузить", + "updateKey": "Обновить ключ", + "productionFolder": "Продакшен", + "databaseServer": "Сервер базы данных", + "developmentServer": "Сервер разработки", + "developmentFolder": "Разработка", + "webServerProduction": "Веб-сервер - Продакшен", + "unknownError": "Неизвестная ошибка", + "failedToInitiatePasswordReset": "Не удалось инициировать сброс пароля", + "failedToVerifyResetCode": "Не удалось проверить код сброса", + "failedToCompletePasswordReset": "Не удалось завершить сброс пароля", + "invalidTotpCode": "Неверный TOTP-код", + "failedToStartOidcLogin": "Не удалось начать вход через OIDC", + "failedToGetUserInfoAfterOidc": "Не удалось получить информацию о пользователе после OIDC-входа", + "loginWithExternalProvider": "Войти через внешнего провайдера", + "loginWithExternal": "Войти через внешнего провайдера", + "sendResetCode": "Отправить код сброса", + "verifyCode": "Проверить код", + "resetPassword": "Сбросить пароль", + "login": "Войти", + "signUp": "Зарегистрироваться", + "failedToUpdateOidcConfig": "Не удалось обновить конфигурацию OIDC", + "failedToMakeUserAdmin": "Не удалось сделать пользователя администратором", + "failedToStartTotpSetup": "Не удалось начать настройку TOTP", + "invalidVerificationCode": "Неверный код проверки", + "failedToDisableTotp": "Не удалось отключить TOTP", + "failedToGenerateBackupCodes": "Не удалось сгенерировать резервные коды" + }, + "mobile": { + "selectHostToStart": "Выберите хост для начала сеанса терминала", + "limitedSupportMessage": "Поддержка мобильного веб-сайта все еще в разработке. Используйте мобильное приложение для лучшего опыта.", + "mobileAppInProgress": "Мобильное приложение в разработке", + "mobileAppInProgressDesc": "Мы работаем над специальным мобильным приложением, чтобы обеспечить лучший опыт на мобильных устройствах.", + "viewMobileAppDocs": "Установить мобильное приложение", + "mobileAppDocumentation": "Документация мобильного приложения" + }, + "dashboard": { + "title": "Панель управления", + "github": "GitHub", + "support": "Поддержка", + "discord": "Discord", + "donate": "Пожертвовать", + "serverOverview": "Обзор сервера", + "version": "Версия", + "upToDate": "Обновлено", + "updateAvailable": "Доступно обновление", + "uptime": "Время работы", + "database": "База данных", + "healthy": "Работает", + "error": "Ошибка", + "totalServers": "Всего серверов", + "totalTunnels": "Всего туннелей", + "totalCredentials": "Всего учетных данных", + "recentActivity": "Недавняя активность", + "reset": "Сбросить", + "loadingRecentActivity": "Загрузка недавней активности...", + "noRecentActivity": "Нет недавней активности", + "quickActions": "Быстрые действия", + "addHost": "Добавить хост", + "addCredential": "Добавить учетные данные", + "adminSettings": "Настройки администратора", + "userProfile": "Профиль пользователя", + "serverStats": "Статистика сервера", + "loadingServerStats": "Загрузка статистики сервера...", + "noServerData": "Данные сервера недоступны", + "cpu": "CPU", + "ram": "RAM", + "notAvailable": "N/A" + } +} \ No newline at end of file