src/Engine/Gallery.ps1

# Gallery.ps1 — read-only PowerShell Gallery search (#38), version lookups and
# update checks.

# Search the gallery by name pattern. Wildcards are added around the query
# unless the caller already supplied any. Returns lightweight result objects.
function Find-PSMMGalleryModule {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Query,
        [int]$First = 40
    )
    $pattern = if ($Query -match '[\*\?]') { $Query } else { "*$Query*" }
    $results = if (Get-Command Find-PSResource -ErrorAction SilentlyContinue) {
        Find-PSResource -Name $pattern -Type Module -ErrorAction SilentlyContinue
    } else {
        Find-Module -Name $pattern -ErrorAction SilentlyContinue
    }
    @($results | Select-Object -First $First | ForEach-Object {
        [pscustomobject]@{
            Name        = $_.Name
            Version     = "$($_.Version)"
            Prerelease  = "$($_.Prerelease)".TrimStart('-')
            Description = "$($_.Description)"
            Author      = "$($_.Author)"
        }
    })
}

# --- version comparison (SemVer-ish, gh#6) --------------------------------
# A gallery version is a [version] base plus an optional prerelease label. The
# base alone cannot order 0.1.0-beta2 / 0.1.0-beta8 / 0.1.0 - they all parse to
# [version]0.1.0 - so the label has to decide. psmm already owns exactly that
# comparison (Compare-PSMMVersion in SelfUpdate.ps1, verified against
# PSResourceGet's behaviour); this is the (version, label) pair adapter for it,
# tolerant of the missing/unparseable versions entry state can hold.
function Compare-PSMMEntryVersion {
    [CmdletBinding()]
    param($VersionA, [string]$PrereleaseA, $VersionB, [string]$PrereleaseB)
    $a = Get-PSMMVersionDisplay -Version $VersionA -Prerelease $PrereleaseA
    $b = Get-PSMMVersionDisplay -Version $VersionB -Prerelease $PrereleaseB
    if (-not $a -and -not $b) { return 0 }
    if (-not $a) { return -1 }
    if (-not $b) { return 1 }
    try { Compare-PSMMVersion -A $a -B $b } catch { 0 }
}

# Split "1.2.3-beta4" into its base version and label (gallery objects usually
# carry them separately, but not every provider does).
function Split-PSMMVersionString {
    [CmdletBinding()]
    param([string]$Text)
    $t = "$Text".Trim()
    $pre = ''
    if ($t -match '^([^-]+)-(.+)$') { $t = $Matches[1]; $pre = $Matches[2] }
    [pscustomobject]@{ Version = $t; Prerelease = $pre }
}

# Normalise one gallery result into @{ Version; Prerelease }.
function ConvertTo-PSMMGalleryVersion {
    [CmdletBinding()]
    param($Result)
    $pre = ''
    try { $pre = "$($Result.Prerelease)".TrimStart('-') } catch { }
    $split = Split-PSMMVersionString -Text "$($Result.Version)"
    if (-not $pre) { $pre = $split.Prerelease }
    [pscustomobject]@{ Version = $split.Version; Prerelease = $pre }
}

# Latest gallery version for one module name ($null when not found).
# Returns @{ Version; Prerelease }; -Prerelease includes prerelease builds.
function Get-PSMMGalleryLatest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Name,
        [switch]$Prerelease
    )
    try {
        if (Get-Command Find-PSResource -ErrorAction SilentlyContinue) {
            $hits = @(Find-PSResource -Name $Name -Prerelease:$Prerelease -ErrorAction Stop)
        } else {
            $hits = @(Find-Module -Name $Name -AllowPrerelease:$Prerelease -ErrorAction Stop)
        }
        $best = $null
        foreach ($h in $hits) {
            $v = ConvertTo-PSMMGalleryVersion -Result $h
            if (-not $Prerelease -and $v.Prerelease) { continue }
            if (-not $best -or (Compare-PSMMEntryVersion -VersionA $v.Version -PrereleaseA $v.Prerelease `
                        -VersionB $best.Version -PrereleaseB $best.Prerelease) -gt 0) {
                $best = $v
            }
        }
        return $best
    } catch { return $null }
}

# Every version of one module that can be pinned: what is on disk (instant,
# offline) and what the gallery offers (network). Newest first, deduplicated.
# Feeds the pin picker (gh#5).
function Get-PSMMAvailableVersion {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Name,
        [switch]$Prerelease,
        [switch]$SkipGallery
    )
    $all = [System.Collections.Generic.List[object]]::new()
    foreach ($m in @(Get-Module -ListAvailable -Name $Name -ErrorAction SilentlyContinue)) {
        $all.Add([pscustomobject]@{
                Version    = "$($m.Version)"
                Prerelease = (Get-PSMMPrereleaseLabel -ModuleInfo $m)
                OnDisk     = $true
                InGallery  = $false
            })
    }
    if (-not $SkipGallery) {
        try {
            $hits = if (Get-Command Find-PSResource -ErrorAction SilentlyContinue) {
                @(Find-PSResource -Name $Name -Version '*' -Prerelease:$Prerelease -ErrorAction Stop)
            } else {
                @(Find-Module -Name $Name -AllVersions -AllowPrerelease:$Prerelease -ErrorAction Stop)
            }
            foreach ($h in $hits) {
                if ("$($h.Name)" -ne $Name) { continue }
                $v = ConvertTo-PSMMGalleryVersion -Result $h
                if (-not $Prerelease -and $v.Prerelease) { continue }
                $all.Add([pscustomobject]@{
                        Version = $v.Version; Prerelease = $v.Prerelease
                        OnDisk = $false; InGallery = $true
                    })
            }
        } catch { }   # offline / not published: disk versions are still useful
    }
    # merge duplicates (a version can be both on disk and in the gallery)
    $merged = [ordered]@{}
    foreach ($v in $all) {
        $key = (Get-PSMMVersionDisplay -Version $v.Version -Prerelease $v.Prerelease)
        if ($merged.Contains($key)) {
            $merged[$key].OnDisk = $merged[$key].OnDisk -or $v.OnDisk
            $merged[$key].InGallery = $merged[$key].InGallery -or $v.InGallery
        } else {
            $merged[$key] = [pscustomobject]@{
                Display = $key; Version = $v.Version; Prerelease = $v.Prerelease
                OnDisk = $v.OnDisk; InGallery = $v.InGallery
            }
        }
    }
    # newest first - Sort-Object has no custom-comparer parameter, and the
    # ordering here is semantic (base version + prerelease label), so sort the
    # backing list with the comparison we already own
    $list = [System.Collections.Generic.List[object]]::new()
    foreach ($v in $merged.Values) { $list.Add($v) }
    $list.Sort([System.Comparison[object]] {
            param($a, $b)
            -1 * (Compare-PSMMEntryVersion -VersionA $a.Version -PrereleaseA $a.Prerelease `
                    -VersionB $b.Version -PrereleaseB $b.Prerelease)
        })
    @($list)
}

# Opt-in update check for a set of entries: marks UpdateAvailable/LatestVersion.
# Network-bound - never runs automatically. Respects exact pins (a pinned
# module reports pinned rather than update-available) and each entry's
# prerelease policy (gh#6).
function Update-PSMMLatestVersion {
    [CmdletBinding()]
    param([AllowNull()][AllowEmptyCollection()] $Entries)   # empty set = normal (zero configs)
    $found = 0
    foreach ($e in @($Entries | Where-Object Installed)) {
        $latest = Get-PSMMGalleryLatest -Name $e.Name -Prerelease:([bool]$e.AllowPrerelease)
        if (-not $latest) { continue }
        $e.LatestVersion = $latest.Version
        $e.LatestPrerelease = $latest.Prerelease
        $e.UpdateAvailable = $false
        if ($e.PinnedExact) { continue }   # pinned: never nag
        if ($e.InstalledVersion) {
            $e.UpdateAvailable = (Compare-PSMMEntryVersion -VersionA $latest.Version -PrereleaseA $latest.Prerelease `
                    -VersionB "$($e.InstalledVersion)" -PrereleaseB "$($e.InstalledPrerelease)") -gt 0
        }
        if ($e.UpdateAvailable) { $found++ }
    }
    $found
}