Providers/Entra/Private/Write-EntraBootstrapNextStep.ps1

function Write-EntraBootstrapNextStep {
    <#
    .SYNOPSIS
        Tells the person who just signed in interactively what to run next

    .DESCRIPTION
        The interactive sign-in is the bootstrap credential and nothing else: it exists so a
        human can create the service app that every later session connects as. What a person
        needs the moment it succeeds is to know whether that has already been done, and the
        exact command for their situation - and before this existed they had to work that out
        from a warning that only appeared after they guessed wrong.

        Takes the scenario Get-EntraBootstrapState decided and writes one short banner: what
        was found in the tenant, what was found on this machine, and the command to run. The
        commands use the shared dispatcher names, because those are what the module exports;
        a provider command such as Connect-EntraEnvironment is not callable from a session.

        Written to the host rather than the output stream, for the same reason the bootstrap's
        own handover banner is: it is an instruction to a person, must survive a caller who is
        capturing or discarding the output, and -PassThru carries the data for anyone who wants
        it as an object.

    .PARAMETER State
        The state Get-EntraBootstrapState returned.

    .PARAMETER FullAccess
        The sign-in asked for the full delegated scope list, so the banner can say the session
        is able to do the whole job as itself rather than only bootstrap.

    .EXAMPLE
        PS> Write-EntraBootstrapNextStep -State (Get-EntraBootstrapState)

        DESCRIPTION: Prints the next-step banner for the active connection
        OUTPUT: Nothing on the pipeline; a banner on the host
        USE CASE: Called by Connect-EntraEnvironment at the end of an interactive sign-in

    .NOTES
        Author: Jeffrey Stuhr
        Blog: https://www.techbyjeff.net
        LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '',
        Justification = 'An instruction to the human who just signed in, naming the exact command to run next. It must survive a caller who is piping or suppressing the other streams, and the data is available as an object from Get-EntraBootstrapState.')]
    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Parameter(Mandatory = $true)]
        [PSTypeName('EntraBootstrapState')]
        [PSObject]$State,

        [Parameter()]
        [switch]$FullAccess
    )

    $found = switch ($State.Scenario) {
        'ReadyToConnect' {
            "Service app '$($State.Applications | Where-Object ClientId -eq $State.RecordClientId | Select-Object -First 1 -ExpandProperty DisplayName)' exists in $($State.TenantName), and this machine holds its credential (key in $($State.KeyProtection))."
        }
        'AppWithoutCredential' {
            $names = ($State.Applications.DisplayName | Sort-Object) -join ', '
            $why = if ($State.RecordExists) { "the record on this machine names a different application ($($State.RecordClientId))" } else { 'this machine holds no credential record for it' }
            "Service app '$names' exists in $($State.TenantName), but $why."
        }
        'StaleRecord' {
            "This machine holds a credential record for $($State.TenantName), but the application it names ($($State.RecordClientId)) is no longer in the tenant."
        }
        'NothingYet' {
            "No service app in $($State.TenantName) yet, and no credential record on this machine."
        }
        'Unknown' {
            $held = if ($State.RecordExists) { 'This machine holds a credential record for it' } else { 'This machine holds no credential record for it' }
            "Could not list applications in $($State.TenantName) with this sign-in: $($State.CheckError). $held."
        }
    }

    $instruction = switch ($State.Scenario) {
        'ReadyToConnect' { 'You are connected as yourself for now. From the next session, connect app-only, or verify the stored credential first:' }
        'AppWithoutCredential' { 'Replace it with one this machine holds the key for:' }
        'StaleRecord' { 'Create a new one; the stale record will be overwritten:' }
        'NothingYet' { 'Create it now, while this sign-in still carries your authority to consent:' }
        'Unknown' { 'If the tenant already has a service app, connect with the record; otherwise create one:' }
    }

    Write-Host ''
    Write-Host " $found" -ForegroundColor Green
    Write-Host " $instruction" -ForegroundColor Green
    Write-Host ''
    foreach ($command in $State.Commands) {
        Write-Host " $command" -ForegroundColor Cyan
    }
    Write-Host ''
    if ($FullAccess) {
        Write-Host ' Or stay connected as yourself: this session asked for every delegated scope the module' -ForegroundColor DarkGray
        Write-Host ' needs, so New-TestEnvironment and Remove-TestEnvironment work here too, audited as you.' -ForegroundColor DarkGray
        Write-Host ''
    }
}