Private/Helpers/ConvertTo-ModuleInfo.ps1

function ConvertTo-ModuleInfo {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline)]
        $InputObject,
        [string]$ProviderType
    )

    process {
        if ($null -eq $InputObject) { return }

        function Compare-ModuleVersion {
            param(
                [string]$Left,
                [string]$Right
            )

            $leftSem = $null
            $rightSem = $null
            $leftSemOk = [System.Management.Automation.SemanticVersion]::TryParse($Left, [ref]$leftSem)
            $rightSemOk = [System.Management.Automation.SemanticVersion]::TryParse($Right, [ref]$rightSem)
            if ($leftSemOk -and $rightSemOk) {
                return $leftSem.CompareTo($rightSem)
            }

            try {
                $leftSys = [System.Version]$Left
                $rightSys = [System.Version]$Right
                return $leftSys.CompareTo($rightSys)
            } catch {
                # Final fallback keeps behavior deterministic when providers return non-standard version strings.
                return [string]::Compare($Left, $Right, [System.StringComparison]::OrdinalIgnoreCase)
            }
        }

        function Get-MetadataExportedCommands {
            param($RawObject)

            $commands = @()

            if ($RawObject.PSObject.Properties['Includes']) {
                $includes = $RawObject.Includes
                foreach ($propName in @('Commands', 'Command', 'Cmdlets', 'Cmdlet', 'Functions', 'Function')) {
                    if (-not $includes.PSObject.Properties[$propName]) { continue }
                    $values = @($includes.$propName)
                    foreach ($value in $values) {
                        if ($value -is [string] -and -not [string]::IsNullOrWhiteSpace($value)) {
                            $commands += $value.Trim()
                        } elseif ($null -ne $value -and $value.PSObject.Properties['Name']) {
                            $commands += [string]$value.Name
                        }
                    }
                }
            }

            if ($RawObject.PSObject.Properties['AdditionalMetadata'] -and $RawObject.AdditionalMetadata) {
                foreach ($key in @('commands', 'Commands', 'cmdlets', 'Cmdlets', 'functions', 'Functions')) {
                    $metaValue = $RawObject.AdditionalMetadata[$key]
                    if (-not $metaValue) { continue }

                    if ($metaValue -is [string]) {
                        $commands += @($metaValue -split '[,;\s]+' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
                    } else {
                        foreach ($item in @($metaValue)) {
                            if ($item -is [string] -and -not [string]::IsNullOrWhiteSpace($item)) {
                                $commands += $item.Trim()
                            } elseif ($null -ne $item -and $item.PSObject.Properties['Name']) {
                                $commands += [string]$item.Name
                            }
                        }
                    }
                }
            }

            return @($commands | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Sort-Object -Unique)
        }

        $info = [GalleryModuleInfo]::new()

        if ($ProviderType -notin @('PSResourceGet', 'PowerShellGet')) {
            # Generic fallback
            $info.Name = $InputObject.Name
            $info.Version = $InputObject.Version?.ToString()
            $info.Author = $InputObject.Author
            $info.Description = $InputObject.Description
        } else {
            # Common fields for both known providers
            $info.Name = $InputObject.Name
            $info.Version = $InputObject.Version?.ToString()
            $info.Author = $InputObject.Author
            $info.Description = $InputObject.Description
            $info.ProjectUri = $InputObject.ProjectUri?.ToString()
            $info.LicenseUri = $InputObject.LicenseUri?.ToString()
            $info.Tags = @($InputObject.Tags)
            $info.Repository = $InputObject.Repository
            $info.PublishedDate = if ($InputObject.PublishedDate) {
                $InputObject.PublishedDate
            } else {
                [datetime]::MinValue
            }
            $info.Dependencies = if ($InputObject.Dependencies) {
                @($InputObject.Dependencies | ForEach-Object {
                        if ($_ -is [string]) { $_ }
                        elseif ($null -ne $_ -and $_.PSObject.Properties['Name']) { $_.Name }
                    })
            } else { @() }

            # Provider-specific fields
            if ($ProviderType -eq 'PSResourceGet') {
                $info.IsPrerelease = $InputObject.IsPrerelease
                # PSResourceGet doesn't expose a first-class DownloadCount property;
                # attempt to read it from AdditionalMetadata (present on NuGet v3 sources).
                $rawCount = $InputObject.AdditionalMetadata?.downloadCount ?? $InputObject.AdditionalMetadata?.DownloadCount
                $info.DownloadCount = if ($rawCount) { [long]$rawCount } else { 0L }
            } else {
                $info.IsPrerelease = $false
                $info.DownloadCount = [long]($InputObject.AdditionalMetadata?.downloadCount ?? 0)
            }
        }

        # Determine install status
        $installed = Get-InstalledModuleInfo -Name $info.Name

        if ($installed) {
            $info.InstalledVersion = $installed.Version.ToString()

            # Compare versions with a semantic-first strategy and resilient fallback behavior.
            try {
                $comparison = Compare-ModuleVersion -Left $info.InstalledVersion -Right $info.Version
                $updateAvailable = ($comparison -lt 0)
            } catch {
                # If version comparison fails unexpectedly, keep status conservative and avoid false positives.
                $updateAvailable = $false
            }

            $info.InstallStatus = if ($updateAvailable) { 'UpdateAvailable' } else { 'Installed' }
            $info.ExportedCommands = @($installed.ExportedCommands.Keys)
        } else {
            $info.InstalledVersion = ''
            $info.InstallStatus = 'NotInstalled'
            $info.ExportedCommands = @(Get-MetadataExportedCommands -RawObject $InputObject)
        }

        return $info
    }
}