#!/usr/bin/env bash # ============================================================================= # AzerothCore Module Manager Functions # ============================================================================= # This file contains all functions related to module management in AzerothCore. # It provides capabilities for installing, updating, removing, and searching # modules with support for advanced syntax and intelligent cross-format matching. # # Main Features: # - Advanced syntax: repo[:dirname][@branch[:commit]] # - Legacy compatibility: repo:branch:commit # - Cross-format module recognition (URLs, SSH, simple names) # - Custom directory naming to prevent conflicts # - Intelligent duplicate prevention # - Interactive menu system for easy management # # Usage: # source "path/to/modules.sh" # inst_module_install "mod-transmog:my-custom-dir@develop:abc123" # inst_module # Interactive menu # inst_module search "transmog" # Direct command # # ============================================================================= CURRENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" || exit ; pwd ) source "$CURRENT_PATH/../../../bash_shared/includes.sh" source "$CURRENT_PATH/../includes.sh" source "$AC_PATH_APPS/bash_shared/menu_system.sh" # ----------------------------------------------------------------------------- # Color support (disabled when not a TTY or NO_COLOR is set) # ----------------------------------------------------------------------------- if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then if command -v tput >/dev/null 2>&1; then _ac_cols=$(tput colors 2>/dev/null || echo 0) else _ac_cols=0 fi else _ac_cols=0 fi if [ "${FORCE_COLOR:-}" != "" ] || [ "${_ac_cols}" -ge 8 ]; then C_RESET='\033[0m' C_BOLD='\033[1m' C_DIM='\033[2m' C_RED='\033[31m' C_GREEN='\033[32m' C_YELLOW='\033[33m' C_BLUE='\033[34m' C_MAGENTA='\033[35m' C_CYAN='\033[36m' else C_RESET='' C_BOLD='' C_DIM='' C_RED='' C_GREEN='' C_YELLOW='' C_BLUE='' C_MAGENTA='' C_CYAN='' fi # Simple helpers for consistent colored output function print_info() { printf "%b\n" "${C_CYAN}$*${C_RESET}"; } function print_warn() { printf "%b\n" "${C_YELLOW}$*${C_RESET}"; } function print_error() { printf "%b\n" "${C_RED}$*${C_RESET}"; } function print_success() { printf "%b\n" "${C_GREEN}$*${C_RESET}"; } function print_skip() { printf "%b\n" "${C_BLUE}$*${C_RESET}"; } function print_header() { printf "%b\n" "${C_BOLD}${C_CYAN}$*${C_RESET}"; } # Module management menu definition # Format: "key|short|description" module_menu_items=( "search|s|Search for available modules" "install|i|Install one or more modules" "update|u|Update installed modules" "remove|r|Remove installed modules" "list|l|List installed modules" "help|h|Show detailed help" "quit|q|Close this menu" ) # Menu command handler for module operations function handle_module_command() { local key="$1" shift case "$key" in "search") inst_module_search "$@" ;; "install") inst_module_install "$@" ;; "update") inst_module_update "$@" ;; "remove") inst_module_remove "$@" ;; "list") inst_module_list "$@" ;; "help") inst_module_help ;; "quit") print_info "Exiting module manager..." return 0 ;; *) print_error "Invalid option. Use 'help' to see available commands." return 1 ;; esac } # Show detailed module help function inst_module_help() { print_header "AzerothCore Module Manager Help" echo "===============================" echo "" echo "Usage:" echo " ./acore.sh module # Interactive menu" echo " ./acore.sh module search [terms...]" echo " ./acore.sh module install [--all | modules...]" echo " ./acore.sh module update [--all | modules...]" echo " ./acore.sh module remove [modules...]" echo " ./acore.sh module list # List installed modules" echo "" echo "Module Specification Syntax:" echo " name # Simple name (e.g., mod-transmog)" echo " owner/name # GitHub repository" echo " name:branch # Specific branch" echo " name:branch:commit # Specific commit" echo " name:dirname@branch # Custom directory name" echo " https://github.com/... # Full URL" echo "" echo "Examples:" echo " ./acore.sh module install mod-transmog" echo " ./acore.sh module install azerothcore/mod-transmog:develop" echo " ./acore.sh module update --all" echo " ./acore.sh module remove mod-transmog" echo "" } # List installed modules function inst_module_list() { print_header "Installed Modules" echo "==================" local count=0 while read -r repo_ref branch commit; do [[ -z "$repo_ref" ]] && continue count=$((count + 1)) printf " %s. %b (%s)%b\n" "$count" "${C_GREEN}${repo_ref}" "${branch}" "${C_RESET}" if [[ "$commit" != "-" ]]; then printf " %bCommit:%b %s\n" "${C_DIM}" "${C_RESET}" "$commit" fi done < <(inst_mod_list_read) if [[ $count -eq 0 ]]; then print_warn " No modules installed." fi echo "" } # Dispatcher for the unified `module` command. # Usage: ./acore.sh module [args...] # ./acore.sh module # Interactive menu function inst_module() { # If no arguments provided, start interactive menu if [[ $# -eq 0 ]]; then menu_run_with_items "MODULE MANAGER" handle_module_command -- "${module_menu_items[@]}" -- return $? fi # Normalize arguments into an array local tokens=() read -r -a tokens <<< "$*" local cmd="${tokens[0]}" local args=("${tokens[@]:1}") case "$cmd" in ""|"help"|"-h"|"--help") inst_module_help ;; "search"|"s") inst_module_search "${args[@]}" ;; "install"|"i") inst_module_install "${args[@]}" ;; "update"|"u") inst_module_update "${args[@]}" ;; "remove"|"r") inst_module_remove "${args[@]}" ;; "list"|"l") inst_module_list "${args[@]}" ;; *) print_error "Unknown module command: $cmd. Use 'help' to see available commands." return 1 ;; esac } # ============================================================================= # Module Specification Parsing # ============================================================================= # Parse a module spec with advanced syntax: # - New syntax: repo[:dirname][@branch[:commit]] # # Examples: # "mod-transmog" -> uses default branch, directory name = mod-transmog # "mod-transmog:custom-dir" -> uses default branch, directory name = custom-dir # "mod-transmog@develop" -> uses develop branch, directory name = mod-transmog # "mod-transmog:custom-dir@develop:abc123" -> custom directory, develop branch, specific commit # # Output: "repo_ref owner name branch commit url dirname" function inst_parse_module_spec() { local spec="$1" local dirname="" branch="" commit="" repo_part="" # Parse the new syntax: repo[:dirname][@branch[:commit]] # First, check if this is a URL (contains :// or starts with git@) local is_url=0 if [[ "$spec" =~ :// ]] || [[ "$spec" =~ ^git@ ]]; then is_url=1 fi # Parse directory and branch differently for URLs vs simple names local repo_with_branch="$spec" if [[ $is_url -eq 1 ]]; then # For URLs, look for :dirname pattern, but be careful about ports # Strategy: only match :dirname if it's clearly after the repository path # Look for :dirname patterns at the end, but not if it looks like a port if [[ "$spec" =~ ^(.*\.git):([^@/:]+)(@.*)?$ ]]; then # Repo ending with .git:dirname repo_with_branch="${BASH_REMATCH[1]}${BASH_REMATCH[3]}" dirname="${BASH_REMATCH[2]}" elif [[ "$spec" =~ ^(.*://[^/]+/[^:]*[^0-9]):([^@/:]+)(@.*)?$ ]]; then # URL with path ending in non-digit:dirname (avoid matching ports) repo_with_branch="${BASH_REMATCH[1]}${BASH_REMATCH[3]}" dirname="${BASH_REMATCH[2]}" fi # If no custom dirname found, repo_with_branch remains the original spec else # For simple names, use the original logic if [[ "$spec" =~ ^([^@:]+):([^@:]+)(@.*)?$ ]]; then repo_with_branch="${BASH_REMATCH[1]}${BASH_REMATCH[3]}" dirname="${BASH_REMATCH[2]}" fi fi # Now parse branch and commit from the repo part # Be careful not to confuse URL @ with branch @ if [[ "$repo_with_branch" =~ :// ]]; then # For URLs, look for @ after the authority part if [[ "$repo_with_branch" =~ ^[^/]*//[^/]+/.*@([^:]+)(:(.+))?$ ]]; then # @ found in path part - treat as branch repo_part="${repo_with_branch%@*}" branch="${BASH_REMATCH[1]}" commit="${BASH_REMATCH[3]:-}" elif [[ "$repo_with_branch" =~ ^([^@]*@[^/]+/.*)@([^:]+)(:(.+))?$ ]]; then # @ found after URL authority @ - treat as branch repo_part="${BASH_REMATCH[1]}" branch="${BASH_REMATCH[2]}" commit="${BASH_REMATCH[4]:-}" else repo_part="$repo_with_branch" fi elif [[ "$repo_with_branch" =~ ^git@ ]]; then # Git SSH format - look for @ after the initial git@host: part if [[ "$repo_with_branch" =~ ^git@[^:]+:.*@([^:]+)(:(.+))?$ ]]; then repo_part="${repo_with_branch%@*}" branch="${BASH_REMATCH[1]}" commit="${BASH_REMATCH[3]:-}" else repo_part="$repo_with_branch" fi else # Non-URL format - use original logic if [[ "$repo_with_branch" =~ ^([^@]+)@([^:]+)(:(.+))?$ ]]; then repo_part="${BASH_REMATCH[1]}" branch="${BASH_REMATCH[2]}" commit="${BASH_REMATCH[4]:-}" else repo_part="$repo_with_branch" fi fi # Normalize repo reference and extract owner/name. local repo_ref owner name url owner_repo repo_ref="$repo_part" # If repo_ref is a URL, extract owner/name from path when possible if [[ "$repo_ref" =~ :// ]] || [[ "$repo_ref" =~ ^git@ ]]; then # Handle various URL formats local path_part="" if [[ "$repo_ref" =~ ^https?://[^/]+:?[0-9]*/(.+)$ ]]; then # HTTPS URL (with or without port) path_part="${BASH_REMATCH[1]}" elif [[ "$repo_ref" =~ ^ssh://.*@[^/]+:?[0-9]*/(.+)$ ]]; then # SSH URL with user@host:port/path format path_part="${BASH_REMATCH[1]}" elif [[ "$repo_ref" =~ ^ssh://[^@/]+:?[0-9]*/(.+)$ ]]; then # SSH URL with host:port/path format (no user@) path_part="${BASH_REMATCH[1]}" elif [[ "$repo_ref" =~ ^git@[^:]+:(.+)$ ]]; then # Git SSH format (git@host:path) path_part="${BASH_REMATCH[1]}" fi # Extract owner/name from path if [[ -n "$path_part" ]]; then # Remove .git suffix and any :dirname suffix path_part="${path_part%.git}" path_part="${path_part%:*}" if [[ "$path_part" == *"/"* ]]; then owner="$(echo "$path_part" | awk -F'/' '{print $(NF-1)}')" name="$(echo "$path_part" | awk -F'/' '{print $NF}')" else owner="unknown" name="$path_part" fi else owner="unknown" name="unknown" fi else owner_repo="$repo_ref" if [[ "$owner_repo" == *"/"* ]]; then owner="$(echo "$owner_repo" | cut -d'/' -f1)" name="$(echo "$owner_repo" | cut -d'/' -f2)" else owner="azerothcore" name="$owner_repo" repo_ref="$owner/$name" fi fi # Build URL only if repo_ref is not already a URL if [[ "$repo_ref" =~ :// ]] || [[ "$repo_ref" =~ ^git@ ]]; then url="$repo_ref" else url="https://github.com/${repo_ref}" fi # Use custom dirname if provided, otherwise default to module name if [ -z "$dirname" ]; then dirname="$name" fi echo "$repo_ref" "$owner" "$name" "${branch:--}" "${commit:--}" "$url" "$dirname" } # ============================================================================= # Cross-Format Module Recognition # ============================================================================= # Extract owner/name from any repository reference for intelligent matching. # This enables recognizing the same module regardless of specification format. # # Supported formats: # - GitHub HTTPS: https://github.com/owner/name.git # - GitHub SSH: git@github.com:owner/name.git # - GitLab HTTPS: https://gitlab.com/owner/name.git # - Owner/name: owner/name # - Simple name: mod-name (assumes azerothcore namespace) # # Returns: "owner/name" format for consistent comparison function inst_extract_owner_name { local repo_ref="$1" # For URLs, don't remove dirname suffix since : is part of the URL local base_ref="$repo_ref" if [[ ! "$repo_ref" =~ :// ]] && [[ ! "$repo_ref" =~ ^git@ ]]; then # Only remove dirname suffix for non-URL formats base_ref="${repo_ref%%:*}" fi # Handle various URL formats with possible ports if [[ "$base_ref" =~ ^https?://[^/]+:?[0-9]*/([^/]+)/([^/?]+) ]]; then # HTTPS URL format (with or without port) - matches github.com, gitlab.com, custom hosts local owner="${BASH_REMATCH[1]}" local name="${BASH_REMATCH[2]}" name="${name%:*}" # Remove any :dirname suffix first name="${name%.git}" # Then remove .git suffix if present echo "$owner/$name" elif [[ "$base_ref" =~ ^ssh://[^/]+:?[0-9]*/([^/]+)/([^/?]+) ]]; then # SSH URL format (with or without port) local owner="${BASH_REMATCH[1]}" local name="${BASH_REMATCH[2]}" name="${name%:*}" # Remove any :dirname suffix first name="${name%.git}" # Then remove .git suffix if present echo "$owner/$name" elif [[ "$base_ref" =~ ^git@[^:]+:([^/]+)/([^/?]+) ]]; then # Git SSH format (git@host:owner/repo) local owner="${BASH_REMATCH[1]}" local name="${BASH_REMATCH[2]}" name="${name%:*}" # Remove any :dirname suffix first name="${name%.git}" # Then remove .git suffix if present echo "$owner/$name" elif [[ "$base_ref" =~ ^[^/]+/[^/]+$ ]]; then # Format: owner/name (check after URL patterns) echo "$base_ref" elif [[ "$base_ref" =~ ^(mod-|module-)?([a-zA-Z0-9-]+)$ ]]; then # Simple module name, assume azerothcore namespace local modname="${BASH_REMATCH[2]}" if [[ "$base_ref" == mod-* ]]; then modname="$base_ref" else modname="mod-$modname" fi echo "azerothcore/$modname" else # Unknown format, return as-is echo "$base_ref" fi } # ============================================================================= # Module List Management # ============================================================================= # Returns path to modules list file (configurable via MODULES_LIST_FILE). function inst_modules_list_path() { local path="${MODULES_LIST_FILE:-"$AC_PATH_ROOT/conf/modules.list"}" echo "$path" } # Ensure the modules list file exists and its directory is created. function inst_mod_list_ensure() { local file file="$(inst_modules_list_path)" mkdir -p "$(dirname "$file")" [ -f "$file" ] || touch "$file" } # Read modules list into stdout as triplets: "name branch commit" # Skips comments (# ...) and blank lines. function inst_mod_list_read() { local file file="$(inst_modules_list_path)" [ -f "$file" ] || return 0 # shellcheck disable=SC2013 while IFS= read -r line; do [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue echo "$line" done < "$file" } # Check whether a module spec matches the exclusion list. # - Reads space/newline separated items from env var MODULES_EXCLUDE_LIST # - Supports cross-format matching via inst_extract_owner_name # Returns 0 if excluded, 1 otherwise. function inst_mod_is_excluded() { local spec="$1" local target_owner_name target_owner_name=$(inst_extract_owner_name "$spec") # No exclusions configured if [[ -z "${MODULES_EXCLUDE_LIST:-}" ]]; then return 1 fi # Split on default IFS (space, tab, newline) local items=() # Use mapfile to split MODULES_EXCLUDE_LIST on newlines; fallback to space if no newlines if [[ "${MODULES_EXCLUDE_LIST}" == *$'\n'* ]]; then mapfile -t items <<< "${MODULES_EXCLUDE_LIST}" else read -r -a items <<< "${MODULES_EXCLUDE_LIST}" fi local it it_owner for it in "${items[@]}"; do [[ -z "$it" ]] && continue it_owner=$(inst_extract_owner_name "$it") if [[ "$it_owner" == "$target_owner_name" ]]; then return 0 fi done return 1 } # Add or update an entry in the list: repo_ref branch commit # Removes any existing entries with the same owner/name to avoid duplicates function inst_mod_list_upsert() { local repo_ref="$1"; shift local branch="$1"; shift local commit="$1"; shift local target_owner_name target_owner_name=$(inst_extract_owner_name "$repo_ref") inst_mod_list_ensure local file tmp tmp_uns tmp_sorted file="$(inst_modules_list_path)" tmp="${file}.tmp" tmp_uns="${file}.unsorted" tmp_sorted="${file}.sorted" # Build a list without existing duplicates : > "$tmp_uns" while read -r existing_ref existing_branch existing_commit; do [[ -z "$existing_ref" ]] && continue local existing_owner_name existing_owner_name=$(inst_extract_owner_name "$existing_ref") if [[ "$existing_owner_name" != "$target_owner_name" ]]; then echo "$existing_ref $existing_branch $existing_commit" >> "$tmp_uns" fi done < <(inst_mod_list_read) # Add/replace the new entry (preserving original repo_ref format) echo "$repo_ref $branch $commit" >> "$tmp_uns" # Create key-prefixed lines to sort by normalized owner/name : > "$tmp" while read -r r b c; do [[ -z "$r" ]] && continue local k k=$(inst_extract_owner_name "$r") printf "%s\t%s %s %s\n" "$k" "$r" "$b" "$c" >> "$tmp" done < "$tmp_uns" # Stable sort by key and strip the key LC_ALL=C sort -t $'\t' -k1,1 -s "$tmp" | cut -f2- > "$tmp_sorted" && mv "$tmp_sorted" "$file" rm -f "$tmp" "$tmp_uns" "$tmp_sorted" 2>/dev/null || true } # Remove an entry from the list by matching owner/name. # This allows removing modules regardless of how they were specified (URL vs owner/name) function inst_mod_list_remove() { local repo_ref="$1" local target_owner_name target_owner_name=$(inst_extract_owner_name "$repo_ref") local file file="$(inst_modules_list_path)" [ -f "$file" ] || return 0 local tmp_uns="${file}.unsorted" local tmp="${file}.tmp" local tmp_sorted="${file}.sorted" # Keep only lines where owner/name doesn't match : > "$tmp_uns" while read -r existing_ref existing_branch existing_commit; do [[ -z "$existing_ref" ]] && continue local existing_owner_name existing_owner_name=$(inst_extract_owner_name "$existing_ref") if [[ "$existing_owner_name" != "$target_owner_name" ]]; then echo "$existing_ref $existing_branch $existing_commit" >> "$tmp_uns" fi done < <(inst_mod_list_read) # Key-prefix and sort for deterministic alphabetical order : > "$tmp" while read -r r b c; do [[ -z "$r" ]] && continue local k k=$(inst_extract_owner_name "$r") printf "%s\t%s %s %s\n" "$k" "$r" "$b" "$c" >> "$tmp" done < "$tmp_uns" LC_ALL=C sort -t $'\t' -k1,1 -s "$tmp" | cut -f2- > "$tmp_sorted" && mv "$tmp_sorted" "$file" rm -f "$tmp" "$tmp_uns" "$tmp_sorted" 2>/dev/null || true } # Check if a module is already installed by comparing owner/name # Returns the existing repo_ref if found, empty if not found function inst_mod_is_installed() { local spec="$1" local target_owner_name target_owner_name=$(inst_extract_owner_name "$spec") # Use a different approach: read into a variable first, then process local modules_content modules_content=$(inst_mod_list_read) # Process each line while IFS= read -r line; do [[ -z "$line" ]] && continue read -r repo_ref branch commit <<< "$line" local existing_owner_name existing_owner_name=$(inst_extract_owner_name "$repo_ref") if [[ "$existing_owner_name" == "$target_owner_name" ]]; then echo "$repo_ref" # Return the existing entry return 0 fi done <<< "$modules_content" return 1 } # ============================================================================= # Conflict Detection and Validation # ============================================================================= # Check for module directory conflicts with helpful error messages function inst_check_module_conflict { local dirname="$1" local repo_ref="$2" if [ -d "$J_PATH_MODULES/$dirname" ]; then print_error "Error: Directory '$dirname' already exists." print_warn "Possible solutions:" echo " 1. Use a different directory name: $repo_ref:my-custom-name" echo " 2. Remove the existing directory first" echo " 3. Use the update command if this is the same module" return 1 fi return 0 } # ============================================================================= # Module Operations # ============================================================================= # Get version and branch information from acore-module.json function inst_getVersionBranch() { local res="master" local v="not-defined" local MODULE_MAJOR=0 local MODULE_MINOR=0 local MODULE_PATCH=0 local MODULE_SPECIAL=0; local ACV_MAJOR=0 local ACV_MINOR=0 local ACV_PATCH=0 local ACV_SPECIAL=0; local curldata=$(curl -f --silent -H 'Cache-Control: no-cache' "$1" || echo "{}") local parsed=$(echo "$curldata" | "$AC_PATH_DEPS/jsonpath/JSONPath.sh" -b '$.compatibility.*.[version,branch]') semverParseInto "$ACORE_VERSION" ACV_MAJOR ACV_MINOR ACV_PATCH ACV_SPECIAL if [[ ! -z "$parsed" ]]; then readarray -t vers < <(echo "$parsed") local idx res="none" # since we've the pair version,branch alternated in not associative and one-dimensional # array, we've to simulate the association with length/2 trick for idx in `seq 0 $((${#vers[*]}/2-1))`; do semverParseInto "${vers[idx*2]}" MODULE_MAJOR MODULE_MINOR MODULE_PATCH MODULE_SPECIAL if [[ $MODULE_MAJOR -eq $ACV_MAJOR && $MODULE_MINOR -le $ACV_MINOR ]]; then res="${vers[idx*2+1]}" v="${vers[idx*2]}" fi done fi echo "$v" "$res" } # Search for modules in the AzerothCore repository function inst_module_search { # Accept 0..N search terms; if none provided, prompt the user. local terms=("$@") if [ ${#terms[@]} -eq 0 ]; then echo "Type what to search (blank for full list)" read -p "Insert name(s): " _line if [ -n "$_line" ]; then read -r -a terms <<< "$_line" fi fi local CATALOG_URL="https://www.azerothcore.org/data/catalogue.json" print_header "Searching ${terms[*]}..." echo "" # Build candidate list from catalogue (full_name = owner/repo) local MODS=() if command -v jq >/dev/null 2>&1; then mapfile -t MODS < <(curl --silent -L "$CATALOG_URL" \ | jq -r ' [ .. | objects | select(.full_name and .topics) | select(.topics | index("azerothcore-module")) ] | unique_by(.full_name) | sort_by(.stargazers_count // 0) | reverse | .[].full_name ') else # Fallback without jq: best-effort extraction of owner/repo mapfile -t MODS < <(curl --silent -L "$CATALOG_URL" \ | grep -oE '\"full_name\"\\s*:\\s*\"[^\"/[:space:]]+/[^\"[:space:]]+\"' \ | sed -E 's/.*\"full_name\"\\s*:\\s*\"([^\"]+)\".*/\\1/' \ | sort -u) fi # Local AND filter on user terms (case-insensitive) against full_name if (( ${#terms[@]} > 0 )); then local filtered=() local item for item in "${MODS[@]}"; do local keep=1 local lower="${item,,}" local t for t in "${terms[@]}"; do [ -z "$t" ] && continue if [[ "$lower" != *"${t,,}"* ]]; then keep=0; break fi done (( keep )) && filtered+=("$item") done MODS=("${filtered[@]}") fi if (( ${#MODS[@]} == 0 )); then echo "No results." echo "" return 0 fi local idx=0 while (( ${#MODS[@]} > idx )); do local mod_full="${MODS[idx++]}" # owner/repo local mod="${mod_full##*/}" # repo name only for display read v b < <(inst_getVersionBranch "https://raw.githubusercontent.com/${mod_full}/master/acore-module.json") if [[ "$b" != "none" ]]; then printf "%b -> %b (tested with AC version: %s)%b\n" "" "${C_GREEN}${mod}${C_RESET}" "$v" "" else printf "%b -> %b %b(NOTE: The module latest tested AC revision is Unknown)%b\n" "" "${C_GREEN}${mod}${C_RESET}" "${C_YELLOW}" "${C_RESET}" fi done echo "" echo "" } # Install one or more modules with advanced syntax support function inst_module_install { # Support multiple modules and the --all flag; prompt if none specified. local args=("$@") local use_all=false if [ ${#args[@]} -gt 0 ] && { [ "${args[0]}" = "--all" ] || [ "${args[0]}" = "-a" ]; }; then use_all=true shift || true fi local modules=("$@") print_header "Installing modules: ${modules[*]}" if $use_all; then # Install all modules from the list (respecting recorded branch and commit). inst_mod_list_ensure local line repo_ref branch commit url owner modname dirname # First pass: detect duplicate target directories (flat structure) declare -A _seen _first local dup_error=0 while read -r repo_ref branch commit; do [ -z "$repo_ref" ] && continue # Skip excluded modules when checking duplicates if inst_mod_is_excluded "$repo_ref"; then continue fi parsed_output=$(inst_parse_module_spec "$repo_ref") IFS=' ' read -r _ owner modname _ _ url dirname <<< "$parsed_output" # dirname defaults to repo name; flat install path uses dirname only if [[ -n "${_seen[$dirname]:-}" ]]; then print_error "Error: duplicate module target directory '$dirname' detected in modules.list:" echo " - ${_first[$dirname]}" echo " - ${repo_ref}" print_warn "Use a custom folder name to disambiguate, e.g.: ${repo_ref}:$dirname-alt" dup_error=1 else _seen[$dirname]=1 _first[$dirname]="$repo_ref" fi done < <(inst_mod_list_read) if [[ "$dup_error" -ne 0 ]]; then return 1 fi # Second pass: install in flat modules directory (no owner subfolders) while read -r repo_ref branch commit; do [ -z "$repo_ref" ] && continue # Skip excluded entries during installation if inst_mod_is_excluded "$repo_ref"; then print_warn "[$repo_ref] Excluded by MODULES_EXCLUDE_LIST (skipping)." continue fi parsed_output=$(inst_parse_module_spec "$repo_ref") IFS=' ' read -r _ owner modname _ _ url dirname <<< "$parsed_output" if [ -d "$J_PATH_MODULES/$dirname" ]; then print_skip "[$repo_ref] Already installed (skipping)." continue fi if Joiner:add_repo "$url" "$dirname" "$branch" ""; then # Checkout the recorded commit if present if [ -n "$commit" ]; then git -C "$J_PATH_MODULES/$dirname" fetch --all --quiet || true if git -C "$J_PATH_MODULES/$dirname" rev-parse --verify "$commit" >/dev/null 2>&1; then git -C "$J_PATH_MODULES/$dirname" checkout --quiet "$commit" fi fi local curCommit curCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "") inst_mod_list_upsert "$repo_ref" "$branch" "$curCommit" print_success "[$repo_ref] Installed." else print_error "[$repo_ref] Install failed." exit 1; fi done < <(inst_mod_list_read) else # Install specified modules; prompt if none specified. if [ ${#modules[@]} -eq 0 ]; then echo "Type the name(s) of the module(s) to install" read -p "Insert name(s): " _line read -r -a modules <<< "$_line" fi local spec name override_branch override_commit v b def curCommit existing_repo_ref dirname for spec in "${modules[@]}"; do [ -z "$spec" ] && continue # Check if module is already installed (by owner/name matching) existing_repo_ref=$(inst_mod_is_installed "$spec" || true) if [ -n "$existing_repo_ref" ]; then print_skip "[$spec] Already installed as [$existing_repo_ref] (skipping)." continue fi parsed_output=$(inst_parse_module_spec "$spec") IFS=' ' read -r repo_ref owner modname override_branch override_commit url dirname <<< "$parsed_output" [ -z "$repo_ref" ] && continue # Check for directory conflicts with custom directory names if ! inst_check_module_conflict "$dirname" "$repo_ref"; then continue fi # override_branch takes precedence; otherwise consult acore-module.json on azerothcore unless repo_ref contains owner or URL if [ -n "$override_branch" ] && [ "$override_branch" != "-" ]; then b="$override_branch" else # For GitHub repositories, use raw.githubusercontent.com to check acore-module.json if [[ "$url" =~ github.com ]] || [[ "$repo_ref" =~ ^[^/]+/[^/]+$ ]]; then read v b < <(inst_getVersionBranch "https://raw.githubusercontent.com/${owner}/${modname}/master/acore-module.json") else # Unknown host: try the repository URL as-is (may fail) read v b < <(inst_getVersionBranch "${url}/master/acore-module.json") fi if [[ "$v" == "none" || "$v" == "not-defined" || "$b" == "none" ]]; then def="$(inst_get_default_branch "$repo_ref")" print_warn "Warning: $repo_ref has no compatible acore-module.json; installing from branch '$def' (latest commit)." b="$def" fi fi # Use flat directory structure with custom directory name if [ -d "$J_PATH_MODULES/$dirname" ]; then print_skip "[$repo_ref] Already installed (skipping)." curCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "") inst_mod_list_upsert "$repo_ref" "$b" "$curCommit" continue fi if Joiner:add_repo "$url" "$dirname" "$b" ""; then # If a commit was provided, try to checkout it if [ -n "$override_commit" ] && [ "$override_commit" != "-" ]; then git -C "$J_PATH_MODULES/$dirname" fetch --all --quiet || true if git -C "$J_PATH_MODULES/$dirname" rev-parse --verify "$override_commit" >/dev/null 2>&1; then git -C "$J_PATH_MODULES/$dirname" checkout --quiet "$override_commit" else print_warn "[$repo_ref] provided commit '$override_commit' not found; staying on branch '$b' HEAD." fi fi curCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "") inst_mod_list_upsert "$repo_ref" "$b" "$curCommit" print_success "[$repo_ref] Installed in '$dirname'. Please re-run compiling and db assembly." else print_error "[$repo_ref] Install failed or module not found" exit 1; fi done fi echo "" echo "" } # Update one or more modules function inst_module_update { # Handle help request if [[ "$1" == "--help" || "$1" == "-h" ]]; then inst_module_help return 0 fi # Support multiple modules and the --all flag; prompt if none specified. local args=("$@") local use_all=false if [ ${#args[@]} -gt 0 ] && { [ "${args[0]}" = "--all" ] || [ "${args[0]}" = "-a" ]; }; then use_all=true shift || true fi local _tmp=$PWD if $use_all; then local line repo_ref branch commit newCommit owner modname url dirname while read -r repo_ref branch commit; do [ -z "$repo_ref" ] && continue # Skip excluded modules during update --all if inst_mod_is_excluded "$repo_ref"; then print_warn "[$repo_ref] Excluded by MODULES_EXCLUDE_LIST (skipping)." continue fi parsed_output=$(inst_parse_module_spec "$repo_ref") IFS=' ' read -r _ owner modname _ _ url dirname <<< "$parsed_output" dirname="${dirname:-$modname}" if [ ! -d "$J_PATH_MODULES/$dirname/" ]; then print_skip "[$repo_ref] Not installed locally, skipping." continue fi if Joiner:upd_repo "$url" "$dirname" "$branch" ""; then newCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "") inst_mod_list_upsert "$repo_ref" "$branch" "$newCommit" print_success "[$repo_ref] Updated to latest commit on '$branch'." else print_error "[$repo_ref] Cannot update" fi done < <(inst_mod_list_read) else local modules=("$@") if [ ${#modules[@]} -eq 0 ]; then echo "Type the name(s) of the module(s) to update" read -p "Insert name(s): " _line read -r -a modules <<< "$_line" fi local spec repo_ref override_branch override_commit owner modname url dirname v b branch def newCommit for spec in "${modules[@]}"; do [ -z "$spec" ] && continue parsed_output=$(inst_parse_module_spec "$spec") IFS=' ' read -r repo_ref owner modname override_branch override_commit url dirname <<< "$parsed_output" dirname="${dirname:-$modname}" if [ -d "$J_PATH_MODULES/$dirname/" ]; then # determine preferred branch if not provided if [ -n "$override_branch" ] && [ "$override_branch" != "-" ]; then b="$override_branch" else # try reading acore-module.json for this repo if [[ "$url" =~ github.com ]]; then read v b < <(inst_getVersionBranch "https://raw.githubusercontent.com/${owner}/${modname}/master/acore-module.json") else read v b < <(inst_getVersionBranch "${url}/master/acore-module.json") fi if [[ "$v" == "none" || "$v" == "not-defined" || "$b" == "none" ]]; then if branch=$(git -C "$J_PATH_MODULES/$dirname" rev-parse --abbrev-ref HEAD 2>/dev/null); then print_warn "Warning: $repo_ref has no compatible acore-module.json; updating current branch '$branch'." b="$branch" else def="$(inst_get_default_branch "$repo_ref")" print_warn "Warning: $repo_ref has no compatible acore-module.json and no git branch detected; updating default branch '$def'." b="$def" fi fi fi if Joiner:upd_repo "$url" "$dirname" "$b" ""; then newCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "") inst_mod_list_upsert "$repo_ref" "$b" "$newCommit" print_success "[$repo_ref] Done, please re-run compiling and db assembly" else print_error "[$repo_ref] Cannot update" fi else print_error "[$repo_ref] Cannot update! Path doesn't exist ($J_PATH_MODULES/$dirname/)" fi done fi echo "" echo "" } # Remove one or more modules function inst_module_remove { # Support multiple modules; prompt if none specified. local modules=("$@") if [ ${#modules[@]} -eq 0 ]; then echo "Type the name(s) of the module(s) to remove" read -p "Insert name(s): " _line read -r -a modules <<< "$_line" fi local spec repo_ref owner modname url override_branch override_commit dirname for spec in "${modules[@]}"; do [ -z "$spec" ] && continue parsed_output=$(inst_parse_module_spec "$spec") IFS=' ' read -r repo_ref owner modname override_branch override_commit url dirname <<< "$parsed_output" [ -z "$repo_ref" ] && continue dirname="${dirname:-$modname}" if Joiner:remove "$dirname" ""; then inst_mod_list_remove "$repo_ref" print_success "[$repo_ref] Done, please re-run compiling" else print_error "[$repo_ref] Cannot remove" fi done echo "" echo "" }