Private/Utility/Repair-PowerShellEnvironment.ps1
|
function Repair-PowerShellEnvironment { <# .SYNOPSIS Attempts to repair issues identified by Test-PowerShellEnvironment. .DESCRIPTION Automatically or interactively fixes PowerShell environment issues including: - Sets UTF-8 encoding for current session - Optionally adds UTF-8 to profile for persistence - Installs and imports missing modules - Provides guidance for non-fixable issues (OS, PS version, Terminal) This function is designed to work with the output from Test-PowerShellEnvironment, but can also run independently by performing its own environment test first. Fixable issues: - UTF-8 Encoding (current session and profile) - Missing Modules (installation and import) Non-fixable issues (guidance provided): - Operating System (not Windows) - OS Version (Windows 10 1607+/Server 2016+ required) - PowerShell Version (5.1 or 7.4+ required) - Windows Terminal (user must install manually) .PARAMETER EnvironmentTest The hashtable returned by Test-PowerShellEnvironment containing diagnostic results. If not provided, Test-PowerShellEnvironment will be run automatically. .PARAMETER Force Bypasses all interactive prompts and applies all fixes automatically. Useful for automation scenarios and non-interactive execution. .PARAMETER SkipProfileUpdate Skips adding UTF-8 encoding to the PowerShell profile. Encoding will still be set for the current session. .PARAMETER SkipModuleInstall Skips installing missing modules. Will still attempt to import modules that are already available. .INPUTS System.Collections.Hashtable You can pipe the output from Test-PowerShellEnvironment to this function. .OUTPUTS PSCustomObject Returns a custom object with the following properties: - EncodingRepaired: Boolean - UTF-8 encoding was set for current session - ProfileUpdated: Boolean - UTF-8 was added to PowerShell profile - ModulesInstalled: String[] - Array of module names that were installed - ModulesImported: String[] - Array of module names that were imported - RemainingIssues: String[] - Array of issues that could not be fixed - Success: Boolean - Overall success status .EXAMPLE Repair-PowerShellEnvironment Runs environment test and interactively repairs issues with user prompts. .EXAMPLE Test-PowerShellEnvironment | Repair-PowerShellEnvironment Pipes test results directly to repair function for targeted fixes. .EXAMPLE Repair-PowerShellEnvironment -Force Automatically repairs all fixable issues without prompting. .EXAMPLE Repair-PowerShellEnvironment -SkipProfileUpdate Repairs issues but doesn't modify the PowerShell profile. .EXAMPLE $result = Repair-PowerShellEnvironment -Verbose if ($result.Success) { Write-Host "Environment repaired successfully" } Stores repair results and checks overall success status. .NOTES This function uses ShouldProcess for operations that modify the system. Use -WhatIf to preview changes without applying them. Use -Confirm to be prompted for each change. Critical environment issues (OS, OS version, PowerShell version) cannot be automatically fixed and will result in guidance messages only. .LINK Test-PowerShellEnvironment #> [CmdletBinding(SupportsShouldProcess)] [OutputType([PSCustomObject])] param( [Parameter(ValueFromPipeline)] [hashtable]$EnvironmentTest, [Parameter()] [switch]$Force, [Parameter()] [switch]$SkipProfileUpdate, [Parameter()] [switch]$SkipModuleInstall ) #requires -Version 5.1 begin { Write-Verbose "Starting PowerShell environment repair..." # Initialize result tracking $encodingRepaired = $false $profileUpdated = $false $modulesInstalled = @() $modulesImported = @() $remainingIssues = @() } process { try { # Run environment test if not provided if (-not $EnvironmentTest) { Write-Verbose "No environment test provided. Running Test-PowerShellEnvironment..." $EnvironmentTest = Test-PowerShellEnvironment } # Critical issues that cannot be fixed (provide guidance only) if (-not $EnvironmentTest.IsWindows) { $remainingIssues += "Operating System: Locksmith 2 requires Windows" Write-Warning "Locksmith 2 is only supported on Windows operating systems." Write-Warning "Current OS cannot be changed automatically." } if (-not $EnvironmentTest.IsSupportedOS) { $remainingIssues += "OS Version: Requires Windows 10 1607+/Server 2016+" Write-Warning "Locksmith 2 requires Windows 10 Anniversary Update (1607) or Windows Server 2016 or later." Write-Warning "Please upgrade your operating system." } if (-not $EnvironmentTest.IsSupportedPS) { $remainingIssues += "PowerShell Version: Requires 5.1 or 7.4+" Write-Warning "Locksmith 2 requires Windows PowerShell 5.1 or PowerShell 7.4+." Write-Warning "Download PowerShell 7.4+ from: https://aka.ms/powershell" Write-Warning "Or install via: winget install Microsoft.PowerShell" } # Fix 1: UTF-8 Encoding (current session) if (-not $EnvironmentTest.IsUtf8) { Write-Verbose "UTF-8 encoding is not set for current session." if ($Force -or $PSCmdlet.ShouldProcess("Console Output Encoding", "Set to UTF-8")) { Update-OutputEncoding $encodingRepaired = $true Write-Verbose "UTF-8 encoding set for current session." } } else { Write-Verbose "UTF-8 encoding already configured for current session." } # Fix 2: UTF-8 Encoding (profile - persistent) if (-not $SkipProfileUpdate -and -not $EnvironmentTest.IsUtf8) { Write-Verbose "Checking if UTF-8 should be added to profile..." $updateProfile = $false if ($Force) { $updateProfile = $true } else { $choice = Read-Choice -Question "Add UTF-8 encoding to your PowerShell profile for future sessions?" -Options @('y', 'n') -Default 'y' $updateProfile = ($choice -eq 'y') } if ($updateProfile) { if ($PSCmdlet.ShouldProcess("PowerShell Profile ($PROFILE)", "Add UTF-8 encoding configuration")) { Update-DollarSignProfile $profileUpdated = $true Write-Verbose "UTF-8 encoding added to PowerShell profile." } } else { Write-Verbose "Skipped profile update per user choice." } } # Fix 3: Missing Modules if (-not $EnvironmentTest.AllModulesLoaded) { Write-Verbose "Processing missing modules: $($EnvironmentTest.MissingModules -join ', ')" foreach ($moduleName in $EnvironmentTest.MissingModules) { Write-Verbose "Processing module: $moduleName" # PSCertutil is mandatory for Locksmith 2 $isMandatory = ($moduleName -eq 'PSCertutil') # Check if module is available (installed but not loaded) $isAvailable = Test-IsModuleAvailable -Name $moduleName if ($isAvailable) { # Module is installed, just needs import Write-Verbose "Module '$moduleName' is available. Importing..." if ($PSCmdlet.ShouldProcess($moduleName, "Import module")) { try { Import-Module -Name $moduleName -Global -ErrorAction Stop $modulesImported += $moduleName Write-Verbose "Module '$moduleName' imported successfully." } catch { if ($isMandatory) { $errorRecord = [System.Management.Automation.ErrorRecord]::new( [System.Exception]::new("Failed to import required module '$moduleName': $_"), 'RequiredModuleImportFailed', [System.Management.Automation.ErrorCategory]::NotInstalled, $moduleName ) $PSCmdlet.ThrowTerminatingError($errorRecord) } else { Write-Warning "Failed to import module '$moduleName': $_" $remainingIssues += "Module Import: $moduleName failed" } } } } else { # Module needs installation if (-not $SkipModuleInstall) { Write-Verbose "Module '$moduleName' is not available. Installation required..." $result = Install-NeededModule -Name $moduleName -Force:$Force -Mandatory:$isMandatory if ($result) { $modulesInstalled += $moduleName $modulesImported += $moduleName Write-Verbose "Module '$moduleName' installed and imported successfully." } else { if ($isMandatory) { $errorRecord = [System.Management.Automation.ErrorRecord]::new( [System.Exception]::new("Required module '$moduleName' could not be installed."), 'RequiredModuleInstallFailed', [System.Management.Automation.ErrorCategory]::NotInstalled, $moduleName ) $PSCmdlet.ThrowTerminatingError($errorRecord) } else { Write-Warning "Module '$moduleName' could not be installed." $remainingIssues += "Module Install: $moduleName failed" } } } else { if ($isMandatory) { $errorRecord = [System.Management.Automation.ErrorRecord]::new( [System.Exception]::new("Required module '$moduleName' is missing and SkipModuleInstall is enabled."), 'RequiredModuleMissing', [System.Management.Automation.ErrorCategory]::NotInstalled, $moduleName ) $PSCmdlet.ThrowTerminatingError($errorRecord) } else { Write-Verbose "Skipping installation of '$moduleName' (SkipModuleInstall enabled)." $remainingIssues += "Module Missing: $moduleName not installed" } } } } } else { Write-Verbose "All required modules are already loaded." } # Guidance 1: Windows Terminal recommendation if (-not $EnvironmentTest.IsWindowsTerminal) { Write-Warning "For the best visual experience, install Windows Terminal:" Write-Host " - Microsoft Store: https://aka.ms/terminal" -ForegroundColor Cyan Write-Host " - Or run: winget install Microsoft.WindowsTerminal" -ForegroundColor Cyan $remainingIssues += "Terminal: Windows Terminal not detected (optional)" } # Guidance 2: PowerShell Core recommendation if (-not $EnvironmentTest.IsPowerShellCore -and $EnvironmentTest.IsSupportedPS) { Write-Warning "You're running Windows PowerShell 5.1. For full interactive features, upgrade to PowerShell 7.4+:" Write-Host " - Download: https://aka.ms/powershell" -ForegroundColor Cyan Write-Host " - Or run: winget install Microsoft.PowerShell" -ForegroundColor Cyan Write-Host " - Locksmith 2 will continue in headless mode." -ForegroundColor Yellow $remainingIssues += "PowerShell Edition: Desktop (5.1) - Interactive mode requires Core 7.4+" } # Build result object $success = ($remainingIssues.Count -eq 0) -or ($remainingIssues | Where-Object { $_ -notlike "*optional*" -and $_ -notlike "*PowerShell Edition*" }).Count -eq 0 $result = [PSCustomObject]@{ EncodingRepaired = $encodingRepaired ProfileUpdated = $profileUpdated ModulesInstalled = $modulesInstalled ModulesImported = $modulesImported RemainingIssues = $remainingIssues Success = $success } Write-Verbose "PowerShell environment repair complete." Write-Verbose "Success: $success" Write-Verbose "Encoding Repaired: $encodingRepaired" Write-Verbose "Profile Updated: $profileUpdated" Write-Verbose "Modules Installed: $($modulesInstalled.Count)" Write-Verbose "Modules Imported: $($modulesImported.Count)" Write-Verbose "Remaining Issues: $($remainingIssues.Count)" return $result } catch { $errorRecord = [System.Management.Automation.ErrorRecord]::new( $_.Exception, 'EnvironmentRepairFailed', [System.Management.Automation.ErrorCategory]::NotSpecified, $EnvironmentTest ) $PSCmdlet.ThrowTerminatingError($errorRecord) } } } |