Public/Install/Install-PwshProfile.ps1

function Install-PwshProfile {
    <#
    .SYNOPSIS
        Interactive wizard that wires ScrewCitySoftware.PwshProfile into a PowerShell profile file.
 
    .DESCRIPTION
        Walks you through a PwshSpectreConsole wizard and writes a marker-wrapped bootstrap block —
        the module import plus a tailored Initialize-PwshProfile call — into a profile file
        (by default $PROFILE). It is the one-time setup companion to Initialize-PwshProfile,
        which then runs every session from inside that block.
 
        Note: this wires the module into your profile *file*; it does not install the module itself
        from the gallery (use Install-PSResource ScrewCitySoftware.PwshProfile for that).
 
        The wizard walks one forward pass — an optional Nerd Font install, a set of winget client
        settings, theme, an optional banner (a yes/no that gates the text/color/alignment/font
        prompts), the step icon, and a grouped feature tree that starts with everything checked
        (uncheck to opt out; oh-my-posh is always on) — then lands on a review screen where any step
        can be re-edited before submitting, or the whole setup cancelled without writing. The Nerd
        Font install uses the NerdFonts module (CurrentUser scope, no admin required), defaulting to
        the recommended Meslo + CascadiaCode pairing. The winget settings (default install scope,
        progress-bar style, anonymize-displayed-paths, suppress-install-notes) are pre-filled from
        the current settings.json and merged back into it via Set-WingetSetting at the end of the
        run — a one-time machine action, not part of the bootstrap block (so re-running the wizard
        re-applies them; -WhatIf previews without touching settings.json).
 
        Your existing profile code is never destroyed:
          - A new file (and its parent directory) is created if needed.
          - An existing managed block is replaced in place, so the command is safe to re-run to
            change options.
          - Any other existing content is left intact, with the block prepended above it.
          - A profile that already contains a bare 'Import-Module ScrewCitySoftware.PwshProfile'
            (no markers) is left untouched unless -Force is given.
 
        This is a user-invoked setup command (not silent startup), so genuine errors throw. When the
        Spectre prompt cmdlets are unavailable, it warns and writes the default settings
        non-interactively rather than failing.
 
    .PARAMETER Path
        The profile file to configure. Defaults to $PROFILE (current user, current host). Pass an
        explicit path to target another profile (e.g. the all-hosts profile or the VS Code host
        profile).
 
    .PARAMETER Force
        When the target already contains a bare module import but no managed markers, prepend the
        managed block anyway instead of treating the file as already wired.
 
    .PARAMETER PassThru
        Emit a result object ([pscustomobject] with Path, Action, and Changed). By default the
        command writes the file and returns nothing.
 
    .EXAMPLE
        Install-PwshProfile
 
        Runs the wizard and writes the bootstrap into $PROFILE, creating it (and its directory) if
        needed.
 
    .EXAMPLE
        Install-PwshProfile -WhatIf
 
        Walks the wizard and previews the write without changing any file.
 
    .EXAMPLE
        Install-PwshProfile -Path $PROFILE.CurrentUserAllHosts
 
        Configures the current user's all-hosts profile instead of the current-host one.
 
    .EXAMPLE
        Install-PwshProfile -Path ~/Documents/PowerShell/Microsoft.VSCode_profile.ps1
 
        Configures the VS Code integrated-terminal host profile.
 
    .NOTES
        $PROFILE is host-specific — the VS Code and ISE hosts use different files than the default
        console. The file is written as UTF-8 without a BOM. Re-run any time to change settings; the
        managed block is rewritten in place. Spectre prompts only render in an interactive console.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '',
        Justification = 'SupportsShouldProcess is declared so -WhatIf/-Confirm are accepted and flow via $WhatIfPreference into the gated writer Write-PwshProfileBlock (and the -not $WhatIfPreference guards on the font/winget steps); this function intentionally delegates rather than calling ShouldProcess itself. Covered by the -WhatIf tests.')]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string]$Path = $PROFILE,

        [Parameter()]
        [switch]$Force,

        [Parameter()]
        [switch]$PassThru
    )

    $def = Get-PwshProfileDefault
    $accent = $def.BannerColor
    $code = '#5fd7ff'   # soft cyan for code literals / paths, matching the wizard's highlighting
    $marker = Get-PwshProfileMarker

    # Detect an existing managed block so the intro can say "updating" and to drive the wizard.
    $reconfiguring = $false
    if (Test-Path -LiteralPath $Path -PathType Leaf) {
        $current = Get-Content -LiteralPath $Path -Raw -Encoding utf8
        if ($current -and $current.Contains($marker.Open)) {
            $reconfiguring = $true
        }
    }

    $interactive = [bool](Get-Command Read-SpectreSelection -ErrorAction SilentlyContinue)

    if ($interactive) {
        Write-Figlet -Text 'Pwsh Profile' -Color $accent
        if (Get-Command Write-SpectreHost -ErrorAction SilentlyContinue) { Write-SpectreHost '' }
        $pathLine = '`' + $Path + '`'   # render the target path as a cyan code literal
        $intro = if ($reconfiguring) {
            "Updating the **ScrewCitySoftware.PwshProfile** bootstrap in:`n$pathLine"
        }
        else {
            "This wizard wires **ScrewCitySoftware.PwshProfile** into:`n$pathLine"
        }
        Format-PwshProfileHelpMarkup -Text $intro -Accent $accent -Code $code -Body default |
            Format-SpectrePanel -Header '◆ Profile setup' -Border Rounded -Color $accent -Expand | Out-Host

        $settings = Invoke-PwshProfileWizard -Reconfiguring:$reconfiguring
    }
    else {
        Write-Warning 'Install-PwshProfile: PwshSpectreConsole prompts are unavailable; writing default settings non-interactively.'
        $settings = $def.Clone()
        # Clone() is shallow, so re-wrap the array values to break aliasing with $def's instances.
        $settings.Skip = @($def.Skip)
        $settings.SkipSection = @($def.SkipSection)
        $settings.NerdFont = $null
        # Seed the winget settings the same way the wizard would, so a non-interactive install still
        # applies the user-scope default (existing settings.json values are read first, so this only
        # adds unset keys / re-affirms current ones).
        $wingetDef = Get-WingetSettingDefault
        $settings.WingetScope = $wingetDef.Scope
        $settings.WingetProgressBar = $wingetDef.ProgressBar
        $settings.WingetAnonymizePath = $wingetDef.AnonymizePath
        $settings.WingetDisableInstallNote = $wingetDef.DisableInstallNote
    }

    # The wizard returns $null when the user cancels at the review screen — write nothing.
    if ($null -eq $settings) {
        if ($interactive -and (Get-Command Format-SpectrePanel -ErrorAction SilentlyContinue)) {
            if (Get-Command Write-SpectreHost -ErrorAction SilentlyContinue) { Write-SpectreHost '' }
            '[grey]Setup cancelled — no changes made.[/]' |
                Format-SpectrePanel -Header '• Cancelled' -Border Rounded -Color Grey -Expand | Out-Host
        }
        else {
            Write-Warning 'Install-PwshProfile: setup cancelled; no changes made.'
        }
        return
    }

    # Optional Nerd Font install (a one-time machine action; not part of the profile bootstrap).
    # Skipped under -WhatIf, since a preview must make no changes (this also installs a module).
    $fonts = @($settings.NerdFont | Where-Object { $_ })
    if ($fonts.Count -and -not $WhatIfPreference) {
        Invoke-Step "Nerd Fonts ($($fonts -join ', '))" -Icon ':gear:' {
            Import-ModuleSafe NerdFonts
            if (Get-Command Install-NerdFont -ErrorAction SilentlyContinue) {
                # Standard variant = the 'MesloLGM Nerd Font' / 'CaskaydiaCove Nerd Font' families
                # Show-NerdFontSetup recommends, and a smaller download than the default 'All'.
                Install-NerdFont -Name $fonts -Scope CurrentUser -Variant Standard
            }
            else {
                Write-Warning "Install-PwshProfile: NerdFonts module unavailable; skipped installing '$($fonts -join ', ')'."
            }
        }
    }

    # Apply the chosen winget client settings to winget's settings.json — a one-time machine action
    # like the font install, not part of the profile bootstrap. Skipped under -WhatIf (a preview must
    # make no changes), and only when the wizard supplied the winget keys.
    if ($settings.ContainsKey('WingetScope') -and -not $WhatIfPreference) {
        Invoke-Step 'Winget settings' -Icon ':gear:' {
            Set-WingetSetting -Scope $settings.WingetScope -ProgressBar $settings.WingetProgressBar `
                -AnonymizePath $settings.WingetAnonymizePath -DisableInstallNote $settings.WingetDisableInstallNote
        }
    }

    # Terminal-font guidance — display-only (runs under -WhatIf), shown every run so users know to
    # point their terminal at a Nerd Font even if they declined the install. Pass -Font only when
    # fonts were chosen so it names the installed families; otherwise it shows the recommended pairing.
    $fontSetupArgs = @{}
    if ($fonts.Count) { $fontSetupArgs.Font = $fonts }
    Show-NerdFontSetup @fontSetupArgs

    $call = Build-PwshProfileInitializeCall -Setting $settings

    if ($interactive -and (Get-Command Format-SpectrePanel -ErrorAction SilentlyContinue)) {
        $preview = Get-PwshProfileBlock -InitializeCall $call
        $preview | Format-SpectrePanel -Header "Bootstrap for $Path" -Border Rounded -Color $accent -Expand | Out-Host
    }

    # The writer carries SupportsShouldProcess, and -WhatIf/-Confirm flow into it via preference
    # variables, so the actual write stays fully gated.
    $writeArgs = @{ Path = $Path; InitializeCall = $call }
    if ($Force) { $writeArgs.Force = $true }
    $result = Write-PwshProfileBlock @writeArgs

    if ($interactive -and -not $WhatIfPreference -and (Get-Command Format-SpectrePanel -ErrorAction SilentlyContinue)) {
        $color = 'Green'
        $msg = switch ($result.Action) {
            'AlreadyPresent' { 'Already configured — no changes made.' }
            'BareImportPresent' {
                $color = 'Yellow'
                'A hand-written import already exists (no managed block). Left as-is — re-run with -Force to add the managed block, or run Uninstall-PwshProfile first.'
            }
            default { 'Bootstrap written. Restart your shell (or run . $PROFILE) to apply.' }
        }
        if (Get-Command Write-SpectreHost -ErrorAction SilentlyContinue) { Write-SpectreHost '' }
        $header = if ($color -eq 'Green') { '✓ Done' } else { '! Heads up' }
        $pathMarkup = Format-PwshProfileHelpMarkup -Text ('`' + $result.Path + '`') -Code $code -Body default
        "[$color]$msg[/]`n$pathMarkup" | Format-SpectrePanel -Header $header -Border Rounded -Color $color -Expand | Out-Host
    }

    if ($PassThru) { $result }
}