chore: add install scripts, doctor command, and AGENTS.md
All checks were successful
ci/woodpecker/push/infra Pipeline was successful
ci/woodpecker/push/api Pipeline was successful

- Add one-line installer (scripts/install.sh) with platform detection
- Add doctor command (scripts/commands/doctor.sh) for environment diagnostics
- Add shared libraries: dependencies, docker, platform, validation
- Update README with quick-start installer instructions
- Add AGENTS.md with codebase patterns for AI agent context

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 11:04:36 -06:00
parent 0ca3945061
commit ab52827d9c
8 changed files with 4290 additions and 4 deletions

834
scripts/install.sh Executable file
View File

@@ -0,0 +1,834 @@
#!/bin/bash
set -euo pipefail
# ============================================================================
# Mosaic Stack Installer
# ============================================================================
# Usage: curl -fsSL https://get.mosaicstack.dev | bash
#
# A comprehensive installer that "just works" across platforms.
# Automatically detects the OS, installs dependencies, and configures
# the system for running Mosaic Stack.
# Script version
INSTALLER_VERSION="1.0.0"
# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# Source library files
# shellcheck source=lib/platform.sh
source "$SCRIPT_DIR/lib/platform.sh"
# shellcheck source=lib/dependencies.sh
source "$SCRIPT_DIR/lib/dependencies.sh"
# shellcheck source=lib/docker.sh
source "$SCRIPT_DIR/lib/docker.sh"
# shellcheck source=lib/validation.sh
source "$SCRIPT_DIR/lib/validation.sh"
# Set up cleanup trap
setup_cleanup_trap
# ============================================================================
# Configuration
# ============================================================================
# Default values
NON_INTERACTIVE=false
DRY_RUN=false
VERBOSE=false
MODE=""
ENABLE_SSO=false
USE_BUNDLED_AUTHENTIK=false
EXTERNAL_AUTHENTIK_URL=""
OLLAMA_MODE="disabled"
OLLAMA_URL=""
MOSAIC_BASE_URL=""
COMPOSE_PROFILES="full"
SKIP_DEPS=false
NO_PORT_CHECK=false
# Ports (defaults, can be overridden)
WEB_PORT="${WEB_PORT:-3000}"
API_PORT="${API_PORT:-3001}"
POSTGRES_PORT="${POSTGRES_PORT:-5432}"
VALKEY_PORT="${VALKEY_PORT:-6379}"
# ============================================================================
# Taglines
# ============================================================================
TAGLINES=(
"Claws out, configs in — let's ship a calm, clean stack."
"Less yak-shaving, more uptime."
"Turnkey today, productive tonight."
"Ports resolved. Secrets sealed. Stack ready."
"All signal, no ceremony."
"Your .env is safe with me."
"One curl away from your personal AI assistant."
"Infrastructure that stays out of your way."
"From zero to AI assistant in under 5 minutes."
"Because you have better things to do than configure Docker."
)
pick_tagline() {
local count=${#TAGLINES[@]}
local idx=$((RANDOM % count))
echo "${TAGLINES[$idx]}"
}
TAGLINE=$(pick_tagline)
# ============================================================================
# Help and Usage
# ============================================================================
print_usage() {
cat << EOF
Mosaic Stack Installer v${INSTALLER_VERSION}
USAGE:
curl -fsSL https://get.mosaicstack.dev | bash
./install.sh [OPTIONS]
OPTIONS:
-h, --help Show this help message
--non-interactive Run without prompts (requires --mode)
--dry-run Preview changes without executing
--verbose Enable debug output
--mode MODE Deployment mode: docker or native
--enable-sso Enable Authentik SSO (Docker only)
--bundled-authentik Use bundled Authentik server
--external-authentik URL Use external Authentik server
--ollama-mode MODE Ollama: local, remote, disabled
--ollama-url URL Remote Ollama server URL
--base-url URL Mosaic base URL
--profiles PROFILES Docker Compose profiles (default: full)
--skip-deps Skip dependency installation
--no-port-check Skip port conflict detection
ENVIRONMENT VARIABLES:
All options can be set via environment variables:
MOSAIC_MODE, MOSAIC_ENABLE_SSO, MOSAIC_OLLAMA_MODE, etc.
EXAMPLES:
# Interactive installation (recommended)
curl -fsSL https://get.mosaicstack.dev | bash
# Non-interactive Docker deployment
curl -fsSL https://get.mosaicstack.dev | bash -s -- --non-interactive --mode docker
# With SSO and local Ollama
curl -fsSL https://get.mosaicstack.dev | bash -s -- \\
--mode docker \\
--enable-sso --bundled-authentik \\
--ollama-mode local
# Preview installation
curl -fsSL https://get.mosaicstack.dev | bash -s -- --dry-run
DOCUMENTATION:
https://docs.mosaicstack.dev
EOF
}
# ============================================================================
# Argument Parsing
# ============================================================================
parse_arguments() {
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
print_usage
exit 0
;;
--non-interactive)
NON_INTERACTIVE=true
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
--verbose)
VERBOSE=true
shift
;;
--mode)
if [[ -z "${2:-}" || "$2" == --* ]]; then
echo -e "${ERROR}Error: --mode requires a value (docker or native)${NC}"
exit 1
fi
MODE="$2"
shift 2
;;
--enable-sso)
ENABLE_SSO=true
shift
;;
--bundled-authentik)
USE_BUNDLED_AUTHENTIK=true
shift
;;
--external-authentik)
if [[ -z "${2:-}" || "$2" == --* ]]; then
echo -e "${ERROR}Error: --external-authentik requires a URL${NC}"
exit 1
fi
EXTERNAL_AUTHENTIK_URL="$2"
shift 2
;;
--ollama-mode)
if [[ -z "${2:-}" || "$2" == --* ]]; then
echo -e "${ERROR}Error: --ollama-mode requires a value (local, remote, disabled)${NC}"
exit 1
fi
OLLAMA_MODE="$2"
shift 2
;;
--ollama-url)
if [[ -z "${2:-}" || "$2" == --* ]]; then
echo -e "${ERROR}Error: --ollama-url requires a URL${NC}"
exit 1
fi
OLLAMA_URL="$2"
shift 2
;;
--base-url)
if [[ -z "${2:-}" || "$2" == --* ]]; then
echo -e "${ERROR}Error: --base-url requires a URL${NC}"
exit 1
fi
MOSAIC_BASE_URL="$2"
shift 2
;;
--profiles)
if [[ -z "${2:-}" || "$2" == --* ]]; then
echo -e "${ERROR}Error: --profiles requires a value${NC}"
exit 1
fi
COMPOSE_PROFILES="$2"
shift 2
;;
--skip-deps)
SKIP_DEPS=true
shift
;;
--no-port-check)
NO_PORT_CHECK=true
shift
;;
*)
echo -e "${ERROR}Error: Unknown option: $1${NC}"
echo "Use --help for usage information"
exit 1
;;
esac
done
# Validate non-interactive mode
if [[ "$NON_INTERACTIVE" == true ]]; then
if [[ -z "$MODE" ]]; then
echo -e "${ERROR}Error: Non-interactive mode requires --mode${NC}"
exit 1
fi
if [[ "$MODE" != "native" && "$MODE" != "docker" ]]; then
echo -e "${ERROR}Error: Invalid mode: $MODE (must be 'docker' or 'native')${NC}"
exit 1
fi
if [[ "$OLLAMA_MODE" == "remote" && -z "$OLLAMA_URL" ]]; then
echo -e "${ERROR}Error: Remote Ollama mode requires --ollama-url${NC}"
exit 1
fi
if [[ "$ENABLE_SSO" == true && "$USE_BUNDLED_AUTHENTIK" != true && -z "$EXTERNAL_AUTHENTIK_URL" ]]; then
echo -e "${ERROR}Error: SSO enabled but no Authentik configuration provided${NC}"
echo "Use --bundled-authentik or --external-authentik URL"
exit 1
fi
fi
}
# ============================================================================
# Banner
# ============================================================================
show_banner() {
echo ""
echo -e "${ACCENT}${BOLD}"
cat << "EOF"
__ __ _ ____ _ _
| \/ | ___ ___ __ _(_) ___ / ___| |_ __ _ ___| | __
| |\/| |/ _ \/ __|/ _` | |/ __|\___ | __/ _` |/ __| |/ /
| | | | (_) \__ \ (_| | | (__ ___/ | || (_| | (__| <
|_| |_|\___/|___/\__,_|_|\___|____/ \__\__,_|\___|_|\_\
EOF
echo -e "${NC}${MUTED} ${TAGLINE}${NC}"
echo ""
}
# ============================================================================
# Mode Selection
# ============================================================================
select_mode() {
if [[ -n "$MODE" ]]; then
return
fi
if [[ "$NON_INTERACTIVE" == true ]]; then
MODE="docker"
return
fi
echo -e "${BOLD}How would you like to run Mosaic Stack?${NC}"
echo ""
echo " 1) Docker (Recommended)"
echo " - Best for production deployment"
echo " - Isolated environment with all dependencies"
echo " - Includes PostgreSQL, Valkey, all services"
echo ""
echo " 2) Native"
echo " - Best for development"
echo " - Runs directly on your system"
echo " - Requires manual dependency installation"
echo ""
local selection
read -r -p "Select deployment mode [1-2]: " selection
case "$selection" in
1) MODE="docker" ;;
2) MODE="native" ;;
*)
echo -e "${INFO}i${NC} Defaulting to Docker mode"
MODE="docker"
;;
esac
echo ""
}
# ============================================================================
# Configuration Collection
# ============================================================================
collect_configuration() {
echo -e "${BOLD}Configuration${NC}"
echo ""
# Check for existing .env
if [[ -f "$PROJECT_ROOT/.env" ]]; then
echo -e "${SUCCESS}${NC} Found existing .env file"
if [[ "$NON_INTERACTIVE" != true ]]; then
read -r -p "Use existing configuration? [Y/n]: " use_existing
case "$use_existing" in
n|N)
echo -e "${INFO}i${NC} Will reconfigure..."
;;
*)
echo -e "${INFO}i${NC} Using existing configuration"
return
;;
esac
fi
fi
# Base URL
if [[ -z "$MOSAIC_BASE_URL" ]]; then
if [[ "$NON_INTERACTIVE" == true ]]; then
MOSAIC_BASE_URL="http://localhost:${WEB_PORT}"
else
echo -e "${INFO}i${NC} Base URL configuration"
echo " - Localhost: http://localhost:${WEB_PORT}"
echo " - Custom: Enter your domain URL"
read -r -p "Base URL [http://localhost:${WEB_PORT}]: " MOSAIC_BASE_URL
MOSAIC_BASE_URL="${MOSAIC_BASE_URL:-http://localhost:${WEB_PORT}}"
fi
fi
echo -e "${SUCCESS}${NC} Base URL: ${INFO}$MOSAIC_BASE_URL${NC}"
# SSO Configuration (Docker mode only)
if [[ "$MODE" == "docker" && "$ENABLE_SSO" != true ]]; then
if [[ "$NON_INTERACTIVE" != true ]]; then
echo ""
read -r -p "Enable Authentik SSO? [y/N]: " enable_sso
case "$enable_sso" in
y|Y)
ENABLE_SSO=true
read -r -p "Use bundled Authentik? [Y/n]: " bundled
case "$bundled" in
n|N)
read -r -p "External Authentik URL: " EXTERNAL_AUTHENTIK_URL
;;
*)
USE_BUNDLED_AUTHENTIK=true
;;
esac
;;
esac
fi
fi
# Ollama Configuration
if [[ "$OLLAMA_MODE" == "disabled" ]]; then
if [[ "$NON_INTERACTIVE" != true ]]; then
echo ""
echo -e "${INFO}i${NC} Ollama Configuration"
echo " 1) Local (bundled Ollama service)"
echo " 2) Remote (connect to existing Ollama)"
echo " 3) Disabled"
read -r -p "Ollama mode [1-3]: " ollama_choice
case "$ollama_choice" in
1) OLLAMA_MODE="local" ;;
2)
OLLAMA_MODE="remote"
read -r -p "Ollama URL: " OLLAMA_URL
;;
*) OLLAMA_MODE="disabled" ;;
esac
fi
fi
echo ""
}
# ============================================================================
# Environment File Generation
# ============================================================================
generate_secrets() {
echo -e "${WARN}${NC} Generating secrets..."
# Generate all required secrets
POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=' | head -c 32)
JWT_SECRET=$(openssl rand -base64 32)
BETTER_AUTH_SECRET=$(openssl rand -base64 32)
ENCRYPTION_KEY=$(openssl rand -hex 32)
AUTHENTIK_SECRET_KEY=$(openssl rand -base64 50)
AUTHENTIK_BOOTSTRAP_PASSWORD=$(openssl rand -base64 16 | tr -d '/+=' | head -c 16)
COORDINATOR_API_KEY=$(openssl rand -base64 32)
ORCHESTRATOR_API_KEY=$(openssl rand -base64 32)
GITEA_WEBHOOK_SECRET=$(openssl rand -hex 32)
echo -e "${SUCCESS}${NC} Secrets generated"
}
generate_env_file() {
local env_file="$PROJECT_ROOT/.env"
echo -e "${WARN}${NC} Generating .env file..."
# Parse base URL
local scheme="http"
local host="localhost"
local port="$WEB_PORT"
if [[ "$MOSAIC_BASE_URL" =~ ^(https?)://([^/:]+)(:([0-9]+))? ]]; then
scheme="${BASH_REMATCH[1]}"
host="${BASH_REMATCH[2]}"
port="${BASH_REMATCH[4]:-$WEB_PORT}"
fi
# Determine profiles
local profiles="$COMPOSE_PROFILES"
# Start with example file if it exists
if [[ -f "$PROJECT_ROOT/.env.example" ]]; then
cp "$PROJECT_ROOT/.env.example" "$env_file"
fi
# Write configuration
cat >> "$env_file" << EOF
# ==============================================
# Generated by Mosaic Stack Installer v${INSTALLER_VERSION}
# Generated at: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
# ==============================================
# Application Ports
WEB_PORT=$port
API_PORT=$API_PORT
POSTGRES_PORT=$POSTGRES_PORT
VALKEY_PORT=$VALKEY_PORT
# Web Configuration
NEXT_PUBLIC_APP_URL=$MOSAIC_BASE_URL
NEXT_PUBLIC_API_URL=${scheme}://${host}:${API_PORT}
# Database
DATABASE_URL=postgresql://mosaic:${POSTGRES_PASSWORD}@postgres:5432/mosaic
POSTGRES_PASSWORD=$POSTGRES_PASSWORD
# Authentication
JWT_SECRET=$JWT_SECRET
BETTER_AUTH_SECRET=$BETTER_AUTH_SECRET
# Encryption
ENCRYPTION_KEY=$ENCRYPTION_KEY
# Compose Profiles
COMPOSE_PROFILES=$profiles
EOF
# Add SSO configuration if enabled
if [[ "$ENABLE_SSO" == true ]]; then
cat >> "$env_file" << EOF
# Authentik SSO
OIDC_ENABLED=true
AUTHENTIK_SECRET_KEY=$AUTHENTIK_SECRET_KEY
AUTHENTIK_BOOTSTRAP_PASSWORD=$AUTHENTIK_BOOTSTRAP_PASSWORD
EOF
if [[ "$USE_BUNDLED_AUTHENTIK" == true ]]; then
echo "AUTHENTIK_PUBLIC_URL=http://localhost:\${AUTHENTIK_PORT_HTTP:-9000}" >> "$env_file"
else
echo "AUTHENTIK_PUBLIC_URL=$EXTERNAL_AUTHENTIK_URL" >> "$env_file"
fi
fi
# Add Ollama configuration if enabled
if [[ "$OLLAMA_MODE" != "disabled" ]]; then
cat >> "$env_file" << EOF
# Ollama
OLLAMA_MODE=$OLLAMA_MODE
EOF
if [[ "$OLLAMA_MODE" == "local" ]]; then
echo "OLLAMA_ENDPOINT=http://ollama:11434" >> "$env_file"
else
echo "OLLAMA_ENDPOINT=$OLLAMA_URL" >> "$env_file"
fi
fi
# Add API keys
cat >> "$env_file" << EOF
# API Keys
COORDINATOR_API_KEY=$COORDINATOR_API_KEY
ORCHESTRATOR_API_KEY=$ORCHESTRATOR_API_KEY
GITEA_WEBHOOK_SECRET=$GITEA_WEBHOOK_SECRET
EOF
# Set restrictive permissions
chmod 600 "$env_file"
echo -e "${SUCCESS}${NC} .env file generated at ${INFO}$env_file${NC}"
}
# ============================================================================
# Port Conflict Resolution
# ============================================================================
check_port_conflicts() {
if [[ "$NO_PORT_CHECK" == true ]]; then
return
fi
echo -e "${BOLD}Checking for port conflicts...${NC}"
echo ""
local conflicts=()
local ports_to_check=("WEB_PORT:$WEB_PORT" "API_PORT:$API_PORT" "POSTGRES_PORT:$POSTGRES_PORT" "VALKEY_PORT:$VALKEY_PORT")
for entry in "${ports_to_check[@]}"; do
local name="${entry%%:*}"
local port="${entry#*:}"
if check_port_in_use "$port"; then
conflicts+=("$name:$port")
fi
done
if [[ ${#conflicts[@]} -eq 0 ]]; then
echo -e "${SUCCESS}${NC} No port conflicts detected"
return
fi
echo -e "${WARN}${NC} Port conflicts detected:"
for conflict in "${conflicts[@]}"; do
local name="${conflict%%:*}"
local port="${conflict#*:}"
local process
process=$(get_process_on_port "$port")
echo " - $name: Port $port is in use (PID: $process)"
done
if [[ "$NON_INTERACTIVE" == true ]]; then
echo -e "${INFO}i${NC} Non-interactive mode: Please free the ports and try again"
exit 1
fi
echo ""
read -r -p "Continue anyway? [y/N]: " continue_anyway
case "$continue_anyway" in
y|Y)
echo -e "${WARN}${NC} Continuing with port conflicts - services may fail to start"
;;
*)
echo -e "${ERROR}Error: Port conflicts must be resolved${NC}"
exit 1
;;
esac
}
# ============================================================================
# Installation Steps
# ============================================================================
install_docker_mode() {
echo -e "${BOLD}Installing Mosaic Stack (Docker mode)${NC}"
echo ""
# Check and install dependencies
if [[ "$SKIP_DEPS" != true ]]; then
if ! check_docker_dependencies; then
echo ""
if [[ "$NON_INTERACTIVE" == true ]] || \
confirm "Install missing dependencies?" "y"; then
install_dependencies "docker"
else
echo -e "${ERROR}Error: Cannot proceed without dependencies${NC}"
exit 1
fi
fi
fi
# Ensure Docker is running
start_docker
# Check port conflicts
check_port_conflicts
# Generate secrets and .env
generate_secrets
generate_env_file
# Pull images
if [[ "$DRY_RUN" != true ]]; then
echo ""
docker_pull_images "$PROJECT_ROOT/docker-compose.yml" "$PROJECT_ROOT/.env"
fi
# Start services
if [[ "$DRY_RUN" != true ]]; then
echo ""
docker_compose_up "$PROJECT_ROOT/docker-compose.yml" "$PROJECT_ROOT/.env" "$COMPOSE_PROFILES"
# Wait for services to be healthy
echo ""
echo -e "${INFO}${NC} Waiting for services to start..."
sleep 10
# Run health checks
wait_for_healthy_container "mosaic-postgres" 60 || true
wait_for_healthy_container "mosaic-valkey" 30 || true
fi
}
install_native_mode() {
echo -e "${BOLD}Installing Mosaic Stack (Native mode)${NC}"
echo ""
# Check and install dependencies
if [[ "$SKIP_DEPS" != true ]]; then
if ! check_native_dependencies; then
echo ""
if [[ "$NON_INTERACTIVE" == true ]] || \
confirm "Install missing dependencies?" "y"; then
install_dependencies "native"
else
echo -e "${ERROR}Error: Cannot proceed without dependencies${NC}"
exit 1
fi
fi
fi
# Generate secrets and .env
generate_secrets
generate_env_file
# Install npm dependencies
if [[ "$DRY_RUN" != true ]]; then
echo ""
echo -e "${WARN}${NC} Installing npm dependencies..."
pnpm install
# Run database migrations
echo ""
echo -e "${WARN}${NC} Running database setup..."
echo -e "${INFO}${NC} Make sure PostgreSQL is running and accessible"
fi
}
# ============================================================================
# Post-Install
# ============================================================================
run_post_install_checks() {
echo ""
echo -e "${BOLD}Post-Installation Checks${NC}"
echo ""
if [[ "$DRY_RUN" == true ]]; then
echo -e "${INFO}${NC} Dry run - skipping checks"
return
fi
# Run doctor
run_doctor "$PROJECT_ROOT/.env" "$PROJECT_ROOT/docker-compose.yml" "$MODE"
local doctor_result=$?
if [[ $doctor_result -eq $CHECK_FAIL ]]; then
echo ""
echo -e "${WARN}${NC} Some checks failed. Review the output above."
fi
}
show_success_message() {
local web_url="$MOSAIC_BASE_URL"
local api_url="${MOSAIC_BASE_URL/http:\/\//http:\/\/}:${API_PORT}"
# If using Traefik, adjust URLs
if [[ "$COMPOSE_PROFILES" == *"traefik"* ]]; then
api_url="${web_url/api./}"
fi
echo ""
echo -e "${BOLD}${SUCCESS}════════════════════════════════════════════════════════════${NC}"
echo -e "${BOLD}${SUCCESS} Mosaic Stack is ready!${NC}"
echo -e "${BOLD}${SUCCESS}════════════════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${INFO}Web UI:${NC} $web_url"
echo -e " ${INFO}API:${NC} $api_url"
echo -e " ${INFO}Database:${NC} localhost:$POSTGRES_PORT"
echo ""
echo -e " ${BOLD}Next steps:${NC}"
echo " 1. Open $web_url in your browser"
echo " 2. Create your first workspace"
echo " 3. Configure AI providers in Settings"
echo ""
echo -e " ${BOLD}Useful commands:${NC}"
if [[ "$MODE" == "docker" ]]; then
echo " To stop: docker compose down"
echo " To restart: docker compose restart"
echo " To view logs: docker compose logs -f"
else
echo " Start API: pnpm --filter api dev"
echo " Start Web: pnpm --filter web dev"
fi
echo ""
echo -e " ${INFO}Documentation:${NC} https://docs.mosaicstack.dev"
echo -e " ${INFO}Support:${NC} https://github.com/mosaicstack/stack/issues"
echo ""
}
# ============================================================================
# Dry Run
# ============================================================================
show_dry_run_summary() {
echo ""
echo -e "${BOLD}${INFO}════════════════════════════════════════════════════════════${NC}"
echo -e "${BOLD}${INFO} Dry Run Summary${NC}"
echo -e "${BOLD}${INFO}════════════════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${INFO}Mode:${NC} $MODE"
echo -e " ${INFO}Base URL:${NC} $MOSAIC_BASE_URL"
echo -e " ${INFO}Profiles:${NC} $COMPOSE_PROFILES"
echo ""
echo -e " ${INFO}SSO:${NC} $([ "$ENABLE_SSO" == true ] && echo "Enabled" || echo "Disabled")"
echo -e " ${INFO}Ollama:${NC} $OLLAMA_MODE"
echo ""
echo -e " ${MUTED}This was a dry run. No changes were made.${NC}"
echo -e " ${MUTED}Run without --dry-run to perform installation.${NC}"
echo ""
}
# ============================================================================
# Main
# ============================================================================
main() {
# Configure verbose mode
if [[ "$VERBOSE" == true ]]; then
set -x
fi
# Show banner
show_banner
# Detect platform
print_platform_summary
echo ""
# Select deployment mode
select_mode
echo -e "${SUCCESS}${NC} Selected: ${INFO}$MODE${NC} mode"
echo ""
# Dry run check
if [[ "$DRY_RUN" == true ]]; then
collect_configuration
show_dry_run_summary
exit 0
fi
# Collect configuration
collect_configuration
# Install based on mode
case "$MODE" in
docker)
install_docker_mode
;;
native)
install_native_mode
;;
esac
# Post-installation checks
run_post_install_checks
# Show success message
show_success_message
}
# Confirm helper
confirm() {
local prompt="$1"
local default="${2:-n}"
local response
if [[ "$default" == "y" ]]; then
prompt="$prompt [Y/n]: "
else
prompt="$prompt [y/N]: "
fi
read -r -p "$prompt" response
response=${response:-$default}
case "$response" in
[Yy]|[Yy][Ee][Ss]) return 0 ;;
*) return 1 ;;
esac
}
# Run if not being sourced
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
parse_arguments "$@"
main
fi