#!/bin/sh # Kazzle Installer — macOS and Linux # Usage: # curl -fsSL https://install.kazzle.app | sh # curl -fsSL https://install.kazzle.app | sh -s -- --token abc123 # # Downloads the full Kazzle app bundle, extracts the daemon binary, # and registers the daemon as an OS service. # # Idempotent: if the installed version matches the latest, exits early. set -e # Source of truth: shared/types/auth.types.ts → DOWNLOAD_URL 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"; } 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 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 } # Fetch latest version manifest MANIFEST_URL="$INSTALL_BASE_URL/latest.json" log "Fetching manifest from $MANIFEST_URL..." MANIFEST=$(fetch "$MANIFEST_URL") # Map OS to manifest key MANIFEST_KEY="linux" if [ "$OS" = "macos" ]; then MANIFEST_KEY="mac"; fi # Extract version + filename from manifest JSON 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 # ─── Idempotent check: skip if already installed and up to date ─── 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 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 log "Already installed and up to date (v$INSTALLED_VERSION). Nothing to do." exit 0 fi if [ -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..." # ─── macOS: DMG → mount → copy Kazzle.app → find daemon inside ─── if [ "$OS" = "macos" ]; then INSTALL_DIR="/Applications" BUNDLE_NAME="Kazzle.app" TMP_FILE=$(mktemp /tmp/kazzle-install.XXXXXX.dmg) download "$DOWNLOAD_URL" "$TMP_FILE" log "Mounting DMG and copying Kazzle.app..." MOUNT_DIR=$(mktemp -d /tmp/kazzle-mount.XXXXXX) hdiutil attach "$TMP_FILE" -mountpoint "$MOUNT_DIR" -nobrowse -quiet cp -R "$MOUNT_DIR/$BUNDLE_NAME" "$INSTALL_DIR/$BUNDLE_NAME" hdiutil detach "$MOUNT_DIR" -quiet rm -f "$TMP_FILE" rmdir "$MOUNT_DIR" 2>/dev/null || true # Save app path so daemon can find it KAZZLE_HOME="${KAZZLE_HOME:-$HOME/.kazzle}" 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_BIN="$DAEMON_APP/Contents/MacOS/kazzle-daemon" STANDALONE_JS="$DAEMON_APP/Contents/Resources/standalone.js" fi # ─── Linux: AppImage → extract → copy daemon out + symlink app binary ─── if [ "$OS" = "linux" ]; then INSTALL_DIR="/opt/kazzle" APPIMAGE_PATH="$INSTALL_DIR/Kazzle.AppImage" 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" # Symlink so app-launcher can find the app at /opt/kazzle/kazzle 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 "$INSTALL_DIR/kazzle-daemon" cp -R squashfs-root/resources/kazzle-daemon "$INSTALL_DIR/kazzle-daemon" cd / rm -rf "$EXTRACT_DIR" # Write version file for idempotent checks if [ -n "$LATEST_VERSION" ]; then echo "$LATEST_VERSION" > "$INSTALL_DIR/VERSION" fi # Save app path so daemon can find it (KAZZLE_HOME defaults to /blaxel on Linux) KAZZLE_HOME="${KAZZLE_HOME:-/blaxel}" mkdir -p "$KAZZLE_HOME" echo "$INSTALL_DIR/kazzle" > "$KAZZLE_HOME/app-path" chmod 600 "$KAZZLE_HOME/app-path" DAEMON_DIR="$INSTALL_DIR/kazzle-daemon" DAEMON_BIN="$DAEMON_DIR/bin/kazzle-daemon" STANDALONE_JS="$DAEMON_DIR/lib/standalone.js" fi if [ ! -f "$DAEMON_BIN" ]; then echo "Error: daemon binary not found at $DAEMON_BIN" exit 1 fi if [ ! -f "$STANDALONE_JS" ]; then echo "Error: standalone.js not found at $STANDALONE_JS" exit 1 fi chmod +x "$DAEMON_BIN" log "Daemon binary: $DAEMON_BIN" # Register as OS service 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