#!/bin/sh
# Osiris starten — per Klick, ohne Terminal.
#
# Derselbe Starter steckt in Osiris.app (macOS, als Contents/MacOS/Osiris) und
# im .deb (Linux, als /opt/osiris/osiris-starten). Er
#
#   1. erkennt System und Rechnerart (unter Rosetta zaehlt der echte Mac),
#   2. richtet beim ersten Start — und wenn der Installer eine NEUERE Fassung
#      mitbringt — die App und die passende Node-Laufzeit im Ordner des
#      Nutzers ein,
#   3. schreibt installation.json,
#   4. holt den PATH der Anmelde-Shell dazu (Claude Code, Homebrew — aus dem
#      Dock gestartet kennt Osiris sonst nur /usr/bin:/bin) und
#   5. startet: ZIEL/runtime/node ZIEL/app/bin/osiris.mjs start --yes,
#      im Hintergrund, Ausgabe nach ZIEL/logs/start.log.
#
# Mit --oeffnen [--] <datei…> (so ruft ihn /opt/osiris/osiris-oeffnen, also
# der Doppelklick auf eine Textdatei unter Linux) tut er 1. bis 4. genauso,
# startet dann aber `osiris.mjs open -- <datei…>`: ein Fenster mit dem Editor
# von Osiris, und Osiris selbst nur, wenn es noch nicht laeuft. Ein zweiter
# Doppelklick waehrend eines Starts geht dabei nicht verloren — er wartet,
# bis eingerichtet ist, und oeffnet dann seine Datei.
#
# Was der Installer mitbringt (QUELLE, nur gelesen — Osiris.app darf auf einem
# schreibgeschuetzten, zufaelligen Pfad liegen, siehe App Translocation):
#   macOS  Osiris.app/Contents/Resources/{installer.txt, app/,
#          runtime/darwin-arm64.tar.xz, runtime/darwin-x64.tar.xz}  (oder .tar.gz)
#   Linux  /opt/osiris/{installer.txt, app/, runtime/{node, LICENSE, STAND}}
#
# Wo Osiris beim Nutzer liegt (ZIEL — derselbe Ort wie frueher bei
# install.sh, so ziehen bestehende Installationen samt Daten nahtlos um):
#   macOS  ~/Library/Application Support/Osiris
#   Linux  ${XDG_DATA_HOME:-~/.local/share}/osiris — liegt dort keine
#          Installation mit Daten, wohl aber unter ~/.local/share/osiris,
#          dann diese
#   darin  app/  runtime/node  installation.json  logs/start.log
#
# Die App aktualisiert sich selbst in ZIEL/app. Ist sie dort neuer als die
# mitgebrachte, bleibt sie. Die Sachen des Nutzers (app/data samt
# Browserprofil, app/osiris.config.local.json) kopiert oder loescht der
# Starter nie, er benennt sie nur mit um — wie install.sh.
#
# Laeuft Osiris gerade, wird app/ nicht unter dem laufenden Server
# ausgetauscht: Die mitgebrachte Fassung wird dann als Update bereitgelegt
# (ZIEL/update/app + BEREIT, wie services/aktualisierung.mjs es tut), und
# `osiris start` spielt sie selbst ein — es kann pruefen, ob eine Sitzung
# laeuft, und beendet sie dafuer geordnet.
#
# Nur POSIX-sh und was jedes macOS und jedes Linux mitbringt (cp, mv, tar,
# sed, date, mkdir). Fehler sagt ein Dialog (osascript, zenity, kdialog …),
# alles steht zusaetzlich im Protokoll.

PATH="${PATH:-/usr/bin:/bin}:/usr/bin:/bin:/usr/sbin:/sbin"
export PATH
# Die umask des Nutzers gilt fuer alles, was der Starter startet (Node, also
# Server, Editor, Terminal und Claude Code — und die Anmelde-Shell): Wer in
# Osiris eine Datei speichert oder anlegt, soll die gewohnten Rechte
# bekommen (eine geerbte 077 machte aus 644 beim Speichern 600, aus 755 700).
# Nur die eigenen Kopien des Starters (app/, runtime/) entstehen eng.
# Chats, Lizenz und Anmeldetoken schuetzt stattdessen gezielt
# schuetze_daten: ZIEL, app/data, logs und browser auf 700.
NUTZER_UMASK="$(umask 2>/dev/null)"
umask 077

# Vor jedem Start von Node (und der Anmelde-Shell): zurueck zur umask des Nutzers.
nutzer_umask() {
  [ -n "$NUTZER_UMASK" ] && umask "$NUTZER_UMASK" 2>/dev/null
  return 0
}

# Starten, Dateien oeffnen — oder von der Mac-Huelle gerufen?
#
# Die Mac-Huelle (release/mac/huelle) ist eine Electron-App: sie bringt das
# Chromium-Fenster selbst mit und braucht den Starter nur noch fuer zwei
# Dinge, die zur gebuendelten Node und zum Ordner des Nutzers gehoeren:
#   --installiere  nur die App aus dem DMG/der Uebersetzungssperre nach
#                  „Programme" legen; das Oeffnen macht die Huelle selbst.
#                  Auf stdout steht danach genau EINE Zeile
#                  `OSIRIS_APP=<absoluter Pfad der Kopie>` — sie kann auch in
#                  ~/Applications liegen, und die Huelle oeffnet genau diese.
#                  Sonst nichts auf stdout (alles andere ins Protokoll).
#   --huelle       den Server headless einrichten und starten — OHNE eigenes
#                  Fenster (das bringt die Huelle) und OHNE Selbstinstallation
#                  (die hat die Huelle schon erledigt).
#   --im-browser   wie --huelle eingerichtet und ohne Selbstinstallation, aber
#                  Osiris oeffnet sich im Standardbrowser (`osiris.mjs start
#                  --yes --im-browser`) — der Menuepunkt „Im Browser oeffnen".
# Die Dateien von --oeffnen werden gleich absolut: Der Starter wechselt
# spaeter in den Heimordner.
MODUS=start
if [ "${1:-}" = --oeffnen ]; then
  MODUS=oeffnen
  shift
  [ "${1:-}" = -- ] && shift
  hier="$(pwd 2>/dev/null)"
  n=$#
  while [ "$n" -gt 0 ]; do
    datei="$1"
    shift
    n=$((n - 1))
    [ -n "$datei" ] || continue
    case "$datei" in /*) ;; *) datei="$hier/$datei" ;; esac
    set -- "$@" "$datei"
  done
elif [ "${1:-}" = --installiere ]; then
  MODUS=installiere
  shift
elif [ "${1:-}" = --huelle ]; then
  MODUS=huelle
  shift
  # Kein eigenes Fenster: Die Huelle bringt es mit. OSIRIS_NO_OPEN haelt den
  # Server headless, auch wenn OSIRIS_HUELLE aus der Umgebung einmal fehlt.
  OSIRIS_NO_OPEN=1
  export OSIRIS_NO_OPEN
elif [ "${1:-}" = --im-browser ]; then
  MODUS=im-browser
  shift
fi
# Unter der Huelle (--huelle, --im-browser) legt sich der Starter nie selbst
# nach „Programme" und zeigt keine eigene Mitteilung — das macht die Huelle.
UNTER_HUELLE=0
case "$MODUS" in huelle|im-browser) UNTER_HUELLE=1 ;; esac

# Was dem Nutzer gehoert — wie EIGENES in services/aktualisierung.mjs.
EIGENES="data osiris.config.local.json"
# Ab dieser Groesse wird start.log beiseitegelegt (start.log.1). Mehr als
# zwei Stueck davon gibt es nie.
LOG_GRENZE=1048576
# So lange (in Schritten zu 0,2 s) sieht der Starter dem Start zu, damit ein
# sofortiger Fehler als Dialog erscheint und nicht nur im Protokoll.
WACHE=15
# So lange gilt ein gerade erfolgter Start als „laeuft an" — ein zweiter
# Klick in dieser Zeit tut nichts.
ANLAUF_S=10

LOG=""
SPERRE=""
SPERRE_BEHALTEN=0

jetzt_iso() { date -u +%Y-%m-%dT%H:%M:%SZ; }
jetzt_s() { date +%s; }
kurz_warten() { sleep 0.2 2>/dev/null || sleep 1; }

protokoll() {
  [ -n "$LOG" ] && printf '[%s] %s\n' "$(jetzt_iso)" "$*" >>"$LOG" 2>/dev/null
  return 0
}

# ------------------------------------------------------------------ System

SYSTEM="$(uname -s 2>/dev/null)"
CPU_ROH="$(uname -m 2>/dev/null)"
OS=""
CPU=""
case "$SYSTEM" in
  Darwin)
    OS=darwin
    case "$CPU_ROH" in
      arm64) CPU=arm64 ;;
      x86_64)
        CPU=x64
        # Unter Rosetta meldet uname auch auf Apple Silicon x86_64. Dann
        # laeuft die arm64-Laufzeit trotzdem — und schneller.
        if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = 1 ]; then CPU=arm64; fi
        ;;
    esac
    ;;
  Linux)
    OS=linux
    case "$CPU_ROH" in
      x86_64|amd64) CPU=x64 ;;
      aarch64|arm64) CPU=arm64 ;;
    esac
    ;;
esac

SPRACHE=en
case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in de|de_*|de.*) SPRACHE=de ;; esac
if [ "$SPRACHE" = en ] && [ "$OS" = darwin ]; then
  case "$(defaults read -g AppleLocale 2>/dev/null)" in de|de_*|de-*) SPRACHE=de ;; esac
fi

# Die Texte der Dialoge. $2 wird eingesetzt.
text() {
  case "$SPRACHE:$1" in
    de:heim) printf '%s' 'Osiris findet deinen Benutzerordner nicht (HOME ist leer).' ;;
    en:heim) printf '%s' 'Osiris cannot find your home folder (HOME is empty).' ;;
    de:cpu) printf 'Osiris läuft auf dieser Rechnerart nicht (%s).' "$2" ;;
    en:cpu) printf 'Osiris does not run on this type of computer (%s).' "$2" ;;
    de:quelle) printf '%s' 'Die Installation ist unvollständig. Bitte Osiris neu herunterladen und installieren.' ;;
    en:quelle) printf '%s' 'The installation is incomplete. Please download and install Osiris again.' ;;
    de:ziel) printf 'Osiris kann seinen Ordner nicht anlegen:\n%s' "$2" ;;
    en:ziel) printf 'Osiris cannot create its folder:\n%s' "$2" ;;
    de:laufzeit_fehlt) printf 'Für diese Rechnerart (%s) liegt keine Node-Laufzeit bei. Bitte den passenden Installer herunterladen.' "$2" ;;
    en:laufzeit_fehlt) printf 'No Node runtime for this type of computer (%s) is included. Please download the matching installer.' "$2" ;;
    de:laufzeit_kaputt) printf '%s' 'Die mitgelieferte Node-Laufzeit ließ sich nicht einrichten. Ist genug Speicherplatz frei?' ;;
    en:laufzeit_kaputt) printf '%s' 'The bundled Node runtime could not be set up. Is there enough free disk space?' ;;
    de:laufzeit_laeuft_nicht) printf 'Die mitgelieferte Node-Laufzeit (%s) lässt sich auf diesem Rechner nicht ausführen.' "$2" ;;
    en:laufzeit_laeuft_nicht) printf 'The bundled Node runtime (%s) cannot run on this computer.' "$2" ;;
    de:rosetta) printf '%s' 'Auf Apple Silicon braucht sie Rosetta: im Terminal „softwareupdate --install-rosetta" ausführen.' ;;
    en:rosetta) printf '%s' 'On Apple Silicon it needs Rosetta: run "softwareupdate --install-rosetta" in Terminal.' ;;
    de:kopie) printf '%s' 'Osiris ließ sich nicht einrichten. Die bisherige Installation ist unverändert. Ist genug Speicherplatz frei?' ;;
    en:kopie) printf '%s' 'Osiris could not be set up. The previous installation is unchanged. Is there enough free disk space?' ;;
    de:zurueck) printf '%s' 'Osiris ließ sich nicht einrichten. Die bisherige Installation ist wiederhergestellt.' ;;
    en:zurueck) printf '%s' 'Osiris could not be set up. The previous installation has been restored.' ;;
    de:liegt_in) printf 'Osiris ließ sich nicht einrichten. Nichts wurde gelöscht — der bisherige Stand samt deinen Daten liegt in:\n%s' "$2" ;;
    en:liegt_in) printf 'Osiris could not be set up. Nothing was deleted — the previous state including your data is in:\n%s' "$2" ;;
    de:start) printf 'Osiris konnte nicht gestartet werden (Code %s).' "$2" ;;
    en:start) printf 'Osiris could not be started (code %s).' "$2" ;;
    de:oeffnen) printf 'Osiris konnte die Datei nicht öffnen (Code %s).' "$2" ;;
    en:oeffnen) printf 'Osiris could not open the file (code %s).' "$2" ;;
    de:beschaeftigt) printf '%s' 'Osiris wird gerade eingerichtet. Bitte gleich noch einmal versuchen.' ;;
    en:beschaeftigt) printf '%s' 'Osiris is being set up right now. Please try again in a moment.' ;;
    de:mac_installiert) printf '%s' 'Osiris liegt jetzt in „Programme" und startet von dort.' ;;
    en:mac_installiert) printf '%s' 'Osiris is now in Applications and starts from there.' ;;
    de:mac_einrichten) printf '%s' 'Osiris wird eingerichtet – einen Moment …' ;;
    en:mac_einrichten) printf '%s' 'Setting up Osiris – one moment …' ;;
    de:logort) printf 'Einzelheiten stehen im Protokoll:\n%s' "$2" ;;
    en:logort) printf 'Details are in the log:\n%s' "$2" ;;
  esac
}

# Einen Fehler zeigen — als Dialog, sonst wenigstens im Protokoll.
melde_fehler() {
  protokoll "FEHLER: $1"
  nachricht="$1"
  if [ -n "$LOG" ]; then
    nachricht="$1

$(text logort "$LOG")"
  fi
  printf 'Osiris: %s\n' "$1" >&2
  if [ "$OS" = darwin ]; then
    # Der Text geht als Argument hinein, nicht in den Skripttext — so kann
    # kein Anfuehrungszeichen darin etwas anrichten.
    if command -v osascript >/dev/null 2>&1; then
      osascript -e 'on run argv' -e 'activate' \
        -e 'display dialog (item 1 of argv) with title "Osiris" buttons {"OK"} default button 1 with icon stop' \
        -e 'end run' "$nachricht" >/dev/null 2>&1
    fi
  elif command -v zenity >/dev/null 2>&1; then
    zenity --error --title=Osiris --no-markup --text="$nachricht" >/dev/null 2>&1
  elif command -v kdialog >/dev/null 2>&1; then
    kdialog --title Osiris --error "$nachricht" >/dev/null 2>&1
  elif command -v notify-send >/dev/null 2>&1; then
    notify-send -a Osiris -i osiris Osiris "$nachricht" >/dev/null 2>&1
  elif command -v xmessage >/dev/null 2>&1; then
    xmessage -center "$nachricht" >/dev/null 2>&1
  fi
  return 0
}

abbruch() {
  melde_fehler "$1"
  exit 1
}

# ------------------------------------------------------------------ Helfer

# Ein Wert aus installer.txt (Zeilen „schluessel=wert").
lies_wert() {
  sed -n "s/^$1=//p" "$2" 2>/dev/null | head -n 1 | tr -d '\r'
}

# Ein Zeichenketten-Feld der obersten Ebene aus einer kleinen JSON-Datei.
json_wert() {
  tr ',{}' '\n\n\n' <"$2" 2>/dev/null \
    | sed -n "s/^[[:space:]]*\"$1\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -n 1
}

# Alle Prozessnummern ("pid": n) in einer kleinen JSON-Datei.
json_pids() {
  tr ',{}' '\n\n\n' <"$1" 2>/dev/null \
    | sed -n 's/^[[:space:]]*"pid"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p'
}

# Hat die Fassung die Form x.y.z (davor ein v, dahinter -vorab erlaubt)?
gueltig() {
  k="${1#v}"
  k="${k%%-*}"
  case "$k" in
    ''|*[!0-9.]*|.*|*.|*..*) return 1 ;;
    *.*.*.*) return 1 ;;
    *.*.*) return 0 ;;
  esac
  return 1
}

# Wahr, wenn $1 neuer ist als $2. Ungueltiges zaehlt als aeltestes — wie
# vergleiche() in services/aktualisierung.mjs; eine Vorabfassung ist aelter
# als dieselbe ohne.
neuer_als() {
  gueltig "$1" || return 1
  gueltig "$2" || return 0
  a="${1#v}"; b="${2#v}"
  ak="${a%%-*}"; bk="${b%%-*}"
  a1="${ak%%.*}"; ar="${ak#*.}"; a2="${ar%%.*}"; a3="${ar#*.}"
  b1="${bk%%.*}"; br="${bk#*.}"; b2="${br%%.*}"; b3="${br#*.}"
  for paar in "$a1 $b1" "$a2 $b2" "$a3 $b3"; do
    x="${paar% *}"; y="${paar#* }"
    [ "$x" -gt "$y" ] && return 0
    [ "$x" -lt "$y" ] && return 1
  done
  case "$a" in *-*) return 1 ;; esac
  case "$b" in *-*) return 0 ;; esac
  return 1
}

# Die Fassung einer App: aus osiris.config.json (vom Bau gestempelt), sonst
# aus package.json.
app_fassung() {
  for datei in "$1/osiris.config.json" "$1/package.json"; do
    [ -f "$datei" ] || continue
    v="$(sed -n 's/^[[:space:]]\{0,2\}"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$datei" | head -n 1)"
    if gueltig "$v"; then printf '%s' "$v"; return 0; fi
  done
  return 1
}

# Liegt in $1 eine Installation mit Daten (app/data nicht leer)? Wie
# hatDaten in services/installation.mjs.
hat_daten() {
  [ -d "$1/app/data" ] && [ -n "$(ls -A "$1/app/data" 2>/dev/null)" ]
}

# Juenger als eine Minute? (fuer Sperren, deren Halter noch nichts
# hineingeschrieben hat)
ist_frisch() {
  [ -e "$1" ] && [ -z "$(find "$1" -prune -mmin +1 2>/dev/null)" ]
}

# macOS: Was aus dem heruntergeladenen DMG kopiert wurde, traegt dessen
# Quarantaene-Vermerk. Osiris.app hat der Nutzer schon freigegeben; ohne das
# hier fragte macOS bei der Laufzeit noch einmal nach.
entquarantaene() {
  [ "$OS" = darwin ] || return 0
  command -v xattr >/dev/null 2>&1 && xattr -dr com.apple.quarantine "$1" >/dev/null 2>&1
  return 0
}

# ------------------------------------------------------------------ Sperre

# Ein Ordner, den nur einer anlegen kann. Darin: „<pid> <phase> <seit>".
#   arbeit     der Starter richtet gerade ein — gilt, solange er lebt
#   gestartet  Osiris laeuft an — gilt ANLAUF_S Sekunden, solange es lebt
# Lebt unter dieser Nummer noch ein Starter bzw. Osiris? Nach einem Absturz
# oder Stromausfall kann die Nummer laengst einem fremden Programm gehoeren —
# dann blockierte die Sperre jeden Klick bis zu zehn Minuten. Laesst sich die
# Kommandozeile nicht lesen, zaehlt wie bisher nur, dass der Prozess lebt.
lebt_unserer() {
  kill -0 "$1" 2>/dev/null || return 1
  befehl="$(ps -p "$1" -o command= 2>/dev/null)" || return 0
  [ -z "$befehl" ] && return 0
  case "$befehl" in
    *osiris-starten*|*osiris-oeffnen*|*Osiris*|*osiris.mjs*) return 0 ;;
  esac
  return 1
}

sperre_veraltet() {
  inhalt="$(cat "$SPERRE/halter" 2>/dev/null)"
  if [ -z "$inhalt" ]; then
    ist_frisch "$SPERRE" && return 1
    return 0
  fi
  pid="${inhalt%% *}"; rest="${inhalt#* }"; phase="${rest%% *}"; seit="${rest#* }"
  case "$pid$seit" in ''|*[!0-9]*) return 0 ;; esac
  lebt_unserer "$pid" || return 0
  alter=$(( $(jetzt_s) - seit ))
  # Ging die Uhr zurueck, weiss niemand, wie alt die Sperre ist — dann gilt sie nicht.
  [ "$alter" -lt 0 ] && return 0
  case "$phase" in
    gestartet) [ "$alter" -ge "$ANLAUF_S" ] && return 0 ;;
    *) [ "$alter" -ge 600 ] && return 0 ;;
  esac
  return 1
}

nimm_sperre() {
  versuch=0
  while ! mkdir "$SPERRE" 2>/dev/null; do
    versuch=$((versuch + 1))
    [ "$versuch" -gt 3 ] && return 1
    sperre_veraltet || return 1
    # Veraltet: erst beiseite, dann weg — das gelingt nur einem von zweien.
    weg="$SPERRE.alt.$$"
    mv "$SPERRE" "$weg" 2>/dev/null && rm -rf "$weg"
  done
  printf '%s arbeit %s\n' "$$" "$(jetzt_s)" >"$SPERRE/halter"
  return 0
}

aufraeumen() {
  [ -n "$SPERRE" ] && [ "$SPERRE_BEHALTEN" != 1 ] && rm -rf "$SPERRE"
  return 0
}

# Richtet gerade ein anderer Start ein (Phase „arbeit", und er lebt)?
sperre_in_arbeit() {
  [ -d "$SPERRE" ] || return 1
  inhalt="$(cat "$SPERRE/halter" 2>/dev/null)"
  if [ -z "$inhalt" ]; then
    ist_frisch "$SPERRE"
    return $?
  fi
  pid="${inhalt%% *}"; rest="${inhalt#* }"; phase="${rest%% *}"
  [ "$phase" = arbeit ] && lebt_unserer "$pid"
}

# Zum Oeffnen einer Datei: warten, bis ein anderer Start fertig eingerichtet
# hat — hoechstens zwei Minuten. Laeuft Osiris nur an (Phase „gestartet"),
# muss niemand warten.
warte_auf_einrichtung() {
  i=0
  while sperre_in_arbeit && [ "$i" -lt 600 ]; do
    kurz_warten
    i=$((i + 1))
  done
}

# ------------------------------------------------------------------ Laeuft Osiris?

# Haelt jemand die Sperre des Update-Ordners (services/aktualisierung.mjs)?
update_gesperrt() {
  datei="$ZIEL/update.sperre"
  [ -f "$datei" ] || return 1
  pid="$(json_pids "$datei" | head -n 1)"
  if [ -z "$pid" ]; then
    ist_frisch "$datei"
    return $?
  fi
  kill -0 "$pid" 2>/dev/null
}

# Startet Osiris gerade fuer ein Update neu? Dann gehoert app/ diesem Lauf,
# auch wenn gerade kein Server antwortet und nichts im Vermerk steht:
#   ZIEL/update.neustart           der Nachfolger beim Update aus der App
#                                  (services/selbstupdate.mjs) wartet oder
#                                  spielt ein
#   ZIEL/app.alt-*/RUECKWEG.json   ein Start wartet, ob der eben eingespielte
#                                  Stand hochkommt — sonst tauscht er zurueck
# Gilt, solange der Halter darin lebt (ohne lesbare Nummer: eine Minute).
# Frueher tauschte der Starter app/ in genau dieser Luecke aus: Der neue
# Server lief aus einem Ordner, den es nicht mehr gab, oder der Rueckweg warf
# die frisch installierte Fassung wieder hinaus.
neustart_laeuft() {
  for datei in "$ZIEL/update.neustart" "$ZIEL"/app.alt-*/RUECKWEG.json; do
    [ -f "$datei" ] || continue
    pid="$(json_pids "$datei" | head -n 1)"
    if [ -z "$pid" ]; then
      ist_frisch "$datei" && return 0
      continue
    fi
    lebt_unserer "$pid" && return 0
  done
  return 1
}

# Laeuft eine Osiris-Sitzung aus ZIEL/app? Der Vermerk data/osiris.pid nennt
# ihre Prozessnummern; gezaehlt wird nur, was lebt und osiris.mjs ausfuehrt.
# Ebenso ein Neustart fuer ein Update (neustart_laeuft). Im Zweifel: ja —
# dann wird bereitgelegt statt getauscht, und das ist immer sicher.
osiris_laeuft() {
  update_gesperrt && return 0
  neustart_laeuft && return 0
  # Nur ein Notstand in app/ (ein Update scheiterte beim Umlegen): Der
  # vorige Stand samt Daten liegt daneben, und Osiris holt ihn selbst zurueck.
  [ -f "$ZIEL/app/NOTSTAND.json" ] && return 0
  vermerk="$ZIEL/app/data/osiris.pid"
  [ -f "$vermerk" ] || return 1
  for pid in $(json_pids "$vermerk"); do
    kill -0 "$pid" 2>/dev/null || continue
    if [ -r "/proc/$pid/cmdline" ]; then
      tr '\000' ' ' <"/proc/$pid/cmdline" | grep -q 'osiris\.mjs' && return 0
      continue
    fi
    befehl="$(ps -p "$pid" -o command= 2>/dev/null)" || return 0
    case "$befehl" in *osiris.mjs*) return 0 ;; esac
  done
  return 1
}

# ------------------------------------------------------------------ Laufzeit

# Die mitgebrachte Laufzeit einer Plattform finden: ein Archiv (macOS) oder
# ein Ordner mit node und STAND (Linux).
finde_mitgebrachte() {
  for endung in tar.xz tar.gz; do
    if [ -f "$QUELLE/runtime/$1.$endung" ]; then
      MIT_LZ="$QUELLE/runtime/$1.$endung"; MIT_LZ_ART=archiv; return 0
    fi
  done
  if [ -f "$QUELLE/runtime/node" ] && [ "$(cut -d ' ' -f 2 <"$QUELLE/runtime/STAND" 2>/dev/null)" = "$1" ]; then
    MIT_LZ="$QUELLE/runtime"; MIT_LZ_ART=ordner; return 0
  fi
  return 1
}

richte_laufzeit_ein() {
  PLATTFORM="$OS-$CPU"
  if ! finde_mitgebrachte "$PLATTFORM"; then
    if [ "$PLATTFORM" = darwin-arm64 ] && finde_mitgebrachte darwin-x64; then
      protokoll "Keine arm64-Laufzeit dabei — nehme x64 (Rosetta)."
      PLATTFORM=darwin-x64
    else
      abbruch "$(text laufzeit_fehlt "$PLATTFORM")"
    fi
  fi

  stand="$(cat "$ZIEL/runtime/STAND" 2>/dev/null)"
  ist_fassung="${stand%% *}"
  ist_plattform="${stand#* }"
  if [ -x "$ZIEL/runtime/node" ] && [ "$ist_plattform" = "$PLATTFORM" ] && ! neuer_als "$LAUFZEIT" "$ist_fassung"; then
    return 0
  fi

  protokoll "Richte Node $LAUFZEIT ($PLATTFORM) ein (bisher: ${stand:-keine})."
  neu="$ZIEL/.runtime.neu.$$"
  alt="$ZIEL/.runtime.alt.$$"
  rm -rf "$neu"
  mkdir -p "$neu" || abbruch "$(text laufzeit_kaputt)"
  if [ "$MIT_LZ_ART" = archiv ]; then
    tar -xf "$MIT_LZ" -C "$neu" >>"$LOG" 2>&1
  else
    cp "$MIT_LZ/node" "$neu/node" && { [ ! -f "$MIT_LZ/LICENSE" ] || cp "$MIT_LZ/LICENSE" "$neu/LICENSE"; }
  fi
  if [ $? -ne 0 ] || [ ! -f "$neu/node" ]; then
    rm -rf "$neu"
    abbruch "$(text laufzeit_kaputt)"
  fi
  chmod 755 "$neu/node"
  chmod -R u+w "$neu"
  printf '%s %s\n' "$LAUFZEIT" "$PLATTFORM" >"$neu/STAND"
  entquarantaene "$neu"

  # Erst pruefen, dass sie hier laeuft — dann erst weicht die alte.
  if ! "$neu/node" --version >>"$LOG" 2>&1; then
    rm -rf "$neu"
    meldung="$(text laufzeit_laeuft_nicht "$PLATTFORM")"
    [ "$PLATTFORM" = darwin-x64 ] && [ "$CPU" = arm64 ] && meldung="$meldung $(text rosetta)"
    abbruch "$meldung"
  fi

  # Tauschen. Ein laufender Server behaelt seine alte Datei, bis er endet.
  if [ -e "$ZIEL/runtime" ] && ! mv "$ZIEL/runtime" "$alt"; then
    rm -rf "$neu"
    abbruch "$(text laufzeit_kaputt)"
  fi
  if ! mv "$neu" "$ZIEL/runtime"; then
    [ -e "$alt" ] && mv "$alt" "$ZIEL/runtime"
    rm -rf "$neu"
    abbruch "$(text laufzeit_kaputt)"
  fi
  rm -rf "$alt"
  return 0
}

# ------------------------------------------------------------------ App

# Ein bereitliegendes Update verwerfen, wenn es nicht neuer ist als $1 — es
# fiele sonst beim naechsten Start ueber die eben eingerichtete Fassung.
raeume_update_auf() {
  [ -f "$ZIEL/update/BEREIT" ] || return 0
  bereit="$(tr -d ' \r\n' <"$ZIEL/update/BEREIT" 2>/dev/null)"
  neuer_als "$bereit" "$1" && return 0
  update_gesperrt && return 0
  rm -rf "$ZIEL/update"
  protokoll "Aelteres Update ($bereit) verworfen."
}

# Die alte App samt Daten zurueck an ihren Platz — nur, wenn alles Eigene
# wieder drueben ist, wird die neue geloescht.
zurueck() {
  for name in $EIGENES; do
    if [ -e "$ZIEL/app/$name" ] && [ ! -e "$alt/$name" ]; then
      mv "$ZIEL/app/$name" "$alt/$name" || return 1
    fi
  done
  rm -rf "$ZIEL/app" && mv "$alt" "$ZIEL/app"
}

# Fehlte app/ (ein Update brach beim Tauschen ab) oder lag darin nur ein
# Notstand (NOTSTAND.json), stecken die eigenen Daten noch im neuesten
# app.alt-* daneben (services/aktualisierung.mjs). Kopiert, nicht verschoben:
# Der alte Stand bleibt als Sicherung liegen (Marke BEHALTEN).
hole_eigenes_aus_altem_stand() {
  vorher=""
  for d in "$ZIEL"/app.alt-*; do
    [ -d "$d" ] || continue
    if [ -e "$d/data" ] || [ -e "$d/osiris.config.local.json" ]; then vorher="$d"; fi
  done
  [ -n "$vorher" ] || return 0
  for name in $EIGENES; do
    [ -e "$vorher/$name" ] || continue
    rm -rf "${ZIEL:?}/app/$name"
    if ! cp -R "$vorher/$name" "$ZIEL/app/$name" >>"$LOG" 2>&1; then
      protokoll "$name liess sich nicht aus $vorher uebernehmen — es liegt weiter dort."
    fi
  done
  protokoll "Eigene Daten aus $(basename "$vorher") uebernommen."
}

# Die mitgebrachte App an die Stelle von ZIEL/app legen. Nur umbenennen,
# nie kopieren und nie loeschen, solange darin Nutzerdaten liegen — wie
# install.sh. Alles bleibt unter ZIEL, also im selben Dateisystem.
tausche_app() {
  neu="$ZIEL/.app.neu.$$"
  alt="$ZIEL/.app.alt.$$"
  rm -rf "$neu"
  if ! cp -R "$MIT_APP" "$neu" >>"$LOG" 2>&1 || [ ! -f "$neu/bin/osiris.mjs" ]; then
    rm -rf "$neu"
    abbruch "$(text kopie)"
  fi
  # Was der Nutzer hat, bringt kein Installer mit.
  for name in $EIGENES; do rm -rf "${neu:?}/$name"; done
  # Aus dem DMG kommt alles schreibgeschuetzt — ein spaeteres Update muss
  # die Dateien ersetzen koennen.
  chmod -R u+w "$neu"
  entquarantaene "$neu"

  ohne_daten=""
  if [ ! -d "$ZIEL/app" ] || [ -f "$ZIEL/app/NOTSTAND.json" ]; then ohne_daten=1; fi
  if [ -d "$ZIEL/app" ] && ! mv "$ZIEL/app" "$alt"; then
    rm -rf "$neu"
    abbruch "$(text kopie)"
  fi
  if ! mv "$neu" "$ZIEL/app"; then
    rm -rf "$neu"
    if [ ! -d "$alt" ] || mv "$alt" "$ZIEL/app"; then abbruch "$(text kopie)"; fi
    abbruch "$(text liegt_in "$alt")"
  fi
  if [ -d "$alt" ]; then
    for name in $EIGENES; do
      [ -e "$alt/$name" ] || continue
      rm -rf "${ZIEL:?}/app/$name"
      if ! mv "$alt/$name" "$ZIEL/app/$name"; then
        if zurueck; then abbruch "$(text zurueck)"; fi
        abbruch "$(text liegt_in "$alt")"
      fi
    done
    rm -rf "$alt"
  fi
  if [ -n "$ohne_daten" ]; then hole_eigenes_aus_altem_stand; fi
  raeume_update_auf "$MIT_FASSUNG"
  protokoll "App $MIT_FASSUNG eingerichtet${IST_FASSUNG:+ (vorher $IST_FASSUNG)}."
}

# Osiris laeuft: die mitgebrachte Fassung als Update bereitlegen — so, wie
# services/aktualisierung.mjs es tut (unter update.sperre, in einer Baustelle
# daneben, die Marke BEREIT zuletzt). Die Daten nimmt das Einspielen selbst
# mit. Gelingt es nicht, startet Osiris eben mit dem bisherigen Stand.
stelle_bereit() {
  if update_gesperrt; then
    protokoll "Osiris laedt oder spielt gerade selbst ein Update ein — die mitgebrachte Fassung $MIT_FASSUNG kommt beim naechsten Start."
    return 0
  fi
  if [ -f "$ZIEL/update/BEREIT" ]; then
    bereit="$(tr -d ' \r\n' <"$ZIEL/update/BEREIT" 2>/dev/null)"
    if ! neuer_als "$MIT_FASSUNG" "$bereit"; then
      protokoll "Update $bereit liegt schon bereit."
      return 0
    fi
  fi
  sperre_datei="$ZIEL/update.sperre"
  if ! (set -C; printf '{"pid":%s,"seit":%s000,"kennung":"starter-%s","zweck":"laden"}' "$$" "$(jetzt_s)" "$$" >"$sperre_datei") 2>/dev/null; then
    protokoll "Update-Sperre belegt — die mitgebrachte Fassung kommt beim naechsten Start."
    return 0
  fi
  baustelle="$ZIEL/update.neu-starter$$"
  rm -rf "$baustelle"
  if mkdir -p "$baustelle" && cp -R "$MIT_APP" "$baustelle/app" >>"$LOG" 2>&1 && [ -f "$baustelle/app/bin/osiris.mjs" ]; then
    for name in $EIGENES; do rm -rf "${baustelle:?}/app/$name"; done
    # Woher der Stand kommt: vom Installer, nicht vom Update-Server. Osiris
    # fragt fuer ihn nicht beim Server nach, ob seine Freigabe noch gilt
    # (pruefeBereit) — er hat keine, und er ist trotzdem nicht zurueckgezogen.
    printf 'installer\n' >"$baustelle/HERKUNFT"
    chmod -R u+w "$baustelle"
    entquarantaene "$baustelle"
    if rm -rf "$ZIEL/update" && mv "$baustelle" "$ZIEL/update" \
      && printf '%s\n' "$MIT_FASSUNG" >"$ZIEL/update/BEREIT.neu" && mv -f "$ZIEL/update/BEREIT.neu" "$ZIEL/update/BEREIT"; then
      protokoll "Osiris laeuft — Fassung $MIT_FASSUNG liegt als Update bereit, Osiris spielt sie selbst ein."
    else
      protokoll "Update liess sich nicht bereitlegen."
    fi
  else
    protokoll "Update liess sich nicht bereitlegen (Kopie gescheitert)."
  fi
  rm -rf "$baustelle"
  rm -f "$sperre_datei"
  return 0
}

richte_app_ein() {
  MIT_APP="$QUELLE/app"
  [ -f "$MIT_APP/bin/osiris.mjs" ] || abbruch "$(text quelle)"
  MIT_FASSUNG="$(app_fassung "$MIT_APP")" || MIT_FASSUNG="$FASSUNG"
  IST_FASSUNG=""
  if [ ! -f "$ZIEL/app/bin/osiris.mjs" ]; then
    # Erster Start — oder ein unvollstaendiger Stand, dessen Daten mitgehen.
    tausche_app
    return 0
  fi
  IST_FASSUNG="$(app_fassung "$ZIEL/app")" || IST_FASSUNG=""
  if ! neuer_als "$MIT_FASSUNG" "$IST_FASSUNG"; then
    # Gleich oder schon neuer (Selbst-Update) — bleibt.
    return 0
  fi
  if osiris_laeuft; then
    stelle_bereit
  else
    tausche_app
  fi
}

# Chats, Lizenz und Anmeldetoken nur fuer den eigenen Benutzer: ZIEL,
# app/data, logs und browser (das Profil des Fensters) auf 700 — gezielt
# diese Ordner, nicht per umask fuer alles (siehe oben). Bei jedem Start,
# auch wenn nichts eingerichtet wird: So bekommen es bestehende
# Installationen gleicher Fassung ebenso. Wie schuetzeDaten in
# services/installation.mjs, das beim Start von Osiris noch einmal laeuft.
schuetze_daten() {
  [ -d "$ZIEL/app" ] && mkdir -p "$ZIEL/app/data" 2>/dev/null
  # $ZIEL/fenster ist das Profil der Electron-Huelle (localStorage, Crashpad).
  for ordner in "$ZIEL" "$ZIEL/app/data" "$ZIEL/logs" "$ZIEL/browser" "$ZIEL/fenster"; do
    [ -d "$ordner" ] && [ -O "$ordner" ] && chmod 700 "$ordner" 2>/dev/null
  done
  return 0
}

schreibe_installation() {
  datei="$ZIEL/installation.json"
  if [ -f "$datei" ] && [ "$(json_wert art "$datei")" = "$ART" ] && [ "$(json_wert version "$datei")" = "$FASSUNG" ]; then
    return 0
  fi
  if printf '{"art":"%s","version":"%s","seit":"%s"}\n' "$ART" "$FASSUNG" "$(jetzt_iso)" >"$datei.neu.$$"; then
    mv -f "$datei.neu.$$" "$datei" || rm -f "$datei.neu.$$"
  fi
  return 0
}

# ------------------------------------------------------------------ PATH

# Aus dem Dock oder dem Startmenue gestartet, bekommt Osiris nur den
# kargen PATH des Systems (/usr/bin:/bin …). Claude Code und Homebrew liegen
# aber in ~/.local/bin, /opt/homebrew/bin und aehnlichen Orten, die erst die
# Anmelde-Shell des Nutzers einrichtet. Also: diese fragen (hoechstens drei
# Sekunden lang, fuer die bekannten Shells), und die ueblichen Orte
# anhaengen, falls es sie gibt.
haenge_an() {
  [ -n "$1" ] && [ -d "$1" ] || return 0
  case ":$PATH:" in *":$1:"*) ;; *) PATH="$PATH:$1" ;; esac
}

ergaenze_pfad() {
  shell="${SHELL:-}"
  case "${shell##*/}" in
    bash|zsh|sh|dash|ksh|mksh) ;;
    *) shell="" ;;
  esac
  if [ -n "$shell" ] && [ -x "$shell" ]; then
    ergebnis="$ZIEL/.pfad.$$"
    # Mit der umask des Nutzers: Was seine Startdateien anwerfen (ein
    # ssh-agent, ein tmux-Server), laeuft womoeglich weiter.
    (nutzer_umask; exec "$shell" -ilc 'printf "\n__OSIRIS_PFAD__%s\n" "$PATH"') >"$ergebnis" 2>/dev/null </dev/null &
    frage=$!
    i=0
    while kill -0 "$frage" 2>/dev/null && [ "$i" -lt "$WACHE" ]; do
      kurz_warten
      i=$((i + 1))
    done
    kill -9 "$frage" 2>/dev/null
    wait "$frage" 2>/dev/null
    gefunden="$(sed -n 's/^__OSIRIS_PFAD__//p' "$ergebnis" 2>/dev/null | tail -n 1)"
    rm -f "$ergebnis"
    if [ -n "$gefunden" ]; then
      alt_pfad="$PATH"
      PATH="$gefunden"
      rest="$alt_pfad"
      while [ -n "$rest" ]; do
        teil="${rest%%:*}"
        haenge_an "$teil"
        [ "$rest" = "$teil" ] && break
        rest="${rest#*:}"
      done
    fi
  fi
  for d in "$HOME/.local/bin" "$HOME/bin" "$HOME/.claude/local" "$HOME/.npm-global/bin" \
    "$HOME/.volta/bin" "$HOME/.bun/bin" /opt/homebrew/bin /opt/homebrew/sbin /usr/local/bin; do
    haenge_an "$d"
  done
  export PATH
}

# ------------------------------------------------------------------ Start

starte() {
  node="$ZIEL/runtime/node"
  cd "$HOME" 2>/dev/null || cd /
  # --im-browser geht an `start` weiter: Osiris oeffnet sich dann im
  # Standardbrowser statt in der Huelle.
  if [ "$MODUS" = im-browser ]; then set -- --im-browser; else set --; fi
  protokoll "Starte Osiris ($node $ZIEL/app/bin/osiris.mjs start --yes${1:+ $1})."
  nutzer_umask
  nohup "$node" "$ZIEL/app/bin/osiris.mjs" start --yes "$@" </dev/null >>"$LOG" 2>&1 &
  kind=$!
  printf '%s gestartet %s\n' "$kind" "$(jetzt_s)" >"$SPERRE/halter"
  SPERRE_BEHALTEN=1

  # Ein paar Sekunden zusehen. Laeuft Osiris schon, holt `start` nur das
  # Fenster nach vorn und endet mit 0 — das ist kein Fehler.
  i=0
  while [ "$i" -lt "$WACHE" ] && kill -0 "$kind" 2>/dev/null; do
    kurz_warten
    i=$((i + 1))
  done
  kill -0 "$kind" 2>/dev/null && return 0
  wait "$kind"
  code=$?
  if [ "$code" -ne 0 ]; then
    SPERRE_BEHALTEN=0
    abbruch "$(text start "$code")"
  fi
  return 0
}

# Dateien im Editor von Osiris oeffnen: `osiris.mjs open`, das Osiris bei
# Bedarf selbst im Hintergrund startet. Den PATH hat der Starter schon
# ergaenzt — die Marke sagt es `open`, damit es nicht noch einmal fragt.
oeffne() {
  node="$ZIEL/runtime/node"
  cd "$HOME" 2>/dev/null || cd /
  protokoll "Oeffne $# Datei(en) im Editor von Osiris."
  nutzer_umask
  OSIRIS_PFAD_ERGAENZT=1 nohup "$node" "$ZIEL/app/bin/osiris.mjs" open -- "$@" </dev/null >>"$LOG" 2>&1 &
  kind=$!
  # Ein paar Sekunden zusehen: Scheitert es sofort (die Datei ist weg),
  # sagt es ein Dialog. Laeuft es noch, startet es gerade Osiris.
  i=0
  while [ "$i" -lt "$WACHE" ] && kill -0 "$kind" 2>/dev/null; do
    kurz_warten
    i=$((i + 1))
  done
  kill -0 "$kind" 2>/dev/null && return 0
  wait "$kind"
  code=$?
  [ "$code" -ne 0 ] && abbruch "$(text oeffnen "$code")"
  return 0
}

# ------------------------------------------------------------------ Ablauf

[ -n "${HOME:-}" ] || abbruch "$(text heim)"

if [ "$OS" = darwin ]; then
  ZIEL="$HOME/Library/Application Support/Osiris"
else
  ZIEL="${XDG_DATA_HOME:-$HOME/.local/share}/osiris"
  # Lief install.sh ohne XDG_DATA_HOME, liegt die Installation samt Chats und
  # Aktivierung unter ~/.local/share/osiris. Ist unter XDG_DATA_HOME keine
  # mit Daten, geht es dort weiter — sonst entstuende daneben eine leere.
  # Wie zielOrdner in services/installation.mjs.
  UEBLICH="$HOME/.local/share/osiris"
  if [ "$ZIEL" != "$UEBLICH" ] && hat_daten "$UEBLICH" && ! hat_daten "$ZIEL"; then
    ZIEL="$UEBLICH"
  fi
fi

# Eine Mitteilung oben rechts (macOS), ohne auf etwas zu warten. Der Text
# geht als Argument hinein, nie in das Skript von osascript.
mitteilung() {
  [ "$OS" = darwin ] || return 0
  command -v osascript >/dev/null 2>&1 || return 0
  osascript -e 'on run argv' -e 'display notification (item 1 of argv) with title "Osiris"' -e 'end run' "$1" >/dev/null 2>&1
}

# macOS: Aus dem Abbild gestartet (/Volumes/…) oder von macOS dorthin
# „verschoben" (App Translocation, bei einer Datei aus dem Internet)? Dann
# legt sich Osiris selbst nach „Programme" und startet von dort neu — kein
# Ziehen noetig. Die Kopie kommt ohne Quarantaene-Merker (ditto --noqtn):
# Die Freigabe hat der Nutzer eben gegeben („Dennoch öffnen"); ohne das
# fragte macOS fuer die Kopie in Programme ein zweites Mal.
# Gibt 0 zurueck, wenn die installierte Kopie gestartet wurde.
installiere_mac_app() {
  [ "$OS" = darwin ] || return 1
  [ -n "$QUELLE" ] || return 1
  bundle="$(cd "$QUELLE/../.." 2>/dev/null && pwd -P)" || return 1
  [ "$(basename "$bundle")" = "Osiris.app" ] || return 1
  case "$bundle" in
    /Volumes/*|*/AppTranslocation/*) ;;
    *) return 1 ;;
  esac
  command -v ditto >/dev/null 2>&1 || return 1
  # Wohin: „Programme", sonst ~/Applications (ohne Adminrechte). Die Tests
  # geben mit OSIRIS_MAC_PROGRAMME eigene Ordner vor (mit : getrennt, der
  # Reihe nach — wie hier /Applications, dann ~/Applications).
  if [ -n "${OSIRIS_MAC_PROGRAMME:-}" ]; then
    alt_ifs="$IFS"
    IFS=:
    set -f
    # shellcheck disable=SC2086
    set -- $OSIRIS_MAC_PROGRAMME
    set +f
    IFS="$alt_ifs"
  else
    set -- /Applications "$HOME/Applications"
  fi
  for ordner in "$@"; do
    mkdir -p "$ordner" 2>/dev/null
    [ -d "$ordner" ] && [ -w "$ordner" ] || continue
    neu="$ordner/Osiris.app"
    zwischen="$ordner/.Osiris.app.neu.$$"
    alt="$ordner/.Osiris.app.alt.$$"
    rm -rf "$zwischen" 2>/dev/null
    if ditto --noqtn "$bundle" "$zwischen" >>"$LOG" 2>&1 && [ -x "$zwischen/Contents/MacOS/Osiris" ]; then
      # Aus dem DMG (ISO 9660) kommt alles schreibgeschuetzt (0555). In
      # „Programme" soll es dem Nutzer gehoeren — sonst laesst sich spaeter
      # weder die Quarantaene entfernen noch die App ersetzen.
      chmod -R u+w "$zwischen" 2>/dev/null
      entquarantaene "$zwischen"
      [ -e "$neu" ] && mv "$neu" "$alt" 2>/dev/null
      if mv "$zwischen" "$neu" 2>/dev/null; then
        rm -rf "$alt" 2>/dev/null
        # Absolut und ohne Umwege — die Huelle oeffnet genau diesen Pfad.
        INSTALLIERT="$(cd "$ordner" 2>/dev/null && pwd -P)/Osiris.app"
        protokoll "Osiris.app aus $bundle nach $neu gelegt."
        # Von der Mac-Huelle (--installiere) nur kopieren: Mitteilung und das
        # Oeffnen der Kopie macht dann die Huelle selbst, sonst ginge Osiris
        # doppelt auf.
        if [ "${NUR_KOPIEREN:-0}" != 1 ]; then
          mitteilung "$(text mac_installiert)"
          open "$neu" >>"$LOG" 2>&1
        fi
        # Das Abbild auswerfen, sobald die Kopie laeuft — es wird nicht mehr gebraucht.
        case "$bundle" in
          /Volumes/*)
            volume="/Volumes/$(printf '%s' "${bundle#/Volumes/}" | cut -d/ -f1)"
            (sleep 5; hdiutil detach "$volume" -quiet) >/dev/null 2>&1 &
            ;;
        esac
        return 0
      fi
      [ -e "$alt" ] && mv "$alt" "$neu" 2>/dev/null
    fi
    rm -rf "$zwischen" 2>/dev/null
  done
  protokoll "Osiris.app ließ sich nicht nach Programme legen — starte aus $bundle."
  return 1
}

# Wo liegt, was der Installer mitbringt? Neben diesem Skript — ein Verweis
# darauf (etwa in ~/bin) zaehlt nicht.
selbst="$0"
while [ -L "$selbst" ]; do
  verweis="$(readlink "$selbst")" || break
  case "$verweis" in
    /*) selbst="$verweis" ;;
    *) selbst="$(dirname "$selbst")/$verweis" ;;
  esac
done
HIER="$(cd "$(dirname "$selbst")" 2>/dev/null && pwd -P)"
QUELLE=""
if [ -n "$HIER" ] && [ -f "$HIER/../Resources/installer.txt" ]; then
  QUELLE="$(cd "$HIER/../Resources" && pwd -P)"
elif [ -n "$HIER" ] && [ -f "$HIER/installer.txt" ]; then
  QUELLE="$HIER"
fi

# Die Ordner ueber ZIEL (etwa ~/.local/share) gehoeren nicht Osiris — sie
# entstehen, falls noch nicht da, mit der umask des Nutzers.
(nutzer_umask; mkdir -p "$(dirname "$ZIEL")") 2>/dev/null
if mkdir -p "$ZIEL/logs" 2>/dev/null; then
  LOG="$ZIEL/logs/start.log"
  if [ -f "$LOG" ]; then
    groesse="$(wc -c <"$LOG" | tr -d ' ')"
    if [ "${groesse:-0}" -gt "$LOG_GRENZE" ] 2>/dev/null; then
      mv -f "$LOG" "$LOG.1" 2>/dev/null
    fi
  fi
else
  abbruch "$(text ziel "$ZIEL")"
fi

[ -n "$QUELLE" ] || abbruch "$(text quelle)"
ART="$(lies_wert art "$QUELLE/installer.txt")"
FASSUNG="$(lies_wert version "$QUELLE/installer.txt")"
LAUFZEIT="$(lies_wert laufzeit "$QUELLE/installer.txt")"
case "$ART" in dmg|deb) ;; *) abbruch "$(text quelle)" ;; esac
gueltig "$FASSUNG" && gueltig "$LAUFZEIT" || abbruch "$(text quelle)"
[ -n "$CPU" ] || abbruch "$(text cpu "$SYSTEM $CPU_ROH")"

# Von der Mac-Huelle gerufen (--installiere): NUR die App nach „Programme"
# legen, nichts einrichten, nichts starten, nicht oeffnen — den Rest macht
# die Huelle. 0 = kopiert, 1 = ging nicht (dann laeuft die Huelle aus dem
# aktuellen Ort weiter). Bei 0 steht auf stdout genau eine Zeile
# OSIRIS_APP=<Pfad>: Die Kopie kann in /Applications ODER ~/Applications
# liegen, und die Huelle soll die oeffnen, die es wirklich gibt. Alles, was
# beim Kopieren sonst auf stdout kaeme, geht ins Protokoll.
if [ "$MODUS" = installiere ]; then
  INSTALLIERT=""
  if NUR_KOPIEREN=1 installiere_mac_app >>"$LOG" && [ -n "$INSTALLIERT" ]; then
    printf 'OSIRIS_APP=%s\n' "$INSTALLIERT"
    exit 0
  fi
  exit 1
fi
# Sonst: die uebliche Selbstinstallation nach „Programme" (Doppelklick aus
# dem DMG). Unter der Huelle (--huelle, --im-browser) hat die Huelle das schon
# erledigt — dann nicht noch einmal, sonst liefe eine zweite Kopie los.
if [ "$UNTER_HUELLE" != 1 ] && installiere_mac_app; then exit 0; fi
# Das erste Einrichten dauert ein paar Sekunden, und Osiris zeigt im Dock
# nichts (LSUIElement) — sonst saehe es aus, als passiere nichts. Unter der
# Huelle zeigt schon ihr Ladebildschirm, dass etwas geschieht.
if [ "$UNTER_HUELLE" != 1 ] && [ "$OS" = darwin ] && [ ! -x "$ZIEL/runtime/node" ]; then mitteilung "$(text mac_einrichten)"; fi

SPERRE="$ZIEL/.start.sperre"
trap aufraeumen EXIT
trap 'exit 1' HUP INT TERM
# Ohne Dateien gibt es nichts zu oeffnen — dann eben Osiris selbst.
[ "$MODUS" = oeffnen ] && [ $# -eq 0 ] && MODUS=start
if ! nimm_sperre; then
  # Die Sperre gehoert einem anderen Start — sie bleibt, wie sie ist.
  SPERRE_BEHALTEN=1
  if [ "$MODUS" = oeffnen ]; then
    # Ein Doppelklick geht nicht verloren: warten, bis eingerichtet ist,
    # dann oeffnen, ohne selbst noch etwas einzurichten.
    warte_auf_einrichtung
    if [ ! -x "$ZIEL/runtime/node" ] || [ ! -f "$ZIEL/app/bin/osiris.mjs" ]; then
      abbruch "$(text beschaeftigt)"
    fi
    protokoll "Osiris-Starter $FASSUNG: ein anderer Start war dabei — oeffne ohne Einrichten."
    ergaenze_pfad
    oeffne "$@"
    exit 0
  fi
  protokoll "Ein anderer Start ist schon dabei — nichts zu tun."
  exit 0
fi

protokoll "Osiris-Starter $FASSUNG ($ART, $OS-$CPU, Quelle $QUELLE)"
richte_laufzeit_ein
richte_app_ein
schreibe_installation
schuetze_daten
if [ "$MODUS" = oeffnen ]; then
  # Eingerichtet ist — die Sperre braucht niemand mehr. Das Oeffnen selbst
  # darf neben einem Klick auf Osiris laufen.
  aufraeumen
  SPERRE=""
  ergaenze_pfad
  oeffne "$@"
  exit 0
fi
ergaenze_pfad
starte
exit 0
