Payload/scripts/kaden/utils/PsCliUiKit/PsCliUiKit.psm1

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

function Get-CliStyleForegroundColor {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Style
    )

    switch ($Style) {
        "Default" { return [ConsoleColor]::Gray }
        "Info"         { return [ConsoleColor]::Cyan }
        "Success" { return [ConsoleColor]::Green }
        "Warning" { return [ConsoleColor]::Yellow }
        "Error"     { return [ConsoleColor]::Red }
        "Muted"     { return [ConsoleColor]::DarkGray }
        "Accent"     { return [ConsoleColor]::Magenta }
        "Code"         { return [ConsoleColor]::White }
        default     { return [ConsoleColor]::Gray }
    }
}

function Get-CliKnownStyleCatalog {
    return @(
        "Default",
        "Info",
        "Success",
        "Warning",
        "Error",
        "Muted",
        "Accent",
        "Code"
    )
}

function Get-CliAnsiSupportsVirtualTerminal {
    # Inline color tokens only render correctly when the host supports VT sequences.
    # This guard allows safe fallback to plain text in limited terminals.
    $ui = $Host.UI
    if ($null -eq $ui) {
        return $false
    }
    $vtProp = $ui.PSObject.Properties['SupportsVirtualTerminal']
    if ($null -eq $vtProp) {
        return $false
    }
    return [bool]$vtProp.Value
}

function Get-CliAnsiForegroundCodeMap {
    return @{
        "Black"                 = 30
        "Red"                     = 31
        "Green"                 = 32
        "Yellow"                 = 33
        "Blue"                     = 34
        "Magenta"             = 35
        "Cyan"                     = 36
        "White"                 = 37
        "Gray"                     = 90
        "Grey"                     = 90
        "BrightBlack"     = 90
        "BrightRed"         = 91
        "BrightGreen"     = 92
        "BrightYellow"     = 93
        "BrightBlue"         = 94
        "BrightMagenta" = 95
        "BrightCyan"         = 96
        "BrightWhite"     = 97
    }
}

function Get-CliAnsiSemanticStyleCodeMap {
    return @{
        # Layout
        "header"         = "96;1"
        "subheader" = "36;1"
        "divider"     = "90"

        # Italic is not consistently supported across terminals.
        "footer"         = "90"
        "muted"         = "90"
        "code"             = "30;47"

        # Feedback
        "success"     = "92"
        "error"         = "91;1"
        "warning"     = "93"
        "info"             = "94"
    }
}

function Get-CliAnsiTokenCodeMap {
    $tokenCodes = @{}
    $fgCodes = Get-CliAnsiForegroundCodeMap
    foreach ($name in $fgCodes.Keys) {
        $tokenCodes[$name.ToLowerInvariant()] = [string]$fgCodes[$name]
    }
    $semanticCodes = Get-CliAnsiSemanticStyleCodeMap
    foreach ($name in $semanticCodes.Keys) {
        $tokenCodes[$name.ToLowerInvariant()] = [string]$semanticCodes[$name]
    }
    return $tokenCodes
}

function Expand-CliPrefixAlias {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Message
    )

    $prefixMap = @{
        "[+]" = "[BrightGreen]__CLI_STATUS_PLUS__[-]"
        "[-]" = "[BrightRed]__CLI_STATUS_MINUS__[-]"
        "[~]" = "[BrightYellow]__CLI_STATUS_TILDE__[-]"
        "[!]" = "[BrightMagenta]__CLI_STATUS_BANG__[-]"
        "[*]" = "[BrightCyan]__CLI_STATUS_STAR__[-]"
    }
    foreach ($prefix in $prefixMap.Keys) {
        if ($Message.StartsWith($prefix, [StringComparison]::Ordinal)) {
            return $prefixMap[$prefix] + $Message.Substring($prefix.Length)
        }
    }
    return $Message
}

function Restore-CliPrefixAliasText {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Message
    )

    $restored = $Message
    $restored = $restored.Replace("__CLI_STATUS_PLUS__", "[+]")
    $restored = $restored.Replace("__CLI_STATUS_MINUS__", "[-]")
    $restored = $restored.Replace("__CLI_STATUS_TILDE__", "[~]")
    $restored = $restored.Replace("__CLI_STATUS_BANG__", "[!]")
    $restored = $restored.Replace("__CLI_STATUS_STAR__", "[*]")
    return $restored
}

function ConvertTo-CliAnsiMessage {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Message
    )

    $esc = [char]0x1b
    $pattern = '\[(?<token>[A-Za-z]+|-)\]'
    $tokenCodes = Get-CliAnsiTokenCodeMap
    $expanded = Expand-CliPrefixAlias -Message $Message

    $evaluator = {
        param($match)

        $token = $match.Groups['token'].Value
        if ($token -eq '-') {
            return "${esc}[0m"
        }
        $key = $token.ToLowerInvariant()
        if ($tokenCodes.ContainsKey($key)) {
            return "${esc}[$($tokenCodes[$key])m"
        }
        return $match.Value
    }
    $ansi = [regex]::Replace($expanded, $pattern, $evaluator)

    $withSymbols = Restore-CliPrefixAliasText -Message $ansi
    # Reset at end so subsequent host output keeps its expected color.
    return "${withSymbols}${esc}[0m"
}

function ConvertFrom-CliTokenMarkup {
    # This is the non-VT fallback path: remove known markup tokens but keep
    # content readable so logs/transcripts still make sense in plain hosts.
    param(
        [Parameter(Mandatory = $true)]
        [string]$Message
    )
    $pattern = '\[(?<token>[A-Za-z]+|-)\]'
    $tokenCodes = Get-CliAnsiTokenCodeMap
    $expanded = Expand-CliPrefixAlias -Message $Message
    $stripEvaluator = {
        param($match)

        $token = $match.Groups['token'].Value
        if ($token -eq "-") {
            return ""
        }
        $key = $token.ToLowerInvariant()
        if ($tokenCodes.ContainsKey($key)) {
            return ""
        }
        return $match.Value
    }
    $stripped = [regex]::Replace($expanded, $pattern, $stripEvaluator)
    return Restore-CliPrefixAliasText -Message $stripped
}

function Write-CliColor {
    <#
    .SYNOPSIS
        Write one colored line to the host.

    .DESCRIPTION
        Lightweight CLI UI helper for scripts that need readable operator output
        without polluting the success pipeline.

    .PARAMETER Message
        Text to print.

    .PARAMETER ForegroundColor
        Console foreground color. Overrides Style when set.

    .PARAMETER BackgroundColor
        Optional console background color.

    .PARAMETER Style
        Semantic style preset for default foreground color.
        Known values: Default, Info, Success, Warning, Error, Muted, Accent, Code.
        An unknown value warns and falls back to Default.
        -OnInvalidStyle Error throws. Silent falls back without a warning.

    .PARAMETER OnInvalidStyle
        Error, Warn, or Silent. Default: Warn.

    .PARAMETER NoNewline
        Writes without a trailing newline.

    .PARAMETER Literal
        Print Message as plain text. Skips inline color tokens such as [-].

    .EXAMPLE
        Write-CliColor -Message "Starting..." -Style Info

    .EXAMPLE
        Write-CliColor -Message "done" -Style Success

    .EXAMPLE
        Write-CliColor -Message "Database is [BrightGreen]Online[-]."

    .EXAMPLE
        Write-CliColor -Message "[+] done"

    .EXAMPLE
        Write-CliColor -Message "[header]Deployment started[-]"

    .EXAMPLE
        Write-CliColor -Message "[divider]================================[-]"

    .EXAMPLE
        "alpha","beta" | Write-CliColor -Style Warning
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSAvoidUsingWriteHost', '',
        Justification = 'Intentional operator-facing console UI; this function is the host-output boundary.'
    )]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [AllowEmptyString()]
        [string]$Message,

        [ConsoleColor]$ForegroundColor,

        [ConsoleColor]$BackgroundColor,

        [string]$Style = "Default",

        [ValidateSet("Error", "Warn", "Silent")]
        [string]$OnInvalidStyle = "Warn",

        [switch]$NoNewline,

        [switch]$Literal
    )

    process {
        $knownStyles = Get-CliKnownStyleCatalog
        if ($knownStyles -notcontains $Style) {
            $validList = $knownStyles -join ", "
            $styleWarning = "Unknown style '$Style'. Valid values: $validList."
            switch ($OnInvalidStyle) {
                "Error" { throw $styleWarning }
                "Warn" { Write-Warning "$styleWarning Falling back to Default." }
                "Silent" { }
            }
            $Style = "Default"
        }

        $resolvedForeground = if ($PSBoundParameters.ContainsKey('ForegroundColor')) {
            $ForegroundColor
        } else {
            Get-CliStyleForegroundColor -Style $Style
        }

        $containsTokens = [regex]::IsMatch($Message, '\[(?:[A-Za-z]+|-|\+|~|!|\*)\]')
        $writeHostParams = @{
            ForegroundColor = $resolvedForeground
        }
        if ($PSBoundParameters.ContainsKey('BackgroundColor')) {
            $writeHostParams.BackgroundColor = $BackgroundColor
        }
        if ($NoNewline) {
            $writeHostParams.NoNewline = $true
        }

        if ($containsTokens -and -not $Literal) {
            if (Get-CliAnsiSupportsVirtualTerminal) {
                $writeHostParams.Object = ConvertTo-CliAnsiMessage -Message $Message
            } else {
                $writeHostParams.Object = ConvertFrom-CliTokenMarkup -Message $Message
            }
        } else {
            $writeHostParams.Object = $Message
        }
        Write-Host @writeHostParams
    }
}

function Write-CliSection {
    <#
    .SYNOPSIS
        Write a titled section, with a rule under header and success titles.

    .EXAMPLE
        Write-CliSection -Title "Deployment"

    .EXAMPLE
        Write-CliSection -Title "Details" -Style subheader
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$Title,

        [ValidateSet('header', 'subheader', 'success', 'info', 'warning', 'error')]
        [string]$Style = "header",

        [ValidateRange(8, 120)]
        [int]$Width = 60
    )

    Write-CliColor -Message ''
    Write-CliColor -Message "[$Style]$Title[-]"
    if ($Style -eq "header" -or $Style -eq "success") {
        Write-CliDivider -Width $Width
    }
}

function Write-CliDivider {
    <#
    .SYNOPSIS
        Write a muted horizontal rule.

    .EXAMPLE
        Write-CliDivider

    .EXAMPLE
        Write-CliDivider -Width 40 -Character '='
    #>

    [CmdletBinding()]
    param(
        [ValidateRange(8, 120)]
        [int]$Width = 60,

        [ValidateLength(1, 1)]
        [string]$Character = "-"
    )

    $line = $Character * $Width
    Write-CliColor -Message "[divider]$line[-]"
}

function Write-CliKeyValue {
    <#
    .SYNOPSIS
        Write one aligned label and value row.

    .EXAMPLE
        Write-CliKeyValue -Label "Version" -Value "1.2.3"

    .EXAMPLE
        Write-CliKeyValue -Label "Service" -Value "[BrightCyan]api[-]" -LabelWidth 12
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Label,

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]$Value,

        [ValidateRange(4, 40)]
        [int]$LabelWidth = 16
    )

    $labelText = $Label.PadRight($LabelWidth)
    Write-CliColor -Message " [muted]$labelText[-] : $Value"
}

function Write-CliList {
    <#
    .SYNOPSIS
        Write an indented list. Items may come from the pipeline.

    .PARAMETER Style
        Bullet uses -Bullet, which defaults to an asterisk.
        Corner uses the Corner glyph, the tree-list mark.

    .PARAMETER Bullet
        Mark printed before each item. Overrides Style when you pass it.

    .EXAMPLE
        Write-CliList -Style Corner -Item "Get-KadenVersion", "Initialize-KadenRepo"

    .EXAMPLE
        "one", "two" | Write-CliList -Bullet "-"
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [AllowEmptyString()]
        [string[]]$Item,

        [ValidateSet("Bullet", "Corner")]
        [string]$Style = "Bullet",

        [string]$Bullet = "*",

        [ValidateRange(0, 12)]
        [int]$Indent = 2,

        [switch]$Literal
    )

    begin {
        $pad = ' ' * $Indent
        $mark = $Bullet
        if ((-not $PSBoundParameters.ContainsKey("Bullet")) -and ($Style -eq "Corner")) {
            $mark = Get-CliGlyph -Name Corner
        }
    }
    process {
        foreach ($one in @($Item)) {
            if ($Literal) {
                Write-CliColor -Message "$pad$mark $one" -Literal
            } else {
                Write-CliColor -Message "$pad$mark $one"
            }
        }
    }
}

function Write-CliStatus {
    <#
    .SYNOPSIS
        Write a status line with a shorthand prefix.

    .EXAMPLE
        Write-CliStatus -State Success -Message "Deployment finished"

    .EXAMPLE
        Write-CliStatus -State Warning -Message "2 warnings"
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet("Success", "Error", "Warning", "Info", "Changed")]
        [string]$State,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [string]$Message
    )

    process {
        $prefix = switch ($State) {
            "Success" { "[+]" }
            "Error"     { "[-]" }
            "Warning" { "[!]" }
            "Info"         { "[*]" }
            "Changed" { "[~]" }
        }
        $style = switch ($State) {
            "Success" { "Success" }
            "Error"     { "Error" }
            "Warning" { "Warning" }
            "Info"         { "Info" }
            "Changed" { "Warning" }
        }
        Write-CliColor -Message "$prefix $Message" -Style $style
    }
}

function Write-CliEnd {
    <#
    .SYNOPSIS
        Write the trailing blank line that separates script output from the next prompt.

    .EXAMPLE
        Write-CliEnd
    #>

    [CmdletBinding()]
    param()

    Write-CliColor -Message ""
}

# Code points only. A literal glyph in this file fails the PowerShell ASCII lint gate.
$script:CliGlyphCatalog = @{
    Prompt              = @{ CodePoint = 0x276F; Fallback = '>' }
    Corner              = @{ CodePoint = 0x23BF; Fallback = '>' }
    TreeBar             = @{ CodePoint = 0x2502; Fallback = '|' }
    TreeBranch          = @{ CodePoint = 0x251C; Fallback = '+' }
    TreeLast            = @{ CodePoint = 0x2514; Fallback = '+' }
    Ok                  = @{ CodePoint = 0x2714; Fallback = '+' }
    Fail                = @{ CodePoint = 0x2716; Fallback = 'x' }
    Warn                = @{ CodePoint = 0x26A0; Fallback = '!' }
    Bullet              = @{ CodePoint = 0x2022; Fallback = '*' }
    BigDot              = @{ CodePoint = 0x25CF; Fallback = 'o' }
    EnDash              = @{ CodePoint = 0x2013; Fallback = '-' }
    EmDash              = @{ CodePoint = 0x2014; Fallback = '-' }
    MiddleDot           = @{ CodePoint = 0x00B7; Fallback = '.' }
    PlusMinus           = @{ CodePoint = 0x00B1; Fallback = '+-' }
    Guillemet           = @{ CodePoint = 0x00BB; Fallback = '>' }
    AngleLeft           = @{ CodePoint = 0x2039; Fallback = '<' }
    AngleRight          = @{ CodePoint = 0x203A; Fallback = '>' }
    Multiply            = @{ CodePoint = 0x00D7; Fallback = 'x' }
    Star                = @{ CodePoint = 0x204E; Fallback = '*' }
    OutlineStar         = @{ CodePoint = 0x2729; Fallback = '*' }
    Flower              = @{ CodePoint = 0x2055; Fallback = '*' }
    Diamond             = @{ CodePoint = 0x1F539; Fallback = '*' }
    LargeDiamond        = @{ CodePoint = 0x1F537; Fallback = '#' }
    GClef               = @{ CodePoint = 0x1D11E; Fallback = 'G' }
    FClef               = @{ CodePoint = 0x1D122; Fallback = 'F' }
    QuarterNote         = @{ CodePoint = 0x1D15F; Fallback = 'q' }
    Arrow               = @{ CodePoint = 0x2192; Fallback = '->' }
    ArrowLeft           = @{ CodePoint = 0x2190; Fallback = '<-' }
    ArrowUp             = @{ CodePoint = 0x2191; Fallback = '^' }
    ArrowDown           = @{ CodePoint = 0x2193; Fallback = 'v' }
    ArrowLeftRight      = @{ CodePoint = 0x2194; Fallback = '<->' }
    ArrowSwap           = @{ CodePoint = 0x21C4; Fallback = '<->' }
    ArrowUpDown         = @{ CodePoint = 0x21C5; Fallback = '^v' }
    DoubleLeft          = @{ CodePoint = 0x21D0; Fallback = '<-' }
    DoubleUp            = @{ CodePoint = 0x21D1; Fallback = '^' }
    DoubleRight         = @{ CodePoint = 0x21D2; Fallback = '->' }
    DoubleDown          = @{ CodePoint = 0x21D3; Fallback = 'v' }
    DoubleLeftRight     = @{ CodePoint = 0x21D4; Fallback = '<->' }
    DoubleUpDown        = @{ CodePoint = 0x21D5; Fallback = '^v' }
    HeavyLeft           = @{ CodePoint = 0x1F870; Fallback = '<-' }
    HeavyUp             = @{ CodePoint = 0x1F871; Fallback = '^' }
    HeavyRight          = @{ CodePoint = 0x1F872; Fallback = '->' }
    HeavyDown           = @{ CodePoint = 0x1F873; Fallback = 'v' }
    HeavyUpLeft         = @{ CodePoint = 0x1F874; Fallback = '^' }
    HeavyUpRight        = @{ CodePoint = 0x1F875; Fallback = '^' }
    HeavyDownLeft       = @{ CodePoint = 0x1F876; Fallback = 'v' }
    HeavyDownRight      = @{ CodePoint = 0x1F877; Fallback = 'v' }
    BoxHorizontal       = @{ CodePoint = 0x2501; Fallback = '-' }
    BoxDownRight        = @{ CodePoint = 0x250D; Fallback = '+' }
    BoxDownLeft         = @{ CodePoint = 0x2511; Fallback = '+' }
    BoxUpRight          = @{ CodePoint = 0x2515; Fallback = '+' }
    BoxUpLeft           = @{ CodePoint = 0x2519; Fallback = '+' }
    BoxVerticalRight    = @{ CodePoint = 0x251D; Fallback = '+' }
    BoxVerticalLeft     = @{ CodePoint = 0x2525; Fallback = '+' }
    BoxDownHorizontal   = @{ CodePoint = 0x252D; Fallback = '+' }
    BoxUpHorizontal     = @{ CodePoint = 0x2535; Fallback = '+' }
    BoxVerticalHorizontal = @{ CodePoint = 0x253D; Fallback = '+' }
}

function Test-CliGlyphHost {
    if ($PSVersionTable.PSVersion.Major -lt 7) {
        return $false
    }
    try {
        if ([Console]::IsOutputRedirected) {
            return $false
        }
    } catch {
        return $false
    }
    return $true
}

function Test-CliGlyphScalar {
    param(
        [Parameter(Mandatory = $true)]
        [int]$CodePoint
    )

    if ($CodePoint -lt 0 -or $CodePoint -gt 0x10FFFF) {
        return $false
    }
    if ($CodePoint -ge 0xD800 -and $CodePoint -le 0xDFFF) {
        return $false
    }
    return $true
}

function Get-CliGlyphFallback {
    param(
        [Parameter(Mandatory = $true)]
        [int]$CodePoint
    )

    foreach ($entry in $script:CliGlyphCatalog.Values) {
        if ($entry.CodePoint -eq $CodePoint) {
            return [string]$entry.Fallback
        }
    }
    return '?'
}

function Get-CliGlyph {
    <#
    .SYNOPSIS
        Return an operator glyph, or its ASCII fallback.

    .DESCRIPTION
        Named marks resolve through the in-module code-point table. -Mode Glyph
        builds the character with ConvertFromUtf32. -Mode Ansi returns the table
        fallback. A code point that is not in the table returns '?' on Ansi.
        -Mode Auto returns a glyph only on PowerShell 7 or later when stdout is
        a real console. Redirected output stays ASCII.

    .PARAMETER Name
        Catalog name. Tab completion lists the marks.

    .PARAMETER CodePoint
        Unicode scalar value to render.

    .PARAMETER Mode
        Auto, Ansi, or Glyph. Default: Auto.

    .EXAMPLE
        Write-CliColor -Message "$(Get-CliGlyph -Name Prompt) Next step" -Style Warning

    .EXAMPLE
        Get-CliGlyph -Name Ok -Mode Ansi

    .EXAMPLE
        Get-CliGlyph -CodePoint 0x2192 -Mode Glyph
    #>

    [CmdletBinding(DefaultParameterSetName = 'Name')]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Name', Position = 0)]
        [ValidateSet(
            'Prompt', 'Corner', 'TreeBar', 'TreeBranch', 'TreeLast',
            'Ok', 'Fail', 'Warn', 'Bullet', 'BigDot',
            'EnDash', 'EmDash', 'MiddleDot', 'PlusMinus', 'Guillemet',
            'AngleLeft', 'AngleRight', 'Multiply', 'Star', 'OutlineStar', 'Flower',
            'Diamond', 'LargeDiamond', 'GClef', 'FClef', 'QuarterNote',
            'Arrow', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'ArrowLeftRight',
            'ArrowSwap', 'ArrowUpDown',
            'DoubleLeft', 'DoubleUp', 'DoubleRight', 'DoubleDown',
            'DoubleLeftRight', 'DoubleUpDown',
            'HeavyLeft', 'HeavyUp', 'HeavyRight', 'HeavyDown',
            'HeavyUpLeft', 'HeavyUpRight', 'HeavyDownLeft', 'HeavyDownRight',
            'BoxHorizontal', 'BoxDownRight', 'BoxDownLeft', 'BoxUpRight', 'BoxUpLeft',
            'BoxVerticalRight', 'BoxVerticalLeft', 'BoxDownHorizontal',
            'BoxUpHorizontal', 'BoxVerticalHorizontal'
        )]
        [string]$Name,

        [Parameter(Mandatory = $true, ParameterSetName = 'CodePoint')]
        [int]$CodePoint,

        [ValidateSet('Auto', 'Ansi', 'Glyph')]
        [string]$Mode = 'Auto'
    )

    $useGlyph = $false
    if ($Mode -eq 'Glyph') {
        $useGlyph = $true
    } elseif ($Mode -eq 'Auto') {
        $useGlyph = Test-CliGlyphHost
    }

    if ($PSCmdlet.ParameterSetName -eq 'Name') {
        $entry = $script:CliGlyphCatalog[$Name]
        if ($useGlyph) {
            return [char]::ConvertFromUtf32($entry.CodePoint)
        }
        return [string]$entry.Fallback
    }

    if (-not (Test-CliGlyphScalar -CodePoint $CodePoint)) {
        throw "Code point $($CodePoint.ToString('X')) is not a Unicode scalar."
    }
    if ($useGlyph) {
        return [char]::ConvertFromUtf32($CodePoint)
    }
    return Get-CliGlyphFallback -CodePoint $CodePoint
}

Export-ModuleMember -Function @(
    "Write-CliColor"
    "Write-CliSection"
    "Write-CliDivider"
    "Write-CliKeyValue"
    "Write-CliList"
    "Write-CliStatus"
    "Write-CliEnd"
    "Get-CliGlyph"
)