diff --git a/Directory.Packages.props b/Directory.Packages.props
index 9709d33..ee2f768 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -7,9 +7,6 @@
-
-
-
@@ -17,14 +14,12 @@
-
-
-
+
\ No newline at end of file
diff --git a/source/Directory.Build.props b/source/Directory.Build.props
index 9f1eeeb..cc41fe5 100644
--- a/source/Directory.Build.props
+++ b/source/Directory.Build.props
@@ -5,7 +5,7 @@
true
- 1.0.0-beta.31
+ 1.0.0-beta.32
Steven T. Cramer
https://github.com/TimeWarpEngineering/timewarp-amuru
Unlicense
diff --git a/source/timewarp-amuru/global-usings.cs b/source/timewarp-amuru/global-usings.cs
index 6043cf5..3036cb6 100644
--- a/source/timewarp-amuru/global-usings.cs
+++ b/source/timewarp-amuru/global-usings.cs
@@ -5,9 +5,6 @@
global using CliWrap;
global using CliWrap.Buffered;
global using CliWrap.EventStream;
-global using NuGet.Common;
-global using NuGet.Configuration;
-global using NuGet.Protocol.Core.Types;
global using NuGet.Versioning;
// global using StreamJsonRpc;
global using System;
@@ -21,4 +18,4 @@
global using System.Text.Json.Serialization;
global using System.Threading;
global using System.Xml.Linq;
-global using TimeWarp.Terminal;
\ No newline at end of file
+global using TimeWarp.Terminal;
diff --git a/source/timewarp-amuru/nu-get/nuget-package-service.cs b/source/timewarp-amuru/nu-get/nuget-package-service.cs
index cfb595a..272d732 100644
--- a/source/timewarp-amuru/nu-get/nuget-package-service.cs
+++ b/source/timewarp-amuru/nu-get/nuget-package-service.cs
@@ -1,59 +1,37 @@
#region Purpose
-// Implementation of NuGet package operations using NuGet Protocol API
+// Implementation of NuGet package operations using NuGet registration API
#endregion
#region Design
-// Uses FindPackageByIdResource instead of PackageMetadataResource because:
-// - GetAllVersionsAsync returns all versions including prerelease in one call
-// - Simpler API surface - no need for multiple queries
-// - Aggregate versions from all enabled NuGet sources for comprehensive results
-// - SourceRepository instances are cached via NuGetSourceCache to avoid repeated init
-// - Removed CLI-based approach (ParseSearchResult) for correctness and performance:
-// dotnet package search only returns latest version, Protocol gives all versions
+// Uses nuget.org's registration index to avoid the NuGet.Protocol dependency chain.
+// Keeps NuGet.Versioning for NuGet-compatible parsing, normalization, and comparison.
+// Registration metadata excludes unlisted packages and matches package update semantics.
+// The service targets nuget.org package version checks, not authenticated/custom feeds.
#endregion
namespace TimeWarp.Amuru;
public sealed class NuGetPackageService : INuGetPackageService
{
- private readonly NuGetSourceCache SourceCache;
+ private const string RegistrationBaseUrl = "https://api.nuget.org/v3/registration5-gz-semver2";
+ private static readonly HttpClient HttpClient = new
+ (
+ new HttpClientHandler
+ {
+ AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate,
+ CheckCertificateRevocationList = true
+ }
+ );
public NuGetPackageService()
- : this(new NuGetSourceCache())
- {
- }
-
- internal NuGetPackageService(NuGetSourceCache sourceCache)
{
- SourceCache = sourceCache;
}
public async Task SearchAsync(string packageId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(packageId);
- SourceRepository repository = SourceCache.GetOrCreate("https://api.nuget.org/v3/index.json");
-
- FindPackageByIdResource? resource = await repository.GetResourceAsync(cancellationToken);
- if (resource == null)
- {
- return null;
- }
-
- using SourceCacheContext cacheContext = new();
- #pragma warning disable IDE0007
- IEnumerable versions = await resource.GetAllVersionsAsync
-#pragma warning restore IDE0007
- (
- packageId,
- cacheContext,
- NullLogger.Instance,
- cancellationToken
- );
-
-#pragma warning disable IDE0007
- List versionList = versions.ToList();
-#pragma warning restore IDE0007
+ List? versionList = await GetVersionsAsync(packageId, cancellationToken);
if (versionList.Count == 0)
{
return null;
@@ -73,24 +51,7 @@ internal NuGetPackageService(NuGetSourceCache sourceCache)
{
ArgumentException.ThrowIfNullOrWhiteSpace(packageId);
- SourceRepository repository = SourceCache.GetOrCreate("https://api.nuget.org/v3/index.json");
-
- FindPackageByIdResource? resource = await repository.GetResourceAsync(cancellationToken);
- if (resource == null)
- {
- return null;
- }
-
- using SourceCacheContext cacheContext = new();
- #pragma warning disable IDE0007
- IEnumerable versions = await resource.GetAllVersionsAsync
-#pragma warning restore IDE0007
- (
- packageId,
- cacheContext,
- NullLogger.Instance,
- cancellationToken
- );
+ List? versions = await GetVersionsAsync(packageId, cancellationToken);
NuGetVersion? stableVersion = null;
NuGetVersion? prereleaseVersion = null;
@@ -189,4 +150,114 @@ public string GetUpdateType(string currentVersion, string latestVersion)
if (latest.Minor > current.Minor) return "minor";
return "patch";
}
-}
\ No newline at end of file
+
+ private static async Task> GetVersionsAsync(string packageId, CancellationToken cancellationToken)
+ {
+ string lowerPackageId = packageId.ToLowerInvariant();
+ string escapedPackageId = Uri.EscapeDataString(lowerPackageId);
+ string url = $"{RegistrationBaseUrl}/{escapedPackageId}/index.json";
+
+ using HttpRequestMessage request = new(HttpMethod.Get, url);
+ using HttpResponseMessage response = await HttpClient.SendAsync(request, cancellationToken);
+
+ if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
+ {
+ return [];
+ }
+
+ response.EnsureSuccessStatusCode();
+
+ using JsonDocument document = await ReadJsonDocumentAsync(response.Content, cancellationToken);
+
+ if (!document.RootElement.TryGetProperty("items", out JsonElement pagesElement) ||
+ pagesElement.ValueKind != JsonValueKind.Array)
+ {
+ return [];
+ }
+
+ List versions = [];
+ foreach (JsonElement pageElement in pagesElement.EnumerateArray())
+ {
+ await AddPageVersionsAsync(pageElement, versions, cancellationToken);
+ }
+
+ return versions;
+ }
+
+ private static async Task AddPageVersionsAsync
+ (
+ JsonElement pageElement,
+ List versions,
+ CancellationToken cancellationToken
+ )
+ {
+ if (!pageElement.TryGetProperty("items", out JsonElement itemsElement))
+ {
+ if (!pageElement.TryGetProperty("@id", out JsonElement pageUrlElement))
+ {
+ return;
+ }
+
+ string? pageUrl = pageUrlElement.GetString();
+ if (string.IsNullOrWhiteSpace(pageUrl))
+ {
+ return;
+ }
+
+ using HttpRequestMessage request = new(HttpMethod.Get, pageUrl);
+ using HttpResponseMessage response = await HttpClient.SendAsync(request, cancellationToken);
+ response.EnsureSuccessStatusCode();
+
+ using JsonDocument pageDocument = await ReadJsonDocumentAsync(response.Content, cancellationToken);
+
+ if (!pageDocument.RootElement.TryGetProperty("items", out itemsElement))
+ {
+ return;
+ }
+ }
+
+ AddLeafVersions(itemsElement, versions);
+ }
+
+ private static void AddLeafVersions(JsonElement itemsElement, List versions)
+ {
+ if (itemsElement.ValueKind != JsonValueKind.Array)
+ {
+ return;
+ }
+
+ foreach (JsonElement itemElement in itemsElement.EnumerateArray())
+ {
+ if (!itemElement.TryGetProperty("catalogEntry", out JsonElement catalogEntryElement) ||
+ !catalogEntryElement.TryGetProperty("version", out JsonElement versionElement))
+ {
+ continue;
+ }
+
+ string? version = versionElement.GetString();
+ if (version != null && NuGetVersion.TryParse(version, out NuGetVersion? parsedVersion))
+ {
+ versions.Add(parsedVersion);
+ }
+ }
+ }
+
+ private static async Task ReadJsonDocumentAsync(HttpContent content, CancellationToken cancellationToken)
+ {
+ byte[] bytes = await content.ReadAsByteArrayAsync(cancellationToken);
+ if (bytes.Length >= 2 && bytes[0] == 0x1F && bytes[1] == 0x8B)
+ {
+ using MemoryStream compressedStream = new(bytes);
+ using System.IO.Compression.GZipStream gzipStream = new
+ (
+ compressedStream,
+ System.IO.Compression.CompressionMode.Decompress
+ );
+ using MemoryStream decompressedStream = new();
+ await gzipStream.CopyToAsync(decompressedStream, cancellationToken);
+ return JsonDocument.Parse(decompressedStream.ToArray());
+ }
+
+ return JsonDocument.Parse(bytes);
+ }
+}
diff --git a/source/timewarp-amuru/nu-get/nuget-source-cache.cs b/source/timewarp-amuru/nu-get/nuget-source-cache.cs
deleted file mode 100644
index 6128f85..0000000
--- a/source/timewarp-amuru/nu-get/nuget-source-cache.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-#region Purpose
-// Cache for NuGet SourceRepository instances per source URL
-#endregion
-
-#region Design
-// SourceRepository creation involves network calls and resource initialization.
-// Caching avoids repeated initialization when querying the same feed multiple times.
-// Uses ConcurrentDictionary for thread-safe access without explicit locking.
-#endregion
-
-namespace TimeWarp.Amuru;
-
-public sealed class NuGetSourceCache
-{
- private readonly ConcurrentDictionary Repositories = new(StringComparer.OrdinalIgnoreCase);
-
- #pragma warning disable CA1054
- public SourceRepository GetOrCreate(string sourceUrl)
-#pragma warning restore CA1054
- {
- ArgumentException.ThrowIfNullOrWhiteSpace(sourceUrl);
-
- return Repositories.GetOrAdd
- (
- sourceUrl,
- static url =>
- {
- PackageSource packageSource = new(url);
- return Repository.CreateSource(Repository.Provider.GetCoreV3(), packageSource);
- }
- );
- }
-
- public void Clear()
- {
- Repositories.Clear();
- }
-}
\ No newline at end of file
diff --git a/source/timewarp-amuru/timewarp-amuru.csproj b/source/timewarp-amuru/timewarp-amuru.csproj
index 53ae516..89b15f9 100644
--- a/source/timewarp-amuru/timewarp-amuru.csproj
+++ b/source/timewarp-amuru/timewarp-amuru.csproj
@@ -10,9 +10,6 @@
-
-
-