#!/usr/bin/env bash # Daemora installer — one-shot setup for non-technical users. # # What this does, in order: # 1. Detects Node.js; installs 22 LTS via the platform package manager # if missing or too old (Homebrew on macOS; apt/dnf/pacman on Linux). # 2. Installs the `daemora` npm package globally so the `daemora` CLI # is on PATH. # 3. Drops a small launcher script at ~/.daemora/bin/daemora-launcher # that starts the agent and waits for the HTTP port before opening # the browser. # 4. Creates a desktop shortcut so the user can launch Daemora from # Applications / their app menu — `Daemora.app` on macOS, # `~/.local/share/applications/daemora.desktop` on Linux. # 5. Runs the launcher so the dashboard opens immediately on first # install. # # First run: the launcher hits `daemora start`, which runs the existing # setup wizard if config isn't done. Subsequent runs: the launcher just # opens the browser (since the agent is already running) or restarts it # if not. # # Usage: curl -fsSL | bash # Env: DAEMORA_PORT (default 8081) set -euo pipefail DAEMORA_PORT="${DAEMORA_PORT:-8081}" LOG_PREFIX="[daemora-install]" log() { printf "%s %s\n" "$LOG_PREFIX" "$*"; } err() { printf "%s ERROR: %s\n" "$LOG_PREFIX" "$*" >&2; } fail() { err "$*"; exit 1; } OS="$(uname -s)" case "$OS" in Darwin) PLATFORM=macos ;; Linux) PLATFORM=linux ;; *) fail "Unsupported OS: $OS. macOS and Linux are supported. Windows users: install.ps1." ;; esac # ── 1. Node.js ────────────────────────────────────────────────────────── need_node() { if ! command -v node >/dev/null 2>&1; then return 0; fi local major major=$(node -p "process.versions.node.split('.')[0]" 2>/dev/null || echo 0) [ "$major" -lt 22 ] } install_node_macos() { if ! command -v brew >/dev/null 2>&1; then log "Homebrew not found — installing it (used to install Node.js)..." /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" fi log "installing Node.js 22 via Homebrew..." brew install node@22 || brew install node brew link --overwrite --force node@22 2>/dev/null || true } install_node_linux() { log "installing Node.js 22 via your package manager..." if command -v apt-get >/dev/null 2>&1; then curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - sudo apt-get install -y nodejs elif command -v dnf >/dev/null 2>&1; then curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo -E bash - sudo dnf install -y nodejs elif command -v pacman >/dev/null 2>&1; then sudo pacman -Sy --noconfirm nodejs npm else fail "No supported package manager found (apt/dnf/pacman). Install Node.js 22+ from https://nodejs.org and rerun." fi } if need_node; then log "Node.js 22+ not found — installing..." if [ "$PLATFORM" = macos ]; then install_node_macos; else install_node_linux; fi else log "Node.js $(node -v) detected — ok." fi # Some package-manager installs don't put npm on PATH for the current # shell. Re-source common profile files so `npm` is available below. hash -r 2>/dev/null || true if ! command -v npm >/dev/null 2>&1; then fail "npm is not on PATH after installing Node. Open a new terminal and rerun the installer." fi # ── 2. daemora — global npm install ───────────────────────────────────── log "installing Daemora (this may take a minute)..." # Use sudo on Linux when npm's global prefix is a system path the user # can't write to. macOS Homebrew prefixes are user-writable. NPM_PREFIX="$(npm config get prefix 2>/dev/null || echo "")" if [ "$PLATFORM" = linux ] && [ -n "$NPM_PREFIX" ] && [ ! -w "$NPM_PREFIX" ]; then sudo npm install -g daemora else npm install -g daemora fi if ! command -v daemora >/dev/null 2>&1; then fail "Daemora installed but the \`daemora\` command isn't on PATH. Try opening a new terminal." fi # ── 3. Launcher ───────────────────────────────────────────────────────── DAEMORA_HOME="$HOME/.daemora" mkdir -p "$DAEMORA_HOME/logs" "$DAEMORA_HOME/bin" LAUNCHER="$DAEMORA_HOME/bin/daemora-launcher" cat > "$LAUNCHER" </dev/null else xdg-open "\$URL" 2>/dev/null || true fi } # If something is already serving on the port, just open the browser. if curl -sf "\$URL" >/dev/null 2>&1; then open_url exit 0 fi # Start daemora detached, log to ~/.daemora/logs/daemora.log. mkdir -p "\$(dirname "\$LOG")" ( daemora start ) >> "\$LOG" 2>&1 & PID=\$! # Wait up to 30s for the HTTP port to come up. for _ in \$(seq 1 30); do if curl -sf "\$URL" >/dev/null 2>&1; then break; fi sleep 1 done open_url # Keep the launcher attached so the .app process isn't orphaned — # closing the launcher stops the agent. wait \$PID EOF chmod +x "$LAUNCHER" # ── 4. Desktop shortcut ───────────────────────────────────────────────── # write_macos_app PATH — build the .app bundle at PATH. Caller picks # the location (/Applications or ~/Applications). Returns 0 on success. write_macos_app() { local target="$1" mkdir -p "$target/Contents/MacOS" "$target/Contents/Resources" || return 1 cp "$LAUNCHER" "$target/Contents/MacOS/Daemora" || return 1 chmod +x "$target/Contents/MacOS/Daemora" || return 1 cat > "$target/Contents/Info.plist" < CFBundleNameDaemora CFBundleDisplayNameDaemora CFBundleExecutableDaemora CFBundleIdentifierai.daemora.app CFBundlePackageTypeAPPL CFBundleVersion1.0 CFBundleShortVersionString1.0 LSMinimumSystemVersion11.0 NSHighResolutionCapable LSUIElement EOF return 0 } if [ "$PLATFORM" = macos ]; then GLOBAL_APP="/Applications/Daemora.app" USER_APP="$HOME/Applications/Daemora.app" INSTALLED_APP="" # Prefer /Applications because Launchpad indexes it. When piped # through `curl | bash` there's no TTY for sudo to prompt, so we # use osascript — it shows a GUI password dialog that works in # non-TTY shells (same trick Homebrew / Anthropic / Cursor use). if [ -w /Applications ]; then log "creating $GLOBAL_APP..." if write_macos_app "$GLOBAL_APP"; then INSTALLED_APP="$GLOBAL_APP" fi elif command -v osascript >/dev/null 2>&1; then log "creating $GLOBAL_APP — macOS will ask for your password (needed to write to /Applications)..." PLIST_TMP="$(mktemp -t daemora-plist)" cat > "$PLIST_TMP" <<'PLIST_EOF' CFBundleNameDaemora CFBundleDisplayNameDaemora CFBundleExecutableDaemora CFBundleIdentifierai.daemora.app CFBundlePackageTypeAPPL CFBundleVersion1.0 CFBundleShortVersionString1.0 LSMinimumSystemVersion11.0 NSHighResolutionCapable LSUIElement PLIST_EOF if osascript -e "do shell script \"mkdir -p '$GLOBAL_APP/Contents/MacOS' '$GLOBAL_APP/Contents/Resources' && cp '$LAUNCHER' '$GLOBAL_APP/Contents/MacOS/Daemora' && chmod +x '$GLOBAL_APP/Contents/MacOS/Daemora' && cp '$PLIST_TMP' '$GLOBAL_APP/Contents/Info.plist'\" with administrator privileges with prompt \"Daemora needs permission to install its app icon into /Applications.\"" >/dev/null 2>&1; then INSTALLED_APP="$GLOBAL_APP" else log "password prompt cancelled — falling back to ~/Applications (no admin needed)." fi rm -f "$PLIST_TMP" fi # Fallback path: ~/Applications/Daemora.app. Always user-writable, # zero sudo, indexed by Spotlight + Finder. Trade-off: Launchpad only # indexes /Applications, so the user-scoped .app won't appear there — # everywhere else does. if [ -z "$INSTALLED_APP" ]; then mkdir -p "$HOME/Applications" log "creating $USER_APP (no admin password needed)..." if write_macos_app "$USER_APP"; then INSTALLED_APP="$USER_APP" else err "failed to create the .app bundle. The launcher at $LAUNCHER still works — run it directly." fi fi # Force Spotlight to index the new bundle so 'Daemora' shows up # immediately in Spotlight search instead of a few minutes later. if [ -n "$INSTALLED_APP" ] && command -v mdimport >/dev/null 2>&1; then mdimport "$INSTALLED_APP" 2>/dev/null || true fi else DESKTOP="$HOME/.local/share/applications/daemora.desktop" mkdir -p "$(dirname "$DESKTOP")" log "creating $DESKTOP..." cat > "$DESKTOP" </dev/null 2>&1; then update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true fi fi # ── 5. Launch ─────────────────────────────────────────────────────────── log "all set. starting Daemora..." nohup "$LAUNCHER" >/dev/null 2>&1 & sleep 1 log "" log "Daemora is running at http://localhost:${DAEMORA_PORT}" log "Open it again any time from your apps menu (search \"Daemora\")."