feat: Add comprehensive setup wizard foundation
Modeled after Calibr setup.sh pattern (~/src/calibr/scripts/setup.sh). Implemented (Foundation): - Platform detection (Ubuntu, Arch, macOS, Fedora) - Dependency checking and installation - Mode selection (Docker vs Native) - Interactive + non-interactive modes - Comprehensive logging (clean console + full trace to log file) - Common utility functions library (450+ lines) Features in common.sh: - Output formatting (colors, headers, success/error/warning) - User input (confirm, select_option) - Platform detection - Dependency checking (Docker, Node, pnpm, PostgreSQL) - Package installation (apt, pacman, dnf, brew) - Validation (URL, email, port, domain) - Secret generation (cryptographically secure) - .env file parsing and management - Port conflict detection - File backup with timestamps To Be Implemented (See scripts/README.md): - Complete configuration collection - .env generation with smart preservation - Port conflict detection - Password/secret generation - Authentik blueprint auto-configuration - Docker deployment execution - Post-install instructions Usage: ./scripts/setup.sh # Interactive ./scripts/setup.sh --help # Show options ./scripts/setup.sh --dry-run # Preview ./scripts/setup.sh --non-interactive # CI/CD Refs: Setup wizard issue (created) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
265
scripts/README.md
Normal file
265
scripts/README.md
Normal file
@@ -0,0 +1,265 @@
|
||||
# Mosaic Stack Setup Scripts
|
||||
|
||||
Comprehensive setup wizard for Mosaic Stack, inspired by the [Calibr setup pattern](https://github.com/noahwoltje/calibr).
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Interactive setup (recommended)
|
||||
./scripts/setup.sh
|
||||
|
||||
# Non-interactive Docker with SSO
|
||||
./scripts/setup.sh --non-interactive --mode docker --enable-sso --bundled-authentik
|
||||
|
||||
# Dry run to see what would happen
|
||||
./scripts/setup.sh --dry-run --mode docker
|
||||
```
|
||||
|
||||
## Current Status
|
||||
|
||||
**✅ Implemented (Foundation):**
|
||||
|
||||
- Platform detection (Ubuntu, Arch, macOS)
|
||||
- Dependency checking (Docker, Node.js, pnpm, PostgreSQL)
|
||||
- Dependency installation
|
||||
- Mode selection (Docker vs Native)
|
||||
- Argument parsing
|
||||
- Comprehensive logging
|
||||
- Common utility functions library
|
||||
|
||||
**🚧 To Be Implemented:**
|
||||
|
||||
- Complete configuration collection (SSO, Ollama, MoltBot, URLs)
|
||||
- .env file generation with smart preservation
|
||||
- Port conflict detection
|
||||
- Password/secret generation
|
||||
- Authentik blueprint auto-configuration
|
||||
- Docker deployment execution
|
||||
- Post-install instructions
|
||||
|
||||
See full implementation plan in [issue tracking](https://git.mosaicstack.dev/mosaic/stack/issues).
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
scripts/
|
||||
├── setup.sh # Main setup wizard (foundation complete)
|
||||
├── lib/
|
||||
│ ├── common.sh # Utility functions (complete)
|
||||
│ └── docker.sh # Docker-specific functions (TODO)
|
||||
└── templates/
|
||||
└── .env.template # Template for .env generation (TODO)
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Platform Detection
|
||||
|
||||
Automatically detects:
|
||||
|
||||
- Operating system (Ubuntu, Arch, Fedora, macOS)
|
||||
- Package manager (apt, pacman, dnf, brew)
|
||||
- Installed dependencies
|
||||
|
||||
### Dependency Management
|
||||
|
||||
Checks and optionally installs:
|
||||
|
||||
- **Docker mode:** Docker, Docker Compose
|
||||
- **Native mode:** Node.js 18+, pnpm, PostgreSQL
|
||||
|
||||
### Interactive Mode
|
||||
|
||||
User-friendly wizard with:
|
||||
|
||||
- Color-coded output
|
||||
- Clear prompts and defaults
|
||||
- Yes/no confirmations
|
||||
- Numbered menu selections
|
||||
|
||||
### Non-Interactive Mode
|
||||
|
||||
Suitable for CI/CD:
|
||||
|
||||
```bash
|
||||
./scripts/setup.sh \
|
||||
--non-interactive \
|
||||
--mode docker \
|
||||
--enable-sso \
|
||||
--bundled-authentik \
|
||||
--ollama-mode local
|
||||
```
|
||||
|
||||
### Dry Run
|
||||
|
||||
Preview changes without execution:
|
||||
|
||||
```bash
|
||||
./scripts/setup.sh --dry-run --mode docker
|
||||
```
|
||||
|
||||
## Common Functions (lib/common.sh)
|
||||
|
||||
### Output Functions
|
||||
|
||||
- `print_header()` - Section headers
|
||||
- `print_success()` - Success messages (green ✓)
|
||||
- `print_error()` - Error messages (red ✗)
|
||||
- `print_warning()` - Warnings (yellow ⚠)
|
||||
- `print_info()` - Info messages (blue ℹ)
|
||||
- `print_step()` - Step indicators (cyan →)
|
||||
|
||||
### User Input
|
||||
|
||||
- `confirm()` - Yes/no prompts with defaults
|
||||
- `select_option()` - Numbered menu selection
|
||||
|
||||
### Platform Detection
|
||||
|
||||
- `detect_os()` - Detect operating system
|
||||
- `detect_package_manager()` - Detect package manager
|
||||
- `get_os_name()` - Human-readable OS name
|
||||
|
||||
### Dependency Checking
|
||||
|
||||
- `check_command()` - Check if command exists
|
||||
- `check_docker()` - Check Docker installation and daemon
|
||||
- `check_docker_compose()` - Check Docker Compose
|
||||
- `check_node()` - Check Node.js version
|
||||
- `check_pnpm()` - Check pnpm installation
|
||||
- `check_postgres()` - Check PostgreSQL
|
||||
|
||||
### Package Installation
|
||||
|
||||
- `get_package_name()` - Get package name for specific manager
|
||||
- `install_package()` - Install package via detected manager
|
||||
- `check_sudo()` - Check/request sudo access
|
||||
|
||||
### Validation
|
||||
|
||||
- `validate_url()` - Validate URL format
|
||||
- `validate_email()` - Validate email format
|
||||
- `validate_port()` - Validate port number (1-65535)
|
||||
- `validate_domain()` - Validate domain name
|
||||
|
||||
### Secret Generation
|
||||
|
||||
- `generate_secret()` - Cryptographically secure random (with special chars)
|
||||
- `generate_password()` - User-friendly password (alphanumeric only)
|
||||
- `mask_value()` - Mask sensitive values for display
|
||||
- `is_placeholder()` - Detect placeholder values
|
||||
|
||||
### .env Management
|
||||
|
||||
- `parse_env_file()` - Parse .env into associative array
|
||||
- `get_env_value()` - Get value from parsed env
|
||||
- `set_env_value()` - Set value in env array
|
||||
|
||||
### File Operations
|
||||
|
||||
- `backup_file()` - Create timestamped backup
|
||||
|
||||
### Port Checking
|
||||
|
||||
- `check_port_in_use()` - Check if port is in use
|
||||
- `suggest_alternative_port()` - Suggest alternative if conflict
|
||||
|
||||
## Logging
|
||||
|
||||
All output is logged to `logs/setup-YYYYMMDD_HHMMSS.log`:
|
||||
|
||||
- Clean output to console
|
||||
- Full trace (set -x) to log file only
|
||||
- Errors and warnings to both
|
||||
|
||||
## CLI Options
|
||||
|
||||
```
|
||||
--non-interactive Run without prompts (requires all options)
|
||||
--dry-run Show what would happen without executing
|
||||
--mode MODE Deployment mode: docker or native
|
||||
--enable-sso Enable Authentik SSO (Docker mode only)
|
||||
--bundled-authentik Use bundled Authentik server (with --enable-sso)
|
||||
--external-authentik URL Use external Authentik server URL
|
||||
--ollama-mode MODE Ollama mode: local, remote, disabled (default)
|
||||
--ollama-url URL Ollama server URL (if remote)
|
||||
--enable-moltbot Enable MoltBot integration
|
||||
--base-url URL Mosaic base URL (e.g., https://mosaic.example.com)
|
||||
--help Show help
|
||||
```
|
||||
|
||||
## Next Steps for Implementation
|
||||
|
||||
### 1. Configuration Collection (High Priority)
|
||||
|
||||
- [ ] URL and domain configuration
|
||||
- [ ] SSO setup (bundled vs external Authentik)
|
||||
- [ ] Ollama configuration (local/remote/disabled)
|
||||
- [ ] MoltBot toggle
|
||||
- [ ] Existing config detection and smart updates
|
||||
|
||||
### 2. .env Generation (High Priority)
|
||||
|
||||
- [ ] Create .env template
|
||||
- [ ] Generate passwords/secrets securely
|
||||
- [ ] Preserve existing non-placeholder values
|
||||
- [ ] Apply port overrides
|
||||
- [ ] Write .admin-credentials file
|
||||
|
||||
### 3. Port Conflict Detection
|
||||
|
||||
- [ ] Check: 3000, 3001 (web), 4000 (API), 5432 (PostgreSQL), 6379 (Valkey), 9000/9443 (Authentik)
|
||||
- [ ] Suggest alternatives automatically
|
||||
- [ ] Update .env with overrides
|
||||
|
||||
### 4. Authentik Blueprint Auto-Configuration
|
||||
|
||||
- [ ] Copy blueprint to authentik-blueprints/
|
||||
- [ ] Create docker-compose.authentik.yml overlay
|
||||
- [ ] Mount blueprint volume
|
||||
- [ ] Document post-install steps (initial setup, client secret)
|
||||
|
||||
### 5. Deployment Execution
|
||||
|
||||
- [ ] Docker: docker compose up -d
|
||||
- [ ] Native: pnpm install, database setup, migrations
|
||||
- [ ] Health checks
|
||||
|
||||
### 6. Post-Install Instructions
|
||||
|
||||
- [ ] Show URLs (web, API, Authentik)
|
||||
- [ ] Show credentials location
|
||||
- [ ] Next steps checklist
|
||||
- [ ] Troubleshooting tips
|
||||
|
||||
### 7. Testing
|
||||
|
||||
- [ ] Test on Ubuntu, Arch, macOS
|
||||
- [ ] Test interactive mode
|
||||
- [ ] Test non-interactive mode
|
||||
- [ ] Test dry-run mode
|
||||
- [ ] Test existing .env preservation
|
||||
- [ ] Test port conflict handling
|
||||
|
||||
## Contributing
|
||||
|
||||
When extending this setup script:
|
||||
|
||||
1. Follow the Calibr pattern for consistency
|
||||
2. Add utility functions to `lib/common.sh`
|
||||
3. Keep main `setup.sh` focused on orchestration
|
||||
4. Test both interactive and non-interactive modes
|
||||
5. Update this README with new features
|
||||
|
||||
## Reference
|
||||
|
||||
Original pattern: `~/src/calibr/scripts/setup.sh` (1,327 lines)
|
||||
|
||||
Key patterns to follow:
|
||||
|
||||
- Comprehensive error handling
|
||||
- Clear user communication
|
||||
- Smart existing config detection
|
||||
- Secure secret generation
|
||||
- Dry-run capability
|
||||
- Both interactive and non-interactive modes
|
||||
510
scripts/lib/common.sh
Normal file
510
scripts/lib/common.sh
Normal file
@@ -0,0 +1,510 @@
|
||||
#!/bin/bash
|
||||
# Common utility functions for Mosaic Stack setup
|
||||
# Adapted from Calibr setup pattern
|
||||
|
||||
# ============================================================================
|
||||
# Output Formatting
|
||||
# ============================================================================
|
||||
|
||||
# Colors
|
||||
if [[ -t 1 ]]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m' # No Color
|
||||
else
|
||||
RED=''
|
||||
GREEN=''
|
||||
YELLOW=''
|
||||
BLUE=''
|
||||
CYAN='\033[0;36m'
|
||||
BOLD=''
|
||||
NC=''
|
||||
fi
|
||||
|
||||
print_header() {
|
||||
echo ""
|
||||
echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${BOLD}${CYAN} $1${NC}"
|
||||
echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗${NC} $1" >&2
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠${NC} $1"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ${NC} $1"
|
||||
}
|
||||
|
||||
print_step() {
|
||||
echo -e "${CYAN}→${NC} $1"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# User Input
|
||||
# ============================================================================
|
||||
|
||||
confirm() {
|
||||
local prompt="$1"
|
||||
local default="${2:-n}"
|
||||
local response
|
||||
|
||||
if [[ "$default" == "y" ]]; then
|
||||
prompt="$prompt [Y/n]: "
|
||||
else
|
||||
prompt="$prompt [y/N]: "
|
||||
fi
|
||||
|
||||
while true; do
|
||||
read -r -p "$prompt" response
|
||||
response=${response:-$default}
|
||||
case "$response" in
|
||||
[Yy]|[Yy][Ee][Ss])
|
||||
return 0
|
||||
;;
|
||||
[Nn]|[Nn][Oo])
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
echo "Please answer yes or no."
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
select_option() {
|
||||
local prompt="$1"
|
||||
shift
|
||||
local options=("$@")
|
||||
local num_options=${#options[@]}
|
||||
|
||||
echo "$prompt"
|
||||
for i in "${!options[@]}"; do
|
||||
printf " %d) %s\n" "$((i + 1))" "${options[$i]}"
|
||||
done
|
||||
echo ""
|
||||
|
||||
local selection
|
||||
while true; do
|
||||
read -r -p "Enter selection [1-$num_options]: " selection
|
||||
if [[ "$selection" =~ ^[0-9]+$ ]] && \
|
||||
[ "$selection" -ge 1 ] && \
|
||||
[ "$selection" -le "$num_options" ]; then
|
||||
echo "${options[$((selection - 1))]}"
|
||||
return 0
|
||||
else
|
||||
print_error "Invalid selection. Please enter a number between 1 and $num_options."
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Platform Detection
|
||||
# ============================================================================
|
||||
|
||||
detect_os() {
|
||||
case "$OSTYPE" in
|
||||
linux-gnu*)
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
source /etc/os-release
|
||||
case "$ID" in
|
||||
ubuntu|debian)
|
||||
echo "debian"
|
||||
;;
|
||||
arch|manjaro|endeavouros)
|
||||
echo "arch"
|
||||
;;
|
||||
fedora|rhel|centos)
|
||||
echo "fedora"
|
||||
;;
|
||||
*)
|
||||
echo "linux"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
echo "linux"
|
||||
fi
|
||||
;;
|
||||
darwin*)
|
||||
echo "macos"
|
||||
;;
|
||||
*)
|
||||
echo "unknown"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
detect_package_manager() {
|
||||
local os="$1"
|
||||
|
||||
case "$os" in
|
||||
debian)
|
||||
echo "apt"
|
||||
;;
|
||||
arch)
|
||||
echo "pacman"
|
||||
;;
|
||||
fedora)
|
||||
echo "dnf"
|
||||
;;
|
||||
macos)
|
||||
if command -v brew >/dev/null 2>&1; then
|
||||
echo "brew"
|
||||
else
|
||||
echo "none"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "unknown"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_os_name() {
|
||||
local os="$1"
|
||||
|
||||
case "$os" in
|
||||
debian)
|
||||
echo "Debian/Ubuntu"
|
||||
;;
|
||||
arch)
|
||||
echo "Arch Linux"
|
||||
;;
|
||||
fedora)
|
||||
echo "Fedora/RHEL"
|
||||
;;
|
||||
macos)
|
||||
echo "macOS"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown OS"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Command and Dependency Checking
|
||||
# ============================================================================
|
||||
|
||||
check_command() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
check_docker() {
|
||||
check_command docker && docker info >/dev/null 2>&1
|
||||
}
|
||||
|
||||
check_docker_compose() {
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
return 0
|
||||
elif check_command docker-compose; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_docker_buildx() {
|
||||
docker buildx version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
check_node() {
|
||||
local min_version="${1:-18}"
|
||||
if ! check_command node; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local node_version
|
||||
node_version=$(node --version | sed 's/v//' | cut -d. -f1)
|
||||
|
||||
if [[ "$node_version" -ge "$min_version" ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_pnpm() {
|
||||
check_command pnpm
|
||||
}
|
||||
|
||||
check_postgres() {
|
||||
check_command psql
|
||||
}
|
||||
|
||||
check_sudo() {
|
||||
if ! command -v sudo >/dev/null 2>&1; then
|
||||
print_error "sudo is not installed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! sudo -n true 2>/dev/null; then
|
||||
print_warning "This script may need elevated privileges"
|
||||
print_info "You may be prompted for your password"
|
||||
sudo -v
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Package Installation
|
||||
# ============================================================================
|
||||
|
||||
get_package_name() {
|
||||
local pkg_manager="$1"
|
||||
local package="$2"
|
||||
|
||||
case "$pkg_manager" in
|
||||
apt)
|
||||
case "$package" in
|
||||
docker) echo "docker.io" ;;
|
||||
docker-compose) echo "docker-compose" ;;
|
||||
node) echo "nodejs" ;;
|
||||
postgres) echo "postgresql postgresql-contrib" ;;
|
||||
*) echo "$package" ;;
|
||||
esac
|
||||
;;
|
||||
pacman)
|
||||
case "$package" in
|
||||
docker) echo "docker" ;;
|
||||
docker-compose) echo "docker-compose" ;;
|
||||
node) echo "nodejs" ;;
|
||||
postgres) echo "postgresql" ;;
|
||||
*) echo "$package" ;;
|
||||
esac
|
||||
;;
|
||||
dnf)
|
||||
case "$package" in
|
||||
docker) echo "docker" ;;
|
||||
docker-compose) echo "docker-compose" ;;
|
||||
node) echo "nodejs" ;;
|
||||
postgres) echo "postgresql-server" ;;
|
||||
*) echo "$package" ;;
|
||||
esac
|
||||
;;
|
||||
brew)
|
||||
case "$package" in
|
||||
docker) echo "docker" ;;
|
||||
node) echo "node" ;;
|
||||
postgres) echo "postgresql@17" ;;
|
||||
*) echo "$package" ;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
echo "$package"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
install_package() {
|
||||
local pkg_manager="$1"
|
||||
local package="$2"
|
||||
|
||||
print_step "Installing $package..."
|
||||
|
||||
case "$pkg_manager" in
|
||||
apt)
|
||||
sudo apt update && sudo apt install -y "$package"
|
||||
;;
|
||||
pacman)
|
||||
sudo pacman -Sy --noconfirm "$package"
|
||||
;;
|
||||
dnf)
|
||||
sudo dnf install -y "$package"
|
||||
;;
|
||||
brew)
|
||||
brew install "$package"
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown package manager: $pkg_manager"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Validation Functions
|
||||
# ============================================================================
|
||||
|
||||
validate_url() {
|
||||
local url="$1"
|
||||
if [[ "$url" =~ ^https?://[a-zA-Z0-9.-]+(:[0-9]+)?(/.*)?$ ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
validate_email() {
|
||||
local email="$1"
|
||||
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
validate_port() {
|
||||
local port="$1"
|
||||
if [[ "$port" =~ ^[0-9]+$ ]] && [ "$port" -ge 1 ] && [ "$port" -le 65535 ]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
validate_domain() {
|
||||
local domain="$1"
|
||||
if [[ "$domain" =~ ^[a-zA-Z0-9][a-zA-Z0-9.-]{0,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$ ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Secret and Password Generation
|
||||
# ============================================================================
|
||||
|
||||
generate_secret() {
|
||||
local length="${1:-32}"
|
||||
# Use /dev/urandom for cryptographically secure random
|
||||
LC_ALL=C tr -dc 'A-Za-z0-9!@#$%^&*()-_=+' </dev/urandom | head -c "$length"
|
||||
}
|
||||
|
||||
generate_password() {
|
||||
local length="${1:-16}"
|
||||
# Passwords without special chars for easier manual entry
|
||||
LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c "$length"
|
||||
}
|
||||
|
||||
mask_value() {
|
||||
local value="$1"
|
||||
local length=${#value}
|
||||
|
||||
if [ "$length" -le 8 ]; then
|
||||
echo "***${value: -2}"
|
||||
else
|
||||
echo "${value:0:3}...${value: -3}"
|
||||
fi
|
||||
}
|
||||
|
||||
is_placeholder() {
|
||||
local value="$1"
|
||||
|
||||
# Check for common placeholder patterns
|
||||
if [[ "$value" =~ ^\$\{.*\}$ ]] || \
|
||||
[[ "$value" =~ ^(change-me|changeme|your-.*|example|placeholder|TODO|FIXME|xxx+)$ ]] || \
|
||||
[[ "$value" =~ ^<.*>$ ]] || \
|
||||
[[ -z "$value" ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# .env File Management
|
||||
# ============================================================================
|
||||
|
||||
# Global associative array to store env values
|
||||
declare -gA ENV_VALUES
|
||||
|
||||
parse_env_file() {
|
||||
local env_file="$1"
|
||||
|
||||
if [[ ! -f "$env_file" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Clear existing values
|
||||
ENV_VALUES=()
|
||||
|
||||
while IFS='=' read -r key value || [[ -n "$key" ]]; do
|
||||
# Skip comments and empty lines
|
||||
[[ "$key" =~ ^#.*$ ]] && continue
|
||||
[[ -z "$key" ]] && continue
|
||||
|
||||
# Remove leading/trailing whitespace
|
||||
key=$(echo "$key" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
||||
value=$(echo "$value" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
||||
|
||||
# Remove quotes if present
|
||||
value="${value%\"}"
|
||||
value="${value#\"}"
|
||||
value="${value%\'}"
|
||||
value="${value#\'}"
|
||||
|
||||
ENV_VALUES["$key"]="$value"
|
||||
done < "$env_file"
|
||||
}
|
||||
|
||||
get_env_value() {
|
||||
local key="$1"
|
||||
echo "${ENV_VALUES[$key]:-}"
|
||||
}
|
||||
|
||||
set_env_value() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
ENV_VALUES["$key"]="$value"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# File Operations
|
||||
# ============================================================================
|
||||
|
||||
backup_file() {
|
||||
local file="$1"
|
||||
if [[ -f "$file" ]]; then
|
||||
local backup="${file}.bak.$(date +%Y%m%d_%H%M%S)"
|
||||
cp "$file" "$backup"
|
||||
print_success "Backed up $file to $backup"
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Port Checking
|
||||
# ============================================================================
|
||||
|
||||
check_port_in_use() {
|
||||
local port="$1"
|
||||
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
ss -tuln | grep -q ":${port} "
|
||||
elif command -v netstat >/dev/null 2>&1; then
|
||||
netstat -tuln | grep -q ":${port} "
|
||||
elif command -v lsof >/dev/null 2>&1; then
|
||||
lsof -i ":${port}" >/dev/null 2>&1
|
||||
else
|
||||
# Can't check, assume available
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
suggest_alternative_port() {
|
||||
local base_port="$1"
|
||||
local offset=100
|
||||
|
||||
for i in {1..10}; do
|
||||
local alt_port=$((base_port + offset * i))
|
||||
if ! check_port_in_use "$alt_port"; then
|
||||
echo "$alt_port"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
echo "$((base_port + 10000))"
|
||||
}
|
||||
480
scripts/setup.sh
Executable file
480
scripts/setup.sh
Executable file
@@ -0,0 +1,480 @@
|
||||
#!/bin/bash
|
||||
# Mosaic Stack Setup Wizard
|
||||
# Interactive installer for Mosaic Stack personal assistant platform
|
||||
# Supports Docker and native deployments
|
||||
|
||||
set -e
|
||||
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# Set up logging
|
||||
LOG_DIR="$PROJECT_ROOT/logs"
|
||||
mkdir -p "$LOG_DIR"
|
||||
LOG_FILE="$LOG_DIR/setup-$(date +%Y%m%d_%H%M%S).log"
|
||||
|
||||
# Redirect stdout/stderr to both console and log file
|
||||
exec > >(tee -a "$LOG_FILE") 2>&1
|
||||
|
||||
# Send trace output (set -x) ONLY to log file on fd 3
|
||||
exec 3>>"$LOG_FILE"
|
||||
export BASH_XTRACEFD=3
|
||||
|
||||
# Enable verbose command tracing (only goes to log file now)
|
||||
set -x
|
||||
|
||||
echo "==================================================================="
|
||||
echo "Mosaic Stack Setup Wizard"
|
||||
echo "Started: $(date)"
|
||||
echo "Full log: $LOG_FILE"
|
||||
echo "==================================================================="
|
||||
echo ""
|
||||
|
||||
# Source common functions
|
||||
# shellcheck source=lib/common.sh
|
||||
source "$SCRIPT_DIR/lib/common.sh"
|
||||
|
||||
# ============================================================================
|
||||
# Global Variables
|
||||
# ============================================================================
|
||||
|
||||
NON_INTERACTIVE=false
|
||||
DRY_RUN=false
|
||||
MODE=""
|
||||
ENABLE_SSO=false
|
||||
USE_BUNDLED_AUTHENTIK=false
|
||||
EXTERNAL_AUTHENTIK_URL=""
|
||||
OLLAMA_MODE="disabled"
|
||||
OLLAMA_URL=""
|
||||
ENABLE_MOLTBOT=false
|
||||
MOSAIC_BASE_URL=""
|
||||
MOSAIC_ALLOWED_HOSTS=""
|
||||
AUTHENTIK_BASE_URL=""
|
||||
|
||||
DETECTED_OS=""
|
||||
DETECTED_PKG_MANAGER=""
|
||||
PORT_OVERRIDES=()
|
||||
|
||||
# ============================================================================
|
||||
# Help and Usage
|
||||
# ============================================================================
|
||||
|
||||
show_help() {
|
||||
cat << EOF
|
||||
Mosaic Stack Setup Wizard
|
||||
|
||||
USAGE:
|
||||
$0 [OPTIONS]
|
||||
|
||||
OPTIONS:
|
||||
-h, --help Show this help message
|
||||
--non-interactive Run in non-interactive mode (requires all options)
|
||||
--dry-run Show what would happen without executing
|
||||
--mode MODE Deployment mode: docker or native
|
||||
--enable-sso Enable Authentik SSO (Docker mode only)
|
||||
--bundled-authentik Use bundled Authentik server (with --enable-sso)
|
||||
--external-authentik URL Use external Authentik server URL (with --enable-sso)
|
||||
--ollama-mode MODE Ollama mode: local, remote, disabled (default: disabled)
|
||||
--ollama-url URL Ollama server URL (if --ollama-mode remote)
|
||||
--enable-moltbot Enable MoltBot integration
|
||||
--base-url URL Mosaic base URL (e.g., https://mosaic.example.com)
|
||||
|
||||
EXAMPLES:
|
||||
# Interactive mode (recommended for first-time setup)
|
||||
$0
|
||||
|
||||
# Non-interactive Docker deployment with bundled SSO
|
||||
$0 --non-interactive --mode docker --enable-sso --bundled-authentik
|
||||
|
||||
# Non-interactive Docker with external Authentik and local Ollama
|
||||
$0 --non-interactive --mode docker --enable-sso --external-authentik "https://auth.example.com" --ollama-mode local
|
||||
|
||||
# Dry run to see what would happen
|
||||
$0 --dry-run --mode docker --enable-sso --bundled-authentik
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Argument Parsing
|
||||
# ============================================================================
|
||||
|
||||
parse_arguments() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
--non-interactive)
|
||||
NON_INTERACTIVE=true
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
;;
|
||||
--mode)
|
||||
MODE="$2"
|
||||
shift
|
||||
;;
|
||||
--enable-sso)
|
||||
ENABLE_SSO=true
|
||||
;;
|
||||
--bundled-authentik)
|
||||
USE_BUNDLED_AUTHENTIK=true
|
||||
;;
|
||||
--external-authentik)
|
||||
EXTERNAL_AUTHENTIK_URL="$2"
|
||||
shift
|
||||
;;
|
||||
--ollama-mode)
|
||||
OLLAMA_MODE="$2"
|
||||
shift
|
||||
;;
|
||||
--ollama-url)
|
||||
OLLAMA_URL="$2"
|
||||
shift
|
||||
;;
|
||||
--enable-moltbot)
|
||||
ENABLE_MOLTBOT=true
|
||||
;;
|
||||
--base-url)
|
||||
MOSAIC_BASE_URL="$2"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown option: $1"
|
||||
echo "Use --help for usage information"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Validate non-interactive mode
|
||||
if [[ "$NON_INTERACTIVE" == true ]]; then
|
||||
if [[ -z "$MODE" ]]; then
|
||||
print_error "Non-interactive mode requires --mode"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$MODE" != "native" && "$MODE" != "docker" ]]; then
|
||||
print_error "Invalid mode: $MODE (must be 'native' or 'docker')"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$OLLAMA_MODE" == "remote" && -z "$OLLAMA_URL" ]]; then
|
||||
print_error "Remote Ollama mode requires --ollama-url"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Welcome Banner
|
||||
# ============================================================================
|
||||
|
||||
show_banner() {
|
||||
if [[ "$NON_INTERACTIVE" == true ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
cat << "EOF"
|
||||
|
||||
__ __ _ ____ _ _
|
||||
| \/ | ___ ___ __ _(_) ___| _ \| |_ __ _ ___| | __
|
||||
| |\/| |/ _ \/ __|/ _` | |/ __| |_) | __/ _` |/ __| |/ /
|
||||
| | | | (_) \__ \ (_| | | (__| __/| || (_| | (__| <
|
||||
|_| |_|\___/|___/\__,_|_|\___|_| \__\__,_|\___|_|\_\
|
||||
|
||||
Multi-Tenant Personal Assistant Platform
|
||||
|
||||
EOF
|
||||
|
||||
echo "Welcome to the Mosaic Stack Setup Wizard!"
|
||||
echo ""
|
||||
echo "This wizard will guide you through setting up Mosaic Stack on your system."
|
||||
echo "You can choose between Docker (production) or native (development) deployment,"
|
||||
echo "and configure optional features like Authentik SSO and Ollama LLM integration."
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Platform Detection
|
||||
# ============================================================================
|
||||
|
||||
detect_platform() {
|
||||
print_header "Detecting Platform"
|
||||
|
||||
DETECTED_OS=$(detect_os)
|
||||
DETECTED_PKG_MANAGER=$(detect_package_manager "$DETECTED_OS")
|
||||
|
||||
local os_name
|
||||
os_name=$(get_os_name "$DETECTED_OS")
|
||||
|
||||
if [[ "$DETECTED_OS" == "unknown" ]]; then
|
||||
print_warning "Could not detect operating system"
|
||||
print_info "Detected OS type: $OSTYPE"
|
||||
echo ""
|
||||
if [[ "$NON_INTERACTIVE" == true ]]; then
|
||||
print_error "Cannot proceed in non-interactive mode on unknown OS"
|
||||
exit 1
|
||||
fi
|
||||
if ! confirm "Continue anyway?"; then
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_success "Detected: $os_name ($DETECTED_PKG_MANAGER)"
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Mode Selection
|
||||
# ============================================================================
|
||||
|
||||
select_deployment_mode() {
|
||||
if [[ -n "$MODE" ]]; then
|
||||
print_header "Deployment Mode"
|
||||
print_info "Using mode: $MODE"
|
||||
return
|
||||
fi
|
||||
|
||||
print_header "Deployment Mode"
|
||||
echo ""
|
||||
echo "How would you like to run Mosaic Stack?"
|
||||
echo ""
|
||||
echo " 1) Docker (Recommended)"
|
||||
echo " - Best for production deployment"
|
||||
echo " - Isolated environment with all dependencies"
|
||||
echo " - Includes PostgreSQL, Valkey, all services"
|
||||
echo " - Optional Authentik SSO integration"
|
||||
echo " - Turnkey deployment"
|
||||
echo ""
|
||||
echo " 2) Native"
|
||||
echo " - Best for development"
|
||||
echo " - Runs directly on your system"
|
||||
echo " - Requires manual dependency installation"
|
||||
echo " - Easier to debug and modify"
|
||||
echo ""
|
||||
|
||||
local selection
|
||||
selection=$(select_option "Select deployment mode:" \
|
||||
"Docker (production, recommended)" \
|
||||
"Native (development)")
|
||||
|
||||
if [[ "$selection" == *"Docker"* ]]; then
|
||||
MODE="docker"
|
||||
else
|
||||
MODE="native"
|
||||
fi
|
||||
|
||||
print_success "Selected: $MODE mode"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Dependency Checking
|
||||
# ============================================================================
|
||||
|
||||
check_and_install_dependencies() {
|
||||
print_header "Checking Dependencies"
|
||||
|
||||
local missing_deps=()
|
||||
|
||||
if [[ "$MODE" == "docker" ]]; then
|
||||
if ! check_docker; then
|
||||
missing_deps+=("Docker")
|
||||
fi
|
||||
if ! check_docker_compose; then
|
||||
missing_deps+=("Docker Compose")
|
||||
fi
|
||||
else
|
||||
if ! check_node 18; then
|
||||
missing_deps+=("Node.js 18+")
|
||||
fi
|
||||
if ! check_pnpm; then
|
||||
missing_deps+=("pnpm")
|
||||
fi
|
||||
if ! check_postgres; then
|
||||
missing_deps+=("PostgreSQL")
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ${#missing_deps[@]} -eq 0 ]]; then
|
||||
print_success "All dependencies satisfied"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
print_warning "Missing dependencies:"
|
||||
for dep in "${missing_deps[@]}"; do
|
||||
echo " - $dep"
|
||||
done
|
||||
echo ""
|
||||
|
||||
if [[ "$NON_INTERACTIVE" == true ]]; then
|
||||
print_error "Dependency check failed in non-interactive mode"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if confirm "Would you like to install missing dependencies?"; then
|
||||
install_missing_dependencies
|
||||
else
|
||||
print_error "Cannot proceed without required dependencies"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_missing_dependencies() {
|
||||
print_header "Installing Dependencies"
|
||||
|
||||
check_sudo
|
||||
|
||||
if [[ "$MODE" == "docker" ]]; then
|
||||
# Install Docker
|
||||
if ! check_docker; then
|
||||
local docker_pkg
|
||||
docker_pkg=$(get_package_name "$DETECTED_PKG_MANAGER" "docker")
|
||||
if [[ -n "$docker_pkg" ]]; then
|
||||
install_package "$DETECTED_PKG_MANAGER" "$docker_pkg"
|
||||
|
||||
# Start Docker service
|
||||
case "$DETECTED_PKG_MANAGER" in
|
||||
pacman|dnf)
|
||||
print_step "Starting Docker service..."
|
||||
sudo systemctl enable --now docker
|
||||
;;
|
||||
esac
|
||||
|
||||
# Add user to docker group
|
||||
print_step "Adding user to docker group..."
|
||||
sudo usermod -aG docker "$USER"
|
||||
print_warning "You may need to log out and back in for docker group membership to take effect"
|
||||
print_info "Or run: newgrp docker"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install Docker Compose
|
||||
if ! check_docker_compose; then
|
||||
local compose_pkg
|
||||
compose_pkg=$(get_package_name "$DETECTED_PKG_MANAGER" "docker-compose")
|
||||
if [[ -n "$compose_pkg" ]]; then
|
||||
install_package "$DETECTED_PKG_MANAGER" "$compose_pkg"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Install Node.js
|
||||
if ! check_node 18; then
|
||||
local node_pkg
|
||||
node_pkg=$(get_package_name "$DETECTED_PKG_MANAGER" "node")
|
||||
if [[ -n "$node_pkg" ]]; then
|
||||
install_package "$DETECTED_PKG_MANAGER" "$node_pkg"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install pnpm
|
||||
if ! check_pnpm; then
|
||||
print_step "Installing pnpm via npm..."
|
||||
npm install -g pnpm
|
||||
fi
|
||||
|
||||
# Install PostgreSQL
|
||||
if ! check_postgres; then
|
||||
local postgres_pkg
|
||||
postgres_pkg=$(get_package_name "$DETECTED_PKG_MANAGER" "postgres")
|
||||
if [[ -n "$postgres_pkg" ]]; then
|
||||
install_package "$DETECTED_PKG_MANAGER" "$postgres_pkg"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Re-check dependencies
|
||||
echo ""
|
||||
local still_missing=false
|
||||
if [[ "$MODE" == "docker" ]]; then
|
||||
if ! check_docker || ! check_docker_compose; then
|
||||
still_missing=true
|
||||
fi
|
||||
else
|
||||
if ! check_node 18 || ! check_pnpm; then
|
||||
still_missing=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$still_missing" == true ]]; then
|
||||
print_error "Some dependencies are still missing after installation"
|
||||
print_info "You may need to install them manually"
|
||||
exit 1
|
||||
else
|
||||
print_success "All dependencies installed successfully"
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Configuration Collection
|
||||
# ============================================================================
|
||||
|
||||
load_existing_env() {
|
||||
if [[ -f "$PROJECT_ROOT/.env" ]]; then
|
||||
print_header "Detecting Existing Configuration"
|
||||
parse_env_file "$PROJECT_ROOT/.env"
|
||||
print_success "Found existing .env file"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
collect_configuration() {
|
||||
print_header "Configuration"
|
||||
|
||||
# Try to load existing .env
|
||||
local has_existing_env=false
|
||||
if load_existing_env; then
|
||||
has_existing_env=true
|
||||
echo ""
|
||||
print_info "Found existing configuration. I'll ask about each setting."
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Continue with remaining configuration...
|
||||
# (This would continue with URL config, SSO, Ollama, MoltBot, etc.)
|
||||
# Keeping this section shorter for now - can be expanded following Calibr pattern
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main Execution
|
||||
# ============================================================================
|
||||
|
||||
main() {
|
||||
parse_arguments "$@"
|
||||
|
||||
show_banner
|
||||
|
||||
detect_platform
|
||||
|
||||
select_deployment_mode
|
||||
|
||||
check_and_install_dependencies
|
||||
|
||||
collect_configuration
|
||||
|
||||
# TODO: generate_env_file
|
||||
# TODO: run_deployment
|
||||
# TODO: show_post_install_info
|
||||
|
||||
echo ""
|
||||
print_success "Setup complete (partial implementation)!"
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo "Setup completed: $(date)"
|
||||
echo "Full log saved to: $LOG_FILE"
|
||||
echo "==================================================================="
|
||||
echo ""
|
||||
print_info "This is a foundation setup script."
|
||||
print_info "Additional features (env generation, deployment) to be implemented."
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user