Public/Functions/Copy-PSModule.ps1

function Copy-PSModuleToFolder {
    <#
    .SYNOPSIS
    Copies PowerShell modules to a destination module path.
 
    .DESCRIPTION
    Finds the latest installed version of each requested module and copies it to
    the destination using the standard module\version folder layout.
 
    .PARAMETER Name
    One or more module names to copy.
 
    .PARAMETER Destination
    Destination root folder for copied modules.
 
    .PARAMETER RemoveOldVersions
    Removes existing module content from the destination before copying.
 
    .EXAMPLE
    Copy-PSModuleToFolder -Name OSD -Destination 'C:\Modules'
    Copies the latest installed OSD module to C:\Modules\OSD\<version>.
 
    .EXAMPLE
    Copy-PSModuleToFolder -Name OSD,PackageManagement -Destination 'C:\Modules' -RemoveOldVersions
    Removes existing destination module content and copies fresh module versions.
 
    .LINK
    https://github.com/OSDeploy/OSD/tree/master/docs
 
    .NOTES
    Author: David Segura - Recast Software
    2026-07-11 - Added comment-based help
    #>

    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [SupportsWildcards()]
        [String[]]$Name,

        [Parameter(Position = 1, Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Folder')]
        [String]$Destination,

        [System.Management.Automation.SwitchParameter]$RemoveOldVersions
    )

    begin {
        Write-Verbose "Destination: $Destination"
    }
    process {
        foreach ($Item in $Name) {

            #GetModule
            $GetModule = @()
            $GetModule = Get-Module -ListAvailable -Name $Item | Select-Object Name, Version, ModuleBase
            $GetModule = $GetModule | Sort-Object Name, Version -Descending | Group-Object Name | ForEach-Object {$_.Group | Select-Object -First 1}

            if ($null -eq $GetModule) {
                Write-Warning "Unable to find Module in Get-Module -ListAvailable -Name '$Item'"
                Continue
            }

            foreach ($Module in $GetModule) {
                Write-Verbose "Module Name: $($Module.Name)"
                Write-Verbose "Module Version: $($Module.Version)"
                Write-Verbose "Module ModuleBase: $($Module.ModuleBase)"

                #Get the Path to the Destination Module
                $DestinationModule = Join-Path -Path $Destination -ChildPath $Module.Name

                #If RemoveOldVersions
                if ($RemoveOldVersions -eq $true) {
                    if (Test-Path $DestinationModule) {
                        Write-Warning "Removing $DestinationModule"
                        Remove-Item -Path $DestinationModule -Recurse -Force -ErrorAction Stop
                    }
                }

                #Remove Module if PSD1 is not in a Version subdirectory
                if (Test-Path "$DestinationModule\*.psd1") {
                    Write-Warning "Destination Module contains a Manifest in '$DestinationModule'. Content will be replaced"
                    Write-Warning "Removing $DestinationModule"
                    Remove-Item -Path $DestinationModule -Recurse -Force -ErrorAction Stop
                }

                #Destination is set to the Windows Format with Version in the Destination
                $DestinationModuleVersion = Join-Path -Path $DestinationModule -ChildPath $Module.Version
                if (Test-Path $DestinationModuleVersion) {
                    Write-Warning "Destination Module exists at '$DestinationModuleVersion'. Content will be replaced"
                    Write-Warning "Removing $DestinationModuleVersion"
                    Remove-Item -Path $DestinationModuleVersion -Recurse -Force -ErrorAction Stop
                }

                #Copy to the Destination
                $sourceModuleBase = $Module.ModuleBase
                $exclude = @(".git")
                Write-Verbose "Copying $sourceModuleBase to $DestinationModuleVersion"

                if (-not (Test-Path $DestinationModuleVersion)) {
                    New-Item -Path $DestinationModuleVersion -ItemType Directory -Force | Out-Null
                }

                #Copy-Item -Path "$sourceModuleBase\*" -Destination $DestinationModuleVersion -Exclude $exclude -Recurse -Force -ErrorAction Stop
                Copy-Item -Path (Get-Item -Path "$sourceModuleBase\*" -Exclude $exclude).FullName -Destination $DestinationModuleVersion -Recurse -Force


                Get-Module -ListAvailable -FullyQualifiedName $DestinationModuleVersion
            }
        }
    }
    end {}
}
function Copy-PSModuleToWim {
    <#
    .SYNOPSIS
    Copies PowerShell modules into an offline Windows image.
 
    .DESCRIPTION
    Mounts one or more WIM images, copies selected modules into the offline
    module path, optionally sets the image execution policy, and saves changes.
 
    .PARAMETER ExecutionPolicy
    Optional execution policy to set in the mounted image.
 
    .PARAMETER ImagePath
    One or more WIM image file paths to mount and update.
 
    .PARAMETER Index
    Image index to mount from each WIM. Default is 1.
 
    .PARAMETER Name
    One or more module names to copy into the image.
 
    .EXAMPLE
    Copy-PSModuleToWim -ImagePath 'C:\Media\boot.wim' -Name OSD
    Copies the latest installed OSD module into index 1 of boot.wim.
 
    .EXAMPLE
    Copy-PSModuleToWim -ImagePath 'C:\Media\boot.wim' -Index 2 -Name OSD -ExecutionPolicy RemoteSigned
    Copies modules to image index 2 and sets execution policy in the mounted image.
 
    .LINK
    https://github.com/OSDeploy/OSD/tree/master/docs
 
    .NOTES
    Author: David Segura - Recast Software
    2026-07-11 - Added comment-based help
    #>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [ValidateSet('Restricted','AllSigned','RemoteSigned','Unrestricted','Bypass','Undefined')]
        [string]$ExecutionPolicy,

        [Parameter(Mandatory = $true,ValueFromPipelineByPropertyName)]
        [string[]]$ImagePath,

        [Parameter(ValueFromPipelineByPropertyName)]
        [UInt32]$Index = 1,

        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
        [SupportsWildcards()]
        [String[]]$Name
    )

    begin {
        #=================================================
        # Blocks
        #=================================================
        Block-WinPE
        if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
            throw "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Elevated Administrator rights are required"
        }
        #=================================================
    }
    process {
        foreach ($Input in $ImagePath) {
            #=================================================
            # Mount-MyWindowsImage
            #=================================================
            $MountMyWindowsImage = Mount-MyWindowsImage -ImagePath $Input -Index $Index
            #=================================================
            # Copy-PSModuleToFolder
            #=================================================
            Copy-PSModuleToFolder -Name $Name -Destination "$($MountMyWindowsImage.Path)\Program Files\WindowsPowerShell\Modules" -RemoveOldVersions
            #=================================================
            # Set-WindowsImageExecutionPolicy
            #=================================================
            if ($ExecutionPolicy) {
                Set-WindowsImageExecutionPolicy -ExecutionPolicy $ExecutionPolicy -Path $MountMyWindowsImage.Path
            }
            #=================================================
            # Dismount-MyWindowsImage
            #=================================================
            $MountMyWindowsImage | Dismount-MyWindowsImage -Save
            #=================================================
        }
    }
    end {}
}