#!/usr/bin/env bash # ────────────────────────────────────────────────────────────────────────── # desktop-prod.sh — Build & launch OmniVoice Studio as a "fresh install" # # This gives you the EXACT same experience as a user downloading the # installer (DMG on macOS, AppImage on Linux): # • Full Rust bootstrap (venv creation, uv sync, model setup) # • Splash screen with live logs # • Region selector, version badge, etc. # # Usage: # bun desktop-prod # build debug + wipe + launch # bun desktop-prod:run # re-launch last build (skip compile) # bun desktop-prod:upgrade # rebuild, but keep data (test upgrade) # ────────────────────────────────────────────────────────────────────────── set -euo pipefail APP_ID="com.debpalash.omnivoice-studio" TAURI_DIR="frontend/src-tauri" APP_NAME="OmniVoice Studio" # ── Detect platform ─────────────────────────────────────────────────────── OS="$(uname -s)" case "$OS" in Darwin) PLATFORM="macos" ;; Linux) PLATFORM="linux" ;; *) echo "❌ Unsupported platform: $OS"; exit 1 ;; esac # ── Platform-specific paths ─────────────────────────────────────────────── # Two directories matter for fresh-install simulation: # APP_DATA — Tauri's bundle dir (keyed by APP_ID); holds the post-install # Python venv + webview state. # BACKEND_DATA — Where backend/core/config.py::get_app_data_dir() writes: # SQLite db, voice profiles, generation outputs, logs. This is # NOT under APP_ID — it's a separate hardcoded name. Cleaning # only APP_DATA leaves all user data behind, defeating the # fresh-emulation promise. if [ "$PLATFORM" = "macos" ]; then APP_DATA="$HOME/Library/Application Support/${APP_ID}" BACKEND_DATA="$HOME/Library/Application Support/OmniVoice" TAURI_LOGS="$HOME/Library/Logs/${APP_ID}" WEBKIT_DATA="$HOME/Library/WebKit/${APP_ID}" else # Linux: backend uses ~/.omnivoice (not XDG — see backend/core/config.py). APP_DATA="${XDG_DATA_HOME:-$HOME/.local/share}/${APP_ID}" BACKEND_DATA="$HOME/.omnivoice" TAURI_LOGS="${XDG_DATA_HOME:-$HOME/.local/share}/${APP_ID}/logs" WEBKIT_DATA="${XDG_DATA_HOME:-$HOME/.local/share}/${APP_ID}/webview" fi # HF cache — where downloaded models live HF_CACHE="${HF_HOME:-$HOME/.cache/huggingface}" # ── Flags ────────────────────────────────────────────────────────────────── SKIP_BUILD=false KEEP_DATA=false PILL_MODE=false for arg in "$@"; do case "$arg" in --skip-build) SKIP_BUILD=true ;; --keep-data) KEEP_DATA=true ;; --pill) PILL_MODE=true ;; -h|--help) echo "Usage: $0 [--skip-build] [--keep-data] [--pill]" echo "" echo " --skip-build Skip cargo build, use last compiled binary" echo " --keep-data Don't wipe app data (test upgrade path)" echo " --pill Launch in dictation-widget mode (no main window)" exit 0 ;; esac done # ── Wipe app data for fresh-install simulation ───────────────────────────── if [ "$KEEP_DATA" = false ]; then echo "🧹 Cleaning all OmniVoice data for fresh prod emulation..." echo "" # 1. App data (Tauri bundle dir: post-install venv + webview state) if [ -d "${APP_DATA}" ]; then echo " ✗ App data: ${APP_DATA}" rm -rf "${APP_DATA}" else echo " ○ App data: (already clean)" fi # 1b. Backend data (SQLite db, voice profiles, outputs, logs) # — separate dir hardcoded in backend/core/config.py, NOT under APP_ID. if [ -d "${BACKEND_DATA}" ]; then BD_SIZE=$(du -sh "${BACKEND_DATA}" 2>/dev/null | cut -f1) echo " ✗ Backend data: ${BACKEND_DATA} (${BD_SIZE})" rm -rf "${BACKEND_DATA}" else echo " ○ Backend data: (already clean)" fi # 2. HF model cache (downloaded .safetensors, tokenizers, etc.) if [ -d "${HF_CACHE}" ]; then HF_SIZE=$(du -sh "${HF_CACHE}" 2>/dev/null | cut -f1) echo " ✗ HF cache: ${HF_CACHE} (${HF_SIZE})" rm -rf "${HF_CACHE}" else echo " ○ HF cache: (already clean)" fi # 3. Tauri log dir if [ -d "${TAURI_LOGS}" ]; then echo " ✗ Tauri logs: ${TAURI_LOGS}" rm -rf "${TAURI_LOGS}" else echo " ○ Tauri logs: (already clean)" fi # 4. WebView cache / local storage if [ -d "${WEBKIT_DATA}" ]; then echo " ✗ WebKit data: ${WEBKIT_DATA}" rm -rf "${WEBKIT_DATA}" else echo " ○ WebKit data: (already clean)" fi echo "" echo " ✅ All clean — next launch bootstraps from zero." else echo "📦 Keeping existing app data (upgrade test mode)" fi # ── Build debug binary ───────────────────────────────────────────────────── if [ "$SKIP_BUILD" = false ]; then echo "" echo "🔨 Building debug bundle (this takes 1-3 min first time)..." # Remove stale bundle so we never accidentally launch old code if [ "$PLATFORM" = "macos" ]; then APP_BUNDLE="${TAURI_DIR}/target/debug/bundle/macos/${APP_NAME}.app" [ -d "$APP_BUNDLE" ] && rm -rf "$APP_BUNDLE" fi # Linux: linuxdeploy uses FUSE to mount itself; if FUSE is unavailable # (containers, some hardened kernels), set APPIMAGE_EXTRACT_AND_RUN=1 to # extract-and-run instead. Safe to always set on Linux. if [ "$PLATFORM" = "linux" ]; then export APPIMAGE_EXTRACT_AND_RUN=1 fi # The build creates the bundle successfully, but then may fail trying # to sign the updater artifact (no TAURI_SIGNING_PRIVATE_KEY) or to # run linuxdeploy. The binary itself is fine — tolerate known errors. BUILD_LOG=$(mktemp) cd frontend set +e bunx tauri build --debug 2>&1 | tee "$BUILD_LOG" BUILD_EXIT=$? set -e cd .. if [ $BUILD_EXIT -ne 0 ]; then # Known-harmless failures: # - Missing TAURI_SIGNING_PRIVATE_KEY (updater signing) # - "failed to run linuxdeploy" (AppImage bundling — binary still works) if grep -qi "TAURI_SIGNING_PRIVATE_KEY\|private key\|failed to run linuxdeploy\|failed to bundle" "$BUILD_LOG"; then echo "⚠️ Non-fatal bundle error — binary is fine (see above for details)." else echo "❌ Build failed with exit code $BUILD_EXIT" rm -f "$BUILD_LOG" exit $BUILD_EXIT fi fi rm -f "$BUILD_LOG" echo "✅ Build complete." else echo "⏭️ Skipping build (--skip-build)" fi # ── Build launch args ────────────────────────────────────────────────────── LAUNCH_ARGS=() if [ "$PILL_MODE" = true ]; then LAUNCH_ARGS+=("--pill") echo "📌 Launch mode: pill (dictation-only widget, no main window)" fi # ── Find and launch the app ──────────────────────────────────────────────── if [ "$PLATFORM" = "macos" ]; then APP_BUNDLE="${TAURI_DIR}/target/debug/bundle/macos/${APP_NAME}.app" BINARY="${TAURI_DIR}/target/debug/omnivoice-studio" if [ -d "$APP_BUNDLE" ]; then echo "" echo "🚀 Launching ${APP_NAME} (.app bundle)..." echo " Bundle: ${APP_BUNDLE}" # macOS `open` needs -n to spawn a fresh instance, --args to forward flags. if [ ${#LAUNCH_ARGS[@]} -gt 0 ]; then open -n "$APP_BUNDLE" --args "${LAUNCH_ARGS[@]}" else open "$APP_BUNDLE" fi elif [ -f "$BINARY" ]; then echo "" echo "🚀 Launching ${APP_NAME} (raw binary — no .app bundle)..." echo " Binary: ${BINARY}" "$BINARY" "${LAUNCH_ARGS[@]}" & else echo "❌ No bundle or binary found. Run without --skip-build first." exit 1 fi else # Linux: prefer AppImage, fall back to raw binary APPIMAGE=$(find "${TAURI_DIR}/target/debug/bundle/appimage" -name "*.AppImage" -type f 2>/dev/null | head -1) BINARY="${TAURI_DIR}/target/debug/omnivoice-studio" if [ -n "$APPIMAGE" ] && [ -f "$APPIMAGE" ]; then echo "" echo "🚀 Launching ${APP_NAME} (AppImage)..." echo " AppImage: ${APPIMAGE}" chmod +x "$APPIMAGE" "$APPIMAGE" "${LAUNCH_ARGS[@]}" & elif [ -f "$BINARY" ]; then echo "" echo "🚀 Launching ${APP_NAME} (raw binary)..." echo " Binary: ${BINARY}" "$BINARY" "${LAUNCH_ARGS[@]}" & else echo "❌ No AppImage or binary found. Run without --skip-build first." exit 1 fi fi echo " App data: ${APP_DATA}" echo "" echo "✅ App launched. Check the splash screen for bootstrap logs." if [ "$PILL_MODE" = true ]; then echo " To re-run pill mode without rebuilding: bun desktop-prod:run:pill" echo " To switch back to studio: bun desktop-prod:run" else echo " To re-run without rebuilding: bun desktop-prod:run" echo " To launch as dictation widget: bun desktop-prod:pill" fi