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://chocolatey-solutions.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 one or more <dependency> nodes into a Chocolatey package nuspec file.
 
.DESCRIPTION
New-Dependency reads an existing .nuspec file and appends one or more <dependency> elements
to the <dependencies> section. If the <dependencies> node does not yet exist it is created
automatically. Optionally the package can be recompiled immediately after the dependency is
injected, with the resulting .nupkg saved to an output directory of your choice.
 
.PARAMETER Nuspec
The full path to the .nuspec file to which the dependency will be added.
 
.PARAMETER Dependency
An array of hashtables, each containing a mandatory 'id' key and a mandatory 'version' key
that specifies the NuGet version range for the dependency (e.g. @{id='git'; version='2.44.0'}).
 
.PARAMETER Recompile
When specified, runs 'choco pack' against the .nuspec file after the dependency has been
injected, producing a .nupkg in the current directory or in -OutputDirectory.
 
.PARAMETER OutputDirectory
The directory to which the recompiled .nupkg should be saved. Only used with -Recompile.
The path must already exist.
 
.EXAMPLE
Add a single versioned dependency
 
New-Dependency -Nuspec 'C:\packages\foo.1.1.1.nuspec' -Dependency @{id='baz'; version='3.4.2'}
 
.EXAMPLE
Add multiple dependencies, one with a version range
 
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 immediately recompile the package
 
$newDependencySplat = @{
    Nuspec = 'C:\packages\foo.1.1.1.nuspec'
    Dependency = @{id='baz'; version='3.4.2'}
    Recompile = $true
}
 
New-Dependency @newDependencySplat
 
.EXAMPLE
Add a dependency, recompile, and save the .nupkg to a specific directory
 
$newDependencySplat = @{
    Nuspec = 'C:\packages\foo.1.1.1.nuspec'
    Dependency = @{id='baz'; version='3.4.2'}
    Recompile = $true
    OutputDirectory = 'C:\recompiled'
}
 
New-Dependency @newDependencySplat
#>

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

        [Parameter(Mandatory)]
        [ValidateScript({
            foreach ($d in $_) {
                if (-not $d.ContainsKey('id')) {
                    throw "Each dependency hashtable must contain an 'id' key."
                }
                if (-not $d.ContainsKey('version')) {
                    throw "Each dependency hashtable must contain a 'version' key. Dependency '$($d['id'])' is missing a version."
                }
            }
            $true
        })]
        [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' 200
#Region '.\public\New-MetaPackage.ps1' -1

function New-Metapackage {
    <#
    .SYNOPSIS
    Creates a new Chocolatey meta (virtual) package.
 
    .DESCRIPTION
    New-Metapackage generates the .nuspec file needed to define a Chocolatey metapackage —
    a package that contains no software itself but declares a set of dependencies that will
    be installed together. The nuspec is written to -Path (defaults to the current directory)
    and can optionally be given an explicit semantic version.
 
    .PARAMETER Id
    The package id for the metapackage (e.g. 'dev-tools'). Used as the nuspec <id> element.
 
    .PARAMETER Summary
    A short, one-line summary of what the metapackage installs.
 
    .PARAMETER Description
    A longer description of the metapackage. Must be at least 30 characters.
 
    .PARAMETER Dependency
    An array of hashtables, each with an 'id' key and an optional 'version' key, describing
    the packages that will be pulled in when this metapackage is installed
    (e.g. @{id='git'; version='2.44.0'}).
 
    .PARAMETER Path
    The directory in which to write the generated .nuspec file. Defaults to the current
    working directory.
 
    .PARAMETER Version
    A valid semantic version for the package (e.g. '1.0.0' or '1.0.0-pre'). Defaults to '0.1.0'.
 
    .EXAMPLE
    Create a minimal metapackage using only mandatory parameters
 
    $newMetapackageSplat = @{
        Id = 'dev-tools'
        Summary = 'Common developer tools'
        Description = 'Installs a curated set of developer tooling for new machines.'
        Dependency = @{id='git'}, @{id='vscode'}, @{id='nodejs'}
    }
 
    New-Metapackage @newMetapackageSplat
 
    .EXAMPLE
    Create a metapackage with a pre-release version saved to a custom path
 
    $newMetapackageSplat = @{
        Id = 'dev-tools'
        Summary = 'Common developer tools'
        Description = 'Installs a curated set of developer tooling for new machines.'
        Dependency = @{id='git'; version='2.44.0'}, @{id='putty'}
        Version = '1.0.0-pre'
        Path = 'C:\chocopackages'
    }
 
    New-Metapackage @newMetapackageSplat
 
    .NOTES
    This function requires Chocolatey to be installed so that the metapackage template can be
    placed in the standard templates directory.
 
    Alias: New-VirtualPackage
    #>

    [Alias('New-VirtualPackage')]
    [CmdletBinding(HelpUri = 'https://chocolatey-solutions.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' 126
#Region '.\public\New-Package.ps1' -1

function New-Package {
    <#
    .SYNOPSIS
    Generates a new Chocolatey package.
 
    .DESCRIPTION
    New-Package scaffolds a new Chocolatey package directory and .nuspec file. Three
    parameter sets are supported:
 
      - Default : creates a basic FOSS-style package using only a package name.
      - File : internalises a local installer (exe/msi/msu/zip) into the package.
                  Requires a Chocolatey for Business license and the Licensed Extension.
      - Url : downloads and embeds an installer from a URL.
                  Requires a Chocolatey for Business license and the Licensed Extension.
 
    Optional dependencies and nuspec metadata can be injected at scaffold time. Pass
    -Recompile to immediately pack the scaffolded directory into a .nupkg.
 
    .PARAMETER Name
    The package id (e.g. 'myapp'). Used as both the directory name and the nuspec <id>.
    Required for the Default parameter set.
 
    .PARAMETER File
    Path to a local installer file (exe, msi, msu, or zip) to embed in the package.
    Required for the File parameter set. Requires a Chocolatey for Business license.
 
    .PARAMETER Url
    URL of an installer to download and embed in the package.
    Required for the Url parameter set. Requires a Chocolatey for Business license.
 
    .PARAMETER Dependency
    One or more dependency hashtables to inject into the nuspec at creation time.
    Each hashtable must contain an 'id' key and a 'version' key.
 
    .PARAMETER Metadata
    A hashtable of additional nuspec metadata fields to populate (e.g. authors, version,
    description, projectUrl). Keys must match valid nuspec element names.
 
    .PARAMETER OutputDirectory
    The directory in which to create the package scaffold. Defaults to the current
    working directory.
 
    .PARAMETER Recompile
    When specified, runs 'choco pack' against the generated .nuspec immediately after
    scaffolding. Only available with the File and Url parameter sets.
 
    .EXAMPLE
    Create a basic package
 
    New-Package -Name 'myapp'
 
    .EXAMPLE
    Create a package from a local installer
 
    New-Package -File 'C:\installers\myapp-setup.exe'
 
    .EXAMPLE
    Create a package from a download URL
 
    New-Package -Url 'https://example.com/myapp-setup.exe'
 
    .EXAMPLE
    Create a package with rich metadata
 
    $newPackageSplat = @{
        Name = 'myapp'
        Metadata = @{
            Authors = 'Acme Corp'
            Version = '2.1.0'
            Description = 'The best app ever made'
            ProjectUrl = 'https://example.com'
        }
    }
 
    New-Package @newPackageSplat
 
    .EXAMPLE
    Scaffold and immediately pack to .nupkg
 
    New-Package -Url 'https://example.com/myapp-setup.exe' -Recompile
 
    .EXAMPLE
    Write output to a specific directory
 
    New-Package -Name 'myapp' -OutputDirectory 'C:\chocopackages'
 
    .NOTES
    The -File and -Url parameter sets require a valid Chocolatey for Business license and
    the Chocolatey Licensed Extension ('chocolatey.extension') to be installed.
 
    .LINK
    https://docs.chocolatey.org/en-us/guides/create/
    #>

    [CmdletBinding(DefaultParameterSetName = 'Default', HelpUri = 'https://chocolatey-solutions.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')]
        [ValidateScript({
                foreach ($d in $_) {
                    if (-not $d.ContainsKey('id')) {
                        throw "Each dependency hashtable must contain an 'id' key."
                    }
                    if (-not $d.ContainsKey('version')) {
                        throw "Each dependency hashtable must contain a 'version' key. Dependency '$($d['id'])' is missing a version."
                    }
                }
                $true
            })]
        [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' 227
#Region '.\public\Open-FondueHelp.ps1' -1

function Open-FondueHelp {
    <#
    .SYNOPSIS
    Opens the Fondue module documentation in the default browser.
 
    .DESCRIPTION
    Open-FondueHelp launches the Fondue documentation website
    (https://chocolatey-solutions.github.io/Fondue/) in the system's default web browser.
    Use this as a quick shortcut to the full command reference without leaving your
    PowerShell session.
 
    .EXAMPLE
    Open the documentation website
 
    Open-FondueHelp
    #>

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

    end {
        Start-Process https://chocolatey-solutions.github.io/Fondue/
    }
}
#EndRegion '.\public\Open-FondueHelp.ps1' 24
#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://chocolatey-solutions.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
    Brings installed software under Chocolatey management using choco sync.
 
    .DESCRIPTION
    Sync-Package wraps the 'choco sync' command provided by the Chocolatey Licensed
    Extension to bring software already visible in Programs and Features under
    Chocolatey management without reinstalling it.
 
    Three modes are supported:
      - Default : syncs all unmanaged programs found in Programs and Features.
      - Package : syncs a single application identified by its Programs and Features
                  display name, mapped to a specific Chocolatey package id.
      - Map : syncs multiple applications from a hashtable of DisplayName → PackageId
                  pairs in one call.
 
    After syncing, Fondue checks for a TODO.txt file in each synced package folder and
    surfaces its contents as a warning if found.
 
    .PARAMETER Id
    The Chocolatey package id to assign to the synced application.
    Required for the Package parameter set.
 
    .PARAMETER DisplayName
    The exact display name of the application as it appears in Programs and Features.
    Required for the Package parameter set.
 
    .PARAMETER Map
    A hashtable of DisplayName → PackageId pairs, used to sync multiple applications
    in a single call. Required for the Map parameter set.
 
    .PARAMETER OutputDirectory
    The directory to which synced package files are written. Defaults to the current
    working directory.
 
    .EXAMPLE
    Sync all unmanaged programs in Programs and Features
 
    Sync-Package
 
    .EXAMPLE
    Sync a single application by display name and package id
 
    Sync-Package -Id 'googlechrome' -DisplayName 'Google Chrome'
 
    .EXAMPLE
    Sync multiple applications from a hashtable
 
    Sync-Package -Map @{
        'Google Chrome' = 'googlechrome'
        'Notepad++' = 'notepadplusplus'
    }
 
    .EXAMPLE
    Sync and save package files to a custom output directory
 
    Sync-Package -Map @{'Notepad++' = 'notepadplusplus'} -OutputDirectory 'C:\synced'
 
    .NOTES
    Requires a Chocolatey for Business license and the Chocolatey Licensed Extension
    ('chocolatey.extension') to be installed.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Default', HelpUri = 'https://chocolatey-solutions.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' 128
#Region '.\public\Test-NupkgFile.ps1' -1

function Test-NupkgFile {
    <#
        .SYNOPSIS
        Tests a NuGet package for compliance with specified rules.
 
        .DESCRIPTION
        The Test-NupkgFile 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-NupkgFile -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-NupkgFile -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://chocolatey-solutions.github.io/Fondue/Test-NupkgFile')]
    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-NupkgFile.ps1' 89
#Region '.\public\Test-NuspecFile.ps1' -1

function Test-NuspecFile {
    <#
        .SYNOPSIS
        Validates a .nuspec file or metadata hashtable against Fondue's built-in rule set.
 
        .DESCRIPTION
        Test-NuspecFile runs Pester-based validation against a Chocolatey .nuspec file or a
        raw metadata hashtable. By default all built-in nuspec tests are executed. Pass
        -SkipBuiltinTests to bypass the built-in suite and run only your own tests via
        -AdditionalTest. Both can be combined to layer organisation-specific rules on top
        of the built-in suite.
 
        .PARAMETER NuspecFile
        Path to the .nuspec file to validate. The file is converted to a metadata hashtable
        internally before testing. Mutually exclusive with -Metadata.
 
        .PARAMETER Metadata
        A hashtable of nuspec metadata to validate directly (e.g. as returned by Convert-Xml).
        Mutually exclusive with -NuspecFile.
 
        .PARAMETER SkipBuiltinTests
        When specified, the built-in test suite is not run. Requires -AdditionalTest to be
        supplied, otherwise an error is thrown.
 
        .PARAMETER AdditionalTest
        One or more paths to Pester test scripts (.tests.ps1) to run alongside (or instead
        of) the built-in suite.
 
        .EXAMPLE
        Run all built-in tests against a nuspec file
 
        Test-NuspecFile -NuspecFile 'C:\packages\myapp.nuspec'
 
        .EXAMPLE
        Test a raw metadata hashtable
 
        $meta = Convert-Xml -File 'C:\packages\myapp.nuspec'
        Test-NuspecFile -Metadata $meta
 
        .EXAMPLE
        Run only custom tests, skipping the built-in suite
 
        Test-NuspecFile -NuspecFile 'C:\packages\myapp.nuspec' -SkipBuiltinTests -AdditionalTest 'C:\tests\my-rules.tests.ps1'
 
        .EXAMPLE
        Add extra tests on top of the built-in suite
 
        Test-NuspecFile -NuspecFile 'C:\packages\myapp.nuspec' -AdditionalTest 'C:\tests\my-rules.tests.ps1'
 
        .NOTES
        Uses Pester 5 internally. Results are returned as a Pester TestResult object.
    #>

    [CmdletBinding(HelpUri = 'https://chocolatey-solutions.github.io/Fondue/Test-NuspecFile')]
    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-NuspecFile.ps1' 117
#Region '.\public\Update-ChocolateyMetadata.ps1' -1

function Update-ChocolateyMetadata {
    <#
        .SYNOPSIS
        Updates metadata fields in a Chocolatey .nuspec file.
 
        .DESCRIPTION
        Update-ChocolateyMetadata accepts a hashtable of metadata key/value pairs and
        writes each value into the matching element of the specified .nuspec file.
        Keys must correspond to valid nuspec element names (e.g. 'version', 'authors',
        'releaseNotes'). Only the elements present in the hashtable are modified;
        all other elements in the file remain untouched.
 
        The function accepts pipeline input, making it easy to chain with Convert-Xml.
 
        .PARAMETER Metadata
        A hashtable of nuspec element names and their new values. Accepts pipeline input.
 
        .PARAMETER NuspecFile
        The full path to the .nuspec file to update. The file must already exist.
 
        .EXAMPLE
        Bump the version and add a release note
 
        $updateMetadataSplat = @{
            NuspecFile = 'C:\packages\myapp.nuspec'
            Metadata = @{
                version = '2.2.0'
                releaseNotes = 'Fixed a critical bug.'
            }
        }
 
        Update-ChocolateyMetadata @updateMetadataSplat
 
        .EXAMPLE
        Copy metadata from one nuspec into another via the pipeline
 
        Convert-Xml -File 'C:\packages\source.nuspec' | Update-ChocolateyMetadata -NuspecFile 'C:\packages\target.nuspec'
 
        .NOTES
        Uses Convert-Xml internally when reading source metadata from a file.
    #>

    [CmdletBinding(HelpUri = 'https://chocolatey-solutions.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' 58
#Region '.\Suffix.ps1' -1

#EndRegion '.\Suffix.ps1' 1