#!/usr/bin/env bash set -euo pipefail if [[ -z "${BASH_VERSINFO+x}" || "${BASH_VERSINFO[0]}" -lt 4 ]]; then echo "Error: scripts/setup/setup.sh requires Bash 4 or newer." >&2 echo "Hint: install a newer bash and run it via 'bash scripts/setup/setup.sh ...'." >&2 exit 1 fi SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LIB_DIR="$SCRIPT_DIR/lib" # shellcheck disable=SC2034 TEMPLATES_DIR="$SCRIPT_DIR/templates" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" declare -A ENV_VALUES declare -A ORIGINAL_ENV_VALUES declare -A COMPOSE_ENV_OVERRIDES declare -A COMPOSE_REWRITE_SERVICE_SET declare -A COMPOSE_SERVICE_IMAGE_OVERRIDES declare -A REQUIRED_DB_TYPES declare -A DOCKER_SERVICE_SET declare -A EXISTING_MANAGED_ROOT_SERVICE_SET declare -a DOCKER_SERVICES SSL_CERT_SOURCE_PATH="" SSL_KEY_SOURCE_PATH="" LIGHTRAG_COMPOSE_SERVER_PORT_MAPPING="" NORMALIZED_SERVER_HOST_FOR_COMPOSE="" FORCE_REWRITE_COMPOSE="no" DEBUG="${DEBUG:-false}" PRESET_VLLM_EMBEDDING=( "EMBEDDING_BINDING=openai" "EMBEDDING_BINDING_HOST=http://localhost:8001/v1" "EMBEDDING_MODEL=BAAI/bge-m3" "EMBEDDING_DIM=1024" "VLLM_EMBED_MODEL=BAAI/bge-m3" "VLLM_EMBED_PORT=8001" "VLLM_EMBED_DEVICE=cpu" ) PRESET_VLLM_RERANKER=( "RERANK_BINDING=cohere" "LIGHTRAG_SETUP_RERANK_PROVIDER=vllm" "RERANK_MODEL=BAAI/bge-reranker-v2-m3" "RERANK_BINDING_HOST=http://localhost:8000/rerank" "VLLM_RERANK_MODEL=BAAI/bge-reranker-v2-m3" "VLLM_RERANK_PORT=8000" "VLLM_RERANK_DEVICE=cpu" ) VLLM_SERVICES=( "vllm-embed" "vllm-rerank" ) STORAGE_SERVICES=( "postgres" "neo4j" "mongodb" "redis" "milvus" "qdrant" "memgraph" "opensearch" ) DEFAULT_RUNTIME_TARGET="host" COMPOSE_LIGHTRAG_WORKING_DIR="/app/data/rag_storage" COMPOSE_LIGHTRAG_INPUT_DIR="/app/data/inputs" COMPOSE_LIGHTRAG_PROMPT_DIR="/app/data/prompts" # shellcheck disable=SC2034 COLOR_RESET="" COLOR_BOLD="" COLOR_BLUE="" COLOR_GREEN="" COLOR_YELLOW="" # shellcheck disable=SC2034 COLOR_RED="" # shellcheck disable=SC1091 source "$LIB_DIR/storage_requirements.sh" # shellcheck disable=SC1091 source "$LIB_DIR/validation.sh" # shellcheck disable=SC1091 source "$LIB_DIR/prompts.sh" # shellcheck disable=SC1091 source "$LIB_DIR/file_ops.sh" # shellcheck disable=SC1091 source "$LIB_DIR/presets.sh" init_colors() { if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then COLOR_RESET=$'\033[0m' COLOR_BOLD=$'\033[1m' COLOR_BLUE=$'\033[34m' COLOR_GREEN=$'\033[32m' COLOR_YELLOW=$'\033[33m' # shellcheck disable=SC2034 COLOR_RED=$'\033[31m' fi } reset_state() { ENV_VALUES=() ORIGINAL_ENV_VALUES=() COMPOSE_ENV_OVERRIDES=() COMPOSE_REWRITE_SERVICE_SET=() COMPOSE_SERVICE_IMAGE_OVERRIDES=() REQUIRED_DB_TYPES=() DOCKER_SERVICE_SET=() EXISTING_MANAGED_ROOT_SERVICE_SET=() DOCKER_SERVICES=() SSL_CERT_SOURCE_PATH="" SSL_KEY_SOURCE_PATH="" LIGHTRAG_COMPOSE_SERVER_PORT_MAPPING="" NORMALIZED_SERVER_HOST_FOR_COMPOSE="" } validate_runtime_target() { local runtime_target="${1:-$DEFAULT_RUNTIME_TARGET}" case "$runtime_target" in host|compose) return 0 ;; *) format_error \ "Invalid LIGHTRAG_RUNTIME_TARGET: ${runtime_target}" \ "Use 'host' or 'compose', or rerun the setup wizard to regenerate .env." return 1 ;; esac } set_runtime_target() { local runtime_target="${1:-$DEFAULT_RUNTIME_TARGET}" if ! validate_runtime_target "$runtime_target"; then return 1 fi ENV_VALUES["LIGHTRAG_RUNTIME_TARGET"]="$runtime_target" } clear_deprecated_vllm_dtype_state() { unset 'ENV_VALUES[VLLM_EMBED_DTYPE]' unset 'ENV_VALUES[VLLM_RERANK_DTYPE]' } normalize_vllm_rerank_binding_state() { if [[ "${ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]:-}" == "vllm" ]]; then ENV_VALUES["RERANK_BINDING"]="cohere" fi } # Backfill sentinel hosts for bedrock/gemini bindings when LLM_BINDING_HOST or # EMBEDDING_BINDING_HOST is missing or empty. Flows that skip collect_llm_config / # collect_embedding_config (--server, --storage) would otherwise let the openai URL # hardcoded in env.example leak into the regenerated .env. normalize_provider_binding_hosts() { local llm_sentinel="" embedding_sentinel="" case "${ENV_VALUES[LLM_BINDING]:-}" in bedrock) llm_sentinel="DEFAULT_BEDROCK_ENDPOINT" ;; gemini) llm_sentinel="DEFAULT_GEMINI_ENDPOINT" ;; esac case "${ENV_VALUES[EMBEDDING_BINDING]:-}" in bedrock) embedding_sentinel="DEFAULT_BEDROCK_ENDPOINT" ;; gemini) embedding_sentinel="DEFAULT_GEMINI_ENDPOINT" ;; esac if [[ -n "$llm_sentinel" && -z "${ENV_VALUES[LLM_BINDING_HOST]:-}" ]]; then ENV_VALUES["LLM_BINDING_HOST"]="$llm_sentinel" fi if [[ -n "$embedding_sentinel" && -z "${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}" ]]; then ENV_VALUES["EMBEDDING_BINDING_HOST"]="$embedding_sentinel" fi } load_existing_env_if_present() { local env_file="${REPO_ROOT}/.env" if [[ -f "$env_file" ]]; then log_debug "Loading existing .env defaults from $env_file" load_env_file "$env_file" clear_deprecated_vllm_dtype_state normalize_vllm_rerank_binding_state normalize_provider_binding_hosts if [[ "${ENV_VALUES[SSL]:-false}" == "true" ]]; then SSL_CERT_SOURCE_PATH="${ENV_VALUES[SSL_CERTFILE]:-}" SSL_KEY_SOURCE_PATH="${ENV_VALUES[SSL_KEYFILE]:-}" fi snapshot_original_env_values fi } snapshot_original_env_values() { local key ORIGINAL_ENV_VALUES=() for key in "${!ENV_VALUES[@]}"; do ORIGINAL_ENV_VALUES["$key"]="${ENV_VALUES[$key]}" done } prepare_compose_output_from_existing() { local output_file="$1" local existing_file="$2" if [[ -z "$existing_file" || "$existing_file" == "$output_file" || -f "$output_file" ]]; then return 0 fi if ! cp "$existing_file" "$output_file"; then format_error "Failed to prepare compose output at ${output_file}" \ "Check file permissions and available disk space, then rerun setup." return 1 fi log_success "Using ${existing_file} as merge input for ${output_file}" } log_debug() { if [[ "$DEBUG" == "true" ]]; then echo "${COLOR_YELLOW}[debug]${COLOR_RESET} $*" fi } log_info() { echo "${COLOR_BLUE}${COLOR_BOLD}$*${COLOR_RESET}" } log_warn() { echo "${COLOR_YELLOW}$*${COLOR_RESET}" } log_success() { echo "${COLOR_GREEN}$*${COLOR_RESET}" } log_step() { echo "${COLOR_BLUE}${COLOR_BOLD}$*${COLOR_RESET}" } normalize_loopback_uri_for_compose() { local uri="$1" if [[ "$uri" =~ ^([a-zA-Z][a-zA-Z0-9+.-]*://)([^/?#]+@)?(localhost|127\.0\.0\.1|0\.0\.0\.0)([/:?].*)?$ ]]; then printf '%s%shost.docker.internal%s' \ "${BASH_REMATCH[1]}" \ "${BASH_REMATCH[2]}" \ "${BASH_REMATCH[4]}" return 0 fi printf '%s' "$uri" } ensure_mongodb_direct_connection_suffix() { local suffix="${1:-}" local path query fragment filtered_query="" local part local -a query_parts=() if [[ "$suffix" == *"#"* ]]; then fragment="#${suffix#*#}" suffix="${suffix%%#*}" else fragment="" fi if [[ "$suffix" == *"?"* ]]; then path="${suffix%%\?*}" query="${suffix#*\?}" else path="$suffix" query="" fi if [[ -z "$path" ]]; then path="/" fi if [[ -n "$query" ]]; then IFS='&' read -r -a query_parts <<< "$query" for part in "${query_parts[@]}"; do if [[ -z "$part" || "$part" == directConnection=* ]]; then continue fi if [[ -n "$filtered_query" ]]; then filtered_query="${filtered_query}&${part}" else filtered_query="$part" fi done fi if [[ -n "$filtered_query" ]]; then printf '%s?%s&directConnection=true%s' "$path" "$filtered_query" "$fragment" else printf '%s?directConnection=true%s' "$path" "$fragment" fi } mongodb_uri_has_direct_connection_true() { local uri="$1" local direct_connection_pattern='[?&]directConnection=true([&#]|$)' [[ "$uri" =~ ^mongodb:// ]] && [[ "$uri" =~ $direct_connection_pattern ]] } is_wizard_managed_local_mongodb_uri() { local uri="$1" [[ "$uri" =~ ^mongodb://([^/?#]+@)?(mongodb|localhost|127\.0\.0\.1|0\.0\.0\.0):27017([/?#].*)?$ ]] && \ mongodb_uri_has_direct_connection_true "$uri" } normalize_mongodb_uri_for_local_service() { local uri="$1" local suffix if [[ "$uri" =~ ^mongodb://([^/?#]+@)?(mongodb|localhost|127\.0\.0\.1|0\.0\.0\.0)(:[0-9]+)?([/?#].*)?$ ]]; then suffix="$(ensure_mongodb_direct_connection_suffix "${BASH_REMATCH[4]:-/}")" printf 'mongodb://localhost:27017%s' "$suffix" return 0 fi printf '%s' "$uri" } normalize_neo4j_uri_for_local_service() { local uri="$1" if [[ "$uri" =~ ^([a-zA-Z][a-zA-Z0-9+.-]*://)([^/?#]+@)?(neo4j|localhost|127\.0\.0\.1|0\.0\.0\.0)(:[0-9]+)?([/?#].*)?$ ]]; then printf '%s%slocalhost:7687%s' \ "${BASH_REMATCH[1]}" \ "${BASH_REMATCH[2]}" \ "${BASH_REMATCH[5]}" return 0 fi printf '%s' "$uri" } normalize_redis_uri_for_local_service() { local uri="$1" if [[ "$uri" =~ ^rediss?://([^/?#]+@)?(redis|localhost|127\.0\.0\.1|0\.0\.0\.0)(:([0-9]+))?(/.*)?$ ]]; then printf 'redis://localhost:6379%s' "${BASH_REMATCH[5]}" return 0 fi printf '%s' "$uri" } normalize_milvus_uri_for_local_service() { local uri="$1" if [[ "$uri" =~ ^(https?://)([^/?#]+@)?(milvus|localhost|127\.0\.0\.1|0\.0\.0\.0)(:[0-9]+)?([/?#].*)?$ ]]; then printf '%slocalhost:19530%s' \ "${BASH_REMATCH[1]}" \ "${BASH_REMATCH[5]}" return 0 fi printf '%s' "$uri" } normalize_qdrant_uri_for_local_service() { local uri="$1" if [[ "$uri" =~ ^(https?://)([^/?#]+@)?(qdrant|localhost|127\.0\.0\.1|0\.0\.0\.0)(:[0-9]+)?([/?#].*)?$ ]]; then printf '%slocalhost:6333%s' \ "${BASH_REMATCH[1]}" \ "${BASH_REMATCH[5]}" return 0 fi printf '%s' "$uri" } normalize_memgraph_uri_for_local_service() { local uri="$1" if [[ "$uri" =~ ^(bolt://)([^/?#]+@)?(memgraph|localhost|127\.0\.0\.1|0\.0\.0\.0)(:[0-9]+)?([/?#].*)?$ ]]; then printf 'bolt://localhost:7687%s' "${BASH_REMATCH[5]}" return 0 fi printf '%s' "$uri" } normalize_loopback_host_for_compose() { local host="$1" if [[ "$host" == "localhost" || "$host" == "127.0.0.1" || "$host" == "0.0.0.0" ]]; then printf 'host.docker.internal' return 0 fi printf '%s' "$host" } normalize_opensearch_hosts_for_compose() { local hosts="$1" local entry="" local trimmed="" local normalized_entry="" local -a raw_entries=() local -a normalized_entries=() IFS=',' read -r -a raw_entries <<< "$hosts" for entry in "${raw_entries[@]}"; do trimmed="${entry#"${entry%%[![:space:]]*}"}" trimmed="${trimmed%"${trimmed##*[![:space:]]}"}" # OPENSEARCH_HOSTS is intentionally limited to bare host[:port] entries. # TLS is configured separately via OPENSEARCH_USE_SSL, so scheme-bearing # URLs are rejected during validation rather than normalized here. normalized_entry="$trimmed" if [[ "$trimmed" =~ ^(localhost|127\.0\.0\.1|0\.0\.0\.0)(:[0-9]+)?$ ]]; then normalized_entry="host.docker.internal${BASH_REMATCH[2]}" fi normalized_entries+=("$normalized_entry") done ( IFS=',' printf '%s' "${normalized_entries[*]}" ) } env_value_is_true() { local value="${1:-}" case "${value,,}" in true|1|yes) return 0 ;; *) return 1 ;; esac } normalize_server_host_for_compose() { # Keep the published bind address/port configurable through compose-time # variable expansion, while forcing the container itself to listen on the # internal service defaults. LIGHTRAG_COMPOSE_SERVER_PORT_MAPPING='${HOST:-0.0.0.0}:${PORT:-9621}:9621' NORMALIZED_SERVER_HOST_FOR_COMPOSE="0.0.0.0" } host_cuda_available() { command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi >/dev/null 2>&1 } resolve_local_device_default() { local configured_device="${1:-}" if [[ "$configured_device" == "cpu" || "$configured_device" == "cuda" ]]; then printf '%s' "$configured_device" return 0 fi if host_cuda_available; then printf 'cuda' else printf 'cpu' fi } default_loopback_url() { local port="$1" local path="${2:-}" printf 'http://localhost:%s%s' "$port" "$path" } uri_points_to_host() { local uri="$1" shift local host="" local allowed_host if [[ "$uri" =~ ^[a-zA-Z][a-zA-Z0-9+.-]*://([^/?#]+@)?(\[[^]]+\]|[^/:?#]+) ]]; then host="${BASH_REMATCH[2]}" for allowed_host in "$@"; do if [[ "$host" == "$allowed_host" ]]; then return 0 fi done fi return 1 } prefer_local_service_uri() { local current_uri="$1" local default_uri="$2" shift 2 if [[ -z "$current_uri" ]]; then printf '%s' "$default_uri" return 0 fi if uri_points_to_host "$current_uri" "$@"; then printf '%s' "$current_uri" return 0 fi printf '%s' "$default_uri" } set_compose_override() { local key="$1" local value="${2:-}" if [[ -n "$value" ]]; then COMPOSE_ENV_OVERRIDES["$key"]="$value" else unset "COMPOSE_ENV_OVERRIDES[$key]" fi } set_managed_service_compose_overrides() { local root_service="$1" case "$root_service" in postgres) if [[ -z "${COMPOSE_ENV_OVERRIDES[POSTGRES_HOST]+set}" ]]; then set_compose_override "POSTGRES_HOST" "postgres" fi # The bundled postgres compose service always listens on 5432 internally. if [[ -z "${COMPOSE_ENV_OVERRIDES[POSTGRES_PORT]+set}" ]]; then set_compose_override "POSTGRES_PORT" "5432" fi ;; neo4j) if [[ -z "${COMPOSE_ENV_OVERRIDES[NEO4J_URI]+set}" ]]; then set_compose_override "NEO4J_URI" "neo4j://neo4j:7687" fi ;; mongodb) if [[ -z "${COMPOSE_ENV_OVERRIDES[MONGO_URI]+set}" ]]; then set_compose_override "MONGO_URI" "mongodb://mongodb:27017/?directConnection=true" fi ;; redis) if [[ -z "${COMPOSE_ENV_OVERRIDES[REDIS_URI]+set}" ]]; then set_compose_override "REDIS_URI" "redis://redis:6379" fi ;; milvus) if [[ -z "${COMPOSE_ENV_OVERRIDES[MILVUS_URI]+set}" ]]; then set_compose_override "MILVUS_URI" "http://milvus:19530" fi ;; qdrant) if [[ -z "${COMPOSE_ENV_OVERRIDES[QDRANT_URL]+set}" ]]; then set_compose_override "QDRANT_URL" "http://qdrant:6333" fi ;; memgraph) if [[ -z "${COMPOSE_ENV_OVERRIDES[MEMGRAPH_URI]+set}" ]]; then set_compose_override "MEMGRAPH_URI" "bolt://memgraph:7687" fi ;; opensearch) if [[ -z "${COMPOSE_ENV_OVERRIDES[OPENSEARCH_HOSTS]+set}" ]]; then set_compose_override "OPENSEARCH_HOSTS" "opensearch:9200" fi ;; esac } prepare_compose_runtime_overrides() { local normalized_value local key local root_service # EMBEDDING_BINDING_HOST: when vllm-embed is part of this compose, the LightRAG # container must reach it by Docker service name, not by a loopback address. # This applies even when the wizard did not visit the embedding step (e.g. # env_server_flow), because vllm-embed is detected and added to DOCKER_SERVICE_SET # before prepare_compose_env_overrides is called. if [[ -z "${COMPOSE_ENV_OVERRIDES[EMBEDDING_BINDING_HOST]+set}" ]]; then if [[ -n "${DOCKER_SERVICE_SET[vllm-embed]+set}" ]]; then set_compose_override "EMBEDDING_BINDING_HOST" \ "http://vllm-embed:${ENV_VALUES[VLLM_EMBED_PORT]:-8001}/v1" elif [[ -n "${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}" ]]; then normalized_value="$(normalize_loopback_uri_for_compose "${ENV_VALUES[EMBEDDING_BINDING_HOST]}")" if [[ "$normalized_value" != "${ENV_VALUES[EMBEDDING_BINDING_HOST]}" ]]; then set_compose_override "EMBEDDING_BINDING_HOST" "$normalized_value" fi fi fi # RERANK_BINDING_HOST: same pattern for vllm-rerank. if [[ -z "${COMPOSE_ENV_OVERRIDES[RERANK_BINDING_HOST]+set}" ]]; then if [[ -n "${DOCKER_SERVICE_SET[vllm-rerank]+set}" ]]; then set_compose_override "RERANK_BINDING_HOST" \ "http://vllm-rerank:${ENV_VALUES[VLLM_RERANK_PORT]:-8000}/rerank" elif [[ -n "${ENV_VALUES[RERANK_BINDING_HOST]:-}" ]]; then normalized_value="$(normalize_loopback_uri_for_compose "${ENV_VALUES[RERANK_BINDING_HOST]}")" if [[ "$normalized_value" != "${ENV_VALUES[RERANK_BINDING_HOST]}" ]]; then set_compose_override "RERANK_BINDING_HOST" "$normalized_value" fi fi fi for root_service in postgres neo4j mongodb redis milvus qdrant memgraph opensearch; do if [[ -n "${DOCKER_SERVICE_SET[$root_service]+set}" ]]; then set_managed_service_compose_overrides "$root_service" fi done for key in \ "LLM_BINDING_HOST" \ "REDIS_URI" \ "MONGO_URI" \ "NEO4J_URI" \ "MILVUS_URI" \ "QDRANT_URL" \ "MEMGRAPH_URI"; do if [[ -n "${COMPOSE_ENV_OVERRIDES[$key]+set}" ]]; then continue fi if [[ -n "${ENV_VALUES[$key]:-}" ]]; then normalized_value="$(normalize_loopback_uri_for_compose "${ENV_VALUES[$key]}")" if [[ "$normalized_value" != "${ENV_VALUES[$key]}" ]]; then set_compose_override "$key" "$normalized_value" fi fi done for key in "OPENSEARCH_HOSTS"; do if [[ -n "${COMPOSE_ENV_OVERRIDES[$key]+set}" ]]; then continue fi if [[ -n "${ENV_VALUES[$key]:-}" ]]; then normalized_value="$(normalize_opensearch_hosts_for_compose "${ENV_VALUES[$key]}")" if [[ "$normalized_value" != "${ENV_VALUES[$key]}" ]]; then set_compose_override "$key" "$normalized_value" fi fi done for key in "POSTGRES_HOST"; do if [[ -n "${COMPOSE_ENV_OVERRIDES[$key]+set}" ]]; then continue fi if [[ -n "${ENV_VALUES[$key]:-}" ]]; then normalized_value="$(normalize_loopback_host_for_compose "${ENV_VALUES[$key]}")" if [[ "$normalized_value" != "${ENV_VALUES[$key]}" ]]; then set_compose_override "$key" "$normalized_value" fi fi done normalize_server_host_for_compose "${ENV_VALUES[HOST]:-0.0.0.0}" normalized_value="$NORMALIZED_SERVER_HOST_FOR_COMPOSE" set_compose_override "HOST" "$normalized_value" set_compose_override "PORT" "9621" } prepare_compose_ssl_overrides() { local cert_name="" local key_name="" if [[ -n "$SSL_CERT_SOURCE_PATH" ]]; then cert_name="$(resolve_staged_ssl_basename "cert" "$SSL_CERT_SOURCE_PATH" "$SSL_KEY_SOURCE_PATH")" set_compose_override "SSL_CERTFILE" "/app/data/certs/${cert_name}" fi if [[ -n "$SSL_KEY_SOURCE_PATH" ]]; then key_name="$(resolve_staged_ssl_basename "key" "$SSL_KEY_SOURCE_PATH" "$SSL_CERT_SOURCE_PATH")" set_compose_override "SSL_KEYFILE" "/app/data/certs/${key_name}" fi } prepare_compose_data_path_overrides() { # Compose mounts always bind the data directories into these container paths. # Force lightrag to use them so values from the mounted .env cannot redirect # storage into a different location. set_compose_override "WORKING_DIR" "$COMPOSE_LIGHTRAG_WORKING_DIR" set_compose_override "INPUT_DIR" "$COMPOSE_LIGHTRAG_INPUT_DIR" set_compose_override "PROMPT_DIR" "$COMPOSE_LIGHTRAG_PROMPT_DIR" } prepare_compose_env_overrides() { prepare_compose_data_path_overrides prepare_compose_runtime_overrides prepare_compose_ssl_overrides } add_docker_service() { local service="$1" if [[ -z "${DOCKER_SERVICE_SET[$service]+set}" ]]; then DOCKER_SERVICE_SET["$service"]=1 DOCKER_SERVICES+=("$service") fi } restore_storage_docker_services_from_env() { local db_type local marker_key="" local service_name="" local db_types=("postgresql" "neo4j" "mongodb" "redis" "milvus" "qdrant" "memgraph" "opensearch") for db_type in "${db_types[@]}"; do marker_key="$(storage_deployment_marker_key "$db_type")" if [[ -n "$marker_key" && "${ENV_VALUES[$marker_key]:-}" == "docker" ]]; then service_name="$(storage_service_name_for_db_type "$db_type")" if [[ -n "$service_name" ]]; then add_docker_service "$service_name" fi fi done } restore_vllm_docker_services_from_env() { if [[ "${ENV_VALUES[LIGHTRAG_SETUP_EMBEDDING_PROVIDER]:-}" == "vllm" ]]; then add_docker_service "vllm-embed" fi if [[ "${ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]:-}" == "vllm" ]]; then add_docker_service "vllm-rerank" fi } compose_has_non_wizard_services() { local compose_file="$1" local service_name="" if [[ -z "$compose_file" || ! -f "$compose_file" ]]; then return 1 fi while IFS= read -r service_name; do if [[ -z "$(_managed_service_root_name "$service_name")" ]]; then return 0 fi done < <(detect_compose_services "$compose_file") return 1 } resolve_compose_output_action() { local existing_compose="$1" local -n action_ref="$2" local -n runtime_target_ref="$3" local -n host_hint_ref="$4" local current_target="${ENV_VALUES[LIGHTRAG_RUNTIME_TARGET]:-$DEFAULT_RUNTIME_TARGET}" action_ref="write_env_only" runtime_target_ref="$DEFAULT_RUNTIME_TARGET" host_hint_ref="no" if ((${#DOCKER_SERVICES[@]} > 0)); then action_ref="rewrite_compose" runtime_target_ref="compose" return 0 fi if compose_has_non_wizard_services "$existing_compose"; then action_ref="rewrite_compose" runtime_target_ref="compose" return 0 fi if [[ -n "$existing_compose" ]]; then if confirm_default_no "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?"; then action_ref="delete_compose_and_switch_host" runtime_target_ref="host" host_hint_ref="yes" else action_ref="rewrite_compose" runtime_target_ref="compose" fi return 0 fi if [[ "$current_target" == "compose" ]]; then if confirm_default_yes "Run LightRAG Server via Docker?"; then action_ref="rewrite_compose" runtime_target_ref="compose" else host_hint_ref="yes" fi else if confirm_default_no "Run LightRAG Server via Docker?"; then action_ref="rewrite_compose" runtime_target_ref="compose" else host_hint_ref="yes" fi fi } mark_compose_service_for_rewrite() { local service="$1" local root_service="" root_service="$(_managed_service_root_name "$service")" if [[ -n "$root_service" ]]; then COMPOSE_REWRITE_SERVICE_SET["$root_service"]=1 fi } record_existing_managed_root_services() { local compose_file="$1" local service_name local root_service EXISTING_MANAGED_ROOT_SERVICE_SET=() if [[ -z "$compose_file" || ! -f "$compose_file" ]]; then return 0 fi while IFS= read -r service_name; do root_service="$(_managed_service_root_name "$service_name")" if [[ -n "$root_service" ]]; then EXISTING_MANAGED_ROOT_SERVICE_SET["$root_service"]=1 fi done < <(detect_managed_root_services "$compose_file") } collect_preserved_storage_service_images() { local compose_file="${1:-}" local service_name="" local image_value="" COMPOSE_SERVICE_IMAGE_OVERRIDES=() if [[ "$FORCE_REWRITE_COMPOSE" == "yes" || -z "$compose_file" || ! -f "$compose_file" ]]; then return 0 fi # Only postgres and neo4j are wizard-managed Docker storage services that users # commonly pin to custom registry images. If new storage backends are added as # wizard-managed Docker services, extend this list accordingly. for service_name in postgres neo4j; do if [[ -z "${COMPOSE_REWRITE_SERVICE_SET[$service_name]+set}" ]] || \ [[ -z "${DOCKER_SERVICE_SET[$service_name]+set}" ]] || \ ! existing_managed_root_service_present "$service_name"; then continue fi image_value="$(read_service_image_value "$compose_file" "$service_name" || true)" if [[ -n "$image_value" ]]; then COMPOSE_SERVICE_IMAGE_OVERRIDES["$service_name"]="$image_value" fi done } backup_existing_compose_if_generating() { local generate_compose="${1:-no}" local existing_compose="${2:-}" local compose_backup_path="" if [[ "$generate_compose" != "yes" ]]; then return 0 fi compose_backup_path="$(backup_compose_file "$existing_compose")" || return 1 if [[ -n "$compose_backup_path" ]]; then log_success "Backed up existing compose file to $compose_backup_path" fi } backup_existing_compose_for_action() { local compose_action="${1:-write_env_only}" local existing_compose="${2:-}" local compose_backup_path="" case "$compose_action" in rewrite_compose|delete_compose_and_switch_host) ;; *) return 0 ;; esac compose_backup_path="$(backup_compose_file "$existing_compose")" || return 1 if [[ -n "$compose_backup_path" ]]; then log_success "Backed up existing compose file to $compose_backup_path" fi } remove_existing_compose_file() { local compose_file="${1:-}" if [[ -z "$compose_file" || ! -f "$compose_file" ]]; then return 0 fi if ! rm "$compose_file"; then format_error "Failed to remove ${compose_file}" \ "Check file permissions, then remove the compose file manually or rerun setup." return 1 fi log_success "Removed ${compose_file}" } existing_managed_root_service_present() { local root_service="$1" [[ -n "${EXISTING_MANAGED_ROOT_SERVICE_SET[$root_service]+set}" ]] } compose_service_block_contains_literal() { local compose_file="$1" local service_name="$2" local literal="$3" if [[ -z "$compose_file" || ! -f "$compose_file" ]]; then return 1 fi awk -v header=" ${service_name}:" -v literal="$literal" ' $0 == header { in_service = 1; next } in_service && $0 ~ /^ [^[:space:]]/ { exit found ? 0 : 1 } in_service && index($0, literal) { found = 1; exit 0 } END { exit found ? 0 : 1 } ' "$compose_file" } mongodb_service_requires_atlas_local_rewrite() { local compose_file="${1:-}" if [[ -z "$compose_file" ]]; then return 1 fi if [[ "${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-}" != "MongoVectorDBStorage" ]]; then return 1 fi if [[ "${ENV_VALUES[LIGHTRAG_SETUP_MONGODB_DEPLOYMENT]:-}" != "docker" ]]; then return 1 fi if [[ -z "${DOCKER_SERVICE_SET[mongodb]+set}" ]] || \ ! existing_managed_root_service_present "mongodb"; then return 1 fi if ! compose_service_block_contains_literal "$compose_file" "mongodb" "image: mongodb/mongodb-atlas-local:"; then return 0 fi if ! compose_service_block_contains_literal "$compose_file" "mongodb" "mongo_config_data:/data/configdb"; then return 0 fi if ! compose_service_block_contains_literal "$compose_file" "mongodb" "mongo_mongot_data:/data/mongot"; then return 0 fi return 1 } configure_mongodb_compose_migration_rewrite() { local existing_compose="${1:-}" if [[ "$FORCE_REWRITE_COMPOSE" == "yes" ]]; then return 0 fi if mongodb_service_requires_atlas_local_rewrite "$existing_compose"; then mark_compose_service_for_rewrite "mongodb" fi } env_value_changed_from_original() { local key="$1" local missing_marker="__LIGHTRAG_MISSING__" local current_value="${ENV_VALUES[$key]-$missing_marker}" local original_value="${ORIGINAL_ENV_VALUES[$key]-$missing_marker}" [[ "$current_value" != "$original_value" ]] } any_env_value_changed_from_original() { local key for key in "$@"; do if env_value_changed_from_original "$key"; then return 0 fi done return 1 } compose_template_variant_for_service() { local service="$1" local snapshot="${2:-current}" local device="" case "$service" in milvus) if [[ "$snapshot" == "original" ]]; then device="${ORIGINAL_ENV_VALUES[MILVUS_DEVICE]:-cpu}" else device="${ENV_VALUES[MILVUS_DEVICE]:-cpu}" fi ;; qdrant) if [[ "$snapshot" == "original" ]]; then device="${ORIGINAL_ENV_VALUES[QDRANT_DEVICE]:-cpu}" else device="${ENV_VALUES[QDRANT_DEVICE]:-cpu}" fi ;; vllm-embed) if [[ "$snapshot" == "original" ]]; then device="${ORIGINAL_ENV_VALUES[VLLM_EMBED_DEVICE]:-cpu}" else device="${ENV_VALUES[VLLM_EMBED_DEVICE]:-cpu}" fi ;; vllm-rerank) if [[ "$snapshot" == "original" ]]; then device="${ORIGINAL_ENV_VALUES[VLLM_RERANK_DEVICE]:-cpu}" else device="${ENV_VALUES[VLLM_RERANK_DEVICE]:-cpu}" fi ;; *) printf 'default' return 0 ;; esac if [[ "$device" == "cuda" ]]; then printf 'gpu' else printf 'cpu' fi } configure_base_compose_rewrites() { if [[ "$FORCE_REWRITE_COMPOSE" == "yes" ]]; then return 0 fi if existing_managed_root_service_present "vllm-embed" && \ [[ -n "${DOCKER_SERVICE_SET[vllm-embed]+set}" ]] && \ [[ "$(compose_template_variant_for_service "vllm-embed" "current")" != \ "$(compose_template_variant_for_service "vllm-embed" "original")" ]]; then mark_compose_service_for_rewrite "vllm-embed" fi if existing_managed_root_service_present "vllm-rerank" && \ [[ -n "${DOCKER_SERVICE_SET[vllm-rerank]+set}" ]] && \ [[ "$(compose_template_variant_for_service "vllm-rerank" "current")" != \ "$(compose_template_variant_for_service "vllm-rerank" "original")" ]]; then mark_compose_service_for_rewrite "vllm-rerank" fi } configure_storage_compose_rewrites() { if [[ "$FORCE_REWRITE_COMPOSE" == "yes" ]]; then return 0 fi if existing_managed_root_service_present "postgres" && \ [[ -n "${DOCKER_SERVICE_SET[postgres]+set}" ]] && \ any_env_value_changed_from_original "POSTGRES_USER" "POSTGRES_PASSWORD" "POSTGRES_DATABASE"; then mark_compose_service_for_rewrite "postgres" fi if existing_managed_root_service_present "neo4j" && \ [[ -n "${DOCKER_SERVICE_SET[neo4j]+set}" ]] && \ any_env_value_changed_from_original "NEO4J_DATABASE"; then mark_compose_service_for_rewrite "neo4j" fi if existing_managed_root_service_present "milvus" && \ [[ -n "${DOCKER_SERVICE_SET[milvus]+set}" ]] && \ [[ "$(compose_template_variant_for_service "milvus" "current")" != \ "$(compose_template_variant_for_service "milvus" "original")" ]]; then mark_compose_service_for_rewrite "milvus" fi if existing_managed_root_service_present "qdrant" && \ [[ -n "${DOCKER_SERVICE_SET[qdrant]+set}" ]] && \ [[ "$(compose_template_variant_for_service "qdrant" "current")" != \ "$(compose_template_variant_for_service "qdrant" "original")" ]]; then mark_compose_service_for_rewrite "qdrant" fi } select_storage_backends() { local deployment_type="$1" local kv_default="JsonKVStorage" local vector_default="NanoVectorDBStorage" local graph_default="NetworkXStorage" local doc_default="JsonDocStatusStorage" local kv_storage vector_storage graph_storage doc_storage if [[ "$deployment_type" == "production" ]]; then kv_default="PGKVStorage" vector_default="MilvusVectorDBStorage" graph_default="Neo4JStorage" doc_default="PGDocStatusStorage" fi kv_default="${ENV_VALUES[LIGHTRAG_KV_STORAGE]:-$kv_default}" vector_default="${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-$vector_default}" graph_default="${ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]:-$graph_default}" doc_default="${ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]:-$doc_default}" while true; do kv_storage="$(prompt_choice "KV storage" "$kv_default" "${KV_STORAGE_OPTIONS[@]}")" vector_storage="$(prompt_choice "Vector storage" "$vector_default" "${VECTOR_STORAGE_OPTIONS[@]}")" graph_storage="$(prompt_choice "Graph storage" "$graph_default" "${GRAPH_STORAGE_OPTIONS[@]}")" doc_storage="$(prompt_choice "Doc status storage" "$doc_default" "${DOC_STATUS_STORAGE_OPTIONS[@]}")" if check_storage_compatibility "$kv_storage" "$vector_storage" "$graph_storage" "$doc_storage"; then break fi if confirm_default_no "Proceed with these storage selections anyway?"; then break fi done ENV_VALUES["LIGHTRAG_KV_STORAGE"]="$kv_storage" ENV_VALUES["LIGHTRAG_VECTOR_STORAGE"]="$vector_storage" ENV_VALUES["LIGHTRAG_GRAPH_STORAGE"]="$graph_storage" ENV_VALUES["LIGHTRAG_DOC_STATUS_STORAGE"]="$doc_storage" for storage in "$kv_storage" "$vector_storage" "$graph_storage" "$doc_storage"; do if [[ -n "${STORAGE_DB_TYPES[$storage]:-}" ]]; then REQUIRED_DB_TYPES["${STORAGE_DB_TYPES[$storage]}"]=1 fi done } initialize_default_storage_backends() { # env-base does not prompt for storage, but its generated .env must remain # self-consistent for first-run users who have not run env-storage yet. ENV_VALUES["LIGHTRAG_KV_STORAGE"]="${ENV_VALUES[LIGHTRAG_KV_STORAGE]:-JsonKVStorage}" ENV_VALUES["LIGHTRAG_VECTOR_STORAGE"]="${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-NanoVectorDBStorage}" ENV_VALUES["LIGHTRAG_GRAPH_STORAGE"]="${ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]:-NetworkXStorage}" ENV_VALUES["LIGHTRAG_DOC_STATUS_STORAGE"]="${ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]:-JsonDocStatusStorage}" } storage_service_name_for_db_type() { local db_type="$1" case "$db_type" in postgresql) printf 'postgres' ;; neo4j|mongodb|redis|milvus|qdrant|memgraph|opensearch) printf '%s' "$db_type" ;; *) printf '' ;; esac } storage_deployment_marker_key() { local db_type="$1" case "$db_type" in postgresql) printf 'LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT' ;; neo4j) printf 'LIGHTRAG_SETUP_NEO4J_DEPLOYMENT' ;; mongodb) printf 'LIGHTRAG_SETUP_MONGODB_DEPLOYMENT' ;; redis) printf 'LIGHTRAG_SETUP_REDIS_DEPLOYMENT' ;; milvus) printf 'LIGHTRAG_SETUP_MILVUS_DEPLOYMENT' ;; qdrant) printf 'LIGHTRAG_SETUP_QDRANT_DEPLOYMENT' ;; memgraph) printf 'LIGHTRAG_SETUP_MEMGRAPH_DEPLOYMENT' ;; opensearch) printf 'LIGHTRAG_SETUP_OPENSEARCH_DEPLOYMENT' ;; *) printf '' ;; esac } storage_default_docker_for_db_type() { local db_type="$1" local marker_key marker_key="$(storage_deployment_marker_key "$db_type")" if [[ -n "$marker_key" && "${ENV_VALUES[$marker_key]:-}" == "docker" ]]; then printf 'yes' else printf 'no' fi } persist_storage_deployment_choice() { local db_type="$1" local deployment_mode="${2:-no}" local marker_key marker_key="$(storage_deployment_marker_key "$db_type")" if [[ -z "$marker_key" ]]; then return 0 fi case "$deployment_mode" in yes|docker) ENV_VALUES["$marker_key"]="docker" ;; no|'') unset "ENV_VALUES[$marker_key]" ;; *) ENV_VALUES["$marker_key"]="$deployment_mode" ;; esac } clear_unused_storage_deployment_markers() { local db_type for db_type in postgresql neo4j mongodb redis milvus qdrant memgraph opensearch; do if [[ -z "${REQUIRED_DB_TYPES[$db_type]+set}" ]]; then persist_storage_deployment_choice "$db_type" "no" fi done } collect_database_config() { local db_type="$1" local default_docker="${2:-no}" local service_name="" local deployment_mode="no" # Storage collector rule for this wizard: # - Existing ENV_VALUES loaded from .env are user-owned configuration. # - Collectors should use those values as defaults and preserve them when they # are already set, even for Docker-managed services. # - A collector may normalize the stored form, or write a hard default only # when the key is absent. # Keep future storage collectors aligned with this behavior so rerunning the # wizard does not silently erase explicit .env overrides. case "$db_type" in postgresql) collect_postgres_config "$default_docker" ;; neo4j) collect_neo4j_config "$default_docker" ;; mongodb) collect_mongodb_config "$default_docker" ;; redis) collect_redis_config "$default_docker" ;; milvus) collect_milvus_config "$default_docker" ;; qdrant) collect_qdrant_config "$default_docker" ;; memgraph) collect_memgraph_config "$default_docker" ;; opensearch) collect_opensearch_config "$default_docker" ;; *) echo "Unknown database type: $db_type" >&2 return 1 ;; esac service_name="$(storage_service_name_for_db_type "$db_type")" if [[ -n "$service_name" && -n "${DOCKER_SERVICE_SET[$service_name]+set}" ]]; then deployment_mode="docker" fi persist_storage_deployment_choice "$db_type" "$deployment_mode" } collect_postgres_config() { local default_docker="${1:-no}" local use_docker="no" local host port user password database local existing_user="" existing_password="" existing_database="" if [[ "$default_docker" == "yes" ]]; then if confirm_default_yes "Run PostgreSQL locally via Docker?"; then use_docker="yes" fi else if confirm_default_no "Run PostgreSQL locally via Docker?"; then use_docker="yes" fi fi if [[ "$use_docker" == "yes" ]]; then add_docker_service "postgres" host="${ENV_VALUES[POSTGRES_HOST]:-localhost}" if [[ "$host" != "localhost" && "$host" != "127.0.0.1" && "$host" != "0.0.0.0" && "$host" != "postgres" ]]; then host="localhost" elif [[ "$host" == "postgres" ]]; then host="localhost" fi else host="${ENV_VALUES[POSTGRES_HOST]:-localhost}" fi host="$(prompt_with_default "PostgreSQL host" "$host")" if [[ "$use_docker" == "yes" ]]; then port="5432" set_compose_override "POSTGRES_HOST" "postgres" set_compose_override "POSTGRES_PORT" "5432" else port="$(prompt_until_valid "PostgreSQL port" "${ENV_VALUES[POSTGRES_PORT]:-5432}" validate_port)" set_compose_override "POSTGRES_HOST" "" set_compose_override "POSTGRES_PORT" "" fi # The bundled postgres image creates its user/password/database from the # POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB env vars on first start, so docker # and host deployments share the same prompts and defaults (rag/rag/lightrag). existing_user="${ORIGINAL_ENV_VALUES[POSTGRES_USER]-${ENV_VALUES[POSTGRES_USER]:-}}" existing_password="${ORIGINAL_ENV_VALUES[POSTGRES_PASSWORD]-${ENV_VALUES[POSTGRES_PASSWORD]:-}}" existing_database="${ORIGINAL_ENV_VALUES[POSTGRES_DATABASE]-${ENV_VALUES[POSTGRES_DATABASE]:-}}" user="$(prompt_with_default "PostgreSQL user" "${existing_user:-rag}")" password="$(prompt_secret_with_default "PostgreSQL password: " "${existing_password:-rag}")" database="$(prompt_with_default "PostgreSQL database" "${existing_database:-lightrag}")" ENV_VALUES["POSTGRES_HOST"]="$host" ENV_VALUES["POSTGRES_PORT"]="$port" ENV_VALUES["POSTGRES_USER"]="$user" ENV_VALUES["POSTGRES_PASSWORD"]="$password" ENV_VALUES["POSTGRES_DATABASE"]="$database" } collect_neo4j_config() { local default_docker="${1:-no}" local use_docker="no" local uri username password database local existing_username="" existing_password="" existing_database="" if [[ "$default_docker" == "yes" ]]; then if confirm_default_yes "Run Neo4j locally via Docker?"; then use_docker="yes" fi else if confirm_default_no "Run Neo4j locally via Docker?"; then use_docker="yes" fi fi if [[ "$use_docker" == "yes" ]]; then add_docker_service "neo4j" uri="$(prefer_local_service_uri "${ENV_VALUES[NEO4J_URI]:-}" "neo4j://localhost:7687" "neo4j" "localhost" "127.0.0.1" "0.0.0.0")" else uri="${ENV_VALUES[NEO4J_URI]:-neo4j://localhost:7687}" fi uri="$(prompt_until_valid "Neo4j URI" "$uri" validate_uri neo4j)" if [[ "$use_docker" == "yes" ]]; then uri="$(normalize_neo4j_uri_for_local_service "$uri")" fi existing_username="${ORIGINAL_ENV_VALUES[NEO4J_USERNAME]-${ENV_VALUES[NEO4J_USERNAME]:-}}" existing_password="${ORIGINAL_ENV_VALUES[NEO4J_PASSWORD]-${ENV_VALUES[NEO4J_PASSWORD]:-}}" existing_database="${ORIGINAL_ENV_VALUES[NEO4J_DATABASE]-${ENV_VALUES[NEO4J_DATABASE]:-}}" if [[ "$use_docker" == "yes" ]]; then username="$(prompt_until_valid "Neo4j username" "${existing_username:-neo4j}" validate_non_empty)" password="$(prompt_secret_until_valid_with_default "Neo4j password: " "${existing_password:-neo4j_password}" validate_non_empty)" if [[ -n "$existing_database" ]]; then database="$(prompt_with_default "Neo4j database" "$existing_database")" else database="neo4j" fi else username="$(prompt_with_default "Neo4j username" "${existing_username:-neo4j}")" password="$(prompt_secret_with_default "Neo4j password: " "${existing_password:-neo4j_password}")" database="$(prompt_with_default "Neo4j database" "${existing_database:-neo4j}")" fi ENV_VALUES["NEO4J_URI"]="$uri" ENV_VALUES["NEO4J_USERNAME"]="$username" ENV_VALUES["NEO4J_PASSWORD"]="$password" ENV_VALUES["NEO4J_DATABASE"]="$database" if [[ "$use_docker" == "yes" ]]; then set_compose_override "NEO4J_URI" "neo4j://neo4j:7687" else set_compose_override "NEO4J_URI" "" fi } collect_mongodb_config() { local default_docker="${1:-no}" local use_docker="no" local uri database local existing_database="" local vector_search_required="no" if [[ "${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-}" == "MongoVectorDBStorage" ]]; then vector_search_required="yes" fi if [[ "$default_docker" == "yes" ]]; then if confirm_default_yes "Run MongoDB locally via Docker?"; then use_docker="yes" fi else if confirm_default_no "Run MongoDB locally via Docker?"; then use_docker="yes" fi fi if [[ "$use_docker" == "yes" ]]; then if [[ "$vector_search_required" == "yes" ]]; then log_info "Docker MongoDB uses Atlas Local, so MongoVectorDBStorage can use Atlas Search / Vector Search locally." fi add_docker_service "mongodb" uri="$(prefer_local_service_uri "${ENV_VALUES[MONGO_URI]:-}" "mongodb://localhost:27017/?directConnection=true" "mongodb" "localhost" "127.0.0.1" "0.0.0.0")" elif [[ "$vector_search_required" == "yes" ]]; then uri="${ENV_VALUES[MONGO_URI]:-}" if [[ -z "$uri" ]] || is_wizard_managed_local_mongodb_uri "$uri"; then uri="mongodb+srv://cluster.example.mongodb.net/" fi else uri="${ENV_VALUES[MONGO_URI]:-mongodb://localhost:27017/}" fi if [[ "$vector_search_required" == "yes" ]]; then uri="$(prompt_until_valid "MongoDB URI (must support Atlas Search / Vector Search)" "$uri" validate_uri mongodb)" else uri="$(prompt_until_valid "MongoDB URI" "$uri" validate_uri mongodb)" fi if [[ "$use_docker" == "yes" ]]; then uri="$(normalize_mongodb_uri_for_local_service "$uri")" fi existing_database="${ORIGINAL_ENV_VALUES[MONGO_DATABASE]-${ENV_VALUES[MONGO_DATABASE]:-}}" database="$(prompt_with_default "MongoDB database" "${existing_database:-LightRAG}")" ENV_VALUES["MONGO_URI"]="$uri" ENV_VALUES["MONGO_DATABASE"]="$database" if [[ "$use_docker" == "yes" ]]; then set_compose_override "MONGO_URI" "mongodb://mongodb:27017/?directConnection=true" else set_compose_override "MONGO_URI" "" fi } collect_redis_config() { local default_docker="${1:-no}" local use_docker="no" local uri if [[ "$default_docker" == "yes" ]]; then if confirm_default_yes "Run Redis locally via Docker?"; then use_docker="yes" fi else if confirm_default_no "Run Redis locally via Docker?"; then use_docker="yes" fi fi if [[ "$use_docker" == "yes" ]]; then add_docker_service "redis" uri="$(prefer_local_service_uri "${ENV_VALUES[REDIS_URI]:-}" "redis://localhost:6379" "redis" "localhost" "127.0.0.1" "0.0.0.0")" else uri="${ENV_VALUES[REDIS_URI]:-redis://localhost:6379}" fi uri="$(prompt_until_valid "Redis URI" "$uri" validate_uri redis)" if [[ "$use_docker" == "yes" ]]; then uri="$(normalize_redis_uri_for_local_service "$uri")" fi ENV_VALUES["REDIS_URI"]="$uri" if [[ "$use_docker" == "yes" ]]; then set_compose_override "REDIS_URI" "redis://redis:6379" else set_compose_override "REDIS_URI" "" fi } collect_milvus_config() { local default_docker="${1:-no}" local use_docker="no" local uri db_name milvus_device="" local existing_db_name="" existing_device="" if [[ "$default_docker" == "yes" ]]; then if confirm_default_yes "Run Milvus locally via Docker?"; then use_docker="yes" fi else if confirm_default_no "Run Milvus locally via Docker?"; then use_docker="yes" fi fi if [[ "$use_docker" == "yes" ]]; then add_docker_service "milvus" uri="$(prefer_local_service_uri "${ENV_VALUES[MILVUS_URI]:-}" "http://localhost:19530" "milvus" "localhost" "127.0.0.1" "0.0.0.0")" else uri="${ENV_VALUES[MILVUS_URI]:-http://localhost:19530}" fi existing_db_name="${ORIGINAL_ENV_VALUES[MILVUS_DB_NAME]-${ENV_VALUES[MILVUS_DB_NAME]:-}}" existing_device="${ORIGINAL_ENV_VALUES[MILVUS_DEVICE]-${ENV_VALUES[MILVUS_DEVICE]:-}}" if [[ "$use_docker" == "yes" ]]; then milvus_device="$(resolve_local_device_default "$existing_device")" milvus_device="$(prompt_choice "Milvus device" "$milvus_device" "cpu" "cuda")" if [[ "$milvus_device" == "cuda" ]] && ! host_cuda_available; then log_warn "CUDA device selected for Milvus but no NVIDIA driver detected on host." fi uri="$(prompt_until_valid "Milvus URI" "$uri" validate_uri milvus)" uri="$(normalize_milvus_uri_for_local_service "$uri")" if [[ -z "${ENV_VALUES[MINIO_ACCESS_KEY_ID]:-}" ]]; then ENV_VALUES["MINIO_ACCESS_KEY_ID"]="minioadmin" fi if [[ -z "${ENV_VALUES[MINIO_SECRET_ACCESS_KEY]:-}" ]]; then ENV_VALUES["MINIO_SECRET_ACCESS_KEY"]="minioadmin" fi else uri="$(prompt_until_valid "Milvus URI" "$uri" validate_uri milvus)" fi db_name="$(prompt_with_default "Milvus database name" "${existing_db_name:-lightrag}")" ENV_VALUES["MILVUS_URI"]="$uri" ENV_VALUES["MILVUS_DB_NAME"]="$db_name" if [[ -n "$milvus_device" ]]; then ENV_VALUES["MILVUS_DEVICE"]="$milvus_device" fi if [[ "$use_docker" == "yes" ]]; then set_compose_override "MILVUS_URI" "http://milvus:19530" else set_compose_override "MILVUS_URI" "" fi } collect_qdrant_config() { local default_docker="${1:-no}" local use_docker="no" local url qdrant_device="" local existing_device="" if [[ "$default_docker" == "yes" ]]; then if confirm_default_yes "Run Qdrant locally via Docker?"; then use_docker="yes" fi else if confirm_default_no "Run Qdrant locally via Docker?"; then use_docker="yes" fi fi if [[ "$use_docker" == "yes" ]]; then add_docker_service "qdrant" url="$(prefer_local_service_uri "${ENV_VALUES[QDRANT_URL]:-}" "http://localhost:6333" "qdrant" "localhost" "127.0.0.1" "0.0.0.0")" else url="${ENV_VALUES[QDRANT_URL]:-http://localhost:6333}" fi existing_device="${ORIGINAL_ENV_VALUES[QDRANT_DEVICE]-${ENV_VALUES[QDRANT_DEVICE]:-}}" if [[ "$use_docker" == "yes" ]]; then qdrant_device="$(resolve_local_device_default "$existing_device")" qdrant_device="$(prompt_choice "Qdrant device" "$qdrant_device" "cpu" "cuda")" if [[ "$qdrant_device" == "cuda" ]] && ! host_cuda_available; then log_warn "CUDA device selected for Qdrant but no NVIDIA driver detected on host." fi url="$(prompt_until_valid "Qdrant URL" "$url" validate_uri qdrant)" url="$(normalize_qdrant_uri_for_local_service "$url")" else url="$(prompt_until_valid "Qdrant URL" "$url" validate_uri qdrant)" fi ENV_VALUES["QDRANT_URL"]="$url" if [[ -n "$qdrant_device" ]]; then ENV_VALUES["QDRANT_DEVICE"]="$qdrant_device" fi if [[ "$use_docker" == "yes" ]]; then set_compose_override "QDRANT_URL" "http://qdrant:6333" else set_compose_override "QDRANT_URL" "" fi } collect_memgraph_config() { local default_docker="${1:-no}" local use_docker="no" local uri if [[ "$default_docker" == "yes" ]]; then if confirm_default_yes "Run Memgraph locally via Docker?"; then use_docker="yes" fi else if confirm_default_no "Run Memgraph locally via Docker?"; then use_docker="yes" fi fi if [[ "$use_docker" == "yes" ]]; then add_docker_service "memgraph" uri="$(prefer_local_service_uri "${ENV_VALUES[MEMGRAPH_URI]:-}" "bolt://localhost:7687" "memgraph" "localhost" "127.0.0.1" "0.0.0.0")" else uri="${ENV_VALUES[MEMGRAPH_URI]:-bolt://localhost:7687}" fi uri="$(prompt_until_valid "Memgraph URI" "$uri" validate_uri memgraph)" if [[ "$use_docker" == "yes" ]]; then uri="$(normalize_memgraph_uri_for_local_service "$uri")" fi ENV_VALUES["MEMGRAPH_URI"]="$uri" if [[ "$use_docker" == "yes" ]]; then set_compose_override "MEMGRAPH_URI" "bolt://memgraph:7687" else set_compose_override "MEMGRAPH_URI" "" fi } collect_opensearch_config() { local default_docker="${1:-no}" local use_docker="no" local hosts user password local existing_user="" existing_password="" local existing_use_ssl="" existing_verify_certs="" local existing_num_shards="" existing_num_replicas="" local use_ssl="true" local verify_certs="false" local use_ssl_default="yes" local verify_certs_default="no" if [[ "$default_docker" == "yes" ]]; then if confirm_default_yes "Run OpenSearch locally via Docker?"; then use_docker="yes" fi else if confirm_default_no "Run OpenSearch locally via Docker?"; then use_docker="yes" fi fi if [[ "$use_docker" == "yes" ]]; then add_docker_service "opensearch" hosts="$(prefer_local_service_uri "${ENV_VALUES[OPENSEARCH_HOSTS]:-}" "localhost:9200" "opensearch" "localhost" "127.0.0.1" "0.0.0.0")" else hosts="${ENV_VALUES[OPENSEARCH_HOSTS]:-localhost:9200}" fi existing_user="${ORIGINAL_ENV_VALUES[OPENSEARCH_USER]-${ENV_VALUES[OPENSEARCH_USER]:-}}" existing_password="${ORIGINAL_ENV_VALUES[OPENSEARCH_PASSWORD]-${ENV_VALUES[OPENSEARCH_PASSWORD]:-}}" existing_use_ssl="${ORIGINAL_ENV_VALUES[OPENSEARCH_USE_SSL]-${ENV_VALUES[OPENSEARCH_USE_SSL]:-}}" existing_verify_certs="${ORIGINAL_ENV_VALUES[OPENSEARCH_VERIFY_CERTS]-${ENV_VALUES[OPENSEARCH_VERIFY_CERTS]:-}}" existing_num_shards="${ORIGINAL_ENV_VALUES[OPENSEARCH_NUMBER_OF_SHARDS]-${ENV_VALUES[OPENSEARCH_NUMBER_OF_SHARDS]:-}}" existing_num_replicas="${ORIGINAL_ENV_VALUES[OPENSEARCH_NUMBER_OF_REPLICAS]-${ENV_VALUES[OPENSEARCH_NUMBER_OF_REPLICAS]:-}}" hosts="$(prompt_until_valid "OpenSearch hosts (host:port, comma-separated)" "$hosts" validate_opensearch_hosts_format)" user="$(prompt_with_default "OpenSearch user" "${existing_user:-admin}")" password="$(prompt_secret_until_valid_with_default "OpenSearch password: " "${existing_password:-LightRAG2026_!@}" validate_opensearch_password_strength)" if [[ "$use_docker" == "yes" ]]; then if [[ -n "$existing_use_ssl" ]]; then env_value_is_true "$existing_use_ssl" && use_ssl="true" || use_ssl="false" else use_ssl="true" fi verify_certs="false" else if [[ -n "$existing_use_ssl" ]] && ! env_value_is_true "$existing_use_ssl"; then use_ssl_default="no" fi if [[ "$use_ssl_default" == "yes" ]]; then confirm_default_yes "Use SSL for OpenSearch?" && use_ssl="true" || use_ssl="false" else confirm_default_no "Use SSL for OpenSearch?" && use_ssl="true" || use_ssl="false" fi if [[ "$use_ssl" == "true" ]]; then if [[ -n "$existing_verify_certs" ]] && env_value_is_true "$existing_verify_certs"; then verify_certs_default="yes" fi if [[ "$verify_certs_default" == "yes" ]]; then confirm_default_yes "Verify OpenSearch TLS certificates?" && verify_certs="true" || verify_certs="false" else confirm_default_no "Verify OpenSearch TLS certificates?" && verify_certs="true" || verify_certs="false" fi fi fi local num_shards num_replicas if [[ "$use_docker" == "yes" ]]; then num_shards="1" num_replicas="0" else num_shards="$(prompt_until_valid "Number of index shards" "${existing_num_shards:-1}" validate_positive_integer)" num_replicas="$(prompt_until_valid "Number of index replicas (use 2 for 3-AZ clusters)" "${existing_num_replicas:-0}" validate_non_negative_integer)" fi ENV_VALUES["OPENSEARCH_HOSTS"]="$hosts" ENV_VALUES["OPENSEARCH_USER"]="$user" ENV_VALUES["OPENSEARCH_PASSWORD"]="$password" ENV_VALUES["OPENSEARCH_USE_SSL"]="$use_ssl" ENV_VALUES["OPENSEARCH_VERIFY_CERTS"]="$verify_certs" ENV_VALUES["OPENSEARCH_NUMBER_OF_SHARDS"]="$num_shards" ENV_VALUES["OPENSEARCH_NUMBER_OF_REPLICAS"]="$num_replicas" if [[ "$use_docker" == "yes" ]]; then set_compose_override "OPENSEARCH_HOSTS" "opensearch:9200" else set_compose_override "OPENSEARCH_HOSTS" "" fi } clear_bedrock_credentials() { unset 'ENV_VALUES[AWS_ACCESS_KEY_ID]' unset 'ENV_VALUES[AWS_SECRET_ACCESS_KEY]' unset 'ENV_VALUES[AWS_SESSION_TOKEN]' unset 'ENV_VALUES[AWS_REGION]' } collect_bedrock_credentials() { local access_key secret_key session_token region log_info "Bedrock ignores LLM_BINDING_API_KEY/EMBEDDING_BINDING_API_KEY; use SigV4 credentials or AWS_BEARER_TOKEN_BEDROCK." if [[ -n "${ENV_VALUES[AWS_ACCESS_KEY_ID]:-}" && -n "${ENV_VALUES[AWS_SECRET_ACCESS_KEY]:-}" ]]; then if confirm_default_yes "Reuse existing AWS Bedrock credentials?"; then region="$(prompt_with_default "AWS region" "${ENV_VALUES[AWS_REGION]:-us-east-1}")" ENV_VALUES["AWS_REGION"]="$region" return 0 fi fi if confirm_default_no "Store explicit AWS Bedrock credentials in .env?"; then access_key="$(prompt_required_secret "AWS access key ID: ")" secret_key="$(prompt_required_secret "AWS secret access key: ")" session_token="$(mask_sensitive_input "AWS session token (optional): ")" region="$(prompt_with_default "AWS region" "${ENV_VALUES[AWS_REGION]:-us-east-1}")" ENV_VALUES["AWS_ACCESS_KEY_ID"]="$access_key" ENV_VALUES["AWS_SECRET_ACCESS_KEY"]="$secret_key" ENV_VALUES["AWS_REGION"]="$region" if [[ -n "$session_token" ]]; then ENV_VALUES["AWS_SESSION_TOKEN"]="$session_token" else unset 'ENV_VALUES[AWS_SESSION_TOKEN]' fi return 0 fi log_info "Using the ambient AWS credential chain (for example IAM roles, AWS profiles, or aws sso login)." clear_bedrock_credentials region="$(prompt_clearable_with_default "AWS region (optional)" "${ENV_VALUES[AWS_REGION]:-}")" apply_clearable_env_value "AWS_REGION" "$region" } store_optional_env_value() { local key="$1" local value="${2:-}" if [[ -n "$value" ]]; then ENV_VALUES["$key"]="$value" else unset "ENV_VALUES[$key]" fi } provider_default_or_existing() { local selected_binding="$1" local existing_binding="${2:-}" local existing_value="${3:-}" local default_value="${4:-}" if [[ "$selected_binding" == "$existing_binding" && -n "$existing_value" ]]; then printf '%s' "$existing_value" return 0 fi printf '%s' "$default_value" } default_llm_model_for_binding() { local binding="$1" case "$binding" in openai|azure_openai) printf 'gpt-5-mini' ;; ollama|lollms|openai-ollama) printf 'mistral-nemo:latest' ;; gemini) printf 'gemini-flash-latest' ;; bedrock) printf 'anthropic.claude-3-5-sonnet-20241022-v2:0' ;; *) printf 'gpt-5-mini' ;; esac } default_embedding_model_for_binding() { local binding="$1" case "$binding" in openai|azure_openai) printf 'text-embedding-3-large' ;; ollama) printf 'bge-m3:latest' ;; jina) printf 'jina-embeddings-v4' ;; gemini) printf 'gemini-embedding-001' ;; bedrock) printf 'amazon.titan-embed-text-v2:0' ;; lollms) printf 'lollms_embedding_model' ;; *) printf 'text-embedding-3-large' ;; esac } default_embedding_dim_for_binding() { local binding="$1" case "$binding" in openai|azure_openai) printf '3072' ;; ollama|bedrock|lollms) printf '1024' ;; jina) printf '2048' ;; gemini) printf '1536' ;; *) printf '3072' ;; esac } collect_llm_config() { local options=("openai" "azure_openai" "ollama" "openai-ollama" "lollms" "gemini" "bedrock") local current_binding="${ENV_VALUES[LLM_BINDING]:-openai}" local binding model model_default host host_default api_key binding="$(prompt_choice "LLM provider" "$current_binding" "${options[@]}")" model_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[LLM_MODEL]:-}" "$(default_llm_model_for_binding "$binding")")" model="$(prompt_with_default "LLM model" "$model_default")" case "$binding" in ollama) host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[LLM_BINDING_HOST]:-}" "$(default_loopback_url 11434)")" host="$(prompt_with_default "Ollama host" "$host_default")" api_key="" ;; openai-ollama) host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[LLM_BINDING_HOST]:-}" "$(default_loopback_url 11434 "/v1")")" host="$(prompt_with_default "OpenAI-compatible Ollama endpoint" "$host_default")" api_key="$(prompt_secret_until_valid_with_default "LLM API key: " "${ENV_VALUES[LLM_BINDING_API_KEY]:-}" validate_api_key openai)" ;; lollms) host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[LLM_BINDING_HOST]:-}" "http://localhost:9600")" host="$(prompt_with_default "LoLLMs host" "$host_default")" api_key="" ;; azure_openai) host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[LLM_BINDING_HOST]:-}" "https://example.openai.azure.com/")" host="$(prompt_with_default "Azure OpenAI endpoint" "$host_default")" api_key="$(prompt_secret_until_valid_with_default "Azure OpenAI API key: " "${ENV_VALUES[LLM_BINDING_API_KEY]:-}" validate_api_key azure_openai)" ;; gemini) host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[LLM_BINDING_HOST]:-}" "DEFAULT_GEMINI_ENDPOINT")" host="$(prompt_with_default "Gemini endpoint" "$host_default")" api_key="$(prompt_secret_until_valid_with_default "Gemini API key: " "${ENV_VALUES[LLM_BINDING_API_KEY]:-}" validate_api_key gemini)" ;; bedrock) host="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[LLM_BINDING_HOST]:-}" "DEFAULT_BEDROCK_ENDPOINT")" api_key="" collect_bedrock_credentials ;; *) host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[LLM_BINDING_HOST]:-}" "https://api.openai.com/v1")" host="$(prompt_with_default "LLM endpoint" "$host_default")" api_key="$(prompt_secret_until_valid_with_default "LLM API key: " "${ENV_VALUES[LLM_BINDING_API_KEY]:-}" validate_api_key "$binding")" ;; esac ENV_VALUES["LLM_BINDING"]="$binding" ENV_VALUES["LLM_MODEL"]="$model" ENV_VALUES["LLM_BINDING_HOST"]="$host" store_optional_env_value "LLM_BINDING_API_KEY" "$api_key" # Role-specific LLM models — default to the base LLM_MODEL when unset in .env. local keyword_default query_default keyword_model query_model keyword_default="${ENV_VALUES[KEYWORD_LLM_MODEL]:-$model}" query_default="${ENV_VALUES[QUERY_LLM_MODEL]:-$model}" keyword_model="$(prompt_with_default "Keyword LLM model" "$keyword_default")" query_model="$(prompt_with_default "Query LLM model" "$query_default")" ENV_VALUES["KEYWORD_LLM_MODEL"]="$keyword_model" ENV_VALUES["QUERY_LLM_MODEL"]="$query_model" } collect_embedding_config() { local options=("openai" "azure_openai" "ollama" "jina" "lollms" "gemini" "bedrock") local current_binding="${ENV_VALUES[EMBEDDING_BINDING]:-openai}" local binding model model_default host host_default api_key dim dim_default if [[ "${ENV_VALUES[LLM_BINDING]:-}" == "openai-ollama" ]]; then binding="ollama" if [[ "$current_binding" != "ollama" ]]; then log_info "openai-ollama uses Ollama embeddings. Forcing embedding provider to ollama." fi else binding="$(prompt_choice "Embedding provider" "$current_binding" "${options[@]}")" fi model_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[EMBEDDING_MODEL]:-}" "$(default_embedding_model_for_binding "$binding")")" dim_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[EMBEDDING_DIM]:-}" "$(default_embedding_dim_for_binding "$binding")")" model="$(prompt_with_default "Embedding model" "$model_default")" dim="$(prompt_with_default "Embedding dimension" "$dim_default")" local llm_host_fallback="" llm_api_key_default="" if [[ "$binding" == "${ENV_VALUES[LLM_BINDING]:-}" ]]; then llm_host_fallback="${ENV_VALUES[LLM_BINDING_HOST]:-}" llm_api_key_default="${ENV_VALUES[LLM_BINDING_API_KEY]:-}" fi case "$binding" in ollama) host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}" "${llm_host_fallback:-$(default_loopback_url 11434)}")" host="$(prompt_with_default "Ollama embedding host" "$host_default")" api_key="" ;; lollms) host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}" "${llm_host_fallback:-http://localhost:9600}")" host="$(prompt_with_default "LoLLMs embedding host" "$host_default")" api_key="" ;; azure_openai) host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}" "${llm_host_fallback:-https://example.openai.azure.com/}")" host="$(prompt_with_default "Azure OpenAI endpoint" "$host_default")" api_key="$(prompt_secret_until_valid_with_default "Azure OpenAI API key: " "${ENV_VALUES[EMBEDDING_BINDING_API_KEY]:-$llm_api_key_default}" validate_api_key azure_openai)" ;; gemini) host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}" "${llm_host_fallback:-DEFAULT_GEMINI_ENDPOINT}")" host="$(prompt_with_default "Gemini endpoint" "$host_default")" api_key="$(prompt_secret_until_valid_with_default "Gemini API key: " "${ENV_VALUES[EMBEDDING_BINDING_API_KEY]:-$llm_api_key_default}" validate_api_key gemini)" ;; bedrock) host="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}" "${llm_host_fallback:-DEFAULT_BEDROCK_ENDPOINT}")" api_key="" collect_bedrock_credentials ;; jina) host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}" "${llm_host_fallback:-https://api.jina.ai/v1/embeddings}")" host="$(prompt_with_default "Jina endpoint" "$host_default")" api_key="$(prompt_secret_until_valid_with_default "Jina API key: " "${ENV_VALUES[EMBEDDING_BINDING_API_KEY]:-$llm_api_key_default}" validate_api_key jina)" ;; *) host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}" "${llm_host_fallback:-https://api.openai.com/v1}")" host="$(prompt_with_default "Embedding endpoint" "$host_default")" api_key="$(prompt_secret_until_valid_with_default "Embedding API key: " "${ENV_VALUES[EMBEDDING_BINDING_API_KEY]:-$llm_api_key_default}" validate_api_key "$binding")" ;; esac ENV_VALUES["EMBEDDING_BINDING"]="$binding" ENV_VALUES["EMBEDDING_MODEL"]="$model" ENV_VALUES["EMBEDDING_DIM"]="$dim" ENV_VALUES["EMBEDDING_BINDING_HOST"]="$host" store_optional_env_value "EMBEDDING_BINDING_API_KEY" "$api_key" # User chose a remote provider — clear the Docker deployment marker. unset 'ENV_VALUES[LIGHTRAG_SETUP_EMBEDDING_PROVIDER]' } collect_rerank_config() { # Pass "yes" to skip the "Enable reranking?" prompt (caller already asked it). # The optional second argument is retained for caller compatibility. local skip_enable_check="${1:-no}" local _docker_choice_override="${2:-prompt}" local options=("cohere" "jina" "aliyun") local current_binding="${ENV_VALUES[RERANK_BINDING]:-cohere}" local binding model host api_key local default_model="" default_host="" model_default="" host_default="" local previous_provider="${ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]:-}" unset 'ENV_VALUES[VLLM_RERANK_DTYPE]' if [[ "$skip_enable_check" != "yes" ]]; then local rerank_was_enabled="no" if [[ -n "${ENV_VALUES[RERANK_BINDING]:-}" && "${ENV_VALUES[RERANK_BINDING]}" != "null" ]]; then rerank_was_enabled="yes" fi local rerank_enabled="no" if [[ "$rerank_was_enabled" == "yes" ]]; then confirm_default_yes "Enable reranking?" && rerank_enabled="yes" else confirm_default_no "Enable reranking?" && rerank_enabled="yes" fi if [[ "$rerank_enabled" != "yes" ]]; then ENV_VALUES["RERANK_BINDING"]="null" unset 'ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]' return fi fi if [[ "$current_binding" == "null" ]]; then current_binding="cohere" fi binding="$(prompt_choice "Rerank provider" "$current_binding" "${options[@]}")" case "$binding" in cohere) default_model="rerank-v3.5" default_host="https://api.cohere.com/v2/rerank" ;; jina) default_model="jina-reranker-v2-base-multilingual" default_host="https://api.jina.ai/v1/rerank" ;; aliyun) default_model="gte-rerank-v2" default_host="https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank" ;; *) default_model="" default_host="" ;; esac if [[ "$previous_provider" == "vllm" ]]; then # Switching away from local vLLM should replace stale localhost/model values. model_default="$default_model" host_default="$default_host" else model_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[RERANK_MODEL]:-}" "$default_model")" host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[RERANK_BINDING_HOST]:-}" "$default_host")" fi model="$(prompt_with_default "Rerank model" "$model_default")" host="$(prompt_with_default "Rerank endpoint" "$host_default")" api_key="$(prompt_secret_until_valid_with_default "Rerank API key: " "${ENV_VALUES[RERANK_BINDING_API_KEY]:-}" validate_api_key "$binding")" ENV_VALUES["RERANK_BINDING"]="$binding" # Only env_base_flow's Docker branch should keep the local vLLM setup marker. unset 'ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]' if [[ -n "$model" ]]; then ENV_VALUES["RERANK_MODEL"]="$model" fi if [[ -n "$host" ]]; then ENV_VALUES["RERANK_BINDING_HOST"]="$host" fi store_optional_env_value "RERANK_BINDING_API_KEY" "$api_key" } collect_server_config() { local host port title description summary_language host="$(prompt_with_default "Server host" "${ENV_VALUES[HOST]:-0.0.0.0}")" port="$(prompt_until_valid "Server port" "${ENV_VALUES[PORT]:-9621}" validate_port)" title="$(prompt_with_default "WebUI title" "${ENV_VALUES[WEBUI_TITLE]:-My Graph KB}")" description="$(prompt_with_default "WebUI description" "${ENV_VALUES[WEBUI_DESCRIPTION]:-Simple and Fast Graph Based RAG System}")" summary_language="$(prompt_with_default "Summary language" "${ENV_VALUES[SUMMARY_LANGUAGE]:-English}")" ENV_VALUES["HOST"]="$host" ENV_VALUES["PORT"]="$port" ENV_VALUES["WEBUI_TITLE"]="$title" ENV_VALUES["WEBUI_DESCRIPTION"]="$description" ENV_VALUES["SUMMARY_LANGUAGE"]="$summary_language" } collect_ssl_config() { local cert key local ssl_enabled_default="no" case "${ENV_VALUES[SSL]:-}" in true|TRUE|True|1|yes|YES|Yes|y|Y|on|ON|On|t|T) ssl_enabled_default="yes" ;; esac if [[ "$ssl_enabled_default" == "yes" ]]; then if ! confirm_default_yes "Enable SSL/TLS for the API server?"; then unset 'ENV_VALUES[SSL]' unset 'ENV_VALUES[SSL_CERTFILE]' unset 'ENV_VALUES[SSL_KEYFILE]' SSL_CERT_SOURCE_PATH="" SSL_KEY_SOURCE_PATH="" return fi else if ! confirm_default_no "Enable SSL/TLS for the API server?"; then unset 'ENV_VALUES[SSL]' unset 'ENV_VALUES[SSL_CERTFILE]' unset 'ENV_VALUES[SSL_KEYFILE]' SSL_CERT_SOURCE_PATH="" SSL_KEY_SOURCE_PATH="" return fi fi cert="$(prompt_until_valid "SSL certificate file" "${ENV_VALUES[SSL_CERTFILE]:-}" validate_existing_file)" key="$(prompt_until_valid "SSL key file" "${ENV_VALUES[SSL_KEYFILE]:-}" validate_existing_file)" ENV_VALUES["SSL"]="true" ENV_VALUES["SSL_CERTFILE"]="$cert" ENV_VALUES["SSL_KEYFILE"]="$key" SSL_CERT_SOURCE_PATH="$cert" SSL_KEY_SOURCE_PATH="$key" } collect_security_config() { local required="${1:-no}" local default_yes="${2:-no}" local auth_accounts token_secret token_expire api_key whitelist local confirm_result=1 local whitelist_default="" local whitelist_is_set="no" if [[ -n "${ENV_VALUES[WHITELIST_PATHS]+set}" ]]; then whitelist_default="${ENV_VALUES[WHITELIST_PATHS]}" whitelist_is_set="yes" fi if [[ "$default_yes" == "yes" ]]; then if confirm_default_yes "Configure authentication and API key settings?"; then confirm_result=0 fi else if confirm_default_no "Configure authentication and API key settings?"; then confirm_result=0 fi fi if ((confirm_result != 0)); then if [[ "$required" == "yes" ]]; then echo "Warning: production deployments should configure AUTH_ACCOUNTS; API keys are optional on top." >&2 fi return fi echo "Press Enter to keep an existing value. Type 'clear' to remove it." >&2 if [[ "$whitelist_is_set" == "no" ]]; then whitelist_default="/health" elif [[ "$required" == "yes" && "$whitelist_default" == "/health,/api/*" ]]; then whitelist_default="/health" fi auth_accounts="$(prompt_clearable_with_default "Auth accounts (user:pass,comma-separated)" "${ENV_VALUES[AUTH_ACCOUNTS]:-}")" token_secret="$(prompt_clearable_secret_with_default "JWT token secret: " "${ENV_VALUES[TOKEN_SECRET]:-}")" token_expire="$(prompt_clearable_with_default "Token expire hours" "${ENV_VALUES[TOKEN_EXPIRE_HOURS]:-48}")" api_key="$(prompt_clearable_secret_with_default "LightRAG API key: " "${ENV_VALUES[LIGHTRAG_API_KEY]:-}")" whitelist="$(prompt_clearable_with_default "Whitelist paths (comma-separated)" "$whitelist_default")" if [[ "$whitelist_is_set" == "yes" && -z "$whitelist_default" && -z "$whitelist" ]]; then whitelist="$CLEAR_INPUT_SENTINEL" fi if [[ -z "$token_secret" ]]; then token_secret="$(openssl rand -hex 32 2>/dev/null || LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 64)" log_info "Generated TOKEN_SECRET and saved to .env." fi apply_clearable_env_value "AUTH_ACCOUNTS" "$auth_accounts" apply_clearable_env_value "TOKEN_SECRET" "$token_secret" apply_clearable_env_value "TOKEN_EXPIRE_HOURS" "$token_expire" apply_clearable_env_value "LIGHTRAG_API_KEY" "$api_key" apply_clearable_env_value "WHITELIST_PATHS" "$whitelist" "empty" } apply_clearable_env_value() { local key="$1" local value="${2:-}" local clear_mode="${3:-unset}" if [[ "$clear_mode" == "empty" && "$value" == "$CLEAR_INPUT_SENTINEL" ]]; then ENV_VALUES["$key"]="" return 0 fi if [[ "$value" == "$CLEAR_INPUT_SENTINEL" || -z "$value" ]]; then unset "ENV_VALUES[$key]" return 0 fi ENV_VALUES["$key"]="$value" } collect_observability_config() { local secret_key public_key host if ! confirm_default_no "Enable Langfuse observability?"; then unset 'ENV_VALUES[LANGFUSE_ENABLE_TRACE]' unset 'ENV_VALUES[LANGFUSE_SECRET_KEY]' unset 'ENV_VALUES[LANGFUSE_PUBLIC_KEY]' unset 'ENV_VALUES[LANGFUSE_HOST]' return fi secret_key="$(prompt_secret_until_valid_with_default "Langfuse secret key: " "${ENV_VALUES[LANGFUSE_SECRET_KEY]:-}" validate_api_key langfuse)" public_key="$(prompt_secret_until_valid_with_default "Langfuse public key: " "${ENV_VALUES[LANGFUSE_PUBLIC_KEY]:-}" validate_api_key langfuse)" host="$(prompt_with_default "Langfuse host" "${ENV_VALUES[LANGFUSE_HOST]:-https://cloud.langfuse.com}")" if [[ -n "$secret_key" ]]; then ENV_VALUES["LANGFUSE_SECRET_KEY"]="$secret_key" fi if [[ -n "$public_key" ]]; then ENV_VALUES["LANGFUSE_PUBLIC_KEY"]="$public_key" fi if [[ -n "$host" ]]; then ENV_VALUES["LANGFUSE_HOST"]="$host" fi ENV_VALUES["LANGFUSE_ENABLE_TRACE"]="true" } show_summary() { local key local value echo log_info "Configuration summary:" if ((${#ENV_VALUES[@]} > 0)); then local -a sorted_keys mapfile -t sorted_keys < <(printf '%s\n' "${!ENV_VALUES[@]}" | sort) for key in "${sorted_keys[@]}"; do value="${ENV_VALUES[$key]}" if is_sensitive_env_key "$key"; then value="***" fi printf ' %s=%s\n' "$key" "$value" done fi if ((${#DOCKER_SERVICES[@]} > 0)); then echo log_info "Docker services to include:" for service in "${DOCKER_SERVICES[@]}"; do echo " - $service" done echo " Compose file: docker-compose.final.yml" fi } # Preserve already-staged SSL mounts when regenerating compose output. The # setup wizards treat .env as the configuration for the current target runtime, # not as a single file guaranteed to work for both host and Docker Compose at # the same time. A later wizard run may rewrite .env again when the operator # switches between host and compose workflows. prepare_inherited_ssl_assets_for_compose() { local existing_compose="${1:-}" local staged_cert_source="$SSL_CERT_SOURCE_PATH" local staged_key_source="$SSL_KEY_SOURCE_PATH" local preserved_cert_path="" local preserved_key_path="" if [[ -n "$SSL_CERT_SOURCE_PATH" ]] && ! validate_existing_file "$SSL_CERT_SOURCE_PATH"; then if [[ -n "$existing_compose" ]]; then preserved_cert_path="$(read_service_environment_value "$existing_compose" "lightrag" "SSL_CERTFILE" || true)" fi if [[ "$preserved_cert_path" == /app/data/certs/* ]]; then log_warn "SSL_CERTFILE source is missing; preserving the existing compose SSL certificate mount." staged_cert_source="" ENV_VALUES["SSL_CERTFILE"]="$preserved_cert_path" set_compose_override "SSL_CERTFILE" "$preserved_cert_path" else format_error "Invalid SSL_CERTFILE" \ "Set it to an existing certificate file, disable SSL, or rerun the wizard to choose a new certificate." return 1 fi fi if [[ -n "$SSL_KEY_SOURCE_PATH" ]] && ! validate_existing_file "$SSL_KEY_SOURCE_PATH"; then if [[ -n "$existing_compose" ]]; then preserved_key_path="$(read_service_environment_value "$existing_compose" "lightrag" "SSL_KEYFILE" || true)" fi if [[ "$preserved_key_path" == /app/data/certs/* ]]; then log_warn "SSL_KEYFILE source is missing; preserving the existing compose SSL key mount." staged_key_source="" ENV_VALUES["SSL_KEYFILE"]="$preserved_key_path" set_compose_override "SSL_KEYFILE" "$preserved_key_path" else format_error "Invalid SSL_KEYFILE" \ "Set it to an existing private key file, disable SSL, or rerun the wizard to choose a new key." return 1 fi fi SSL_CERT_SOURCE_PATH="$staged_cert_source" SSL_KEY_SOURCE_PATH="$staged_key_source" if [[ -n "$SSL_CERT_SOURCE_PATH" || -n "$SSL_KEY_SOURCE_PATH" ]]; then stage_ssl_assets "$SSL_CERT_SOURCE_PATH" "$SSL_KEY_SOURCE_PATH" fi } prepare_managed_service_assets_for_compose() { local existing_compose="${1:-}" if ! prepare_inherited_ssl_assets_for_compose "$existing_compose"; then return 1 fi if [[ -n "${DOCKER_SERVICE_SET[redis]:-}" ]]; then stage_redis_config_asset || return 1 fi } env_base_flow() { local vllm_embed_api_key="" local vllm_rerank_api_key="" local existing_vllm_embed_model="" local existing_embedding_dim="" local existing_vllm_embed_port="" local existing_vllm_embed_host="" local existing_vllm_embed_device="" local previous_embedding_provider="" local existing_vllm_rerank_model="" local existing_vllm_rerank_port="" local existing_vllm_rerank_host="" local existing_vllm_rerank_device="" local previous_rerank_provider="" if host_cuda_available; then log_info "GPU detected: NVIDIA GPU found. New local vLLM services default to CUDA (GPU image + float16)." else log_info "GPU detection: no NVIDIA GPU found. New local vLLM services default to CPU image + float32." fi reset_state load_existing_env_if_present initialize_default_storage_backends log_info "Base configuration wizard (LLM / Embedding / Reranker)" echo "This wizard only modifies LLM, embedding, and reranker settings." echo "Storage, server, and security settings are preserved." echo "" log_step "LLM configuration" collect_llm_config echo "" # ── Embedding ──────────────────────────────────────────────────────────────── log_step "Embedding configuration" local docker_embed_default="no" previous_embedding_provider="${ENV_VALUES[LIGHTRAG_SETUP_EMBEDDING_PROVIDER]:-}" if [[ "$previous_embedding_provider" == "vllm" ]]; then docker_embed_default="yes" fi local use_docker_embed="no" if [[ "$docker_embed_default" == "yes" ]]; then confirm_default_yes "Run embedding model locally via Docker (vLLM)?" && use_docker_embed="yes" || use_docker_embed="no" else confirm_default_no "Run embedding model locally via Docker (vLLM)?" && use_docker_embed="yes" || use_docker_embed="no" fi if [[ "$use_docker_embed" == "yes" ]]; then existing_vllm_embed_model="${ORIGINAL_ENV_VALUES[VLLM_EMBED_MODEL]-${ENV_VALUES[VLLM_EMBED_MODEL]:-}}" existing_embedding_dim="${ORIGINAL_ENV_VALUES[EMBEDDING_DIM]-${ENV_VALUES[EMBEDDING_DIM]:-}}" existing_vllm_embed_port="${ORIGINAL_ENV_VALUES[VLLM_EMBED_PORT]-${ENV_VALUES[VLLM_EMBED_PORT]:-}}" existing_vllm_embed_host="${ORIGINAL_ENV_VALUES[EMBEDDING_BINDING_HOST]-${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}}" existing_vllm_embed_device="${ORIGINAL_ENV_VALUES[VLLM_EMBED_DEVICE]-${ENV_VALUES[VLLM_EMBED_DEVICE]:-}}" apply_preset_overwrite "${PRESET_VLLM_EMBEDDING[@]}" local vllm_embed_device vllm_embed_device="$(resolve_local_device_default "$existing_vllm_embed_device")" vllm_embed_device="$(prompt_choice "Embedding device" "$vllm_embed_device" "cpu" "cuda")" if [[ "$vllm_embed_device" == "cuda" ]] && ! host_cuda_available; then log_warn "CUDA device selected for vLLM embedding but no NVIDIA driver detected on host." fi if [[ -n "$existing_vllm_embed_port" ]]; then ENV_VALUES["VLLM_EMBED_PORT"]="$existing_vllm_embed_port" fi if [[ -n "$existing_embedding_dim" ]]; then ENV_VALUES["EMBEDDING_DIM"]="$existing_embedding_dim" fi if [[ "$previous_embedding_provider" == "vllm" && -n "$existing_vllm_embed_host" ]]; then ENV_VALUES["EMBEDDING_BINDING_HOST"]="$existing_vllm_embed_host" else ENV_VALUES["EMBEDDING_BINDING_HOST"]="http://localhost:${ENV_VALUES[VLLM_EMBED_PORT]:-8001}/v1" fi local embed_model embed_model="$(prompt_with_default "Embedding model" "${existing_vllm_embed_model:-${ENV_VALUES[VLLM_EMBED_MODEL]:-BAAI/bge-m3}}")" ENV_VALUES["VLLM_EMBED_MODEL"]="$embed_model" ENV_VALUES["EMBEDDING_MODEL"]="$embed_model" ENV_VALUES["VLLM_EMBED_DEVICE"]="$vllm_embed_device" ENV_VALUES["LIGHTRAG_SETUP_EMBEDDING_PROVIDER"]="vllm" vllm_embed_api_key="${ENV_VALUES[VLLM_EMBED_API_KEY]:-${ENV_VALUES[EMBEDDING_BINDING_API_KEY]:-}}" if [[ -z "$vllm_embed_api_key" ]]; then vllm_embed_api_key="$(openssl rand -hex 16 2>/dev/null || LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 32)" fi ENV_VALUES["VLLM_EMBED_API_KEY"]="$vllm_embed_api_key" ENV_VALUES["EMBEDDING_BINDING_API_KEY"]="$vllm_embed_api_key" add_docker_service "vllm-embed" set_compose_override "EMBEDDING_BINDING_HOST" \ "http://vllm-embed:${ENV_VALUES[VLLM_EMBED_PORT]:-8001}/v1" else collect_embedding_config fi echo "" # ── Reranker ───────────────────────────────────────────────────────────────── log_step "Reranker configuration" local rerank_enabled_default="no" if [[ -n "${ENV_VALUES[RERANK_BINDING]:-}" && "${ENV_VALUES[RERANK_BINDING]}" != "null" ]]; then rerank_enabled_default="yes" fi previous_rerank_provider="${ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]:-}" local enable_reranking="no" if [[ "$rerank_enabled_default" == "yes" ]]; then confirm_default_yes "Enable reranking?" && enable_reranking="yes" || enable_reranking="no" else confirm_default_no "Enable reranking?" && enable_reranking="yes" || enable_reranking="no" fi if [[ "$enable_reranking" == "yes" ]]; then local docker_rerank_default="no" if [[ "$previous_rerank_provider" == "vllm" ]]; then docker_rerank_default="yes" fi local use_docker_rerank="no" if [[ "$docker_rerank_default" == "yes" ]]; then confirm_default_yes "Run rerank service locally via Docker?" && use_docker_rerank="yes" || use_docker_rerank="no" else confirm_default_no "Run rerank service locally via Docker?" && use_docker_rerank="yes" || use_docker_rerank="no" fi if [[ "$use_docker_rerank" == "yes" ]]; then existing_vllm_rerank_model="${ORIGINAL_ENV_VALUES[VLLM_RERANK_MODEL]-${ENV_VALUES[VLLM_RERANK_MODEL]:-}}" existing_vllm_rerank_port="${ORIGINAL_ENV_VALUES[VLLM_RERANK_PORT]-${ENV_VALUES[VLLM_RERANK_PORT]:-}}" existing_vllm_rerank_host="${ORIGINAL_ENV_VALUES[RERANK_BINDING_HOST]-${ENV_VALUES[RERANK_BINDING_HOST]:-}}" existing_vllm_rerank_device="${ORIGINAL_ENV_VALUES[VLLM_RERANK_DEVICE]-${ENV_VALUES[VLLM_RERANK_DEVICE]:-}}" apply_preset_overwrite "${PRESET_VLLM_RERANKER[@]}" local vllm_rerank_device vllm_rerank_device="$(resolve_local_device_default "$existing_vllm_rerank_device")" vllm_rerank_device="$(prompt_choice "Rerank device" "$vllm_rerank_device" "cpu" "cuda")" if [[ "$vllm_rerank_device" == "cuda" ]] && ! host_cuda_available; then log_warn "CUDA device selected for vLLM rerank but no NVIDIA driver detected on host." fi local rerank_model rerank_port if [[ -n "$existing_vllm_rerank_port" ]]; then ENV_VALUES["VLLM_RERANK_PORT"]="$existing_vllm_rerank_port" fi if [[ "$previous_rerank_provider" == "vllm" && -n "$existing_vllm_rerank_host" ]]; then ENV_VALUES["RERANK_BINDING_HOST"]="$existing_vllm_rerank_host" else ENV_VALUES["RERANK_BINDING_HOST"]="http://localhost:${ENV_VALUES[VLLM_RERANK_PORT]:-8000}/rerank" fi rerank_model="$(prompt_with_default "Rerank model" "${existing_vllm_rerank_model:-${ENV_VALUES[VLLM_RERANK_MODEL]:-BAAI/bge-reranker-v2-m3}}")" rerank_port="${ENV_VALUES[VLLM_RERANK_PORT]:-8000}" ENV_VALUES["VLLM_RERANK_MODEL"]="$rerank_model" ENV_VALUES["RERANK_MODEL"]="$rerank_model" ENV_VALUES["VLLM_RERANK_PORT"]="$rerank_port" ENV_VALUES["VLLM_RERANK_DEVICE"]="$vllm_rerank_device" ENV_VALUES["LIGHTRAG_SETUP_RERANK_PROVIDER"]="vllm" vllm_rerank_api_key="${ENV_VALUES[VLLM_RERANK_API_KEY]:-${ENV_VALUES[RERANK_BINDING_API_KEY]:-}}" if [[ -z "$vllm_rerank_api_key" ]]; then vllm_rerank_api_key="$(openssl rand -hex 16 2>/dev/null || LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 32)" fi ENV_VALUES["VLLM_RERANK_API_KEY"]="$vllm_rerank_api_key" ENV_VALUES["RERANK_BINDING_API_KEY"]="$vllm_rerank_api_key" add_docker_service "vllm-rerank" set_compose_override "RERANK_BINDING_HOST" \ "http://vllm-rerank:${rerank_port}/rerank" else # Reranking enabled but not via Docker — ask provider/host/model/api_key collect_rerank_config "yes" "no" fi else ENV_VALUES["RERANK_BINDING"]="null" unset 'ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]' fi echo "" finalize_base_setup } finalize_base_setup() { local backup_path local compose_file local existing_compose local compose_action="write_env_only" local runtime_target="$DEFAULT_RUNTIME_TARGET" local show_host_start_hint="no" local svc_names="" if [[ ! -f "${REPO_ROOT}/env.example" ]]; then format_error "env.example is missing in $REPO_ROOT" "Restore env.example before running setup." return 1 fi if [[ ! -w "$REPO_ROOT" ]]; then format_error "No write permission in $REPO_ROOT" "Run the setup from a writable directory." return 1 fi if ! validate_sensitive_env_literals; then return 1 fi if ! validate_mongo_vector_storage_config \ "${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-}" \ "${ENV_VALUES[MONGO_URI]:-}" \ "${ENV_VALUES[LIGHTRAG_SETUP_MONGODB_DEPLOYMENT]:-}"; then return 1 fi show_summary if ! confirm_required_yes_no "${COLOR_YELLOW}Ready to proceed and write .env${COLOR_RESET}"; then log_warn "Setup cancelled." return 1 fi existing_compose="$(find_generated_compose_file)" compose_file="${REPO_ROOT}/docker-compose.final.yml" record_existing_managed_root_services "$existing_compose" restore_storage_docker_services_from_env configure_mongodb_compose_migration_rewrite "$existing_compose" configure_base_compose_rewrites if ((${#DOCKER_SERVICES[@]} > 0)); then # LightRAG depends on managed Docker services; it must run via Docker. svc_names="$(printf '%s ' "${DOCKER_SERVICES[@]}")" svc_names="${svc_names% }" echo "LightRAG requires Docker services: ${svc_names}" if ! confirm_default_yes "${COLOR_YELLOW}The compose file will be created/updated. Continue?${COLOR_RESET}"; then log_warn "Setup cancelled." return 1 fi compose_action="rewrite_compose" runtime_target="compose" else resolve_compose_output_action \ "$existing_compose" \ compose_action \ runtime_target \ show_host_start_hint fi if [[ "$compose_action" == "rewrite_compose" ]]; then backup_existing_compose_for_action "$compose_action" "$existing_compose" || return 1 if ! prepare_managed_service_assets_for_compose "$existing_compose"; then return 1 fi collect_preserved_storage_service_images "$existing_compose" prepare_compose_env_overrides elif [[ "$compose_action" == "delete_compose_and_switch_host" ]]; then backup_existing_compose_for_action "$compose_action" "$existing_compose" || return 1 fi backup_path="$(backup_env_file)" if [[ -n "$backup_path" ]]; then log_success "Backed up existing .env to $backup_path" fi clear_deprecated_vllm_dtype_state set_runtime_target "$runtime_target" || return 1 generate_env_file "${REPO_ROOT}/env.example" "${REPO_ROOT}/.env" log_success "Wrote .env" case "$compose_action" in rewrite_compose) prepare_compose_output_from_existing "$compose_file" "$existing_compose" || return 1 generate_docker_compose "$compose_file" log_success "Wrote ${compose_file}" echo " To start: docker compose -f ${compose_file} up -d" ;; delete_compose_and_switch_host) remove_existing_compose_file "$existing_compose" || return 1 echo " To start: lightrag-server" ;; *) if [[ "$show_host_start_hint" == "yes" ]]; then echo " To start: lightrag-server" fi ;; esac } env_storage_flow() { local env_file="${REPO_ROOT}/.env" local db_type local db_order=("postgresql" "neo4j" "mongodb" "redis" "milvus" "qdrant" "memgraph" "opensearch") if [[ ! -f "$env_file" ]]; then format_error "No .env file found." "Run 'make env-base' first to configure LLM and embedding." return 1 fi reset_state load_existing_env_if_present log_info "Storage configuration wizard" echo "This wizard only modifies storage backend settings." echo "LLM, embedding, reranker, server, and security settings are preserved." echo "" log_step "Storage backend selection" select_storage_backends "custom" log_debug "Storage selections: kv=${ENV_VALUES[LIGHTRAG_KV_STORAGE]:-} vector=${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-} graph=${ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]:-} doc=${ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]:-}" clear_unused_storage_deployment_markers log_step "Database configuration" for db_type in "${db_order[@]}"; do if [[ -n "${REQUIRED_DB_TYPES[$db_type]+set}" ]]; then collect_database_config "$db_type" "$(storage_default_docker_for_db_type "$db_type")" echo "" fi done finalize_storage_setup } finalize_storage_setup() { local backup_path local compose_file local existing_compose local compose_action="write_env_only" local runtime_target="$DEFAULT_RUNTIME_TARGET" local show_host_start_hint="no" if [[ ! -f "${REPO_ROOT}/env.example" ]]; then format_error "env.example is missing in $REPO_ROOT" "Restore env.example before running setup." return 1 fi if [[ ! -w "$REPO_ROOT" ]]; then format_error "No write permission in $REPO_ROOT" "Run the setup from a writable directory." return 1 fi if [[ -n "${ENV_VALUES[LIGHTRAG_KV_STORAGE]:-}" ]]; then if ! validate_required_variables \ "${ENV_VALUES[LIGHTRAG_KV_STORAGE]}" \ "${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]}" \ "${ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]}" \ "${ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]}"; then return 1 fi fi if ! validate_mongo_vector_storage_config \ "${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-}" \ "${ENV_VALUES[MONGO_URI]:-}" \ "${ENV_VALUES[LIGHTRAG_SETUP_MONGODB_DEPLOYMENT]:-}"; then return 1 fi if ! validate_sensitive_env_literals; then return 1 fi show_summary if ! confirm_required_yes_no "${COLOR_YELLOW}Ready to proceed and write .env${COLOR_RESET}"; then log_warn "Setup cancelled." return 1 fi existing_compose="$(find_generated_compose_file)" compose_file="${REPO_ROOT}/docker-compose.final.yml" record_existing_managed_root_services "$existing_compose" restore_vllm_docker_services_from_env configure_storage_compose_rewrites configure_mongodb_compose_migration_rewrite "$existing_compose" resolve_compose_output_action \ "$existing_compose" \ compose_action \ runtime_target \ show_host_start_hint if [[ "$compose_action" == "rewrite_compose" ]]; then backup_existing_compose_for_action "$compose_action" "$existing_compose" || return 1 if ! prepare_managed_service_assets_for_compose "$existing_compose"; then return 1 fi collect_preserved_storage_service_images "$existing_compose" prepare_compose_env_overrides elif [[ "$compose_action" == "delete_compose_and_switch_host" ]]; then backup_existing_compose_for_action "$compose_action" "$existing_compose" || return 1 fi backup_path="$(backup_env_file)" if [[ -n "$backup_path" ]]; then log_success "Backed up existing .env to $backup_path" fi clear_deprecated_vllm_dtype_state set_runtime_target "$runtime_target" || return 1 generate_env_file "${REPO_ROOT}/env.example" "${REPO_ROOT}/.env" log_success "Wrote .env" case "$compose_action" in rewrite_compose) prepare_compose_output_from_existing "$compose_file" "$existing_compose" || return 1 generate_docker_compose "$compose_file" log_success "Wrote ${compose_file}" echo " To start: docker compose -f ${compose_file} up -d" ;; delete_compose_and_switch_host) remove_existing_compose_file "$existing_compose" || return 1 echo " To start: lightrag-server" ;; *) if [[ "$show_host_start_hint" == "yes" ]]; then echo " To start: lightrag-server" fi ;; esac } env_server_flow() { local env_file="${REPO_ROOT}/.env" if [[ ! -f "$env_file" ]]; then format_error "No .env file found." "Run 'make env-base' first to configure LLM and embedding." return 1 fi reset_state load_existing_env_if_present log_info "Server configuration wizard" echo "This wizard only modifies server, security, and SSL settings." echo "LLM, embedding, reranker, and storage settings are preserved." echo "" log_step "Server configuration" collect_server_config echo "" log_step "Security configuration" collect_security_config "no" "no" echo "" log_step "SSL configuration" collect_ssl_config echo "" finalize_server_setup } finalize_server_setup() { local backup_path local compose_file local existing_compose local compose_action="write_env_only" local runtime_target="$DEFAULT_RUNTIME_TARGET" local show_host_start_hint="no" if [[ ! -f "${REPO_ROOT}/env.example" ]]; then format_error "env.example is missing in $REPO_ROOT" "Restore env.example before running setup." return 1 fi if [[ ! -w "$REPO_ROOT" ]]; then format_error "No write permission in $REPO_ROOT" "Run the setup from a writable directory." return 1 fi if ! validate_sensitive_env_literals; then return 1 fi if ! validate_auth_accounts_runtime_config \ "${ENV_VALUES[AUTH_ACCOUNTS]:-}"; then return 1 fi if ! validate_mongo_vector_storage_config \ "${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-}" \ "${ENV_VALUES[MONGO_URI]:-}" \ "${ENV_VALUES[LIGHTRAG_SETUP_MONGODB_DEPLOYMENT]:-}"; then return 1 fi show_summary if ! confirm_required_yes_no "${COLOR_YELLOW}Ready to proceed and write .env${COLOR_RESET}"; then log_warn "Setup cancelled." return 1 fi existing_compose="$(find_generated_compose_file)" compose_file="${REPO_ROOT}/docker-compose.final.yml" record_existing_managed_root_services "$existing_compose" restore_storage_docker_services_from_env restore_vllm_docker_services_from_env configure_mongodb_compose_migration_rewrite "$existing_compose" resolve_compose_output_action \ "$existing_compose" \ compose_action \ runtime_target \ show_host_start_hint if [[ "$compose_action" == "rewrite_compose" ]]; then backup_existing_compose_for_action "$compose_action" "$existing_compose" || return 1 if ! prepare_managed_service_assets_for_compose "$existing_compose"; then return 1 fi collect_preserved_storage_service_images "$existing_compose" prepare_compose_env_overrides elif [[ "$compose_action" == "delete_compose_and_switch_host" ]]; then backup_existing_compose_for_action "$compose_action" "$existing_compose" || return 1 if [[ -n "$SSL_CERT_SOURCE_PATH" ]] && ! validate_existing_file "$SSL_CERT_SOURCE_PATH"; then format_error "Invalid SSL_CERTFILE" \ "Set it to an existing certificate file, disable SSL, or rerun the wizard to choose a new certificate." return 1 fi if [[ -n "$SSL_KEY_SOURCE_PATH" ]] && ! validate_existing_file "$SSL_KEY_SOURCE_PATH"; then format_error "Invalid SSL_KEYFILE" \ "Set it to an existing private key file, disable SSL, or rerun the wizard to choose a new key." return 1 fi else if [[ -n "$SSL_CERT_SOURCE_PATH" ]] && ! validate_existing_file "$SSL_CERT_SOURCE_PATH"; then format_error "Invalid SSL_CERTFILE" \ "Set it to an existing certificate file, disable SSL, or rerun the wizard to choose a new certificate." return 1 fi if [[ -n "$SSL_KEY_SOURCE_PATH" ]] && ! validate_existing_file "$SSL_KEY_SOURCE_PATH"; then format_error "Invalid SSL_KEYFILE" \ "Set it to an existing private key file, disable SSL, or rerun the wizard to choose a new key." return 1 fi fi backup_path="$(backup_env_file)" if [[ -n "$backup_path" ]]; then log_success "Backed up existing .env to $backup_path" fi clear_deprecated_vllm_dtype_state set_runtime_target "$runtime_target" || return 1 generate_env_file "${REPO_ROOT}/env.example" "${REPO_ROOT}/.env" log_success "Wrote .env" case "$compose_action" in rewrite_compose) prepare_compose_output_from_existing "$compose_file" "$existing_compose" || return 1 generate_docker_compose "$compose_file" log_success "Wrote ${compose_file}" log_success "Server port and security settings updated in compose." echo " To restart: docker compose -f ${compose_file} up -d --force-recreate lightrag" ;; delete_compose_and_switch_host) remove_existing_compose_file "$existing_compose" || return 1 echo " To start: lightrag-server" ;; *) if [[ "$show_host_start_hint" == "yes" ]]; then echo " To start: lightrag-server" fi ;; esac } load_env_file() { local env_file="$1" local line key value if [[ ! -f "$env_file" ]]; then format_error ".env file not found at $env_file" "Run make env-base to generate it." return 1 fi while IFS= read -r line || [[ -n "$line" ]]; do if [[ "$line" =~ ^[A-Za-z0-9_]+= ]]; then key="${line%%=*}" value="${line#*=}" if [[ "$value" =~ ^\".*\"$ ]]; then value="${value:1:${#value}-2}" value="${value//\\\$/\$}" value="${value//\\\"/\"}" value="${value//\\\\/\\}" elif [[ "$value" =~ ^\'.*\'$ ]]; then value="${value:1:${#value}-2}" fi ENV_VALUES["$key"]="$value" fi done < "$env_file" } validate_ssl_runtime_path() { local path="$1" local runtime_target="${ENV_VALUES[LIGHTRAG_RUNTIME_TARGET]:-$DEFAULT_RUNTIME_TARGET}" local staged_path="" if validate_existing_file "$path"; then return 0 fi if [[ "$runtime_target" == "compose" && "$path" == /app/data/certs/* ]]; then staged_path="${REPO_ROOT}/data/certs/${path#/app/data/certs/}" validate_existing_file "$staged_path" return $? fi return 1 } validate_env_file() { local env_file="${REPO_ROOT}/.env" local errors=0 local kv vector graph doc_status local runtime_target local storage db_type local -A referenced_db_types=() reset_state if ! load_env_file "$env_file"; then return 1 fi kv="${ENV_VALUES[LIGHTRAG_KV_STORAGE]:-}" vector="${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-}" graph="${ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]:-}" doc_status="${ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]:-}" runtime_target="${ENV_VALUES[LIGHTRAG_RUNTIME_TARGET]:-$DEFAULT_RUNTIME_TARGET}" for storage in "$kv" "$vector" "$graph" "$doc_status"; do if [[ -z "$storage" ]]; then continue fi db_type="${STORAGE_DB_TYPES[$storage]:-}" if [[ -n "$db_type" ]]; then referenced_db_types["$db_type"]=1 fi done if ! validate_runtime_target "$runtime_target"; then errors=1 fi if [[ -z "$kv" || -z "$vector" || -z "$graph" || -z "$doc_status" ]]; then format_error "Storage selections are missing in .env" "Set LIGHTRAG_*_STORAGE variables." return 1 fi if ! validate_mongo_vector_storage_config \ "$vector" \ "${ENV_VALUES[MONGO_URI]:-}" \ "${ENV_VALUES[LIGHTRAG_SETUP_MONGODB_DEPLOYMENT]:-}"; then errors=1 fi if ! validate_required_variables "$kv" "$vector" "$graph" "$doc_status"; then errors=1 fi if ! validate_auth_accounts_runtime_config \ "${ENV_VALUES[AUTH_ACCOUNTS]:-}"; then errors=1 fi if ! validate_sensitive_env_literals; then errors=1 fi if [[ "${ENV_VALUES[SSL]:-false}" == "true" ]]; then if ! validate_ssl_runtime_path "${ENV_VALUES[SSL_CERTFILE]:-}"; then format_error "Invalid SSL_CERTFILE" "Set it to an existing certificate file when SSL=true." errors=1 fi if ! validate_ssl_runtime_path "${ENV_VALUES[SSL_KEYFILE]:-}"; then format_error "Invalid SSL_KEYFILE" "Set it to an existing private key file when SSL=true." errors=1 fi fi if [[ -n "${referenced_db_types[neo4j]+set}" ]] && [[ -n "${ENV_VALUES[NEO4J_URI]:-}" ]] && ! validate_uri "${ENV_VALUES[NEO4J_URI]}" neo4j; then format_error "Invalid NEO4J_URI" "Use neo4j:// or bolt:// format." errors=1 fi if [[ -n "${referenced_db_types[mongodb]+set}" ]] && [[ -n "${ENV_VALUES[MONGO_URI]:-}" ]] && ! validate_uri "${ENV_VALUES[MONGO_URI]}" mongodb; then format_error "Invalid MONGO_URI" "Use mongodb:// or mongodb+srv:// format." errors=1 fi if [[ -n "${referenced_db_types[redis]+set}" ]] && [[ -n "${ENV_VALUES[REDIS_URI]:-}" ]] && ! validate_uri "${ENV_VALUES[REDIS_URI]}" redis; then format_error "Invalid REDIS_URI" "Use redis:// or rediss:// format." errors=1 fi if [[ -n "${referenced_db_types[milvus]+set}" ]] && [[ -n "${ENV_VALUES[MILVUS_URI]:-}" ]] && ! validate_uri "${ENV_VALUES[MILVUS_URI]}" milvus; then format_error "Invalid MILVUS_URI" "Use http://host:port format." errors=1 fi if [[ -n "${referenced_db_types[qdrant]+set}" ]] && [[ -n "${ENV_VALUES[QDRANT_URL]:-}" ]] && ! validate_uri "${ENV_VALUES[QDRANT_URL]}" qdrant; then format_error "Invalid QDRANT_URL" "Use http://host:port format." errors=1 fi if [[ -n "${referenced_db_types[memgraph]+set}" ]] && [[ -n "${ENV_VALUES[MEMGRAPH_URI]:-}" ]] && ! validate_uri "${ENV_VALUES[MEMGRAPH_URI]}" memgraph; then format_error "Invalid MEMGRAPH_URI" "Use bolt://host:port format." errors=1 fi if [[ -n "${referenced_db_types[postgresql]+set}" ]] && [[ -n "${ENV_VALUES[POSTGRES_PORT]:-}" ]] && ! validate_port "${ENV_VALUES[POSTGRES_PORT]}"; then format_error "Invalid POSTGRES_PORT" "Use a port between 1 and 65535." errors=1 fi if [[ -n "${referenced_db_types[opensearch]+set}" ]] && [[ -v 'ENV_VALUES[OPENSEARCH_HOSTS]' ]] && [[ -z "${ENV_VALUES[OPENSEARCH_HOSTS]}" ]]; then format_error "Empty OPENSEARCH_HOSTS" "Set it to host:port (e.g. localhost:9200)." errors=1 fi if [[ -n "${referenced_db_types[opensearch]+set}" ]]; then if ! validate_opensearch_config \ "${ENV_VALUES[LIGHTRAG_SETUP_OPENSEARCH_DEPLOYMENT]:-}" \ "${ENV_VALUES[OPENSEARCH_HOSTS]:-}" \ "${ENV_VALUES[OPENSEARCH_USER]:-}" \ "${ENV_VALUES[OPENSEARCH_PASSWORD]:-}" \ "${ENV_VALUES[OPENSEARCH_NUMBER_OF_SHARDS]-1}" \ "${ENV_VALUES[OPENSEARCH_NUMBER_OF_REPLICAS]-0}"; then errors=1 fi fi if ((errors != 0)); then return 1 fi log_success "Validation passed." } report_security_issue() { local message="$1" local suggestion="${2:-}" echo "${COLOR_YELLOW:-}Security issue:${COLOR_RESET:-} $message" if [[ -n "$suggestion" ]]; then echo " Suggestion: $suggestion" fi } security_check_env_file() { local env_file="${REPO_ROOT}/.env" local findings=0 local auth_accounts="" local token_secret="" local api_key="" local whitelist_paths="" local whitelist_is_set="no" local effective_whitelist="" local kv="" local vector="" local graph="" local doc_status="" local storage="" local db_type="" local opensearch_in_use="no" local key value local invalid_sensitive_keys=() local -A referenced_db_types=() reset_state if ! load_env_file "$env_file"; then return 1 fi auth_accounts="${ENV_VALUES[AUTH_ACCOUNTS]:-}" token_secret="${ENV_VALUES[TOKEN_SECRET]:-}" api_key="${ENV_VALUES[LIGHTRAG_API_KEY]:-}" kv="${ENV_VALUES[LIGHTRAG_KV_STORAGE]:-}" vector="${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-}" graph="${ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]:-}" doc_status="${ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]:-}" if [[ -n "${ENV_VALUES[WHITELIST_PATHS]+set}" ]]; then whitelist_paths="${ENV_VALUES[WHITELIST_PATHS]}" whitelist_is_set="yes" fi for storage in "$kv" "$vector" "$graph" "$doc_status"; do if [[ -z "$storage" ]]; then continue fi if [[ ! -v "STORAGE_DB_TYPES[$storage]" ]]; then continue fi db_type="${STORAGE_DB_TYPES[$storage]}" if [[ -n "$db_type" ]]; then referenced_db_types["$db_type"]=1 fi done if [[ -n "${referenced_db_types[opensearch]+set}" || "${ENV_VALUES[LIGHTRAG_SETUP_OPENSEARCH_DEPLOYMENT]:-}" == "docker" ]]; then opensearch_in_use="yes" fi for key in "${!ENV_VALUES[@]}"; do if ! is_sensitive_env_key "$key"; then continue fi value="${ENV_VALUES[$key]:-}" if [[ -n "$value" ]] && contains_env_interpolation_syntax "$value"; then invalid_sensitive_keys+=("$key") fi done if ((${#invalid_sensitive_keys[@]} > 0)); then report_security_issue \ "Sensitive values still contain \${...} interpolation syntax: ${invalid_sensitive_keys[*]}" \ "Replace them with literal values or inject those secrets at runtime." findings=$((findings + 1)) fi if [[ -z "$auth_accounts" && -z "$api_key" ]]; then report_security_issue \ "No API protection is configured." \ "Set AUTH_ACCOUNTS and TOKEN_SECRET, add LIGHTRAG_API_KEY, or put the service behind a trusted reverse proxy." findings=$((findings + 1)) fi if [[ -n "$auth_accounts" ]]; then if ! validate_auth_accounts_format "$auth_accounts"; then report_security_issue \ "AUTH_ACCOUNTS is malformed." \ "Use comma-separated user:password pairs such as admin:{bcrypt} or admin:secret,reader:another-secret." findings=$((findings + 1)) elif ! validate_auth_accounts_password_safety "$auth_accounts"; then report_security_issue \ "AUTH_ACCOUNTS uses a predictable password prefix." \ "Passwords must not start with 'admin' or 'pass'. Choose a stronger password or use lightrag-hash-password." findings=$((findings + 1)) fi if [[ -z "$token_secret" ]]; then report_security_issue \ "AUTH_ACCOUNTS is set but TOKEN_SECRET is missing." \ "Set a non-empty JWT signing secret before enabling account-based authentication." findings=$((findings + 1)) elif [[ "$token_secret" == "lightrag-jwt-default-secret" ]]; then report_security_issue \ "TOKEN_SECRET still uses the built-in default value." \ "Generate a unique JWT signing secret and update TOKEN_SECRET." findings=$((findings + 1)) fi effective_whitelist="$whitelist_paths" if [[ "$whitelist_is_set" != "yes" ]]; then effective_whitelist="/health,/api/*" fi if whitelist_exposes_api_routes "$effective_whitelist"; then report_security_issue \ "WHITELIST_PATHS exposes /api routes while AUTH_ACCOUNTS is enabled." \ "Use a minimal whitelist such as /health,/docs and keep /api routes authenticated." findings=$((findings + 1)) fi fi if [[ -z "$auth_accounts" && -n "$api_key" ]]; then effective_whitelist="$whitelist_paths" if [[ "$whitelist_is_set" != "yes" ]]; then effective_whitelist="/health,/api/*" fi if whitelist_exposes_api_routes "$effective_whitelist"; then report_security_issue \ "WHITELIST_PATHS exposes /api routes while LIGHTRAG_API_KEY is the only active auth mechanism." \ "Use a minimal whitelist such as /health,/docs and keep /api routes protected by the API key." findings=$((findings + 1)) fi fi if [[ "$opensearch_in_use" == "yes" ]] && [[ -n "${ENV_VALUES[OPENSEARCH_PASSWORD]:-}" ]]; then local os_pass="${ENV_VALUES[OPENSEARCH_PASSWORD]}" if [[ "$os_pass" == "admin" || "$os_pass" == "LightRAG2026_!@" ]]; then report_security_issue \ "OPENSEARCH_PASSWORD uses a well-known default value." \ "Set a unique, strong password for the OpenSearch admin account." findings=$((findings + 1)) fi fi if ((findings == 0)); then log_success "No obvious security issues found in ${env_file}." return 0 fi log_warn "Security check found ${findings} issue(s) in ${env_file}." return 1 } backup_only() { local backup_path local compose_backup_path backup_path="$(backup_env_file)" if [[ -z "$backup_path" ]]; then format_error "No .env file found to back up." "Create one with make env-base first." return 1 fi echo "Backed up .env to $backup_path" compose_backup_path="$(backup_compose_file)" || return 1 if [[ -n "$compose_backup_path" ]]; then echo "Backed up compose file to $compose_backup_path" fi } print_help() { cat <<'HELP' Usage: scripts/setup/setup.sh [--base|--storage|--server|--validate|--security-check|--backup] [--rewrite-compose] Options: --base Configure LLM, embedding, and reranker (run first) --storage Configure storage backends and databases (requires .env) --server Configure server, security, and SSL (requires .env) --validate Validate an existing .env file --security-check Audit an existing .env for security risks --backup Backup the current .env and generated compose file when present --rewrite-compose Force regeneration of all wizard-managed compose services --debug Enable debug logging --help Show this help message HELP } _sigint_handler() { echo "" echo "Setup interrupted." exit 130 } main() { trap '_sigint_handler' INT init_colors local mode="help" while [[ $# -gt 0 ]]; do case "$1" in --base) mode="base" ;; --storage) mode="storage" ;; --server) mode="server" ;; --validate) mode="validate" ;; --security-check) mode="security-check" ;; --backup) mode="backup" ;; --debug) DEBUG="true" ;; --rewrite-compose) FORCE_REWRITE_COMPOSE="yes" ;; --help|-h) mode="help" ;; *) echo "Unknown option: $1" >&2 print_help return 1 ;; esac shift done case "$mode" in base) env_base_flow ;; storage) env_storage_flow ;; server) env_server_flow ;; validate) validate_env_file ;; security-check) security_check_env_file ;; backup) backup_only ;; *) print_help ;; esac } if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then main "$@" fi