ElvUI.psm1

[CmdletBinding()]
param()
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath)
$script:PSModuleInfo = Import-PowerShellDataFile -Path "$PSScriptRoot\$baseName.psd1"
$script:PSModuleInfo | Format-List | Out-String -Stream | ForEach-Object { Write-Debug $_ }
$scriptName = $script:PSModuleInfo.Name
Write-Debug "[$scriptName] - Importing module"

#region [functions] - [private]
Write-Debug "[$scriptName] - [functions] - [private] - Processing folder"
#region [functions] - [private] - [Get-TukuiAddon]
Write-Debug "[$scriptName] - [functions] - [private] - [Get-TukuiAddon] - Importing"
function Get-TukuiAddon {
    <#
        .SYNOPSIS
        Fetches addon info from the Tukui API.

        .DESCRIPTION
        Returns metadata for a Tukui-hosted addon including version, download URL,
        description, supported patches, and more. When called without parameters,
        returns all available addons.

        .EXAMPLE
        Get-TukuiAddon -Name elvui

        Returns metadata for ElvUI.

        .EXAMPLE
        Get-TukuiAddon

        Returns all available Tukui addons.

        .OUTPUTS
        PSCustomObject
    #>

    [CmdletBinding()]
    param(
        # The slug name of the addon to retrieve. Omit to return all addons.
        [Parameter()]
        [ValidateSet('elvui', 'tukui')]
        [string] $Name
    )

    if ($Name) {
        $url = "https://api.tukui.org/v1/addon/$Name"
    } else {
        $url = 'https://api.tukui.org/v1/addons'
    }

    $response = Invoke-RestMethod -Uri $url

    foreach ($addon in @($response)) {
        [PSCustomObject]@{
            Id            = $addon.id
            Slug          = $addon.slug
            Name          = $addon.name
            Author        = $addon.author
            Version       = $addon.version
            DownloadUrl   = $addon.url
            ChangelogUrl  = $addon.changelog_url
            TicketUrl     = $addon.ticket_url
            GitUrl        = $addon.git_url
            Patches       = $addon.patch
            LastUpdate    = $addon.last_update
            WebUrl        = $addon.web_url
            DonateUrl     = $addon.donate_url
            Description   = $addon.small_desc
            ScreenshotUrl = $addon.screenshot_url
            GalleryUrls   = $addon.gallery_url
            LogoUrl       = $addon.logo_url
            LogoSquareUrl = $addon.logo_square_url
            Directories   = $addon.directories
        }
    }
}
Write-Debug "[$scriptName] - [functions] - [private] - [Get-TukuiAddon] - Done"
#endregion [functions] - [private] - [Get-TukuiAddon]
#region [functions] - [private] - [Get-TukuiInstalledVersion]
Write-Debug "[$scriptName] - [functions] - [private] - [Get-TukuiInstalledVersion] - Importing"
function Get-TukuiInstalledVersion {
    <#
        .SYNOPSIS
        Gets the currently installed version of a Tukui addon from its .toc file.

        .DESCRIPTION
        Reads the .toc file for the specified addon in the AddOns folder and
        extracts the version string. Returns $null if the addon is not installed.

        .EXAMPLE
        Get-TukuiInstalledVersion -AddOnsPath 'C:\...\AddOns' -Name elvui

        Returns the installed ElvUI version string, or $null if not found.

        .OUTPUTS
        System.String or $null if not installed.
    #>

    [CmdletBinding()]
    param(
        # The full path to the WoW AddOns directory.
        [Parameter(Mandatory)]
        [string] $AddOnsPath,

        # The slug name of the addon to check.
        [Parameter(Mandatory)]
        [ValidateSet('elvui', 'tukui')]
        [string] $Name
    )

    $addonFolder = switch ($Name) {
        'elvui' { 'ElvUI' }
        'tukui' { 'Tukui' }
    }

    $tocCandidates = @(
        (Join-Path -Path $AddOnsPath -ChildPath $addonFolder | Join-Path -ChildPath "${addonFolder}_Mainline.toc")
        (Join-Path -Path $AddOnsPath -ChildPath $addonFolder | Join-Path -ChildPath "$addonFolder.toc")
    )

    $tocPath = $tocCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
    if (-not $tocPath) {
        return $null
    }

    $tocContent = Get-Content -LiteralPath $tocPath -Raw -ErrorAction Stop
    if ($tocContent -match '(?m)^## Version:\s*(.+)$') {
        return $Matches[1].Trim().TrimStart('v')
    }
    return $null
}
Write-Debug "[$scriptName] - [functions] - [private] - [Get-TukuiInstalledVersion] - Done"
#endregion [functions] - [private] - [Get-TukuiInstalledVersion]
#region [functions] - [private] - [Get-WoWAddOnsPath]
Write-Debug "[$scriptName] - [functions] - [private] - [Get-WoWAddOnsPath] - Importing"
function Get-WoWAddOnsPath {
    <#
        .SYNOPSIS
        Resolves the WoW AddOns folder path for a given flavor.

        .DESCRIPTION
        Constructs and validates the full path to the World of Warcraft AddOns directory
        based on the installation path and game flavor.

        .EXAMPLE
        Get-WoWAddOnsPath

        Returns the default retail AddOns path: C:\Program Files (x86)\World of Warcraft\_retail_\Interface\AddOns

        .EXAMPLE
        Get-WoWAddOnsPath -WoWPath 'D:\Games\World of Warcraft' -Flavor '_classic_'

        Returns the classic AddOns path under a custom installation directory.

        .OUTPUTS
        System.String
    #>

    [CmdletBinding()]
    param(
        # Path to the World of Warcraft installation folder.
        [Parameter()]
        [string] $WoWPath = 'C:\Program Files (x86)\World of Warcraft',

        # WoW game flavor to target.
        [Parameter()]
        [ValidateSet('_retail_', '_classic_', '_classic_era_')]
        [string] $Flavor = '_retail_'
    )

    $addOnsPath = Join-Path -Path $WoWPath -ChildPath $Flavor | Join-Path -ChildPath 'Interface' | Join-Path -ChildPath 'AddOns'
    if (-not (Test-Path -LiteralPath $addOnsPath)) {
        throw "AddOns folder not found: $addOnsPath"
    }
    $addOnsPath
}
Write-Debug "[$scriptName] - [functions] - [private] - [Get-WoWAddOnsPath] - Done"
#endregion [functions] - [private] - [Get-WoWAddOnsPath]
#region [functions] - [private] - [Install-TukuiAddon]
Write-Debug "[$scriptName] - [functions] - [private] - [Install-TukuiAddon] - Importing"
function Install-TukuiAddon {
    <#
        .SYNOPSIS
        Downloads and installs a Tukui addon to the WoW AddOns folder.

        .DESCRIPTION
        Downloads the ZIP archive from the Tukui API, extracts it, removes old addon
        folders, and copies the new ones into place. Uses a temporary directory
        that is cleaned up automatically.

        .EXAMPLE
        $addon = Get-TukuiAddon -Name elvui
        Install-TukuiAddon -AddOnsPath 'C:\...\AddOns' -Addon $addon

        Downloads and installs ElvUI to the specified AddOns directory.
    #>

    [CmdletBinding()]
    param(
        # The full path to the WoW AddOns directory.
        [Parameter(Mandatory)]
        [string] $AddOnsPath,

        # The addon object returned by Get-TukuiAddon containing metadata and download URL.
        [Parameter(Mandatory)]
        [PSCustomObject] $Addon
    )

    $tempDir = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath "Tukui_$($Addon.Slug)_Update_$([guid]::NewGuid().ToString('N'))"
    try {
        if (Test-Path $tempDir) {
            Remove-Item $tempDir -Recurse -Force -ErrorAction Stop
        }
        $null = New-Item -ItemType Directory -Path $tempDir -ErrorAction Stop

        # Download
        $zipPath = Join-Path -Path $tempDir -ChildPath "$($Addon.Slug)-$($Addon.Version).zip"
        Write-Verbose "Downloading $($Addon.Name) $($Addon.Version) ..."
        Invoke-WebRequest -Uri $Addon.DownloadUrl -OutFile $zipPath -ErrorAction Stop

        # Extract
        $extractPath = Join-Path -Path $tempDir -ChildPath 'extracted'
        Write-Verbose 'Extracting...'
        Expand-Archive -Path $zipPath -DestinationPath $extractPath -Force -ErrorAction Stop

        # Remove old addon folders matching the known directory names
        $normalizedAddOnsPath = [System.IO.Path]::GetFullPath($AddOnsPath)
        $trimChars = @([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar)
        $normalizedAddOnsPath = $normalizedAddOnsPath.TrimEnd($trimChars) + [System.IO.Path]::DirectorySeparatorChar
        foreach ($dir in $Addon.Directories) {
            if ([string]::IsNullOrWhiteSpace($dir) -or $dir.Contains('\') -or $dir.Contains('/') -or $dir.Contains('..')) {
                throw "Invalid addon directory entry '$dir' returned by API."
            }

            $oldPath = Join-Path -Path $AddOnsPath -ChildPath $dir
            $resolvedOldPath = [System.IO.Path]::GetFullPath($oldPath)
            if (-not $resolvedOldPath.StartsWith($normalizedAddOnsPath, [System.StringComparison]::OrdinalIgnoreCase)) {
                throw "Resolved addon directory path '$resolvedOldPath' is outside the AddOns directory."
            }

            if (Test-Path -LiteralPath $resolvedOldPath) {
                Write-Verbose " Removing $dir"
                Remove-Item -LiteralPath $resolvedOldPath -Recurse -Force -ErrorAction Stop
            }
        }

        # Copy new folders
        $extractedFolders = Get-ChildItem -LiteralPath $extractPath -Directory -ErrorAction Stop
        foreach ($folder in $extractedFolders) {
            $destination = Join-Path -Path $AddOnsPath -ChildPath $folder.Name
            Write-Verbose " Installing $($folder.Name)"
            Copy-Item -LiteralPath $folder.FullName -Destination $destination -Recurse -Force -ErrorAction Stop
        }

        Write-Verbose "$($Addon.Name) $($Addon.Version) installed successfully!"
    } finally {
        if (Test-Path -LiteralPath $tempDir) {
            Remove-Item -LiteralPath $tempDir -Recurse -Force -ErrorAction SilentlyContinue
        }
    }
}
Write-Debug "[$scriptName] - [functions] - [private] - [Install-TukuiAddon] - Done"
#endregion [functions] - [private] - [Install-TukuiAddon]
Write-Debug "[$scriptName] - [functions] - [private] - Done"
#endregion [functions] - [private]
#region [functions] - [public]
Write-Debug "[$scriptName] - [functions] - [public] - Processing folder"
#region [functions] - [public] - [Install-ElvUI]
Write-Debug "[$scriptName] - [functions] - [public] - [Install-ElvUI] - Importing"
function Install-ElvUI {
    <#
        .SYNOPSIS
        Downloads and installs ElvUI to the WoW AddOns folder.

        .DESCRIPTION
        Fetches the latest ElvUI release from the Tukui API, downloads the ZIP archive,
        and installs it to the World of Warcraft AddOns directory. Any existing ElvUI
        folders are removed before the new version is copied into place.

        .EXAMPLE
        Install-ElvUI

        Installs ElvUI to the default retail WoW AddOns folder.

        .EXAMPLE
        Install-ElvUI -WoWPath 'D:\Games\World of Warcraft'

        Installs ElvUI using a custom WoW installation path.

        .EXAMPLE
        Install-ElvUI -Flavor '_classic_'

        Installs ElvUI to the Classic WoW AddOns folder.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        # Path to the World of Warcraft installation folder.
        [Parameter()]
        [string] $WoWPath = 'C:\Program Files (x86)\World of Warcraft',

        # WoW game flavor to target.
        [Parameter()]
        [ValidateSet('_retail_', '_classic_', '_classic_era_')]
        [string] $Flavor = '_retail_'
    )

    $addOnsPath = Get-WoWAddOnsPath -WoWPath $WoWPath -Flavor $Flavor
    $addon = Get-TukuiAddon -Name elvui

    if ($PSCmdlet.ShouldProcess($addOnsPath, "Install $($addon.Name) $($addon.Version)")) {
        Write-Verbose "Installing $($addon.Name) $($addon.Version) ..."
        Install-TukuiAddon -AddOnsPath $addOnsPath -Addon $addon
    }
}
Write-Debug "[$scriptName] - [functions] - [public] - [Install-ElvUI] - Done"
#endregion [functions] - [public] - [Install-ElvUI]
#region [functions] - [public] - [Update-ElvUI]
Write-Debug "[$scriptName] - [functions] - [public] - [Update-ElvUI] - Importing"
function Update-ElvUI {
    <#
        .SYNOPSIS
        Updates the ElvUI addon to the latest version.

        .DESCRIPTION
        Checks the installed ElvUI version against the latest available version from the
        Tukui API. If an update is available (or -Force is used), downloads and installs
        the new version. If ElvUI is not installed, performs a fresh install.

        .EXAMPLE
        Update-ElvUI

        Updates ElvUI in the default retail WoW installation.

        .EXAMPLE
        Update-ElvUI -WoWPath 'D:\Games\World of Warcraft'

        Updates ElvUI using a custom WoW installation path.

        .EXAMPLE
        Update-ElvUI -Flavor '_classic_'

        Updates ElvUI in the Classic WoW AddOns folder.

        .EXAMPLE
        Update-ElvUI -Force

        Reinstalls ElvUI even if the installed version matches the latest.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        # Path to the World of Warcraft installation folder.
        [Parameter()]
        [string] $WoWPath = 'C:\Program Files (x86)\World of Warcraft',

        # WoW game flavor to target.
        [Parameter()]
        [ValidateSet('_retail_', '_classic_', '_classic_era_')]
        [string] $Flavor = '_retail_',

        # Force reinstall even if the installed version matches the latest.
        [Parameter()]
        [switch] $Force
    )

    $addOnsPath = Get-WoWAddOnsPath -WoWPath $WoWPath -Flavor $Flavor
    $installedVersion = Get-TukuiInstalledVersion -AddOnsPath $addOnsPath -Name elvui

    if ($installedVersion) {
        Write-Verbose "Installed version: $installedVersion"
    } else {
        Write-Verbose 'No existing ElvUI installation detected. Installing fresh.'
    }

    $addon = Get-TukuiAddon -Name elvui
    Write-Verbose "Latest ElvUI version: $($addon.Version)"

    # Compare versions to prevent unintentional downgrades
    $installedVer = $null
    $latestVer = $null
    $canParseInstalled = [version]::TryParse($installedVersion, [ref]$installedVer)
    $canParseLatest = [version]::TryParse($addon.Version, [ref]$latestVer)
    $canCompareVersions = $installedVersion -and $canParseInstalled -and $canParseLatest

    if ($canCompareVersions -and $installedVer -gt $latestVer -and -not $Force) {
        Write-Verbose "Installed version ($installedVersion) is newer than latest ($($addon.Version)). Use -Force to reinstall."
        return
    }

    if ($installedVersion -eq $addon.Version -and -not $Force) {
        Write-Verbose 'ElvUI is already up to date. Use -Force to reinstall.'
        return
    }

    if (-not $canCompareVersions -and $installedVersion -and -not $Force) {
        $msg = "Cannot compare version formats (installed: '$installedVersion',"
        $msg += " latest: '$($addon.Version)'). Use -Force to proceed."
        Write-Verbose $msg
        return
    }

    if ($installedVersion -eq $addon.Version) {
        Write-Verbose "Forcing reinstall of $($addon.Version) ..."
    } elseif ($canCompareVersions -and $installedVer -gt $latestVer) {
        Write-Verbose "Forcing reinstall — installed ($installedVersion) is newer than latest ($($addon.Version))."
    } elseif (-not $canCompareVersions -and $installedVersion) {
        Write-Verbose "Forcing install — cannot compare versions (installed: '$installedVersion')."
    } elseif ($installedVersion) {
        Write-Verbose "Updating from $installedVersion to $($addon.Version) ..."
    }

    if ($PSCmdlet.ShouldProcess($addOnsPath, "Install $($addon.Name) $($addon.Version)")) {
        Install-TukuiAddon -AddOnsPath $addOnsPath -Addon $addon
    }
}
Write-Debug "[$scriptName] - [functions] - [public] - [Update-ElvUI] - Done"
#endregion [functions] - [public] - [Update-ElvUI]
Write-Debug "[$scriptName] - [functions] - [public] - Done"
#endregion [functions] - [public]

#region Member exporter
$exports = @{
    Alias    = '*'
    Cmdlet   = ''
    Function = @(
        'Install-ElvUI'
        'Update-ElvUI'
    )
    Variable = ''
}
Export-ModuleMember @exports
#endregion Member exporter