Private/Install/Build-PwshProfileInitializeCall.ps1

function Build-PwshProfileInitializeCall {
    <#
    .SYNOPSIS
        Turns a settings hashtable into the Initialize-PwshProfile command line to embed in
        a profile.
 
    .DESCRIPTION
        Renders the single line that Install-PwshProfile writes into the managed bootstrap
        block. Most parameters are emitted only when they differ from the defaults, to keep the line
        tidy. Tool selection is the deliberate exception: it is ALWAYS emitted explicitly (-EnableAll,
        or -Enable with the chosen tokens, or -Enable @() for nothing), because that explicit pin is
        what stops a tool added in a later module version from auto-installing on the next shell.
 
        The theme drives the comparison baseline: the banner branding (text/color/icon) is compared
        against Get-PwshProfileDefault for the *selected* theme, so a forestcity install that keeps
        the Forest City branding emits just "-Theme forestcity" rather than re-spelling the matching
        banner text/color/icon. A bundled theme other than screwcity emits "-Theme <name>"; a custom
        theme path emits "-CustomTheme '<path>'" (the two are mutually exclusive in the generated
        call, mirroring Initialize-PwshProfile's parameter sets).
 
        String values are single-quoted (embedded single quotes are doubled) so values such as
        the ':nut_and_bolt:' step icon survive verbatim. The one exception is -BannerText, which is
        double-quoted so values like $env:COMPUTERNAME interpolate at profile startup (embedded
        double quotes and backticks are backtick-escaped; $ is intentionally left unescaped).
 
        Tool-specific params are kept consistent with the selection: -ReplaceCat / -BatTheme / -BatStyle
        (bat), -ReplaceMore (less), and -ZoxideCommand (zoxide) are emitted only when their tool is in
        the enabled set, and the banner params are omitted under -NoBanner — so a generated call never
        carries a flag for a disabled feature.
 
    .PARAMETER Setting
        The settings hashtable (keys as produced by Get-PwshProfileDefault / the wizard:
        Theme, CustomTheme, BannerText, BannerColor, BannerAlignment, BannerFont, StepIcon,
        ZoxideCommand, BatTheme, BatStyle, ReplaceCat, ReplaceMore, NoBanner, Enable, EnableAll). Keys
        that are absent fall back to the default and are not emitted.
 
    .PARAMETER Default
        The baseline to compare against. When omitted it is resolved as Get-PwshProfileDefault for
        the setting's selected theme; exposed mainly for testing.
 
    .EXAMPLE
        Build-PwshProfileInitializeCall -Setting (Get-PwshProfileDefault)
 
        Returns 'Initialize-PwshProfile -Enable @()' (the default has nothing selected, so it pins an
        empty enable list rather than a bare call).
 
    .EXAMPLE
        $s = Get-PwshProfileDefault -Theme forestcity; $s.Enable = @('Zoxide', 'Bat')
        Build-PwshProfileInitializeCall -Setting $s
 
        Returns 'Initialize-PwshProfile -Theme forestcity -Enable Zoxide,Bat'.
 
    .EXAMPLE
        $s = Get-PwshProfileDefault; $s.EnableAll = $true
        Build-PwshProfileInitializeCall -Setting $s
 
        Returns 'Initialize-PwshProfile -EnableAll' (every current tool plus future additions).
 
    .EXAMPLE
        $s = Get-PwshProfileDefault; $s.Enable = @('Bat'); $s.ReplaceCat = $true
        Build-PwshProfileInitializeCall -Setting $s
 
        Returns 'Initialize-PwshProfile -ReplaceCat -Enable Bat' (the cat -> bat switch is emitted
        because bat is enabled).
 
    .EXAMPLE
        $s = Get-PwshProfileDefault; $s.Enable = @('Zoxide'); $s.NoBanner = $true
        Build-PwshProfileInitializeCall -Setting $s
 
        Returns 'Initialize-PwshProfile -NoBanner -Enable Zoxide' (banner params are omitted).
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [hashtable]$Setting,

        [Parameter(Position = 1)]
        [hashtable]$Default
    )

    # The selected theme drives both the -Theme/-CustomTheme tokens and the banner comparison
    # baseline; 'screwcity' is the global default, so it is never emitted as -Theme.
    $theme = if ($Setting.ContainsKey('Theme') -and $Setting.Theme) { $Setting.Theme } else { 'screwcity' }
    $customTheme = if ($Setting.ContainsKey('CustomTheme')) { $Setting.CustomTheme } else { '' }
    if (-not $PSBoundParameters.ContainsKey('Default')) { $Default = Get-PwshProfileDefault -Theme $theme }

    # Single-quote a value for safe inclusion in the generated command, doubling embedded quotes.
    $quote = { param($value) "'" + ($value -replace "'", "''") + "'" }

    # Double-quote a value so PowerShell interpolation (e.g. $env:COMPUTERNAME) happens at startup.
    # Escape backticks first, then double quotes; $ is left intact deliberately so it interpolates.
    $quoteDouble = { param($value) '"' + ($value -replace '`', '``' -replace '"', '`"') + '"' }

    # Resolve a key from the supplied settings, falling back to the default when absent.
    $value = { param($key) if ($Setting.ContainsKey($key)) { $Setting[$key] } else { $Default[$key] } }

    $parts = [System.Collections.Generic.List[string]]::new()

    # Resolve the tool-selection shape up front. -EnableAll covers the whole catalog (and future
    # additions); otherwise the explicit Enable list is authoritative. The resolved set gates which
    # tool-specific params are worth emitting, so a disabled tool's flags never appear.
    $enableAll = [bool](& $value 'EnableAll')
    $enableList = @(& $value 'Enable')
    $enabledSet = if ($enableAll) { Get-PwshProfileToolCatalog -Token } else { $enableList }
    $noBanner = [bool](& $value 'NoBanner')

    # Theme selection: a custom theme path takes precedence (and is mutually exclusive with a bundled
    # -Theme); a bundled theme is emitted only when it isn't the screwcity default.
    if ($customTheme) {
        $parts.Add("-CustomTheme $(& $quote $customTheme)")
    }
    elseif ($theme -ne 'screwcity') {
        $parts.Add("-Theme $theme")
    }

    # -NoBanner suppresses the banner; the banner params below are then omitted as moot.
    if ($noBanner) { $parts.Add('-NoBanner') }

    # Scalar string parameters: emit only when they differ from the (themed) default. BannerText is
    # double-quoted (interpolation); the rest are single-quoted (verbatim). Banner params are skipped
    # under -NoBanner, and tool-specific params (zoxide/bat) only emit when that tool is enabled.
    $bannerKeys = @('BannerText', 'BannerColor', 'BannerAlignment', 'BannerFont')
    $keyTool = @{ ZoxideCommand = 'Zoxide'; BatTheme = 'Bat'; BatStyle = 'Bat' }
    foreach ($key in @($bannerKeys + @('StepIcon', 'ZoxideCommand', 'BatTheme', 'BatStyle'))) {
        if ($noBanner -and $bannerKeys -contains $key) { continue }
        if ($keyTool.ContainsKey($key) -and $enabledSet -notcontains $keyTool[$key]) { continue }
        $v = & $value $key
        if ($v -ne $Default[$key]) {
            $rendered = if ($key -eq 'BannerText') { & $quoteDouble $v } else { & $quote $v }
            $parts.Add("-$key $rendered")
        }
    }

    # Boolean switches: emitted as bare flags only when set, differing from the default ($false), and
    # the owning tool is enabled (the flag is a no-op otherwise).
    $replaceCat = & $value 'ReplaceCat'
    if ([bool]$replaceCat -ne [bool]$Default['ReplaceCat'] -and $replaceCat -and $enabledSet -contains 'Bat') {
        $parts.Add('-ReplaceCat')
    }
    $replaceMore = & $value 'ReplaceMore'
    if ([bool]$replaceMore -ne [bool]$Default['ReplaceMore'] -and $replaceMore -and $enabledSet -contains 'Less') {
        $parts.Add('-ReplaceMore')
    }

    # Tool selection is always emitted explicitly — that's what pins the set against future-tool drift.
    # -EnableAll for "everything + future"; otherwise -Enable with the chosen tokens, or -Enable @()
    # to deterministically enable nothing without triggering the bare-call prompt.
    if ($enableAll) {
        $parts.Add('-EnableAll')
    }
    elseif ($enableList.Count -gt 0) {
        $parts.Add("-Enable $($enableList -join ',')")
    }
    else {
        $parts.Add('-Enable @()')
    }

    return "Initialize-PwshProfile $($parts -join ' ')"
}