From 03d106eb5ca0a87728b52109d23fe51032a76652 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Apr 2026 22:30:23 +0200 Subject: [PATCH 01/14] Remove all template placeholder files --- examples/General.ps1 | 19 --- src/classes/private/SecretWriter.ps1 | 15 -- src/classes/public/Book.ps1 | 147 ------------------ src/finally.ps1 | 3 - src/formats/CultureInfo.Format.ps1xml | 37 ----- src/formats/Mygciview.Format.ps1xml | 65 -------- .../private/Get-InternalPSModule.ps1 | 18 --- .../private/Set-InternalPSModule.ps1 | 22 --- .../public/PSModule/Get-PSModuleTest.ps1 | 26 ---- .../public/PSModule/New-PSModuleTest.ps1 | 40 ----- src/functions/public/PSModule/PSModule.md | 3 - .../public/SomethingElse/Set-PSModuleTest.ps1 | 25 --- .../public/SomethingElse/SomethingElse.md | 1 - src/functions/public/Test-PSModuleTest.ps1 | 21 --- src/functions/public/completers.ps1 | 8 - src/init/initializer.ps1 | 3 - src/modules/OtherPSModule.psm1 | 19 --- src/scripts/loader.ps1 | 3 - src/types/DirectoryInfo.Types.ps1xml | 21 --- src/types/FileInfo.Types.ps1xml | 14 -- src/variables/private/PrivateVariables.ps1 | 47 ------ src/variables/public/Moons.ps1 | 6 - src/variables/public/Planets.ps1 | 20 --- src/variables/public/SolarSystems.ps1 | 17 -- tests/PSModuleTest.Tests.ps1 | 25 --- 25 files changed, 625 deletions(-) delete mode 100644 examples/General.ps1 delete mode 100644 src/classes/private/SecretWriter.ps1 delete mode 100644 src/classes/public/Book.ps1 delete mode 100644 src/finally.ps1 delete mode 100644 src/formats/CultureInfo.Format.ps1xml delete mode 100644 src/formats/Mygciview.Format.ps1xml delete mode 100644 src/functions/private/Get-InternalPSModule.ps1 delete mode 100644 src/functions/private/Set-InternalPSModule.ps1 delete mode 100644 src/functions/public/PSModule/Get-PSModuleTest.ps1 delete mode 100644 src/functions/public/PSModule/New-PSModuleTest.ps1 delete mode 100644 src/functions/public/PSModule/PSModule.md delete mode 100644 src/functions/public/SomethingElse/Set-PSModuleTest.ps1 delete mode 100644 src/functions/public/SomethingElse/SomethingElse.md delete mode 100644 src/functions/public/Test-PSModuleTest.ps1 delete mode 100644 src/functions/public/completers.ps1 delete mode 100644 src/init/initializer.ps1 delete mode 100644 src/modules/OtherPSModule.psm1 delete mode 100644 src/scripts/loader.ps1 delete mode 100644 src/types/DirectoryInfo.Types.ps1xml delete mode 100644 src/types/FileInfo.Types.ps1xml delete mode 100644 src/variables/private/PrivateVariables.ps1 delete mode 100644 src/variables/public/Moons.ps1 delete mode 100644 src/variables/public/Planets.ps1 delete mode 100644 src/variables/public/SolarSystems.ps1 delete mode 100644 tests/PSModuleTest.Tests.ps1 diff --git a/examples/General.ps1 b/examples/General.ps1 deleted file mode 100644 index e193423..0000000 --- a/examples/General.ps1 +++ /dev/null @@ -1,19 +0,0 @@ -<# - .SYNOPSIS - This is a general example of how to use the module. -#> - -# Import the module -Import-Module -Name 'PSModule' - -# Define the path to the font file -$FontFilePath = 'C:\Fonts\CodeNewRoman\CodeNewRomanNerdFontPropo-Regular.tff' - -# Install the font -Install-Font -Path $FontFilePath -Verbose - -# List installed fonts -Get-Font -Name 'CodeNewRomanNerdFontPropo-Regular' - -# Uninstall the font -Get-Font -Name 'CodeNewRomanNerdFontPropo-Regular' | Uninstall-Font -Verbose diff --git a/src/classes/private/SecretWriter.ps1 b/src/classes/private/SecretWriter.ps1 deleted file mode 100644 index 1b1732a..0000000 --- a/src/classes/private/SecretWriter.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -class SecretWriter { - [string] $Alias - [string] $Name - [string] $Secret - - SecretWriter([string] $alias, [string] $name, [string] $secret) { - $this.Alias = $alias - $this.Name = $name - $this.Secret = $secret - } - - [string] GetAlias() { - return $this.Alias - } -} diff --git a/src/classes/public/Book.ps1 b/src/classes/public/Book.ps1 deleted file mode 100644 index 8917d9a..0000000 --- a/src/classes/public/Book.ps1 +++ /dev/null @@ -1,147 +0,0 @@ -class Book { - # Class properties - [string] $Title - [string] $Author - [string] $Synopsis - [string] $Publisher - [datetime] $PublishDate - [int] $PageCount - [string[]] $Tags - # Default constructor - Book() { $this.Init(@{}) } - # Convenience constructor from hashtable - Book([hashtable]$Properties) { $this.Init($Properties) } - # Common constructor for title and author - Book([string]$Title, [string]$Author) { - $this.Init(@{Title = $Title; Author = $Author }) - } - # Shared initializer method - [void] Init([hashtable]$Properties) { - foreach ($Property in $Properties.Keys) { - $this.$Property = $Properties.$Property - } - } - # Method to calculate reading time as 2 minutes per page - [timespan] GetReadingTime() { - if ($this.PageCount -le 0) { - throw 'Unable to determine reading time from page count.' - } - $Minutes = $this.PageCount * 2 - return [timespan]::new(0, $Minutes, 0) - } - # Method to calculate how long ago a book was published - [timespan] GetPublishedAge() { - if ( - $null -eq $this.PublishDate -or - $this.PublishDate -eq [datetime]::MinValue - ) { throw 'PublishDate not defined' } - - return (Get-Date) - $this.PublishDate - } - # Method to return a string representation of the book - [string] ToString() { - return "$($this.Title) by $($this.Author) ($($this.PublishDate.Year))" - } -} - -class BookList { - # Static property to hold the list of books - static [System.Collections.Generic.List[Book]] $Books - # Static method to initialize the list of books. Called in the other - # static methods to avoid needing to explicit initialize the value. - static [void] Initialize() { [BookList]::Initialize($false) } - static [bool] Initialize([bool]$force) { - if ([BookList]::Books.Count -gt 0 -and -not $force) { - return $false - } - - [BookList]::Books = [System.Collections.Generic.List[Book]]::new() - - return $true - } - # Ensure a book is valid for the list. - static [void] Validate([book]$Book) { - $Prefix = @( - 'Book validation failed: Book must be defined with the Title,' - 'Author, and PublishDate properties, but' - ) -join ' ' - if ($null -eq $Book) { throw "$Prefix was null" } - if ([string]::IsNullOrEmpty($Book.Title)) { - throw "$Prefix Title wasn't defined" - } - if ([string]::IsNullOrEmpty($Book.Author)) { - throw "$Prefix Author wasn't defined" - } - if ([datetime]::MinValue -eq $Book.PublishDate) { - throw "$Prefix PublishDate wasn't defined" - } - } - # Static methods to manage the list of books. - # Add a book if it's not already in the list. - static [void] Add([Book]$Book) { - [BookList]::Initialize() - [BookList]::Validate($Book) - if ([BookList]::Books.Contains($Book)) { - throw "Book '$Book' already in list" - } - - $FindPredicate = { - param([Book]$b) - - $b.Title -eq $Book.Title -and - $b.Author -eq $Book.Author -and - $b.PublishDate -eq $Book.PublishDate - }.GetNewClosure() - if ([BookList]::Books.Find($FindPredicate)) { - throw "Book '$Book' already in list" - } - - [BookList]::Books.Add($Book) - } - # Clear the list of books. - static [void] Clear() { - [BookList]::Initialize() - [BookList]::Books.Clear() - } - # Find a specific book using a filtering scriptblock. - static [Book] Find([scriptblock]$Predicate) { - [BookList]::Initialize() - return [BookList]::Books.Find($Predicate) - } - # Find every book matching the filtering scriptblock. - static [Book[]] FindAll([scriptblock]$Predicate) { - [BookList]::Initialize() - return [BookList]::Books.FindAll($Predicate) - } - # Remove a specific book. - static [void] Remove([Book]$Book) { - [BookList]::Initialize() - [BookList]::Books.Remove($Book) - } - # Remove a book by property value. - static [void] RemoveBy([string]$Property, [string]$Value) { - [BookList]::Initialize() - $Index = [BookList]::Books.FindIndex({ - param($b) - $b.$Property -eq $Value - }.GetNewClosure()) - if ($Index -ge 0) { - [BookList]::Books.RemoveAt($Index) - } - } -} - -enum Binding { - Hardcover - Paperback - EBook -} - -enum Genre { - Mystery - Thriller - Romance - ScienceFiction - Fantasy - Horror -} diff --git a/src/finally.ps1 b/src/finally.ps1 deleted file mode 100644 index d8fc207..0000000 --- a/src/finally.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -Write-Verbose '------------------------------' -Write-Verbose '--- THIS IS A LAST LOADER ---' -Write-Verbose '------------------------------' diff --git a/src/formats/CultureInfo.Format.ps1xml b/src/formats/CultureInfo.Format.ps1xml deleted file mode 100644 index a715e08..0000000 --- a/src/formats/CultureInfo.Format.ps1xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - System.Globalization.CultureInfo - - System.Globalization.CultureInfo - - - - - 16 - - - 16 - - - - - - - - LCID - - - Name - - - DisplayName - - - - - - - - diff --git a/src/formats/Mygciview.Format.ps1xml b/src/formats/Mygciview.Format.ps1xml deleted file mode 100644 index 4c972c2..0000000 --- a/src/formats/Mygciview.Format.ps1xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - mygciview - - System.IO.DirectoryInfo - System.IO.FileInfo - - - PSParentPath - - - - - - 7 - Left - - - - 26 - Right - - - - 26 - Right - - - - 14 - Right - - - - Left - - - - - - - - ModeWithoutHardLink - - - LastWriteTime - - - CreationTime - - - Length - - - Name - - - - - - - - diff --git a/src/functions/private/Get-InternalPSModule.ps1 b/src/functions/private/Get-InternalPSModule.ps1 deleted file mode 100644 index 89f053c..0000000 --- a/src/functions/private/Get-InternalPSModule.ps1 +++ /dev/null @@ -1,18 +0,0 @@ -function Get-InternalPSModule { - <# - .SYNOPSIS - Performs tests on a module. - - .EXAMPLE - Test-PSModule -Name 'World' - - "Hello, World!" - #> - [CmdletBinding()] - param ( - # Name of the person to greet. - [Parameter(Mandatory)] - [string] $Name - ) - Write-Output "Hello, $Name!" -} diff --git a/src/functions/private/Set-InternalPSModule.ps1 b/src/functions/private/Set-InternalPSModule.ps1 deleted file mode 100644 index cf870ba..0000000 --- a/src/functions/private/Set-InternalPSModule.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -function Set-InternalPSModule { - <# - .SYNOPSIS - Performs tests on a module. - - .EXAMPLE - Test-PSModule -Name 'World' - - "Hello, World!" - #> - [Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', - Justification = 'Reason for suppressing' - )] - [CmdletBinding()] - param ( - # Name of the person to greet. - [Parameter(Mandatory)] - [string] $Name - ) - Write-Output "Hello, $Name!" -} diff --git a/src/functions/public/PSModule/Get-PSModuleTest.ps1 b/src/functions/public/PSModule/Get-PSModuleTest.ps1 deleted file mode 100644 index a07d05b..0000000 --- a/src/functions/public/PSModule/Get-PSModuleTest.ps1 +++ /dev/null @@ -1,26 +0,0 @@ -#Requires -Modules Utilities -#Requires -Modules @{ ModuleName = 'PSSemVer'; RequiredVersion = '1.1.4' } -#Requires -Modules @{ ModuleName = 'DynamicParams'; ModuleVersion = '1.1.8' } -#Requires -Modules @{ ModuleName = 'Store'; ModuleVersion = '0.3.1' } - -function Get-PSModuleTest { - <# - .SYNOPSIS - Performs tests on a module. - - .DESCRIPTION - Performs tests on a module. - - .EXAMPLE - Test-PSModule -Name 'World' - - "Hello, World!" - #> - [CmdletBinding()] - param ( - # Name of the person to greet. - [Parameter(Mandatory)] - [string] $Name - ) - Write-Output "Hello, $Name!" -} diff --git a/src/functions/public/PSModule/New-PSModuleTest.ps1 b/src/functions/public/PSModule/New-PSModuleTest.ps1 deleted file mode 100644 index e003841..0000000 --- a/src/functions/public/PSModule/New-PSModuleTest.ps1 +++ /dev/null @@ -1,40 +0,0 @@ -#Requires -Modules @{ModuleName='PSSemVer'; ModuleVersion='1.1.4'} - -function New-PSModuleTest { - <# - .SYNOPSIS - Performs tests on a module. - - .DESCRIPTION - Performs tests on a module. - - .EXAMPLE - Test-PSModule -Name 'World' - - "Hello, World!" - - .NOTES - Testing if a module can have a [Markdown based link](https://example.com). - !"#¤%&/()=?`´^¨*'-_+§½{[]}<>|@£$€¥¢:;.," - \[This is a test\] - #> - [Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', - Justification = 'Reason for suppressing' - )] - [Alias('New-PSModuleTestAlias1')] - [Alias('New-PSModuleTestAlias2')] - [CmdletBinding()] - param ( - # Name of the person to greet. - [Parameter(Mandatory)] - [string] $Name - ) - Write-Output "Hello, $Name!" -} - -New-Alias New-PSModuleTestAlias3 New-PSModuleTest -New-Alias -Name New-PSModuleTestAlias4 -Value New-PSModuleTest - - -Set-Alias New-PSModuleTestAlias5 New-PSModuleTest diff --git a/src/functions/public/PSModule/PSModule.md b/src/functions/public/PSModule/PSModule.md deleted file mode 100644 index a657773..0000000 --- a/src/functions/public/PSModule/PSModule.md +++ /dev/null @@ -1,3 +0,0 @@ -# PSModule - -This is a sub page for PSModule. diff --git a/src/functions/public/SomethingElse/Set-PSModuleTest.ps1 b/src/functions/public/SomethingElse/Set-PSModuleTest.ps1 deleted file mode 100644 index 23ec98e..0000000 --- a/src/functions/public/SomethingElse/Set-PSModuleTest.ps1 +++ /dev/null @@ -1,25 +0,0 @@ -function Set-PSModuleTest { - <# - .SYNOPSIS - Performs tests on a module. - - .DESCRIPTION - Performs tests on a module. - - .EXAMPLE - Test-PSModule -Name 'World' - - "Hello, World!" - #> - [Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', - Justification = 'Reason for suppressing' - )] - [CmdletBinding()] - param ( - # Name of the person to greet. - [Parameter(Mandatory)] - [string] $Name - ) - Write-Output "Hello, $Name!" -} diff --git a/src/functions/public/SomethingElse/SomethingElse.md b/src/functions/public/SomethingElse/SomethingElse.md deleted file mode 100644 index d9f7e9e..0000000 --- a/src/functions/public/SomethingElse/SomethingElse.md +++ /dev/null @@ -1 +0,0 @@ -# This is SomethingElse diff --git a/src/functions/public/Test-PSModuleTest.ps1 b/src/functions/public/Test-PSModuleTest.ps1 deleted file mode 100644 index 0c27510..0000000 --- a/src/functions/public/Test-PSModuleTest.ps1 +++ /dev/null @@ -1,21 +0,0 @@ -function Test-PSModuleTest { - <# - .SYNOPSIS - Performs tests on a module. - - .DESCRIPTION - Performs tests on a module. - - .EXAMPLE - Test-PSModule -Name 'World' - - "Hello, World!" - #> - [CmdletBinding()] - param ( - # Name of the person to greet. - [Parameter(Mandatory)] - [string] $Name - ) - Write-Output "Hello, $Name!" -} diff --git a/src/functions/public/completers.ps1 b/src/functions/public/completers.ps1 deleted file mode 100644 index 6b1adbb..0000000 --- a/src/functions/public/completers.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -Register-ArgumentCompleter -CommandName New-PSModuleTest -ParameterName Name -ScriptBlock { - param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) - $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters - - 'Alice', 'Bob', 'Charlie' | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { - [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) - } -} diff --git a/src/init/initializer.ps1 b/src/init/initializer.ps1 deleted file mode 100644 index 28396fb..0000000 --- a/src/init/initializer.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -Write-Verbose '-------------------------------' -Write-Verbose '--- THIS IS AN INITIALIZER ---' -Write-Verbose '-------------------------------' diff --git a/src/modules/OtherPSModule.psm1 b/src/modules/OtherPSModule.psm1 deleted file mode 100644 index 5d6af8e..0000000 --- a/src/modules/OtherPSModule.psm1 +++ /dev/null @@ -1,19 +0,0 @@ -function Get-OtherPSModule { - <# - .SYNOPSIS - Performs tests on a module. - - .DESCRIPTION - A longer description of the function. - - .EXAMPLE - Get-OtherPSModule -Name 'World' - #> - [CmdletBinding()] - param( - # Name of the person to greet. - [Parameter(Mandatory)] - [string] $Name - ) - Write-Output "Hello, $Name!" -} diff --git a/src/scripts/loader.ps1 b/src/scripts/loader.ps1 deleted file mode 100644 index 973735a..0000000 --- a/src/scripts/loader.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -Write-Verbose '-------------------------' -Write-Verbose '--- THIS IS A LOADER ---' -Write-Verbose '-------------------------' diff --git a/src/types/DirectoryInfo.Types.ps1xml b/src/types/DirectoryInfo.Types.ps1xml deleted file mode 100644 index aef538b..0000000 --- a/src/types/DirectoryInfo.Types.ps1xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - System.IO.FileInfo - - - Status - Success - - - - - System.IO.DirectoryInfo - - - Status - Success - - - - diff --git a/src/types/FileInfo.Types.ps1xml b/src/types/FileInfo.Types.ps1xml deleted file mode 100644 index 4cfaf6b..0000000 --- a/src/types/FileInfo.Types.ps1xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - System.IO.FileInfo - - - Age - - ((Get-Date) - ($this.CreationTime)).Days - - - - - diff --git a/src/variables/private/PrivateVariables.ps1 b/src/variables/private/PrivateVariables.ps1 deleted file mode 100644 index f1fc2c3..0000000 --- a/src/variables/private/PrivateVariables.ps1 +++ /dev/null @@ -1,47 +0,0 @@ -$script:HabitablePlanets = @( - @{ - Name = 'Earth' - Mass = 5.97 - Diameter = 12756 - DayLength = 24.0 - }, - @{ - Name = 'Mars' - Mass = 0.642 - Diameter = 6792 - DayLength = 24.7 - }, - @{ - Name = 'Proxima Centauri b' - Mass = 1.17 - Diameter = 11449 - DayLength = 5.15 - }, - @{ - Name = 'Kepler-442b' - Mass = 2.34 - Diameter = 11349 - DayLength = 5.7 - }, - @{ - Name = 'Kepler-452b' - Mass = 5.0 - Diameter = 17340 - DayLength = 20.0 - } -) - -$script:InhabitedPlanets = @( - @{ - Name = 'Earth' - Mass = 5.97 - Diameter = 12756 - DayLength = 24.0 - }, - @{ - Name = 'Mars' - Mass = 0.642 - Diameter = 6792 - DayLength = 24.7 - } -) diff --git a/src/variables/public/Moons.ps1 b/src/variables/public/Moons.ps1 deleted file mode 100644 index dd0f33c..0000000 --- a/src/variables/public/Moons.ps1 +++ /dev/null @@ -1,6 +0,0 @@ -$script:Moons = @( - @{ - Planet = 'Earth' - Name = 'Moon' - } -) diff --git a/src/variables/public/Planets.ps1 b/src/variables/public/Planets.ps1 deleted file mode 100644 index 5927bc5..0000000 --- a/src/variables/public/Planets.ps1 +++ /dev/null @@ -1,20 +0,0 @@ -$script:Planets = @( - @{ - Name = 'Mercury' - Mass = 0.330 - Diameter = 4879 - DayLength = 4222.6 - }, - @{ - Name = 'Venus' - Mass = 4.87 - Diameter = 12104 - DayLength = 2802.0 - }, - @{ - Name = 'Earth' - Mass = 5.97 - Diameter = 12756 - DayLength = 24.0 - } -) diff --git a/src/variables/public/SolarSystems.ps1 b/src/variables/public/SolarSystems.ps1 deleted file mode 100644 index acbcedf..0000000 --- a/src/variables/public/SolarSystems.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -$script:SolarSystems = @( - @{ - Name = 'Solar System' - Planets = $script:Planets - Moons = $script:Moons - }, - @{ - Name = 'Alpha Centauri' - Planets = @() - Moons = @() - }, - @{ - Name = 'Sirius' - Planets = @() - Moons = @() - } -) diff --git a/tests/PSModuleTest.Tests.ps1 b/tests/PSModuleTest.Tests.ps1 deleted file mode 100644 index b856855..0000000 --- a/tests/PSModuleTest.Tests.ps1 +++ /dev/null @@ -1,25 +0,0 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSReviewUnusedParameter', '', - Justification = 'Required for Pester tests' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', '', - Justification = 'Required for Pester tests' -)] -[CmdletBinding()] -param() - -Describe 'Module' { - It 'Function: Get-PSModuleTest' { - Get-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' - } - It 'Function: New-PSModuleTest' { - New-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' - } - It 'Function: Set-PSModuleTest' { - Set-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' - } - It 'Function: Test-PSModuleTest' { - Test-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' - } -} From ae7943e8af3b85ccd72f6ed9a53f3bf9f4bbd4bd Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Apr 2026 22:31:09 +0200 Subject: [PATCH 02/14] Add private Tukui helper functions --- src/functions/private/Get-TukuiAddon.ps1 | 63 +++++++++++++++++ .../private/Get-TukuiInstalledVersion.ps1 | 50 ++++++++++++++ src/functions/private/Get-WoWAddOnsPath.ps1 | 40 +++++++++++ src/functions/private/Install-TukuiAddon.ps1 | 68 +++++++++++++++++++ 4 files changed, 221 insertions(+) create mode 100644 src/functions/private/Get-TukuiAddon.ps1 create mode 100644 src/functions/private/Get-TukuiInstalledVersion.ps1 create mode 100644 src/functions/private/Get-WoWAddOnsPath.ps1 create mode 100644 src/functions/private/Install-TukuiAddon.ps1 diff --git a/src/functions/private/Get-TukuiAddon.ps1 b/src/functions/private/Get-TukuiAddon.ps1 new file mode 100644 index 0000000..a685fd1 --- /dev/null +++ b/src/functions/private/Get-TukuiAddon.ps1 @@ -0,0 +1,63 @@ +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 -UseBasicParsing + + 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 + } + } +} diff --git a/src/functions/private/Get-TukuiInstalledVersion.ps1 b/src/functions/private/Get-TukuiInstalledVersion.ps1 new file mode 100644 index 0000000..60ec421 --- /dev/null +++ b/src/functions/private/Get-TukuiInstalledVersion.ps1 @@ -0,0 +1,50 @@ +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 $AddOnsPath $addonFolder "${addonFolder}_Mainline.toc") + (Join-Path $AddOnsPath $addonFolder "$addonFolder.toc") + ) + + $tocPath = $tocCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1 + if (-not $tocPath) { + return $null + } + + $tocContent = Get-Content -Path $tocPath -Raw + if ($tocContent -match '(?m)^## Version:\s*(.+)$') { + return $Matches[1].Trim().TrimStart('v') + } + return $null +} diff --git a/src/functions/private/Get-WoWAddOnsPath.ps1 b/src/functions/private/Get-WoWAddOnsPath.ps1 new file mode 100644 index 0000000..054acf8 --- /dev/null +++ b/src/functions/private/Get-WoWAddOnsPath.ps1 @@ -0,0 +1,40 @@ +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 $WoWPath $Flavor 'Interface' 'AddOns' + if (-not (Test-Path $addOnsPath)) { + throw "AddOns folder not found: $addOnsPath" + } + $addOnsPath +} diff --git a/src/functions/private/Install-TukuiAddon.ps1 b/src/functions/private/Install-TukuiAddon.ps1 new file mode 100644 index 0000000..ca5c96d --- /dev/null +++ b/src/functions/private/Install-TukuiAddon.ps1 @@ -0,0 +1,68 @@ +function Install-TukuiAddon { + <# + .SYNOPSIS + Downloads and installs a Tukui addon to the WoW AddOns folder. + + .DESCRIPTION + Downloads the zip 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 ([System.IO.Path]::GetTempPath()) "Tukui_$($Addon.Slug)_Update" + try { + if (Test-Path $tempDir) { + Remove-Item $tempDir -Recurse -Force + } + New-Item -ItemType Directory -Path $tempDir | Out-Null + + # Download + $zipPath = Join-Path $tempDir "$($Addon.Slug)-$($Addon.Version).zip" + Write-Host "Downloading $($Addon.Name) $($Addon.Version) ..." -ForegroundColor Cyan + Invoke-WebRequest -Uri $Addon.DownloadUrl -OutFile $zipPath -UseBasicParsing + + # Extract + $extractPath = Join-Path $tempDir 'extracted' + Write-Host 'Extracting...' -ForegroundColor Cyan + Expand-Archive -Path $zipPath -DestinationPath $extractPath -Force + + # Remove old addon folders matching the known directory names + foreach ($dir in $Addon.Directories) { + $oldPath = Join-Path $AddOnsPath $dir + if (Test-Path $oldPath) { + Write-Host " Removing $dir" -ForegroundColor Yellow + Remove-Item $oldPath -Recurse -Force + } + } + + # Copy new folders + $extractedFolders = Get-ChildItem -Path $extractPath -Directory + foreach ($folder in $extractedFolders) { + $destination = Join-Path $AddOnsPath $folder.Name + Write-Host " Installing $($folder.Name)" -ForegroundColor Cyan + Copy-Item -Path $folder.FullName -Destination $destination -Recurse -Force + } + + Write-Host "$($Addon.Name) $($Addon.Version) installed successfully!" -ForegroundColor Green + } finally { + if (Test-Path $tempDir) { + Remove-Item $tempDir -Recurse -Force + } + } +} From 260b21af385ca03b7175a53ec6969d4cc274cba4 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Apr 2026 22:31:36 +0200 Subject: [PATCH 03/14] Add Install-ElvUI and Update-ElvUI public functions --- src/functions/public/Install-ElvUI.ps1 | 43 ++++++++++++++++ src/functions/public/Update-ElvUI.ps1 | 71 ++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 src/functions/public/Install-ElvUI.ps1 create mode 100644 src/functions/public/Update-ElvUI.ps1 diff --git a/src/functions/public/Install-ElvUI.ps1 b/src/functions/public/Install-ElvUI.ps1 new file mode 100644 index 0000000..01388be --- /dev/null +++ b/src/functions/public/Install-ElvUI.ps1 @@ -0,0 +1,43 @@ +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()] + 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 + + Write-Host "Installing $($addon.Name) $($addon.Version) ..." -ForegroundColor Cyan + Install-TukuiAddon -AddOnsPath $addOnsPath -Addon $addon +} diff --git a/src/functions/public/Update-ElvUI.ps1 b/src/functions/public/Update-ElvUI.ps1 new file mode 100644 index 0000000..f7aa11e --- /dev/null +++ b/src/functions/public/Update-ElvUI.ps1 @@ -0,0 +1,71 @@ +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()] + 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-Host "Installed version: $installedVersion" -ForegroundColor Cyan + } else { + Write-Host 'No existing ElvUI installation detected. Installing fresh.' -ForegroundColor Yellow + } + + $addon = Get-TukuiAddon -Name elvui + Write-Host "Latest ElvUI version: $($addon.Version)" -ForegroundColor Green + + if ($installedVersion -eq $addon.Version -and -not $Force) { + Write-Host 'ElvUI is already up to date. Use -Force to reinstall.' -ForegroundColor Green + return + } + + if ($installedVersion -eq $addon.Version) { + Write-Host "Forcing reinstall of $($addon.Version) ..." -ForegroundColor Yellow + } elseif ($installedVersion) { + Write-Host "Updating from $installedVersion to $($addon.Version) ..." -ForegroundColor Yellow + } + + Install-TukuiAddon -AddOnsPath $addOnsPath -Addon $addon +} From c2311e4964bb18d4468159f31cdb9a289cc17d39 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Apr 2026 22:31:52 +0200 Subject: [PATCH 04/14] Remove placeholder values from data files --- src/data/Config.psd1 | 1 - src/data/Settings.psd1 | 1 - 2 files changed, 2 deletions(-) diff --git a/src/data/Config.psd1 b/src/data/Config.psd1 index fea4466..0537b0e 100644 --- a/src/data/Config.psd1 +++ b/src/data/Config.psd1 @@ -1,3 +1,2 @@ @{ - RandomKey = 'RandomValue' } diff --git a/src/data/Settings.psd1 b/src/data/Settings.psd1 index bcfa7b4..0537b0e 100644 --- a/src/data/Settings.psd1 +++ b/src/data/Settings.psd1 @@ -1,3 +1,2 @@ @{ - RandomSetting = 'RandomSettingValue' } From b5f5f64ac8346502fbb51f7e0f0ea4fc617ac321 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Apr 2026 22:32:25 +0200 Subject: [PATCH 05/14] Update README with ElvUI module documentation --- README.md | 48 +++++++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 6319793..2a0b90c 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,55 @@ -# {{ NAME }} +# ElvUI -{{ DESCRIPTION }} +A PowerShell module for installing and updating the [ElvUI](https://www.tukui.org/elvui) addon +for [World of Warcraft](https://worldofwarcraft.blizzard.com/). ## Prerequisites -This uses the following external resources: +- Windows PowerShell 5.1 or PowerShell 7+ +- A World of Warcraft installation - The [PSModule framework](https://github.com/PSModule/Process-PSModule) for building, testing and publishing the module. ## Installation -To install the module from the PowerShell Gallery, you can use the following command: +To install the module from the PowerShell Gallery: ```powershell -Install-PSResource -Name {{ NAME }} -Import-Module -Name {{ NAME }} +Install-PSResource -Name ElvUI +Import-Module -Name ElvUI ``` ## Usage -Here is a list of example that are typical use cases for the module. +### Update ElvUI to the latest version -### Example 1: Greet an entity +```powershell +Update-ElvUI +``` -Provide examples for typical commands that a user would like to do with the module. +### Force reinstall even if already up to date ```powershell -Greet-Entity -Name 'World' -Hello, World! +Update-ElvUI -Force ``` -### Example 2 +### Install ElvUI fresh + +```powershell +Install-ElvUI +``` -Provide examples for typical commands that a user would like to do with the module. +### Target a different WoW installation or flavor ```powershell -Import-Module -Name PSModuleTemplate +Update-ElvUI -WoWPath 'D:\Games\World of Warcraft' -Flavor '_classic_' ``` ### Find more examples To find more examples of how to use the module, please refer to the [examples](examples) folder. -Alternatively, you can use the Get-Command -Module 'This module' to find more commands that are available in the module. -To find examples of each of the commands you can use Get-Help -Examples 'CommandName'. - -## Documentation - -Link to further documentation if available, or describe where in the repository users can find more detailed documentation about -the module's functions and features. +You can also use `Get-Command -Module ElvUI` to list available commands, +and `Get-Help -Examples ` to see usage examples for each. ## Contributing @@ -63,7 +65,3 @@ Please see the issues tab on this project and submit a new issue that matches yo If you do code, we'd love to have your contributions. Please read the [Contribution guidelines](CONTRIBUTING.md) for more information. You can either help by picking up an existing issue or submit a new one if you have an idea for a new feature or improvement. - -## Acknowledgements - -Here is a list of people and projects that helped this project in some way. From 858f7e233510775bca0b2c5ade6834e0f9cfdec4 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Apr 2026 22:33:04 +0200 Subject: [PATCH 06/14] Add Pester tests for Install-ElvUI and Update-ElvUI --- tests/ElvUI.Tests.ps1 | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/ElvUI.Tests.ps1 diff --git a/tests/ElvUI.Tests.ps1 b/tests/ElvUI.Tests.ps1 new file mode 100644 index 0000000..ad9ca3f --- /dev/null +++ b/tests/ElvUI.Tests.ps1 @@ -0,0 +1,38 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester tests' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester tests' +)] +[CmdletBinding()] +param() + +Describe 'ElvUI' { + Context 'Install-ElvUI' { + It 'Should be available' { + Get-Command Install-ElvUI -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty + } + It 'Should have WoWPath parameter' { + (Get-Command Install-ElvUI).Parameters.ContainsKey('WoWPath') | Should -BeTrue + } + It 'Should have Flavor parameter' { + (Get-Command Install-ElvUI).Parameters.ContainsKey('Flavor') | Should -BeTrue + } + } + Context 'Update-ElvUI' { + It 'Should be available' { + Get-Command Update-ElvUI -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty + } + It 'Should have WoWPath parameter' { + (Get-Command Update-ElvUI).Parameters.ContainsKey('WoWPath') | Should -BeTrue + } + It 'Should have Flavor parameter' { + (Get-Command Update-ElvUI).Parameters.ContainsKey('Flavor') | Should -BeTrue + } + It 'Should have Force parameter' { + (Get-Command Update-ElvUI).Parameters.ContainsKey('Force') | Should -BeTrue + } + } +} From df34f061058a24e0e0da5e883b9cb2368b3b7cd5 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Apr 2026 22:33:13 +0200 Subject: [PATCH 07/14] Add usage examples --- examples/General.ps1 | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 examples/General.ps1 diff --git a/examples/General.ps1 b/examples/General.ps1 new file mode 100644 index 0000000..3cc2a68 --- /dev/null +++ b/examples/General.ps1 @@ -0,0 +1,19 @@ +<# + .SYNOPSIS + Examples of how to use the ElvUI module. +#> + +# Import the module +Import-Module -Name ElvUI + +# Update ElvUI to the latest version +Update-ElvUI + +# Force reinstall even if already up to date +Update-ElvUI -Force + +# Install ElvUI fresh +Install-ElvUI + +# Target a specific WoW installation and game flavor +Update-ElvUI -WoWPath 'D:\Games\World of Warcraft' -Flavor '_classic_' From eb3e3e5f8332e5608e3fc44edd7a3fe5f257292c Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Apr 2026 22:41:54 +0200 Subject: [PATCH 08/14] Fix PSScriptAnalyzer linter errors: use named parameters, replace Write-Host with Write-Verbose, add ShouldProcess support --- .../private/Get-TukuiInstalledVersion.ps1 | 4 ++-- src/functions/private/Get-WoWAddOnsPath.ps1 | 2 +- src/functions/private/Install-TukuiAddon.ps1 | 22 +++++++++---------- src/functions/public/Install-ElvUI.ps1 | 8 ++++--- src/functions/public/Update-ElvUI.ps1 | 18 ++++++++------- 5 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/functions/private/Get-TukuiInstalledVersion.ps1 b/src/functions/private/Get-TukuiInstalledVersion.ps1 index 60ec421..37ec8a7 100644 --- a/src/functions/private/Get-TukuiInstalledVersion.ps1 +++ b/src/functions/private/Get-TukuiInstalledVersion.ps1 @@ -33,8 +33,8 @@ } $tocCandidates = @( - (Join-Path $AddOnsPath $addonFolder "${addonFolder}_Mainline.toc") - (Join-Path $AddOnsPath $addonFolder "$addonFolder.toc") + (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 $_ } | Select-Object -First 1 diff --git a/src/functions/private/Get-WoWAddOnsPath.ps1 b/src/functions/private/Get-WoWAddOnsPath.ps1 index 054acf8..36508fa 100644 --- a/src/functions/private/Get-WoWAddOnsPath.ps1 +++ b/src/functions/private/Get-WoWAddOnsPath.ps1 @@ -32,7 +32,7 @@ [string] $Flavor = '_retail_' ) - $addOnsPath = Join-Path $WoWPath $Flavor 'Interface' 'AddOns' + $addOnsPath = Join-Path -Path $WoWPath -ChildPath $Flavor | Join-Path -ChildPath 'Interface' | Join-Path -ChildPath 'AddOns' if (-not (Test-Path $addOnsPath)) { throw "AddOns folder not found: $addOnsPath" } diff --git a/src/functions/private/Install-TukuiAddon.ps1 b/src/functions/private/Install-TukuiAddon.ps1 index ca5c96d..f20b353 100644 --- a/src/functions/private/Install-TukuiAddon.ps1 +++ b/src/functions/private/Install-TukuiAddon.ps1 @@ -14,7 +14,7 @@ Downloads and installs ElvUI to the specified AddOns directory. #> - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess)] param( # The full path to the WoW AddOns directory. [Parameter(Mandatory)] @@ -25,7 +25,7 @@ [PSCustomObject] $Addon ) - $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "Tukui_$($Addon.Slug)_Update" + $tempDir = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath "Tukui_$($Addon.Slug)_Update" try { if (Test-Path $tempDir) { Remove-Item $tempDir -Recurse -Force @@ -33,20 +33,20 @@ New-Item -ItemType Directory -Path $tempDir | Out-Null # Download - $zipPath = Join-Path $tempDir "$($Addon.Slug)-$($Addon.Version).zip" - Write-Host "Downloading $($Addon.Name) $($Addon.Version) ..." -ForegroundColor Cyan + $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 -UseBasicParsing # Extract - $extractPath = Join-Path $tempDir 'extracted' - Write-Host 'Extracting...' -ForegroundColor Cyan + $extractPath = Join-Path -Path $tempDir -ChildPath 'extracted' + Write-Verbose 'Extracting...' Expand-Archive -Path $zipPath -DestinationPath $extractPath -Force # Remove old addon folders matching the known directory names foreach ($dir in $Addon.Directories) { - $oldPath = Join-Path $AddOnsPath $dir + $oldPath = Join-Path -Path $AddOnsPath -ChildPath $dir if (Test-Path $oldPath) { - Write-Host " Removing $dir" -ForegroundColor Yellow + Write-Verbose " Removing $dir" Remove-Item $oldPath -Recurse -Force } } @@ -54,12 +54,12 @@ # Copy new folders $extractedFolders = Get-ChildItem -Path $extractPath -Directory foreach ($folder in $extractedFolders) { - $destination = Join-Path $AddOnsPath $folder.Name - Write-Host " Installing $($folder.Name)" -ForegroundColor Cyan + $destination = Join-Path -Path $AddOnsPath -ChildPath $folder.Name + Write-Verbose " Installing $($folder.Name)" Copy-Item -Path $folder.FullName -Destination $destination -Recurse -Force } - Write-Host "$($Addon.Name) $($Addon.Version) installed successfully!" -ForegroundColor Green + Write-Verbose "$($Addon.Name) $($Addon.Version) installed successfully!" } finally { if (Test-Path $tempDir) { Remove-Item $tempDir -Recurse -Force diff --git a/src/functions/public/Install-ElvUI.ps1 b/src/functions/public/Install-ElvUI.ps1 index 01388be..9a77149 100644 --- a/src/functions/public/Install-ElvUI.ps1 +++ b/src/functions/public/Install-ElvUI.ps1 @@ -23,7 +23,7 @@ Installs ElvUI to the Classic WoW AddOns folder. #> - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess)] param( # Path to the World of Warcraft installation folder. [Parameter()] @@ -38,6 +38,8 @@ $addOnsPath = Get-WoWAddOnsPath -WoWPath $WoWPath -Flavor $Flavor $addon = Get-TukuiAddon -Name elvui - Write-Host "Installing $($addon.Name) $($addon.Version) ..." -ForegroundColor Cyan - Install-TukuiAddon -AddOnsPath $addOnsPath -Addon $addon + if ($PSCmdlet.ShouldProcess($addOnsPath, "Install $($addon.Name) $($addon.Version)")) { + Write-Verbose "Installing $($addon.Name) $($addon.Version) ..." + Install-TukuiAddon -AddOnsPath $addOnsPath -Addon $addon + } } diff --git a/src/functions/public/Update-ElvUI.ps1 b/src/functions/public/Update-ElvUI.ps1 index f7aa11e..73f7ee1 100644 --- a/src/functions/public/Update-ElvUI.ps1 +++ b/src/functions/public/Update-ElvUI.ps1 @@ -28,7 +28,7 @@ Reinstalls ElvUI even if the installed version matches the latest. #> - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess)] param( # Path to the World of Warcraft installation folder. [Parameter()] @@ -48,24 +48,26 @@ $installedVersion = Get-TukuiInstalledVersion -AddOnsPath $addOnsPath -Name elvui if ($installedVersion) { - Write-Host "Installed version: $installedVersion" -ForegroundColor Cyan + Write-Verbose "Installed version: $installedVersion" } else { - Write-Host 'No existing ElvUI installation detected. Installing fresh.' -ForegroundColor Yellow + Write-Verbose 'No existing ElvUI installation detected. Installing fresh.' } $addon = Get-TukuiAddon -Name elvui - Write-Host "Latest ElvUI version: $($addon.Version)" -ForegroundColor Green + Write-Verbose "Latest ElvUI version: $($addon.Version)" if ($installedVersion -eq $addon.Version -and -not $Force) { - Write-Host 'ElvUI is already up to date. Use -Force to reinstall.' -ForegroundColor Green + Write-Verbose 'ElvUI is already up to date. Use -Force to reinstall.' return } if ($installedVersion -eq $addon.Version) { - Write-Host "Forcing reinstall of $($addon.Version) ..." -ForegroundColor Yellow + Write-Verbose "Forcing reinstall of $($addon.Version) ..." } elseif ($installedVersion) { - Write-Host "Updating from $installedVersion to $($addon.Version) ..." -ForegroundColor Yellow + Write-Verbose "Updating from $installedVersion to $($addon.Version) ..." } - Install-TukuiAddon -AddOnsPath $addOnsPath -Addon $addon + if ($PSCmdlet.ShouldProcess($addOnsPath, "Install $($addon.Name) $($addon.Version)")) { + Install-TukuiAddon -AddOnsPath $addOnsPath -Addon $addon + } } From e7d912afdc6455802421b9c77074da0003e46421 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Apr 2026 22:49:28 +0200 Subject: [PATCH 09/14] Fix CI: replace Out-Null with $null assignment, add per-function test files, fix zip terminology for textlint --- src/functions/private/Install-TukuiAddon.ps1 | 4 ++-- src/functions/public/Install-ElvUI.ps1 | 2 +- tests/Install-ElvUI.Tests.ps1 | 22 +++++++++++++++++ tests/Update-ElvUI.Tests.ps1 | 25 ++++++++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 tests/Install-ElvUI.Tests.ps1 create mode 100644 tests/Update-ElvUI.Tests.ps1 diff --git a/src/functions/private/Install-TukuiAddon.ps1 b/src/functions/private/Install-TukuiAddon.ps1 index f20b353..ef0cb19 100644 --- a/src/functions/private/Install-TukuiAddon.ps1 +++ b/src/functions/private/Install-TukuiAddon.ps1 @@ -4,7 +4,7 @@ Downloads and installs a Tukui addon to the WoW AddOns folder. .DESCRIPTION - Downloads the zip from the Tukui API, extracts it, removes old addon + 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. @@ -30,7 +30,7 @@ if (Test-Path $tempDir) { Remove-Item $tempDir -Recurse -Force } - New-Item -ItemType Directory -Path $tempDir | Out-Null + $null = New-Item -ItemType Directory -Path $tempDir # Download $zipPath = Join-Path -Path $tempDir -ChildPath "$($Addon.Slug)-$($Addon.Version).zip" diff --git a/src/functions/public/Install-ElvUI.ps1 b/src/functions/public/Install-ElvUI.ps1 index 9a77149..6dac536 100644 --- a/src/functions/public/Install-ElvUI.ps1 +++ b/src/functions/public/Install-ElvUI.ps1 @@ -4,7 +4,7 @@ Downloads and installs ElvUI to the WoW AddOns folder. .DESCRIPTION - Fetches the latest ElvUI release from the Tukui API, downloads the zip archive, + 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. diff --git a/tests/Install-ElvUI.Tests.ps1 b/tests/Install-ElvUI.Tests.ps1 new file mode 100644 index 0000000..6513506 --- /dev/null +++ b/tests/Install-ElvUI.Tests.ps1 @@ -0,0 +1,22 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester tests' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester tests' +)] +[CmdletBinding()] +param() + +Describe 'Install-ElvUI' { + It 'Should be available' { + Get-Command Install-ElvUI -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty + } + It 'Should have WoWPath parameter' { + (Get-Command Install-ElvUI).Parameters.ContainsKey('WoWPath') | Should -BeTrue + } + It 'Should have Flavor parameter' { + (Get-Command Install-ElvUI).Parameters.ContainsKey('Flavor') | Should -BeTrue + } +} diff --git a/tests/Update-ElvUI.Tests.ps1 b/tests/Update-ElvUI.Tests.ps1 new file mode 100644 index 0000000..c41849e --- /dev/null +++ b/tests/Update-ElvUI.Tests.ps1 @@ -0,0 +1,25 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Required for Pester tests' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Required for Pester tests' +)] +[CmdletBinding()] +param() + +Describe 'Update-ElvUI' { + It 'Should be available' { + Get-Command Update-ElvUI -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty + } + It 'Should have WoWPath parameter' { + (Get-Command Update-ElvUI).Parameters.ContainsKey('WoWPath') | Should -BeTrue + } + It 'Should have Flavor parameter' { + (Get-Command Update-ElvUI).Parameters.ContainsKey('Flavor') | Should -BeTrue + } + It 'Should have Force parameter' { + (Get-Command Update-ElvUI).Parameters.ContainsKey('Force') | Should -BeTrue + } +} From 8b3b15ba3c58f9e031d89494491784e8f2668576 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 19 Apr 2026 23:41:46 +0200 Subject: [PATCH 10/14] Address Copilot review: remove -UseBasicParsing, add GUID to temp dir, remove SupportsShouldProcess from private helper, add function invocation tests --- src/functions/private/Get-TukuiAddon.ps1 | 2 +- src/functions/private/Install-TukuiAddon.ps1 | 6 +++--- tests/Install-ElvUI.Tests.ps1 | 3 +++ tests/Update-ElvUI.Tests.ps1 | 3 +++ 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/functions/private/Get-TukuiAddon.ps1 b/src/functions/private/Get-TukuiAddon.ps1 index a685fd1..1423ee9 100644 --- a/src/functions/private/Get-TukuiAddon.ps1 +++ b/src/functions/private/Get-TukuiAddon.ps1 @@ -35,7 +35,7 @@ $url = 'https://api.tukui.org/v1/addons' } - $response = Invoke-RestMethod -Uri $url -UseBasicParsing + $response = Invoke-RestMethod -Uri $url foreach ($addon in @($response)) { [PSCustomObject]@{ diff --git a/src/functions/private/Install-TukuiAddon.ps1 b/src/functions/private/Install-TukuiAddon.ps1 index ef0cb19..c331fe1 100644 --- a/src/functions/private/Install-TukuiAddon.ps1 +++ b/src/functions/private/Install-TukuiAddon.ps1 @@ -14,7 +14,7 @@ Downloads and installs ElvUI to the specified AddOns directory. #> - [CmdletBinding(SupportsShouldProcess)] + [CmdletBinding()] param( # The full path to the WoW AddOns directory. [Parameter(Mandatory)] @@ -25,7 +25,7 @@ [PSCustomObject] $Addon ) - $tempDir = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath "Tukui_$($Addon.Slug)_Update" + $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 @@ -35,7 +35,7 @@ # 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 -UseBasicParsing + Invoke-WebRequest -Uri $Addon.DownloadUrl -OutFile $zipPath # Extract $extractPath = Join-Path -Path $tempDir -ChildPath 'extracted' diff --git a/tests/Install-ElvUI.Tests.ps1 b/tests/Install-ElvUI.Tests.ps1 index 6513506..79458f1 100644 --- a/tests/Install-ElvUI.Tests.ps1 +++ b/tests/Install-ElvUI.Tests.ps1 @@ -19,4 +19,7 @@ Describe 'Install-ElvUI' { It 'Should have Flavor parameter' { (Get-Command Install-ElvUI).Parameters.ContainsKey('Flavor') | Should -BeTrue } + It 'Should throw when WoW path does not exist' { + { Install-ElvUI -WoWPath 'TestDrive:\NonExistent' } | Should -Throw + } } diff --git a/tests/Update-ElvUI.Tests.ps1 b/tests/Update-ElvUI.Tests.ps1 index c41849e..8c9b00b 100644 --- a/tests/Update-ElvUI.Tests.ps1 +++ b/tests/Update-ElvUI.Tests.ps1 @@ -22,4 +22,7 @@ Describe 'Update-ElvUI' { It 'Should have Force parameter' { (Get-Command Update-ElvUI).Parameters.ContainsKey('Force') | Should -BeTrue } + It 'Should throw when WoW path does not exist' { + { Update-ElvUI -WoWPath 'TestDrive:\NonExistent' } | Should -Throw + } } From 988d8d0c9c9ec90b8490cf53a28c0b290c2af5b7 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 20 Apr 2026 08:03:15 +0200 Subject: [PATCH 11/14] Fix version downgrade risk in Update-ElvUI; add path traversal guard and ErrorAction Stop in Install-TukuiAddon --- src/functions/private/Install-TukuiAddon.ps1 | 27 ++++++++++++++------ src/functions/public/Update-ElvUI.ps1 | 14 ++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/functions/private/Install-TukuiAddon.ps1 b/src/functions/private/Install-TukuiAddon.ps1 index c331fe1..7dd6ea9 100644 --- a/src/functions/private/Install-TukuiAddon.ps1 +++ b/src/functions/private/Install-TukuiAddon.ps1 @@ -28,35 +28,46 @@ $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 + Remove-Item $tempDir -Recurse -Force -ErrorAction Stop } - $null = New-Item -ItemType Directory -Path $tempDir + $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 + 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 + Expand-Archive -Path $zipPath -DestinationPath $extractPath -Force -ErrorAction Stop # Remove old addon folders matching the known directory names + $normalizedAddOnsPath = [System.IO.Path]::GetFullPath($AddOnsPath) + $normalizedAddOnsPath = $normalizedAddOnsPath.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) + [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 - if (Test-Path $oldPath) { + $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 $oldPath -Recurse -Force + Remove-Item -LiteralPath $resolvedOldPath -Recurse -Force -ErrorAction Stop } } # Copy new folders - $extractedFolders = Get-ChildItem -Path $extractPath -Directory + $extractedFolders = Get-ChildItem -Path $extractPath -Directory -ErrorAction Stop foreach ($folder in $extractedFolders) { $destination = Join-Path -Path $AddOnsPath -ChildPath $folder.Name Write-Verbose " Installing $($folder.Name)" - Copy-Item -Path $folder.FullName -Destination $destination -Recurse -Force + Copy-Item -Path $folder.FullName -Destination $destination -Recurse -Force -ErrorAction Stop } Write-Verbose "$($Addon.Name) $($Addon.Version) installed successfully!" diff --git a/src/functions/public/Update-ElvUI.ps1 b/src/functions/public/Update-ElvUI.ps1 index 73f7ee1..0fa5bf8 100644 --- a/src/functions/public/Update-ElvUI.ps1 +++ b/src/functions/public/Update-ElvUI.ps1 @@ -56,6 +56,18 @@ $addon = Get-TukuiAddon -Name elvui Write-Verbose "Latest ElvUI version: $($addon.Version)" + # Compare versions to prevent unintentional downgrades + $installedVer = $null + $latestVer = $null + $canCompareVersions = $installedVersion -and + [version]::TryParse($installedVersion, [ref]$installedVer) -and + [version]::TryParse($addon.Version, [ref]$latestVer) + + if ($canCompareVersions -and $installedVer -gt $latestVer -and -not $Force) { + Write-Verbose "Installed version ($installedVersion) is newer than the latest available ($($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 @@ -63,6 +75,8 @@ if ($installedVersion -eq $addon.Version) { Write-Verbose "Forcing reinstall of $($addon.Version) ..." + } elseif ($canCompareVersions -and $installedVer -gt $latestVer) { + Write-Verbose "Forcing reinstall — installed version ($installedVersion) is newer than latest available ($($addon.Version))." } elseif ($installedVersion) { Write-Verbose "Updating from $installedVersion to $($addon.Version) ..." } From 5057fe646433983e9e0f983f956abb62fccf880f Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 20 Apr 2026 08:37:17 +0200 Subject: [PATCH 12/14] Address review: use -LiteralPath for path operations, require -Force for non-comparable versions, fix lint violations - Get-WoWAddOnsPath: use Test-Path -LiteralPath to handle wildcard chars in user paths - Get-TukuiInstalledVersion: use Test-Path -LiteralPath and Get-Content -LiteralPath -ErrorAction Stop - Update-ElvUI: require -Force when version strings can't be parsed (prevents accidental downgrades from beta/dev builds) - Install-TukuiAddon: extract TrimEnd char array to fix PSAvoidLongLines lint violation - Update-ElvUI: fix PSUseConsistentIndentation lint violations on version parse lines --- .../private/Get-TukuiInstalledVersion.ps1 | 4 ++-- src/functions/private/Get-WoWAddOnsPath.ps1 | 2 +- src/functions/private/Install-TukuiAddon.ps1 | 3 ++- src/functions/public/Update-ElvUI.ps1 | 19 ++++++++++++++----- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/functions/private/Get-TukuiInstalledVersion.ps1 b/src/functions/private/Get-TukuiInstalledVersion.ps1 index 37ec8a7..271e78f 100644 --- a/src/functions/private/Get-TukuiInstalledVersion.ps1 +++ b/src/functions/private/Get-TukuiInstalledVersion.ps1 @@ -37,12 +37,12 @@ (Join-Path -Path $AddOnsPath -ChildPath $addonFolder | Join-Path -ChildPath "$addonFolder.toc") ) - $tocPath = $tocCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1 + $tocPath = $tocCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 if (-not $tocPath) { return $null } - $tocContent = Get-Content -Path $tocPath -Raw + $tocContent = Get-Content -LiteralPath $tocPath -Raw -ErrorAction Stop if ($tocContent -match '(?m)^## Version:\s*(.+)$') { return $Matches[1].Trim().TrimStart('v') } diff --git a/src/functions/private/Get-WoWAddOnsPath.ps1 b/src/functions/private/Get-WoWAddOnsPath.ps1 index 36508fa..1064949 100644 --- a/src/functions/private/Get-WoWAddOnsPath.ps1 +++ b/src/functions/private/Get-WoWAddOnsPath.ps1 @@ -33,7 +33,7 @@ ) $addOnsPath = Join-Path -Path $WoWPath -ChildPath $Flavor | Join-Path -ChildPath 'Interface' | Join-Path -ChildPath 'AddOns' - if (-not (Test-Path $addOnsPath)) { + if (-not (Test-Path -LiteralPath $addOnsPath)) { throw "AddOns folder not found: $addOnsPath" } $addOnsPath diff --git a/src/functions/private/Install-TukuiAddon.ps1 b/src/functions/private/Install-TukuiAddon.ps1 index 7dd6ea9..83dbb4c 100644 --- a/src/functions/private/Install-TukuiAddon.ps1 +++ b/src/functions/private/Install-TukuiAddon.ps1 @@ -44,7 +44,8 @@ # Remove old addon folders matching the known directory names $normalizedAddOnsPath = [System.IO.Path]::GetFullPath($AddOnsPath) - $normalizedAddOnsPath = $normalizedAddOnsPath.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) + [System.IO.Path]::DirectorySeparatorChar + $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." diff --git a/src/functions/public/Update-ElvUI.ps1 b/src/functions/public/Update-ElvUI.ps1 index 0fa5bf8..d88fe76 100644 --- a/src/functions/public/Update-ElvUI.ps1 +++ b/src/functions/public/Update-ElvUI.ps1 @@ -59,12 +59,12 @@ # Compare versions to prevent unintentional downgrades $installedVer = $null $latestVer = $null - $canCompareVersions = $installedVersion -and - [version]::TryParse($installedVersion, [ref]$installedVer) -and - [version]::TryParse($addon.Version, [ref]$latestVer) + $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 the latest available ($($addon.Version)). Use -Force to reinstall." + Write-Verbose "Installed version ($installedVersion) is newer than latest ($($addon.Version)). Use -Force to reinstall." return } @@ -73,10 +73,19 @@ 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 version ($installedVersion) is newer than latest available ($($addon.Version))." + 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) ..." } From 8338956e260a68fabe1dc7b8cd972621a3c7cdaa Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 20 Apr 2026 14:17:29 +0200 Subject: [PATCH 13/14] Use -LiteralPath consistently in Install-TukuiAddon; add PS 5.1 install instructions to README - Switch Get-ChildItem and Copy-Item in extraction section to -LiteralPath for consistency with the function's path safety checks (review thread #13) - Use -LiteralPath and -ErrorAction SilentlyContinue in finally cleanup to prevent masking original errors (review thread #14) - Add Windows PowerShell 5.1 Install-Module alternative alongside Install-PSResource in README (review thread #15) --- README.md | 5 +++++ src/functions/private/Install-TukuiAddon.ps1 | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2a0b90c..98104be 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,13 @@ for [World of Warcraft](https://worldofwarcraft.blizzard.com/). To install the module from the PowerShell Gallery: ```powershell +# PowerShell 7.4+ (Install-PSResource is built-in) Install-PSResource -Name ElvUI Import-Module -Name ElvUI + +# Windows PowerShell 5.1 (use Install-Module instead) +Install-Module -Name ElvUI -Scope CurrentUser +Import-Module -Name ElvUI ``` ## Usage diff --git a/src/functions/private/Install-TukuiAddon.ps1 b/src/functions/private/Install-TukuiAddon.ps1 index 83dbb4c..0148d36 100644 --- a/src/functions/private/Install-TukuiAddon.ps1 +++ b/src/functions/private/Install-TukuiAddon.ps1 @@ -64,17 +64,17 @@ } # Copy new folders - $extractedFolders = Get-ChildItem -Path $extractPath -Directory -ErrorAction Stop + $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 -Path $folder.FullName -Destination $destination -Recurse -Force -ErrorAction Stop + Copy-Item -LiteralPath $folder.FullName -Destination $destination -Recurse -Force -ErrorAction Stop } Write-Verbose "$($Addon.Name) $($Addon.Version) installed successfully!" } finally { - if (Test-Path $tempDir) { - Remove-Item $tempDir -Recurse -Force + if (Test-Path -LiteralPath $tempDir) { + Remove-Item -LiteralPath $tempDir -Recurse -Force -ErrorAction SilentlyContinue } } } From c5dd49a8966749825178a501913046779865dd06 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 20 Apr 2026 14:36:51 +0200 Subject: [PATCH 14/14] Remove obsolete files and configurations from the repository --- src/README.md | 3 --- src/assemblies/LsonLib.dll | Bin 43520 -> 0 bytes src/data/Config.psd1 | 2 -- src/data/Settings.psd1 | 2 -- src/header.ps1 | 3 --- src/manifest.psd1 | 5 ----- 6 files changed, 15 deletions(-) delete mode 100644 src/README.md delete mode 100644 src/assemblies/LsonLib.dll delete mode 100644 src/data/Config.psd1 delete mode 100644 src/data/Settings.psd1 delete mode 100644 src/header.ps1 delete mode 100644 src/manifest.psd1 diff --git a/src/README.md b/src/README.md deleted file mode 100644 index af76160..0000000 --- a/src/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Details - -For more info about the expected structure of a module repository, please refer to [Build-PSModule](https://github.com/PSModule/Build-PSModule) diff --git a/src/assemblies/LsonLib.dll b/src/assemblies/LsonLib.dll deleted file mode 100644 index 36618070d5c9f5131ec66720aa0565c13e86d23f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43520 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