summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYehonal <yehonal.azeroth@gmail.com>2025-08-30 23:44:07 +0200
committerGitHub <noreply@github.com>2025-08-30 23:44:07 +0200
commit5a79a4edce0f39541d2e9b363dcbe0cc79c32a1e (patch)
treef0bb15f28881a000f17a8f6d1a74d72e93c6b84b
parent5c31e3b411ba8dfec9ecdfacca494accf7f59119 (diff)
Feat/refactoring-module-menu (#22733)
-rw-r--r--.github/workflows/dashboard-ci.yml9
-rwxr-xr-xapps/compiler/test/test_compiler.bats6
-rw-r--r--apps/installer/includes/functions.sh154
-rw-r--r--apps/installer/includes/modules-manager/README.md188
-rw-r--r--apps/installer/includes/modules-manager/modules.sh735
-rw-r--r--apps/installer/main.sh81
-rw-r--r--apps/installer/test/bats.conf14
-rwxr-xr-xapps/installer/test/test_module_commands.bats354
-rw-r--r--conf/dist/config.sh13
-rwxr-xr-xdeps/acore/joiner/joiner.sh36
10 files changed, 1399 insertions, 191 deletions
diff --git a/.github/workflows/dashboard-ci.yml b/.github/workflows/dashboard-ci.yml
index 25843a6906..fbdd881246 100644
--- a/.github/workflows/dashboard-ci.yml
+++ b/.github/workflows/dashboard-ci.yml
@@ -42,7 +42,12 @@ jobs:
- name: Install requirements
run: |
- sudo apt install -y bats
+ sudo apt-get update
+ # Install bats-core >= 1.5.0 to support bats_require_minimum_version
+ sudo apt-get install -y git curl
+ git clone --depth 1 https://github.com/bats-core/bats-core.git /tmp/bats-core
+ sudo /tmp/bats-core/install.sh /usr/local
+ bats --version
./acore.sh install-deps
- name: Run bash script tests for ${{ matrix.test-module }}
@@ -50,7 +55,7 @@ jobs:
TERM: xterm-256color
run: |
cd apps/test-framework
- ./run-tests.sh --tap
+ ./run-tests.sh --tap --all
build-and-test:
name: Build and Integration Test
diff --git a/apps/compiler/test/test_compiler.bats b/apps/compiler/test/test_compiler.bats
index ff217e6389..79152b68e5 100755
--- a/apps/compiler/test/test_compiler.bats
+++ b/apps/compiler/test/test_compiler.bats
@@ -1,7 +1,9 @@
#!/usr/bin/env bats
-# Require minimum BATS version to avoid warnings
-bats_require_minimum_version 1.5.0
+# Require minimum BATS version when supported (older distro packages lack this)
+if type -t bats_require_minimum_version >/dev/null 2>&1; then
+ bats_require_minimum_version 1.5.0
+fi
# AzerothCore Compiler Scripts Test Suite
# Tests the functionality of the compiler scripts using the unified test framework
diff --git a/apps/installer/includes/functions.sh b/apps/installer/includes/functions.sh
index b4bd14caf5..eb9fe1a24e 100644
--- a/apps/installer/includes/functions.sh
+++ b/apps/installer/includes/functions.sh
@@ -118,142 +118,28 @@ function inst_allInOne() {
inst_download_client_data
}
-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
+############################################################
+# Module helpers and dispatcher #
+############################################################
+
+# Returns the default branch name of a GitHub repo in the azerothcore org.
+# If the API call fails, defaults to "master".
+function inst_get_default_branch() {
+ local repo="$1"
+ local def
+ def=$(curl --silent "https://api.github.com/repos/azerothcore/${repo}" \
+ | "$AC_PATH_DEPS/jsonpath/JSONPath.sh" -b '$.default_branch')
+ if [ -z "$def" ]; then
+ def="master"
fi
-
- echo "$v" "$res"
-}
-
-function inst_module_search {
-
- local res="$1"
- local idx=0;
-
- if [ -z "$1" ]; then
- echo "Type what to search or leave blank for full list"
- read -p "Insert name: " res
- fi
-
- local search="+$res"
-
- echo "Searching $res..."
- echo "";
-
- readarray -t MODS < <(curl --silent "https://api.github.com/search/repositories?q=org%3Aazerothcore${search}+fork%3Atrue+topic%3Acore-module+sort%3Astars&type=" \
- | "$AC_PATH_DEPS/jsonpath/JSONPath.sh" -b '$.items.*.name')
- while (( ${#MODS[@]} > idx )); do
- mod="${MODS[idx++]}"
- read v b < <(inst_getVersionBranch "https://raw.githubusercontent.com/azerothcore/$mod/master/acore-module.json")
-
- if [[ "$b" != "none" ]]; then
- echo "-> $mod (tested with AC version: $v)"
- else
- echo "-> $mod (no revision available for AC v$AC_VERSION, it could not work!)"
- fi
- done
-
- echo "";
- echo "";
-}
-
-function inst_module_install {
- local res
- if [ -z "$1" ]; then
- echo "Type the name of the module to install"
- read -p "Insert name: " res
- else
- res="$1"
- fi
-
- read v b < <(inst_getVersionBranch "https://raw.githubusercontent.com/azerothcore/$res/master/acore-module.json")
-
- if [[ "$b" != "none" ]]; then
- Joiner:add_repo "https://github.com/azerothcore/$res" "$res" "$b" && echo "Done, please re-run compiling and db assembly. Read instruction on module repository for more information"
- else
- echo "Cannot install $res module: it doesn't exists or no version compatible with AC v$ACORE_VERSION are available"
- fi
-
- echo "";
- echo "";
-}
-
-function inst_module_update {
- local res;
- local _tmp;
- local branch;
- local p;
-
- if [ -z "$1" ]; then
- echo "Type the name of the module to update"
- read -p "Insert name: " res
- else
- res="$1"
- fi
-
- _tmp=$PWD
-
- if [ -d "$J_PATH_MODULES/$res/" ]; then
- read v b < <(inst_getVersionBranch "https://raw.githubusercontent.com/azerothcore/$res/master/acore-module.json")
-
- cd "$J_PATH_MODULES/$res/"
-
- # use current branch if something wrong with json
- if [[ "$v" == "none" || "$v" == "not-defined" ]]; then
- b=`git rev-parse --abbrev-ref HEAD`
- fi
-
- Joiner:upd_repo "https://github.com/azerothcore/$res" "$res" "$b" && echo "Done, please re-run compiling and db assembly" || echo "Cannot update"
- cd $_tmp
- else
- echo "Cannot update! Path doesn't exist"
- fi;
-
- echo "";
- echo "";
-}
-
-function inst_module_remove {
- if [ -z "$1" ]; then
- echo "Type the name of the module to remove"
- read -p "Insert name: " res
- else
- res="$1"
- fi
-
- Joiner:remove "$res" && echo "Done, please re-run compiling" || echo "Cannot remove"
-
- echo "";
- echo "";
+ echo "$def"
}
+# =============================================================================
+# Module Management System
+# =============================================================================
+# Load the module manager functions from the dedicated modules-manager directory
+source "$AC_PATH_INSTALLER/includes/modules-manager/modules.sh"
function inst_simple_restarter {
echo "Running $1 ..."
@@ -292,4 +178,4 @@ function inst_download_client_data {
&& echo "unzip downloaded file in $path..." && unzip -q -o "$zipPath" -d "$path/" \
&& echo "Remove downloaded file" && rm "$zipPath" \
&& echo "INSTALLED_VERSION=$VERSION" > "$dataVersionFile"
-}
+} \ No newline at end of file
diff --git a/apps/installer/includes/modules-manager/README.md b/apps/installer/includes/modules-manager/README.md
new file mode 100644
index 0000000000..f478035beb
--- /dev/null
+++ b/apps/installer/includes/modules-manager/README.md
@@ -0,0 +1,188 @@
+# AzerothCore Module Manager
+
+This directory contains the module management system for AzerothCore, providing advanced functionality for installing, updating, and managing server modules.
+
+## 🚀 Features
+
+- **Advanced Syntax**: Support for `repo[:dirname][@branch[:commit]]` format
+- **Cross-Format Recognition**: Intelligent matching across URLs, SSH, and simple names
+- **Custom Directory Naming**: Prevent conflicts with custom directory names
+- **Duplicate Prevention**: Smart detection and prevention of duplicate installations
+- **Multi-Host Support**: GitHub, GitLab, and other Git hosts
+
+## 📁 File Structure
+
+```
+modules-manager/
+├── modules.sh # Core module management functions
+└── README.md # This documentation file
+```
+
+## 🔧 Module Specification Syntax
+
+The module manager supports flexible syntax for specifying modules:
+
+### New Syntax Format
+```bash
+repo[:dirname][@branch[:commit]]
+```
+
+### Examples
+
+| Specification | Description |
+|---------------|-------------|
+| `mod-transmog` | Simple module name, uses default branch and directory |
+| `mod-transmog:my-custom-dir` | Custom directory name |
+| `mod-transmog@develop` | Specific branch |
+| `mod-transmog:custom@develop:abc123` | Custom directory, branch, and commit |
+| `https://github.com/owner/repo.git@main` | Full URL with branch |
+| `git@github.com:owner/repo.git:custom-dir` | SSH URL with custom directory |
+
+## 🎯 Usage Examples
+
+### Installing Modules
+
+```bash
+# Simple module installation
+./acore.sh module install mod-transmog
+
+# Install with custom directory name
+./acore.sh module install mod-transmog:my-transmog-dir
+
+# Install specific branch
+./acore.sh module install mod-transmog@develop
+
+# Install with full specification
+./acore.sh module install mod-transmog:custom-dir@develop:abc123
+
+# Install from URL
+./acore.sh module install https://github.com/azerothcore/mod-transmog.git@main
+
+# Install multiple modules
+./acore.sh module install mod-transmog mod-eluna:custom-eluna
+
+# Install all modules from list
+./acore.sh module install --all
+```
+
+### Updating Modules
+
+```bash
+# Update specific module
+./acore.sh module update mod-transmog
+
+# Update all modules
+./acore.sh module update --all
+
+# Update with branch specification
+./acore.sh module update mod-transmog@develop
+```
+
+### Removing Modules
+
+```bash
+# Remove by simple name (cross-format recognition)
+./acore.sh module remove mod-transmog
+
+# Remove by URL (recognizes same module)
+./acore.sh module remove https://github.com/azerothcore/mod-transmog.git
+
+# Remove multiple modules
+./acore.sh module remove mod-transmog mod-eluna
+```
+
+### Searching Modules
+
+```bash
+# Search for modules
+./acore.sh module search transmog
+
+# Search with multiple terms
+./acore.sh module search auction house
+
+# Show all available modules
+./acore.sh module search
+```
+
+## 🔍 Cross-Format Recognition
+
+The system intelligently recognizes the same module across different specification formats:
+
+```bash
+# These all refer to the same module:
+mod-transmog
+azerothcore/mod-transmog
+https://github.com/azerothcore/mod-transmog.git
+git@github.com:azerothcore/mod-transmog.git
+```
+
+This allows:
+- Installing with one format and removing with another
+- Preventing duplicates regardless of specification format
+- Consistent module tracking across different input methods
+
+## 🛡️ Conflict Prevention
+
+The system prevents common conflicts:
+
+### Directory Conflicts
+```bash
+# If 'mod-transmog' directory already exists:
+$ ./acore.sh module install mod-transmog:mod-transmog
+Error: Directory 'mod-transmog' already exists.
+Possible solutions:
+ 1. Use a different directory name: mod-transmog:my-custom-name
+ 2. Remove the existing directory first
+ 3. Use the update command if this is the same module
+```
+
+## 🔄 Integration
+
+### Including in Scripts
+```bash
+# Source the module functions
+source "$AC_PATH_INSTALLER/includes/modules-manager/modules.sh"
+
+# Use module functions
+inst_module_install "mod-transmog:custom-dir@develop"
+```
+
+### Testing
+The module system is tested through the main installer test suite:
+```bash
+./apps/installer/test/test_module_commands.bats
+```
+
+## 📋 Module List Format
+
+Modules are tracked in `conf/modules.list` with the format:
+```
+# Comments start with #
+repo_reference branch commit
+
+# Examples:
+azerothcore/mod-transmog master abc123def456
+https://github.com/custom/mod-custom.git develop def456abc789
+mod-eluna:custom-eluna-dir main 789abc123def
+```
+## 🔧 Configuration
+
+### Environment Variables
+- `MODULES_LIST_FILE`: Override default modules list path
+- `J_PATH_MODULES`: Modules installation directory
+- `AC_PATH_ROOT`: AzerothCore root path
+
+### Default Paths
+- Modules list: `$AC_PATH_ROOT/conf/modules.list`
+
+## 🤝 Contributing
+
+When modifying the module manager:
+
+1. Maintain backwards compatibility
+2. Update tests in `test_module_commands.bats`
+3. Update this documentation
+4. Test cross-format recognition thoroughly
+5. Ensure helpful error messages
+
+
diff --git a/apps/installer/includes/modules-manager/modules.sh b/apps/installer/includes/modules-manager/modules.sh
new file mode 100644
index 0000000000..93e6433067
--- /dev/null
+++ b/apps/installer/includes/modules-manager/modules.sh
@@ -0,0 +1,735 @@
+#!/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
+#
+# Usage:
+# source "path/to/modules.sh"
+# inst_module_install "mod-transmog:my-custom-dir@develop:abc123"
+#
+# =============================================================================
+
+# Dispatcher for the unified `module` command.
+# Usage: ./acore.sh module <search|install|update|remove> [args...]
+function inst_module() {
+ # 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")
+ echo "Usage:"
+ echo " ./acore.sh module search [terms...]"
+ echo " ./acore.sh module install [--all | modules...]"
+ echo " modules can be specified as: name[:branch[:commit]]"
+ echo " ./acore.sh module update [modules...]"
+ echo " ./acore.sh module remove [modules...]"
+ ;;
+ "search"|"s")
+ inst_module_search "${args[@]}"
+ ;;
+ "install"|"i")
+ inst_module_install "${args[@]}"
+ ;;
+ "update"|"u")
+ inst_module_update "${args[@]}"
+ ;;
+ "remove"|"r")
+ inst_module_remove "${args[@]}"
+ ;;
+ *)
+ echo "Unknown subcommand: $cmd"
+ echo "Try: ./acore.sh module help"
+ ;;
+ 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, extract custom directory name if present (format: repo:dirname@branch)
+ local repo_with_branch="$spec"
+ if [[ "$spec" =~ ^([^@:]+):([^@:]+)(@.*)?$ ]]; then
+ repo_with_branch="${BASH_REMATCH[1]}${BASH_REMATCH[3]}"
+ dirname="${BASH_REMATCH[2]}"
+ fi
+
+ # Now parse branch and commit from the repo part
+ 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
+
+ # 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
+ # Extract owner/name (last two path components)
+ owner_repo=$(echo "$repo_ref" | sed -E 's#(git@[^:]+:|https?://[^/]+/|ssh://[^/]+/)?(.*?)(\.git)?$#\2#')
+ owner="$(echo "$owner_repo" | awk -F'/' '{print $(NF-1)}')"
+ name="$(echo "$owner_repo" | awk -F'/' '{print $NF}' | sed -E 's/\.git$//')"
+ 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
+
+ if [[ "$base_ref" =~ ^https?://github\.com/([^/]+)/([^/]+)(\.git)?(/.*)?$ ]]; then
+ # HTTPS URL format - check this first before owner/name pattern
+ local name="${BASH_REMATCH[2]}"
+ name="${name%.git}" # Remove .git suffix if present
+ echo "${BASH_REMATCH[1]}/$name"
+ elif [[ "$base_ref" =~ ^https?://gitlab\.com/([^/]+)/([^/]+)(\.git)?(/.*)?$ ]]; then
+ # GitLab URL format
+ local name="${BASH_REMATCH[2]}"
+ name="${name%.git}" # Remove .git suffix if present
+ echo "${BASH_REMATCH[1]}/$name"
+ elif [[ "$base_ref" =~ ^git@github\.com:([^/]+)/([^/]+)(\.git)?$ ]]; then
+ # SSH URL format
+ local name="${BASH_REMATCH[2]}"
+ name="${name%.git}" # Remove .git suffix if present
+ echo "${BASH_REMATCH[1]}/$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"
+}
+
+# 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
+ echo "Error: Directory '$dirname' already exists."
+ echo "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"
+
+ echo "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
+ echo "-> $mod (tested with AC version: $v)"
+ else
+ echo "-> $mod (NOTE: The module latest tested AC revision is Unknown)"
+ 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=("$@")
+
+ echo "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
+ 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
+ echo "Error: duplicate module target directory '$dirname' detected in modules.list:"
+ echo " - ${_first[$dirname]}"
+ echo " - ${repo_ref}"
+ echo "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
+ parsed_output=$(inst_parse_module_spec "$repo_ref")
+ IFS=' ' read -r _ owner modname _ _ url dirname <<< "$parsed_output"
+
+ if [ -d "$J_PATH_MODULES/$dirname" ]; then
+ echo "[$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"
+ echo "[$repo_ref] Installed."
+ else
+ echo "[$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
+ echo "[$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")"
+ echo "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
+ echo "[$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
+ echo "[$repo_ref] Warning: 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"
+ echo "[$repo_ref] Installed in '$dirname'. Please re-run compiling and db assembly."
+ else
+ echo "[$repo_ref] Install failed or module not found"
+ exit 1;
+ fi
+ done
+ fi
+
+ echo ""
+ echo ""
+}
+
+# Update one or more modules
+function inst_module_update {
+ # 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
+ 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
+ echo "[$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"
+ echo "[$repo_ref] Updated to latest commit on '$branch'."
+ else
+ echo "[$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
+ echo "Warning: $repo_ref has no compatible acore-module.json; updating current branch '$branch'."
+ b="$branch"
+ else
+ def="$(inst_get_default_branch "$repo_ref")"
+ echo "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"
+ echo "[$repo_ref] Done, please re-run compiling and db assembly"
+ else
+ echo "[$repo_ref] Cannot update"
+ fi
+ else
+ echo "[$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"
+ echo "[$repo_ref] Done, please re-run compiling"
+ else
+ echo "[$repo_ref] Cannot remove"
+ fi
+ done
+
+ echo ""
+ echo ""
+}
diff --git a/apps/installer/main.sh b/apps/installer/main.sh
index 396dafaad5..5911ccbf74 100644
--- a/apps/installer/main.sh
+++ b/apps/installer/main.sh
@@ -6,22 +6,22 @@ source "$CURRENT_PATH/includes/includes.sh"
PS3='[Please enter your choice]: '
options=(
- "init (i): First Installation" # 1
- "install-deps (d): Configure OS dep" # 2
- "pull (u): Update Repository" # 3
- "reset (r): Reset & Clean Repository" # 4
- "compiler (c): Run compiler tool" # 5
- "module-search (ms): Module Search by keyword" # 6
- "module-install (mi): Module Install by name" # 7
- "module-update (mu): Module Update by name" # 8
- "module-remove: (mr): Module Remove by name" # 9
- "client-data: (gd): download client data from github repository (beta)" # 10
- "run-worldserver (rw): execute a simple restarter for worldserver" # 11
- "run-authserver (ra): execute a simple restarter for authserver" # 12
- "docker (dr): Run docker tools" # 13
- "version (v): Show AzerothCore version" # 14
- "service-manager (sm): Run service manager to run authserver and worldserver in background" # 15
- "quit: Exit from this menu" # 16
+ "init (i): First Installation"
+ "install-deps (d): Configure OS dep"
+ "pull (u): Update Repository"
+ "reset (r): Reset & Clean Repository"
+ "compiler (c): Run compiler tool"
+ "module (m): Module manager (search/install/update/remove)"
+ "module-install (mi): Module Install by name [DEPRECATED]"
+ "module-update (mu): Module Update by name [DEPRECATED]"
+ "module-remove: (mr): Module Remove by name [DEPRECATED]"
+ "client-data: (gd): download client data from github repository (beta)"
+ "run-worldserver (rw): execute a simple restarter for worldserver"
+ "run-authserver (ra): execute a simple restarter for authserver"
+ "docker (dr): Run docker tools"
+ "version (v): Show AzerothCore version"
+ "service-manager (sm): Run service manager to run authserver and worldserver in background"
+ "quit (q): Exit from this menu"
)
function _switch() {
@@ -29,56 +29,64 @@ function _switch() {
_opt="$2"
case $_reply in
- ""|"i"|"init"|"1")
+ ""|"i"|"init")
inst_allInOne
;;
- ""|"d"|"install-deps"|"2")
+ ""|"d"|"install-deps")
inst_configureOS
;;
- ""|"u"|"pull"|"3")
+ ""|"u"|"pull")
inst_updateRepo
;;
- ""|"r"|"reset"|"4")
+ ""|"r"|"reset")
inst_resetRepo
;;
- ""|"c"|"compiler"|"5")
+ ""|"c"|"compiler")
bash "$AC_PATH_APPS/compiler/compiler.sh" $_opt
;;
- ""|"ms"|"module-search"|"6")
- inst_module_search "$_opt"
+ ""|"m"|"module")
+ # Unified module command: supports subcommands search|install|update|remove
+ inst_module "${@:2}"
;;
- ""|"mi"|"module-install"|"7")
- inst_module_install "$_opt"
+ ""|"ms"|"module-search")
+ echo "[DEPRECATED] Use: ./acore.sh module search <terms...>"
+ inst_module_search "${@:2}"
;;
- ""|"mu"|"module-update"|"8")
- inst_module_update "$_opt"
+ ""|"mi"|"module-install")
+ echo "[DEPRECATED] Use: ./acore.sh module install <modules...>"
+ inst_module_install "${@:2}"
;;
- ""|"mr"|"module-remove"|"9")
- inst_module_remove "$_opt"
+ ""|"mu"|"module-update")
+ echo "[DEPRECATED] Use: ./acore.sh module update <modules...>"
+ inst_module_update "${@:2}"
;;
- ""|"gd"|"client-data"|"10")
+ ""|"mr"|"module-remove")
+ echo "[DEPRECATED] Use: ./acore.sh module remove <modules...>"
+ inst_module_remove "${@:2}"
+ ;;
+ ""|"gd"|"client-data")
inst_download_client_data
;;
- ""|"rw"|"run-worldserver"|"11")
+ ""|"rw"|"run-worldserver")
inst_simple_restarter worldserver
;;
- ""|"ra"|"run-authserver"|"12")
+ ""|"ra"|"run-authserver")
inst_simple_restarter authserver
;;
- ""|"dr"|"docker"|"13")
+ ""|"dr"|"docker")
DOCKER=1 bash "$AC_PATH_ROOT/apps/docker/docker-cmd.sh" "${@:2}"
exit
;;
- ""|"v"|"version"|"14")
+ ""|"v"|"version")
# denoRunFile "$AC_PATH_APPS/installer/main.ts" "version"
printf "AzerothCore Rev. %s\n" "$ACORE_VERSION"
exit
;;
- ""|"sm"|"service-manager"|"15")
+ ""|"sm"|"service-manager")
bash "$AC_PATH_APPS/startup-scripts/src/service-manager.sh" "${@:2}"
exit
;;
- ""|"quit"|"16")
+ ""|"q"|"quit")
echo "Goodbye!"
exit
;;
@@ -102,4 +110,5 @@ do
_switch $REPLY
break
done
+ echo "opt: $opt"
done
diff --git a/apps/installer/test/bats.conf b/apps/installer/test/bats.conf
new file mode 100644
index 0000000000..8034254e72
--- /dev/null
+++ b/apps/installer/test/bats.conf
@@ -0,0 +1,14 @@
+# BATS Test Configuration
+
+# Set test timeout (in seconds)
+export BATS_TEST_TIMEOUT=30
+
+# Enable verbose output for debugging
+export BATS_VERBOSE_RUN=1
+
+# Test output format
+export BATS_FORMATTER=pretty
+
+# Enable colored output
+export BATS_NO_PARALLELIZE_ACROSS_FILES=1
+export BATS_NO_PARALLELIZE_WITHIN_FILE=1
diff --git a/apps/installer/test/test_module_commands.bats b/apps/installer/test/test_module_commands.bats
new file mode 100755
index 0000000000..40c2849416
--- /dev/null
+++ b/apps/installer/test/test_module_commands.bats
@@ -0,0 +1,354 @@
+#!/usr/bin/env bats
+
+# Tests for installer module commands (search/install/update/remove)
+# Focused on installer:module install behavior using a mocked joiner
+
+load '../../test-framework/bats_libs/acore-support'
+load '../../test-framework/bats_libs/acore-assert'
+
+setup() {
+ acore_test_setup
+ # Point to the installer src directory (not needed in this test)
+
+ # Set installer/paths environment for the test
+ export AC_PATH_APPS="$TEST_DIR/apps"
+ export AC_PATH_ROOT="$TEST_DIR"
+ export AC_PATH_DEPS="$TEST_DIR/deps"
+ export AC_PATH_MODULES="$TEST_DIR/modules"
+ export MODULES_LIST_FILE="$TEST_DIR/conf/modules.list"
+
+ # Create stubbed deps: joiner.sh (sourced by includes) and semver
+ mkdir -p "$TEST_DIR/deps/acore/joiner"
+ cat > "$TEST_DIR/deps/acore/joiner/joiner.sh" << 'EOF'
+#!/usr/bin/env bash
+# Stub joiner functions used by installer
+Joiner:add_repo() {
+ # arguments: url name branch basedir
+ echo "ADD $@" > "$TEST_DIR/joiner_called.txt"
+ return 0
+}
+Joiner:upd_repo() {
+ echo "UPD $@" > "$TEST_DIR/joiner_called.txt"
+ return 0
+}
+Joiner:remove() {
+ echo "REM $@" > "$TEST_DIR/joiner_called.txt"
+ return 0
+}
+EOF
+ chmod +x "$TEST_DIR/deps/acore/joiner/joiner.sh"
+
+ mkdir -p "$TEST_DIR/deps/semver_bash"
+ # Minimal semver stub
+ cat > "$TEST_DIR/deps/semver_bash/semver.sh" << 'EOF'
+#!/usr/bin/env bash
+# semver stub
+semver::satisfies() { return 0; }
+EOF
+ chmod +x "$TEST_DIR/deps/semver_bash/semver.sh"
+
+ # Provide a minimal compiler includes file expected by installer
+ mkdir -p "$TEST_DIR/apps/compiler/includes"
+ touch "$TEST_DIR/apps/compiler/includes/includes.sh"
+
+ # Provide minimal bash_shared includes to satisfy installer include
+ mkdir -p "$TEST_DIR/apps/bash_shared"
+ cat > "$TEST_DIR/apps/bash_shared/includes.sh" << 'EOF'
+#!/usr/bin/env bash
+# minimal stub
+EOF
+
+ # Copy the real installer app into the test apps dir
+ mkdir -p "$TEST_DIR/apps"
+ cp -r "$(cd "$AC_TEST_ROOT/apps/installer" && pwd)" "$TEST_DIR/apps/installer"
+}
+
+teardown() {
+ acore_test_teardown
+}
+
+@test "module install should call joiner and record entry in modules list" {
+ cd "$TEST_DIR"
+
+ # Source installer includes and call the install function directly to avoid menu interaction
+ run bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_install example-module@main:abcd1234"
+
+ # Check that joiner was called
+ [ -f "$TEST_DIR/joiner_called.txt" ]
+ grep -q "ADD" "$TEST_DIR/joiner_called.txt"
+
+ # Check modules list was created and contains the repo_ref and branch
+ [ -f "$TEST_DIR/conf/modules.list" ]
+ grep -q "azerothcore/example-module main" "$TEST_DIR/conf/modules.list"
+}
+
+@test "module install with owner/name format should work" {
+ cd "$TEST_DIR"
+
+ # Test with owner/name format
+ run bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_install myorg/mymodule"
+
+ # Check that joiner was called with correct URL
+ [ -f "$TEST_DIR/joiner_called.txt" ]
+ grep -q "ADD https://github.com/myorg/mymodule mymodule" "$TEST_DIR/joiner_called.txt"
+
+ # Check modules list contains the entry
+ [ -f "$TEST_DIR/conf/modules.list" ]
+ grep -q "myorg/mymodule" "$TEST_DIR/conf/modules.list"
+}
+
+@test "module remove should call joiner remove and update modules list" {
+ cd "$TEST_DIR"
+
+ # First install a module
+ bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_install test-module"
+
+ # Then remove it
+ run bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_remove test-module"
+
+ # Check that joiner remove was called
+ [ -f "$TEST_DIR/joiner_called.txt" ]
+ # With flat structure, basedir is empty; ensure name is present
+ grep -q "REM test-module" "$TEST_DIR/joiner_called.txt"
+
+ # Check modules list no longer contains the entry
+ [ -f "$TEST_DIR/conf/modules.list" ]
+ ! grep -q "azerothcore/test-module" "$TEST_DIR/conf/modules.list"
+}
+
+# Tests for intelligent module management (duplicate prevention and cross-format removal)
+
+@test "inst_extract_owner_name should extract owner/name from various formats" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Test simple name
+ run inst_extract_owner_name "mod-transmog"
+ [ "$output" = "azerothcore/mod-transmog" ]
+
+ # Test owner/name format
+ run inst_extract_owner_name "azerothcore/mod-transmog"
+ [ "$output" = "azerothcore/mod-transmog" ]
+
+ # Test HTTPS URL
+ run inst_extract_owner_name "https://github.com/azerothcore/mod-transmog.git"
+ [ "$output" = "azerothcore/mod-transmog" ]
+
+ # Test SSH URL
+ run inst_extract_owner_name "git@github.com:azerothcore/mod-transmog.git"
+ [ "$output" = "azerothcore/mod-transmog" ]
+
+ # Test GitLab URL
+ run inst_extract_owner_name "https://gitlab.com/myorg/mymodule.git"
+ [ "$output" = "myorg/mymodule" ]
+}
+
+@test "duplicate module entries should be prevented across different formats" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Add module via simple name
+ inst_mod_list_upsert "mod-transmog" "master" "abc123"
+
+ # Verify it's in the list
+ grep -q "mod-transmog master abc123" "$TEST_DIR/conf/modules.list"
+
+ # Add same module via owner/name format - should replace, not duplicate
+ inst_mod_list_upsert "azerothcore/mod-transmog" "dev" "def456"
+
+ # Should only have one entry (the new one)
+ [ "$(grep -c "azerothcore/mod-transmog" "$TEST_DIR/conf/modules.list")" -eq 1 ]
+ grep -q "azerothcore/mod-transmog dev def456" "$TEST_DIR/conf/modules.list"
+ ! grep -q "mod-transmog master abc123" "$TEST_DIR/conf/modules.list"
+}
+
+@test "module installed via URL should be recognized when checking with different formats" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Install via HTTPS URL
+ inst_mod_list_upsert "https://github.com/azerothcore/mod-transmog.git" "master" "abc123"
+
+ # Should be detected as installed using simple name
+ run inst_mod_is_installed "mod-transmog"
+ [ "$status" -eq 0 ]
+
+ # Should be detected as installed using owner/name
+ run inst_mod_is_installed "azerothcore/mod-transmog"
+ [ "$status" -eq 0 ]
+
+ # Should be detected as installed using SSH URL
+ run inst_mod_is_installed "git@github.com:azerothcore/mod-transmog.git"
+ [ "$status" -eq 0 ]
+
+ # Non-existent module should not be detected
+ run inst_mod_is_installed "mod-nonexistent"
+ [ "$status" -ne 0 ]
+}
+
+@test "cross-format module removal should work" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Install via SSH URL
+ inst_mod_list_upsert "git@github.com:azerothcore/mod-transmog.git" "master" "abc123"
+
+ # Verify it's installed
+ grep -q "git@github.com:azerothcore/mod-transmog.git" "$TEST_DIR/conf/modules.list"
+
+ # Remove using simple name
+ inst_mod_list_remove "mod-transmog"
+
+ # Should be completely removed
+ ! grep -q "azerothcore/mod-transmog" "$TEST_DIR/conf/modules.list"
+ ! grep -q "git@github.com:azerothcore/mod-transmog.git" "$TEST_DIR/conf/modules.list"
+}
+
+@test "module installation should prevent duplicates when already installed" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Install via simple name first
+ inst_mod_list_upsert "mod-worldchat" "master" "abc123"
+
+ # Try to install same module via URL - should detect it's already installed
+ run inst_mod_is_installed "https://github.com/azerothcore/mod-worldchat.git"
+ [ "$status" -eq 0 ]
+
+ # Add via URL should replace the existing entry
+ inst_mod_list_upsert "https://github.com/azerothcore/mod-worldchat.git" "dev" "def456"
+
+ # Should only have one entry
+ [ "$(grep -c "azerothcore/mod-worldchat" "$TEST_DIR/conf/modules.list")" -eq 1 ]
+ grep -q "https://github.com/azerothcore/mod-worldchat.git dev def456" "$TEST_DIR/conf/modules.list"
+}
+
+@test "module update --all uses flat structure (no branch subfolders)" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Prepare modules.list with one entry and a matching local directory
+ mkdir -p "$TEST_DIR/conf"
+ echo "azerothcore/mod-transmog master abc123" > "$TEST_DIR/conf/modules.list"
+ mkdir -p "$TEST_DIR/modules/mod-transmog"
+
+ # Run update all
+ run bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_update --all"
+
+ # Verify Joiner:upd_repo received flat structure args (no basedir)
+ [ -f "$TEST_DIR/joiner_called.txt" ]
+ grep -q "UPD https://github.com/azerothcore/mod-transmog mod-transmog master" "$TEST_DIR/joiner_called.txt"
+}
+
+@test "module update specific uses flat structure with override branch" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Create local directory so update proceeds
+ mkdir -p "$TEST_DIR/modules/mymodule"
+
+ # Run update specifying owner/name and branch
+ run bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_update myorg/mymodule@dev"
+
+ # Should call joiner with name 'mymodule' and branch 'dev' (no basedir)
+ [ -f "$TEST_DIR/joiner_called.txt" ]
+ grep -q "UPD https://github.com/myorg/mymodule mymodule dev" "$TEST_DIR/joiner_called.txt"
+}
+
+@test "custom directory names should work with new syntax" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Test parsing with custom directory name
+ run inst_parse_module_spec "mod-transmog:my-custom-dir@develop:abc123"
+ [ "$status" -eq 0 ]
+ # Should output: repo_ref owner name branch commit url dirname
+ IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
+ [ "$repo_ref" = "azerothcore/mod-transmog" ]
+ [ "$owner" = "azerothcore" ]
+ [ "$name" = "mod-transmog" ]
+ [ "$branch" = "develop" ]
+ [ "$commit" = "abc123" ]
+ [ "$url" = "https://github.com/azerothcore/mod-transmog" ]
+ [ "$dirname" = "my-custom-dir" ]
+}
+
+@test "directory conflict detection should work" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Create a fake existing directory
+ mkdir -p "$TEST_DIR/modules/existing-dir"
+
+ # Should detect conflict
+ run inst_check_module_conflict "existing-dir" "mod-test"
+ [ "$status" -eq 1 ]
+ [[ "$output" =~ "Directory 'existing-dir' already exists" ]]
+ [[ "$output" =~ "Use a different directory name: mod-test:my-custom-name" ]]
+
+ # Should not detect conflict for non-existing directory
+ run inst_check_module_conflict "non-existing-dir" "mod-test"
+ [ "$status" -eq 0 ]
+}
+
+@test "module update should work with custom directories" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # First add module with custom directory to list
+ inst_mod_list_upsert "azerothcore/mod-transmog:custom-dir" "master" "abc123"
+
+ # Create fake module directory structure
+ mkdir -p "$TEST_DIR/modules/custom-dir/.git"
+ echo "ref: refs/heads/master" > "$TEST_DIR/modules/custom-dir/.git/HEAD"
+
+ # Mock git commands in the fake module directory
+ cat > "$TEST_DIR/modules/custom-dir/.git/config" << 'EOF'
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = false
+[remote "origin"]
+ url = https://github.com/azerothcore/mod-transmog
+ fetch = +refs/heads/*:refs/remotes/origin/*
+[branch "master"]
+ remote = origin
+ merge = refs/heads/master
+EOF
+
+ # Test update with custom directory should work
+ # Note: This would require more complex mocking for full integration test
+ # For now, just test the parsing recognizes the custom directory
+ run inst_parse_module_spec "azerothcore/mod-transmog:custom-dir"
+ [ "$status" -eq 0 ]
+ IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
+ [ "$dirname" = "custom-dir" ]
+}
+
+@test "URL formats should be properly normalized" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Test various URL formats produce same owner/name
+ run inst_extract_owner_name "https://github.com/azerothcore/mod-transmog"
+ local url_format="$output"
+
+ run inst_extract_owner_name "https://github.com/azerothcore/mod-transmog.git"
+ local url_git_format="$output"
+
+ run inst_extract_owner_name "git@github.com:azerothcore/mod-transmog.git"
+ local ssh_format="$output"
+
+ run inst_extract_owner_name "azerothcore/mod-transmog"
+ local owner_name_format="$output"
+
+ run inst_extract_owner_name "mod-transmog"
+ local simple_format="$output"
+
+ # All should normalize to the same owner/name
+ [ "$url_format" = "azerothcore/mod-transmog" ]
+ [ "$url_git_format" = "azerothcore/mod-transmog" ]
+ [ "$ssh_format" = "azerothcore/mod-transmog" ]
+ [ "$owner_name_format" = "azerothcore/mod-transmog" ]
+ [ "$simple_format" = "azerothcore/mod-transmog" ]
+}
diff --git a/conf/dist/config.sh b/conf/dist/config.sh
index df7ddd3fc2..2e105fd445 100644
--- a/conf/dist/config.sh
+++ b/conf/dist/config.sh
@@ -149,4 +149,17 @@ export CPUPROFILESIGNAL=${CPUPROFILESIGNAL:-12}
# Other values for HEAPCHECK: minimal, normal (equivalent to "1"), strict, draconian
#export HEAPCHECK=${HEAPCHECK:-normal}
+##############################################
+#
+# MODULES LIST FILE (for installer `module` commands)
+#
+# Path to the file where the installer records installed modules
+# with their branch and commit. You can override this path by
+# setting the MODULES_LIST_FILE inside your config.sh or as an environment variable.
+# By default it points inside the repository conf folder.
+# Format of each line:
+# <module-name> <branch> <commit>
+# Lines starting with '#' and empty lines are ignored.
+export MODULES_LIST_FILE=${MODULES_LIST_FILE:-"$AC_PATH_ROOT/conf/modules.list"}
+
diff --git a/deps/acore/joiner/joiner.sh b/deps/acore/joiner/joiner.sh
index be95b673a1..1b13007162 100755
--- a/deps/acore/joiner/joiner.sh
+++ b/deps/acore/joiner/joiner.sh
@@ -94,11 +94,11 @@ function Joiner:add_repo() (
basedir="${4:-""}"
[[ -z $url ]] && hasReq=false || hasReq=true
- Joiner:_help $hasReq "$1" "Syntax: joiner.sh add-repo [-d] [-e] url name branch [basedir]"
+ Joiner:_help "$hasReq" "$1" "Syntax: joiner.sh add-repo [-d] [-e] url name branch [basedir]"
# retrieving info from url if not set
if [[ -z $name ]]; then
- basename=$(basename $url)
+ basename=$(basename "$url")
name=${basename%%.*}
if [[ -z "$basedir" ]]; then
@@ -115,10 +115,12 @@ function Joiner:add_repo() (
if [ -e "$path/.git/" ]; then
# if exists , update
- git --git-dir="$path/.git/" rev-parse && git --git-dir="$path/.git/" pull origin $branch | grep 'Already up-to-date.' && changed="no" || true
+ echo "Updating $name on branch $branch..."
+ git --git-dir="$path/.git/" --work-tree="$path" rev-parse && git --git-dir="$path/.git/" --work-tree="$path" pull origin "$branch" | grep 'Already up-to-date.' && changed="no" || true
else
# otherwise clone
- git clone $url -c advice.detachedHead=0 -b $branch "$path"
+ echo "Cloning $name on branch $branch..."
+ git clone "$url" -c advice.detachedHead=0 -b "$branch" "$path"
fi
if [ "$?" -ne "0" ]; then
@@ -140,16 +142,16 @@ function Joiner:add_git_submodule() (
basedir=${4:-""}
[[ -z $url ]] && hasReq=false || hasReq=true
- Joiner:_help $hasReq "$1" "Syntax: joiner.sh add-git-submodule [-d] [-e] url name branch [basedir]"
+ Joiner:_help "$hasReq" "$1" "Syntax: joiner.sh add-git-submodule [-d] [-e] url name branch [basedir]"
# retrieving info from url if not set
if [[ -z $name ]]; then
- basename=$(basename $url)
+ basename=$(basename "$url")
name=${basename%%.*}
if [[ -z $basedir ]]; then
- dir=$(dirname $url)
- basedir=$(basename $dir)
+ dir=$(dirname "$url")
+ basedir=$(basename "$dir")
fi
name="${name,,}" #to lowercase
@@ -158,17 +160,17 @@ function Joiner:add_git_submodule() (
path="$J_PATH_MODULES/$basedir/$name"
valid_path=`Joiner:_searchFirstValiPath "$path"`
- rel_path=${path#$valid_path}
+ rel_path=${path#"$valid_path"}
rel_path=${rel_path#/}
- if [ -e $path/ ]; then
+ if [ -e "$path/" ]; then
# if exists , update
- (cd "$path" && git pull origin $branch)
- (cd "$valid_path" && git submodule update -f --init $rel_path)
+ (cd "$path" && git pull origin "$branch")
+ (cd "$valid_path" && git submodule update -f --init "$rel_path")
else
# otherwise add
- (cd "$valid_path" && git submodule add -f -b $branch $url $rel_path)
- (cd "$valid_path" && git submodule update -f --init $rel_path)
+ (cd "$valid_path" && git submodule add -f -b "$branch" "$url" "$rel_path")
+ (cd "$valid_path" && git submodule update -f --init "$rel_path")
fi
if [ "$?" -ne "0" ]; then
@@ -324,7 +326,7 @@ function Joiner:self_update() {
if [ ! -z "$J_VER_REQ" ]; then
# if J_VER_REQ is defined then update only if tag is different
_cur_branch=`git --git-dir="$J_PATH/.git/" --work-tree="$J_PATH/" rev-parse --abbrev-ref HEAD`
- _cur_ver=`git --git-dir="$J_PATH/.git/" --work-tree="$J_PATH/" name-rev --tags --name-only $_cur_branch`
+ _cur_ver=`git --git-dir="$J_PATH/.git/" --work-tree="$J_PATH/" name-rev --tags --name-only "$_cur_branch"`
if [ "$_cur_ver" != "$J_VER_REQ" ]; then
git --git-dir="$J_PATH/.git/" --work-tree="$J_PATH/" rev-parse && git --git-dir="$J_PATH/.git/" fetch --tags origin "$_cur_branch" --quiet
git --git-dir="$J_PATH/.git/" --work-tree="$J_PATH/" checkout "tags/$J_VER_REQ" -b "$_cur_branch"
@@ -416,8 +418,8 @@ function Joiner:menu() {
while true
do
# run option directly if specified in argument
- [ ! -z $1 ] && _switch $@
- [ ! -z $1 ] && exit 0
+ [ ! -z "$1" ] && _switch $@
+ [ ! -z "$1" ] && exit 0
echo ""
echo "==== JOINER MENU ===="