Stepper.psm1
|
function Clear-StepperState { <# .SYNOPSIS Removes the saved state file. .DESCRIPTION Deletes the stepper state file from disk, effectively resetting all progress. Supports -WhatIf and -Confirm for safety. .PARAMETER Path The path to the state file to remove. If not specified, uses the default path from Get-StateFilePath. .EXAMPLE Clear-StepperState Removes the default state file. .EXAMPLE Clear-StepperState -WhatIf Shows what would happen without actually removing the file. .EXAMPLE Clear-StepperState -Path 'C:\Temp\my-state.json' Removes a custom state file. .NOTES If the file doesn't exist, a warning is displayed. #> [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $false)] [string]$Path = (Get-StateFilePath) ) if (Test-Path $Path) { if ($PSCmdlet.ShouldProcess($Path, "Remove stepper state file")) { try { Remove-Item -Path $Path -Force -ErrorAction Stop Write-Host "State file removed: $Path" -ForegroundColor Green Write-Verbose "State successfully cleared" } catch { Write-Error "Failed to remove state file '$Path': $_" } } } else { Write-Warning "No state file found at: $Path" } } function Get-StateFilePath { <# .SYNOPSIS Returns the path to the state file. .DESCRIPTION Returns the full path to the stepper state file, creating the directory if it doesn't exist. .PARAMETER FileName The name of the state file. Default is 'stepper-state.json'. .OUTPUTS System.String - The full path to the state file. .EXAMPLE $statePath = Get-StateFilePath Gets the default state file path. .EXAMPLE $statePath = Get-StateFilePath -FileName 'custom-state.json' Gets a custom state file path. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $false)] [string]$FileName = 'stepper-state.json' ) # Store in user profile for persistence across sessions # Alternative: Use temp folder for session-only persistence $stateDir = Join-Path -Path $env:USERPROFILE -ChildPath '.stepper' if (-not (Test-Path $stateDir)) { New-Item -Path $stateDir -ItemType Directory -Force | Out-Null Write-Verbose "Created state directory: $stateDir" } $statePath = Join-Path -Path $stateDir -ChildPath $FileName Write-Verbose "State file path: $statePath" return $statePath } function Get-StepperState { <# .SYNOPSIS Loads the stepper state from file, or creates a new one. .DESCRIPTION Attempts to load a saved stepper state from disk. If the file doesn't exist or can't be loaded, returns a new empty state. Converts the JSON PSCustomObject back to a hashtable for easier manipulation in PowerShell. .PARAMETER Path The path to the state file. If not specified, uses the default path from Get-StateFilePath. .OUTPUTS System.Collections.Hashtable - The loaded or new stepper state. .EXAMPLE $state = Get-StepperState Loads the state from the default location or creates new. .EXAMPLE $state = Get-StepperState -Path 'C:\Temp\my-state.json' Loads the state from a custom location. .NOTES If loading fails, warnings are displayed and a new state is returned. This ensures the stepper can always proceed. #> [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory = $false)] [string]$Path = (Get-StateFilePath) ) if (Test-Path $Path) { try { Write-Verbose "Loading state from: $Path" $json = Get-Content -Path $Path -Raw -Encoding UTF8 $stateObject = $json | ConvertFrom-Json # Convert PSCustomObject back to hashtable for easier manipulation $state = @{ Version = $stateObject.Version StepperId = $stateObject.StepperId StartedAt = $stateObject.StartedAt LastUpdated = $stateObject.LastUpdated CompletedSteps = @($stateObject.CompletedSteps) CurrentStepIndex = $stateObject.CurrentStepIndex Status = $stateObject.Status StepResults = @{} Metadata = @{} } # Convert nested objects if ($stateObject.StepResults) { $stateObject.StepResults.PSObject.Properties | ForEach-Object { $state.StepResults[$_.Name] = $_.Value } } if ($stateObject.Metadata) { $stateObject.Metadata.PSObject.Properties | ForEach-Object { $state.Metadata[$_.Name] = $_.Value } } Write-Verbose "State loaded successfully (ID: $($state.StepperId))" return $state } catch { Write-Warning "Failed to load state file: $_" Write-Warning "Starting with fresh state..." return New-StepperState } } else { Write-Verbose "No existing state file found, creating new state" return New-StepperState } } function Invoke-StepperStep { <# .SYNOPSIS Executes a single stepper step and tracks the result. .DESCRIPTION Runs the script block for a given stepper step, tracks timing, handles errors, and records results in the state object. .PARAMETER Step A hashtable containing the step definition with Name, Description, and ScriptBlock properties. .PARAMETER State The stepper state hashtable to update with results. .PARAMETER AllResults Optional hashtable containing results from all previously completed steps. Passed to steps that need access to prior results. .OUTPUTS System.Boolean - $true if step succeeded, $false if it failed. .EXAMPLE $success = Invoke-StepperStep -Step $stepDef -State $state Executes a step and updates the state. .EXAMPLE $success = Invoke-StepperStep -Step $stepDef -State $state -AllResults $allResults Executes a step that needs access to previous results. .NOTES Results are automatically added to the state object. Duration is tracked automatically. Errors are caught and logged in the state. #> [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory = $true)] [hashtable]$Step, [Parameter(Mandatory = $true)] [hashtable]$State, [Parameter(Mandatory = $false)] [hashtable]$AllResults = @{} ) $stepName = $Step.Name $startTime = Get-Date Write-Verbose "Executing step: $stepName" try { # Execute the step's script block # Check if step needs access to all results (using AcceptsAllResults property from config) $result = if ($Step.AcceptsAllResults -eq $true) { Write-Verbose "Step accepts AllResults parameter, passing previous results" & $Step.ScriptBlock -AllResults $AllResults } else { Write-Verbose "Step does not accept AllResults, executing directly" & $Step.ScriptBlock } $duration = (Get-Date) - $startTime # Record the result $State.StepResults[$stepName] = @{ Status = 'Completed' CompletedAt = Get-Date -Format 'o' Duration = "$([math]::Round($duration.TotalSeconds, 2))s" Result = $result } # Mark as completed if ($State.CompletedSteps -notcontains $stepName) { $State.CompletedSteps += $stepName } Write-Host "`n ✓ Step completed successfully in $([math]::Round($duration.TotalSeconds, 2))s" -ForegroundColor Green Write-Verbose "Step '$stepName' completed successfully" return $true } catch { $duration = (Get-Date) - $startTime # Record the failure $State.StepResults[$stepName] = @{ Status = 'Failed' FailedAt = Get-Date -Format 'o' Duration = "$([math]::Round($duration.TotalSeconds, 2))s" Error = $_.Exception.Message ErrorDetails = $_.ScriptStackTrace } Write-Host "`n ✗ Step failed: $($_.Exception.Message)" -ForegroundColor Red Write-Host " $($_.ScriptStackTrace)" -ForegroundColor DarkRed Write-Verbose "Step '$stepName' failed: $($_.Exception.Message)" return $false } } function New-StepperState { <# .SYNOPSIS Creates a new empty stepper state object. .DESCRIPTION Initializes a new hashtable containing the default structure for tracking stepper progress, including metadata about the environment. .OUTPUTS System.Collections.Hashtable - A new stepper state object. .EXAMPLE $state = New-StepperState Creates a new stepper state with default values. .NOTES The state object includes: - Version: State file format version - StepperId: Unique identifier for this stepper run - StartedAt: Timestamp when stepper started - LastUpdated: Timestamp of last state update - CompletedSteps: Array of completed step names - CurrentStepIndex: Index of current/next step - Status: Current status (InProgress, Completed, Failed) - StepResults: Hashtable of results per step - Metadata: Environment information #> [CmdletBinding()] [OutputType([hashtable])] param() Write-Verbose "Creating new stepper state" return @{ Version = '1.0.0' StepperId = [guid]::NewGuid().ToString() StartedAt = Get-Date -Format 'o' LastUpdated = Get-Date -Format 'o' CompletedSteps = @() CurrentStepIndex = 0 Status = 'InProgress' StepResults = @{} Metadata = @{ ComputerName = $env:COMPUTERNAME UserName = $env:USERNAME PSVersion = $PSVersionTable.PSVersion.ToString() Domain = $env:USERDNSDOMAIN } } } function Save-StepperState { <# .SYNOPSIS Saves the stepper state to a JSON file. .DESCRIPTION Serializes the stepper state hashtable to JSON format and saves it to disk. Updates the LastUpdated timestamp automatically. .PARAMETER State The stepper state hashtable to save. .PARAMETER Path The path where the state file should be saved. If not specified, uses the default path from Get-StateFilePath. .EXAMPLE Save-StepperState -State $state Saves the state to the default location. .EXAMPLE Save-StepperState -State $state -Path 'C:\Temp\my-state.json' Saves the state to a custom location. .NOTES The state is saved with indentation for readability. Throws an error if save fails. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [hashtable]$State, [Parameter(Mandatory = $false)] [string]$Path = (Get-StateFilePath) ) try { # Update timestamp $State.LastUpdated = Get-Date -Format 'o' Write-Verbose "Saving state to: $Path" # Convert to JSON with good depth for nested objects $json = $State | ConvertTo-Json -Depth 10 -Compress:$false # Save to file $json | Set-Content -Path $Path -Force -Encoding UTF8 Write-Verbose "State saved successfully" Write-Debug "State content: $json" } catch { Write-Error "Failed to save state to '$Path': $_" throw } } function Show-StepperHeader { <# .SYNOPSIS Displays a formatted header for the stepper. .DESCRIPTION Shows a visually formatted header with the stepper tool name and version at the start of the stepper. .PARAMETER Title The title to display in the header. Default is "Multi-Step stepper Tool". .PARAMETER Version The version string to display. Default is "1.0.0". .EXAMPLE Show-StepperHeader Displays the default header. .EXAMPLE Show-StepperHeader -Title "Security stepper" -Version "2.0.0" Displays a custom header. #> [CmdletBinding()] param( [Parameter(Mandatory = $false)] [string]$Title = "Multi-Step stepper Tool", [Parameter(Mandatory = $false)] [string]$Version = "1.0.0" ) $border = "=" * 70 Write-Host $border -ForegroundColor Cyan Write-Host " $Title" -ForegroundColor Cyan Write-Host " Version $Version" -ForegroundColor Cyan Write-Host $border -ForegroundColor Cyan Write-Host "" } function Show-StepperProgress { <# .SYNOPSIS Displays current progress summary. .DESCRIPTION Shows a formatted summary of the stepper progress including: - stepper ID - Start and update timestamps - Current status - Completed steps with durations - Overall percentage complete .PARAMETER State The stepper state hashtable containing progress information. .PARAMETER TotalSteps The total number of steps in the stepper. .EXAMPLE Show-StepperProgress -State $state -TotalSteps 5 Displays progress for a 5-step stepper. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [hashtable]$State, [Parameter(Mandatory = $true)] [int]$TotalSteps ) $completed = $State.CompletedSteps.Count $percentComplete = if ($TotalSteps -gt 0) { [math]::Round(($completed / $TotalSteps) * 100, 1) } else { 0 } Write-Host "`nProgress Summary:" -ForegroundColor Cyan Write-Host (" " + ("-" * 50)) -ForegroundColor Gray Write-Host " stepper ID : $($State.StepperId)" -ForegroundColor Gray Write-Host " Started At : $($State.StartedAt)" -ForegroundColor Gray Write-Host " Last Updated : $($State.LastUpdated)" -ForegroundColor Gray Write-Host " Status : $($State.Status)" -ForegroundColor $( if ($State.Status -eq 'Completed') { 'Green' } elseif ($State.Status -eq 'Failed') { 'Red' } else { 'Yellow' } ) Write-Host " Completed Steps : $completed / $TotalSteps ($percentComplete%)" -ForegroundColor Gray Write-Host (" " + ("-" * 50)) -ForegroundColor Gray if ($State.CompletedSteps.Count -gt 0) { Write-Host "`nCompleted Steps:" -ForegroundColor Green $State.CompletedSteps | ForEach-Object { $stepResult = $State.StepResults[$_] $duration = if ($stepResult.Duration) { " ($($stepResult.Duration))" } else { "" } Write-Host " ✓ $_$duration" -ForegroundColor Green } } Write-Host "" } function Show-StepperStepHeader { <# .SYNOPSIS Displays a header for the current stepper step. .DESCRIPTION Shows a formatted header indicating the current step number, total steps, and step name. .PARAMETER StepName The name of the current step. .PARAMETER StepNumber The current step number (1-based). .PARAMETER TotalSteps The total number of steps in the stepper. .EXAMPLE Show-StepperStepHeader -StepName "Environment Check" -StepNumber 1 -TotalSteps 5 Displays header for step 1 of 5. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$StepName, [Parameter(Mandatory = $true)] [int]$StepNumber, [Parameter(Mandatory = $true)] [int]$TotalSteps ) $border = "-" * 70 Write-Host "`n$border" -ForegroundColor Cyan Write-Host "Step $StepNumber of ${TotalSteps}: $StepName" -ForegroundColor Cyan Write-Host "$border" -ForegroundColor Cyan } function Test-StepperStateValidity { <# .SYNOPSIS Validates if the saved state is still relevant. .DESCRIPTION Checks the stepper state for validity by verifying: - Version compatibility - Age of the state (default max 7 days) - Valid timestamp format .PARAMETER State The stepper state hashtable to validate. .PARAMETER MaxAgeDays Maximum age in days before state is considered stale. Default is 7 days. .OUTPUTS System.Boolean - $true if state is valid, $false otherwise. .EXAMPLE if (Test-StepperStateValidity -State $state) { # State is valid, proceed } Validates state with default 7-day age limit. .EXAMPLE if (Test-StepperStateValidity -State $state -MaxAgeDays 30) { # State is valid, proceed } Validates state with custom 30-day age limit. .NOTES Displays warnings if state is invalid. #> [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory = $true)] [hashtable]$State, [Parameter(Mandatory = $false)] [int]$MaxAgeDays = 7 ) # Check version compatibility if ($State.Version -ne '1.0.0') { Write-Warning "State file version mismatch. Expected 1.0.0, found $($State.Version)" return $false } # Check age if ($State.LastUpdated) { try { $lastUpdate = [DateTime]::Parse($State.LastUpdated) $age = (Get-Date) - $lastUpdate if ($age.TotalDays -gt $MaxAgeDays) { Write-Warning "State is $([math]::Round($age.TotalDays, 1)) days old (max: $MaxAgeDays days)" return $false } Write-Verbose "State age: $([math]::Round($age.TotalDays, 1)) days (valid)" } catch { Write-Warning "Invalid LastUpdated timestamp in state: $($State.LastUpdated)" return $false } } else { Write-Warning "State missing LastUpdated timestamp" return $false } Write-Verbose "State validation passed" return $true } function Get-StepperSteps { <# .SYNOPSIS Loads stepper steps from JSON configuration. .DESCRIPTION Reads the stepper-config.json file and dynamically loads step scripts from the configured paths. Returns an array of hashtables with Name, Description, and ScriptBlock properties. Steps are loaded from individual .ps1 files in the Steps directory. Only enabled steps are included in the returned array. .PARAMETER ConfigPath Path to the configuration JSON file. Defaults to stepper-config.json in the module root directory. .OUTPUTS System.Array - Array of step definition hashtables. .EXAMPLE $steps = Get-StepperSteps foreach ($step in $steps) { # Process each step } Gets all enabled stepper steps from the configuration. .NOTES Configuration file structure: { "stepperSteps": [ { "name": "StepName", "description": "Step description", "scriptPath": "Steps/Step-ScriptName.ps1", "enabled": true, "order": 1, "acceptsAllResults": false } ] } To add new steps: 1. Create a new .ps1 file in the Steps directory 2. Add an entry to stepper-config.json 3. Set "enabled": true and assign an order number To disable a step without deleting it: - Set "enabled": false in the JSON configuration #> [CmdletBinding()] [OutputType([array])] param( [Parameter(Mandatory = $false)] [string]$ConfigPath ) Write-Verbose "Loading stepper step configuration" # Determine module root directory $moduleRoot = Split-Path -Parent $PSScriptRoot # Default config path if (-not $ConfigPath) { $ConfigPath = Join-Path -Path $moduleRoot -ChildPath 'stepper-config.json' } # Validate config file exists if (-not (Test-Path -Path $ConfigPath)) { throw "Configuration file not found: $ConfigPath" } Write-Verbose "Reading configuration from: $ConfigPath" # Load and parse JSON configuration try { $configContent = Get-Content -Path $ConfigPath -Raw -ErrorAction Stop $config = $configContent | ConvertFrom-Json -ErrorAction Stop } catch { throw "Failed to parse configuration file: $_" } # Validate configuration structure if (-not $config.stepperSteps) { throw "Invalid configuration: 'stepperSteps' property not found" } Write-Verbose "Found $($config.stepperSteps.Count) step(s) in configuration" # Build step definitions $steps = @() # Sort by order and filter to enabled only $enabledSteps = $config.stepperSteps | Where-Object { $_.enabled -eq $true } | Sort-Object -Property order Write-Verbose "Processing $($enabledSteps.Count) enabled step(s)" foreach ($stepConfig in $enabledSteps) { Write-Verbose "Loading step: $($stepConfig.name)" # Resolve script path (relative to module root) $scriptPath = Join-Path -Path $moduleRoot -ChildPath $stepConfig.scriptPath # Validate script file exists if (-not (Test-Path -Path $scriptPath)) { Write-Warning "Step script not found: $scriptPath (skipping step '$($stepConfig.name)')" continue } Write-Verbose " Script path: $scriptPath" Write-Verbose " Accepts AllResults: $($stepConfig.acceptsAllResults)" # Load script content try { $scriptContent = Get-Content -Path $scriptPath -Raw -ErrorAction Stop # Create scriptblock $scriptBlock = [ScriptBlock]::Create($scriptContent) # Build step definition $stepDefinition = @{ Name = $stepConfig.name Description = $stepConfig.description ScriptBlock = $scriptBlock AcceptsAllResults = $stepConfig.acceptsAllResults } $steps += $stepDefinition Write-Verbose " ✓ Successfully loaded step: $($stepConfig.name)" } catch { Write-Warning "Failed to load step '$($stepConfig.name)': $_" continue } } Write-Verbose "Successfully loaded $($steps.Count) step(s)" if ($steps.Count -eq 0) { Write-Warning "No enabled steps were loaded from configuration" } return $steps } function Reset-StepperState { <# .SYNOPSIS Clears all saved stepper state. .DESCRIPTION Removes the saved state file, effectively resetting the stepper to allow starting fresh. Prompts for confirmation before removing. Supports -WhatIf and -Confirm parameters. .EXAMPLE Reset-StepperState Prompts for confirmation then clears the state. .EXAMPLE Reset-StepperState -Confirm:$false Clears the state without prompting. .EXAMPLE Reset-StepperState -WhatIf Shows what would happen without actually clearing state. .NOTES This action cannot be undone. All stepper progress will be lost. #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param() Show-StepperHeader $statePath = Get-StateFilePath if (Test-Path $statePath) { if ($PSCmdlet.ShouldProcess($statePath, "Remove stepper state file and reset all progress")) { Clear-StepperState -Confirm:$false Write-Host "`nstepper state has been reset." -ForegroundColor Green Write-Host "Run 'Start-Stepper' to begin a new stepper." -ForegroundColor Cyan } } else { Write-Host "No saved stepper state found." -ForegroundColor Yellow Write-Host "Nothing to reset." -ForegroundColor Gray } } function Show-StepperStatus { <# .SYNOPSIS Displays the current stepper progress without running any steps. .DESCRIPTION Shows the progress summary of the current or last stepper including completed steps, current status, and overall progress percentage. Useful for checking on stepper status without starting or resuming. .EXAMPLE Show-StepperStatus Displays current stepper progress. .NOTES If no stepper state exists, indicates that no stepper has been started. #> [CmdletBinding()] param() Show-StepperHeader $state = Get-StepperState $steps = Get-StepperSteps if ($state.CompletedSteps.Count -eq 0 -and $state.Status -eq 'InProgress' -and [DateTime]::Parse($state.StartedAt) -gt (Get-Date).AddMinutes(-1)) { # This is a brand new state that was just created Write-Host "No stepper progress found." -ForegroundColor Yellow Write-Host "Run 'Start-Stepper' to begin a new stepper." -ForegroundColor Gray } else { Show-StepperProgress -State $state -TotalSteps $steps.Count if ($state.Status -eq 'InProgress') { Write-Host "Run 'Start-Stepper -Resume' to continue the stepper." -ForegroundColor Cyan } elseif ($state.Status -eq 'Failed') { Write-Host "Run 'Start-Stepper -Resume' to retry from the failed step." -ForegroundColor Yellow } else { Write-Host "stepper is complete. Run 'Reset-StepperState' to start fresh." -ForegroundColor Green } } } function Start-Stepper { <# .SYNOPSIS Main function to run the multi-step stepper. .DESCRIPTION Orchestrates the execution of all stepper steps, manages state persistence, handles errors, and provides progress feedback. By default, resumes from the last saved state. Use -Fresh to start over. .PARAMETER Fresh Start a completely new stepper, ignoring any saved state. .EXAMPLE Start-Stepper Resumes from the last checkpoint (default behavior). .EXAMPLE Start-Stepper -Fresh Starts a completely new stepper from the beginning. .NOTES State is automatically saved after each step. The stepper can be safely interrupted and resumed later. Completed steps can be skipped or re-run interactively. #> [CmdletBinding()] param( [Parameter(Mandatory = $false)] [switch]$Fresh ) Show-StepperHeader # Get all stepper steps $steps = Get-StepperSteps $totalSteps = $steps.Count Write-Verbose "Total steps defined: $totalSteps" # Load or create state (default is to resume) $state = if ($Fresh) { # User explicitly wants to start fresh Write-Host "Starting fresh stepper (ignoring saved state)..." -ForegroundColor Cyan New-StepperState } else { # Default behavior: try to resume $existingState = Get-StepperState # Default behavior: try to resume $existingState = Get-StepperState if ($existingState.Status -eq 'Completed') { Write-Host "Previous stepper was completed." -ForegroundColor Yellow Write-Host "Do you want to start a new stepper? (Y/N): " -NoNewline -ForegroundColor Yellow $response = Read-Host if ($response -eq 'Y') { New-StepperState } else { Write-Host "Exiting..." -ForegroundColor Gray return } } elseif (-not (Test-StepperStateValidity -State $existingState)) { Write-Host "Saved state is invalid or too old." -ForegroundColor Yellow Write-Host "Do you want to start fresh? (Y/N): " -NoNewline -ForegroundColor Yellow $response = Read-Host if ($response -eq 'Y') { Clear-StepperState -Confirm:$false New-StepperState } else { Write-Host "Using existing state anyway..." -ForegroundColor Yellow $existingState } } else { Write-Host "Resuming from saved state..." -ForegroundColor Cyan $existingState } } # Show current progress Show-StepperProgress -State $state -TotalSteps $totalSteps # Determine starting point $startIndex = $state.CurrentStepIndex if ($startIndex -ge $totalSteps) { $startIndex = 0 } Write-Verbose "Starting from step index: $startIndex" # Execute steps for ($i = $startIndex; $i -lt $totalSteps; $i++) { $step = $steps[$i] Show-StepperStepHeader -StepName $step.Name -StepNumber ($i + 1) -TotalSteps $totalSteps Write-Host " $($step.Description)" -ForegroundColor Gray Write-Host "" # Check if already completed if ($state.CompletedSteps -contains $step.Name) { Write-Host " This step was already completed." -ForegroundColor Yellow Write-Host " Do you want to skip it? (Y/N): " -NoNewline -ForegroundColor Yellow $skip = Read-Host if ($skip -eq 'Y') { Write-Host " Skipping..." -ForegroundColor Gray continue } else { Write-Host " Re-running..." -ForegroundColor Gray } } # Prepare all results for steps that need them $allResults = @{} foreach ($completedStep in $state.CompletedSteps) { if ($state.StepResults.ContainsKey($completedStep)) { $allResults[$completedStep] = $state.StepResults[$completedStep].Result } } # Execute the step $success = Invoke-StepperStep -Step $step -State $state -AllResults $allResults # Update current step index $state.CurrentStepIndex = $i + 1 # Save state after each step Save-StepperState -State $state if (-not $success) { $state.Status = 'Failed' Save-StepperState -State $state Write-Host "`nstepper stopped due to error." -ForegroundColor Red Write-Host "State saved. You can resume later with the -Resume switch." -ForegroundColor Yellow Write-Host "State file location: $(Get-StateFilePath)" -ForegroundColor Gray return } # Prompt to continue after each step (except last) if ($i -lt ($totalSteps - 1)) { Write-Host "`n Press Enter to continue to the next step, or Ctrl+C to stop..." -ForegroundColor Cyan Read-Host | Out-Null } } # Mark as completed $state.Status = 'Completed' $state.CompletedAt = Get-Date -Format 'o' Save-StepperState -State $state # Show final summary Write-Host "`n" + ("=" * 70) -ForegroundColor Green Write-Host " stepper Complete!" -ForegroundColor Green Write-Host ("=" * 70) -ForegroundColor Green Show-StepperProgress -State $state -TotalSteps $totalSteps $totalDuration = ([DateTime]::Parse($state.CompletedAt)) - ([DateTime]::Parse($state.StartedAt)) Write-Host "Total Duration: $([math]::Round($totalDuration.TotalMinutes, 2)) minutes`n" -ForegroundColor Gray } # Export functions and aliases as required Export-ModuleMember -Function @('Get-StepperSteps', 'Reset-StepperState', 'Show-StepperStatus', 'Start-Stepper') -Alias @() |