commit f405ae2bcf29e3976e737f8fcb49dd8aa48f164c
Author: Александр <153805936+Cheviiot@users.noreply.github.com>
Date: Sat Jul 25 00:49:32 2026 +1000
feat: create autonomous Nivora package repository
Maintain a validated cross-distribution package catalog.
Automate upstream updates, isolated builds, diagnostics, and direct publication.
Build the official GitHub Desktop sources for Linux with working OAuth.
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..988d9b3
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+* text=auto eol=lf
+*.png binary
+*.svg text eol=lf
+*.sh text eol=lf
+Staplerfile text eol=lf
diff --git a/.github/actions/setup-stplr-spec/action.yml b/.github/actions/setup-stplr-spec/action.yml
new file mode 100644
index 0000000..127b393
--- /dev/null
+++ b/.github/actions/setup-stplr-spec/action.yml
@@ -0,0 +1,42 @@
+name: Подготовить stplr-spec
+description: Устанавливает закреплённые Go, системные инструменты и stplr-spec
+
+inputs:
+ system-packages:
+ description: Дополнительные пакеты Ubuntu через пробел
+ required: false
+ default: ""
+
+runs:
+ using: composite
+ steps:
+ - name: Установить Go
+ uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5
+ with:
+ go-version: 1.26.5
+ cache: false
+
+ - name: Установить системные инструменты
+ if: ${{ inputs.system-packages != '' }}
+ shell: bash
+ env:
+ SYSTEM_PACKAGES: ${{ inputs.system-packages }}
+ run: |
+ sudo apt-get update
+ read -r -a packages <<<"$SYSTEM_PACKAGES"
+ sudo apt-get install -y "${packages[@]}"
+
+ - name: Собрать закреплённый stplr-spec
+ shell: bash
+ env:
+ STPLR_UTILS_COMMIT: c6ddbb5e4e5637d97bb7b2587729178d715c6c52
+ run: |
+ git clone --filter=blob:none \
+ https://altlinux.space/stapler/stplr-utils.git \
+ "$RUNNER_TEMP/stplr-utils"
+ git -C "$RUNNER_TEMP/stplr-utils" checkout "$STPLR_UTILS_COMMIT"
+ (
+ cd "$RUNNER_TEMP/stplr-utils"
+ GOBIN="$RUNNER_TEMP/bin" go install ./cmd/stplr-spec
+ )
+ echo "$RUNNER_TEMP/bin" >>"$GITHUB_PATH"
diff --git a/.github/assets/nivora.png b/.github/assets/nivora.png
new file mode 100644
index 0000000..26e211e
Binary files /dev/null and b/.github/assets/nivora.png differ
diff --git a/.github/workflows/check-updates.yml b/.github/workflows/check-updates.yml
new file mode 100644
index 0000000..3899c49
--- /dev/null
+++ b/.github/workflows/check-updates.yml
@@ -0,0 +1,201 @@
+name: CI · Автономное обновление пакетов
+
+on:
+ schedule:
+ # 03:00 Asia/Vladivostok (UTC+10)
+ - cron: "0 17 * * *"
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ issues: write
+
+concurrency:
+ group: nivora-autonomous-updates
+ cancel-in-progress: false
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ detect:
+ name: Найти обновления
+ runs-on: ubuntu-24.04
+ timeout-minutes: 30
+ outputs:
+ packages: ${{ steps.updates.outputs.packages }}
+ has_updates: ${{ steps.updates.outputs.has_updates }}
+ github_desktop_update: ${{ steps.updates.outputs.github_desktop_update }}
+ github_desktop_version: ${{ steps.updates.outputs.github_desktop_version }}
+ steps:
+ - name: Получить репозиторий
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
+
+ - name: Подготовить инструменты
+ uses: ./.github/actions/setup-stplr-spec
+ with:
+ system-packages: jq
+
+ - name: Сформировать план обновления
+ id: updates
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ run: |
+ packages_output="$(tools/package_updates.sh outdated)"
+ mapfile -t packages <<<"$packages_output"
+ if [[ -z "$packages_output" ]]; then
+ packages=()
+ fi
+ if [[ "${#packages[@]}" -eq 0 ]]; then
+ {
+ echo 'packages=[]'
+ echo 'has_updates=false'
+ echo 'github_desktop_update=false'
+ echo 'github_desktop_version='
+ } >>"$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ packages_json="$(
+ printf '%s\n' "${packages[@]}" |
+ jq -Rsc 'split("\n") | map(select(length > 0))'
+ )"
+ github_desktop_update=false
+ github_desktop_version=''
+ for package in "${packages[@]}"; do
+ if [[ "$package" == github-desktop ]]; then
+ github_desktop_update=true
+ read -r _ github_desktop_version < <(
+ tools/package_updates.sh check github-desktop
+ )
+ fi
+ done
+ {
+ echo "packages=${packages_json}"
+ echo 'has_updates=true'
+ echo "github_desktop_update=${github_desktop_update}"
+ echo "github_desktop_version=${github_desktop_version}"
+ } >>"$GITHUB_OUTPUT"
+
+ github-desktop:
+ name: Подготовить GitHub Desktop
+ needs: detect
+ if: ${{ needs.detect.outputs.github_desktop_update == 'true' }}
+ uses: ./.github/workflows/github-desktop-linux.yml
+ with:
+ version: ${{ needs.detect.outputs.github_desktop_version }}
+ publish: true
+ permissions:
+ contents: write
+
+ update:
+ name: Изолированно обновить и опубликовать
+ needs:
+ - detect
+ - github-desktop
+ if: >-
+ ${{
+ always() &&
+ needs.detect.result == 'success' &&
+ needs.detect.outputs.has_updates == 'true'
+ }}
+ runs-on: ubuntu-24.04
+ timeout-minutes: 360
+ steps:
+ - name: Получить репозиторий
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
+ with:
+ ref: main
+ fetch-depth: 0
+
+ - name: Подготовить инструменты
+ uses: ./.github/actions/setup-stplr-spec
+ with:
+ system-packages: jq rpm shellcheck
+
+ - name: Изолированно обновить и проверить каждый пакет
+ id: isolated
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ PACKAGES_JSON: ${{ needs.detect.outputs.packages }}
+ AUTONOMOUS_UPDATE_RESULTS_DIR: ${{ runner.temp }}/update-results
+ run: |
+ mapfile -t packages < <(jq -r '.[]' <<<"$PACKAGES_JSON")
+ tools/autonomous_package_updates.sh "${packages[@]}"
+
+ successful_json="$(
+ jq -Rsc 'split("\n") | map(select(length > 0))' \
+ <"$AUTONOMOUS_UPDATE_RESULTS_DIR/successful-packages"
+ )"
+ failed_json="$(
+ cut -f1 "$AUTONOMOUS_UPDATE_RESULTS_DIR/failed-packages" |
+ jq -Rsc 'split("\n") | map(select(length > 0))'
+ )"
+ echo "successful=${successful_json}" >>"$GITHUB_OUTPUT"
+ echo "failed=${failed_json}" >>"$GITHUB_OUTPUT"
+
+ - name: Сохранить журналы и diff каждого пакета
+ if: ${{ always() }}
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
+ with:
+ name: autonomous-update-diagnostics-${{ github.run_id }}
+ path: ${{ runner.temp }}/update-results
+ if-no-files-found: error
+ retention-days: 30
+
+ - name: Применить только успешные обновления
+ env:
+ AUTONOMOUS_UPDATE_RESULTS_DIR: ${{ runner.temp }}/update-results
+ run: |
+ while IFS= read -r package; do
+ [[ -n "$package" ]] || continue
+ git apply \
+ "$AUTONOMOUS_UPDATE_RESULTS_DIR/${package}/update.patch"
+ done <"$AUTONOMOUS_UPDATE_RESULTS_DIR/successful-packages"
+ tools/sync_readme_versions.py
+ tools/run_checks.sh
+
+ - name: Отправить обновления прямо в main
+ id: publish
+ env:
+ SUCCESSFUL_PACKAGES: ${{ steps.isolated.outputs.successful }}
+ run: |
+ if git diff --quiet; then
+ echo 'Изменений после обновления нет'
+ exit 0
+ fi
+
+ git config user.name 'github-actions[bot]'
+ git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
+ git add README.md ./*/Staplerfile
+ packages="$(jq -r 'join(", ")' <<<"$SUCCESSFUL_PACKAGES")"
+ git commit -m "chore: autonomously update ${packages}"
+ git pull --rebase origin main
+ git push origin HEAD:main
+
+ - name: Синхронизировать постоянные отчёты о сбоях
+ if: ${{ always() && steps.isolated.outcome == 'success' }}
+ env:
+ AUTONOMOUS_UPDATE_RESULTS_DIR: ${{ runner.temp }}/update-results
+ GH_TOKEN: ${{ github.token }}
+ run: tools/report_update_failures.sh
+
+ - name: Сообщить о несовместимых пакетах
+ if: ${{ always() && steps.isolated.outputs.failed != '[]' }}
+ env:
+ FAILED_PACKAGES: ${{ steps.isolated.outputs.failed }}
+ AUTONOMOUS_UPDATE_RESULTS_DIR: ${{ runner.temp }}/update-results
+ run: |
+ {
+ echo '## Требуется ручная диагностика'
+ echo
+ echo 'Обновление остальных пакетов продолжено и опубликовано.'
+ echo 'Для несовместимых пакетов сохранён artifact с логами и diff:'
+ echo
+ while IFS=$'\t' read -r package phase; do
+ printf -- '- **%s**: сбой на фазе **%s**\n' "$package" "$phase"
+ done <"$AUTONOMOUS_UPDATE_RESULTS_DIR/failed-packages"
+ } >>"$GITHUB_STEP_SUMMARY"
+
+ echo "::warning::Требуется ручная диагностика: $(jq -r 'join(\", \")' <<<"$FAILED_PACKAGES")"
diff --git a/.github/workflows/clean-build.yml b/.github/workflows/clean-build.yml
new file mode 100644
index 0000000..fa6ebd1
--- /dev/null
+++ b/.github/workflows/clean-build.yml
@@ -0,0 +1,47 @@
+name: CI · Ручная сборка пакетов
+
+on:
+ workflow_dispatch:
+ inputs:
+ packages:
+ description: "all или package ID через пробел"
+ required: true
+ default: all
+ type: string
+
+permissions:
+ contents: read
+
+concurrency:
+ group: nivora-clean-build
+ cancel-in-progress: false
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ build:
+ name: Изолированная сборка
+ runs-on: ubuntu-24.04
+ timeout-minutes: 300
+ steps:
+ - name: Получить репозиторий
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
+
+ - name: Подготовить инструменты
+ uses: ./.github/actions/setup-stplr-spec
+
+ - name: Собрать пакеты
+ env:
+ REQUESTED_PACKAGES: ${{ inputs.packages }}
+ run: |
+ if [[ "$REQUESTED_PACKAGES" == all ]]; then
+ tools/clean_build.sh --all
+ tools/verify_artifacts.sh --all
+ NIVORA_DEB_BUILD_MODE=host tools/test_package_lifecycle.sh
+ else
+ read -r -a packages <<<"$REQUESTED_PACKAGES"
+ tools/clean_build.sh "${packages[@]}"
+ tools/verify_artifacts.sh "${packages[@]}"
+ fi
diff --git a/.github/workflows/github-desktop-linux.yml b/.github/workflows/github-desktop-linux.yml
new file mode 100644
index 0000000..670789f
--- /dev/null
+++ b/.github/workflows/github-desktop-linux.yml
@@ -0,0 +1,166 @@
+name: CI · Сборка GitHub Desktop для Linux
+
+on:
+ workflow_call:
+ inputs:
+ version:
+ description: "Версия desktop/desktop без префикса release-"
+ required: true
+ type: string
+ publish:
+ description: "Опубликовать tar.gz в GitHub Release Nivora"
+ required: false
+ default: true
+ type: boolean
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Версия desktop/desktop без префикса release-"
+ required: true
+ default: "3.6.3"
+ type: string
+ publish:
+ description: "Опубликовать tar.gz в GitHub Release Nivora"
+ required: true
+ default: true
+ type: boolean
+
+permissions:
+ contents: write
+
+concurrency:
+ group: nivora-github-desktop-linux-${{ inputs.version || '3.6.3' }}
+ cancel-in-progress: false
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ build:
+ name: Linux ${{ matrix.arch }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - runner: ubuntu-24.04
+ arch: x64
+ - runner: ubuntu-24.04-arm
+ arch: arm64
+ runs-on: ${{ matrix.runner }}
+ timeout-minutes: 180
+ steps:
+ - name: Установить Node.js
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
+ with:
+ node-version: "24.15.0"
+
+ - name: Установить системные зависимости
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y build-essential libsecret-1-dev pkg-config python3
+
+ - name: Получить официальные исходники GitHub Desktop
+ env:
+ VERSION: ${{ inputs.version || '3.6.3' }}
+ run: |
+ git clone \
+ --branch "release-${VERSION}" \
+ --depth 1 \
+ --recurse-submodules \
+ --shallow-submodules \
+ https://github.com/desktop/desktop.git \
+ github-desktop
+ git -C github-desktop remote set-url origin \
+ https://github.com/desktop/desktop.git
+
+ - name: Добавить поддержку OAuth callback в Linux
+ working-directory: github-desktop
+ run: |
+ main_process='app/src/main-process/main.ts'
+ perl -0pi -e \
+ "s/if \\(__DEV_SECRETS__\\) \\{\\n possibleProtocols\\.add\\('x-github-desktop-dev-auth'\\)\\n\\} else \\{\\n possibleProtocols\\.add\\('x-github-desktop-auth'\\)\\n\\}/possibleProtocols.add('x-github-desktop-auth')\\nif (__DEV_SECRETS__) {\\n possibleProtocols.add('x-github-desktop-dev-auth')\\n}/" \
+ "$main_process"
+ perl -0pi -e \
+ "s/if \\(__WIN32__ && args\\['protocol-launcher'\\] === true\\)/if ((__WIN32__ || __LINUX__) \\&\\& args['protocol-launcher'] === true)/" \
+ "$main_process"
+ grep -Fq "possibleProtocols.add('x-github-desktop-auth')" \
+ "$main_process"
+ grep -Fq "(__WIN32__ || __LINUX__)" "$main_process"
+
+ - name: Установить зависимости из upstream lockfile
+ working-directory: github-desktop
+ run: |
+ node vendor/yarn-1.21.1.js install --frozen-lockfile --non-interactive
+
+ - name: Собрать официальный Electron-клиент
+ working-directory: github-desktop
+ env:
+ NODE_ENV: production
+ RELEASE_CHANNEL: production
+ run: node vendor/yarn-1.21.1.js build:prod
+
+ - name: Упаковать Linux-сборку
+ env:
+ ARCH: ${{ matrix.arch }}
+ VERSION: ${{ inputs.version || '3.6.3' }}
+ run: |
+ dist="github-desktop/dist/desktop-linux-${ARCH}"
+ test -x "${dist}/desktop"
+ grep -aFq 'x-github-desktop-auth' \
+ "${dist}/resources/app/main.js"
+ mkdir -p package/github-desktop
+ cp -a "${dist}/." package/github-desktop/
+ printf '%s\n' "$(git -C github-desktop rev-parse HEAD)" \
+ >package/github-desktop/UPSTREAM_COMMIT
+ tar \
+ --sort=name \
+ --mtime='UTC 2020-01-01' \
+ --owner=0 \
+ --group=0 \
+ --numeric-owner \
+ -C package \
+ -cf "github-desktop-${VERSION}-linux-${ARCH}.tar" \
+ github-desktop
+ gzip -n -9 "github-desktop-${VERSION}-linux-${ARCH}.tar"
+
+ - name: Сохранить артефакт
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
+ with:
+ name: github-desktop-${{ inputs.version || '3.6.3' }}-linux-${{ matrix.arch }}
+ path: github-desktop-${{ inputs.version || '3.6.3' }}-linux-${{ matrix.arch }}.tar.gz
+ if-no-files-found: error
+ retention-days: 14
+
+ publish:
+ name: Опубликовать Linux-артефакты
+ if: ${{ inputs.publish }}
+ needs: build
+ runs-on: ubuntu-24.04
+ timeout-minutes: 15
+ steps:
+ - name: Скачать обе архитектуры
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
+ with:
+ pattern: github-desktop-${{ inputs.version || '3.6.3' }}-linux-*
+ merge-multiple: true
+
+ - name: Создать контрольные суммы
+ run: sha256sum github-desktop-*.tar.gz >SHA256SUMS
+
+ - name: Создать или обновить GitHub Release
+ env:
+ GH_TOKEN: ${{ github.token }}
+ GH_REPO: ${{ github.repository }}
+ VERSION: ${{ inputs.version || '3.6.3' }}
+ run: |
+ tag="github-desktop-${VERSION}-linux"
+ if ! gh release view "$tag" >/dev/null 2>&1; then
+ gh release create "$tag" \
+ --title "GitHub Desktop ${VERSION} for Linux" \
+ --notes "Linux artifacts built by Nivora CI from the official desktop/desktop release-${VERSION} source and its pinned submodules."
+ fi
+ gh release upload "$tag" \
+ github-desktop-*.tar.gz \
+ SHA256SUMS \
+ --clobber
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
new file mode 100644
index 0000000..a40a2e6
--- /dev/null
+++ b/.github/workflows/quality.yml
@@ -0,0 +1,49 @@
+name: CI · Проверка репозитория
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: nivora-quality-${{ github.ref }}
+ cancel-in-progress: true
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ quality:
+ name: Статика, метаданные и тесты
+ runs-on: ubuntu-24.04
+ timeout-minutes: 35
+ steps:
+ - name: Получить репозиторий
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
+
+ - name: Подготовить инструменты
+ uses: ./.github/actions/setup-stplr-spec
+ with:
+ system-packages: jq rpm shellcheck
+
+ - name: Выполнить проверки
+ run: tools/run_checks.sh
+
+ - name: Сверить upstream-версии
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ run: |
+ set +e
+ tools/package_updates.sh check-all
+ status=$?
+ set -e
+ if [[ "$status" -eq 10 ]]; then
+ echo "::warning::Для одного или нескольких пакетов доступны обновления"
+ elif [[ "$status" -ne 0 ]]; then
+ exit "$status"
+ fi
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..094692f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+*.deb
+*.rpm
+*.pkg.tar.*
+*.apk
+.stplr-spec-bc-export/
+__pycache__/
+*.pyc
+.venv/
+.idea/
+.vscode/
+*~
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..3c1cc0c
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,92 @@
+# История изменений
+
+В файле фиксируются значимые изменения Nivora.
+
+## 2026-07-24 — GitHub Desktop
+
+### Добавлено
+
+- Добавлен GitHub Desktop 3.6.3 для `amd64` и `arm64`, собираемый напрямую из
+ официального `desktop/desktop` и официальных submodule-репозиториев GitHub.
+- Сторонний Linux-форк `shiftkey/desktop` и готовые артефакты других авторов не
+ используются.
+- Тяжёлая сборка вынесена в отдельный GitHub Actions workflow для нативных
+ `x64`/`arm64` runner’ов; результаты и SHA-256 публикуются в релизе Nivora.
+
+## 2026-07-24 — очистка переходной инфраструктуры
+
+### Изменено
+
+- Удалены одноразовый мигратор прежнего каталога, переходные package ID,
+ совместимые команды и относящиеся к ним документация и тесты.
+- Транзакционная проверка теперь тестирует обновление внутри Nivora без фиктивных
+ пакетов стороннего каталога.
+- Codex обновлён до `26.721.31836`; новый upstream-сбросил `release` в `1`.
+- У Яндекс Браузера canonical desktop-id совпадает с идентификатором окна и XDG
+ Portal, а прежний ID скрыт и сохранён только для совместимости: GNOME больше
+ не разделяет launcher и запущенное окно на два приложения.
+
+## 2026-07-24 — Яндекс Браузер
+
+### Добавлено
+
+- В каталог добавлен Яндекс Браузер 26.4.1.1110 из официального stable-репозитория
+ для `amd64`.
+- Сохранены штатные desktop-id, XDG Portal-интеграция, MIME-типы и иконки.
+- Обновления браузера переданы Stapler: upstream cron-задача и автозапуск
+ системной настройки не устанавливаются.
+
+## 2026-07-24 — Ventoy
+
+### Добавлено
+
+- В каталог добавлен Ventoy 1.1.16 с официальным нативным Linux GUI.
+- Пакет поддерживает `amd64` и `arm64`, отображается в меню приложений как
+ **Ventoy** и запрашивает права на работу с накопителями через `pkexec`.
+
+### Обновлено
+
+- Clash Verge Rev обновлён до 2.5.2, Claude — до 1.24012.0,
+ Codex — до 26.721.30844, Happ — до 3.3.6, NetBird — до 0.75.0,
+ OpenCode — до 1.18.4.
+- Tray-иконки Happ переработаны на основе оригинального разорванного H-знака:
+ графитовая версия для отключённого состояния и лаймовые акценты для соединения.
+- Codex и OpenCode возвращены к чистым upstream-бинарникам: удалены runtime/ASAR-патчи,
+ wrapper OpenCode, лимиты памяти и GPU-fallback; сохранены только системная упаковка,
+ отображаемые названия и иконки.
+- Для Codex восстановлен предусмотренный Linux Computer Use: bundled plugin и
+ backend `computer-use-linux` 0.4.1 собираются из закреплённых upstream-исходников,
+ а Linux-патчи взяты из `ilysenko/codex-desktop-linux` без обхода серверного
+ feature gate.
+
+## 2026-07-19 — ClaudeAlt и balenaEtcher
+
+### Добавлено
+
+- В каталог добавлен balenaEtcher 2.1.6 для безопасной записи образов на
+ SD-карты и USB-накопители (`amd64`).
+
+### Изменено
+
+- Второй профиль Claude заменён самостоятельным приложением ClaudeAlt с отдельными
+ executable/resources tree, Electron `productName`, Wayland `app_id`, X11 `WM_CLASS`,
+ desktop-файлом, каталогом данных и Cowork VM socket.
+- Старые команды второго аккаунта сохранены как совместимые алиасы ClaudeAlt.
+- Существующий каталог `Claude-Account-2` автоматически используется после обновления,
+ чтобы сохранить авторизацию второго аккаунта.
+- ClaudeAlt и Nivora получили новые PNG-иконки с настоящей
+ прозрачностью вне скруглённых границ.
+- Иконка ClaudeAlt подключена к desktop-файлу и Electron `BrowserWindow`.
+- Штатный tray-глиф Claude окрашен в оранжевый для основного приложения и в
+ бирюзовый для ClaudeAlt; большая app-иконка больше не используется в лотке.
+- Размер и визуальные отступы иконки ClaudeAlt нормализованы под
+ upstream-иконку Claude.
+
+## 2026-07-17 — первый релиз
+
+### Добавлено
+
+- Независимый Stapler-каталог из 15 пакетов.
+- Новые документация, логотип, валидатор, тесты и GitHub Actions.
+- Изолированная сборка всех пакетов и проверка фактического содержимого RPM.
+- Транзакционные тесты установки, обновления и удаления DEB/RPM для критичных пакетов.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..5828845
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,40 @@
+# Участие в Nivora
+
+Проект принимает исправления рецептов, автоматизации и документации.
+
+## Перед изменением
+
+1. Проверьте официальное название, package ID, desktop-id и AppStream component-id.
+2. Найдите официальный HTTPS-источник для каждой архитектуры.
+3. Уточните лицензию и возможность переупаковки.
+4. Сверьте зависимости на поддерживаемых дистрибутивах.
+
+## Требования к пакету
+
+- Имя каталога точно совпадает с `name`.
+- Версия совпадает с upstream; изменение рецепта повышает `release`.
+- Скачиваемые sources привязаны к версии и защищены SHA-256.
+- `local:///`-файлы присутствуют в каталоге, а скрипты имеют executable bit.
+- `provides`, `replaces` и `conflicts` описывают package names, а не имена бинарников или desktop-id.
+- Lifecycle-скрипты идемпотентны и не удаляют пользовательские данные.
+- `files()` описывает весь payload и не захватывает чужие пути.
+
+## Проверка
+
+```bash
+tools/run_checks.sh
+stplr-spec verify-checksums --path package/Staplerfile
+tools/clean_build.sh package
+```
+
+Для изменения package ID нужны отдельные DEB/RPM-тесты замены и проверка
+сохранности данных.
+
+## Pull request
+
+Один pull request должен решать одну задачу. В описании укажите:
+
+- что изменилось;
+- ссылку на upstream;
+- выполненные проверки и сборки;
+- оставшиеся ограничения.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..39bb9f6
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Cheviiot
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f85040c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,124 @@
+
+
+
+
+# Nivora
+
+**Независимый каталог Linux-приложений для Stapler.**
+
+Nivora предлагает открытые рецепты упаковки десктопных приложений и
+инструментов, которых может не быть в системном репозитории. Это не
+официальный репозиторий Stapler и не официальные пакеты upstream-проектов.
+
+
+**19 пакетов** · **6 категорий** · `amd64`, `arm64` и `all`
+
+[](https://github.com/Cheviiot/Nivora/actions/workflows/quality.yml)
+
+## Подключение
+
+Требуется [Stapler](https://stplr.dev/docs/intro/) `v0.1.1` или новее.
+
+```bash
+sudo stplr repo add nivora https://github.com/Cheviiot/Nivora.git
+sudo stplr refresh
+```
+
+Установка пакета:
+
+```bash
+sudo stplr install nivora/codex
+```
+
+Проверить описание до установки:
+
+```bash
+stplr info nivora/codex
+```
+
+## Каталог
+
+Название ведёт на источник приложения. `all` означает, что сам пакет не
+содержит архитектурно-зависимых бинарников.
+
+### Интернет, сеть и VPN
+
+| Приложение | Версия | Архитектуры | Установка |
+|:--|:--:|:--:|:--|
+| [Clash Verge Rev](https://github.com/clash-verge-rev/clash-verge-rev) | `2.5.2` | `amd64`, `arm64` | `stplr install nivora/clash-verge-rev` |
+| [Happ](https://happ.su/) | `3.3.6` | `amd64`, `arm64` | `stplr install nivora/happ` |
+| [NetBird](https://netbird.io/) | `0.75.0` | `amd64`, `arm64` | `stplr install nivora/netbird` |
+| [Tailscale](https://tailscale.com/) | `1.98.9` | `amd64`, `arm64` | `stplr install nivora/tailscale` |
+| [Яндекс Браузер](https://browser.yandex.ru/) | `26.4.1.1110` | `amd64` | `stplr install nivora/yandex-browser-stable` |
+
+### Удалённый доступ
+
+| Приложение | Версия | Архитектуры | Установка |
+|:--|:--:|:--:|:--|
+| [Parsec](https://parsec.app/downloads) | `150-104a` | `amd64` | `stplr install nivora/parsec` |
+
+### AI и разработка
+
+| Приложение | Версия | Архитектуры | Установка |
+|:--|:--:|:--:|:--|
+| [Chatbox](https://chatboxai.app/ru) | `1.20.3` | `amd64`, `arm64` | `stplr install nivora/chatbox` |
+| [Claude](https://code.claude.com/docs/en/desktop-quickstart) | `1.24012.0` | `amd64`, `arm64` | `stplr install nivora/claude-desktop` |
+| [Codex](docs/packages/codex.md) | `26.721.31836` | `amd64` | `stplr install nivora/codex` |
+| [GitHub Desktop](docs/packages/github-desktop.md) | `3.6.3` | `amd64`, `arm64` | `stplr install nivora/github-desktop` |
+| [OpenCode](https://opencode.ai/) | `1.18.4` | `amd64`, `arm64` | `stplr install nivora/opencode` |
+
+### Рабочий стол
+
+| Приложение | Версия | Архитектуры | Установка |
+|:--|:--:|:--:|:--|
+| [Adwyra](https://github.com/Cheviiot/Adwyra) | `0.6.1` | `all` | `stplr install nivora/adwyra` |
+| [AniDesk](https://github.com/theDesConnet/AniDesk) | `0.0.1-beta.7` | `amd64` | `stplr install nivora/anidesk` |
+
+### Игры
+
+| Приложение | Версия | Архитектуры | Установка |
+|:--|:--:|:--:|:--|
+| [PineconeMC](https://pineconemc.com/) | `11.0.3` | `amd64`, `arm64` | `stplr install nivora/pineconemc` |
+| [Vual](https://github.com/Cheviiot/Vual) | `0.3.1` | `all` | `stplr install nivora/vual` |
+
+### Системные инструменты
+
+| Приложение | Версия | Архитектуры | Установка |
+|:--|:--:|:--:|:--|
+| [balenaEtcher](https://etcher.balena.io/) | `2.1.6` | `amd64` | `stplr install nivora/balena-etcher` |
+| [Fisher](https://github.com/jorgebucaran/fisher) | `4.4.8` | `all` | `stplr install nivora/fisher` |
+| [Nivora Stapler Helper](docs/packages/nivora-stplr.md) | `0.3.0` | `all` | `stplr install nivora/nivora-stplr` |
+| [Ventoy](docs/packages/ventoy.md) | `1.1.17` | `amd64`, `arm64` | `stplr install nivora/ventoy` |
+
+## Обновление
+
+```bash
+sudo stplr refresh
+sudo stplr upgrade
+```
+
+Рецепты сохраняют пути пользовательских конфигураций. Обычное обновление и
+удаление пакета не должно сбрасывать настройки или выполнять logout.
+
+## Безопасность и доверие
+
+- Каждый `Staplerfile` доступен для проверки.
+- Файлы загружаются из указанных upstream-источников.
+- SHA-256 проверяет целостность загрузки, но не делает upstream автоматически безопасным.
+- Условия проприетарных приложений определяются их разработчиками.
+- Наличие CI не обещает абсолютную безопасность или совместимость с любой системой.
+
+Подробнее: [модель доверия](docs/security-model.md) и [политика безопасности](SECURITY.md).
+
+## Для сопровождающих
+
+```bash
+tools/run_checks.sh
+tools/package_updates.sh check-all
+tools/clean_build.sh --all
+tools/verify_artifacts.sh --all
+tools/test_package_lifecycle.sh
+```
+
+Правила изменений описаны в [CONTRIBUTING.md](CONTRIBUTING.md), а порядок сопровождения — в
+[docs/maintenance.md](docs/maintenance.md).
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..5e5c53b
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,27 @@
+# Политика безопасности
+
+## Что относится к Nivora
+
+Сообщайте о проблемах в:
+
+- `Staplerfile`, wrappers и lifecycle-скриптах;
+- неверных source URL или checksum;
+- опасном поведении при установке, обновлении или удалении;
+- потере или неожиданной передаче пользовательских данных из-за логики пакета;
+- секретах, случайно попавших в репозиторий.
+
+Уязвимости самого приложения следует передавать upstream-разработчику.
+
+## Как сообщить
+
+Для обычной ошибки создайте [issue](https://github.com/Cheviiot/Nivora/issues) и укажите:
+
+- пакет и архитектуру;
+- дистрибутив и версию Stapler;
+- точную команду и полный текст ошибки;
+- ожидаемое и фактическое поведение.
+
+Не публикуйте пароли, API-ключи, setup keys, cookies и диагностические архивы без
+предварительной проверки.
+
+Подробная модель доверия: [docs/security-model.md](docs/security-model.md).
diff --git a/adwyra/.stapler/update-check b/adwyra/.stapler/update-check
new file mode 100755
index 0000000..efb050a
--- /dev/null
+++ b/adwyra/.stapler/update-check
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -euo pipefail
+
+package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+repo_root="$(cd "${package_dir}/.." && pwd)"
+exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")"
diff --git a/adwyra/LICENSE b/adwyra/LICENSE
new file mode 100644
index 0000000..7543550
--- /dev/null
+++ b/adwyra/LICENSE
@@ -0,0 +1,13 @@
+Сведения о лицензии пакета Adwyra
+
+Лицензия: GPL-3.0-or-later.
+Исходный проект: https://github.com/Cheviiot/Adwyra
+
+Нюансы пакета:
+- Пакет устанавливает исходный Python/GTK-проект Adwyra в системную файловую структуру.
+- Код приложения распространяется на условиях GNU GPL версии 3 или любой более поздней версии.
+- При распространении измененных версий нужно сохранять условия GPL и предоставлять соответствующий исходный код.
+- Пакет не добавляет закрытых бинарных компонентов.
+
+Полный официальный текст GPL-3.0:
+https://www.gnu.org/licenses/gpl-3.0.txt
diff --git a/adwyra/Staplerfile b/adwyra/Staplerfile
new file mode 100644
index 0000000..29c4c47
--- /dev/null
+++ b/adwyra/Staplerfile
@@ -0,0 +1,123 @@
+name='adwyra'
+version='0.6.1'
+release=3
+summary='Minimal GNOME application launcher'
+summary_ru='Минималистичный лаунчер приложений для GNOME'
+desc='Adwyra is a compact GTK 4/libadwaita application launcher for GNOME with an application grid, search, folders, pinned apps, drag-and-drop ordering, themes, hotkeys and plugin support.'
+desc_ru='Adwyra — компактный лаунчер приложений для GNOME на GTK 4/libadwaita с сеткой приложений, поиском, папками, закреплением, drag-and-drop сортировкой, темами, горячими клавишами и плагинами.'
+group='Graphical desktop/GNOME'
+homepage='https://github.com/Cheviiot/Adwyra'
+maintainer='Cheviiot '
+
+architectures=('all')
+
+license=('GPL-3.0-or-later')
+
+provides=()
+replaces=('adwyra')
+conflicts=()
+
+auto_reqprov_method="dirty"
+auto_req=0
+auto_prov=0
+
+# Зависимости для запуска
+deps=(
+ 'python3'
+ 'gtk4'
+ 'libadwaita'
+)
+
+# Скрипты установки/удаления
+scripts=(
+ ['postinstall']='postinstall.sh'
+ ['postremove']='postremove.sh'
+)
+deps_debian=(
+ 'python3'
+ 'python3-gi'
+ 'gir1.2-gtk-4.0'
+ 'gir1.2-adw-1'
+)
+deps_ubuntu=("${deps_debian[@]}")
+deps_fedora=(
+ 'python3'
+ 'python3-gobject'
+ 'gtk4'
+ 'libadwaita'
+)
+deps_arch=(
+ 'python'
+ 'python-gobject'
+ 'gtk4'
+ 'libadwaita'
+)
+deps_altlinux=(
+ 'python3'
+ 'python3-module-pygobject3'
+ 'libgtk4'
+ 'libadwaita'
+)
+deps_opensuse=(
+ 'python3'
+ 'python3-gobject'
+ 'gtk4'
+ 'libadwaita'
+)
+
+# Источники - используем архив для предотвращения проблем с кэшем
+sources=(
+ "https://github.com/Cheviiot/adwyra/archive/refs/tags/v${version}.tar.gz"
+ 'local:///LICENSE'
+)
+checksums=(
+ '4acb67460a6d302d5c60c369fe68e616baff2aeafb760c421cb65a54dc8df39a'
+ '4acd6128d4fbff11fd374e862520cf93aacad91650de1fe16fc4e424bd1ddc14'
+)
+
+# AppStream
+appstream_app_id='com.github.adwyra'
+
+package() {
+ # GitHub создаёт архив с именем репозитория (Adwyra)
+ src="${srcdir}/Adwyra-${version}"
+
+ # Бинарный файл
+ install -Dm755 "${src}/adwyra" "${pkgdir}/usr/bin/adwyra"
+
+ # Python модуль
+ install -dm755 "${pkgdir}/usr/lib/adwyra"
+ cp -r "${src}/src/adwyra" "${pkgdir}/usr/lib/adwyra/"
+ find "${pkgdir}/usr/lib/adwyra" -depth -path '*/__pycache__*' -delete
+
+ # Обновляем путь в бинарнике
+ sed -i 's|os.path.join(os.path.dirname(os.path.abspath(__file__)), "src")|'\''/usr/lib/adwyra'\''|' \
+ "${pkgdir}/usr/bin/adwyra"
+
+ # Desktop файл
+ install-desktop "${src}/data/com.github.adwyra.desktop"
+
+ # Иконки всех размеров
+ for size in 16 24 32 48 64 128 256 512; do
+ install -Dm644 "${src}/data/icons/hicolor/${size}x${size}/apps/com.github.adwyra.png" \
+ "${pkgdir}/usr/share/icons/hicolor/${size}x${size}/apps/com.github.adwyra.png"
+ done
+
+ # AppStream metainfo
+ install -Dm644 "${src}/data/com.github.adwyra.metainfo.xml" \
+ "${pkgdir}/usr/share/metainfo/com.github.adwyra.metainfo.xml"
+
+ # Лицензия
+ install-license "${src}/LICENSE" "adwyra/LICENSE.upstream"
+ install-license "${srcdir}/LICENSE" "adwyra/LICENSE.nivora"
+}
+
+files() {
+ files-find-binary
+ files-find-desktop
+ files-find-license
+ files-find \
+ "/usr/lib/adwyra/**/*" \
+ "/usr/share/icons/hicolor/*/apps/com.github.adwyra.png" \
+ "/usr/share/metainfo/com.github.adwyra.metainfo.xml"
+}
diff --git a/adwyra/postinstall.sh b/adwyra/postinstall.sh
new file mode 100755
index 0000000..3dab81c
--- /dev/null
+++ b/adwyra/postinstall.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+set -euo pipefail
+
+optional_refresh() {
+ local label=$1
+ shift
+ if ! "$@"; then
+ echo "Предупреждение: не удалось обновить ${label}." >&2
+ fi
+}
+
+# Обновляем кэш иконок
+if command -v gtk-update-icon-cache >/dev/null 2>&1; then
+ optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor
+fi
+
+# Обновляем desktop базу данных
+if command -v update-desktop-database >/dev/null 2>&1; then
+ optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications
+fi
diff --git a/adwyra/postremove.sh b/adwyra/postremove.sh
new file mode 100755
index 0000000..3dab81c
--- /dev/null
+++ b/adwyra/postremove.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+set -euo pipefail
+
+optional_refresh() {
+ local label=$1
+ shift
+ if ! "$@"; then
+ echo "Предупреждение: не удалось обновить ${label}." >&2
+ fi
+}
+
+# Обновляем кэш иконок
+if command -v gtk-update-icon-cache >/dev/null 2>&1; then
+ optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor
+fi
+
+# Обновляем desktop базу данных
+if command -v update-desktop-database >/dev/null 2>&1; then
+ optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications
+fi
diff --git a/adwyra/stapler-repo.toml b/adwyra/stapler-repo.toml
new file mode 100644
index 0000000..d35f9bf
--- /dev/null
+++ b/adwyra/stapler-repo.toml
@@ -0,0 +1 @@
+[inherit]
diff --git a/anidesk/.stapler/update-check b/anidesk/.stapler/update-check
new file mode 100755
index 0000000..efb050a
--- /dev/null
+++ b/anidesk/.stapler/update-check
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -euo pipefail
+
+package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+repo_root="$(cd "${package_dir}/.." && pwd)"
+exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")"
diff --git a/anidesk/.stapler/update-run b/anidesk/.stapler/update-run
new file mode 100755
index 0000000..af9ff2a
--- /dev/null
+++ b/anidesk/.stapler/update-run
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+set -euo pipefail
+
+new_version="${1:?new version is required}"
+package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+staplerfile="${package_dir}/Staplerfile"
+debian_version="${new_version/-beta./.beta.}"
+
+stplr-spec set-field --path "$staplerfile" version "$new_version"
+stplr-spec set-field --path "$staplerfile" release 1
+stplr-spec set-field --path "$staplerfile" debian_version "$debian_version"
+stplr-spec update-checksums --path "$staplerfile"
diff --git a/anidesk/LICENSE b/anidesk/LICENSE
new file mode 100644
index 0000000..1aa15d8
--- /dev/null
+++ b/anidesk/LICENSE
@@ -0,0 +1,21 @@
+Сведения о лицензии пакета AniDesk
+
+Лицензия: GPL-2.0-only.
+Исходный проект: https://github.com/theDesConnet/AniDesk
+Официальная страница релизов: https://github.com/theDesConnet/AniDesk/releases
+Сайт проекта: https://anidesk.ds1nc.ru
+
+Нюансы пакета:
+- Пакет перепаковывает официальный Linux deb-артефакт AniDesk из релиза theDesConnet/AniDesk.
+- AniDesk является неофициальным desktop-клиентом Anixart; сервис Anixart, учетные записи, API и сетевые функции не входят в пакет и регулируются правилами соответствующих сервисов.
+- Встроенный видеоплеер использует внешние источники и сетевые запросы приложения. Доступность отдельных источников зависит от upstream-приложения и внешних сервисов.
+- Anime4K-апскейл использует WebGPU/Vulkan. Для него нужны рабочие драйверы GPU, Vulkan loader и Mesa/проприетарный Vulkan-драйвер; без них базовый запуск приложения возможен, но ускорение видео может быть недоступно.
+- Команда `anidesk` запускает upstream-приложение без отключения GPU. Если из-за драйвера Vulkan/GLX приложение падает на старте, можно временно запустить `ANIDESK_DISABLE_GPU=1 anidesk`; этот режим отключает GPU-ускорение Chromium и не предназначен для Anime4K/WebGPU.
+- Upstream deb поставляет иконку `anidesk.png` как ICO-контейнер с неверным расширением. Пакет Nivora заменяет ее на PNG-экспорт того же изображения, чтобы desktop-меню корректно показывали иконку.
+- Upstream-приложение создает API-клиент только при старте renderer-страницы. Пакет Nivora добавляет runtime-патч: после смены endpoint API в настройках renderer автоматически перезагружается, чтобы контент сразу грузился с выбранного адреса.
+- Discord Rich Presence работает только при установленном и запущенном Discord-клиенте с доступным локальным IPC.
+- Пользовательские настройки, авторизация, кэш, история и загруженные данные создаются приложением в домашнем каталоге пользователя после запуска и не поставляются внутри пакета.
+- В `/usr/share/licenses/anidesk` дополнительно устанавливаются bundled-лицензии Electron/Chromium, которые поставляет upstream-артефакт.
+
+Полный официальный текст GPL-2.0:
+https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
diff --git a/anidesk/Staplerfile b/anidesk/Staplerfile
new file mode 100644
index 0000000..79e7ac1
--- /dev/null
+++ b/anidesk/Staplerfile
@@ -0,0 +1,236 @@
+name='anidesk'
+version='0.0.1-beta.7'
+debian_version='0.0.1.beta.7'
+release=5
+summary='Unofficial Anixart desktop client'
+summary_ru='Неофициальный desktop-клиент Anixart'
+group='Video/Players'
+desc='AniDesk is an unofficial Electron desktop client for Anixart with account support, a built-in video player and Anime4K WebGPU upscaling.'
+desc_ru='AniDesk — неофициальный Electron desktop-клиент Anixart с поддержкой аккаунта, встроенным видеоплеером и Anime4K-апскейлом через WebGPU.'
+homepage='https://github.com/theDesConnet/AniDesk'
+maintainer='DesConnet, hack1exe '
+
+architectures=('amd64')
+
+license=('GPL-2.0-only')
+
+provides=()
+replaces=('anidesk')
+conflicts=()
+
+auto_reqprov_method="dirty"
+auto_req=0
+auto_prov=0
+
+deps=()
+deps_debian=('libgtk-3-0' 'libnotify4' 'libnss3' 'xdg-utils' 'libatspi2.0-0' 'libdrm2' 'libgbm1' 'libxcb-dri3-0' 'libasound2' 'libvulkan1' 'mesa-vulkan-drivers')
+deps_ubuntu=("${deps_debian[@]}")
+deps_fedora=('gtk3' 'libnotify' 'nss' 'xdg-utils' 'at-spi2-core' 'libdrm' 'mesa-libgbm' 'libxcb' 'alsa-lib' 'vulkan-loader' 'mesa-vulkan-drivers')
+deps_arch=('gtk3' 'libnotify' 'nss' 'xdg-utils' 'at-spi2-core' 'libdrm' 'mesa' 'libxcb' 'alsa-lib' 'vulkan-icd-loader' 'vulkan-radeon')
+deps_opensuse=('gtk3' 'libnotify4' 'mozilla-nss' 'xdg-utils' 'at-spi2-core' 'libdrm2' 'libgbm1' 'libxcb-dri3-0' 'libasound2' 'libvulkan1' 'Mesa-vulkan-device-select')
+deps_altlinux=('libgtk+3' 'libnotify' 'libnss' 'xdg-utils' 'libat-spi2-core' 'libdrm' 'libgbm' 'libxcb' 'libalsa' 'libvulkan1' 'mesa-dri-drivers')
+
+build_deps=('binutils')
+build_deps_debian=('binutils' 'python3')
+build_deps_ubuntu=("${build_deps_debian[@]}")
+build_deps_fedora=('binutils' 'python3')
+build_deps_arch=('binutils' 'python')
+build_deps_opensuse=('binutils' 'python3')
+build_deps_altlinux=('binutils' 'python3')
+
+opt_deps=()
+opt_deps_debian=('gnome-keyring' 'discord')
+opt_deps_ubuntu=("${opt_deps_debian[@]}")
+opt_deps_fedora=('gnome-keyring' 'discord')
+opt_deps_arch=('gnome-keyring' 'discord')
+opt_deps_opensuse=('gnome-keyring' 'discord')
+opt_deps_altlinux=('gnome-keyring')
+
+sources=(
+ "https://github.com/theDesConnet/AniDesk/releases/download/v${version}/anidesk_${debian_version}_amd64.deb?~archive=false&~name=anidesk.deb"
+ "local:///anidesk.png"
+ "local:///anidesk-wrapper"
+ "local:///LICENSE"
+)
+
+checksums=(
+ 'sha256:70fc1207824757f7ddab63f9e210f31bb907ea43dee3d2827fe79c3ef00252b6'
+ 'sha256:6d7791424bd7db712b121f7e66206289b1d31eb2a8ea998d9aeced2c7075dfe8'
+ 'sha256:f58b9f720bd63ff49053f949dd3f0a738afc2aa2cee57fedbd2b73440c123721'
+ 'sha256:892e6b268f44a850a102cc6367fec45853fe3556840d44a6106f08487553e6c4'
+)
+
+scripts=(
+ ['postinstall']='postinstall.sh'
+ ['postremove']='postremove.sh'
+)
+
+package() {
+ cd "${srcdir}"
+
+ mkdir -p "${srcdir}/extracted"
+ ar x anidesk.deb
+ tar -xf data.tar.* -C "${srcdir}/extracted"
+
+ install -dm755 "${pkgdir}/usr/lib"
+ cp -a "${srcdir}/extracted/usr/lib/anidesk" "${pkgdir}/usr/lib/"
+ chmod 0755 "${pkgdir}/usr/lib/anidesk/chrome-sandbox"
+ chmod 755 "${pkgdir}/usr/lib/anidesk/anidesk"
+
+ python3 - "${pkgdir}/usr/lib/anidesk/resources/app.asar" <<'PY'
+import copy
+import hashlib
+import json
+import struct
+import sys
+from pathlib import Path
+
+asar_path = Path(sys.argv[1])
+data = asar_path.read_bytes()
+_, _, _, header_size = struct.unpack("{x.set(t),set(o,!0),'
+ 'baseSettings.restartRequired=!0}'
+ )
+ new = (
+ 'onChangeCallback:(e,t)=>{get(n)!==t&&(x.set(t),set(o,!0),'
+ 'baseSettings.restartRequired=!0,queueMicrotask(()=>location.reload()))}'
+ )
+ if old not in text:
+ raise SystemExit("AniDesk ASAR patch failed: endpoint dropdown marker not found")
+ patched[path_name] = text.replace(old, new, 1).encode()
+ bundle_patch_applied = True
+ elif path_name == "src/app/settings-pages/SettingsApp.svelte":
+ text = content.decode()
+ old = """ onChangeCallback={(e, v) => {
+ endpointUrlRaw.set(v);
+ restartRequired = true;
+ baseSettings.restartRequired = true;
+ }}
+"""
+ new = """ onChangeCallback={(e, v) => {
+ if (endpointUrl === v) {
+ return;
+ }
+ endpointUrlRaw.set(v);
+ restartRequired = true;
+ baseSettings.restartRequired = true;
+ queueMicrotask(() => window.location.reload());
+ }}
+"""
+ if old not in text:
+ raise SystemExit("AniDesk ASAR patch failed: endpoint source marker not found")
+ patched[path_name] = text.replace(old, new, 1).encode()
+ source_patch_applied = True
+
+if not bundle_patch_applied:
+ raise SystemExit("AniDesk ASAR patch failed: runtime bundle was not patched")
+if not source_patch_applied:
+ raise SystemExit("AniDesk ASAR patch failed: source view was not patched")
+
+
+def rebuild(node, prefix="", offset_state=None, payload=None):
+ if offset_state is None:
+ offset_state = {"offset": 0}
+ if payload is None:
+ payload = []
+ new_node = copy.deepcopy(node)
+ for name, entry in list(new_node.get("files", {}).items()):
+ path_name = f"{prefix}{name}"
+ if "files" in entry:
+ new_node["files"][name] = rebuild(
+ entry, f"{path_name}/", offset_state, payload
+ )
+ continue
+ content = None
+ for candidate_path, candidate_content in entries:
+ if candidate_path == path_name:
+ content = patched.get(path_name, candidate_content)
+ break
+ if content is None:
+ continue
+ entry["size"] = len(content)
+ entry["offset"] = str(offset_state["offset"])
+ digest = hashlib.sha256(content).hexdigest()
+ entry["integrity"] = {
+ "algorithm": "SHA256",
+ "hash": digest,
+ "blockSize": 4194304,
+ "blocks": [digest],
+ }
+ offset_state["offset"] += len(content)
+ payload.append(content)
+ if prefix == "":
+ return new_node, payload
+ return new_node
+
+
+new_header, payload = rebuild(header)
+header_json = json.dumps(new_header, separators=(",", ":")).encode()
+header_padded_size = (len(header_json) + 3) & ~3
+asar_path.write_bytes(
+ struct.pack("&2
+ fi
+}
+
+sandbox=/usr/lib/anidesk/chrome-sandbox
+if [[ ! -f "$sandbox" ]]; then
+ echo "Ошибка: не найден Electron sandbox: ${sandbox}" >&2
+ exit 1
+fi
+if [[ -L /proc/self/ns/user ]] && command -v unshare >/dev/null 2>&1 \
+ && unshare --user true 2>/dev/null; then
+ chmod 0755 "$sandbox"
+else
+ chmod 4755 "$sandbox"
+fi
+
+if command -v update-desktop-database >/dev/null 2>&1; then
+ optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications
+fi
+if command -v gtk-update-icon-cache >/dev/null 2>&1; then
+ optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor
+fi
+if command -v kbuildsycoca6 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca6 --noincremental
+elif command -v kbuildsycoca5 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca5 --noincremental
+fi
diff --git a/anidesk/postremove.sh b/anidesk/postremove.sh
new file mode 100755
index 0000000..44d2528
--- /dev/null
+++ b/anidesk/postremove.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+set -euo pipefail
+
+optional_refresh() {
+ local label=$1
+ shift
+ if ! "$@"; then
+ echo "Предупреждение: не удалось обновить ${label}." >&2
+ fi
+}
+
+if command -v update-desktop-database >/dev/null 2>&1; then
+ optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications
+fi
+if command -v gtk-update-icon-cache >/dev/null 2>&1; then
+ optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor
+fi
+if command -v kbuildsycoca6 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca6 --noincremental
+elif command -v kbuildsycoca5 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca5 --noincremental
+fi
diff --git a/anidesk/stapler-repo.toml b/anidesk/stapler-repo.toml
new file mode 100644
index 0000000..9c222ce
--- /dev/null
+++ b/anidesk/stapler-repo.toml
@@ -0,0 +1 @@
+include = "../stapler-repo.toml"
diff --git a/balena-etcher/.stapler/update-check b/balena-etcher/.stapler/update-check
new file mode 100755
index 0000000..efb050a
--- /dev/null
+++ b/balena-etcher/.stapler/update-check
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -euo pipefail
+
+package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+repo_root="$(cd "${package_dir}/.." && pwd)"
+exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")"
diff --git a/balena-etcher/Staplerfile b/balena-etcher/Staplerfile
new file mode 100644
index 0000000..42538b7
--- /dev/null
+++ b/balena-etcher/Staplerfile
@@ -0,0 +1,144 @@
+name='balena-etcher'
+version='2.1.6'
+release=1
+summary='Safe SD card and USB drive image flasher'
+summary_ru='Безопасная запись образов на SD-карты и USB-накопители'
+group='System/Configuration/Hardware'
+desc='balenaEtcher is a graphical utility for writing operating-system images to SD cards and USB drives with validation after flashing.'
+desc_ru='balenaEtcher — графическая утилита для записи образов операционных систем на SD-карты и USB-накопители с проверкой данных после прошивки.'
+homepage='https://etcher.balena.io/'
+maintainer='Balena Ltd. '
+
+architectures=('amd64')
+
+license=('Apache-2.0')
+
+provides=()
+replaces=('balena-etcher')
+conflicts=()
+
+# Electron и нативный helper поставляются в официальном DEB.
+auto_reqprov_method="dirty"
+auto_req=0
+auto_prov=0
+
+deps=()
+deps_debian=(
+ 'libgtk-3-0' 'libnotify4' 'libnss3' 'libxtst6' 'xdg-utils'
+ 'libatspi2.0-0' 'libdrm2' 'libgbm1' 'libxcb-dri3-0' 'libasound2'
+ 'libatk1.0-0' 'libc6' 'libcairo2' 'libcups2' 'libdbus-1-3' 'libexpat1'
+ 'libfontconfig1' 'libfreetype6' 'libgcc1' 'libgdk-pixbuf2.0-0 | libgdk-pixbuf-2.0-0'
+ 'libglib2.0-0' 'liblzma5' 'libnspr4' 'libpango1.0-0 | libpango-1.0-0'
+ 'libstdc++6' 'libx11-6' 'libxcomposite1' 'libxcursor1' 'libxdamage1'
+ 'libxext6' 'libxfixes3' 'libxi6' 'libxrandr2' 'libxrender1' 'libxss1'
+ 'kde-cli-tools | kde-runtime | trash-cli | libglib2.0-bin | gvfs-bin'
+ 'polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1'
+)
+deps_ubuntu=("${deps_debian[@]}")
+deps_fedora=(
+ 'gtk3' 'libnotify' 'nss' 'libXtst' 'xdg-utils' 'at-spi2-core'
+ 'libdrm' 'mesa-libgbm' 'libxcb' 'alsa-lib' 'atk' 'glibc' 'cairo'
+ 'cups-libs' 'dbus-libs' 'expat' 'fontconfig' 'freetype' 'libgcc'
+ 'gdk-pixbuf2' 'glib2' 'xz-libs' 'nspr' 'pango' 'libstdc++' 'libX11'
+ 'libXcomposite' 'libXcursor' 'libXdamage' 'libXext' 'libXfixes' 'libXi'
+ 'libXrandr' 'libXrender' 'libXScrnSaver' 'polkit'
+)
+deps_arch=(
+ 'gtk3' 'libnotify' 'nss' 'libxtst' 'xdg-utils' 'at-spi2-core'
+ 'libdrm' 'mesa' 'libxcb' 'alsa-lib' 'atk' 'glibc' 'cairo' 'cups'
+ 'dbus' 'expat' 'fontconfig' 'freetype2' 'gcc-libs' 'gdk-pixbuf2'
+ 'glib2' 'xz' 'nspr' 'pango' 'libx11' 'libxcomposite' 'libxcursor'
+ 'libxdamage' 'libxext' 'libxfixes' 'libxi' 'libxrandr' 'libxrender'
+ 'libxss' 'polkit'
+)
+deps_opensuse=(
+ 'gtk3' 'libnotify4' 'mozilla-nss' 'libXtst6' 'xdg-utils' 'at-spi2-core'
+ 'libdrm2' 'libgbm1' 'libxcb-dri3-0' 'libasound2' 'libatk-1_0-0'
+ 'glibc' 'libcairo2' 'libcups2' 'libdbus-1-3' 'libexpat1'
+ 'fontconfig' 'libfreetype6' 'libgcc_s1' 'gdk-pixbuf-loader-rsvg'
+ 'glib2' 'liblzma5' 'mozilla-nspr' 'libpango-1_0-0' 'libstdc++6'
+ 'libX11-6' 'libXcomposite1' 'libXcursor1' 'libXdamage1' 'libXext6'
+ 'libXfixes3' 'libXi6' 'libXrandr2' 'libXrender1' 'libXss1' 'polkit'
+)
+deps_altlinux=(
+ 'libgtk+3' 'libnotify' 'libnss' 'libXtst' 'xdg-utils' 'libat-spi2-core'
+ 'libdrm' 'libgbm' 'libxcb' 'libalsa' 'libatk' 'glibc' 'libcairo'
+ 'libcups' 'libdbus' 'libexpat' 'fontconfig' 'libfreetype' 'libgcc1'
+ 'libgdk-pixbuf' 'glib2' 'liblzma' 'libnspr' 'libpango' 'libstdc++6'
+ 'libX11' 'libXcomposite' 'libXcursor' 'libXdamage' 'libXext' 'libXfixes'
+ 'libXi' 'libXrandr' 'libXrender' 'libXScrnSaver' 'polkit'
+)
+
+opt_deps=()
+opt_deps_debian=()
+opt_deps_ubuntu=("${opt_deps_debian[@]}")
+opt_deps_fedora=('polkit-gnome | polkit-kde' 'gvfs')
+opt_deps_arch=('polkit-gnome | polkit-kde-agent' 'gvfs')
+opt_deps_opensuse=('polkit-gnome | polkit-kde-agent-6' 'glib2-tools')
+opt_deps_altlinux=(
+ 'polkit-gnome | polkit-kde-agent | lxqt-policykit | xfce-polkit'
+ 'gvfs'
+)
+
+build_deps=('binutils')
+
+sources=(
+ "https://github.com/balena-io/etcher/releases/download/v${version}/balena-etcher_${version}_amd64.deb?~archive=false&~name=balena-etcher.deb"
+ "https://raw.githubusercontent.com/balena-io/etcher/v${version}/LICENSE?~archive=false&~name=LICENSE"
+)
+
+checksums=(
+ 'sha256:2bdebb46c9f750a9abf11c188ff69a405b4a4fed114333d634c3b3fe59a64057'
+ 'sha256:39445b459f86621683f9731fb6a7d070819dc379840e8ef52f62bb1c68942291'
+)
+
+scripts=(
+ ['postinstall']='postinstall.sh'
+ ['postremove']='postremove.sh'
+)
+
+package() {
+ cd "${srcdir}"
+
+ mkdir -p "${srcdir}/extracted"
+ ar x balena-etcher.deb
+ tar -xf data.tar.* -C "${srcdir}/extracted"
+
+ install -dm755 "${pkgdir}/usr/lib"
+ cp -a "${srcdir}/extracted/usr/lib/balena-etcher" "${pkgdir}/usr/lib/"
+ chmod 0755 "${pkgdir}/usr/lib/balena-etcher/chrome-sandbox"
+ chmod 0755 \
+ "${pkgdir}/usr/lib/balena-etcher/balena-etcher" \
+ "${pkgdir}/usr/lib/balena-etcher/resources/etcher-util"
+
+ # Upstream DEB содержит balenaEtcher-ссылку на каталог GitHub Actions.
+ # Сохраняем совместимое имя, но направляем его на локальный executable.
+ ln -sfn balena-etcher "${pkgdir}/usr/lib/balena-etcher/balenaEtcher"
+ install -dm755 "${pkgdir}/usr/bin"
+ ln -sfn ../lib/balena-etcher/balena-etcher \
+ "${pkgdir}/usr/bin/balena-etcher"
+
+ install -Dm644 \
+ "${srcdir}/extracted/usr/share/applications/balena-etcher.desktop" \
+ "${pkgdir}/usr/share/applications/balena-etcher.desktop"
+ install -Dm644 \
+ "${srcdir}/extracted/usr/share/pixmaps/balena-etcher.png" \
+ "${pkgdir}/usr/share/pixmaps/balena-etcher.png"
+
+ install-license LICENSE "balena-etcher/LICENSE"
+ install-license \
+ "${srcdir}/extracted/usr/share/doc/balena-etcher/copyright" \
+ "balena-etcher/copyright"
+ install-license \
+ "${srcdir}/extracted/usr/lib/balena-etcher/LICENSES.chromium.html" \
+ "balena-etcher/LICENSES.chromium.html"
+}
+
+files() {
+ files-find-binary
+ files-find-desktop
+ files-find-license
+ files-find \
+ "/usr/lib/balena-etcher/**/*" \
+ "/usr/share/pixmaps/balena-etcher.png"
+}
diff --git a/balena-etcher/postinstall.sh b/balena-etcher/postinstall.sh
new file mode 100755
index 0000000..b36c9aa
--- /dev/null
+++ b/balena-etcher/postinstall.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+set -euo pipefail
+
+optional_refresh() {
+ local label=$1
+ shift
+ if ! "$@"; then
+ echo "Предупреждение: не удалось обновить ${label}." >&2
+ fi
+}
+
+sandbox=/usr/lib/balena-etcher/chrome-sandbox
+if [[ ! -f "$sandbox" ]]; then
+ echo "Ошибка: не найден Electron sandbox: ${sandbox}" >&2
+ exit 1
+fi
+if [[ -L /proc/self/ns/user ]] && command -v unshare >/dev/null 2>&1 \
+ && unshare --user true 2>/dev/null; then
+ chmod 0755 "$sandbox"
+else
+ chmod 4755 "$sandbox"
+fi
+
+if command -v update-desktop-database >/dev/null 2>&1; then
+ optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications
+fi
+if command -v kbuildsycoca6 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca6 --noincremental
+elif command -v kbuildsycoca5 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca5 --noincremental
+fi
diff --git a/balena-etcher/postremove.sh b/balena-etcher/postremove.sh
new file mode 100755
index 0000000..684bfe1
--- /dev/null
+++ b/balena-etcher/postremove.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+set -euo pipefail
+
+optional_refresh() {
+ local label=$1
+ shift
+ if ! "$@"; then
+ echo "Предупреждение: не удалось обновить ${label}." >&2
+ fi
+}
+
+if command -v update-desktop-database >/dev/null 2>&1; then
+ optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications
+fi
+if command -v kbuildsycoca6 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca6 --noincremental
+elif command -v kbuildsycoca5 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca5 --noincremental
+fi
diff --git a/balena-etcher/stapler-repo.toml b/balena-etcher/stapler-repo.toml
new file mode 100644
index 0000000..9c222ce
--- /dev/null
+++ b/balena-etcher/stapler-repo.toml
@@ -0,0 +1 @@
+include = "../stapler-repo.toml"
diff --git a/chatbox/.stapler/update-check b/chatbox/.stapler/update-check
new file mode 100755
index 0000000..efb050a
--- /dev/null
+++ b/chatbox/.stapler/update-check
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -euo pipefail
+
+package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+repo_root="$(cd "${package_dir}/.." && pwd)"
+exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")"
diff --git a/chatbox/LICENSE b/chatbox/LICENSE
new file mode 100644
index 0000000..dd97e7f
--- /dev/null
+++ b/chatbox/LICENSE
@@ -0,0 +1,20 @@
+Сведения о лицензии пакета Chatbox
+
+Лицензия: GPL-3.0-only.
+Исходный проект: https://github.com/chatboxai/chatbox
+Официальный сайт: https://chatboxai.app/ru
+Официальная страница загрузки: https://chatboxai.app/ru
+Условия использования сервиса: https://chatboxai.app/ru/terms
+Политика конфиденциальности: https://chatboxai.app/ru/privacy
+
+Нюансы пакета:
+- Пакет перепаковывает официальный Linux deb-артефакт Chatbox Desktop из релиза `chatboxai/chatbox`.
+- Chatbox Community Edition распространяется как открытый клиент под GPL-3.0; bundled-зависимости Electron/Chromium устанавливаются вместе с upstream-артефактом.
+- Последние релизы Chatbox могут выходить без Linux-артефактов. Пакет Nivora использует последний upstream-релиз, где реально опубликованы Linux deb-файлы для amd64 и arm64.
+- Chatbox может работать с Chatbox AI Service, OpenAI, Claude, Gemini, Ollama и другими внешними провайдерами. Учетные записи, API-ключи, подписки, сетевые функции и модели регулируются условиями соответствующих сервисов.
+- История чатов, настройки, кэш, авторизация и локальные базы данных создаются приложением в домашнем каталоге пользователя после запуска и не поставляются внутри пакета.
+- Пакет добавляет команды `chatbox`, `chatbox-ai` и `chatbox-desktop` для совместимого запуска приложения.
+- В `/usr/share/licenses/chatbox` дополнительно устанавливаются bundled-лицензии Electron/Chromium, если они присутствуют в upstream-артефакте.
+
+Полный официальный текст GPL-3.0:
+https://www.gnu.org/licenses/gpl-3.0.txt
diff --git a/chatbox/Staplerfile b/chatbox/Staplerfile
new file mode 100644
index 0000000..eedaf7a
--- /dev/null
+++ b/chatbox/Staplerfile
@@ -0,0 +1,115 @@
+name='chatbox'
+version='1.20.3'
+release=3
+summary='Desktop AI client for multiple LLM providers'
+summary_ru='Десктопный AI-клиент для разных LLM-провайдеров'
+group='Development/Tools'
+desc='Chatbox is a desktop AI client for Chatbox AI, OpenAI, Claude, Gemini, Ollama and other LLM providers with local chat history and file support.'
+desc_ru='Chatbox — десктопный AI-клиент для Chatbox AI, OpenAI, Claude, Gemini, Ollama и других LLM-провайдеров с локальной историей чатов и поддержкой файлов.'
+homepage='https://chatboxai.app/ru'
+maintainer='Chatbox '
+
+architectures=('amd64' 'arm64')
+
+license=('GPL-3.0-only')
+
+provides=()
+replaces=('chatbox')
+conflicts=()
+
+auto_reqprov_method="dirty"
+auto_req=0
+auto_prov=0
+
+deps=()
+deps_debian=('libgtk-3-0' 'libnotify4' 'libnss3' 'libxss1' 'libxtst6' 'xdg-utils' 'libatspi2.0-0' 'libuuid1' 'libsecret-1-0' 'libdrm2' 'libgbm1' 'libxcb-dri3-0' 'libasound2')
+deps_ubuntu=("${deps_debian[@]}")
+deps_fedora=('gtk3' 'libnotify' 'nss' 'libXScrnSaver' 'libXtst' 'xdg-utils' 'at-spi2-core' 'libuuid' 'libsecret' 'libdrm' 'mesa-libgbm' 'libxcb' 'alsa-lib')
+deps_arch=('gtk3' 'libnotify' 'nss' 'libxss' 'libxtst' 'xdg-utils' 'at-spi2-core' 'libutil-linux' 'libsecret' 'libdrm' 'mesa' 'libxcb' 'alsa-lib')
+deps_opensuse=('gtk3' 'libnotify4' 'mozilla-nss' 'libXScrnSaver1' 'libXtst6' 'xdg-utils' 'at-spi2-core' 'libuuid1' 'libsecret-1-0' 'libdrm2' 'libgbm1' 'libxcb-dri3-0' 'libasound2')
+deps_altlinux=('libgtk+3' 'libnotify' 'libnss' 'libXScrnSaver' 'libXtst' 'xdg-utils' 'libat-spi2-core' 'libuuid' 'libsecret' 'libdrm' 'libgbm' 'libxcb' 'libalsa')
+
+opt_deps=()
+opt_deps_debian=('libappindicator3-1' 'gnome-keyring')
+opt_deps_ubuntu=("${opt_deps_debian[@]}")
+opt_deps_fedora=('libappindicator-gtk3' 'gnome-keyring')
+opt_deps_arch=('libappindicator-gtk3' 'gnome-keyring')
+opt_deps_opensuse=('libappindicator3-1' 'gnome-keyring')
+opt_deps_altlinux=('libappindicator-gtk3' 'gnome-keyring')
+
+build_deps=('binutils')
+
+sources=(
+ "https://github.com/chatboxai/chatbox/releases/download/v${version}/Chatbox-${version}-amd64.deb?~archive=false&~name=chatbox.deb"
+ 'local:///LICENSE'
+)
+
+sources_arm64=(
+ "https://github.com/chatboxai/chatbox/releases/download/v${version}/Chatbox-${version}-arm64.deb?~archive=false&~name=chatbox.deb"
+ 'local:///LICENSE'
+)
+
+checksums=(
+ 'sha256:7a5b86200c180142ee7d4ed5d56edaabba3a3b37b3b8d8c4eed32678d327e141'
+ 'sha256:9408c95380fe46177c49bb9ba845dc0d706e88cdd2d637b5d2b9168730713acd'
+)
+
+checksums_arm64=(
+ 'sha256:909c8ccbb5965a63967af2428210456eb2c9fb7baf88a1a6971e3b07ead81d60'
+ 'sha256:9408c95380fe46177c49bb9ba845dc0d706e88cdd2d637b5d2b9168730713acd'
+)
+
+scripts=(
+ ['postinstall']='postinstall.sh'
+ ['postremove']='postremove.sh'
+)
+
+package() {
+ cd "${srcdir}"
+
+ mkdir -p "${srcdir}/extracted"
+ ar x chatbox.deb
+ tar -xf data.tar.* -C "${srcdir}/extracted"
+
+ install -dm755 "${pkgdir}/opt"
+ cp -a "${srcdir}/extracted/opt/Chatbox" "${pkgdir}/opt/"
+ chmod 0755 "${pkgdir}/opt/Chatbox/chrome-sandbox"
+ chmod 755 "${pkgdir}/opt/Chatbox/xyz.chatboxapp.app"
+
+ install -dm755 "${pkgdir}/usr/share"
+ cp -a "${srcdir}/extracted/usr/share/applications" "${pkgdir}/usr/share/"
+ cp -a "${srcdir}/extracted/usr/share/icons" "${pkgdir}/usr/share/"
+
+ if [[ -d "${srcdir}/extracted/usr/share/doc" ]]; then
+ cp -a "${srcdir}/extracted/usr/share/doc" "${pkgdir}/usr/share/"
+ fi
+
+ sed -i 's/^Categories=.*/Categories=Development;/' \
+ "${pkgdir}/usr/share/applications/xyz.chatboxapp.app.desktop"
+
+ install -dm755 "${pkgdir}/usr/bin"
+ ln -sfn /opt/Chatbox/xyz.chatboxapp.app "${pkgdir}/usr/bin/chatbox"
+ ln -sfn /opt/Chatbox/xyz.chatboxapp.app "${pkgdir}/usr/bin/chatbox-ai"
+ ln -sfn /opt/Chatbox/xyz.chatboxapp.app "${pkgdir}/usr/bin/chatbox-desktop"
+
+ install-license LICENSE "chatbox/LICENSE.nivora"
+
+ if [[ -f "${srcdir}/extracted/opt/Chatbox/LICENSE.electron.txt" ]]; then
+ install-license "${srcdir}/extracted/opt/Chatbox/LICENSE.electron.txt" \
+ "chatbox/LICENSE.electron.txt"
+ fi
+ if [[ -f "${srcdir}/extracted/opt/Chatbox/LICENSES.chromium.html" ]]; then
+ install-license "${srcdir}/extracted/opt/Chatbox/LICENSES.chromium.html" \
+ "chatbox/LICENSES.chromium.html"
+ fi
+}
+
+files() {
+ files-find-binary
+ files-find-desktop
+ files-find-license
+ files-find \
+ "/opt/Chatbox/**/*" \
+ "/usr/share/doc/xyz.chatboxapp.app/*" \
+ "/usr/share/icons/hicolor/*/apps/xyz.chatboxapp.app.png"
+}
diff --git a/chatbox/postinstall.sh b/chatbox/postinstall.sh
new file mode 100755
index 0000000..e0793ed
--- /dev/null
+++ b/chatbox/postinstall.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+set -euo pipefail
+
+optional_refresh() {
+ local label=$1
+ shift
+ if ! "$@"; then
+ echo "Предупреждение: не удалось обновить ${label}." >&2
+ fi
+}
+
+sandbox=/opt/Chatbox/chrome-sandbox
+if [[ ! -f "$sandbox" ]]; then
+ echo "Ошибка: не найден Electron sandbox: ${sandbox}" >&2
+ exit 1
+fi
+if [[ -L /proc/self/ns/user ]] && command -v unshare >/dev/null 2>&1 \
+ && unshare --user true 2>/dev/null; then
+ chmod 0755 "$sandbox"
+else
+ chmod 4755 "$sandbox"
+fi
+
+if command -v update-desktop-database >/dev/null 2>&1; then
+ optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications
+fi
+if command -v gtk-update-icon-cache >/dev/null 2>&1; then
+ optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor
+fi
+if command -v kbuildsycoca6 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca6 --noincremental
+elif command -v kbuildsycoca5 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca5 --noincremental
+fi
diff --git a/chatbox/postremove.sh b/chatbox/postremove.sh
new file mode 100755
index 0000000..44d2528
--- /dev/null
+++ b/chatbox/postremove.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+set -euo pipefail
+
+optional_refresh() {
+ local label=$1
+ shift
+ if ! "$@"; then
+ echo "Предупреждение: не удалось обновить ${label}." >&2
+ fi
+}
+
+if command -v update-desktop-database >/dev/null 2>&1; then
+ optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications
+fi
+if command -v gtk-update-icon-cache >/dev/null 2>&1; then
+ optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor
+fi
+if command -v kbuildsycoca6 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca6 --noincremental
+elif command -v kbuildsycoca5 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca5 --noincremental
+fi
diff --git a/chatbox/stapler-repo.toml b/chatbox/stapler-repo.toml
new file mode 100644
index 0000000..9c222ce
--- /dev/null
+++ b/chatbox/stapler-repo.toml
@@ -0,0 +1 @@
+include = "../stapler-repo.toml"
diff --git a/clash-verge-rev/.stapler/update-check b/clash-verge-rev/.stapler/update-check
new file mode 100755
index 0000000..efb050a
--- /dev/null
+++ b/clash-verge-rev/.stapler/update-check
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -euo pipefail
+
+package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+repo_root="$(cd "${package_dir}/.." && pwd)"
+exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")"
diff --git a/clash-verge-rev/LICENSE b/clash-verge-rev/LICENSE
new file mode 100644
index 0000000..fd66f7d
--- /dev/null
+++ b/clash-verge-rev/LICENSE
@@ -0,0 +1,14 @@
+Сведения о лицензии пакета Clash Verge Rev
+
+Лицензия: GPL-3.0-only.
+Исходный проект: https://github.com/clash-verge-rev/clash-verge-rev
+Официальный сайт: https://www.clashverge.dev
+
+Нюансы пакета:
+- Пакет перепаковывает официальный deb-артефакт Clash Verge Rev для Linux.
+- Приложение распространяется на условиях GNU GPL версии 3.
+- GUI-клиент работает с профилями Mihomo/Clash; сами профили, подписки и внешние серверы в пакет не входят.
+- При дальнейшем распространении измененных сборок нужно соблюдать требования GPL-3.0-only.
+
+Полный официальный текст GPL-3.0:
+https://www.gnu.org/licenses/gpl-3.0.txt
diff --git a/clash-verge-rev/Staplerfile b/clash-verge-rev/Staplerfile
new file mode 100644
index 0000000..1a3912f
--- /dev/null
+++ b/clash-verge-rev/Staplerfile
@@ -0,0 +1,91 @@
+name='clash-verge-rev'
+version=2.5.2
+release=2
+summary='Tauri GUI client for Mihomo/Clash proxy profiles'
+summary_ru='GUI-клиент на Tauri для профилей Mihomo/Clash'
+group='Networking/Other'
+desc='Clash Verge Rev is a modern cross-platform GUI client based on Tauri for a tailored proxy experience with Mihomo/Clash profiles.'
+desc_ru='Clash Verge Rev — современный кроссплатформенный GUI-клиент на Tauri для удобной работы с профилями прокси Mihomo/Clash.'
+homepage='https://www.clashverge.dev'
+maintainer='clash-verge-rev contributors '
+
+architectures=('amd64' 'arm64')
+
+license=('GPL-3.0-only')
+
+provides=()
+replaces=('clash-verge-rev')
+conflicts=()
+
+auto_reqprov_method="dirty"
+auto_req=0
+auto_prov=0
+
+deps=()
+deps_debian=('libc6' 'libgtk-3-0' 'libwebkit2gtk-4.1-0' 'libayatana-appindicator3-1' 'libssl3 | libssl3t64' 'libcap2-bin')
+deps_ubuntu=("${deps_debian[@]}")
+deps_fedora=('glibc' 'gtk3' 'webkit2gtk4.1' 'libayatana-appindicator' 'openssl-libs' 'libcap')
+deps_arch=('glibc' 'gtk3' 'webkit2gtk-4.1' 'libayatana-appindicator' 'openssl' 'libcap')
+deps_opensuse=('glibc' 'gtk3' 'libwebkit2gtk-4_1-0' 'libayatana-appindicator3-1' 'libopenssl3' 'libcap-progs')
+deps_altlinux=('glibc' 'libgtk+3' 'libwebkit2gtk4.1' 'libayatana-appindicator3-1' 'libssl3' 'libcap-utils')
+
+build_deps=('binutils')
+
+sources=(
+ "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v${version}/Clash.Verge_${version}_amd64.deb?~archive=false&~name=clash-verge.deb"
+ 'local:///LICENSE'
+)
+
+sources_arm64=(
+ "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v${version}/Clash.Verge_${version}_arm64.deb?~archive=false&~name=clash-verge.deb"
+ 'local:///LICENSE'
+)
+
+checksums=(
+ 'sha256:035c83ed14b16df1dd397e5d710b34bedd5d27beb6678549ceaeeadf9bc167ed'
+ 'sha256:5b886bd36073c4604b97926ad81ad26ea64b9172f47f014c127b6a05f80e37a9'
+)
+
+checksums_arm64=(
+ 'sha256:598a5a852d7bf9dc40a976780ef2afc9a4e5bfe7b99533e5f956f9e2f9def72f'
+ 'sha256:5b886bd36073c4604b97926ad81ad26ea64b9172f47f014c127b6a05f80e37a9'
+)
+
+scripts=(
+ ['postinstall']='postinstall.sh'
+ ['postremove']='postremove.sh'
+)
+
+package() {
+ cd "${srcdir}"
+
+ mkdir -p "${srcdir}/extracted"
+ ar x clash-verge.deb
+ tar -xf data.tar.* -C "${srcdir}/extracted"
+
+ for path in usr opt etc; do
+ if [[ -d "${srcdir}/extracted/${path}" ]]; then
+ mkdir -p "${pkgdir}/${path}"
+ cp -a "${srcdir}/extracted/${path}/." "${pkgdir}/${path}/"
+ fi
+ done
+
+ chmod 755 \
+ "${pkgdir}/usr/bin/clash-verge" \
+ "${pkgdir}/usr/bin/clash-verge-service" \
+ "${pkgdir}/usr/bin/clash-verge-service-install" \
+ "${pkgdir}/usr/bin/clash-verge-service-uninstall" \
+ "${pkgdir}/usr/bin/verge-mihomo" \
+ "${pkgdir}/usr/bin/verge-mihomo-alpha"
+
+ install-license "${srcdir}/LICENSE" "clash-verge-rev/LICENSE.nivora"
+}
+
+files() {
+ files-find-binary
+ files-find-desktop
+ files-find-license
+ files-find \
+ "/usr/lib/Clash Verge/**/*" \
+ "/usr/share/icons/**/*"
+}
diff --git a/clash-verge-rev/postinstall.sh b/clash-verge-rev/postinstall.sh
new file mode 100755
index 0000000..2462783
--- /dev/null
+++ b/clash-verge-rev/postinstall.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+set -euo pipefail
+
+optional_refresh() {
+ local label=$1
+ shift
+ if ! "$@"; then
+ echo "Предупреждение: не удалось обновить ${label}." >&2
+ fi
+}
+
+if command -v update-desktop-database >/dev/null 2>&1; then
+ optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications
+fi
+if command -v gtk-update-icon-cache >/dev/null 2>&1; then
+ optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor
+fi
diff --git a/clash-verge-rev/postremove.sh b/clash-verge-rev/postremove.sh
new file mode 100755
index 0000000..2462783
--- /dev/null
+++ b/clash-verge-rev/postremove.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+set -euo pipefail
+
+optional_refresh() {
+ local label=$1
+ shift
+ if ! "$@"; then
+ echo "Предупреждение: не удалось обновить ${label}." >&2
+ fi
+}
+
+if command -v update-desktop-database >/dev/null 2>&1; then
+ optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications
+fi
+if command -v gtk-update-icon-cache >/dev/null 2>&1; then
+ optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor
+fi
diff --git a/clash-verge-rev/stapler-repo.toml b/clash-verge-rev/stapler-repo.toml
new file mode 100644
index 0000000..d35f9bf
--- /dev/null
+++ b/clash-verge-rev/stapler-repo.toml
@@ -0,0 +1 @@
+[inherit]
diff --git a/claude-desktop/.stapler/update-check b/claude-desktop/.stapler/update-check
new file mode 100755
index 0000000..efb050a
--- /dev/null
+++ b/claude-desktop/.stapler/update-check
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -euo pipefail
+
+package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+repo_root="$(cd "${package_dir}/.." && pwd)"
+exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")"
diff --git a/claude-desktop/LICENSE b/claude-desktop/LICENSE
new file mode 100644
index 0000000..8e98cf1
--- /dev/null
+++ b/claude-desktop/LICENSE
@@ -0,0 +1,26 @@
+Сведения о лицензии пакета Claude
+
+Лицензия: Proprietary.
+Исходный продукт: https://claude.ai/download
+Документация desktop-приложения Claude: https://code.claude.com/docs/en/desktop-quickstart
+Официальный Linux-репозиторий: https://downloads.claude.ai/claude-desktop/apt/stable
+
+Нюансы пакета:
+- Пакет перепаковывает официальный Linux deb-артефакт `claude-desktop` из APT-репозитория Anthropic.
+- В пакет входит десктопное Electron-приложение Claude с поддержкой сценариев Claude Code.
+- Команда `claude-desktop` запускает основное desktop-приложение.
+- Для одновременной работы с двумя аккаунтами пакет добавляет самостоятельное приложение `ClaudeAlt` с командами `claude-alt` / `claude-code-alt`, отдельной app-иконкой, executable/resources tree и Electron-идентичностью `com.anthropic.ClaudeAlt`.
+- Tray-иконки основного Claude и ClaudeAlt являются перекрашенными вариантами штатного глифа Anthropic и регулируются теми же проприетарными условиями, что и исходный продукт.
+- ClaudeAlt использует отдельный каталог web-данных `${XDG_CONFIG_HOME:-~/.config}/ClaudeAlt`, чтобы не смешивать cookies, авторизацию, OAuth-кэш, IndexedDB, Local Storage и Chromium lock-файлы. При обновлении существующий `${XDG_CONFIG_HOME:-~/.config}/Claude-Account-2` продолжает использоваться для сохранения авторизации.
+- Путь профиля можно переопределить через `CLAUDE_ALT_DATA_DIR`; прежняя переменная `CLAUDE_DESKTOP_ACCOUNT2_DIR` поддерживается для совместимости.
+- Глобальная память и конфигурация Claude Code в `~/.claude` могут использоваться обоими приложениями, поскольку этот путь принадлежит Claude Code, а не Electron-профилю.
+- Облачные чаты и память Claude не объединяются пакетом: эти данные принадлежат конкретной учетной записи и хранятся на стороне Anthropic.
+- Команда `claude` намеренно не добавляется, чтобы не конфликтовать с отдельным Claude Code CLI.
+- Для полноценной работы требуется учетная запись Anthropic и подходящая подписка или доступ к сервису Claude.
+- Настройки, авторизация, история, рабочие каталоги и данные проектов создаются приложением после запуска и не поставляются внутри пакета.
+- Функции, зависящие от виртуализации или интеграции окружения, могут требовать дополнительные системные компоненты: QEMU, virtiofsd, portal backend, keyring/kwallet и аудиоподсистему.
+- Исходный продукт является проприетарным. Использование Claude, Claude Code, сетевых функций, моделей и учетной записи регулируется юридическими условиями Anthropic.
+- Внутри upstream-пакета также поставляются компоненты Electron/Chromium и virtiofsd со своими лицензиями; их тексты устанавливаются в `/usr/share/licenses/claude-desktop`.
+
+Официальные условия:
+https://www.anthropic.com/legal/consumer-terms
diff --git a/claude-desktop/Staplerfile b/claude-desktop/Staplerfile
new file mode 100644
index 0000000..142ffaf
--- /dev/null
+++ b/claude-desktop/Staplerfile
@@ -0,0 +1,388 @@
+name='claude-desktop'
+version=1.24012.0
+release=4
+summary='Claude desktop application'
+summary_ru='Десктопное приложение Claude'
+group='Development/Tools'
+desc='Claude is the Anthropic desktop application for Claude and Claude Code workflows with chat, project context, integrated terminal, editor and visual diff review.'
+desc_ru='Claude — десктопное приложение Anthropic для Claude и сценариев Claude Code: чат, контекст проекта, встроенный терминал, редактор и визуальный просмотр diff.'
+homepage='https://code.claude.com/docs/en/desktop-quickstart'
+maintainer='Anthropic PBC '
+
+architectures=('amd64' 'arm64')
+
+license=('Custom')
+nonfree=1
+nonfree_msg='Claude Desktop является проприетарным приложением Anthropic. Использование Claude, Claude Code, учетной записи, подписки, сетевых функций и моделей регулируется условиями Anthropic.'
+nonfree_url='https://www.anthropic.com/legal/consumer-terms'
+
+provides=()
+replaces=('claude-desktop')
+conflicts=()
+
+auto_reqprov_method="dirty"
+auto_req=0
+auto_prov=0
+
+deps=()
+deps_debian=('libgtk-3-0' 'libnotify4' 'libnss3' 'xdg-utils' 'libatspi2.0-0' 'libdrm2' 'libgbm1' 'libxcb-dri3-0' 'libsecret-1-0' 'libc6' 'libxtst6' 'libuuid1' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk | xdg-desktop-portal-gnome | xdg-desktop-portal-kde')
+deps_ubuntu=("${deps_debian[@]}")
+deps_fedora=('gtk3' 'libnotify' 'nss' 'xdg-utils' 'at-spi2-core' 'libdrm' 'mesa-libgbm' 'libxcb' 'libsecret' 'glibc' 'libXtst' 'libuuid' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk')
+deps_arch=('gtk3' 'libnotify' 'nss' 'xdg-utils' 'at-spi2-core' 'libdrm' 'libgbm' 'libxcb' 'libsecret' 'glibc' 'libxtst' 'libutil-linux' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk')
+deps_opensuse=('gtk3' 'libnotify4' 'mozilla-nss' 'xdg-utils' 'at-spi2-core' 'libdrm2' 'libgbm1' 'libxcb-dri3-0' 'libsecret-1-0' 'glibc' 'libXtst6' 'libuuid1' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk')
+deps_altlinux=('libgtk+3' 'libnotify' 'libnss' 'xdg-utils' 'libat-spi2-core' 'libdrm' 'libgbm' 'libxcb' 'libsecret' 'glibc' 'libXtst' 'libuuid' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk')
+
+opt_deps=()
+opt_deps_debian=('libasound2t64 | libasound2 | pulseaudio' 'libayatana-appindicator3-1 | libappindicator3-1' 'ca-certificates' 'gnome-keyring | kwalletd6 | kwalletd5' 'qemu-system-x86' 'ovmf' 'virtiofsd')
+opt_deps_ubuntu=("${opt_deps_debian[@]}")
+opt_deps_fedora=('alsa-lib' 'pulseaudio-libs' 'libappindicator-gtk3' 'ca-certificates' 'gnome-keyring' 'kwallet' 'qemu-system-x86' 'edk2-ovmf' 'virtiofsd')
+opt_deps_arch=('alsa-lib' 'pulseaudio' 'libappindicator-gtk3' 'ca-certificates' 'gnome-keyring' 'kwallet' 'qemu-system-x86' 'edk2-ovmf' 'virtiofsd')
+opt_deps_opensuse=('libasound2' 'libpulse0' 'libappindicator3-1' 'ca-certificates' 'gnome-keyring' 'kwallet6' 'qemu-x86' 'ovmf' 'virtiofsd')
+opt_deps_altlinux=('libalsa' 'libappindicator-gtk3' 'ca-certificates' 'gnome-keyring' 'qemu-system-x86' 'virtiofsd')
+
+build_deps=('binutils')
+build_deps_debian=('binutils' 'python3')
+build_deps_ubuntu=("${build_deps_debian[@]}")
+build_deps_fedora=('binutils' 'python3')
+build_deps_arch=('binutils' 'python')
+build_deps_opensuse=('binutils' 'python3')
+build_deps_altlinux=('binutils' 'python3')
+
+sources=(
+ "https://downloads.claude.ai/claude-desktop/apt/stable/pool/main/c/claude-desktop/claude-desktop_${version}_amd64.deb?~archive=false&~name=claude-desktop.deb"
+ "local:///claude-alt"
+ "local:///claude-alt.png"
+ "local:///claude-tray-orange.png"
+ "local:///claude-alt-tray-turquoise.png"
+ 'local:///LICENSE'
+)
+
+sources_arm64=(
+ "https://downloads.claude.ai/claude-desktop/apt/stable/pool/main/c/claude-desktop/claude-desktop_${version}_arm64.deb?~archive=false&~name=claude-desktop.deb"
+ "local:///claude-alt"
+ "local:///claude-alt.png"
+ "local:///claude-tray-orange.png"
+ "local:///claude-alt-tray-turquoise.png"
+ 'local:///LICENSE'
+)
+
+checksums=(
+ 'sha256:1096a8063956f8430fdbe504bb1c5e02985f31d0f45d082e4966e5d30ea7e3ac'
+ 'sha256:00d9fe6549d82f564a99c6e8ec54d1380fdf8f0a20c99bb9b3cda1f689adf57d'
+ 'sha256:f23de36a74c3f6ddae92511fc0dc659816853bf1d28520a6e0232fe2606fd0b4'
+ 'sha256:be51081c0e0b2eec3489c25da38194dccf68fe7445928b32c30e1b8051eb557b'
+ 'sha256:21d1db80c30921346252ed405fa36cb5c48c287afdd8ebe474073389fd88e85e'
+ 'sha256:40efe7a2755d02c9fe13f98cae18705e2d31e5bbb24816ebbd4951c5e3a56b3f'
+)
+
+checksums_arm64=(
+ 'sha256:37dd5dc1b0bfcb3192561acbd33cb59037806fe9b95221a3ddf38244203196f1'
+ 'sha256:00d9fe6549d82f564a99c6e8ec54d1380fdf8f0a20c99bb9b3cda1f689adf57d'
+ 'sha256:f23de36a74c3f6ddae92511fc0dc659816853bf1d28520a6e0232fe2606fd0b4'
+ 'sha256:be51081c0e0b2eec3489c25da38194dccf68fe7445928b32c30e1b8051eb557b'
+ 'sha256:21d1db80c30921346252ed405fa36cb5c48c287afdd8ebe474073389fd88e85e'
+ 'sha256:40efe7a2755d02c9fe13f98cae18705e2d31e5bbb24816ebbd4951c5e3a56b3f'
+)
+
+scripts=(
+ ['postinstall']='postinstall.sh'
+ ['postremove']='postremove.sh'
+)
+
+package() {
+ cd "${srcdir}"
+
+ mkdir -p "${srcdir}/extracted"
+ ar x claude-desktop.deb
+ tar -xf data.tar.* -C "${srcdir}/extracted"
+
+ install -dm755 "${pkgdir}/usr/lib"
+ cp -a "${srcdir}/extracted/usr/lib/claude-desktop" "${pkgdir}/usr/lib/"
+ chmod 0755 "${pkgdir}/usr/lib/claude-desktop/chrome-sandbox"
+ chmod 755 \
+ "${pkgdir}/usr/lib/claude-desktop/claude-desktop" \
+ "${pkgdir}/usr/lib/claude-desktop/resources/chrome-native-host" \
+ "${pkgdir}/usr/lib/claude-desktop/resources/cowork-linux-helper" \
+ "${pkgdir}/usr/lib/claude-desktop/resources/virtiofsd"
+
+ # Сохраняем штатный tray-глиф Claude, меняя только его цвет.
+ for icon_name in TrayIconLinux.png TrayIconLinux-Dark.png; do
+ install -Dm644 "${srcdir}/claude-tray-orange.png" \
+ "${pkgdir}/usr/lib/claude-desktop/resources/${icon_name}"
+ done
+
+ # ClaudeAlt получает отдельные executable и app.asar. Остальные неизменяемые
+ # Electron-файлы подключаются к основной копии относительными symlink-ами.
+ # Это сохраняет отдельный resourcesPath, но не удваивает пакет целиком.
+ install -dm755 \
+ "${pkgdir}/usr/lib/claude-alt" \
+ "${pkgdir}/usr/lib/claude-alt/resources"
+ cp -p "${pkgdir}/usr/lib/claude-desktop/claude-desktop" \
+ "${pkgdir}/usr/lib/claude-alt/claude-alt-bin"
+
+ for entry in "${pkgdir}/usr/lib/claude-desktop"/*; do
+ entry_name="${entry##*/}"
+ case "${entry_name}" in
+ claude-desktop | resources) continue ;;
+ esac
+ ln -s "../claude-desktop/${entry_name}" \
+ "${pkgdir}/usr/lib/claude-alt/${entry_name}"
+ done
+
+ for entry in "${pkgdir}/usr/lib/claude-desktop/resources"/*; do
+ entry_name="${entry##*/}"
+ case "${entry_name}" in
+ app.asar)
+ cp -p "${entry}" "${pkgdir}/usr/lib/claude-alt/resources/app.asar"
+ ;;
+ icon.png | TrayIconLinux.png | TrayIconLinux-Dark.png)
+ # Эти ресурсы ниже заменяются иконкой ClaudeAlt.
+ ;;
+ *)
+ ln -s "../../claude-desktop/resources/${entry_name}" \
+ "${pkgdir}/usr/lib/claude-alt/resources/${entry_name}"
+ ;;
+ esac
+ done
+
+ # Большая иконка ClaudeAlt используется окном, а в tray устанавливается
+ # отдельный бирюзовый вариант штатного глифа Claude.
+ install -Dm644 "${srcdir}/claude-alt.png" \
+ "${pkgdir}/usr/lib/claude-alt/resources/icon.png"
+ for icon_name in TrayIconLinux.png TrayIconLinux-Dark.png; do
+ install -Dm644 "${srcdir}/claude-alt-tray-turquoise.png" \
+ "${pkgdir}/usr/lib/claude-alt/resources/${icon_name}"
+ done
+
+ alt_asar="${pkgdir}/usr/lib/claude-alt/resources/app.asar"
+
+ python3 - "${alt_asar}" <<'PY'
+import copy
+import hashlib
+import json
+import struct
+import sys
+from pathlib import Path
+
+asar_path = Path(sys.argv[1])
+data = asar_path.read_bytes()
+if len(data) < 16:
+ raise SystemExit("ClaudeAlt app.asar is too small")
+
+_, _, _, header_size = struct.unpack(" dict:
+ blocks = [
+ hashlib.sha256(content[index : index + block_size]).hexdigest()
+ for index in range(0, len(content), block_size)
+ ]
+ if not blocks:
+ blocks = [hashlib.sha256(b"").hexdigest()]
+ return {
+ "algorithm": "SHA256",
+ "hash": hashlib.sha256(content).hexdigest(),
+ "blockSize": block_size,
+ "blocks": blocks,
+ }
+
+
+def rebuild_node(node: dict, prefix: str = "") -> dict:
+ global patched_cowork_socket, patched_package_json, patched_user_agent
+
+ rebuilt = {}
+ for name, child in node.get("files", {}).items():
+ path = f"{prefix}/{name}" if prefix else name
+ if "files" in child:
+ rebuilt[name] = {
+ **{key: value for key, value in child.items() if key != "files"},
+ "files": rebuild_node(child, path),
+ }
+ continue
+
+ entry = copy.deepcopy(child)
+ if entry.get("unpacked"):
+ rebuilt[name] = entry
+ continue
+
+ offset = int(entry["offset"])
+ size = int(entry["size"])
+ content = data[base_offset + offset : base_offset + offset + size]
+ content_changed = False
+
+ if path == "package.json":
+ package_json = json.loads(content)
+ expected = {
+ "productName": "Claude",
+ "desktopName": "com.anthropic.Claude.desktop",
+ }
+ for field, expected_value in expected.items():
+ if package_json.get(field) != expected_value:
+ raise SystemExit(
+ f"unexpected Claude package.json {field}: "
+ f"{package_json.get(field)!r} != {expected_value!r}"
+ )
+ # Отдельный productName нужен Electron для независимого instance
+ # lock. Официальный User-Agent восстанавливается ниже отдельно.
+ package_json["productName"] = "ClaudeAlt"
+ package_json["desktopName"] = "com.anthropic.ClaudeAlt.desktop"
+ content = (json.dumps(package_json, indent=2) + "\n").encode("utf-8")
+ patched_package_json += 1
+ content_changed = True
+
+ socket_name = b"claude-cowork-vm.sock"
+ socket_matches = content.count(socket_name)
+ if socket_matches:
+ content = content.replace(socket_name, b"claude-alt-cowork-vm.sock")
+ patched_cowork_socket += socket_matches
+ content_changed = True
+
+ user_agent_marker = (
+ b'P.app.isPackaged||P.app.setName("Claude");RRn();'
+ )
+ user_agent_matches = content.count(user_agent_marker)
+ if user_agent_matches:
+ content = content.replace(
+ user_agent_marker,
+ b'P.app.isPackaged||P.app.setName("Claude");'
+ b'P.app.userAgentFallback=P.app.userAgentFallback'
+ b'.replaceAll("ClaudeAlt","Claude");RRn();',
+ )
+ patched_user_agent += user_agent_matches
+ content_changed = True
+
+ entry["offset"] = str(len(output))
+ if content_changed:
+ entry["size"] = len(content)
+ entry["integrity"] = integrity_for(
+ content,
+ entry.get("integrity", {}).get("blockSize", 4194304),
+ )
+ output.extend(content)
+ rebuilt[name] = entry
+
+ return rebuilt
+
+
+rebuilt_header = {"files": rebuild_node(header)}
+if patched_package_json != 1:
+ raise SystemExit(
+ f"expected one ClaudeAlt package.json, got {patched_package_json}"
+ )
+if patched_cowork_socket != 1:
+ raise SystemExit(
+ f"expected one ClaudeAlt Cowork socket, got {patched_cowork_socket}"
+ )
+if patched_user_agent != 1:
+ raise SystemExit(
+ f"expected one ClaudeAlt User-Agent patch, got {patched_user_agent}"
+ )
+
+header_bytes = json.dumps(rebuilt_header, separators=(",", ":")).encode("utf-8")
+header_padded_size = (len(header_bytes) + 3) & ~3
+patched_data = (
+ struct.pack(
+ "&2
+ return 1
+ }
+ sed -i \
+ -e '0,/^Name=/{s/^Name=.*/Name=Claude\nName[ru]=Claude/}' \
+ -e 's/^Categories=.*/Categories=Development;/' \
+ "${desktop_file}"
+
+ install -Dm644 "${srcdir}/claude-alt.png" \
+ "${pkgdir}/usr/share/icons/hicolor/512x512/apps/claude-alt.png"
+
+ install -Dm644 /dev/stdin "${pkgdir}/usr/share/applications/com.anthropic.ClaudeAlt.desktop" <<'EOF'
+[Desktop Entry]
+Name=ClaudeAlt
+Name[ru]=ClaudeAlt
+Comment=Independent secondary Claude desktop application
+Comment[ru]=Независимое второе приложение Claude
+GenericName=AI Assistant
+Keywords=AI;Chat;Assistant;Claude;Code;LLM;
+Exec=claude-alt %U
+Icon=claude-alt
+Type=Application
+StartupNotify=true
+StartupWMClass=com.anthropic.ClaudeAlt
+SingleMainWindow=true
+Categories=Development;
+Actions=NewChat;NewCode;
+
+[Desktop Action NewChat]
+Name=New chat
+Name[ru]=Новый чат
+Exec=claude-alt claude://claude.ai/new
+
+[Desktop Action NewCode]
+Name=New Claude Code session
+Name[ru]=Новая сессия Claude Code
+Exec=claude-alt claude://code/new
+EOF
+
+ install -dm755 "${pkgdir}/usr/bin"
+ ln -sfn /usr/lib/claude-desktop/claude-desktop "${pkgdir}/usr/bin/claude-desktop"
+ ln -sfn /usr/lib/claude-alt/claude-alt "${pkgdir}/usr/bin/claude-alt"
+ ln -sfn /usr/lib/claude-alt/claude-alt "${pkgdir}/usr/bin/claude-code-alt"
+ ln -sfn /usr/lib/claude-alt/claude-alt "${pkgdir}/usr/bin/claude-desktop-account2"
+
+ install -dm755 "${pkgdir}/usr/share/licenses/claude-desktop"
+ install-license "${srcdir}/LICENSE" "claude-desktop/LICENSE.nivora"
+ install -Dm644 "${srcdir}/extracted/usr/share/doc/claude-desktop/copyright" \
+ "${pkgdir}/usr/share/licenses/claude-desktop/copyright"
+ install -Dm644 "${srcdir}/extracted/usr/lib/claude-desktop/LICENSES.chromium.html" \
+ "${pkgdir}/usr/share/licenses/claude-desktop/LICENSES.chromium.html"
+ install -Dm644 "${srcdir}/extracted/usr/lib/claude-desktop/resources/virtiofsd.LICENSE-APACHE" \
+ "${pkgdir}/usr/share/licenses/claude-desktop/virtiofsd.LICENSE-APACHE"
+ install -Dm644 "${srcdir}/extracted/usr/lib/claude-desktop/resources/virtiofsd.LICENSE-BSD-3-Clause" \
+ "${pkgdir}/usr/share/licenses/claude-desktop/virtiofsd.LICENSE-BSD-3-Clause"
+}
+
+files() {
+ files-find-binary
+ files-find-desktop
+ files-find-license
+ files-find \
+ "/usr/lib/claude-desktop/**/*" \
+ "/usr/lib/claude-alt/**/*" \
+ "/usr/share/icons/hicolor/*/apps/claude-desktop.png" \
+ "/usr/share/icons/hicolor/*/apps/claude-alt.png"
+}
diff --git a/claude-desktop/claude-alt b/claude-desktop/claude-alt
new file mode 100755
index 0000000..a23848b
--- /dev/null
+++ b/claude-desktop/claude-alt
@@ -0,0 +1,40 @@
+#!/bin/sh
+set -eu
+
+if [ -z "${HOME:-}" ] && [ -z "${XDG_CONFIG_HOME:-}" ]; then
+ echo "claude-alt: HOME or XDG_CONFIG_HOME is required" >&2
+ exit 1
+fi
+
+config_home="${XDG_CONFIG_HOME:-${HOME}/.config}"
+default_data_dir="${config_home}/ClaudeAlt"
+legacy_data_dir="${CLAUDE_DESKTOP_ACCOUNT2_DIR:-${config_home}/Claude-Account-2}"
+claude_alt_executable="${CLAUDE_ALT_EXECUTABLE:-/usr/lib/claude-alt/claude-alt-bin}"
+
+if [ -n "${CLAUDE_ALT_DATA_DIR:-}" ]; then
+ data_dir="${CLAUDE_ALT_DATA_DIR}"
+elif [ -d "${default_data_dir}" ]; then
+ data_dir="${default_data_dir}"
+elif [ -d "${legacy_data_dir}" ]; then
+ # Сохраняем авторизацию прежнего второго профиля после обновления пакета.
+ data_dir="${legacy_data_dir}"
+else
+ data_dir="${default_data_dir}"
+fi
+
+if [ ! -x "${claude_alt_executable}" ]; then
+ echo "claude-alt: executable not found: ${claude_alt_executable}" >&2
+ exit 1
+fi
+
+mkdir -p "${data_dir}"
+
+# Electron использует это имя для portal-интеграций. Основной Wayland app_id
+# задаётся отдельным desktopName внутри ClaudeAlt app.asar.
+CHROME_DESKTOP='com.anthropic.ClaudeAlt.desktop'
+export CHROME_DESKTOP
+
+exec "${claude_alt_executable}" \
+ --user-data-dir="${data_dir}" \
+ --class=com.anthropic.ClaudeAlt \
+ "$@"
diff --git a/claude-desktop/claude-alt-tray-turquoise.png b/claude-desktop/claude-alt-tray-turquoise.png
new file mode 100644
index 0000000..a650109
Binary files /dev/null and b/claude-desktop/claude-alt-tray-turquoise.png differ
diff --git a/claude-desktop/claude-alt.png b/claude-desktop/claude-alt.png
new file mode 100644
index 0000000..aa38a42
Binary files /dev/null and b/claude-desktop/claude-alt.png differ
diff --git a/claude-desktop/claude-tray-orange.png b/claude-desktop/claude-tray-orange.png
new file mode 100644
index 0000000..9900a9f
Binary files /dev/null and b/claude-desktop/claude-tray-orange.png differ
diff --git a/claude-desktop/postinstall.sh b/claude-desktop/postinstall.sh
new file mode 100755
index 0000000..fc73778
--- /dev/null
+++ b/claude-desktop/postinstall.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+set -euo pipefail
+
+optional_refresh() {
+ local label=$1
+ shift
+ if ! "$@"; then
+ echo "Предупреждение: не удалось обновить ${label}." >&2
+ fi
+}
+
+sandbox=/usr/lib/claude-desktop/chrome-sandbox
+if [[ ! -f "$sandbox" ]]; then
+ echo "Ошибка: не найден Electron sandbox: ${sandbox}" >&2
+ exit 1
+fi
+if [[ -L /proc/self/ns/user ]] && command -v unshare >/dev/null 2>&1 \
+ && unshare --user true 2>/dev/null; then
+ chmod 0755 "$sandbox"
+else
+ chmod 4755 "$sandbox"
+fi
+
+if command -v update-desktop-database >/dev/null 2>&1; then
+ optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications
+fi
+if command -v gtk-update-icon-cache >/dev/null 2>&1; then
+ optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor
+fi
+if command -v kbuildsycoca6 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca6 --noincremental
+elif command -v kbuildsycoca5 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca5 --noincremental
+fi
diff --git a/claude-desktop/postremove.sh b/claude-desktop/postremove.sh
new file mode 100755
index 0000000..44d2528
--- /dev/null
+++ b/claude-desktop/postremove.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+set -euo pipefail
+
+optional_refresh() {
+ local label=$1
+ shift
+ if ! "$@"; then
+ echo "Предупреждение: не удалось обновить ${label}." >&2
+ fi
+}
+
+if command -v update-desktop-database >/dev/null 2>&1; then
+ optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications
+fi
+if command -v gtk-update-icon-cache >/dev/null 2>&1; then
+ optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor
+fi
+if command -v kbuildsycoca6 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca6 --noincremental
+elif command -v kbuildsycoca5 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca5 --noincremental
+fi
diff --git a/claude-desktop/stapler-repo.toml b/claude-desktop/stapler-repo.toml
new file mode 100644
index 0000000..9c222ce
--- /dev/null
+++ b/claude-desktop/stapler-repo.toml
@@ -0,0 +1 @@
+include = "../stapler-repo.toml"
diff --git a/codex/.stapler/update-check b/codex/.stapler/update-check
new file mode 100755
index 0000000..efb050a
--- /dev/null
+++ b/codex/.stapler/update-check
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -euo pipefail
+
+package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+repo_root="$(cd "${package_dir}/.." && pwd)"
+exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")"
diff --git a/codex/LICENSE b/codex/LICENSE
new file mode 100644
index 0000000..497e48e
--- /dev/null
+++ b/codex/LICENSE
@@ -0,0 +1,21 @@
+Сведения о лицензии пакета codex-app
+
+Лицензия: Custom.
+Источник перепаковки: https://github.com/Boria138/codex-app-linux
+Linux-адаптация: https://github.com/ilysenko/codex-desktop-linux
+Backend Computer Use: https://github.com/agent-sh/computer-use-linux
+
+Нюансы пакета:
+- Пакет устанавливает неофициальную Linux-перепаковку проприетарного настольного приложения.
+- Исходный репозиторий перепаковки не публикует единую корневую лицензию для всего устанавливаемого артефакта.
+- Внутри архива исходного проекта присутствуют отдельные лицензии Electron, Chromium и других компонентов; при сборке они устанавливаются в каталог лицензий пакета.
+- Linux-патчи Computer Use из codex-desktop-linux и backend computer-use-linux
+ распространяются по лицензии MIT; их исходные файлы LICENSE устанавливаются
+ рядом с остальными лицензиями пакета.
+- Использование приложения и сетевых функций регулируется условиями исходного сервиса.
+- Пакет помечен как несвободный через `nonfree=1`.
+
+Справочные ссылки:
+- https://github.com/Boria138/codex-app-linux
+- https://github.com/ilysenko/codex-desktop-linux
+- https://github.com/agent-sh/computer-use-linux
diff --git a/codex/Staplerfile b/codex/Staplerfile
new file mode 100644
index 0000000..e273085
--- /dev/null
+++ b/codex/Staplerfile
@@ -0,0 +1,176 @@
+name='codex'
+version=26.721.31836
+release=1
+summary='Unofficial Linux repack of the Codex desktop app'
+summary_ru='Неофициальная Linux-перепаковка десктопного приложения Codex'
+group='Development/Tools'
+desc='codex-app-linux repacks the Codex desktop application into Linux Electron artifacts and bundles a Linux codex CLI binary for local workflows.'
+desc_ru='codex-app-linux перепаковывает десктопное приложение Codex в Linux-артефакты Electron и включает Linux-бинарник codex CLI для локальных сценариев.'
+homepage='https://github.com/Boria138/codex-app-linux'
+maintainer='Boria138 '
+
+architectures=('amd64')
+
+license=('Custom')
+nonfree=1
+nonfree_msg='codex-app устанавливает неофициальную Linux-перепаковку проприетарного настольного приложения. Использование приложения и сетевых функций регулируется условиями исходного сервиса.'
+nonfree_url='https://github.com/Boria138/codex-app-linux'
+
+provides=()
+replaces=('codex')
+conflicts=()
+
+auto_reqprov_method="dirty"
+auto_req=0
+auto_prov=0
+
+deps=()
+deps_debian=('ca-certificates' 'libc6' 'libasound2' 'libatk-bridge2.0-0' 'libatk1.0-0' 'libatspi2.0-0' 'libcairo2' 'libcups2' 'libcurl3-gnutls | libcurl4' 'libdbus-1-3' 'libexpat1' 'libgbm1' 'libglib2.0-0' 'libgtk-3-0' 'libnspr4' 'libnss3' 'libpango-1.0-0' 'libsecret-1-0' 'libudev1' 'libx11-6' 'libxcb1' 'libxcomposite1' 'libxdamage1' 'libxext6' 'libxfixes3' 'libxkbcommon0' 'libxkbfile1' 'libxrandr2' 'libxss1' 'libxtst6' 'xdg-desktop-portal' 'xdg-utils')
+deps_ubuntu=("${deps_debian[@]}")
+deps_fedora=('ca-certificates' 'glibc' 'alsa-lib' 'atk' 'at-spi2-atk' 'at-spi2-core' 'cairo' 'cups-libs' 'libcurl' 'dbus-libs' 'expat' 'mesa-libgbm' 'glib2' 'gtk3' 'nspr' 'nss' 'pango' 'libsecret' 'systemd-libs' 'libX11' 'libxcb' 'libXcomposite' 'libXdamage' 'libXext' 'libXfixes' 'libxkbcommon' 'libxkbfile' 'libXrandr' 'libXScrnSaver' 'libXtst' 'xdg-desktop-portal' 'xdg-utils')
+deps_arch=('ca-certificates' 'glibc' 'alsa-lib' 'atk' 'at-spi2-atk' 'at-spi2-core' 'cairo' 'cups' 'curl' 'dbus' 'expat' 'libgbm' 'glib2' 'gtk3' 'nspr' 'nss' 'pango' 'libsecret' 'systemd-libs' 'libx11' 'libxcb' 'libxcomposite' 'libxdamage' 'libxext' 'libxfixes' 'libxkbcommon' 'libxkbfile' 'libxrandr' 'libxss' 'libxtst' 'xdg-desktop-portal' 'xdg-utils')
+deps_opensuse=('ca-certificates' 'glibc' 'alsa' 'libatk-1_0-0' 'libatk-bridge-2_0-0' 'libatspi0' 'libcairo2' 'cups-libs' 'libcurl4' 'libdbus-1-3' 'libexpat1' 'libgbm1' 'glib2' 'gtk3' 'mozilla-nspr' 'mozilla-nss' 'pango' 'libsecret-1-0' 'libudev1' 'libX11-6' 'libxcb1' 'libXcomposite1' 'libXdamage1' 'libXext6' 'libXfixes3' 'libxkbcommon0' 'libxkbfile1' 'libXrandr2' 'libXss1' 'libXtst6' 'xdg-desktop-portal' 'xdg-utils')
+deps_altlinux=('ca-certificates' 'glibc' 'libalsa' 'at-spi2-atk' 'libat-spi2-core' 'libatk' 'libcairo' 'libcups' 'libcurl' 'libdbus' 'libexpat' 'libgbm' 'glib2' 'libgtk+3' 'libnspr' 'libnss' 'libpango' 'libsecret' 'libudev0' 'libX11' 'libxcb' 'libXcomposite' 'libXdamage' 'libXext' 'libXfixes' 'libxkbcommon' 'libxkbfile' 'libXrandr' 'libXScrnSaver' 'libXtst' 'xdg-desktop-portal' 'xdg-utils')
+
+build_deps=('binutils')
+build_deps_debian=('binutils' 'build-essential' 'cargo' 'libwayland-dev' 'nodejs' 'pkg-config' 'python3' 'rustc')
+build_deps_ubuntu=("${build_deps_debian[@]}")
+build_deps_fedora=('binutils' 'cargo' 'gcc' 'gcc-c++' 'make' 'nodejs' 'pkgconf-pkg-config' 'python3' 'rust' 'wayland-devel')
+build_deps_arch=('base-devel' 'binutils' 'cargo' 'nodejs' 'pkgconf' 'python' 'rust' 'wayland')
+build_deps_opensuse=('binutils' 'cargo' 'gcc' 'gcc-c++' 'make' 'nodejs' 'pkg-config' 'python3' 'rust' 'wayland-devel')
+build_deps_altlinux=('binutils' 'gcc-c++' 'libwayland-client-devel' 'make' 'node' 'pkg-config' 'python3' 'rust' 'rust-cargo')
+
+sources=(
+ "https://github.com/Boria138/codex-app-linux/releases/download/${version}/codex-app-${version}-x86_64.tar.gz?~archive=false&~name=codex-app.tar.gz"
+ 'https://github.com/ilysenko/codex-desktop-linux/archive/15f3bd4fabc44bc0afba85b16d0121f0b4abd052.tar.gz?~archive=false&~name=codex-linux-port.tar.gz'
+ 'https://github.com/agent-sh/computer-use-linux/archive/refs/tags/v0.4.1.tar.gz?~archive=false&~name=computer-use-linux.tar.gz'
+ 'local:///asar_tree.py'
+ 'local:///patch-computer-use.js'
+ "local:///codex-app.png"
+ 'local:///LICENSE'
+)
+
+checksums=(
+ 'sha256:f48c0ae3fca556d66525b863dd009a0bd0702115517f83c39f8a44cf4d128eb3'
+ 'sha256:8cde2dac642947cdfab23d302056ef81b574cc7f5684f25f7116f80411ec551c'
+ 'sha256:fab36489be4f1e92591d2ebcd28a96a95f788b5d1ba87824985623ab7df5e3f4'
+ 'sha256:9259dc4b9ae217855d55414b2266db08d9b40c5b0cdbca20a183234ff55ba006'
+ 'sha256:91b3cc2a6b660e3962f24976a29a261dae9a38f879758e6e6463e1295477e172'
+ 'sha256:5c4fe03d5777a9684e37ec9a2331302abf525f53db83762e6adaafd2fc5eb510'
+ 'sha256:755f407295f07facd26c968b73a204db5a6f25deac890d230062b09c33eb395e'
+)
+
+scripts=(
+ ['postinstall']='postinstall.sh'
+ ['postremove']='postremove.sh'
+)
+
+package() {
+ cd "${srcdir}"
+
+ tar -xzf codex-app.tar.gz
+ tar -xzf codex-linux-port.tar.gz
+ tar -xzf computer-use-linux.tar.gz
+ src="$(find "${srcdir}" -maxdepth 1 -type d -name "codex-app-${version}-x64" | head -1)"
+ linux_port_src="$(find "${srcdir}" -maxdepth 1 -type d -name 'codex-desktop-linux-*' | head -1)"
+ computer_use_src="$(find "${srcdir}" -maxdepth 1 -type d -name 'computer-use-linux-*' | head -1)"
+
+ cargo build \
+ --manifest-path "${computer_use_src}/Cargo.toml" \
+ --target-dir "${srcdir}/computer-use-target" \
+ --release \
+ --locked
+
+ install -dm755 "${pkgdir}/opt/codex-app"
+ cp -a "${src}/." "${pkgdir}/opt/codex-app/"
+
+ codex_asar="${pkgdir}/opt/codex-app/resources/codex.asar"
+ extracted_asar="${srcdir}/codex-asar"
+ python3 "${srcdir}/asar_tree.py" extract "${codex_asar}" "${extracted_asar}"
+ node "${srcdir}/patch-computer-use.js" "${linux_port_src}" "${extracted_asar}"
+ python3 "${srcdir}/asar_tree.py" rebuild "${codex_asar}" "${extracted_asar}"
+
+ bundled_root="${pkgdir}/opt/codex-app/resources/plugins/openai-bundled"
+ plugin_dir="${bundled_root}/plugins/computer-use"
+ install -dm755 "${plugin_dir}"
+ cp -a \
+ "${linux_port_src}/plugins/openai-bundled/plugins/computer-use/." \
+ "${plugin_dir}/"
+ find "${plugin_dir}" -name '.gitkeep' -delete
+ install -Dm755 \
+ "${srcdir}/computer-use-target/release/computer-use-linux" \
+ "${plugin_dir}/bin/codex-computer-use-linux"
+ install -Dm755 \
+ "${srcdir}/computer-use-target/release/computer-use-linux-cosmic" \
+ "${plugin_dir}/bin/codex-computer-use-cosmic"
+ install -Dm755 \
+ "${srcdir}/computer-use-target/release/computer-use-linux-cosmic" \
+ "${plugin_dir}/bin/computer-use-linux-cosmic"
+ install -Dm644 codex-app.png "${plugin_dir}/assets/app-icon.png"
+
+ install -Dm644 /dev/stdin \
+ "${bundled_root}/.agents/plugins/marketplace.json" <<'EOF'
+{
+ "plugins": [
+ {
+ "name": "computer-use",
+ "source": {
+ "source": "local",
+ "path": "./plugins/computer-use"
+ },
+ "policy": {
+ "installation": "AVAILABLE",
+ "authentication": "ON_INSTALL"
+ },
+ "category": "Productivity"
+ }
+ ]
+}
+EOF
+
+ chmod 755 "${pkgdir}/opt/codex-app/codex-app"
+ chmod 755 "${pkgdir}/opt/codex-app/resources/codex"
+ if [[ -f "${pkgdir}/opt/codex-app/chrome-sandbox" ]]; then
+ chmod 0755 "${pkgdir}/opt/codex-app/chrome-sandbox"
+ fi
+
+ install -dm755 "${pkgdir}/usr/bin"
+ ln -sfn /opt/codex-app/codex-app "${pkgdir}/usr/bin/codex-app"
+ ln -sfn \
+ /opt/codex-app/resources/plugins/openai-bundled/plugins/computer-use/bin/codex-computer-use-linux \
+ "${pkgdir}/usr/bin/codex-computer-use-linux"
+
+ install -Dm644 codex-app.png \
+ "${pkgdir}/usr/share/icons/hicolor/512x512/apps/codex-app.png"
+
+ install -Dm644 "${src}/LICENSE.electron.txt" \
+ "${pkgdir}/usr/share/licenses/codex/LICENSE.electron.txt"
+ install -Dm644 "${src}/LICENSES.chromium.html" \
+ "${pkgdir}/usr/share/licenses/codex/LICENSES.chromium.html"
+ install -Dm644 "${linux_port_src}/LICENSE" \
+ "${pkgdir}/usr/share/licenses/codex/LICENSE.codex-desktop-linux"
+ install -Dm644 "${computer_use_src}/LICENSE" \
+ "${pkgdir}/usr/share/licenses/codex/LICENSE.computer-use-linux"
+ install-license "${srcdir}/LICENSE" "codex/LICENSE.nivora"
+
+ install -Dm644 /dev/stdin "${pkgdir}/usr/share/applications/codex-app.desktop" <<'EOF'
+[Desktop Entry]
+Name=Codex
+Comment=Desktop coding agent client
+Comment[ru]=Десктопный клиент coding agent
+Exec=/usr/bin/codex-app %U
+Terminal=false
+Type=Application
+Icon=codex-app
+Categories=Development;IDE;
+StartupWMClass=codex-app
+EOF
+}
+
+files() {
+ files-find "/opt/codex-app/**/*"
+ files-find-binary
+ files-find-desktop
+ files-find-license
+ files-find "/usr/share/icons/hicolor/*/apps/codex-app.png"
+}
diff --git a/codex/asar_tree.py b/codex/asar_tree.py
new file mode 100755
index 0000000..f9c2c19
--- /dev/null
+++ b/codex/asar_tree.py
@@ -0,0 +1,211 @@
+#!/usr/bin/env python3
+"""Extract and rebuild an Electron ASAR while preserving its metadata tree."""
+
+from __future__ import annotations
+
+import argparse
+import hashlib
+import json
+import os
+import struct
+import tempfile
+from pathlib import Path
+from typing import Any, Iterator
+
+
+class AsarError(RuntimeError):
+ pass
+
+
+def align4(value: int) -> int:
+ return (value + 3) & ~3
+
+
+def read_archive(path: Path) -> tuple[dict[str, Any], bytes, int]:
+ payload = path.read_bytes()
+ if len(payload) < 16:
+ raise AsarError(f"{path}: truncated ASAR header")
+
+ size_pickle_payload, header_pickle_size, header_payload_size, json_size = (
+ struct.unpack_from(" len(payload) or 16 + json_size > data_offset:
+ raise AsarError(f"{path}: invalid ASAR header size")
+
+ try:
+ header = json.loads(payload[16 : 16 + json_size].decode("utf-8"))
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
+ raise AsarError(f"{path}: invalid ASAR JSON header") from error
+ if not isinstance(header, dict) or not isinstance(header.get("files"), dict):
+ raise AsarError(f"{path}: ASAR header has no file tree")
+ return header, payload, data_offset
+
+
+def safe_component(name: str) -> None:
+ if (
+ not name
+ or name in {".", ".."}
+ or "/" in name
+ or "\\" in name
+ or "\0" in name
+ ):
+ raise AsarError(f"unsafe ASAR entry name: {name!r}")
+
+
+def entries(
+ files: dict[str, Any], prefix: tuple[str, ...] = ()
+) -> Iterator[tuple[tuple[str, ...], dict[str, Any]]]:
+ for name, metadata in files.items():
+ safe_component(name)
+ if not isinstance(metadata, dict):
+ raise AsarError(f"invalid metadata for {'/'.join((*prefix, name))}")
+ relative = (*prefix, name)
+ children = metadata.get("files")
+ if children is not None:
+ if not isinstance(children, dict):
+ raise AsarError(f"invalid directory entry: {'/'.join(relative)}")
+ yield from entries(children, relative)
+ else:
+ yield relative, metadata
+
+
+def output_path(root: Path, relative: tuple[str, ...]) -> Path:
+ candidate = root.joinpath(*relative)
+ try:
+ candidate.resolve().relative_to(root.resolve())
+ except ValueError as error:
+ raise AsarError(f"entry escapes extraction root: {'/'.join(relative)}") from error
+ return candidate
+
+
+def extract(archive: Path, destination: Path) -> None:
+ header, payload, data_offset = read_archive(archive)
+ destination.mkdir(parents=True, exist_ok=True)
+
+ for relative, metadata in entries(header["files"]):
+ if metadata.get("unpacked") is True or "link" in metadata:
+ continue
+ try:
+ size = int(metadata["size"])
+ offset = int(metadata["offset"])
+ except (KeyError, TypeError, ValueError) as error:
+ raise AsarError(f"invalid file entry: {'/'.join(relative)}") from error
+ start = data_offset + offset
+ end = start + size
+ if offset < 0 or size < 0 or end > len(payload):
+ raise AsarError(f"file lies outside archive: {'/'.join(relative)}")
+
+ target = output_path(destination, relative)
+ target.parent.mkdir(parents=True, exist_ok=True)
+ target.write_bytes(payload[start:end])
+ if metadata.get("executable") is True:
+ target.chmod(target.stat().st_mode | 0o111)
+
+
+def integrity(content: bytes, block_size: int) -> dict[str, Any]:
+ blocks = [
+ hashlib.sha256(content[offset : offset + block_size]).hexdigest()
+ for offset in range(0, len(content), block_size)
+ ]
+ if not blocks:
+ blocks = [hashlib.sha256(b"").hexdigest()]
+ return {
+ "algorithm": "SHA256",
+ "hash": hashlib.sha256(content).hexdigest(),
+ "blockSize": block_size,
+ "blocks": blocks,
+ }
+
+
+def rebuild(archive: Path, source: Path, output: Path) -> None:
+ header, _, _ = read_archive(archive)
+ content_chunks: list[bytes] = []
+ offset = 0
+
+ for relative, metadata in entries(header["files"]):
+ if metadata.get("unpacked") is True or "link" in metadata:
+ continue
+ target = output_path(source, relative)
+ if not target.is_file():
+ raise AsarError(f"missing extracted file: {'/'.join(relative)}")
+ content = target.read_bytes()
+ metadata["size"] = len(content)
+ metadata["offset"] = str(offset)
+ if "integrity" in metadata:
+ block_size = int(metadata["integrity"].get("blockSize", 4 * 1024 * 1024))
+ metadata["integrity"] = integrity(content, block_size)
+ content_chunks.append(content)
+ offset += len(content)
+
+ json_payload = json.dumps(
+ header, ensure_ascii=False, separators=(",", ":")
+ ).encode("utf-8")
+ padded_json_size = align4(len(json_payload))
+ header_payload_size = 4 + padded_json_size
+ prefix = struct.pack(
+ " int:
+ parser = argparse.ArgumentParser()
+ subparsers = parser.add_subparsers(dest="command", required=True)
+
+ extract_parser = subparsers.add_parser("extract")
+ extract_parser.add_argument("archive", type=Path)
+ extract_parser.add_argument("destination", type=Path)
+
+ rebuild_parser = subparsers.add_parser("rebuild")
+ rebuild_parser.add_argument("archive", type=Path)
+ rebuild_parser.add_argument("source", type=Path)
+ rebuild_parser.add_argument("output", type=Path, nargs="?")
+
+ arguments = parser.parse_args()
+ if arguments.command == "extract":
+ extract(arguments.archive, arguments.destination)
+ else:
+ rebuild(
+ arguments.archive,
+ arguments.source,
+ arguments.output or arguments.archive,
+ )
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/codex/codex-app.png b/codex/codex-app.png
new file mode 100644
index 0000000..5084b8c
Binary files /dev/null and b/codex/codex-app.png differ
diff --git a/codex/patch-computer-use.js b/codex/patch-computer-use.js
new file mode 100755
index 0000000..54fc503
--- /dev/null
+++ b/codex/patch-computer-use.js
@@ -0,0 +1,139 @@
+#!/usr/bin/env node
+"use strict";
+
+// This file only adapts upstream patch discovery to the current hashed bundle
+// name. Computer Use behavior remains sourced from codex-desktop-linux.
+
+const fs = require("node:fs");
+const path = require("node:path");
+
+const linuxPortRoot = path.resolve(process.argv[2] ?? "");
+const extractedApp = path.resolve(process.argv[3] ?? "");
+if (!fs.existsSync(linuxPortRoot) || !fs.existsSync(extractedApp)) {
+ console.error("usage: patch-computer-use.js LINUX_PORT_ROOT EXTRACTED_APP");
+ process.exit(2);
+}
+
+process.env.CODEX_LINUX_ENABLE_COMPUTER_USE_UI = "1";
+
+const { patchExtractedApp } = require(
+ path.join(linuxPortRoot, "scripts/patches/runner.js"),
+);
+const implementation = require(
+ path.join(linuxPortRoot, "scripts/patches/impl/computer-use.js"),
+);
+const coreRoot = path.join(
+ linuxPortRoot,
+ "scripts/patches/core/all-linux",
+);
+
+patchExtractedApp(extractedApp, {
+ corePatchRoot: path.join(coreRoot, "main-process/computer-use"),
+});
+patchExtractedApp(extractedApp, {
+ corePatchRoot: path.join(coreRoot, "webview/computer-use-ui"),
+});
+
+const assetsDirectory = path.join(extractedApp, "webview/assets");
+const javaScriptAssets = fs.readdirSync(assetsDirectory)
+ .filter((name) => name.endsWith(".js"))
+ .map((name) => path.join(assetsDirectory, name));
+
+function applyToUniqueAsset(label, patchFunction, alreadyPatched, isCandidate) {
+ const changed = [];
+ const existing = [];
+ for (const target of javaScriptAssets) {
+ const source = fs.readFileSync(target, "utf8");
+ if (alreadyPatched(source)) {
+ existing.push(target);
+ continue;
+ }
+ if (!isCandidate(source)) {
+ continue;
+ }
+ const patched = patchFunction(source);
+ if (patched !== source) {
+ changed.push({ target, patched });
+ }
+ }
+ if (existing.length === 1 && changed.length === 0) {
+ return;
+ }
+ if (existing.length !== 0 || changed.length !== 1) {
+ throw new Error(
+ `${label}: expected one target, found existing=${existing.length}, changed=${changed.length}`,
+ );
+ }
+ fs.writeFileSync(changed[0].target, changed[0].patched, "utf8");
+ console.log(`${label}: ${path.basename(changed[0].target)}`);
+}
+
+applyToUniqueAsset(
+ "Linux Computer Use host platform",
+ implementation.applyLinuxComputerUseHostPlatformPatch,
+ (source) =>
+ /featureName:`computer_use`[\s\S]{0,2200}?isHostCompatiblePlatform:[A-Za-z_$][\w$]*===`linux`\|\|/.test(
+ source,
+ ),
+ (source) => source.includes("featureName:`computer_use`"),
+);
+applyToUniqueAsset(
+ "Linux Computer Use install flow",
+ implementation.applyLinuxComputerUseInstallFlowPatch,
+ (source) =>
+ source.includes("plugin detail query requires pluginName") &&
+ /let [A-Za-z_$][\w$]*=[A-Za-z_$][\w$]*&&[A-Za-z_$][\w$]*!==`computer-use`,/.test(
+ source,
+ ),
+ (source) => source.includes("plugin detail query requires pluginName"),
+);
+
+const iconCandidates = fs.readdirSync(assetsDirectory)
+ .filter((name) => /^computer-use-plugin-icon-[^.]+\.png$/.test(name))
+ .filter((name) => name !== "computer-use-plugin-icon-linux.png");
+if (iconCandidates.length !== 1) {
+ throw new Error(
+ `Computer Use icon: expected one source, found ${iconCandidates.length}`,
+ );
+}
+let iconReferences = 0;
+for (const target of javaScriptAssets) {
+ const source = fs.readFileSync(target, "utf8");
+ if (!source.includes("computer-use-plugin-icon-linux.png")) {
+ continue;
+ }
+ fs.writeFileSync(
+ target,
+ source.replaceAll(
+ "computer-use-plugin-icon-linux.png",
+ iconCandidates[0],
+ ),
+ "utf8",
+ );
+ iconReferences += 1;
+}
+if (iconReferences !== 1) {
+ throw new Error(
+ `Computer Use icon: expected one patched reference, found ${iconReferences}`,
+ );
+}
+
+const mainBundle = fs.readdirSync(path.join(extractedApp, ".vite/build"))
+ .filter((name) => /^main-[^.]+\.js$/.test(name));
+if (mainBundle.length !== 1) {
+ throw new Error(`main bundle: expected one target, found ${mainBundle.length}`);
+}
+const mainSource = fs.readFileSync(
+ path.join(extractedApp, ".vite/build", mainBundle[0]),
+ "utf8",
+);
+for (const marker of [
+ "codexLinuxNativeDesktopApps(",
+ "codexLinuxRegisterComputerUseCursorHandler",
+ "platform!==`darwin`&&",
+]) {
+ if (!mainSource.includes(marker)) {
+ throw new Error(`main bundle: missing upstream marker ${marker}`);
+ }
+}
+console.log("Linux Computer Use upstream patches applied");
diff --git a/codex/postinstall.sh b/codex/postinstall.sh
new file mode 100755
index 0000000..8392fb2
--- /dev/null
+++ b/codex/postinstall.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+
+set -euo pipefail
+
+optional_refresh() {
+ local label=$1
+ shift
+ if ! "$@"; then
+ echo "Предупреждение: не удалось обновить ${label}." >&2
+ fi
+}
+
+sandbox=/opt/codex-app/chrome-sandbox
+if [[ ! -f "$sandbox" ]]; then
+ echo "Ошибка: не найден Electron sandbox: ${sandbox}" >&2
+ exit 1
+fi
+if [[ -L /proc/self/ns/user ]] && command -v unshare >/dev/null 2>&1 \
+ && unshare --user true 2>/dev/null; then
+ chmod 0755 "$sandbox"
+else
+ chmod 4755 "$sandbox"
+fi
+
+if command -v update-desktop-database >/dev/null 2>&1; then
+ optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications
+fi
+
+if command -v gtk-update-icon-cache >/dev/null 2>&1; then
+ optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor
+fi
+
+if command -v kbuildsycoca6 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca6 --noincremental
+elif command -v kbuildsycoca5 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca5 --noincremental
+fi
diff --git a/codex/postremove.sh b/codex/postremove.sh
new file mode 100755
index 0000000..8d4bf97
--- /dev/null
+++ b/codex/postremove.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+set -euo pipefail
+
+optional_refresh() {
+ local label=$1
+ shift
+ if ! "$@"; then
+ echo "Предупреждение: не удалось обновить ${label}." >&2
+ fi
+}
+
+if command -v update-desktop-database >/dev/null 2>&1; then
+ optional_refresh "desktop-базу" update-desktop-database -q /usr/share/applications
+fi
+
+if command -v gtk-update-icon-cache >/dev/null 2>&1; then
+ optional_refresh "кэш иконок" gtk-update-icon-cache -f -q /usr/share/icons/hicolor
+fi
+
+if command -v kbuildsycoca6 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca6 --noincremental
+elif command -v kbuildsycoca5 >/dev/null 2>&1; then
+ optional_refresh "кэш KDE" kbuildsycoca5 --noincremental
+fi
diff --git a/codex/stapler-repo.toml b/codex/stapler-repo.toml
new file mode 100644
index 0000000..d35f9bf
--- /dev/null
+++ b/codex/stapler-repo.toml
@@ -0,0 +1 @@
+[inherit]
diff --git a/docs/maintenance.md b/docs/maintenance.md
new file mode 100644
index 0000000..4d5388a
--- /dev/null
+++ b/docs/maintenance.md
@@ -0,0 +1,106 @@
+# Сопровождение Nivora
+
+## Инварианты
+
+- В репозитории ровно 19 каталогов с `Staplerfile`.
+- Каталог совпадает с `name` и командой в README.
+- Upstream-версия не меняется из-за патча рецепта; для этого повышается `release`.
+- Desktop-id, AppStream component-id, units и пути данных не меняются без отдельной миграции.
+- `provides` и `conflicts` остаются пустыми, а `replaces` содержит только собственное
+ базовое имя пакета.
+
+Stapler сам добавляет текущее `name` в generated `Provides` и `Conflicts`. Рецепты не
+добавляют переходные package ID и не зависят от других Stapler-каталогов.
+
+## Названия
+
+| Package ID | Основание |
+|:--|:--|
+| `clash-verge-rev` | Upstream-репозиторий и release title: Clash Verge Rev |
+| `claude-desktop` | Upstream DEB: `Package: claude-desktop`; desktop-id `com.anthropic.Claude` сохранён |
+| `codex` | Отображаемое имя `Codex`; команда и desktop-id `codex-app` сохранены |
+| `github-desktop` | Официальный upstream `desktop/desktop`; Linux-сборка без стороннего форка |
+| `nivora-stplr` | Собственный helper Nivora |
+
+## Локальные проверки
+
+```bash
+tools/run_checks.sh
+tools/package_updates.sh check-all
+tools/verify_artifacts.sh --all
+tools/test_package_lifecycle.sh
+```
+
+`run_checks.sh` выполняет `bash -n`, ShellCheck, Python compile, unit-тесты, validator и чтение
+всех `Staplerfile` через `stplr-spec`.
+
+`verify_artifacts.sh` сопоставляет готовые RPM с `files()`, проверяет владельцев путей,
+права, desktop-файлы, systemd units, иконки, лицензии и метаданные совместимости.
+
+`test_package_lifecycle.sh` собирает настоящие DEB текущей версии и использует настоящие
+RPM из clean-build. Минимальные fixtures изображают предыдущую версию того же пакета Nivora.
+В одноразовых Ubuntu и ALT-контейнерах через APT проверяются:
+
+1. обновление с предыдущей версии Nivora на текущую;
+2. `Provides`, `Replaces` и `Conflicts`;
+3. наличие команды, desktop-файла или systemd unit;
+4. сохранение пользовательского состояния после обновления и удаления.
+
+Полный жизненный цикл проверяется для восьми критичных пакетов. Остальные пакеты
+покрываются validator, clean-build и проверкой payload.
+
+Локально DEB собираются в привилегированном контейнере. На GitHub-hosted runner используется
+`NIVORA_DEB_BUILD_MODE=host`: закреплённый stplr запускается непосредственно на одноразовом
+Ubuntu runner, потому что вложенный sandbox stplr запрещён внутри Docker. Ubuntu 24.04 может
+дополнительно блокировать непривилегированные user namespaces через AppArmor: тест временно
+снимает только это ограничение, проверяет полный набор namespaces перед сборкой и
+восстанавливает исходное значение при завершении. Для совместимости с моделью привилегий
+Stapler временный builder включается в группу `wheel`, отсутствующую в Ubuntu по умолчанию.
+Транзакционные сценарии в обоих режимах остаются изолированными в контейнерах.
+
+## Обновление пакета
+
+```bash
+stplr-spec update-package package
+stplr-spec verify-checksums --path package/Staplerfile
+tools/run_checks.sh
+tools/clean_build.sh package
+```
+
+Нестандартная логика обнаружения версий находится в `tools/package_updates.sh`, а каждый
+`.stapler/update-check` вызывает его для своего package ID.
+
+Плановый workflow обновляет пакеты автономно и отправляет проверенные изменения
+прямо в `main`. Он запускается ежедневно в 03:00 по Владивостоку
+(`17:00 UTC`). Каждый пакет обрабатывается в отдельном временном worktree, поэтому
+несовместимое обновление одного upstream не блокирует остальные. При сбое workflow
+сохраняет на 30 дней диагностический artifact с полным логом, фазой сбоя, diff и
+получившимся `Staplerfile`; успешно собранные пакеты всё равно публикуются. Для
+каждого несовместимого пакета создаётся один постоянный issue: повторные сбои
+обновляют его, а успешное восстановление автоматически закрывает. Ожидаемый сбой
+отдельного пакета помечается предупреждением и не делает весь этап обновления
+неуспешным.
+
+## Clean-build
+
+```bash
+tools/clean_build.sh package
+tools/clean_build.sh --all
+tools/verify_artifacts.sh --all
+```
+
+Скрипт всегда выполняет сборку в собственном одноразовом контейнере ALT и не
+подключает сторонние Stapler-каталоги.
+
+## Проверка жизненного цикла
+
+Для каждого критичного пакета нужно:
+
+1. Собрать fixture предыдущей версии и текущий RPM/DEB.
+2. Создать тестовый файл в каталоге данных.
+3. Обновить пакет до текущей версии.
+4. Проверить `Provides/Replaces/Conflicts`, payload и тестовый файл.
+5. Проверить удаление пакета без удаления пользовательского состояния.
+
+Автоматизированная проверка выполняется командой `tools/test_package_lifecycle.sh` в
+одноразовых контейнерах, а не на рабочей системе сопровождающего.
diff --git a/docs/packages/claude-desktop.md b/docs/packages/claude-desktop.md
new file mode 100644
index 0000000..d034845
--- /dev/null
+++ b/docs/packages/claude-desktop.md
@@ -0,0 +1,52 @@
+# Claude Desktop
+
+Пакет `claude-desktop` переупаковывает DEB из APT-источника Anthropic и сохраняет upstream
+desktop-id `com.anthropic.Claude`.
+
+```bash
+sudo stplr install nivora/claude-desktop
+```
+
+## Два независимых приложения
+
+- `claude-desktop` запускает основное приложение Claude.
+- `claude-alt` и `claude-code-alt` запускают отдельное приложение ClaudeAlt.
+- `claude-desktop-account2` — дополнительное имя запуска ClaudeAlt.
+- Команда `claude` не создаётся, чтобы не конфликтовать с Claude Code CLI.
+
+ClaudeAlt имеет отдельные executable/resources tree, Electron `productName`, Wayland `app_id`,
+X11 `WM_CLASS`, desktop-файл, Chromium-профиль, Cowork VM socket, оконную и
+tray-иконки. В системном лотке используется штатный глиф Claude: оранжевый у
+основного приложения и бирюзовый у ClaudeAlt. Большие app-иконки окон при этом
+остаются отдельными. Поэтому окружение рабочего стола группирует Claude и ClaudeAlt
+как разные приложения, а `requestSingleInstanceLock()` и Cowork runtime каждого
+приложения работают независимо.
+
+Новая установка ClaudeAlt хранит cookies, OAuth, IndexedDB и Chromium locks в
+`${XDG_CONFIG_HOME:-~/.config}/ClaudeAlt`. Если уже существует прежний каталог
+`${XDG_CONFIG_HOME:-~/.config}/Claude-Account-2`, launcher использует его, чтобы после обновления
+не потерять авторизацию второго аккаунта. Глобальные данные Claude Code в `~/.claude` могут
+по-прежнему использоваться обоими приложениями.
+
+## Настройка
+
+| Переменная | Назначение |
+|:--|:--|
+| `CLAUDE_ALT_DATA_DIR` | Явно задать каталог профиля ClaudeAlt |
+| `CLAUDE_DESKTOP_ACCOUNT2_DIR` | Совместимое имя переменной прежнего второго профиля |
+| `CLAUDE_ALT_EXECUTABLE` | Переопределить executable ClaudeAlt для диагностики |
+
+Основной Claude продолжает использовать upstream desktop-id `com.anthropic.Claude`, а ClaudeAlt
+использует `com.anthropic.ClaudeAlt`.
+
+## Сетевая изоляция
+
+Claude и ClaudeAlt имеют разные Chromium-профили, single-instance sockets,
+cookies, browser storage, Crashpad-каталоги и Cowork VM sockets. Временные OAuth
+callback-серверы по умолчанию получают свободный loopback-порт от ОС.
+
+Приложения не являются сетевыми песочницами: оба выходят в Интернет с
+одного хоста и публичного IP-адреса. Общий `claude://` URL handler намеренно
+остаётся за основным Claude. Если в обоих приложениях вручную задать
+один и тот же фиксированный OAuth callback-порт и запустить авторизацию
+одновременно, второе приложение получит `EADDRINUSE`.
diff --git a/docs/packages/codex.md b/docs/packages/codex.md
new file mode 100644
index 0000000..6e6a1ad
--- /dev/null
+++ b/docs/packages/codex.md
@@ -0,0 +1,57 @@
+# Codex
+
+Пакет устанавливает неофициальную Linux-перепаковку Codex Desktop и
+предусмотренную Linux-реализацию Computer Use из проектов
+[`codex-desktop-linux`](https://github.com/ilysenko/codex-desktop-linux) и
+[`computer-use-linux`](https://github.com/agent-sh/computer-use-linux).
+
+В пакет входят:
+
+- приложение `codex-app`;
+- bundled plugin `computer-use@openai-bundled`;
+- нативный backend `codex-computer-use-linux`, собранный из исходников для
+ версии glibc текущего дистрибутива;
+- интеграция с AT-SPI, XDG Desktop Portal и доступными средствами ввода Linux.
+
+Патчи не включают серверную возможность в обход Codex. Они добавляют Linux в
+список поддерживаемых локальных платформ и сохраняют проверку feature gate
+`computer_use`, которую возвращает сервис.
+
+## Первичная настройка
+
+После установки полностью перезапустите Codex. Состояние backend проверяется
+без изменения системы:
+
+```bash
+codex-computer-use-linux doctor
+```
+
+Рекомендуемую настройку доступности и ввода выполняет upstream-мастер:
+
+```bash
+codex-computer-use-linux setup
+```
+
+Для получения списка окон в GNOME может понадобиться отдельная настройка
+расширения:
+
+```bash
+codex-computer-use-linux setup-window-targeting
+```
+
+Команда `setup` идемпотентно включает AT-SPI в текущем пользовательском сеансе;
+она изменяет пользовательскую настройку доступности без отдельного запроса.
+Разрешения на захват экрана и управление вводом выдаются пользователем через
+системные диалоги портала.
+
+## Диагностика
+
+```bash
+codex-computer-use-linux doctor
+codex-computer-use-linux apps
+codex-computer-use-linux windows
+```
+
+Если интерфейс всё ещё показывает недоступность, сначала убедитесь, что
+установлен пакет Nivora с `release=2` или новее, затем закройте все процессы
+Codex и запустите приложение заново.
diff --git a/docs/packages/github-desktop.md b/docs/packages/github-desktop.md
new file mode 100644
index 0000000..58432d0
--- /dev/null
+++ b/docs/packages/github-desktop.md
@@ -0,0 +1,27 @@
+# GitHub Desktop
+
+Пакет собирает оригинальный GitHub Desktop для Linux непосредственно из
+официального репозитория [`desktop/desktop`](https://github.com/desktop/desktop).
+Готовые артефакты, патчи и исходники `shiftkey/desktop` либо других форков не
+используются.
+
+GitHub официально выпускает установщики только для macOS и Windows. Linux-сборка
+в Nivora поэтому является неофициальным пакетом, но код приложения и закреплённые
+submodule-исходники берутся только из официальных репозиториев GitHub.
+
+Тяжёлая сборка выполняется workflow `Сборка GitHub Desktop для Linux` на
+нативных GitHub-hosted runner’ах `x64` и `arm64`. Workflow публикует
+детерминированно упакованные tar.gz и `SHA256SUMS` в релизе Nivora, а рецепт
+скачивает эти готовые артефакты с закреплёнными контрольными суммами.
+
+Установка:
+
+```bash
+sudo stplr install nivora/github-desktop
+```
+
+Команда запуска:
+
+```bash
+github-desktop
+```
diff --git a/docs/packages/nivora-stplr.md b/docs/packages/nivora-stplr.md
new file mode 100644
index 0000000..30208c8
--- /dev/null
+++ b/docs/packages/nivora-stplr.md
@@ -0,0 +1,43 @@
+# Nivora Stapler Helper
+
+`nivora-stplr` добавляет короткие команды для Stapler.
+
+```bash
+sudo stplr install nivora/nivora-stplr
+```
+
+## Команды
+
+| Alias | Вызов Stapler |
+|:--|:--|
+| `sli` | `sudo stplr install` |
+| `slr` | `sudo stplr remove` |
+| `slu` | `sudo stplr up` |
+| `slf` | `sudo stplr fix` |
+| `slref` | `sudo stplr refresh` |
+| `sls` | `stplr search` |
+| `slii` | `stplr info` |
+| `sll` | `stplr list` |
+| `sl` | Диспетчер тех же команд |
+
+Примеры:
+
+```bash
+sli --repo nivora parsec
+slii nivora/codex
+slu --clean
+```
+
+Короткое имя пакета не привязывается к Nivora по умолчанию. Префикс задаётся явно:
+
+```bash
+export NIVORA_STPLR_REPO=nivora
+```
+
+| Переменная | Назначение |
+|:--|:--|
+| `NIVORA_STPLR_REPO` | Префикс для коротких package names |
+| `NIVORA_STPLR_SUDO` | Команда повышения привилегий, по умолчанию `sudo` |
+| `NIVORA_STPLR_QUIET=1` | Не печатать команду перед запуском |
+
+Helper не передаёт произвольные shell-строки через `sudo`.
diff --git a/docs/packages/opencode.md b/docs/packages/opencode.md
new file mode 100644
index 0000000..d6bc8cf
--- /dev/null
+++ b/docs/packages/opencode.md
@@ -0,0 +1,16 @@
+# OpenCode
+
+Пакет устанавливает официальный Linux desktop-артефакт OpenCode без изменений его кода и ресурсов.
+
+```bash
+sudo stplr install nivora/opencode
+```
+
+## Запуск
+
+- из меню приложений — `OpenCode`;
+- из терминала — `opencode-desktop`.
+
+Команда `opencode-desktop` является прямой ссылкой на upstream-бинарник
+`/opt/OpenCode/ai.opencode.desktop`: wrapper, лимиты памяти, GPU-fallback и другие поведенческие
+патчи не применяются.
diff --git a/docs/packages/ventoy.md b/docs/packages/ventoy.md
new file mode 100644
index 0000000..8cdb1ca
--- /dev/null
+++ b/docs/packages/ventoy.md
@@ -0,0 +1,21 @@
+# Ventoy
+
+Пакет устанавливает официальный нативный Linux GUI Ventoy и показывает его в
+меню приложений под коротким названием **Ventoy**.
+
+## Запуск
+
+```bash
+ventoy
+```
+
+При запуске Ventoy запрашивает административные права через `pkexec`, потому
+что программа размечает накопители и записывает загрузочные структуры напрямую.
+
+## Важно
+
+- Перед установкой Ventoy внимательно проверьте выбранное устройство.
+- Первичная установка стирает данные на выбранном накопителе.
+- Обновление уже подготовленного Ventoy-накопителя сохраняет раздел с образами,
+ однако резервная копия важных данных всё равно рекомендуется.
+- Пакет использует официальный архив upstream и поддерживает `amd64` и `arm64`.
diff --git a/docs/security-model.md b/docs/security-model.md
new file mode 100644
index 0000000..8e0a628
--- /dev/null
+++ b/docs/security-model.md
@@ -0,0 +1,34 @@
+# Модель доверия
+
+Nivora — независимый community-репозиторий. Он не контролируется командой Stapler или
+разработчиками упакованных приложений.
+
+## Что можно проверить
+
+- `Staplerfile` описывает sources, зависимости, сборку и состав payload.
+- Локальные wrappers и hooks хранятся рядом с рецептом.
+- Каждому source соответствует SHA-256.
+- Валидатор запрещает HTTP, `SKIP`, отсутствующие локальные sources и незакреплённые Git URL.
+
+## Что не гарантирует SHA-256
+
+Контрольная сумма обнаруживает замену файла после фиксации рецепта. Она не доказывает:
+
+- отсутствие уязвимостей в upstream-приложении;
+- корректность сетевого сервиса;
+- соответствие файла исходному коду без воспроизводимой upstream-сборки.
+
+## Проприетарные пакеты
+
+Для `Custom` и `Proprietary` Nivora переупаковывает опубликованный артефакт, но не изменяет
+условия его владельца. Учётные записи, подписки, API и сетевые сервисы в пакет не входят.
+
+## Данные пользователя
+
+Пакетные scripts не должны удалять каталоги в `$HOME`, выполнять logout или сбрасывать учётную
+запись при обычном удалении. Отдельные команды очистки Tailscale и NetBird требуют `--yes`.
+
+## Границы CI
+
+CI проверяет структуру, shell/Python, metadata, checksums и выбранные сборки. Разница в версиях
+библиотек, драйверах, desktop-окружениях и внешних API остаётся ограничением.
diff --git a/fisher/.stapler/update-check b/fisher/.stapler/update-check
new file mode 100755
index 0000000..efb050a
--- /dev/null
+++ b/fisher/.stapler/update-check
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -euo pipefail
+
+package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+repo_root="$(cd "${package_dir}/.." && pwd)"
+exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")"
diff --git a/fisher/LICENSE b/fisher/LICENSE
new file mode 100644
index 0000000..debbcb9
--- /dev/null
+++ b/fisher/LICENSE
@@ -0,0 +1,12 @@
+Сведения о лицензии пакета Fisher
+
+Лицензия: MIT.
+Исходный проект: https://github.com/jorgebucaran/fisher
+
+Нюансы пакета:
+- Пакет устанавливает Fisher — менеджер плагинов для Fish shell.
+- Код распространяется на условиях MIT License.
+- Пакет не добавляет закрытых бинарных компонентов.
+
+Полный официальный текст MIT:
+https://opensource.org/licenses/MIT
diff --git a/fisher/Staplerfile b/fisher/Staplerfile
new file mode 100644
index 0000000..7cadf05
--- /dev/null
+++ b/fisher/Staplerfile
@@ -0,0 +1,54 @@
+name='fisher'
+version='4.4.8'
+release=3
+summary='Fish plugin manager'
+summary_ru='Менеджер плагинов для Fish shell'
+desc='Fisher is a plugin manager for Fish shell — zero configuration, pure Fish, blazing fast concurrent downloads.'
+desc_ru='Fisher — менеджер плагинов для Fish shell. Ноль конфигурации, чистый Fish, молниеносная параллельная загрузка.'
+group='Shells'
+homepage='https://github.com/jorgebucaran/fisher'
+maintainer='Cheviiot '
+
+architectures=('all')
+
+license=('MIT')
+
+provides=()
+replaces=('fisher')
+conflicts=()
+
+auto_reqprov_method="dirty"
+auto_req=0
+auto_prov=0
+
+deps=(
+ 'fish'
+)
+
+deps_debian=('fish')
+deps_ubuntu=('fish')
+deps_fedora=('fish')
+deps_arch=('fish')
+deps_altlinux=('fish')
+deps_opensuse=('fish')
+
+sources=(
+ "https://raw.githubusercontent.com/jorgebucaran/fisher/${version}/functions/fisher.fish"
+ 'local:///LICENSE'
+)
+checksums=(
+ '0fb6c81ae3003e95b5671766fa6c25c3597066e29965b7772f6c1b007387356d'
+ '567ce81f1514e7338d5c1a1e5c2d8ef772c1ff01d77d7b76501e67c148b14dbb'
+)
+
+package() {
+ install -Dm644 "${srcdir}/fisher.fish" \
+ "${pkgdir}/usr/share/fish/vendor_functions.d/fisher.fish"
+ install-license "${srcdir}/LICENSE" "fisher/LICENSE.nivora"
+}
+
+files() {
+ files-find \
+ "/usr/share/fish/vendor_functions.d/fisher.fish"
+ files-find-license
+}
diff --git a/fisher/stapler-repo.toml b/fisher/stapler-repo.toml
new file mode 100644
index 0000000..d35f9bf
--- /dev/null
+++ b/fisher/stapler-repo.toml
@@ -0,0 +1 @@
+[inherit]
diff --git a/github-desktop/.stapler/update-check b/github-desktop/.stapler/update-check
new file mode 100755
index 0000000..efb050a
--- /dev/null
+++ b/github-desktop/.stapler/update-check
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -euo pipefail
+
+package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+repo_root="$(cd "${package_dir}/.." && pwd)"
+exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")"
diff --git a/github-desktop/Staplerfile b/github-desktop/Staplerfile
new file mode 100644
index 0000000..debdc6c
--- /dev/null
+++ b/github-desktop/Staplerfile
@@ -0,0 +1,109 @@
+name='github-desktop'
+version='3.6.3'
+release=2
+summary='GitHub Desktop built from the official source'
+summary_ru='GitHub Desktop, собранный из официальных исходников'
+group='Development/Tools'
+desc='The original GitHub Desktop application, built for Linux by Nivora CI directly from the official desktop/desktop source tree without third-party forks.'
+desc_ru='Оригинальное приложение GitHub Desktop, собранное для Linux в CI Nivora напрямую из официального дерева desktop/desktop без сторонних форков.'
+homepage='https://github.com/desktop/desktop'
+maintainer='GitHub '
+
+architectures=('amd64' 'arm64')
+
+license=('MIT')
+
+provides=()
+replaces=('github-desktop')
+conflicts=()
+
+auto_reqprov_method="dirty"
+auto_req=0
+auto_prov=0
+
+deps=()
+deps_debian=('ca-certificates' 'git' 'libasound2' 'libatk-bridge2.0-0' 'libatk1.0-0' 'libatspi2.0-0' 'libcairo2' 'libcups2' 'libdbus-1-3' 'libexpat1' 'libgbm1' 'libglib2.0-0' 'libgtk-3-0' 'libnspr4' 'libnss3' 'libpango-1.0-0' 'libsecret-1-0' 'libudev1' 'libx11-6' 'libxcb1' 'libxcomposite1' 'libxdamage1' 'libxext6' 'libxfixes3' 'libxkbcommon0' 'libxrandr2' 'libxss1' 'libxtst6' 'xdg-utils')
+deps_ubuntu=("${deps_debian[@]}")
+deps_fedora=('ca-certificates' 'git' 'alsa-lib' 'atk' 'at-spi2-atk' 'at-spi2-core' 'cairo' 'cups-libs' 'dbus-libs' 'expat' 'mesa-libgbm' 'glib2' 'gtk3' 'nspr' 'nss' 'pango' 'libsecret' 'systemd-libs' 'libX11' 'libxcb' 'libXcomposite' 'libXdamage' 'libXext' 'libXfixes' 'libxkbcommon' 'libXrandr' 'libXScrnSaver' 'libXtst' 'xdg-utils')
+deps_arch=('ca-certificates' 'git' 'alsa-lib' 'atk' 'at-spi2-atk' 'at-spi2-core' 'cairo' 'cups' 'dbus' 'expat' 'libgbm' 'glib2' 'gtk3' 'nspr' 'nss' 'pango' 'libsecret' 'systemd-libs' 'libx11' 'libxcb' 'libxcomposite' 'libxdamage' 'libxext' 'libxfixes' 'libxkbcommon' 'libxrandr' 'libxss' 'libxtst' 'xdg-utils')
+deps_opensuse=('ca-certificates' 'git' 'alsa' 'libatk-1_0-0' 'libatk-bridge-2_0-0' 'libatspi0' 'libcairo2' 'cups-libs' 'libdbus-1-3' 'libexpat1' 'libgbm1' 'glib2' 'gtk3' 'mozilla-nspr' 'mozilla-nss' 'pango' 'libsecret-1-0' 'libudev1' 'libX11-6' 'libxcb1' 'libXcomposite1' 'libXdamage1' 'libXext6' 'libXfixes3' 'libxkbcommon0' 'libXrandr2' 'libXss1' 'libXtst6' 'xdg-utils')
+deps_altlinux=('ca-certificates' 'git' 'libalsa' 'at-spi2-atk' 'libat-spi2-core' 'libatk' 'libcairo' 'libcups' 'libdbus' 'libexpat' 'libgbm' 'glib2' 'libgtk+3' 'libnspr' 'libnss' 'libpango' 'libsecret' 'libudev0' 'libX11' 'libxcb' 'libXcomposite' 'libXdamage' 'libXext' 'libXfixes' 'libxkbcommon' 'libXrandr' 'libXScrnSaver' 'libXtst' 'xdg-utils')
+
+build_deps=('binutils')
+
+sources=(
+ "https://github.com/Cheviiot/Nivora/releases/download/github-desktop-${version}-linux/github-desktop-${version}-linux-x64.tar.gz?~archive=false&~name=github-desktop.tar.gz"
+ "https://raw.githubusercontent.com/desktop/desktop/release-${version}/LICENSE?~archive=false&~name=LICENSE"
+ "https://raw.githubusercontent.com/desktop/desktop/release-${version}/app/static/logos/prod/icon-logo.icon/Assets/1%20%E2%80%93%20Layer%203.svg?~archive=false&~name=github-desktop.svg"
+)
+
+sources_arm64=(
+ "https://github.com/Cheviiot/Nivora/releases/download/github-desktop-${version}-linux/github-desktop-${version}-linux-arm64.tar.gz?~archive=false&~name=github-desktop.tar.gz"
+ "https://raw.githubusercontent.com/desktop/desktop/release-${version}/LICENSE?~archive=false&~name=LICENSE"
+ "https://raw.githubusercontent.com/desktop/desktop/release-${version}/app/static/logos/prod/icon-logo.icon/Assets/1%20%E2%80%93%20Layer%203.svg?~archive=false&~name=github-desktop.svg"
+)
+
+checksums=(
+ 'sha256:78d55b126a200c3585c7d0e3a32b95fe5d0d19e71921169c3e376af520d9a179'
+ 'sha256:891d678cd6aa67c0712f663b5fee690f24d11d360795300814f7bf2eb91ba530'
+ 'sha256:182efd5af36dccaa389086695e38140ff42af0befd2caaa6661e4cc0ffe614b7'
+)
+
+checksums_arm64=(
+ 'sha256:b1934f1d8d621ee879ef6b7d7dabe9704134c4dc861813b776f42deedce5a038'
+ 'sha256:891d678cd6aa67c0712f663b5fee690f24d11d360795300814f7bf2eb91ba530'
+ 'sha256:182efd5af36dccaa389086695e38140ff42af0befd2caaa6661e4cc0ffe614b7'
+)
+
+scripts=(
+ ['postinstall']='postinstall.sh'
+ ['postremove']='postremove.sh'
+)
+
+package() {
+ cd "${srcdir}"
+
+ tar -xzf github-desktop.tar.gz
+ [[ -x "${srcdir}/github-desktop/desktop" ]]
+
+ install -dm755 "${pkgdir}/opt/github-desktop"
+ cp -a "${srcdir}/github-desktop/." "${pkgdir}/opt/github-desktop/"
+ chmod 0755 "${pkgdir}/opt/github-desktop/desktop"
+ if [[ -f "${pkgdir}/opt/github-desktop/chrome-sandbox" ]]; then
+ chmod 0755 "${pkgdir}/opt/github-desktop/chrome-sandbox"
+ fi
+
+ install -dm755 "${pkgdir}/usr/bin"
+ ln -sfn /opt/github-desktop/desktop "${pkgdir}/usr/bin/github-desktop"
+
+ install -Dm644 github-desktop.svg \
+ "${pkgdir}/usr/share/icons/hicolor/scalable/apps/github-desktop.svg"
+ install -Dm644 /dev/stdin \
+ "${pkgdir}/usr/share/applications/github-desktop.desktop" <<'EOF'
+[Desktop Entry]
+Name=GitHub Desktop
+Comment=Simple collaboration from your desktop
+Comment[ru]=Графический клиент для работы с GitHub
+Exec=/usr/bin/github-desktop --protocol-launcher %U
+Terminal=false
+Type=Application
+Icon=github-desktop
+Categories=Development;RevisionControl;
+MimeType=x-scheme-handler/x-github-desktop-auth;x-scheme-handler/x-github-client;x-scheme-handler/github-mac;
+StartupWMClass=desktop
+EOF
+
+ install-license LICENSE "github-desktop/LICENSE"
+ install -Dm644 "${srcdir}/github-desktop/LICENSE" \
+ "${pkgdir}/usr/share/licenses/github-desktop/LICENSE.electron.txt"
+ install -Dm644 "${srcdir}/github-desktop/LICENSES.chromium.html" \
+ "${pkgdir}/usr/share/licenses/github-desktop/LICENSES.chromium.html"
+}
+
+files() {
+ files-find "/opt/github-desktop/**/*"
+ files-find-binary
+ files-find-desktop
+ files-find-license
+ files-find "/usr/share/icons/hicolor/*/apps/github-desktop.svg"
+}
diff --git a/github-desktop/postinstall.sh b/github-desktop/postinstall.sh
new file mode 100755
index 0000000..b6f7d74
--- /dev/null
+++ b/github-desktop/postinstall.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+set -euo pipefail
+
+sandbox=/opt/github-desktop/chrome-sandbox
+if [[ -f "$sandbox" ]]; then
+ if [[ -L /proc/self/ns/user ]] && command -v unshare >/dev/null 2>&1 \
+ && unshare --user true 2>/dev/null; then
+ chmod 0755 "$sandbox"
+ else
+ chmod 4755 "$sandbox"
+ fi
+fi
+
+if command -v update-desktop-database >/dev/null 2>&1; then
+ update-desktop-database -q /usr/share/applications || true
+fi
+if command -v gtk-update-icon-cache >/dev/null 2>&1; then
+ gtk-update-icon-cache -f -q /usr/share/icons/hicolor || true
+fi
diff --git a/github-desktop/postremove.sh b/github-desktop/postremove.sh
new file mode 100755
index 0000000..c18930b
--- /dev/null
+++ b/github-desktop/postremove.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+set -euo pipefail
+
+if command -v update-desktop-database >/dev/null 2>&1; then
+ update-desktop-database -q /usr/share/applications || true
+fi
+if command -v gtk-update-icon-cache >/dev/null 2>&1; then
+ gtk-update-icon-cache -f -q /usr/share/icons/hicolor || true
+fi
diff --git a/happ/.stapler/update-check b/happ/.stapler/update-check
new file mode 100755
index 0000000..efb050a
--- /dev/null
+++ b/happ/.stapler/update-check
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -euo pipefail
+
+package_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+repo_root="$(cd "${package_dir}/.." && pwd)"
+exec "${repo_root}/tools/package_updates.sh" check "$(basename "$package_dir")"
diff --git a/happ/LICENSE b/happ/LICENSE
new file mode 100644
index 0000000..2ee1b0d
--- /dev/null
+++ b/happ/LICENSE
@@ -0,0 +1,12 @@
+Сведения о лицензии пакета Happ
+
+Лицензия: Custom.
+Официальный сайт: https://happ.su/
+Условия лицензии: https://happ.su/license
+
+Нюансы пакета:
+- Пакет перепаковывает официальный deb-артефакт Happ Desktop для Linux.
+- Happ является проприетарным приложением FlyFrog LLC.
+- В составе приложения могут использоваться проприетарные компоненты и сетевые функции, связанные с xray-core/sing-box-сценариями.
+- Использование клиента, подписок и подключений регулируется условиями Happ и провайдера сервиса.
+- Пакет помечен как несвободный через `nonfree=1`.
diff --git a/happ/Staplerfile b/happ/Staplerfile
new file mode 100644
index 0000000..8b80f61
--- /dev/null
+++ b/happ/Staplerfile
@@ -0,0 +1,336 @@
+name='happ'
+version=3.3.6
+release=4
+summary='User-friendly GUI proxy client for xray-core'
+summary_ru='Удобный GUI-прокси-клиент для xray-core'
+group='Networking/Other'
+desc='Happ is a GUI client for xray-core with TUN/VPN mode via sing-box, anti-censorship tools, QR code import and subscription management.'
+desc_ru='Happ — GUI-клиент для xray-core с режимом TUN/VPN через sing-box, средствами обхода блокировок, импортом QR-кодов и управлением подписками.'
+homepage='https://happ.su/'
+maintainer='FlyFrog LLC '
+
+architectures=('amd64' 'arm64')
+
+license=('Custom')
+nonfree=1
+nonfree_msg='Happ является проприетарным приложением FlyFrog LLC. Использование клиента, подписок и сетевых функций регулируется условиями Happ.'
+nonfree_url='https://happ.su/license'
+
+provides=()
+replaces=('happ')
+conflicts=()
+
+# auto_req отключен — Qt-приложение поставляется с bundled библиотеками
+auto_reqprov_method="dirty"
+auto_req=0
+auto_prov=0
+
+deps=()
+deps_debian=('libc6' 'libssl3 | libssl3t64')
+deps_ubuntu=('libc6' 'libssl3 | libssl3t64')
+deps_fedora=('glibc' 'openssl-libs')
+deps_arch=('glibc' 'openssl')
+deps_opensuse=('glibc' 'libopenssl1_1')
+deps_altlinux=('glibc' 'libssl3')
+
+build_deps=('binutils')
+build_deps_debian=('binutils' 'python3')
+build_deps_ubuntu=("${build_deps_debian[@]}")
+build_deps_fedora=('binutils' 'python3')
+build_deps_arch=('binutils' 'python')
+build_deps_opensuse=('binutils' 'python3')
+build_deps_altlinux=('binutils' 'python3')
+
+sources=(
+ "https://github.com/Happ-proxy/happ-desktop/releases/download/${version}/Happ.linux.x64.deb?~archive=false&~name=happ.deb"
+ 'local:///patch-tray-icons.py'
+ 'local:///happ-launcher'
+ 'local:///tray-idle.svg'
+ 'local:///tray-connected.svg'
+ 'local:///LICENSE'
+)
+
+sources_arm64=(
+ "https://github.com/Happ-proxy/happ-desktop/releases/download/${version}/Happ.linux.arm64.deb?~archive=false&~name=happ.deb"
+ 'local:///patch-tray-icons.py'
+ 'local:///happ-launcher'
+ 'local:///tray-idle.svg'
+ 'local:///tray-connected.svg'
+ 'local:///LICENSE'
+)
+
+checksums=(
+ 'a7dac51277387bfe1049b1ad40f40f2e74af233a5eab020b5be1a622effc46a4'
+ '5b0106dda286b039d6bdce45208f44ea3eb7a2deb4e00455190ce7a07177dde0'
+ 'fa4d741a4e2df82b0a19291e2c7bad4a8753a1e59237b2bfe5fef7de8c22efd2'
+ 'af47a069497a8063b9028549dc6db111776589818c6165c7a46cb8b25a822ead'
+ '7e105f63e4535676e15e4d613426d6af843376a09fd25674bbbf367778668ae5'
+ '5f0fbc1b0b0e8399c8a04425512168e3dd87960381e85a7cb62a41d55b054b0f'
+)
+
+checksums_arm64=(
+ 'a4d3d6dcab1db61db23cdf0f86bce736014887262b570927befe48cb49f14aa6'
+ '5b0106dda286b039d6bdce45208f44ea3eb7a2deb4e00455190ce7a07177dde0'
+ 'fa4d741a4e2df82b0a19291e2c7bad4a8753a1e59237b2bfe5fef7de8c22efd2'
+ 'af47a069497a8063b9028549dc6db111776589818c6165c7a46cb8b25a822ead'
+ '7e105f63e4535676e15e4d613426d6af843376a09fd25674bbbf367778668ae5'
+ '5f0fbc1b0b0e8399c8a04425512168e3dd87960381e85a7cb62a41d55b054b0f'
+)
+
+scripts=(
+ ['postinstall']='postinstall.sh'
+ ['postremove']='postremove.sh'
+)
+
+package() {
+ cd "${srcdir}"
+
+ mkdir -p "${srcdir}/happ-extracted"
+ ar x happ.deb
+ tar -xf data.tar.* -C "${srcdir}/happ-extracted"
+
+ cp -a "${srcdir}/happ-extracted/opt" "${pkgdir}/opt"
+ python3 patch-tray-icons.py \
+ "${pkgdir}/opt/happ/bin/Happ" \
+ tray-idle.svg \
+ tray-connected.svg
+
+ install-systemd "${srcdir}/happ-extracted/etc/systemd/system/happd.service"
+
+ sed -i 's|^Exec=/opt/happ/bin/Happ|Exec=/usr/bin/happ|' \
+ "${srcdir}/happ-extracted/usr/share/applications/Happ.desktop"
+ install-desktop "${srcdir}/happ-extracted/usr/share/applications/Happ.desktop"
+
+ python3 - \
+ "${srcdir}/happ-extracted/usr/share/icons/hicolor/256x256/apps/happ.png" \
+ "${pkgdir}/usr/share/icons/hicolor/512x512/apps/happ.png" <<'PY'
+import math
+import struct
+import sys
+import zlib
+from pathlib import Path
+
+PNG_SIG = b"\x89PNG\r\n\x1a\n"
+
+
+def read_png(path):
+ data = Path(path).read_bytes()
+ if not data.startswith(PNG_SIG):
+ raise ValueError("not a PNG")
+
+ pos = len(PNG_SIG)
+ width = height = bit_depth = color_type = None
+ idat = bytearray()
+ while pos < len(data):
+ length = struct.unpack(">I", data[pos:pos + 4])[0]
+ pos += 4
+ chunk_type = data[pos:pos + 4]
+ pos += 4
+ chunk = data[pos:pos + length]
+ pos += length + 4
+ if chunk_type == b"IHDR":
+ width, height, bit_depth, color_type, _, _, _ = struct.unpack(
+ ">IIBBBBB", chunk
+ )
+ elif chunk_type == b"IDAT":
+ idat.extend(chunk)
+ elif chunk_type == b"IEND":
+ break
+
+ if bit_depth != 8 or color_type not in (2, 6):
+ raise ValueError("unsupported PNG format")
+
+ channels = 4 if color_type == 6 else 3
+ raw = zlib.decompress(bytes(idat))
+ stride = width * channels
+ rows = []
+ prev = [0] * stride
+ index = 0
+
+ for _ in range(height):
+ filter_type = raw[index]
+ index += 1
+ encoded = list(raw[index:index + stride])
+ index += stride
+ row = [0] * stride
+
+ for x, value in enumerate(encoded):
+ left = row[x - channels] if x >= channels else 0
+ up = prev[x]
+ upper_left = prev[x - channels] if x >= channels else 0
+
+ if filter_type == 0:
+ decoded = value
+ elif filter_type == 1:
+ decoded = (value + left) & 255
+ elif filter_type == 2:
+ decoded = (value + up) & 255
+ elif filter_type == 3:
+ decoded = (value + ((left + up) // 2)) & 255
+ elif filter_type == 4:
+ predictor = left + up - upper_left
+ distances = (
+ abs(predictor - left),
+ abs(predictor - up),
+ abs(predictor - upper_left),
+ )
+ decoded = (value + (left, up, upper_left)[distances.index(min(distances))]) & 255
+ else:
+ raise ValueError("unsupported PNG filter")
+
+ row[x] = decoded
+
+ prev = row
+ rows.append(row)
+
+ pixels = []
+ for row in rows:
+ out_row = []
+ for x in range(width):
+ offset = x * channels
+ if channels == 4:
+ out_row.append(tuple(row[offset:offset + 4]))
+ else:
+ out_row.append((row[offset], row[offset + 1], row[offset + 2], 255))
+ pixels.append(out_row)
+
+ return width, height, pixels
+
+
+def write_png(path, width, height, pixels):
+ def chunk(chunk_type, payload):
+ crc = zlib.crc32(chunk_type + payload) & 0xFFFFFFFF
+ return struct.pack(">I", len(payload)) + chunk_type + payload + struct.pack(">I", crc)
+
+ raw = bytearray()
+ for y in range(height):
+ raw.append(0)
+ for x in range(width):
+ raw.extend(bytes(pixels[y][x]))
+
+ ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
+ Path(path).write_bytes(
+ PNG_SIG + chunk(b"IHDR", ihdr) + chunk(b"IDAT", zlib.compress(bytes(raw), 9))
+ + chunk(b"IEND", b"")
+ )
+
+
+def rounded_rect_coverage(px, py, size, margin, radius, samples=4):
+ covered = 0
+ left = top = margin
+ right = bottom = size - margin
+
+ for sy in range(samples):
+ for sx in range(samples):
+ x = px + (sx + 0.5) / samples
+ y = py + (sy + 0.5) / samples
+ if x < left or x > right or y < top or y > bottom:
+ continue
+ cx = min(max(x, left + radius), right - radius)
+ cy = min(max(y, top + radius), bottom - radius)
+ if (x - cx) ** 2 + (y - cy) ** 2 <= radius ** 2:
+ covered += 1
+
+ return covered / (samples * samples)
+
+
+def sample_alpha(alpha, x, y):
+ height = len(alpha)
+ width = len(alpha[0])
+ if x < 0 or y < 0 or x > width - 1 or y > height - 1:
+ return 0.0
+
+ x0 = int(math.floor(x))
+ y0 = int(math.floor(y))
+ x1 = min(x0 + 1, width - 1)
+ y1 = min(y0 + 1, height - 1)
+ dx = x - x0
+ dy = y - y0
+
+ top = alpha[y0][x0] * (1 - dx) + alpha[y0][x1] * dx
+ bottom = alpha[y1][x0] * (1 - dx) + alpha[y1][x1] * dx
+ return top * (1 - dy) + bottom * dy
+
+
+src_path, dest_path = sys.argv[1], sys.argv[2]
+src_width, src_height, source = read_png(src_path)
+background = 43
+
+logo_alpha = []
+for y in range(src_height):
+ row = []
+ for x in range(src_width):
+ r, g, b, a = source[y][x]
+ luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
+ row.append(max(0.0, min(1.0, (luminance - background) / (255 - background))) * (a / 255.0))
+ logo_alpha.append(row)
+
+xs = [x for y in range(src_height) for x in range(src_width) if logo_alpha[y][x] > 0.04]
+ys = [y for y in range(src_height) for x in range(src_width) if logo_alpha[y][x] > 0.04]
+left, right = min(xs), max(xs)
+top, bottom = min(ys), max(ys)
+crop_width = right - left + 1
+crop_height = bottom - top + 1
+cropped = [
+ [logo_alpha[y][x] for x in range(left, right + 1)]
+ for y in range(top, bottom + 1)
+]
+
+size = 512
+margin = 38
+radius = 116
+logo_height = 292
+logo_width = round(crop_width * logo_height / crop_height)
+logo_x = (size - logo_width) // 2
+logo_y = (size - logo_height) // 2 + 6
+
+pixels = [[(0, 0, 0, 0) for _ in range(size)] for _ in range(size)]
+for y in range(size):
+ for x in range(size):
+ coverage = rounded_rect_coverage(x, y, size, margin, radius)
+ if coverage:
+ pixels[y][x] = (43, 43, 43, round(255 * coverage))
+
+for y in range(logo_height):
+ source_y = (y + 0.5) * crop_height / logo_height - 0.5
+ target_y = logo_y + y
+ if target_y < 0 or target_y >= size:
+ continue
+
+ for x in range(logo_width):
+ source_x = (x + 0.5) * crop_width / logo_width - 0.5
+ target_x = logo_x + x
+ if target_x < 0 or target_x >= size:
+ continue
+
+ alpha = sample_alpha(cropped, source_x, source_y)
+ if alpha <= 0:
+ continue
+
+ dr, dg, db, da = pixels[target_y][target_x]
+ da_float = da / 255.0
+ out_alpha = alpha + da_float * (1 - alpha)
+ out_rgb = round((255 * alpha + dr * da_float * (1 - alpha)) / out_alpha)
+ pixels[target_y][target_x] = (out_rgb, out_rgb, out_rgb, round(out_alpha * 255))
+
+write_png(dest_path, size, size, pixels)
+PY
+
+ install -Dm644 "${srcdir}/happ-extracted/usr/share/mime/packages/happ-mime.xml" \
+ "${pkgdir}/usr/share/mime/packages/happ-mime.xml"
+
+ install -Dm755 "${srcdir}/happ-launcher" "${pkgdir}/usr/bin/happ"
+ install-license "${srcdir}/LICENSE" "happ/LICENSE.nivora"
+}
+
+files() {
+ files-find "/opt/happ/**/*"
+ files-find-binary
+ files-find-desktop
+ files-find-systemd
+ files-find-license
+ files-find \
+ "/usr/share/icons/hicolor/512x512/apps/happ.png" \
+ "/usr/share/mime/packages/happ-mime.xml"
+}
diff --git a/happ/happ-launcher b/happ/happ-launcher
new file mode 100755
index 0000000..a8e337d
--- /dev/null
+++ b/happ/happ-launcher
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+# Happ's bundled Qt falls back to the generic GNOME platform theme, which
+# reports Qt::ColorScheme::Unknown. The bundled portal plugin follows the
+# desktop color-scheme setting and emits updates when it changes.
+QT_QPA_PLATFORMTHEME="${QT_QPA_PLATFORMTHEME:-xdgdesktopportal}"
+export QT_QPA_PLATFORMTHEME
+
+exec /opt/happ/bin/Happ "$@"
diff --git a/happ/patch-tray-icons.py b/happ/patch-tray-icons.py
new file mode 100755
index 0000000..369085e
--- /dev/null
+++ b/happ/patch-tray-icons.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import hashlib
+import struct
+import sys
+from pathlib import Path
+
+
+UPSTREAM_RESOURCES = {
+ "idle": (
+ "c0939b3a81a59983f0db07cdfa08eae90d6ddc211b7b790c425beb72628355ea",
+ b'