Private/Search-WingetPackage.ps1

function Search-WingetPackage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$App
    )

    if (-not $env:GITHUB_TOKEN) {
        throw "Broad package name search requires GITHUB_TOKEN. Use an exact package identifier such as 'Publisher.Package' for unauthenticated lookup."
    }

    if ($App -match '[\x00-\x1F\x7F]') {
        throw [System.ArgumentException]::new('App cannot contain control characters.', 'App')
    }

    $normalizedApp = $App -replace '[^\p{L}\p{Nd}]', ''
    $escapedApp = $App.Replace('\', '\\').Replace('"', '\"')
    $query = '"{0}" repo:{1}/{2} path:{3} extension:yaml' -f @(
        $escapedApp,
        $script:WinGetRepoOwner,
        $script:WinGetRepoName,
        $script:ManifestPath
    )
    $encodedQuery = [Uri]::EscapeDataString($query)
    $candidates = [System.Collections.Generic.List[object]]::new()
    $seenPaths = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
    $page = 1

    do {
        $searchResult = Invoke-WingetGitHubRequest -UriFragment "search/code?q=$encodedQuery&per_page=100&page=$page" -Method Get -Description "Search WinGet manifests for $App" -ErrorAction Stop
        if ($searchResult.incomplete_results) {
            throw "GitHub code search returned incomplete results for '$App' on page $page"
        }
        if ([long]$searchResult.total_count -gt 1000) {
            throw "GitHub code search reported more than the 1,000-result API limit for '$App'"
        }

        $searchItems = @($searchResult.items)
        foreach ($item in $searchItems) {
            $pathParts = @($item.path -split '/')
            if ($pathParts.Count -lt 6 -or
                $pathParts[0] -ne $script:ManifestPath -or
                $pathParts[1] -notmatch '^[0-9a-z]$' -or
                $pathParts[-1] -notlike '*.yaml' -or
                @($pathParts | Where-Object { -not $_ -or $_ -eq '.' -or $_ -eq '..' }).Count -gt 0) {
                continue
            }

            $packageParts = @($pathParts[2..($pathParts.Count - 3)])
            if ($packageParts.Count -lt 2) {
                continue
            }

            $packageId = $packageParts -join '.'
            $packageName = $packageParts[1..($packageParts.Count - 1)] -join '.'
            $normalizedPackageId = $packageId -replace '[^\p{L}\p{Nd}]', ''
            $normalizedPackageName = $packageName -replace '[^\p{L}\p{Nd}]', ''
            $comparison = [System.StringComparison]::OrdinalIgnoreCase
            $rawMatch = $packageId.IndexOf($App, $comparison) -ge 0 -or
                $packageName.IndexOf($App, $comparison) -ge 0 -or
                $App.IndexOf($packageName, $comparison) -ge 0
            $normalizedMatch = $normalizedApp -and (
                $normalizedPackageId.IndexOf($normalizedApp, $comparison) -ge 0 -or
                $normalizedPackageName.IndexOf($normalizedApp, $comparison) -ge 0 -or
                $normalizedApp.IndexOf($normalizedPackageName, $comparison) -ge 0
            )
            if (-not $rawMatch -and -not $normalizedMatch) {
                continue
            }

            $packagePath = $pathParts[0..($pathParts.Count - 3)] -join '/'
            if ($seenPaths.Add($packagePath)) {
                $candidates.Add([PSCustomObject]@{
                    Publisher = $packageParts[0]
                    Package   = $packageName
                    Path      = $packagePath
                    PackageId = $packageId
                })
            }
        }

        $page++
    } while ($searchItems.Count -eq 100 -and (($page - 1) * 100) -lt $searchResult.total_count -and $page -le 10)

    return $candidates
}