install.sh 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883
  1. #!/usr/bin/env bash
  2. #
  3. # BamBuddy Native Installation Script
  4. # Supports: Debian/Ubuntu, RHEL/Fedora/CentOS, Arch Linux, macOS
  5. #
  6. # Usage:
  7. # Interactive: curl -fsSL https://raw.githubusercontent.com/maziggy/bambuddy/main/install/install.sh -o install.sh && chmod +x install.sh && ./install.sh
  8. # Unattended: ./install.sh --path /opt/bambuddy --port 8000 --yes
  9. #
  10. # Options:
  11. # --path PATH Installation directory (default: /opt/bambuddy)
  12. # --port PORT Port to listen on (default: 8000)
  13. # --bind ADDRESS Bind address: 0.0.0.0 (network) or 127.0.0.1 (local only)
  14. # --tz TIMEZONE Timezone (default: system timezone or UTC)
  15. # --data-dir PATH Data directory (default: INSTALL_PATH/data)
  16. # --log-dir PATH Log directory (default: INSTALL_PATH/logs)
  17. # --debug Enable debug mode
  18. # --log-level LEVEL Log level: DEBUG, INFO, WARNING, ERROR (default: INFO)
  19. # --no-service Skip systemd service setup (Linux only)
  20. # --set-system-tz Set system timezone to match (for unattended installs)
  21. # --yes, -y Non-interactive mode, accept defaults
  22. # --help, -h Show this help message
  23. #
  24. set -e
  25. # Colors for output
  26. RED='\033[0;31m'
  27. GREEN='\033[0;32m'
  28. YELLOW='\033[1;33m'
  29. BLUE='\033[0;34m'
  30. CYAN='\033[0;36m'
  31. NC='\033[0m' # No Color
  32. BOLD='\033[1m'
  33. # Default values
  34. DEFAULT_INSTALL_PATH="/opt/bambuddy"
  35. DEFAULT_PORT="8000"
  36. DEFAULT_BIND_ADDRESS="0.0.0.0"
  37. DEFAULT_LOG_LEVEL="INFO"
  38. DEFAULT_DEBUG="false"
  39. # Script variables
  40. INSTALL_PATH=""
  41. PORT=""
  42. BIND_ADDRESS=""
  43. TIMEZONE=""
  44. DATA_DIR=""
  45. LOG_DIR=""
  46. DEBUG_MODE=""
  47. LOG_LEVEL=""
  48. SKIP_SERVICE="false"
  49. SET_SYSTEM_TZ=""
  50. NON_INTERACTIVE="false"
  51. OS_TYPE=""
  52. PKG_MANAGER=""
  53. PYTHON_CMD=""
  54. SERVICE_USER="bambuddy"
  55. # -----------------------------------------------------------------------------
  56. # Helper Functions
  57. # -----------------------------------------------------------------------------
  58. print_banner() {
  59. echo -e "${CYAN}"
  60. echo "╔════════════════════════════════════════════════════════╗"
  61. echo "║ ║"
  62. echo "║ ____ _ _ _ ║"
  63. echo "║ | __ ) __ _ _ __ ___ | |__ _ _ __| | __| |_ _ ║"
  64. echo "║ | _ \\ / _\` | '_ \` _ \\| '_ \\| | | |/ _\` |/ _\` | | | | ║"
  65. echo "║ | |_) | (_| | | | | | | |_) | |_| | (_| | (_| | |_| | ║"
  66. echo "║ |____/ \\__,_|_| |_| |_|_.__/ \\__,_|\\__,_|\\__,_|\\__, | ║"
  67. echo "║ |___/ ║"
  68. echo "║ ║"
  69. echo "║ Native Installation Script ║"
  70. echo "║ ║"
  71. echo "╚════════════════════════════════════════════════════════╝"
  72. echo -e "${NC}"
  73. }
  74. log_info() {
  75. echo -e "${BLUE}[INFO]${NC} $1"
  76. }
  77. log_success() {
  78. echo -e "${GREEN}[OK]${NC} $1"
  79. }
  80. log_warn() {
  81. echo -e "${YELLOW}[WARN]${NC} $1"
  82. }
  83. log_error() {
  84. echo -e "${RED}[ERROR]${NC} $1"
  85. }
  86. prompt() {
  87. local prompt_text="$1"
  88. local default_value="$2"
  89. local var_name="$3"
  90. if [[ "$NON_INTERACTIVE" == "true" ]]; then
  91. eval "$var_name=\"$default_value\""
  92. return
  93. fi
  94. if [[ -n "$default_value" ]]; then
  95. echo -en "${BOLD}$prompt_text${NC} [${CYAN}$default_value${NC}]: "
  96. else
  97. echo -en "${BOLD}$prompt_text${NC}: "
  98. fi
  99. read -r input
  100. if [[ -z "$input" ]]; then
  101. eval "$var_name=\"$default_value\""
  102. else
  103. eval "$var_name=\"$input\""
  104. fi
  105. }
  106. prompt_yes_no() {
  107. local prompt_text="$1"
  108. local default="$2" # y or n
  109. if [[ "$NON_INTERACTIVE" == "true" ]]; then
  110. [[ "$default" == "y" ]] && return 0 || return 1
  111. fi
  112. local yn_hint="[y/n]"
  113. [[ "$default" == "y" ]] && yn_hint="[Y/n]"
  114. [[ "$default" == "n" ]] && yn_hint="[y/N]"
  115. while true; do
  116. echo -en "${BOLD}$prompt_text${NC} $yn_hint: "
  117. read -r yn
  118. [[ -z "$yn" ]] && yn="$default"
  119. case "$yn" in
  120. [Yy]* ) return 0;;
  121. [Nn]* ) return 1;;
  122. * ) echo "Please answer yes or no.";;
  123. esac
  124. done
  125. }
  126. show_help() {
  127. echo "BamBuddy Native Installation Script"
  128. echo ""
  129. echo "Usage: $0 [OPTIONS]"
  130. echo ""
  131. echo "Options:"
  132. echo " --path PATH Installation directory (default: /opt/bambuddy)"
  133. echo " --port PORT Port to listen on (default: 8000)"
  134. echo " --bind ADDRESS Bind address: 0.0.0.0 (network) or 127.0.0.1 (local only)"
  135. echo " --tz TIMEZONE Timezone (default: system timezone or UTC)"
  136. echo " --data-dir PATH Data directory (default: INSTALL_PATH/data)"
  137. echo " --log-dir PATH Log directory (default: INSTALL_PATH/logs)"
  138. echo " --debug Enable debug mode"
  139. echo " --log-level LEVEL Log level: DEBUG, INFO, WARNING, ERROR (default: INFO)"
  140. echo " --no-service Skip systemd service setup (Linux only)"
  141. echo " --set-system-tz Set system timezone to match (for unattended installs)"
  142. echo " --yes, -y Non-interactive mode, accept defaults"
  143. echo " --help, -h Show this help message"
  144. echo ""
  145. echo "Examples:"
  146. echo " Interactive installation:"
  147. echo " ./install.sh"
  148. echo ""
  149. echo " Unattended installation with custom settings:"
  150. echo " ./install.sh --path /srv/bambuddy --port 3000 --tz America/New_York --yes"
  151. echo ""
  152. echo " Minimal unattended installation:"
  153. echo " ./install.sh -y"
  154. exit 0
  155. }
  156. # -----------------------------------------------------------------------------
  157. # System Detection
  158. # -----------------------------------------------------------------------------
  159. detect_os() {
  160. if [[ "$OSTYPE" == "darwin"* ]]; then
  161. OS_TYPE="macos"
  162. PKG_MANAGER="brew"
  163. return
  164. fi
  165. if [[ -f /etc/os-release ]]; then
  166. . /etc/os-release
  167. case "$ID" in
  168. ubuntu|debian|raspbian|linuxmint|pop)
  169. OS_TYPE="debian"
  170. PKG_MANAGER="apt"
  171. ;;
  172. fedora|rhel|centos|rocky|almalinux|ol)
  173. OS_TYPE="rhel"
  174. if command -v dnf &>/dev/null; then
  175. PKG_MANAGER="dnf"
  176. else
  177. PKG_MANAGER="yum"
  178. fi
  179. ;;
  180. arch|manjaro|endeavouros)
  181. OS_TYPE="arch"
  182. PKG_MANAGER="pacman"
  183. ;;
  184. opensuse*|sles)
  185. OS_TYPE="suse"
  186. PKG_MANAGER="zypper"
  187. ;;
  188. *)
  189. log_error "Unsupported Linux distribution: $ID"
  190. exit 1
  191. ;;
  192. esac
  193. else
  194. log_error "Cannot detect operating system"
  195. exit 1
  196. fi
  197. }
  198. detect_python() {
  199. # Try python3 first, then python
  200. if command -v python3 &>/dev/null; then
  201. PYTHON_CMD="python3"
  202. elif command -v python &>/dev/null; then
  203. local version
  204. version=$(python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1)
  205. if [[ "$version" -ge 3 ]]; then
  206. PYTHON_CMD="python"
  207. fi
  208. fi
  209. if [[ -z "$PYTHON_CMD" ]]; then
  210. return 1
  211. fi
  212. # Check version >= 3.10
  213. local version
  214. version=$($PYTHON_CMD -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
  215. local major minor
  216. major=$(echo "$version" | cut -d'.' -f1)
  217. minor=$(echo "$version" | cut -d'.' -f2)
  218. if [[ "$major" -lt 3 ]] || { [[ "$major" -eq 3 ]] && [[ "$minor" -lt 10 ]]; }; then
  219. log_warn "Python $version found, but 3.10 or newer is required"
  220. return 1
  221. fi
  222. log_success "Found Python $version"
  223. return 0
  224. }
  225. detect_timezone() {
  226. if [[ -n "$TIMEZONE" ]]; then
  227. return 0
  228. fi
  229. # Try to get system timezone (with error handling for set -e)
  230. TIMEZONE=""
  231. if [[ -f /etc/timezone ]]; then
  232. TIMEZONE=$(cat /etc/timezone 2>/dev/null) || true
  233. fi
  234. if [[ -z "$TIMEZONE" ]] && [[ -L /etc/localtime ]]; then
  235. TIMEZONE=$(readlink /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||') || true
  236. fi
  237. if [[ -z "$TIMEZONE" ]] && command -v timedatectl &>/dev/null; then
  238. TIMEZONE=$(timedatectl show --property=Timezone --value 2>/dev/null) || true
  239. fi
  240. # Default to UTC if not found (use if/then to avoid set -e issue with &&)
  241. if [[ -z "$TIMEZONE" ]]; then
  242. TIMEZONE="UTC"
  243. fi
  244. return 0
  245. }
  246. # -----------------------------------------------------------------------------
  247. # Package Installation
  248. # -----------------------------------------------------------------------------
  249. install_dependencies() {
  250. log_info "Installing system dependencies..."
  251. case "$PKG_MANAGER" in
  252. apt)
  253. sudo apt-get update
  254. sudo apt-get install -y python3 python3-pip python3-venv git curl ffmpeg
  255. ;;
  256. dnf|yum)
  257. sudo $PKG_MANAGER install -y python3 python3-pip git curl ffmpeg
  258. ;;
  259. pacman)
  260. sudo pacman -Sy --noconfirm python python-pip git curl ffmpeg
  261. ;;
  262. zypper)
  263. sudo zypper install -y python3 python3-pip git curl ffmpeg
  264. ;;
  265. brew)
  266. # Check if Homebrew is installed
  267. if ! command -v brew &>/dev/null; then
  268. log_error "Homebrew not found. Please install it first: https://brew.sh"
  269. exit 1
  270. fi
  271. brew install python git curl ffmpeg
  272. ;;
  273. esac
  274. log_success "System dependencies installed"
  275. }
  276. # -----------------------------------------------------------------------------
  277. # Installation Steps
  278. # -----------------------------------------------------------------------------
  279. create_user() {
  280. if [[ "$OS_TYPE" == "macos" ]]; then
  281. return # Skip user creation on macOS
  282. fi
  283. if id "$SERVICE_USER" &>/dev/null; then
  284. log_info "User '$SERVICE_USER' already exists"
  285. return
  286. fi
  287. log_info "Creating service user '$SERVICE_USER'..."
  288. sudo useradd --system --shell /usr/sbin/nologin --home-dir "$INSTALL_PATH" "$SERVICE_USER"
  289. log_success "Service user created"
  290. }
  291. download_bambuddy() {
  292. log_info "Downloading BamBuddy..."
  293. if [[ -d "$INSTALL_PATH/.git" ]]; then
  294. log_info "Existing installation found, updating..."
  295. # Add safe.directory to avoid "dubious ownership" error when running as root
  296. git config --global --add safe.directory "$INSTALL_PATH" 2>/dev/null || true
  297. cd "$INSTALL_PATH"
  298. git fetch origin
  299. git reset --hard origin/main
  300. # Ensure correct ownership after update
  301. sudo chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_PATH" 2>/dev/null || true
  302. else
  303. sudo mkdir -p "$INSTALL_PATH"
  304. sudo chown "$SERVICE_USER:$SERVICE_USER" "$INSTALL_PATH" 2>/dev/null || true
  305. git clone https://github.com/maziggy/bambuddy.git "$INSTALL_PATH"
  306. sudo chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_PATH" 2>/dev/null || true
  307. fi
  308. log_success "BamBuddy downloaded to $INSTALL_PATH"
  309. }
  310. setup_virtualenv() {
  311. log_info "Setting up Python virtual environment..."
  312. cd "$INSTALL_PATH"
  313. if [[ "$OS_TYPE" == "macos" ]]; then
  314. $PYTHON_CMD -m venv venv
  315. source venv/bin/activate
  316. else
  317. sudo -u "$SERVICE_USER" $PYTHON_CMD -m venv venv 2>/dev/null || $PYTHON_CMD -m venv venv
  318. source venv/bin/activate
  319. fi
  320. pip install --upgrade pip
  321. pip install -r requirements.txt
  322. log_success "Virtual environment configured"
  323. }
  324. check_node_version() {
  325. # Returns 0 if Node.js 20+ is available, 1 otherwise
  326. if ! command -v node &>/dev/null; then
  327. return 1
  328. fi
  329. local version
  330. version=$(node --version 2>/dev/null | sed 's/^v//')
  331. local major
  332. major=$(echo "$version" | cut -d'.' -f1)
  333. if [[ "$major" -ge 20 ]]; then
  334. log_success "Found Node.js v$version"
  335. return 0
  336. else
  337. log_warn "Found Node.js v$version (need 20+)"
  338. return 1
  339. fi
  340. }
  341. install_nodejs() {
  342. log_info "Installing Node.js 22..."
  343. case "$PKG_MANAGER" in
  344. apt)
  345. # Remove old nodejs if present
  346. sudo apt-get remove -y nodejs npm 2>/dev/null || true
  347. curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
  348. sudo apt-get install -y nodejs
  349. ;;
  350. dnf|yum)
  351. sudo $PKG_MANAGER remove -y nodejs npm 2>/dev/null || true
  352. curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
  353. sudo $PKG_MANAGER install -y nodejs
  354. ;;
  355. pacman)
  356. sudo pacman -S --noconfirm nodejs npm
  357. ;;
  358. zypper)
  359. sudo zypper install -y nodejs22
  360. ;;
  361. brew)
  362. brew install node@22
  363. brew link --overwrite node@22
  364. ;;
  365. *)
  366. log_error "Please install Node.js 20+ manually: https://nodejs.org/"
  367. exit 1
  368. ;;
  369. esac
  370. # Refresh PATH
  371. hash -r 2>/dev/null || true
  372. }
  373. build_frontend() {
  374. log_info "Building frontend..."
  375. cd "$INSTALL_PATH/frontend"
  376. # Check for Node.js 20+
  377. if ! check_node_version; then
  378. install_nodejs
  379. # Verify installation
  380. if ! check_node_version; then
  381. log_error "Failed to install Node.js 20+. Please install manually."
  382. exit 1
  383. fi
  384. fi
  385. npm ci
  386. npm run build
  387. log_success "Frontend built"
  388. }
  389. create_directories() {
  390. log_info "Creating data directories..."
  391. sudo mkdir -p "$DATA_DIR" "$LOG_DIR"
  392. if [[ "$OS_TYPE" != "macos" ]]; then
  393. sudo chown -R "$SERVICE_USER:$SERVICE_USER" "$DATA_DIR" "$LOG_DIR"
  394. fi
  395. log_success "Directories created"
  396. }
  397. create_env_file() {
  398. log_info "Creating environment configuration..."
  399. local env_file="$INSTALL_PATH/.env"
  400. # Note: Only include settings recognized by the app's pydantic Settings class
  401. # Other settings (PORT, BIND_ADDRESS, DATA_DIR, LOG_DIR, TZ) are set in systemd service
  402. cat > /tmp/bambuddy.env << EOF
  403. # BamBuddy Configuration
  404. # Generated by install.sh on $(date)
  405. # Debug mode (true = verbose logging)
  406. DEBUG=$DEBUG_MODE
  407. # Log level (only used when DEBUG=false)
  408. # Options: DEBUG, INFO, WARNING, ERROR
  409. LOG_LEVEL=$LOG_LEVEL
  410. # Enable file logging
  411. LOG_TO_FILE=true
  412. EOF
  413. sudo mv /tmp/bambuddy.env "$env_file"
  414. if [[ "$OS_TYPE" != "macos" ]]; then
  415. sudo chown "$SERVICE_USER:$SERVICE_USER" "$env_file"
  416. fi
  417. sudo chmod 600 "$env_file"
  418. log_success "Environment file created at $env_file"
  419. }
  420. create_systemd_service() {
  421. if [[ "$OS_TYPE" == "macos" ]] || [[ "$SKIP_SERVICE" == "true" ]]; then
  422. return
  423. fi
  424. log_info "Creating systemd service..."
  425. cat > /tmp/bambuddy.service << EOF
  426. [Unit]
  427. Description=BamBuddy - Bambu Lab Print Management
  428. Documentation=https://github.com/maziggy/bambuddy
  429. After=network.target
  430. [Service]
  431. Type=simple
  432. User=$SERVICE_USER
  433. Group=$SERVICE_USER
  434. WorkingDirectory=$INSTALL_PATH
  435. # App settings from .env file
  436. EnvironmentFile=$INSTALL_PATH/.env
  437. # Service settings (not in .env to avoid pydantic validation errors)
  438. Environment="DATA_DIR=$DATA_DIR"
  439. Environment="LOG_DIR=$LOG_DIR"
  440. Environment="TZ=$TIMEZONE"
  441. ExecStart=$INSTALL_PATH/venv/bin/uvicorn backend.app.main:app --host $BIND_ADDRESS --port $PORT
  442. Restart=on-failure
  443. RestartSec=5
  444. StandardOutput=journal
  445. StandardError=journal
  446. # Security hardening
  447. NoNewPrivileges=true
  448. PrivateTmp=true
  449. ProtectSystem=strict
  450. ProtectHome=true
  451. ReadWritePaths=$DATA_DIR $LOG_DIR $INSTALL_PATH
  452. [Install]
  453. WantedBy=multi-user.target
  454. EOF
  455. sudo mv /tmp/bambuddy.service /etc/systemd/system/bambuddy.service
  456. sudo systemctl daemon-reload
  457. log_success "Systemd service created"
  458. if prompt_yes_no "Enable BamBuddy to start on boot?" "y"; then
  459. sudo systemctl enable bambuddy
  460. log_success "Service enabled"
  461. fi
  462. if prompt_yes_no "Start BamBuddy now?" "y"; then
  463. sudo systemctl start bambuddy
  464. sleep 2
  465. if sudo systemctl is-active --quiet bambuddy; then
  466. log_success "BamBuddy is running"
  467. else
  468. log_warn "Service may have failed to start. Check: sudo journalctl -u bambuddy -f"
  469. fi
  470. fi
  471. }
  472. create_launchd_service() {
  473. if [[ "$OS_TYPE" != "macos" ]] || [[ "$SKIP_SERVICE" == "true" ]]; then
  474. return
  475. fi
  476. log_info "Creating launchd service..."
  477. local plist_path="$HOME/Library/LaunchAgents/com.bambuddy.app.plist"
  478. cat > "$plist_path" << EOF
  479. <?xml version="1.0" encoding="UTF-8"?>
  480. <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
  481. <plist version="1.0">
  482. <dict>
  483. <key>Label</key>
  484. <string>com.bambuddy.app</string>
  485. <key>ProgramArguments</key>
  486. <array>
  487. <string>$INSTALL_PATH/venv/bin/uvicorn</string>
  488. <string>backend.app.main:app</string>
  489. <string>--host</string>
  490. <string>$BIND_ADDRESS</string>
  491. <string>--port</string>
  492. <string>$PORT</string>
  493. </array>
  494. <key>WorkingDirectory</key>
  495. <string>$INSTALL_PATH</string>
  496. <key>EnvironmentVariables</key>
  497. <dict>
  498. <key>DEBUG</key>
  499. <string>$DEBUG_MODE</string>
  500. <key>LOG_LEVEL</key>
  501. <string>$LOG_LEVEL</string>
  502. <key>DATA_DIR</key>
  503. <string>$DATA_DIR</string>
  504. <key>LOG_DIR</key>
  505. <string>$LOG_DIR</string>
  506. <key>TZ</key>
  507. <string>$TIMEZONE</string>
  508. </dict>
  509. <key>RunAtLoad</key>
  510. <true/>
  511. <key>KeepAlive</key>
  512. <true/>
  513. <key>StandardOutPath</key>
  514. <string>$LOG_DIR/bambuddy.log</string>
  515. <key>StandardErrorPath</key>
  516. <string>$LOG_DIR/bambuddy.error.log</string>
  517. </dict>
  518. </plist>
  519. EOF
  520. log_success "Launchd plist created at $plist_path"
  521. if prompt_yes_no "Load BamBuddy service now?" "y"; then
  522. launchctl load "$plist_path"
  523. sleep 2
  524. if launchctl list | grep -q "com.bambuddy.app"; then
  525. log_success "BamBuddy is running"
  526. else
  527. log_warn "Service may have failed to start. Check: cat $LOG_DIR/bambuddy.error.log"
  528. fi
  529. fi
  530. }
  531. # -----------------------------------------------------------------------------
  532. # Main Installation Flow
  533. # -----------------------------------------------------------------------------
  534. parse_args() {
  535. while [[ $# -gt 0 ]]; do
  536. case "$1" in
  537. --path)
  538. INSTALL_PATH="$2"
  539. shift 2
  540. ;;
  541. --port)
  542. PORT="$2"
  543. shift 2
  544. ;;
  545. --bind)
  546. BIND_ADDRESS="$2"
  547. shift 2
  548. ;;
  549. --tz)
  550. TIMEZONE="$2"
  551. shift 2
  552. ;;
  553. --data-dir)
  554. DATA_DIR="$2"
  555. shift 2
  556. ;;
  557. --log-dir)
  558. LOG_DIR="$2"
  559. shift 2
  560. ;;
  561. --debug)
  562. DEBUG_MODE="true"
  563. shift
  564. ;;
  565. --log-level)
  566. LOG_LEVEL="$2"
  567. shift 2
  568. ;;
  569. --no-service)
  570. SKIP_SERVICE="true"
  571. shift
  572. ;;
  573. --set-system-tz)
  574. SET_SYSTEM_TZ="true"
  575. shift
  576. ;;
  577. --yes|-y)
  578. NON_INTERACTIVE="true"
  579. shift
  580. ;;
  581. --help|-h)
  582. show_help
  583. ;;
  584. *)
  585. log_error "Unknown option: $1"
  586. show_help
  587. ;;
  588. esac
  589. done
  590. }
  591. gather_config() {
  592. echo ""
  593. echo -e "${BOLD}Installation Configuration${NC}"
  594. echo -e "${CYAN}─────────────────────────────────────────${NC}"
  595. echo ""
  596. # Installation path
  597. [[ -z "$INSTALL_PATH" ]] && prompt "Installation directory" "$DEFAULT_INSTALL_PATH" INSTALL_PATH
  598. # Port
  599. [[ -z "$PORT" ]] && prompt "Port to listen on" "$DEFAULT_PORT" PORT
  600. # Bind address
  601. if [[ -z "$BIND_ADDRESS" ]]; then
  602. echo ""
  603. echo "Network access:"
  604. echo " 0.0.0.0 - Accessible from other devices on your network (recommended)"
  605. echo " 127.0.0.1 - Only accessible from this machine"
  606. prompt "Bind address" "$DEFAULT_BIND_ADDRESS" BIND_ADDRESS
  607. fi
  608. # Timezone
  609. detect_timezone
  610. prompt "Timezone" "$TIMEZONE" TIMEZONE
  611. # Offer to set system timezone if different from current (skip if already set via --set-system-tz)
  612. if [[ -z "$SET_SYSTEM_TZ" ]]; then
  613. local current_tz
  614. current_tz=$(timedatectl show --property=Timezone --value 2>/dev/null) || true
  615. if [[ -n "$TIMEZONE" ]] && [[ "$TIMEZONE" != "$current_tz" ]]; then
  616. # Default to "n" so unattended installs don't change system TZ unless --set-system-tz is used
  617. if prompt_yes_no "Set system timezone to $TIMEZONE?" "n"; then
  618. SET_SYSTEM_TZ="true"
  619. else
  620. SET_SYSTEM_TZ="false"
  621. fi
  622. else
  623. SET_SYSTEM_TZ="false"
  624. fi
  625. fi
  626. # Data directory
  627. [[ -z "$DATA_DIR" ]] && DATA_DIR="$INSTALL_PATH/data"
  628. prompt "Data directory" "$DATA_DIR" DATA_DIR
  629. # Log directory
  630. [[ -z "$LOG_DIR" ]] && LOG_DIR="$INSTALL_PATH/logs"
  631. prompt "Log directory" "$LOG_DIR" LOG_DIR
  632. # Debug mode
  633. if [[ -z "$DEBUG_MODE" ]]; then
  634. if prompt_yes_no "Enable debug mode?" "n"; then
  635. DEBUG_MODE="true"
  636. else
  637. DEBUG_MODE="false"
  638. fi
  639. fi
  640. # Log level
  641. if [[ -z "$LOG_LEVEL" ]]; then
  642. echo ""
  643. echo "Log levels: DEBUG, INFO, WARNING, ERROR"
  644. prompt "Log level" "$DEFAULT_LOG_LEVEL" LOG_LEVEL
  645. fi
  646. # Confirm
  647. echo ""
  648. echo -e "${BOLD}Installation Summary${NC}"
  649. echo -e "${CYAN}─────────────────────────────────────────${NC}"
  650. echo -e " Install path: ${GREEN}$INSTALL_PATH${NC}"
  651. echo -e " Port: ${GREEN}$PORT${NC}"
  652. echo -e " Bind address: ${GREEN}$BIND_ADDRESS${NC}"
  653. echo -e " Timezone: ${GREEN}$TIMEZONE${NC}"
  654. echo -e " Data dir: ${GREEN}$DATA_DIR${NC}"
  655. echo -e " Log dir: ${GREEN}$LOG_DIR${NC}"
  656. echo -e " Debug mode: ${GREEN}$DEBUG_MODE${NC}"
  657. echo -e " Log level: ${GREEN}$LOG_LEVEL${NC}"
  658. echo ""
  659. if ! prompt_yes_no "Proceed with installation?" "y"; then
  660. echo "Installation cancelled."
  661. exit 0
  662. fi
  663. }
  664. main() {
  665. parse_args "$@"
  666. print_banner
  667. # Check if running via pipe (curl | bash) - interactive mode won't work
  668. if [[ ! -t 0 ]] && [[ "$NON_INTERACTIVE" != "true" ]]; then
  669. log_error "Interactive mode requires a terminal."
  670. log_info "When using 'curl | bash', you must use non-interactive mode:"
  671. echo ""
  672. echo " curl -fsSL URL | bash -s -- --yes"
  673. echo ""
  674. log_info "Or download and run directly:"
  675. echo ""
  676. echo " curl -fsSL URL -o install.sh && chmod +x install.sh && ./install.sh"
  677. echo ""
  678. exit 1
  679. fi
  680. # Check for root (we need sudo for some operations)
  681. if [[ "$EUID" -eq 0 ]] && [[ "$OS_TYPE" != "macos" ]]; then
  682. log_warn "Running as root. Consider using a regular user with sudo privileges."
  683. fi
  684. # Detect system
  685. log_info "Detecting system..."
  686. detect_os
  687. log_success "Detected: $OS_TYPE (package manager: $PKG_MANAGER)"
  688. # Check/install Python
  689. if ! detect_python; then
  690. log_info "Python 3.10+ not found, will install..."
  691. fi
  692. # Gather configuration
  693. gather_config
  694. # Install steps
  695. echo ""
  696. echo -e "${BOLD}Starting Installation${NC}"
  697. echo -e "${CYAN}─────────────────────────────────────────${NC}"
  698. echo ""
  699. install_dependencies
  700. detect_python || { log_error "Failed to install Python"; exit 1; }
  701. # Set system timezone if requested
  702. if [[ "$SET_SYSTEM_TZ" == "true" ]]; then
  703. log_info "Setting system timezone to $TIMEZONE..."
  704. if [[ "$OS_TYPE" == "macos" ]]; then
  705. sudo systemsetup -settimezone "$TIMEZONE" 2>/dev/null || true
  706. else
  707. sudo timedatectl set-timezone "$TIMEZONE" 2>/dev/null || true
  708. fi
  709. log_success "System timezone set to $TIMEZONE"
  710. fi
  711. if [[ "$OS_TYPE" != "macos" ]]; then
  712. create_user
  713. else
  714. SERVICE_USER="$USER"
  715. fi
  716. download_bambuddy
  717. setup_virtualenv
  718. build_frontend
  719. create_directories
  720. create_env_file
  721. if [[ "$OS_TYPE" == "macos" ]]; then
  722. create_launchd_service
  723. else
  724. create_systemd_service
  725. fi
  726. # Done!
  727. echo ""
  728. echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
  729. echo -e "${GREEN}║ ║${NC}"
  730. echo -e "${GREEN}║ Installation Complete! ║${NC}"
  731. echo -e "${GREEN}║ ║${NC}"
  732. echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}"
  733. echo ""
  734. # Show appropriate URL based on bind address
  735. if [[ "$BIND_ADDRESS" == "0.0.0.0" ]]; then
  736. local ip_addr
  737. ip_addr=$(hostname -I 2>/dev/null | awk '{print $1}') || ip_addr="<your-ip>"
  738. echo -e " ${BOLD}Access BamBuddy:${NC} ${CYAN}http://localhost:$PORT${NC}"
  739. echo -e " ${CYAN}http://$ip_addr:$PORT${NC} (from other devices)"
  740. else
  741. echo -e " ${BOLD}Access BamBuddy:${NC} ${CYAN}http://localhost:$PORT${NC}"
  742. fi
  743. echo ""
  744. if [[ "$OS_TYPE" == "macos" ]]; then
  745. echo -e " ${BOLD}Manage service:${NC}"
  746. echo -e " Start: launchctl load ~/Library/LaunchAgents/com.bambuddy.app.plist"
  747. echo -e " Stop: launchctl unload ~/Library/LaunchAgents/com.bambuddy.app.plist"
  748. echo -e " Logs: tail -f $LOG_DIR/bambuddy.log"
  749. else
  750. echo -e " ${BOLD}Manage service:${NC}"
  751. echo -e " Status: sudo systemctl status bambuddy"
  752. echo -e " Start: sudo systemctl start bambuddy"
  753. echo -e " Stop: sudo systemctl stop bambuddy"
  754. echo -e " Logs: sudo journalctl -u bambuddy -f"
  755. fi
  756. echo ""
  757. echo -e " ${BOLD}Update BamBuddy:${NC}"
  758. echo -e " cd $INSTALL_PATH && git pull && source venv/bin/activate"
  759. echo -e " pip install -r requirements.txt && cd frontend && npm ci && npm run build"
  760. if [[ "$OS_TYPE" != "macos" ]]; then
  761. echo -e " sudo systemctl restart bambuddy"
  762. fi
  763. echo ""
  764. echo -e " ${BOLD}Documentation:${NC} ${CYAN}https://wiki.bambuddy.cool${NC}"
  765. echo ""
  766. }
  767. main "$@"