Ctrl K
ComunidadGuías…

install.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
#
# SocialLive installer — Linux / macOS / Termux
#
#   curl -fsSL https://raw.githubusercontent.com/AxionAura/social-live/main/install.sh | bash
#
# Installs Node.js and FFmpeg if missing (prefers user-local runtimes, asks for
# sudo only when a system package is the cleanest option), clones SocialLive,
# builds it, installs a background service (systemd user service on Linux,
# launchd on macOS), and prints the dashboard URL.
#
# Read before running: this file lives in the repository and every step is visible.
#
# Flags:
#   --uninstall     remove SocialLive, its service and data
#   --no-service    install the app but skip the background service
#   --port N        dashboard port (default 3000)
#   --dir PATH      install location (default $HOME/.social-live)
#
# Environment overrides: SOCIAL_LIVE_DIR, APP_PORT, SKIP_SERVICE=1, DRY_RUN=1
set -euo pipefail
# ══ AxionInstaller v1 (auto-embedded — edit AxionInstaller/lib instead) ══
#!/usr/bin/env bash
# ══════════════════════════════════════════════════════════════════
#  AxionInstaller — bash UI library          v1.0 · AxionAura
#  Branded terminal UI for project installers.
#  Design tokens: indigo #6366f1 · violet #8b5cf6 · purple #a855f7 ·
#  fuchsia #d946ef · bg #06090f · fg #e6edf3 · muted #8b949e
#
#  Public API:
#    axion_init  <Project Name> [Tagline]
#    axion_step  "label"  <command args...>     (runs cmd, atom spinner)
#    axion_info / axion_ok / axion_warn / axion_fail "msg"
#    axion_banner                                (big atom + wordmark)
#    axion_done  "line1" ["line2" ...]           (summary card)
#  Rules:
#    • Call axion_init first. Non-TTY output falls back to plain logs
#      automatically (CI / piped output safe).
#    • Long work goes through axion_step — never bare echo during work.
# ══════════════════════════════════════════════════════════════════

# ── AxionInstaller palette (truecolor; falls back gracefully) ──
AX_C_INDIGO='\033[38;2;99;102;241m'
AX_C_VIOLET='\033[38;2;139;92;246m'
AX_C_PURPLE='\033[38;2;168;85;247m'
AX_C_FUCHSIA='\033[38;2;217;70;239m'
AX_C_FG='\033[38;2;230;237;243m'
AX_C_MUTED='\033[38;2;139;148;158m'
AX_C_OK='\033[38;2;63;185;80m'
AX_C_ERR='\033[38;2;248;81;73m'
AX_C_DIM='\033[38;2;48;54;61m'
AX_C_BOLD='\033[1m'
AX_RESET='\033[0m'

AX_PROJECT=""
AX_TTY=0

axion_init() {
  AX_PROJECT="${1:-AxionInstaller}"
  AX_TAGLINE="${2:-Open source by default. Free for everyone.}"
  if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then AX_TTY=1; fi
}

_ax() { if [ "$AX_TTY" = "1" ]; then printf '%b' "$1"; else printf '%b' "$2"; fi; }

axion_info() { _ax "${AX_C_VIOLET}●${AX_C_FG} $*${AX_RESET}\n" "● $*\n"; }
axion_ok()   { _ax "${AX_C_OK}✔${AX_C_FG} $*${AX_RESET}\n" "✔ $*\n"; }
axion_warn() { _ax "${AX_C_FUCHSIA}▲${AX_C_FG} $*${AX_RESET}\n" "▲ $*\n"; }
axion_fail() { _ax "${AX_C_ERR}✘${AX_C_FG} $*${AX_RESET}\n" "✘ $*\n"; }

# ── The atom: 6 frames, electrons orbiting three gradient orbitals ──
# Each frame is 5 lines; electrons ● ride the orbital ring.
AX_FRAMES=(
'    ·────────·
   ╱          ╲
  ●────┼──────●
   ╲          ╱
    ·────────·'
'    ·───●────·
   ╱     │    ╲
  ·─────┼─────●
   ╲    │    ╱
    ·────────·'
'    ·──────·─·
   ╱     │   ╲
  ·──────┼────●
   ╲    │    ╱
    ·──●─────·'
'    ·──────·─●
   ╱     │   ╲
  ·──────┼────·
   ╲    │    ╱
    ·──●─────·'
'    ·──●─────·
   ╱    │    ╲
  ·─────┼─────·
   ╲    │    ╱
    ·────────●·'
'    ·────────·
   ╱    ●    ╲
  ●─────┼─────·
   ╲    │    ╱
    ·────────·'
)
AX_FI=0

axion_atom_frame() {
  # gradient-lit frame: orbital dim, electrons in AxionAura gradient
  local f="${AX_FRAMES[$((AX_FI % ${#AX_FRAMES[@]}))]}"
  AX_FI=$((AX_FI + 1))
  _ax "${AX_C_DIM}${f//'─'/${AX_C_DIM}─${AX_C_DIM}}${AX_RESET}" "${f}\n"
}

# ── axion_step: run a command behind the orbiting atom ──
# Output is captured; on failure the last lines are shown (debuggable UX).
axion_step() {
  local label="$1"; shift
  local tmplog
  tmplog="$(mktemp "${TMPDIR:-/tmp}/axion-step.XXXXXX")"
  if [ "$AX_TTY" != "1" ]; then
    axion_info "$label"
    "$@" 2>&1 | tee "$tmplog"
    local rc=${PIPESTATUS[0]}
    rm -f "$tmplog"
    if [ "$rc" -eq 0 ]; then
      axion_ok "$label"
    else
      axion_fail "$label (exit $rc) — log above"
    fi
    return "$rc"
  fi
  "$@" >"$tmplog" 2>&1 &
  local pid=$! rc
  # প্রতি ইটারেশনে ঠিক ৭ লাইন ছাপা হয় (divider + ৫-লাইনের পরমাণু + label)
  # তাই ঠিক ৭ লাইনই মুছতে হয় — কম মুছলে frame গুলো স্তূপ হয়ে যায়
  local clr=$'\033[2K\033[1A\033[2K\033[1A\033[2K\033[1A\033[2K\033[1A\033[2K\033[1A\033[2K\033[1A\033[2K\r'
  local spin='⠋⠙⠹⠸⠼⠴⠦⠧' si=0
  printf '\033[?25l'                                  # hide cursor
  local frame="${AX_FRAMES[$((AX_FI % ${#AX_FRAMES[@]}))]}"; AX_FI=$((AX_FI + 1))
  printf '%b\n' "${AX_C_DIM}  ────────────────────────────${AX_RESET}"
  printf '%b\n' "$(printf '%b' "${AX_C_DIM}${frame}${AX_RESET}" | sed $'s/●/\033[38;2;168;85;247m●\033[38;2;48;54;61m/g')"
  printf '%b' "${AX_C_VIOLET}${spin:0:1}${AX_RESET} ${AX_C_FG}${label}${AX_RESET} ${AX_C_MUTED}…"
  while kill -0 "$pid" 2>/dev/null; do
    sleep 0.12
    local b="${spin:$((si % ${#spin})):1}"; si=$((si + 1))
    frame="${AX_FRAMES[$((AX_FI % ${#AX_FRAMES[@]}))]}"; AX_FI=$((AX_FI + 1))
    printf '%b' "$clr"
    printf '%b\n' "${AX_C_DIM}  ────────────────────────────${AX_RESET}"
    printf '%b\n' "$(printf '%b' "${AX_C_DIM}${frame}${AX_RESET}" | sed $'s/●/\033[38;2;168;85;247m●\033[38;2;48;54;61m/g')"
    printf '%b' "${AX_C_VIOLET}${b}${AX_RESET} ${AX_C_FG}${label}${AX_RESET} ${AX_C_MUTED}…"
  done
  wait "$pid"; rc=$?
  printf '%b' "$clr"
  printf '\033[?25h'
  if [ $rc -eq 0 ]; then
    axion_ok "$label"
  else
    axion_fail "$label (exit $rc)"
    axion_warn 'last output lines:'
    tail -n 12 "$tmplog" | sed 's/^/    /'
  fi
  rm -f "$tmplog"
  return $rc
}

# ── banner: gradient wordmark + atom ──
axion_banner() {
  if [ "$AX_TTY" != "1" ]; then
    printf '════════════════════════════════\n  %s — %s\n════════════════════════════════\n' \
      "$AX_PROJECT" "$AX_TAGLINE"
    return
  fi
  printf '%b' "
${AX_C_DIM}        ·────────·
       ╱    ${AX_C_PURPLE}●${AX_C_DIM}     ╲
      ${AX_C_INDIGO}●${AX_C_DIM}──────┼──────${AX_C_FUCHSIA}●${AX_C_DIM}
       ╲          ╱
        ·────────·${AX_RESET}
"
  printf '%b' "${AX_C_BOLD}${AX_C_INDIGO}  Axion${AX_C_PURPLE}Aura${AX_C_FG}  ${AX_RESET}${AX_C_MUTED}presents${AX_RESET}\n"
  printf '%b' "${AX_C_BOLD}${AX_C_FG}  $AX_PROJECT${AX_RESET}  ${AX_C_MUTED}$AX_TAGLINE${AX_RESET}\n\n"
}

axion_done() {
  printf '%b' "${AX_C_DIM}  ────────────────────────────${AX_RESET}\n"
  printf '%b' "${AX_C_OK}  ✔${AX_C_BOLD}${AX_C_FG}  $AX_PROJECT is ready${AX_RESET}\n"
  local line
  for line in "$@"; do
    printf '%b' "${AX_C_MUTED}  · ${AX_C_FG}$line${AX_RESET}\n"
  done
  printf '%b' "${AX_C_VIOLET}  ── AxionAura · Open source by default. Free for everyone. ──${AX_RESET}\n\n"
}

# ══ end AxionInstaller ══

# ────────────────────────── configuration ──────────────────────────
NODE_VERSION="22.14.0"          # pinned nodejs.org runtime used when the system Node is too old
REQUIRED_NODE_MAJOR=22
REQUIRED_NODE_MINOR=13

INSTALL_DIR="${SOCIAL_LIVE_DIR:-$HOME/.social-live}"
APP_PORT="${APP_PORT:-3000}"
SKIP_SERVICE="${SKIP_SERVICE:-0}"
DRY_RUN="${DRY_RUN:-0}"
DO_UNINSTALL=0

while [ $# -gt 0 ]; do
  case "$1" in
    --uninstall) DO_UNINSTALL=1 ;;
    --no-service) SKIP_SERVICE=1 ;;
    --port) APP_PORT="$2"; shift ;;
    --dir) INSTALL_DIR="$2"; shift ;;
    *) echo "Unknown option: $1 (supported: --uninstall, --no-service, --port N, --dir PATH)"; exit 1 ;;
  esac
  shift
done

# ────────────────────────── helpers ──────────────────────────
axion_init "SocialLive" "Self-hosted live streaming dashboard"
info()  { axion_info "$*"; }
warn()  { axion_warn "$*"; }
die()   { axion_fail "$*"; exit 1; }
have()  { command -v "$1" >/dev/null 2>&1; }
run()   { if [ "$DRY_RUN" = "1" ]; then info "(dry-run) $*"; else "$@"; fi; }

SUDO=""
if [ "$(id -u)" -ne 0 ]; then
  if have sudo; then SUDO="sudo"; fi
fi

# ────────────────────────── OS detection ──────────────────────────
OS="unknown"      # linux | macos | termux
PKG="none"        # apt | dnf | pacman | brew | termux | none

if [ -n "${TERMUX_VERSION:-}" ] || [ -n "${TERMUX_PREFIX:-}" ]; then
  OS="termux"; PKG="termux"
elif [ "$(uname -s)" = "Darwin" ]; then
  OS="macos"; have brew && PKG="brew"
elif [ "$(uname -s)" = "Linux" ]; then
  OS="linux"
  if have apt-get; then PKG="apt"
  elif have dnf; then PKG="dnf"
  elif have pacman; then PKG="pacman"
  fi
fi
[ "$OS" = "unknown" ] && die "Unsupported OS: $(uname -s). Supported: Linux, macOS, Termux."

ARCH="$(uname -m)"
case "$ARCH" in
  x86_64|aarch64|arm64) ;;
  *) warn "Unusual architecture '$ARCH' — dependency downloads may fail." ;;
esac

pkg_install() {
  case "$PKG" in
    termux) run pkg install -y "$@" ;;
    apt)    run $SUDO apt-get update && run $SUDO apt-get install -y "$@" ;;
    dnf)    run $SUDO dnf install -y "$@" ;;
    pacman) run $SUDO pacman -S --noconfirm "$@" ;;
    brew)   run brew install "$@" ;;
    *)      return 1 ;;
  esac
}

# ────────────────────────── uninstall path ──────────────────────────
if [ "$DO_UNINSTALL" = "1" ]; then
  info "Uninstalling SocialLive from $INSTALL_DIR"
  if [ "$OS" = "linux" ] && have systemctl; then
    run systemctl --user disable --now social-live.service || true
    run rm -f "$HOME/.config/systemd/user/social-live.service"
    run systemctl --user daemon-reload || true
  elif [ "$OS" = "macos" ]; then
    run launchctl bootout "gui/$(id -u)" "$HOME/Library/LaunchAgents/com.axionaura.social-live.plist" || true
    run rm -f "$HOME/Library/LaunchAgents/com.axionaura.social-live.plist"
  fi
  run rm -f "$HOME/.local/bin/social-live"
  run rm -rf "$INSTALL_DIR"
  info "Removed. Your dashboard data went with $INSTALL_DIR — the encryption key too."
  exit 0
fi

# ────────────────────────── Node.js ──────────────────────────
node_ok() {
  have node || return 1
  [ "$(node -p 'process.versions.node.split(".").map(Number).slice(0,2).reduce((a,b,i)=>a+b*[100,1][i],0)')" \
      -ge $((REQUIRED_NODE_MAJOR * 100 + REQUIRED_NODE_MINOR)) ] 2>/dev/null
}

RUNTIME_DIR="$INSTALL_DIR/runtime"

install_node_tarball() {
  case "$ARCH" in
    aarch64|arm64) NODE_ARCH="arm64" ;;
    *)             NODE_ARCH="x64" ;;
  esac
  case "$OS" in
    macos) NODE_OS="darwin" ;;
    *)     NODE_OS="linux"  ;;
  esac
  NODE_TARBALL="node-v${NODE_VERSION}-${NODE_OS}-${NODE_ARCH}.tar.gz"
  NODE_URL="https://nodejs.org/dist/v${NODE_VERSION}/${NODE_TARBALL}"
  info "Downloading Node.js v${NODE_VERSION} (${NODE_OS}-${NODE_ARCH}) → $RUNTIME_DIR"
  run mkdir -p "$RUNTIME_DIR"
  axion_step "Downloading Node.js runtime (portable)" fetch "$NODE_URL" "$RUNTIME_DIR/$NODE_TARBALL"
  run tar -xzf "$RUNTIME_DIR/$NODE_TARBALL" -C "$RUNTIME_DIR"
  run rm -f "$RUNTIME_DIR/$NODE_TARBALL"
  run ln -sfn "$RUNTIME_DIR/node-v${NODE_VERSION}-${NODE_OS}-${NODE_ARCH}/bin/node" "$RUNTIME_DIR/node"
  run ln -sfn "$RUNTIME_DIR/node-v${NODE_VERSION}-${NODE_OS}-${NODE_ARCH}/bin/npm"  "$RUNTIME_DIR/npm"
  run ln -sfn "$RUNTIME_DIR/node-v${NODE_VERSION}-${NODE_OS}-${NODE_ARCH}/bin/npx"  "$RUNTIME_DIR/npx"
  export PATH="$RUNTIME_DIR:$PATH"
}

if node_ok; then
  info "Node.js $(node --version) found — OK (needs >= ${REQUIRED_NODE_MAJOR}.${REQUIRED_NODE_MINOR})"
else
  if [ "$PKG" != "none" ] && [ "$OS" != "macos" ]; then
    # distro repos rarely ship Node >= 22; go straight to the official runtime
    install_node_tarball
  elif [ "$PKG" = "brew" ]; then
    pkg_install node || install_node_tarball
  else
    install_node_tarball
  fi
  node_ok || die "Node.js ${REQUIRED_NODE_MAJOR}.${REQUIRED_NODE_MINOR}+ could not be provisioned. Install it manually and re-run."
  info "Node.js $(node --version) provisioned at $RUNTIME_DIR"
fi

# ────────────────────────── FFmpeg ──────────────────────────
if [ -n "$SUDO" ]; then
  if sudo -n true 2>/dev/null; then
    info "Sudo: passwordless ✓"
  else
    info "System packages need your sudo password (asked once, cached ~15 min)"
    sudo -v || warn "Sudo failed — will fall back to user-local downloads where possible."
  fi
fi

if have ffmpeg; then
  info "FFmpeg $(ffmpeg -version 2>/dev/null | head -1 | cut -d' ' -f3) found — OK"
else
  case "$PKG" in
    none)   die "FFmpeg is missing and no package manager was found. Install FFmpeg, then re-run." ;;
    termux|brew)
            pkg_install ffmpeg || die "Could not install FFmpeg via $PKG. Install it manually and re-run." ;;
    *)
            if [ "$(id -u)" = "0" ] || sudo -n true 2>/dev/null; then
              axion_step "Installing FFmpeg (system package via $PKG)" pkg_install ffmpeg \
                || die "Could not install FFmpeg via $PKG. Install it manually and re-run."
            else
              warn "FFmpeg is missing and sudo needs a password."
              die "Run 'sudo $PKG install ffmpeg' first (or re-run this installer and enter your password)."
            fi ;;
  esac
  have ffmpeg || die "FFmpeg still not found — install it manually and re-run."
  info "FFmpeg installed."
fi

# ────────────────────────── downloads ──────────────────────────
# curl বা wget — যেটা পাওয়া যায়। দুটোই না থাকলে স্পষ্ট বার্তা।
fetch() { # $1 = URL, $2 = output file
  if have curl; then run curl -fsSL "$1" -o "$2"
  elif have wget; then run wget -qO "$2" "$1"
  else die "Neither curl nor wget was found. Install either one ('sudo $PKG install curl') and re-run."
  fi
}

# ────────────────────────── application ──────────────────────────
# git-মুক্ত: সরাসরি সোর্স tarball নামানো হয় (Windows installer-এর মতোই)।
# data/ (ডাটাবেজ, ভিডিও, encryption key) কখনোই ওভাররাইট হয় না।
INSTALLER_URL="https://raw.githubusercontent.com/AxionAura/social-live/main/install.sh"
SRC_URL="https://codeload.github.com/AxionAura/social-live/tar.gz/refs/heads/main"

if [ -f "$INSTALL_DIR/package.json" ]; then
  info "Existing installation found at $INSTALL_DIR — updating (your data stays safe)"
else
  info "Downloading SocialLive → $INSTALL_DIR"
  run mkdir -p "$INSTALL_DIR"
fi

SRC="$INSTALL_DIR/.src.tar.gz"
axion_step "Downloading SocialLive" fetch "$SRC_URL" "$SRC"
run tar -xzf "$SRC" -C "$INSTALL_DIR" --strip-components=1
run rm -f "$SRC"

axion_step 'Installing dependencies (npm ci)' sh -c "cd '$INSTALL_DIR' && npm ci --no-audit --no-fund"

axion_step 'Building dashboard and server' sh -c "cd '$INSTALL_DIR' && npm run build"

# .env: port only — session secret / encryption key are auto-generated by the
# server on first start and persisted under data/config/ (never committed).
if [ ! -f "$INSTALL_DIR/.env" ]; then
  run sh -c "printf 'APP_PORT=%s\nDATA_DIR=%s/data\n' '$APP_PORT' '$INSTALL_DIR' > '$INSTALL_DIR/.env'"
fi

# ────────────────────────── CLI shim ──────────────────────────
write_shim() {
  # quoted heredoc: কিছুই generation-এ expand হয় না — placeholder পরে sed হয়,
  # তাই set -u-তে unbound variable বা escape-জুয়ার কোনো সুযোগ নেই
  cat > "$1" <<'AXSHIM'
#!/usr/bin/env bash
# SocialLive control command (generated by install.sh)
export SOCIAL_LIVE_DIR="__DIR__"
export PATH="__RUNTIME__:$PATH"
cd "$SOCIAL_LIVE_DIR" || exit 1
set -a; [ -f .env ] && . ./.env; set +a
if [ "${1:-}" = "update" ]; then
  if have curl; then curl -fsSL "__INSTALLER__" | bash -s -- --dir "$SOCIAL_LIVE_DIR" ${APP_PORT:+--port "$APP_PORT"}
  elif have wget; then wget -qO- "__INSTALLER__" | bash -s -- --dir "$SOCIAL_LIVE_DIR" ${APP_PORT:+--port "$APP_PORT"}
  else echo "curl or wget is required for update"; exit 1; fi
  exit 0
fi
exec node scripts/social-live.mjs "$@"
AXSHIM
  sed -i "s|__DIR__|$INSTALL_DIR|; s|__RUNTIME__|$RUNTIME_DIR|; s|__INSTALLER__|$INSTALLER_URL|" "$1"
  chmod +x "$1"
}

BIN_DIR="$HOME/.local/bin"
[ "$OS" = "termux" ] && BIN_DIR="$PREFIX/bin"
run mkdir -p "$BIN_DIR"
if [ "$DRY_RUN" = "1" ]; then
  info "(dry-run) write CLI shim $BIN_DIR/social-live"
else
  write_shim "$BIN_DIR/social-live"
  info "Control command installed: social-live (start | stop | status | doctor | update)"
  case ":$PATH:" in
    *":$BIN_DIR:"*) ;;
    *) warn "$BIN_DIR is not on your PATH. Add it:  echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.$([ "$OS" = "macos" ] && echo zshrc || echo bashrc)" ;;
  esac
fi

# ────────────────────────── background service ──────────────────────────
NODE_BIN="$(command -v node)"

install_service_linux() {
  UNIT_DIR="$HOME/.config/systemd/user"
  run mkdir -p "$UNIT_DIR"
  if [ "$DRY_RUN" = "1" ]; then
    info "(dry-run) write $UNIT_DIR/social-live.service"
    cat <<EOF
[Unit]
Description=SocialLive streaming dashboard
After=network-online.target

[Service]
Type=simple
WorkingDirectory=$INSTALL_DIR
EnvironmentFile=$INSTALL_DIR/.env
ExecStart=$NODE_BIN apps/server/dist/index.js
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target
EOF
    return
  fi
  cat > "$UNIT_DIR/social-live.service" <<EOF
[Unit]
Description=SocialLive streaming dashboard
After=network-online.target

[Service]
Type=simple
WorkingDirectory=$INSTALL_DIR
EnvironmentFile=$INSTALL_DIR/.env
ExecStart=$NODE_BIN apps/server/dist/index.js
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target
EOF
  run systemctl --user daemon-reload
  run systemctl --user enable social-live.service
  # restart (not just --now): an already-running old server must pick up
  # the freshly built code — otherwise the dashboard serves stale versions
  run systemctl --user restart social-live.service
}

install_service_macos() {
  PLIST="$HOME/Library/LaunchAgents/com.axionaura.social-live.plist"
  run mkdir -p "$HOME/Library/LaunchAgents"
  if [ "$DRY_RUN" = "1" ]; then
    info "(dry-run) write $PLIST"; return
  fi
  cat > "$PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>com.axionaura.social-live</string>
  <key>WorkingDirectory</key><string>$INSTALL_DIR</string>
  <key>ProgramArguments</key>
  <array>
    <string>$NODE_BIN</string><string>apps/server/dist/index.js</string>
  </array>
  <key>EnvironmentVariables</key><dict>
    <key>APP_PORT</key><string>$APP_PORT</string>
    <key>DATA_DIR</key><string>$INSTALL_DIR/data</string>
  </dict>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
</dict></plist>
EOF
  run launchctl bootout "gui/$(id -u)" "$PLIST" 2>/dev/null || true
  run launchctl bootstrap "gui/$(id -u)" "$PLIST"
}

start_termux() {
  if [ "$DRY_RUN" = "1" ]; then
    info "(dry-run) write $INSTALL_DIR/start.sh"; return
  fi
  cat > "$INSTALL_DIR/start.sh" <<EOF
#!/usr/bin/env bash
cd "$INSTALL_DIR" && node apps/server/dist/index.js
EOF
  chmod +x "$INSTALL_DIR/start.sh"
  info "Termux has no system services — start SocialLive with:  $INSTALL_DIR/start.sh"
  if have termux-wake-lock; then
    info "Tip: run 'termux-wake-lock' so Android does not suspend the stream."
  fi
}

if [ "$SKIP_SERVICE" = "1" ]; then
  warn "Skipping background service (--no-service). Start manually:  social-live start"
else
  case "$OS" in
    linux)  install_service_linux
            if have systemctl && systemctl --user is-active --quiet social-live.service 2>/dev/null; then
              info "Service running (systemd user unit — restarts on failure, boots on login)."
              [ "$DRY_RUN" = "0" ] && have loginctl && ! loginctl show-user "$USER" 2>/dev/null | grep -q '^Linger=yes' && \
                warn "Optional: 'sudo loginctl enable-linger $USER' starts the dashboard even before you log in."
            else
              warn "Service installed but not active — start it later with:  systemctl --user start social-live"
            fi ;;
    macos)  install_service_macos; info "Service installed (launchd — runs at login, restarts on crash)." ;;
    termux) start_termux ;;
  esac
fi

# ────────────────────────── done ──────────────────────────
LAN_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
[ -z "${LAN_IP:-}" ] && [ "$OS" = "macos" ] && LAN_IP="$(ipconfig getifaddr en0 2>/dev/null || true)"

axion_done \
  "Dashboard:   http://localhost:$APP_PORT" \
  "Wi-Fi:       http://${LAN_IP:-<your-ip>}:$APP_PORT" \
  "Install dir: $INSTALL_DIR" \
  "Update:      social-live update" \
  "Keys:        $INSTALL_DIR/data/config/ — back this folder up"