Skip to content

Instantly share code, notes, and snippets.

@JohnLBevan
Last active November 11, 2025 13:24
Show Gist options
  • Select an option

  • Save JohnLBevan/fcf39cea886b71d7af6da43fec01708d to your computer and use it in GitHub Desktop.

Select an option

Save JohnLBevan/fcf39cea886b71d7af6da43fec01708d to your computer and use it in GitHub Desktop.
Code to help tidy up local module versions (thanks to ChatGPT)
function Get-ModuleBundleParents {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$ChildModuleName
)
$isMatch = {
param($entry, $child)
switch ($entry) {
# RequiredModules may contain strings or hashtables
{ $_ -is [string] } { return $_ -eq $child }
{ $_ -is [hashtable] } { return ($_.ModuleName ?? $_.Name) -eq $child }
{ $_ -is [System.Management.Automation.ModuleSpecification] } { return $_.Name -eq $child }
default { $false }
}
}
$parents = @()
# Look through all installed modules (both editions/scopes)
$candidates = Get-Module -ListAvailable | Select-Object -Unique Name,ModuleBase,Version,ModuleType
foreach ($c in $candidates) {
# Only manifest/script/manifest-with-binaries bundles will have a psd1
$psd1 = Join-Path $c.ModuleBase ($c.Name + '.psd1')
if (-not (Test-Path $psd1)) { continue }
try {
# Robust: read raw hashtable (handles mixed types in RequiredModules)
$data = Import-PowerShellDataFile -Path $psd1
$requires = @()
if ($data.ContainsKey('RequiredModules')) { $requires += @($data.RequiredModules) }
$nested = @()
if ($data.ContainsKey('NestedModules')) { $nested += @($data.NestedModules) }
$declaresChild =
($requires | Where-Object { & $isMatch $_ $ChildModuleName }) -or
($nested | Where-Object {
# NestedModules often list files or module specs; match by bare name
if ($_ -is [string]) {
# handle 'Sub\Sub.psm1' etc.
$bn = [IO.Path]::GetFileNameWithoutExtension($_)
return $bn -eq $ChildModuleName
} elseif ($_ -is [hashtable]) {
return (($_.ModuleName ?? $_.Name) -eq $ChildModuleName)
} elseif ($_ -is [System.Management.Automation.ModuleSpecification]) {
return $_.Name -eq $ChildModuleName
} else { $false }
})
if ($declaresChild) { $parents += $c }
}
catch { continue }
}
# Heuristic: treat a module as a “bundle” if it has 6+ required/nested modules
# or if its name is a known rollup prefix (Az, Microsoft.Graph, VMware.PowerCLI, etc.)
$knownPrefixes = 'Az','Microsoft.Graph','VMware.PowerCLI'
$parents | Where-Object {
$psd1 = Join-Path $_.ModuleBase ($_.Name + '.psd1')
$data = Import-PowerShellDataFile -Path $psd1
$count = @($data.RequiredModules).Count + @($data.NestedModules).Count
($count -ge 6) -or ($knownPrefixes -contains $_.Name) -or ($knownPrefixes | Where-Object { $_ + '.' -and $($_ + '.') -and ($_.Name -like ($_ + '*')) })
} | Sort-Object -Property Name -Unique
}
function Ensure-OnlyModuleVersion {
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)][string]$ModuleName,
[Parameter(Mandatory)][ValidatePattern('^\d+(\.\d+){1,3}$')][string]$KeepVersion,
[switch]$AutoRemoveBundles, # detect & uninstall any meta-modules that require $ModuleName
[switch]$AlsoCleanCaches,
[switch]$VerboseListing
)
# 0) Optionally remove bundle parents first (prevents re-introducing versions)
if ($AutoRemoveBundles) {
$parents = Get-ModuleBundleParents -ChildModuleName $ModuleName
foreach ($p in ($parents | Sort-Object Name -Unique)) {
try {
if ($PSCmdlet.ShouldProcess($p.Name, 'Uninstall bundle parent')) {
Uninstall-Module -Name $p.Name -AllVersions -Force -ErrorAction Stop
}
} catch {
Write-Warning "Failed to uninstall bundle '$($p.Name)': $($_.Exception.Message)"
}
}
}
# 1) Unload any loaded copies
Get-Module -Name "$ModuleName*" -All | ForEach-Object {
try { Remove-Module $_.Name -Force -ErrorAction Stop } catch {}
}
# 2) Edition/scope roots (PS 7+ and 5.1)
$roots = @(
Join-Path $HOME 'Documents\PowerShell\Modules'
Join-Path ${env:ProgramFiles} 'PowerShell\Modules'
Join-Path $HOME 'Documents\WindowsPowerShell\Modules'
Join-Path ${env:ProgramFiles} 'WindowsPowerShell\Modules'
) | Where-Object { Test-Path $_ } | Select-Object -Unique
if ($VerboseListing) {
Write-Host "Searching module roots:" -ForegroundColor Cyan
$roots | ForEach-Object { Write-Host " $_" }
}
$removed = @(); $kept=@()
foreach ($root in $roots) {
$moduleDir = Join-Path $root $ModuleName
if (-not (Test-Path $moduleDir)) { continue }
Get-ChildItem -Path $moduleDir -Directory -ErrorAction SilentlyContinue | ForEach-Object {
$ver = $_.Name
if ($ver -eq $KeepVersion) { $kept += $_.FullName; return }
if ($PSCmdlet.ShouldProcess($_.FullName,"Remove version $ver")) {
$didUninstall = $false
try {
$installed = Get-InstalledModule -Name $ModuleName -AllVersions -ErrorAction SilentlyContinue |
Where-Object { $_.Version.ToString() -eq $ver }
if ($installed) {
Uninstall-Module -Name $ModuleName -RequiredVersion $ver -Force -ErrorAction Stop
$didUninstall = $true
}
} catch {}
if (-not $didUninstall) {
try { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction Stop } catch {}
}
$removed += $_.FullName
}
}
if ((Test-Path $moduleDir) -and -not (Get-ChildItem $moduleDir -Directory -ErrorAction SilentlyContinue)) {
try { Remove-Item $moduleDir -Force -ErrorAction SilentlyContinue } catch {}
}
}
# 3) Ensure the desired version is present
$present = Get-Module -ListAvailable -Name $ModuleName | Where-Object { $_.Version -eq $KeepVersion }
if (-not $present) {
if ($PSCmdlet.ShouldProcess("$ModuleName $KeepVersion",'Install-Module -Scope AllUsers')) {
try {
Install-Module -Name $ModuleName -RequiredVersion $KeepVersion -Scope AllUsers -Force -ErrorAction Stop
} catch {
Install-Module -Name $ModuleName -RequiredVersion $KeepVersion -Scope CurrentUser -Force
}
}
}
# 4) Optional: clean caches (PowerShellGet & PSResourceGet)
if ($AlsoCleanCaches) {
$cacheRoots = @(
Join-Path $env:LOCALAPPDATA 'PackageManagement\NuGet\Packages'
Join-Path $env:LOCALAPPDATA 'Microsoft\PowerShell\PackageManagement\NuGet'
Join-Path $env:LOCALAPPDATA 'Microsoft\Windows\PowerShell\PowerShellGet\Cache'
Join-Path $env:LOCALAPPDATA 'PowerShell\PSResourceRepository'
) | Where-Object { Test-Path $_ } | Select-Object -Unique
foreach ($c in $cacheRoots) {
Get-ChildItem $c -Recurse -Directory -ErrorAction SilentlyContinue |
Where-Object { $_.Name -like "$ModuleName*" } |
ForEach-Object {
if ($PSCmdlet.ShouldProcess($_.FullName,'Remove cache')) {
try { Remove-Item $_.FullName -Recurse -Force -ErrorAction Stop } catch {}
}
}
}
}
if ($VerboseListing) {
Write-Host "`nKept:" -ForegroundColor Green
$kept | ForEach-Object { Write-Host " $_" }
Write-Host "`nRemoved:" -ForegroundColor Yellow
$removed | ForEach-Object { Write-Host " $_" }
}
Write-Host "`nPin in code:" -ForegroundColor Cyan
Write-Host " Import-Module $ModuleName -RequiredVersion '$KeepVersion'"
}
# Example
<#
Ensure-OnlyModuleVersion `
-ModuleName 'Microsoft.Graph.Users' `
-KeepVersion '2.32.0' `
-AutoRemoveBundles `
-AlsoCleanCaches `
-VerboseListing
#>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment