PSFramework.NuGet.psm1

$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PSFPowerShellDataFile -Path "$($script:ModuleRoot)\PSFramework.NuGet.psd1").ModuleVersion

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = Get-PSFConfigValue -FullName PSFramework.NuGet.Import.DoDotSource -Fallback $false
if ($PSFramework_NuGet_dotsourcemodule) { $script:doDotSource = $true }

<#
Note on Resolve-Path:
All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator.
This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS.
Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist.
This is important when testing for paths.
#>


# Detect whether at some level loading individual module files, rather than the compiled module was enforced
$importIndividualFiles = Get-PSFConfigValue -FullName PSFramework.NuGet.Import.IndividualFiles -Fallback $false
if ($PSFramework_NuGet_importIndividualFiles) { $importIndividualFiles = $true }
if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true }
if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true }
    
function Import-ModuleFile
{
    <#
        .SYNOPSIS
            Loads files into the module on module import.
         
        .DESCRIPTION
            This helper function is used during module initialization.
            It should always be dotsourced itself, in order to proper function.
             
            This provides a central location to react to files being imported, if later desired
         
        .PARAMETER Path
            The path to the file to load
         
        .EXAMPLE
            PS C:\> . Import-ModuleFile -File $function.FullName
     
            Imports the file stored in $function according to import policy
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Path
    )
    
    $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath
    if ($doDotSource) { . $resolvedPath }
    else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) }
}

#region Load individual files
if ($importIndividualFiles)
{
    # Execute Preimport actions
    foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) {
        . Import-ModuleFile -Path $path
    }
    
    # Import all internal functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Import all public functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Execute Postimport actions
    foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) {
        . Import-ModuleFile -Path $path
    }
    
    # End it here, do not load compiled code below
    return
}
#endregion Load individual files

#region Load compiled code
<#
This file loads the strings documents from the respective language folders.
This allows localizing messages and errors.
Load psd1 language files for each language you wish to support.
Partial translations are acceptable - when missing a current language message,
it will fallback to English or another available language.
#>

Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'PSFramework.NuGet' -Language 'en-US'

function New-PublishResult {
    <#
    .SYNOPSIS
        Creates a new publish result object, provided as result of Save-PSFModule.
     
    .DESCRIPTION
        Creates a new publish result object, provided as result of Save-PSFModule.
     
    .PARAMETER ComputerName
        The computer the module was deployed to.
     
    .PARAMETER Module
        The module that was deployed.
     
    .PARAMETER Version
        The version of the module that was deployed.
     
    .PARAMETER Success
        Whether the deployment action succeeded.
        Even if there is a message - which usually means something went wrong - success is possible.
        For example, when a cleanup step failed, but the intended action worked.
     
    .PARAMETER Message
        A message added to the result.
        Usually describes what went wrong - fully or partially.
        Some messages may be included with a success - when the actual goal was met, but something less important went wrong anyway.
     
    .PARAMETER Path
        The path deployed to.
        When deploying to a remote computer, this will include the local path from the perspective of the remote computer.
     
    .EXAMPLE
        PS C:\> New-PublishResult -ComputerName server1 -Module PSFramework -Version 1.12.346 -Success $true -Path 'C:\Program Files\WindowsPowerShell\Modules'
 
        Creates a report of how PSFramework in version 1.12.346 was successfully deployed to the default modules folder on server1
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [string]
        $ComputerName,

        [string]
        $Module,

        [string]
        $Version,

        [bool]
        $Success,

        [string]
        $Message,

        [string]
        $Path
    )

    [PSCustomObject]@{
        PSTypeName   = 'PSFramework.NuGet.PublishResult'
        Computername = $ComputerName
        Module       = $Module
        Version      = $Version
        Success      = $Success
        Message      = $Message
        Path         = $Path
    }
}

function Publish-StagingModule {
    <#
    .SYNOPSIS
        Dispatches module publishing commands.
     
    .DESCRIPTION
        Dispatches module publishing commands.
        This command takes the path to where the locally cached modules are held before copying them to their target location, then ensures they are sent there.
        It differentiates between local deployments and remote deployments, all remote deployments being performed in parallel.
     
    .PARAMETER Path
        The path to where the modules lie that need to be deployed.
     
    .PARAMETER TargetPath
        The targeting information that determines where the modules get published.
        Contrary to the name, this is not a string but expects the output from Resolve-RemotePath.
        The object includes the paths (plural) and the session information needed for remote deployments.
     
    .PARAMETER ThrottleLimit
        Up to how many computers to deploy the modules to in parallel.
        Defaults to: 5
        Default can be configured under the 'PSFramework.NuGet.Remoting.Throttling' setting.
     
    .PARAMETER Force
        Redeploy a module that already exists in the target path.
        By default it will skip modules that do already exist in the target path.
     
    .PARAMETER Cmdlet
        The $PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user.
     
    .EXAMPLE
        PS C:\> Publish-StagingModule -Path $stagingDirectory -TargetPath $targets -Force:$Force -Cmdlet $PSCmdlet
 
        Deploys all modules under $stagingDirectory to the target paths/computers in $targets.
        Will overwrite existing modules if $Force is $true.
    #>

    [CmdletBinding()]
    param (
        [string]
        $Path,

        $TargetPath,

        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ThrottleLimit = (Get-PSFConfigValue -FullName 'PSFramework.NuGet.Remoting.Throttling'),

        [switch]
        $Force,

        $Cmdlet
    )
    
    process {
        $localPaths = @($TargetPath).Where{ -not $_.Session }[0]
        $remotePaths = @($TargetPath).Where{ $_.Session }

        if ($localPaths) {
            Publish-StagingModuleLocal -Path $Path -TargetPath $localPaths -Force:$Force -Cmdlet $Cmdlet
        }
        if ($remotePaths) {
            Publish-StagingModuleRemote -Path $Path -TargetPath $remotePaths -ThrottleLimit $ThrottleLimit -Force:$Force
        }
    }
}

function Publish-StagingModuleLocal {
    <#
    .SYNOPSIS
        Deploys modules to a local path.
     
    .DESCRIPTION
        Deploys modules to a local path.
     
    .PARAMETER Path
        The path from where modules are copied.
     
    .PARAMETER TargetPath
        The destination path information where to deploy the modules.
        Not a string, but the return objects from Resolve-RemotePath (which contrary to its name is also capable of resolving local paths)
     
    .PARAMETER Force
        Redeploy a module that already exists in the target path.
        By default it will skip modules that do already exist in the target path.
     
    .PARAMETER Cmdlet
        The $PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user.
     
    .EXAMPLE
        PS C:\> Publish-StagingModuleLocal -Path $stagingDirectory -TargetPath $targets -Force:$Force -Cmdlet $PSCmdlet
 
        Deploys all modules under $stagingDirectory to the target paths/computers in $targets.
        Will overwrite existing modules if $Force is $true.
    #>

    [CmdletBinding()]
    param (
        [string]
        $Path,

        $TargetPath,

        [switch]
        $Force,

        $Cmdlet
    )

    $msgParam = @{
        PSCmdlet = $Cmdlet
    }
    $publishCommon = @{
        ComputerName = $env:COMPUTERNAME
    }
    $oldSuffix = "old_$(Get-Random -Minimum 100 -Maximum 999)"
    $killIt = $ErrorActionPreference -eq 'Stop'

    foreach ($module in Get-ChildItem -Path $Path) {
        foreach ($version in Get-ChildItem -Path $module.FullName) {
            foreach ($destination in $TargetPath.Results) {
                if (-not $destination.Exists) { continue }

                $publishCommon.Path = $destination.Path
                $publishCommon.Module = $module.Name
                $publishCommon.Version = $version.Name

                $testPath = Join-Path -Path $destination.Path -ChildPath "$($module.Name)/$($version.Name)/$($module.DirectoryName).psd1"
                $alreadyExists = Test-Path -Path $testPath
                if ($alreadyExists -and -not $Force) {
                    Write-PSFMessage @msgParam -String 'Publish-StagingModule.Skipping.AlreadyExists' -StringValues $module.Name, $version.Name, $destination.Path
                    continue
                }

                $targetVersionRoot = Join-Path -Path $destination.Path -ChildPath $module.Name
                $targetVersionDirectory = Join-Path -Path $destination.Path -ChildPath "$($module.Name)/$($version.Name)"

                # Rename old version
                if ($alreadyExists) {
                    Invoke-PSFProtectedCommand -ActionString 'Publish-StagingModule.Deploying.RenameOld' -ActionStringValues $module.Name, $version.Name -Target $TargetPath -ScriptBlock {
                        Rename-Item -LiteralPath $targetVersionDirectory -NewName "$($version.Name)_$oldSuffix" -Force -ErrorAction Stop
                    } -PSCmdlet $Cmdlet -EnableException $killIt -Continue -ErrorEvent {
                        $result = New-PublishResult @publishCommon -Success $false -Message "Failed to rename old version: $_"
                        $PSCmdlet.WriteObject($result, $true)
                    }
                }

                # Deploy New Version
                Invoke-PSFProtectedCommand -ActionString 'Publish-StagingModule.Deploying.Local' -ActionStringValues $module.Name, $version.Name, $destination.Path -Target $TargetPath -ScriptBlock {
                    if (-not (Test-Path $targetVersionRoot)) { $null = New-Item -Path $destination.Path -Name $module.Name -ItemType Directory -Force }
                    Copy-Item -Path $version.FullName -Destination $targetVersionRoot -Recurse -Force
                } -PSCmdlet $Cmdlet -EnableException $killIt -Continue -ErrorEvent {
                    # Rollback to old version in case of deployment error
                    if ($alreadyExists) {
                        Remove-Item -Path $targetVersionDirectory -Force -ErrorAction SilentlyContinue
                        Rename-Item -LiteralPath "$($targetVersionDirectory)_$oldSuffix" -NewName $version.Name -Force -ErrorAction Continue # Don't interfere with the formal error handling, but show extra error if applicable
                    }

                    $result = New-PublishResult @publishCommon -Success $false -Message "Failed to deploy version: $_"
                    $PSCmdlet.WriteObject($result, $true)
                }

                # Remove old version
                if ($alreadyExists) {
                    Invoke-PSFProtectedCommand -ActionString 'Publish-StagingModule.Deploying.DeleteOld' -ActionStringValues $module.Name, $version.Name -Target $TargetPath -ScriptBlock {
                        Remove-Item -LiteralPath "$($targetVersionDirectory)_$oldSuffix" -Force -ErrorAction Stop -Recurse
                    } -PSCmdlet $Cmdlet -EnableException $false -Continue -ErrorEvent {
                        $result = New-PublishResult @publishCommon -Success $true -Message "Failed to cleanup previous version: $_"
                        $PSCmdlet.WriteObject($result, $true)
                    }
                }

                New-PublishResult @publishCommon -Success $true
            }
        }
    }
}

function Publish-StagingModuleRemote {
    <#
        .SYNOPSIS
        Deploys modules to a remote path.
     
    .DESCRIPTION
        Deploys modules to a remote path.
        This happens in parallel using psframework Runspace Workflows.
     
    .PARAMETER Path
        The path from where modules are copied.
     
    .PARAMETER TargetPath
        The destination path information where to deploy the modules.
        Not a string, but the return objects from Resolve-RemotePath.
        This object also includes the PSSession objects needed to execute the transfer.
     
    .PARAMETER ThrottleLimit
        Up to how many computers to deploy the modules to in parallel.
        Defaults to: 5
        Default can be configured under the 'PSFramework.NuGet.Remoting.Throttling' setting.
 
    .PARAMETER Force
        Redeploy a module that already exists in the target path.
        By default it will skip modules that do already exist in the target path.
     
    .EXAMPLE
        PS C:\> Publish-StagingModuleRemote -Path $stagingDirectory -TargetPath $targets -Force:$Force -Cmdlet $PSCmdlet
 
        Deploys all modules under $stagingDirectory to the target paths/computers in $targets.
        Will overwrite existing modules if $Force is $true.
    #>

    [CmdletBinding()]
    param (
        [string]
        $Path,

        $TargetPath,

        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ThrottleLimit = 5,

        [switch]
        $Force
    )

    begin {
        #region Worker Code
        $code = {
            param (
                $TargetPath
            )

            <#
            Inherited Variables:
            - $Path - Where the modules to publish lie
            - $Force - Whether to overwrite/redeploy modules that already exist in that path in that version
            #>


            $PSDefaultParameterValues['Write-PSFMessage:ModuleName'] = 'PSFramework.NuGet'
            $PSDefaultParameterValues['Write-PSFMessage:FunctionName'] = 'Publish-StagingModule'

            #region Functions
            function Get-GlobalFailResult {
                [CmdletBinding()]
                param (
                    [string]
                    $ComputerName,

                    [string]
                    $Path,

                    [System.Management.Automation.ErrorRecord]
                    $ErrorRecord
                )

                foreach ($module in Get-ChildItem -Path $Path) {
                    foreach ($version in Get-ChildItem -Path $module.FullName) {
                        New-PublishResult -ComputerName $ComputerName -Module $module.Name -Version $version.Name -Success $false -Path 'n/a' -Message $ErrorRecord
                    }
                }
            }
            #endregion Functions

            trap {
                Write-PSFMessage -Level Error -String 'Publish-StagingModule.Error.General' -StringValues $TargetPath.ComputerName -ErrorRecord $_

                #region Cleanup Staging Directory
                if ($stagingDirectory) {
                    $null = Invoke-SessionCommand @sessionCommon -Code {
                        param ($Path)
                        Remove-Item -Path $Path -Recurse -Force -ErrorAction Ignore
                    } -ArgumentList $stagingDirectory
                }
                #endregion Cleanup Staging Directory

                Get-GlobalFailResult -Path $Path -ComputerName $TargetPath.ComputerName -ErrorRecord $_

                $__PSF_Workflow.Data.Failed[$TargetPath.ComputerName] = $true
                $__PSF_Workflow.Data.Completed[$TargetPath.ComputerName] = $true
                $null = $__PSF_Workflow.Data.InProgress.TryRemove($TargetPath.ComputerName, [ref]$null)
                return
            }

            $publishCommon = @{
                ComputerName = $TargetPath.ComputerName
            }
            $sessionCommon = @{
                Session = $TargetPath.Session.Session
            }

            $oldSuffix = "old_$(Get-Random -Minimum 100 -Maximum 999)"
            $anyFailed = $false

            #region Prepare Staging Directory
            # This allows us to minimize the cutover time, when replacing an existing module
            $stagingResult = Invoke-SessionCommand @sessionCommon -Code {
                $tempDir = $env:TEMP
                if (-not $tempDir) {
                    $localAppData = $env:LOCALAPPDATA
                    if (-not $localAppData -and -not $IsLinux -and -not $IsMacOS) { $localAppData = [Environment]::GetFolderPath("LocalApplicationData") }
                    if (-not $localAppData -and $Env:XDG_CONFIG_HOME) { $localAppData = $Env:XDG_CONFIG_HOME }
                    if (-not $localAppData) { $localAppData = Join-Path -Path $HOME -ChildPath '.config' }
                    $tempDir = Join-Path -Path $localAppData -ChildPath 'Temp'
                }
                if (-not (Test-Path -Path $tempDir)) {
                    New-Item -Path $tempDir -ItemType Directory -Force -ErrorAction Stop
                }
                $stagingPath = Join-Path -Path $tempDir -ChildPath "PsfGet-$(Get-Random)"
                (New-Item -Path $stagingPath -ItemType Directory -Force -ErrorAction Stop).FullName
            }
            if (-not $stagingResult.Success) {
                Write-PSFMessage -Level Warning -String 'Publish-StagingModule.Remote.Error.TempStagingSetup' -StringValues $TargetPath.ComputerName, $stagingResult.Error -Tag error, temp, setup
                throw $stagingResult.Error
            }
            $stagingDirectory = $stagingResult.Data
            #endregion Prepare Staging Directory

            #region Send Modules
            foreach ($module in Get-ChildItem -Path $Path) {
                foreach ($version in Get-ChildItem -Path $module.FullName) {
                    foreach ($destination in $TargetPath.Results) {
                        if (-not $destination.Exists) { continue }

                        #region Verify Existence
                        $publishCommon.Path = $destination.Path
                        $publishCommon.Module = $module.Name
                        $publishCommon.Version = $version.Name

                        $testPath = Join-Path -Path $destination.Path -ChildPath "$($module.Name)/$($version.Name)/$($module.Name).psd1"
                        $alreadyExists = Invoke-Command -Session $TargetPath.Session.Session -ScriptBlock {
                            param ($TestPath)
                            Test-Path -Path $TestPath
                        } -ArgumentList $testPath

                        if ($alreadyExists -and -not $Force) {
                            Write-PSFMessage -String 'Publish-StagingModule.Remote.Skipping.AlreadyExists' -StringValues $TargetPath.ComputerName, $module.Name, $version.Name -Target ("$($module.Name) ($($version.Name))")
                            New-PublishResult @publishCommon -Success $true -Message 'Module already deployed'
                            continue
                        }
                        #endregion Verify Existence
        
                        $targetStagingRoot = Join-Path -Path $stagingDirectory -ChildPath $module.Name
                        $targetStagingVersionDirectory = Join-Path -Path $targetStagingRoot -ChildPath $version.Name
                        $targetVersionRoot = Join-Path -Path $destination.Path -ChildPath $module.Name
                        $targetVersionDirectory = Join-Path -Path $targetVersionRoot -ChildPath $version.Name

                        #region Send Module to Staging
                        Write-PSFMessage -String 'Publish-StagingModule.Remote.DeployStaging' -StringValues $TargetPath.ComputerName, $module.Name, $version.Name, $targetStagingRoot -Target ("$($module.Name) ($($version.Name))")
                        $createResult = Invoke-SessionCommand @sessionCommon -Code {
                            param ($ModuleRoot)
                            New-Item -Path $ModuleRoot -ItemType Directory -Force -ErrorAction Stop
                        } -ArgumentList $targetStagingRoot
                        if (-not $createResult.Success) {
                            Write-PSFMessage -String 'Publish-StagingModule.Remote.DeployStaging.FailedDirectory' -StringValues $TargetPath.ComputerName, $module.Name, $version.Name, $targetStagingRoot, $createResult.Error -Target ("$($module.Name) ($($version.Name))")
                            New-PublishResult @publishCommon -Success $false -Message "Failed to create staging module folder $targetStagingRoot on $($TargetPath.ComputerName): $($createResult.Error)"
                            $anyFailed = $true
                            continue
                        }
                        try { Copy-Item -LiteralPath $version.FullName -Destination $targetStagingRoot -Recurse -Force -ToSession $TargetPath.Session.Session -ErrorAction Stop }
                        catch {
                            Write-PSFMessage -String 'Publish-StagingModule.Remote.DeployStaging.FailedCopy' -StringValues $TargetPath.ComputerName, $module.Name, $version.Name, $targetStagingRoot -Target ("$($module.Name) ($($version.Name))") -ErrorRecord $_
                            New-PublishResult @publishCommon -Success $false -Message "Failed to copy module to folder $targetStagingRoot on $($TargetPath.ComputerName): $_"
                            $anyFailed = $true
                            continue
                        }
                        #endregion Send Module to Staging
        
                        #region Rename old version
                        if ($alreadyExists) {
                            Write-PSFMessage -String 'Publish-StagingModule.Remote.Deploying.RenameOld' -StringValues $TargetPath.ComputerName, $module.Name, $version.Name, $testPath -Target ("$($module.Name) ($($version.Name))")
                            $renameResult = Invoke-SessionCommand @sessionCommon -Code {
                                param ($Path, $NewName)
                                Rename-Item -LiteralPath $Path -NewName $NewName -ErrorAction Stop -Force
                            } -ArgumentList $targetVersionDirectory, "$($version.Name)_$oldSuffix"
                            if ($renameResult.Success) {
                                Write-PSFMessage -String 'Publish-StagingModule.Remote.Deploying.RenameOld.Success' -StringValues $TargetPath.ComputerName, $module.Name, $version.Name, $testPath -Target ("$($module.Name) ($($version.Name))")
                            }
                            else {
                                Write-PSFMessage -Level Warning -String 'Publish-StagingModule.Remote.Deploying.RenameOld.NoSuccess' -StringValues $TargetPath.ComputerName, $module.Name, $version.Name, $testPath, $renameResult.Error -Target ("$($module.Name) ($($version.Name))")
                                $anyFailed = $true
                                New-PublishResult @publishCommon -Success $false -Message "Failed to rename old version: $($renameResult.Error)"
                                continue
                            }
                        }
                        #endregion Rename old version
    
                        #region Deploy New Version
                        Write-PSFMessage -String 'Publish-StagingModule.Deploying.Remote' -StringValues $TargetPath.ComputerName, $module.Name, $version.Name, $targetVersionRoot -Target ("$($module.Name) ($($version.Name))")
                        $deployResult = Invoke-SessionCommand @sessionCommon -Code {
                            param ($Path, $Destination)
                            if (-not (Test-Path -Path $Destination)) { $null = New-Item -Path $Destination -ItemType Directory -Force -ErrorAction Stop }
                            Move-Item -Path $Path -Destination $Destination -Force -ErrorAction Stop
                        } -ArgumentList $targetStagingVersionDirectory, $targetVersionRoot

                        if (-not $deployResult.Success) {
                            Write-PSFMessage -Level Warning -String 'Publish-StagingModule.Deploying.Remote.Failed' -StringValues $TargetPath.ComputerName, $module.Name, $version.Name, $targetVersionRoot, $deployResult.Error -Target ("$($module.Name) ($($version.Name))")
                            $anyFailed = $true
                            if (-not $alreadyExists) {
                                New-PublishResult @publishCommon -Success $false -Message "Failed to deploy version: $($deployResult.Error)"
                                continue
                            }

                            $rollbackResult = Invoke-SessionCommand @sessionCommon -Code {
                                param ($Path, $TempName)
                                $parent, $name = Split-Path -Path $Path
                                if (Test-Path -Path $Path) { Remove-Item -Path $Path -Recurse -Force -ErrorAction Stop }

                                $original = Join-Path -Path $parent -ChildPath $TempName
                                Rename-Item -Path $original -NewName $name -Force -ErrorAction Stop
                            } -ArgumentList $targetVersionDirectory, "$($version.Name)_$oldSuffix"

                            if ($rollbackResult.Success) {
                                New-PublishResult @publishCommon -Success $false -Message "Failed to re-deploy version, rollback was successful: $($deployResult.Error)"
                            }
                            else {
                                New-PublishResult @publishCommon -Success $false -Message "Failed to re-deploy version, rollback failed: $($deployResult.Error)"
                            }
                            continue
                        }
                        #endregion Deploy New Version

                        #region Remove Old Version
                        if ($alreadyExists) {
                            $cleanupResult = Invoke-SessionCommand @sessionCommon -Code {
                                param ($Path)
                                Remove-Item -LiteralPath $Path -Force -ErrorAction Stop -Recurse
                            } -ArgumentList "$($targetVersionDirectory)_$oldSuffix"

                            if (-not $cleanupResult.Success) {
                                New-PublishResult @publishCommon -Success $true -Message "Failed to cleanup previous version: $($cleanupResult.Error)"
                                continue
                            }
                        }
                        #endregion Remove Old Version

                        New-PublishResult @publishCommon -Success $true
                    }
                }
            }
            #endregion Send Modules

            #region Cleanup Staging Directory
            $null = Invoke-SessionCommand @sessionCommon -Code {
                param ($Path)
                Remove-Item -Path $Path -Recurse -Force -ErrorAction Ignore
            } -ArgumentList $stagingDirectory
            #endregion Cleanup Staging Directory
        
            $__PSF_Workflow.Data.Completed[$TargetPath.ComputerName] = $true
            if ($anyFailed) { $__PSF_Workflow.Data.Failed[$TargetPath.ComputerName] = $true }
            else { $__PSF_Workflow.Data.Success[$TargetPath.ComputerName] = $true }
            $null = $__PSF_Workflow.Data.InProgress.TryRemove($TargetPath.ComputerName, [ref]$null)
        }
        #endregion Worker Code

        # Limit Worker Count to Target Count
        if ($TargetPath.Count -lt $ThrottleLimit) { $ThrottleLimit = $TargetPath.Count }

        $workflow = New-PSFRunspaceWorkflow -Name PublishModule -Force
        $null = $workflow | Add-PSFRunspaceWorker -Name Publisher -InQueue Input -OutQueue Results -ScriptBlock $code -CloseOutQueue -Count $ThrottleLimit -Functions @{
            'New-PublishResult' = [scriptblock]::Create((Get-Command New-PublishResult).Definition)
            'Invoke-SessionCommand' = [scriptblock]::Create((Get-Command Invoke-SessionCommand).Definition)
        } -Variables @{
            Path  = $Path
            Force = $Force
        }
        $workflow | Write-PSFRunspaceQueue -Name Input -BulkValues $TargetPath -Close

        # Add Tracking for Progress Information
        $workflow.Data['InProgress'] = [System.Collections.Concurrent.ConcurrentDictionary[string, string]]::new()
        $workflow.Data['Failed'] = [System.Collections.Concurrent.ConcurrentDictionary[string, string]]::new()
        $workflow.Data['Success'] = [System.Collections.Concurrent.ConcurrentDictionary[string, string]]::new()
        $workflow.Data['Completed'] = [System.Collections.Concurrent.ConcurrentDictionary[string, string]]::new()

        $progressId = Get-Random
    }
    process {
        try {
            $workflow | Start-PSFRunspaceWorkflow
            Write-Progress -Id $progressId -Activity 'Deploying Modules'

            while ($workflow.Data.Completed.Count -lt $TargetPath.Count) {
                Start-Sleep -Seconds 1
                $status = 'In Progress: {0} | Failed: {1} | Succeeded: {2} | Completed: {3}' -f $workflow.Data.InProgress.Count, $workflow.Data.Failed.Count, $workflow.Data.Success.Count, $workflow.Data.Completed.Count
                $percent = ($workflow.Data.Completed.Count / $TargetPath.Count * 100) -as [int]
                if ($percent -gt 100) { $percent = 100 }
                Write-Progress -Id $progressId -Activity 'Deploying Modules' -Status $status -PercentComplete $percent
            }
            $workflow | Wait-PSFRunspaceWorkflow -Queue Results -Closed
        }
        finally {
            # Ensure finally executes without interruption, lest an impatient admin leads to leftover state
            Disable-PSFConsoleInterrupt
            $workflow | Stop-PSFRunspaceWorkflow
            $results = $workflow | Read-PSFRunspaceQueue -Name Results -All | Write-Output # Needs to bextra enumerated if multiple results happen in a single worker
            $workflow | Remove-PSFRunspaceWorkflow
            Enable-PSFConsoleInterrupt
            $results
        }
    }
}

function Read-VersionString {
    <#
    .SYNOPSIS
        Parses a Version String to work for PSGet V2 & V3
     
    .DESCRIPTION
        Parses a Version String to work for PSGet V2 & V3
 
        Supported Syntax:
        <Prefix><Version><Connector><Version><Suffix>
 
        Prefix: "[" (-ge) or "(" (-gt) or nothing (-ge)
        Version: A valid version of 2-4 elements or nothing
        Connector: A "," or a "-"
        Suffix: "]" (-le) or ")" (-lt) or nothing (-le)
     
    .PARAMETER Version
        The Version string to parse.
     
    .PARAMETER Cmdlet
        The $PSCmdlet variable of the caller.
        As this is an internal utility command, this allows it to terminate in the context of the calling command and remain invisible to the user.
     
    .EXAMPLE
        PS C:\> Read-VersionString -Version '[1.0.0,2.0.0)' -Cmdlet $PSCmdlet
         
        Resolves to a version object with a minimum version of 1.0.0 and less than 2.0.0.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Version,

        $Cmdlet = $PSCmdlet
    )
    process {
        $result = [PSCustomObject]@{
            V3String = ''
            Required = ''
            Minimum = ''
            Maximum = ''
            Prerelease = $false
        }

        # Plain Version
        if ($Version -as [version]) {
            $result.V3String = $Version
            $result.Required = $Version
            return $result
        }

        # Plain Version with Prerelease Tag
        if ($Version -match '^\d+(\.\d+){1,3}-\D') {
            $result.V3String = $Version -replace '-\D.*$'
            $result.Required = $Version -replace '-\D.*$'
            $result.Prerelease = $true
            return $result
        }

        <#
        Must match <Prefix><Version><Connector><Version><Suffix>
        Prefix: "[" (-ge) or "(" (-gt) or nothing (-ge)
        Version: A valid version of 2-4 elements or nothing
        Connector: A "," or a "-"
        Suffix: "]" (-le) or ")" (-lt) or nothing (-le)
        #>

        if ($Version -notmatch '^(\[|\(){0,1}(\d+(\.\d+){1,3}){0,1}(-|,)(\d+(\.\d+){1,3}){0,1}(\]|\)){0,1}$') {
            Stop-PSFFunction -String 'Read-VersionString.Error.BadFormat' -StringValues $Version -EnableException $true -Cmdlet $Cmdlet -Category InvalidArgument
        }

        $startGT = $Version -match '^\('
        $endGT = $Version -match '\)$'
        $lower, $higher = $Version -replace '\[|\]|\(|\)' -split ',|-'

        $v3Start = '['
        if ($startGT) { $v3Start = '(' }
        $v3End = ']'
        if ($endGT) { $v3End = ')' }
        $result.V3String = "$($v3Start)$($lower),$($higher)$($v3End)"
        if ($lower) {
            $result.Minimum = $lower -as [version]
            if ($startGT) {
                $parts = $lower -split '\.'
                $parts[-1] = 1 + $parts[-1]
                $result.Minimum = $parts -join '.'
            }
        }
        if ($higher) {
            if ($higher -match '^0(\.0){1,3}$') {
                Stop-PSFFunction -String 'Read-VersionString.Error.BadFormat.ZeroUpperBound' -StringValues $Version -EnableException $true -Cmdlet $Cmdlet -Category InvalidArgument
            }

            $result.Maximum = $higher -as [version]
            if ($endGT) {
                $parts = $higher -split '\.'
                $index = $parts.Count - 1
                do {
                    if (0 -lt $parts[$index]) {
                        $parts[$index] = -1 + $parts[$index]
                        break
                    }
                    $index--
                }
                until ($index -lt 0)

                if ($index -lt ($parts.Count - 1)) {
                    foreach ($position in ($index + 1)..($parts.Count - 1)) {
                        $parts[$position] = 999
                    }
                }

                $result.Maximum = $parts -join '.'
            }
        }

        $result
    }
}

function Resolve-ModuleScopePath {
    <#
    .SYNOPSIS
        Resolves the paths associated with the selected scope.
     
    .DESCRIPTION
        Resolves the paths associated with the selected scope.
        Returns separate results per computer, to account for differentiated, dynamic scope-path resolution.
 
    .PARAMETER Scope
        The scope to resolve the paths for.
        Defaults to "CurrentUser" on local deployments.
        Defaults to "AllUsers" on remote deployments.
     
    .PARAMETER ManagedSession
        Managed remoting sessions (if any).
        Use New-ManagedSession to establish these.
     
    .PARAMETER PathHandling
        Whether all specified paths must exist on a target computer, or whether a single finding counts as success.
        Defaults to: All
     
    .PARAMETER TargetHandling
        How the command should handle unsuccessful computer targets:
        All unsuccessful checks lead to a non-terminating exception.
        However, depending on this parameter, a forced terminating exception might be thrown:
        - "All": Even a single unsuccessful computer leads to terminal errors.
        - "Any": If no target was successful, terminate
        - "None": Never terminate
        Defaults to: None
     
    .PARAMETER Cmdlet
        The $PSCmdlet variable of the caller.
        As this is an internal utility command, this allows it to terminate in the context of the calling command and remain invisible to the user.
     
    .EXAMPLE
        PS C:\> Resolve-ModuleScopePath -Scope AllUsers -ManagedSession $managedSessions -TargetHandling Any -PathHandling Any -Cmdlet $PSCmdlet
 
        Resolves the path to use for the "AllUsers" scope for each computer in $managedSessions - or the local computer if none.
        If the scope resolves to multiple paths, any single existing one will consider the respective computer as successul.
        If any computer at all resolved successfully, the command will return and allow the caller to continue.
        Otherwise it will end the calling command with a terminating exception.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")]
    [CmdletBinding()]
    param (
        [AllowEmptyString()]
        [string]
        $Scope,

        [AllowNull()]
        $ManagedSession,

        [ValidateSet('All', 'Any')]
        [string]
        $PathHandling = 'All',

        [ValidateSet('All', 'Any', 'None')]
        [string]
        $TargetHandling = 'None',

        $Cmdlet = $PSCmdlet
    )
    begin {
        #region Code
        $code = {
            param (
                $Data,
                $PathHandling
            )

            $result = [PSCustomObject]@{
                ComputerName = $env:COMPUTERNAME
                Scope        = $Data.Name
                Path         = $Data.Path
                Results      = @()
                ExistsAll    = $false
                ExistsAny    = $false
                Success      = $false
                Error        = $null
                SessionID    = $global:__PsfSessionId
                Session      = $null
            }

            #region Calculate Target paths
            $targetPaths = $Data.Path
            if ($Data.ScriptBlock) {
                $pathCalculator = [ScriptBlock]::Create($Data.ScriptBlock.ToString())
                if (Get-Module PSFramework) {
                    try { $targetPaths = ([PsfScriptBlock]$pathCalculator).InvokeGlobal() }
                    catch {
                        $result.Error = $_
                        return $result
                    }
                }
                else {
                    try {
                        $targetPaths = & $pathCalculator
                    }
                    catch {
                        $result.Error = $_
                        return $result
                    }
                }
            }
            #endregion Calculate Target paths

            $pathResults = foreach ($path in $targetPaths) {
                if (-not $path) { continue }
                try {
                    $resolvedPaths = Resolve-Path -Path $path -ErrorAction Stop
                }
                catch {
                    [PSCustomObject]@{
                        ComputerName = $env:COMPUTERNAME
                        Path         = $path
                        Exists       = $false
                    }
                    continue
                }
                foreach ($resolvedPath in $resolvedPaths) {
                    [PSCustomObject]@{
                        ComputerName = $env:COMPUTERNAME
                        Path         = $resolvedPath
                        Exists       = $true
                    }
                }
            }

            $result.Results = $pathResults
            $result.ExistsAll = @($pathResults).Where{ -not $_.Exists }.Count -lt 1
            $result.ExistsAny = @($pathResults).Where{ $_.Exists }.Count -gt 0

            if ($PathHandling -eq 'All') { $result.Success = $result.ExistsAll }
            else { $result.Success = $result.ExistsAny }

            if (-not $result.Success) {
                $message = "[$env:COMPUTERNAME] Path not found: $(@($pathResults).Where{ -not $_.Exists }.ForEach{ "'$($_.Path)'" } -join ', ')"
                $result.Error = [System.Management.Automation.ErrorRecord]::new(
                    [System.Exception]::new($message),
                    'PathNotFound',
                    [System.Management.Automation.ErrorCategory]::ObjectNotFound,
                    @(@($pathResults).Where{ -not $_.Exists }.ForEach{ "'$($_.Path)'" })
                )
            }

            $result
        }
        #endregion Code

        $killIt = $ErrorActionPreference -eq 'Stop'
        if (-not $Scope) {
            $Scope = 'AllUsers'
            if (-not $ManagedSession) { $Scope = 'CurrentUser' }
        }
        $scopeObject = $script:moduleScopes[$Scope]
        if (-not $scopeObject) {
            Stop-PSFFunction -String 'Resolve-ModuleScopePath.Error.ScopeNotFound' -StringValues $Scope, ((Get-PSFModuleScope).Name -join ', ') -Cmdlet $Cmdlet -EnableException $killIt
            return
        }
    }
    process {
        if (Test-PSFFunctionInterrupt) { return }

        #region Collect Test-Results
        if (-not $ManagedSession) {
            $testResult = & $code $scopeObject, $PathHandling
        }
        else {
            $failed = $null
            $testResult = Invoke-PSFCommand -ComputerName $ManagedSession.Session -ScriptBlock $code -ArgumentList $scopeObject, $PathHandling -ErrorAction SilentlyContinue -ErrorVariable failed
            $failedResults = foreach ($failedTarget in $failed) {
                [PSCustomObject]@{
                    ComputerName = $failedTarget.TargetObject
                    Scope        = $scopeObject.Name
                    Path         = $scopeObject.Path
                    Results      = @()
                    ExistsAll    = $null
                    ExistsAny    = $null
                    Success      = $false
                    Error        = $failedTarget
                    SessionID    = $null
                    Session      = $null
                }
            }
            $testResult = @($testResult) + @($failedResults) | Remove-PSFNull
        }
        #endregion Collect Test-Results

        #region Evaluate Success
        foreach ($result in $testResult) {
            if ($result.SessionID) { $result.Session = @($ManagedSession).Where{ $_.ID -eq $result.SessionID }[0] }
            [PSFramework.Object.ObjectHost]::AddScriptMethod($result, 'ToString', { '{0}: {1}' -f $this.ComputerName, ($this.Path -join ' | ') })
            if ($result.Success) { continue }

            if (-not $result.Results) {
                Write-PSFMessage -String 'Resolve-ModuleScopePath.Error.UnReached' -StringValues $result.ComputerName, ($result.Path -join ' | ') -Tag fail, connect -Target $result
            }
            else {
                Write-PSFMessage -String 'Resolve-ModuleScopePath.Error.NotFound' -StringValues $result.ComputerName, (@($result.Results).Where{ -not $_.Exists }.Path -join ' | ') -Tag fail, notfound -Target $result
            }

            $Cmdlet.WriteError($result.Error)
        }

        if ($TargetHandling -eq 'All' -and @($testResult).Where{ -not $_.Success }.Count -gt 0) {
            Stop-PSFFunction -String 'Resolve-ModuleScopePath.Fail.NotAll' -StringValues (@($testResult).Where{-not $_.Success }.ComputerName -join ' | ') -EnableException $true -Cmdlet $Cmdlet
        }
        if ($TargetHandling -eq 'Any' -and @($testResult).Where{ $_.Success }.Count -eq 0) {
            Stop-PSFFunction -String 'Resolve-ModuleScopePath.Fail.NotAny' -StringValues ($testResult.ComputerName -join ' | ') -EnableException $true -Cmdlet $Cmdlet
        }
        #endregion Evaluate Success

        $testResult
    }
}

function Resolve-ModuleTarget {
    <#
    .SYNOPSIS
        Resolves the search criteria for modules to save or install.
     
    .DESCRIPTION
        Resolves the search criteria for modules to save or install.
        For each specified module, it will return a result including the parameters Save-Module and Save-PSResource will need.
     
    .PARAMETER InputObject
        A module object to retrieve. Can be the output of Get-Module, Find-Module, Find-PSResource or Find-PSFModule.
     
    .PARAMETER Name
        The name of the module to resolve.
     
    .PARAMETER Version
        The version condition for the module. Supports a fairly flexible syntax.
        Examples:
        - 2.0.0 # Exactly v2.0.0
        - 2.1.0-RC2 # Preview "RC2" of exactly version 2.1.0
        - 2.0.0-2.4.5 # Any version at least 2.0.0 and at most 2.4.5
        - [2.0,3.0) # At least 2.0 but less than 3.0
        - [2.0-3.0) # At least 2.0 but less than 3.0
     
    .PARAMETER Prerelease
        Include Prerelease versions.
        Redundant if asking for a specific version with a specific prerelease suffix.
     
    .PARAMETER Cmdlet
        The $PSCmdlet variable of the caller.
        As this is an internal utility command, this allows it to terminate in the context of the calling command and remain invisible to the user.
     
    .EXAMPLE
        PS C:\> Resolve-ModuleTarget -InputObject $InputObject -Cmdlet $PSCmdlet
 
        Resolves the object as a module target.
        In case of error, the terminating error will happen within the scope of the caller.
    #>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true, ParameterSetName = 'ByObject')]
        [object[]]
        $InputObject,

        [Parameter(ParameterSetName = 'ByName')]
        [string[]]
        $Name,

        [Parameter(ParameterSetName = 'ByName')]
        [AllowEmptyString()]
        [string]
        $Version,

        [Parameter(ParameterSetName = 'ByName')]
        [switch]
        $Prerelease,

        $Cmdlet = $PSCmdlet
    )
    begin {
        function New-ModuleTarget {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
            [CmdletBinding()]
            param (
                $Object,

                [string]
                $Name,

                [AllowEmptyString()]
                [string]
                $Version,

                [switch]
                $Prerelease,

                $Cmdlet
            )

            $v2Param = @{ }
            $v3Param = @{ }
            $actualName = $Name
            $versionString = ''

            if ($Object) {
                $v3Param.InputObject = $Object
                $v2Param.Name = $Object.Name
                $v2Param.RequiredVersion = $Object.AdditionalMetadata.NormalizedVersion
                $versionString = $Object.AdditionalMetadata.NormalizedVersion
                $actualName = $Object.Name

                # V3
                if ($Object.IsPrerelease) { $v2Param.AllowPrerelease = $true }
                # V2
                if ($Object.AdditionalMetadata.IsPrerelease) { $v2Param.AllowPrerelease = $true }

                # Get-Module
                if ($Object -is [System.Management.Automation.PSModuleInfo]) {
                    $versionString = $Object.Version
                    $v2Param.RequiredVersion = $Object.Version
                    if ($Object.PrivateData.PSData.Prerelease) {
                        $v2Param.AllowPrerelease = $true
                        $v2Param.RequiredVersion = '{0}-{1}' -f $Object.Version, $Object.PrivateData.PSData.Prerelease
                    }
                }
            }
            else {
                $v2Param.Name = $Name
                $v3Param.Name = $Name
                if ($Prerelease) {
                    $v2Param.AllowPrerelease = $true
                    $v3Param.Prerelease = $true
                }
                if ($Version) {
                    $versionData = Read-VersionString -Version $Version -Cmdlet $Cmdlet
                    $v3Param.Version = $versionData.V3String
                    if ($versionData.Required) { $v2Param.RequiredVersion = $versionData.Required }
                    else {
                        if ($versionData.Minimum) { $v2Param.MinimumVersion = $versionData.Minimum }
                        if ($versionData.Maximum) { $v2Param.MaximumVersion = $versionData.Maximum }
                    }
                    if ($versionData.Prerelease) {
                        $v2Param.AllowPrerelease = $true
                        $v3Param.Prerelease = $true
                    }
                }
            }

            [PSCustomObject]@{
                PSTypeName = 'PSFramework.NuGet.ModuleTarget'
                Name       = $actualName
                TargetName = "$actualName ($versionString)"
                Version    = $versionString
                V2Param    = $v2Param
                V3Param    = $v3Param
            }
        }
    }
    process {
        foreach ($object in $InputObject) {
            # Case 1: Find-PSFModule
            if ($object.PSObject.TypeNames -contains 'PSFramework.NuGet.ModuleInfo') {
                New-ModuleTarget -Object $object.Object -Cmdlet $Cmdlet
            }
            # Case 2: Find-Module
            # Case 3: Find-PSResource
            # Case 4: Get-Module
            else {
                New-ModuleTarget -Object $object -Cmdlet $Cmdlet
            }
        }
        foreach ($nameEntry in $Name) {
            New-ModuleTarget -Name $nameEntry -Version $Version -Prerelease:$Prerelease -Cmdlet $Cmdlet
        }
    }
}

function Resolve-RemotePath {
    <#
    .SYNOPSIS
        Test for target paths on remote computers.
     
    .DESCRIPTION
        Test for target paths on remote computers.
 
        Has differentiated error handling (see description on TargetHandling or examples),
        in order to ensure proper tracking of all parallely processed targets.
     
    .PARAMETER Path
        The paths to check.
     
    .PARAMETER ComputerName
        The computers to check the paths on.
        Supports established PSSession objects.
 
    .PARAMETER ManagedSession
        Managed Remoting Sessions to associate with the paths resolved.
        Used later to bulk-process the paths in parallel.
     
    .PARAMETER PathHandling
        Whether all specified paths must exist on a target computer, or whether a single finding counts as success.
        Defaults to: All
     
    .PARAMETER TargetHandling
        How the command should handle unsuccessful computer targets:
        All unsuccessful checks lead to a non-terminating exception.
        However, depending on this parameter, a forced terminating exception might be thrown:
        - "All": Even a single unsuccessful computer leads to terminal errors.
        - "Any": If no target was successful, terminate
        - "None": Never terminate
        Defaults to: None
     
    .PARAMETER Cmdlet
        The $PSCmdlet variable of the caller.
        As this is an internal utility command, this allows it to terminate in the context of the calling command and remain invisible to the user.
     
    .EXAMPLE
        PS C:\> Resolve-RemotePath -Path C:\Temp -ComputerName $computers
 
        Checks for C:\Temp on all computers in $computers
        Will not generate any terminating errors.
 
    .EXAMPLE
        PS C:\> Resolve-RemotePath -Path C:\Temp -ComputerName $computers -TargetHandling All
 
        Checks for C:\Temp on all computers in $computers
        If even a single computer cannot be reached or does not have the path, this will terminate the command.
 
    .EXAMPLE
        PS C:\> Resolve-RemotePath -Path C:\Temp, C:\Tmp -ComputerName $computers -TargetHandling All -PathHandling Any
 
        Checks for C:\Temp or C:\Tmp on all computers in $computers
        Each computer is considered successful, if one of the two paths exist on it.
        If even a single computer is not successful - has neither path or cannot be reached - this command will terminate.
 
    .EXAMPLE
        PS C:\> Resolve-RemotePath -Path C:\Temp, C:\Tmp -ComputerName $computers -TargetHandling Any -PathHandling Any -ErrorAction SilentlyContinue
 
        Checks for C:\Temp or C:\Tmp on all computers in $computers
        Each computer is considered successful, if one of the two paths exist on it.
        This command will continue unbothered, so long as at least one computer is successful.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string[]]
        $Path,

        [AllowEmptyCollection()]
        [AllowNull()]
        [PSFComputer[]]
        $ComputerName,

        $ManagedSession,

        [ValidateSet('All', 'Any')]
        [string]
        $PathHandling = 'All',

        [ValidateSet('All', 'Any', 'None')]
        [string]
        $TargetHandling = 'None',

        $Cmdlet = $PSCmdlet
    )
    begin {
        #region Implementing Code
        $code = {
            param ($Data)

            $pathResults = foreach ($path in $Data.Path) {
                try {
                    $resolvedPaths = Resolve-Path -Path $path -ErrorAction Stop
                }
                catch {
                    [PSCustomObject]@{
                        ComputerName = $env:COMPUTERNAME
                        Path         = $path
                        Exists       = $false
                    }
                    continue
                }
                foreach ($resolvedPath in $resolvedPaths) {
                    [PSCustomObject]@{
                        ComputerName = $env:COMPUTERNAME
                        Path         = $resolvedPath
                        Exists       = $true
                    }
                }
            }

            $result = [PSCustomObject]@{
                ComputerName = $env:COMPUTERNAME
                Path         = $Data.Path
                Results      = $pathResults
                ExistsAll    = @($pathResults).Where{ -not $_.Exists }.Count -lt 1
                ExistsAny    = @($pathResults).Where{ $_.Exists }.Count -gt 0
                Success      = $null
                Error        = $null
                SessionID    = $global:__PsfSessionId
                Session      = $null
            }
            if ($Data.PathHandling -eq 'All') { $result.Success = $result.ExistsAll }
            else { $result.Success = $result.ExistsAny }

            if (-not $result.Success) {
                $message = "[$env:COMPUTERNAME] Path not found: $(@($pathResults).Where{ -not $_.Exists }.ForEach{ "'$($_.Path)'" } -join ', ')"
                $result.Error = [System.Management.Automation.ErrorRecord]::new(
                    [System.Exception]::new($message),
                    'PathNotFound',
                    [System.Management.Automation.ErrorCategory]::ObjectNotFound,
                    @(@($pathResults).Where{ -not $_.Exists }.ForEach{ "'$($_.Path)'" })
                )
            }

            $result
        }
        #endregion Implementing Code

        # Passing a single array-argument as a hashtable is more reliable
        $data = @{ Path = $Path; PathHandling = $PathHandling }
    }
    process {
        #region Collect Test-Results
        if (-not $ComputerName) {
            $testResult = & $code $data
        }
        else {
            $failed = $null
            $testResult = Invoke-PSFCommand -ComputerName $ComputerName -ScriptBlock $code -ArgumentList $data -ErrorAction SilentlyContinue -ErrorVariable failed
            $failedResults = foreach ($failedTarget in $failed) {
                [PSCustomObject]@{
                    ComputerName = $failedTarget.TargetObject
                    Path         = $Path
                    Results      = @()
                    ExistsAll    = $null
                    ExistsAny    = $null
                    Success      = $false
                    Error        = $failedTarget
                    SessionID    = $null
                    Session      = $null
                }
            }
            $testResult = @($testResult) + @($failedResults) | Remove-PSFNull
        }
        #endregion Collect Test-Results

        foreach ($result in $testResult) {
            if ($result.SessionID) { $result.Session = @($ManagedSession).Where{ $_.ID -eq $result.SessionID }[0] }
            [PSFramework.Object.ObjectHost]::AddScriptMethod($result, 'ToString', { '{0}: {1}' -f $this.ComputerName, ($this.Path -join ' | ') })
            if ($result.Success) { continue }

            if (-not $result.Results) {
                Write-PSFMessage -String 'Resolve-RemotePath.Error.UnReached' -StringValues $result.ComputerName, ($Path -join ' | ') -Tag fail, connect -Target $result
            }
            else {
                Write-PSFMessage -String 'Resolve-RemotePath.Error.NotFound' -StringValues $result.ComputerName, (@($result.Results).Where{ -not $_.Exists }.Path -join ' | ') -Tag fail, notfound -Target $result
            }

            $Cmdlet.WriteError($result.Error)
        }

        if ($TargetHandling -eq 'All' -and @($testResult).Where{ -not $_.Success }.Count -gt 0) {
            Stop-PSFFunction -String 'Resolve-RemotePath.Fail.NotAll' -StringValues (@($testResult).Where{-not $_.Success }.ComputerName -join ' | '), ($Path -join ' | ') -EnableException $true -Cmdlet $Cmdlet
        }
        if ($TargetHandling -eq 'Any' -and @($testResult).Where{ $_.Success }.Count -eq 0) {
            Stop-PSFFunction -String 'Resolve-RemotePath.Fail.NotAny' -StringValues ($testResult.ComputerName -join ' | '), ($Path -join ' | ') -EnableException $true -Cmdlet $Cmdlet
        }

        $testResult
    }
}

function Resolve-Repository {
    <#
    .SYNOPSIS
        Resolves the PowerShell Repository to use, including their order.
     
    .DESCRIPTION
        Resolves the PowerShell Repository to use, including their order.
        This differs from Get-PSFRepository by throwing a terminating exception in case no repository was found.
     
    .PARAMETER Name
        Names of the Repositories to lookup.
        Can be multiple, can use wildcards.
     
    .PARAMETER Type
        Whether to return PSGet V2, V3 or all repositories.
        Defaults to: "All"
     
    .PARAMETER Cmdlet
        The $PSCmdlet variable of the caller.
        As this is an internal utility command, this allows it to terminate in the context of the calling command and remain invisible to the user.
     
    .EXAMPLE
        PS C:\> Resolve-Repository -Name PSGallery, Contoso -Cmdlet $PSCmdlet
 
        Returns all repositories instances named PSGallery or Contoso, whether registered in V2 or V3
 
    .EXAMPLE
        Ps C:\> Resolve-Repository -Name PSGallery -Type V3 -Cmdlet $PSCmdlet
 
        Returns the PSGet V3 instance of the PSGallery repository.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string[]]
        $Name,

        [ValidateSet('All', 'V2', 'V3')]
        [string]
        $Type = 'All',

        $Cmdlet = $PSCmdlet
    )
    process {
        $repos = Get-PSFRepository -Name $Name -Type $Type

        if (-not $repos) {
            Stop-PSFFunction -String 'Resolve-Repository.Error.NoRepo' -StringValues ($Name -join ', '), $Type -EnableException $true -Cmdlet $Cmdlet -Category ObjectNotFound
        }
        $repos
    }
}

function Save-StagingModule {
    <#
    .SYNOPSIS
        Downloads modules from a repository into a specified, local path.
     
    .DESCRIPTION
        Downloads modules from a repository into a specified, local path.
        This is used internally by Save-PSFModule to cache modules to deploy in one central location that is computer-local.
     
    .PARAMETER InstallData
        The specifics of the module to download.
        The result of the Resolve-ModuleTarget command, it contains V2/V3 specific targeting information.
     
    .PARAMETER Path
        The path where to save them to.
     
    .PARAMETER Repositories
        The repositories to contact.
        Must be repository objects as returned by Get-PSFRepository.
        Repository priority will be adhered.
     
    .PARAMETER Credential
        The Credentials to use for accessing the repositories.
     
    .PARAMETER SkipDependency
        Do not include any dependencies.
        Works with PowerShellGet V1/V2 as well.
     
    .PARAMETER AuthenticodeCheck
        Whether modules must be correctly signed by a trusted source.
        Uses "Get-PSFModuleSignature" for validation.
        Defaults to: $false
        Default can be configured under the 'PSFramework.NuGet.Install.AuthenticodeSignature.Check' setting.
     
    .PARAMETER TrustRepository
        Whether we should trust the repository installed from and NOT ask users for confirmation.
     
    .PARAMETER Cmdlet
        The $PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user.
     
    .EXAMPLE
        PS C:\> Save-StagingModule -InstallData $installData -Path $tempDirectory -Repositories $repositories -Cmdlet $PSCmdlet -Credential $Credential -SkipDependency:$SkipDependency -AuthenticodeCheck:$AuthenticodeCheck
     
        Downloads modules from a repository into a specified, local path.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [CmdletBinding()]
    param (
        [object[]]
        $InstallData,
        
        [string]
        $Path,

        [object[]]
        $Repositories,

        [AllowNull()]
        [PSCredential]
        $Credential,

        [switch]
        $SkipDependency,

        [switch]
        $AuthenticodeCheck,

        [switch]
        $TrustRepository,

        $Cmdlet = $PSCmdlet
    )
    begin {
        #region Implementing Functions
        function Save-StagingModuleV2 {
            [CmdletBinding()]
            param (
                $Repository,

                $Item,

                [string]
                $Path,

                [AllowNull()]
                [PSCredential]
                $Credential,

                [switch]
                $SkipDependency,

                [switch]
                $AuthenticodeCheck
            )

            Write-PSFMessage -String 'Save-StagingModule.SavingV2.Start' -StringValues $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item

            $callSpecifics = @{
                AcceptLicense = $true
                ErrorAction   = 'Stop'
                Repository    = $Repository.Name
            }
            if ($Credential) { $callSpecifics.Credential = $Credential }
            if ($Repository.Credential) { $callSpecifics.Credential = $Repository.Credential }

            $result = [PSCustomObject]@{
                Success        = $false
                Error          = $null
                ModuleName     = $Item.Name
                ModuleVersion  = $item.Version
                RepositoryName = $Repository.Name
                RepositoryType = $Repository.Type
            }

            $tempDirectory = New-PSFTempDirectory -Name StagingSub -ModuleName PSFramework.NuGet
            $param = $Item.v2Param
            # 1) Save to temp folder
            try { Save-Module @param -Path $tempDirectory @callSpecifics }
            catch {
                Write-PSFMessage -String 'Save-StagingModule.SavingV2.Error.Download' -StringValues $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item -Tag fail, save -ErrorRecord $_
                $result.Error = $_

                Remove-PSFTempItem -Name StagingSub -ModuleName PSFramework.NuGet
                Write-PSFMessage -String 'Save-StagingModule.SavingV2.Done' -StringValues $result.Success, $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item
                return $result
            }
            # 2) Remove redundant modules
            if ($SkipDependency) {
                # V2 Does not support saving without its dependencies coming along, so we cleanup in pre-staging
                try { Get-ChildItem -Path $tempDirectory | Where-Object Name -NE $Item.Name | Remove-Item -Force -Recurse -ErrorAction Stop }
                catch {
                    Write-PSFMessage -String 'Save-StagingModule.SavingV2.Error.DependencyCleanup' -StringValues $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item -Tag fail, cleanup -ErrorRecord $_
                    $result.Error = $_
    
                    Remove-PSFTempItem -Name StagingSub -ModuleName PSFramework.NuGet
                    Write-PSFMessage -String 'Save-StagingModule.SavingV2.Done' -StringValues $result.Success, $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item
                    return $result
                }
            }
            # 3) Verify Signature
            if ($AuthenticodeCheck) {
                $signatures = foreach ($moduleBase in Get-ChildItem -Path $tempDirectory) {
                    Get-PSFModuleSignature -Path (Get-Item -Path "$moduleBase\*").FullName
                }
                foreach ($signature in $signatures) {
                    Write-PSFMessage -String 'Save-StagingModule.SavingV2.SignatureCheck' -StringValues $signature.Name, $signature.Version, $signature.IsSigned -Target $signature
                }

                if ($unsigned = @($signatures).Where{ -not $_.IsSigned }) {
                    $result.Error = [System.Management.Automation.ErrorRecord]::new(
                        [System.Exception]::new("Modules are not signed by a trusted code signer: $($unsigned.Name -join ', ')"),
                        'NotTrusted',
                        [System.Management.Automation.ErrorCategory]::SecurityError,
                        $unsigned
                    )
                    Write-PSFMessage -String 'Save-StagingModule.SavingV2.Error.Unsigned' -StringValues $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item -Tag fail, signed -ErrorRecord $result.Error
                    Remove-PSFTempItem -Name StagingSub -ModuleName PSFramework.NuGet
                    Write-PSFMessage -String 'Save-StagingModule.SavingV2.Done' -StringValues $result.Success, $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item
                    return $result
                }
            }
            # 4) Move to Staging
            try { Get-ChildItem -Path $tempDirectory | Copy-Item -Destination $Path -Recurse -Force -ErrorAction Stop }
            catch {
                Write-PSFMessage -String 'Save-StagingModule.SavingV2.Error.Transfer' -StringValues $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item -Tag fail, save -ErrorRecord $_
                $result.Error = $_

                Remove-PSFTempItem -Name StagingSub -ModuleName PSFramework.NuGet
                Write-PSFMessage -String 'Save-StagingModule.SavingV2.Done' -StringValues $result.Success, $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item
                return $result
            }

            Remove-PSFTempItem -Name StagingSub -ModuleName PSFramework.NuGet
            $result.Success = $true
            Write-PSFMessage -String 'Save-StagingModule.SavingV2.Done' -StringValues $result.Success, $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item
            $result
        }

        function Save-StagingModuleV3 {
            [CmdletBinding()]
            param (
                $Repository,

                $Item,

                [string]
                $Path,

                [AllowNull()]
                [PSCredential]
                $Credential,

                [switch]
                $SkipDependency,

                [switch]
                $AuthenticodeCheck,

                [switch]
                $TrustRepository
            )

            Write-PSFMessage -String 'Save-StagingModule.SavingV3.Start' -StringValues $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item

            $callSpecifics = @{
                ErrorAction   = 'Stop'
                Repository    = $Repository.Name
            }
            if ((Get-Command Save-PSResource).Parameters.Keys -contains 'AcceptLicense') {
                $callSpecifics.AcceptLicense = $true
            }
            if ($Credential) { $callSpecifics.Credential = $Credential }
            if ($Repository.Credential) { $callSpecifics.Credential = $Repository.Credential }
            if ($SkipDependency) { $callSpecifics.SkipDependencyCheck = $true }

            $result = [PSCustomObject]@{
                Success        = $false
                Error          = $null
                ModuleName     = $Item.Name
                ModuleVersion  = $item.Version
                RepositoryName = $Repository.Name
                RepositoryType = $Repository.Type
            }

            $tempDirectory = New-PSFTempDirectory -Name StagingSub -ModuleName PSFramework.NuGet
            $param = $Item.v3Param
            # 1) Save to temp folder
            try { Save-PSResource @param -Path $tempDirectory @callSpecifics }
            catch {
                Write-PSFMessage -String 'Save-StagingModule.SavingV3.Error.Download' -StringValues $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item -Tag fail, save -ErrorRecord $_
                $result.Error = $_

                Remove-PSFTempItem -Name StagingSub -ModuleName PSFramework.NuGet
                Write-PSFMessage -String 'Save-StagingModule.SavingV3.Done' -StringValues $result.Success, $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item
                return $result
            }
            # 2) Verify Signature
            if ($AuthenticodeCheck) {
                $signatures = foreach ($moduleBase in Get-ChildItem -Path $tempDirectory) {
                    Get-PSFModuleSignature -Path (Get-Item -Path "$moduleBase\*").FullName
                }
                foreach ($signature in $signatures) {
                    Write-PSFMessage -String 'Save-StagingModule.SavingV3.SignatureCheck' -StringValues $signature.Name, $signature.Version, $signature.IsSigned -Target $signature
                }

                if ($unsigned = @($signatures).Where{ -not $_.IsSigned }) {
                    $result.Error = [System.Management.Automation.ErrorRecord]::new(
                        [System.Exception]::new("Modules are not signed by a trusted code signer: $($unsigned.Name -join ', ')"),
                        'NotTrusted',
                        [System.Management.Automation.ErrorCategory]::SecurityError,
                        $unsigned
                    )
                    Write-PSFMessage -String 'Save-StagingModule.SavingV3.Error.Unsigned' -StringValues $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item -Tag fail, signed -ErrorRecord $result.Error
                    Remove-PSFTempItem -Name StagingSub -ModuleName PSFramework.NuGet
                    Write-PSFMessage -String 'Save-StagingModule.SavingV3.Done' -StringValues $result.Success, $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item
                    return $result
                }
            }
            # 3) Move to Staging
            try { Get-ChildItem -Path $tempDirectory | Copy-Item -Destination $Path -Recurse -Force -ErrorAction Stop }
            catch {
                Write-PSFMessage -String 'Save-StagingModule.SavingV3.Error.Transfer' -StringValues $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item -Tag fail, save -ErrorRecord $_
                $result.Error = $_

                Remove-PSFTempItem -Name StagingSub -ModuleName PSFramework.NuGet
                Write-PSFMessage -String 'Save-StagingModule.SavingV3.Done' -StringValues $result.Success, $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item
                return $result
            }

            Remove-PSFTempItem -Name StagingSub -ModuleName PSFramework.NuGet
            $result.Success = $true
            Write-PSFMessage -String 'Save-StagingModule.SavingV3.Done' -StringValues $result.Success, $Item.Name, $Item.Version, $Repository.Name, $Repository.Type -Target $Item
            $result
        }
        #endregion Implementing Functions

        $common = @{
            SkipDependency    = $SkipDependency
            AuthenticodeCheck = $AuthenticodeCheck
            Path              = $Path
            Credential        = $Credential
        }
    }
    process {
        :item foreach ($installItem in $InstallData) {
            $saveResults = foreach ($repository in $Repositories | Set-PSFObjectOrder -Property Priority, '>Type') {
                $saveResult = switch ($repository.Type) {
                    V2 { Save-StagingModuleV2 -Repository $repository -Item $installItem @common }
                    V3 { Save-StagingModuleV3 -Repository $repository -Item $installItem -TrustRepository:$TrustRepository @common }
                    default { Stop-PSFFunction -String 'Save-StagingModule.Error.UnknownRepoType' -StringValues $repository.Type, $repository.Name -Target $repository -Cmdlet $Cmdlet -EnableException $true }
                }
                if ($saveResult.Success) { continue item }
                $saveResult
            }
            # Only reached if no repository was successful
            foreach ($result in $saveResults) {
                $Cmdlet.WriteError($result.Error)
            }
            Stop-PSFFunction -String 'Save-StagingModule.Error.SaveFailed' -StringValues $installItem.Name, $installItem.Version, (@($repository).ForEach{ '{0} ({1})' -f $_.Name, $_.Type } -join ', ') -Target $installItem -Cmdlet $Cmdlet -EnableException $true
        }
    }
}

function Assert-V2Publishing {
    <#
    .SYNOPSIS
        Ensures users are warned when trying to publish using GetV2 on a system possibly not configured as such.
     
    .DESCRIPTION
        Ensures users are warned when trying to publish using GetV2 on a system possibly not configured as such.
        Warning only shown once per session.
     
    .PARAMETER Cmdlet
        The PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user.
     
    .EXAMPLE
        ps C:\> Assert-V2Publishing -Cmdlet $PSCmdlet
         
        Ensures users are warned when trying to publish using GetV2 on a system possibly not configured as such.
    #>

    [CmdletBinding()]
    param (
        $Cmdlet = $PSCmdlet
    )
    process {
        if ($script:psget.v2CanPublish) { return }
        Write-PSFMessage -Level Warning -String 'Assert-V2Publishing.CannotPublish' -PSCmdlet $Cmdlet -Once GetV2Publish
    }
}

function Copy-Module {
    <#
    .SYNOPSIS
        Copies the content of a module to a staging path and returns information about the module copied.
     
    .DESCRIPTION
        Copies the content of a module to a staging path and returns information about the module copied.
        This is intended to simplify the pre-publish preparation steps and help avoid modifying the actual sources by accident.
     
    .PARAMETER Path
        Path where the module files are.
     
    .PARAMETER Destination
        Destination Path to copy to.
     
    .PARAMETER Cmdlet
        The PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user.
     
    .PARAMETER Continue
        In case of error, call continue unless ErrorAction is set to Stop.
        Simplifies error handling in non-terminating situations.
     
    .PARAMETER ContinueLabel
        When used together with "-Contionue", it allowd you to specify the label/name of the loop to continue with.
     
    .EXAMPLE
        PS C:\> Copy-Module -Path $sourceModule -Destination $workingDirectory -Cmdlet $PSCmdlet -Continue
 
        Creates a copy of $sourceModule in $workingDirectory
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter(Mandatory = $true)]
        [string]
        $Destination,

        $Cmdlet = $PSCmdlet,

        [switch]
        $Continue,

        [string]
        $ContinueLabel
    )
    begin {
        $killIt = $ErrorActionPreference -eq 'Stop'
        $stopCommon = @{
            Cmdlet          = $Cmdlet
            EnableException = $killIt
        }
        if ($Continue) { $stopCommon.Continue = $true }
        if ($ContinueLabel) { $stopCommon.ContinueLabel = $ContinueLabel }
    }
    process {
        $sourceDirectoryPath = $Path
        if ($Path -like '*.psd1') { $sourceDirectoryPath = Split-Path -Path $Path }

        $moduleName = Split-Path -Path $sourceDirectoryPath -Leaf
        if ($moduleName -match '^\d+(\.\d+){1,3}$') {
            $moduleName = Split-Path -Path (Split-Path -Path $sourceDirectoryPath) -Leaf
        }

        #region Validation
        $manifestPath = Join-Path -Path $sourceDirectoryPath -ChildPath "$moduleName.psd1"
        if (-not (Test-Path -Path $manifestPath)) {
            Stop-PSFFunction -String 'Copy-Module.Error.ManifestNotFound' -StringValues $Path -Target $Path @stopCommon -Category ObjectNotFound
            return
        }

        $tokens = $null
        $errors = $null
        $ast = [System.Management.Automation.Language.Parser]::ParseFile($manifestPath, [ref]$tokens, [ref]$errors)

        if ($errors) {
            Stop-PSFFunction -String 'Copy-Module.Error.ManifestSyntaxError' -StringValues $manifestPath -Target $Path @stopCommon -Category ObjectNotFound
            return
        }
        #endregion Validation

        #region Deploy to Staging
        try { $null = New-Item -Path $Destination -Name $moduleName -ItemType Directory -ErrorAction Stop }
        catch {
            Stop-PSFFunction -String 'Copy-Module.Error.StagingFolderFailed' -StringValues $Path -Target $Path @stopCommon -ErrorRecord $_
            return
        }

        $destinationPath = Join-Path -Path $Destination -ChildPath $moduleName
        try { Copy-Item -Path "$($sourceDirectoryPath.Trim('\/'))\*" -Destination $destinationPath -Recurse -Force -ErrorAction Stop }
        catch {
            Stop-PSFFunction -String 'Copy-Module.Error.StagingFolderCopy' -StringValues $Path -Target $Path @stopCommon -ErrorRecord $_
            return
        }
        #endregion Deploy to Staging

        $hashtableAst = $ast.EndBlock.Statements[0].PipelineElements[0].Expression
        [PSCustomObject]@{
            Name            = $moduleName
            Path            = $destinationPath
            ManifestPath    = Join-Path -Path $destinationPath -ChildPath "$moduleName.psd1"
            SourcePath      = $sourceDirectoryPath
            Author          = @($hashtableAst.KeyValuePairs).Where{ $_.Item1.Value -eq 'Author' }.Item2.PipelineElements.Expression.Value
            Version         = @($hashtableAst.KeyValuePairs).Where{ $_.Item1.Value -eq 'ModuleVersion' }.Item2.PipelineElements.Expression.Value
            Description     = @($hashtableAst.KeyValuePairs).Where{ $_.Item1.Value -eq 'Description' }.Item2.PipelineElements.Expression.Value
            RequiredModules = Read-ManifestDependency -Path $manifestPath
        }
    }
}

function New-DummyModule {
    <#
    .SYNOPSIS
        Creates an empty dummy module.
     
    .DESCRIPTION
        Creates an empty dummy module.
        This is used for publishing Resource Modules, the purpose of which are the files later copied into it and not its nature as a module.
     
    .PARAMETER Path
        Where to create the dummy module.
     
    .PARAMETER Name
        The name of the module to assign.
     
    .PARAMETER Version
        What version should the module have?
        Defaults to: 1.0.0
     
    .PARAMETER Description
        A description to include in the dummy module.
        Defaults to a pointless placeholder.
     
    .PARAMETER Author
        Who is the author?
        Defaults to the current user's username.
     
    .PARAMETER RequiredModules
        Any dependencies to include.
        Uses the default module-spec syntax.
     
    .PARAMETER Cmdlet
        The PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user.
     
    .EXAMPLE
        PS C:\> New-DummyModule -Path $stagingDirectory -Name $Name -Version $Version -RequiredModules $RequiredModules -Description $Description -Author $Author -Cmdlet $PSCmdlet
 
        Creates a new dummy module in $stagingDirectory
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [string]
        $Version = '1.0.0',

        [string]
        $Description = '<Dummy Description>',

        [AllowEmptyString()]
        [string]
        $Author,

        [object[]]
        $RequiredModules
    )
    process {
        $param = @{
            Path = Join-Path -Path $Path -ChildPath "$Name.psd1"
            RootModule = "$Name.psm1"
            ModuleVersion = $Version
            Description = $Description
        }
        if ($Author) { $param.Author = $Author }
        if ($RequiredModules) { $param.RequiredModules = $RequiredModules }

        New-ModuleManifest @param
        $null = New-Item -Path $Path -Name "$Name.psm1" -ItemType File
    }
}

function Publish-ModuleToPath {
    <#
    .SYNOPSIS
        Publishes a module to a specific target path-
     
    .DESCRIPTION
        Publishes a module to a specific target path-
        This will create an appropriate .nupkg file in the target location.
        Dependencies will not be considered when publishing like this.
 
        The module cannot already exist in the target path.
     
    .PARAMETER Module
        The module to publish.
        Expects a module information object as returned by Copy-Module.
     
    .PARAMETER Path
        The path to publish to.
     
    .PARAMETER ForceV3
        Force publishing via PSResourceGet.
        By default it uses the latest version the module detected as available.
        This is primarily used for internal testing of the command without a module context.
     
    .PARAMETER Cmdlet
        The PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user.
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .EXAMPLE
        PS C:\> Publish-ModuleToPath -Module $moduleData -Path $DestinationPath -Cmdlet $PSCmdlet
 
        Publishes the module to the provided $DestinationPath.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        $Module,

        [string]
        $Path,

        [switch]
        $ForceV3,

        $Cmdlet = $PSCmdlet
    )
    begin {
        $killIt = $ErrorActionPreference -eq 'Stop'
        $useV3 = $script:psget.V3 -or $ForceV3
        if (-not $useV3) {
            Assert-V2Publishing -Cmdlet $Cmdlet
        }
        $stagingDirectory = New-PSFTempDirectory -ModuleName PSFramework.NuGet -Name Publish.StagingLocalCopy
    }
    process {
        #region Verify Existing Module in Repository
        $fileName = '{0}.{1}.nupkg' -f $Module.Name, $Module.Version
        $destinationFile = Join-Path -Path $Path -ChildPath $fileName

        if (Test-Path -Path $destinationFile) {
            Stop-PSFFunction -String 'Publish-ModuleToPath.Error.AlreadyPublished' -StringValues $Module.Name, $Module.Version, $Path -EnableException $killIt -Category InvalidOperation
            return
        }
        #endregion Verify Existing Module in Repository

        $repoName = "PSF_Temp_$(Get-Random)"
        #region V3
        if ($useV3) {
            try {
                Register-PSResourceRepository -Name $repoName -Uri $stagingDirectory -Trusted
                Publish-PSResource -Path $Module.Path -Repository $repoName -SkipDependenciesCheck
            }
            catch {
                Stop-PSFFunction -String 'Publish-ModuleToPath.Error.FailedToStaging.V3' -StringValues $module.Name, $module.Version -Cmdlet $Cmdlet -ErrorRecord $_ -EnableException $killIt
                return
            }
            finally {
                Unregister-PSResourceRepository -Name $repoName
            }
        }
        #endregion V3

        #region V2
        else {
            try {
                Register-PSRepository -Name $repoName -SourceLocation $stagingDirectory -PublishLocation $stagingDirectory -InstallationPolicy Trusted
                Disable-ModuleCommand -Name 'Get-ModuleDependencies' -ModuleName 'PowerShellGet'
                Publish-Module -Path $Module.Path -Repository $repoName
            }
            catch {
                Stop-PSFFunction -String 'Publish-ModuleToPath.Error.FailedToStaging.V2' -StringValues $module.Name, $module.Version -Cmdlet $Cmdlet -ErrorRecord $_ -EnableException $killIt
                return
            }
            finally {
                Enable-ModuleCommand -Name 'Get-ModuleDependencies' -ModuleName 'PowerShellGet'
                Unregister-PSRepository -Name $repoName
            }
        }
        #endregion V2
    
        #region Copy New Package
        $sourcePath = Join-Path -Path $stagingDirectory -ChildPath $fileName
        Invoke-PSFProtectedCommand -ActionString 'Publish-ModuleToPath.Publishing' -ActionStringValues $module.Name, $module.Version -Target $Path -ScriptBlock {
            Copy-Item -Path $sourcePath -Destination $Path -Force -ErrorAction Stop -Confirm:$false
        } -PSCmdlet $Cmdlet -EnableException $killIt
        #endregion Copy New Package
    }
    end {
        Remove-PSFTempItem -ModuleName PSFramework.NuGet -Name Publish.StagingLocalCopy
    }
}

function Publish-ModuleV2 {
    <#
    .SYNOPSIS
        Publishes a PowerShell module using PowerShellGet V2.
     
    .DESCRIPTION
        Publishes a PowerShell module using PowerShellGet V2.
     
    .PARAMETER Module
        The module to publish.
        Expects an module information object as returned by Copy-Module.
     
    .PARAMETER Repository
        The repository to publish to.
        Expects a repository object as returned by Get-PSFRepository.
     
    .PARAMETER ApiKey
        The ApiKey for authenticating the request.
        Generally needed when publishing to the PowerShell gallery.
     
    .PARAMETER Credential
        The credentials to use for authenticating the request.
        Generally needed when publishing to internal repositories.
     
    .PARAMETER SkipDependenciesCheck
        Do not check for required modules, do not validate the module manifest.
        By default, it will check, whether all required modules are already published to the repository.
        However, it also - usually - requires all modules to be locally available when publishing.
        With this parameter set, that is no longer an issue.
     
    .PARAMETER Continue
        In case of error, call continue unless ErrorAction is set to Stop.
        Simplifies error handling in non-terminating situations.
     
    .PARAMETER ContinueLabel
        When used together with "-Contionue", it allowd you to specify the label/name of the loop to continue with.
     
    .PARAMETER Cmdlet
        The PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user.
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .EXAMPLE
        PS C:\> Publish-ModuleV2 -Module $module -Repository $repo -SkipDependenciesCheck -ApiKey $key
     
        Publishes the module provided in $module to the repository $repo, authenticating the request with the Api key $key.
        It will not validate any dependencies as it does so.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        $Module,

        $Repository,

        [string]
        $ApiKey,

        [PSCredential]
        $Credential,

        [switch]
        $SkipDependenciesCheck,

        [switch]
        $Continue,

        [string]
        $ContinueLabel,

        $Cmdlet = $PSCmdlet
    )
    process {
        $killIt = $ErrorActionPreference -eq 'Stop'
        
        $commonPublish = @{
            Repository = $Repository.Name
            Confirm = $false
        }
        if ($Repository.Credential) { $commonPublish.Credential = $Credential }
        if ($Credential) { $commonPublish.Credential = $Credential }
        if ($ApiKey) { $commonPublish.NuGetApiKey = $ApiKey }

        if ($SkipDependenciesCheck) {
            Disable-ModuleCommand -Name 'Get-ModuleDependencies' -ModuleName 'PowerShellGet'

            $customReturn = Get-Module $Module.Path -ListAvailable
            Disable-ModuleCommand -Name 'Microsoft.PowerShell.Core\Test-ModuleManifest' -ModuleName 'PowerShellGet' -Return $customReturn
        }

        try {
            Invoke-PSFProtectedCommand -ActionString 'Publish-ModuleV2.Publish' -ActionStringValues $Module.Name, $Module.Version, $Repository.Name -ScriptBlock {
                Publish-Module @commonPublish -Path $Module.Path -ErrorAction Stop
            } -Target "$($Module.Name) ($($Module.Version))" -PSCmdlet $Cmdlet -EnableException $killIt -Continue:$Continue -ContinueLabel $ContinueLabel
        }
        finally {
            if ($SkipDependenciesCheck) {
                Enable-ModuleCommand -Name 'Get-ModuleDependencies' -ModuleName 'PowerShellGet'
                Enable-ModuleCommand -Name 'Microsoft.PowerShell.Core\Test-ModuleManifest' -ModuleName 'PowerShellGet'
            }
        }
    }
}

function Publish-ModuleV3 {
    <#
    .SYNOPSIS
        Publishes a PowerShell module using PowerShellGet V3.
     
    .DESCRIPTION
        Publishes a PowerShell module using PowerShellGet V3.
     
    .PARAMETER Module
        The module to publish.
        Expects an module information object as returned by Copy-Module.
     
    .PARAMETER Repository
        The repository to publish to.
        Expects a repository object as returned by Get-PSFRepository.
     
    .PARAMETER ApiKey
        The ApiKey for authenticating the request.
        Generally needed when publishing to the PowerShell gallery.
     
    .PARAMETER Credential
        The credentials to use for authenticating the request.
        Generally needed when publishing to internal repositories.
     
    .PARAMETER SkipDependenciesCheck
        Do not check for required modules, do not validate the module manifest.
        By default, it will check, whether all required modules are already published to the repository.
        However, it also - usually - requires all modules to be locally available when publishing.
        With this parameter set, that is no longer an issue.
     
    .PARAMETER Continue
        In case of error, call continue unless ErrorAction is set to Stop.
        Simplifies error handling in non-terminating situations.
     
    .PARAMETER ContinueLabel
        When used together with "-Contionue", it allowd you to specify the label/name of the loop to continue with.
     
    .PARAMETER Cmdlet
        The PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user.
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .EXAMPLE
        PS C:\> Publish-ModuleV2 -Module $module -Repository $repo -SkipDependenciesCheck -ApiKey $key
     
        Publishes the module provided in $module to the repository $repo, authenticating the request with the Api key $key.
        It will not validate any dependencies as it does so.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        $Module,

        $Repository,

        [string]
        $ApiKey,

        [PSCredential]
        $Credential,

        [switch]
        $SkipDependenciesCheck,

        [switch]
        $Continue,

        [string]
        $ContinueLabel,

        $Cmdlet = $PSCmdlet
    )
    process {
        $killIt = $ErrorActionPreference -eq 'Stop'

        # Ensure to now overwrite a local file
        if ($Repository.Uri -like 'file:*') {
            $targetPath = $Repository.Uri -replace '^file:///' -replace 'file:'
            $targetFile = Join-Path -Path $targetPath -ChildPath "$($Module.Name).$($Module.Version).nupkg"
            if (Test-Path -path $targetFile) {
                Stop-PSFFunction -String 'Publish-ModuleV3.Error.AlreadyPublished' -StringValues $Module.Name, $Module.Version, $Repository.Name -Cmdlet $Cmdlet -EnableException $killIt -Continue:$Continue -ContinueLabel $ContinueLabel -Target "$($Module.Name) ($($Module.Version))"
                return
            }
        }

        $commonPublish = @{
            Repository = $Repository.Name
            Confirm = $false
        }
        if ($Repository.Credential) { $commonPublish.Credential = $Credential }
        if ($Credential) { $commonPublish.Credential = $Credential }
        if ($ApiKey) { $commonPublish.ApiKey = $ApiKey }
        if ($SkipDependenciesCheck) {
            $commonPublish.SkipDependenciesCheck = $SkipDependenciesCheck
            # Parity with V2 - Disabling the dependency check will also prevent Manifest Validation there
            $commonPublish.SkipModuleManifestValidate = $true
        }

        Invoke-PSFProtectedCommand -ActionString 'Publish-ModuleV3.Publish' -ActionStringValues $Module.Name, $Module.Version, $Repository.Name -ScriptBlock {
            Publish-PSResource @commonPublish -Path $Module.Path -ErrorAction Stop
        } -Target "$($Module.Name) ($($Module.Version))" -PSCmdlet $Cmdlet -EnableException $killIt -Continue:$Continue -ContinueLabel $ContinueLabel
    }
}

function Read-ManifestDependency {
    <#
    .SYNOPSIS
        Reads the RequiredModules from a manifest, using AST.
     
    .DESCRIPTION
        Reads the RequiredModules from a manifest, using AST.
 
        Will return a list of objects with the following properties:
        - Name: Name of the required module
        - Version: Version of the required module. Will return 0.0.0 if no version is required.
        - Exact: Whether the version constraint means EXACTLY this version, rather than AT LEAST this version.
     
    .PARAMETER Path
        Path to the manifest to read.
     
    .EXAMPLE
        PS C:\> Read-ManifestDependency -Path C:\Code\MyModule\MyModule.psd1
 
        Returns the modules required by the MyModule module.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Path
    )
    process {
        $tokens = $null
        $errors = $null
        $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$errors)

        $requirements = foreach ($requirement in @($ast.EndBlock.Statements[0].PipelineElements[0].Expression.KeyValuePairs).Where{ $_.Item1.Value -eq 'RequiredModules' }.Item2.PipelineElements.Expression.SubExpression.Statements) {
            $actualRequirement = $requirement.PipelineElements[0].Expression
            switch ($actualRequirement.GetType().Name) {
                'HashtableAst' {
                    [PSCustomObject]@{
                        Name    = $actualRequirement.KeyValuePairs.Where{ $_.Item1.Value -eq 'ModuleName' }.Item2.PipelineElements.Expression.Value
                        Version = $actualRequirement.KeyValuePairs.Where{ $_.Item1.Value -match 'Version$' }.Item2.PipelineElements.Expression.Value -as [version]
                        Exact   = $actualRequirement.KeyValuePairs.Item1.Value -contains 'RequiredVersion'
                    }
                }
                'StringConstantExpressionAst' {
                    [PSCustomObject]@{
                        Name    = $actualRequirement.Value
                        Version = '0.0.0' -as [version]
                        Exact   = $false
                    }
                }
                default {
                    throw "Unexpected Module Dependency AST in $Path : $($actualRequirement.GetType().Name)"
                }
            }
        }
        foreach ($requirement in $requirements) {
            if ($requirement.Exact -and -not $requirement.Version) {
                Write-PSFMessage -Level Warning -String 'Read-ManifestDependency.Warning.VersionError' -StringValues $Path, $requirement.Name
            }
            if (-not $requirement.Version) {
                $requirement.Version = '0.0.0' -as [version]
            }

            $requirement
        }
    }
}

function Disable-ModuleCommand {
    <#
    .SYNOPSIS
        Disables a specific command in a specific module.
     
    .DESCRIPTION
        Disables a specific command in a specific module.
        This hides the command with an alias pointing to a mostly empty function that cares not about the parameters provided.
 
        Use "Enable-ModuleCommand" to revert the changes applied.
     
    .PARAMETER Name
        The name of the command to hide.
     
    .PARAMETER ModuleName
        The module the command to hide is from
     
    .PARAMETER Return
        The object the command should return when called.
        By default, nothing is returned.
     
    .EXAMPLE
        PS C:\> Disable-ModuleCommand -Name 'Get-ModuleDependencies' -ModuleName 'PowerShellGet'
 
        Prevents the command Get-ModuleDependencies from PowerShellGet from returning anything.
 
    .EXAMPLE
        PS C:\> Disable-ModuleCommand -Name 'Microsoft.PowerShell.Core\Test-ModuleManifest' -ModuleName 'PowerShellGet' -Return $customReturn
 
        Prevents the command Microsoft.PowerShell.Core\Test-ModuleManifest from doing its usual job.
        Instead it will statically return the value in $customReturn.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string]
        $ModuleName,

        $Return
    )
    process {
        if ($PSBoundParameters.Keys -contains 'Return') {
            $script:ModuleCommandReturns[$Name] = $Return
        }


        Import-Module $ModuleName -Verbose:$False
        & (Get-Module $ModuleName) {
            function script:psfFunctionOverride {
                $calledAs = $MyInvocation.InvocationName
                $returns = & (Get-Module PSFramework.NuGet) { $script:ModuleCommandReturns }
                if ($returns.Keys -contains $calledAs) {
                    $returns[$calledAs]
                }
            }
            Set-Alias -Name $args[0] -Value psfFunctionOverride -Scope Script
        } $Name
    }
}

function Enable-ModuleCommand {
    <#
    .SYNOPSIS
        Re-Enables a command that was previously disabled.
     
    .DESCRIPTION
        Re-Enables a command that was previously disabled.
        Use Disable-ModuleCommand to disable/override a command.
     
    .PARAMETER Name
        Name of the command to restore.
     
    .PARAMETER ModuleName
        Name of the module the command is from.
     
    .EXAMPLE
        PS C:\> Enable-ModuleCommand -Name 'Get-ModuleDependencies' -ModuleName 'PowerShellGet'
 
        Enables the command Get-ModuleDependencies from the module PowerShellGet
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string]
        $ModuleName
    )
    process {
        Import-Module $ModuleName -Verbose:$False
        $module = Get-Module -Name $ModuleName

        $internal = [PSFramework.Utility.UtilityHost]::GetPrivateProperty('Internal', $module.SessionState)
        $mscope = [PSFramework.Utility.UtilityHost]::GetPrivateProperty('ModuleScope', $internal)
        [PSFramework.Utility.UtilityHost]::InvokePrivateMethod("RemoveAlias", $mscope, @($Name, $true))
    }
}

function Resolve-AkaMsLink {
    <#
    .SYNOPSIS
        Resolves an aka.ms shortcut link to its full address.
     
    .DESCRIPTION
        Resolves an aka.ms shortcut link to its full address.
        This is done by sending the web request against it while limiting the redirect count to 1, then reading the error.
     
    .PARAMETER Name
        The full link or shorthand to resolve.
        Can take any of the following notations:
        + https://aka.ms/psgetv3
        + aka.ms/psgetv3
        + psgetv3
     
    .EXAMPLE
        PS C:\> Resolve-AkaMsLink -Name psgetv3
         
        Returns the Url https://aka.ms/psgetv3 points to.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $Name
    )

    process {
        if ($Name -notmatch 'aka\.ms') {
            $Name = 'https://aka.ms/{0}' -f $Name.TrimStart("/")
        }
        if ($Name -notmatch '^https://') {
            $Name = 'https://{0}' -f $Name.TrimStart("/")
        }
    
        try { $null = Invoke-WebRequest -Uri $Name -MaximumRedirection 1 -ErrorAction Stop }
        catch {
            # Not doing a version check, since exact cut-over version between behaviors unknown
            # PS 5.1
            if ($_.TargetObject.Address.AbsoluteUri) {
                $_.TargetObject.Address.AbsoluteUri
            }
            # PS ?+
            else {
                $_.TargetObject.RequestUri.AbsoluteUri
            }
        }
    }
}

function Invoke-SessionCommand {
    <#
    .SYNOPSIS
        Executes a command in an already provided session and returns the results in a consistent manner.
     
    .DESCRIPTION
        Executes a command in an already provided session and returns the results in a consistent manner.
        This simplifies error handling, especially ErrorAction for errors that happen remotely.
        This command will never throw an error - it will always only return an object with three properties:
 
        + Success (bool): Whether the operation succeeded.
        + Error (ErrorRecord): If it failed, the error record. Will be deserialized, if it was not a remoting error.
        + Data (object): Any return values the scriptblock generated.
     
    .PARAMETER Session
        The session to invoke the command in.
     
    .PARAMETER Code
        The Code to execute
     
    .PARAMETER ArgumentList
        The arguments to pass to the code
     
    .EXAMPLE
        PS C:\> Invoke-SessionCommand -Session $session -Code { Remove-Item -Path C:\Temp\* -Force -Recurse -ErrorAction stop }
         
        Tries to delete all items under C:\Temp in the remote session.
        Successful or not, it will always return a return object, reporting the details.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.Runspaces.PSSession]
        $Session,

        [Parameter(Mandatory = $true)]
        [scriptblock]
        $Code,

        [object[]]
        $ArgumentList
    )
    process {
        $data = @{
            Code         = $Code
            ArgumentList = $ArgumentList
        }

        $scriptblock = {
            param ($Data)
            try {
                # Local Execution, but with Invoke-Command so that the arguments properly enumerate
                $result = Invoke-Command ([scriptblock]::Create($Data.Code.ToString())) -ArgumentList $Data.ArgumentList
                [PSCustomObject]@{
                    Success = $true
                    Error   = $null
                    Data    = $result
                }
            }
            catch {
                [PSCustomObject]@{
                    Success = $false
                    Error   = $_
                    Data    = $null
                }
            }
        }
        try { Invoke-Command -Session $Session -ScriptBlock $scriptblock -ArgumentList $data -ErrorAction Stop }
        catch {
            [PSCustomObject]@{
                Success = $false
                Error   = $_
                Data    = $null
            }
        }
    }
}

function New-ManagedSession {
    <#
    .SYNOPSIS
        Creates new remoting sessions where needed and flags them all with an ID that is shared, both locally and remotely.
     
    .DESCRIPTION
        Creates new remoting sessions where needed and flags them all with an ID that is shared, both locally and remotely.
        This allows easily mapping arguments when parallel invocation makes argument separation difficult.
 
        Note: While a nifty feature in general, this has been superseded in Save-PSFModule, what it originally was developed for.
        The command still provides useful convenience of standardizing input when provided mixed input types,
        as sessions outside of the PSFramework management are needed for now, but its original intent is no longer critical.
     
    .PARAMETER ComputerName
        The computers to deploy the modules to.
        Accepts both names or established PSRemoting sessions.
     
    .PARAMETER Credential
        Credentials to use for remoting connections (if present).
     
    .PARAMETER ConfigurationName
        The name of the PSSessionConfiguration to use for the remoting connection.
        Changing this allows you to execute remote code in PowerShell 7 if configured on the other side.
        This setting can be updated via configuration, using the 'PSFramework.NuGet.Remoting.DefaultConfiguration' setting.
     
    .PARAMETER Type
        What kind of session to create.
        + Temporary: Should be deleted after use.
        + Persistent: Should be kept around.
        Computer targets that are already established PSSessions will be flagged as "External" instead.
     
    .PARAMETER Cmdlet
        The $PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this internal helper command from the user.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .EXAMPLE
        PS C:\> New-ManagedSession -ComputerName $ComputerName -Credential $RemotingCredential -Cmdlet $PSCmdlet -Type Temporary
         
        Establishes sessions to all targets in $ComputerName if needed, using the credentials in $RemotingCredential (if any).
        The newly-established sessions will be considered temporary and should be purged before the task is done.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [AllowNull()]
        [PSFComputer[]]
        $ComputerName,

        [PSCredential]
        $Credential,

        [string]
        $ConfigurationName = (Get-PSFConfigValue -FullName 'PSFramework.NuGet.Remoting.DefaultConfiguration'),

        [Parameter(Mandatory = $true)]
        [ValidateSet('Persistent', 'Temporary')]
        [string]
        $Type,

        $Cmdlet,

        [switch]
        $EnableException
    )
    begin {
        $param = @{ }
        if ($ConfigurationName -ne 'Microsoft.PowerShell') { $param.ConfigurationName = $ConfigurationName }
        if ($Credential) { $param.Credential = $Credential }

        $killIt = $EnableException -or $ErrorActionPreference -eq 'Stop'
    }
    process {
        if (-not $ComputerName) { return }

        #region Collect Sessions
        $sessionHash = @{ }
        @($ComputerName).Where{ $_.Type -eq 'PSSession' }.ForEach{
            if ($_.InputObject.State -ne 'Opened') {
                Stop-PSFFunction -String 'New-ManagedSession.Error.BrokenSession' -StringValues "$_" -FunctionName 'New-ManagedSession' -ModuleName 'PSFramework.NuGet' -Cmdlet $Cmdlet -EnableException $killIt
                return
            }
            $sessionHash["$($_.InputObject.InstanceId)"] = [PSCustomObject]@{
                PSTypeName   = 'PSFramework.NuGet.ManagedSession'
                Type         = 'Extern'
                ComputerName = $_.InputObject.Computername
                Session      = $_.InputObject
                ID           = $null
            }
        }

        $nonSessions = @($ComputerName).Where{ $_.Type -ne 'PSSession' }
        if ($nonSessions) {
            $pssessions = New-PSSession -ComputerName $nonSessions @param -ErrorAction SilentlyContinue -ErrorVariable failedConnections
            foreach ($fail in $failedConnections) {
                Write-PSFMessage -Level Warning -String 'New-ManagedSession.Error.Connect' -StringValues $fail.TargetObject -ErrorRecord $fail -Target $fail.TargetObject -PSCmdlet $Cmdlet -EnableException $killIt
            }

            @($pssessions).ForEach{
                $sessionHash["$($_.InstanceId)"] = [PSCustomObject]@{
                    PSTypeName   = 'PSFramework.NuGet.ManagedSession'
                    Type         = $Type
                    ComputerName = $_.Computername
                    Session      = $_
                    ID           = $null
                }
            }
        }
        #endregion Collect Sessions

        if ($sessionHash.Count -eq 0) { return }

        #region Identify Sessions
        $identifiers = Invoke-Command -Session $sessionHash.Values.Session -ScriptBlock {
            if (-not $global:__PsfSessionId) { $global:__PsfSessionId = "$([Guid]::NewGuid())" }

            [PSCustomObject]@{
                ID = $global:__PsfSessionId
            }
        }
        @($identifiers).ForEach{ $sessionHash["$($_.RunspaceId)"].ID = $_.ID }
        #endregion Identify Sessions

        $sessionHash.Values
    }
}

function Find-PSFModule {
    <#
    .SYNOPSIS
        Search for modules in PowerShell repositories.
     
    .DESCRIPTION
        Search for modules in PowerShell repositories.
     
    .PARAMETER Name
        Name(s) of the module(s) to look for.
     
    .PARAMETER Repository
        The repositories to search in.
     
    .PARAMETER Tag
        Tags to search by.
     
    .PARAMETER Credential
        Credentials to use to access repositories.
     
    .PARAMETER AllowPrerelease
        Whether to include modules flagged as "Prerelease" as part of the results
     
    .PARAMETER IncludeDependencies
        Whether to also list all required dependencies.
     
    .PARAMETER Version
        Version constrains for the module to search.
        Will use the latest version available within the limits.
        Examples:
        - "1.0.0": EXACTLY this one version
        - "1.0.0-1.999.999": Any version between the two limits (including the limit values)
        - "[1.0.0-2.0.0)": Any version greater or equal to 1.0.0 but less than 2.0.0
        - "2.3.0-": Any version greater or equal to 2.3.0.
 
        Supported Syntax:
        <Prefix><Version><Connector><Version><Suffix>
 
        Prefix: "[" (-ge) or "(" (-gt) or nothing (-ge)
        Version: A valid version of 2-4 elements or nothing
        Connector: A "," or a "-"
        Suffix: "]" (-le) or ")" (-lt) or nothing (-le)
     
    .PARAMETER AllVersions
        Whether all versions available should be returned together
     
    .PARAMETER Type
        What kind of repository to search in.
        + All: (default) Use all, irrespective of type
        + V2: Only search classic repositories, as would be returned by Get-PSRepository
        + V3: Only search modern repositories, as would be returned by Get-PSResourceRepository
     
    .EXAMPLE
        PS C:\> Find-PSFModule -Name PSFramework
 
        Search all configured repositories for the module "PSFramework"
    #>

    [CmdletBinding(DefaultParameterSetName = 'default')]
    Param (
        [Parameter(Position = 0)]
        [string[]]
        $Name,

        [PsfArgumentCompleter('PSFramework.NuGet.Repository')]
        [Parameter(Position = 1)]
        [string[]]
        $Repository,

        [string[]]
        $Tag,

        [PSCredential]
        $Credential,

        [switch]
        $AllowPrerelease,

        [switch]
        $IncludeDependencies,

        [Parameter(ParameterSetName = 'Version')]
        [string]
        $Version,

        [Parameter(ParameterSetName = 'AllVersions')]
        [switch]
        $AllVersions,

        [ValidateSet('All', 'V2', 'V3')]
        [string]
        $Type = 'All'
    )
    
    begin {
        #region Functions
        function ConvertFrom-ModuleInfo {
            [CmdletBinding()]
            param (
                [Parameter(ValueFromPipeline = $true)]
                $InputObject
            )

            process {
                if ($null -eq $InputObject) { return }
                $type = 'V2'
                if ($InputObject.GetType().Name -eq 'PSResourceInfo') { $type = 'V3' }

                [PSCustomObject]@{
                    PSTypeName = 'PSFramework.NuGet.ModuleInfo'
                    Name       = $InputObject.Name
                    Version    = $InputObject.Version
                    Type       = $type
                    Repository = $InputObject.Repository
                    Author     = $InputObject.Author
                    Commands   = $InputObject.Includes.Command
                    Object     = $InputObject
                }
            }
        }
        #endregion Functions

        $useVersionFilter = $Version -or $AllVersions
        if ($Version) {
            $convertedVersion = Read-VersionString -Version $Version -Cmdlet $PSCmdlet
            $versionFilter = $convertedVersion.V3String
        }
        if ($PSCmdlet.ParameterSetName -eq 'AllVersions') {
            $versionFilter = '*'
        }

        $param = $PSBoundParameters | ConvertTo-PSFHashtable -Include Name, Repository, Tag, Credential, IncludeDependencies
    }
    process {
        #region V2
        if ($script:psget.V2 -and $Type -in 'All', 'V2') {
            $paramClone = $param.Clone()
            $paramClone += $PSBoundParameters | ConvertTo-PSFHashtable -Include AllVersions, AllowPrerelease
            if ($Version) {
                if ($convertedVersion.Required) { $paramClone.RequiredVersion = $convertedVersion.Required }
                if ($convertedVersion.Minimum) { $paramClone.MinimumVersion = $convertedVersion.Minimum }
                if ($convertedVersion.Maximum) { $paramClone.MaximumVersion = $convertedVersion.Maximum }
            }
            $execute = $true
            if ($paramClone.Repository) {
                $paramClone.Repository = $paramClone.Repository | Where-Object {
                    $_ -match '\*' -or
                    $_ -in (Get-PSFRepository -Type V2).Name
                }
                $execute = $paramClone.Repository -as [bool]
            }

            if ($execute) {
                Find-Module @paramClone | ConvertFrom-ModuleInfo
            }
        }
        #endregion V2

        #region V3
        if ($script:psget.V3 -and $Type -in 'All', 'V3') {
            $paramClone = $param.Clone()
            $paramClone += $PSBoundParameters | ConvertTo-PSFHashtable -Include AllowPrerelease -Remap @{
                AllowPrerelease = 'Prerelease'
            }
            if ($useVersionFilter) {
                $paramClone.Version = $versionFilter
            }
            $paramClone.Type = 'Module'
            $execute = $true
            if ($paramClone.Repository) {
                $paramClone.Repository = $paramClone.Repository | Where-Object {
                    $_ -match '\*' -or
                    $_ -in (Get-PSFRepository -Type V3).Name
                }
                $execute = $paramClone.Repository -as [bool]
            }
            if ($execute) {
                Find-PSResource @paramClone | ConvertFrom-ModuleInfo
            }
        }
        #endregion V3
    }
}

function Get-PSFModuleScope {
    <#
    .SYNOPSIS
        Lists the registered module scopes.
     
    .DESCRIPTION
        Lists the registered module scopes.
        These are used as presets with Install-PSFModule's '-Scope' parameter.
 
        Use Register-PSFModuleScope to add additional scopes.
     
    .PARAMETER Name
        The name of the scope to filter by.
        Defaults to '*'
     
    .EXAMPLE
        PS C:\> Get-PSFModuleScope
         
        Lists all registered module scopes.
    #>

    [CmdletBinding()]
    param (
        [PsfArgumentCompleter('PSFramework.NuGet.ModuleScope')]
        [string]
        $Name = '*'
    )
    process {
        ($script:moduleScopes.Values) | Where-Object Name -Like $Name
    }
}

function Install-PSFModule {
    <#
    .SYNOPSIS
        Installs PowerShell modules from a PowerShell repository.
     
    .DESCRIPTION
        Installs PowerShell modules from a PowerShell repository.
        They can be installed locally or to remote computers.
     
    .PARAMETER Name
        Name of the module to install.
     
    .PARAMETER Version
        Version constrains for the module to install.
        Will use the latest version available within the limits.
        Examples:
        - "1.0.0": EXACTLY this one version
        - "1.0.0-1.999.999": Any version between the two limits (including the limit values)
        - "[1.0.0-2.0.0)": Any version greater or equal to 1.0.0 but less than 2.0.0
        - "2.3.0-": Any version greater or equal to 2.3.0.
 
        Supported Syntax:
        <Prefix><Version><Connector><Version><Suffix>
 
        Prefix: "[" (-ge) or "(" (-gt) or nothing (-ge)
        Version: A valid version of 2-4 elements or nothing
        Connector: A "," or a "-"
        Suffix: "]" (-le) or ")" (-lt) or nothing (-le)
     
    .PARAMETER Prerelease
        Whether to include prerelease versions in the potential results.
     
    .PARAMETER Scope
        Where to install the module to.
        Use Register-PSFModuleScope to add additional scopes to the list of options.
        Scopes can either use a static path or dynamic code to calculate - per computer - where to install the module.
        If not specified, it will default to:
        - CurrentUser - for local installation (irrespective of whether the console is run "As Administrator" or not.)
        - AllUsers - for remote installations when using the -ComputerName parameter.
     
    .PARAMETER ComputerName
        The computers to deploy the modules to.
        Accepts both names or established PSRemoting sessions.
        All transfer happens via PowerShell Remoting.
 
        If you provide names, by default this module will connect to the "Microsoft.PowerShell" configuration name.
        To change that name, use the 'PSFramework.NuGet.Remoting.DefaultConfiguration' configuration setting.
     
    .PARAMETER SkipDependency
        Do not include any dependencies.
        Works with PowerShellGet V1/V2 as well.
     
    .PARAMETER AuthenticodeCheck
        Whether modules must be correctly signed by a trusted source.
        Uses "Get-PSFModuleSignature" for validation.
        Defaults to: $false
        Default can be configured under the 'PSFramework.NuGet.Install.AuthenticodeSignature.Check' setting.
     
    .PARAMETER Force
        Redeploy a module that already exists in the target path.
        By default it will skip modules that do already exist in the target path.
     
    .PARAMETER Credential
        The credentials to use for connecting to the Repository (NOT the remote computers).
 
    .PARAMETER RemotingCredential
        The credentials to use for connecting to remote computers we want to deploy modules to via remoting.
        These will NOT be used for repository access.
     
    .PARAMETER ThrottleLimit
        Up to how many computers to deploy the modules to in parallel.
        Defaults to: 5
        Default can be configured under the 'PSFramework.NuGet.Remoting.Throttling' setting.
     
    .PARAMETER Repository
        Repositories to install from. Respects the priority order of repositories.
        See Get-PSFRepository for available repositories (and their priority).
        Lower numbers are installed from first.
     
    .PARAMETER TrustRepository
        Whether we should trust the repository installed from and NOT ask users for confirmation.
     
    .PARAMETER Type
        What type of repository to download from.
        V2 uses classic Save-Module.
        V3 uses Save-PSResource.
        Availability depends on the installed PSGet module versions and configured repositories.
        Use Install-PSFPowerShellGet to deploy the latest versions of the package modules.
 
        Only the version on the local computer matters, even when deploying to remote computers.
     
    .PARAMETER InputObject
        The module to install.
        Takes the output of Get-Module, Find-Module, Find-PSResource and Find-PSFModule, to specify the exact version and name of the module.
        Even when providing a locally available version, the module will still be downloaded from the repositories chosen.
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .EXAMPLE
        PS C:\> Install-PSFModule -Name EntraAuth
 
        Installs the EntraAuth module locally for the CurrentUser.
 
    .EXAMPLE
        PS C:\> Install-PSFModule -Name ADMF -ComputerName AdminHost1, AdminHost2
 
        Installs the ADMF module (and all of its dependencies) for All Users on the computers AdminHost1 and AdminHost2
 
    .EXAMPLE
        PS C:\> Install-PSFModule -Name string, PoshRSJob -ComputerName $sshSessions -Scope ScriptModules
 
        Installs the String and PoshRSJob module to all computers with an established session in $sshSessions.
        The modules will be installed to the "ScriptModules" scope - something that must have first been registered
        using the Register-PSFModuleScope command.
    #>

    [CmdletBinding(PositionalBinding = $false, DefaultParameterSetName = 'ByName', SupportsShouldProcess = $true)]
    Param (
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'ByName')]
        [string[]]
        $Name,

        [Parameter(ParameterSetName = 'ByName')]
        [string]
        $Version,

        [Parameter(ParameterSetName = 'ByName')]
        [switch]
        $Prerelease,

        [PsfValidateSet(TabCompletion = 'PSFramework.NuGet.ModuleScope')]
        [PsfArgumentCompleter('PSFramework.NuGet.ModuleScope')]
        [string]
        $Scope,

        [PSFComputer[]]
        $ComputerName,

        [switch]
        $SkipDependency,

        [switch]
        $AuthenticodeCheck = (Get-PSFConfigValue -FullName 'PSFramework.NuGet.Install.AuthenticodeSignature.Check'),

        [switch]
        $Force,

        [PSCredential]
        $Credential,

        [PSCredential]
        $RemotingCredential,

        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ThrottleLimit = (Get-PSFConfigValue -FullName 'PSFramework.NuGet.Remoting.Throttling'),

        [PsfArgumentCompleter('PSFramework.NuGet.Repository')]
        [string[]]
        $Repository = ((Get-PSFrepository).Name | Sort-Object -Unique),

        [switch]
        $TrustRepository,

        [ValidateSet('All', 'V2', 'V3')]
        [string]
        $Type = 'All',

        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ByObject')]
        [object[]]
        $InputObject
    )
    
    begin {
        $killIt = $ErrorActionPreference -eq 'Stop'
        $cleanedUp = $false

        # Resolution only happens to early detect impossible parameterization. Will be called again in Save-PSFModule.
        $null = Resolve-Repository -Name $Repository -Type $Type -Cmdlet $PSCmdlet # Terminates if no repositories found
        $managedSessions = New-ManagedSession -ComputerName $ComputerName -Credential $RemotingCredential -Cmdlet $PSCmdlet -Type Temporary
        if ($ComputerName -and -not $managedSessions) {
            Stop-PSFFunction -String 'Install-PSFModule.Error.NoComputerValid' -StringValues ($ComputerName -join ', ') -EnableException $killIt -Cmdlet $PSCmdlet
            return
        }
        $resolvedPaths = Resolve-ModuleScopePath -Scope $Scope -ManagedSession $managedSessions -TargetHandling Any -PathHandling Any -Cmdlet $PSCmdlet # Errors for bad paths, terminates if no path

        # Used to declare variable in the current scope, to prevent variable lookup snafus when det
        $command = $null
        $saveParam = $PSBoundParameters | ConvertTo-PSFHashtable -ReferenceCommand Save-PSFModule -Exclude ComputerName, RemotingCredential
        $saveParam.Path = '<placeholder>' # Meet Parameterset requirements
    }
    process {
        if (Test-PSFFunctionInterrupt) { return }

        $stopParam = @{ StringValues = $Name -join ', '}
        if ($InputObject) {
            $names = foreach ($item in $InputObject) {
                if ($item -is [string]) { $item }
                elseif ($item.ModuleName) { $item.ModuleName }
            }
            $stopParam = @{ StringValues = $names -join ', '}
        }
        
        #region Start Nested Save-PSFModule
        if (-not $command) {
            $command = { Save-PSFModule @saveParam -PathInternal $resolvedPaths -Cmdlet $PSCmdlet -ErrorAction $ErrorActionPreference }.GetSteppablePipeline()
            try { $command.Begin((-not $Name)) }
            catch {
                if (-not $cleanedUp -and $managedSessions) { $managedSessions | Where-Object Type -EQ 'Temporary' | ForEach-Object Session | Remove-PSSession }
                $cleanedUp = $true
                Stop-PSFFunction -String 'Install-PSFModule.Error.Setup' @stopParam -ErrorRecord $_ -EnableException $killIt -Cmdlet $PSCmdlet
                return
            }
        }
        #endregion Start Nested Save-PSFModule

        #region Execute Process
        try {
            if ($Name) { $command.Process() }
            else { $command.Process($InputObject) }
        }
        catch {
            if (-not $cleanedUp -and $managedSessions) { $managedSessions | Where-Object Type -EQ 'Temporary' | ForEach-Object Session | Remove-PSSession }
            $cleanedUp = $true
            Stop-PSFFunction -String 'Install-PSFModule.Error.Installation' @stopParam -ErrorRecord $_ -EnableException $killIt -Cmdlet $PSCmdlet
            return
        }
        #endregion Execute Process
    }
    end {
        if (-not $cleanedUp -and $managedSessions) { $managedSessions | Where-Object Type -EQ 'Temporary' | ForEach-Object Session | Remove-PSSession }
        if (Test-PSFFunctionInterrupt) { return }
        $null = $command.End()
    }
}

function Publish-PSFModule {
    <#
    .SYNOPSIS
        Publish a PowerShell module.
     
    .DESCRIPTION
        Publish a PowerShell module.
        Allows publishing to either nuget repositories or as .nupkg file to disk.
     
    .PARAMETER Path
        The path to the module to publish.
        Either the directory or the psd1 file.
     
    .PARAMETER Repository
        The repository to publish to.
     
    .PARAMETER Type
        What kind of repository to publish to.
        - All (default): All types of repositories are eligible.
        - V2: Only repositories from the old PowerShellGet are eligible.
        - V3: Only repositories from the new PSResourceGet are eligible.
        If multiple repositories of the same name are found, the one at the highest version among them is chosen.
     
    .PARAMETER Credential
        The credentials to use to authenticate to the Nuget service.
        Mostly used for internal repository servers.
     
    .PARAMETER ApiKey
        The ApiKey to use to authenticate to the Nuget service.
        Mostly used for publishing to the PSGallery.
     
    .PARAMETER SkipDependenciesCheck
        Do not validate dependencies or the module manifest.
        This removes the need to have the dependencies installed when publishing using PSGet v2
     
    .PARAMETER DestinationPath
        Rather than publish to a repository, place the finished .nupgk file in this path.
        Use when doing the final publish step outside of PowerShell code.
     
    .PARAMETER Tags
        Tags to add to the module.
     
    .PARAMETER LicenseUri
        The LicenseUri for the module.
        Mostly used as metadata for the PSGallery.
     
    .PARAMETER IconUri
        The Icon Uri for the module.
        Mostly used as metadata for the PSGallery.
     
    .PARAMETER ProjectUri
        The Link to the project - frequently the Github repository hosting your module.
        Mostly used as metadata for the PSGallery.
     
    .PARAMETER ReleaseNotes
        The release notes of your module - or at least the link to them.
        Mostly used as metadata for the PSGallery.
     
    .PARAMETER Prerelease
        The prerelease tag to include.
        This flags the module as "Prerelease", hiding it from regular Find-PSFModule / Install-PSFModule use.
        Use to provide test versions that only affect those in the know.
     
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
 
    .EXAMPLE
        PS C:\> Publish-PSFModule -Path C:\code\MyModule -Repository PSGallery -ApiKey $key
 
        Publishes the module "MyModule" to the PSGallery.
 
    .EXAMPLE
        PS C:\> Publish-PSFModule -Path C:\code\MyModule -Repository AzDevOps -Credential $cred -SkipDependenciesCheck
 
        Publishes the module "MyModule" to the repository "AzDevOps".
        It will not check for any dependencies and use the credentials stored in $cred to authenticate the request.
 
    .EXAMPLE
        PS C:\> Publish-PSFModule -Path C:\code\MyModule -Repository AzDevOps -SkipDependenciesCheck
 
        Publishes the module "MyModule" to the repository "AzDevOps".
        It will not check for any dependencies.
        If there are any credentials assigned to the repository (Use Set-PSFRepository to assign), those will be used to authenticate the request.
        Otherwise it will try default windows authentication (Which may well work, if the repository is hosted by an on-prem Azure DevOps Server in an Active Directory environment).
 
    .EXAMPLE
        PS C:\> Publish-PSFModule -Path C:\code\MyModule -DestinationPath \\contoso.com\it\packages
 
        Wraps the module "MyModule" into a .nupkg file and copies that to '\\contoso.com\it\packages'
    #>

    [CmdletBinding(DefaultParameterSetName = 'ToRepository', SupportsShouldProcess = $true)]
    Param (
        [Parameter(Mandatory = $true)]
        [PsfPath]
        $Path,

        [Parameter(Mandatory = $true, ParameterSetName = 'ToRepository')]
        [PsfValidateSet(TabCompletion = 'PSFramework.NuGet.Repository')]
        [PsfArgumentCompleter('PSFramework.NuGet.Repository')]
        [string[]]
        $Repository,

        [Parameter(ParameterSetName = 'ToRepository')]
        [ValidateSet('All', 'V2', 'V3')]
        [string]
        $Type = 'All',

        [Parameter(ParameterSetName = 'ToRepository')]
        [PSCredential]
        $Credential,

        [Parameter(ParameterSetName = 'ToRepository')]
        [string]
        $ApiKey,

        [Parameter(ParameterSetName = 'ToRepository')]
        [switch]
        $SkipDependenciesCheck,

        [Parameter(Mandatory = $true, ParameterSetName = 'ToPath')]
        [PsfDirectory]
        $DestinationPath,

        [string[]]
        $Tags,

        [string]
        $LicenseUri,

        [string]
        $IconUri,

        [string]
        $ProjectUri,

        [string]
        $ReleaseNotes,

        [string]
        $Prerelease
    )
    
    begin {
        #region Setup
        $killIt = $ErrorActionPreference -eq 'Stop'
        if ($Repository) {
            # Resolve Repositories
            Search-PSFPowerShellGet
            $repositories = Resolve-Repository -Name $Repository -Type $Type -Cmdlet $PSCmdlet | Group-Object Name | ForEach-Object {
                @($_.Group | Sort-Object Type -Descending)[0]
            }
        }
        # Create Temp Directories
        $workingDirectory = New-PSFTempDirectory -ModuleName PSFramework.NuGet -Name Publish.Work

        $commonPublish = @{
            Cmdlet           = $PSCmdlet
            Continue         = $true
            ContinueLabel    = 'repo'
        }
        if ($ApiKey) { $commonPublish.ApiKey = $ApiKey }
        if ($Credential) { $commonPublish.Credential = $Credential }
        if ($SkipDependenciesCheck) { $commonPublish.SkipDependenciesCheck = $SkipDependenciesCheck }
        #endregion Setup
    }
    process {
        try {
            foreach ($sourceModule in $Path) {
                # Update Metadata per Parameter
                $moduleData = Copy-Module -Path $sourceModule -Destination $workingDirectory -Cmdlet $PSCmdlet -Continue
                Update-PSFModuleManifest -Path $moduleData.ManifestPath -Tags $Tags -LicenseUri $LicenseUri -IconUri $IconUri -ProjectUri $ProjectUri -ReleaseNotes $ReleaseNotes -Prerelease $Prerelease -Cmdlet $PSCmdlet -Continue

                # Case 1: Publish to Destination Path
                if ($DestinationPath) {
                    Publish-ModuleToPath -Module $moduleData -Path $DestinationPath -Cmdlet $PSCmdlet
                    continue
                }

                # Case 2: Publish to Repository
                :repo foreach ($repositoryObject in $repositories) {
                    switch ($repositoryObject.Type) {
                        V2 {
                            Publish-ModuleV2 @commonPublish -Module $moduleData -Repository $repositoryObject
                        }
                        V3 {
                            Publish-ModuleV3 @commonPublish -Module $moduleData -Repository $repositoryObject
                        }
                        default {
                            Stop-PSFFunction -String 'Publish-PSFModule.Error.UnexpectedRepositoryType' -StringValues $repositoryObject.Name, $repositoryObject.Type -Continue -Cmdlet $PSCmdlet -EnableException $killIt
                        }
                    }
                }
            }
        }
        finally {
            # Cleanup Temp Directory
            Remove-PSFTempItem -ModuleName PSFramework.NuGet -Name Publish.*
        }
    }
}

function Register-PSFModuleScope {
    <#
    .SYNOPSIS
        Provide a scope you can install modules to.
     
    .DESCRIPTION
        Provide a scope you can install modules to.
        Those are used by Install-PFModule to pick what path to install to.
     
    .PARAMETER Name
        Name of the scope.
        Must be unique, otherwise it will overwrite an existing scope.
     
    .PARAMETER Path
        Path where modules should be stored.
 
    .PARAMETER Mode
        Specifying a mode will add the path provided to the PSModulePath variable for this session.
        - Append: Adds the path as the last option, making it the last location PowerShell will look for modules.
        - Prepend: Adds the path as the first option, making it take precedence over all other module paths.
     
    .PARAMETER ScriptBlock
        Logic determining, where modules should be stored.
        This scriptblock will not receive any parameters.
        Used to dynamically determine the path, may be executed against remote computers,
        when installing to remote computers.
        Keep in mind that dependencies may not be available.
     
    .PARAMETER Description
        A description to add to the module scope registered.
        Purely for documentation purposes.
 
    .PARAMETER Persist
        Remember the configured scope.
        For the current user, even when starting a new console, this scope will still exist.
        This will NOT remember the "Mode" parameter - configure your PSModulePath environment evariable separately, if desired.
        Not compatible with a ScriptBlock-based setting.
     
    .EXAMPLE
        PS C:\> Register-PSFModuleScope -Name WinPSAllUsers -Path 'C:\Program Files\WindowsPowerShell\Modules'
         
        Registers the module-scope "WinPSAllusers" with the default path for Modules in Windows PowerShell.
        This would allow installing modules for Windows PowerShell from PowerShell 7.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'Path')]
        [string]
        $Path,

        [Parameter(ParameterSetName = 'Path')]
        [ValidateSet('Append', 'Prepend')]
        [string]
        $Mode,

        [Parameter(Mandatory = $true, ParameterSetName = 'Scriptblock')]
        [PsfValidateLanguageMode('FullLanguage')]
        [scriptblock]
        $ScriptBlock,

        [string]
        $Description,

        [Parameter(ParameterSetName = 'Path')]
        [switch]
        $Persist
    )
    process {
        $typeMap = @{
            $true  = 'Dynamic'
            $false = 'Static'
        }
        $script:moduleScopes[$Name] = [PSCustomObject]@{
            PSTypeName  = 'PSFramework.NuGet.ModulePath'
            Name        = $Name
            Type        = $typeMap[($ScriptBlock -as [bool])]
            Path        = $Path
            ScriptBlock = $ScriptBlock
            Description = $Description
        }

        if ($Persist) {
            Set-PSFConfig -Module 'PSFramework.NuGet' -Name "ModuleScopes.$Name.Path" -Value $Path -PassThru | Register-PSFConfig
            if ($Description) {
                Set-PSFConfig -Module 'PSFramework.NuGet' -Name "ModuleScopes.$Name.Description" -Value $Description -PassThru | Register-PSFConfig
            }
        }

        if (-not $Mode) { return }

        $envPaths = $env:PSModulePath -split ';'
        if ($Path -in $envPaths) { return }
        switch ($Mode) {
            'Append' {
                $envPaths = @($envPaths) + $Path
            }
            'Prepend' {
                $envPaths = @($Path) + $envPaths
            }
        }
        $env:PSModulePath = $envPaths -join ';'
    }
}

function Save-PSFModule {
    <#
    .SYNOPSIS
        Downloads modules to a specified path.
     
    .DESCRIPTION
        Downloads modules to a specified path.
        Supports flexible repository resolution, modern versioning and deployment to remote systems.
 
        When specifying remote computers, all file transfer is performed via PSRemoting only.
 
        ErrorAction is only honored for local deployments.
     
    .PARAMETER Name
        Name of the module to download.
     
    .PARAMETER Version
        Version constrains for the module to save.
        Will use the latest version available within the limits.
        Examples:
        - "1.0.0": EXACTLY this one version
        - "1.0.0-1.999.999": Any version between the two limits (including the limit values)
        - "[1.0.0-2.0.0)": Any version greater or equal to 1.0.0 but less than 2.0.0
        - "2.3.0-": Any version greater or equal to 2.3.0.
 
        Supported Syntax:
        <Prefix><Version><Connector><Version><Suffix>
 
        Prefix: "[" (-ge) or "(" (-gt) or nothing (-ge)
        Version: A valid version of 2-4 elements or nothing
        Connector: A "," or a "-"
        Suffix: "]" (-le) or ")" (-lt) or nothing (-le)
     
    .PARAMETER Prerelease
        Whether to include prerelease versions in the potential results.
     
    .PARAMETER Path
        Where to store the modules.
        If used together with the -ComputerName parameter, this is considered a local path from within the context of a remoting session to that computer,
        If you want to deploy a module to "\\server1\C$\Scripts\Modules" provide "C:\Scripts\Modules" as -Path, with "-ComputerName server1".
        Unless you actually WANT to deploy without remoting but with SMB (in which case do not provide a -ComputerName)
        See examples for less confusion :)
     
    .PARAMETER ComputerName
        The computers to deploy the modules to.
        Accepts both names or established PSRemoting sessions.
        The -Path parameter will be considered as a local path from within a remoting session.
        If you want to deploy a module to "\\ComputerName\C$\Scripts\Modules" provide "C:\Scripts\Modules" as -Path.
        See examples for less confusion :)
 
        If you provide names, by default this module will connect to the "Microsoft.PowerShell" configuration name.
        To change that name, use the 'PSFramework.NuGet.Remoting.DefaultConfiguration' configuration setting.
     
    .PARAMETER SkipDependency
        Do not include any dependencies.
        Works with PowerShellGet V1/V2 as well.
     
    .PARAMETER AuthenticodeCheck
        Whether modules must be correctly signed by a trusted source.
        Uses "Get-PSFModuleSignature" for validation.
        Defaults to: $false
        Default can be configured under the 'PSFramework.NuGet.Install.AuthenticodeSignature.Check' setting.
     
    .PARAMETER Force
        Redeploy a module that already exists in the target path.
        By default it will skip modules that do already exist in the target path.
     
    .PARAMETER Credential
        The credentials to use for connecting to the Repository (NOT the remote computers).
 
    .PARAMETER RemotingCredential
        The credentials to use for connecting to remote computers we want to deploy modules to via remoting.
        These will NOT be used for repository access.
     
    .PARAMETER ThrottleLimit
        Up to how many computers to deploy the modules to in parallel.
        Defaults to: 5
        Default can be configured under the 'PSFramework.NuGet.Remoting.Throttling' setting.
     
    .PARAMETER Repository
        Repositories to install from. Respects the priority order of repositories.
        See Get-PSFRepository for available repositories (and their priority).
        Lower numbers are installed from first.
     
    .PARAMETER TrustRepository
        Whether we should trust the repository installed from and NOT ask users for confirmation.
     
    .PARAMETER Type
        What type of repository to download from.
        V2 uses classic Save-Module.
        V3 uses Save-PSResource.
        Availability depends on the installed PSGet module versions and configured repositories.
        Use Install-PSFPowerShellGet to deploy the latest versions of the package modules.
 
        Only the version on the local computer matters, even when deploying to remote computers.
     
    .PARAMETER InputObject
        The module to install.
        Takes the output of Get-Module, Find-Module, Find-PSResource and Find-PSFModule, to specify the exact version and name of the module.
        Even when providing a locally available version, the module will still be downloaded from the repositories chosen.
 
    .PARAMETER PathInternal
        For internal use only.
        Used to pass scope-based path resolution from Install-PSFModule into Save-PSFModule.
 
    .PARAMETER Cmdlet
        The $PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this command from the user.
        Should be used when trying to hide Save-PSFModule - e.g. when called from Install-PSFModule.
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .EXAMPLE
        PS C:\> Save-PSFModule EntraAuth -Path C:\temp
         
        Downloads the module "EntraAuth" to the local C:\temp path.
 
    .EXAMPLE
        PS C:\> Save-PSFModule -Name EntraAuth -Path 'C:\Program Files\WindowsPowerShell\Modules'
 
        Downloads the latest version of EntraAuth and places it where both PowerShell versions look for modules.
 
    .EXAMPLE
        PS C:\> Save-PSFModule -Name EntraAuth -Path 'C:\Program Files\WindowsPowerShell\Modules' -Force
 
        Downloads the latest version of EntraAuth and places it where both PowerShell versions look for modules.
        If the module has already been installed previously in the same version, it will replace the old install with the newly downloaded one.
 
    .EXAMPLE
        PS C:\> Save-PSFModule -Name EntraAuth -Path '\\server1\C$\Program Files\WindowsPowerShell\Modules'
 
        Downloads the latest version of EntraAuth and places it where both PowerShell versions look for modules ... on computer "server1".
        File transfer happens via SMB - lets hope that works.
 
    .EXAMPLE
        PS C:\> Save-PSFModule -Name EntraAuth -Path 'C:\Program Files\WindowsPowerShell\Modules' -ComputerName server1
 
        Downloads the latest version of EntraAuth and places it where both PowerShell versions look for modules ... on computer "server1".
        File transfer happens via PSRemoting, assuming our account has local admin rights on the remote computer.
 
    .EXAMPLE
        PS C:\> Save-PSFModule -Name EntraAuth -Path 'C:\Program Files\WindowsPowerShell\Modules' -ComputerName server1 -RemotingCredential $cred
 
        Downloads the latest version of EntraAuth and places it where both PowerShell versions look for modules ... on computer "server1".
        File transfer happens via PSRemoting, assuming the account in $cred has local admin rights on the remote computer.
 
    .EXAMPLE
        PS C:\> Save-PSFModule -Name EntraAuth -Path '/usr/local/share/powershell/Modules' -ComputerName $sessions
 
        Downloads the latest version of EntraAuth and places it where both PowerShell versions look for modules on linux distributions ... on the computers previously connected.
        On PowerShell 7, these can be remoting sessions established via SSH.
        File transfer happens via PSRemoting.
    #>

    [CmdletBinding(PositionalBinding = $false, DefaultParameterSetName = 'ByName', SupportsShouldProcess = $true)]
    Param (
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'ByName')]
        [string[]]
        $Name,

        [Parameter(ParameterSetName = 'ByName')]
        [string]
        $Version,

        [Parameter(ParameterSetName = 'ByName')]
        [switch]
        $Prerelease,

        [Parameter(Mandatory = $true, Position = 1)]
        [string]
        $Path,

        [PSFComputer[]]
        $ComputerName,

        [switch]
        $SkipDependency,

        [switch]
        $AuthenticodeCheck = (Get-PSFConfigValue -FullName 'PSFramework.NuGet.Install.AuthenticodeSignature.Check'),

        [switch]
        $Force,

        [PSCredential]
        $Credential,

        [PSCredential]
        $RemotingCredential,

        [ValidateRange(1, [int]::MaxValue)]
        [int]
        $ThrottleLimit = (Get-PSFConfigValue -FullName 'PSFramework.NuGet.Remoting.Throttling'),

        [PsfArgumentCompleter('PSFramework.NuGet.Repository')]
        [string[]]
        $Repository = ((Get-PSFrepository).Name | Sort-Object -Unique),

        [switch]
        $TrustRepository,

        [ValidateSet('All', 'V2', 'V3')]
        [string]
        $Type = 'All',

        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ByObject')]
        [object[]]
        $InputObject,

        [Parameter(DontShow = $true)]
        $PathInternal,

        [Parameter(DontShow = $true)]
        $Cmdlet = $PSCmdlet
    )
    
    begin {
        $repositories = Resolve-Repository -Name $Repository -Type $Type -Cmdlet $Cmdlet # Terminates if no repositories found
        if ($PathInternal) {
            $resolvedPaths = $PathInternal
            $shouldProcessMessage = "Saving modules to $(@($PathInternal)[0].Scope)"
        }
        else {
            $shouldProcessMessage = "Saving modules to $Path"
            $managedSessions = New-ManagedSession -ComputerName $ComputerName -Credential $RemotingCredential -Cmdlet $Cmdlet -Type Temporary
            if ($ComputerName -and -not $managedSessions) {
                Stop-PSFFunction -String 'Save-PSFModule.Error.NoComputerValid' -StringValues ($ComputerName -join ', ') -EnableException ($ErrorActionPreference -eq 'Stop') -Cmdlet $Cmdlet
                return
            }
            $resolvedPaths = Resolve-RemotePath -Path $Path -ComputerName $managedSessions.Session -ManagedSession $managedSessions -TargetHandling Any -Cmdlet $Cmdlet # Errors for bad paths, terminates if no path
        }
        
        $tempDirectory = New-PSFTempDirectory -Name Staging -ModuleName PSFramework.NuGet
    }
    process {
        if (Test-PSFFunctionInterrupt) { return }

        try {
            $installData = switch ($PSCmdlet.ParameterSetName) {
                ByObject { Resolve-ModuleTarget -InputObject $InputObject -Cmdlet $Cmdlet }
                ByName { Resolve-ModuleTarget -Name $Name -Version $Version -Prerelease:$Prerelease -Cmdlet $Cmdlet }
            }
            if (-not $Cmdlet.ShouldProcess(($installData.TargetName -join ', '), $shouldProcessMessage)) {
                return
            }
            
            Save-StagingModule -InstallData $installData -Path $tempDirectory -Repositories $repositories -Cmdlet $Cmdlet -Credential $Credential -SkipDependency:$SkipDependency -AuthenticodeCheck:$AuthenticodeCheck -TrustRepository:$TrustRepository
            Publish-StagingModule -Path $tempDirectory -TargetPath $resolvedPaths -Force:$Force -Cmdlet $Cmdlet -ThrottleLimit $ThrottleLimit
        }
        finally {
            # Cleanup Managed sessions only if created locally. With -PathInternal, managed sessions are managed by the caller.
            if (-not $PathInternal) {
                $managedSessions | Where-Object Type -EQ 'Temporary' | ForEach-Object Session | Remove-PSSession
            }
            Remove-PSFTempItem -Name Staging -ModuleName PSFramework.NuGet
        }
    }
}

function Update-PSFModuleManifest {
    <#
    .SYNOPSIS
        Modifies an existing module manifest.
     
    .DESCRIPTION
        Modifies an existing module manifest.
        The manifest in question must have a ModuleVersion and a RootModule entry present.
     
    .PARAMETER Path
        Path to the manifest file to modify.
     
    .PARAMETER Guid
        The guid of the module.
        Usually has no effect.
     
    .PARAMETER Author
        The author that wrote the module.
     
    .PARAMETER CompanyName
        The company that owns the module (if any).
     
    .PARAMETER Copyright
        The Copyright short-string.
        Example: 'Copyright (c) 2025 Contoso ltd.'
     
    .PARAMETER RootModule
        The root file of the module.
        For script based modules, that would be the psm1 file. For binary modules the root .dll file.
        Paths relative to the module root path.
        Examples:
        - MyModule.psm1
        - bin\MyModule.dll
     
    .PARAMETER ModuleVersion
        The version of the module.
        Most package services reject module uploads with versions that already exist in the service.
     
    .PARAMETER Description
        The description the module should include.
        A description is required for successful module uploads.
        Most package services use the description field to explain the module in their module lists.
     
    .PARAMETER ProcessorArchitecture
        The architecture thhe module requires.
        Do not provide unless you actually use hardware features for a specific architecture set.
     
    .PARAMETER CompatiblePSEditions
        What PowerShell editions this module is compatible with.
        - Desktop: Windows PowerShell
        - Core: PowerShell 6+
        Has little effect, other than documentation.
        When set to "Desktop"-only, loading the module into a core session will lead to it being imported into an implicit remoting session instead.
     
    .PARAMETER PowerShellVersion
        The minimum version of PowerShell to require for your module.
        There is no option to define a maximum version.
        To declare "this module only runs on Windows PowerShell" use -CompatiblePSEditions instead.
     
    .PARAMETER ClrVersion
        What minimum version of the Common Language Runtime you require.
        If this has you wondering "What is the Common Language Runtime" you do not need to specify this parameter.
        If it does not, you still probably won't need it.
     
    .PARAMETER DotNetFrameworkVersion
        What version of the .NET Framework to require as a minimum.
        A pointless requirement compared to requiring a minimum version of PowerShell.
        Usually not necessary.
     
    .PARAMETER PowerShellHostName
        What PowerShell host your module requires.
        This can enforce your module only being loaded into a specific hosting process, such as "This only works in the Powershell ISE".
        Use this to read the name you need to provide here:
        $host.Name
        Usually only useful for modules that act as PlugIn for the PowerShell ISE.
 
        Example values:
        - "ConsoleHost"
        - "Windows PowerShell ISE Host"
     
    .PARAMETER PowerShellHostVersion
        The minimum version of the host you require.
        Use this to read the current version of a host:
        $host.Version -as [string]
     
    .PARAMETER RequiredModules
        What modules your module requires to run.
        Taking a dependency like this means, that when someone installs your module, they also automatically
        download all the dependencies without needing additional input.
        Can either take a string or a hashtable with the default module definitions (see below):
 
        Examples:
        - "PSFramework" # any version of the PSFramework
        - @{ ModuleName = "PSFramework"; ModuleVersion = "1.2.346" } # The module "PSFramework" with AT LEAST version 1.2.346
        - @{ ModuleName = "PSFramework"; RequiredVersion = "1.2.346" } # The module "PSFramework" with EXACTLY version 1.2.346
 
        Generally it is recommended to NOT use "RequiredVersion" unless as an emergency stopgap while you try to fix a compatibility issue.
        Using "RequiredVersion" significantly raises the risk of conflict between modules taking a dependency on the same module.
        It also prevents updating the dependency independently, which your users may need to do (e.g. critical security patch) without waiting on you.
 
        Generally, it is recommended to be cautious about what module you take a dependency on, when you do not control the dependency.
        For non-public modules, you can minimize the risk of breaking things by having an internal repository and testing new versions
        of modules you take a dependency on, before introducing them into your environment.
     
    .PARAMETER TypesToProcess
        Type extension XML to load when importing the module.
        These allow you to add methods and properties to existing objects, without calling Add-Member on each of them.
        For more details, see: https://learn.microsoft.com/en-us/powershell/scripting/developer/cmdlet/extending-output-objects
     
    .PARAMETER FormatsToProcess
        Format definition XML to load when importing the module.
        These allow you to determine how objects your commands return should be displayed.
        For more details, see: https://learn.microsoft.com/en-us/powershell/scripting/developer/format/formatting-file-overview
        You can use the module PSModuleDevelopment and its "New-PSMDFormatTableDefinition" command to auto-generate the XML
        for your objects.
     
    .PARAMETER ScriptsToProcess
        Any scripts to run before your module imports.
        Any failure here will stop the module import.
        This should NOT be used to load nested files of your module project!
        Generally, this parameter is not needed, instead place the import sequence in your psm1 file.
 
        For an example layout that does that, check out the PSModuleDevelopment module's default module template:
        Invoke-PSMDTemplate MiniModule
     
    .PARAMETER RequiredAssemblies
        Assemblies you require.
        These DLL files will be loaded as part of your import sequence.
        Failure to do so (e.g. file not found, or dependency not found) will cause your module import to fail.
        Can be the name of an assembly from GAC or the relative path to the file within your module's folder layout.
     
    .PARAMETER FileList
        List of files your module contains.
        Documentation only, has no effect.
     
    .PARAMETER ModuleList
        The modules included in your module.
        Generally not needed.
     
    .PARAMETER FunctionsToExport
        What functions your module makes available.
        Functions are PowerShell-native commands written in script code and usually the main point of writing a module.
        You should not export '*', as that makes it hard for PowerShell to know what commands your module exposes.
        This will lead to issues with automatically importing it when just running a command by name from your module.
     
    .PARAMETER AliasesToExport
        What aliases your module makes available.
        Aliases not listed here will not lead to automatic module import if needed.
        Do not export '*'.
     
    .PARAMETER VariablesToExport
        Not really used, no point in doing so.
     
    .PARAMETER CmdletsToExport
        Cmdlets your module makes available.
        Cmdlets are PowerShell-native commands written in C#* and compiled into a .DLL
        This is usually only needed when writing a binary module in C# or a hybrid module with a significant
        portion of compiled code.
 
        *Usually. Technically, other languages are also possible, but they all must be compiled into an assembly.
     
    .PARAMETER DscResourcesToExport
        What DSC resources your module provides.
        If you are wondering what DSC (Desired State Configuration) is, you are probably missing out, but this parameter
        is not (yet) for you.
     
    .PARAMETER Tags
        Tags to include in your module.
        Modules in nuget repositories can be searched by their tag.
     
    .PARAMETER LicenseUri
        The link to the license your module uses.
        This will be shown in the PSGallery and is usually a good idea to include in your module manifest.
     
    .PARAMETER IconUri
        The link to the icon to display with your module.
        Only affects how the module is displayed in the PSGallery.
     
    .PARAMETER ProjectUri
        The link to your project.
        This will be shown in the PSGallery and is usually a good idea to include in your module manifest.
     
    .PARAMETER ReleaseNotes
        What changed in the latest version of your module?
        Either provide the change text or the link to where your changes are being tracked.
     
    .PARAMETER Prerelease
        The prerelease tag, such as "Alpha" or "RC1".
        Including this will hide your module in most repositories by flagging it as a prerelease version.
        Only uses who include "-AllowPrerelease" in their Install-PSFModule call will install this version.
        Adding this is a good way to provide a test preview power users can test, without affecting the broader audience right away.
     
    .PARAMETER ExternalModuleDependencies
        Modules your own module requires, that are not distributed via powershell repositories.
        For example, if your module requires the "ActiveDirectory" module, this is the place to specify it.
        Generally only needed for modules not distribtued via gallery, such as RSAT tools to manage windows features or
        vendor modules that require you to deploy the module via installer.
 
        Uses the same module notation syntax as "-RequiredModules".
     
    .PARAMETER HelpInfoUri
        Where to get more information about your module.
     
    .PARAMETER DefaultCommandPrefix
        Default prefix to include with commands in your module.
        Generally not recommended for use.
     
    .PARAMETER NestedModules
        DO NOT USE.
        DON'T.
        IT'S A MISTAKE.
        CEASE AND DESIST!
 
        Nested modules allow you to include a module inside of your own module, which will be invisible to outsiders.
        Compared to traditional dependencies via RequiredModules this has the advantage of you getting EXACTLY the version
        you are expecting.
        Theoretically, this sounds good - it gives you the full control over what module version, zero risk of accidental breakage
        when the original author updates the module.
        Right?
        Not really.
 
        The key issue is, that most modules cannot coexist in different versions of the same module in the same process or at
        least runspace. The module you include as a NestedModule can - and WILL - still conflict with other modules requiring
        the same dependency.
        So you still get all the same version conflicts a RequiredModule with "RequiredVersion" defined has, but with horribly
        worse error message to the user (who is not aware of a potential conflict AND IS NOT INFORMED OF A CONFLICT!!!).
         
        By whatever is holy, sacred or venerable to you, please do not use NestedModules.
     
    .PARAMETER PassThru
        Rather than modifying the file, return the new manifest text as string.
     
    .PARAMETER Cmdlet
        The PSCmdlet variable of the calling command, used to ensure errors happen within the scope of the caller, hiding this command from the user.
     
    .PARAMETER Continue
        In case of error, when not specifying ErrorAction as stop, this command will call the continue statement.
        By default, it will just end with a warning.
        This parameter makes it easier to integrate in some flow control scenarios but is mostly intended for internal use only.
     
    .EXAMPLE
        PS C:\> Update-PSFModuleManifest -Path .\MyModule\MyModule.psd1 -FunctionsToExport $functions.BaseName
         
        Updates MyModule.psd1 to export the functions stored in $functions.
        This will _replace_ the existing entries.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PsfValidateScript('PSFramework.Validate.FSPath.File', ErrorString = 'PSFramework.Validate.FSPath.File')]
        [string]
        $Path,

        [guid]
        $Guid,

        [string]
        $Author,

        [string]
        $CompanyName,

        [string]
        $Copyright,

        [string]
        $RootModule,
        
        [version]
        $ModuleVersion,

        [string]
        $Description,

        [ValidateSet('', 'X86', 'Amd64')]
        [string]
        $ProcessorArchitecture,

        [ValidateSet('Core', 'Desktop')]
        [string[]]
        $CompatiblePSEditions,

        [version]
        $PowerShellVersion,
        
        [version]
        $ClrVersion,

        [version]
        $DotNetFrameworkVersion,

        [string]
        $PowerShellHostName,

        [version]
        $PowerShellHostVersion,

        [object[]]
        $RequiredModules,

        [string[]]
        $TypesToProcess,

        [string[]]
        $FormatsToProcess,

        [string[]]
        $ScriptsToProcess,

        [string[]]
        $RequiredAssemblies,

        [string[]]
        $FileList,

        [object[]]
        $ModuleList,

        [string[]]
        $FunctionsToExport,

        [string[]]
        $AliasesToExport,

        [string[]]
        $VariablesToExport,

        [string[]]
        $CmdletsToExport,

        [string[]]
        $DscResourcesToExport,

        [string[]]
        $Tags,
        
        [string]
        $LicenseUri,
        
        [string]
        $IconUri,

        [string]
        $ProjectUri,

        [string]
        $ReleaseNotes,

        [string]
        $Prerelease,

        [object[]]
        $ExternalModuleDependencies,

        [uri]
        $HelpInfoUri,

        [string]
        $DefaultCommandPrefix,

        [object[]]
        $NestedModules,

        [switch]
        $PassThru,

        [Parameter(DontShow = $true)]
        $Cmdlet = $PSCmdlet,

        [Parameter(DontShow = $true)]
        [switch]
        $Continue
    )
    begin {
        #region Utility Functions
        function ConvertTo-ModuleRequirement {
            [OutputType([System.Collections.Specialized.OrderedDictionary])]
            [CmdletBinding()]
            param (
                [Parameter(ValueFromPipeline = $true)]
                [AllowEmptyCollection()]
                [AllowNull()]
                [AllowEmptyString()]
                $InputObject,

                [bool]
                $EnableException,

                $Cmdlet
            )
            process {
                foreach ($item in $InputObject) {
                    if (-not $item) { continue }

                    if ($item -is [string]) { $item; continue }

                    if (-not $item.ModuleName) {
                        Stop-PSFFunction -String 'Update-PSFModuleManifest.Error.InvalidModuleReference' -StringValues $item -Target $item -EnableException $EnableException -Cmdlet $Cmdlet -Category InvalidArgument -Continue
                    }

                    $data = [ordered]@{ ModuleName = $item.ModuleName }
                    if ($item.RequiredVersion) { $data.RequiredVersion = '{0}' -f $item.RequiredVersion }
                    elseif ($item.ModuleVersion) { $data.ModuleVersion = '{0}' -f $item.ModuleVersion }

                    $data
                }
            }
        }
        function Update-ManifestProperty {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
            [OutputType([System.Management.Automation.Language.ScriptBlockAst])]
            [CmdletBinding()]
            param (
                [Parameter(Mandatory = $true)]
                [System.Management.Automation.Language.Ast]
                $Ast,

                [Parameter(Mandatory = $true)]
                [string]
                $Property,

                [Parameter(Mandatory = $true)]
                $Value,

                [Parameter(Mandatory = $true)]
                [ValidateSet('String', 'StringArray', 'HashtableArray')]
                [string]
                $Type
            )

            $mainHash = $Ast.FindAll({
                    $args[0] -is [System.Management.Automation.Language.HashtableAst] -and
                    $args[0].KeyValuePairs.Item1.Value -contains 'RootModule' -and
                    $args[0].KeyValuePairs.Item1.Value -Contains 'ModuleVersion'
                }, $true)

            $entry = $mainhash.KeyValuePairs | Where-Object { $_.Item1.Value -eq $Property }
            $stringValue = switch ($Type) {
                'String' { "$Value" | ConvertTo-Psd1 }
                'StringArray' { , @(, @($Value)) | ConvertTo-Psd1 }
                'HashtableArray' { , @(, @($Value)) | ConvertTo-Psd1 }
            }
            $format = '{0}'
            #region Case: Key Already Exists
            if ($entry) {
                $start = $entry.Item2.Extent.StartOffset
                $end = $entry.Item2.Extent.EndOffset
            }
            #endregion Case: Key Already Exists

            #region Case: Key Does not exist
            else {
                $line = $Ast.Extent.Text -split "`n" | Where-Object { $_ -match "#\s+$Property = " }
                # Entry already exists but is commented out
                if ($line) {
                    $format = "$Property = {0}"
                    $index = $Ast.Extent.Text.IndexOf($line)
                    $start = $index + $line.Length - $line.TrimStart().Length
                    $end = $index + $line.Length
                }
                # Entry does not exist already
                else {
                    $indent = ($Ast.Extent.Text -split "`n" | Where-Object { $_ -match "^\s+ModuleVersion" }) -replace '^(\s*).+$', '$1'
                    $format = "$($indent)$($Property) = {0}`n"
                    $start = $mainHash.Extent.EndOffset - 1
                    $end = $mainHash.Extent.EndOffset - 1
                }
            }
            #endregion Case: Key Does not exist

            $newText = $Ast.Extent.Text.SubString(0, $start) + ($format -f $stringValue) + $Ast.Extent.Text.SubString($end)
            [System.Management.Automation.Language.Parser]::ParseInput($newText, [ref]$null, [ref]$null)
        }
        
        function Update-PrivateDataProperty {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
            [OutputType([System.Management.Automation.Language.ScriptBlockAst])]
            [CmdletBinding()]
            param (
                [Parameter(Mandatory = $true)]
                [System.Management.Automation.Language.Ast]
                $Ast,

                [string[]]
                $Tags,
                
                [string]
                $LicenseUri,
                
                [string]
                $IconUri,
                
                [string]
                $ProjectUri,

                [string]
                $ReleaseNotes,

                [string]
                $Prerelease,

                [object[]]
                $ExternalModuleDependencies,

                [bool]
                $EnableException,
                $Cmdlet
            )

            $mainHash = $Ast.FindAll({
                    $args[0] -is [System.Management.Automation.Language.HashtableAst] -and
                    $args[0].KeyValuePairs.Item1.Value -contains 'RootModule' -and
                    $args[0].KeyValuePairs.Item1.Value -Contains 'ModuleVersion'
                }, $true)

            $privateData = [ordered]@{
                PSData = [ordered]@{ }
            }
            $replacements = @{ }
    
            $privateDataAst = $mainHash.KeyValuePairs | Where-Object { $_.Item1.Value -eq 'PrivateData' } | ForEach-Object { $_.Item2.PipelineElements[0].Expression }

            if ($privateDataAst) {
                foreach ($pair in $privateDataAst.KeyValuePairs) {
                    if ($pair.Item1.Value -ne 'PSData') {
                        $id = "%PSF_$(Get-Random)%"
                        $privateData[$pair.Item1.Value] = $id
                        $replacements[$id] = $pair.Item2.Extent.Text
                        continue
                    }

                    foreach ($subPair in $pair.Item2.PipelineElements[0].Expression.KeyValuePairs) {
                        $id = "%PSF_$(Get-Random)%"
                        $privateData.PSData[$subPair.Item1.Value] = $id
                        $replacements[$id] = $subPair.Item2.Extent.Text
                    }
                }
            }

            if ($Tags) { $privateData.PSData['Tags'] = $Tags }
            if ($LicenseUri) { $privateData.PSData['LicenseUri'] = $LicenseUri }
            if ($IconUri) { $privateData.PSData['IconUri'] = $IconUri }
            if ($ProjectUri) { $privateData.PSData['ProjectUri'] = $ProjectUri }
            if ($ReleaseNotes) { $privateData.PSData['ReleaseNotes'] = $ReleaseNotes }
            if ($Prerelease) { $privateData.PSData['Prerelease'] = $Prerelease }
            if ($ExternalModuleDependencies) { $privateData.PSData['ExternalModuleDependencies'] = ConvertTo-ModuleRequirement -InputObject $ExternalModuleDependencies -Cmdlet $Cmdlet -EnableException $EnableException }

            $privateDataString = $privateData | ConvertTo-Psd1 -Depth 5
            foreach ($pair in $replacements.GetEnumerator()) {
                $privateDataString = $privateDataString -replace "'$($pair.Key)'", $pair.Value
            }

            if (-not $privateDataAst) {
                $newManifest = $ast.Extent.Text.Insert(($mainHash.Extent.EndOffset - 1), "PrivateData = $privateDataString`n")
            }
            else {
                $newManifest = $ast.Extent.Text.SubString(0, $privateDataAst.Extent.StartOffset) + $privateDataString + $ast.Extent.Text.SubString($privateDataAst.Extent.EndOffset)
            }
            [System.Management.Automation.Language.Parser]::ParseInput($newManifest, [ref]$null, [ref]$null)
        }
        #endregion Utility Functions
    }
    process {
        $killIt = $ErrorActionPreference -eq 'Stop'

        $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$null, [ref]$null)

        $mainHash = $ast.FindAll({
                $args[0] -is [System.Management.Automation.Language.HashtableAst] -and
                $args[0].KeyValuePairs.Item1.Value -contains 'RootModule' -and
                $args[0].KeyValuePairs.Item1.Value -Contains 'ModuleVersion'
            }, $true)

        if (-not $mainHash) {
            Stop-PSFFunction -String 'Update-PSFModuleManifest.Error.BadManifest' -StringValues (Get-Item -Path $Path).BaseName, $Path -Cmdlet $Cmdlet -EnableException $killIt -Continue:$Continue
            return
        }

        #region Main Properties
        $stringProperties = 'Guid', 'Author', 'CompanyName', 'Copyright', 'RootModule', 'ModuleVersion', 'Description', 'ProcessorArchitecture', 'PowerShellVersion', 'ClrVersion', 'DotNetFrameworkVersion', 'PowerShellHostName', 'PowerShellHostVersion', 'HelpInfoUri', 'DefaultCommandPrefix'
        foreach ($property in $stringProperties) {
            if ($PSBoundParameters.Keys -notcontains $property) { continue }
            $ast = Update-ManifestProperty -Ast $ast -Property $property -Value $PSBoundParameters.$property -Type String
        }
        $stringArrayProperties = 'CompatiblePSEditions', 'TypesToProcess', 'FormatsToProcess', 'ScriptsToProcess', 'RequiredAssemblies', 'FileList', 'FunctionsToExport', 'AliasesToExport', 'VariablesToExport', 'CmdletsToExport', 'DscResourcesToExport'
        foreach ($property in $stringArrayProperties) {
            if ($PSBoundParameters.Keys -notcontains $property) { continue }
            $ast = Update-ManifestProperty -Ast $ast -Property $property -Value $PSBoundParameters.$property -Type StringArray
        }
        $moduleProperties = 'RequiredModules', 'ModuleList', 'NestedModules'
        foreach ($property in $moduleProperties) {
            if ($PSBoundParameters.Keys -notcontains $property) { continue }
            $ast = Update-ManifestProperty -Ast $ast -Property $property -Value ($PSBoundParameters.$property | ConvertTo-ModuleRequirement -EnableException $killIt -Cmdlet $Cmdlet) -Type StringArray
        }
        #endregion Main Properties
        
        #region PrivateData Content
        if ($Tags -or $LicenseUri -or $IconUri -or $ProjectUri -or $ReleaseNotes -or $Prerelease -or $ExternalModuleDependencies) {
            $updateParam = $PSBoundParameters | ConvertTo-PSFHashtable -ReferenceCommand Update-PrivateDataProperty
            $updateParam.Cmdlet = $Cmdlet
            $updateParam.EnableException = $killIt
            $ast = Update-PrivateDataProperty -Ast $ast @updateParam
        }
        #endregion PrivateData Content

        if ($PassThru) { $ast.Extent.Text }
        else { $ast.Extent.Text | Set-Content -Path $Path }
    }
}

function Get-PSFPowerShellGet {
    <#
    .SYNOPSIS
        Returns the availability state for PowerShellGet.
     
    .DESCRIPTION
        Returns the availability state for PowerShellGet.
        Will verify, whether required prerequisites for module installation or publishing exist
        for v1/v2 versions of PowerShellGet.
        It will only check for the all users configuration, ignoring binaries stored in appdata.
     
    .PARAMETER ComputerName
        The computer to scan.
        Defaults to localhost.
     
    .PARAMETER Credential
        Credentials to use for the connection to the remote computers.
     
    .EXAMPLE
        PS C:\> Get-PSFPowerShellGet
         
        Returns, what the local PowerShellGet configuration is like.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(ValueFromPipeline = $true)]
        [PSFComputer[]]
        $ComputerName = $env:COMPUTERNAME,

        [PSCredential]
        $Credential
    )
    
    begin {
        $code = {
            $modules = Get-Module -Name PowerShellGet -ListAvailable
            $modulesV3 = Get-Module -Name Microsoft.PowerShell.PSResourceGet -ListAvailable

            $isOnWindows = $PSVersionTable.PSVersion.Major -lt 6 -or $isWindows
            if ($isOnWindows) {
                $nugetPath = "$env:ProgramFiles\Microsoft\Windows\PowerShell\PowerShellGet\NuGet.exe"
                $v2CanInstall = Test-Path -Path "$env:ProgramFiles\PackageManagement\ProviderAssemblies\nuget\2.8.5.208\Microsoft.PackageManagement.NuGetProvider.dll"
            }
            else {
                $nugetPath = "$HOME/.config/powershell/powershellget/NuGet.exe"
                $v2CanInstall = $true
            }
            if ($modules | Where-Object { $_.Version.Major -lt 3 -and $_.Version -ge ([version]'2.5.0') }) {
                $v2CanInstall = $true
            }

            [PSCustomObject]@{
                PSTypeName = 'PSFramework.NuGet.GetReport'
                ComputerName = $env:COMPUTERNAME
                V2           = ($modules | Where-Object { $_.Version.Major -lt 3 }) -as [bool]
                V3           = $modulesV3 -as [bool]
                V2CanInstall = $v2CanInstall
                V2CanPublish = Test-Path -Path $nugetPath
                Modules      = $modules
            }
        }
    }
    process {
        Invoke-PSFCommand -ComputerName $ComputerName -ScriptBlock $code -Credential $Credential
    }
}

function Install-PSFPowerShellGet {
    <#
    .SYNOPSIS
        Deploys the different versions of PowerShellGet and PSResourceGet.
     
    .DESCRIPTION
        Deploys the different versions of PowerShellGet and PSResourceGet.
        With this command you can bulk-deploy PowerShell package management at scale.
 
        It can install:
        + latest version of PowerShellGet & PackageManagement (elsewhere referred to as V2/classic)
        + binaries needed to use PowerShellGet & PackageManagement without bootstrapping from the internet
        + latest version of Microsoft.PowerShell.PSResourceget (elsewhere referred to as V3/modern)
 
        It can do all that via PSRemoting, no SMB access needed.
        This command needs no internet access to deploy them - you can transport it into an offline environment and still profit from that.
     
    .PARAMETER Type
        What should be deployed/installed.
        + V2Binaries: What is required to use Get V2.
        + V2Latest: The latest version of Get V2
        + V3Latest: The latest version of Get V3
        Defaults to: V2Binaries
     
    .PARAMETER ComputerName
        The computer(s) to install to.
        Can be names, ADComputer objects, SQL Server connection strings or alreadya established PSSessions.
        Defaults to: localhost
     
    .PARAMETER Credential
        Credentials to use for establishing new remoting connections.
     
    .PARAMETER SourcePath
        Custom Path to get the module sources to deplo.
        You can download the latest module & binary versions from an online machine and then transport them into an offline environment.
        This allows you to update the version of Get V3 being deployed, without having to update (or wait for an update) of PSFramework.NuGet.
     
    .PARAMETER Offline
        Force a full offline mode.
        By default, the module will on install automatically try to check online for a newer version.
        It will still continue anyway if this fails, but if you want to avoid the network traffic & signals, use this switch.
     
    .PARAMETER NotInternal
        Do not use the internally provided PowerShellGet module versions.
        This REQUIRES you to either provide the module data via -SourcePath or to have live online access.
     
    .EXAMPLE
        PS C:\> Install-PSFPowerShell -Type V3Latest -ComputerName (Get-ADComputer -Filter * -SearchBase $myOU)
         
        This will install the latest version of PSResourceGet (V3) on all computers under the OU distinguishedName stored in $myOU
    #>

    [CmdletBinding()]
    Param (
        [ValidateSet('V2Binaries', 'V2Latest', 'V3Latest')]
        [string[]]
        $Type = 'V2Binaries',

        [Parameter(ValueFromPipeline = $true)]
        [PSFComputer[]]
        $ComputerName = $env:COMPUTERNAME,

        [PSCredential]
        $Credential,

        [string]
        $SourcePath = (Join-Path -Path (Get-PSFPath -Name AppData) -ChildPath 'PowerShell/PSFramework/modules/PowerShellGet'),

        [switch]
        $Offline,

        [switch]
        $NotInternal
    )
    
    begin {
        #region Functions
        function Resolve-PowerShellGet {
            [OutputType([hashtable])]
            [CmdletBinding()]
            param (
                [string]
                $Type,

                [string]
                $SourcePath,

                [switch]
                $Offline,

                [switch]
                $NotInternal
            )

            #region V2Binaries
            if ('V2Binaries' -eq $Type) {
                @{
                    Type    = $Type
                    NuGet   = [System.IO.File]::ReadAllBytes("$script:ModuleRoot\bin\NuGet.exe")
                    PkgMgmt = [System.IO.File]::ReadAllBytes("$script:ModuleRoot\bin\Microsoft.PackageManagement.NuGetProvider.dll")
                }
                return
            }
            #endregion V2Binaries

            $internalVersion = Get-Content -Path "$script:ModuleRoot\modules\modules.json" | ConvertFrom-Json
            if ($NotInternal) { $internalVersion = @{ } }
            $sourceVersion = @{ }
            $onlineVersion = @{ }
            $sourceFile = Join-Path -Path $SourcePath -ChildPath modules.json
            if (Test-Path -Path $sourceFile) {
                $sourceVersion = Get-Content -Path $sourceFile | ConvertFrom-Json
            }

            #region Check Online
            if (-not $Offline) {
                $links = @(
                    'PSGetV2'
                    'PSGetV3'
                    'PSPkgMgmt'
                )

                foreach ($link in $links) {
                    $resolvedUrl = Resolve-AkaMsLink -Name $link
                    if (-not $resolvedUrl) { continue }

                    $onlineVersion[$link] = [PSCustomObject]@{
                        Type     = $link
                        Name     = ($resolvedUrl -split '/')[-2]
                        Version  = ($resolvedUrl -split '/')[-1]
                        Resolved = $resolvedUrl
                        FileName = ''
                    }
                    $onlineVersion[$link].FileName = '{0}-{1}.zip' -f $onlineVersion[$link].Name, $onlineVersion[$link].Version
                }
            }
            #endregion Check Online
        
            $source = 'Internal'
            $typeTag = switch ($Type) {
                'V2Latest' { 'PSGetV2' }
                'V3Latest' { 'PSGetV3' }
            }
            if ($sourceVersion.$typeTag.Version -and $sourceVersion.$typeTag.Version -ne $internalVersion.$typeTag.Version) {
                $source = 'Source'
            }
            if ($onlineVersion.$typeTag.Version -and $onlineVersion.$typeTag.Version -ne $internalVersion.$typeTag.Version) {
                $source = 'Online'
            }
            
            # If online version is newer than internal, download to appdata as cached version
            if ('Online' -eq $source) {
                if (-not (Test-Path -Path $SourcePath)) { $null = New-Item -Path $SourcePath -ItemType Directory -Force }
                Save-PSFPowerShellGet -Path $SourcePath # This can never happen if the user specified a path, so no risk of overwriting.
            }

            $rootPath = switch ($source) {
                Internal { "$script:ModuleRoot\modules" }
                Source { $SourcePath }
                Online { $SourcePath }
            }

            $actualConfiguration = Import-PSFPowerShellDataFile -Path (Join-Path -Path $rootPath -ChildPath 'modules.json')
            $data = @{
                Type = $Type
                Config = $actualConfiguration
            }
            switch ($Type) {
                'V2Latest' {
                    $data.PSGetV2 = [System.IO.File]::ReadAllBytes((Join-Path -Path $rootPath -ChildPath $actualConfiguration.PSGetV2.FileName))
                    $data.PSPkgMgmt = [System.IO.File]::ReadAllBytes((Join-Path -Path $rootPath -ChildPath $actualConfiguration.PSPkgMgmt.FileName))
                }
                'V3Latest' {
                    $data.PSGetV3 = [System.IO.File]::ReadAllBytes((Join-Path -Path $rootPath -ChildPath $actualConfiguration.PSGetV3.FileName))
                }
            }
            $data
        }
        #endregion Functions

        #region Actual Code
        $code = {
            param (
                $Data
            )

            #region Functions
            function Install-ZipModule {
                [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
                [CmdletBinding()]
                param (
                    $Config,

                    [string]
                    $ModulesFolder,

                    [string]
                    $TempFolder
                )

                $modulePath = Join-Path -Path $ModulesFolder -ChildPath ('{0}/{1}' -f $Config.Name, ($Config.Version -replace '\-.+$'))

                if (Test-Path -Path $modulePath) { return }

                $null = New-Item -Path $modulePath -ItemType Directory -Force
                Expand-Archive -Path (Join-Path -Path $TempFolder -ChildPath $Config.FileName) -DestinationPath $modulePath
            }
            #endregion Functions

            ## Create temporary folder
            $tempFolder = (New-Item -Path $env:TEMP -Name "PSGet-$(Get-Random)" -ItemType Directory -Force).FullName

            #region Write binary data
            if ($Data.NuGet) {
                [System.IO.File]::WriteAllBytes((Join-Path -Path $tempFolder -ChildPath 'NuGet.exe'), $Data.NuGet)
            }
            if ($Data.PkgMgmt) {
                [System.IO.File]::WriteAllBytes((Join-Path -Path $tempFolder -ChildPath 'Microsoft.PackageManagement.NuGetProvider.dll'), $Data.PkgMgmt)
            }
            if ($Data.PSGetV2) {
                [System.IO.File]::WriteAllBytes((Join-Path -Path $tempFolder -ChildPath $Data.Config.PSGetV2.FileName), $Data.PSGetV2)
            }
            if ($Data.PSGetV3) {
                [System.IO.File]::WriteAllBytes((Join-Path -Path $tempFolder -ChildPath $Data.Config.PSGetV3.FileName), $Data.PSGetV3)
            }
            if ($Data.PSPkgMgmt) {
                [System.IO.File]::WriteAllBytes((Join-Path -Path $tempFolder -ChildPath $Data.Config.PSPkgMgmt.FileName), $Data.PSPkgMgmt)
            }
            #endregion Write binary data

            #region Copy to destination
            $isOnWindows = $PSVersionTable.PSVersion.Major -lt 6 -or $isWindows
            switch ($Data.Type) {
                #region V2 Bootstrap
                V2Binaries {
                    if ($isOnWindows) {
                        if (-not (Test-Path -Path "$env:ProgramFiles\Microsoft\Windows\PowerShell\PowerShellGet")) {
                            $null = New-Item -Path "$env:ProgramFiles\Microsoft\Windows\PowerShell\PowerShellGet" -ItemType Directory -Force
                        }
                        Copy-Item -Path (Join-Path -Path $tempFolder -ChildPath 'NuGet.exe') -Destination "$env:ProgramFiles\Microsoft\Windows\PowerShell\PowerShellGet" -Force
                        if (-not (Test-Path -Path "$env:ProgramFiles\PackageManagement\ProviderAssemblies\nuget\2.8.5.208")) {
                            $null = New-Item -Path "$env:ProgramFiles\PackageManagement\ProviderAssemblies\nuget\2.8.5.208" -ItemType Directory -Force
                        }
                        Copy-Item -Path (Join-Path -Path $tempFolder -ChildPath 'Microsoft.PackageManagement.NuGetProvider.dll') -Destination "$env:ProgramFiles\PackageManagement\ProviderAssemblies\nuget\2.8.5.208" -Force
                    }
                    else {
                        Copy-Item -Path (Join-Path -Path $tempFolder -ChildPath 'NuGet.exe') -Destination "$HOME/.config/powershell/powershellget" -Force
                    }
                }
                #endregion V2 Bootstrap

                #region V2 Latest
                V2Latest {
                    $modulesFolder = "$env:ProgramFiles\WindowsPowerShell\modules"
                    if (-not $isOnWindows) { $modulesFolder = "/usr/local/share/powershell/Modules" }

                    Install-ZipModule -Config $data.Config.PSGetV2 -ModulesFolder $modulesFolder -TempFolder $tempFolder
                    Install-ZipModule -Config $data.Config.PSPkgMgmt -ModulesFolder $modulesFolder -TempFolder $tempFolder
                }
                #endregion V2 Latest

                #region V3 Latest
                V3Latest {
                    $modulesFolder = "$env:ProgramFiles\WindowsPowerShell\modules"
                    if (-not $isOnWindows) { $modulesFolder = "/usr/local/share/powershell/Modules" }

                    Install-ZipModule -Config $data.Config.PSGetV3 -ModulesFolder $modulesFolder -TempFolder $tempFolder
                }
                #endregion V3 Latest
            }
            #endregion Copy to destination

            ## Cleanup
            Remove-Item -Path $tempFolder -Recurse -Force
        }
        #endregion Actual Code

        #region Resolve Source Configuration
        $stayOffline = $Offline
        $useInternal = -not $NotInternal
        if ($PSBoundParameters.Keys -contains 'SourcePath') {
            if ($PSBoundParameters.Keys -notcontains 'Offline') {
                $stayOffline = $true
            }
            if ($PSBoundParameters.Keys -notcontains 'NotInternal') {
                $useInternal = $false
            }
        }
        #endregion Resolve Source Configuration
    }
    process {
        # If installing the latest V2 modules, you'll also want the binaries needed
        if ('V2Latest' -in $Type -and 'V2Binaries' -notin $Type) {
            $Type = @($Type) + 'V2Binaries'
        }

        foreach ($typeEntry in $Type) {
            # Get Binaries / Modules to deploy
            $binaries = Resolve-PowerShellGet -Type $typeEntry -Offline:$stayOffline -SourcePath $SourcePath -NotInternal:$useInternal
    
            # Execute Deployment
            Invoke-PSFCommand -ComputerName $ComputerName -ScriptBlock $code -Credential $Credential -ArgumentList $binaries
        }
    }
    end {
        Search-PSFPowerShellGet
    }
}

function Save-PSFPowerShellGet {
    <#
    .SYNOPSIS
        Downloads and provides the latest packages for both PowerShellGet V2 and V3.
     
    .DESCRIPTION
        Downloads and provides the latest packages for both PowerShellGet V2 and V3.
        These can then be used by this module to deploy and bootstrap offline computers with package management tooling.
     
    .PARAMETER Path
        The path where to deploy the module packages as zip-files.
        Must be a directory.
        Defaults to: %AppData%/PowerShell/PSFramework/modules/PowerShellGet
     
    .EXAMPLE
        PS C:\> Save-PSFPowerShellGet
         
        Downloads and deploys the latest version of Get V2 & V3 to "%AppData%/PowerShell/PSFramework/modules/PowerShellGet"
    #>

    [CmdletBinding()]
    param (
        [PsfValidateScript('PSFramework.Validate.FSPath.Folder', ErrorString = 'PSFramework.Validate.FSPath.Folder')]
        [string]
        $Path
    )

    if (-not $Path) {
        $rootPath = Join-Path -Path (Get-PSFPath -Name AppData) -ChildPath 'PowerShell/PSFramework/modules/PowerShellGet'
        if (-not (Test-Path -LiteralPath $rootPath)) {
            $null = New-Item -LiteralPath $rootPath -ItemType Directory -Force
        }
    }

    $links = @(
        'PSGetV2'
        'PSGetV3'
        'PSPkgMgmt'
    )

    $pkgData = @{ }

    foreach ($link in $links) {
        $resolvedUrl = Resolve-AkaMsLink -Name $link
        if (-not $resolvedUrl) {
            Stop-PSFFunction -String 'Save-PowerShellGet.Error.UnableToResolve' -StringValues $link -EnableException $true -Cmdlet $PSCmdlet
        }

        $pkgData[$link] = [PSCustomObject]@{
            Type = $link
            Name = ($resolvedUrl -split '/')[-2]
            Version = ($resolvedUrl -split '/')[-1]
            Resolved = $resolvedUrl
            FileName = ''
        }
        $pkgData[$link].FileName = '{0}-{1}.zip' -f $pkgData[$link].Name, $pkgData[$link].Version
    }

    $directory = New-PSFTempDirectory -Name psget -ModuleName PSFramework.NuGet
    foreach ($entry in $pkgData.Values) {
        Invoke-WebRequest -Uri $entry.Resolved -OutFile "$directory\temp-$($entry.Type).zip"
        $rootFolder = "$directory\$($entry.Type)"
        Expand-Archive -Path "$directory\temp-$($entry.Type).zip" -DestinationPath $rootFolder -Force

        # Cleanup nupkg residue
        $contentTypesPath = Join-Path -Path $rootFolder -ChildPath '[Content_Types].xml'
        Remove-Item -LiteralPath $contentTypesPath # LiteralPath so that the brackets don't interfere
        $relsPath = Join-Path -Path $rootFolder -ChildPath '_rels'
        Remove-Item -LiteralPath $relsPath -Force -Recurse
        $specPath = Join-Path -Path $rootFolder -ChildPath "$($entry.Name).nuspec"
        Remove-Item -LiteralPath $specPath -Force -Recurse
        $packagePath = Join-Path -Path $rootFolder -ChildPath 'package'
        Remove-Item -LiteralPath $packagePath -Force -Recurse

        # Cleanup Original download zip
        Remove-Item "$directory\temp-$($entry.Type).zip"

        # Create new zip file and delete old folder
        Compress-Archive -Path "$rootFolder\*" -DestinationPath "$directory\$($entry.FileName)"
        Remove-Item -LiteralPath $rootFolder -Recurse -Force
    }
    $pkgData | ConvertTo-Json | Set-Content -Path "$directory\modules.json"

    Copy-Item -Path $directory\* -Destination $Path -Force -Recurse
    Remove-PSFTempItem -Name psget -ModuleName PSFramework.NuGet
}

function Search-PSFPowerShellGet {
    <#
    .SYNOPSIS
        Scan for available PowerShellGet versions.
     
    .DESCRIPTION
        Scan for available PowerShellGet versions.
        The module caches the availability of PowerShellGet features on import.
        It also automatically updates those settings when it knows to do so.
 
        However, if you change the configuration outside of the PSFramework.NuGet module,
        you may need to manually trigger the scan for the module to take the changes into account.
 
        For example, if you use Install-Module, rather than Install-PSFModule to install the
        latest version of PowerShellGet, use this command to make the module aware of the fact.
        Otherwise, this will automatically be run the next time the module is loaded.
     
    .PARAMETER UseCache
        Whether to respect the already available data and not do anything after all.
        Mostly for internal use.
     
    .EXAMPLE
        PS C:\> Search-PSFPowerShellGet
         
        Scan for available PowerShellGet versions.
    #>

    [CmdletBinding()]
    Param (
        [switch]
        $UseCache
    )
    
    process {
        if ($UseCache -and $script:psget.Count -gt 0) { return }

        $configuration = Get-PSFPowerShellGet
        $script:psget = @{
            'v2'           = $configuration.V2
            'v2CanInstall' = $configuration.V2CanInstall
            'v2CanPublish' = $configuration.V2CanPublish
            'v3'           = $configuration.V3
        }
    }
}


function Get-PSFRepository {
    <#
    .SYNOPSIS
        Lists available PowerShell repositories.
     
    .DESCRIPTION
        Lists available PowerShell repositories.
        Includes both classic (V2 | Get-PSRepository) and new (V3 | Get-PSResourceRepository) repositories.
        This will also include additional metadata, including priority, which in this module is also applicable to classic repositories.
 
        Note on Status:
        In V2 repositories, the status can show "NoPublish" or "NoInstall".
        This is determined by whether it has been bootstrapped at the system level.
        If you have already bootstrapped it in user-mode, this may not be reflected correctly.
        If your computer is internet-facing, it can also automatically bootstrap itself without any issues.
     
    .PARAMETER Name
        Name of the repository to list.
     
    .PARAMETER Type
        What kind of repository to return:
        + All: (default) Return all, irrespective of type
        + V2: Only return classic repositories, as would be returned by Get-PSRepository
        + V3: Only return modern repositories, as would be returned by Get-PSResourceRepository
     
    .EXAMPLE
        PS C:\> Get-PSFRepository
 
        List all available repositories.
    #>

    [CmdletBinding()]
    Param (
        [PsfArgumentCompleter('PSFramework.NuGet.Repository')]
        [string[]]
        $Name = '*',

        [ValidateSet('All','V2','V3')]
        [string]
        $Type = 'All'
    )
    
    begin {
        Search-PSFPowerShellGet -UseCache
    }
    process {
        if ($script:psget.V3 -and $Type -in 'All','V3') {
            foreach ($repository in Get-PSResourceRepository -Name $Name -ErrorAction Ignore) {
                if (-not $repository) { continue }
                [PSCustomObject]@{
                    PSTypeName = 'PSFramework.NuGet.Repository'
                    Name       = $repository.Name
                    Type       = 'V3'
                    Status     = 'OK'
                    Trusted    = $repository.Trusted
                    Priority   = Get-PSFConfigValue -FullName "PSFramework.NuGet.Repositories.$($repository.Name).Priority" -Fallback $repository.Priority
                    Uri        = $repository.Uri
                    Object     = $repository
                    Credential = Get-PSFConfigValue -FullName "PSFramework.NuGet.Repositories.$($repository.Name).Credential"
                }
            }
        }
        if ($script:psget.V2 -and $Type -in 'All','V2') {
            $status = 'OK'
            if (-not $script:psget.v2CanPublish) { $status = 'NoPublish' }
            if (-not $script:psget.v2CanInstall) { $status = 'NoInstall' }

            foreach ($repository in Get-PSRepository -Name $Name -ErrorAction Ignore) {
                if (-not $repository) { continue }
                [PSCustomObject]@{
                    PSTypeName = 'PSFramework.NuGet.Repository'
                    Name       = $repository.Name
                    Type       = 'V2'
                    Status     = $status
                    Trusted    = $repository.Trusted
                    Priority   = Get-PSFConfigValue -FullName "PSFramework.NuGet.Repositories.$($repository.Name).Priority" -Fallback 100
                    Uri        = $repository.SourceLocation
                    Object     = $repository
                    Credential = Get-PSFConfigValue -FullName "PSFramework.NuGet.Repositories.$($repository.Name).Credential"
                }
            }
        }
    }
}

function Set-PSFRepository {
    <#
    .SYNOPSIS
        Configure existing powershell repositories or define a new one.
     
    .DESCRIPTION
        Configure existing powershell repositories or define a new one.
        This allows you to modify their metadata, notably registering credentials to use on all requests or modifying its priority.
        For defining new repositories, it is required to at least define "Type" and "Uri"
         
        Some updates - the Uri and Trusted state - require updating the configuration on the PSGet repository settings, rather than just being contained within this module.
        The command will handle that, which will be slightly slower and also affect direct use of the PSGet commands (such as install-Module or Install-PSResource).
 
        Settings will apply to all repositories with the same name.
        If you have the same repository configured in both V2 and V3, they BOTH will receive the update.
     
    .PARAMETER Name
        Name of the repository to modify.
        Wildcards not supported (unless you actually name a repository with a wildcard in the name. In which case you probably want reconsider your naming strategy.)
     
    .PARAMETER Priority
        The priority the repository should have.
        Lower-numbered repositories will beu sed before repositories with higher numbers.
     
    .PARAMETER Credential
        Credentials to use on all requests against the repository.
 
    .PARAMETER Uri
        The Uri from which modules are installed (and to which they are published).
        Will update the PSGet repositories objects.
 
    .PARAMETER Trusted
        Whether the repository is considered trusted.
 
    .PARAMETER Type
        What version of PSGet it should use.
        - Any: Will register as V3 if available, otherwise V2. Will not update to V3 if already on V2.
        - Update: Will register under highest version available, upgrading from older versions if already available on old versions
        - All: Will register on ALL available versions
        - V2: Will only register on V2. V3 - if present and configured - will be unregistered.
        - V2Preferred: Will only register on V2. If V2 does not exist, existing V3 repositories will be allowed.
        - V3: Will only register on V3. If V2 is present, it will be unregistered, irrespective of whether V3 is available.
     
    .PARAMETER Persist
        Whether the settings should be remembered.
        If settings are not persisted, they only last until the console is closed.
        When persisting credentials, they are - at least on windows - stored encrypted in registry (HKCU) and are only readable by the same user on the same computer.
     
    .EXAMPLE
        PS C:\> Set-PSFRepository -Name AzDevOps -Credential $cred
 
        Assigns for the repository "AzDevOps" the credentials stored in $cred.
        All subsequent PSGet calls through this module will be made using those credentials.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding(PositionalBinding = $false)]
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [PsfArgumentCompleter('PSFramework.NuGet.Repository')]
        [string]
        $Name,

        [int]
        $Priority,

        [PSCredential]
        $Credential,

        [string]
        $Uri,

        [bool]
        $Trusted,

        [ValidateSet('Any', 'Update', 'All', 'V2', 'V2Preferred', 'V3')]
        [string]
        $Type,

        [switch]
        $Persist
    )
    process {
        # Not all changes require a repository update run
        $mustUpdate = $false

        if ($PSBoundParameters.Keys -contains 'Priority') {
            Set-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Priority" -Value $Priority
            if ($Persist) { Register-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Priority" }
        }
        if ($PSBoundParameters.Keys -contains 'Credential') {
            Set-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Credential" -Value $Credential
            if ($Persist) { Register-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Credential" }
        }

        if ($Uri) {
            Set-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Uri" -Value $Uri
            $mustUpdate = $true
            if ($Persist) { Register-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Uri" }
        }
        if ($PSBoundParameters.Keys -contains 'Trusted') {
            Set-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Trusted" -Value $Trusted
            $mustUpdate = $true
            if ($Persist) { Register-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Trusted" }
        }
        if ($Type) {
            Set-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Type" -Value $Type
            $mustUpdate = $true
            if ($Persist) { Register-PSFConfig -FullName "PSFramework.NuGet.Repositories.$($Name).Type" }
        }

        if ($mustUpdate) {
            Update-PSFRepository
        }
    }
}

function Update-PSFRepository {
    <#
    .SYNOPSIS
        Executes configured repository settings.
     
    .DESCRIPTION
        Executes configured repository settings.
        Using configuration settings - for example applied per GPO or configuration file - it is possible to define intended repositories.
 
        The configuration settings must be named as 'PSFramework.NuGet.Repositories.<Repository Name>.<Setting>'
 
        Available settings:
        - Uri: Url or filesystem path to the repository. Used for both install and publish.
        - Priority: Priority of a PowerShell Repository. Numeric value, determines repository precedence.
        - Type: What kind of PowerShellGet version to apply the configuration to. Details on the options below. Defaults to 'Any'.
        - Trusted: Whether the repository should be trusted. Can be set to 0, 1, $false or $true. Defaults to $true.
        - Present: Whether the repository should exist at all. Can be set to 0, 1, $false or $true. Defaults to $true.
                   Allows creating delete orders. Does not differentiate between V2 & V3
        - Proxy: Link to the proxy to use. Property only available when creating a new repository, not for updating an existing one.
        
        Supported "Type" settings to handle different PowerShellGet versions:
        - Any: Will register as V3 if available, otherwise V2. Will not update to V3 if already on V2.
        - Update: Will register under highest version available, upgrading from older versions if already available on old versions
        - All: Will register on ALL available versions
        - V2: Will only register on V2. V3 - if present and configured - will be unregistered.
        - V2Preferred: Will only register on V2. If V2 does not exist, existing V3 repositories will be allowed.
        - V3: Will only register on V3. If V2 is present, it will be unregistered, irrespective of whether V3 is available.
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .EXAMPLE
        PS C:\> Update-PSFRepository
         
        Executes configured repository settings, creating, updating and deleting repositories as defined.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    Param (
    
    )
    
    begin {
        #region Functions
        function Compare-Repository {
            [CmdletBinding()]
            param (
                $Actual,
                $Configured
            )

            $supportedTypes = 'Any', 'Update', 'All', 'V2', 'V2Preferred', 'V3'

            foreach ($configuredRepo in $Configured) {
                # Incomplete configurations are processed in a limited manner, as not enough information is present to create or delete
                $isIncomplete = -not $configuredRepo.Type -or -not $configuredRepo.Uri
                
                if (-not $isIncomplete -and $configuredRepo.Type -notin $supportedTypes) {
                    Write-PSFMessage -Level Warning -String 'Update-PSFRepository.Error.InvalidType' -StringValues $configuredRepo.Type, ($supportedTypes -join ', ')
                    continue
                }

                $matching = $Actual | Where-Object Name -EQ $configuredRepo._Name
                # An incomplete configuration can only be used to modify an existing repository, so skip if nothing matches
                if ($isIncomplete -and -not $matching) { continue }

                if (-not $isIncomplete) {
                    $shouldExist = -not ($configuredRepo.PSObject.Properties.Name -contains 'Present' -and -not $configuredRepo.Present)

                    $mayBeV2 = $configuredRepo.Type -in 'Any', 'Update', 'All', 'V2', 'V2Preferred'
                    if ('Update' -eq $configuredRepo.Type -and $script:psget.V3) { $mayBeV2 = $false }
                    $mustBeV2 = $configuredRepo.Type -in 'All', 'V2'
                    $mayBeV3 = $configuredRepo.Type -in 'Any', 'Update', 'All', 'V3', 'V2Preferred'
                    if ('V2Preferred' -eq $configuredRepo.Type -and $script:psget.V2) { $mayBeV3 = $false }
                    $mustBeV3 = $configuredRepo.Type -in 'Update', 'All', 'V3'

                    # Case: Should not exist and does not
                    if (-not $shouldExist -and -not $matching) {
                        continue
                    }

                    #region Deletion
                    foreach ($matchingRepo in $matching) {
                        if (
                            # Should exist
                            $shouldExist -and
                            (
                                $matchingRepo.Type -eq 'V2' -and $mayBeV2 -or
                                $matchingRepo.Type -eq 'V3' -and $mayBeV3
                            )
                        ) {
                            continue
                        }

                        [PSCustomObject]@{
                            Type       = 'Delete'
                            Configured = $configuredRepo
                            Actual     = $matchingRepo
                            Changes    = @{ }
                        }
                    }
                    if (-not $shouldExist) { continue }
                    #endregion Deletion

                    #region Creation
                    # Case: Should exist but does not
                    if ($shouldExist -and -not $matching) {
                        [PSCustomObject]@{
                            Type       = 'Create'
                            Configured = $configuredRepo
                            Actual     = $null
                            Changes    = @{ }
                        }
                        continue
                    }

                    # Case: Must exist on V2 but does not
                    if ($mustBeV2 -and $matching.Type -notcontains 'V2' -and $script:psget.V2) {
                        [PSCustomObject]@{
                            Type       = 'Create'
                            Configured = $configuredRepo
                            Actual     = $matching
                            Changes    = @{ }
                        }
                    }

                    # Case: Must exist on V3 but does not
                    if ($mustBeV3 -and $matching.Type -notcontains 'V3' -and $script:psget.V3) {
                        [PSCustomObject]@{
                            Type       = 'Create'
                            Configured = $configuredRepo
                            Actual     = $matching
                            Changes    = @{ }
                        }
                    }
                    
                    # If there is no matching, existing repository, there is no need to update
                    if (-not $matching) { continue }
                    #endregion Creation
                }

                #region Update
                foreach ($matchingRepo in $matching) {
                    $intendedUri = $configuredRepo.Uri
                    if ('V2' -eq $matchingRepo.Type) { $intendedUri = $intendedUri -replace 'v3/index.json$', 'v2' }
                    $trusted = $configuredRepo.Trusted -as [int]
                    if ($null -eq $trusted -and $configuredRepo.Trusted -in 'True', 'False') {
                        $trusted = $configuredRepo.Trusted -eq 'True'
                    }
                    if ($null -eq $trusted) { $trusted = $true }
                    
                    $changes = @{ }
                    if (-not $isIncomplete -and $matchingRepo.Uri -ne $intendedUri) { $changes.Uri = $intendedUri }
                    if ($matchingRepo.Trusted -ne $trusted) { $changes.Trusted = $trusted -as [bool] }
                    if (
                        $configuredRepo.Priority -and
                        $matchingRepo.Type -ne 'V2' -and
                        $matchingRepo.Priority -ne $configuredRepo.Priority
                    ) {
                        $changes.Priority = $configuredRepo.Priority
                    }

                    if ($changes.Count -eq 0) { continue }

                    [PSCustomObject]@{
                        Type       = 'Update'
                        Configured = $configuredRepo
                        Actual     = $matchingRepo
                        Changes    = $changes
                    }
                }
                #endregion Update
            }
        }
        
        function New-Repository {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
            [CmdletBinding()]
            param (
                $Change
            )

            $registerV2 = $script:psget.V2
            $registerV3 = $script:psget.V3

            if ($Change.Actual.Type -contains 'V3') { $registerV3 = $false }
            if ($Change.Actual.Type -contains 'V2') { $registerV2 = $false }

            # If any already exists, we obviously want to create the other and need not process types again
            if (-not $Change.Actual) {
                switch ($Change.Configured.Type) {
                    'Any' {
                        if ($registerV3) { $registerV2 = $false }
                    }
                    'Update' {
                        if ($registerV3) { $registerV2 = $false }
                    }
                    'V2' { $registerV3 = $false }
                    'V2Preferrred' { $registerV3 = $false }
                    'V3' { $registerV2 = $false }
                }
            }

            $trusted = $Change.Configured.Trusted -as [int]
            if ($null -eq $trusted -and $Change.Configured.Trusted -in 'True', 'False') {
                $trusted = $Change.Configured.Trusted -eq 'True'
            }
            if ($null -eq $trusted) { $trusted = $true }
            
            if ($registerV2) {
                $uri = $Change.Configured.Uri -replace 'v3/index.json$', 'v2'

                $param = @{
                    Name            = $Change.Configured._Name
                    SourceLocation  = $uri
                    PublishLocation = $uri
                    ErrorAction     = 'Stop'
                }
                if ($trusted) { $param.InstallationPolicy = 'Trusted' }
                if ($Change.Configured.Proxy) { $param.Proxy = $Change.Configured.Proxy }
                try { Register-PSRepository @param }
                catch {
                    Write-PSFMessage -Level Warning -String 'Update-PSFRepository.Register.Failed' -StringValues V2, $param.Name, $uri -ErrorRecord $_
                }
            }
            if ($registerV3) {
                $param = @{
                    Name        = $Change.Configured._Name
                    Uri         = $Change.Configured.Uri
                    Trusted     = $trusted
                    ErrorAction = 'Stop'
                }
                if ($null -ne $Change.Configured.Priority) {
                    $param.Priority = $Change.Configured.Priority
                }
                if ($Change.Configured.Proxy) { $param.Proxy = $Change.Configured.Proxy }
                try { Register-PSResourceRepository @param }
                catch {
                    Write-PSFMessage -Level Warning -String 'Update-PSFRepository.Register.Failed' -StringValues V3, $param.Name, $param.Uri -ErrorRecord $_
                }
            }
        }
        
        function Remove-Repository {
            [CmdletBinding(SupportsShouldProcess = $true)]
            param (
                $Change
            )

            switch ($Change.Actual.Type) {
                'V2' {
                    Invoke-PSFProtectedCommand -ActionString 'Update-PSFRepository.Repository.Unregister' -ActionStringValues $change.Actual.Type, $Change.Actual.Name -ScriptBlock {
                        Unregister-PSRepository -Name $change.Actual.Name -ErrorAction Stop
                    } -Target $change.Actual.Name -PSCmdlet $PSCmdlet -EnableException $false
                }
                'V3' {
                    Invoke-PSFProtectedCommand -ActionString 'Update-PSFRepository.Repository.Unregister' -ActionStringValues $change.Actual.Type, $Change.Actual.Name -ScriptBlock {
                        Unregister-PSResourceRepository -Name $change.Actual.Name -ErrorAction Stop
                    } -Target $change.Actual.Name -PSCmdlet $PSCmdlet -EnableException $false
                }
            }
        }
        
        function Set-Repository {
            [CmdletBinding(SupportsShouldProcess = $true)]
            param (
                $Change
            )

            $param = @{
                Name = $change.Actual.Name
            }
            switch ($Change.Actual.Type) {
                'V2' {
                    if ($Change.Changes.Uri) {
                        $param.SourceLocation = $Change.Changes.Uri
                        $param.PublishLocation = $Change.Changes.Uri
                    }
                    if ($Change.Changes.Keys -contains 'Trusted') {
                        if ($Change.Changes.Trusted) { $param.InstallationPolicy = 'Trusted' }
                        else { $param.InstallationPolicy = 'Untrusted' }
                    }

                    Invoke-PSFProtectedCommand -ActionString 'Update-PSFRepository.Repository.Update' -ActionStringValues $change.Actual.Type, $Change.Actual.Name, ($param.Keys -join ',') -ScriptBlock {
                        Set-PSRepository @param -ErrorAction Stop
                    } -Target $change.Actual.Name -PSCmdlet $PSCmdlet -EnableException $false
                }
                'V3' {
                    if ($Change.Changes.Uri) {
                        $param.Uri = $Change.Changes.Uri
                    }
                    if ($Change.Changes.Keys -contains 'Priority') {
                        $param.Priority = $Change.Changes.Priority
                    }
                    if ($Change.Changes.Keys -contains 'Trusted') {
                        $param.Trusted = $Change.Changes.Trusted
                    }

                    Invoke-PSFProtectedCommand -ActionString 'Update-PSFRepository.Repository.Update' -ActionStringValues $change.Actual.Type, $Change.Actual.Name, ($param.Keys -join ',') -ScriptBlock {
                        Set-PSResourceRepository @param -ErrorAction Stop
                    } -Target $change.Actual.Name -PSCmdlet $PSCmdlet -EnableException $false
                }
            }
        }
        #endregion Functions
    }
    process {
        $repositories = Get-PSFRepository
        $configuredRepositories = Select-PSFConfig -FullName PSFramework.NuGet.Repositories.* -Depth 3
        $changes = Compare-Repository -Actual $repositories -Configured $configuredRepositories
        foreach ($change in $changes) {
            switch ($change.Type) {
                'Create' { New-Repository -Change $change }
                'Delete' { Remove-Repository -Change $change }
                'Update' { Set-Repository -Change $change }
            }
        }
    }
}

function Publish-PSFResourceModule {
    <#
    .SYNOPSIS
        Publishes a pseudo-module, the purpose of which is to transport arbitrary files & folders.
     
    .DESCRIPTION
        Publishes a pseudo-module, the purpose of which is to transport arbitrary files & folders.
        This allows using nuget repositories to distribute arbitrary files, not bound to its direct PowerShell
        use as "Publish-PSFModule" would enforce.
 
        For example, with this, a templating engine could offer commands such as:
        - Publish-Template
        - Install-Template
        - Update-Template
     
    .PARAMETER Name
        Name of the module to create.
     
    .PARAMETER Version
        Version of the module to create.
        Defaults to "1.0.0".
     
    .PARAMETER Path
        Path to the files and folders to include.
     
    .PARAMETER Repository
        The repository to publish to.
     
    .PARAMETER Type
        What kind of repository to publish to.
        - All (default): All types of repositories are eligible.
        - V2: Only repositories from the old PowerShellGet are eligible.
        - V3: Only repositories from the new PSResourceGet are eligible.
        If multiple repositories of the same name are found, the one at the highest version among them is chosen.
     
    .PARAMETER Credential
        The credentials to use to authenticate to the Nuget service.
        Mostly used for internal repository servers.
     
    .PARAMETER ApiKey
        The ApiKey to use to authenticate to the Nuget service.
        Mostly used for publishing to the PSGallery.
     
    .PARAMETER SkipDependenciesCheck
        Do not validate dependencies or the module manifest.
        This removes the need to have the dependencies installed when publishing using PSGet v2
     
    .PARAMETER DestinationPath
        Rather than publish to a repository, place the finished .nupgk file in this path.
        Use when doing the final publish step outside of PowerShell code.
     
    .PARAMETER RequiredModules
        The modules your resource module requires.
        These dependencies will be treated as Resoource modules as well, not regular modules.
     
    .PARAMETER Description
        Description of your resource module.
        Will be shown in repository services hosting it.
     
    .PARAMETER Author
        The author of your resource module.
        Defaults to your user name.
     
    .PARAMETER Tags
        Tags to include in your resource module.
     
    .PARAMETER LicenseUri
        Link to the license governing your resource module.
     
    .PARAMETER IconUri
        Link to the icon to present in the PSGallery.
     
    .PARAMETER ProjectUri
        Link to the project your resources originate from.
        Used in the PSGallery to guide visitors to more information.
     
    .PARAMETER ReleaseNotes
        Release notes for your resource module.
        Or at least a link to them.
     
    .PARAMETER Prerelease
        The prerelease flag to tag your resource module under.
        This allows hiding it from most users.
     
    .EXAMPLE
        PS C:\> Publish-PSFResourceModule -Name Psmd.Template.MyFunction -Version 1.1.0 -Path .\MyFunction\* -Repository PSGallery -ApiKey $key
         
        Publishes all files under the MyFunction folder to the PSGallery.
        The resource module will be named "Psmd.Template.MyFunction" and versioned as '1.1.0'
    #>

    [CmdletBinding(DefaultParameterSetName = 'ToRepository')]
    Param (
        [PsfValidateScript('PSFramework.Validate.SafeName', ErrorString = 'PSFramework.Validate.SafeName')]
        [string]
        $Name,

        [string]
        $Version = '1.0.0',

        [Parameter(Mandatory = $true)]
        [PsfPath]
        $Path,

        [Parameter(Mandatory = $true, ParameterSetName = 'ToRepository')]
        [PsfValidateSet(TabCompletion = 'PSFramework.NuGet.Repository')]
        [PsfArgumentCompleter('PSFramework.NuGet.Repository')]
        [string[]]
        $Repository,

        [Parameter(ParameterSetName = 'ToRepository')]
        [ValidateSet('All', 'V2', 'V3')]
        [string]
        $Type = 'All',

        [Parameter(ParameterSetName = 'ToRepository')]
        [PSCredential]
        $Credential,

        [Parameter(ParameterSetName = 'ToRepository')]
        [string]
        $ApiKey,

        [Parameter(ParameterSetName = 'ToRepository')]
        [switch]
        $SkipDependenciesCheck,

        [Parameter(Mandatory = $true, ParameterSetName = 'ToPath')]
        [PsfDirectory]
        $DestinationPath,

        [object[]]
        $RequiredModules,

        [string]
        $Description = '<Dummy Description>',

        [string]
        $Author,

        [string[]]
        $Tags,

        [string]
        $LicenseUri,

        [string]
        $IconUri,

        [string]
        $ProjectUri,

        [string]
        $ReleaseNotes,

        [string]
        $Prerelease
    )
    
    begin {
        $killIt = $ErrorActionPreference -eq 'Stop'
        $stagingDirectory = New-PSFTempDirectory -ModuleName 'PSFramework.NuGet' -Name Publish.ResourceModule -DirectoryName $Name
        $publishParam = $PSBoundParameters | ConvertTo-PSFHashtable -ReferenceCommand Publish-PSFModule -Exclude Path, ErrorAction
    }
    process {
        try {
            New-DummyModule -Path $stagingDirectory -Name $Name -Version $Version -RequiredModules $RequiredModules -Description $Description -Author $Author
            $resources = New-Item -Path $stagingDirectory -Name Resources -ItemType Directory -Force
            $Path | Copy-Item -Destination $resources.FullName -Recurse -Force -Confirm:$false -WhatIf:$false

            Publish-PSFModule @publishParam -Path $stagingDirectory -ErrorAction Stop
        }
        catch {
            Stop-PSFFunction -String 'Publish-PSFResourceModule.Error' -StringValues $Name, ($Repository -join ', ') -EnableException $killIt -ErrorRecord $_ -Cmdlet $PSCmdlet
            return
        }
        finally {
            Remove-PSFTempItem -ModuleName 'PSFramework.NuGet' -Name Publish.ResourceModule
        }
    }
}


function Save-PSFResourceModule {
    <#
    .SYNOPSIS
    Short description
     
    .DESCRIPTION
    Long description
     
    .PARAMETER Name
        Name of the module to download.
     
    .PARAMETER Version
        Version constrains for the resource to save.
        Will use the latest version available within the limits.
        Examples:
        - "1.0.0": EXACTLY this one version
        - "1.0.0-1.999.999": Any version between the two limits (including the limit values)
        - "[1.0.0-2.0.0)": Any version greater or equal to 1.0.0 but less than 2.0.0
        - "2.3.0-": Any version greater or equal to 2.3.0.
 
        Supported Syntax:
        <Prefix><Version><Connector><Version><Suffix>
 
        Prefix: "[" (-ge) or "(" (-gt) or nothing (-ge)
        Version: A valid version of 2-4 elements or nothing
        Connector: A "," or a "-"
        Suffix: "]" (-le) or ")" (-lt) or nothing (-le)
     
    .PARAMETER Prerelease
        Whether to include prerelease versions in the potential results.
     
    .PARAMETER Path
        Where to store the resource.
     
    .PARAMETER SkipDependency
        Do not include any dependencies.
        Works with PowerShellGet V1/V2 as well.
     
    .PARAMETER AuthenticodeCheck
        Whether resource modules must be correctly signed by a trusted source.
        Uses "Get-PSFModuleSignature" for validation.
        Defaults to: $false
        Default can be configured under the 'PSFramework.NuGet.Install.AuthenticodeSignature.Check' setting.
     
    .PARAMETER Force
        Overwrite files and folders that already exist in the target path.
        By default it will skip modules that do already exist in the target path.
     
    .PARAMETER Credential
        The credentials to use for connecting to the Repository.
     
    .PARAMETER Repository
        Repositories to install from. Respects the priority order of repositories.
        See Get-PSFRepository for available repositories (and their priority).
        Lower numbers are installed from first.
     
    .PARAMETER TrustRepository
        Whether we should trust the repository installed from and NOT ask users for confirmation.
     
    .PARAMETER Type
        What type of repository to download from.
        V2 uses classic Save-Module.
        V3 uses Save-PSResource.
        Availability depends on the installed PSGet module versions and configured repositories.
        Use Install-PSFPowerShellGet to deploy the latest versions of the package modules.
 
        Only the version on the local computer matters, even when deploying to remote computers.
     
    .PARAMETER InputObject
        The resource module to install.
        Takes the output of Get-Module, Find-Module, Find-PSResource and Find-PSFModule, to specify the exact version and name of the resource module.
        Even when providing a locally available version, the resource module will still be downloaded from the repositories chosen.
 
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .EXAMPLE
        PS C:\> Save-PSFResourceModule -Name Psmd.Templates.MiniModule -Path .
 
        Downloads the resource module "Psmd.Templates.MiniModule" and extracts its resources into the current path.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    Param (
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'ByName')]
        [string[]]
        $Name,

        [Parameter(ParameterSetName = 'ByName')]
        [string]
        $Version,

        [Parameter(ParameterSetName = 'ByName')]
        [switch]
        $Prerelease,

        [Parameter(Mandatory = $true, Position = 1)]
        [PSFDirectory]
        $Path,

        [switch]
        $SkipDependency,

        [switch]
        $AuthenticodeCheck = (Get-PSFConfigValue -FullName 'PSFramework.NuGet.Install.AuthenticodeSignature.Check'),

        [switch]
        $Force,

        [PSCredential]
        $Credential,

        [PsfArgumentCompleter('PSFramework.NuGet.Repository')]
        [string[]]
        $Repository = ((Get-PSFrepository).Name | Sort-Object -Unique),

        [switch]
        $TrustRepository,

        [ValidateSet('All', 'V2', 'V3')]
        [string]
        $Type = 'All',

        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ByObject')]
        [object[]]
        $InputObject
    )
    
    begin {
        $killIt = $ErrorActionPreference -eq 'Stop'
    }
    process {
        $tempDirectory = New-PSFTempDirectory -ModuleName 'PSFramework.NuGet' -Name ResourceModule
        try {
            $saveParam = $PSBoundParameters | ConvertTo-PSFHashtable -ReferenceCommand Save-PSFModule -Exclude Path, ErrorAction
            Invoke-PSFProtectedCommand -ActionString 'Save-PSFResourceModule.Downloading' -ActionStringValues ($Name -join ', ') -ScriptBlock {
                $null = Save-PSFModule @saveParam -Path $tempDirectory -ErrorAction Stop -WhatIf:$false -Confirm:$false
            } -PSCmdlet $PSCmdlet -EnableException $killIt -WhatIf:$false -Confirm:$false
            if (Test-PSFFunctionInterrupt) { return }

            foreach ($pathEntry in $Path) {
                foreach ($module in Get-ChildItem -Path $tempDirectory) {
                    foreach ($versionFolder in Get-ChildItem -LiteralPath $module.FullName) {
                        $dataPath = Join-Path -Path $versionFolder.FullName -ChildPath 'Resources'
                        if (-not (Test-Path -Path $dataPath)) {
                            Write-PSFMessage -String 'Save-PSFResourceModule.Skipping.InvalidResource' -StringValues $module.Name, $versionFolder.Name
                            continue
                        }
                        if (-not $PSCmdlet.ShouldProcess("$($module.Name) ($($versionFolder.Name))", "Deploy to $pathEntry")) {
                            continue
                        }

                        foreach ($item in Get-ChildItem -LiteralPath $dataPath) {
                            $targetPath = Join-Path -Path $pathEntry -ChildPath $item.Name
                            if (-not $Force -and (Test-path -Path $targetPath)) {
                                Write-PSFMessage -String 'Save-PSFResourceModule.Skipping.AlreadyExists' -StringValues $module.Name, $versionFolder.Name, $item.Name, $pathEntry
                                continue
                            }

                            Invoke-PSFProtectedCommand -ActionString 'Save-PSFResourceModule.Deploying' -ActionStringValues $module.Name, $versionFolder.Name, $item.Name, $pathEntry -ScriptBlock {
                                Move-Item -LiteralPath $item.FullName -Destination $pathEntry -Force -ErrorAction Stop -Confirm:$false -WhatIf:$false
                            } -Target $item.Name -PSCmdlet $PSCmdlet -EnableException $killIt -Continue -Confirm:$false -WhatIf:$false
                        }
                    }
                }
            }
        }
        finally {
            Remove-PSFTempItem -ModuleName 'PSFramework.NuGet' -Name ResourceModule
        }
    }
}

function Get-PSFModuleSignature {
    <#
    .SYNOPSIS
        Verifies, whether a module is properly signed.
     
    .DESCRIPTION
        Verifies, whether a module is properly signed.
        Iterates over every module file and verifies its signature.
 
        The result reports:
        - Overall signing status
        - Signatures not Timestamped count
        - Status Summary
        - Subject of signing certs summary
        - Issuer of signing certs summary
 
        A module should be considered signed, when ...
        - the over signing status is valid
        - the subjects are expected (A microsoft module being signed by a microsoft code signing cert, etc.)
        - the issuer CAs are expected (A microsoft module being signed by a cert issued by Microsoft, etc.)
     
    .PARAMETER Path
        Path to the module(s) to scan.
        Should be the path to either a module-root or a psd1 file.
     
    .EXAMPLE
        PS C:\> Get-PSFModuleSignature -Path .
         
        Returns, whether the module in the current path is signed.
 
    .EXAMPLE
        PS C:\> Get-PSFModuleSignature -Path \\contoso.com\it\coding\modules\ContosoTools
 
        Verifies the code signing of the module stored in \\contoso.com\it\coding\modules\ContosoTools
 
    .EXAMPLE
        PS C:\> Get-Module | Get-PSFModuleSignature
 
        Verifies for each currently loaded module, whether they are signed.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('ModuleBase', 'FullName')]
        [string[]]
        $Path
    )
    begin {
        function Resolve-ModulePath {
            [CmdletBinding()]
            param (
                [string[]]
                $Path,

                $Cmdlet
            )

            foreach ($pathItem in $Path) {
                try { $resolvedPaths = Resolve-Path $pathItem }
                catch {
                    $record = [System.Management.Automation.ErrorRecord]::new(
                        [Exception]::new("Path not found: $pathItem", $_.Exception),
                        "InvalidPath",
                        [System.Management.Automation.ErrorCategory]::InvalidArgument,
                        $pathItem
                    )
                    $Cmdlet.WriteError($record)
                    continue
                }

                foreach ($resolvedPath in $resolvedPaths) {
                    $item = Get-Item -LiteralPath $resolvedPath
                    if ($item.PSIsContainer) {
                        $manifests = Get-ChildItem -LiteralPath $item.FullName -Filter *.psd1 -Recurse -ErrorAction SilentlyContinue
                        if (-not $manifests) {
                            $record = [System.Management.Automation.ErrorRecord]::new(
                                [Exception]::new("No module found in: $resolvedPath (resolved from $pathItem)"),
                                "ObjectNotFound",
                                [System.Management.Automation.ErrorCategory]::InvalidArgument,
                                $pathItem
                            )
                            $Cmdlet.WriteError($record)
                            continue
                        }

                        foreach ($manifest in $manifests) {
                            $manifest.Directory.FullName
                        }
                        continue
                    }

                    if ($item.Extension -in '.psd1', 'psm1') {
                        $item.Directory.FullName
                        continue
                    }
                    if (Get-Item -Path "$($item.Directory.FullName)\*.psd1") {
                        $item.Directory.FullName
                        continue
                    }

                    $record = [System.Management.Automation.ErrorRecord]::new(
                        [Exception]::new("Unexpected file: $resolvedPaht from $pathItem"),
                        "UnexpectedPath",
                        [System.Management.Automation.ErrorCategory]::InvalidArgument,
                        $pathItem
                    )
                    $Cmdlet.WriteError($record)
                }
            }
        }
        
        function Get-ModuleSignatureInternal {
            [CmdletBinding()]
            param (
                [Parameter(Mandatory = $true)]
                [string]
                $Path
            )

            $signatureStatus = Get-ChildItem -LiteralPath $Path -Recurse -File | ForEach-Object {
                $currentItem = $_.FullName
                try { Get-AuthenticodeSignature -LiteralPath $currentItem }
                catch {
                    [PSCustomObject]@{
                        PSTypeName             = 'System.Management.Automation.Signature'
                        SignerCertificate      = $null
                        TimeStamperCertificate = $null
                        Status                 = 'AccessError'
                        StatusMessage          = $_
                        Path                   = $currentItem
                        SignatureType          = $null
                        IsOSBinary             = $false
                    }
                }
            }

            $manifest = Get-ChildItem -LiteralPath $Path -Filter *.psd1 | Select-Object -First 1
            $manifestData = @{}
            if ($manifest) {
                $manifestData = Import-PowerShellDataFile -LiteralPath $manifest.FullName
            }

            [PSCustomObject]@{
                ModuleBase       = $Path
                Name             = $manifest.BaseName
                Version          = $manifestData.ModuleVersion
                IsSigned         = -not @($signatureStatus).Where{ $_.Status -notin 'Valid', 'UnknownError' }
                FileCount        = @($signatureStatus).Count
                NoTimestampCount = @($signatureStatus).Where{ $_.SignerCertificate -and -not $_.TimeStamperCertificate }.Count
                ByStatus         = ConvertTo-SigningSummary -Results $signatureStatus -Type Status
                ByIssuer         = ConvertTo-SigningSummary -Results $signatureStatus -Type Issuer
                BySubject        = ConvertTo-SigningSummary -Results $signatureStatus -Type Subject
                Signatures       = $signatureStatus
            }
        }
        function ConvertTo-SigningSummary {
            [CmdletBinding()]
            param (
                [AllowEmptyCollection()]
                $Results,

                [Parameter(Mandatory = $true)]
                [ValidateSet('Issuer', 'Subject', 'Status')]
                [string]
                $Type
            )

            $groupBy = @{
                Issuer  = { $_.SignerCertificate.Issuer }
                Subject = { $_.SignerCertificate.Subject }
                Status  = 'Status'
            }

            $groups = $Results | Group-Object $groupBy[$Type]
            $hash = @{ }
            foreach ($group in $groups) {
                $hash[$group.Name] = $group.Group
            }
            $entry = [PSCustomObject]@{
                TotalCount = @($Results).Count
                GroupCount = @($groups).Count
                Results    = $hash
            }
            Add-Member -InputObject $entry -MemberType ScriptMethod -Name ToString -Force -Value {
                $lines = foreach ($pair in $this.Results.GetEnumerator()) {
                    if (-not $pair.Key) { continue }
                    if ($pair.Key -eq 'UnknownError') { '{0}: {1} (Usually: File format that cannot be signed)' -f $pair.Value.Count, $pair.Key }
                    else { '{0}: {1}' -f $pair.Value.Count, $pair.Key }
                }
                $lines -join "`n"
            }
            $entry
        }
    }
    process {
        foreach ($inputPath in Resolve-ModulePath -Path $Path -Cmdlet $PSCmdlet | Sort-Object -Unique) {
            Get-ModuleSignatureInternal -Path $inputPath
        }
    }
}

<#
This is an example configuration file
 
By default, it is enough to have a single one of them,
however if you have enough configuration settings to justify having multiple copies of it,
feel totally free to split them into multiple files.
#>


<#
# Example Configuration
Set-PSFConfig -Module 'PSFramework.NuGet' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'"
#>


Set-PSFConfig -Module 'PSFramework.NuGet' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging."
Set-PSFConfig -Module 'PSFramework.NuGet' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments."

Set-PSFConfig -Module 'PSFramework.NuGet' -Name 'Install.AuthenticodeSignature.Check' -Value $false -Initialize -Validation 'bool' -Description 'Whether on installation or download of module its code-signing will be checked first.'
Set-PSFConfig -Module 'PSFramework.NuGet' -Name 'Remoting.DefaultConfiguration' -Value 'Microsoft.PowerShell' -Initialize -Validation string -Description 'The PSSessionConfiguration to use when initializing new PS remoting sessions'
Set-PSFConfig -Module 'PSFramework.NuGet' -Name 'Remoting.Throttling' -Value 5 -Initialize -Validation integerpositive -Description 'Up to how many remote computers to deploy to in parallel.'

<#
Stored scriptblocks are available in [PsfValidateScript()] attributes.
This makes it easier to centrally provide the same scriptblock multiple times,
without having to maintain it in separate locations.
 
It also prevents lengthy validation scriptblocks from making your parameter block
hard to read.
 
Set-PSFScriptblock -Name 'PSFramework.NuGet.ScriptBlockName' -Scriptblock {
     
}
#>


Register-PSFTeppScriptblock -Name 'PSFramework.NuGet.ModuleScope' -ScriptBlock {
    foreach ($scope in Get-PSFModuleScope) {
        @{ Text = $scope.Name; ToolTip = $scope.Description }
    }
} -Global

<#
# Example:
Register-PSFTeppScriptblock -Name "PSFramework.NuGet.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' }
#>


Register-PSFTeppScriptblock -Name "PSFramework.NuGet.Repository" -ScriptBlock {
    foreach ($repository in Get-PSFRepository | Group-Object Name) {
        @{
            Text = $repository.Name
            Tooltip = '[{0}] {1}' -f (($repository.Group.Type | Sort-Object) -join ', '), $repository.Name
        }
    }
}

<#
# Example:
Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name PSFramework.NuGet.alcohol
#>


# What Get Version is available
<#
$script:psget = @{
    'v2' = $configuration.V2
    'v2CanInstall' = $configuration.V2CanInstall
    'v2CanPublish' = $configuration.V2CanPublish
    'v3' = $configuration.V3
}
#>

$script:psget = @{ }

# What paths are available for Install-PSFModule's "-Scope" parameter
$script:moduleScopes = @{ }

# Static Override values provided when using Disable-ModuleCommand
$script:ModuleCommandReturns = @{ }

# Load what PowerShellGet versions are available
Search-PSFPowerShellGet

# Ensure all configured repositories exist, and all unintended repositories are gone
Update-PSFRepository

New-PSFLicense -Product 'PSFramework.NuGet' -Manufacturer 'Friedrich Weinmann' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2023-03-13") -Text @"
Copyright (c) 2023 Friedrich Weinmann
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"@


$code = {
    if ($PSVersionTable.PSVersion.Major -le 5) {
        return "$([Environment]::GetFolderPath("ProgramFiles"))\WindowsPowerShell\Modules"
    }
    if ($IsWindows) {
        return "$([Environment]::GetFolderPath("ProgramFiles"))\PowerShell\Modules"
    }
    '/usr/local/share/powershell/Modules'
}
$scopeParam = @{
    Name = 'AllUsers'
    ScriptBlock = $code
    Description = 'Default path for modules visible to all users.'
}
Register-PSFModuleScope @scopeParam

$code = {
    if ($PSVersionTable.PSVersion.Major -le 5) {
        return "$([Environment]::GetFolderPath("ProgramFiles"))\WindowsPowerShell\Modules"
    }
    if ($IsWindows) {
        return "$([Environment]::GetFolderPath("ProgramFiles"))\WindowsPowerShell\Modules"
    }
    '/usr/local/share/powershell/Modules'
}
$scopeParam = @{
    Name = 'AllUsersWinPS'
    ScriptBlock = $code
    Description = 'Default PS 5.1 path for modules visible to all users. Modules will be available to all versions of PowerShell. Will still work on non-Windows systems, but be no different to "AllUsers".'
}
Register-PSFModuleScope @scopeParam

$code = {
    if ($PSVersionTable.PSVersion.Major -le 5) {
        return "$([Environment]::GetFolderPath("MyDocuments"))\WindowsPowerShell\Modules"
    }
    if ($IsWindows) {
        return "$([Environment]::GetFolderPath("MyDocuments"))\PowerShell\Modules"
    }
    '~/.local/share/powershell/Modules'
}
$scopeParam = @{
    Name = 'CurrentUser'
    ScriptBlock = $code
    Description = 'Default path for modules visible to the current user only.'
}
Register-PSFModuleScope @scopeParam

foreach ($setting in Select-PSFConfig -FullName PSFramework.NuGet.ModuleScopes.* -Depth 3) {
    $regParam = @{
        Name = $setting._Name
        Path = $setting.Path
    }
    if ($setting.Description) { $regParam.Description = $setting.Description }
    Register-PSFModuleScope @regparam
}
#endregion Load compiled code