Private/DFCatalog.Choco.ps1
|
#Requires -Version 7.0 # Chocolatey community-repository provider (query-cache kind). The OData API is # slow and rate-limited, hence the extended 72h TTL set in DFCatalog.ps1. # Installed state comes from $Env:ChocolateyInstall\lib nuspecs — no choco.exe. function ConvertFrom-DFCatalogODataEntry { <# .SYNOPSIS Maps NuGet v2 OData feed entries (choco / PSGallery) to DotForge.ToolSourceInfo. .PARAMETER Source Provider name to stamp on the results. .PARAMETER Query Normalized query (drives MatchKind). .PARAMETER Entry The feed entry XML elements as returned by Invoke-RestMethod. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Source, [Parameter(Mandatory)] [string]$Query, [AllowEmptyCollection()] [AllowNull()] [object[]]$Entry ) foreach ($e in @($Entry)) { if (-not $e) { continue } # Every read below goes through Get-DFXmlText: <d:Id> is absent from # <m:properties> by design (feed customization — see Get-DFXmlMember), # so probing for it must not throw under strict mode. The <title> # fallback is where the id actually lives. $props = Get-DFXmlMember -Element $e -Name 'properties' $id = Get-DFXmlText -Element $props -Name 'Id' if (-not $id) { $id = Get-DFXmlText -Element $e -Name 'title' } if (-not $id) { continue } $publishedRaw = Get-DFXmlText -Element $props -Name 'Published' $published = $null if ($publishedRaw) { try { $published = [datetime]$publishedRaw } catch {} } New-DFToolSourceInfo -Source $Source ` -PackageId $id -Name $id ` -Description ([string](Get-DFXmlText -Element $props -Name 'Description')) ` -LatestVersion ([string](Get-DFXmlText -Element $props -Name 'Version')) ` -Homepage ([string](Get-DFXmlText -Element $props -Name 'ProjectUrl')) ` -PublishedAt $published ` -MatchKind ($id -eq $Query ? 'exact-name' : 'keyword') } } function Invoke-DFCatalogChocoFetch { <# .SYNOPSIS Live Chocolatey community OData search. .PARAMETER Query Normalized query text. #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$Query ) $term = [uri]::EscapeDataString(($Query -replace "'", "''")) $uri = "https://community.chocolatey.org/api/v2/Search()?`$filter=IsLatestVersion&`$top=15&searchTerm='$term'&targetFramework=''&includePrerelease=false" $entries = Invoke-RestMethod -Uri $uri -TimeoutSec 15 ConvertFrom-DFCatalogODataEntry -Source 'choco' -Query $Query -Entry @($entries) } function Search-DFCatalogChoco { <# .SYNOPSIS Cache-first Chocolatey community search. .PARAMETER Query Name or keywords. .PARAMETER Fresh Block on a live fetch. #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$Query, [switch]$Fresh ) Search-DFCatalogQueryCache -Provider 'choco' -Query $Query -Fresh:$Fresh ` -Fetch { param($q) Invoke-DFCatalogChocoFetch -Query $q } } function Get-DFCatalogChocoInstalled { <# .SYNOPSIS Lists choco-installed packages by parsing lib\*\*.nuspec — no choco.exe. .PARAMETER ChocolateyInstall Chocolatey root (defaults to $Env:ChocolateyInstall). #> [CmdletBinding()] param( [string]$ChocolateyInstall = $Env:ChocolateyInstall ) if (-not $ChocolateyInstall) { return } $lib = Join-Path $ChocolateyInstall 'lib' if (-not (Test-Path $lib)) { return } foreach ($dir in Get-ChildItem $lib -Directory) { $nuspec = Get-ChildItem $dir.FullName -Filter '*.nuspec' -File -ErrorAction Ignore | Select-Object -First 1 if (-not $nuspec) { continue } try { $metadata = ([xml](Get-Content $nuspec.FullName -Raw)).package.metadata } catch { continue } if (-not $metadata.id) { continue } [pscustomobject]@{ Source = 'choco' Name = [string]$metadata.id PackageId = [string]$metadata.id InstalledVersion = [string]$metadata.version } } } function ConvertFrom-DFCatalogODataDetailEntry { <# .SYNOPSIS Maps one NuGet v2 OData entry (choco / PSGallery) to a DotForge.ToolSourceDetail. Shared by both OData providers. .PARAMETER Source Provider name to stamp on the result. .PARAMETER Entry One feed entry as returned by Invoke-RestMethod. .PARAMETER InstallHint The catalog's install one-liner for this package. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Source, [AllowNull()] [object]$Entry, [Parameter(Mandatory)] [string]$InstallHint ) if (-not $Entry) { return $null } $props = Get-DFXmlMember -Element $Entry -Name 'properties' # NOTE: no <title> fallback here, unlike ConvertFrom-DFCatalogODataEntry. # Preserved as-is during the 2026-07-15 strict retrofit (behavior must not # change), but suspicious: if the detail endpoint's feed omits <d:Id> the way # the search feed does, this returns $null for every real package. Verify # against a live FindPackagesById response before changing. $id = Get-DFXmlText -Element $props -Name 'Id' if (-not $id) { return $null } $downloads = $null $raw = Get-DFXmlText -Element $props -Name 'DownloadCount' if ($raw) { try { $downloads = [long]$raw } catch {} } $deps = @(foreach ($spec in ((Get-DFXmlText -Element $props -Name 'Dependencies') -split '\|') -ne '') { # A dependency spec is 'id' or 'id:versionrange'. -split on a spec with # no colon yields a 1-element array, so $parts[1] would be an # out-of-bounds index — which throws under strict mode rather than # returning $null. Check the count instead of indexing speculatively. $parts = $spec -split ':' if ($parts.Count -gt 1 -and $parts[1]) { "$($parts[0]) ($($parts[1]))" } else { $parts[0] } }) $repositoryUrl = Get-DFXmlText -Element $props -Name 'ProjectSourceUrl' if (-not $repositoryUrl) { $repositoryUrl = Get-DFXmlText -Element $props -Name 'ProjectUrl' } New-DFToolSourceDetail -Source $Source ` -PackageId $id ` -Publisher ([string](Get-DFXmlText -Element $props -Name 'Authors')) ` -Dependencies $deps ` -Tags @((([string](Get-DFXmlText -Element $props -Name 'Tags')) -split '\s+') -ne '') ` -Downloads $downloads ` -ReleaseNotes ([string](Get-DFXmlText -Element $props -Name 'ReleaseNotes')) ` -RepositoryUrl ([string]$repositoryUrl) ` -DocsUrl ([string](Get-DFXmlText -Element $props -Name 'DocsUrl')) ` -InstallHint $InstallHint } function Invoke-DFCatalogChocoDetailFetch { <# .SYNOPSIS Live Chocolatey OData detail lookup (latest version of one id). .PARAMETER PackageId The choco package id. #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$PackageId ) $term = [uri]::EscapeDataString(($PackageId -replace "'", "''")) $uri = "https://community.chocolatey.org/api/v2/FindPackagesById()?id='$term'&`$filter=IsLatestVersion" $entry = @(Invoke-RestMethod -Uri $uri -TimeoutSec 15) | Select-Object -First 1 ConvertFrom-DFCatalogODataDetailEntry -Source 'choco' -Entry $entry -InstallHint "choco install $PackageId" } function Get-DFCatalogChocoDetail { <# .SYNOPSIS Cache-first Chocolatey detail lookup (72h TTL — slow, rate-limited API). .PARAMETER PackageId The choco package id. .PARAMETER Fresh Force a live fetch. #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$PackageId, [switch]$Fresh ) Get-DFCatalogDetailCache -Provider 'choco' -PackageId $PackageId -Fresh:$Fresh ` -Fetch { param($id) Invoke-DFCatalogChocoDetailFetch -PackageId $id } } if (-not (Get-Variable -Name DFCatalogProviders -Scope Script -ErrorAction Ignore)) { $script:DFCatalogProviders = @{} } $script:DFCatalogProviders['choco'] = @{ Name = 'choco' Kind = 'query-cache' Test = { $true } Search = { param($Query, $Fresh) Search-DFCatalogChoco -Query $Query -Fresh:$Fresh } GetInstalled = { Get-DFCatalogChocoInstalled } Refresh = { param($Query) if ($Query) { $null = Search-DFCatalogChoco -Query $Query -Fresh } } Detail = { param($PackageId, $Fresh) Get-DFCatalogChocoDetail -PackageId $PackageId -Fresh:$Fresh } } |