Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,19 @@
<!-- External packages -->
<PackageVersion Include="CliWrap" Version="3.10.1" />
<PackageVersion Include="ModelContextProtocol.Core" Version="1.2.0" />
<PackageVersion Include="NuGet.Common" Version="7.3.1" />
<PackageVersion Include="NuGet.Configuration" Version="7.3.1" />
<PackageVersion Include="NuGet.Protocol" Version="7.3.1" />
<PackageVersion Include="NuGet.Versioning" Version="7.3.1" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="StreamJsonRpc" Version="2.24.84" />
<PackageVersion Include="TimeWarp.Build.Tasks" Version="1.0.0" />
<PackageVersion Include="TimeWarp.Jaribu" Version="1.0.0-beta.12" />
<PackageVersion Include="TimeWarp.Nuru" Version="3.0.0-beta.68" />
<PackageVersion Include="TimeWarp.Terminal" Version="1.0.0-beta.12" />

<!-- Analyzers -->
<PackageVersion Include="Roslynator.Analyzers" Version="4.15.0" />
<PackageVersion Include="Roslynator.CodeAnalysis.Analyzers" Version="4.15.0" />
<PackageVersion Include="Roslynator.Formatting.Analyzers" Version="4.15.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.203" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeStyle" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />

<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion source/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<!-- Default package metadata (can be overridden in individual projects) -->
<PropertyGroup Label="Package Metadata">
<IsPackable>true</IsPackable>
<Version>1.0.0-beta.31</Version>
<Version>1.0.0-beta.32</Version>
<Authors>Steven T. Cramer</Authors>
<RepositoryUrl>https://github.com/TimeWarpEngineering/timewarp-amuru</RepositoryUrl>
<PackageLicenseExpression>Unlicense</PackageLicenseExpression>
Expand Down
5 changes: 1 addition & 4 deletions source/timewarp-amuru/global-usings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,4 +18,4 @@
global using System.Text.Json.Serialization;
global using System.Threading;
global using System.Xml.Linq;
global using TimeWarp.Terminal;
global using TimeWarp.Terminal;
183 changes: 127 additions & 56 deletions source/timewarp-amuru/nu-get/nuget-package-service.cs
Original file line number Diff line number Diff line change
@@ -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<NuGetSearchResult?> 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<FindPackageByIdResource>(cancellationToken);
if (resource == null)
{
return null;
}

using SourceCacheContext cacheContext = new();
#pragma warning disable IDE0007
IEnumerable<NuGetVersion> versions = await resource.GetAllVersionsAsync
#pragma warning restore IDE0007
(
packageId,
cacheContext,
NullLogger.Instance,
cancellationToken
);

#pragma warning disable IDE0007
List<NuGetVersion> versionList = versions.ToList();
#pragma warning restore IDE0007
List<NuGetVersion>? versionList = await GetVersionsAsync(packageId, cancellationToken);
if (versionList.Count == 0)
{
return null;
Expand All @@ -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<FindPackageByIdResource>(cancellationToken);
if (resource == null)
{
return null;
}

using SourceCacheContext cacheContext = new();
#pragma warning disable IDE0007
IEnumerable<NuGetVersion> versions = await resource.GetAllVersionsAsync
#pragma warning restore IDE0007
(
packageId,
cacheContext,
NullLogger.Instance,
cancellationToken
);
List<NuGetVersion>? versions = await GetVersionsAsync(packageId, cancellationToken);

NuGetVersion? stableVersion = null;
NuGetVersion? prereleaseVersion = null;
Expand Down Expand Up @@ -189,4 +150,114 @@ public string GetUpdateType(string currentVersion, string latestVersion)
if (latest.Minor > current.Minor) return "minor";
return "patch";
}
}

private static async Task<List<NuGetVersion>> 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<NuGetVersion> versions = [];
foreach (JsonElement pageElement in pagesElement.EnumerateArray())
{
await AddPageVersionsAsync(pageElement, versions, cancellationToken);
}

return versions;
}

private static async Task AddPageVersionsAsync
(
JsonElement pageElement,
List<NuGetVersion> 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<NuGetVersion> 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<JsonDocument> 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);
}
}
38 changes: 0 additions & 38 deletions source/timewarp-amuru/nu-get/nuget-source-cache.cs

This file was deleted.

3 changes: 0 additions & 3 deletions source/timewarp-amuru/timewarp-amuru.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@

<ItemGroup>
<PackageReference Include="CliWrap" />
<PackageReference Include="NuGet.Common" />
<PackageReference Include="NuGet.Configuration" />
<PackageReference Include="NuGet.Protocol" />
<PackageReference Include="NuGet.Versioning" />
<!-- <PackageReference Include="StreamJsonRpc" /> -->
<PackageReference Include="TimeWarp.Terminal" />
Expand Down
Loading