Public/Find-WingetDuplicate.ps1

function Find-WingetDuplicate {
    <#
    .SYNOPSIS
        Detect duplicate and redundant packages on the system.
 
    .DESCRIPTION
        Scans all installed winget packages and identifies potential duplicates:
        - Same package installed from multiple sources (winget + msstore)
        - Multiple versions of the same package family (e.g., Python 3.10 + 3.11 + 3.12)
        - Packages with overlapping functionality (same publisher, similar names)
 
        Helps reclaim disk space and reduce system clutter.
 
    .PARAMETER IncludeVersions
        Also flag multiple versions of the same package family.
 
    .PARAMETER ExportHtml
        Generate an HTML report of findings.
 
    .EXAMPLE
        Find-WingetDuplicate
        Finds packages installed from multiple sources.
 
    .EXAMPLE
        Find-WingetDuplicate -IncludeVersions
        Also detects multiple versions of the same package.
 
    .LINK
        https://github.com/thebubbsy/WingetBatch
    #>


    [CmdletBinding()]
    param(
        [Parameter()]
        [switch]$IncludeVersions,

        [Parameter()]
        [switch]$ExportHtml
    )

    begin {
        if (-not (Get-Module -Name Microsoft.WinGet.Client)) {
            try { Import-Module Microsoft.WinGet.Client -ErrorAction Stop }
            catch {
                Write-Error "Microsoft.WinGet.Client module is required."
                return
            }
        }
        if (-not (Get-Module -Name PwshSpectreConsole)) {
            if (Get-Module -ListAvailable -Name PwshSpectreConsole) {
                Import-Module PwshSpectreConsole -ErrorAction SilentlyContinue
            }
        }
    }

    process {
        Write-Host ""
        Write-Host " Scanning for duplicate packages..." -ForegroundColor Cyan

        $installed = Get-WinGetPackage -ErrorAction SilentlyContinue
        if (-not $installed -or $installed.Count -eq 0) {
            Write-Host " No installed packages found." -ForegroundColor Yellow
            return
        }

        Write-Host " Analyzing $($installed.Count) installed packages..." -ForegroundColor DarkGray

        $duplicates = [System.Collections.Generic.List[PSCustomObject]]::new()
        $versionClusters = [System.Collections.Generic.List[PSCustomObject]]::new()

        # Strategy 1: Same package ID from multiple sources
        $idGroups = $installed | Group-Object -Property Id | Where-Object { $_.Count -gt 1 }
        foreach ($group in $idGroups) {
            $sources = ($group.Group | Select-Object -ExpandProperty Source -Unique) -join ', '
            $duplicates.Add([PSCustomObject]@{
                Type       = 'Multi-Source'
                PackageId  = $group.Name
                Name       = $group.Group[0].Name
                Count      = $group.Count
                Detail     = "Installed from: $sources"
                Versions   = ($group.Group | ForEach-Object { "$($_.Source):$($_.InstalledVersion)" }) -join ', '
            })
        }

        # Strategy 2: Same name, different IDs (potential duplicates)
        $nameGroups = $installed | Where-Object { $_.Name } | Group-Object -Property Name | Where-Object { $_.Count -gt 1 }
        foreach ($group in $nameGroups) {
            $ids = ($group.Group | Select-Object -ExpandProperty Id -Unique)
            if ($ids.Count -gt 1) {
                $duplicates.Add([PSCustomObject]@{
                    Type       = 'Name Collision'
                    PackageId  = ($ids -join ', ')
                    Name       = $group.Name
                    Count      = $group.Count
                    Detail     = "Multiple IDs: $($ids -join ', ')"
                    Versions   = ($group.Group | ForEach-Object { "$($_.Id):$($_.InstalledVersion)" }) -join ', '
                })
            }
        }

        # Strategy 3: Version clusters (same package family, multiple versions)
        if ($IncludeVersions) {
            # Group by package family (e.g., Python.Python.3.x -> Python.Python)
            $familyMap = @{}
            foreach ($pkg in $installed) {
                if (-not $pkg.Id) { continue }
                # Extract family: remove last segment if it looks like a version number
                $parts = $pkg.Id -split '\.'
                $family = $pkg.Id
                if ($parts.Count -ge 3 -and $parts[-1] -match '^\d+$') {
                    $family = ($parts[0..($parts.Count - 2)]) -join '.'
                }

                if (-not $familyMap.ContainsKey($family)) {
                    $familyMap[$family] = [System.Collections.Generic.List[object]]::new()
                }
                $familyMap[$family].Add($pkg)
            }

            foreach ($family in $familyMap.Keys) {
                $members = $familyMap[$family]
                if ($members.Count -gt 1) {
                    $versions = ($members | ForEach-Object { "$($_.Id) (v$($_.InstalledVersion))" }) -join ', '
                    $versionClusters.Add([PSCustomObject]@{
                        Type       = 'Version Cluster'
                        PackageId  = $family
                        Name       = $members[0].Name
                        Count      = $members.Count
                        Detail     = $versions
                        Versions   = ($members | ForEach-Object { $_.InstalledVersion }) -join ', '
                    })
                }
            }
        }

        # Display results
        $totalFindings = $duplicates.Count + $versionClusters.Count

        Write-Host ""
        if ($totalFindings -eq 0) {
            Write-Host " [OK] No duplicates or redundant packages detected." -ForegroundColor Green
            Write-Host ""
            return
        }

        Write-Host " Found " -ForegroundColor White -NoNewline
        Write-Host "$totalFindings" -ForegroundColor Yellow -NoNewline
        Write-Host " potential issue(s):" -ForegroundColor White
        Write-Host ""

        # Display duplicates
        if ($duplicates.Count -gt 0) {
            Write-Host " DUPLICATES" -ForegroundColor Red
            Write-Host " $('─' * 56)" -ForegroundColor DarkGray

            foreach ($dup in $duplicates) {
                Write-Host " [$($dup.Type)] " -ForegroundColor Red -NoNewline
                Write-Host $dup.Name -ForegroundColor White -NoNewline
                Write-Host " ($($dup.Count) copies)" -ForegroundColor Gray
                Write-Host " $($dup.Detail)" -ForegroundColor DarkGray
                Write-Host ""
            }
        }

        # Display version clusters
        if ($versionClusters.Count -gt 0) {
            Write-Host " VERSION CLUSTERS" -ForegroundColor Yellow
            Write-Host " $('─' * 56)" -ForegroundColor DarkGray

            foreach ($cluster in $versionClusters) {
                Write-Host " [$($cluster.Count) versions] " -ForegroundColor Yellow -NoNewline
                Write-Host $cluster.PackageId -ForegroundColor White
                Write-Host " $($cluster.Detail)" -ForegroundColor DarkGray
                Write-Host ""
            }
        }

        # Recommendations
        Write-Host " RECOMMENDATIONS" -ForegroundColor Cyan
        Write-Host " $('─' * 56)" -ForegroundColor DarkGray

        if ($duplicates.Count -gt 0) {
            Write-Host " - Remove duplicate source installs:" -ForegroundColor White
            foreach ($dup in $duplicates | Where-Object { $_.Type -eq 'Multi-Source' }) {
                Write-Host " Uninstall-WinGetPackage -Id '$($dup.PackageId)' --Source msstore" -ForegroundColor DarkGray
            }
        }
        if ($versionClusters.Count -gt 0) {
            Write-Host " - Consider removing old versions you no longer need:" -ForegroundColor White
            foreach ($cluster in $versionClusters) {
                Write-Host " Review: $($cluster.Detail)" -ForegroundColor DarkGray
            }
        }
        Write-Host ""

        # HTML Export
        if ($ExportHtml) {
            $allFindings = @($duplicates) + @($versionClusters)
            $timestamp = (Get-Date).ToString("yyyyMMdd_HHmmss")
            $exportPath = Join-Path $env:TEMP "WingetBatch_Duplicates_$timestamp.html"

            try {
                Export-WingetHtmlReport -Data $allFindings -ReportTitle "Duplicate Package Analysis" -FilePath $exportPath
                if (Test-Path $exportPath) {
                    Write-Host " [OK] Report saved: $exportPath" -ForegroundColor Green
                    Invoke-Item $exportPath
                }
            } catch {
                Write-Host " [FAIL] Could not generate report: $_" -ForegroundColor Red
            }
        }

        # Return structured data for pipeline
        [PSCustomObject]@{
            Duplicates      = $duplicates
            VersionClusters = $versionClusters
            TotalFindings   = $totalFindings
        }
    }
}