Public/Add-ColorScriptProfile.ps1

function Add-ColorScriptProfile {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function already implements explicit ShouldProcess semantics.')]
    [CmdletBinding(SupportsShouldProcess = $true, HelpUri = 'https://nick2bad4u.github.io/PS-Color-Scripts-Enhanced/docs/help-redirect.html?cmdlet=Add-ColorScriptProfile')]
    param(
        [Alias('help')]
        [switch]$h,

        [Alias('Path')]
        [ValidateScript({ Test-ColorScriptPathValue $_ })]
        [string]$ProfilePath,

        [ValidateScript({ Test-ColorScriptNameValue $_ -AllowEmpty })]
        [string]$DefaultStartupScript,

        [switch]$AutoShow,

        [switch]$SkipStartupScript,

        [switch]$IncludePokemon,

        [switch]$SkipPokemonPrompt,

        [ValidateSet('Y', 'N', 'Yes', 'No')]
        [string]$PokemonPromptResponse,

        [switch]$SkipCacheBuild,

        [switch]$Force
    )

    if ($h) {
        Show-ColorScriptHelp -CommandName 'Add-ColorScriptProfile'
        return
    }

    $remoteInfo = $null
    try {
        $remoteInfo = Get-Variable -Name PSSenderInfo -Scope Global -ValueOnly -ErrorAction Stop
    }
    catch {
        $remoteInfo = $null
    }

    if ($remoteInfo) {
        return [pscustomobject]@{
            Path    = $null
            Changed = $false
            Message = $script:Messages.ProfileUpdatesNotSupportedInRemote
        }
    }

    $profileScope = 'CurrentUserAllHosts'
    $profileSpec = $ProfilePath

    if (-not $profileSpec) {
        $profileValue = $PROFILE

        if ($profileValue -is [System.Management.Automation.PSObject]) {
            if ($profileValue.PSObject.Properties['CurrentUserAllHosts'] -and -not [string]::IsNullOrWhiteSpace([string]$profileValue.CurrentUserAllHosts)) {
                $profileSpec = [string]$profileValue.CurrentUserAllHosts
            }
            else {
                $firstDefinedProfile = $profileValue.PSObject.Properties | Where-Object { $_.Value } | Select-Object -First 1
                if ($firstDefinedProfile) {
                    $profileSpec = [string]$firstDefinedProfile.Value
                    $profileScope = [string]$firstDefinedProfile.Name
                }
            }
        }
        elseif (-not [string]::IsNullOrWhiteSpace([string]$profileValue)) {
            $profileSpec = [string]$profileValue
        }
    }

    if ([string]::IsNullOrWhiteSpace($profileSpec)) {
        Invoke-ColorScriptError -Message ($script:Messages.ProfilePathNotDefinedForScope -f $profileScope) -ErrorId 'ColorScriptsEnhanced.ProfilePathUndefined' -Category ([System.Management.Automation.ErrorCategory]::InvalidArgument) -Cmdlet $PSCmdlet
    }

    $profilePathResolved = $null
    try {
        $profilePathResolved = Resolve-CachePath -Path $profileSpec
    }
    catch {
        $profilePathResolved = $null
    }

    if ($profilePathResolved) {
        $profileSpec = $profilePathResolved
    }
    else {
        if (-not $script:IsWindows -and $profileSpec -match '^[A-Za-z]:') {
            Invoke-ColorScriptError -Message ($script:Messages.UnableToResolveProfilePath -f $profileSpec) -ErrorId 'ColorScriptsEnhanced.InvalidProfilePath' -Category ([System.Management.Automation.ErrorCategory]::InvalidArgument) -Cmdlet $PSCmdlet
        }

        if ([System.IO.Path]::IsPathRooted($profileSpec)) {
            Invoke-ColorScriptError -Message ($script:Messages.UnableToResolveProfilePath -f $profileSpec) -ErrorId 'ColorScriptsEnhanced.InvalidProfilePath' -Category ([System.Management.Automation.ErrorCategory]::InvalidArgument) -Cmdlet $PSCmdlet
        }

        try {
            $basePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath('.')
        }
        catch {
            $basePath = $null
        }

        if (-not $basePath) {
            try {
                $basePath = (Get-Location -PSProvider FileSystem).Path
            }
            catch {
                $basePath = $null
            }
        }

        try {
            $profileSpec = [System.IO.Path]::GetFullPath((Join-Path -Path $basePath -ChildPath $profileSpec))
        }
        catch {
            Invoke-ColorScriptError -Message ($script:Messages.UnableToResolveProfilePath -f $profileSpec) -ErrorId 'ColorScriptsEnhanced.InvalidProfilePath' -Category ([System.Management.Automation.ErrorCategory]::InvalidArgument) -Exception $_.Exception -Cmdlet $PSCmdlet
        }
    }

    $profileDirectory = [System.IO.Path]::GetDirectoryName($profileSpec)
    if (-not [string]::IsNullOrWhiteSpace($profileDirectory) -and -not (Test-Path -LiteralPath $profileDirectory)) {
        New-Item -ItemType Directory -Path $profileDirectory -Force | Out-Null
    }

    $existingContent = ''
    if (Test-Path -LiteralPath $profileSpec) {
        $existingContent = Get-Content -LiteralPath $profileSpec -Raw
    }

    $newline = if ($existingContent -match "`r`n") {
        "`r`n"
    }
    elseif ($existingContent -match "`n") {
        "`n"
    }
    else {
        [Environment]::NewLine
    }

    $timestamp = (Get-Date).ToString('u')

    $configuration = $null
    try {
        $configuration = Get-ColorScriptConfiguration
    }
    catch {
        Write-Verbose ("Get-ColorScriptConfiguration failed: {0}" -f $_.Exception.Message)
        $configuration = $null
    }

    $startupConfig = $null
    if ($configuration -and $configuration.Startup) {
        $startupConfig = $configuration.Startup
    }
    elseif ($script:ConfigurationData -and $script:ConfigurationData.Startup) {
        $startupConfig = $script:ConfigurationData.Startup
    }
    else {
        $startupConfig = $script:DefaultConfiguration.Startup
    }

    $profileAutoShow = $false
    $defaultScriptName = $null

    if ($startupConfig -is [System.Collections.IDictionary]) {
        if ($startupConfig.Contains('ProfileAutoShow')) {
            $profileAutoShow = [bool]$startupConfig['ProfileAutoShow']
        }

        if ($startupConfig.Contains('DefaultScript')) {
            $defaultScriptName = [string]$startupConfig['DefaultScript']
        }
    }
    else {
        if ($startupConfig -and ($startupConfig.PSObject.Properties.Name -contains 'ProfileAutoShow')) {
            $profileAutoShow = [bool]$startupConfig.ProfileAutoShow
        }

        if ($startupConfig -and ($startupConfig.PSObject.Properties.Name -contains 'DefaultScript')) {
            $defaultScriptName = [string]$startupConfig.DefaultScript
        }
    }

    if ($PSBoundParameters.ContainsKey('AutoShow')) {
        $profileAutoShow = [bool]$AutoShow
    }

    if ($SkipStartupScript) {
        $profileAutoShow = $false
    }

    if ($PSBoundParameters.ContainsKey('DefaultStartupScript')) {
        $defaultScriptName = $DefaultStartupScript
        if (-not $SkipStartupScript) {
            $profileAutoShow = $true
        }
    }

    $explicitPokemonResponse = $null
    if ($PSBoundParameters.ContainsKey('PokemonPromptResponse') -and $PokemonPromptResponse) {
        switch ($PokemonPromptResponse.ToLowerInvariant()) {
            'y' { $explicitPokemonResponse = $true }
            'yes' { $explicitPokemonResponse = $true }
            'n' { $explicitPokemonResponse = $false }
            'no' { $explicitPokemonResponse = $false }
        }
    }

    if ($null -eq $explicitPokemonResponse) {
        $envPromptResponse = [Environment]::GetEnvironmentVariable('COLOR_SCRIPTS_ENHANCED_POKEMON_PROMPT_RESPONSE')
        if (-not [string]::IsNullOrWhiteSpace($envPromptResponse)) {
            switch ($envPromptResponse.ToLowerInvariant()) {
                'y' { $explicitPokemonResponse = $true }
                'yes' { $explicitPokemonResponse = $true }
                'n' { $explicitPokemonResponse = $false }
                'no' { $explicitPokemonResponse = $false }
            }
        }
    }

    if ($null -eq $explicitPokemonResponse) {
        try {
            $globalPrompt = Get-Variable -Name ColorScriptsEnhancedPokemonPromptResponse -Scope Global -ValueOnly -ErrorAction Stop
            if (-not [string]::IsNullOrWhiteSpace([string]$globalPrompt)) {
                switch ([string]$globalPrompt.ToLowerInvariant()) {
                    'y' { $explicitPokemonResponse = $true }
                    'yes' { $explicitPokemonResponse = $true }
                    'n' { $explicitPokemonResponse = $false }
                    'no' { $explicitPokemonResponse = $false }
                }
            }
        }
        catch {
            Write-Verbose 'Global Pokemon prompt response override not defined.'
        }
    }

    if ($null -eq $explicitPokemonResponse -and $PSBoundParameters.ContainsKey('Confirm') -and ($PSBoundParameters['Confirm'] -eq $false)) {
        $explicitPokemonResponse = $false
    }

    $includePokemonChoice = if ($PSBoundParameters.ContainsKey('IncludePokemon')) {
        $IncludePokemon.IsPresent
    }
    elseif ($null -ne $explicitPokemonResponse) {
        $explicitPokemonResponse
    }
    else {
        $false
    }

    # If the user explicitly passed -IncludePokemon, force opt-in regardless of config defaults.
    if ($IncludePokemon.IsPresent) {
        $includePokemonChoice = $true
    }

    $promptedForPokemon = $false
    $cacheBuilt = $false

    if ($profileAutoShow) {
        $hasExplicitProfilePath = (
            $PSBoundParameters.ContainsKey('ProfilePath') -or
            $PSBoundParameters.ContainsKey('Path') -or
            (-not [string]::IsNullOrWhiteSpace($ProfilePath))
        )

        $shouldPromptForPokemon = (
            -not $SkipPokemonPrompt.IsPresent -and
            -not $PSBoundParameters.ContainsKey('IncludePokemon') -and
            ($null -eq $explicitPokemonResponse) -and
            -not $hasExplicitProfilePath
        )

        if ($shouldPromptForPokemon) {
            $shouldPromptForPokemon = $true
            try {
                $shouldPromptForPokemon = -not [Console]::IsInputRedirected
            }
            catch {
                $shouldPromptForPokemon = $false
            }

            if ($shouldPromptForPokemon) {
                $prompt = 'Include Pokémon colorscripts on startup? (y/N)'
                $response = Read-Host -Prompt $prompt
                $promptedForPokemon = $true
                if ($response -match '^(?i)y(?:es)?$') {
                    $includePokemonChoice = $true
                }
                else {
                    $includePokemonChoice = $false
                }
            }
        }
        elseif ($null -ne $explicitPokemonResponse -and -not $IncludePokemon.IsPresent) {
            $includePokemonChoice = $explicitPokemonResponse
        }
    }
    else {
        $includePokemonChoice = $false
    }

    $skipCacheBuildRequested = $SkipCacheBuild.IsPresent
    if (-not $skipCacheBuildRequested) {
        $envSkipCache = [Environment]::GetEnvironmentVariable('COLOR_SCRIPTS_ENHANCED_SKIP_CACHE_BUILD')
        if (-not [string]::IsNullOrWhiteSpace($envSkipCache)) {
            switch ($envSkipCache.ToLowerInvariant()) {
                { $_ -in @('1', 'true', 'yes', 'y') } { $skipCacheBuildRequested = $true }
            }
        }
    }

    if (-not $skipCacheBuildRequested) {
        try {
            $globalSkipCache = Get-Variable -Name ColorScriptsEnhancedSkipCacheBuild -Scope Global -ValueOnly -ErrorAction Stop
            if ($globalSkipCache) {
                $skipCacheBuildRequested = $true
            }
        }
        catch {
            Write-Verbose 'Global cache skip override not defined.'
        }
    }

    if (-not $skipCacheBuildRequested) {
        $profileFullPath = $null
        try {
            $providerPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($profileSpec)
        }
        catch {
            $providerPath = $profileSpec
        }

        try {
            $profileFullPath = [System.IO.Path]::GetFullPath($providerPath)
        }
        catch {
            $profileFullPath = $providerPath
        }

        try {
            $tempRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath())
        }
        catch {
            $tempRoot = [System.IO.Path]::GetTempPath()
        }

        if ($profileFullPath -and $tempRoot) {
            $isUnderTemp = $false
            try {
                $relative = [System.IO.Path]::GetRelativePath($tempRoot, $profileFullPath)
                if (-not [string]::IsNullOrWhiteSpace($relative) -and -not $relative.StartsWith('..', [System.StringComparison]::Ordinal)) {
                    $isUnderTemp = $true
                }
            }
            catch {
                if ($profileFullPath.StartsWith($tempRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
                    $isUnderTemp = $true
                }
            }

            if ($isUnderTemp) {
                $skipCacheBuildRequested = $true
            }
        }
    }

    if (-not $profileAutoShow -and $IncludePokemon) {
        Write-Verbose 'IncludePokemon was specified but AutoShow is disabled. The switch has no effect.'
    }

    $snippetLines = [System.Collections.Generic.List[string]]::new()
    [void]$snippetLines.Add("# Added by ColorScripts-Enhanced on $timestamp")
    [void]$snippetLines.Add('Import-Module ColorScripts-Enhanced')

    if ($profileAutoShow) {
        if (-not [string]::IsNullOrWhiteSpace($defaultScriptName)) {
            $safeName = $defaultScriptName -replace "'", "''"
            $showCommand = "Show-ColorScript -Name '$safeName'"
        }
        else {
            $showCommand = 'Show-ColorScript'
        }

        if ($includePokemonChoice) {
            $showCommand += ' -IncludePokemon'
        }

        [void]$snippetLines.Add('try {')
        [void]$snippetLines.Add(" $showCommand")
        [void]$snippetLines.Add('}')
        [void]$snippetLines.Add('catch {')
        [void]$snippetLines.Add(' Write-Warning "ColorScripts-Enhanced startup snippet failed: $($_.Exception.Message)"')
        [void]$snippetLines.Add('}')
    }

    $snippet = ($snippetLines.ToArray() -join $newline)
    $updatedContent = $existingContent

    $snippetPattern = '(?ms)^# Added by ColorScripts-Enhanced.*?(?:\r?\n){2}'
    if ($updatedContent -match $snippetPattern) {
        if (-not $Force) {
            Write-Verbose $script:Messages.ProfileAlreadyContainsSnippet
            return [pscustomobject]@{
                Path           = $profileSpec
                Changed        = $false
                Message        = $script:Messages.ProfileAlreadyConfigured
                IncludePokemon = $includePokemonChoice
                CacheBuilt     = $false
            }
        }

        $updatedContent = [System.Text.RegularExpressions.Regex]::Replace($updatedContent, $snippetPattern, '', 'MultiLine')
    }

    $importPattern = '(?mi)^\s*Import-Module\s+ColorScripts-Enhanced\b.*$'

    if (-not $Force -and $existingContent -match $importPattern) {
        Write-Verbose $script:Messages.ProfileAlreadyImportsModule
        return [pscustomobject]@{
            Path           = $profileSpec
            Changed        = $false
            Message        = $script:Messages.ProfileAlreadyConfigured
            IncludePokemon = $includePokemonChoice
            CacheBuilt     = $false
        }
    }

    if ($Force) {
        $updatedContent = [System.Text.RegularExpressions.Regex]::Replace($updatedContent, $importPattern + '(?:\r?\n)?', '', 'Multiline')
        $showPattern = '(?mi)^\s*(Show-ColorScript|scs)\b.*(?:\r?\n)?'
        $updatedContent = [System.Text.RegularExpressions.Regex]::Replace($updatedContent, $showPattern, '', 'Multiline')
    }

    if ($PSCmdlet.ShouldProcess($profileSpec, 'Add ColorScripts-Enhanced profile snippet')) {
        $trimmedExisting = $updatedContent.TrimEnd()
        if ($trimmedExisting) {
            $updatedContent = $trimmedExisting + $newline + $newline + $snippet
        }
        else {
            $updatedContent = $snippet
        }

        try {
            Invoke-FileWriteAllText -Path $profileSpec -Content ($updatedContent + $newline) -Encoding $script:Utf8NoBomEncoding
        }
        catch {
            $errorTemplate = if ($script:Messages -and $script:Messages.ContainsKey('ProfileSnippetWriteFailed')) {
                $script:Messages.ProfileSnippetWriteFailed
            }
            else {
                "Unable to write ColorScripts-Enhanced profile snippet to '{0}': {1}"
            }

            $errorMessage = $errorTemplate -f $profileSpec, $_.Exception.Message
            Invoke-ColorScriptError -Message $errorMessage -ErrorId 'ColorScriptsEnhanced.ProfileWriteFailed' -Category ([System.Management.Automation.ErrorCategory]::WriteError) -TargetObject $profileSpec -Exception $_.Exception -Cmdlet $PSCmdlet
        }

        $infoTemplate = if ($script:Messages -and $script:Messages.ContainsKey('ProfileSnippetAdded')) {
            $script:Messages.ProfileSnippetAdded
        }
        else {
            '[OK] Added ColorScripts-Enhanced startup snippet to {0}'
        }

        $infoMessage = $infoTemplate -f $profileSpec
        Write-ColorScriptInformation -Message $infoMessage -PreferConsole -Color 'Green'

        if ($profileAutoShow -and -not $skipCacheBuildRequested) {
            if ($PSCmdlet.ShouldProcess('ColorScripts cache', 'Build Colorscript cache for startup snippet')) {
                try {
                    $cacheParams = @{ }
                    if ($includePokemonChoice) {
                        $cacheParams.IncludePokemon = $true
                    }

                    ColorScripts-Enhanced\New-ColorScriptCache -All @cacheParams | Out-Null
                    $cacheBuilt = $true
                }
                catch {
                    Write-Verbose ("New-ColorScriptCache warm-up failed: {0}" -f $_.Exception.Message)
                }
            }
        }

        return [pscustomobject]@{
            Path           = $profileSpec
            Changed        = $true
            Message        = $script:Messages.ProfileSnippetAddedMessage
            IncludePokemon = $includePokemonChoice
            CacheBuilt     = $cacheBuilt
        }
    }
}