PSProfile.psm1

enum PSProfileLogLevel {
    Information
    Warning
    Error
    Debug
    Verbose
    Quiet
}
enum PSProfileSecretType {
    PSCredential
    SecureString
}
class PSProfileEvent {
    hidden [datetime] $Time
    [timespan] $Total
    [timespan] $Last
    [PSProfileLogLevel] $LogLevel
    [string] $Section
    [string] $Message

    PSProfileEvent(
        [datetime] $time,
        [timespan] $last,
        [timespan] $total,
        [PSProfileLogLevel] $logLevel,
        [string] $section,
        [string] $message
    ) {
        $this.Time = $time
        $this.Last = $last
        $this.Total = $total
        $this.Section = $section
        $this.Message = $message
        $this.LogLevel = $logLevel
    }
}
class PSProfileSecret {
    [PSProfileSecretType] $Type
    hidden [pscredential] $PSCredential
    hidden [securestring] $SecureString

    PSProfileSecret([string]$userName, [securestring]$password) {
        $this.Type = [PSProfileSecretType]::PSCredential
        $this.PSCredential = [PSCredential]::new($userName,$password)
    }
    PSProfileSecret([pscredential]$psCredential) {
        $this.Type = [PSProfileSecretType]::PSCredential
        $this.PSCredential = $psCredential
    }
    PSProfileSecret([SecureString]$secureString) {
        $this.Type = [PSProfileSecretType]::SecureString
        $this.SecureString = $secureString
    }
}
class PSProfileVault : Hashtable {
    [hashtable] $_secrets

    PSProfileVault() {
        $this._secrets = @{ }
    }
    [void] SetSecret([string]$name, [string]$userName, [securestring]$password) {
        $this._secrets[$name] = [PSCredential]::new(
            $userName,
            $password
        )
    }
    [void] SetSecret([pscredential]$psCredential) {
        $this._secrets[$psCredential.UserName] = $psCredential
    }
    [void] SetSecret([string]$name, [pscredential]$psCredential) {
        $this._secrets[$name] = $psCredential
    }
    [void] SetSecret([string]$name, [securestring]$secureString) {
        $this._secrets[$name] = $secureString
    }
    [pscredential] GetSecret() {
        if ($env:USERNAME) {
            return $this._secrets[$env:USERNAME]
        }
        elseif ($env:USER) {
            return $this._secrets[$env:USER]
        }
        else {
            return $null
        }
    }
    [object] GetSecret([string]$name) {
        return $this._secrets[$name]
    }
    [void] RemoveSecret([string]$name) {
        $this._secrets.Remove($name)
    }
}
class PSProfile {
    hidden [System.Collections.Generic.List[PSProfileEvent]] $Log
    [hashtable] $_internal
    [hashtable] $Settings
    [datetime] $LastRefresh
    [string] $RefreshFrequency
    [hashtable] $GitPathMap
    [hashtable] $PSBuildPathMap
    [object[]] $ModulesToImport
    [object[]] $ModulesToInstall
    [hashtable] $PathAliases
    [hashtable] $CommandAliases
    [hashtable[]] $Plugins
    [string[]] $PluginPaths
    [string[]] $ProjectPaths
    [hashtable] $Prompts
    [string[]] $ScriptPaths
    [hashtable] $SymbolicLinks
    [hashtable] $Variables
    [PSProfileVault] $Vault

    PSProfile() {
        $this.Log = [System.Collections.Generic.List[PSProfileEvent]]::new()
        $this.Vault = [PSProfileVault]::new()
        $this._internal = @{ }
        $this.GitPathMap = @{ }
        $this.PSBuildPathMap = @{ }
        $this.SymbolicLinks = @{ }
        $this.Prompts = @{
            Default = '"PS $($executionContext.SessionState.Path.CurrentLocation)$(">" * ($nestedPromptLevel + 1)) ";
            # .Link
            # https://go.microsoft.com/fwlink/?LinkID=225750
            # .ExternalHelp System.Management.Automation.dll-help.xml'

            SCRTHQ  = '$lastStatus = $?
            $lastColor = if ($lastStatus -eq $true) {
                "Green"
            }
            else {
                "Red"
            }
            Write-Host "[" -NoNewline
            Write-Host -ForegroundColor Cyan "#$($MyInvocation.HistoryId)" -NoNewline
            Write-Host "] " -NoNewline
            Write-Host "[" -NoNewLine
            $verColor = @{
                ForegroundColor = if ($PSVersionTable.PSVersion.Major -eq 7) {
                    "Yellow"
                }
                elseif ($PSVersionTable.PSVersion.Major -eq 6) {
                    "Magenta"
                }
                else {
                    "Cyan"
                }
            }
            Write-Host @verColor ("PS {0}" -f (Get-PSVersion)) -NoNewline
            Write-Host "] " -NoNewline
            Write-Host "[" -NoNewline
            Write-Host -ForegroundColor $lastColor ("{0}" -f (Get-LastCommandDuration)) -NoNewline
            Write-Host "] [" -NoNewline
            Write-Host ("{0}" -f $(Get-PathAlias)) -NoNewline -ForegroundColor DarkYellow
            Write-Host "]" -NoNewline
            if ($PWD.Path -notlike "\\*" -and $env:DisablePoshGit -ne $true -and (Test-IfGit)) {
                Write-VcsStatus
                $GitPromptSettings.EnableWindowTitle = "PS {0} @" -f (Get-PSVersion)
            }
            else {
                $Host.UI.RawUI.WindowTitle = "PS {0}" -f (Get-PSVersion)
            }
            "`n>> "'

        }
        $this.Variables = @{
            Environment = @{
                Home         = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::UserProfile)
                UserName     = [System.Environment]::UserName
                ComputerName = [System.Environment]::MachineName
            }
            Global      = @{
                PathAliasDirectorySeparator    = "$([System.IO.Path]::DirectorySeparatorChar)"
                AltPathAliasDirectorySeparator = "$([char]0xe0b1)"
            }
        }
        $this.Settings = @{
            DefaultPrompt          = $null
            PSVersionStringLength  = 3
            ConfigurationPath = (Join-Path (Get-ConfigurationPath -CompanyName 'SCRT HQ' -Name PSProfile) 'Configuration.psd1')
        }
        $this.RefreshFrequency = (New-TimeSpan -Hours 1).ToString()
        $this.LastRefresh = [datetime]::Now.AddHours(-2)
        $this.ProjectPaths = @()
        $this.PluginPaths = @()
        $this.ScriptPaths = @()
        $this.PathAliases = @{
            '~' = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::UserProfile)
        }
        $this.CommandAliases = @{ }
        $this.Plugins = @()
    }
    [void] Load() {
        $this._internal['ProfileLoadStart'] = [datetime]::Now
        $this._log(
            "SECTION START",
            "MAIN",
            "Debug"
        )
        $this._loadConfiguration()
        $plugPaths = @()
        $curVer = (Import-Metadata (Join-Path $PSScriptRoot "PSProfile.psd1")).ModuleVersion
        $this.PluginPaths | Where-Object { $_ -match "[\/\\](Modules|BuildOutput)[\/\\]PSProfile[\/\\]$curVer" -or $_ -notmatch "[\/\\](Modules|BuildOutput)[\/\\]PSProfile[\/\\]\d+\.\d+\.\d+" } | ForEach-Object {
            $plugPaths += $_
        }
        @(
            $env:PSModulePath.Split([System.IO.Path]::PathSeparator)
            (Get-Module PSProfile* | Select-Object -ExpandProperty ModuleBase)
            (Join-Path $PSScriptRoot "Plugins")
        ) | ForEach-Object {
            if ($_ -notin $this.PluginPaths) {
                $plugPaths += $_
            }
        }
        $this.PluginPaths = $plugPaths
        if (-not ($this.Plugins | Where-Object { $_.Name -eq 'PSProfile.PowerTools' })) {
            $plugs = @(@{Name = 'PSProfile.PowerTools' })
            $this.Plugins | ForEach-Object {
                $plugs += $_
            }
            $this.Plugins = $plugs
        }
        if (([datetime]::Now - $this.LastRefresh) -gt [timespan]$this.RefreshFrequency) {
            $withRefresh = ' with refresh.'
            $this.Refresh()
        }
        else {
            $withRefresh = '.'
            $this._log(
                "Skipped full refresh! Frequency set to '$($this.RefreshFrequency)', but last refresh was: $($this.LastRefresh.ToString())",
                "MAIN",
                "Verbose"
            )
        }
        $this._importModules()
        $this._loadPlugins()
        $this._invokeScripts()
        $this._setVariables()
        $this._setCommandAliases()
        $this._loadPrompt()
        $this._internal['ProfileLoadEnd'] = [datetime]::Now
        $this._internal['ProfileLoadDuration'] = $this._internal.ProfileLoadEnd - $this._internal.ProfileLoadStart
        $this._log(
            "SECTION END",
            "MAIN",
            "Debug"
        )
        Write-Host "Loading PSProfile alone took $([Math]::Round($this._internal.ProfileLoadDuration.TotalMilliseconds))ms$withRefresh"
    }
    [void] Refresh() {
        $this._log(
            "Refreshing project map, checking for modules to install and creating symbolic links",
            "MAIN",
            "Verbose"
        )
        $this._findProjects()
        $this._installModules()
        $this._createSymbolicLinks()
        $this._formatPrompts()
        $this.LastRefresh = [datetime]::Now
        $this.Save()
    }
    [void] Save() {
        $out = @{ }
        $this.PSObject.Properties.Name | Where-Object { $_ -ne '_internal' } | ForEach-Object {
            $out[$_] = $this.$_
        }
        $out | Export-Configuration -Name PSProfile -CompanyName 'SCRT HQ'
    }
    hidden [string] _globalize([string]$content) {
        $noScopePattern = 'function\s+(?<Name>[\w+_-]{1,})\s+\{'
        $globalScopePattern = 'function\s+global\:'
        $noScope = [RegEx]::Matches($content, $noScopePattern, "Multiline, IgnoreCase")
        $globalScope = [RegEx]::Matches($content,$globalScopePattern,"Multiline, IgnoreCase")
        if ($noScope.Count -ge $globalScope.Count) {
            foreach ($match in $noScope) {
                $fullValue = ($match.Groups | Where-Object { $_.Name -eq 0 }).Value
                $funcName = ($match.Groups | Where-Object { $_.Name -eq 'Name' }).Value
                $content = $content.Replace($fullValue, "function global:$funcName {")
            }
        }
        return $content
    }
    hidden [void] _loadPrompt() {
        $this._log(
            "SECTION START",
            "LoadPrompt",
            "Debug"
        )
        if (-not [String]::IsNullOrEmpty($this.Settings.DefaultPrompt)) {
            $this._log(
                "Loading default prompt: $($this.Settings.DefaultPrompt)",
                "LoadPrompt",
                "Verbose"
            )
            Switch-PSProfilePrompt -Name $this.Settings.DefaultPrompt
        }
        else {
            $this._log(
                "No default prompt name found on PSProfile. Retaining current prompt.",
                "LoadPrompt",
                "Verbose"
            )
        }
        $this._log(
            "SECTION END",
            "LoadPrompt",
            "Debug"
        )
    }
    hidden [void] _formatPrompts() {
        $this._log(
            "SECTION START",
            "FormatPrompts",
            "Debug"
        )
        $final = @{ }
        $Global:PSProfile.Prompts.GetEnumerator() | ForEach-Object {
            $this._log(
                "Formatting prompt '$($_.Key)' via Trim()",
                "FormatPrompts",
                "Verbose"
            )
            $updated = ($_.Value -split "[\r\n]" | Where-Object { $_ }).Trim() -join "`n"
            $final[$_.Key] = $updated
        }
        $Global:PSProfile.Prompts = $final
        $this._log(
            "SECTION END",
            "FormatPrompts",
            "Debug"
        )
    }
    hidden [void] _loadAdditionalConfiguration([string]$configurationPath) {
        $this._log(
            "SECTION START",
            "AddlConfiguration",
            "Debug"
        )
        $this._log(
            "Importing additional file: $configurationPath",
            "AddlConfiguration",
            "Verbose"
        )
        $additional = Import-Metadata -Path $configurationPath
        $this._log(
            "Adding additional configuration to PSProfile object",
            "AddlConfiguration",
            "Verbose"
        )
        $this | Update-Object $additional
        $this._log(
            "SECTION END",
            "AddlConfiguration",
            "Debug"
        )
    }
    hidden [void] _loadConfiguration() {
        $this._log(
            "SECTION START",
            "Configuration",
            "Debug"
        )
        $this._log(
            "Importing layered Configuration",
            "Configuration",
            "Verbose"
        )
        $conf = Import-Configuration -Name PSProfile -CompanyName 'SCRT HQ' -DefaultPath (Join-Path $PSScriptRoot "Configuration.psd1")
        $this._log(
            "Adding layered configuration to PSProfile object",
            "Configuration",
            "Verbose"
        )
        $this | Update-Object $conf
        $this._log(
            "SECTION END",
            "Configuration",
            "Debug"
        )
    }
    hidden [void] _setCommandAliases() {
        $this._log(
            "SECTION START",
            'SetCommandAliases',
            'Debug'
        )
        $this.CommandAliases.GetEnumerator() | ForEach-Object {
            try {
                $Name = $_.Key
                $Value = $_.Value
                if ($null -eq (Get-Alias "$Name*")) {
                    New-Alias -Name $Name -Value $Value -Scope Global -Option AllScope -ErrorAction SilentlyContinue
                    $this._log(
                        "Set command alias: $Name > $Value",
                        'SetCommandAliases',
                        'Verbose'
                    )
                }
                else {
                    $this._log(
                        "Alias already in use, skipping: $Name",
                        'SetCommandAliases',
                        'Verbose'
                    )
                }
            }
            catch {
                $this._log(
                    "Failed to set command alias: $Name > $Value :: $($_)",
                    'SetCommandAliases',
                    'Warning'
                )
            }
        }
        $this._log(
            "SECTION END",
            'SetCommandAliases',
            'Debug'
        )
    }
    hidden [void] _createSymbolicLinks() {
        $this._log(
            "SECTION START",
            'CreateSymbolicLinks',
            'Debug'
        )
        if ($null -ne $this.SymbolicLinks.Keys) {
            $null = $this.SymbolicLinks.GetEnumerator() | Start-RSJob -Name { "_PSProfile_SymbolicLinks_" + $_.Key } -ScriptBlock {
                if (-not (Test-Path $_.Key) -or ((Get-Item $_.Key).LinkType -eq 'SymbolicLink' -and (Get-Item $_.Key).Target -ne $_.Value)) {
                    New-Item -ItemType SymbolicLink -Path $_.Key -Value $_.Value -Force
                }
            }
        }
        else {
            $this._log(
                "No symbolic links specified!",
                'CreateSymbolicLinks',
                'Verbose'
            )
        }
        $this._log(
            "SECTION END",
            'CreateSymbolicLinks',
            'Debug'
        )
    }
    hidden [void] _setVariables() {
        $this._log(
            "SECTION START",
            'SetVariables',
            'Debug'
        )
        if ($null -ne $this.Variables.Keys) {
            foreach ($varType in $this.Variables.Keys) {
                switch ($varType) {
                    Environment {
                        $this.Variables.Environment.GetEnumerator() | ForEach-Object {
                            $this._log(
                                "`$env:$($_.Key) = '$($_.Value)'",
                                'SetVariables',
                                'Verbose'
                            )
                            Set-Item "Env:\$($_.Key)" -Value $_.Value -Force
                        }
                    }
                    default {
                        $this.Variables.Global.GetEnumerator() | ForEach-Object {
                            $this._log(
                                "`$global:$($_.Key) = '$($_.Value)'",
                                'SetVariables',
                                'Verbose'
                            )
                            Set-Variable -Name $_.Key -Value $_.Value -Scope Global -Force
                        }
                    }
                }
            }
        }
        else {
            $this._log(
                "No variables key/value pairs provided!",
                'SetVariables',
                'Verbose'
            )
        }
        $this._log(
            "SECTION END",
            'SetVariables',
            'Debug'
        )
    }
    hidden [void] _findProjects() {
        $this._log(
            "SECTION START",
            'FindProjects',
            'Debug'
        )
        if (-not [string]::IsNullOrEmpty((-join $this.ProjectPaths))) {
            $this.GitPathMap = @{ }
            $this.ProjectPaths | ForEach-Object {
                $p = $_
                $cnt = 0
                if (Test-Path $p) {
                    $p = (Resolve-Path $p).Path
                    $cnt++
                    $pInfo = [System.IO.DirectoryInfo]::new($p)
                    $this.PathAliases["@$($pInfo.Name)"] = $pInfo.FullName
                    $this._log(
                        "Added path alias: @$($pInfo.Name) >> $($pInfo.FullName)",
                        'FindProjects',
                        'Verbose'
                    )
                    $g = 0
                    $b = 0
                    $pInfo.EnumerateDirectories('.git',[System.IO.SearchOption]::AllDirectories) | ForEach-Object {
                        $g++
                        $this._log(
                            "Found git project @ $($_.Parent.FullName)",
                            'FindProjects',
                            'Verbose'
                        )
                        $this.GitPathMap[$_.Parent.BaseName] = $_.Parent.FullName
                        $bldPath = [System.IO.Path]::Combine($_.Parent.FullName,'build.ps1')
                        if ([System.IO.File]::Exists($bldPath)) {
                            $b++
                            $this._log(
                                "Found build script @ $($_.FullName)",
                                'FindProjects',
                                'Verbose'
                            )
                            $this.PSBuildPathMap[$_.Parent.BaseName] = $_.Parent.FullName
                        }
                    }
                    $this._log(
                        "$p :: Found: $g git | $b build",
                        'FindProjects',
                        'Verbose'
                    )
                }
                else {
                    $this._log(
                        "'$p' Unable to resolve path!",
                        'FindProjects',
                        'Verbose'
                    )
                }
            }
        }
        else {
            $this._log(
                "No project paths specified to search in!",
                'FindProjects',
                'Verbose'
            )
        }
        $this._log(
            "SECTION END",
            'FindProjects',
            'Debug'
        )
    }
    hidden [void] _invokeScripts() {
        $this._log(
            "SECTION START",
            'InvokeScripts',
            'Debug'
        )
        if (-not [string]::IsNullOrEmpty((-join $this.ScriptPaths))) {
            $this.ScriptPaths | ForEach-Object {
                $p = $_
                if (Test-Path $p) {
                    $i = Get-Item $p
                    $p = $i.FullName
                    if ($p -match '\.ps1$') {
                        try {
                            $this._log(
                                "'$($i.Name)' Invoking script",
                                'InvokeScripts',
                                'Verbose'
                            )
                            $sb = [scriptblock]::Create($this._globalize(([System.IO.File]::ReadAllText($i.FullName))))
                            .$sb
                        }
                        catch {
                            $e = $_
                            $this._log(
                                "'$($i.Name)' Failed to invoke script! Error: $e",
                                'InvokeScripts',
                                'Warning'
                            )
                        }
                    }
                    else {
                        [System.IO.DirectoryInfo]::new($p).EnumerateFiles('*.ps1',[System.IO.SearchOption]::AllDirectories) | Where-Object { $_.BaseName -notmatch '^(profile|CONFIG|WIP)' } | ForEach-Object {
                            $s = $_
                            try {
                                $this._log(
                                    "'$($s.Name)' Invoking script",
                                    'InvokeScripts',
                                    'Verbose'
                                )
                                $sb = [scriptblock]::Create($this._globalize(([System.IO.File]::ReadAllText($s.FullName))))
                                .$sb
                            }
                            catch {
                                $e = $_
                                $this._log(
                                    "'$($s.Name)' Failed to invoke script! Error: $e",
                                    'InvokeScripts',
                                    'Warning'
                                )
                            }
                        }
                    }
                }
                else {
                    $this._log(
                        "'$p' Unable to resolve path!",
                        'FindProjects',
                        'Verbose'
                    )
                }
            }
        }
        else {
            $this._log(
                "No script paths specified to invoke!",
                'InvokeScripts',
                'Verbose'
            )
        }
        $this._log(
            "SECTION END",
            'InvokeScripts',
            'Debug'
        )
    }
    hidden [void] _installModules() {
        $this._log(
            "SECTION START",
            'InstallModules',
            'Debug'
        )
        if (-not [string]::IsNullOrEmpty((-join $this.ModulesToInstall))) {
            $null = $this.ModulesToInstall | Start-RSJob -Name { "_PSProfile_InstallModule_$($_)" } -VariablesToImport this -ScriptBlock {
                Param (
                    [parameter()]
                    [object]
                    $Module
                )
                $params = if ($Module -is [string]) {
                    @{Name = $Module }
                }
                elseif ($Module -is [hashtable]) {
                    $Module
                }
                else {
                    $null
                }
                $this._log(
                    "Checking if module is installed already: $($params | ConvertTo-Json -Compress)",
                    'InstallModules',
                    'Verbose'
                )
                if ($null -eq (Get-Module $params['Name'] -ListAvailable)) {
                    $this._log(
                        "Installing missing module to CurrentUser scope: $($params | ConvertTo-Json -Compress)",
                        'InstallModules',
                        'Verbose'
                    )
                    Install-Module -Name @params -Scope CurrentUser -AllowClobber -SkipPublisherCheck
                }
                else {
                    $this._log(
                        "Module already installed, skipping: $($params | ConvertTo-Json -Compress)",
                        'InstallModules',
                        'Verbose'
                    )
                }
            }
        }
        else {
            $this._log(
                "No modules specified to install!",
                'InstallModules',
                'Verbose'
            )
        }
        $this._log(
            "SECTION END",
            'InstallModules',
            'Debug'
        )
    }
    hidden [void] _importModules() {
        $this._log(
            "SECTION START",
            'ImportModules',
            'Debug'
        )
        if (-not [string]::IsNullOrEmpty((-join $this.ModulesToImport))) {
            $this.ModulesToImport | ForEach-Object {
                try {
                    $params = if ($_ -is [string]) {
                        @{Name = $_ }
                    }
                    elseif ($_ -is [hashtable]) {
                        $_
                    }
                    else {
                        $null
                    }
                    if ($null -ne $params) {
                        @('ErrorAction','Verbose') | ForEach-Object {
                            if ($params.ContainsKey($_)) {
                                $params.Remove($_)
                            }
                        }
                        Import-Module @params -Global -ErrorAction SilentlyContinue -Verbose:$false
                        $this._log(
                            "Module imported: $($params | ConvertTo-Json -Compress)",
                            'ImportModules',
                            'Verbose'
                        )
                    }
                    else {
                        $this._log(
                            "Module must be either a string or a hashtable!",
                            'ImportModules',
                            'Verbose'
                        )
                    }
                }
                catch {
                    $this._log(
                        "'$($params['Name'])' Error importing module: $($Error[0].Exception.Message)",
                        "ImportModules",
                        "Warning"
                    )
                }
            }
        }
        else {
            $this._log(
                "No modules specified to import!",
                'ImportModules',
                'Verbose'
            )
        }
        $this._log(
            "SECTION END",
            'ImportModules',
            'Debug'
        )
    }
    hidden [void] _loadPlugins() {
        $this._log(
            "SECTION START",
            'LoadPlugins',
            'Debug'
        )
        if ($this.Plugins.Count) {
            $this.Plugins.ForEach( {
                    $plugin = $_
                    $this._log(
                        "'$($plugin.Name)' Searching for plugin",
                        'LoadPlugins',
                        'Verbose'
                    )
                    try {
                        $found = $null
                        $importParams = @{
                            ErrorAction = 'Stop'
                            Global      = $true
                        }
                        if ($plugin.ArgumentList) {
                            $importParams['ArgumentList'] = $plugin.ArgumentList
                        }
                        foreach ($plugPath in $this.PluginPaths) {
                            $fullPath = [System.IO.Path]::Combine($plugPath,"$($plugin.Name).ps1")
                            $this._log(
                                "'$($plugin.Name)' Checking path: $fullPath",
                                'LoadPlugins',
                                'Debug'
                            )
                            if (Test-Path $fullPath) {
                                $sb = [scriptblock]::Create($this._globalize(([System.IO.File]::ReadAllText($fullPath))))
                                if ($plugin.ArgumentList) {
                                    .$sb($plugin.ArgumentList)
                                }
                                else {
                                    .$sb
                                }
                                $found = $fullPath
                                break
                            }
                        }
                        if ($null -ne $found) {
                            $this._log(
                                "'$($plugin.Name)' plugin loaded from path: $found",
                                'LoadPlugins',
                                'Verbose'
                            )
                        }
                        else {
                            if ($null -ne $plugin.Name -and $null -ne (Get-Module $plugin.Name -ListAvailable -ErrorAction SilentlyContinue)) {
                                Import-Module $plugin.Name @importParams
                                $this._log(
                                    "'$($plugin.Name)' plugin loaded from PSModulePath!",
                                    'LoadPlugins'
                                )
                            }
                            else {
                                $this._log(
                                    "'$($plugin.Name)' plugin not found!",
                                    'LoadPlugins',
                                    'Warning'
                                )
                            }
                        }
                    }
                    catch {
                        throw
                    }
                })
        }
        else {
            $this._log(
                "No plugins specified to load!",
                'LoadPlugins',
                'Verbose'
            )
        }
        $this._log(
            "SECTION END",
            'LoadPlugins',
            'Debug'
        )
    }
    hidden [void] _log([string]$message,[string]$section,[PSProfileLogLevel]$logLevel) {
        $dt = Get-Date
        $shortMessage = "[$($dt.ToString('HH:mm:ss'))] $message"

        $lastCommand = if ($this.Log.Count) {
            $dt - $this.Log[-1].Time
        }
        else {
            New-TimeSpan
        }
        $this.Log.Add(
            [PSProfileEvent]::new(
                $dt,
                $lastCommand,
                ($dt - $this._internal.ProfileLoadStart),
                $logLevel,
                $section,
                $message
            )
        )
        switch ($logLevel) {
            Information {
                Write-Host $shortMessage
            }
            Verbose {
                Write-Verbose $shortMessage
            }
            Warning {
                Write-Warning $shortMessage
            }
            Error {
                Write-Error $shortMessage
            }
            Debug {
                Write-Debug $shortMessage
            }
        }
    }
    hidden [void] _log([string]$message,[string]$section) {
        $this._log($message,$section,'Quiet')
    }
}


function Decrypt {
    param($Item)
    if ($Item -is [System.Security.SecureString]) {
        [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
            [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR(
                $Item
            )
        )
    }
    else {
        $Item
    }
}


function Encrypt {
    param($Item)
    if ($Item -is [System.Security.SecureString]) {
        $Item
    }
    elseif ($Item -is [System.String] -and -not [System.String]::IsNullOrWhiteSpace($Item)) {
        ConvertTo-SecureString -String $Item -AsPlainText -Force
    }
}


function Add-PSProfileCommandAlias {
    <#
    .SYNOPSIS
    Adds a command alias to your PSProfile configuration to set during PSProfile import.
 
    .DESCRIPTION
    Adds a command alias to your PSProfile configuration to set during PSProfile import.
 
    .PARAMETER Alias
    The alias to set for the command.
 
    .PARAMETER Command
    The name of the command to set the alias for.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Add-PSProfileCommandAlias -Alias code -Command Open-Code -Save
 
    Adds the command alias 'code' targeting the command 'Open-Code' and saves your PSProfile configuration.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory,Position = 0)]
        [String]
        $Alias,
        [Parameter(Mandatory,Position = 1)]
        [String]
        $Command,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        Write-Verbose "Adding alias '$Alias' for command '$Command' to PSProfile"
        New-Alias -Name $Alias -Value $Command -Option AllScope -Scope Global
        $Global:PSProfile.CommandAliases[$Alias] = $Command
        if ($Save) {
            Save-PSProfile
        }
    }
}

Register-ArgumentCompleter -CommandName Get-PSProfileCommandAlias -ParameterName Command -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    (Get-Command "$wordToComplete*").Name | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Add-PSProfileCommandAlias'

function Get-PSProfileCommandAlias {
    <#
    .SYNOPSIS
    Gets an alias from $PSProfile.CommandAliases.
 
    .DESCRIPTION
    Gets an alias from $PSProfile.CommandAliases.
 
    .PARAMETER Alias
    The alias to get from $PSProfile.CommandAliases.
 
    .EXAMPLE
    Get-PSProfileCommandAlias -Alias code
 
    Gets the alias 'code' from $PSProfile.CommandAliases.
 
    .EXAMPLE
    Get-PSProfileCommandAlias
 
    Gets the list of command aliases from $PSProfile.CommandAliases.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0,ValueFromPipeline)]
        [String[]]
        $Alias
    )
    Process {
        if ($PSBoundParameters.ContainsKey('Alias')) {
            Write-Verbose "Getting command alias '$Alias' from `$PSProfile.CommandAliases"
            $Global:PSProfile.CommandAliases.GetEnumerator() | Where-Object {$_.Key -in $Alias}
        }
        else {
            Write-Verbose "Getting all command aliases from `$PSProfile.CommandAliases"
            $Global:PSProfile.CommandAliases
        }
    }
}

Register-ArgumentCompleter -CommandName Get-PSProfileCommandAlias -ParameterName Alias -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.CommandAliases.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Get-PSProfileCommandAlias'

function Remove-PSProfileCommandAlias {
    <#
    .SYNOPSIS
    Removes an alias from $PSProfile.CommandAliases.
 
    .DESCRIPTION
    Removes an alias from $PSProfile.CommandAliases.
 
    .PARAMETER Alias
    The alias to remove from $PSProfile.CommandAliases.
 
    .PARAMETER Force
    If $true, also removes the alias itself from the session if it exists.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Remove-PSProfileCommandAlias -Alias code -Save
 
    Removes the alias 'code' from $PSProfile.CommandAliases then saves the updated configuration.
    #>

    [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")]
    Param (
        [Parameter(Mandatory,Position = 0,ValueFromPipeline)]
        [String]
        $Alias,
        [Parameter()]
        [Switch]
        $Force,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        if ($PSCmdlet.ShouldProcess("Removing '$Alias' from `$PSProfile.CommandAliases")) {
            Write-Verbose "Removing '$Alias' from `$PSProfile.CommandAliases"
            $Global:PSProfile.CommandAliases.Remove($Alias)
            if ($Force -and $null -ne (Get-Alias "$Alias*")) {
                Write-Verbose "Removing Alias: $Alias"
                Remove-Item $LinkPath -Force
            }
            if ($Save) {
                Save-PSProfile
            }
        }
    }
}

Register-ArgumentCompleter -CommandName Remove-PSProfileCommandAlias -ParameterName Alias -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.CommandAliases.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Remove-PSProfileCommandAlias'

function Export-PSProfileConfiguration {
    <#
    .SYNOPSIS
    Exports the PSProfile configuration as a PSD1 file to the desired path.
 
    .DESCRIPTION
    Exports the PSProfile configuration as a PSD1 file to the desired path.
 
    .PARAMETER Path
    The existing folder or file path with PSD1 extension to export the configuration to. If a folder path is provided, the configuration will be exported to the path with the file name 'PSProfile.Configuration.psd1'.
 
    .PARAMETER Force
    If $true and the resolved file path exists, overwrite it with the current configuration.
 
    .EXAMPLE
    Export-PSProfileConfiguration ~\MyPSProfileConfig.psd1
 
    Exports the configuration to the specified path.
 
    .EXAMPLE
    Export-PSProfileConfiguration ~\MyScripts -Force
 
    Exports the configuration to the resolved path of '~\MyScripts\PSProfile.Configuration.psd1' and overwrites the file if it already exists.
 
    .NOTES
    *Any secrets stored in the `$PSProfile.Vault` will be exported, but will be unable to be decrypted on another machine or by another user on the same machine due to encryption via Data Protection API.*
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory,Position = 0)]
        [ValidateScript({
            if ($_ -like '*.psd1') {
                $true
            }
            elseif ((Test-Path $_) -and (Get-Item $_).PSIsContainer) {
                $true
            }
            else {
                throw "The path provided was not an existing folder path or a file path ending in a PSD1 extension. Please provide either an existing folder to export the PSProfile configuration to or an exact file path ending in a PSD1 extension to export the configuration to. Path provided: $_"
            }
        })]
        [String]
        $Path,
        [Parameter()]
        [Switch]
        $Force
    )
    Process {
        if (Test-Path $Path) {
            $item = Get-Item $Path
            if ($item.PSIsContainer) {
                $finalPath = [System.IO.Path]::Combine($item.FullName,'PSProfile.Configuration.psd1')
            }
            else {
                if ($item.Extension -ne '.psd1') {
                    Write-Error "Please provide either a file path for a psd1"
                }
                else {
                    $finalPath = $item.FullName
                }
            }
        }
        else {
            $finalPath = $Path
        }
        if ((Test-Path $finalPath) -and -not $Force) {
            Write-Error "File path already exists: $finalPath. Use the -Force parameter to overwrite the contents with the current PSProfile configuration."
        }
        else {
            try {
                if (Test-Path $finalPath) {
                    Write-Verbose "Force specified! Removing existing file: $finalPath"
                    Remove-Item $finalPath -ErrorAction Stop
                }
                Write-Verbose "Importing metadata from path: $($Global:PSProfile.Settings.ConfigurationPath)"
                $metadata = Import-Metadata -Path $Global:PSProfile.Settings.ConfigurationPath -ErrorAction Stop
                Write-Verbose "Exporting cleaned PSProfile configuration to path: $finalPath"
                $metadata | Export-Metadata -Path $finalPath -ErrorAction Stop
            }
            catch {
                Write-Error $_
            }
        }
    }
}


Export-ModuleMember -Function 'Export-PSProfileConfiguration'

function Import-PSProfile {
    <#
    .SYNOPSIS
    Reloads your PSProfile by running $PSProfile.Load()
 
    .DESCRIPTION
    Reloads your PSProfile by running $PSProfile.Load()
 
    .EXAMPLE
    Import-PSProfile
 
    .EXAMPLE
    Load-PSProfile
    #>

    [CmdletBinding()]
    Param()
    Process {
        Write-Verbose "Loading PSProfile configuration!"
        $global:PSProfile.Load()
    }
}


Export-ModuleMember -Function 'Import-PSProfile'

function Import-PSProfileConfiguration {
    <#
    .SYNOPSIS
    Imports a Configuration.psd1 file from a specific path and overwrites differing values on the PSProfile, if any.
 
    .DESCRIPTION
    Imports a Configuration.psd1 file from a specific path and overwrites differing values on the PSProfile, if any.
 
    .PARAMETER Path
    The path to the PSD1 file you would like to import.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after importing.
 
    .EXAMPLE
    Import-PSProfileConfiguration -Path ~\MyProfile.psd1 -Save
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory,Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Alias('FullName')]
        [String]
        $Path,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        $Path = (Resolve-Path $Path).Path
        Write-Verbose "Loading PSProfile configuration from path: $Path"
        $Global:PSProfile._loadAdditionalConfiguration($Path)
        if ($Save) {
            Save-PSProfile
        }
    }
}


Export-ModuleMember -Function 'Import-PSProfileConfiguration'

function Save-PSProfile {
    <#
    .SYNOPSIS
    Saves the current PSProfile configuration by calling the $PSProfile.Save() method.
 
    .DESCRIPTION
    Saves the current PSProfile configuration by calling the $PSProfile.Save() method.
 
    .EXAMPLE
    Save-PSProfile
    #>

    [CmdletBinding()]
    Param()
    Process {
        Write-Verbose "Saving PSProfile configuration!"
        $global:PSProfile.Save()
    }
}


Export-ModuleMember -Function 'Save-PSProfile'

function Update-PSProfileConfig {
    <#
    .SYNOPSIS
    Force refreshes the current PSProfile configuration by calling the $PSProfile.Refresh() method.
 
    .DESCRIPTION
    Force refreshes the current PSProfile configuration by calling the $PSProfile.Refresh() method. This will update the GitPathMap with any new projects found and other tasks that don't run on every PSProfile load.
 
    .EXAMPLE
    Update-PSProfileConfig
 
    .EXAMPLE
    Refresh-PSProfile
 
    Uses the shorter alias command instead of the long command.
    #>

    [CmdletBinding()]
    Param()
    Process {
        Write-Verbose "Refreshing PSProfile config!"
        $global:PSProfile.Refresh()
    }
}


Export-ModuleMember -Function 'Update-PSProfileConfig'

function Update-PSProfileRefreshFrequency {
    <#
    .SYNOPSIS
    Sets the Refresh Frequency for PSProfile. The $PSProfile.Refresh() runs tasks that aren't run during every profile load, i.e. SymbolicLink creation, Git project path discovery, module installation, etc.
 
    .DESCRIPTION
    Sets the Refresh Frequency for PSProfile. The $PSProfile.Refresh() runs tasks that aren't run during every profile load, i.e. SymbolicLink creation, Git project path discovery, module installation, etc.
 
    .PARAMETER Timespan
    The frequency that you would like to refresh your PSProfile configuration. Refresh will occur during the profile load after the time since last refresh has surpassed the desired refresh frequency.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Update-PSProfileRefreshFrequency -Timespan '03:00:00' -Save
 
    Updates the RefreshFrequency to 3 hours and saves the PSProfile configuration after updating.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory,Position = 0)]
        [timespan]
        $Timespan,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        Write-Verbose "Updating PSProfile RefreshFrequency to '$($Timespan.ToString())'"
        $Global:PSProfile.RefreshFrequency = $Timespan.ToString()
        if ($Save) {
            Save-PSProfile
        }
    }
}


Export-ModuleMember -Function 'Update-PSProfileRefreshFrequency'

function Update-PSProfileSetting {
    <#
    .SYNOPSIS
    Update a PSProfile property's value by tab-completing the available keys.
 
    .DESCRIPTION
    Update a PSProfile property's value by tab-completing the available keys.
 
    .PARAMETER Path
    The property path you would like to update, e.g. Settings.PSVersionStringLength
 
    .PARAMETER Value
    The value you would like to update for the specified setting path.
 
    .PARAMETER Add
    If $true, adds the value to the specified PSProfile setting value array instead of overwriting the current value.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Update-PSProfileSetting -Path Settings.PSVersionStringLength -Value 3 -Save
 
    Updates the PSVersionStringLength setting to 3 and saves the configuration.
 
    .EXAMPLE
    Update-PSProfileSetting -Path ScriptPaths -Value ~\ProfileLoad.ps1 -Add -Save
 
    *Adds* the 'ProfileLoad.ps1' script to the $PSProfile.ScriptPaths array of scripts to invoke during profile load, then saves the configuration.
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory,Position = 0)]
        [String]
        $Path,
        [Parameter(Mandatory,Position = 1)]
        [object]
        $Value,
        [Parameter()]
        [switch]
        $Add,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        Write-Verbose "Updating PSProfile.$Path with value '$Value'"
        $split = $Path.Split('.')
        switch ($split.Count) {
            5 {
                if ($Add) {
                    $Global:PSProfile."$($split[0])"."$($split[1])"."$($split[2])"."$($split[3])"."$($split[4])" += $Value
                }
                else {
                    $Global:PSProfile."$($split[0])"."$($split[1])"."$($split[2])"."$($split[3])"."$($split[4])" = $Value
                }
            }
            4 {
                if ($Add) {
                    $Global:PSProfile."$($split[0])"."$($split[1])"."$($split[2])"."$($split[3])" += $Value
                }
                else{
                    $Global:PSProfile."$($split[0])"."$($split[1])"."$($split[2])"."$($split[3])" = $Value
                }
            }
            3 {
                if ($Add) {
                    $Global:PSProfile."$($split[0])"."$($split[1])"."$($split[2])" += $Value
                }
                else{
                    $Global:PSProfile."$($split[0])"."$($split[1])"."$($split[2])" = $Value
                }
            }
            2 {
                if ($Add) {
                    $Global:PSProfile."$($split[0])"."$($split[1])" += $Value
                }
                else{
                    $Global:PSProfile."$($split[0])"."$($split[1])" = $Value
                }
            }
            1 {
                if ($Add) {
                    $Global:PSProfile.$Path += $Value
                }
                else{
                    $Global:PSProfile.$Path = $Value
                }
            }
        }
        if ($Save) {
            Save-PSProfile
        }
    }
}

Register-ArgumentCompleter -CommandName 'Update-PSProfileSetting' -ParameterName Path -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    Get-PSProfileArguments @PSBoundParameters
}


Export-ModuleMember -Function 'Update-PSProfileSetting'

function Copy-DynamicParameters {
    <#
    .SYNOPSIS
    Copies parameters from a file or function and returns a RuntimeDefinedParameterDictionary with the parameters replicated. Used in DynamicParam blocks.
 
    .DESCRIPTION
    Copies parameters from a file or function and returns a RuntimeDefinedParameterDictionary with the parameters replicated. Used in DynamicParam blocks.
 
    .PARAMETER File
    The file to replicate parameters from.
 
    .PARAMETER Function
    The function to replicate parameters from.
 
    .PARAMETER ExcludeParameter
    The parameter or list of parameters to exclude from replicating into the returned Dictionary.
 
    .EXAMPLE
    function Start-Build {
        [CmdletBinding()]
        Param ()
        DynamicParam {
            Copy-DynamicParameters -File ".\build.ps1"
        }
        Process {
            #Function logic
        }
    }
 
    Replicates the parameters from the build.ps1 script into the Start-Build function.
    #>

    [OutputType('System.Management.Automation.RuntimeDefinedParameterDictionary')]
    [CmdletBinding(DefaultParameterSetName = "File")]
    Param (
        [Parameter(Mandatory,Position = 0,ParameterSetName = "File")]
        [String]
        $File,
        [Parameter(Mandatory,ParameterSetName = "Function")]
        [String]
        $Function,
        [Parameter()]
        [String[]]
        $ExcludeParameter = @()
    )
    Begin {
        $RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        $ast = switch ($PSCmdlet.ParameterSetName) {
            File {
                [System.Management.Automation.Language.Parser]::ParseFile($PSBoundParameters['File'],[ref]$null,[ref]$null)
            }
            Function {
                [System.Management.Automation.Language.Parser]::ParseInput((Get-Command $PSBoundParameters['Function']).Definition,[ref]$null,[ref]$null)
            }
        }
    }
    Process {
        foreach ($parameter in $ast.ParamBlock.Parameters | Where-Object {$_.Name.VariablePath.UserPath -notin $ExcludeParameter}) {
            $AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
            foreach ($paramAtt in $parameter.Attributes) {
                switch ($paramAtt.TypeName.FullName) {
                    Parameter {
                        $attribute = New-Object System.Management.Automation.ParameterAttribute
                        foreach ($boolParam in @('Mandatory','DontShow','ValueFromPipeline','ValueFromPipelineByPropertyName','ValueFromRemainingArguments')) {
                            $attribute.$boolParam = ($null -ne ($paramAtt.NamedArguments | Where-Object {$_.ArgumentName -eq $boolParam -and $_.Argument.Value -eq $true}))
                        }
                        foreach ($otherParam in @('Position','HelpMessage','HelpMessageBaseName','HelpMessageResourceId','ParameterSetName')) {
                            if ($item = $paramAtt.NamedArguments | Where-Object {$_.ArgumentName -eq $otherParam}) {
                                $attribute.$otherParam = $item.Argument.Value
                            }
                        }
                        $AttribColl.Add($attribute)
                    }
                    Alias {
                        $AttribColl.Add((New-Object System.Management.Automation.AliasAttribute($paramAtt.PositionalArguments.SafeGetValue())))
                    }
                    ValidateScript {
                        $AttribColl.Add((New-Object System.Management.Automation.ValidateScriptAttribute($paramAtt.PositionalArguments.ScriptBlock.GetScriptBlock())))
                    }
                    ValidateRange {
                        $AttribColl.Add((New-Object System.Management.Automation.ValidateRangeAttribute($paramAtt.PositionalArguments.SafeGetValue())))
                    }
                    ValidateCount {
                        $AttribColl.Add((New-Object System.Management.Automation.ValidateCountAttribute($paramAtt.PositionalArguments.SafeGetValue())))
                    }
                    ValidatePattern {
                        $AttribColl.Add((New-Object System.Management.Automation.ValidatePatternAttribute($paramAtt.PositionalArguments.SafeGetValue())))
                    }
                    ValidateSet {
                        $AttribColl.Add((New-Object System.Management.Automation.ValidateSetAttribute($paramAtt.PositionalArguments.SafeGetValue())))
                    }
                    ValidateLength {
                        $AttribColl.Add((New-Object System.Management.Automation.ValidateLengthAttribute($paramAtt.PositionalArguments.SafeGetValue())))
                    }
                }
            }
            $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter(
                $parameter.Name.VariablePath.UserPath,
                $parameter.StaticType,
                $AttribColl
            )
            $RuntimeParamDic.Add($parameter.Name.VariablePath.UserPath,$RuntimeParam)
        }
    }
    End {
        return $RuntimeParamDic
    }
}

Register-ArgumentCompleter -CommandName Copy-DynamicParameters -ParameterName ExcludeParameter -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $set = if (-not [String]::IsNullOrEmpty($fakeBoundParameter.File)) {
        ([System.Management.Automation.Language.Parser]::ParseFile(
            $fakeBoundParameter.File, [ref]$null, [ref]$null
        )).ParamBlock.Parameters.Name.VariablePath.UserPath
    }
    elseif (-not [String]::IsNullOrEmpty($fakeBoundParameter.Function)) {
        ([System.Management.Automation.Language.Parser]::ParseInput(
            (Get-Command $fakeBoundParameter.Function).Definition, [ref]$null, [ref]$null
        )).ParamBlock.Parameters.Name.VariablePath.UserPath
    }
    else {
        @()
    }
    $set | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}

Register-ArgumentCompleter -CommandName Copy-DynamicParameters -ParameterName Function -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    (Get-Command "$wordToComplete*").Name | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Copy-DynamicParameters'

function Get-LastCommandDuration {
    <#
    .SYNOPSIS
    Gets the elapsed time of the last command via Get-History. Intended to be used in prompts.
 
    .DESCRIPTION
    Gets the elapsed time of the last command via Get-History. Intended to be used in prompts.
 
    .PARAMETER Id
    The Id of the command to get from the history.
 
    .PARAMETER Format
    The format string for the resulting timestamp.
 
    .EXAMPLE
    Get-LastCommandDuration
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [int]
        $Id,
        [Parameter()]
        [string]
        $Format = "{0:h\:mm\:ss\.ffff}"
    )
    $null = $PSBoundParameters.Remove("Format")
    $LastCommand = Get-History -Count 1 @PSBoundParameters
    if (!$LastCommand) {
        return "0:00:00.0000"
    }
    elseif ($null -ne $LastCommand.Duration) {
        $Format -f $LastCommand.Duration
    }
    else {
        $Duration = $LastCommand.EndExecutionTime - $LastCommand.StartExecutionTime
        $Format -f $Duration
    }
}


Export-ModuleMember -Function 'Get-LastCommandDuration'

function Get-PathAlias {
    <#
    .SYNOPSIS
    Gets the Path alias using either the short name from $PSProfile.GitPathMap or a path alias stored in $PSProfile.PathAliases, falls back to using a shortened version of the root drive + current directory.
 
    .DESCRIPTION
    Gets the Path alias using either the short name from $PSProfile.GitPathMap or a path alias stored in $PSProfile.PathAliases, falls back to using a shortened version of the root drive + current directory.
 
    .PARAMETER Path
    The full path to get the PathAlias for. Defaults to $PWD.Path
 
    .PARAMETER DirectorySeparator
    The desired DirectorySeparator character. Defaults to $global:PathAliasDirectorySeparator if present, falls back to [System.IO.Path]::DirectorySeparatorChar if not.
 
    .EXAMPLE
    Get-PathAlias
    #>

    [CmdletBinding()]
    Param (
        [parameter(Position = 0)]
        [string]
        $Path = $PWD.Path,
        [parameter(Position = 1)]
        [string]
        $DirectorySeparator = $(if ($null -ne $global:PathAliasDirectorySeparator) {
                $global:PathAliasDirectorySeparator
            }
            else {
                [System.IO.Path]::DirectorySeparatorChar
            })
    )
    Begin {
        try {
            $origPath = $Path
            if ($null -eq $global:PSProfile) {
                $global:PSProfile = @{
                    Settings     = @{
                        PSVersionStringLength = 3
                    }
                    PathAliasMap = @{
                        '~' = $env:USERPROFILE
                    }
                }
            }
            elseif ($null -eq $global:PSProfile._internal) {
                $global:PSProfile._internal = @{
                    PathAliasMap = @{
                        '~' = $env:USERPROFILE
                    }
                }
            }
            elseif ($null -eq $global:PSProfile._internal.PathAliasMap) {
                $global:PSProfile._internal.PathAliasMap = @{
                    '~' = $env:USERPROFILE
                }
            }
            if ($gitRepo = Test-IfGit) {
                $gitIcon = [char]0xe0a0
                $key = $gitIcon + $gitRepo.Repo
                if (-not $global:PSProfile._internal.PathAliasMap.ContainsKey($key)) {
                    $global:PSProfile._internal.PathAliasMap[$key] = $gitRepo.TopLevel
                }
            }
            $leaf = Split-Path $Path -Leaf
            if (-not $global:PSProfile._internal.PathAliasMap.ContainsKey('~')) {
                $global:PSProfile._internal.PathAliasMap['~'] = $env:USERPROFILE
            }
            Write-Verbose "Alias map => JSON: $($global:PSProfile._internal.PathAliasMap | ConvertTo-Json -Depth 5)"
            $aliasKey = $null
            $aliasValue = $null
            foreach ($hash in $global:PSProfile._internal.PathAliasMap.GetEnumerator() | Sort-Object { $_.Value.Length } -Descending) {
                if ($Path -like "$($hash.Value)*") {
                    $Path = $Path.Replace($hash.Value,$hash.Key)
                    $aliasKey = $hash.Key
                    $aliasValue = $hash.Value
                    Write-Verbose "AliasKey [$aliasKey] || AliasValue [$aliasValue]"
                    break
                }
            }
        }
        catch {
            Write-Error $_
            return $origPath
        }
    }
    Process {
        try {
            if ($null -ne $aliasKey -and $origPath -eq $aliasValue) {
                Write-Verbose "Matched original path! Returning alias base path"
                $finalPath = $Path
            }
            elseif ($null -ne $aliasKey) {
                Write-Verbose "Matched alias key [$aliasKey]! Returning path alias with leaf"
                $drive = "$($aliasKey)\"
                $finalPath = if ((Split-Path $origPath -Parent) -eq $aliasValue) {
                    "$($drive)$($leaf)"
                }
                else {
                    "$($drive)$([char]0x2026)\$($leaf)"
                }
            }
            else {
                $drive = (Get-Location).Drive.Name + ':\'
                Write-Verbose "Matched base drive [$drive]! Returning base path"
                $finalPath = if ($Path -eq $drive) {
                    $drive
                }
                elseif ((Split-Path $Path -Parent) -eq $drive) {
                    "$($drive)$($leaf)"
                }
                else {
                    "$($drive)..\$($leaf)"
                }
            }
            if ($DirectorySeparator -notin @($null,([System.IO.Path]::DirectorySeparatorChar))) {
                $finalPath.Replace(([System.IO.Path]::DirectorySeparatorChar),$DirectorySeparator)
            }
            else {
                $finalPath
            }
        }
        catch {
            Write-Error $_
            return $origPath
        }
    }
}


Export-ModuleMember -Function 'Get-PathAlias'

function Get-PSProfileArguments {
    <#
    .SYNOPSIS
    Used for PSProfile Plugins to provide easy Argument Completers using PSProfile constructs.
 
    .DESCRIPTION
    Used for PSProfile Plugins to provide easy Argument Completers using PSProfile constructs.
 
    .PARAMETER FinalKeyOnly
    Returns only the final key of the completed argument to the list of completers. If $false, returns the full path.
 
    .PARAMETER WordToComplete
    The word to complete, typically passed in from the scriptblock arguments.
 
    .PARAMETER CommandName
    Here to allow passing @PSBoundParameters directly to this function from Register-ArgumentCompleter
 
    .PARAMETER ParameterName
    Here to allow passing @PSBoundParameters directly to this function from Register-ArgumentCompleter
 
    .PARAMETER CommandAst
    Here to allow passing @PSBoundParameters directly to this function from Register-ArgumentCompleter
 
    .PARAMETER FakeBoundParameter
    Here to allow passing @PSBoundParameters directly to this function from Register-ArgumentCompleter
 
    .EXAMPLE
    Get-PSProfileArguments -WordToComplete "Prompts.$wordToComplete" -FinalKeyOnly
 
    Gets the list of prompt names under the Prompts PSProfile primary key.
 
    .EXAMPLE
    Get-PSProfileArguments -WordToComplete "GitPathMap.$wordToComplete" -FinalKeyOnly
 
    Gets the list of Git Path short names under the GitPathMap PSProfile primary key.
    #>

    [OutputType('System.Management.Automation.CompletionResult')]
    [CmdletBinding()]
    Param(
        [switch]
        $FinalKeyOnly,
        [string]
        $WordToComplete,
        [object]
        $CommandName,
        [object]
        $ParameterName,
        [object]
        $CommandAst,
        [object]
        $FakeBoundParameter
    )
    Process {
        Write-Verbose "Getting PSProfile command argument completions"
        $split = $WordToComplete.Split('.')
        $setting = $null
        switch ($split.Count) {
            5 {
                $setting = $Global:PSProfile."$($split[0])"."$($split[1])"."$($split[2])"."$($split[3])"
                $base = "$($split[0])"."$($split[1])"."$($split[2])"."$($split[3])"
            }
            4 {
                $setting = $Global:PSProfile."$($split[0])"."$($split[1])"."$($split[2])"
                $base = "$($split[0])"."$($split[1])"."$($split[2])"
            }
            3 {
                $setting = $Global:PSProfile."$($split[0])"."$($split[1])"
                $base = "$($split[0])"."$($split[1])"
            }
            2 {
                $setting = $Global:PSProfile."$($split[0])"
                $base = $split[0]
            }
        }
        if ($null -eq $setting) {
            $setting = $Global:PSProfile
            $base = $null
            $final = $WordToComplete
        }
        else {
            $final = $split | Select-Object -Last 1
        }
        if ($setting.GetType() -notin @([string],[int],[long],[version],[timespan],[datetime],[bool])) {
            $props = if ($setting.PSTypeNames -match 'Hashtable') {
                $setting.Keys | Where-Object {$_ -ne '_internal' -and $_ -like "$final*"} | Sort-Object
            }
            else {
                ($setting | Get-Member -MemberType Property,NoteProperty).Name | Where-Object {$_ -notmatch '^_' -and $_ -like "$final*"} | Sort-Object
            }
            $props | ForEach-Object {
                $result = if (-not $FinalKeyOnly -and $null -ne $base) {
                    @($base,$_) -join "."
                }
                else {
                    $_
                }
                [System.Management.Automation.CompletionResult]::new($result, $result, 'ParameterValue', $result)
            }
        }
    }
}

Register-ArgumentCompleter -CommandName Get-PSProfileArguments -ParameterName WordToComplete -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    Get-PSProfileArguments @PSBoundParameters
}


Export-ModuleMember -Function 'Get-PSProfileArguments'

function Get-PSVersion {
    <#
    .SYNOPSIS
    Gets the short formatted PSVersion string for use in a prompt or wherever else desired.
 
    .DESCRIPTION
    Gets the short formatted PSVersion string for use in a prompt or wherever else desired.
 
    .PARAMETER Places
    How many decimal places you would like the returned version string to be. Defaults to $PSProfile.Settings.PSVersionStringLength if present.
 
    .EXAMPLE
    Get-PSVersion -Places 2
 
    Returns `6.2` when using PowerShell 6.2.2, or `5.1` when using Windows PowerShell 5.1.18362.10000
    #>


    [OutputType('System.String')]
    [CmdletBinding()]
    Param (
        [parameter(Position = 0)]
        [AllowNull()]
        [int]
        $Places = $global:PSProfile.Settings.PSVersionStringLength
    )
    Process {
        $version = $PSVersionTable.PSVersion.ToString()
        if ($null -ne $Places) {
            $split = ($version -split '\.')[0..($Places - 1)]
            if ("$($split[-1])".Length -gt 1) {
                $split[-1] = "$($split[-1])".Substring(0,1)
            }
            $joined = $split -join '.'
            if ($version -match '[a-zA-Z]+') {
                $joined += "-$(($Matches[0]).Substring(0,1))"
                if ($version -match '\d+$') {
                    $joined += $Matches[0]
                }
            }
            $joined
        }
        else {
            $version
        }
    }
}


Export-ModuleMember -Function 'Get-PSVersion'

function Test-IfGit {
    <#
    .SYNOPSIS
    Tests if the current path is in a Git repo folder and returns the basic details as an object if so. Useful in prompts when determining current folder's Git status
 
    .DESCRIPTION
    Tests if the current path is in a Git repo folder and returns the basic details as an object if so. Useful in prompts when determining current folder's Git status
 
    .EXAMPLE
    Test-IfGit
    #>

    [CmdletBinding()]
    Param ()
    Process {
        try {
            $topLevel = git rev-parse --show-toplevel *>&1
            if ($topLevel -like 'fatal: *') {
                $Global:Error.Remove($Global:Error[0])
                $false
            }
            else {
                $origin = git remote get-url origin
                $repo = Split-Path -Leaf $origin
                [PSCustomObject]@{
                    TopLevel = (Resolve-Path $topLevel).Path
                    Origin   = $origin
                    Repo     = $(if ($repo -notmatch '(\.git|\.ssh|\.tfs)$') {
                            $repo
                        }
                        else {
                            $repo.Substring(0,($repo.LastIndexOf('.')))
                        })
                }
            }
        }
        catch {
            $false
            $Global:Error.Remove($Global:Error[0])
        }
    }
}


Export-ModuleMember -Function 'Test-IfGit'

function Write-PSProfileLog {
    <#
    .SYNOPSIS
    Adds a log entry to the current PSProfile Log.
 
    .DESCRIPTION
    Adds a log entry to the current PSProfile Log. Used for external plugins to hook into the existing log so items like Plugin load logging are contained in one place.
 
    .PARAMETER Message
    The message to log.
 
    .PARAMETER Section
    The name of the section you are logging for, e.g. the name of the plugin or overall what action is being done.
 
    .PARAMETER LogLevel
    The Level of the Log event. Defaults to Debug.
 
    .EXAMPLE
    Write-PSProfileLog -Message "Hunting for missing KBs" -Section 'KBUpdate' -LogLevel 'Verbose'
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory,Position = 0)]
        [String]
        $Message,
        [Parameter(Mandatory,Position = 1)]
        [String]
        $Section,
        [Parameter(Position = 2)]
        [PSProfileLogLevel]
        $LogLevel = 'Debug'
    )
    Process {
        $Global:PSProfile._log(
            $Message,
            $Section,
            $LogLevel
        )
    }
}


Export-ModuleMember -Function 'Write-PSProfileLog'

function Get-PSProfileCommand {
    <#
    .SYNOPSIS
    Gets the list of commands provided by PSProfile directly.
 
    .DESCRIPTION
    Gets the list of commands provided by PSProfile directly.
 
    .PARAMETER Command
    The command to get from the list of PSProfile commands.
 
    .EXAMPLE
    Get-PSProfileCommand
 
    Gets the full list of commands provided by PSProfile directly.
    #>

    [OutputType('System.Management.Automation.FunctionInfo')]
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0,ValueFromPipeline)]
        [String[]]
        $Command
    )
    Begin {
        $commands = Get-Command -Module PSProfile | Where-Object {$_.Name -in (Get-Module PSProfile).ExportedCommands.Keys}
    }
    Process {
        if ($PSBoundParameters.ContainsKey('Command')) {
            Write-Verbose "Getting PSProfile command '$Command'"
            $commands | Where-Object {$_.Name -in $Command}
        }
        else {
            Write-Verbose "Getting all commands provided by PSProfile directly"
            $commands
        }
    }
}

Register-ArgumentCompleter -CommandName Get-PSProfileCommand -ParameterName Command -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    (Get-Module PSProfile).ExportedCommands.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Get-PSProfileCommand'

function Get-PSProfileImportedCommand {
    <#
    .SYNOPSIS
    Gets the list of commands imported from scripts and plugins that are not part of PSProfile itself.
 
    .DESCRIPTION
    Gets the list of commands imported from scripts and plugins that are not part of PSProfile itself.
 
    .PARAMETER Command
    The command to get from the list of imported commands.
 
    .EXAMPLE
    Get-PSProfileImportedCommand
 
    Gets the full list of commands imported during PSProfile load.
    #>

    [OutputType('System.Management.Automation.FunctionInfo')]
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0,ValueFromPipeline)]
        [String[]]
        $Command
    )
    Begin {
        $commands = Get-Command -Module PSProfile | Where-Object {$_.Name -notin (Get-Module PSProfile).ExportedCommands.Keys}
    }
    Process {
        if ($PSBoundParameters.ContainsKey('Command')) {
            Write-Verbose "Getting imported command '$Command'"
            $commands | Where-Object {$_.Name -in $Command}
        }
        else {
            Write-Verbose "Getting commands imported during PSProfile load that are not part of PSProfile itself"
            $commands
        }
    }
}

Register-ArgumentCompleter -CommandName Get-PSProfileImportedCommand -ParameterName Command -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    (Get-Command -Module PSProfile | Where-Object {$_.Name -notin (Get-Module PSProfile).ExportedCommands.Keys} | Where-Object {$_ -like "$wordToComplete*"}).Name | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Get-PSProfileImportedCommand'

function Get-PSProfileLog {
    <#
    .SYNOPSIS
    Gets the PSProfile Log events.
 
    .DESCRIPTION
    Gets the PSProfile Log events.
 
    .PARAMETER Section
    Limit results to only a specific section.
 
    .PARAMETER LogLevel
    Limit results to only a specific LogLevel.
 
    .PARAMETER Summary
    Get a high-level summary of the PSProfile Log.
 
    .PARAMETER Raw
    Return the raw PSProfile Events. Returns the results via Format-Table for readability otherwise.
 
    .EXAMPLE
    Get-PSProfileLog
 
    Gets the current Log in full.
 
    .EXAMPLE
    Get-PSProfileLog -Summary
 
    Gets the Log summary.
 
    .EXAMPLE
    Get-PSProfileLog -Section InvokeScripts,LoadPlugins -Raw
 
    Gets the Log Events for only sections 'InvokeScripts' and 'LoadPlugins' and returns the raw Event objects.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Full')]
    Param(
        [Parameter(Position = 0,ParameterSetName = 'Full')]
        [String[]]
        $Section,
        [Parameter(Position = 1,ParameterSetName = 'Full')]
        [PSProfileLogLevel[]]
        $LogLevel,
        [Parameter(ParameterSetName = 'Summary')]
        [Switch]
        $Summary,
        [Parameter(ParameterSetName = 'Full')]
        [Switch]
        $Raw
    )
    Process {
        if ($Summary) {
            Write-Verbose "Getting PSProfile Log summary"
            $Global:PSProfile.Log | Group-Object Section | ForEach-Object {
                $sectName = $_.Name
                $Group = $_.Group
                $sectCaps = $Group | Where-Object {$_.Message -match '^SECTION (START|END)$'}
                [PSCustomObject]@{
                    Name = $sectName
                    Start = $sectCaps[0].Time.ToString('HH:mm:ss.fff')
                    SectionDuration = "$([Math]::Round(($sectCaps[-1].Time - $sectCaps[0].Time).TotalMilliseconds))ms"
                    FullDuration = "$([Math]::Round(($Group[-1].Time - $Group[0].Time).TotalMilliseconds))ms"
                    RunningJobs = Get-RSJob -State Running | Where-Object {$_.Name -match $sectName} | Select-Object -ExpandProperty Name
                }
            } | Sort-Object Start | Format-Table -AutoSize
        }
        else {
            Write-Verbose "Getting PSProfile Log"
            $items = if ($Section) {
                $Global:PSProfile.Log | Where-Object {$_.Section -in $Section}
            }
            else {
                $Global:PSProfile.Log
            }
            if ($LogLevel) {
                $items = $items | Where-Object {$_.LogLevel -in $LogLevel}
            }
            if (-not $Raw) {
                $items | Format-Table -AutoSize
            }
            else {
                $items
            }
        }
    }
}

Register-ArgumentCompleter -CommandName Get-PSProfileLog -ParameterName 'Section' -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.Log.Section | Sort-Object -Unique | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Get-PSProfileLog'

function Add-PSProfileModuleToImport {
    <#
    .SYNOPSIS
    Adds a module to import during PSProfile import.
 
    .DESCRIPTION
    Adds a module to import during PSProfile import.
 
    .PARAMETER Name
    The name of the module to import.
 
    .PARAMETER Prefix
    Add the specified prefix to the nouns in the names of imported module members.
 
    .PARAMETER MinimumVersion
    Import only a version of the module that is greater than or equal to the specified value. If no version qualifies, Import-Module generates an error.
 
    .PARAMETER RequiredVersion
    Import only the specified version of the module. If the version is not installed, Import-Module generates an error.
 
    .PARAMETER ArgumentList
    Specifies arguments (parameter values) that are passed to a script module during the Import-Module command. Valid only when importing a script module.
 
    .PARAMETER Force
    If the module already exists in $PSProfile.ModulesToImport, use -Force to overwrite the existing value.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Add-PSProfileModuleToImport -Name posh-git -RequiredVersion '0.7.3' -Save
 
    Specifies to import posh-git version 0.7.3 during PSProfile import then saves the updated configuration.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory,Position = 0,ValueFromPipeline)]
        [String]
        $Name,
        [Parameter()]
        [String]
        $Prefix,
        [Parameter()]
        [String]
        $MinimumVersion,
        [Parameter()]
        [String]
        $RequiredVersion,
        [Parameter()]
        [Object[]]
        $ArgumentList,
        [Parameter()]
        [Switch]
        $Force,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        if (-not $Force -and $null -ne ($Global:PSProfile.ModulesToImport | Where-Object {$_.Name -eq $Name})) {
            Write-Error "Unable to add module to `$PSProfile.ModulesToImport as it already exists. Use -Force to overwrite the existing value if desired."
        }
        else {
            $moduleParams = $PSBoundParameters
            foreach ($key in $moduleParams.Keys | Where-Object {$_ -in @('Verbose','Confirm','Force') -or $_ -notin (Get-Command Import-Module).Parameters.Keys}) {
                $null = $moduleParams.Remove($key)
            }
            Write-Verbose "Adding '$Name' to `$PSProfile.ModulesToImport"
            $Global:PSProfile.ModulesToImport = @($Global:PSProfile.ModulesToImport,$moduleParams)
            if ($Save) {
                Save-PSProfile
            }
        }
    }
}

Register-ArgumentCompleter -CommandName Add-PSProfileModuleToImport -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    Get-Module "$wordToComplete*" -ListAvailable | Select-Object -ExpandProperty Name | Sort-Object -Unique | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Add-PSProfileModuleToImport'

function Get-PSProfileModuleToImport {
    <#
    .SYNOPSIS
    Gets a module from $PSProfile.ModulesToImport.
 
    .DESCRIPTION
    Gets a module from $PSProfile.ModulesToImport.
 
    .PARAMETER Name
    The name of the module to get from $PSProfile.ModulesToImport.
 
    .EXAMPLE
    Get-PSProfileModuleToImport -Name posh-git
 
    Gets posh-git from $PSProfile.ModulesToImport
 
    .EXAMPLE
    Get-PSProfileModuleToImport
 
    Gets the list of modules to import from $PSProfile.ModulesToImport
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0,ValueFromPipeline)]
        [String[]]
        $Name
    )
    Process {
        if ($PSBoundParameters.ContainsKey('Name')) {
            Write-Verbose "Getting ModuleToImport '$Name' from `$PSProfile.ModulesToImport"
            $Global:PSProfile.ModulesToImport | Where-Object {$_ -in $Name -or $_.Name -in $Name}
        }
        else {
            Write-Verbose "Getting all command aliases from `$PSProfile.ModulesToImport"
            $Global:PSProfile.ModulesToImport
        }
    }
}

Register-ArgumentCompleter -CommandName Get-PSProfileModuleToImport -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.ModulesToImport | ForEach-Object {
        if ($_ -is [hashtable]) {
            $_.Name
        }
        else {
            $_
        }
    } | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Get-PSProfileModuleToImport'

function Remove-PSProfileModuleToImport {
    <#
    .SYNOPSIS
    Removes a module from $PSProfile.ModulesToImport.
 
    .DESCRIPTION
    Removes a module from $PSProfile.ModulesToImport.
 
    .PARAMETER Name
    The name of the module to remove from $PSProfile.ModulesToImport.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Remove-PSProfileModuleToImport -Name posh-git -Save
 
    Removes posh-git from $PSProfile.ModulesToImport then saves the updated configuration.
    #>

    [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")]
    Param (
        [Parameter(Mandatory,Position = 0,ValueFromPipeline)]
        [String]
        $Name,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        if ($PSCmdlet.ShouldProcess("Removing '$Name' from `$PSProfile.ModulesToImport")) {
            Write-Verbose "Removing '$Name' from `$PSProfile.ModulesToImport"
            $Global:PSProfile.ModulesToImport = $Global:PSProfile.ModulesToImport | Where-Object {($_ -is [hashtable] -and $_.Name -ne $Name) -or ($_ -is [string] -and $_ -ne $Name)}
            if ($Save) {
                Save-PSProfile
            }
        }
    }
}

Register-ArgumentCompleter -CommandName Remove-PSProfileModuleToImport -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.ModulesToImport | ForEach-Object {
        if ($_ -is [hashtable]) {
            $_.Name
        }
        else {
            $_
        }
    } | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Remove-PSProfileModuleToImport'

function Add-PSProfileModuleToInstall {
    <#
    .SYNOPSIS
    Adds a module to ensure is installed in the CurrentUser scope. Module installations are handled via background job during PSProfile import.
 
    .DESCRIPTION
    Adds a module to ensure is installed in the CurrentUser scope. Module installations are handled via background job during PSProfile import.
 
    .PARAMETER Name
    The name of the module to install.
 
    .PARAMETER Repository
    The repository to install the module from. Defaults to the PowerShell Gallery.
 
    .PARAMETER MinimumVersion
    The minimum version of the module to install.
 
    .PARAMETER RequiredVersion
    The required version of the module to install.
 
    .PARAMETER AcceptLicense
    If $true, accepts the license for the module if necessary.
 
    .PARAMETER AllowPrerelease
    If $true, allows installation of prerelease versions of the module.
 
    .PARAMETER Force
    If the module already exists in $PSProfile.ModulesToInstall, use -Force to overwrite the existing value.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Add-PSProfileModuleToInstall -Name posh-git -RequiredVersion '0.7.3' -Save
 
    Specifies to install posh-git version 0.7.3 during PSProfile import if missing then saves the updated configuration.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory,Position = 0,ValueFromPipeline)]
        [String]
        $Name,
        [Parameter()]
        [String]
        $Repository,
        [Parameter()]
        [String]
        $MinimumVersion,
        [Parameter()]
        [String]
        $RequiredVersion,
        [Parameter()]
        [Switch]
        $AcceptLicense,
        [Parameter()]
        [Switch]
        $AllowPrerelease,
        [Parameter()]
        [Switch]
        $Force,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        if (-not $Force -and $null -ne ($Global:PSProfile.ModulesToInstall | Where-Object {$_.Name -eq $Name})) {
            Write-Error "Unable to add module to `$PSProfile.ModulesToInstall as it already exists. Use -Force to overwrite the existing value if desired."
        }
        else {
            $moduleParams = $PSBoundParameters
            foreach ($key in $moduleParams.Keys | Where-Object {$_ -in @('Verbose','Confirm','Force') -or $_ -notin (Get-Command Install-Module).Parameters.Keys}) {
                $moduleParams.Remove($key)
            }
            Write-Verbose "Adding '$Name' to `$PSProfile.ModulesToInstall"
            $Global:PSProfile.ModulesToInstall = @($Global:PSProfile.ModulesToInstall,$moduleParams)
            if ($Save) {
                Save-PSProfile
            }
        }
    }
}

Register-ArgumentCompleter -CommandName Add-PSProfileModuleToInstall -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    Get-Module "$wordToComplete*" -ListAvailable | Select-Object -ExpandProperty Name | Sort-Object -Unique | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Add-PSProfileModuleToInstall'

function Get-PSProfileModuleToInstall {
    <#
    .SYNOPSIS
    Gets a module from $PSProfile.ModulesToInstall.
 
    .DESCRIPTION
    Gets a module from $PSProfile.ModulesToInstall.
 
    .PARAMETER Name
    The name of the module to get from $PSProfile.ModulesToInstall.
 
    .EXAMPLE
    Get-PSProfileModuleToInstall -Name posh-git
 
    Gets posh-git from $PSProfile.ModulesToInstall
 
    .EXAMPLE
    Get-PSProfileModuleToInstall
 
    Gets the list of modules to install from $PSProfile.ModulesToInstall
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0,ValueFromPipeline)]
        [String[]]
        $Name
    )
    Process {
        if ($PSBoundParameters.ContainsKey('Name')) {
            Write-Verbose "Getting ModuleToImport '$Name' from `$PSProfile.ModulesToInstall"
            $Global:PSProfile.ModulesToInstall | Where-Object {$_ -in $Name -or $_.Name -in $Name}
        }
        else {
            Write-Verbose "Getting all command aliases from `$PSProfile.ModulesToInstall"
            $Global:PSProfile.ModulesToInstall
        }
    }
}

Register-ArgumentCompleter -CommandName Get-PSProfileModuleToInstall -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.ModulesToInstall | ForEach-Object {
        if ($_ -is [hashtable]) {
            $_.Name
        }
        else {
            $_
        }
    } | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Get-PSProfileModuleToInstall'

function Remove-PSProfileModuleToInstall {
    <#
    .SYNOPSIS
    Removes a module from $PSProfile.ModulesToInstall.
 
    .DESCRIPTION
    Removes a module from $PSProfile.ModulesToInstall.
 
    .PARAMETER Name
    The name of the module to remove from $PSProfile.ModulesToInstall.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Remove-PSProfileModuleToInstall -Name posh-git -Save
 
    Removes posh-git from $PSProfile.ModulesToInstall then saves the updated configuration.
    #>

    [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")]
    Param (
        [Parameter(Mandatory,Position = 0,ValueFromPipeline)]
        [String]
        $Name,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        if ($PSCmdlet.ShouldProcess("Removing '$Name' from `$PSProfile.ModulesToInstall")) {
            Write-Verbose "Removing '$Name' from `$PSProfile.ModulesToInstall"
            $Global:PSProfile.ModulesToInstall = $Global:PSProfile.ModulesToInstall | Where-Object {($_ -is [hashtable] -and $_.Name -ne $Name) -or ($_ -is [string] -and $_ -ne $Name)}
            if ($Save) {
                Save-PSProfile
            }
        }
    }
}

Register-ArgumentCompleter -CommandName Remove-PSProfileModuleToInstall -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.ModulesToInstall | ForEach-Object {
        if ($_ -is [hashtable]) {
            $_.Name
        }
        else {
            $_
        }
    } | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Remove-PSProfileModuleToInstall'

function Add-PSProfilePathAlias {
    <#
    .SYNOPSIS
    Adds a path alias to your PSProfile configuration. Path aliases are used for path shortening in prompts via Get-PathAlias.
 
    .DESCRIPTION
    Adds a path alias to your PSProfile configuration. Path aliases are used for path shortening in prompts via Get-PathAlias.
 
    .PARAMETER Alias
    The alias to substitute the full path for in prompts via Get-PathAlias.
 
    .PARAMETER Path
    The full path to be substituted.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Add-PSProfilePathAlias -Alias ~ -Path $env:USERPROFILE -Save
 
    Adds a path alias of ~ for the current UserProfile folder and saves your PSProfile configuration.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory,Position = 0)]
        [String]
        $Alias,
        [Parameter(Mandatory,Position = 1)]
        [String]
        $Path,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        Write-Verbose "Adding alias '$Alias' to path '$Path' to PSProfile"
        $Global:PSProfile.PathAliases[$Alias] = $Path
        if ($Save) {
            Save-PSProfile
        }
    }
}


Export-ModuleMember -Function 'Add-PSProfilePathAlias'

function Get-PSProfilePathAlias {
    <#
    .SYNOPSIS
    Gets a module from $PSProfile.PathAliases.
 
    .DESCRIPTION
    Gets a module from $PSProfile.PathAliases.
 
    .PARAMETER Alias
    The Alias to get from $PSProfile.PathAliases.
 
    .EXAMPLE
    Get-PSProfilePathAlias -Alias ~
 
    Gets the alias '~' from $PSProfile.PathAliases
 
    .EXAMPLE
    Get-PSProfilePathAlias
 
    Gets the list of path aliases from $PSProfile.PathAliases
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0,ValueFromPipeline)]
        [String[]]
        $Alias
    )
    Process {
        if ($PSBoundParameters.ContainsKey('Alias')) {
            Write-Verbose "Getting Path Alias '$Alias' from `$PSProfile.PathAliases"
            $Global:PSProfile.PathAliases.GetEnumerator() | Where-Object {$_.Key -in $Alias}
        }
        else {
            Write-Verbose "Getting all command aliases from `$PSProfile.PathAliases"
            $Global:PSProfile.PathAliases
        }
    }
}

Register-ArgumentCompleter -CommandName Get-PSProfilePathAlias -ParameterName Alias -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.PathAliases.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Get-PSProfilePathAlias'

function Remove-PSProfilePathAlias {
    <#
    .SYNOPSIS
    Removes an alias from $PSProfile.PathAliases.
 
    .DESCRIPTION
    Removes an alias from $PSProfile.PathAliases.
 
    .PARAMETER Alias
    The alias to remove from $PSProfile.PathAliases.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Remove-PSProfilePathAlias -Alias Workplace -Save
 
    Removes the alias 'Workplace' from $PSProfile.PathAliases then saves the updated configuration.
    #>

    [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")]
    Param (
        [Parameter(Mandatory,Position = 0,ValueFromPipeline)]
        [String]
        $Alias,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        if ($PSCmdlet.ShouldProcess("Removing '$Alias' from `$PSProfile.PathAliases")) {
            Write-Verbose "Removing '$Alias' from `$PSProfile.PathAliases"
            $Global:PSProfile.PathAliases.Remove($Alias)
            if ($Save) {
                Save-PSProfile
            }
        }
    }
}

Register-ArgumentCompleter -CommandName Remove-PSProfilePathAlias -ParameterName Alias -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.PathAliases.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Remove-PSProfilePathAlias'

function Add-PSProfilePluginPath {
    <#
    .SYNOPSIS
    Adds a PluginPath to your PSProfile to search for PSProfile plugins in during module load.
 
    .DESCRIPTION
    Adds a PluginPath to your PSProfile to search for PSProfile plugins in during module load.
 
    .PARAMETER Path
    The path of the folder to add to your $PSProfile.PluginPaths. This path should contain PSProfile.Plugins
 
    .PARAMETER NoRefresh
    If $true, skips refreshing your PSProfile after updating plugin paths.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Add-PSProfilePluginPath -Path ~\PSProfilePlugins -Save
 
    Adds the folder ~\PSProfilePlugins to $PSProfile.PluginPaths and saves the configuration after updating.
 
    .EXAMPLE
    Add-PSProfilePluginPath C:\PSProfilePlugins -Verbose
 
    Adds the path C:\PSProfilePlugins to your $PSProfile.PluginPaths, refreshes your PathDict but does not save. Call Save-PSProfile after if satisfied with the results.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory,Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [ValidateScript({if ((Get-Item $_).PSIsContainer){$true}else{throw "$_ is not a folder! Please add only folders to this PSProfile property. If you would like to add a script, use Add-PSProfileScriptPath instead."}})]
        [Alias('FullName')]
        [String[]]
        $Path,
        [Parameter()]
        [Switch]
        $NoRefresh,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        foreach ($p in $Path) {
            $fP = (Resolve-Path $p).Path
            if ($Global:PSProfile.PluginPaths -notcontains $fP) {
                Write-Verbose "Adding PluginPath to PSProfile: $fP"
                $Global:PSProfile.PluginPaths += $fP
            }
            else {
                Write-Verbose "PluginPath already in PSProfile: $fP"
            }
        }
        if (-not $NoRefresh) {
            Import-PSProfile
        }
        if ($Save) {
            Save-PSProfile
        }
    }
}


Export-ModuleMember -Function 'Add-PSProfilePluginPath'

function Get-PSProfilePluginPath {
    <#
    .SYNOPSIS
    Gets a plugin path from $PSProfile.PluginPaths.
 
    .DESCRIPTION
    Gets a plugin path from $PSProfile.PluginPaths.
 
    .PARAMETER Path
    The plugin path to get from $PSProfile.PluginPaths.
 
    .EXAMPLE
    Get-PSProfilePluginPath -Path E:\MyPSProfilePlugins
 
    Gets the path 'E:\MyPSProfilePlugins' from $PSProfile.PluginPaths
 
    .EXAMPLE
    Get-PSProfilePluginPath
 
    Gets the list of plugin paths from $PSProfile.PluginPaths
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0,ValueFromPipeline)]
        [String[]]
        $Path
    )
    Process {
        if ($PSBoundParameters.ContainsKey('Path')) {
            Write-Verbose "Getting plugin path '$Path' from `$PSProfile.PluginPaths"
            $Global:PSProfile.PluginPaths | Where-Object {$_ -in $Path}
        }
        else {
            Write-Verbose "Getting all plugin paths from `$PSProfile.PluginPaths"
            $Global:PSProfile.PluginPaths
        }
    }
}

Register-ArgumentCompleter -CommandName Get-PSProfilePluginPath -ParameterName Path -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.PluginPaths | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Get-PSProfilePluginPath'

function Remove-PSProfilePluginPath {
    <#
    .SYNOPSIS
    Removes a Plugin Path from $PSProfile.PluginPaths.
 
    .DESCRIPTION
    Removes a Plugin Path from $PSProfile.PluginPaths.
 
    .PARAMETER Path
    The path to remove from $PSProfile.PluginPaths.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Remove-PSProfilePluginPath -Name E:\MyPluginPaths -Save
 
    Removes the path 'E:\MyPluginPaths' from $PSProfile.PluginPaths then saves the updated configuration.
    #>

    [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")]
    Param (
        [Parameter(Mandatory,Position = 0,ValueFromPipeline)]
        [String]
        $Path,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        if ($PSCmdlet.ShouldProcess("Removing '$Path' from `$PSProfile.PluginPaths")) {
            Write-Verbose "Removing '$Path' from `$PSProfile.PluginPaths"
            $Global:PSProfile.PluginPaths = $Global:PSProfile.PluginPaths | Where-Object {$_ -notin @($Path,(Resolve-Path $Path).Path)}
            if ($Save) {
                Save-PSProfile
            }
        }
    }
}

Register-ArgumentCompleter -CommandName Remove-PSProfilePluginPath -ParameterName Path -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.PluginPaths | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Remove-PSProfilePluginPath'

function Add-PSProfilePlugin {
    <#
    .SYNOPSIS
    Adds a PSProfile Plugin to the list of plugins. If the plugin already exists, it will overwrite it. Re-imports your PSProfile once done to load any newly added plugins.
 
    .DESCRIPTION
    Adds a PSProfile Plugin to the list of plugins. If the plugin already exists, it will overwrite it. Re-imports your PSProfile once done to load any newly added plugins.
 
    .PARAMETER Name
    The name of the Plugin to add, e.g. 'PSProfile.PowerTools'
 
    .PARAMETER ArgumentList
    Any arguments that need to be passed to the plugin on import, such as a hashtable to process.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Add-PSProfilePlugin -Name 'PSProfile.PowerTools' -Save
 
    Adds the included plugin 'PSProfile.PowerTools' to your PSProfile and saves it so it persists.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory,Position = 0)]
        [String[]]
        $Name,
        [Parameter(Position = 1)]
        [Object]
        $ArgumentList,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        foreach ($pName in $Name) {
            Write-Verbose "Adding plugin '$pName' to `$PSProfile.Plugins"
            $plugin = @{
                Name = $pName
            }
            if ($PSBoundParameters.ContainsKey('ArgumentList')) {
                $plugin['ArgumentList'] = $ArgumentList
            }
            $temp = $Global:PSProfile.Plugins | Where-Object {$_.Name -ne $pName}
            $temp += $plugin
            $Global:PSProfile.Plugins = $temp
        }
        if ($Save) {
            Save-PSProfile
        }
        Import-PSProfile -Verbose:$false
    }
}

Register-ArgumentCompleter -CommandName Add-PSProfilePlugin -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.PluginPaths | Get-ChildItem | Select-Object -ExpandProperty BaseName -Unique | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Add-PSProfilePlugin'

function Get-PSProfilePlugin {
    <#
    .SYNOPSIS
    Gets a Plugin from $PSProfile.Plugins.
 
    .DESCRIPTION
    Gets a Plugin from $PSProfile.Plugins.
 
    .PARAMETER Name
    The name of the Plugin to get from $PSProfile.Plugins.
 
    .EXAMPLE
    Get-PSProfilePlugin -Name PSProfile.Prompt
 
    Gets PSProfile.Prompt from $PSProfile.Plugins
 
    .EXAMPLE
    Get-PSProfilePlugin
 
    Gets the list of Plugins from $PSProfile.Plugins
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0,ValueFromPipeline)]
        [String[]]
        $Name
    )
    Process {
        if ($PSBoundParameters.ContainsKey('Name')) {
            Write-Verbose "Getting Plugin '$Name' from `$PSProfile.Plugins"
            $Global:PSProfile.Plugins | Where-Object {$_.Name -in $Name}
        }
        else {
            Write-Verbose "Getting all Plugins from `$PSProfile.Plugins"
            $Global:PSProfile.Plugins
        }
    }
}

Register-ArgumentCompleter -CommandName Get-PSProfilePlugin -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.Plugins | ForEach-Object {$_.Name} | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Get-PSProfilePlugin'

function Remove-PSProfilePlugin {
    <#
    .SYNOPSIS
    Removes a PSProfile Plugin from $PSProfile.Plugins.
 
    .DESCRIPTION
    Removes a PSProfile Plugin from $PSProfile.Plugins.
 
    .PARAMETER Name
    The name of the Plugin to remove from $PSProfile.Plugins.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Remove-PSProfilePlugin -Name 'PSProfile.PowerTools' -Save
 
    Removes the Plugin 'PSProfile.PowerTools' from $PSProfile.Plugins then saves the updated configuration.
    #>

    [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")]
    Param (
        [Parameter(Mandatory,Position = 0,ValueFromPipeline)]
        [String]
        $Name,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        if ($PSCmdlet.ShouldProcess("Removing '$Name' from `$PSProfile.Plugins")) {
            Write-Verbose "Removing '$Name' from `$PSProfile.Plugins"
            $Global:PSProfile.Plugins = $Global:PSProfile.Plugins | Where-Object {$_.Name -ne $Name}
            if ($Save) {
                Save-PSProfile
            }
        }
    }
}

Register-ArgumentCompleter -CommandName Remove-PSProfilePlugin -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.Plugins.Name | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Remove-PSProfilePlugin'

function Add-PSProfileProjectPath {
    <#
    .SYNOPSIS
    Adds a ProjectPath to your PSProfile to find Git project folders under during PSProfile refresh. These will be available via tab-completion
 
    .DESCRIPTION
    Adds a ProjectPath to your PSProfile to find Git project folders under during PSProfile refresh.
 
    .PARAMETER Path
    The path of the folder to add to your $PSProfile.ProjectPaths. This path should contain Git repo folders underneath it.
 
    .PARAMETER NoRefresh
    If $true, skips refreshing your PSProfile after updating project paths.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Add-PSProfileProjectPath -Path ~\GitRepos -Save
 
    Adds the folder ~\GitRepos to $PSProfile.ProjectPaths and saves the configuration after updating.
 
    .EXAMPLE
    Add-PSProfileProjectPath C:\Git -Verbose
 
    Adds the path C:\Git to your $PSProfile.ProjectPaths, refreshes your PathDict but does not save. Call Save-PSProfile after if satisfied with the results.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory,Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [ValidateScript({if ((Get-Item $_).PSIsContainer){$true}else{throw "$_ is not a folder! Please add only folders to this PSProfile property. If you would like to add a script, use Add-PSProfileScriptPath instead."}})]
        [Alias('FullName')]
        [String[]]
        $Path,
        [Parameter()]
        [Switch]
        $NoRefresh,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        foreach ($p in $Path) {
            $fP = (Resolve-Path $p).Path
            if ($Global:PSProfile.ProjectPaths -notcontains $fP) {
                Write-Verbose "Adding ProjectPath to PSProfile: $fP"
                $Global:PSProfile.ProjectPaths += $fP
            }
            else {
                Write-Verbose "ProjectPath already in PSProfile: $fP"
            }
        }
        if (-not $NoRefresh) {
            Update-PSProfileConfig
        }
        if ($Save) {
            Save-PSProfile
        }
    }
}


Export-ModuleMember -Function 'Add-PSProfileProjectPath'

function Get-PSProfileProjectPath {
    <#
    .SYNOPSIS
    Gets a project path from $PSProfile.ProjectPaths.
 
    .DESCRIPTION
    Gets a project path from $PSProfile.ProjectPaths.
 
    .PARAMETER Path
    The project path to get from $PSProfile.ProjectPaths.
 
    .EXAMPLE
    Get-PSProfileProjectPath -Path E:\Git
 
    Gets the path 'E:\Git' from $PSProfile.ProjectPaths
 
    .EXAMPLE
    Get-PSProfileProjectPath
 
    Gets the list of project paths from $PSProfile.ProjectPaths
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0,ValueFromPipeline)]
        [String[]]
        $Path
    )
    Process {
        if ($PSBoundParameters.ContainsKey('Path')) {
            Write-Verbose "Getting project path '$Path' from `$PSProfile.ProjectPaths"
            $Global:PSProfile.ProjectPaths | Where-Object {$_ -in $Path}
        }
        else {
            Write-Verbose "Getting all project paths from `$PSProfile.ProjectPaths"
            $Global:PSProfile.ProjectPaths
        }
    }
}

Register-ArgumentCompleter -CommandName Get-PSProfileProjectPath -ParameterName Path -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.ProjectPaths | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Get-PSProfileProjectPath'

function Remove-PSProfileProjectPath {
    <#
    .SYNOPSIS
    Removes a Project Path from $PSProfile.ProjectPaths.
 
    .DESCRIPTION
    Removes a Project Path from $PSProfile.ProjectPaths.
 
    .PARAMETER Path
    The path to remove from $PSProfile.ProjectPaths.
 
    .PARAMETER NoRefresh
    If $true, skips refreshing your PSProfile after updating project paths.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Remove-PSProfileProjectPath -Name E:\Git -Save
 
    Removes the path 'E:\Git' from $PSProfile.ProjectPaths then saves the updated configuration.
    #>

    [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")]
    Param (
        [Parameter(Mandatory,Position = 0,ValueFromPipeline)]
        [String]
        $Path,
        [Parameter()]
        [Switch]
        $NoRefresh,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        if ($PSCmdlet.ShouldProcess("Removing '$Path' from `$PSProfile.ProjectPaths")) {
            Write-Verbose "Removing '$Path' from `$PSProfile.ProjectPaths"
            $Global:PSProfile.ProjectPaths = $Global:PSProfile.ProjectPaths | Where-Object {$_ -notin @($Path,(Resolve-Path $Path).Path)}
            if (-not $NoRefresh) {
                Update-PSProfileConfig
            }
            if ($Save) {
                Save-PSProfile
            }
        }
    }
}

Register-ArgumentCompleter -CommandName Remove-PSProfileProjectPath -ParameterName Path -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.ProjectPaths | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Remove-PSProfileProjectPath'

function Add-PSProfilePrompt {
    <#
    .SYNOPSIS
    Saves the Content to $PSProfile.Prompts as the Name provided for recall later.
 
    .DESCRIPTION
    Saves the Content to $PSProfile.Prompts as the Name provided for recall later.
 
    .PARAMETER Name
    The Name to save the prompt as.
 
    .PARAMETER Content
    The prompt content itself.
 
    .PARAMETER SetAsDefault
    If $true, sets the prompt as default by updated $PSProfile.Settings.DefaultPrompt.
 
    .EXAMPLE
    Add-PSProfilePrompt -Name Demo -Content '"PS > "'
 
    Saves a prompt named 'Demo' with the provided content.
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position = 0)]
        [String]
        $Name = $global:PSProfile.Settings.DefaultPrompt,
        [Parameter()]
        [object]
        $Content,
        [Parameter()]
        [switch]
        $SetAsDefault
    )
    Process {
        if ($null -eq $Name) {
            throw "No value set for the Name parameter or resolved from PSProfile!"
        }
        else {
            Write-Verbose "Saving prompt '$Name' to `$PSProfile.Prompts"
            $tempContent = if ($Content) {
                $Content.ToString()
            }
            else {
                Get-PSProfilePrompt -Raw
            }
            $cleanContent = (($tempContent -split "[\r\n]" | Where-Object {$_}) -join "`n").Trim()
            $global:PSProfile.Prompts[$Name] = $cleanContent
            if ($SetAsDefault) {
                $global:PSProfile.Settings.DefaultPrompt = $Name
            }
            Save-PSProfile
        }
    }
}


Export-ModuleMember -Function 'Add-PSProfilePrompt'

function Edit-PSProfilePrompt {
    <#
    .SYNOPSIS
    Enables editing the prompt from the desired editor. Once temporary file is saved, the prompt is updated in $PSProfile.Prompts.
 
    .DESCRIPTION
    Enables editing the prompt from the desired editor. Once temporary file is saved, the prompt is updated in $PSProfile.Prompts.
 
    .PARAMETER Save
    If $true, saves prompt back to your PSProfile after updating.
 
    .EXAMPLE
    Edit-PSProfilePrompt
 
    Opens the current prompt as a temporary file in Visual Studio Code to edit. Once the file is saved and closed, the active prompt is updated with the changes.
    #>

    [CmdletBinding()]
    Param(
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        $in = @{
            StdIn   = Get-PSProfilePrompt -Global
            TmpFile = [System.IO.Path]::Combine(([System.IO.Path]::GetTempPath()),"ps-prompt-$(-join ((97..(97+25)|%{[char]$_}) | Get-Random -Count 3)).ps1")
        }
        $handler = {
            Param(
                [hashtable]
                $in
            )
            try {
                $code = (Get-Command code -All | Where-Object { $_.CommandType -notin @('Function','Alias') })[0].Source
                $in.StdIn | Set-Content $in.TmpFile -Force
                & $code $in.TmpFile --wait
            }
            catch {
                throw
            }
            finally {
                if (Test-Path $in.TmpFile -ErrorAction SilentlyContinue) {
                    Invoke-Expression ([System.IO.File]::ReadAllText($in.TmpFile))
                    Remove-Item $in.TmpFile -Force
                }
            }
        }
        Write-Verbose "Opening prompt in VS Code"
        .$handler($in)
        if ($Save) {
            Add-PSProfilePrompt
        }
    }
}


Export-ModuleMember -Function 'Edit-PSProfilePrompt'

function Get-PSProfilePrompt {
    <#
    .SYNOPSIS
    Gets the current prompt's definition as a string. Useful for inspection of the prompt in use. If PSScriptAnalyzer is installed, formats the prompt for readability before returning the prompt function string.
 
    .DESCRIPTION
    Gets the current prompt's definition as a string. Useful for inspection of the prompt in use. If PSScriptAnalyzer is installed, formats the prompt for readability before returning the prompt function string.
 
    .PARAMETER Name
    The Name of the prompt from $PSProfile.Prompts to get. If excluded, gets the current prompt.
 
    .PARAMETER Global
    If $true, adds the global scope to the returned prompt, e.g. `function global:prompt`
 
    .PARAMETER NoPSSA
    If $true, does not use PowerShell Script Analyzer's Invoke-Formatter to format the resulting prompt definition.
 
    .PARAMETER Raw
    If $true, returns only the prompt definition and does not add the `function prompt {...}` enclosure.
 
    .EXAMPLE
    Get-PSProfilePrompt
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position = 0)]
        [String]
        $Name,
        [Parameter()]
        [Switch]
        $Global,
        [Parameter()]
        [Switch]
        $NoPSSA,
        [Parameter()]
        [Switch]
        $Raw
    )
    Begin {
        $pssa = if ($NoPSSA -or $null -eq (Get-Module PSScriptAnalyzer* -ListAvailable)) {
            $false
        }
        else {
            $true
            Import-Module PSScriptAnalyzer -Verbose:$false
        }
        $pContents = if ($PSBoundParameters.ContainsKey('Name')) {
            $Global:PSProfile.Prompts[$Name]
        }
        else {
            (Get-Command prompt).Definition
        }
    }
    Process {
        Write-Verbose "Getting current prompt"
        $i = 0
        $lws = $null
        $g = if ($Global) {
            'global:prompt'
        }
        else {
            'prompt'
        }
        $header = if ($Raw) {
            ''
        }
        else {
            "function $g {`n"
        }
        $content = $pContents -split "`n" | ForEach-Object {
            if (-not [String]::IsNullOrWhiteSpace($_)) {
                if ($null -eq $lws) {
                    $lws = if ($_ -match '^\s+') {
                        $Matches.Values[0].Length
                    }
                    else {
                        $null
                    }
                }
                $_ -replace "^\s{0,$lws}",' '
                "`n"
            }
            elseif ($i) {
                $_
                "`n"
            }
            $i++
        }
        $footer = if ($Raw) {
            ''
        }
        else {
            "}"
        }
        $p = ((@($header,(($content | Where-Object {"$_".Trim()}) -join "`n"),$footer) -split "[\r\n]") | Where-Object {"$_".Trim()}) -join "`n"
        if (-not $NoPSSA -and $pssa) {
            Write-Verbose "Formatting prompt with Invoke-Formatter"
            Invoke-Formatter $p -Verbose:$false
        }
        else {
            $p
        }
    }
}

Register-ArgumentCompleter -CommandName Get-PSProfilePrompt -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    Get-PSProfileArguments -WordToComplete "Prompts.$wordToComplete" -FinalKeyOnly
}


Export-ModuleMember -Function 'Get-PSProfilePrompt'

function Remove-PSProfilePrompt {
    <#
    .SYNOPSIS
    Removes a Prompt from $PSProfile.Prompts.
 
    .DESCRIPTION
    Removes a Prompt from $PSProfile.Prompts.
 
    .PARAMETER Name
    The name of the prompt to remove from $PSProfile.Prompts.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Remove-PSProfilePrompt -Name Demo -Save
 
    Removes the Prompt named 'Demo' from $PSProfile.Prompts then saves the updated configuration.
    #>

    [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")]
    Param (
        [Parameter(Mandatory,Position = 0)]
        [String]
        $Name,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        if ($PSCmdlet.ShouldProcess("Removing prompt '$Name' from `$PSProfile.Prompts")) {
            Write-Verbose "Removing prompt '$Name' from `$PSProfile.Prompts"
            if ($Global:PSProfile.Prompts.ContainsKey($Name)) {
                $Global:PSProfile.Prompts.Remove($Name)
            }
            if ($Save) {
                Save-PSProfile
            }
        }
    }
}

Register-ArgumentCompleter -CommandName Remove-PSProfilePrompt -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.Prompts.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Remove-PSProfilePrompt'

function Switch-PSProfilePrompt {
    <#
    .SYNOPSIS
    Sets the prompt to the desired prompt by either the Name of the prompt as stored in $PSProfile.Prompts or the provided prompt content.
 
    .DESCRIPTION
    Sets the prompt to the desired prompt by either the Name of the prompt as stored in $PSProfile.Prompts or the provided prompt content.
 
    .PARAMETER Name
    The Name of the prompt to set as active from $PSProfile.Prompts.
 
    .PARAMETER Temporary
    If $true, does not update $PSProfile.Settings.DefaultPrompt with the selected prompt so that prompt selection does not persist after the current session.
 
    .PARAMETER Content
    If Content is provided as either a ScriptBlock or String, sets the current prompt to that. Equivalent to passing `function prompt {$Content}`
 
    .EXAMPLE
    Switch-PSProfilePrompt -Name Demo
 
    Sets the active prompt to the prompt named 'Demo' from $PSProfile.Prompts and saves it as the Default prompt for session persistence.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Name')]
    Param(
        [Parameter(Mandatory,Position = 0,ParameterSetName = 'Name')]
        [String]
        $Name,
        [Parameter(ParameterSetName = 'Name')]
        [switch]
        $Temporary,
        [Parameter(Mandatory,ParameterSetName = 'Content')]
        [object]
        $Content
    )
    Process {
        switch ($PSCmdlet.ParameterSetName) {
            Name {
                if ($global:PSProfile.Prompts.ContainsKey($Name)) {
                    Write-Verbose "Setting active prompt to '$Name'"
                    $function:prompt = $global:PSProfile.Prompts[$Name]
                    if (-not $Temporary) {
                        $global:PSProfile.Settings.DefaultPrompt = $Name
                        Save-PSProfile
                    }
                }
                else {
                    Write-Warning "Falling back to default prompt -- '$Name' not found in Configuration prompts!"
                    $function:prompt = '
                    "PS $($executionContext.SessionState.Path.CurrentLocation)$(''>'' * ($nestedPromptLevel + 1)) ";
                    # .Link
                    # https://go.microsoft.com/fwlink/?LinkID=225750
                    # .ExternalHelp System.Management.Automation.dll-help.xml
                    '

                }
            }
            Content {
                Write-Verbose "Setting active prompt to provided content directly"
                $function:prompt = $Content
            }
        }
    }
}

Register-ArgumentCompleter -CommandName Switch-PSProfilePrompt -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    Get-PSProfileArguments -WordToComplete "Prompts.$wordToComplete" -FinalKeyOnly
}


Export-ModuleMember -Function 'Switch-PSProfilePrompt'

function Add-PSProfileScriptPath {
    <#
    .SYNOPSIS
    Adds a ScriptPath to your PSProfile to invoke during profile load.
 
    .DESCRIPTION
    Adds a ScriptPath to your PSProfile to invoke during profile load.
 
    .PARAMETER Path
    The path of the script to add to your $PSProfile.ScriptPaths.
 
    .PARAMETER Invoke
    If $true, invokes the script path after adding to $PSProfile.ScriptPaths to make it immediately available in the current session.
 
    .PARAMETER Invoke
    If $true, invokes the script at the path specified to load it into the current session.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Add-PSProfileScriptPath -Path ~\MyProfileScript.ps1 -Save
 
    Adds the script 'MyProfileScript.ps1' to $PSProfile.ScriptPaths and saves the configuration after updating.
 
    .EXAMPLE
    Get-ChildItem .\MyProfileScripts -Recurse -File | Add-PSProfileScriptPath -Verbose
 
    Adds all scripts under the MyProfileScripts folder to $PSProfile.ScriptPaths but does not save to allow inspection. Call Save-PSProfile after to save the results if satisfied.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory,Position = 0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [Alias('FullName')]
        [String[]]
        $Path,
        [Parameter()]
        [Switch]
        $Invoke,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        foreach ($p in $Path) {
            if ($p -match '\.ps1$') {
                $fP = (Resolve-Path $p).Path
                if ($Global:PSProfile.ScriptPaths -notcontains $fP) {
                    Write-Verbose "Adding ScriptPath to PSProfile: $fP"
                    $Global:PSProfile.ScriptPaths += $fP
                }
                else {
                    Write-Verbose "ScriptPath already in PSProfile: $fP"
                }
                if ($Invoke) {
                    . $fp
                }
            }
            else {
                Write-Verbose "Skipping non-ps1 file: $fP"
            }
        }
        if ($Save) {
            Save-PSProfile
        }
    }
}


Export-ModuleMember -Function 'Add-PSProfileScriptPath'

function Get-PSProfileScriptPath {
    <#
    .SYNOPSIS
    Gets a script path from $PSProfile.ScriptPaths.
 
    .DESCRIPTION
    Gets a script path from $PSProfile.ScriptPaths.
 
    .PARAMETER Path
    The script path to get from $PSProfile.ScriptPaths.
 
    .EXAMPLE
    Get-PSProfileScriptPath -Path E:\Git\MyProfileScript.ps1
 
    Gets the path 'E:\Git\MyProfileScript.ps1' from $PSProfile.ScriptPaths
 
    .EXAMPLE
    Get-PSProfileScriptPath
 
    Gets the list of script paths from $PSProfile.ScriptPaths
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0,ValueFromPipeline)]
        [String[]]
        $Path
    )
    Process {
        if ($PSBoundParameters.ContainsKey('Path')) {
            Write-Verbose "Getting script path '$Path' from `$PSProfile.ScriptPaths"
            $Global:PSProfile.ScriptPaths | Where-Object {$_ -in $Path}
        }
        else {
            Write-Verbose "Getting all script paths from `$PSProfile.ScriptPaths"
            $Global:PSProfile.ScriptPaths
        }
    }
}

Register-ArgumentCompleter -CommandName Get-PSProfileScriptPath -ParameterName Path -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.ScriptPaths | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Get-PSProfileScriptPath'

function Remove-PSProfileScriptPath {
    <#
    .SYNOPSIS
    Removes a Script Path from $PSProfile.ScriptPaths.
 
    .DESCRIPTION
    Removes a Script Path from $PSProfile.ScriptPaths.
 
    .PARAMETER Path
    The path to remove from $PSProfile.ScriptPaths.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Remove-PSProfileScriptPath -Name ~\Scripts\ProfileLoadScript.ps1 -Save
 
    Removes the path '~\Scripts\ProfileLoadScript.ps1' from $PSProfile.ScriptPaths then saves the updated configuration.
    #>

    [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")]
    Param (
        [Parameter(Mandatory,Position = 0,ValueFromPipeline)]
        [String]
        $Path,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        if ($PSCmdlet.ShouldProcess("Removing '$Path' from `$PSProfile.ScriptPaths")) {
            Write-Verbose "Removing '$Path' from `$PSProfile.ScriptPaths"
            $Global:PSProfile.ScriptPaths = $Global:PSProfile.ScriptPaths | Where-Object {$_ -notin @($Path,(Resolve-Path $Path).Path)}
            if ($Save) {
                Save-PSProfile
            }
        }
    }
}

Register-ArgumentCompleter -CommandName Remove-PSProfileScriptPath -ParameterName Path -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.ScriptPaths | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Remove-PSProfileScriptPath'

function Add-PSProfileSecret {
    <#
    .SYNOPSIS
    Adds a PSCredential object or named SecureString to the PSProfile Vault then saves the current PSProfile.
 
    .DESCRIPTION
    Adds a PSCredential object or named SecureString to the PSProfile Vault then saves the current PSProfile.
 
    .PARAMETER Credential
    The PSCredential to add to the Vault. PSCredentials are recallable by the UserName from the stored PSCredential object via either `Get-MyCreds` or `Get-PSProfileSecret -UserName $UserName`.
 
    .PARAMETER Name
    For SecureString secrets, the friendly name to store them as for easy recall later via `Get-PSProfileSecret`.
 
    .PARAMETER SecureString
    The SecureString to store as the provided Name for recall later.
 
    .PARAMETER Force
    If $true and the PSCredential's UserName or SecureString's Name already exists, it overwrites it. Defaults to $false to prevent accidentally overwriting existing secrets in the $PSProfile.Vault.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Add-PSProfileSecret (Get-Credential) -Save
 
    Opens a Get-Credential window or prompt to enable entering credentials securely, then stores it in the Vault and saves your PSProfile configuration after updating.
 
    .EXAMPLE
    Add-PSProfileSecret -Name HomeApiKey -Value (ConvertTo-SecureString 1234567890xxx -AsPlainText -Force) -Save
 
    Stores the secret value '1234567890xxx' as the name 'HomeApiKey' in $PSProfile.Vault and saves your PSProfile configuration after updating.
    #>

    [CmdletBinding(DefaultParameterSetName = "PSCredential")]
    Param (
        [Parameter(Mandatory,ValueFromPipeline,Position = 0,ParameterSetName = "PSCredential")]
        [pscredential]
        $Credential,
        [Parameter(Mandatory,ParameterSetName = "SecureString")]
        [string]
        $Name,
        [Parameter(Mandatory,ParameterSetName = "SecureString")]
        [securestring]
        $SecureString,
        [Parameter()]
        [Switch]
        $Force,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        switch ($PSCmdlet.ParameterSetName) {
            PSCredential {
                if ($Force -or $null -eq $Global:PSProfile.Vault.GetSecret($Credential.UserName)) {
                    Write-Verbose "Adding PSCredential for user '$($Credential.UserName)' to `$PSProfile.Vault"
                    $Global:PSProfile.Vault.SetSecret($Credential)
                }
                elseif (-not $Force -and $null -ne $Global:PSProfile.Vault.GetSecret($Credential.UserName)) {
                    Write-Error "A secret with the name '$($Credential.UserName)' already exists! Include -Force to overwrite it."
                }
            }
            SecureString {
                if ($Force -or $null -eq $Global:PSProfile.Vault.GetSecret($Name)) {
                    Write-Verbose "Adding SecureString secret with name '$Name' to `$PSProfile.Vault"
                    $Global:PSProfile.Vault.SetSecret($Name,$SecureString)
                }
                elseif (-not $Force -and $null -ne $Global:PSProfile.Vault.GetSecret($Name)) {
                    Write-Error "A secret with the name '$Name' already exists! Include -Force to overwrite it."
                }
            }
        }
        if ($Save) {
            Save-PSProfile
        }
    }
}


Export-ModuleMember -Function 'Add-PSProfileSecret'

function Get-MyCreds {
    <#
    .SYNOPSIS
    Gets a credential object from the PSProfile Vault. Defaults to getting your current user's PSCredentials if stored in the Vault.
 
    .DESCRIPTION
    Gets a credential object from the PSProfile Vault. Defaults to getting your current user's PSCredentials if stored in the Vault.
 
    .PARAMETER Item
    The name of the Secret you would like to retrieve from the Vault.
 
    .PARAMETER IncludeDomain
    If $true, prepends the domain found in $env:USERDOMAIN to the Username on the PSCredential object before returning it. If not currently in a domain, prepends the MachineName instead.
 
    .EXAMPLE
    Get-MyCreds
 
    Gets the current user's PSCredentials from the Vault.
 
    .EXAMPLE
    Invoke-Command -ComputerName Server01 -Credential (Creds)
 
    Passes your current user credentials via the `Creds` alias to the Credential parameter of Invoke-Command to make a call against Server01 using your PSCredential
 
    .EXAMPLE
    Invoke-Command -ComputerName Server01 -Credential (Get-MyCreds SvcAcct07)
 
    Passes the credentials for account SvcAcct07 to the Credential parameter of Invoke-Command to make a call against Server01 using a different PSCredential than your own.
    #>

    [OutputType('PSCredential')]
    [CmdletBinding()]
    Param(
        [parameter(Mandatory = $false,Position = 0)]
        [String]
        $Item = $(if ($env:USERNAME) {
                $env:USERNAME
            }
            elseif ($env:USER) {
                $env:USER
            }),
        [parameter(Mandatory = $false)]
        [Alias('d','Domain')]
        [Switch]
        $IncludeDomain
    )
    Process {
        if ($Item) {
            Write-Verbose "Checking Credential Vault for user '$Item'"
            if ($creds = $global:PSProfile.Vault.GetSecret($Item)) {
                Write-Verbose "Found item in CredStore"
                if (!$env:USERDOMAIN) {
                    $env:USERDOMAIN = [System.Environment]::MachineName
                }
                if ($IncludeDomain -and $creds.UserName -notlike "$($env:USERDOMAIN)\*") {
                    $creds = New-Object PSCredential "$($env:USERDOMAIN)\$($creds.UserName)",$creds.Password
                }
                return $creds
            }
            else {
                $PSCmdlet.ThrowTerminatingError(
                    [System.Management.Automation.ErrorRecord]::new(
                        ([System.Management.Automation.ItemNotFoundException]"Could not find secret item '$Item' in the PSProfileVault"),
                        'PSProfile.Vault.SecretNotFound',
                        [System.Management.Automation.ErrorCategory]::InvalidArgument,
                        $global:PSProfile
                    )
                )
            }
        }
        else {
            $global:PSProfile.Vault._secrets
        }
    }
}

Register-ArgumentCompleter -CommandName Get-MyCreds -ParameterName Item -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.Vault._secrets.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Get-MyCreds'

function Get-PSProfileSecret {
    <#
    .SYNOPSIS
    Gets a Secret from the $PSProfile.Vault.
 
    .DESCRIPTION
    Gets a Secret from the $PSProfile.Vault.
 
    .PARAMETER Name
    The name of the Secret you would like to retrieve from the Vault.
 
    .EXAMPLE
    Get-PSProfileSecret -Name MyApiKey
 
    Gets the Secret named 'MyApiKey' from the $PSProfile.Vault.
    #>

    [CmdletBinding()]
    Param(
        [parameter(Mandatory,Position = 0)]
        [String]
        $Name
    )
    Process {
        Write-Verbose "Getting Secret '$Name' from `$PSProfile.Vault"
        $global:PSProfile.Vault.GetSecret($Name)
    }
}

Register-ArgumentCompleter -CommandName Get-PSProfileSecret -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.Vault._secrets.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Get-PSProfileSecret'

function Remove-PSProfileSecret {
    <#
    .SYNOPSIS
    Removes a Secret from $PSProfile.Vault.
 
    .DESCRIPTION
    Removes a Secret from $PSProfile.Vault.
 
    .PARAMETER Name
    The Secret's Name or UserName to remove from the Vault.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Remove-PSProfileSecret -Name $env:USERNAME -Save
 
    Removes the current user's stored credentials from the $PSProfile.Vault, then saves the configuration after updating.
    #>

    [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")]
    Param (
        [Parameter(Mandatory,Position = 0)]
        [Alias('UserName')]
        [String]
        $Name,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        if ($PSCmdlet.ShouldProcess("Removing '$Name' from `$PSProfile.Vault")) {
            if ($Global:PSProfile.Vault._secrets.ContainsKey($Name)) {
                Write-Verbose "Removing '$Name' from `$PSProfile.Vault"
                $Global:PSProfile.Vault.RemoveSecret($Name)
            }
            if ($Save) {
                Save-PSProfile
            }
        }
    }
}

Register-ArgumentCompleter -CommandName Remove-PSProfileSecret -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.Vault._secrets.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Remove-PSProfileSecret'

function Add-PSProfileSymbolicLink {
    <#
    .SYNOPSIS
    Adds a SymbolicLink to set if missing during profile load via background task.
 
    .DESCRIPTION
    Adds a SymbolicLink to set if missing during profile load via background task.
 
    .PARAMETER LinkPath
    The path of the symbolic link to create if missing.
 
    .PARAMETER ActualPath
    The actual target path of the symbolic link to set.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Add-PSProfileSymbolicLink -LinkPath C:\workstation -ActualPath E:\Git\workstation -Save
 
    Adds a symbolic link at path 'C:\workstation' targeting the actual path 'E:\Git\workstation' and saves your PSProfile configuration.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory,Position = 0)]
        [Alias('Path','Name')]
        [String]
        $LinkPath,
        [Parameter(Mandatory,Position = 1)]
        [Alias('Target','Value')]
        [String]
        $ActualPath,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        Write-Verbose "Adding SymbolicLink '$LinkPath' pointing at ActualPath '$ActualPath'"
        $Global:PSProfile.SymbolicLinks[$LinkPath] = $ActualPath
        if ($Save) {
            Save-PSProfile
        }
    }
}


Export-ModuleMember -Function 'Add-PSProfileSymbolicLink'

function Get-PSProfileSymbolicLink {
    <#
    .SYNOPSIS
    Gets a module from $PSProfile.SymbolicLinks.
 
    .DESCRIPTION
    Gets a module from $PSProfile.SymbolicLinks.
 
    .PARAMETER LinkPath
    The LinkPath to get from $PSProfile.SymbolicLinks.
 
    .EXAMPLE
    Get-PSProfileSymbolicLink -LinkPath C:\workstation
 
    Gets the LinkPath 'C:\workstation' from $PSProfile.SymbolicLinks
 
    .EXAMPLE
    Get-PSProfileSymbolicLink
 
    Gets the list of LinkPaths from $PSProfile.SymbolicLinks
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0,ValueFromPipeline)]
        [String[]]
        $LinkPath
    )
    Process {
        if ($PSBoundParameters.ContainsKey('LinkPath')) {
            Write-Verbose "Getting Path LinkPath '$LinkPath' from `$PSProfile.SymbolicLinks"
            $Global:PSProfile.SymbolicLinks.GetEnumerator() | Where-Object {$_.Key -in $LinkPath}
        }
        else {
            Write-Verbose "Getting all command aliases from `$PSProfile.SymbolicLinks"
            $Global:PSProfile.SymbolicLinks
        }
    }
}

Register-ArgumentCompleter -CommandName Get-PSProfileSymbolicLink -ParameterName LinkPath -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.SymbolicLinks.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Get-PSProfileSymbolicLink'

function Remove-PSProfileSymbolicLink {
    <#
    .SYNOPSIS
    Removes a Symbolic Link from $PSProfile.SymbolicLinks.
 
    .DESCRIPTION
    Removes a PSProfile Plugin from $PSProfile.SymbolicLinks.
 
    .PARAMETER LinkPath
    The path of the symbolic link to remove from $PSProfile.SymbolicLinks.
 
    .PARAMETER Force
    If $true, also removes the SymbolicLink itself from the OS if it exists.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Remove-PSProfileSymbolicLink -LinkPath 'C:\workstation' -Force -Save
 
    Removes the SymbolicLink 'C:\workstation' from $PSProfile.SymbolicLinks, removes the then saves the updated configuration.
    #>

    [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")]
    Param (
        [Parameter(Mandatory,Position = 0)]
        [String]
        $LinkPath,
        [Parameter()]
        [Switch]
        $Force,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        if ($PSCmdlet.ShouldProcess("Removing '$LinkPath' from `$PSProfile.SymbolicLinks")) {
            Write-Verbose "Removing '$LinkPath' from `$PSProfile.SymbolicLinks"
            @($LinkPath,(Resolve-Path $LinkPath).Path) | Select-Object -Unique | ForEach-Object {
                if ($Global:PSProfile.SymbolicLinks.ContainsKey($_)) {
                    $Global:PSProfile.SymbolicLinks.Remove($_)
                }
            }
            if ($Force -and (Test-Path $LinkPath)) {
                Write-Verbose "Removing SymbolicLink: $LinkPath"
                Remove-Item $LinkPath -Force
            }
            if ($Save) {
                Save-PSProfile
            }
        }
    }
}

Register-ArgumentCompleter -CommandName Remove-PSProfileSymbolicLink -ParameterName LinkPath -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.SymbolicLinks.Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Remove-PSProfileSymbolicLink'

function Add-PSProfileVariable {
    <#
    .SYNOPSIS
    Adds a global or environment variable to your PSProfile configuration. Variables added to PSProfile will be set during profile load.
 
    .DESCRIPTION
    Adds a global or environment variable to your PSProfile configuration. Variables added to PSProfile will be set during profile load.
 
    .PARAMETER Name
    The name of the variable.
 
    .PARAMETER Value
    The value to set the variable to.
 
    .PARAMETER Scope
    The scope of the variable to set between Environment or Global. Defaults to Environment.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Add-PSProfileVariable -Name HomeBase -Value C:\HomeBase -Save
 
    Adds the environment variable named 'HomeBase' to be set to the path 'C:\HomeBase' during profile load and saves your PSProfile configuration.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory,Position = 0)]
        [String]
        $Name,
        [Parameter(Mandatory,Position = 1)]
        [Object]
        $Value,
        [Parameter(Position = 2)]
        [ValidateSet('Environment','Global')]
        [String]
        $Scope = 'Environment',
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        if (-not ($Global:PSProfile.Variables.ContainsKey($Scope))) {
            $Global:PSProfile.Variables[$Scope] = @{}
        }
        Write-Verbose "Adding $Scope variable '$Name' to PSProfile"
        $Global:PSProfile.Variables[$Scope][$Name] = $Value
        if ($Save) {
            Save-PSProfile
        }
    }
}


Export-ModuleMember -Function 'Add-PSProfileVariable'

function Get-PSProfileVariable {
    <#
    .SYNOPSIS
    Gets a global or environment variable from your PSProfile configuration.
 
    .DESCRIPTION
    Gets a global or environment variable from your PSProfile configuration.
 
    .PARAMETER Scope
    The scope of the variable to get the variable from between Environment or Global.
 
    .PARAMETER Name
    The name of the variable to get.
 
    .EXAMPLE
    Get-PSProfileVariable -Name HomeBase
 
    Gets the environment variable named 'HomeBase' and its value from $PSProfile.Variables.
 
    .EXAMPLE
    Get-PSProfileVariable
 
    Gets the list of environment variables from $PSProfile.Variables.
 
    .EXAMPLE
    Get-PSProfileVariable -Scope Global
 
    Gets the list of Global variables from $PSProfile.Variables.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory, Position = 0)]
        [ValidateSet('Environment','Global')]
        [String]
        $Scope,
        [Parameter(Position = 1)]
        [String]
        $Name
    )
    Process {
        if ($Global:PSProfile.Variables.ContainsKey($Scope)) {
            if ($PSBoundParameters.ContainsKey('Name')) {
                Write-Verbose "Getting $Scope variable '$Name' from PSProfile"
                $Global:PSProfile.Variables[$Scope].GetEnumerator() | Where-Object {$_.Key -in $Name}
            }
            else {
                Write-Verbose "Getting $Scope variable list from PSProfile"
                $Global:PSProfile.Variables[$Scope]
            }
        }
    }
}

Register-ArgumentCompleter -CommandName Get-PSProfileVariable -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.Variables[$fakeBoundParameter.Scope].Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Get-PSProfileVariable'

function Remove-PSProfileVariable {
    <#
    .SYNOPSIS
    Removes a Variable from $PSProfile.Variables.
 
    .DESCRIPTION
    Removes a Variable from $PSProfile.Variables.
 
    .PARAMETER Name
    The name of the Variable to remove from $PSProfile.Variables.
 
    .PARAMETER Scope
    The scope of the Variable to remove between Environment or Global.
 
    .PARAMETER Save
    If $true, saves the updated PSProfile after updating.
 
    .EXAMPLE
    Remove-PSProfileVariable -Scope Environment -Name '~' -Save
 
    Removes the Environment variable '~' from $PSProfile.Variables then saves the updated configuration.
    #>

    [CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")]
    Param (
        [Parameter(Mandatory, Position = 0)]
        [ValidateSet('Environment','Global')]
        [String]
        $Scope,
        [Parameter(Mandatory,Position = 1)]
        [String]
        $Name,
        [Parameter()]
        [Switch]
        $Save
    )
    Process {
        if ($PSCmdlet.ShouldProcess("Removing $Scope variable '$Name' from `$PSProfile.Variables")) {
            Write-Verbose "Removing $Scope variable '$Name' from `$PSProfile.Variables"
            if ($Global:PSProfile.Variables[$Scope].ContainsKey($Name)) {
                $Global:PSProfile.Variables[$Scope].Remove($Name)
            }
            if ($Save) {
                Save-PSProfile
            }
        }
    }
}

Register-ArgumentCompleter -CommandName Remove-PSProfileVariable -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    $Global:PSProfile.Variables[$fakeBoundParameter.Scope].Keys | Where-Object {$_ -like "$wordToComplete*"} | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}


Export-ModuleMember -Function 'Remove-PSProfileVariable'

New-Alias -Name 'Set-Prompt' -Value 'Switch-PSProfilePrompt' -Scope Global -Force
Export-ModuleMember -Alias 'Set-Prompt'
New-Alias -Name 'Refresh-PSProfile' -Value 'Update-PSProfileConfig' -Scope Global -Force
Export-ModuleMember -Alias 'Refresh-PSProfile'
New-Alias -Name 'Creds' -Value 'Get-MyCreds' -Scope Global -Force
Export-ModuleMember -Alias 'Creds'
New-Alias -Name 'Switch-Prompt' -Value 'Switch-PSProfilePrompt' -Scope Global -Force
Export-ModuleMember -Alias 'Switch-Prompt'
New-Alias -Name 'Edit-Prompt' -Value 'Edit-PSProfilePrompt' -Scope Global -Force
Export-ModuleMember -Alias 'Edit-Prompt'
New-Alias -Name 'Save-Prompt' -Value 'Add-PSProfilePrompt' -Scope Global -Force
Export-ModuleMember -Alias 'Save-Prompt'
New-Alias -Name 'Get-Prompt' -Value 'Get-PSProfilePrompt' -Scope Global -Force
Export-ModuleMember -Alias 'Get-Prompt'
New-Alias -Name 'Remove-Prompt' -Value 'Remove-PSProfilePrompt' -Scope Global -Force
Export-ModuleMember -Alias 'Remove-Prompt'
New-Alias -Name 'Load-PSProfile' -Value 'Import-PSProfile' -Scope Global -Force
Export-ModuleMember -Alias 'Load-PSProfile'
# If we're in an interactive shell, load the profile.
if ([Environment]::UserInteractive -or ($null -eq [Environment]::UserInteractive -and $null -eq ([Environment]::GetCommandLineArgs() | Where-Object {$_ -like '-NonI*'}))) {
    $global:PSProfile = [PSProfile]::new()
    $global:PSProfile.Load()
    Export-ModuleMember -Variable PSProfile
}