Update-MicrosoftGraph.ps1
|
<#
.SYNOPSIS Performs a clean installation of Microsoft Graph PowerShell modules. .DESCRIPTION This script provides a comprehensive solution for resolving Microsoft Graph PowerShell module version conflicts and ensuring a clean, consistent installation. It is designed to address common issues that occur when multiple versions of modules are installed, which can cause assembly loading errors and command failures. The script performs the following operations: 1. SESSION CLEANUP Removes all currently loaded Microsoft Graph modules from the PowerShell session to prevent file locking issues during uninstallation. 2. MODULE UNINSTALLATION (Iterative) Systematically uninstalls all installed modules using an iterative approach with garbage collection to handle dependencies and file locks. Uses both Get-InstalledModule and Get-Module -ListAvailable for comprehensive detection. 3. FOLDER CLEANUP Scans common PowerShell module directories for any leftover module folders that may have been orphaned, and removes them to ensure a clean slate. 4. FRESH INSTALLATION Installs your choice of Microsoft.Graph and/or Microsoft.Graph.Beta modules from the PowerShell Gallery. 5. VALIDATION Verifies the installation was successful and confirms that all module versions are aligned to prevent future version mismatch issues. This script is particularly useful when encountering errors such as: - "Assembly with same name is already loaded" - "Could not load file or assembly 'Microsoft.Graph.Authentication'" - Commands not being recognized after module updates - Multiple authentication prompts when using Graph/Entra cmdlets .INPUTS None. This script does not accept pipeline input. .OUTPUTS Console output showing the progress and results of each step. Upon completion, displays a summary of installed module versions. .EXAMPLE .\Update-MicrosoftGraph.ps1 Runs the script to perform a complete reset and fresh installation of Microsoft Graph modules. .EXAMPLE powershell -ExecutionPolicy Bypass -File .\Update-MicrosoftGraph.ps1 Runs the script with execution policy bypass if scripts are restricted. .NOTES File Name : Update-MicrosoftGraph.ps1 Author : Mark Orr Prerequisite : PowerShell 5.1 or later Internet connectivity to PowerShell Gallery Administrator rights may be needed for system-wide module locations Version : 1.1 .LINK https://learn.microsoft.com/en-us/powershell/microsoftgraph/installation .LINK https://www.powershellgallery.com/packages/Microsoft.Graph #> # ============================================================ # Self-elevation to Administrator if not already elevated # ============================================================ $IsAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $IsAdmin) { Write-Host "" Write-Host "This script requires Administrator privileges." -ForegroundColor Yellow Write-Host "Elevating to Administrator..." -ForegroundColor Yellow Write-Host "" # Build the argument list to re-run this script $ScriptPath = $MyInvocation.MyCommand.Definition try { # Start new elevated PowerShell process $Process = Start-Process -FilePath "pwsh.exe" -ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "`"$ScriptPath`"" -Verb RunAs -Wait -PassThru exit $Process.ExitCode } catch { Write-Host "ERROR: Failed to elevate to Administrator." -ForegroundColor Red Write-Host "Please run this script as Administrator manually." -ForegroundColor Red Write-Host "" Read-Host "Press Enter to exit" exit 1 } } Write-Host "" Write-Host "Running as Administrator" -ForegroundColor Green Write-Host "" # Save original window title to restore later $OriginalTitle = $Host.UI.RawUI.WindowTitle # Create synchronized hashtable to share state with background runspace $script:TitleState = [hashtable]::Synchronized(@{ StartTime = $null CurrentStep = "" IsRunning = $false OriginalTitle = $OriginalTitle RawUI = $Host.UI.RawUI }) # Create background runspace for real-time clock updates $script:TitleRunspace = [runspacefactory]::CreateRunspace() $script:TitleRunspace.ApartmentState = "STA" $script:TitleRunspace.ThreadOptions = "ReuseThread" $script:TitleRunspace.Open() $script:TitleRunspace.SessionStateProxy.SetVariable("TitleState", $script:TitleState) $script:TitlePipeline = $script:TitleRunspace.CreatePipeline() $script:TitlePipeline.Commands.AddScript({ $LastTitle = "" while ($TitleState.IsRunning) { if ($TitleState.StartTime) { $Elapsed = [DateTime]::Now - $TitleState.StartTime $TimeStr = "{0:D2}:{1:D2}:{2:D2}" -f [int]$Elapsed.TotalHours, $Elapsed.Minutes, $Elapsed.Seconds if ($TitleState.CurrentStep) { $NewTitle = "Microsoft Graph Update - Elapsed: $TimeStr - $($TitleState.CurrentStep)" } else { $NewTitle = "Microsoft Graph Update - Elapsed: $TimeStr" } # Only update if title changed to reduce flicker if ($NewTitle -ne $LastTitle) { $TitleState.RawUI.WindowTitle = $NewTitle $LastTitle = $NewTitle } } Start-Sleep -Seconds 1 } $TitleState.RawUI.WindowTitle = $TitleState.OriginalTitle }) # Function to update the current step (background runspace handles the clock) function Update-WindowTitle { param ( [string]$CurrentStep = "" ) $script:TitleState.CurrentStep = $CurrentStep } # Step header function function Write-Progress-Step { param ( [int]$Step, [int]$TotalSteps, [string]$StepName, [string]$Status = "In Progress" ) Update-WindowTitle -CurrentStep "Step $Step of $TotalSteps : $StepName" Write-Host "" Write-Host " ── Step $Step of $TotalSteps : $StepName" -ForegroundColor Cyan Write-Host "" } $TotalSteps = 6 Write-Host "" Write-Host "================================================" -ForegroundColor Cyan Write-Host " Microsoft Graph Module Updater " -ForegroundColor Cyan Write-Host "================================================" -ForegroundColor Cyan Write-Host "" Write-Host " This script will:" -ForegroundColor White Write-Host " 1. Clear loaded modules from session" -ForegroundColor Gray Write-Host " 2. Uninstall existing modules (iterative)" -ForegroundColor Gray Write-Host " 3. Clean up leftover module folders" -ForegroundColor Gray Write-Host " 4. Install packages (you choose which)" -ForegroundColor Gray Write-Host " 5. Import the new modules" -ForegroundColor Gray Write-Host " 6. Validate the installation" -ForegroundColor Gray Write-Host "" Write-Host "================================================" -ForegroundColor Cyan Write-Host "" # Discover installed modules Write-Host " Scanning for installed modules..." -ForegroundColor Gray # Check for any Microsoft.Graph modules (stable - not Beta) # Checks both old PowerShellGet (Get-InstalledModule) and new PSResourceGet (Get-InstalledPSResource) $GraphModules = @() $GraphModules += Get-InstalledModule Microsoft.Graph* -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike "Microsoft.Graph.Beta*" } $GraphModules += Get-InstalledPSResource -Name "Microsoft.Graph*" -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike "Microsoft.Graph.Beta*" } $GraphPathModules = Get-Module -ListAvailable Microsoft.Graph* -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike "Microsoft.Graph.Beta*" } $HasGraph = ($GraphModules.Count -gt 0) -or ($GraphPathModules.Count -gt 0) $GraphVer = if ($GraphModules.Count -gt 0) { "v$($GraphModules[0].Version)" } elseif ($GraphPathModules.Count -gt 0) { "v$($GraphPathModules[0].Version) (path only)" } else { "" } # Check for any Microsoft.Graph.Beta modules $BetaModules = @() $BetaModules += Get-InstalledModule Microsoft.Graph.Beta* -ErrorAction SilentlyContinue $BetaModules += Get-InstalledPSResource -Name "Microsoft.Graph.Beta*" -ErrorAction SilentlyContinue $BetaPathModules = Get-Module -ListAvailable Microsoft.Graph.Beta* -ErrorAction SilentlyContinue $HasBeta = ($BetaModules.Count -gt 0) -or ($BetaPathModules.Count -gt 0) $BetaVer = if ($BetaModules.Count -gt 0) { "v$($BetaModules[0].Version)" } elseif ($BetaPathModules.Count -gt 0) { "v$($BetaPathModules[0].Version) (path only)" } else { "" } Write-Host "" Write-Host " Discovered modules:" -ForegroundColor White if ($HasGraph) { Write-Host " - Microsoft.Graph (stable) $GraphVer" -ForegroundColor Green } if ($HasBeta) { Write-Host " - Microsoft.Graph.Beta $BetaVer" -ForegroundColor Green } if (-not $HasGraph -and -not $HasBeta) { Write-Host " (none found)" -ForegroundColor Yellow } Write-Host "" # Prompt user for which modules to manage Write-Host " Which modules would you like to uninstall?" -ForegroundColor Cyan Write-Host "" Write-Host " [1] Microsoft.Graph (stable) only" -ForegroundColor White Write-Host " [2] Microsoft.Graph.Beta only" -ForegroundColor White Write-Host " [3] Both Microsoft.Graph and Microsoft.Graph.Beta" -ForegroundColor White Write-Host "" $ModuleChoice = Read-Host " Enter your choice (1-3) [default: 3]" if ([string]::IsNullOrWhiteSpace($ModuleChoice)) { $ModuleChoice = "3" } # Set flags based on choice $script:IncludeGraph = $false $script:IncludeBeta = $false switch ($ModuleChoice) { "1" { $script:IncludeGraph = $true } "2" { $script:IncludeBeta = $true } "3" { $script:IncludeGraph = $true; $script:IncludeBeta = $true } default { Write-Host " Invalid choice. Defaulting to Microsoft.Graph and Microsoft.Graph.Beta." -ForegroundColor Yellow $script:IncludeGraph = $true $script:IncludeBeta = $true } } # Build module filter pattern based on selection $script:ModulePatterns = @() if ($script:IncludeGraph -or $script:IncludeBeta) { $script:ModulePatterns += "Microsoft.Graph*" } Write-Host "" Write-Host " Selected for uninstall:" -ForegroundColor Gray if ($script:IncludeGraph) { Write-Host " - Microsoft.Graph (stable)" -ForegroundColor Yellow } if ($script:IncludeBeta) { Write-Host " - Microsoft.Graph.Beta" -ForegroundColor Yellow } Write-Host "" Start-Sleep -Seconds 2 # Start the stopwatch for timing (script scope so Update-WindowTitle can access it) $script:Stopwatch = [System.Diagnostics.Stopwatch]::StartNew() # Start the background title updater $script:TitleState.StartTime = [DateTime]::Now $script:TitleState.IsRunning = $true $script:TitlePipeline.InvokeAsync() # ============================================================ # Step 1: Remove loaded modules from current session (0% -> 20%) # ============================================================ Write-Progress-Step -Step 1 -TotalSteps $TotalSteps -StepName "Clearing loaded modules from session" # Get loaded modules based on user selection $LoadedModules = @() foreach ($Pattern in $script:ModulePatterns) { $LoadedModules += Get-Module $Pattern | Select-Object -ExpandProperty Name } $LoadedModules = $LoadedModules | Select-Object -Unique # Apply selection filter so only the chosen module family is affected if ($script:IncludeGraph -and -not $script:IncludeBeta) { $LoadedModules = @($LoadedModules | Where-Object { $_ -notlike "Microsoft.Graph.Beta*" }) } elseif (-not $script:IncludeGraph -and $script:IncludeBeta) { $LoadedModules = @($LoadedModules | Where-Object { $_ -like "Microsoft.Graph.Beta*" }) } if ($LoadedModules) { $LoadedTotal = @($LoadedModules).Count $LoadedCounter = 0 $LoadedSuccess = 0 $LoadedFailed = 0 foreach ($Module in $LoadedModules) { $LoadedCounter++ try { Remove-Module -Name $Module -Force -ErrorAction Stop $LoadedSuccess++ } catch { $LoadedFailed++ } } if ($LoadedFailed -eq 0) { Write-Host " Cleared $LoadedTotal loaded module(s) from session." -ForegroundColor Green } else { Write-Host " Cleared $LoadedSuccess of $LoadedTotal loaded module(s). $LoadedFailed could not be removed." -ForegroundColor Yellow } } else { Write-Host " No matching modules loaded in session." -ForegroundColor Green } # ============================================================ # Step 2: Uninstall modules (iterative with garbage collection) # ============================================================ Write-Progress-Step -Step 2 -TotalSteps $TotalSteps -StepName "Uninstalling existing modules" $Iteration = 1 $MaxIterations = 10 do { # Get installed modules via old PowerShellGet (Get-InstalledModule) $InstalledModulesOld = @() foreach ($Pattern in $script:ModulePatterns) { $InstalledModulesOld += Get-InstalledModule $Pattern -ErrorAction SilentlyContinue } # Get installed modules via new PSResourceGet (Get-InstalledPSResource) $InstalledModulesNew = @() foreach ($Pattern in $script:ModulePatterns) { $InstalledModulesNew += Get-InstalledPSResource -Name $Pattern -ErrorAction SilentlyContinue } # Merge both lists, normalising to Name/Version/ModuleBase $InstalledModules = @() $InstalledModules += $InstalledModulesOld | Select-Object -Property Name, Version, @{N='ModuleBase';E={$_.InstalledLocation}}, @{N='Source';E={'Old'}} foreach ($PSRMod in $InstalledModulesNew) { $AlreadyListed = $InstalledModules | Where-Object { $_.Name -eq $PSRMod.Name -and $_.Version -eq $PSRMod.Version.ToString() } if (-not $AlreadyListed) { $InstalledModules += [PSCustomObject]@{ Name = $PSRMod.Name Version = $PSRMod.Version.ToString() ModuleBase = $PSRMod.InstalledLocation Source = 'New' } } } # Get available modules using Get-Module -ListAvailable (catches modules not in gallery) $AvailableModules = @() foreach ($Pattern in $script:ModulePatterns) { $AvailableModules += Get-Module -ListAvailable $Pattern -ErrorAction SilentlyContinue } $AvailableModules = $AvailableModules | Select-Object -Unique -Property Name, Version, ModuleBase # Apply selection filter so only the chosen module family is uninstalled if ($script:IncludeGraph -and -not $script:IncludeBeta) { $InstalledModules = @($InstalledModules | Where-Object { $_.Name -notlike "Microsoft.Graph.Beta*" }) $AvailableModules = @($AvailableModules | Where-Object { $_.Name -notlike "Microsoft.Graph.Beta*" }) } elseif (-not $script:IncludeGraph -and $script:IncludeBeta) { $InstalledModules = @($InstalledModules | Where-Object { $_.Name -like "Microsoft.Graph.Beta*" }) $AvailableModules = @($AvailableModules | Where-Object { $_.Name -like "Microsoft.Graph.Beta*" }) } $TotalFound = @($InstalledModules).Count + @($AvailableModules).Count if ($TotalFound -eq 0) { Write-Host " No modules found. Cleanup complete!" -ForegroundColor Green break } Write-Host " Get-InstalledModule found: $(@($InstalledModulesOld).Count) module(s)" -ForegroundColor Gray Write-Host " Get-InstalledPSResource found: $(@($InstalledModulesNew).Count) module(s)" -ForegroundColor Gray Write-Host " Get-Module -ListAvailable found: $(@($AvailableModules).Count) module(s)" -ForegroundColor Gray Write-Host "" # Uninstall gallery-installed modules — try PSResourceGet first, fall back to old PowerShellGet if ($InstalledModules) { # Group sub-modules by their root package $InstallGroups = @{} foreach ($Module in $InstalledModules) { $Root = if ($Module.Name -like "Microsoft.Graph.Beta*") { "Microsoft.Graph.Beta" } elseif ($Module.Name -like "Microsoft.Graph*") { "Microsoft.Graph" } else { $Module.Name } if (-not $InstallGroups.ContainsKey($Root)) { $InstallGroups[$Root] = [System.Collections.Generic.List[object]]::new() } $InstallGroups[$Root].Add($Module) } foreach ($Root in ($InstallGroups.Keys | Sort-Object)) { Write-Host " Uninstalling $Root..." -ForegroundColor Yellow Write-Host " (This removes all sub-modules)" -ForegroundColor Gray Write-Host "" $PendingCount = 0 $FailMessages = [System.Collections.Generic.List[string]]::new() foreach ($Module in $InstallGroups[$Root]) { $Uninstalled = $false # Try Uninstall-PSResource (exact version) try { Uninstall-PSResource -Name $Module.Name -Version $Module.Version -ErrorAction Stop $Uninstalled = $true } catch { $LastError = $_.Exception.Message } # Try Uninstall-PSResource (no version — catches all) if (-not $Uninstalled) { try { Uninstall-PSResource -Name $Module.Name -ErrorAction Stop $Uninstalled = $true } catch { $LastError = $_.Exception.Message } } # Fall back to old Uninstall-Module if (-not $Uninstalled) { try { Uninstall-Module -Name $Module.Name -AllVersions -Force -ErrorAction Stop $Uninstalled = $true } catch { $LastError = $_.Exception.Message } } if (-not $Uninstalled) { $PendingCount++ $FailMessages.Add(" $($Module.Name) v$($Module.Version): $LastError") } } if ($PendingCount -eq 0) { Write-Host " $Root uninstalled successfully." -ForegroundColor Green } else { Write-Host " ${Root}: $PendingCount sub-module(s) could not be uninstalled:" -ForegroundColor Yellow foreach ($Msg in $FailMessages) { Write-Host $Msg -ForegroundColor DarkGray } } Write-Host "" } } # Handle modules found via Get-Module -ListAvailable that aren't tracked by either package manager. # Try proper uninstall first — only fall back to folder deletion if that fails. if ($AvailableModules) { # Filter to only modules not already handled above $OrphanModules = $AvailableModules | Where-Object { $ModName = $_.Name -not ($InstalledModules | Where-Object { $_.Name -eq $ModName }) } if ($OrphanModules) { # Group by root module name $OrphanGroups = @{} foreach ($Module in $OrphanModules) { $Root = if ($Module.Name -like "Microsoft.Graph.Beta*") { "Microsoft.Graph.Beta" } elseif ($Module.Name -like "Microsoft.Graph*") { "Microsoft.Graph" } elseif ($Module.Name -like "Microsoft.Entra*") { "Microsoft.Entra" } else { $Module.Name } if (-not $OrphanGroups.ContainsKey($Root)) { $OrphanGroups[$Root] = [System.Collections.Generic.List[object]]::new() } $OrphanGroups[$Root].Add($Module) } foreach ($Root in ($OrphanGroups.Keys | Sort-Object)) { Write-Host " Uninstalling $Root (untracked)..." -ForegroundColor Yellow Write-Host " (This removes all sub-modules)" -ForegroundColor Gray Write-Host "" $FailCount = 0 foreach ($Module in $OrphanGroups[$Root]) { $Uninstalled = $false # Try Uninstall-PSResource first try { Uninstall-PSResource -Name $Module.Name -ErrorAction Stop $Uninstalled = $true } catch { } # Fall back to Uninstall-Module if (-not $Uninstalled) { try { Uninstall-Module -Name $Module.Name -AllVersions -Force -ErrorAction Stop $Uninstalled = $true } catch { } } # Last resort: folder deletion if (-not $Uninstalled) { try { if (Test-Path $Module.ModuleBase) { Remove-Item -Path $Module.ModuleBase -Recurse -Force -ErrorAction Stop $Uninstalled = $true } } catch { Write-Host " Failed to remove: $($Module.ModuleBase) - $_" -ForegroundColor Red $FailCount++ } } } if ($FailCount -eq 0) { Write-Host " $Root removed successfully." -ForegroundColor Green } else { Write-Host " ${Root}: $FailCount sub-module(s) could not be deleted." -ForegroundColor Yellow } Write-Host "" } } } # Force garbage collection to release any file locks [System.GC]::Collect() [System.GC]::WaitForPendingFinalizers() Start-Sleep -Seconds 2 $Iteration++ if ($Iteration -gt $MaxIterations) { Write-Host " Reached maximum iterations ($MaxIterations). Some modules may need manual removal." -ForegroundColor Yellow break } } while ($true) Write-Host "" Write-Host " Uninstall phase complete." -ForegroundColor Green # ============================================================ # Step 3: Clean up leftover module folders (40% -> 60%) # ============================================================ Write-Progress-Step -Step 3 -TotalSteps $TotalSteps -StepName "Cleaning up leftover module folders" # All possible module locations to check (PowerShell 7 only) $ModulePaths = @( "$env:USERPROFILE\Documents\PowerShell\Modules", "$env:USERPROFILE\OneDrive\Documents\PowerShell\Modules", "$env:ProgramFiles\PowerShell\Modules", "$env:ProgramFiles\PowerShell\7\Modules", "$env:LOCALAPPDATA\PowerShell\Modules" ) # Also add paths from PSModulePath environment variable (exclude WindowsPowerShell paths) $PSModulePaths = $env:PSModulePath -split ';' | Where-Object { $_ -ne '' -and $_ -notlike '*WindowsPowerShell*' } foreach ($PSPath in $PSModulePaths) { if ($PSPath -and ($ModulePaths -notcontains $PSPath)) { $ModulePaths += $PSPath } } # Remove duplicates and non-existent paths $ModulePaths = $ModulePaths | Select-Object -Unique | Where-Object { Test-Path $_ -ErrorAction SilentlyContinue } # Build folder filter patterns based on user selection $FolderPatterns = @() if ($script:IncludeGraph -or $script:IncludeBeta) { $FolderPatterns += "Microsoft.Graph*" } # Collect all items to delete across all module paths $ItemsToDelete = [System.Collections.Generic.List[string]]::new() foreach ($Path in $ModulePaths) { $TargetFolders = @() foreach ($Pattern in $FolderPatterns) { $TargetFolders += Get-ChildItem -Path $Path -Directory -Filter $Pattern -ErrorAction SilentlyContinue } # Apply selection filter so only the chosen module family is cleaned up if ($script:IncludeGraph -and -not $script:IncludeBeta) { $TargetFolders = @($TargetFolders | Where-Object { $_.Name -notlike "Microsoft.Graph.Beta*" }) } elseif (-not $script:IncludeGraph -and $script:IncludeBeta) { $TargetFolders = @($TargetFolders | Where-Object { $_.Name -like "Microsoft.Graph.Beta*" }) } foreach ($Folder in $TargetFolders) { $ItemsToDelete.Add($Folder.FullName) } } if ($ItemsToDelete.Count -eq 0) { Write-Host " No leftover folders found." -ForegroundColor Green } else { Write-Host " Found $($ItemsToDelete.Count) item(s) to remove." -ForegroundColor Gray Write-Host "" $MaxRetries = 3 $RetryCount = 0 $Pending = $ItemsToDelete.ToArray() $TotalRemoved = 0 while ($Pending.Count -gt 0 -and $RetryCount -lt $MaxRetries) { if ($RetryCount -gt 0) { Write-Host " Retrying $($Pending.Count) locked item(s) (attempt $($RetryCount + 1) of $MaxRetries)..." -ForegroundColor Yellow [System.GC]::Collect() [System.GC]::WaitForPendingFinalizers() [System.GC]::Collect() Start-Sleep -Seconds 3 } $StillPending = [System.Collections.Generic.List[string]]::new() foreach ($ItemPath in $Pending) { if (-not (Test-Path $ItemPath)) { $TotalRemoved++ continue } try { Remove-Item -Path $ItemPath -Recurse -Force -ErrorAction Stop $TotalRemoved++ } catch { $StillPending.Add($ItemPath) } } $Pending = $StillPending.ToArray() $RetryCount++ } if ($Pending.Count -eq 0) { Write-Host " Folder cleanup complete. Removed $TotalRemoved item(s)." -ForegroundColor Green } else { Write-Host " $($Pending.Count) item(s) could not be deleted (locked by session DLLs):" -ForegroundColor Yellow foreach ($Remaining in $Pending) { Write-Host " - $Remaining" -ForegroundColor Yellow } Write-Host "" Write-Host " These are most likely DLL files still locked by this PowerShell session." -ForegroundColor Yellow Write-Host " To complete the cleanup:" -ForegroundColor Yellow Write-Host " 1. Close this PowerShell window" -ForegroundColor White Write-Host " 2. Open a new elevated PowerShell window" -ForegroundColor White Write-Host " 3. Re-run this script" -ForegroundColor White Write-Host "" Write-Host " The fresh install in Step 4 will still proceed." -ForegroundColor DarkGray } } # ============================================================ # Step 4: Install modules (60% -> 80%) # ============================================================ Write-Progress-Step -Step 4 -TotalSteps $TotalSteps -StepName "Installing selected modules" Write-Host "" Write-Host " Which modules would you like to install?" -ForegroundColor Cyan Write-Host "" Write-Host " [1] Microsoft.Graph (stable) only" -ForegroundColor White Write-Host " [2] Microsoft.Graph.Beta only" -ForegroundColor White Write-Host " [3] Both Microsoft.Graph and Microsoft.Graph.Beta" -ForegroundColor White Write-Host " [0] Skip installation" -ForegroundColor White Write-Host "" $InstallChoice = Read-Host " Enter your choice (0-3) [default: 3]" if ([string]::IsNullOrWhiteSpace($InstallChoice)) { $InstallChoice = "3" } # Set install flags based on choice (script scope for Steps 5 & 6) $script:InstallGraph = $false $script:InstallBeta = $false switch ($InstallChoice) { "0" { Write-Host " Skipping installation." -ForegroundColor Yellow } "1" { $script:InstallGraph = $true } "2" { $script:InstallBeta = $true } "3" { $script:InstallGraph = $true; $script:InstallBeta = $true } default { Write-Host " Invalid choice. Defaulting to Microsoft.Graph and Microsoft.Graph.Beta." -ForegroundColor Yellow $script:InstallGraph = $true $script:InstallBeta = $true } } # Ask for installation scope if user chose to install modules $script:InstallScope = "AllUsers" if ($InstallChoice -ne "0") { Write-Host "" Write-Host " Where would you like to install the modules?" -ForegroundColor Cyan Write-Host "" Write-Host " [1] All Users" -ForegroundColor White Write-Host " [2] Current User Only" -ForegroundColor White Write-Host "" Write-Host " Recommended: All Users" -ForegroundColor Gray Write-Host "" $ScopeChoice = Read-Host " Enter your choice (1-2) [default: 1]" if ([string]::IsNullOrWhiteSpace($ScopeChoice)) { $ScopeChoice = "1" } switch ($ScopeChoice) { "1" { $script:InstallScope = "AllUsers" } "2" { $script:InstallScope = "CurrentUser" } default { Write-Host " Invalid choice. Defaulting to All Users." -ForegroundColor Yellow $script:InstallScope = "AllUsers" } } $ScopeDisplay = if ($script:InstallScope -eq "AllUsers") { "All Users" } else { "Current User Only" } Write-Host "" Write-Host " Modules will be installed: $ScopeDisplay" -ForegroundColor Gray } if ($InstallChoice -ne "0") { Write-Host "" $InstallSuccess = 0 $InstallFailed = 0 # Suppress native Install-Module progress output $PrevProgressPreference = $ProgressPreference $ProgressPreference = 'SilentlyContinue' if ($script:InstallGraph) { Write-Host " Installing Microsoft.Graph..." -ForegroundColor Yellow Write-Host " (This will install all sub-modules - may take several minutes)" -ForegroundColor Gray Write-Host "" try { Install-Module Microsoft.Graph -Scope $script:InstallScope -Force -AllowClobber -ErrorAction Stop Write-Host " Microsoft.Graph installed successfully." -ForegroundColor Green $InstallSuccess++ } catch { Write-Host " ERROR: Failed to install Microsoft.Graph - $_" -ForegroundColor Red Write-Host " Check your internet connection and try again." -ForegroundColor DarkGray $InstallFailed++ } Write-Host "" } if ($script:InstallBeta) { Write-Host " Installing Microsoft.Graph.Beta..." -ForegroundColor Yellow Write-Host " (This will install all sub-modules - may take several minutes)" -ForegroundColor Gray Write-Host "" try { Install-Module Microsoft.Graph.Beta -Scope $script:InstallScope -Force -AllowClobber -ErrorAction Stop Write-Host " Microsoft.Graph.Beta installed successfully." -ForegroundColor Green $InstallSuccess++ } catch { Write-Host " ERROR: Failed to install Microsoft.Graph.Beta - $_" -ForegroundColor Red Write-Host " Check your internet connection and try again." -ForegroundColor DarkGray $InstallFailed++ } Write-Host "" } $ProgressPreference = $PrevProgressPreference # Summary message Write-Host " Install complete: $InstallSuccess succeeded, $InstallFailed failed." -ForegroundColor Green if ($InstallFailed -gt 0) { Write-Host " WARNING: Some packages failed to install." -ForegroundColor Yellow } } # ============================================================ # Step 5: Import the new modules (80% -> 90%) # ============================================================ Write-Progress-Step -Step 5 -TotalSteps $TotalSteps -StepName "Importing installed modules" Write-Host " Skipping bulk import - PowerShell auto-loads modules on demand." -ForegroundColor Gray Write-Host " Importing only Microsoft.Graph.Authentication for immediate use..." -ForegroundColor Gray Write-Host "" try { Import-Module Microsoft.Graph.Authentication -Force -ErrorAction Stop Write-Host " Imported: Microsoft.Graph.Authentication" -ForegroundColor Green } catch { Write-Host " Failed to import Microsoft.Graph.Authentication: $_" -ForegroundColor Yellow } Write-Host "" # ============================================================ # Step 6: Validation (90% -> 100%) # ============================================================ Write-Progress-Step -Step 6 -TotalSteps $TotalSteps -StepName "Validating installation" Write-Host " Checking installed modules..." -ForegroundColor Gray # Get module info based on what was installed $GraphModule = $null $GraphBetaModule = $null $AuthModule = $null if ($script:InstallGraph) { $GraphModule = Get-InstalledModule Microsoft.Graph -ErrorAction SilentlyContinue if (-not $GraphModule) { $GraphModule = Get-InstalledPSResource -Name "Microsoft.Graph" -ErrorAction SilentlyContinue | Sort-Object Version -Descending | Select-Object -First 1 } $AuthModule = Get-InstalledModule Microsoft.Graph.Authentication -ErrorAction SilentlyContinue if (-not $AuthModule) { $AuthModule = Get-InstalledPSResource -Name "Microsoft.Graph.Authentication" -ErrorAction SilentlyContinue | Sort-Object Version -Descending | Select-Object -First 1 } } if ($script:InstallBeta) { $GraphBetaModule = Get-InstalledModule Microsoft.Graph.Beta -ErrorAction SilentlyContinue if (-not $GraphBetaModule) { $GraphBetaModule = Get-InstalledPSResource -Name "Microsoft.Graph.Beta" -ErrorAction SilentlyContinue | Sort-Object Version -Descending | Select-Object -First 1 } } # Stop the stopwatch $script:Stopwatch.Stop() $ElapsedTime = $script:Stopwatch.Elapsed $TimeFormatted = "{0:D2}:{1:D2}:{2:D2}" -f $ElapsedTime.Hours, $ElapsedTime.Minutes, $ElapsedTime.Seconds Write-Host "" Write-Host " INSTALLED MODULES:" -ForegroundColor White Write-Host " -------------------------------------------" -ForegroundColor Gray # Display Graph modules if installed if ($script:InstallGraph) { if ($GraphModule) { Write-Host " Microsoft.Graph: v$($GraphModule.Version)" -ForegroundColor Green } else { Write-Host " Microsoft.Graph: NOT INSTALLED" -ForegroundColor Red } if ($AuthModule) { Write-Host " Microsoft.Graph.Authentication: v$($AuthModule.Version)" -ForegroundColor Green } else { Write-Host " Microsoft.Graph.Authentication: NOT INSTALLED" -ForegroundColor Red } } # Display Beta modules if installed if ($script:InstallBeta) { if ($GraphBetaModule) { Write-Host " Microsoft.Graph.Beta: v$($GraphBetaModule.Version)" -ForegroundColor Green } else { Write-Host " Microsoft.Graph.Beta: NOT INSTALLED" -ForegroundColor Red } } Write-Host " -------------------------------------------" -ForegroundColor Gray # Final status check $AllSuccess = $true if ($script:InstallGraph -and (-not $GraphModule)) { $AllSuccess = $false } if ($script:InstallBeta -and (-not $GraphBetaModule)) { $AllSuccess = $false } # Version match check for Graph modules only (if both installed) $VersionMismatch = $false if ($script:InstallGraph -and $script:InstallBeta -and $GraphModule -and $GraphBetaModule) { if ($GraphModule.Version -ne $GraphBetaModule.Version) { $VersionMismatch = $true } } if ($AllSuccess -and -not $VersionMismatch) { Write-Host "" Write-Host " STATUS: SUCCESS" -ForegroundColor Green Write-Host " All selected modules installed successfully!" -ForegroundColor Green } elseif ($AllSuccess -and $VersionMismatch) { Write-Host "" Write-Host " STATUS: WARNING" -ForegroundColor Yellow Write-Host " Modules installed but Graph versions do not match." -ForegroundColor Yellow } else { Write-Host "" Write-Host " STATUS: FAILED" -ForegroundColor Red Write-Host " One or more modules failed to install." -ForegroundColor Red } Write-Host "" Write-Host " IMPORTANT: Open a NEW PowerShell window before running any Graph commands." -ForegroundColor Yellow Write-Host "" Write-Host " Total time: $TimeFormatted" -ForegroundColor Cyan Write-Host "" # Stop the background title updater and restore original title $script:TitleState.IsRunning = $false Start-Sleep -Milliseconds 600 # Give runspace time to restore title $script:TitleRunspace.Close() $script:TitleRunspace.Dispose() Read-Host " Press Enter to exit" |