#!/bin/sh # # ThoughtSync desktop — one-command Linux installer. # # curl -fsSL https://git.fabledsword.com/bvandeusen/thoughtsync/raw/branch/dev/desktop/packaging/install.sh | sh # # Two channels, the SAME two the app's own updater offers (src-tauri/src/update.rs): # stable (default) — the newest tagged v* release. # dev — the rolling build from every green push to `dev`. # Pick one with `--channel dev` or `TS_CHANNEL=dev`. Through a pipe the options go # after a `--`: curl -fsSL | sh -s -- --channel dev # # Served from `dev` rather than `main`: `main` exists but trails day-to-day work by # a long way, so the copy there would install an older script. Move the documented # URL to `main` after a dev→main merge lands, not before. # # Fetches the LATEST published release on the chosen channel for this machine's # architecture and installs it, ending with a working app + menu entry. Native-first: # * Arch/CachyOS (pacman) -> the native .pkg.tar.zst (system libs; needs sudo). # * Debian/Ubuntu (dpkg+apt) -> the native .deb (system libs; needs sudo). # * everything else (Fedora/openSUSE/…) -> the de-bundled AppImage, # installed user-locally (no sudo). The AppImage's graphics libs are # stripped in CI (see debundle-graphics.sh), so it uses the host GPU stack # and renders where a stock Tauri AppImage would black-window (issue 2021). # # POSIX sh (dash-safe) so `curl … | sh` works everywhere. Dependency-light and # auditable on purpose — read it before you pipe it. set -eu INSTANCE="https://git.fabledsword.com" REPO="bvandeusen/thoughtsync" API="$INSTANCE/api/v1/repos/$REPO" say() { printf '==> %s\n' "$1"; } die() { printf 'error: %s\n' "$1" >&2; exit 1; } have() { command -v "$1" >/dev/null 2>&1; } usage() { cat <<'USAGE' ThoughtSync desktop installer. install.sh [--channel stable|dev] --channel stable newest tagged release (default) --channel dev rolling build from the latest green push to `dev` -h, --help this text The channel can also come from TS_CHANNEL. Through a pipe, pass options after `--`: curl -fsSL | sh -s -- --channel dev USAGE } # --- channel ---------------------------------------------------------------- channel="${TS_CHANNEL:-stable}" while [ $# -gt 0 ]; do case "$1" in --channel) [ $# -ge 2 ] || die "--channel needs a value (stable or dev)." channel="$2"; shift 2 ;; --channel=*) channel="${1#*=}"; shift ;; -h | --help) usage; exit 0 ;; *) die "unknown option: $1 (try --help)" ;; esac done case "$channel" in stable | dev) : ;; *) die "unknown channel '$channel' — expected stable or dev." ;; esac have curl || die "curl is required." # --- architecture gate ------------------------------------------------------ # Only x86_64 is built today; arm64 will be added when the CI matrix grows. The # release-asset naming carries the arch, but since only one arch ships now we # match by file extension below and just guard the arch here. arch="$(uname -m)" case "$arch" in x86_64 | amd64) : ;; *) die "ThoughtSync ships x86_64 Linux builds only right now (this machine: $arch)." ;; esac # --- resolve the release for this channel ----------------------------------- say "Finding the latest ThoughtSync build on the $channel channel…" if [ "$channel" = "dev" ]; then # A release whose tag never moves and whose assets are pruned to the current # build — so the tag alone always names the newest dev build. json="$(curl -fsSL "$API/releases/tags/dev" 2>/dev/null)" || die "the dev channel has nothing published yet." else # Ask the stable channel's own manifest which version is current, then install # THAT release. This is the same file the in-app updater reads, so the installer # and the updater can never disagree about what `stable` means. # # Not `/releases/latest`: that returns the newest non-prerelease release by date, # and the `stable` pointer release (manifest only, no bundles — see # write-manifest.sh) is itself a non-prerelease created moments after the # versioned one. It would win, and it carries nothing installable. manifest="$(curl -fsSL "$INSTANCE/$REPO/releases/download/stable/latest.json" 2>/dev/null || true)" stable_version="$(printf '%s' "$manifest" | grep -oE '"version"[[:space:]]*:[[:space:]]*"[^"]+"' | head -1 | sed -E 's/.*"([^"]+)"$/\1/')" if [ -n "$stable_version" ]; then json="$(curl -fsSL "$API/releases/tags/v$stable_version" 2>/dev/null)" || die "the stable channel names $stable_version, but there is no v$stable_version release to install." else # No stable pointer yet — the channel predates the updater. Fall back to the # newest non-prerelease release, which is what stable meant before there was # a manifest to ask. json="$(curl -fsSL "$API/releases/latest" 2>/dev/null)" || die "no stable release published yet — try --channel dev, or ask the maintainer to tag one." fi fi # Pull asset URLs straight out of the release JSON (no jq). Anchored on the closing # quote so a `…AppImage.sig` URL can't be truncated into a match of its own. asset_url() { printf '%s' "$json" | grep -oE "https?://[^\"]+$1\"" | head -1 | tr -d '"' } appimage_url="$(asset_url '\.AppImage')" deb_url="$(asset_url '\.deb')" pkg_url="$(asset_url '\.pkg\.tar\.[a-z]+')" version="$(printf '%s' "$json" | grep -oE '"tag_name":"[^"]+"' | head -1 | sed -E 's/.*:"([^"]+)".*/\1/')" [ -n "$appimage_url" ] || [ -n "$deb_url" ] || [ -n "$pkg_url" ] || die "the $channel release (${version:-unknown}) has no installable Linux asset." say "Installing ${version:-unknown} from the $channel channel" tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT INT TERM # Both native paths install system-wide, so they need root. Resolved once here # rather than duplicated per branch; the AppImage path below never calls this. need_root() { if [ "$(id -u)" -eq 0 ]; then sudo=""; else have sudo || die "a native install needs root; re-run as root or install sudo." sudo="sudo" fi say "Installing (you may be prompted for your password)…" } # Both native paths are package-manager-owned, so the app cannot replace itself # in place (update.rs refuses, by design). Say so at the end of those paths rather # than letting someone discover it from a greyed-out button. native_update_note() { printf ' A package-manager install can'\''t update itself in-app.\n' if [ "$channel" = "dev" ]; then printf ' Re-run this script with --channel dev to move to a newer dev build.\n' else printf ' Re-run this script to move to a newer release.\n' fi } # --- native pacman path (Arch/CachyOS/Manjaro) ------------------------------ # Preferred over the AppImage on Arch: pacman pulls webkit2gtk-4.1 itself and the # app then runs against the host graphics stack, which is what keeps the # EGL_BAD_PARAMETER black window (issue 2021) from coming back. It also means the # app is tracked by the package manager and uninstalls cleanly. if have pacman && [ -n "$pkg_url" ]; then say "Arch-family system detected — installing the native pacman package" # Keep the published filename: pacman -U expects a *.pkg.tar.* name and refuses # a file that doesn't look like a package, whatever its actual contents. pkg_file="$tmp/$(basename "$pkg_url")" curl -fSL -o "$pkg_file" "$pkg_url" need_root $sudo pacman -U --noconfirm "$pkg_file" say "Done. Launch ThoughtSync from your application menu, or run thoughtsync." native_update_note exit 0 fi # --- native .deb path (Debian/Ubuntu) --------------------------------------- if have dpkg && have apt-get && [ -n "$deb_url" ]; then say "Debian-family system detected — installing the native .deb" curl -fSL -o "$tmp/thoughtsync.deb" "$deb_url" need_root # apt-get resolves the .deb's dependencies (webkit2gtk etc.). dpkg is the # fallback if this apt is too old for local-file installs — it leaves the deps # unconfigured, so `apt-get -f install` is what actually completes that path. $sudo apt-get install -y "$tmp/thoughtsync.deb" || { $sudo dpkg -i "$tmp/thoughtsync.deb" || true; $sudo apt-get -f install -y; } say "Done. Launch ThoughtSync from your application menu." native_update_note exit 0 fi # --- universal AppImage path (user-local, no sudo) -------------------------- # Install into ~/Applications/ThoughtSync.AppImage — the SAME location the app's # own self-integration uses (src/integration.rs) — so the running app sees # itself already installed and never makes a second copy or menu entry. say "Installing the de-bundled AppImage (user-local, no sudo)" [ -n "$appimage_url" ] || die "the $channel release (${version:-unknown}) has no AppImage asset." apps_dir="$HOME/Applications" dest="$apps_dir/ThoughtSync.AppImage" mkdir -p "$apps_dir" say "Downloading $(basename "$appimage_url")…" curl -fSL -o "$tmp/ThoughtSync.AppImage" "$appimage_url" chmod +x "$tmp/ThoughtSync.AppImage" mv -f "$tmp/ThoughtSync.AppImage" "$dest" # Menu entry — written to match integration.rs verbatim (same paths + fields), # so the app reports is_integrated=true and won't duplicate it. apps_menu="$HOME/.local/share/applications" icons_dir="$HOME/.local/share/icons" mkdir -p "$apps_menu" "$icons_dir" # Best-effort: pull the real icon out of the AppImage (.DirIcon) so the menu # entry looks right immediately. Extraction is a non-GUI unsquash (no FUSE, no # black-window risk); if it fails we fall back to the themed name and the app # writes its embedded icon on first launch anyway. icon_ref="thoughtsync" if ( cd "$tmp" && "$dest" --appimage-extract .DirIcon >/dev/null 2>&1 ) \ && cp -L "$tmp/squashfs-root/.DirIcon" "$icons_dir/thoughtsync.png" 2>/dev/null; then icon_ref="$icons_dir/thoughtsync.png" fi rm -rf "$tmp/squashfs-root" 2>/dev/null || true cat > "$apps_menu/thoughtsync.desktop" </dev/null 2>&1 || true # Convenience CLI launcher. mkdir -p "$HOME/.local/bin" ln -sf "$dest" "$HOME/.local/bin/thoughtsync" say "Installed to $dest" printf ' Launch it from your application menu, or run \033[1mthoughtsync\033[0m' printf ' (if ~/.local/bin is on your PATH).\n' # The AppImage CAN update itself, but the app's own channel is a separate, # stable-by-default setting — installing from dev here does not move it. if [ "$channel" = "dev" ]; then printf ' You installed from the \033[1mdev\033[0m channel — set the app to match in\n' printf ' Sync → App updates → Development, or in-app updates will follow stable.\n' fi