JBUtils.psm1

#Region '.\Private\Get-UsernameSID.ps1' -1

<#
.SYNOPSIS
Create a function to retrieve the SID for a user account on a machine.
#>

function Get-UsernameSID {
    param (
        [String]$AccountName
    )

    $ntUserObject = New-Object System.Security.Principal.NTAccount($AccountName)
    $ntUserSid = $ntUserObject.Translate([System.Security.Principal.SecurityIdentifier])
    $ntUserSid.Value
}
#EndRegion '.\Private\Get-UsernameSID.ps1' 14
#Region '.\Private\Invoke-Timeout.ps1' -1

<#
.SYNOPSIS
Wrapper for timeout.exe to support Pester mocking.
 
.DESCRIPTION
Wrapper for timeout.exe to support Pester mocking.
 
.EXAMPLE
Invoke-Timeout /t 30
 
.NOTES
N/A
#>


function Invoke-Timeout {
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet('/t')]
        [String]$TimeSwitch,
        [Parameter(Mandatory = $true)]
        [Int]$Seconds,
        [ValidateSet('/nobreak')]
        [String]$NoBreakSwitch
    )

    $timeout = Get-Command -Name "$env:windir/System32/timeout.exe" -ErrorAction Stop

    $ArgumentList = ($TimeSwitch, $Seconds)
    if ($NoBreakSwitch) {
        $ArgumentList += $NoBreakSwitch
    }

    $stderrOutFile = New-TemporaryFile
    try {
        Start-Process `
            -FilePath $timeout.Source `
            -ArgumentList $ArgumentList `
            -NoNewWindow `
            -Wait `
            -ErrorAction Stop
    }
    catch {
        Write-Verbose -Message $_.Exception.Message
        Start-Process `
            -FilePath $timeout.Source `
            -ArgumentList $ArgumentList `
            -Wait `
            -RedirectStandardError $stderrOutFile.FullName
    }

    $stdErr = Get-Content -Path $stderrOutFile.FullName -Raw
    $stderrOutFile | Remove-Item -Force -ErrorAction SilentlyContinue
    if ($null -ne $stdErr) {
        throw $stdErr
    }
}
#EndRegion '.\Private\Invoke-Timeout.ps1' 57
#Region '.\Public\Add-AzPipelinesPathEntry.ps1' -1

<#
.SYNOPSIS
Prepend a value to the PATH environment variable in Azure Pipelines that can be used in subsequent tasks.
 
.DESCRIPTION
Prepend a value to the PATH environment variable in Azure Pipelines that can be used in subsequent tasks. It will
only print the console syntax if running on a pipeline agent.
 
.PARAMETER Path
Path to prepend.
 
.EXAMPLE
Add-AzPipelinesPathEntry -Path C:/path/to/prepend
 
.NOTES
N/A
#>

function Add-AzPipelinesPathEntry {
    [CmdletBinding()]
    param (
        [String[]]$Path
    )

    if ($env:AGENT_JOBNAME) {
        $processedPaths = @(
            foreach ($entry in $Path) {
                $entry.Split([System.IO.Path]::PathSeparator) |
                    Where-Object -FilterScript { $_ }
            }
        )

        $resolvedPaths = @()
        $resolvedPaths += foreach ($entry in $processedPaths) {
            try {
                ( Resolve-Path -Path $entry -ErrorAction Stop ).Path
            }
            catch {
                Write-Warning -Message $_.Exception.Message
                $entry
            }
        }

        $joinedPaths = $resolvedPaths -join [System.IO.Path]::PathSeparator

        Write-Host -Object "##vso[task.prependpath]$joinedPaths"
    }
}
#EndRegion '.\Public\Add-AzPipelinesPathEntry.ps1' 48
#Region '.\Public\ConvertFrom-EncryptedSecureString.ps1' -1

<#
.SYNOPSIS
Converts a SecureString to plain text in one step.
 
.DESCRIPTION
Converts a SecureString to plain text in one step.
 
.PARAMETER SecureString
SecureString to convert.
 
.EXAMPLE
$secureStringVar | ConvertFrom-EncryptedSecureString
 
.LINK
https://stackoverflow.com/a/28353003/14628263
 
.NOTES
N/A
#>

function ConvertFrom-EncryptedSecureString {
    [CmdletBinding()]
    [OutputType([String])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [SecureString]$SecureString
    )

    process {
        $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)
        [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
        [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR)
    }
}
#EndRegion '.\Public\ConvertFrom-EncryptedSecureString.ps1' 34
#Region '.\Public\ConvertTo-Psd1.ps1' -1

<#
.SYNOPSIS
Serializes an object to PowerShell data-file (.psd1) literal syntax.
 
.DESCRIPTION
Recursively renders strings, numbers, booleans, arrays, and dictionaries as PowerShell data-file
literal text, suitable for writing a hand-editable .psd1 file. Unsupported types are rendered as
their quoted string representation.
 
.PARAMETER InputObject
The object to serialize. Accepts pipeline input.
 
.PARAMETER Indent
The current indentation level, in 4-space increments. Used internally for recursive calls; callers
normally omit it.
 
.OUTPUTS
System.String
 
.EXAMPLE
ConvertTo-Psd1 -InputObject @{ Name = 'MyModule'; Tags = @('PSEdition_Core') }
 
.NOTES
N/A
#>

function ConvertTo-Psd1 {
    [CmdletBinding()]
    [OutputType([String])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [AllowNull()]
        $InputObject,

        [Int]$Indent = 0
    )

    process {
        $indentText = ' ' * ($Indent * 4)
        $childPad = ' ' * (($Indent + 1) * 4)

        if ($null -eq $InputObject) {
            '$null'
        }
        elseif ($InputObject -is [Bool]) {
            if ($InputObject) {
                '$true'
            }
            else {
                '$false'
            }
        }
        elseif ($InputObject -is [Int] -or $InputObject -is [Long] -or
            $InputObject -is [Double] -or $InputObject -is [Decimal]) {
            "$InputObject"
        }
        elseif ($InputObject -is [System.Collections.IDictionary]) {
            if ($InputObject.Count -eq 0) {
                '@{}'
            }
            else {
                $keyWidth = ($InputObject.Keys | ForEach-Object -Process { "$_".Length } |
                        Measure-Object -Maximum).Maximum
                $lines = [System.Collections.Generic.List[String]]::new()
                $lines.Add('@{')
                foreach ($key in $InputObject.Keys) {
                    # The recursive call already carries absolute indentation, so the opening
                    # token goes inline after "key =" and continuation lines are added as-is.
                    $rendered = ConvertTo-Psd1 -InputObject $InputObject[$key] -Indent ($Indent + 1)
                    $lines.Add("$childPad$("$key".PadRight($keyWidth)) = $rendered")
                }
                $lines.Add("$indentText}")
                $lines -join "`n"
            }
        }
        elseif ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [String]) {
            $items = @($InputObject)
            if ($items.Count -eq 0) {
                '@()'
            }
            else {
                $lines = [System.Collections.Generic.List[String]]::new()
                $lines.Add('@(')
                foreach ($item in $items) {
                    $lines.Add("$childPad$(ConvertTo-Psd1 -InputObject $item -Indent ($Indent + 1))")
                }
                $lines.Add("$indentText)")
                $lines -join "`n"
            }
        }
        else {
            "'" + ("$InputObject" -replace "'", "''") + "'"
        }
    }
}
#EndRegion '.\Public\ConvertTo-Psd1.ps1' 95
#Region '.\Public\Enable-Tls12.ps1' -1

<#
.SYNOPSIS
Changes the security protocol of the current session to 1.2.
 
.DESCRIPTION
Changes the security protocol of the current session to 1.2.
 
.EXAMPLE
Enable-Tls12
Enables TLS 1.2 for the current session.
 
.EXAMPLE
Enable-Tls12 -Persist
Enables TLS 1.2 machine-wide.
 
.LINK
https://docs.microsoft.com/en-us/troubleshoot/azure/active-directory/enable-support-tls-environment
 
.NOTES
N/A
#>

function Enable-Tls12 {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingCmdletAliases', '')]
    [CmdletBinding()]
    param (
        [Switch]$Persist
    )

    Write-Verbose -Message '[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12'
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

    if ($Persist) {
        $null = Test-PSEnvironment -CheckAdmin -Exit
        $psRegPaths = (
            'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client',
            'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server'
        )
        foreach ($regPath in $psRegPaths) {
            $progress = @{
                Activity = 'Modifying Registry'
                Status   = "Key: $regPath"
            }
            Write-Progress @progress
            $null = New-Item -Path $regPath -Force
            $null = New-ItemProperty -Path $regPath -Name DisabledByDefault -Value 0 -PropertyType DWord -Force
            $null = New-ItemProperty -Path $regPath -Name Enabled -Value 1 -PropertyType DWord -Force
        }

        # Use reg instead of New-ItemProperty because it's not clear how
        # to modify both the 32 and 64 bit registries via PowerShell
        $startProcess = @{
            FilePath    = 'reg'
            NoNewWindow = $true
            Wait        = $true
        }
        $regPaths = (
            'HKLM\SOFTWARE\Microsoft\.NETFramework\v2.0.50727',
            'HKLM\SOFTWARE\Microsoft\.NETFramework\v4.0.30319',
            'HKLM\SOFTWARE\Wow6432Node\Microsoft\.NETFramework\v2.0.50727',
            'HKLM\SOFTWARE\Wow6432Node\Microsoft\.NetFramework\v4.0.30319'
        )
        $regValues = (
            'SystemDefaultTlsVersions',
            'SchUseStrongCrypto'
        )
        $enable = (
            '/t',
            'REG_DWORD',
            '/d',
            '1'
        )
        $registries = (
            '/reg:32',
            '/reg:64'
        )

        $totalCommands = $regPaths.Count * $regValues.Count * $registries.Count
        $count = 0
        foreach ($regPath in $regPaths) {
            foreach ($regValue in $regValues) {
                foreach ($registry in $registries) {
                    $count++
                    $startProcess['ArgumentList'] = ( 'add', $regPath, '/f' )
                    # Start-Process @startProcess
                    $startProcess['ArgumentList'] += ( '/v', $regValue, $registry )
                    $startProcess['ArgumentList'] += $enable
                    $message = 'reg ' + ($startProcess['ArgumentList']) -join ' '
                    Write-Verbose -Message $message
                    $progress = @{
                        Activity         = 'Modifying Registry'
                        Status           = "Key: $regPath, SubKey: $regValue, Value: 1"
                        CurrentOperation = "$count/$totalCommands"
                        PercentComplete  = (($count / $totalCommands) * 100)
                    }
                    Write-Progress @progress
                    Start-Process @startProcess -RedirectStandardOutput "$env:TEMP/stdout.log"
                }
            }
        }
        Write-Progress @progress -Completed
    }
}
#EndRegion '.\Public\Enable-Tls12.ps1' 103
#Region '.\Public\Export-Screenshot.ps1' -1

<#
.SYNOPSIS
Exports a screenshot as a bitmap.
 
.DESCRIPTION
Exports a screenshot as a bitmap.
 
.PARAMETER OutFile
Path to save the screenshot to.
 
.EXAMPLE
Export-Screenshot -OutFile $env:USERPROFILE/Downloads/Screenshot.bmp
 
.NOTES
N/A
 
.LINK
https://www.pdq.com/blog/capturing-screenshots-with-powershell-and-net/
#>

function Export-Screenshot {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseCompatibleTypes',
        '',
        Justification = 'System.Windows.Forms is loaded explicitly before the Windows-only type is used.'
    )]
    [CmdletBinding()]
    param (
        [String]$OutFile = "$env:TEMP/$( New-Guid ).bmp"
    )

    begin {
        Add-Type -AssemblyName System.Windows.Forms
        Add-Type -AssemblyName System.Drawing

        $screen = [System.Windows.Forms.SystemInformation]::VirtualScreen
        $Script:width = $screen.Width
        $Script:height = $screen.Height
        $Script:left = $screen.Left
        $Script:top = $screen.Top
    }

    process {
        $bitmap = New-Object -TypeName System.Drawing.Bitmap -ArgumentList ($Script:width, $Script:height)
        $graphic = [System.Drawing.Graphics]::FromImage($bitmap)
        $graphic.CopyFromScreen($Script:left, $Script:top, 0, 0, $bitmap.Size)
        $bitmap.Save($OutFile)
        Get-Item -Path $OutFile
    }
}
#EndRegion '.\Public\Export-Screenshot.ps1' 50
#Region '.\Public\Get-EnvironmentVariable.ps1' -1

<#
.SYNOPSIS
Gets the value of of an environment variable.
 
.DESCRIPTION
Gets the value of of an environment variable.
 
.PARAMETER Name
Name of the environment variable.
 
.PARAMETER Scope
Environment scope: Machine, Process, or User.
 
.EXAMPLE
Get-EnvironmentVariable -Name PATH -Scope User
 
.NOTES
N/A
#>


function Get-EnvironmentVariable {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String[]]$Name,
        [ValidateSet('Machine', 'Process', 'User')]
        [String]$Scope = 'Process'
    )

    process {
        foreach ($item in $Name) {
            [PSCustomObject]@{
                Name  = $item
                Value = [System.Environment]::GetEnvironmentVariable($item, $Scope)
                Scope = $Scope
            }
        }
    }
}
#EndRegion '.\Public\Get-EnvironmentVariable.ps1' 40
#Region '.\Public\Get-PatPSCredential.ps1' -1

<#
.SYNOPSIS
Creates a PSCredential object for a Personal Access Token.
 
.DESCRIPTION
Creates a PSCredential object for a Personal Access Token.
 
.PARAMETER Pat
The Personal Access Token (PAT) to use.
 
.PARAMETER Username
The username to associate with the PAT.
 
.EXAMPLE
An example
 
.NOTES
General notes
Parameter description
 
.PARAMETER Username
Parameter description
 
.EXAMPLE
Get-PatPSCredential -Pat 'my-personal-access-token' -Username 'my-username'
 
.NOTES
N/A
#>

function Get-PatPSCredential {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')]
    [CmdletBinding()]
    param (
        [Parameter()]
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN,

        [String]$Username = 'PAT'
    )

    if ([string]::IsNullOrEmpty($Pat)) {
        throw 'No PAT provided and SYSTEM_ACCESSTOKEN environment variable is not set'
    }

    $securePat = ConvertTo-SecureString -String $Pat -AsPlainText -Force
    New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Username, $securePat
}
#EndRegion '.\Public\Get-PatPSCredential.ps1' 47
#Region '.\Public\Get-PSVersion.ps1' -1

<#
.SYNOPSIS
A simple function wrapper for getting the value of $PSVersionTable.PSVersion.
 
.DESCRIPTION
A simple function wrapper for getting the value of $PSVersionTable.PSVersion, used for unit testing.
 
.EXAMPLE
Get-PSVersion
 
.NOTES
N/A
#>

function Get-PSVersion {
    [CmdletBinding()]
    param()

    $PSVersionTable.PSVersion
}
#EndRegion '.\Public\Get-PSVersion.ps1' 20
#Region '.\Public\Get-TempDirectory.ps1' -1

<#
.SYNOPSIS
Gets a temporary directory path for the current host.
 
.DESCRIPTION
Uses the runtime temp-path resolver because $env:TEMP is not set reliably on macOS or Linux.
 
.OUTPUTS
System.String
 
.EXAMPLE
Get-TempDirectory
 
.NOTES
N/A
#>

function Get-TempDirectory {
    [CmdletBinding()]
    [OutputType([String])]
    param()

    [System.IO.Path]::GetTempPath()
}
#EndRegion '.\Public\Get-TempDirectory.ps1' 24
#Region '.\Public\Initialize-GitConfig.ps1' -1

<#
.SYNOPSIS
Sets the git user name and email in a given scope.
 
.DESCRIPTION
Sets the git user name and email in a given scope.
 
.PARAMETER Path
Path of the repo.
 
.PARAMETER UserName
User name to add to the git config.
 
.PARAMETER UserEmail
Email to add to the git config.
 
.PARAMETER Scope
Scope of the git config.
 
.EXAMPLE
Initialize-GitConfig -UserName "Git User" -UserEmail email@example.com
 
.NOTES
N/A
#>

function Initialize-GitConfig {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName', 'LiteralPath')]
        [String[]]$Path = $PWD,
        [String]$UserName = $env:BUILD_REQUESTEDFOR,
        [String]$UserEmail = $env:BUILD_REQUESTEDFOREMAIL,
        [ValidateSet('System', 'Global', 'WorkTree', 'Local')]
        [String]$Scope = 'Local'
    )

    begin {
        $script:CurrentLocation = $PWD
        $script:Git = @{
            FilePath    = Get-Command -Name git.exe | Select-Object -ExpandProperty Source
            NoNewWindow = $true
            Wait        = $true
        }
        $script:Config = @(
            'config',
            "--$($Scope.ToLower())"
        )
    }

    process {
        Start-Process @script:Git -ArgumentList ($script:Config + ('http.version', 'HTTP/1.1'))
        $userEntries = @(
            @{
                ArgumentList = $script:Config + (
                    '--replace-all',
                    'user.email',
                    "`"$UserEmail`""
                )
            },
            @{
                ArgumentList = $script:Config + (
                    '--replace-all',
                    'user.name',
                    "`"$UserName`""
                )
            }
        )

        foreach ($location in $Path) {
            foreach ($user in $userEntries) {
                Start-Process @script:Git @user -WorkingDirectory $location
            }
        }
    }

    end {
        Set-Location -Path $script:CurrentLocation
    }
}
#EndRegion '.\Public\Initialize-GitConfig.ps1' 81
#Region '.\Public\Install-NugetCLI.ps1' -1

<#
.SYNOPSIS
Installs NuGet CLI.
 
.DESCRIPTION
Installs NuGet CLI and adds it to the path.
 
.PARAMETER Path
Path to download and execute nuget.exe from.
 
.EXAMPLE
Install-NugetCli
 
.NOTES
N/A
#>

function Install-NugetCli {
    [CmdletBinding()]
    param (
        [String]$Path = ( Join-Path -Path $env:APPDATA -ChildPath 'NuGet' )
    )

    $null = New-Item -Path $Path -ItemType Directory -Force -ErrorAction SilentlyContinue
    $currentProgressPreference = $ProgressPreference
    $ProgressPreference = 'SilentlyContinue'
    Write-Host -Object "Downloading nuget.exe to $Path..."
    Invoke-WebRequest `
        -Uri 'https://dist.nuget.org/win-x86-commandline/latest/nuget.exe' `
        -UseBasicParsing `
        -OutFile "$Path/nuget.exe"
    $ProgressPreference = $currentProgressPreference
    Write-Host -Object "Adding $Path to %PATH%..."
    $null = Set-EnvironmentVariable -Name 'PATH' -Value $Path -Scope User -Append 3>&1
    $env:PATH = $Path + [System.IO.Path]::PathSeparator + $env:PATH
    Remove-Item -Path 'Alias:nuget' -ErrorAction SilentlyContinue
    Set-Alias -Name 'nuget' -Value ( Join-Path -Path $Path -ChildPath 'nuget.exe' )
    Get-Command -Name 'nuget.exe'
}
#EndRegion '.\Public\Install-NugetCLI.ps1' 39
#Region '.\Public\Reset-ConsoleColor.ps1' -1

<#
.SYNOPSIS
Resets the current console to the default colors.
 
.DESCRIPTION
Resets the current console to the default colors.
 
.EXAMPLE
Reset-ConsoleColor
 
.NOTES
N/A
#>


function Reset-ConsoleColor {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions',
        '',
        Justification = 'Resetting colors affects only the current console presentation.'
    )]
    [CmdletBinding()]
    param ()
    [Console]::ResetColor()
}
#EndRegion '.\Public\Reset-ConsoleColor.ps1' 25
#Region '.\Public\Set-EnvironmentVariable.ps1' -1

<#
.SYNOPSIS
Sets an environment variable.
 
.DESCRIPTION
Sets an environment variable.
 
.PARAMETER Name
Name of the environment variable.
 
.PARAMETER Value
Parameter description
 
.PARAMETER Scope
Environment scope: Machine, Process, or User.
 
.PARAMETER Append
Appends the given value to the variable instead of replacing it.
 
.PARAMETER Delete
Deletes the named environment variable.
 
.PARAMETER PassThru
Outputs an environment variable object to the pipeline.
 
.PARAMETER Force
Sets the value even if it already exists in the environment variable, and doesn't prompt to modify the Path.
 
.EXAMPLE
Set-EnvironmentVariable -Name PATH -Value 'C:/Program Files/NuGet' -Scope Machine -Append
Adds 'C:/Program Files/NuGet' to the System PATH variable.
.NOTES
N/A
#>


function Set-EnvironmentVariable {
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Set')]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [String]$Name,

        [Parameter(ParameterSetName = 'Set', ValueFromPipelineByPropertyName = $true)]
        [String]$Value,

        [ValidateSet('Machine', 'Process', 'User')]
        [String]$Scope = 'Process',

        [Parameter(ParameterSetName = 'Set')]
        [Switch]$Append,

        [Parameter(ParameterSetName = 'Delete')]
        [Switch]$Delete,

        [Parameter(ParameterSetName = 'Set')]
        [Switch]$PassThru,

        [Parameter(ParameterSetName = 'Set')]
        [Switch]$Force
    )

    begin {
        if ($Scope -eq 'Machine') {
            $null = Test-PSEnvironment -CheckAdmin -Exit
        }
    }

    process {
        if ($Delete) {
            if ($PSCmdlet.ShouldProcess("$($Scope):$Name", 'Delete') -or $Force) {
                [System.Environment]::SetEnvironmentVariable($Name, $null, $Scope)
                Set-Item -Path "env:$Name" -Value $null -Force
            }
            return
        }

        $envVariableValue = Get-EnvironmentVariable -Name $Name -Scope $Scope |
            Select-Object -ExpandProperty Value
        if ($null -eq $envVariableValue) {
            Write-Verbose -Message "Environment variable $($Scope):$Name does not exist."
            $envVariableValue = ''
        }

        $isNotArray = $envVariableValue -notmatch [System.IO.Path]::PathSeparator
        $isNotEqualToEnvValue = $envVariableValue.Trim() -ne $Value.Trim()
        $isNotArrayIsNotEqual = $isNotArray -and $isNotEqualToEnvValue
        $isArray = $envVariableValue -match [System.IO.Path]::PathSeparator
        $isNotValueInEnvArray = $envVariableValue -notmatch [Regex]::Escape($Value)
        $isArrayIsNotMatching = $isArray -and $isNotValueInEnvArray

        if ($Force -or $isNotArrayIsNotEqual -or $isArrayIsNotMatching) {
            if ($Name -eq 'PATH' -and (-not $Append) -and (-not $Force)) {
                Write-Warning -Message (
                    "This will overwrite all entries in the $($Scope):PATH variable with: $Value"
                )
                $shouldAppend = Read-Host -Prompt "Should $Value be appended instead? (y/n)"
                if ($shouldAppend -eq 'y') {
                    $Append = $true
                }
            }

            # Ensure that the existing PATH variable doesn't get corrupted when appending
            $valueWithPathSeparator = if (
                $Append -and
                $Name -eq 'PATH' -and
                $Value[0] -ne [System.IO.Path]::PathSeparator
            ) {
                [System.IO.Path]::PathSeparator + $Value
            }
            else {
                $Value
            }

            $finalValue = if ($Append) {
                $envVariableValue + $valueWithPathSeparator
            }
            else {
                $valueWithPathSeparator
            }

            if ($PSCmdlet.ShouldProcess("$($Scope):$Name", "Setting value to $finalValue") -or $Force) {
                [System.Environment]::SetEnvironmentVariable($Name, $finalValue, $Scope)

                if ($Name -match 'path' -and $isArray) {
                    $scopedValue = (
                        Get-Item -Path "env:$($Name.ToUpper())"
                    ).Value.Split([System.IO.Path]::PathSeparator)
                    $newValue = $finalValue.Split([System.IO.Path]::PathSeparator)
                    if (
                        Compare-Object `
                            -ReferenceObject ( $scopedValue | Sort-Object ) `
                            -DifferenceObject ( $newValue | Sort-Object )
                    ) {
                        $combinedValue = $scopedValue + $newValue
                        $finalValue = $combinedValue -join [System.IO.Path]::PathSeparator
                    }
                }

                # Only update process environment if we're targeting Process scope or PATH
                if ($Scope -eq 'Process' -or $Name -match 'path') {
                    Set-Item -Path "env:$Name" -Value $finalValue -Force
                }
            }
        }
        else {
            Write-Warning -Message (
                "The environment variable $($Scope):$Name already contains a value of $Value. " +
                'Nothing was modified; use -Force to overwrite it.'
            )
        }
        if ($PassThru) {
            Get-EnvironmentVariable -Name $Name -Scope $Scope
        }
    }
}
#EndRegion '.\Public\Set-EnvironmentVariable.ps1' 155
#Region '.\Public\Set-JsonFile.ps1' -1

<#
.SYNOPSIS
Writes an object to a JSON file.
 
.DESCRIPTION
Serializes the input object to JSON (depth 100, to cover deeply nested config/manifest objects) and
writes it to the given path.
 
.PARAMETER InputObject
The object to serialize. Accepts pipeline input.
 
.PARAMETER Path
The file path to write to.
 
.EXAMPLE
$manifest | Set-JsonFile -Path './release-manifest.json'
 
.NOTES
N/A
#>

function Set-JsonFile {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [Object]$InputObject,

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

    process {
        if ($PSCmdlet.ShouldProcess($Path, 'Write JSON file')) {
            $InputObject | ConvertTo-Json -Depth 100 | Set-Content -LiteralPath $Path -Encoding utf8
        }
    }
}
#EndRegion '.\Public\Set-JsonFile.ps1' 37
#Region '.\Public\Show-ConsoleColor.ps1' -1

<#
.SYNOPSIS
Shows available colors that can be used in the console.
 
.DESCRIPTION
Shows available colors that can be used in the console and examples of what they will look like.
 
.EXAMPLE
Show-ConsoleColor
 
Lists available colors and shows what the color will look like in the current console.
 
.NOTES
N/A
#>


function Show-ConsoleColor {
    [CmdletBinding()]
    param()
    Write-Host -Object ('{0,-120}' -f ' ') -ForegroundColor Black -BackgroundColor White
    foreach ($heading in 'Color', 'Foreground', 'Background') {
        Write-Host -Object ('{0,-40}' -f $heading) -ForegroundColor Black -BackgroundColor White -NoNewline
    }
    Write-Host
    $colors = [enum]::GetValues([System.ConsoleColor])
    foreach ($color in $colors) {
        $object = @{ Object = ('{0,-40}' -f $color) }
        Write-Host @object -NoNewline
        Write-Host @object -ForegroundColor $color -NoNewline
        Write-Host @object -ForegroundColor $colors[$colors.Count - $color - 1] -BackgroundColor $color
    }
}
#EndRegion '.\Public\Show-ConsoleColor.ps1' 33
#Region '.\Public\Start-CliProcess.ps1' -1

<#
.SYNOPSIS
Starts a process using the .net Process class instead of Start-Process.
 
.DESCRIPTION
Starts a process using the .net Process class instead of Start-Process. This allows console output to be captured
as well as non-zero exit codes at the same time.
 
.PARAMETER FilePath
Path to the process to start.
 
.PARAMETER ArgumentList
Arguments for the process.
 
.PARAMETER WorkingDirectory
Directory to execute the process in.
 
.PARAMETER PassThru
Pass the output to the pipeline.
 
.PARAMETER Title
Title of the progress bar that displays while the CLI is running.
 
.PARAMETER NoProgress
Output the command details to the information stream instead of as a progress bar.
 
.PARAMETER RedirectStandardError
Redirects the standard error stream to the standard output stream. Exe's that use StdErr for info: git
 
.EXAMPLE
Start-CliProcess -FilePath 'cmd' -ArgumentList '/c "echo hello"' -PassThru
 
.NOTES
Replaces Start-Process and Invoke-Process.
 
Notes on avoiding process deadlocks when updating the output logic:
https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.standarderror#remarks
#>

function Start-CliProcess {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingCmdletAliases', '')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(ValueFromPipelineByPropertyName = $true, Position = 0)]
        [Alias('LiteralPath', 'FullName')]
        [String[]]$FilePath,
        [String[]]$ArgumentList,
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias('Directory')]
        [String]$WorkingDirectory = $PWD,
        [Switch]$PassThru,
        [Alias('Activity')]
        [String]$Title = 'Start CLI Process',
        [Switch]$NoProgress,
        [Switch]$RedirectStandardError
    )

    begin {
        $script:processes = @()
        $script:noProgress = if ($NoProgress -or $env:AGENT_JOBNAME) {
            $true
        }
        else {
            $false
        }
    }

    process {
        $procInfoArgs = if ($ArgumentList.Count -gt 1) {
            $ArgumentList | ForEach-Object -Process {
                $thisTrimmed = $_.Trim()
                if ($thisTrimmed -match ' ' -and $thisTrimmed -notmatch '"') {
                    "`"$thisTrimmed`""
                }
                else {
                    $thisTrimmed
                }
            }
        }
        elseif ($ArgumentList.Count -eq 1) {
            $ArgumentList.Trim()
        }
        foreach ($file in $FilePath) {
            $fileInfo = ( Get-Command -Name $file ).Source | Get-Item
            $command = "`"$($fileInfo.FullName)`" $($ArgumentList -join ' ')"
            if (-not $PSCmdlet.ShouldProcess($command, "Start process in '$WorkingDirectory'")) {
                continue
            }

            $processInfo = New-Object -TypeName 'System.Diagnostics.ProcessStartInfo'
            $processInfo.FileName = $fileInfo.FullName
            $processInfo.Arguments = $procInfoArgs
            $processInfo.WorkingDirectory = $WorkingDirectory
            $processInfo.CreateNoWindow = $true
            $processInfo.UseShellExecute = $false
            $processInfo.RedirectStandardOutput = $true
            $processInfo.RedirectStandardError = $true
            $process = New-Object -TypeName 'System.Diagnostics.Process'
            $process.StartInfo = $processInfo

            $progress = @{
                Activity         = $Title
                Status           = "Starting $(( Get-Command -Name $fileInfo.FullName ).Name) in $WorkingDirectory"
                CurrentOperation = $command
            }

            if (-not $script:noProgress) {
                Write-Progress @progress
            }
            else {
                Write-ProgressToHost @progress
            }

            $null = $process.Start()
            $procState = 'Running'

            do {
                if ($procState -eq 'LastRun') {
                    $readMethod = 'ReadToEnd'
                    $procState = 'Finished'
                }
                else {
                    $readMethod = 'ReadLine'
                }

                while (-not $process.StandardOutput.EndOfStream) {
                    $process.StandardOutput.$readMethod() | ForEach-Object -Process {
                        if ($PassThru) {
                            $_
                        }
                        else {
                            Write-Host -Object $_
                        }
                        if (-not $script:noProgress) {
                            Write-Progress @progress
                        }
                    }
                }

                $stdErr = @()
                while (-not $process.StandardError.EndOfStream) {
                    $process.StandardError.$readMethod() | ForEach-Object -Process {
                        if ($_) {
                            $stdErr += $_
                        }
                        if ($PassThru) {
                            $_
                        }
                        elseif ($RedirectStandardError) {
                            Write-Host -Object $_
                        }
                        if (-not $script:noProgress) {
                            Write-Progress @progress
                        }
                    }
                }
                if ($stdErr) {
                    $stdErrString = $stdErr -join "`n"
                    if (-not $RedirectStandardError) {
                        Write-Error `
                            -Message $stdErrString `
                            -Category FromStdErr `
                            -TargetObject $fileInfo.Name
                        if (-not $script:noProgress) {
                            Write-Progress @progress
                        }
                    }
                }

                if ($process.HasExited -and $procState -eq 'Running') {
                    $procState = 'LastRun'
                }
            } while ($procState -ne 'Finished')
            if (-not $script:noProgress) {
                Write-Progress @progress -Completed
            }

            if (
                ($process.ExitCode -ne 0) -and
                ($ErrorActionPreference -ne 'SilentlyContinue') -and
                ($ErrorActionPreference -ne 'Ignore')
            ) {
                throw $process.ExitCode
            }
            $script:processes += $process.Id
        }
    }

    end {
        if ($script:processes) {
            Stop-Process -Id $script:processes -Force -PassThru -ErrorAction SilentlyContinue | Wait-Process
        }
    }
}
#EndRegion '.\Public\Start-CliProcess.ps1' 195
#Region '.\Public\Start-StopWatch.ps1' -1

<#
.SYNOPSIS
Starts a System.Diagnostics.Stopwatch instance.
 
.DESCRIPTION
Starts a System.Diagnostics.Stopwatch instance.
 
.PARAMETER InputObject
An existing Stopwatch object to start.
 
.EXAMPLE
$sw = Start-Stopwatch
 
.NOTES
N/A
#>

function Start-Stopwatch {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions',
        '',
        Justification = 'Starting an in-memory stopwatch does not change external system state.'
    )]
    [OutputType([System.Diagnostics.Stopwatch])]
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true)]
        [System.Object]$InputObject
    )

    process {
        if ($InputObject) {
            $InputObject.Start()
            $InputObject
        }
        else {
            [System.Diagnostics.Stopwatch]::StartNew()
        }
    }
}
#EndRegion '.\Public\Start-StopWatch.ps1' 40
#Region '.\Public\Start-Timeout.ps1' -1

<#
.SYNOPSIS
Waits for a specified time with the option to press a key to continue.
 
.DESCRIPTION
Waits for a specified time with the option to press a key to continue.
 
.PARAMETER Seconds
The amount of seconds to wait before continuing.
 
.PARAMETER NoBreak
Removes the option to press a key to continue.
 
.EXAMPLE
Start-Timeout -Seconds 5
 
.NOTES
Is just a more PS friendly wrapper for timeout.exe
#>


function Start-Timeout {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions',
        '',
        Justification = 'Waiting does not change external system state.'
    )]
    [CmdletBinding()]
    param (
        [Int]$Seconds = 0,
        [Switch]$NoBreak
    )

    if (Test-IsNonInteractiveShell) {
        for ($i = $Seconds; $i -ge 0; $i--) {
            $activity = "Waiting for $i seconds,"
            Write-Progress -Activity $activity -Status 'press CTRL+C to quit ...'
            Start-Sleep -Seconds 1
        }
        Write-Progress $activity -Completed
    }
    else {
        $scriptString = "Invoke-Timeout /t $Seconds"
        if ($NoBreak) {
            $scriptString += ' /nobreak'
        }
        $timeout = [ScriptBlock]::Create($scriptString)
        Invoke-Command -ScriptBlock $timeout
    }
}
#EndRegion '.\Public\Start-Timeout.ps1' 50
#Region '.\Public\Stop-DevProcess.ps1' -1

<#
.SYNOPSIS
Stops any processes that may interfere with a product build.
 
.DESCRIPTION
Stops any processes that may interfere with a product build, includes Visual Studio and Selenium ChromeDriver by
default.
 
.PARAMETER Optional
A list of process names that will prompt to stop before stopping them. Always includes Visual Studio.
 
.PARAMETER Required
A list of process names that will be stopped without prompting. Always includes Selenium ChromeDriver.
 
.EXAMPLE
Stop-DevProcess
 
.EXAMPLE
Stop-DevProcess -Required @('*chromedriver', 'foviawebsdk')
#>

function Stop-DevProcess {
    [CmdletBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact = 'Low'
    )]
    param (
        [Parameter(Position = 0)]
        [string[]]$Optional,
        [string[]]$Required
    )

    # $Optional += @(
    # 'devenv'
    # )
    $Required += @(
        '*chromedriver'
    )

    foreach ($processName in $Optional) {
        $processes = @( Get-Process -Name $processName -ErrorAction SilentlyContinue )
        foreach ($process in $processes) {
            $programName = if ($process.Product) {
                $process.Product
            }
            else {
                $process.ProcessName
            }
            $nameString = "$programName [$($process.Id)]"
            $stop = Read-Host -Prompt (
                "$nameString is running and could cause issues. Would you like to stop it? (y/n):"
            )
            if ($stop.ToLower() -eq 'y') {
                if ($PSCmdlet.ShouldProcess($nameString, 'Stop Process')) {
                    $null = Test-PSEnvironment -CheckAdmin -Exit
                    $process | Stop-ProcessTree
                }
            }
        }
    }
    foreach ($processName in $Required) {
        $processes = @( Get-Process -Name $processName -ErrorAction SilentlyContinue )
        foreach ($process in $processes) {
            $programName = if ($process.Product) {
                $process.Product
            }
            else {
                $process.ProcessName
            }
            $nameString = "$programName [$($process.Id)]"
            $stopMessage = "$nameString is running and will be stopped."
            Write-Host -Object $stopMessage
            if ($PSCmdlet.ShouldProcess($nameString, 'Stop Process')) {
                $null = Test-PSEnvironment -CheckAdmin -Exit
                $process | Stop-ProcessTree
            }
        }
    }
}
#EndRegion '.\Public\Stop-DevProcess.ps1' 79
#Region '.\Public\Stop-ProcessTree.ps1' -1

<#
.SYNOPSIS
Stops a process and all child processes that it spawned.
 
.DESCRIPTION
Stops a process and all child processes that it spawned.
 
.PARAMETER ProcessId
ID of the process to stop.
 
.EXAMPLE
Get-Process cmd | Stop-ProcessTree
 
.LINK
https://stackoverflow.com/a/55942155/14628263
 
.NOTES
N/A
#>

function Stop-ProcessTree {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [Int[]]$ProcessId
    )

    begin {
        $script:CurrentProcess = Get-CimInstance -ClassName Win32_Process |
            Where-Object -FilterScript { $_.ProcessId -eq $PID }
    }

    process {
        foreach ($id in $ProcessId) {
            Get-CimInstance -ClassName Win32_Process |
                Where-Object -FilterScript {
                    $_.ParentProcessId -eq $id -and
                    (
                        $script:CurrentProcess.ProcessId,
                        $script:CurrentProcess.ParentProcessId
                    ) -notcontains $_.ProcessId
                } |
                ForEach-Object -Process { Stop-ProcessTree -ProcessId $_.ProcessId }
            $processToStop = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue
            if ($processToStop) {
                <# Use for debugging unexpected kill behavior
                $processToStop |
                    Format-Table -HideTableHeaders |
                    Out-File -FilePath "$PSScriptRoot/Stop-ProcessTree.log" -Append
                #>

                $processToStop | Stop-Process -Force -PassThru | Wait-Process
            }
        }
    }
}
#EndRegion '.\Public\Stop-ProcessTree.ps1' 56
#Region '.\Public\Stop-Stopwatch.ps1' -1

<#
.SYNOPSIS
Stops a stopwatch started from Start-Stopwatch.
 
.DESCRIPTION
Stops a stopwatch started from Start-Stopwatch.
 
.PARAMETER InputObject
A stopwatch object from Start-StopWatch.
 
.EXAMPLE
$sw = Start-Stopwatch; $sw | Stop-Stopwatch
 
.NOTES
N/A
#>

function Stop-Stopwatch {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions',
        '',
        Justification = 'Stopping an in-memory stopwatch does not change external system state.'
    )]
    [OutputType([System.Diagnostics.Stopwatch])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.Object]$InputObject
    )

    process {
        $InputObject.Stop()
        $InputObject
    }
}
#EndRegion '.\Public\Stop-Stopwatch.ps1' 35
#Region '.\Public\Test-CommandAvailable.ps1' -1

<#
.SYNOPSIS
Verifies a required CLI command is available on PATH.
 
.DESCRIPTION
Throws a clear, actionable error when the command cannot be found, rather than letting a later native
command invocation fail with an unhelpful "command not found" error.
 
.PARAMETER CommandName
The command to check for.
 
.PARAMETER ErrorMessage
A custom error message. Defaults to a generic "not on PATH" message naming the command.
 
.EXAMPLE
Test-CommandAvailable -CommandName 'docker'
 
.NOTES
N/A
#>

function Test-CommandAvailable {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [String]$CommandName,

        [String]$ErrorMessage = (
            "The '$CommandName' command is not on PATH. " +
            'Install the CLI or add its bin directory to PATH.'
        )
    )

    if (-not (Get-Command -Name $CommandName -ErrorAction SilentlyContinue)) {
        throw $ErrorMessage
    }
}
#EndRegion '.\Public\Test-CommandAvailable.ps1' 37
#Region '.\Public\Test-IsAdmin.ps1' -1

<#
.SYNOPSIS
Tests whether the current console is running with admin priviledges.
 
.DESCRIPTION
Tests whether the current console is running with admin priviledges.
 
.EXAMPLE
Test-IsAdmin
 
.NOTES
N/A
#>

function Test-IsAdmin {
    [OutputType([Bool])]
    [CmdletBinding()]
    param ()

    $currentUser = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
    $currentUser.IsInRole([Security.Principal.WindowsBuiltInRole]'Administrator')
}
#EndRegion '.\Public\Test-IsAdmin.ps1' 22
#Region '.\Public\Test-IsFileLocked.ps1' -1

<#
.SYNOPSIS
Short description
 
.DESCRIPTION
Long description
 
.PARAMETER Path
Parameter description
 
.EXAMPLE
An example
 
.LINK
https://mcpmag.com/articles/2018/07/10/check-for-locked-file-using-powershell.aspx
 
.NOTES
General notes
#>

function Test-IsFileLocked {
    [OutputType([Bool])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [Alias('FullName', 'PSPath', 'LiteralPath', 'Path')]
        [string[]]$FilePath
    )

    process {
        foreach ($file in $FilePath) {
            $file = Convert-Path -Path $file
            if ([System.IO.File]::Exists($file)) {
                try {
                    $filestream = [System.IO.File]::Open($file, 'Open', 'Write')
                    $filestream.Close()
                    $filestream.Dispose()
                    $false
                }
                catch [System.UnauthorizedAccessException] {
                    $true
                }
                catch {
                    $true
                }
            }
        }
    }
}
#EndRegion '.\Public\Test-IsFileLocked.ps1' 49
#Region '.\Public\Test-IsNonInteractiveShell.ps1' -1

<#
.SYNOPSIS
Returns boolean determining if prompt was run non-interactively.
 
.DESCRIPTION
First, we check `[Environment]::UserInteractive` to determine if the shell is running
interactively. An example of not running interactively would be if the shell is running as a service.
If we are running interactively, we check the Command Line Arguments to see if the `-NonInteractive`
switch was used; or an abbreviation of the switch.
 
.LINK
https://github.com/Vertigion/Test-IsNonInteractiveShell
#>

function Test-IsNonInteractiveShell {
    [CmdletBinding()]
    [OutputType([Boolean])]
    param ()

    if ([Environment]::UserInteractive) {
        $commandLineArgs = [Environment]::GetCommandLineArgs()
        $isNonInteractive = $commandLineArgs -contains '-NonInteractive'
        $isVsCode = foreach ($arg in $commandLineArgs) {
            if ($arg -match "-HostProfileId\ 'Microsoft\.VSCode'") {
                $true
            }
        }
    }

    if (
        $env:AGENT_JOBNAME -or
        (
            $isNonInteractive -and
            -not $isVsCode
        )
    ) {
        $true
    }
    else {
        $false
    }
}
#EndRegion '.\Public\Test-IsNonInteractiveShell.ps1' 42
#Region '.\Public\Test-PSEnvironment.ps1' -1

<#
.SYNOPSIS
Checks whether the current PowerShell environment is sufficient to run the script.
 
.DESCRIPTION
Checks whether the current PowerShell environment is sufficient to run the script by checking the installed
version and whether it is being ran as an admin.
 
.PARAMETER MinimumVersion
Minimum version of PowerShell to check for.
 
.PARAMETER MaximumVersion
Minimum version of PowerShell to check for.
 
.PARAMETER CheckAdmin
Check to see whether the prompt is running as an administrator.
 
.PARAMETER Exit
Throws an error instead of returning $false.
 
.EXAMPLE
Test-PSEnvironment
 
.EXAMPLE
Test-PSEnvironment -CheckAdmin -Exit
 
.NOTES
N/A
#>


function Test-PSEnvironment {
    [OutputType([Bool])]
    [CmdletBinding()]
    param (
        [AllowEmptyString()]
        [AllowNull()]
        [System.Object]
        $MinimumVersion = [System.Version]::new('5.1.0'),

        [AllowEmptyString()]
        [AllowNull()]
        [System.Object]
        $MaximumVersion,

        [Switch]
        $CheckAdmin,

        [Switch]
        $Exit
    )

    $errMsg = @()
    if ($CheckAdmin -eq $true) {
        if ( Test-IsAdmin ) {
            Write-Verbose -Message 'Host is running with admin priviledges.'
        }
        else {
            $errMsg += 'Host is not running with admin priviledges.'
        }
    }

    $hostVersion = Get-PSVersion
    if ($MinimumVersion) {
        if ($MinimumVersion.GetType().Name -eq 'String') {
            $MinimumVersion = [System.Version]::new($MinimumVersion)
        }
        Write-Verbose -Message (
            "Checking host version: $hostVersion against minimum " +
            "version $MinimumVersion."
        )
        if ($hostVersion -lt $MinimumVersion) {
            $errMsg += (
                "The minimum version of Windows PowerShell that is required by the script ($MinimumVersion) " +
                "does not match the currently running version ($hostVersion) of Windows PowerShell."
            )
        }
    }

    if ($MaximumVersion) {
        if ($MaximumVersion.GetType().Name -eq 'String') {
            $MaximumVersion = [System.Version]::new($MaximumVersion)
        }
        Write-Verbose -Message (
            "Checking host version: $hostVersion against maximum " +
            "version $MaximumVersion."
        )
        if ($hostVersion -gt $MaximumVersion) {
            $errMsg += (
                "The maximum version of Windows PowerShell that is required by the script ($MaximumVersion) " +
                "does not match the currently running version ($hostVersion) of Windows PowerShell."
            )
        }
    }

    if ($errMsg) {
        $false
        if ($Exit) {
            throw $errMsg
        }
        else {
            Write-Host -Object $errMsg
        }
    }
    else {
        $true
    }
}
#EndRegion '.\Public\Test-PSEnvironment.ps1' 108
#Region '.\Public\Uninstall-ProgramByName.ps1' -1

<#
.SYNOPSIS
Uninstalls a program by name.
 
.DESCRIPTION
Uninstalls a program by name, instead of having to call its installer.
 
.PARAMETER Name
Name to search for. Accepts wildcard characters. Accepts pipeline input.
 
.EXAMPLE
Uninstall-ProgramByName -Name 'visual studio code'
 
.NOTES
General notes
#>


function Uninstall-ProgramByName {
    #[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingCmdletAliases', '')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [String]$Name,
        [Switch]$Wmi
    )

    begin {
        $null = Test-PSEnvironment -MinimumVersion 5.1 -CheckAdmin -Exit
    }

    process {
        $Activity = "Uninstall $Name"
        if ($Wmi) {
            $status = 'Finding the installation...'
            Write-Progress -Activity $Activity -Status $status
            $attempts = 0
            $app = @()
            do {
                $attempts++
                $uninstallError = $false
                if ($attempts -gt 1) {
                    Write-Progress -Activity $Activity -Status $status -CurrentOperation "Attempt: $attempts"
                }
                try {
                    Write-Verbose -Message 'Gathering all installed apps...'
                    $apps = Get-CimInstance -ClassName 'Win32_Product' -ErrorAction Stop
                    Write-Verbose -Message "Finding $Name to uninstall..."
                    $app += $apps | Where-Object {
                        $_.Name -match [Regex]::Escape($Name)
                    }
                }
                catch {
                    Write-Verbose -Message "Attempt $attempts failed."
                    Write-Verbose -Message $_
                    $uninstallError = $true
                }
            }
            while (($uninstallError -eq $true) -and ($attempts -le 3))
            if (-not $app) {
                Write-Progress -Activity $Activity -Completed
                Write-Warning "$Name is either not installed, or can't be found by this function."
            }
            else {
                Write-Verbose -Message "Uninstalled $Name on attempt $attempts."
            }
            foreach ($instance in $app) {
                Write-Verbose -Message "Uninstalling $($instance.Name) $($instance.Version)..."
                Write-Progress -Activity $Activity -Status "Uninstalling $($instance.Name) $($instance.Version)..."
                Invoke-CimMethod -InputObject $instance -MethodName 'Uninstall'
                Write-Progress -Activity $Activity -Completed
            }
        }
        else {
            Write-Progress -Activity $Activity -Status 'Finding the installation...'
            $programLocations = @(
                'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall',
                'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
            )
            $apps = Get-ChildItem -Path $programLocations | Get-ItemProperty | Sort-Object -Property DisplayName
            $app = @()
            $app += $apps | Where-Object {
                $_.DisplayName -match [Regex]::Escape($Name)
            }
            if (-not $app) {
                Write-Progress -Activity $Activity -Completed
                Write-Warning "$Name is either not installed, or can't be found by this function."
            }
            foreach ($instance in $app) {
                Write-Verbose -Message "Uninstalling $($instance.Name) $($instance.DisplayVersion)..."
                Write-Progress `
                    -Activity $Activity `
                    -Status "Uninstalling $($instance.Name) $($instance.DisplayVersion)..."
                $uninstallString = $instance.UninstallString
                $isExeOnly = Test-Path -LiteralPath $uninstallString
                if (-not $isExeOnly) {
                    $uninstallString += ' /passive /norestart'
                    # Need to explicitly set uninstall for installers that just call themselves again to uninstall
                    $uninstallString = $uninstallString.Replace('/I', '/uninstall ')
                }
                $process = Start-Process -FilePath cmd -ArgumentList ('/c', $uninstallString) -PassThru
                $process | Wait-Process
                $process | Select-Object -Property ProcessName, ExitCode
                Write-Progress -Activity $Activity -Completed
            }
        }
    }
}
#EndRegion '.\Public\Uninstall-ProgramByName.ps1' 108
#Region '.\Public\Write-ConsoleMessage.ps1' -1

<#
.SYNOPSIS
Writes a formatted message to the host.
 
.DESCRIPTION
Two ways to style output: -ForegroundColor for direct color control with no other formatting, or
-Intent for semantic meaning. The intent controls color and output-stream routing, while -Layout
controls presentation and -Child selects the secondary form of that layout or intent. Legacy
compound intents remain available while callers migrate to the separated parameters.
 
.PARAMETER Message
The message to display.
 
.PARAMETER ForegroundColor
Writes the message in this color with no other formatting. Mutually exclusive with -Intent.
 
.PARAMETER Intent
The semantic purpose of the message: Info, Action, Success, Warning, Caution, or Muted. Mutually
exclusive with -ForegroundColor. Legacy compound values remain available for compatibility.
 
.PARAMETER Layout
The message presentation: Text, Heading, Usage, KeyValue, or Route.
 
.PARAMETER Child
Uses the child presentation for a heading, action, muted message, or usage line.
 
.PARAMETER Label
A label prefix, required when -Layout is KeyValue or Route.
 
.PARAMETER NoNewline
Suppresses the trailing newline. Not supported with -Intent Warning, since warnings use the warning
stream rather than Write-Host.
 
.EXAMPLE
Write-ConsoleMessage 'Building...' -ForegroundColor Cyan
 
.EXAMPLE
Write-ConsoleMessage 'Build' -Layout Heading
 
.EXAMPLE
Write-ConsoleMessage 'Compiling case' -Intent Action -Child
 
.NOTES
N/A
#>

function Write-ConsoleMessage {
    [CmdletBinding(DefaultParameterSetName = 'Intent')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSAvoidUsingWriteHost',
        '',
        Justification = 'The intended purpose of this function is formatted host output.'
    )]
    param(
        [Parameter(Position = 0)]
        [AllowEmptyString()]
        [String]$Message = '',

        [Parameter(ParameterSetName = 'Color')]
        [ConsoleColor]$ForegroundColor,

        [Parameter(ParameterSetName = 'Intent')]
        [ValidateSet(
            # Primary intents
            'Info',
            'Action',
            'Success',
            'Warning',
            'Caution',
            'Muted',
            # Compatibility intents
            'Default',
            'Header',
            'SubHeader',
            'ActionDetail',
            'Detail',
            'Endpoint',
            'MutedDetail',
            'Route',
            'Usage',
            'UsageStep'
        )]
        [String]$Intent = 'Info',

        [Parameter(ParameterSetName = 'Intent')]
        [ValidateSet(
            'Text',
            'Heading',
            'Usage',
            'KeyValue',
            'Route'
        )]
        [String]$Layout = 'Text',

        [Parameter(ParameterSetName = 'Intent')]
        [Switch]$Child,

        [Parameter(ParameterSetName = 'Intent')]
        [String]$Label,

        [Switch]$NoNewline
    )

    if ($PSCmdlet.ParameterSetName -eq 'Color') {
        $writeHostParameters = @{ Object = $Message }
        if ($PSBoundParameters.ContainsKey('ForegroundColor')) {
            $writeHostParameters.ForegroundColor = $ForegroundColor
        }
        if ($NoNewline) {
            $writeHostParameters.NoNewline = $true
        }

        Write-Host @writeHostParameters
        return
    }

    $effectiveIntent = $Intent
    $effectiveLayout = $Layout
    $effectiveChild = $Child.IsPresent

    # Compatibility mappings for the original compound intent vocabulary.
    switch ($Intent) {
        'Default' {
            $effectiveIntent = 'Info'
        }
        'Header' {
            $effectiveIntent = 'Info'
            $effectiveLayout = 'Heading'
        }
        'SubHeader' {
            $effectiveIntent = 'Info'
            $effectiveLayout = 'Heading'
            $effectiveChild = $true
        }
        'ActionDetail' {
            $effectiveIntent = 'Action'
            $effectiveChild = $true
        }
        'Detail' {
            $effectiveIntent = 'Info'
            $effectiveChild = $true
        }
        'Endpoint' {
            $effectiveIntent = 'Info'
            $effectiveLayout = 'KeyValue'
        }
        'MutedDetail' {
            $effectiveIntent = 'Muted'
            $effectiveChild = $true
        }
        'Route' {
            $effectiveIntent = 'Muted'
            $effectiveLayout = 'Route'
        }
        'Usage' {
            $effectiveIntent = 'Info'
            $effectiveLayout = 'Usage'
        }
        'UsageStep' {
            $effectiveIntent = 'Info'
            $effectiveLayout = 'Usage'
            $effectiveChild = $true
        }
    }

    if ($effectiveIntent -eq 'Warning') {
        if ($NoNewline) {
            throw '-NoNewline is not supported with -Intent Warning because warnings use the warning stream.'
        }

        Write-Warning -Message $Message
        return
    }

    $intentColor = switch ($effectiveIntent) {
        'Info' { $null }
        'Action' { 'Cyan' }
        'Success' { 'Green' }
        'Caution' { 'Yellow' }
        'Muted' { 'DarkGray' }
    }

    if ($effectiveLayout -eq 'Heading') {
        $intentColor = if ($effectiveChild) { 'DarkCyan' } else { 'Cyan' }
    }
    elseif ($effectiveLayout -eq 'KeyValue') {
        $intentColor = 'Gray'
    }
    elseif ($effectiveLayout -eq 'Route') {
        $intentColor = 'DarkGray'
    }
    elseif (
        $effectiveLayout -eq 'Text' -and
        $effectiveChild -and
        $effectiveIntent -in @('Info', 'Action')
    ) {
        $intentColor = 'Gray'
    }

    if ($effectiveLayout -in @('KeyValue', 'Route') -and -not $Label) {
        $labelContext = if ($Intent -in @('Endpoint', 'Route')) {
            $Intent
        }
        else {
            $effectiveLayout
        }
        throw "-Label is required when using $labelContext."
    }

    if ($effectiveLayout -eq 'Heading' -and -not $effectiveChild) {
        Write-ConsoleMessage
        $Message = "=== $Message ==="
    }
    elseif (
        $effectiveIntent -eq 'Success' -and
        $effectiveLayout -eq 'Text' -and
        -not $effectiveChild -and
        -not $NoNewline
    ) {
        Write-ConsoleMessage
    }
    elseif ($effectiveLayout -eq 'Usage') {
        $indent = if ($effectiveChild) { ' ' } else { ' ' }
        $Message = "$indent$Message"
    }
    elseif ($effectiveLayout -eq 'KeyValue') {
        $Message = ' {0,-19}{1}' -f ('{0}:' -f $Label), $Message
    }
    elseif ($effectiveLayout -eq 'Route') {
        $Message = ' {0,-15}-> {1}' -f $Label, $Message
    }
    elseif ($effectiveLayout -eq 'Text' -and $effectiveChild) {
        $indent = if ($effectiveIntent -eq 'Muted') { ' ' } else { ' ' }
        $Message = "$indent$Message"
    }

    $writeHostParameters = @{ Object = $Message }
    if ($intentColor) {
        $writeHostParameters.ForegroundColor = $intentColor
    }
    if ($NoNewline) {
        $writeHostParameters.NoNewline = $true
    }

    Write-Host @writeHostParameters
}
#EndRegion '.\Public\Write-ConsoleMessage.ps1' 246
#Region '.\Public\Write-ProgressToHost.ps1' -1


<#
.SYNOPSIS
Use in place of Write-Progress to output the content to the console instead of a progress bar.
#>

function Write-ProgressToHost {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingCmdletAliases', '')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
    [CmdletBinding()]
    param(
        [Parameter(Position = 0, Mandatory = $true)]
        [String]$Activity,
        [String]$Status,
        [Int32]$Id,
        [Int32]$PercentComplete,
        [Int32]$SecondsRemaining,
        [String]$CurrentOperation,
        [Int32]$ParentId,
        [Switch]$Completed,
        [Int32]$SourceId
    )

    $paramOutputOrder = @(
        'Activity',
        'Status',
        'PercentComplete',
        'CurrentOperation',
        'SecondsRemaining',
        'Completed'
    )

    $params = $paramOutputOrder |
        Where-Object -FilterScript { $PSBoundParameters.Keys -contains $_ }

    $paramValues = $params | ForEach-Object -Process {
        switch ($_) {
            'PercentComplete' {
                $percent = '['
                $progressByTen = [Math]::Floor($PercentComplete / 10)
                for ($i = 0; $i -lt $progressByTen; $i++) {
                    $percent += '#'
                }
                for ($i = 0; $i -lt 10 - $progressByTen; $i++) {
                    $percent += ' '
                }
                $percent += ']'
                $percent
            }
            'SecondsRemaining' { "-$($SecondsRemaining)s" }
            'Completed' { 'Completed' }
            default { $PSBoundParameters[$_] }
        }
    }

    $message = ''
    if ($env:AGENT_JOBNAME) {
        $message += '##[info] '
    }
    $message += ($paramValues -join ' | ')
    if ($ProgressPreference -eq 'Continue') {
        Write-ConsoleMessage -Message $message -ForegroundColor DarkGray
    }
    else {
        Write-Verbose -Message $message
    }
}
#EndRegion '.\Public\Write-ProgressToHost.ps1' 67