#!/bin/sh set -e INSTALL_BASE_URL="${KAZZLE_INSTALL_URL:-https://download.kazzle.com}" TOKEN="" while [ $# -gt 0 ]; do case "$1" in --token) TOKEN="$2"; shift 2 ;; *) shift ;; esac done log() { echo "[kazzle] $1"; } validate_daemon_payload() { PAYLOAD_DIR="$1" DAEMON_BIN_PATH="$2" STANDALONE_JS_PATH="$3" if [ ! -f "$DAEMON_BIN_PATH" ] || [ ! -f "$STANDALONE_JS_PATH" ]; then return 1 fi chmod +x "$DAEMON_BIN_PATH" 2>/dev/null || true "$DAEMON_BIN_PATH" "$STANDALONE_JS_PATH" --validate-payload "$PAYLOAD_DIR" >/dev/null 2>&1 } swap_payload_dir() { STAGE_DIR="$1" LIVE_DIR="$2" OLD_DIR="$3" rm -rf "$OLD_DIR" if [ -d "$LIVE_DIR" ]; then mv "$LIVE_DIR" "$OLD_DIR" fi if mv "$STAGE_DIR" "$LIVE_DIR"; then rm -rf "$OLD_DIR" return 0 fi rm -rf "$LIVE_DIR" if [ -d "$OLD_DIR" ]; then mv "$OLD_DIR" "$LIVE_DIR" fi return 1 } SKIP_DOWNLOAD=0 detect_os() { case "$(uname -s)" in Darwin) echo "macos" ;; Linux) echo "linux" ;; *) echo "unsupported" ;; esac } detect_arch() { case "$(uname -m)" in x86_64|amd64) echo "x64" ;; arm64|aarch64) echo "arm64" ;; *) echo "unsupported" ;; esac } OS=$(detect_os) ARCH=$(detect_arch) if [ "$OS" = "unsupported" ] || [ "$ARCH" = "unsupported" ]; then echo "Error: unsupported platform $(uname -s) $(uname -m)" echo "Kazzle supports macOS (x64/arm64) and Linux (x64/arm64)." exit 1 fi if [ "$OS" = "linux" ] && [ "$(id -u)" -ne 0 ]; then log "Root permissions are needed to install Kazzle. Don't worry — the app itself won't have root permissions." SELF_SCRIPT=$(mktemp /tmp/kazzle-install-self.XXXXXX.sh) if [ -f "$0" ] && [ "$0" != "sh" ] && [ "$0" != "/bin/sh" ]; then cp "$0" "$SELF_SCRIPT" else if command -v curl >/dev/null 2>&1; then curl -fsSL "${KAZZLE_INSTALL_URL:-https://install.kazzle.app}" > "$SELF_SCRIPT" else wget -qO "$SELF_SCRIPT" "${KAZZLE_INSTALL_URL:-https://install.kazzle.app}" fi fi exec sudo sh "$SELF_SCRIPT" "$@" fi log "Detected: $OS $ARCH" fetch() { if command -v curl >/dev/null 2>&1; then curl -fsSL "$@" elif command -v wget >/dev/null 2>&1; then wget -qO- "$@" else echo "Error: curl or wget required" exit 1 fi } download() { if command -v curl >/dev/null 2>&1; then curl -fsSL -o "$2" "$1" else wget -qO "$2" "$1" fi } MANIFEST_URL="$INSTALL_BASE_URL/latest.json" log "Fetching manifest from $MANIFEST_URL..." MANIFEST=$(fetch "$MANIFEST_URL") MANIFEST_KEY="linux" if [ "$OS" = "macos" ]; then MANIFEST_KEY="mac"; fi LATEST_VERSION=$(echo "$MANIFEST" | grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"') FILENAME=$(echo "$MANIFEST" | grep -o "\"$MANIFEST_KEY\"[[:space:]]*:[[:space:]]*{[^}]*}" | grep -o '"filename"[[:space:]]*:[[:space:]]*"[^"]*"' | grep -o '"[^"]*"$' | tr -d '"') if [ -z "$FILENAME" ]; then echo "Error: no download found for $MANIFEST_KEY in manifest" exit 1 fi VERSION_FILE="" if [ "$OS" = "linux" ]; then VERSION_FILE="/opt/kazzle/VERSION"; fi if [ "$OS" = "macos" ]; then VERSION_FILE="/Applications/Kazzle.app/Contents/Info.plist"; fi LIVE_DAEMON_DIR="" LIVE_DAEMON_BIN="" LIVE_STANDALONE_JS="" if [ "$OS" = "linux" ]; then LIVE_DAEMON_DIR="/opt/kazzle/kazzle-daemon" LIVE_DAEMON_BIN="$LIVE_DAEMON_DIR/bin/kazzle-daemon" LIVE_STANDALONE_JS="$LIVE_DAEMON_DIR/lib/standalone.js" fi if [ "$OS" = "macos" ]; then LIVE_DAEMON_APP="/Applications/Kazzle.app/Contents/Resources/Kazzle Daemon.app" LIVE_DAEMON_DIR="$LIVE_DAEMON_APP/Contents" LIVE_DAEMON_BIN="$LIVE_DAEMON_APP/Contents/MacOS/kazzle-daemon" LIVE_STANDALONE_JS="$LIVE_DAEMON_APP/Contents/Resources/standalone.js" fi if [ -n "$LATEST_VERSION" ] && [ -n "$VERSION_FILE" ] && [ -f "$VERSION_FILE" ]; then INSTALLED_VERSION="" if [ "$OS" = "linux" ]; then INSTALLED_VERSION=$(cat "$VERSION_FILE" 2>/dev/null || echo "") elif [ "$OS" = "macos" ]; then INSTALLED_VERSION=$(grep -A1 CFBundleShortVersionString "$VERSION_FILE" 2>/dev/null | grep string | sed 's/.*\(.*\)<\/string>.*/\1/' || echo "") fi if [ "$INSTALLED_VERSION" = "$LATEST_VERSION" ]; then if validate_daemon_payload "$LIVE_DAEMON_DIR" "$LIVE_DAEMON_BIN" "$LIVE_STANDALONE_JS"; then if [ -n "$TOKEN" ]; then log "Already installed and up to date (v$INSTALLED_VERSION). Skipping download and ensuring daemon is running." SKIP_DOWNLOAD=1 else log "Already installed and up to date (v$INSTALLED_VERSION). Nothing to do." exit 0 fi else log "Installed version matches v$INSTALLED_VERSION, but daemon payload is broken. Repairing." fi fi if [ "$SKIP_DOWNLOAD" != "1" ] && [ -n "$INSTALLED_VERSION" ]; then log "Updating from v$INSTALLED_VERSION to v$LATEST_VERSION..." fi fi DOWNLOAD_URL="${INSTALL_BASE_URL}/download/${FILENAME}" log "Downloading from $DOWNLOAD_URL..." if [ "$OS" = "macos" ]; then INSTALL_DIR="/Applications" BUNDLE_NAME="Kazzle.app" LIVE_APP="$INSTALL_DIR/$BUNDLE_NAME" STAGE_APP="$INSTALL_DIR/$BUNDLE_NAME.new" OLD_APP="$INSTALL_DIR/$BUNDLE_NAME.old" if [ "$SKIP_DOWNLOAD" != "1" ]; then TMP_FILE=$(mktemp /tmp/kazzle-install.XXXXXX.dmg) download "$DOWNLOAD_URL" "$TMP_FILE" log "Mounting DMG and staging Kazzle.app..." MOUNT_DIR=$(mktemp -d /tmp/kazzle-mount.XXXXXX) hdiutil attach "$TMP_FILE" -mountpoint "$MOUNT_DIR" -nobrowse -quiet rm -rf "$STAGE_APP" "$OLD_APP" cp -R "$MOUNT_DIR/$BUNDLE_NAME" "$STAGE_APP" STAGE_DAEMON_APP="$STAGE_APP/Contents/Resources/Kazzle Daemon.app" STAGE_DAEMON_DIR="$STAGE_DAEMON_APP/Contents" STAGE_DAEMON_BIN="$STAGE_DAEMON_APP/Contents/MacOS/kazzle-daemon" STAGE_STANDALONE_JS="$STAGE_DAEMON_APP/Contents/Resources/standalone.js" if ! validate_daemon_payload "$STAGE_DAEMON_DIR" "$STAGE_DAEMON_BIN" "$STAGE_STANDALONE_JS"; then hdiutil detach "$MOUNT_DIR" -quiet rm -rf "$STAGE_APP" rm -f "$TMP_FILE" rmdir "$MOUNT_DIR" 2>/dev/null || true echo "Error: staged daemon payload failed validation" exit 1 fi hdiutil detach "$MOUNT_DIR" -quiet rm -f "$TMP_FILE" rmdir "$MOUNT_DIR" 2>/dev/null || true if ! swap_payload_dir "$STAGE_APP" "$LIVE_APP" "$OLD_APP"; then rm -rf "$STAGE_APP" echo "Error: failed to swap staged Kazzle.app into place" exit 1 fi fi KAZZLE_HOME="${KAZZLE_HOME:-$HOME/.kazzle}" export KAZZLE_HOME mkdir -p "$KAZZLE_HOME" echo "$INSTALL_DIR/$BUNDLE_NAME/Contents/MacOS/Kazzle" > "$KAZZLE_HOME/app-path" chmod 600 "$KAZZLE_HOME/app-path" DAEMON_APP="$INSTALL_DIR/$BUNDLE_NAME/Contents/Resources/Kazzle Daemon.app" DAEMON_DIR="$DAEMON_APP/Contents" DAEMON_BIN="$DAEMON_APP/Contents/MacOS/kazzle-daemon" STANDALONE_JS="$DAEMON_APP/Contents/Resources/standalone.js" fi if [ "$OS" = "linux" ] && ! command -v notify-send >/dev/null 2>&1; then log "Installing notification support..." if command -v apt-get >/dev/null 2>&1; then apt-get install -y libnotify-bin >/dev/null 2>&1 || true elif command -v dnf >/dev/null 2>&1; then dnf install -y libnotify >/dev/null 2>&1 || true elif command -v pacman >/dev/null 2>&1; then pacman -S --noconfirm libnotify >/dev/null 2>&1 || true elif command -v apk >/dev/null 2>&1; then apk add --no-cache libnotify >/dev/null 2>&1 || true fi fi if [ "$OS" = "linux" ]; then INSTALL_DIR="/opt/kazzle" APPIMAGE_PATH="$INSTALL_DIR/Kazzle.AppImage" STAGE_DAEMON_DIR="$INSTALL_DIR/kazzle-daemon.new" OLD_DAEMON_DIR="$INSTALL_DIR/kazzle-daemon.old" if [ -n "$TOKEN" ] && [ -f "$LIVE_DAEMON_BIN" ] && [ -f "$LIVE_STANDALONE_JS" ]; then log "Stopping existing daemon before refreshing Linux daemon creds..." pkill -f "$LIVE_DAEMON_BIN" 2>/dev/null || true "$LIVE_DAEMON_BIN" "$LIVE_STANDALONE_JS" --stop || true fi if [ "$SKIP_DOWNLOAD" != "1" ]; then TMP_FILE=$(mktemp /tmp/kazzle-install.XXXXXX.AppImage) download "$DOWNLOAD_URL" "$TMP_FILE" log "Installing AppImage to $INSTALL_DIR..." mkdir -p "$INSTALL_DIR" mv "$TMP_FILE" "$APPIMAGE_PATH" chmod +x "$APPIMAGE_PATH" ln -sf "$APPIMAGE_PATH" "$INSTALL_DIR/kazzle" log "Extracting daemon from AppImage..." EXTRACT_DIR=$(mktemp -d /tmp/kazzle-extract.XXXXXX) cd "$EXTRACT_DIR" "$APPIMAGE_PATH" --appimage-extract >/dev/null 2>&1 rm -rf "$STAGE_DAEMON_DIR" "$OLD_DAEMON_DIR" cp -R squashfs-root/resources/kazzle-daemon "$STAGE_DAEMON_DIR" STAGE_DAEMON_BIN="$STAGE_DAEMON_DIR/bin/kazzle-daemon" STAGE_STANDALONE_JS="$STAGE_DAEMON_DIR/lib/standalone.js" if ! validate_daemon_payload "$STAGE_DAEMON_DIR" "$STAGE_DAEMON_BIN" "$STAGE_STANDALONE_JS"; then cd / rm -rf "$STAGE_DAEMON_DIR" "$EXTRACT_DIR" echo "Error: staged daemon payload failed validation" exit 1 fi if ! swap_payload_dir "$STAGE_DAEMON_DIR" "$LIVE_DAEMON_DIR" "$OLD_DAEMON_DIR"; then cd / rm -rf "$STAGE_DAEMON_DIR" "$EXTRACT_DIR" echo "Error: failed to swap staged daemon into place" exit 1 fi cd / rm -rf "$EXTRACT_DIR" if [ -n "$LATEST_VERSION" ]; then echo "$LATEST_VERSION" > "$INSTALL_DIR/VERSION" fi fi KAZZLE_HOME="${KAZZLE_HOME:-$HOME/.kazzle}" export KAZZLE_HOME mkdir -p "$KAZZLE_HOME" echo "$INSTALL_DIR/kazzle" > "$KAZZLE_HOME/app-path" chmod 600 "$KAZZLE_HOME/app-path" DAEMON_DIR="$LIVE_DAEMON_DIR" DAEMON_BIN="$DAEMON_DIR/bin/kazzle-daemon" STANDALONE_JS="$DAEMON_DIR/lib/standalone.js" fi if ! validate_daemon_payload "$DAEMON_DIR" "$DAEMON_BIN" "$STANDALONE_JS"; then echo "Error: installed daemon payload failed validation" exit 1 fi chmod +x "$DAEMON_BIN" log "Daemon binary: $DAEMON_BIN" REGISTER_ARGS="--autostart" if [ -n "$TOKEN" ]; then REGISTER_ARGS="$REGISTER_ARGS --token $TOKEN" fi log "Registering daemon as OS service..." "$DAEMON_BIN" "$STANDALONE_JS" $REGISTER_ARGS log "Installation complete!" if [ -n "$TOKEN" ]; then log "Daemon is connecting to server with provided token." else log "Open Kazzle.app and log in to connect this computer." fi