fix(desktop updates): Linux package installs update via their package manager
CI / Build & Quality Checks (pull_request) Failing after 1m40s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Playwright smoke (e2e) (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 6s

Reported on CachyOS: "Check for Updates → The update downloaded but
couldn't be installed … Permission denied (os error 13) at path
/usr/bin/tauri_current_app…". The app was installed from the Arch package;
Tauri's Linux updater can only replace an AppImage.

With cinny-desktop's new update_install_kind command:
- pacman / deb installs: the toast says the update is available and opens
  Settings → General → App Updates, which shows the package-manager command
  (`sudo pacman -U …pkg.tar.zst`, or the .deb + `sudo apt install`) with
  Copy command and Download package — no Install & Restart that can't work.
  "Copied" only when the clipboard write actually succeeded.
- other distros: a link to the downloads page.
- Windows, AppImage, and desktop builds without the command: unchanged
  in-app update.
- a native "package-managed" refusal shows the same help.

Tests: unit (kinds, commands, refusal detection); in the real client with a
simulated desktop bridge: pacman → toast + Settings command/buttons,
install never attempted; older desktop → in-app flow as before. Unit 1321,
Playwright 26 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
Lotus CI
2026-09-30 19:00:10 -04:00
co-authored by Claude Opus 5.5
parent 747400ea25
commit 27d659118f
5 changed files with 203 additions and 42 deletions
+18 -3
View File
@@ -1,7 +1,7 @@
import { useCallback } from 'react';
import { useCallback, useEffect } from 'react';
import { atom, useAtom, useSetAtom } from 'jotai';
import { useTauriEvent } from './useTauri';
import { UpdatePhase, parseUpdateError } from '../utils/updateErrors';
import { InstallKind, UpdatePhase, parseUpdateError, toInstallKind } from '../utils/updateErrors';
type TauriInternals = { invoke: (cmd: string, args?: Record<string, unknown>) => Promise<unknown> };
const tauriInvoke = (): TauriInternals['invoke'] | undefined =>
@@ -36,6 +36,11 @@ export type UpdateFailure = { phase: UpdatePhase; message: string };
// mid-request by the resolve/reject below, so nothing gets stuck.
const updateStatusAtom = atom<UpdateStatus>({ state: 'idle' });
// How this install gets updated; asked once. A desktop build without the
// command (older than it) rejects the call: keep the old in-app behaviour.
const installKindAtom = atom<InstallKind | undefined>(undefined);
let installKindRequested = false;
/**
* Mirror native download progress into the status. Mounted once (in
* TauriUpdateFeature) so the listener isn't duplicated per consumer.
@@ -50,6 +55,16 @@ export function useTauriUpdateProgress(): void {
export function useTauriUpdater() {
const isTauri = !!tauriInvoke();
const [status, setStatus] = useAtom(updateStatusAtom);
const [installKind, setInstallKind] = useAtom(installKindAtom);
useEffect(() => {
const invoke = tauriInvoke();
if (!invoke || installKindRequested) return;
installKindRequested = true;
invoke('update_install_kind')
.then((kind) => setInstallKind(toInstallKind(kind)))
.catch(() => setInstallKind('in-app'));
}, [setInstallKind]);
const check = useCallback(async () => {
const invoke = tauriInvoke();
@@ -89,5 +104,5 @@ export function useTauriUpdater() {
}
}, [setStatus]);
return { isTauri, status, check, install };
return { isTauri, status, check, install, installKind: installKind ?? 'in-app' };
}