#!/bin/sh set -e BASE="https://frontend.todofor.ai/latest" PREFIX="${TODOFORAI_PREFIX:-$HOME/.todoforai/app}" info() { printf "\033[36m::\033[0m %s\n" "$*"; } ok() { printf "\033[32m✓\033[0m %s\n" "$*"; } err() { printf "\033[31m✗\033[0m %s\n" "$*" >&2; exit 1; } OS="$(uname -s)" ARCH="$(uname -m)" case "$ARCH" in x86_64|amd64) ARCH_TAG="x64" ;; arm64|aarch64) ARCH_TAG="arm64" ;; *) err "Unsupported architecture: $ARCH" ;; esac # The artifacts under /latest are unversioned; latest.json carries the version. VERSION="$(curl -fsSL "$BASE/latest.json" 2>/dev/null | tr ',' '\n' | grep '"version"' | head -1 | cut -d'"' -f4)" [ -n "$VERSION" ] && VER_LABEL=" v$VERSION" || VER_LABEL="" case "$OS" in Darwin) DMG_URL="$BASE/todoforai-app-macos-$ARCH_TAG.dmg" TMP="$(mktemp -d)" MOUNT="$TMP/mount" mkdir -p "$MOUNT" trap 'hdiutil detach "$MOUNT" >/dev/null 2>&1 || true; rm -rf "$TMP"' EXIT info "Downloading TODOforAI$VER_LABEL for macOS ($ARCH_TAG)..." curl -fSL --progress-bar "$DMG_URL" -o "$TMP/todoforai.dmg" || err "Download failed: $DMG_URL" info "Mounting disk image..." hdiutil attach "$TMP/todoforai.dmg" -mountpoint "$MOUNT" -nobrowse -noverify -noautoopen >/dev/null || err "Failed to mount DMG" APP="$(find "$MOUNT" -maxdepth 1 -name '*.app' | head -1)" [ -n "$APP" ] || err "No .app found in DMG" NAME="$(basename "$APP")" if [ -w /Applications ] || [ ! -e /Applications ]; then APPDIR="/Applications"; else APPDIR="$HOME/Applications"; mkdir -p "$APPDIR" info "/Applications not writable — installing to $APPDIR" fi DEST="$APPDIR/$NAME" info "Installing $NAME to $APPDIR..." STAGE="$APPDIR/.$NAME.new"; rm -rf "$STAGE" cp -R "$APP" "$STAGE" || err "Copy failed" OLD="$APPDIR/.$NAME.old"; rm -rf "$OLD" [ -e "$DEST" ] && mv "$DEST" "$OLD" mv "$STAGE" "$DEST"; rm -rf "$OLD" xattr -dr com.apple.quarantine "$DEST" 2>/dev/null || true ok "Installed $NAME$VER_LABEL" info "Launching..." open "$DEST" || true ;; Linux) [ "$ARCH_TAG" = "x64" ] || err "Linux builds are x64 only (got $ARCH)" URL="$BASE/todoforai-app-linux-x64.AppImage" TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT info "Downloading TODOforAI$VER_LABEL for Linux ($ARCH_TAG)..." curl -fSL --progress-bar "$URL" -o "$TMP/todoforai.AppImage" || err "Download failed: $URL" info "Installing to $PREFIX..." mkdir -p "$PREFIX" mv "$TMP/todoforai.AppImage" "$PREFIX/todoforai" chmod +x "$PREFIX/todoforai" # Extract the app icon from the AppImage so the launcher entry can show it # (AppImages carry it as .DirIcon at the mount root). Best-effort only. ICON="" if ( cd "$TMP" && "$PREFIX/todoforai" --appimage-extract .DirIcon >/dev/null 2>&1 ) && [ -f "$TMP/squashfs-root/.DirIcon" ]; then cp "$TMP/squashfs-root/.DirIcon" "$PREFIX/todoforai.png" && ICON="$PREFIX/todoforai.png" rm -rf "$TMP/squashfs-root" fi # Symlink onto PATH so `todoforai` launches it; fall back to a note if no # writable bin dir exists. for d in "$HOME/.local/bin" "$HOME/bin"; do if mkdir -p "$d" 2>/dev/null && [ -w "$d" ]; then ln -sf "$PREFIX/todoforai" "$d/todoforai"; BIN="$d"; break fi done # Desktop entry so it shows in the app launcher. Distinct filename from the # app's runtime todoforai:// protocol registration (which writes a hidden # NoDisplay entry) so they don't clobber each other. Exec is quoted for # prefixes with spaces. APPS="$HOME/.local/share/applications"; mkdir -p "$APPS" cat > "$APPS/todoforai-app.desktop" </dev/null 2>&1 & ;; *) err "Unsupported OS: $OS (Windows: irm https://todofor.ai/app.ps1 | iex)" ;; esac