diff --git a/CHANGELOG.md b/CHANGELOG.md index 505809b0e..50e657c9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,15 @@ All notable changes to Bambuddy will be documented in this file. - Job Failed: When a job fails to start (enabled by default) - Queue Complete: When all queued jobs finish - New "Print Queue" section in notification provider settings +- **Installation Scripts** - Interactive install scripts for Linux and macOS: + - Native install (`install.sh`): Python venv, systemd/launchd service, Node.js 22 + - Docker install (`docker-install.sh`): Docker Compose setup with health checks + - Interactive prompts for: install path, port, bind address, timezone, data/log directories + - Unattended mode with `--yes` flag for automation + - Auto-detects OS and package manager (apt, dnf, pacman, brew) + - Option to set system timezone during installation + - Shows IP address for network access when binding to 0.0.0.0 + - Supports updating existing installations ### Fixes - **Multi-Plate Thumbnail in Queue** - Fixed queue items showing wrong thumbnail for multi-plate files (Issue #166): diff --git a/deploy/bambuddy.service b/deploy/bambuddy.service index b096b3d29..93c779466 100644 --- a/deploy/bambuddy.service +++ b/deploy/bambuddy.service @@ -1,31 +1,61 @@ +# BamBuddy Systemd Service Template +# +# INSTALLATION: +# 1. Copy this file to /etc/systemd/system/bambuddy.service +# 2. Replace placeholders: +# - INSTALL_PATH: Where BamBuddy is installed (e.g., /opt/bambuddy) +# - SERVICE_USER: User to run as (e.g., bambuddy) +# - DATA_DIR: Data directory (e.g., /opt/bambuddy/data) +# - LOG_DIR: Log directory (e.g., /opt/bambuddy/logs) +# 3. Run: sudo systemctl daemon-reload +# 4. Run: sudo systemctl enable bambuddy +# 5. Run: sudo systemctl start bambuddy +# +# Or use the install script: ./install/install.sh +# + [Unit] -Description=BamBuddy Print Archive +Description=BamBuddy - Bambu Lab Print Management +Documentation=https://github.com/maziggy/bambuddy After=network.target [Service] Type=simple -User=claude -Group=claude -WorkingDirectory=/bambuddy -Environment="PATH=/bambuddy/logs -ExecStartPre=+/bin/mkdir -p /bambuddy/archive -ExecStartPre=+/bin/chown -R : /bambuddy/logs -ExecStartPre=+/bin/chown -R : /bambuddy/archive +# Logging +StandardOutput=journal +StandardError=journal +SyslogIdentifier=bambuddy -ExecStart=/bambuddy/venv/bin/uvicorn backend.app.main:app --host 0.0.0.0 --port 8000 -Restart=always -RestartSec=10 +# Security hardening +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=DATA_DIR LOG_DIR INSTALL_PATH [Install] WantedBy=multi-user.target diff --git a/install/README.md b/install/README.md new file mode 100644 index 000000000..658303ef2 --- /dev/null +++ b/install/README.md @@ -0,0 +1,234 @@ +# BamBuddy Installation Scripts + +Interactive installation scripts for BamBuddy with support for both native and Docker deployments. + +## Quick Start + +### Docker Installation (Recommended) + +**Linux/macOS:** +```bash +curl -fsSL https://raw.githubusercontent.com/maziggy/bambuddy/main/install/docker-install.sh | bash +``` + +### Native Installation + +**Linux/macOS:** +```bash +curl -fsSL https://raw.githubusercontent.com/maziggy/bambuddy/main/install/install.sh | bash +``` + +--- + +## Scripts Overview + +| Script | Platform | Method | +|--------|----------|--------| +| `install.sh` | Linux, macOS | Native (Python venv) | +| `docker-install.sh` | Linux, macOS | Docker | + +--- + +## Native Installation Scripts + +### `install.sh` (Linux/macOS) + +Installs BamBuddy with Python virtual environment and optional systemd/launchd service. + +**Supported Systems:** +- Debian/Ubuntu (apt) +- RHEL/Fedora/CentOS (dnf/yum) +- Arch Linux (pacman) +- openSUSE (zypper) +- macOS (Homebrew) + +**Options:** +``` +--path PATH Installation directory (default: /opt/bambuddy) +--port PORT Port to listen on (default: 8000) +--tz TIMEZONE Timezone (default: system timezone) +--data-dir PATH Data directory (default: INSTALL_PATH/data) +--log-dir PATH Log directory (default: INSTALL_PATH/logs) +--debug Enable debug mode +--log-level LEVEL Log level: DEBUG, INFO, WARNING, ERROR (default: INFO) +--no-service Skip systemd/launchd service setup +--yes, -y Non-interactive mode, accept defaults +``` + +**Examples:** +```bash +# Interactive installation +./install.sh + +# Unattended with custom settings +./install.sh --path /srv/bambuddy --port 3000 --tz America/New_York --yes + +# Minimal unattended +./install.sh -y + +# Skip service setup +./install.sh --no-service -y +``` + +--- + +## Docker Installation Scripts + +### `docker-install.sh` (Linux/macOS) + +Installs BamBuddy using Docker containers. + +**Options:** +``` +--path PATH Installation directory (default: ~/bambuddy) +--port PORT Port to expose (default: 8000) +--tz TIMEZONE Timezone (default: system timezone) +--build Build from source instead of using pre-built image +--yes, -y Non-interactive mode, accept defaults +``` + +**Examples:** +```bash +# Interactive installation +./docker-install.sh + +# Unattended with custom settings +./docker-install.sh --path /srv/bambuddy --port 3000 --tz Europe/Berlin --yes + +# Build from source +./docker-install.sh --build --yes +``` + +--- + +## Configuration Options + +All scripts support these configuration options: + +| Option | Description | Default | +|--------|-------------|---------| +| Install Path | Where BamBuddy is installed | `/opt/bambuddy` (Linux/Docker) | +| Port | HTTP port for web interface | `8000` | +| Timezone | Server timezone | System timezone or `UTC` | +| Data Directory | Database and archives | `INSTALL_PATH/data` | +| Log Directory | Application logs | `INSTALL_PATH/logs` | +| Debug Mode | Enable verbose logging | `false` | +| Log Level | INFO, WARNING, ERROR, DEBUG | `INFO` | + +--- + +## Post-Installation + +### Accessing BamBuddy + +After installation, open your browser to: +``` +http://localhost:8000 +``` + +Or use the port you specified during installation. + +### Service Management + +**Linux (systemd):** +```bash +sudo systemctl status bambuddy # Check status +sudo systemctl start bambuddy # Start +sudo systemctl stop bambuddy # Stop +sudo systemctl restart bambuddy # Restart +sudo journalctl -u bambuddy -f # View logs +``` + +**macOS (launchd):** +```bash +launchctl list | grep bambuddy # Check status +launchctl load ~/Library/LaunchAgents/com.bambuddy.app.plist # Start +launchctl unload ~/Library/LaunchAgents/com.bambuddy.app.plist # Stop +``` + +**Docker:** +```bash +docker compose ps # Check status +docker compose up -d # Start +docker compose down # Stop +docker compose restart # Restart +docker compose logs -f # View logs +``` + +### Updating + +**Native installation:** +```bash +cd /opt/bambuddy +git pull +source venv/bin/activate +pip install -r requirements.txt +cd frontend && npm ci && npm run build +sudo systemctl restart bambuddy # Linux +``` + +**Docker (pre-built image):** +```bash +cd ~/bambuddy +docker compose pull +docker compose up -d +``` + +**Docker (from source):** +```bash +cd ~/bambuddy +git pull +docker compose up -d --build +``` + +--- + +## Troubleshooting + +### Permission Denied (Linux) +Run with `sudo` or ensure your user has appropriate permissions: +```bash +sudo ./install.sh +``` + +### Docker: Printer Discovery Not Working +Docker Desktop for macOS doesn't support host networking. Add printers manually by IP address in the BamBuddy web interface. + +### Service Won't Start +Check logs for errors: +```bash +# Linux +sudo journalctl -u bambuddy -n 50 + +# Docker +docker compose logs bambuddy +``` + +### Port Already in Use +Choose a different port during installation or stop the conflicting service: +```bash +# Find what's using port 8000 +sudo lsof -i :8000 # Linux/macOS +``` + +--- + +## Requirements + +### Native Installation +- Python 3.10+ (automatically installed if missing) +- Node.js 18+ (automatically installed if missing) +- Git (automatically installed if missing) +- ~500MB disk space + +### Docker Installation +- Docker Engine 20+ or Docker Desktop +- ~1GB disk space (includes image) + +--- + +## Support + +- **Documentation:** https://wiki.bambuddy.cool +- **Discord:** https://discord.gg/aFS3ZfScHM +- **Issues:** https://github.com/maziggy/bambuddy/issues diff --git a/install/docker-install.sh b/install/docker-install.sh new file mode 100755 index 000000000..c8d48833a --- /dev/null +++ b/install/docker-install.sh @@ -0,0 +1,541 @@ +#!/usr/bin/env bash +# +# BamBuddy Docker Installation Script +# Supports: Linux (all distros), macOS +# +# Usage: +# Interactive: curl -fsSL https://raw.githubusercontent.com/maziggy/bambuddy/main/install/docker-install.sh | bash +# Unattended: ./docker-install.sh --path /opt/bambuddy --port 8000 --yes +# +# Options: +# --path PATH Installation directory (default: /opt/bambuddy) +# --port PORT Port to expose (default: 8000) +# --bind ADDRESS Bind address: 0.0.0.0 (network) or 127.0.0.1 (local only) +# --tz TIMEZONE Timezone (default: system timezone or UTC) +# --build Build from source instead of using pre-built image +# --yes, -y Non-interactive mode, accept defaults +# --help, -h Show this help message +# + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color +BOLD='\033[1m' + +# Default values +DEFAULT_INSTALL_PATH="/opt/bambuddy" +DEFAULT_PORT="8000" +DEFAULT_BIND_ADDRESS="0.0.0.0" + +# Script variables +INSTALL_PATH="" +PORT="" +BIND_ADDRESS="" +TIMEZONE="" +BUILD_FROM_SOURCE="false" +NON_INTERACTIVE="false" +OS_TYPE="" +DOCKER_CMD="" + +# ----------------------------------------------------------------------------- +# Helper Functions +# ----------------------------------------------------------------------------- + +print_banner() { + echo -e "${CYAN}" + echo "╔════════════════════════════════════════════════════════╗" + echo "║ ║" + echo "║ ____ _ _ _ ║" + echo "║ | __ ) __ _ _ __ ___ | |__ _ _ __| | __| |_ _ ║" + echo "║ | _ \\ / _\` | '_ \` _ \\| '_ \\| | | |/ _\` |/ _\` | | | | ║" + echo "║ | |_) | (_| | | | | | | |_) | |_| | (_| | (_| | |_| | ║" + echo "║ |____/ \\__,_|_| |_| |_|_.__/ \\__,_|\\__,_|\\__,_|\\__, | ║" + echo "║ |___/ ║" + echo "║ ║" + echo "║ Docker Installation Script ║" + echo "║ ║" + echo "╚════════════════════════════════════════════════════════╝" + echo -e "${NC}" +} + +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[OK]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +prompt() { + local prompt_text="$1" + local default_value="$2" + local var_name="$3" + + if [[ "$NON_INTERACTIVE" == "true" ]]; then + eval "$var_name=\"$default_value\"" + return + fi + + if [[ -n "$default_value" ]]; then + echo -en "${BOLD}$prompt_text${NC} [${CYAN}$default_value${NC}]: " + else + echo -en "${BOLD}$prompt_text${NC}: " + fi + + read -r input + if [[ -z "$input" ]]; then + eval "$var_name=\"$default_value\"" + else + eval "$var_name=\"$input\"" + fi +} + +prompt_yes_no() { + local prompt_text="$1" + local default="$2" # y or n + + if [[ "$NON_INTERACTIVE" == "true" ]]; then + [[ "$default" == "y" ]] && return 0 || return 1 + fi + + local yn_hint="[y/n]" + [[ "$default" == "y" ]] && yn_hint="[Y/n]" + [[ "$default" == "n" ]] && yn_hint="[y/N]" + + while true; do + echo -en "${BOLD}$prompt_text${NC} $yn_hint: " + read -r yn + [[ -z "$yn" ]] && yn="$default" + case "$yn" in + [Yy]* ) return 0;; + [Nn]* ) return 1;; + * ) echo "Please answer yes or no.";; + esac + done +} + +show_help() { + echo "BamBuddy Docker Installation Script" + echo "" + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --path PATH Installation directory (default: /opt/bambuddy)" + echo " --port PORT Port to expose (default: 8000)" + echo " --bind ADDRESS Bind address: 0.0.0.0 (network) or 127.0.0.1 (local only)" + echo " --tz TIMEZONE Timezone (default: system timezone or UTC)" + echo " --build Build from source instead of using pre-built image" + echo " --yes, -y Non-interactive mode, accept defaults" + echo " --help, -h Show this help message" + echo "" + echo "Examples:" + echo " Interactive installation:" + echo " ./docker-install.sh" + echo "" + echo " Unattended installation with custom settings:" + echo " ./docker-install.sh --path /srv/bambuddy --port 3000 --tz America/New_York --yes" + echo "" + echo " Build from source:" + echo " ./docker-install.sh --build --yes" + exit 0 +} + +# ----------------------------------------------------------------------------- +# System Detection +# ----------------------------------------------------------------------------- + +detect_os() { + if [[ "$OSTYPE" == "darwin"* ]]; then + OS_TYPE="macos" + return + fi + + if [[ -f /etc/os-release ]]; then + OS_TYPE="linux" + else + log_error "Cannot detect operating system" + exit 1 + fi +} + +detect_docker() { + # Check for docker compose (v2) or docker-compose (v1) + if docker compose version &>/dev/null 2>&1; then + DOCKER_CMD="docker compose" + log_success "Found Docker Compose v2" + return 0 + elif docker-compose --version &>/dev/null 2>&1; then + DOCKER_CMD="docker-compose" + log_success "Found Docker Compose v1" + return 0 + fi + return 1 +} + +detect_timezone() { + if [[ -n "$TIMEZONE" ]]; then + return 0 + fi + + # Try to get system timezone (with error handling for set -e) + TIMEZONE="" + if [[ -f /etc/timezone ]]; then + TIMEZONE=$(cat /etc/timezone 2>/dev/null) || true + fi + + if [[ -z "$TIMEZONE" ]] && [[ -L /etc/localtime ]]; then + TIMEZONE=$(readlink /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||') || true + fi + + if [[ -z "$TIMEZONE" ]] && command -v timedatectl &>/dev/null; then + TIMEZONE=$(timedatectl show --property=Timezone --value 2>/dev/null) || true + fi + + # Default to UTC if not found (use if/then to avoid set -e issue with &&) + if [[ -z "$TIMEZONE" ]]; then + TIMEZONE="UTC" + fi + return 0 +} + +# ----------------------------------------------------------------------------- +# Installation Functions +# ----------------------------------------------------------------------------- + +install_docker() { + log_info "Docker not found, installing..." + + case "$OS_TYPE" in + linux) + # Use Docker's convenience script + curl -fsSL https://get.docker.com | sh + + # Add current user to docker group + if [[ -n "$SUDO_USER" ]]; then + sudo usermod -aG docker "$SUDO_USER" + log_warn "Added $SUDO_USER to docker group. You may need to log out and back in." + else + sudo usermod -aG docker "$USER" + log_warn "Added $USER to docker group. You may need to log out and back in." + fi + + # Start Docker service + sudo systemctl enable docker + sudo systemctl start docker + ;; + macos) + log_error "Docker Desktop not found." + log_error "Please install Docker Desktop for Mac from: https://www.docker.com/products/docker-desktop" + exit 1 + ;; + esac + + log_success "Docker installed" +} + +create_install_dir() { + log_info "Creating installation directory..." + + mkdir -p "$INSTALL_PATH" + cd "$INSTALL_PATH" + + log_success "Directory created: $INSTALL_PATH" +} + +download_compose_file() { + log_info "Downloading docker-compose.yml..." + + if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then + # Clone the full repo for building + if [[ -d ".git" ]]; then + log_info "Existing repository found, updating..." + git fetch origin + git reset --hard origin/main + else + git clone https://github.com/maziggy/bambuddy.git . + fi + else + # Just download the compose file + curl -fsSL -o docker-compose.yml \ + https://raw.githubusercontent.com/maziggy/bambuddy/main/docker-compose.yml + fi + + log_success "docker-compose.yml ready" +} + +create_env_file() { + log_info "Creating environment configuration..." + + cat > .env << EOF +# BamBuddy Docker Configuration +# Generated by docker-install.sh on $(date) + +# Port BamBuddy runs on +PORT=$PORT + +# Timezone +TZ=$TIMEZONE +EOF + + log_success "Environment file created" +} + +customize_compose() { + # Detect if we need to disable host networking (macOS/Windows in Docker Desktop) + if [[ "$OS_TYPE" == "macos" ]]; then + log_warn "Docker Desktop detected. Host networking is not supported." + log_info "Modifying docker-compose.yml for port mapping..." + + # Create a modified compose file for macOS + if [[ -f docker-compose.yml ]]; then + # Comment out network_mode: host and uncomment ports section + sed -i.bak \ + -e 's/^[[:space:]]*network_mode: host/# network_mode: host/' \ + -e 's/^[[:space:]]*#ports:/ ports:/' \ + -e 's/^[[:space:]]*#[[:space:]]*- "\${PORT:-8000}:8000"/ - "\${PORT:-8000}:8000"/' \ + docker-compose.yml + + log_warn "Printer discovery may not work. Add printers manually by IP address." + fi + fi +} + +start_container() { + log_info "Starting BamBuddy..." + + if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then + $DOCKER_CMD up -d --build + else + $DOCKER_CMD up -d + fi + + # Wait for container to start + log_info "Waiting for container to start..." + local max_attempts=15 + local attempt=0 + + while [[ $attempt -lt $max_attempts ]]; do + # Check if container is running (Up) + if $DOCKER_CMD ps | grep -q "Up"; then + log_success "BamBuddy container is running" + return 0 + fi + + # Check if container failed + if $DOCKER_CMD ps -a | grep -q "Exited"; then + log_error "Container failed to start" + log_info "Check logs with: $DOCKER_CMD logs bambuddy" + return 1 + fi + + sleep 2 + ((attempt++)) + done + + log_warn "Container may still be starting. Check with: $DOCKER_CMD ps" +} + +# ----------------------------------------------------------------------------- +# Main Installation Flow +# ----------------------------------------------------------------------------- + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --path) + INSTALL_PATH="$2" + shift 2 + ;; + --port) + PORT="$2" + shift 2 + ;; + --bind) + BIND_ADDRESS="$2" + shift 2 + ;; + --tz) + TIMEZONE="$2" + shift 2 + ;; + --build) + BUILD_FROM_SOURCE="true" + shift + ;; + --yes|-y) + NON_INTERACTIVE="true" + shift + ;; + --help|-h) + show_help + ;; + *) + log_error "Unknown option: $1" + show_help + ;; + esac + done +} + +gather_config() { + echo "" + echo -e "${BOLD}Installation Configuration${NC}" + echo -e "${CYAN}─────────────────────────────────────────${NC}" + echo "" + + # Installation path + [[ -z "$INSTALL_PATH" ]] && prompt "Installation directory" "$DEFAULT_INSTALL_PATH" INSTALL_PATH + + # Port + [[ -z "$PORT" ]] && prompt "Port to expose" "$DEFAULT_PORT" PORT + + # Bind address + if [[ -z "$BIND_ADDRESS" ]]; then + echo "" + echo "Network access:" + echo " 0.0.0.0 - Accessible from other devices on your network (recommended)" + echo " 127.0.0.1 - Only accessible from this machine" + prompt "Bind address" "$DEFAULT_BIND_ADDRESS" BIND_ADDRESS + fi + + # Timezone + detect_timezone + prompt "Timezone" "$TIMEZONE" TIMEZONE + + # Build from source? + if [[ "$BUILD_FROM_SOURCE" != "true" ]] && [[ "$NON_INTERACTIVE" != "true" ]]; then + if prompt_yes_no "Build from source? (No = use pre-built image)" "n"; then + BUILD_FROM_SOURCE="true" + fi + fi + + # Confirm + echo "" + echo -e "${BOLD}Installation Summary${NC}" + echo -e "${CYAN}─────────────────────────────────────────${NC}" + echo -e " Install path: ${GREEN}$INSTALL_PATH${NC}" + echo -e " Port: ${GREEN}$PORT${NC}" + echo -e " Bind address: ${GREEN}$BIND_ADDRESS${NC}" + echo -e " Timezone: ${GREEN}$TIMEZONE${NC}" + echo -e " Build source: ${GREEN}$BUILD_FROM_SOURCE${NC}" + echo "" + + if ! prompt_yes_no "Proceed with installation?" "y"; then + echo "Installation cancelled." + exit 0 + fi +} + +main() { + parse_args "$@" + print_banner + + # Check if running via pipe (curl | bash) - interactive mode won't work + if [[ ! -t 0 ]] && [[ "$NON_INTERACTIVE" != "true" ]]; then + log_error "Interactive mode requires a terminal." + log_info "When using 'curl | bash', you must use non-interactive mode:" + echo "" + echo " curl -fsSL URL | bash -s -- --yes" + echo "" + log_info "Or download and run directly:" + echo "" + echo " curl -fsSL URL -o docker-install.sh && chmod +x docker-install.sh && ./docker-install.sh" + echo "" + exit 1 + fi + + # Detect system + log_info "Detecting system..." + detect_os + log_success "Detected: $OS_TYPE" + + # Check for Docker + if ! command -v docker &>/dev/null; then + install_docker + fi + + if ! detect_docker; then + log_error "Docker Compose not found. Please install Docker Compose." + exit 1 + fi + + # Check if Docker daemon is running + if ! docker info &>/dev/null; then + log_error "Docker daemon is not running. Please start Docker and try again." + exit 1 + fi + + # Gather configuration + gather_config + + # Install steps + echo "" + echo -e "${BOLD}Starting Installation${NC}" + echo -e "${CYAN}─────────────────────────────────────────${NC}" + echo "" + + create_install_dir + download_compose_file + create_env_file + customize_compose + start_container + + # Done! + echo "" + echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}║ ║${NC}" + echo -e "${GREEN}║ Installation Complete! ║${NC}" + echo -e "${GREEN}║ ║${NC}" + echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}" + echo "" + # Show appropriate URL based on bind address + if [[ "$BIND_ADDRESS" == "0.0.0.0" ]]; then + local ip_addr + ip_addr=$(hostname -I 2>/dev/null | awk '{print $1}') || ip_addr="" + echo -e " ${BOLD}Access BamBuddy:${NC} ${CYAN}http://localhost:$PORT${NC}" + echo -e " ${CYAN}http://$ip_addr:$PORT${NC} (from other devices)" + else + echo -e " ${BOLD}Access BamBuddy:${NC} ${CYAN}http://localhost:$PORT${NC}" + fi + echo "" + echo -e " ${BOLD}Manage container:${NC}" + echo -e " Status: cd $INSTALL_PATH && $DOCKER_CMD ps" + echo -e " Logs: cd $INSTALL_PATH && $DOCKER_CMD logs -f bambuddy" + echo -e " Stop: cd $INSTALL_PATH && $DOCKER_CMD down" + echo -e " Start: cd $INSTALL_PATH && $DOCKER_CMD up -d" + echo -e " Restart: cd $INSTALL_PATH && $DOCKER_CMD restart" + echo "" + echo -e " ${BOLD}Update BamBuddy:${NC}" + if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then + echo -e " cd $INSTALL_PATH && git pull && $DOCKER_CMD up -d --build" + else + echo -e " cd $INSTALL_PATH && $DOCKER_CMD pull && $DOCKER_CMD up -d" + fi + echo "" + echo -e " ${BOLD}Data location:${NC} Docker volumes (bambuddy_data, bambuddy_logs)" + echo "" + echo -e " ${BOLD}Documentation:${NC} ${CYAN}https://wiki.bambuddy.cool${NC}" + echo "" + + if [[ "$OS_TYPE" == "macos" ]]; then + echo -e " ${YELLOW}Note:${NC} Printer discovery may not work with Docker Desktop." + echo -e " Add printers manually using their IP address." + echo "" + fi +} + +main "$@" diff --git a/install/install.sh b/install/install.sh new file mode 100755 index 000000000..a6b15709d --- /dev/null +++ b/install/install.sh @@ -0,0 +1,883 @@ +#!/usr/bin/env bash +# +# BamBuddy Native Installation Script +# Supports: Debian/Ubuntu, RHEL/Fedora/CentOS, Arch Linux, macOS +# +# Usage: +# Interactive: curl -fsSL https://raw.githubusercontent.com/maziggy/bambuddy/main/install/install.sh | bash +# Unattended: ./install.sh --path /opt/bambuddy --port 8000 --yes +# +# Options: +# --path PATH Installation directory (default: /opt/bambuddy) +# --port PORT Port to listen on (default: 8000) +# --bind ADDRESS Bind address: 0.0.0.0 (network) or 127.0.0.1 (local only) +# --tz TIMEZONE Timezone (default: system timezone or UTC) +# --data-dir PATH Data directory (default: INSTALL_PATH/data) +# --log-dir PATH Log directory (default: INSTALL_PATH/logs) +# --debug Enable debug mode +# --log-level LEVEL Log level: DEBUG, INFO, WARNING, ERROR (default: INFO) +# --no-service Skip systemd service setup (Linux only) +# --set-system-tz Set system timezone to match (for unattended installs) +# --yes, -y Non-interactive mode, accept defaults +# --help, -h Show this help message +# + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color +BOLD='\033[1m' + +# Default values +DEFAULT_INSTALL_PATH="/opt/bambuddy" +DEFAULT_PORT="8000" +DEFAULT_BIND_ADDRESS="0.0.0.0" +DEFAULT_LOG_LEVEL="INFO" +DEFAULT_DEBUG="false" + +# Script variables +INSTALL_PATH="" +PORT="" +BIND_ADDRESS="" +TIMEZONE="" +DATA_DIR="" +LOG_DIR="" +DEBUG_MODE="" +LOG_LEVEL="" +SKIP_SERVICE="false" +SET_SYSTEM_TZ="" +NON_INTERACTIVE="false" +OS_TYPE="" +PKG_MANAGER="" +PYTHON_CMD="" +SERVICE_USER="bambuddy" + +# ----------------------------------------------------------------------------- +# Helper Functions +# ----------------------------------------------------------------------------- + +print_banner() { + echo -e "${CYAN}" + echo "╔════════════════════════════════════════════════════════╗" + echo "║ ║" + echo "║ ____ _ _ _ ║" + echo "║ | __ ) __ _ _ __ ___ | |__ _ _ __| | __| |_ _ ║" + echo "║ | _ \\ / _\` | '_ \` _ \\| '_ \\| | | |/ _\` |/ _\` | | | | ║" + echo "║ | |_) | (_| | | | | | | |_) | |_| | (_| | (_| | |_| | ║" + echo "║ |____/ \\__,_|_| |_| |_|_.__/ \\__,_|\\__,_|\\__,_|\\__, | ║" + echo "║ |___/ ║" + echo "║ ║" + echo "║ Native Installation Script ║" + echo "║ ║" + echo "╚════════════════════════════════════════════════════════╝" + echo -e "${NC}" +} + +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[OK]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +prompt() { + local prompt_text="$1" + local default_value="$2" + local var_name="$3" + + if [[ "$NON_INTERACTIVE" == "true" ]]; then + eval "$var_name=\"$default_value\"" + return + fi + + if [[ -n "$default_value" ]]; then + echo -en "${BOLD}$prompt_text${NC} [${CYAN}$default_value${NC}]: " + else + echo -en "${BOLD}$prompt_text${NC}: " + fi + + read -r input + if [[ -z "$input" ]]; then + eval "$var_name=\"$default_value\"" + else + eval "$var_name=\"$input\"" + fi +} + +prompt_yes_no() { + local prompt_text="$1" + local default="$2" # y or n + + if [[ "$NON_INTERACTIVE" == "true" ]]; then + [[ "$default" == "y" ]] && return 0 || return 1 + fi + + local yn_hint="[y/n]" + [[ "$default" == "y" ]] && yn_hint="[Y/n]" + [[ "$default" == "n" ]] && yn_hint="[y/N]" + + while true; do + echo -en "${BOLD}$prompt_text${NC} $yn_hint: " + read -r yn + [[ -z "$yn" ]] && yn="$default" + case "$yn" in + [Yy]* ) return 0;; + [Nn]* ) return 1;; + * ) echo "Please answer yes or no.";; + esac + done +} + +show_help() { + echo "BamBuddy Native Installation Script" + echo "" + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --path PATH Installation directory (default: /opt/bambuddy)" + echo " --port PORT Port to listen on (default: 8000)" + echo " --bind ADDRESS Bind address: 0.0.0.0 (network) or 127.0.0.1 (local only)" + echo " --tz TIMEZONE Timezone (default: system timezone or UTC)" + echo " --data-dir PATH Data directory (default: INSTALL_PATH/data)" + echo " --log-dir PATH Log directory (default: INSTALL_PATH/logs)" + echo " --debug Enable debug mode" + echo " --log-level LEVEL Log level: DEBUG, INFO, WARNING, ERROR (default: INFO)" + echo " --no-service Skip systemd service setup (Linux only)" + echo " --set-system-tz Set system timezone to match (for unattended installs)" + echo " --yes, -y Non-interactive mode, accept defaults" + echo " --help, -h Show this help message" + echo "" + echo "Examples:" + echo " Interactive installation:" + echo " ./install.sh" + echo "" + echo " Unattended installation with custom settings:" + echo " ./install.sh --path /srv/bambuddy --port 3000 --tz America/New_York --yes" + echo "" + echo " Minimal unattended installation:" + echo " ./install.sh -y" + exit 0 +} + +# ----------------------------------------------------------------------------- +# System Detection +# ----------------------------------------------------------------------------- + +detect_os() { + if [[ "$OSTYPE" == "darwin"* ]]; then + OS_TYPE="macos" + PKG_MANAGER="brew" + return + fi + + if [[ -f /etc/os-release ]]; then + . /etc/os-release + case "$ID" in + ubuntu|debian|raspbian|linuxmint|pop) + OS_TYPE="debian" + PKG_MANAGER="apt" + ;; + fedora|rhel|centos|rocky|almalinux|ol) + OS_TYPE="rhel" + if command -v dnf &>/dev/null; then + PKG_MANAGER="dnf" + else + PKG_MANAGER="yum" + fi + ;; + arch|manjaro|endeavouros) + OS_TYPE="arch" + PKG_MANAGER="pacman" + ;; + opensuse*|sles) + OS_TYPE="suse" + PKG_MANAGER="zypper" + ;; + *) + log_error "Unsupported Linux distribution: $ID" + exit 1 + ;; + esac + else + log_error "Cannot detect operating system" + exit 1 + fi +} + +detect_python() { + # Try python3 first, then python + if command -v python3 &>/dev/null; then + PYTHON_CMD="python3" + elif command -v python &>/dev/null; then + local version + version=$(python --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1) + if [[ "$version" -ge 3 ]]; then + PYTHON_CMD="python" + fi + fi + + if [[ -z "$PYTHON_CMD" ]]; then + return 1 + fi + + # Check version >= 3.10 + local version + version=$($PYTHON_CMD -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') + local major minor + major=$(echo "$version" | cut -d'.' -f1) + minor=$(echo "$version" | cut -d'.' -f2) + + if [[ "$major" -lt 3 ]] || { [[ "$major" -eq 3 ]] && [[ "$minor" -lt 10 ]]; }; then + log_warn "Python $version found, but 3.10+ is required" + return 1 + fi + + log_success "Found Python $version" + return 0 +} + +detect_timezone() { + if [[ -n "$TIMEZONE" ]]; then + return 0 + fi + + # Try to get system timezone (with error handling for set -e) + TIMEZONE="" + if [[ -f /etc/timezone ]]; then + TIMEZONE=$(cat /etc/timezone 2>/dev/null) || true + fi + + if [[ -z "$TIMEZONE" ]] && [[ -L /etc/localtime ]]; then + TIMEZONE=$(readlink /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||') || true + fi + + if [[ -z "$TIMEZONE" ]] && command -v timedatectl &>/dev/null; then + TIMEZONE=$(timedatectl show --property=Timezone --value 2>/dev/null) || true + fi + + # Default to UTC if not found (use if/then to avoid set -e issue with &&) + if [[ -z "$TIMEZONE" ]]; then + TIMEZONE="UTC" + fi + return 0 +} + +# ----------------------------------------------------------------------------- +# Package Installation +# ----------------------------------------------------------------------------- + +install_dependencies() { + log_info "Installing system dependencies..." + + case "$PKG_MANAGER" in + apt) + sudo apt-get update + sudo apt-get install -y python3 python3-pip python3-venv git curl ffmpeg + ;; + dnf|yum) + sudo $PKG_MANAGER install -y python3 python3-pip git curl ffmpeg + ;; + pacman) + sudo pacman -Sy --noconfirm python python-pip git curl ffmpeg + ;; + zypper) + sudo zypper install -y python3 python3-pip git curl ffmpeg + ;; + brew) + # Check if Homebrew is installed + if ! command -v brew &>/dev/null; then + log_error "Homebrew not found. Please install it first: https://brew.sh" + exit 1 + fi + brew install python git curl ffmpeg + ;; + esac + + log_success "System dependencies installed" +} + +# ----------------------------------------------------------------------------- +# Installation Steps +# ----------------------------------------------------------------------------- + +create_user() { + if [[ "$OS_TYPE" == "macos" ]]; then + return # Skip user creation on macOS + fi + + if id "$SERVICE_USER" &>/dev/null; then + log_info "User '$SERVICE_USER' already exists" + return + fi + + log_info "Creating service user '$SERVICE_USER'..." + sudo useradd --system --shell /usr/sbin/nologin --home-dir "$INSTALL_PATH" "$SERVICE_USER" + log_success "Service user created" +} + +download_bambuddy() { + log_info "Downloading BamBuddy..." + + if [[ -d "$INSTALL_PATH/.git" ]]; then + log_info "Existing installation found, updating..." + # Add safe.directory to avoid "dubious ownership" error when running as root + git config --global --add safe.directory "$INSTALL_PATH" 2>/dev/null || true + cd "$INSTALL_PATH" + git fetch origin + git reset --hard origin/main + # Ensure correct ownership after update + sudo chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_PATH" 2>/dev/null || true + else + sudo mkdir -p "$INSTALL_PATH" + sudo chown "$SERVICE_USER:$SERVICE_USER" "$INSTALL_PATH" 2>/dev/null || true + git clone https://github.com/maziggy/bambuddy.git "$INSTALL_PATH" + sudo chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_PATH" 2>/dev/null || true + fi + + log_success "BamBuddy downloaded to $INSTALL_PATH" +} + +setup_virtualenv() { + log_info "Setting up Python virtual environment..." + + cd "$INSTALL_PATH" + + if [[ "$OS_TYPE" == "macos" ]]; then + $PYTHON_CMD -m venv venv + source venv/bin/activate + else + sudo -u "$SERVICE_USER" $PYTHON_CMD -m venv venv 2>/dev/null || $PYTHON_CMD -m venv venv + source venv/bin/activate + fi + + pip install --upgrade pip + pip install -r requirements.txt + + log_success "Virtual environment configured" +} + +check_node_version() { + # Returns 0 if Node.js 20+ is available, 1 otherwise + if ! command -v node &>/dev/null; then + return 1 + fi + + local version + version=$(node --version 2>/dev/null | sed 's/^v//') + local major + major=$(echo "$version" | cut -d'.' -f1) + + if [[ "$major" -ge 20 ]]; then + log_success "Found Node.js v$version" + return 0 + else + log_warn "Found Node.js v$version (need 20+)" + return 1 + fi +} + +install_nodejs() { + log_info "Installing Node.js 22..." + case "$PKG_MANAGER" in + apt) + # Remove old nodejs if present + sudo apt-get remove -y nodejs npm 2>/dev/null || true + curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - + sudo apt-get install -y nodejs + ;; + dnf|yum) + sudo $PKG_MANAGER remove -y nodejs npm 2>/dev/null || true + curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash - + sudo $PKG_MANAGER install -y nodejs + ;; + pacman) + sudo pacman -S --noconfirm nodejs npm + ;; + zypper) + sudo zypper install -y nodejs22 + ;; + brew) + brew install node@22 + brew link --overwrite node@22 + ;; + *) + log_error "Please install Node.js 20+ manually: https://nodejs.org/" + exit 1 + ;; + esac + # Refresh PATH + hash -r 2>/dev/null || true +} + +build_frontend() { + log_info "Building frontend..." + + cd "$INSTALL_PATH/frontend" + + # Check for Node.js 20+ + if ! check_node_version; then + install_nodejs + # Verify installation + if ! check_node_version; then + log_error "Failed to install Node.js 20+. Please install manually." + exit 1 + fi + fi + + npm ci + npm run build + + log_success "Frontend built" +} + +create_directories() { + log_info "Creating data directories..." + + sudo mkdir -p "$DATA_DIR" "$LOG_DIR" + + if [[ "$OS_TYPE" != "macos" ]]; then + sudo chown -R "$SERVICE_USER:$SERVICE_USER" "$DATA_DIR" "$LOG_DIR" + fi + + log_success "Directories created" +} + +create_env_file() { + log_info "Creating environment configuration..." + + local env_file="$INSTALL_PATH/.env" + + # Note: Only include settings recognized by the app's pydantic Settings class + # Other settings (PORT, BIND_ADDRESS, DATA_DIR, LOG_DIR, TZ) are set in systemd service + cat > /tmp/bambuddy.env << EOF +# BamBuddy Configuration +# Generated by install.sh on $(date) + +# Debug mode (true = verbose logging) +DEBUG=$DEBUG_MODE + +# Log level (only used when DEBUG=false) +# Options: DEBUG, INFO, WARNING, ERROR +LOG_LEVEL=$LOG_LEVEL + +# Enable file logging +LOG_TO_FILE=true +EOF + + sudo mv /tmp/bambuddy.env "$env_file" + if [[ "$OS_TYPE" != "macos" ]]; then + sudo chown "$SERVICE_USER:$SERVICE_USER" "$env_file" + fi + sudo chmod 600 "$env_file" + + log_success "Environment file created at $env_file" +} + +create_systemd_service() { + if [[ "$OS_TYPE" == "macos" ]] || [[ "$SKIP_SERVICE" == "true" ]]; then + return + fi + + log_info "Creating systemd service..." + + cat > /tmp/bambuddy.service << EOF +[Unit] +Description=BamBuddy - Bambu Lab Print Management +Documentation=https://github.com/maziggy/bambuddy +After=network.target + +[Service] +Type=simple +User=$SERVICE_USER +Group=$SERVICE_USER +WorkingDirectory=$INSTALL_PATH + +# App settings from .env file +EnvironmentFile=$INSTALL_PATH/.env + +# Service settings (not in .env to avoid pydantic validation errors) +Environment="DATA_DIR=$DATA_DIR" +Environment="LOG_DIR=$LOG_DIR" +Environment="TZ=$TIMEZONE" + +ExecStart=$INSTALL_PATH/venv/bin/uvicorn backend.app.main:app --host $BIND_ADDRESS --port $PORT +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal + +# Security hardening +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=$DATA_DIR $LOG_DIR $INSTALL_PATH + +[Install] +WantedBy=multi-user.target +EOF + + sudo mv /tmp/bambuddy.service /etc/systemd/system/bambuddy.service + sudo systemctl daemon-reload + + log_success "Systemd service created" + + if prompt_yes_no "Enable BamBuddy to start on boot?" "y"; then + sudo systemctl enable bambuddy + log_success "Service enabled" + fi + + if prompt_yes_no "Start BamBuddy now?" "y"; then + sudo systemctl start bambuddy + sleep 2 + if sudo systemctl is-active --quiet bambuddy; then + log_success "BamBuddy is running" + else + log_warn "Service may have failed to start. Check: sudo journalctl -u bambuddy -f" + fi + fi +} + +create_launchd_service() { + if [[ "$OS_TYPE" != "macos" ]] || [[ "$SKIP_SERVICE" == "true" ]]; then + return + fi + + log_info "Creating launchd service..." + + local plist_path="$HOME/Library/LaunchAgents/com.bambuddy.app.plist" + + cat > "$plist_path" << EOF + + + + + Label + com.bambuddy.app + ProgramArguments + + $INSTALL_PATH/venv/bin/uvicorn + backend.app.main:app + --host + $BIND_ADDRESS + --port + $PORT + + WorkingDirectory + $INSTALL_PATH + EnvironmentVariables + + DEBUG + $DEBUG_MODE + LOG_LEVEL + $LOG_LEVEL + DATA_DIR + $DATA_DIR + LOG_DIR + $LOG_DIR + TZ + $TIMEZONE + + RunAtLoad + + KeepAlive + + StandardOutPath + $LOG_DIR/bambuddy.log + StandardErrorPath + $LOG_DIR/bambuddy.error.log + + +EOF + + log_success "Launchd plist created at $plist_path" + + if prompt_yes_no "Load BamBuddy service now?" "y"; then + launchctl load "$plist_path" + sleep 2 + if launchctl list | grep -q "com.bambuddy.app"; then + log_success "BamBuddy is running" + else + log_warn "Service may have failed to start. Check: cat $LOG_DIR/bambuddy.error.log" + fi + fi +} + +# ----------------------------------------------------------------------------- +# Main Installation Flow +# ----------------------------------------------------------------------------- + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --path) + INSTALL_PATH="$2" + shift 2 + ;; + --port) + PORT="$2" + shift 2 + ;; + --bind) + BIND_ADDRESS="$2" + shift 2 + ;; + --tz) + TIMEZONE="$2" + shift 2 + ;; + --data-dir) + DATA_DIR="$2" + shift 2 + ;; + --log-dir) + LOG_DIR="$2" + shift 2 + ;; + --debug) + DEBUG_MODE="true" + shift + ;; + --log-level) + LOG_LEVEL="$2" + shift 2 + ;; + --no-service) + SKIP_SERVICE="true" + shift + ;; + --set-system-tz) + SET_SYSTEM_TZ="true" + shift + ;; + --yes|-y) + NON_INTERACTIVE="true" + shift + ;; + --help|-h) + show_help + ;; + *) + log_error "Unknown option: $1" + show_help + ;; + esac + done +} + +gather_config() { + echo "" + echo -e "${BOLD}Installation Configuration${NC}" + echo -e "${CYAN}─────────────────────────────────────────${NC}" + echo "" + + # Installation path + [[ -z "$INSTALL_PATH" ]] && prompt "Installation directory" "$DEFAULT_INSTALL_PATH" INSTALL_PATH + + # Port + [[ -z "$PORT" ]] && prompt "Port to listen on" "$DEFAULT_PORT" PORT + + # Bind address + if [[ -z "$BIND_ADDRESS" ]]; then + echo "" + echo "Network access:" + echo " 0.0.0.0 - Accessible from other devices on your network (recommended)" + echo " 127.0.0.1 - Only accessible from this machine" + prompt "Bind address" "$DEFAULT_BIND_ADDRESS" BIND_ADDRESS + fi + + # Timezone + detect_timezone + prompt "Timezone" "$TIMEZONE" TIMEZONE + + # Offer to set system timezone if different from current (skip if already set via --set-system-tz) + if [[ -z "$SET_SYSTEM_TZ" ]]; then + local current_tz + current_tz=$(timedatectl show --property=Timezone --value 2>/dev/null) || true + if [[ -n "$TIMEZONE" ]] && [[ "$TIMEZONE" != "$current_tz" ]]; then + # Default to "n" so unattended installs don't change system TZ unless --set-system-tz is used + if prompt_yes_no "Set system timezone to $TIMEZONE?" "n"; then + SET_SYSTEM_TZ="true" + else + SET_SYSTEM_TZ="false" + fi + else + SET_SYSTEM_TZ="false" + fi + fi + + # Data directory + [[ -z "$DATA_DIR" ]] && DATA_DIR="$INSTALL_PATH/data" + prompt "Data directory" "$DATA_DIR" DATA_DIR + + # Log directory + [[ -z "$LOG_DIR" ]] && LOG_DIR="$INSTALL_PATH/logs" + prompt "Log directory" "$LOG_DIR" LOG_DIR + + # Debug mode + if [[ -z "$DEBUG_MODE" ]]; then + if prompt_yes_no "Enable debug mode?" "n"; then + DEBUG_MODE="true" + else + DEBUG_MODE="false" + fi + fi + + # Log level + if [[ -z "$LOG_LEVEL" ]]; then + echo "" + echo "Log levels: DEBUG, INFO, WARNING, ERROR" + prompt "Log level" "$DEFAULT_LOG_LEVEL" LOG_LEVEL + fi + + # Confirm + echo "" + echo -e "${BOLD}Installation Summary${NC}" + echo -e "${CYAN}─────────────────────────────────────────${NC}" + echo -e " Install path: ${GREEN}$INSTALL_PATH${NC}" + echo -e " Port: ${GREEN}$PORT${NC}" + echo -e " Bind address: ${GREEN}$BIND_ADDRESS${NC}" + echo -e " Timezone: ${GREEN}$TIMEZONE${NC}" + echo -e " Data dir: ${GREEN}$DATA_DIR${NC}" + echo -e " Log dir: ${GREEN}$LOG_DIR${NC}" + echo -e " Debug mode: ${GREEN}$DEBUG_MODE${NC}" + echo -e " Log level: ${GREEN}$LOG_LEVEL${NC}" + echo "" + + if ! prompt_yes_no "Proceed with installation?" "y"; then + echo "Installation cancelled." + exit 0 + fi +} + +main() { + parse_args "$@" + print_banner + + # Check if running via pipe (curl | bash) - interactive mode won't work + if [[ ! -t 0 ]] && [[ "$NON_INTERACTIVE" != "true" ]]; then + log_error "Interactive mode requires a terminal." + log_info "When using 'curl | bash', you must use non-interactive mode:" + echo "" + echo " curl -fsSL URL | bash -s -- --yes" + echo "" + log_info "Or download and run directly:" + echo "" + echo " curl -fsSL URL -o install.sh && chmod +x install.sh && ./install.sh" + echo "" + exit 1 + fi + + # Check for root (we need sudo for some operations) + if [[ "$EUID" -eq 0 ]] && [[ "$OS_TYPE" != "macos" ]]; then + log_warn "Running as root. Consider using a regular user with sudo privileges." + fi + + # Detect system + log_info "Detecting system..." + detect_os + log_success "Detected: $OS_TYPE (package manager: $PKG_MANAGER)" + + # Check/install Python + if ! detect_python; then + log_info "Python 3.10+ not found, will install..." + fi + + # Gather configuration + gather_config + + # Install steps + echo "" + echo -e "${BOLD}Starting Installation${NC}" + echo -e "${CYAN}─────────────────────────────────────────${NC}" + echo "" + + install_dependencies + detect_python || { log_error "Failed to install Python"; exit 1; } + + # Set system timezone if requested + if [[ "$SET_SYSTEM_TZ" == "true" ]]; then + log_info "Setting system timezone to $TIMEZONE..." + if [[ "$OS_TYPE" == "macos" ]]; then + sudo systemsetup -settimezone "$TIMEZONE" 2>/dev/null || true + else + sudo timedatectl set-timezone "$TIMEZONE" 2>/dev/null || true + fi + log_success "System timezone set to $TIMEZONE" + fi + + if [[ "$OS_TYPE" != "macos" ]]; then + create_user + else + SERVICE_USER="$USER" + fi + + download_bambuddy + setup_virtualenv + build_frontend + create_directories + create_env_file + + if [[ "$OS_TYPE" == "macos" ]]; then + create_launchd_service + else + create_systemd_service + fi + + # Done! + echo "" + echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}║ ║${NC}" + echo -e "${GREEN}║ Installation Complete! ║${NC}" + echo -e "${GREEN}║ ║${NC}" + echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}" + echo "" + # Show appropriate URL based on bind address + if [[ "$BIND_ADDRESS" == "0.0.0.0" ]]; then + local ip_addr + ip_addr=$(hostname -I 2>/dev/null | awk '{print $1}') || ip_addr="" + echo -e " ${BOLD}Access BamBuddy:${NC} ${CYAN}http://localhost:$PORT${NC}" + echo -e " ${CYAN}http://$ip_addr:$PORT${NC} (from other devices)" + else + echo -e " ${BOLD}Access BamBuddy:${NC} ${CYAN}http://localhost:$PORT${NC}" + fi + echo "" + if [[ "$OS_TYPE" == "macos" ]]; then + echo -e " ${BOLD}Manage service:${NC}" + echo -e " Start: launchctl load ~/Library/LaunchAgents/com.bambuddy.app.plist" + echo -e " Stop: launchctl unload ~/Library/LaunchAgents/com.bambuddy.app.plist" + echo -e " Logs: tail -f $LOG_DIR/bambuddy.log" + else + echo -e " ${BOLD}Manage service:${NC}" + echo -e " Status: sudo systemctl status bambuddy" + echo -e " Start: sudo systemctl start bambuddy" + echo -e " Stop: sudo systemctl stop bambuddy" + echo -e " Logs: sudo journalctl -u bambuddy -f" + fi + echo "" + echo -e " ${BOLD}Update BamBuddy:${NC}" + echo -e " cd $INSTALL_PATH && git pull && source venv/bin/activate" + echo -e " pip install -r requirements.txt && cd frontend && npm ci && npm run build" + if [[ "$OS_TYPE" != "macos" ]]; then + echo -e " sudo systemctl restart bambuddy" + fi + echo "" + echo -e " ${BOLD}Documentation:${NC} ${CYAN}https://wiki.bambuddy.cool${NC}" + echo "" +} + +main "$@"