ITFabrik.Stepper.psm1

# Built file. Do not edit directly; edit source files and rebuild.
# Build timestamp: 2026-03-06T12:47:18+00:00

# region Private\State.ps1
# Module-scoped state and helpers (not exported)

if (-not $script:StepStack) { $script:StepStack = New-Object System.Collections.Stack }
if (-not $script:StepStateLock) { $script:StepStateLock = New-Object object }

function Push-Step {
    param([Step]$Step)
    [System.Threading.Monitor]::Enter($script:StepStateLock)
    try { $null = $script:StepStack.Push($Step) }
    finally { [System.Threading.Monitor]::Exit($script:StepStateLock) }
}

function Pop-Step {
    [System.Threading.Monitor]::Enter($script:StepStateLock)
    try { if ($script:StepStack.Count -gt 0) { return $script:StepStack.Pop() } }
    finally { [System.Threading.Monitor]::Exit($script:StepStateLock) }
}

function Get-StepStackTop {
    [System.Threading.Monitor]::Enter($script:StepStateLock)
    try { if ($script:StepStack.Count -gt 0) { return $script:StepStack.Peek() } }
    finally { [System.Threading.Monitor]::Exit($script:StepStateLock) }
}

# (Removed) StopExecution flag was unused; keeping state minimal
# endregion

# region Private\Helpers.ps1
# Initialisation du flag d'exécution interne d'un Step
if ($null -eq $script:InsideStep) { $script:InsideStep = $false }

<#
.SYNOPSIS
Affiche un message d'étape avec indentation.

.DESCRIPTION
Affiche un message d'étape à l'écran, en gris, avec un niveau d'indentation optionnel.

.PARAMETER Message
Le message à afficher.

.PARAMETER IndentLevel
Niveau d'indentation (nombre d'espaces).
#>

function Get-StepManagerLogger {
    [CmdletBinding()]
    param()

    $currentScopeVariable = Get-Variable -Name 'StepManagerLogger' -ErrorAction SilentlyContinue
    if ($null -ne $currentScopeVariable) {
        return $currentScopeVariable.Value
    }

    foreach ($scope in @('Script', 'Global')) {
        $variable = Get-Variable -Name 'StepManagerLogger' -Scope $scope -ErrorAction SilentlyContinue
        if ($null -ne $variable) {
            return $variable.Value
        }
    }

    return $null
}

function Write-StepMessage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string]$Severity,
        [Parameter(Mandatory)] [string]$Message,
        [int]$IndentLevel = 0,
        [string]$StepName = '',
        [string]$ForegroundColor
    )

    # Détection de PowerShell 7+
    $isPwsh7 = $PSVersionTable.PSVersion.Major -ge 7

    # Dictionnaire d'icônes par sévérité (Unicode, fallback via [Severity] si non supporté)
    $icons = @{
        'Info'    = 'ℹ'
        'Success' = '✓'
        'Warning' = '⚠'
        'Error'   = '✖'
        'Debug'   = '⚙'
        'Verbose' = '…'
    }

    $prefixRaw = if ($isPwsh7 -and $icons.ContainsKey($Severity)) { $icons[$Severity] } else { "[$Severity]" }
    # Padding spécifique par sévérité pour un alignement optimal
    $nbsp = [char]0x2007
    function Get-NbspString($count) { [string]::new(@($nbsp) * $count) }
    
    switch ($Severity) {
        'Info'    { $prefix = $prefixRaw + (Get-NbspString 4) ; $ForegroundColor = if(-not $ForegroundColor){ 'Gray'} else{$ForegroundColor} }
        'Success' { $prefix = $prefixRaw + (Get-NbspString 3) ; $ForegroundColor = if(-not $ForegroundColor){ 'Green'} else{$ForegroundColor}}
        'Warning' { $prefix = $prefixRaw + (Get-NbspString 4) ; $ForegroundColor = if(-not $ForegroundColor){ 'Yellow'} else{$ForegroundColor}}
        'Error'   { $prefix = $prefixRaw + (Get-NbspString 3) ; $ForegroundColor = if(-not $ForegroundColor){ 'Red'} else{$ForegroundColor}}
        'Debug'   { $prefix = $prefixRaw + (Get-NbspString 3) ; $ForegroundColor = if(-not $ForegroundColor){ 'Cyan'} else{$ForegroundColor}}
        'Verbose' { $prefix = $prefixRaw + (Get-NbspString 3) ; $ForegroundColor = if(-not $ForegroundColor){ 'Magenta'} else{$ForegroundColor}}
        default   { $prefix = $prefixRaw + (Get-NbspString 3) ; $ForegroundColor = if(-not $ForegroundColor){ 'White'} else{$ForegroundColor}}
    }

    $indent = if ($IndentLevel -gt 0) { ' ' * ($IndentLevel * 2) } else { '' }
    $now = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
    $step = if ($StepName) { "[$StepName]" } else { '' }
    # Ne plus afficher le Component ici pour éviter la duplication d'étiquettes
    $text = "[$now] $prefix$indent$step $Message"
    Write-Host $text -ForegroundColor $ForegroundColor
}
# endregion

# region Private\Classes\Step.ps1

class Step {
    [string]$Name
    [ValidateSet('Pending','Success','Error')]
    [string]$Status = 'Pending'
    [int]$Level = 0
    [Step]$ParentStep = $null
    [System.Collections.Generic.List[Step]]$Children = [System.Collections.Generic.List[Step]]::new()
    [string]$Detail = ''
    [bool]$ContinueOnError = $false
    [datetime]$StartTime
    [Nullable[datetime]]$EndTime
    [TimeSpan]$Duration = [TimeSpan]::Zero

    Step([string]$Name, [Step]$ParentStep = $null, [bool]$ContinueOnError = $false) {
        $this.Name = $Name
        $this.Status = 'Pending'
        $this.Detail = ''
        $this.ParentStep = $ParentStep
        $this.Level = if ($ParentStep) { $ParentStep.Level + 1 } else { 0 }
        $this.ContinueOnError = $ContinueOnError
        $this.StartTime = Get-Date
        $this.EndTime = $null
        if ($ParentStep) {
            $ParentStep.Children.Add($this)
        }
    }

    [string] ToString() {
        $dur = if ($this.EndTime) { ($this.EndTime - $this.StartTime) } else { [TimeSpan]::Zero }
        return "{0} [{1}] L{2} ({3:c})" -f $this.Name, $this.Status, $this.Level, $dur
    }
}
# endregion

# region Private\Functions\Complete-Step.ps1
function Complete-Step {
    [CmdletBinding()]
    param()

    $current = Get-CurrentStep
    if ($current) {
        $current.EndTime = Get-Date
        if ($null -ne $current.StartTime -and $null -ne $current.EndTime) {
            $current.Duration = ($current.EndTime - $current.StartTime)
        }
    }

    Invoke-Logger -Component 'StepManager' -Severity 'Verbose' -Message "Étape [$($current.Name)] terminée." -IndentLevel ($current.Level)


    # Dépile le niveau d'indentation contextuel (protégé pour runspace)
    [System.Threading.Monitor]::Enter($script:StepStateLock)
    try {
        if ($script:CurrentStepIndentStack) {
            $script:CurrentStepIndentStack = $script:CurrentStepIndentStack[0..($script:CurrentStepIndentStack.Count-2)]
            if ($script:CurrentStepIndentStack.Count -eq 0) {
                Remove-Variable -Name CurrentStepIndentStack -Scope Script -ErrorAction SilentlyContinue
            }
        }
    } finally { [System.Threading.Monitor]::Exit($script:StepStateLock) }

    # Pop current step and restore parent (if any)
    $null = Pop-Step
}
# endregion

# region Private\Functions\Get-CurrentStep.ps1
function Get-CurrentStep {
    [CmdletBinding()] param()
    return (Get-StepStackTop)
}
# endregion

# region Private\Functions\Invoke-Logger.ps1
<#
.SYNOPSIS
    Centralise l'affichage des messages de log pour StepManager.
.DESCRIPTION
    Permet d'afficher des messages typés (Info, Success, Warning, Error, Debug, Verbose) avec indentation et support d'un logger personnalisé.
    Si aucun logger n'est défini, affiche le message formaté dans la console.
    Les messages Debug ne sont affichés que si $DebugPreference le permet.
.PARAMETER Component
    Nom du composant ou de l'étape à l'origine du message.
.PARAMETER Message
    Le message à afficher.
.PARAMETER Severity
    Le niveau de sévérité du message : Info, Success, Warning, Error, Debug, Verbose.
.PARAMETER IndentLevel
    Niveau d'indentation pour l'affichage (entier, optionnel).
#>

function Invoke-Logger {
    param(
        [Parameter(Mandatory)] [string]$Component,
        [Parameter(Mandatory)] [string]$Message,
        [Parameter(Mandatory)] [ValidateSet('Info','Success','Warning','Error','Debug','Verbose')] [string]$Severity,
        [int]$IndentLevel = [int]::MinValue
    )
    # Indentation: respecte une valeur explicite sinon calcule depuis la step courante
    $explicitIndent = ($IndentLevel -ne [int]::MinValue)
    $finalIndent = if ($explicitIndent) { $IndentLevel } else { 0 }
    if (-not $explicitIndent) {
        if ($script:InsideStep) {
            try {
                $currentStep = Get-CurrentStep
                if ($null -ne $currentStep) { $finalIndent = $currentStep.Level + 1 } else { $finalIndent = 1 }
            } catch { $finalIndent = 1 }
        }
    }
    # Gestion du niveau Debug et Verbose
    if ($Severity -eq 'Debug') { if ($DebugPreference -eq 'SilentlyContinue') { return } }
    if ($Severity -eq 'Verbose') { if ($VerbosePreference -eq 'SilentlyContinue') { return } }
    $logger = Get-StepManagerLogger
    if ($null -eq $logger) {
        Write-StepMessage -Severity $Severity -Message ("[$Component] $Message") -IndentLevel $finalIndent
    } else {
        & $logger $Component $Message $Severity $finalIndent
    }
}
# endregion

# region Private\Functions\New-Step.ps1
<#
.SYNOPSIS
Crée une nouvelle étape et la pousse dans la pile d'exécution.

.DESCRIPTION
Crée un objet Step, gère l'imbrication et l'état, et affiche le nom de l'étape. Les messages sont affichés en gris par défaut.

.PARAMETER Name
Nom de l'étape à créer.

.PARAMETER ContinueOnError
Indique si l'exécution doit continuer en cas d'erreur dans l'étape.

.OUTPUTS
Step
#>

function New-Step {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Name,
        [switch]$ContinueOnError
    )

    if (-not $PSCmdlet.ShouldProcess($Name, 'Create step')) {
        return
    }

    $parent = Get-CurrentStep
    $step = [Step]::new($Name, $parent, $ContinueOnError.IsPresent)

    # Gestion d'une pile d'indentation contextuelle pour chaque Step imbriquée (protégée pour runspace)
    [System.Threading.Monitor]::Enter($script:StepStateLock)
    try {
        if (-not $script:CurrentStepIndentStack) { $script:CurrentStepIndentStack = @() }
        $script:CurrentStepIndentStack += ($step.Level + 1)
    }
    finally { [System.Threading.Monitor]::Exit($script:StepStateLock) }

    Push-Step -Step $step

    Invoke-Logger -Component 'StepManager' -Message "Création de l'étape : $Name" -Severity Verbose -IndentLevel $step.Level
    Invoke-Logger -Component 'StepManager' -Message "$Name" -Severity Info -IndentLevel $step.Level

    return $step
}
# endregion

# region Private\Functions\Set-Step.ps1
function Set-Step {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)] [ValidateSet('Pending','Success','Error')] [string]$Status,
        [string]$Detail = ''
    )

    $current = Get-CurrentStep
    if (-not $current) { return }
    if (-not $PSCmdlet.ShouldProcess($current.Name, "Set step status to $Status")) {
        return
    }

    $current.Status = $Status
    $current.Detail = $Detail

    if ($Status -eq 'Error') {
        # Laisser l'auto-indentation gérer le niveau quand non spécifié
        Invoke-Logger -Component 'StepManager' -Severity 'Error' -Message "Erreur dans l'étape [$($current.Name)] : $Detail"
    }
    else{
        Invoke-Logger -Component 'StepManager' -Severity 'Verbose' -Message "Étape [$($current.Name)] définie sur le statut : $Status"
    }
}
# endregion

# region Public\Invoke-Step.ps1
<#
.SYNOPSIS
Exécute un bloc de script dans une étape avec gestion d'état, d'imbrication et d'erreur.

.DESCRIPTION
Encapsule l'exécution d'un ScriptBlock dans une étape typée, avec gestion du statut (Success, Error), imbrication et option de poursuite sur erreur. Les messages sont affichés en gris par défaut.

.PARAMETER Name
Nom de l'étape à exécuter.

.PARAMETER ScriptBlock
Bloc de code à exécuter dans l'étape.

.PARAMETER ContinueOnError
Indique si l'exécution doit continuer en cas d'erreur dans l'étape. Par défaut : $false.

.OUTPUTS
Step

.EXAMPLE
Invoke-Step -Name 'Préparation' -ScriptBlock {
    # Instructions de préparation
}

.EXAMPLE
Invoke-Step -Name 'Installation' -ScriptBlock {
    Invoke-Step -Name 'Télécharger' -ScriptBlock {
        # Téléchargement
    } -ContinueOnError

    Invoke-Step -Name 'Configurer' -ScriptBlock {
        # Configuration
    }
}

.EXAMPLE
$items = 'A', 'B', 'C'
$steps = foreach ($item in $items) {
    Invoke-Step -Name "Traitement $item" -ScriptBlock {
        # Traitement spécifique à $item
        "Traitement de $item terminé."
    }
}
# $steps contient la liste des objets Step pour chaque élément

.EXAMPLE
Invoke-Step -Name 'Exemple' -ScriptBlock {
    throw 'Erreur volontaire'
} -ContinueOnError
# L'étape sera en statut 'Error', mais l'exécution continue

Invoke-Step -Name 'Exemple' -ScriptBlock {
    throw 'Erreur volontaire'
}
# L'étape sera en statut 'Error' (propagation possible selon le parent)

#>

function Invoke-Step {
    [CmdletBinding()]
    [OutputType('Step')]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            if ($_.Length -gt 80) { throw "Le nom de l'étape ne doit pas dépasser 80 caractères." }
            if ($_ -match '[\\/:*?"<>|]') { throw "Le nom de l'étape contient des caractères interdits (\\ / : * ? \"" < > |)." }
            return $true
        })]
        [string]$Name,
        [switch]$ContinueOnError = $false,
        [Parameter(Mandatory)]
        [ScriptBlock]$ScriptBlock,
        [switch]$PassThru
    )

    # Autoriser les ScriptBlock vides (no-op)
    $step = New-Step -Name $Name -ContinueOnError:$ContinueOnError.IsPresent
    $errorDetail = $null
    $threw = $false
    $shouldThrow = $false
    $script:InsideStep = $true
    try {
        $null = & $ScriptBlock
    }
    catch {
        $threw = $true
        $errorDetail = $_.Exception.Message
        Set-Step -Status Error -Detail $errorDetail | Out-Null
        # Propagation contrôlée
        $shouldThrow = -not $ContinueOnError.IsPresent
        if ($shouldThrow -and $step.ParentStep -and $step.ParentStep.ContinueOnError) {
            $shouldThrow = $false
        }
    }
    finally {
        $script:InsideStep = $false
        if (-not $threw) { Set-Step -Status Success | Out-Null }
        Complete-Step
    }
    if ($threw -and $shouldThrow) { throw }
    if ($PassThru) { return $step }
}
# endregion

# region Public\Write-Log.ps1
<#
.SYNOPSIS
    Fonction publique pour écrire un message de log utilisateur dans StepManager.
.DESCRIPTION
    Permet à l'utilisateur d'écrire un message dans le log StepManager, avec un niveau d'indentation adapté pour les logs utilisateurs (toujours un cran de plus que la Step courante).
.PARAMETER Message
    Le message à afficher.
.PARAMETER Severity
    Le niveau de sévérité du message : Info, Success, Warning, Error, Debug, Verbose.
.EXAMPLE
    Write-Log -Message 'Début du traitement' -Severity 'Info'
#>

function Write-Log {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string]$Message,
        [Parameter(Mandatory=$false)] [ValidateSet('Info','Success','Warning','Error','Debug','Verbose')] [string]$Severity = 'Info'
    )
    
    try {
        $currentStep = Get-CurrentStep
        if ($null -ne $currentStep) {
            $finalIndent = $currentStep.Level + 1
        } else {
            $finalIndent = 1
        }
    } catch {
        $finalIndent = 1
    }

    if ($Severity -eq 'Info') {
        $ForegroundColor = 'DarkGray'
    }
    
    $logger = Get-StepManagerLogger
    if ($null -eq $logger) {
        # Affiche uniquement le StepName pour éviter la duplication [Step][Component]
        Write-StepMessage -Severity $Severity -Message $Message -IndentLevel $finalIndent -StepName $($currentStep.Name) -ForegroundColor $ForegroundColor
    } else {
        $component = if ($null -ne $currentStep -and -not [string]::IsNullOrWhiteSpace($currentStep.Name)) {
            $currentStep.Name
        } else {
            'StepManager'
        }
        & $logger $component $Message $Severity $finalIndent
    }
}
# endregion

# Exports
Export-ModuleMember -Function Invoke-Step,Write-Log -Alias @()