Initialize-Module.ps1

<#PSScriptInfo
 
.VERSION 1.5
 
.GUID 0e34e80c-49a0-4579-8113-fd793a44d92c
 
.AUTHOR Jonathan Pitre
 
.TAGS PowerShell Module Automation
 
.RELEASENOTES
 
1.5 - 2025-01-27
- Added -AllowPrerelease switch parameter to enable installation of prerelease module versions.
- Enhanced Find-PSResource, Install-PSResource, and Update-PSResource calls to support prerelease versions when specified.
- Added verbose logging to indicate when prerelease versions are being searched for or installed.
 
1.4 - 2025-01-27
- Fixed version comparison error when Find-PSResource returns multiple module versions as an array.
- Added proper array handling to select the latest version when multiple versions are available.
- Improved error handling for cases where modules are not found in PSGallery repository.
 
1.3 - 2025-06-20
- Replaced Install-Module, Update-Module, and Get-Module commands with Install-PSResource, Update-PSResource, and Get-InstalledPSResource commands for improved performance and modern PowerShell Gallery interaction.
- Enhanced module management efficiency by leveraging the Microsoft.PowerShell.PSResourceGet module's optimized cmdlets.
- Fixed an error with the transcript file not being created on a fresh reboot.
 
1.2 - 2025-06-17
- Added error action silently continue to the Import-Module commands for Microsoft.PowerShell.PSResourceGet to avoid errors when the module is not installed.
 
1.1 - 2025-06-16
Added log file and transcript.
Added log path and file parameters.
 
1.0 - 2025-05-30
Initial release.
 
#>


<#
.SYNOPSIS
    Installs (or updates) and imports PowerShell modules from the PowerShell Gallery.
 
.DESCRIPTION
    This function ensures that specified PowerShell modules are installed, up-to-date, and imported for use.
    It handles necessary prerequisites for PowerShell Gallery interaction, including:
    - Setting the security protocol to TLS 1.2.
    - Ensuring the NuGet package provider is available.
    - Installing/updating and importing the 'Microsoft.PowerShell.PSResourceGet' module for modern gallery operations.
 
    For each module specified by the -Name parameter, the function will:
    1. Attempt to install or update it to the latest version from the PowerShell Gallery. Modules are typically installed for AllUsers.
    2. Import the module into the current session.
 
    This automates the common tasks required to make PowerShell modules ready for use.
 
.PARAMETER Name
    The name of the module or an array of module names to initialize.
    These modules will be installed/updated from the PowerShell Gallery and then imported. This parameter is mandatory.
 
.PARAMETER LogPath
    The path where the log file will be created. Defaults to "$env:ProgramData\Microsoft\IntuneManagementExtension\Logs".
 
.PARAMETER LogFile
    The name of the log file. Defaults to 'Initialize-Module.log'.
 
.PARAMETER AllowPrerelease
    Switch parameter to allow installation of prerelease module versions.
    When specified, the script will check for and install prerelease versions if they are newer than the current stable version.
 
 
.EXAMPLE
    Initialize-Module -Name 'Pester'
    # Ensures the Pester module is installed (or updated to the latest stable version) and imported.
 
.EXAMPLE
    Initialize-Module -Name 'Module1', 'Module2'
    # Initializes both 'Module1' and 'Module2', ensuring they are installed/updated and imported.
 
.EXAMPLE
    Initialize-Module -Name 'PSReadLine' -LogPath 'C:\Logs' -LogFile 'ModuleInit.log'
    # Initializes the PSReadLine module with custom logging location.
 
.EXAMPLE
    Initialize-Module -Name 'Pester' -AllowPrerelease
    # Initializes the Pester module, allowing installation of prerelease versions if they are newer.
 
.NOTES
    - Requires an active internet connection to access the PowerShell Gallery.
    - Administrative privileges are generally required to install modules for 'AllUsers' and to install package providers or the 'Microsoft.PowerShell.PSResourceGet' module itself.
    - The function relies on 'Microsoft.PowerShell.PSResourceGet' for robust interaction with the PowerShell Gallery.
    - All operations are logged to the specified log file for troubleshooting purposes.
#>


[CmdletBinding()]
param (
    [Parameter(Mandatory = $true, Position = 0)]
    [ValidateNotNullOrEmpty()]
    [System.String[]]$Name,

    [Parameter(Mandatory = $false)]
    [string]$LogPath = "$env:ProgramData\Microsoft\IntuneManagementExtension\Logs",

    [Parameter(Mandatory = $false)]
    [string]$LogFile = 'Initialize-Module.log',

    [Parameter(Mandatory = $false)]
    [switch]$AllowPrerelease
)

begin {
    $ProgressPreference = 'SilentlyContinue'

    # Create log directory if it doesn't exist
    if (-not (Test-Path -Path $LogPath)) {
        New-Item -Path $LogPath -ItemType Directory -Force | Out-Null
    }

    # Start transcript
    Start-Transcript -Path (Join-Path -Path $LogPath -ChildPath $LogFile) -Force -Append -Verbose

    Write-Host 'Starting module initialization process...' -ForegroundColor Cyan

    # Ensure TLS 1.2 is enabled for compatibility with newer repositories
    if (-not ([Net.ServicePointManager]::SecurityProtocol.HasFlag([Net.SecurityProtocolType]::Tls12))) {
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        Write-Verbose 'Set security protocol to TLS 1.2.'
    } else {
        Write-Verbose 'TLS 1.2 is already enabled in the current security protocol settings.'
    }

    # Try to import the built-in PackageManagement module first
    try {
        Import-Module -Name PackageManagement -Force -Scope Global -ErrorAction Stop
        Write-Verbose 'Successfully imported the PackageManagement module.'
    } catch {
        Write-Warning 'Failed to import the PackageManagement module directly. Proceeding to ensure NuGet provider and Microsoft.PowerShell.PSResourceGet module are available.'
    }

    # Ensure NuGet package provider is installed.
    # This is a prerequisite for interacting with PSGallery, especially for Install-PSResource.
    if (-not (Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue)) {
        Write-Verbose 'NuGet package provider not found. Attempting to install.'
        try {
            $null = Install-PackageProvider -Name NuGet -Force -Scope AllUsers -ErrorAction Stop
            Write-Verbose 'Successfully installed the NuGet package provider.'
        } catch {
            Write-Warning "Failed to install the NuGet package provider. Error details: $($_.Exception.Message)."
        }
    } else {
        Write-Verbose 'NuGet package provider is already available.'
    }

    # Add the PowerShell Gallery as trusted repository
    try {
        # Check if PSGallery is registered
        $PSGallery = Get-PSRepository -Name 'PSGallery' -ErrorAction SilentlyContinue

        if ($PSGallery) {
            Write-Verbose 'The PSGallery repository is already registered.'

            # Check if PSGallery is trusted
            if ($PSGallery.InstallationPolicy -ne 'Trusted') {
                Write-Verbose 'Setting the PSGallery repository installation policy to Trusted.'
                Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted -ErrorAction Stop
                Write-Verbose 'Successfully set the PSGallery repository installation policy to Trusted.'
            } else {
                Write-Verbose 'The PSGallery repository installation policy is already Trusted.'
            }
        } else {
            Write-Verbose 'PSGallery repository not found. Attempting to register and set it to Trusted.'
            Register-PSRepository -Default -ErrorAction Stop
            Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted -ErrorAction Stop
            Write-Verbose 'Successfully registered the PSGallery repository and set its installation policy to Trusted.'
        }
    } catch {
        Write-Error "Failed to configure the PSGallery repository (register or set trust). Error details: $($_.Exception.Message)"
    }

    # Attempt to import Microsoft.PowerShell.PSResourceGet, which is the newer module for PSGallery.
    $PSResourceGetModule = 'Microsoft.PowerShell.PSResourceGet'
    $PSResourceGet = Get-Module -Name $PSResourceGetModule -ListAvailable -ErrorAction SilentlyContinue
    if ([bool]($PSResourceGet)) {
        Write-Verbose "$PSResourceGetModule module is available locally."
        # Get the current version of the $PSResourceGetModule module
        $localPSResourceGet = Get-Module -Name $PSResourceGetModule -ListAvailable -ErrorAction SilentlyContinue
        if ($localPSResourceGet) {
            # Handle case where multiple versions might be returned
            if ($localPSResourceGet -is [array]) {
                $currentVersion = ($localPSResourceGet | Sort-Object -Property Version -Descending | Select-Object -First 1).Version
            } else {
                $currentVersion = $localPSResourceGet.Version
            }
        }
        # Update $PSResourceGetModule module if needed
        if ([bool]($currentVersion)) {
            Write-Verbose "Currently installed $PSResourceGetModule version: $currentVersion."
            try {
                # Build Find-PSResource parameters based on prerelease preference
                $findParams = @{
                    Name = $PSResourceGetModule
                    Repository = 'PSGallery'
                    ErrorAction = 'SilentlyContinue'
                }

                if ($AllowPrerelease) {
                    $findParams['Prerelease'] = $true
                    Write-Verbose "Searching for $PSResourceGetModule including prerelease versions."
                } else {
                    Write-Verbose "Searching for $PSResourceGetModule stable versions only."
                }

                $onlineModule = Find-PSResource @findParams
                if ($onlineModule) {
                    # Handle case where multiple versions might be returned
                    if ($onlineModule -is [array]) {
                        $latestVersion = ($onlineModule | Sort-Object -Property Version -Descending | Select-Object -First 1).Version
                    } else {
                        $latestVersion = $onlineModule.Version
                    }
                    Write-Verbose "Latest available $PSResourceGetModule version: $latestVersion."
                    Write-Verbose "Checking for updates to $PSResourceGetModule module online..."
                    if (([version]$latestVersion -gt [version]$currentVersion)) {
                        Write-Verbose "Attempting to update $PSResourceGetModule from $currentVersion to $latestVersion."

                        # Build Update-PSResource parameters based on prerelease preference
                        $updateParams = @{
                            Name = $PSResourceGetModule
                            Scope = 'AllUsers'
                            Repository = 'PSGallery'
                            TrustRepository = $true
                            Quiet = $true
                            AcceptLicense = $true
                            Force = $true
                            ErrorAction = 'SilentlyContinue'
                        }

                        if ($AllowPrerelease) {
                            $updateParams['Prerelease'] = $true
                        }

                        Update-PSResource @updateParams
                        Write-Verbose "Successfully updated the $PSResourceGetModule module."
                    } else {
                        Write-Verbose "$PSResourceGetModule module is up to date."
                    }
                } else {
                    Write-Warning "Unable to find $PSResourceGetModule module in PSGallery repository."
                }
            } catch {
                Write-Warning "Unable to check for or apply updates to $PSResourceGetModule module. Error details: $($_.Exception.Message). The currently installed version will be used."
            }
        }
        try {
            Import-Module -Name $PSResourceGetModule -Force -Scope Global -ErrorAction Stop
            Write-Verbose "Successfully imported the $PSResourceGetModule module."
        } catch {
            Write-Error "Failed to import the $PSResourceGetModule module. Error details: $($_.Exception.Message)."
        }
    } else {
        try {
            Write-Warning "The $PSResourceGetModule module is not installed. Attempting to install it..."
            Install-Module -Name $PSResourceGetModule -Repository PSGallery -Force -Scope AllUsers -AllowClobber -ErrorAction Stop
            Import-Module -Name $PSResourceGetModule -Force -Scope Global -ErrorAction Stop
            Write-Verbose "Successfully installed and imported the $PSResourceGetModule module for AllUsers."
        } catch {
            Write-Warning "Failed to install and import the $PSResourceGetModule module. Error details: $($_.Exception.Message)."
        }
    }
}

process {
    foreach ($Module in $Name) {
        Write-Verbose "Processing module: $Module."
        try {
            # Check if module is already imported and available locally in one operation
            $localModule = Get-Module -Name $Module -ListAvailable -ErrorAction SilentlyContinue

            if ([bool]($localModule)) {
                Write-Verbose "Module $Module is already installed and imported."
                $currentVersion = $localModule | Sort-Object -Property Version -Descending | Select-Object -First 1 -ExpandProperty Version
                $modulePath = ($localModule).ModuleBase
                Write-Verbose "Module $Module version $currentVersion found locally at '$modulePath'."

                # Module is available locally, check for updates
                try {
                    Write-Verbose "Checking for updates online for module $Module."

                    # Build Find-PSResource parameters based on prerelease preference
                    $findParams = @{
                        Name = $Module
                        Repository = 'PSGallery'
                        ErrorAction = 'Stop'
                    }

                    if ($AllowPrerelease) {
                        $findParams['Prerelease'] = $true
                        Write-Verbose "Searching for $Module including prerelease versions."
                    } else {
                        Write-Verbose "Searching for $Module stable versions only."
                    }

                    $onlineModule = Find-PSResource @findParams
                    if ($onlineModule) {
                        # Handle case where multiple versions might be returned
                        if ($onlineModule -is [array]) {
                            $onlineVersion = ($onlineModule | Sort-Object -Property Version -Descending | Select-Object -First 1).Version
                        } else {
                            $onlineVersion = $onlineModule.Version
                        }
                        Write-Verbose "Latest available version for $Module is $onlineVersion."

                        # Update if online version is newer
                        if ([version]$onlineVersion -gt [version]$currentVersion) {
                            Write-Verbose "Attempting to update module $Module from version $currentVersion to $onlineVersion..."
                            try {
                                # Build Update-PSResource parameters based on prerelease preference
                                $updateParams = @{
                                    Name = $Module
                                    Scope = 'AllUsers'
                                    Repository = 'PSGallery'
                                    TrustRepository = $true
                                    Quiet = $true
                                    AcceptLicense = $true
                                    Force = $true
                                    ErrorAction = 'SilentlyContinue'
                                }

                                if ($AllowPrerelease) {
                                    $updateParams['Prerelease'] = $true
                                }

                                Update-PSResource @updateParams
                                if (Test-Path -Path $modulePath) {
                                    # Check if path still exists before attempting removal
                                    Write-Verbose "Attempting to remove old module files from '$modulePath' after update."
                                    Remove-Item -Path $modulePath -Force -Recurse -ErrorAction SilentlyContinue
                                }
                                Write-Host "Successfully updated module $Module to version $($onlineVersion)." -ForegroundColor Green
                            } catch {
                                Write-Warning "An error occurred during the update process for module $Module (Update-PSResource or manual folder removal at '$modulePath'). Attempting a full reinstall. Error details: $($_.Exception.Message)"
                                Uninstall-PSResource -Name $Module -Scope AllUsers -ErrorAction SilentlyContinue # Best effort uninstall
                                Write-Verbose "Attempting to install module $Module after failed update."

                                # Build Install-PSResource parameters based on prerelease preference
                                $reinstallParams = @{
                                    Name = $Module
                                    Scope = 'AllUsers'
                                    Repository = 'PSGallery'
                                    TrustRepository = $true
                                    Quiet = $true
                                    AcceptLicense = $true
                                    ErrorAction = 'Stop'
                                }

                                if ($AllowPrerelease) {
                                    $reinstallParams['Prerelease'] = $true
                                }

                                Install-PSResource @reinstallParams
                                Write-Host "Successfully reinstalled module $Module (version $onlineVersion)." -ForegroundColor Green
                            }
                        } else {
                            Write-Verbose "Module $Module (version $currentVersion) is already up to date."
                        }
                    } else {
                        Write-Warning "Unable to find module $Module in PSGallery repository."
                    }
                } catch {
                    Write-Warning "Unable to check for updates online for module $Module. Error details: $($_.Exception.Message). The locally installed version $currentVersion will be used."
                }

                # Import the module
                try {
                    Write-Verbose "Attempting to import module $Module."
                    Import-Module -Name $Module -Force -Global -DisableNameChecking -ErrorAction Stop
                    Write-Host "Successfully imported module $Module (version $currentVersion)." -ForegroundColor Green
                } catch {
                    Write-Error "Failed to import module $Module. Error details: $($_.Exception.Message)"
                }
            } else {
                # Module is not available locally, try to install from gallery
                Write-Verbose "Module $Module not found locally."
                try {
                    Write-Verbose "Searching for module $Module in the PSGallery repository."

                    # Build Find-PSResource parameters based on prerelease preference
                    $findParams = @{
                        Name = $Module
                        ErrorAction = 'Stop'
                    }

                    if ($AllowPrerelease) {
                        $findParams['Prerelease'] = $true
                        Write-Verbose "Searching for $Module including prerelease versions."
                    } else {
                        Write-Verbose "Searching for $Module stable versions only."
                    }

                    $onlineModule = Find-PSResource @findParams # Ensure it exists before install
                    if ($onlineModule) {
                        # Handle case where multiple versions might be returned
                        if ($onlineModule -is [array]) {
                            $onlineVersion = ($onlineModule | Sort-Object -Property Version -Descending | Select-Object -First 1).Version
                        } else {
                            $onlineVersion = $onlineModule.Version
                        }
                        Write-Verbose "Module $Module version $onlineVersion found in PSGallery. Attempting to install..."

                        # Build Install-PSResource parameters based on prerelease preference
                        $installParams = @{
                            Name = $Module
                            Scope = 'AllUsers'
                            Repository = 'PSGallery'
                            TrustRepository = $true
                            Quiet = $true
                            AcceptLicense = $true
                            ErrorAction = 'Stop'
                        }

                        if ($AllowPrerelease) {
                            $installParams['Prerelease'] = $true
                        }

                        Install-PSResource @installParams
                        Write-Verbose "Attempting to import module $Module after installation."
                        Import-Module -Name $Module -Force -Global -DisableNameChecking -ErrorAction Stop
                        Write-Host "Successfully installed and imported module $Module (version $onlineVersion)." -ForegroundColor Green
                    } else {
                        Write-Error "Module $Module not found in PSGallery repository."
                        throw "Module $Module is not installed locally and could not be found in the PSGallery repository. Initialization cannot proceed for this module."
                    }
                } catch {
                    Write-Error "Failed to find or install module $Module from the PSGallery repository. Error details: $($_.Exception.Message)"
                    throw "Module $Module is not installed locally and could not be found in or installed from the PSGallery repository. Initialization cannot proceed for this module."
                }
            }
        } catch {
            Write-Error "Failed to initialize module $Module. Error details: $($_.Exception.Message)"
            # This error means the current module in the loop failed. The loop will continue to the next module.
        }
    }
}

end {
    # Stop transcript
    Stop-Transcript
}