Fondue.psm1

#Region '.\Prefix.ps1' -1

$currentPrincipal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()

if (-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
    throw 'Fondue must be imported in an elevated (Administrator) PowerShell session.'
}
#EndRegion '.\Prefix.ps1' 6
#Region '.\private\Assert-LicenseValid.ps1' -1

function Assert-LicenseValid {
    [CmdletBinding()]
    Param(
        [Parameter()]
        [String]
        $LicenseFile = "$env:ChocolateyInstall\license\chocolatey.license.xml"
    )

    end {

        $licenseFound = Test-Path $LicenseFile

        $xmlDoc = [System.Xml.XmlDocument]::new()
        $xmlDoc.Load($LicenseFile)

        $licenseNode = $xmlDoc.SelectSingleNode('/license')
        $expirationDate = [datetime]::Parse($licenseNode.Attributes["expiration"].Value)
        $LicenseExpired = $expirationDate -lt (Get-Date)

        if($licenseFound -and (-not $LicenseExpired)){
            return $true
        } else {
            return $false
        }

    }
}
#EndRegion '.\private\Assert-LicenseValid.ps1' 28
#Region '.\private\Scaffold-Nuspec.ps1' -1

function Scaffold-Nuspec {
    Param(
        [Parameter(Mandatory)]
        [String]
        $Path
    )

    $settings = [System.Xml.XmlWriterSettings]::new()
    $settings.Indent = $true

    $utf8WithoutBom = [System.Text.UTF8Encoding]::new($false)
    $stream = [System.IO.StreamWriter]::new($Path, $false, $utf8WithoutBom)
    
    try {
        $writer = [System.Xml.XmlWriter]::Create($stream, $settings)

        $writer.WriteStartDocument()
        $writer.WriteComment("Do not remove this test for UTF-8: if 'Ω' doesn’t appear as greek uppercase omega letter enclosed in quotation marks, you should use an editor that supports UTF-8, not this one.")
        $writer.WriteStartElement('', 'package', 'http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd')
        $writer.WriteStartElement('metadata')
        $writer.WriteFullEndElement() # metadata
        $writer.WriteEndElement() # package
        $writer.WriteEndDocument()
    }
    finally {
        $writer.Flush()
        $writer.Close()
        $stream.Close()
        $stream.Dispose()
        $writer.Dispose()
    }
    return (Get-Item $Path)
}
#EndRegion '.\private\Scaffold-Nuspec.ps1' 34
#Region '.\private\Write-Metadata.ps1' -1

function Write-Metadata {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]
        [Hashtable]
        $Metadata,

        [Parameter(Mandatory)]
        [String]
        $NuspecFile
    )

    process {
        [xml]$xmlDoc = Get-Content $NuspecFile

        $namespaceManager = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
        $namespaceManager.AddNamespace("ns", "http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd")
        $metadataNode = $xmlDoc.SelectSingleNode("//*[local-name()='metadata']", $namespaceManager)

        $Metadata.GetEnumerator() | ForEach-Object {
            $node = $xmlDoc.SelectSingleNode("//*[local-name()='$($_.Key)']", $namespaceManager)
            if (-not $node) {
                $node = $xmlDoc.CreateElement($_.Key)
            } else {
                'Node exists: {0}, updating' -f $_.Key
            }
            $null = $node.InnerText = $_.Value
            $null = $metadataNode.AppendChild($node)
        }

        #we don't need the namespace on all the nodes, so strip it off
        $xmlDoc = $xmlDoc.OuterXml -replace 'xmlns=""', ''
        $settings = New-Object System.Xml.XmlWriterSettings
        $settings.Indent = $true
        $settings.Encoding = [System.Text.Encoding]::UTF8

        $writer = [System.Xml.XmlWriter]::Create($NuspecFile, $settings)
        try {
            $xmlDoc.WriteTo($writer)
        }
        finally {
            $writer.Flush()
            $writer.Close()
            $writer.Dispose()
        }
    }
}
#EndRegion '.\private\Write-Metadata.ps1' 48
#Region '.\public\Convert-Xml.ps1' -1

Function Convert-Xml {
    <#
    .SYNOPSIS
    Converts XML from a URL or a file to a hash table.
 
    .DESCRIPTION
    The Convert-Xml function takes a URL or a file path as input and converts the XML content to a hash table.
    If a URL is provided, the function downloads the XML content from the URL.
    If a file path is provided, the function reads the XML content from the file.
    The function then converts the XML content to a hash table and returns it.
 
    .PARAMETER Url
    The URL of the XML content to convert. If this parameter is provided, the function will download the XML content from the URL.
 
    .PARAMETER File
    The file path of the XML content to convert. If this parameter is provided, the function will read the XML content from the file.
 
    .EXAMPLE
    Convert-Xml -Url "http://example.com/data.xml"
 
    This example downloads the XML content from the specified URL and converts it to a hash table.
 
    .EXAMPLE
    Convert-Xml -File "C:\path\to\data.xml"
 
    This example reads the XML content from the specified file and converts it to a hash table.
 
    .NOTES
    The function does not support XML content that contains dependencies or comments.
    #>

    [cmdletBinding(HelpUri='https://steviecoaster.github.io/Fondue/Convert-Xml')]
    Param(
        [Parameter()]
        [String]
        $Url,

        [Parameter()]
        [String]
        $File
    )

    process {    
        if ($url) {
            [xml]$xml = [System.Net.WebClient]::new().DownloadString($url)
        }

        if ($File) {
            [xml]$xml = Get-Content $File
        }

        $hash = @{}

        foreach ($node in ($xml.package.metadata.ChildNodes | Where-Object {$_.Name -notmatch 'dependencies|#comment'})) {
            $hash.Add($node.Name, $node.'#text')
        }

        return $hash
    }
}
#EndRegion '.\public\Convert-Xml.ps1' 60
#Region '.\public\New-Dependency.ps1' -1

function New-Dependency {
    <#
.SYNOPSIS
Injects a <dependency> node into a Chocolatey package nuspec file
 
.DESCRIPTION
Long description
 
.PARAMETER Nuspec
THe Chocolatey package nuspec to which add a dependency
 
.PARAMETER Dependency
A hashtable containing the package id and version range for the dependency
 
.PARAMETER Recompile
Recompile the Chocolatey package with the new dependency information.
 
.PARAMETER OutputDirectory
Save the recompiled Chocolatey Package to this location
 
.EXAMPLE
Add a single dependency with a version
 
New-Dependency -Nuspec C:\packages\foo.1.1.1.nuspec -dependency @{id= 'baz'; version='3.4.2'}
 
.EXAMPLE
Add multiple dependencies
 
New-Dependency -Nuspec C:\packages\foo.1.1.0.nuspec -Dependency @{id='baz'; version='1.1.1'},@{id='boo';version=[1.0.1,2.9.0]}
 
.EXAMPLE
Add a dependency and recompile the package
 
$newDependencySplat = @{
    Nuspec = 'C:\packagesfoo.1.1.1.nuspec'
    Dependency = @{id= 'baz'; version='3.4.2'}
    Recompile = $true
}
 
New-Dependency @newDependencySplat
         
.EXAMPLE
Add a dependency, recompile the package, and save it to a new location
 
$newDependencySplat = @{
    Nuspec = 'C:\packagesfoo.1.1.1.nuspec'
    Dependency = @{id= 'baz'; version='3.4.2'}
    Recompile = $true
    OutputDirectory = 'C:\recompiled'
}
 
New-Dependency @newDependencySplat
#>

    [CmdletBinding(HelpUri = 'https://steviecoaster.github.io/Fondue/New-Dependency')]
    Param(
        [Parameter(Mandatory)]
        [String]
        $Nuspec,

        [Parameter(Mandatory)]
        [Hashtable[]]
        $Dependency,

        [Parameter()]
        [Switch]
        $Recompile,

        [Parameter()]
        [String]
        [ValidateScript({ Test-Path $_ })]
        $OutputDirectory
    )

    process {
        [xml]$xmlContent = Get-Content $Nuspec

        # Define the XML namespace
        $namespaceManager = New-Object System.Xml.XmlNamespaceManager($xmlContent.NameTable)
        $namespaceManager.AddNamespace("ns", "http://schemas.microsoft.com/packaging/2015/08/nuspec.xsd")

        # Check if the package node exists and verify its namespace
        $packageNode = $xmlContent.SelectSingleNode("//*[local-name()='package']", $namespaceManager)
        if ($null -eq $packageNode) {
            Write-Error "Package node not found. Exiting." -Category ObjectNotFound
            break
        }
        else {
            Write-Verbose "Package node found."
        }

        # Check if the metadata node exists within the package node
        $metadataNode = $xmlContent.SelectSingleNode("//*[local-name()='metadata']", $namespaceManager)
        if ($null -eq $metadataNode) {
            Write-Error "Metadata node not found." -Category ObjectNotFound
            break
        }
        else {
            Write-Verbose "Metadata node found."
        }

        # Find the dependencies node
        $dependenciesNode = $xmlContent.SelectSingleNode("//*[local-name()='dependencies']", $namespaceManager)

        if ($null -eq $dependenciesNode) {
            $null = $dependenciesNode = $xmlContent.CreateElement('dependencies')
            $null = $metadataNode.AppendChild($dependenciesNode)
        }
        else {
            Write-Verbose "Dependencies node found."
        }

        #Loop over the given dependencies and create new nodes for each
        foreach ($D in $Dependency) {
            # Create a new XmlDocument
            $newDoc = New-Object System.Xml.XmlDocument

            # Create a new dependency element in the new document
            $newDependency = $newDoc.CreateElement("dependency")
            $newDependency.SetAttribute("id", "$($D['id'])")
            if ($D.version) {

                # Check if the version string contains invalid characters
                # Valid ranges: https://learn.microsoft.com/en-us/nuget/concepts/package-versioning?tabs=semver20sort#version-ranges
                if ($($D['version']) -match '\([^,]*?\)') {
                    Write-Error "Invalid version string: $($D['version']) for package $($D['id'])"
                    continue
                }
                $newDependency.SetAttribute("version", "$($D['version'])")
            }
            # Import the new dependency into the original document
            $importedDependency = $xmlContent.ImportNode($newDependency, $true)

            # Append the imported dependency to the dependencies node
            $null = $dependenciesNode.AppendChild($importedDependency)
        }

        # Save the xml back to the nuspec file
        $settings = New-Object System.Xml.XmlWriterSettings
        $settings.Indent = $true
        $settings.Encoding = [System.Text.Encoding]::UTF8

        $writer = [System.Xml.XmlWriter]::Create($Nuspec, $settings)
        try {
            $xmlContent.WriteTo($writer)
        }
        finally {
            $writer.Flush()
            $writer.Close()
            $writer.Dispose()
        }

        # Stupid hack to get rid of the 'xlmns=' part of the new dependency nodes. .Net methods are "overly helpful"
        $content = Get-Content -Path $Nuspec -Raw
        $content = $content -replace ' xmlns=""', ''
        Set-Content -Path $Nuspec -Value $content

        if ($Recompile) {
            if (-not (Get-Command choco)) {
                Write-Error "Choco is required to recompile the package but was not found on this system" -Category ResourceUnavailable
            }
            else {
                $OD = if ($OutputDirectory) {
                    $OutputDirectory
                }
                else {
                    Split-Path -Parent $Nuspec
                }

                $chocoArgs = ('pack', $Nuspec, $OD)
                $choco = (Get-Command choco).Source
                $null = & $choco @chocoArgs

                if ($LASTEXITCODE -eq 0) {
                    'Package is ready and available at {0}' -f $OD
                }
                else {
                    throw 'Recompile had an error, see chocolatey.log for details'
                }
            }
        }
    }
}
#EndRegion '.\public\New-Dependency.ps1' 183
#Region '.\public\New-MetaPackage.ps1' -1

function New-Metapackage {
    <#
    .SYNOPSIS
    Creates a new Chocolatey Meta (virtual) package
     
    .DESCRIPTION
    This function generates the necessary nuspec needed to create a Chocolatey metapackage
     
    .PARAMETER Id
    The id of the meta package to create
     
    .PARAMETER Summary
    Provide a brief summary of what the metapackage provides
     
    .PARAMETER Dependency
    An array of hashtables containing the id and (optional) version of the package to include in the metapackage.
     
    .PARAMETER Path
    The folder in which to generate the metapackage
     
    .PARAMETER Version
    A valid semver for the package. Defaults to 0.1.0
     
    .EXAMPLE
    A minimal example of a metapackage using mandatory parameters
 
    $newMetapackageSplat = @{
        Id = 'example'
        Summary = 'This is a simple example'
        Dependency = @{id='putty'}, @{id='git';version = '2.5.98'}
    }
 
    New-Metapackage @newMetapackageSplat
 
    .EXAMPLE
    Create a meta package with a pre-release version, and save it to C:\chocopackages
 
    $newMetapackageSplat = @{
        Id = 'example'
        Summary = 'This is a simple example'
        Dependency = @{id='putty'}, @{id='git';version = '2.5.98'}
        Version = 1.0.0-pre
        Path = C:\chocopackages
    }
 
    New-Metapackage @newMetapackageSplat
 
    .NOTES
    General notes
    #>

    [Alias('New-VirtualPackage')]
    [CmdletBinding(HelpUri = 'https://steviecoaster.github.io/Fondue/New-Metapackage')]
    Param(
        [Parameter(Position = 0, Mandatory)]
        [String]
        $Id,

        [Parameter(Position = 1, Mandatory)]
        [String]
        $Summary,

        [Parameter(Position = 2, Mandatory)]
        [Hashtable[]]
        $Dependency,

        [Parameter(Mandatory)]
        [ValidateScript({
                if ($_.Length -ge 30) {
                    $true
                }
                else {
                    throw "Description must be at least 30 characters long."
                }
            })]
        [String]
        $Description,

        [Parameter()]
        [String]
        $Path = $PWD,

        [Parameter()]
        [ValidateScript({
                $matcher = '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'
                $_ -match $matcher
            })]
        [String]
        $Version = '0.1.0'
    )

    begin {
        $chocoTemplatesPath = 'C:\ProgramData\chocolatey\templates'

        if (-not (Test-Path $chocoTemplatesPath)) {
            $null = New-Item -ItemType Directory -Path $chocoTemplatesPath -Force
        }

        $copyItemSplat = @{
            Path        = Join-Path $PSScriptRoot 'template\metapackage'
            Destination = $chocoTemplatesPath
            Recurse     = $true
            Force       = $true
        }
        Copy-Item @copyItemSplat
    }

    end {
        $chocoArgs = @('new', "$Id", '--template="metapackage"', "Id=$Id", "Summary=$Summary", "Description=$Description", "Version=$Version")
        & choco @chocoArgs
    }
}
#EndRegion '.\public\New-MetaPackage.ps1' 112
#Region '.\public\New-Package.ps1' -1

function New-Package {
    <#
    .SYNOPSIS
    Generates a new Chocolatey Package
     
    .DESCRIPTION
    Generates a Chocolatey Package. Can be used to generate from an installer (licensed versions only), meta (virtual) packages, or FOSS style packages
     
    .PARAMETER Name
    The name (Id) to give to the package
     
    .PARAMETER IsMetapackage
    Instructs Chocolatey to create a MetaPackage
     
    .PARAMETER File
    An installer to package. Should be of type exe, msi, msu, or zip
     
    .PARAMETER Url
    The url of an installer to package. Installer will be downloaded and embedded into package. Supports same extensions as -File parameter.
     
    .PARAMETER Dependency
    Dependencies to inject into the package. Required when using -IsMetaPackage.
     
    .PARAMETER Metadata
    A hashtable of MetaData to include in the Nuspec File
     
    .PARAMETER OutputDirectory
    This is where the Chocolatey packaging will be generated. Defaults to the current working directory.
     
    .PARAMETER Recompile
    When used runs the pack command against the nuspec file
     
    .EXAMPLE
 
    Creating a basic package with a name:
 
    $params = @{
        Name = "MyPackage"
    }
    New-Package @params
 
    .EXAMPLE
 
    Create a package from an installer file
 
    New-Package -Name "MyInstallerPackage" -File "C:\path\to\installer.exe"
     
    .EXAMPLE
 
    Create a package from an installer URL
 
    New-Package -Name "MyUrlPackage" -Url "http://example.com/installer.exe"
 
    .EXAMPLE
 
    Create a package with metadata
 
    New-Package -Name "MyPackageWithMetadata" -Metadata @{Author="Me"; Version="1.0.0"}
 
    .EXAMPLE
 
    Create a package in a specific output directory
 
    New-Package -Name "MyPackage" -OutputDirectory "C:\path\to\output\directory"
 
    .NOTES
     
    .LINK
    https://docs.chocolatey.org/en-us/guides/create/
    #>

    [CmdletBinding(DefaultParameterSetName = 'Default', HelpUri = 'https://steviecoaster.github.io/Fondue/New-Package')]
    Param(
        [Parameter(Mandatory, ParameterSetName = 'Default')]
        [String]
        $Name,

        [Parameter(Mandatory, ParameterSetName = 'File')]
        [ValidateScript({ Test-Path $_ })]
        [String]
        $File,

        [Parameter(Mandatory, ParameterSetName = 'Url')]
        [String]
        $Url,

        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'File')]
        [Parameter(ParameterSetName = 'Url')]
        [Hashtable[]]
        $Dependency,

        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'File')]
        [Parameter(ParameterSetName = 'Url')]
        [Hashtable]
        $Metadata,

        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'File')]
        [Parameter(ParameterSetName = 'Url')]
        [String]
        $OutputDirectory = $PWD,

        [Parameter(ParameterSetName = 'FIle')]
        [Parameter(ParameterSetName = 'Url')]
        [Switch]
        $Recompile
    )

    process {
        switch ($PSCmdlet.ParameterSetName) {
            'File' {

                $licenseValid = Assert-LicenseValid
                $extensionInstalled = Test-Path "$env:ChocolateyInstall\lib\chocolatey.extension"

                if (-not $licenseValid) {
                    throw 'A valid Chocolatey license is required to use -File but was not found on this system.'
                }

                if (-not $extensionInstalled) {
                    throw 'A valid license file was found, but the Chocolatey Licensed Extension is not installed. The Chocolatey Licensed Extension is required to use -File.'
                }

                $chocoArgs = @('new', "--file='$file'", "--output-directory='$OutputDirectory'", '--build-package')
                $i = 4
            }
            'Url' {

                $licenseValid = Assert-LicenseValid
                $extensionInstalled = Test-Path "$env:ChocolateyInstall\lib\chocolatey.extension"

                if (-not $licenseValid) {
                    throw 'A valid Chocolatey license is required to use -Url but was not found on this system.'
                }

                if (-not $extensionInstalled) {
                    throw 'A valid license file was found, but the Chocolatey Licensed Extension is not installed. The Chocolatey Licensed Extension is required to use -Url.'
                }

                $chocoArgs = @('new', "--url='$url'", "--output-directory='$OutputDirectory'", '--build-package', '--no-progress')
                $i = 7
            }

            default {
                $chocoArgs = @('new', "$Name", "--output-directory='$OutputDirectory'")
                $i = 3
            }
        }

        $matcher = "(?<nuspec>(?<=').*(?='))"

        $choco = & choco @chocoArgs
        Write-Verbose -Message $('Matching against {0}' -f $choco[$i])
        $null = $choco[$i] -match $matcher

        if ($matches.nuspec) {
            'Adding dependencies to package {0}, if any' -f $matches.nuspec
        }
        else {
            throw 'Something went wrong, check the chocolatey.log file for details!'
        }

        if ($Dependency) {
            $newDependencySplat = @{
                Nuspec          = $matches.nuspec
                Dependency      = $Dependency
                OutputDirectory = $OutputDirectory
            }

            New-Dependency @newDependencySplat
        }

        if ($Metadata) {
            Write-Metadata -Metadata $Metadata -NuspecFile $matches.nuspec
        }
        
        if ($Recompile) {
            $chocoArgs = ('pack', $matches.nuspec, $OutputDirectory)
            $choco = (Get-Command choco).Source
            $null = & $choco @chocoArgs

            if ($LASTEXITCODE -eq 0) {
                'Package is ready and available at {0}' -f $OutputDirectory
            }
            else {
                throw 'Recompile had an error, see chocolatey.log for details'
            }

        }
    }
}
#EndRegion '.\public\New-Package.ps1' 193
#Region '.\public\Open-FondueHelp.ps1' -1

function Open-FondueHelp {
    <#
    .SYNOPSIS
     
    Opens the documentation for the Fondue PowerShell module
    #>

    [CmdletBinding(HelpUri = 'https://steviecoaster.github.io/Fondue/Open-FondueHelp')]
    Param()

    end {
        Start-Process https://steviecoaster.github.io/Fondue/
    }
}
#EndRegion '.\public\Open-FondueHelp.ps1' 14
#Region '.\public\Remove-Dependency.ps1' -1

function Remove-Dependency {
    <#
        .SYNOPSIS
        Removes a dependency from a NuGet package specification file.
 
        .DESCRIPTION
        The Remove-Dependency function takes a NuGet package specification (.nuspec) file and an array of dependencies as input.
        It removes the specified dependencies from the .nuspec file.
 
        .PARAMETER PackageNuspec
        The path to the .nuspec file from which to remove dependencies. This parameter is mandatory.
 
        .PARAMETER Dependency
        An array of dependencies to remove from the .nuspec file. This parameter is mandatory.
 
        .EXAMPLE
        Remove-Dependency -PackageNuspec "C:\path\to\package.nuspec" -Dependency "Dependency1", "Dependency2"
 
        This example removes the dependencies "Dependency1" and "Dependency2" from the .nuspec file at the specified path.
 
        .NOTES
        The function does not support removing dependencies that are not directly listed in the .nuspec file.
    #>

    [CmdletBinding(HelpUri = 'https://steviecoaster.github.io/Fondue/Remove-Dependency')]
    Param(
        [Parameter(Mandatory)]
        [String]
        $PackageNuspec,

        [Parameter(Mandatory)]
        [String[]]
        $Dependency
    )

    process {
        $xmlDoc = [System.Xml.XmlDocument]::new()
        $xmlDoc.Load($PackageNuspec)

        # Create an XmlNamespaceManager and add the namespace
        $nsManager = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
        $nsManager.AddNamespace('ns', $xmlDoc.DocumentElement.NamespaceURI)

        # Use the XmlNamespaceManager when selecting nodes
        $dependenciesNode = $xmlDoc.SelectSingleNode('//ns:metadata/ns:dependencies', $nsManager)
        foreach ($d in $Dependency) {
            if ($null -ne $dependenciesNode) {
                $dependencyToRemove = $dependenciesNode.SelectSingleNode("ns:dependency[@id='$d']", $nsManager)
        
                if ($null -ne $dependencyToRemove) {
                    $null = $dependenciesNode.RemoveChild($dependencyToRemove)
                }
            }
        }

        $settings = New-Object System.Xml.XmlWriterSettings
        $settings.Indent = $true
        $settings.Encoding = [System.Text.Encoding]::UTF8

        $writer = [System.Xml.XmlWriter]::Create($PackageNuspec, $settings)
        try {
            $xmlDoc.WriteTo($writer)
        }
        finally {
            $writer.Flush()
            $writer.Close()
        }
    }
}
#EndRegion '.\public\Remove-Dependency.ps1' 69
#Region '.\public\Sync-Package.ps1' -1

function Sync-Package {
    <#
    .SYNOPSIS
    Runs choco sync on a system
     
    .DESCRIPTION
    Run choco sync against a system with eiher a single item or from a map
     
    .PARAMETER Id
    The Package id for the synced package
     
    .PARAMETER DisplayName
    The Diplay Name From Programs and Features
     
    .PARAMETER Map
    A hashtable of DisplayName and PackageIds
     
    .EXAMPLE
    Sync-Package
 
    Sync everything from Programs and Features not under Chocolatey management
 
    .EXAMPLE
    Sync-Package -Id googlechrome -DisplayName 'Google Chrome'
 
    Sync the Google Chrome application from Programs and Features to the googlechrome package id
 
    .EXAMPLE
    Sync-Package -Map @{'Google Chrome' = 'googlechrome'}
 
    Sync from a hashtable of DisplayName:PackageId pairs
     
    .NOTES
    Requires a Chocolatey For Business license
    #>

    [CmdletBinding(DefaultParameterSetName = 'Default', HelpUri = 'https://steviecoaster.github.io/Fondue/Sync-Package')]
    Param(
        [Parameter(Mandatory, ParameterSetName = 'Package')]
        [String]
        $Id,

        [Parameter(Mandatory, ParameterSetName = 'Package')]
        [String]
        $DisplayName,

        [Parameter(Mandatory, ParameterSetName = 'Map')]
        [hashtable]
        $Map,

        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'Map')]
        [Parameter(ParameterSetName = 'Package')]
        [String]
        $OutputDirectory = $PWD
    )

    begin {
        
        $licenseValid = Assert-LicenseValid
        $extensionInstalled = Test-Path "$env:ChocolateyInstall\lib\chocolatey.extension"

        if (-not $licenseValid) {
            throw 'A valid Chocolatey license is required to use -File but was not found on this system.'
        }

        if (-not $extensionInstalled) {
            throw 'A valid license file was found, but the Chocolatey Licensed Extension is not installed. The Chocolatey Licensed Extension is required to use -File.'
        }
    }
    end {
        switch ($PSCmdlet.ParameterSetName) {
            'Package' {
                choco sync --id="$DisplayName" --package-id="$Id" --output-directory="$OutputDirectory"
                $packageFolder = Join-path $OutputDirectory -ChildPath "sync\$Id"
                $todo = Join-Path $packageFolder -ChildPath 'TODO.txt'

                if (Test-Path $todo) {
                    Write-Warning (Get-Content $todo)
                }
            }

            'Map' {
                $map.GetEnumerator() | Foreach-Object {
                    choco sync --id="$($_.Key)" --package-id="$($_.Value)" --output-directory="$OutputDirectory"
                    $packageFolder = Join-path $OutputDirectory -ChildPath $_.Value
                    $todo = Join-Path $packageFolder -ChildPath 'TODO.txt'

                    if (Test-Path $todo) {
                        Write-Warning (Get-Content $todo)
                    }
                }
            }

            default {
                choco sync --output-directory="$OutputDirectory"
            }
        }
    }
}
#EndRegion '.\public\Sync-Package.ps1' 100
#Region '.\public\Test-Nuspec.ps1' -1

function Test-Nuspec {
    <#
        .SYNOPSIS
        Tests a NuGet package specification (.nuspec) file for compliance with specified rules.
 
        .DESCRIPTION
        The Test-Nuspec function takes a .nuspec file and a set of rules as input. It tests the .nuspec file for compliance with the specified rules. The function can test for compliance with only the required rules, or it can also test for compliance with community rules. Additional tests can be specified.
 
        .PARAMETER NuspecFile
        The path to the .nuspec file to test. This parameter is validated to ensure that it is a valid path.
 
        .PARAMETER Metadata
        A hash table of metadata to test. This parameter is optional.
 
        .PARAMETER OnlyRequiredRules
        A switch that, when present, causes the function to test for compliance with only the required rules.
 
        .PARAMETER TestCommunityRules
        A switch that, when present, causes the function to test for compliance with community rules.
 
        .PARAMETER AdditionalTest
        An array of additional tests to run. This parameter is optional.
 
        .EXAMPLE
        Test-Nuspec -NuspecFile "C:\path\to\package.nuspec" -OnlyRequiredRules
 
        This example tests the .nuspec file at the specified path for compliance with only the required rules.
 
        .EXAMPLE
        Test-Nuspec -NuspecFile "C:\path\to\package.nuspec" -TestCommunityRules -AdditionalTest "Test1", "Test2"
 
        This example tests the .nuspec file at the specified path for compliance with community rules and runs the additional tests "Test1" and "Test2".
 
        .NOTES
        The function uses the Convert-Xml function to convert the .nuspec file to a hash table of metadata.
    #>

    [CmdletBinding(HelpUri = 'https://steviecoaster.github.io/Fondue/Test-Nuspec')]
    Param(
        [Parameter()]
        [ValidateScript({ Test-Path $_ })]
        [String]
        $NuspecFile,

        [Parameter()]
        [Hashtable]
        $Metadata,

        [Parameter()]
        [Switch]
        $SkipBuiltinTests,

        [Parameter()]
        [String[]]
        $AdditionalTest
    )

    process {

        $data = if ($NuspecFile) {
            $Metadata = Convert-Xml -File $NuspecFile
            @{ metadata = $Metadata }
        }
        else {
            @{ Metadata = $Metadata }
        }

        $moduleRoot = (Get-Module Fondue).ModuleBase
        $SystemTests = (Get-ChildItem (Join-Path $moduleRoot -ChildPath 'module_tests') -Recurse -Filter nuspec*.tests.ps1) | Select-Object Name, FullName
        $containerCollection = [System.Collections.Generic.List[psobject]]::new()

        if(-not $SkipBuiltinTests){
            $SystemTests |ForEach-Object{ $containerCollection.Add($_.FullName)}
        }

        if ($AdditionalTest) {
            $AdditionalTest | ForEach-Object { $containerCollection.Add($_) }
        }

        if($SkipBuiltinTests -and (-not $AdditionalTest)){
            throw '-SkipBuiltinTests was passed, but not additional tests. Please pass additional tests, or remove -SkipBuiltinTests'
        }
       
        $containers = $containerCollection | Foreach-object { New-PesterContainer -Path $_ -Data $data }

        $configuration = [PesterConfiguration]@{
            Run        = @{
                Container = $Containers
                Passthru  = $true
            }
            Output     = @{
                Verbosity = 'Detailed'
            }
            TestResult = @{
                Enabled = $false
            }
        }
    
        $results = Invoke-Pester -Configuration $configuration
    }
}
#EndRegion '.\public\Test-Nuspec.ps1' 101
#Region '.\public\Test-Package.ps1' -1

function Test-Package {
    <#
        .SYNOPSIS
        Tests a NuGet package for compliance with specified rules.
 
        .DESCRIPTION
        The Test-Package function takes a NuGet package and a set of rules as input. It tests the package for compliance with the specified rules. The function can test for compliance with only the required rules, or it can also test for compliance with all system rules. Additional tests can be specified.
 
        .PARAMETER PackagePath
        The path to the NuGet package to test. This parameter is mandatory and validated to ensure that it is a valid path.
 
        .PARAMETER OnlyRequiredRules
        A switch that, when present, causes the function to test for compliance with only the required rules.
 
        .PARAMETER AdditionalTest
        An array of additional tests to run. This parameter is optional.
 
        .EXAMPLE
        Test-Package -PackagePath "C:\path\to\package.nupkg" -OnlyRequiredRules
 
        This example tests the NuGet package at the specified path for compliance with only the required rules.
 
        .EXAMPLE
        Test-Package -PackagePath "C:\path\to\package.nupkg" -AdditionalTest "Test1", "Test2"
 
        This example tests the NuGet package at the specified path and runs the additional tests "Test1" and "Test2".
 
        .NOTES
        The function uses the Fondue module to perform the tests.
    #>

    [CmdletBinding(HelpUri = 'https://steviecoaster.github.io/Fondue/Test-Package')]
    Param(
        [Parameter(Mandatory)]
        [ValidateScript({ Test-Path $_ })]
        [String]
        $PackagePath,

        [Parameter()]
        [Switch]
        $OnlyRequiredRules,

        [Parameter()]
        [String[]]
        $AdditionalTest
    )

    process {

        $Data = @{ PackagePath = $PackagePath }
        $moduleRoot = (Get-Module Fondue).ModuleBase
        
        $SystemTests = (Get-ChildItem (Join-Path $moduleRoot -ChildPath 'module_tests') -Recurse -Filter package*.tests.ps1) | Select-Object Name, FullName

        $containerCollection = [System.Collections.Generic.List[psobject]]::new()

        if ($OnlyRequiredRules) {
            $tests = ($SystemTests | Where-Object Name -match 'required').FullName
            $containerCollection.Add($tests)
        }
        else {
            $tests = ($SystemTests).FullName
            $containerCollection.Add($tests)
        }

        if ($AdditionalTest) {
            $AdditionalTest | ForEach-Object { $containerCollection.Add($_) }
        }
        
        $containers = $containerCollection | Foreach-object { New-PesterContainer -Path $_ -Data $Data }

        $configuration = [PesterConfiguration]@{
            Run        = @{
                Container = $Containers
                Passthru  = $true
            }
            Output     = @{
                Verbosity = 'Detailed'
            }
            TestResult = @{
                Enabled = $false
            }
        }
    
        $results = Invoke-Pester -Configuration $configuration

    } 
    
}
#EndRegion '.\public\Test-Package.ps1' 89
#Region '.\public\Update-ChocolateyMetadata.ps1' -1

function Update-ChocolateyMetadata {
    <#
        .SYNOPSIS
        Updates the metadata of a Chocolatey package.
 
        .DESCRIPTION
        The Update-ChocolateyMetadata function takes a metadata hash table and a .nuspec file as input. It updates the metadata of the Chocolatey package specified by the .nuspec file with the values in the metadata hash table.
 
        .PARAMETER Metadata
        A hash table of metadata to update. The keys should be the names of the metadata elements to update, and the values should be the new values for these elements.
 
        .PARAMETER NuspecFile
        The path to the .nuspec file of the Chocolatey package to update. This parameter is mandatory and validated to ensure that it is a valid path.
 
        .EXAMPLE
        $Metadata = @{ id = "newId"; version = "1.0.1" }
        Update-ChocolateyMetadata -Metadata $Metadata -NuspecFile "C:\path\to\package.nuspec"
 
        This example updates the id and version of the Chocolatey package specified by the .nuspec file at the specified path.
 
        .NOTES
        The function uses the Write-Metadata function to write the updated metadata to the .nuspec file.
    #>

    [CmdletBinding(HelpUri = 'https://steviecoaster.github.io/Fondue/Update-ChocolateyMetadata')]
    Param(
        [Parameter(Mandatory,ValueFromPipeline,ValueFromRemainingArguments)]
        [Hashtable]
        $Metadata,

        [Parameter(Mandatory)]
        [ValidateScript({Test-Path $_})]
        [String]
        $NuspecFile
    )

    process {
        Write-metadata -MetaData $Metadata -Nuspecfile $NuspecFile
    }
}
#EndRegion '.\public\Update-ChocolateyMetadata.ps1' 40
#Region '.\Suffix.ps1' -1

#EndRegion '.\Suffix.ps1' 1