[appimage] Auto-Updater: Package Registry + update.json + Nix-Wrapper
All checks were successful
Build AppImage / build (push) Successful in 7m52s
All checks were successful
Build AppImage / build (push) Successful in 7m52s
- update.rs: Umstellung auf Package-Registry-Manifest mit SHA256-Verify, Basic-Auth, dev/APPIMAGE/Nix-Wrapper-Modus. Liest binary_filename im Nix-Modus (AppImage laeuft auf NixOS nicht) - Nix-Wrapper-Paket (nix/default.nix): LD_LIBRARY_PATH-korrekter Launcher + Installer-Script, User-Home-Binary (writable fuer Auto-Update) - CI laedt jetzt AppImage UND natives Binary + update.json v2 (binary_filename/binary_sha256) in die Package Registry - Svelte: Store-basierter Update-Trigger, manueller Check im Settings-Panel, "Kein Update"-Dialog-Variante, expectedSha256-Param - install.sh: One-Click-Installer fuer NixOS (curl | bash) - sha2-Dep fuer Integritaets-Check des Downloads Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4e36b04cc9
commit
506f1d3fdc
14 changed files with 963 additions and 236 deletions
|
|
@ -28,10 +28,22 @@ jobs:
|
|||
rustc --version
|
||||
cargo --version
|
||||
|
||||
- name: App-Version festlegen
|
||||
# Version EINMAL am Anfang festlegen (KB #160) — wird als Env an alle Folge-Steps durchgereicht
|
||||
run: |
|
||||
APP_VERSION="$(date +%Y%m%d-%H%M)"
|
||||
echo "APP_VERSION=${APP_VERSION}" >> $GITHUB_ENV
|
||||
echo "App-Version: ${APP_VERSION}"
|
||||
|
||||
- name: Install npm packages
|
||||
run: npm ci
|
||||
|
||||
- name: Build Tauri App
|
||||
env:
|
||||
APP_VERSION: ${{ env.APP_VERSION }}
|
||||
# Wird als Basic-Auth (user: "data") fuer /api/packages gebraucht.
|
||||
# REGISTRY_TOKEN hat Package-Read-Rechte und ist im Repo bereits gesetzt.
|
||||
UPDATE_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
npm run tauri build -- --bundles appimage
|
||||
ls -la src-tauri/target/release/bundle/appimage/
|
||||
|
|
@ -88,51 +100,100 @@ jobs:
|
|||
|
||||
ls -la "$BUNDLE_DIR/"
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(grep '^version' src-tauri/Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "Version: ${VERSION}"
|
||||
|
||||
- name: Upload to Package Registry
|
||||
run: |
|
||||
set -e
|
||||
# --- AppImage vorbereiten ---
|
||||
ORIG=$(ls src-tauri/target/release/bundle/appimage/*.AppImage | head -1)
|
||||
# Tauri benennt mit "Claude Desktop_..." (Leerzeichen) -> URL-unsicher.
|
||||
# Umbenennen zu "Claude-Desktop_..." vor dem Upload.
|
||||
SAFE_NAME=$(basename "$ORIG" | tr ' ' '-')
|
||||
APPIMAGE="$(dirname "$ORIG")/$SAFE_NAME"
|
||||
mv "$ORIG" "$APPIMAGE"
|
||||
FILENAME="$SAFE_NAME"
|
||||
VERSION=$(grep '^version' src-tauri/Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||
|
||||
echo "Lade $FILENAME (v${VERSION}) in Package Registry..."
|
||||
# --- Natives Binary fuer Nix-Wrapper-Installationen vorbereiten ---
|
||||
# Cargo legt's unter target/release/claude-desktop (ohne CARGO_TARGET_DIR)
|
||||
BINARY_SRC="src-tauri/target/release/claude-desktop"
|
||||
if [ ! -x "$BINARY_SRC" ]; then
|
||||
echo "❌ Erwartetes Binary nicht gefunden: $BINARY_SRC" >&2
|
||||
exit 1
|
||||
fi
|
||||
BINARY_NAME="claude-desktop_${APP_VERSION}_linux-x86_64"
|
||||
cp "$BINARY_SRC" "/tmp/${BINARY_NAME}"
|
||||
|
||||
echo "Lade $FILENAME + $BINARY_NAME (v${APP_VERSION}) in Package Registry..."
|
||||
|
||||
BASE="https://git.data-it-solution.de/api/packages/data/generic/claude-desktop"
|
||||
|
||||
# Latest + versionierte Datei loeschen falls vorhanden
|
||||
# (Forgejo Package Registry weist PUT auf existierenden Pfad mit 409 ab)
|
||||
curl -sS -X DELETE \
|
||||
--user "data:${{ secrets.REGISTRY_TOKEN }}" \
|
||||
"${BASE}/latest/${FILENAME}" >/dev/null 2>&1 || true
|
||||
curl -sS -X DELETE \
|
||||
--user "data:${{ secrets.REGISTRY_TOKEN }}" \
|
||||
"${BASE}/${VERSION}/${FILENAME}" >/dev/null 2>&1 || true
|
||||
# SHA256-Hashes fuer Integritaets-Check in update.json
|
||||
SHA256=$(sha256sum "$APPIMAGE" | awk '{print $1}')
|
||||
BINARY_SHA256=$(sha256sum "/tmp/${BINARY_NAME}" | awk '{print $1}')
|
||||
NOTES=$(git log -1 --pretty=%s)
|
||||
RELEASED_AT=$(date -Iseconds)
|
||||
|
||||
# Versioniert hochladen
|
||||
# update.json-Manifest bauen — wird vom Client-Updater (update.rs) gelesen.
|
||||
# binary_* werden im Nix-Wrapper-Modus verwendet (siehe nix/default.nix).
|
||||
cat > /tmp/update.json <<EOF
|
||||
{
|
||||
"version": "${APP_VERSION}",
|
||||
"filename": "${FILENAME}",
|
||||
"sha256": "${SHA256}",
|
||||
"binary_filename": "${BINARY_NAME}",
|
||||
"binary_sha256": "${BINARY_SHA256}",
|
||||
"notes": $(printf '%s' "$NOTES" | jq -Rs .),
|
||||
"released_at": "${RELEASED_AT}"
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "--- update.json ---"
|
||||
cat /tmp/update.json
|
||||
|
||||
# Alte Dateien loeschen (Forgejo weist PUT auf existierende Pfade mit 409 ab)
|
||||
for PATH_SUFFIX in \
|
||||
"latest/${FILENAME}" "${APP_VERSION}/${FILENAME}" \
|
||||
"latest/${BINARY_NAME}" "${APP_VERSION}/${BINARY_NAME}" \
|
||||
"latest/update.json" "${APP_VERSION}/update.json"; do
|
||||
curl -sS -X DELETE \
|
||||
--user "data:${{ secrets.REGISTRY_TOKEN }}" \
|
||||
"${BASE}/${PATH_SUFFIX}" >/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
# AppImage versioniert + latest hochladen
|
||||
curl --fail -sS -X PUT \
|
||||
--user "data:${{ secrets.REGISTRY_TOKEN }}" \
|
||||
--upload-file "$APPIMAGE" \
|
||||
"${BASE}/${VERSION}/${FILENAME}"
|
||||
"${BASE}/${APP_VERSION}/${FILENAME}"
|
||||
|
||||
# Latest hochladen
|
||||
curl --fail -sS -X PUT \
|
||||
--user "data:${{ secrets.REGISTRY_TOKEN }}" \
|
||||
--upload-file "$APPIMAGE" \
|
||||
"${BASE}/latest/${FILENAME}"
|
||||
|
||||
echo "Upload abgeschlossen: ${FILENAME} (v${VERSION})"
|
||||
# Natives Binary versioniert + latest hochladen
|
||||
curl --fail -sS -X PUT \
|
||||
--user "data:${{ secrets.REGISTRY_TOKEN }}" \
|
||||
--upload-file "/tmp/${BINARY_NAME}" \
|
||||
"${BASE}/${APP_VERSION}/${BINARY_NAME}"
|
||||
|
||||
curl --fail -sS -X PUT \
|
||||
--user "data:${{ secrets.REGISTRY_TOKEN }}" \
|
||||
--upload-file "/tmp/${BINARY_NAME}" \
|
||||
"${BASE}/latest/${BINARY_NAME}"
|
||||
|
||||
# update.json versioniert + latest hochladen
|
||||
curl --fail -sS -X PUT \
|
||||
--user "data:${{ secrets.REGISTRY_TOKEN }}" \
|
||||
--upload-file /tmp/update.json \
|
||||
"${BASE}/${APP_VERSION}/update.json"
|
||||
|
||||
curl --fail -sS -X PUT \
|
||||
--user "data:${{ secrets.REGISTRY_TOKEN }}" \
|
||||
--upload-file /tmp/update.json \
|
||||
"${BASE}/latest/update.json"
|
||||
|
||||
echo "Upload abgeschlossen: ${FILENAME} + ${BINARY_NAME} (v${APP_VERSION})"
|
||||
echo " AppImage SHA256: ${SHA256}"
|
||||
echo " Binary SHA256: ${BINARY_SHA256}"
|
||||
|
||||
- name: Upload to Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -10,6 +10,10 @@ dist/
|
|||
src-tauri/target/
|
||||
src-tauri/gen/
|
||||
|
||||
# Nix build outputs
|
||||
result
|
||||
result-*
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
|
|
|||
70
BUILD-NIXOS.md
Normal file
70
BUILD-NIXOS.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Claude Desktop — Native Build auf NixOS
|
||||
|
||||
Anleitung für den nativen Produktiv-Build auf NixOS. **Das CI-AppImage funktioniert auf NixOS nicht** (WebKit2GTK/Mesa EGL-ABI-Konflikt, siehe KB #381) — deshalb wird hier immer nativ über die Nix-Dev-Shell gebaut.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- NixOS-System mit `nix-shell` im PATH
|
||||
- `shell.nix` im Projekt-Root (liefert Rust, Node 22, WebKitGTK 4.1, GTK3, OpenSSL, libsoup3 etc.)
|
||||
- Projekt liegt auf SMB-Mount — `/mnt/17 - Entwicklungen/20 - Projekte/ClaudeDesktop`
|
||||
|
||||
## Warum `CARGO_TARGET_DIR=/tmp/...`?
|
||||
|
||||
Das Projekt liegt auf einem SMB-Share. Cargo kann dort nicht zuverlässig schreiben (I/O-Errors bei Lockfiles, Rename-Operationen scheitern). Deshalb muss das Build-Target auf ein lokales Dateisystem (tmpfs/ext4) umgeleitet werden. Siehe KB #382.
|
||||
|
||||
## Build-Befehl (der Pflicht-Weg)
|
||||
|
||||
```bash
|
||||
CARGO_TARGET_DIR=/tmp/claude-target \
|
||||
nix-shell shell.nix --run 'npm ci && npm run tauri build -- --bundles appimage'
|
||||
```
|
||||
|
||||
Phasen:
|
||||
|
||||
1. `npm ci` — Frontend-Abhängigkeiten (überspringbar wenn `node_modules/` aktuell ist)
|
||||
2. SvelteKit-Build — produziert `build/` (Vite)
|
||||
3. Cargo-Build `--release` — kompiliert Rust-Binary in `/tmp/claude-target/release/claude-desktop`
|
||||
4. Tauri-Bundle (AppImage) — **crasht auf NixOS oft in der pkg-config-Phase von `linuxdeploy`**
|
||||
|
||||
> Das Bundling darf scheitern — das Binary aus Phase 3 ist trotzdem lauffähig.
|
||||
|
||||
## Binary starten
|
||||
|
||||
```bash
|
||||
nix-shell shell.nix --run /tmp/claude-target/release/claude-desktop
|
||||
```
|
||||
|
||||
Der Start muss durch `nix-shell` laufen, damit `LD_LIBRARY_PATH` korrekt gesetzt ist (WebKitGTK, GTK3 etc.).
|
||||
|
||||
## Inkrementelle Builds
|
||||
|
||||
Nach Code-Änderungen reicht:
|
||||
|
||||
```bash
|
||||
CARGO_TARGET_DIR=/tmp/claude-target \
|
||||
nix-shell shell.nix --run 'npm run tauri build -- --bundles appimage'
|
||||
```
|
||||
|
||||
- Nur Rust geändert → Cargo baut inkrementell neu (~10–60 s)
|
||||
- Nur Frontend geändert → Vite+Cargo linken neu (~30 s)
|
||||
- Abhängigkeiten neu → erster Cargo-Build dauert 5–15 min
|
||||
|
||||
## Typische Fehler
|
||||
|
||||
| Symptom | Ursache | Fix |
|
||||
|---|---|---|
|
||||
| `error: could not write to ...` | Cargo auf SMB | `CARGO_TARGET_DIR=/tmp/...` vorne anstellen |
|
||||
| `pkg-config exited with status code 1` am Ende | Tauri-Bundler-Bug bei AppImage auf NixOS | ignorieren, Binary ist fertig |
|
||||
| `EGL_BAD_PARAMETER` beim Start | AppImage statt nativem Binary gestartet | `/tmp/claude-target/release/claude-desktop` direkt starten |
|
||||
| `webkit2gtk-4.1 not found` | außerhalb von `nix-shell` | Start immer via `nix-shell shell.nix --run …` |
|
||||
| `rustc: command not found` | außerhalb von `nix-shell` | siehe oben |
|
||||
|
||||
## Hot-Reload-Dev-Mode (Alternative)
|
||||
|
||||
Für UI-Entwicklung reicht meist der Dev-Modus — kein Release-Build nötig:
|
||||
|
||||
```bash
|
||||
nix-shell shell.nix --run 'npm ci && npm run tauri:dev'
|
||||
```
|
||||
|
||||
Vite auf Port 1420 + Tauri-Window. Rust wird unoptimiert gebaut, deutlich schneller für Iteration.
|
||||
148
install.sh
Normal file
148
install.sh
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
#!/usr/bin/env bash
|
||||
# Claude Desktop — One-Click-Installer für NixOS
|
||||
#
|
||||
# Installiert:
|
||||
# - Nix-Wrapper-Paket (nix/default.nix) ins User-Profil via nix-env
|
||||
# - Natives Binary aus der Forgejo Package Registry nach ~/.local/share/claude-desktop/bin
|
||||
# - Desktop-Entry im System-Menue (KDE/GNOME/…)
|
||||
#
|
||||
# Aufruf:
|
||||
# curl -fsSL https://git.data-it-solution.de/data/claude-desktop/raw/branch/main/install.sh | bash
|
||||
#
|
||||
# Oder lokal:
|
||||
# ./install.sh
|
||||
#
|
||||
# Env-Vars (optional):
|
||||
# CLAUDE_DESKTOP_TOKEN — Forgejo Read-Token (sonst wird einmalig gefragt)
|
||||
# CLAUDE_DESKTOP_BRANCH — Git-Branch für Wrapper-Dateien (default: main)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- Konfiguration ---------------------------------------------------------
|
||||
REPO_URL="https://git.data-it-solution.de/data/claude-desktop"
|
||||
PKG_BASE="https://git.data-it-solution.de/api/packages/data/generic/claude-desktop/latest"
|
||||
APP_DIR="$HOME/.local/share/claude-desktop"
|
||||
BIN_DIR="$APP_DIR/bin"
|
||||
TOKEN_FILE="$HOME/.config/claude-desktop/token"
|
||||
BRANCH="${CLAUDE_DESKTOP_BRANCH:-main}"
|
||||
|
||||
# --- Farben ----------------------------------------------------------------
|
||||
if [ -t 1 ]; then
|
||||
BOLD="$(tput bold)"; RED="$(tput setaf 1)"; GRN="$(tput setaf 2)"
|
||||
YEL="$(tput setaf 3)"; BLU="$(tput setaf 4)"; RST="$(tput sgr0)"
|
||||
else
|
||||
BOLD=""; RED=""; GRN=""; YEL=""; BLU=""; RST=""
|
||||
fi
|
||||
|
||||
step() { echo "${BOLD}${BLU}▶${RST} $*"; }
|
||||
ok() { echo " ${GRN}✓${RST} $*"; }
|
||||
err() { echo "${RED}✗${RST} $*" >&2; exit 1; }
|
||||
|
||||
# --- 1. Voraussetzungen pruefen -------------------------------------------
|
||||
step "Voraussetzungen pruefen"
|
||||
for cmd in nix-build nix-env curl jq sha256sum; do
|
||||
command -v "$cmd" >/dev/null || err "$cmd nicht installiert. Abbruch."
|
||||
done
|
||||
ok "Nix, curl, jq, sha256sum vorhanden"
|
||||
|
||||
if [ ! -f /etc/NIXOS ] && [ ! -e /etc/nix/nix.conf ]; then
|
||||
echo " ${YEL}!${RST} Kein NixOS-System erkannt — der Installer setzt Nix voraus."
|
||||
fi
|
||||
|
||||
# --- 2. Forgejo-Token besorgen --------------------------------------------
|
||||
step "Forgejo-Token laden"
|
||||
TOKEN="${CLAUDE_DESKTOP_TOKEN:-}"
|
||||
if [ -z "$TOKEN" ] && [ -f "$TOKEN_FILE" ]; then
|
||||
TOKEN=$(cat "$TOKEN_FILE")
|
||||
ok "Token aus $TOKEN_FILE geladen"
|
||||
fi
|
||||
if [ -z "$TOKEN" ]; then
|
||||
echo " Forgejo Read-Token für Package Registry (Basic-Auth User: ${BOLD}data${RST}):"
|
||||
read -rs -p " Token: " TOKEN
|
||||
echo
|
||||
mkdir -p "$(dirname "$TOKEN_FILE")"
|
||||
printf '%s' "$TOKEN" > "$TOKEN_FILE"
|
||||
chmod 600 "$TOKEN_FILE"
|
||||
ok "Token in $TOKEN_FILE gespeichert (chmod 600)"
|
||||
fi
|
||||
|
||||
# --- 3. Manifest laden ----------------------------------------------------
|
||||
step "Manifest abrufen (latest/update.json)"
|
||||
MANIFEST=$(curl -fsSL --user "data:$TOKEN" "$PKG_BASE/update.json") \
|
||||
|| err "Manifest nicht erreichbar (Token falsch? Netz?)"
|
||||
|
||||
VERSION=$(echo "$MANIFEST" | jq -r '.version')
|
||||
BINARY_NAME=$(echo "$MANIFEST" | jq -r '.binary_filename // empty')
|
||||
BINARY_SHA=$(echo "$MANIFEST" | jq -r '.binary_sha256 // empty')
|
||||
|
||||
if [ -z "$BINARY_NAME" ] || [ -z "$BINARY_SHA" ]; then
|
||||
err "update.json enthaelt kein binary_filename/binary_sha256. Alter CI-Build? Erst neue Pipeline laufen lassen."
|
||||
fi
|
||||
|
||||
ok "Version $VERSION, Binary $BINARY_NAME"
|
||||
|
||||
# --- 4. Binary herunterladen ----------------------------------------------
|
||||
step "Binary laden -> $BIN_DIR"
|
||||
mkdir -p "$BIN_DIR"
|
||||
TMP_BIN=$(mktemp)
|
||||
trap 'rm -f "$TMP_BIN"' EXIT
|
||||
|
||||
curl -fsSL --user "data:$TOKEN" -o "$TMP_BIN" "$PKG_BASE/$BINARY_NAME" \
|
||||
|| err "Binary-Download fehlgeschlagen"
|
||||
|
||||
ACTUAL_SHA=$(sha256sum "$TMP_BIN" | awk '{print $1}')
|
||||
if [ "$ACTUAL_SHA" != "$BINARY_SHA" ]; then
|
||||
err "SHA256-Mismatch: erwartet $BINARY_SHA, bekommen $ACTUAL_SHA"
|
||||
fi
|
||||
ok "SHA256 verifiziert"
|
||||
|
||||
mv "$TMP_BIN" "$BIN_DIR/claude-desktop"
|
||||
chmod +x "$BIN_DIR/claude-desktop"
|
||||
ok "Binary installiert: $BIN_DIR/claude-desktop"
|
||||
|
||||
# --- 5. Nix-Wrapper bauen -------------------------------------------------
|
||||
step "Nix-Wrapper-Paket bauen"
|
||||
WORKDIR=$(mktemp -d)
|
||||
trap 'rm -f "$TMP_BIN"; rm -rf "$WORKDIR"' EXIT
|
||||
|
||||
mkdir -p "$WORKDIR/nix" "$WORKDIR/src-tauri/icons"
|
||||
for f in \
|
||||
"nix/default.nix" \
|
||||
"nix/claude-desktop.desktop" \
|
||||
"src-tauri/icons/icon.png"; do
|
||||
curl -fsSL -o "$WORKDIR/$f" "$REPO_URL/raw/branch/$BRANCH/$f" \
|
||||
|| err "Kann $f nicht laden ($REPO_URL/raw/branch/$BRANCH/$f)"
|
||||
done
|
||||
ok "Wrapper-Quelldateien geladen"
|
||||
|
||||
pushd "$WORKDIR" >/dev/null
|
||||
RESULT=$(nix-build nix/default.nix --no-out-link 2>&1 | tail -1)
|
||||
popd >/dev/null
|
||||
[ -d "$RESULT" ] || err "nix-build fehlgeschlagen"
|
||||
ok "Paket gebaut: $RESULT"
|
||||
|
||||
# --- 6. Ins User-Profil installieren --------------------------------------
|
||||
step "Ins User-Profil installieren (nix-env)"
|
||||
nix-env --uninstall claude-desktop 2>/dev/null || true
|
||||
nix-env -i "$RESULT"
|
||||
ok "claude-desktop im \$PATH verfuegbar"
|
||||
|
||||
# --- 7. Desktop-Datenbank refreshen ---------------------------------------
|
||||
step "Desktop-Menue aktualisieren"
|
||||
if command -v update-desktop-database >/dev/null; then
|
||||
update-desktop-database "$HOME/.nix-profile/share/applications" 2>/dev/null || true
|
||||
fi
|
||||
if command -v gtk-update-icon-cache >/dev/null; then
|
||||
gtk-update-icon-cache -q "$HOME/.nix-profile/share/icons/hicolor" 2>/dev/null || true
|
||||
fi
|
||||
ok "Menue + Icon-Cache aktualisiert"
|
||||
|
||||
# --- Fertig ---------------------------------------------------------------
|
||||
echo
|
||||
echo "${BOLD}${GRN}✓ Claude Desktop v${VERSION} installiert${RST}"
|
||||
echo
|
||||
echo "Starten:"
|
||||
echo " ${BOLD}claude-desktop${RST} (Terminal)"
|
||||
echo " oder aus dem System-Menue: 'Claude Desktop'"
|
||||
echo
|
||||
echo "Updates holt die App selbstaendig (Button in Settings oder Auto-Check beim Start)."
|
||||
94
nix/INSTALL.md
Normal file
94
nix/INSTALL.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Claude Desktop — NixOS-Integration
|
||||
|
||||
Dauerhaftes Einbinden der App ins System **und** funktionierendes Auto-Update. Der Trick: das Nix-Paket liefert nur einen Wrapper + Desktop-Entry, das eigentliche Binary lebt im User-Home (`~/.local/share/claude-desktop/bin/`), damit der Auto-Updater es ersetzen kann — der Nix-Store ist read-only.
|
||||
|
||||
## Einmaliger Setup
|
||||
|
||||
### 1. Binary einmal nativ bauen
|
||||
|
||||
```bash
|
||||
cd <repo>
|
||||
CARGO_TARGET_DIR=/tmp/claude-target \
|
||||
nix-shell shell.nix --run 'npm ci && npm run tauri build -- --bundles appimage'
|
||||
```
|
||||
|
||||
Das liefert das Binary unter `/tmp/claude-target/release/claude-desktop` (~13 MB). Das Bundling-Fail am Ende ignorieren — das Binary ist fertig.
|
||||
|
||||
### 2. Nix-Paket in die System-Config einbinden
|
||||
|
||||
**Variante A — configuration.nix (system-weit):**
|
||||
```nix
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
environment.systemPackages = [
|
||||
(import /pfad/zum/claude-desktop/nix/default.nix { inherit pkgs; })
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
Dann `sudo nixos-rebuild switch`.
|
||||
|
||||
**Variante B — home-manager (nur eigener User):**
|
||||
```nix
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
home.packages = [
|
||||
(import /pfad/zum/claude-desktop/nix/default.nix { inherit pkgs; })
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
Dann `home-manager switch`.
|
||||
|
||||
**Variante C — nur testen ohne System-Config:**
|
||||
```bash
|
||||
nix-build nix/default.nix
|
||||
nix-env -i ./result
|
||||
```
|
||||
|
||||
### 3. Binary in den User-Home kopieren
|
||||
|
||||
```bash
|
||||
claude-desktop-install
|
||||
```
|
||||
|
||||
Macht `cp /tmp/claude-target/release/claude-desktop ~/.local/share/claude-desktop/bin/`. Das Paket liefert diesen Helper mit.
|
||||
|
||||
### 4. Starten
|
||||
|
||||
- Aus dem KDE/GNOME-Menü: „Claude Desktop"
|
||||
- Oder im Terminal: `claude-desktop`
|
||||
|
||||
## Wie der Auto-Update in diesem Setup funktioniert
|
||||
|
||||
- Der Launcher setzt `CLAUDE_DESKTOP_NIX_WRAPPER=1` und `CLAUDE_DESKTOP_BIN=$HOME/.local/share/claude-desktop/bin/claude-desktop` als Env-Vars
|
||||
- `apply_update` in der App erkennt diesen Modus und überschreibt das User-Binary statt `$APPIMAGE`
|
||||
- Der Rename-Trick klappt, weil `~/.local/share` schreibbar ist
|
||||
- Nach Update-Apply macht Tauri `app.restart()` — der Nix-Wrapper startet das neue Binary mit denselben LD-Libs
|
||||
|
||||
## Bekannte Einschränkungen (v1)
|
||||
|
||||
- **Auto-Update lädt aktuell nur das AppImage** aus der Package Registry, das im Nix-Modus nicht direkt läuft. Kurzfristig wird die heruntergeladene Datei ins User-Binary-Ziel geschrieben → beim Neustart schlägt sie fehl. **Workaround:** bei Update-Hinweis manuell neu bauen: `git pull; npm run tauri build; claude-desktop-install`.
|
||||
- **Saubere Lösung (geplant):** CI lädt zusätzlich ein natives Binary in die Package Registry (extract aus dem AppImage-squashfs), `update.rs` nimmt je nach Modus AppImage oder Binary.
|
||||
|
||||
## Deinstallation
|
||||
|
||||
```bash
|
||||
# Aus System-Config entfernen (configuration.nix/home.nix bearbeiten, dann switch)
|
||||
# User-Binary + Daten loeschen:
|
||||
rm -rf ~/.local/share/claude-desktop
|
||||
rm -rf ~/.local/share/de.alles-watt-laeuft.claude-desktop
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
```bash
|
||||
# Sehen was der Launcher macht
|
||||
bash -x $(which claude-desktop)
|
||||
|
||||
# Pruefen ob das User-Binary existiert & ausfuehrbar ist
|
||||
ls -l ~/.local/share/claude-desktop/bin/claude-desktop
|
||||
|
||||
# Library-Pfad der wrapped-Version inspizieren
|
||||
cat $(which claude-desktop)
|
||||
```
|
||||
13
nix/claude-desktop.desktop
Normal file
13
nix/claude-desktop.desktop
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=Claude Desktop
|
||||
GenericName=KI-Assistent
|
||||
Comment=Native Desktop-App für Claude Code
|
||||
Exec=claude-desktop %U
|
||||
Icon=claude-desktop
|
||||
Terminal=false
|
||||
Categories=Development;IDE;Utility;
|
||||
Keywords=claude;ai;code;assistant;anthropic;
|
||||
StartupWMClass=claude-desktop
|
||||
StartupNotify=true
|
||||
129
nix/default.nix
Normal file
129
nix/default.nix
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# Claude Desktop — Nix-Wrapper-Paket
|
||||
#
|
||||
# Liefert:
|
||||
# $out/bin/claude-desktop — Launcher mit LD_LIBRARY_PATH
|
||||
# $out/bin/claude-desktop-install — Installer (kopiert Binary nach ~/.local/share)
|
||||
# $out/share/applications/… — Desktop-Entry
|
||||
# $out/share/icons/… — Icon
|
||||
#
|
||||
# Das eigentliche Binary lebt unter ~/.local/share/claude-desktop/bin/claude-desktop
|
||||
# (writable), damit der Auto-Updater es ersetzen kann. Nix-Store ist read-only und
|
||||
# waere deshalb inkompatibel mit dem Rename-Trick in apply_update().
|
||||
#
|
||||
# Einbinden in /etc/nixos/configuration.nix:
|
||||
#
|
||||
# environment.systemPackages = [
|
||||
# (import /pfad/zum/claude-desktop/nix/default.nix { inherit pkgs; })
|
||||
# ];
|
||||
#
|
||||
# Oder per home-manager:
|
||||
# home.packages = [ (import ./claude-desktop/nix/default.nix { inherit pkgs; }) ];
|
||||
|
||||
{ pkgs ? import <nixpkgs> {} }:
|
||||
|
||||
let
|
||||
# Alle Laufzeit-Libs, die das Tauri-Binary braucht (parallel zu shell.nix).
|
||||
runtimeLibs = with pkgs; [
|
||||
webkitgtk_4_1
|
||||
libappindicator-gtk3
|
||||
librsvg
|
||||
gtk3
|
||||
glib
|
||||
cairo
|
||||
pango
|
||||
gdk-pixbuf
|
||||
libsoup_3
|
||||
at-spi2-atk
|
||||
openssl
|
||||
];
|
||||
in
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "claude-desktop";
|
||||
version = "0.1.0";
|
||||
# Keine Quelldateien — Wir packen nur Wrapper + Desktop-Entry
|
||||
dontUnpack = true;
|
||||
|
||||
nativeBuildInputs = [ pkgs.makeWrapper ];
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
mkdir -p $out/bin $out/share/applications $out/share/icons/hicolor/256x256/apps
|
||||
|
||||
# 1) Launcher: startet das User-Binary mit Nix-LD_LIBRARY_PATH
|
||||
cat > $out/bin/claude-desktop <<'LAUNCHER'
|
||||
#!${pkgs.bash}/bin/bash
|
||||
# Claude Desktop — NixOS-Launcher
|
||||
set -e
|
||||
|
||||
APP_DIR="$HOME/.local/share/claude-desktop"
|
||||
BIN="$APP_DIR/bin/claude-desktop"
|
||||
|
||||
if [ ! -x "$BIN" ]; then
|
||||
echo "⚠️ Claude-Desktop-Binary nicht gefunden: $BIN" >&2
|
||||
echo "" >&2
|
||||
echo "Erst installieren (aus fertigem Build in /tmp/claude-target):" >&2
|
||||
echo " claude-desktop-install" >&2
|
||||
echo "" >&2
|
||||
echo "Oder neu bauen im Repo:" >&2
|
||||
echo " CARGO_TARGET_DIR=/tmp/claude-target \\" >&2
|
||||
echo " nix-shell shell.nix --run 'npm ci && npm run tauri build'" >&2
|
||||
echo " claude-desktop-install" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Marker fuer update.rs: wir laufen unter Nix-Wrapper
|
||||
export CLAUDE_DESKTOP_NIX_WRAPPER=1
|
||||
export CLAUDE_DESKTOP_BIN="$BIN"
|
||||
|
||||
exec "$BIN" "$@"
|
||||
LAUNCHER
|
||||
chmod +x $out/bin/claude-desktop
|
||||
|
||||
# LD_LIBRARY_PATH dauerhaft ans Launcher-Script binden
|
||||
wrapProgram $out/bin/claude-desktop \
|
||||
--prefix LD_LIBRARY_PATH : ${pkgs.lib.makeLibraryPath runtimeLibs}
|
||||
|
||||
# 2) Installer: kopiert ein frisch gebautes Binary an den Ziel-Ort
|
||||
cat > $out/bin/claude-desktop-install <<'INSTALLER'
|
||||
#!${pkgs.bash}/bin/bash
|
||||
set -e
|
||||
SRC="''${1:-/tmp/claude-target/release/claude-desktop}"
|
||||
DEST_DIR="$HOME/.local/share/claude-desktop/bin"
|
||||
DEST="$DEST_DIR/claude-desktop"
|
||||
|
||||
if [ ! -x "$SRC" ]; then
|
||||
echo "❌ Quelle nicht gefunden oder nicht ausfuehrbar: $SRC" >&2
|
||||
echo "" >&2
|
||||
echo "Erst bauen:" >&2
|
||||
echo " cd <repo>; CARGO_TARGET_DIR=/tmp/claude-target \\" >&2
|
||||
echo " nix-shell shell.nix --run 'npm ci && npm run tauri build'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$DEST_DIR"
|
||||
cp "$SRC" "$DEST"
|
||||
chmod +x "$DEST"
|
||||
|
||||
echo "✅ Claude Desktop installiert nach $DEST"
|
||||
echo " Starten mit: claude-desktop (oder aus KDE-Menue)"
|
||||
INSTALLER
|
||||
chmod +x $out/bin/claude-desktop-install
|
||||
|
||||
# 3) Desktop-Entry
|
||||
cp ${./claude-desktop.desktop} $out/share/applications/claude-desktop.desktop
|
||||
|
||||
# 4) Icon
|
||||
cp ${../src-tauri/icons/icon.png} $out/share/icons/hicolor/256x256/apps/claude-desktop.png
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description = "Native Desktop-App fuer Claude Code (Wrapper-Paket, Binary in ~/.local/share)";
|
||||
homepage = "https://git.data-it-solution.de/data/claude-desktop";
|
||||
license = licenses.mit;
|
||||
platforms = platforms.linux;
|
||||
mainProgram = "claude-desktop";
|
||||
};
|
||||
}
|
||||
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
|
|
@ -487,6 +487,7 @@ dependencies = [
|
|||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-shell",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ reqwest = { version = "0.12", features = ["json", "multipart", "stream"] }
|
|||
base64 = "0.22"
|
||||
tokio-tungstenite = "0.23"
|
||||
futures-util = "0.3"
|
||||
sha2 = "0.10"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
|
|
|
|||
|
|
@ -1,43 +1,58 @@
|
|||
// Claude Desktop — Auto-Update System
|
||||
// Prüft Forgejo-Releases und aktualisiert das AppImage
|
||||
// Liest update.json aus der Forgejo Package Registry, lädt AppImage, prüft SHA256, startet neu.
|
||||
// Version-Schema YYYYMMDD-HHMM wird von der CI als APP_VERSION zur Build-Zeit injiziert.
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::path::PathBuf;
|
||||
use tauri::{AppHandle, Manager, Emitter};
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
|
||||
/// Forgejo API Konfiguration
|
||||
const FORGEJO_URL: &str = "https://git.data-it-solution.de";
|
||||
const REPO_OWNER: &str = "data";
|
||||
const REPO_NAME: &str = "claude-desktop";
|
||||
/// Endpoint der Manifest-Datei
|
||||
const UPDATE_JSON_URL: &str =
|
||||
"https://git.data-it-solution.de/api/packages/data/generic/claude-desktop/latest/update.json";
|
||||
|
||||
/// Aktuelle App-Version (aus Cargo.toml)
|
||||
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
/// Basis-URL für versionierte Asset-Downloads (Datei wird angehängt)
|
||||
const PACKAGE_BASE_URL: &str =
|
||||
"https://git.data-it-solution.de/api/packages/data/generic/claude-desktop/latest/";
|
||||
|
||||
/// Release-Info von Forgejo
|
||||
/// Version der laufenden App. Wird von der CI als APP_VERSION zur Build-Zeit gesetzt.
|
||||
/// Lokaler Build ohne Env-Var → "dev" → Update-Check wird übersprungen.
|
||||
const CURRENT_VERSION: &str = match option_env!("APP_VERSION") {
|
||||
Some(v) => v,
|
||||
None => "dev",
|
||||
};
|
||||
|
||||
/// Read-Only-Token für Forgejo-Package-Registry (Basic-Auth user: "data")
|
||||
/// Wird ebenfalls zur Build-Zeit gesetzt. Ohne Token → Updates nicht abrufbar.
|
||||
const UPDATE_TOKEN: Option<&str> = option_env!("UPDATE_TOKEN");
|
||||
|
||||
/// Basic-Auth-User für Forgejo
|
||||
const UPDATE_USER: &str = "data";
|
||||
|
||||
/// Manifest aus latest/update.json.
|
||||
/// `filename`/`sha256` → AppImage (Standard-Installation).
|
||||
/// `binary_filename`/`binary_sha256` → pures Binary (Nix-Wrapper-Installation).
|
||||
/// Ältere Manifest-Versionen ohne Binary-Felder werden toleriert (Option).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReleaseInfo {
|
||||
pub id: i64,
|
||||
pub tag_name: String,
|
||||
pub name: String,
|
||||
pub body: Option<String>,
|
||||
pub draft: bool,
|
||||
pub prerelease: bool,
|
||||
pub created_at: String,
|
||||
pub published_at: Option<String>,
|
||||
pub assets: Vec<ReleaseAsset>,
|
||||
pub struct UpdateManifest {
|
||||
pub version: String,
|
||||
pub filename: String,
|
||||
pub sha256: String,
|
||||
#[serde(default)]
|
||||
pub binary_filename: Option<String>,
|
||||
#[serde(default)]
|
||||
pub binary_sha256: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub released_at: Option<String>,
|
||||
}
|
||||
|
||||
/// Release-Asset (z.B. AppImage)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReleaseAsset {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub size: i64,
|
||||
pub download_count: i64,
|
||||
pub browser_download_url: String,
|
||||
/// Prüft ob die App via Nix-Wrapper läuft (Env-Var vom Launcher-Script in nix/default.nix).
|
||||
fn is_nix_wrapper() -> bool {
|
||||
std::env::var("CLAUDE_DESKTOP_NIX_WRAPPER").ok().as_deref() == Some("1")
|
||||
}
|
||||
|
||||
/// Update-Status für Frontend
|
||||
/// Update-Status für das Frontend
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct UpdateStatus {
|
||||
pub available: bool,
|
||||
|
|
@ -46,69 +61,7 @@ pub struct UpdateStatus {
|
|||
pub release_notes: Option<String>,
|
||||
pub download_url: Option<String>,
|
||||
pub download_size: Option<i64>,
|
||||
}
|
||||
|
||||
/// Prüft ob ein Update verfügbar ist
|
||||
#[tauri::command]
|
||||
pub async fn check_for_update() -> Result<UpdateStatus, String> {
|
||||
let url = format!(
|
||||
"{}/api/v1/repos/{}/{}/releases?limit=1",
|
||||
FORGEJO_URL, REPO_OWNER, REPO_NAME
|
||||
);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(&url)
|
||||
.header("Accept", "application/json")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Netzwerkfehler: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Ok(UpdateStatus {
|
||||
available: false,
|
||||
current_version: CURRENT_VERSION.to_string(),
|
||||
latest_version: None,
|
||||
release_notes: None,
|
||||
download_url: None,
|
||||
download_size: None,
|
||||
});
|
||||
}
|
||||
|
||||
let releases: Vec<ReleaseInfo> = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("JSON-Fehler: {}", e))?;
|
||||
|
||||
let latest = match releases.into_iter().find(|r| !r.draft && !r.prerelease) {
|
||||
Some(r) => r,
|
||||
None => {
|
||||
return Ok(UpdateStatus {
|
||||
available: false,
|
||||
current_version: CURRENT_VERSION.to_string(),
|
||||
latest_version: None,
|
||||
release_notes: None,
|
||||
download_url: None,
|
||||
download_size: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Version vergleichen (tag_name ohne 'v' Prefix)
|
||||
let latest_version = latest.tag_name.trim_start_matches('v').to_string();
|
||||
let is_newer = version_compare(&latest_version, CURRENT_VERSION);
|
||||
|
||||
// AppImage-Asset finden
|
||||
let appimage = latest.assets.iter().find(|a| a.name.ends_with(".AppImage"));
|
||||
|
||||
Ok(UpdateStatus {
|
||||
available: is_newer,
|
||||
current_version: CURRENT_VERSION.to_string(),
|
||||
latest_version: Some(latest_version),
|
||||
release_notes: latest.body,
|
||||
download_url: appimage.map(|a| a.browser_download_url.clone()),
|
||||
download_size: appimage.map(|a| a.size),
|
||||
})
|
||||
pub sha256: Option<String>,
|
||||
}
|
||||
|
||||
/// Download-Fortschritt
|
||||
|
|
@ -119,31 +72,136 @@ pub struct DownloadProgress {
|
|||
pub percent: f32,
|
||||
}
|
||||
|
||||
/// Lädt das Update herunter
|
||||
/// Baut einen reqwest::RequestBuilder mit Basic-Auth (wenn Token vorhanden)
|
||||
fn authed_get(client: &reqwest::Client, url: &str) -> reqwest::RequestBuilder {
|
||||
let req = client.get(url).header("Accept", "application/json");
|
||||
match UPDATE_TOKEN {
|
||||
Some(token) => {
|
||||
let creds = B64.encode(format!("{}:{}", UPDATE_USER, token));
|
||||
req.header("Authorization", format!("Basic {}", creds))
|
||||
}
|
||||
None => req,
|
||||
}
|
||||
}
|
||||
|
||||
/// Prüft ob ein Update verfügbar ist.
|
||||
/// Lokale Dev-Builds ohne APP_VERSION-Env geben sofort available=false zurück.
|
||||
#[tauri::command]
|
||||
pub async fn check_for_update() -> Result<UpdateStatus, String> {
|
||||
// Dev-Build → kein Update-Check
|
||||
if CURRENT_VERSION == "dev" {
|
||||
return Ok(UpdateStatus {
|
||||
available: false,
|
||||
current_version: CURRENT_VERSION.to_string(),
|
||||
latest_version: None,
|
||||
release_notes: Some("Entwicklungs-Build — Updates deaktiviert.".to_string()),
|
||||
download_url: None,
|
||||
download_size: None,
|
||||
sha256: None,
|
||||
});
|
||||
}
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = authed_get(&client, UPDATE_JSON_URL)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Netzwerkfehler beim Update-Check: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!(
|
||||
"Manifest-Abruf fehlgeschlagen ({}). Token konfiguriert: {}",
|
||||
response.status(),
|
||||
UPDATE_TOKEN.is_some()
|
||||
));
|
||||
}
|
||||
|
||||
let manifest: UpdateManifest = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Manifest-JSON-Fehler: {}", e))?;
|
||||
|
||||
let available = is_newer(&manifest.version, CURRENT_VERSION);
|
||||
|
||||
// Im Nix-Wrapper-Modus das pure Binary ziehen (AppImage laeuft auf NixOS nicht).
|
||||
// Fehlt das Binary-Feld im Manifest (alte CI-Version) → Update als nicht-anwendbar markieren.
|
||||
let (chosen_filename, chosen_sha256) = if is_nix_wrapper() {
|
||||
match (&manifest.binary_filename, &manifest.binary_sha256) {
|
||||
(Some(f), Some(h)) => (f.clone(), h.clone()),
|
||||
_ => {
|
||||
return Ok(UpdateStatus {
|
||||
available: false,
|
||||
current_version: CURRENT_VERSION.to_string(),
|
||||
latest_version: Some(manifest.version),
|
||||
release_notes: Some(
|
||||
"Update vorhanden, aber das Manifest enthält kein Binary für den Nix-Wrapper-Modus. Bitte manuell neu bauen."
|
||||
.to_string(),
|
||||
),
|
||||
download_url: None,
|
||||
download_size: None,
|
||||
sha256: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(manifest.filename, manifest.sha256)
|
||||
};
|
||||
|
||||
let download_url = format!("{}{}", PACKAGE_BASE_URL, chosen_filename);
|
||||
|
||||
Ok(UpdateStatus {
|
||||
available,
|
||||
current_version: CURRENT_VERSION.to_string(),
|
||||
latest_version: Some(manifest.version),
|
||||
release_notes: manifest.notes,
|
||||
download_url: Some(download_url),
|
||||
// Größe kennen wir erst beim Download (Content-Length) — hier None
|
||||
download_size: None,
|
||||
sha256: Some(chosen_sha256),
|
||||
})
|
||||
}
|
||||
|
||||
/// Lädt das Update herunter, verifiziert SHA256, gibt den Pfad zurück.
|
||||
#[tauri::command]
|
||||
pub async fn download_update(
|
||||
app: AppHandle,
|
||||
download_url: String,
|
||||
expected_sha256: Option<String>,
|
||||
) -> Result<PathBuf, String> {
|
||||
use std::io::Write;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
let response = match UPDATE_TOKEN {
|
||||
Some(token) => {
|
||||
let creds = B64.encode(format!("{}:{}", UPDATE_USER, token));
|
||||
client
|
||||
.get(&download_url)
|
||||
.header("Authorization", format!("Basic {}", creds))
|
||||
}
|
||||
None => client.get(&download_url),
|
||||
}
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Download-Fehler: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!(
|
||||
"Download-HTTP-Fehler: {}",
|
||||
response.status()
|
||||
));
|
||||
}
|
||||
|
||||
let total_size = response.content_length().unwrap_or(0);
|
||||
|
||||
// Temp-Verzeichnis für Download
|
||||
let cache_dir = app.path().app_cache_dir()
|
||||
// Ziel-Verzeichnis im App-Cache
|
||||
let cache_dir = app
|
||||
.path()
|
||||
.app_cache_dir()
|
||||
.map_err(|e| format!("Cache-Verzeichnis nicht gefunden: {}", e))?;
|
||||
std::fs::create_dir_all(&cache_dir).ok();
|
||||
|
||||
let file_name = download_url.split('/').last().unwrap_or("update.AppImage");
|
||||
// Sanitize: Nur Basename, keine Pfad-Traversal (z.B. "../böse.sh")
|
||||
let file_name = std::path::Path::new(file_name)
|
||||
// Nur Basename aus URL extrahieren — keine Pfad-Traversal
|
||||
let file_name_raw = download_url.split('/').last().unwrap_or("update.AppImage");
|
||||
let file_name = std::path::Path::new(file_name_raw)
|
||||
.file_name()
|
||||
.and_then(|f| f.to_str())
|
||||
.unwrap_or("update.AppImage");
|
||||
|
|
@ -152,6 +210,7 @@ pub async fn download_update(
|
|||
let mut file = std::fs::File::create(&download_path)
|
||||
.map_err(|e| format!("Datei erstellen fehlgeschlagen: {}", e))?;
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
let mut downloaded: u64 = 0;
|
||||
let mut stream = response.bytes_stream();
|
||||
|
||||
|
|
@ -160,10 +219,9 @@ pub async fn download_update(
|
|||
let chunk = chunk.map_err(|e| format!("Download-Chunk-Fehler: {}", e))?;
|
||||
file.write_all(&chunk)
|
||||
.map_err(|e| format!("Schreibfehler: {}", e))?;
|
||||
|
||||
hasher.update(&chunk);
|
||||
downloaded += chunk.len() as u64;
|
||||
|
||||
// Fortschritt an Frontend senden
|
||||
let progress = DownloadProgress {
|
||||
downloaded,
|
||||
total: total_size,
|
||||
|
|
@ -176,6 +234,19 @@ pub async fn download_update(
|
|||
app.emit("update-progress", &progress).ok();
|
||||
}
|
||||
|
||||
// SHA256 verifizieren (wenn erwartet)
|
||||
if let Some(expected) = expected_sha256 {
|
||||
let actual = format!("{:x}", hasher.finalize());
|
||||
if !actual.eq_ignore_ascii_case(&expected) {
|
||||
// kaputte Datei weg
|
||||
std::fs::remove_file(&download_path).ok();
|
||||
return Err(format!(
|
||||
"SHA256-Mismatch: erwartet {}, berechnet {}",
|
||||
expected, actual
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Ausführbar machen
|
||||
#[cfg(unix)]
|
||||
{
|
||||
|
|
@ -188,73 +259,76 @@ pub async fn download_update(
|
|||
.map_err(|e| format!("Permissions-Fehler: {}", e))?;
|
||||
}
|
||||
|
||||
println!("✅ Update heruntergeladen: {:?}", download_path);
|
||||
println!("✅ Update heruntergeladen + verifiziert: {:?}", download_path);
|
||||
Ok(download_path)
|
||||
}
|
||||
|
||||
/// Wendet das Update an (ersetzt AppImage und startet neu)
|
||||
/// Wendet das Update an (ersetzt Ziel-Datei und startet neu).
|
||||
/// Unterstützte Modi:
|
||||
/// 1. AppImage-Runtime → `$APPIMAGE` zeigt auf die laufende AppImage-Datei
|
||||
/// 2. Nix-Wrapper-Modus → `$CLAUDE_DESKTOP_NIX_WRAPPER=1` + `$CLAUDE_DESKTOP_BIN` zeigt
|
||||
/// auf ~/.local/share/claude-desktop/bin/claude-desktop (writable, siehe nix/default.nix)
|
||||
/// 3. Entwicklungs-Build → Fehlerhinweis mit Build-Anleitung
|
||||
#[tauri::command]
|
||||
pub async fn apply_update(app: AppHandle, update_path: PathBuf) -> Result<(), String> {
|
||||
// Prüfen ob wir in einem AppImage laufen
|
||||
let appimage_path = std::env::var("APPIMAGE").ok();
|
||||
// Modus bestimmen: AppImage > Nix-Wrapper > Dev
|
||||
let target_path = if let Ok(appimage) = std::env::var("APPIMAGE") {
|
||||
Some((PathBuf::from(appimage), "AppImage"))
|
||||
} else if std::env::var("CLAUDE_DESKTOP_NIX_WRAPPER").ok().as_deref() == Some("1") {
|
||||
std::env::var("CLAUDE_DESKTOP_BIN")
|
||||
.ok()
|
||||
.map(|p| (PathBuf::from(p), "Nix-Wrapper-Binary"))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(appimage) = appimage_path {
|
||||
let appimage_path = PathBuf::from(&appimage);
|
||||
|
||||
// Backup erstellen
|
||||
let backup_path = appimage_path.with_extension("AppImage.backup");
|
||||
std::fs::rename(&appimage_path, &backup_path)
|
||||
.map_err(|e| format!("Backup fehlgeschlagen: {}", e))?;
|
||||
|
||||
// Neues AppImage verschieben
|
||||
std::fs::rename(&update_path, &appimage_path)
|
||||
.map_err(|e| {
|
||||
// Backup wiederherstellen bei Fehler
|
||||
std::fs::rename(&backup_path, &appimage_path).ok();
|
||||
format!("Update-Installation fehlgeschlagen: {}", e)
|
||||
let (target, mode_label) = target_path.ok_or_else(|| {
|
||||
"Update nur für AppImage- oder Nix-Wrapper-Builds verfügbar.\n\
|
||||
Für Dev-Builds: `git pull` und manuell neu bauen.\n\
|
||||
Für Nix-System: `claude-desktop-install` nach dem Build ausführen."
|
||||
.to_string()
|
||||
})?;
|
||||
|
||||
// Backup-Pfad: gleiches Verzeichnis, Suffix .backup
|
||||
let backup_path = {
|
||||
let mut p = target.clone();
|
||||
let file_name = target
|
||||
.file_name()
|
||||
.and_then(|f| f.to_str())
|
||||
.unwrap_or("claude-desktop");
|
||||
p.set_file_name(format!("{}.backup", file_name));
|
||||
p
|
||||
};
|
||||
|
||||
std::fs::rename(&target, &backup_path)
|
||||
.map_err(|e| format!("Backup fehlgeschlagen ({}): {}", mode_label, e))?;
|
||||
|
||||
std::fs::rename(&update_path, &target).map_err(|e| {
|
||||
// Rollback
|
||||
std::fs::rename(&backup_path, &target).ok();
|
||||
format!("Update-Installation fehlgeschlagen ({}): {}", mode_label, e)
|
||||
})?;
|
||||
|
||||
// Backup löschen
|
||||
std::fs::remove_file(&backup_path).ok();
|
||||
|
||||
println!("✅ Update installiert, starte neu...");
|
||||
|
||||
// App neustarten (kehrt nicht zurück)
|
||||
println!("✅ Update installiert ({}), starte neu...", mode_label);
|
||||
app.restart();
|
||||
}
|
||||
|
||||
// Nicht in AppImage — Entwicklungsmodus
|
||||
Err("Update nur für AppImage-Builds verfügbar. In Entwicklung: manuell `git pull` und neu bauen.".to_string())
|
||||
}
|
||||
|
||||
/// Gibt aktuelle Version zurück
|
||||
/// Liefert die aktuelle Version (für UI-Anzeige)
|
||||
#[tauri::command]
|
||||
pub fn get_current_version() -> String {
|
||||
CURRENT_VERSION.to_string()
|
||||
}
|
||||
|
||||
/// Vergleicht zwei Versionen (semver-ähnlich)
|
||||
fn version_compare(new: &str, current: &str) -> bool {
|
||||
let parse = |v: &str| -> Vec<u32> {
|
||||
v.split('.')
|
||||
.filter_map(|s| s.parse().ok())
|
||||
.collect()
|
||||
};
|
||||
|
||||
let new_parts = parse(new);
|
||||
let current_parts = parse(current);
|
||||
|
||||
for i in 0..3 {
|
||||
let n = new_parts.get(i).copied().unwrap_or(0);
|
||||
let c = current_parts.get(i).copied().unwrap_or(0);
|
||||
if n > c {
|
||||
return true;
|
||||
}
|
||||
if n < c {
|
||||
/// String-Vergleich für `YYYYMMDD-HHMM`-Versionen.
|
||||
/// Lexikographisch > ist bei Zeitstempel-Format korrekt.
|
||||
/// "dev" ist immer "nicht neuer".
|
||||
fn is_newer(candidate: &str, current: &str) -> bool {
|
||||
if candidate == "dev" || current == "dev" {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
false
|
||||
candidate > current
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -262,12 +336,17 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_version_compare() {
|
||||
assert!(version_compare("1.0.1", "1.0.0"));
|
||||
assert!(version_compare("1.1.0", "1.0.9"));
|
||||
assert!(version_compare("2.0.0", "1.9.9"));
|
||||
assert!(!version_compare("1.0.0", "1.0.0"));
|
||||
assert!(!version_compare("1.0.0", "1.0.1"));
|
||||
assert!(!version_compare("0.9.0", "1.0.0"));
|
||||
fn test_is_newer_timestamp() {
|
||||
assert!(is_newer("20260420-1201", "20260420-1200"));
|
||||
assert!(is_newer("20260421-0001", "20260420-2359"));
|
||||
assert!(!is_newer("20260420-1200", "20260420-1200"));
|
||||
assert!(!is_newer("20260420-1159", "20260420-1200"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_newer_dev_always_false() {
|
||||
assert!(!is_newer("dev", "20260420-1200"));
|
||||
assert!(!is_newer("20260420-1200", "dev"));
|
||||
assert!(!is_newer("dev", "dev"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { onMount } from 'svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { currentModel, agentMode, type AgentMode } from '$lib/stores/app';
|
||||
import { updateCheckManual } from '$lib/stores/updateTrigger';
|
||||
|
||||
interface ModelInfo {
|
||||
id: string;
|
||||
|
|
@ -13,6 +14,7 @@
|
|||
let selectedModel = '';
|
||||
let loading = true;
|
||||
let saving = false;
|
||||
let appVersion = '';
|
||||
|
||||
// Modell-Icons
|
||||
const modelIcons: Record<string, string> = {
|
||||
|
|
@ -73,12 +75,19 @@
|
|||
// Agent-Modus laden
|
||||
const currentMode: string = await invoke('get_agent_mode');
|
||||
$agentMode = currentMode as AgentMode;
|
||||
|
||||
// Version laden
|
||||
appVersion = await invoke('get_current_version');
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden:', err);
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
|
||||
function triggerUpdateCheck() {
|
||||
updateCheckManual.set(true);
|
||||
}
|
||||
|
||||
async function changeMode(modeId: AgentMode) {
|
||||
if (modeId === $agentMode) return;
|
||||
|
||||
|
|
@ -209,6 +218,25 @@
|
|||
<span class="setting-value placeholder">Wird aus Session geladen</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<h3>🔄 Version & Updates</h3>
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">Aktuelle Version</span>
|
||||
<span class="setting-value">{appVersion || '—'}</span>
|
||||
</div>
|
||||
<div class="update-actions">
|
||||
<button class="update-btn" on:click={triggerUpdateCheck}>
|
||||
Nach Updates suchen
|
||||
</button>
|
||||
{#if appVersion === 'dev'}
|
||||
<p class="dev-hint">
|
||||
Entwicklungs-Build — Auto-Update ist deaktiviert.
|
||||
Nur Pipeline-Builds können aktualisiert werden.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -437,4 +465,40 @@
|
|||
.mode-info strong {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Update-Sektion */
|
||||
.update-actions {
|
||||
margin-top: var(--spacing-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.update-btn {
|
||||
align-self: flex-start;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.update-btn:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.dev-hint {
|
||||
margin: 0;
|
||||
padding: var(--spacing-sm);
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
background: rgba(234, 179, 8, 0.08);
|
||||
border: 1px solid rgba(234, 179, 8, 0.2);
|
||||
border-radius: var(--radius-sm);
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { onMount, onDestroy } from 'svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { updateDialogOpen, updateCheckManual } from '$lib/stores/updateTrigger';
|
||||
|
||||
interface UpdateStatus {
|
||||
available: boolean;
|
||||
|
|
@ -10,6 +11,7 @@
|
|||
release_notes: string | null;
|
||||
download_url: string | null;
|
||||
download_size: number | null;
|
||||
sha256: string | null;
|
||||
}
|
||||
|
||||
interface DownloadProgress {
|
||||
|
|
@ -18,39 +20,72 @@
|
|||
percent: number;
|
||||
}
|
||||
|
||||
let showDialog = false;
|
||||
let updateInfo: UpdateStatus | null = null;
|
||||
let downloading = false;
|
||||
let progress: DownloadProgress | null = null;
|
||||
let error: string | null = null;
|
||||
let downloadedPath: string | null = null;
|
||||
let checking = false;
|
||||
let manualMode = false;
|
||||
|
||||
let progressListener: UnlistenFn | null = null;
|
||||
let manualUnsub: (() => void) | null = null;
|
||||
|
||||
// Reaktiv: wenn Store schließt → State zurücksetzen
|
||||
$: if (!$updateDialogOpen) {
|
||||
resetState();
|
||||
}
|
||||
|
||||
// Manueller Check-Trigger aus dem Settings-Panel
|
||||
$: manualMode = $updateCheckManual;
|
||||
|
||||
onMount(async () => {
|
||||
// Update-Check beim Start (mit kurzer Verzögerung)
|
||||
setTimeout(checkForUpdate, 3000);
|
||||
|
||||
// Progress-Events vom Backend
|
||||
progressListener = await listen<DownloadProgress>('update-progress', (event) => {
|
||||
progress = event.payload;
|
||||
});
|
||||
|
||||
// Manueller Check wird via Store gestartet
|
||||
manualUnsub = updateCheckManual.subscribe((active) => {
|
||||
if (active) {
|
||||
// Store-Reset, bevor wir starten — damit erneutes Klicken wieder triggert
|
||||
updateCheckManual.set(false);
|
||||
runCheck(true);
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-Check 3s nach Start (nur wenn Dialog nicht bereits offen)
|
||||
setTimeout(() => runCheck(false), 3000);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
progressListener?.();
|
||||
manualUnsub?.();
|
||||
});
|
||||
|
||||
async function checkForUpdate() {
|
||||
async function runCheck(manual: boolean) {
|
||||
checking = true;
|
||||
error = null;
|
||||
try {
|
||||
const status: UpdateStatus = await invoke('check_for_update');
|
||||
if (status.available) {
|
||||
updateInfo = status;
|
||||
showDialog = true;
|
||||
console.log('🔄 Update verfügbar:', status.latest_version);
|
||||
if (status.available) {
|
||||
manualMode = manual;
|
||||
updateDialogOpen.set(true);
|
||||
} else if (manual) {
|
||||
// Bei manuellem Check auch "kein Update"-Dialog zeigen
|
||||
manualMode = true;
|
||||
updateDialogOpen.set(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.debug('Update-Check fehlgeschlagen:', err);
|
||||
if (manual) {
|
||||
error = String(err);
|
||||
updateDialogOpen.set(true);
|
||||
} else {
|
||||
console.debug('Auto-Update-Check fehlgeschlagen:', err);
|
||||
}
|
||||
} finally {
|
||||
checking = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -59,11 +94,12 @@
|
|||
|
||||
downloading = true;
|
||||
error = null;
|
||||
progress = { downloaded: 0, total: updateInfo.download_size || 0, percent: 0 };
|
||||
progress = { downloaded: 0, total: 0, percent: 0 };
|
||||
|
||||
try {
|
||||
downloadedPath = await invoke('download_update', {
|
||||
downloadUrl: updateInfo.download_url,
|
||||
expectedSha256: updateInfo.sha256,
|
||||
});
|
||||
console.log('✅ Download abgeschlossen:', downloadedPath);
|
||||
} catch (err) {
|
||||
|
|
@ -74,21 +110,24 @@
|
|||
|
||||
async function applyUpdate() {
|
||||
if (!downloadedPath) return;
|
||||
|
||||
try {
|
||||
await invoke('apply_update', { updatePath: downloadedPath });
|
||||
// App wird neugestartet, kein weiterer Code erreicht
|
||||
// App startet neu, kein weiterer Code erreicht
|
||||
} catch (err) {
|
||||
error = String(err);
|
||||
}
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
showDialog = false;
|
||||
updateDialogOpen.set(false);
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
downloading = false;
|
||||
progress = null;
|
||||
error = null;
|
||||
downloadedPath = null;
|
||||
manualMode = false;
|
||||
}
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
|
|
@ -96,19 +135,33 @@
|
|||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
$: isNoUpdateDialog = $updateDialogOpen && updateInfo && !updateInfo.available && !error;
|
||||
</script>
|
||||
|
||||
{#if showDialog && updateInfo}
|
||||
{#if $updateDialogOpen && updateInfo}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="modal-overlay" on:click={closeDialog}>
|
||||
<div class="modal" on:click|stopPropagation>
|
||||
<div class="modal-header">
|
||||
{#if isNoUpdateDialog}
|
||||
<h2>✅ Aktuell</h2>
|
||||
{:else if error && !updateInfo.available}
|
||||
<h2>⚠️ Update-Check fehlgeschlagen</h2>
|
||||
{:else}
|
||||
<h2>🔄 Update verfügbar</h2>
|
||||
{/if}
|
||||
<button class="close-btn" on:click={closeDialog}>✕</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
{#if isNoUpdateDialog}
|
||||
<p class="no-update-text">
|
||||
Du verwendest bereits die neueste Version:
|
||||
<strong>v{updateInfo.current_version}</strong>
|
||||
</p>
|
||||
{:else if updateInfo.available}
|
||||
<div class="version-info">
|
||||
<span class="current">v{updateInfo.current_version}</span>
|
||||
<span class="arrow">→</span>
|
||||
|
|
@ -124,23 +177,22 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if updateInfo.download_size}
|
||||
<p class="download-size">
|
||||
Download: {formatSize(updateInfo.download_size)}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if downloading && progress}
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: {progress.percent}%"></div>
|
||||
</div>
|
||||
<span class="progress-text">
|
||||
{#if progress.total > 0}
|
||||
{formatSize(progress.downloaded)} / {formatSize(progress.total)}
|
||||
({progress.percent.toFixed(0)}%)
|
||||
{:else}
|
||||
{formatSize(progress.downloaded)} geladen...
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<div class="error">
|
||||
|
|
@ -150,7 +202,9 @@
|
|||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
{#if downloadedPath}
|
||||
{#if isNoUpdateDialog}
|
||||
<button class="btn btn-primary" on:click={closeDialog}>OK</button>
|
||||
{:else if downloadedPath}
|
||||
<button class="btn btn-primary" on:click={applyUpdate}>
|
||||
Jetzt installieren & neustarten
|
||||
</button>
|
||||
|
|
@ -158,13 +212,13 @@
|
|||
<button class="btn btn-disabled" disabled>
|
||||
Wird heruntergeladen...
|
||||
</button>
|
||||
{:else}
|
||||
<button class="btn btn-secondary" on:click={closeDialog}>
|
||||
Später
|
||||
</button>
|
||||
{:else if updateInfo.available}
|
||||
<button class="btn btn-secondary" on:click={closeDialog}>Später</button>
|
||||
<button class="btn btn-primary" on:click={startDownload}>
|
||||
Jetzt aktualisieren
|
||||
</button>
|
||||
{:else}
|
||||
<button class="btn btn-primary" on:click={closeDialog}>Schließen</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -243,6 +297,12 @@
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
.no-update-text {
|
||||
text-align: center;
|
||||
font-size: 0.95rem;
|
||||
margin: var(--spacing-md) 0;
|
||||
}
|
||||
|
||||
.release-notes {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
|
|
@ -263,12 +323,6 @@
|
|||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.download-size {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin: var(--spacing-md) 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// Stores re-export
|
||||
export * from './app';
|
||||
export * from './events';
|
||||
export * from './updateTrigger';
|
||||
|
|
|
|||
8
src/lib/stores/updateTrigger.ts
Normal file
8
src/lib/stores/updateTrigger.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { writable } from 'svelte/store';
|
||||
|
||||
// Steuert Sichtbarkeit des UpdateDialog
|
||||
export const updateDialogOpen = writable(false);
|
||||
|
||||
// Setzt das Dialog in den "manueller Check"-Modus:
|
||||
// Ergebnis wird immer angezeigt — auch wenn kein Update vorliegt.
|
||||
export const updateCheckManual = writable(false);
|
||||
Loading…
Reference in a new issue