From f60d139ef8075955b38b5d17f474fe73be6151e9 Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Mon, 17 Mar 2025 22:24:13 -0500 Subject: [PATCH 01/17] Fix Choco get property --- API/Choco/Choco.cs | 2 +- API/Choco/Constants.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/API/Choco/Choco.cs b/API/Choco/Choco.cs index 9a79d94b..8868fe45 100644 --- a/API/Choco/Choco.cs +++ b/API/Choco/Choco.cs @@ -40,7 +40,7 @@ public static async Task GetPackageAsync(string id, Version version) public static async Task GetPackagePropertyAsync(string id, Version version, string propertyName) { - var entry = await Constants.CHOCOLATEY_API_HOST.AppendPathSegment($"Packages(Id='{id}', Version='{version}')") + var entry = await Constants.CHOCOLATEY_API_HOST.AppendPathSegment($"Packages(Id='{id}',Version='{version}')") .AppendPathSegment(propertyName) .GetXDocumentAsync(); return entry.Root.Value; diff --git a/API/Choco/Constants.cs b/API/Choco/Constants.cs index e7daebc1..f2c0979a 100644 --- a/API/Choco/Constants.cs +++ b/API/Choco/Constants.cs @@ -4,7 +4,8 @@ namespace Chocolatey { public static class Constants { - private const string CHOCOLATEY_API_BASE = "community.chocolatey.org/api/v2"; + private const string CHOCOLATEY_DOMAIN = "chocolatey.org"; + private const string CHOCOLATEY_API_BASE = CHOCOLATEY_DOMAIN + "/api/v2"; public const string CHOCOLATEY_API_HOST = "https://" + CHOCOLATEY_API_BASE; public static readonly XNamespace XMLNS_ATOM = "http://www.w3.org/2005/Atom"; From 2566254eb4960a0e081ac66f942b72c2e8364d9c Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Mon, 17 Mar 2025 22:25:40 -0500 Subject: [PATCH 02/17] Update ChocolateyTests to .NET 8 --- Tests/ChocolateyTests/ChocolateyTests.csproj | 38 ++++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Tests/ChocolateyTests/ChocolateyTests.csproj b/Tests/ChocolateyTests/ChocolateyTests.csproj index 2b87dcab..dffa8e06 100644 --- a/Tests/ChocolateyTests/ChocolateyTests.csproj +++ b/Tests/ChocolateyTests/ChocolateyTests.csproj @@ -1,26 +1,26 @@  - - net6.0 + + net8.0 - false - + false + - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + - - - + + + From 6de859f6e3c829d2143280e370b981c4b5f6176e Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Mon, 17 Mar 2025 22:28:24 -0500 Subject: [PATCH 03/17] Print search results --- PublishTool/Properties/launchSettings.json | 2 +- Tests/ChocolateyTests/Search.cs | 94 ++++++++++++---------- 2 files changed, 54 insertions(+), 42 deletions(-) diff --git a/PublishTool/Properties/launchSettings.json b/PublishTool/Properties/launchSettings.json index bf4b952a..babfe8a0 100644 --- a/PublishTool/Properties/launchSettings.json +++ b/PublishTool/Properties/launchSettings.json @@ -3,7 +3,7 @@ "PublishTool": { "commandName": "Project", //"commandLineArgs": "--repo=E:\\Repos\\yoshiask\\FluentStore -f" - "commandLineArgs": "--repo=E:\\Repos\\yoshiask\\FluentStore --id=FluentStore.Sources.FluentStore -v -f --install" + "commandLineArgs": "--repo=E:\\Repos\\yoshiask\\FluentStore --id=FluentStore.Sources.Microsoft -v -f --install" } } } \ No newline at end of file diff --git a/Tests/ChocolateyTests/Search.cs b/Tests/ChocolateyTests/Search.cs index ba131e88..f796bc58 100644 --- a/Tests/ChocolateyTests/Search.cs +++ b/Tests/ChocolateyTests/Search.cs @@ -1,57 +1,69 @@ using Chocolatey; using System.Threading.Tasks; using Xunit; +using Xunit.Abstractions; -namespace ChocolateyTests +namespace ChocolateyTests; + +public class Search(ITestOutputHelper output) { - public class Search + [Fact] + public async Task SearchAsync_Minimum1() + { + var actualResults = await Choco.SearchAsync("git"); + foreach (var result in actualResults) + output.WriteLine($"{result.Title} {result.Version}"); + + Assert.Equal(30, actualResults.Count); + + var firstResult = actualResults[0]; + Assert.Equal("git", firstResult.Id); + Assert.Equal("Git", firstResult.Title); + Assert.StartsWith("https://community.chocolatey.org/api/v2/package/git/", firstResult.DownloadUrl); + } + + [Fact] + public async Task SearchAsync_MinimumWithSpace() { - [Fact] - public async Task SearchAsync_Minimum1() + var actualResults = await Choco.SearchAsync("google chrome"); + foreach (var result in actualResults) + output.WriteLine($"{result.Title} {result.Version}"); + + Assert.Equal(30, actualResults.Count); + foreach (var result in actualResults) { - var actualResult = await Choco.SearchAsync("git"); - Assert.Equal(30, actualResult.Count); - - var firstResult = actualResult[0]; - Assert.Equal("git", firstResult.Id); - Assert.Equal("Git", firstResult.Title); - Assert.StartsWith("https://community.chocolatey.org/api/v2/package/git/", firstResult.DownloadUrl); + Assert.NotNull(result.Id); + Assert.NotNull(result.Title); } + } + + [Fact] + public async Task SearchAsync_Minimum2Pages() + { + string query = "python"; + int pageSize = 10; - [Fact] - public async Task SearchAsync_MinimumWithSpace() + var results1 = await Choco.SearchAsync(query, top: pageSize, skip: 0); + foreach (var result in results1) + output.WriteLine($"{result.Title} {result.Version}"); + + Assert.Equal(pageSize, results1.Count); + foreach (var result in results1) { - var actualResult = await Choco.SearchAsync("google chrome"); - Assert.Equal(30, actualResult.Count); - foreach (var result in actualResult) - { - Assert.NotNull(result.Id); - Assert.NotNull(result.Title); - } + Assert.NotNull(result.Id); + Assert.NotNull(result.Title); } - [Fact] - public async Task SearchAsync_Minimum2Pages() + var results2 = await Choco.SearchAsync(query, top: pageSize, skip: pageSize); + foreach (var result in results2) + output.WriteLine($"{result.Title} {result.Version}"); + + Assert.Equal(pageSize, results2.Count); + foreach (var result in results2) { - string query = "python"; - int pageSize = 10; - - var results1 = await Choco.SearchAsync(query, top: pageSize, skip: 0); - Assert.Equal(pageSize, results1.Count); - foreach (var result in results1) - { - Assert.NotNull(result.Id); - Assert.NotNull(result.Title); - } - - var results2 = await Choco.SearchAsync(query, top: pageSize, skip: pageSize); - Assert.Equal(pageSize, results2.Count); - foreach (var result in results2) - { - Assert.NotNull(result.Id); - Assert.NotNull(result.Title); - Assert.DoesNotContain(results1, r1 => r1.Id == result.Id); - } + Assert.NotNull(result.Id); + Assert.NotNull(result.Title); + Assert.DoesNotContain(results1, r1 => r1.Id == result.Id); } } } From b1394b132d3909ccf0a2e92b2869311a7846ceca Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Mon, 17 Mar 2025 23:15:25 -0500 Subject: [PATCH 04/17] Move parse delegate to own file --- API/Choco/Choco.cs | 104 +++++++++++++++++-------------------- API/Choco/ParseDelegate.cs | 6 +++ 2 files changed, 55 insertions(+), 55 deletions(-) create mode 100644 API/Choco/ParseDelegate.cs diff --git a/API/Choco/Choco.cs b/API/Choco/Choco.cs index 8868fe45..a744df7b 100644 --- a/API/Choco/Choco.cs +++ b/API/Choco/Choco.cs @@ -8,68 +8,62 @@ using System.Threading.Tasks; using System.Xml.Linq; -namespace Chocolatey +namespace Chocolatey; + +public static class Choco { - public static class Choco + public static async Task> SearchAsync(string query, string targetFramework = "", bool includePrerelease = false, int top = 30, int skip = 0) { - /// - /// A method that parses a into the specified . - /// - public delegate T Parse(string str); - - public static async Task> SearchAsync(string query, string targetFramework = "", bool includePrerelease = false, int top = 30, int skip = 0) - { - var doc = await Constants.CHOCOLATEY_API_HOST.AppendPathSegment("Search()") - .SetQueryParam("$filter", "IsLatestVersion").SetQueryParam("$top", top).SetQueryParam("$skip", skip) - .SetQueryParam("searchTerm", "'" + query + "'") - .SetQueryParam("targetFramework", "'" + targetFramework + "'") - .SetQueryParam("includePrerelease", includePrerelease.ToLowerString()) - .GetXDocumentAsync(); - IEnumerable entries = doc.Root.Elements().Where(elem => elem.Name.LocalName == "entry"); - - // Parse Atom entries into Package model - return entries.Select(entry => new Package(entry)).ToList(); - } + var doc = await Constants.CHOCOLATEY_API_HOST.AppendPathSegment("Search()") + .SetQueryParam("$filter", "IsLatestVersion").SetQueryParam("$top", top).SetQueryParam("$skip", skip) + .SetQueryParam("searchTerm", "'" + query + "'") + .SetQueryParam("targetFramework", "'" + targetFramework + "'") + .SetQueryParam("includePrerelease", includePrerelease.ToLowerString()) + .GetXDocumentAsync(); + IEnumerable entries = doc.Root.Elements().Where(elem => elem.Name.LocalName == "entry"); - public static async Task GetPackageAsync(string id, Version version) - { - var entry = await Constants.CHOCOLATEY_API_HOST.AppendPathSegment($"Packages(Id='{id}',Version='{version}')") - .GetXDocumentAsync(); - return new Package(entry.Root); - } + // Parse Atom entries into Package model + return entries.Select(entry => new Package(entry)).ToList(); + } - public static async Task GetPackagePropertyAsync(string id, Version version, string propertyName) - { - var entry = await Constants.CHOCOLATEY_API_HOST.AppendPathSegment($"Packages(Id='{id}',Version='{version}')") - .AppendPathSegment(propertyName) - .GetXDocumentAsync(); - return entry.Root.Value; - } + public static async Task GetPackageAsync(string id, Version version) + { + var entry = await Constants.CHOCOLATEY_API_HOST.AppendPathSegment($"Packages(Id='{id}',Version='{version}')") + .GetXDocumentAsync(); + return new Package(entry.Root); + } - public static async Task GetPackagePropertyAsync(string id, Version version, string propertyName, Parse parse) - { - return parse(await GetPackagePropertyAsync(id, version, propertyName)); - } + public static async Task GetPackagePropertyAsync(string id, Version version, string propertyName) + { + var entry = await Constants.CHOCOLATEY_API_HOST.AppendPathSegment($"Packages(Id='{id}',Version='{version}')") + .AppendPathSegment(propertyName) + .GetXDocumentAsync(); + return entry.Root.Value; + } - #region GetPackage*PropertyAsync + public static async Task GetPackagePropertyAsync(string id, Version version, string propertyName, Parse parse) + { + return parse(await GetPackagePropertyAsync(id, version, propertyName)); + } - public static async Task GetPackageDatePropertyAsync(string id, Version version, string propertyName) - { - return await GetPackagePropertyAsync(id, version, propertyName, DateTimeOffset.Parse); - } - public static async Task GetPackageBooleanPropertyAsync(string id, Version version, string propertyName) - { - return await GetPackagePropertyAsync(id, version, propertyName, bool.Parse); - } - public static async Task GetPackageInt32PropertyAsync(string id, Version version, string propertyName) - { - return await GetPackagePropertyAsync(id, version, propertyName, int.Parse); - } - public static async Task GetPackageInt64PropertyAsync(string id, Version version, string propertyName) - { - return await GetPackagePropertyAsync(id, version, propertyName, long.Parse); - } + #region GetPackage*PropertyAsync - #endregion + public static async Task GetPackageDatePropertyAsync(string id, Version version, string propertyName) + { + return await GetPackagePropertyAsync(id, version, propertyName, DateTimeOffset.Parse); } + public static async Task GetPackageBooleanPropertyAsync(string id, Version version, string propertyName) + { + return await GetPackagePropertyAsync(id, version, propertyName, bool.Parse); + } + public static async Task GetPackageInt32PropertyAsync(string id, Version version, string propertyName) + { + return await GetPackagePropertyAsync(id, version, propertyName, int.Parse); + } + public static async Task GetPackageInt64PropertyAsync(string id, Version version, string propertyName) + { + return await GetPackagePropertyAsync(id, version, propertyName, long.Parse); + } + + #endregion } diff --git a/API/Choco/ParseDelegate.cs b/API/Choco/ParseDelegate.cs new file mode 100644 index 00000000..4fd9829e --- /dev/null +++ b/API/Choco/ParseDelegate.cs @@ -0,0 +1,6 @@ +namespace Chocolatey; + +/// +/// A method that parses a into the specified . +/// +public delegate T Parse(string str); From 5aec6a64d40b248c41444327a6af365e657ef07e Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Mon, 17 Mar 2025 23:31:10 -0500 Subject: [PATCH 05/17] Create search interface --- API/Choco/Choco.cs | 69 --------------------- API/Choco/ChocoCommunityWebClient.cs | 43 +++++++++++++ API/Choco/IChocoSearchService.cs | 41 ++++++++++++ Tests/ChocolateyTests/GetPackageProperty.cs | 28 +++++---- Tests/ChocolateyTests/Search.cs | 10 +-- 5 files changed, 105 insertions(+), 86 deletions(-) delete mode 100644 API/Choco/Choco.cs create mode 100644 API/Choco/ChocoCommunityWebClient.cs create mode 100644 API/Choco/IChocoSearchService.cs diff --git a/API/Choco/Choco.cs b/API/Choco/Choco.cs deleted file mode 100644 index a744df7b..00000000 --- a/API/Choco/Choco.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Chocolatey.Models; -using Flurl; -using Flurl.Http; -using Flurl.Http.Xml; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.Xml.Linq; - -namespace Chocolatey; - -public static class Choco -{ - public static async Task> SearchAsync(string query, string targetFramework = "", bool includePrerelease = false, int top = 30, int skip = 0) - { - var doc = await Constants.CHOCOLATEY_API_HOST.AppendPathSegment("Search()") - .SetQueryParam("$filter", "IsLatestVersion").SetQueryParam("$top", top).SetQueryParam("$skip", skip) - .SetQueryParam("searchTerm", "'" + query + "'") - .SetQueryParam("targetFramework", "'" + targetFramework + "'") - .SetQueryParam("includePrerelease", includePrerelease.ToLowerString()) - .GetXDocumentAsync(); - IEnumerable entries = doc.Root.Elements().Where(elem => elem.Name.LocalName == "entry"); - - // Parse Atom entries into Package model - return entries.Select(entry => new Package(entry)).ToList(); - } - - public static async Task GetPackageAsync(string id, Version version) - { - var entry = await Constants.CHOCOLATEY_API_HOST.AppendPathSegment($"Packages(Id='{id}',Version='{version}')") - .GetXDocumentAsync(); - return new Package(entry.Root); - } - - public static async Task GetPackagePropertyAsync(string id, Version version, string propertyName) - { - var entry = await Constants.CHOCOLATEY_API_HOST.AppendPathSegment($"Packages(Id='{id}',Version='{version}')") - .AppendPathSegment(propertyName) - .GetXDocumentAsync(); - return entry.Root.Value; - } - - public static async Task GetPackagePropertyAsync(string id, Version version, string propertyName, Parse parse) - { - return parse(await GetPackagePropertyAsync(id, version, propertyName)); - } - - #region GetPackage*PropertyAsync - - public static async Task GetPackageDatePropertyAsync(string id, Version version, string propertyName) - { - return await GetPackagePropertyAsync(id, version, propertyName, DateTimeOffset.Parse); - } - public static async Task GetPackageBooleanPropertyAsync(string id, Version version, string propertyName) - { - return await GetPackagePropertyAsync(id, version, propertyName, bool.Parse); - } - public static async Task GetPackageInt32PropertyAsync(string id, Version version, string propertyName) - { - return await GetPackagePropertyAsync(id, version, propertyName, int.Parse); - } - public static async Task GetPackageInt64PropertyAsync(string id, Version version, string propertyName) - { - return await GetPackagePropertyAsync(id, version, propertyName, long.Parse); - } - - #endregion -} diff --git a/API/Choco/ChocoCommunityWebClient.cs b/API/Choco/ChocoCommunityWebClient.cs new file mode 100644 index 00000000..9f8c4e29 --- /dev/null +++ b/API/Choco/ChocoCommunityWebClient.cs @@ -0,0 +1,43 @@ +using Chocolatey.Models; +using Flurl; +using Flurl.Http; +using Flurl.Http.Xml; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace Chocolatey; + +public class ChocoCommunityWebClient : IChocoSearchService +{ + public async Task> SearchAsync(string query, string targetFramework = "", bool includePrerelease = false, int top = 30, int skip = 0) + { + var doc = await Constants.CHOCOLATEY_API_HOST.AppendPathSegment("Search()") + .SetQueryParam("$filter", "IsLatestVersion").SetQueryParam("$top", top).SetQueryParam("$skip", skip) + .SetQueryParam("searchTerm", "'" + query + "'") + .SetQueryParam("targetFramework", "'" + targetFramework + "'") + .SetQueryParam("includePrerelease", includePrerelease.ToLowerString()) + .GetXDocumentAsync(); + IEnumerable entries = doc.Root.Elements().Where(elem => elem.Name.LocalName == "entry"); + + // Parse Atom entries into Package model + return entries.Select(entry => new Package(entry)).ToList(); + } + + public async Task GetPackageAsync(string id, Version version) + { + var entry = await Constants.CHOCOLATEY_API_HOST.AppendPathSegment($"Packages(Id='{id}',Version='{version}')") + .GetXDocumentAsync(); + return new Package(entry.Root); + } + + public async Task GetPackagePropertyAsync(string id, Version version, string propertyName) + { + var entry = await Constants.CHOCOLATEY_API_HOST.AppendPathSegment($"Packages(Id='{id}',Version='{version}')") + .AppendPathSegment(propertyName) + .GetXDocumentAsync(); + return entry.Root.Value; + } +} diff --git a/API/Choco/IChocoSearchService.cs b/API/Choco/IChocoSearchService.cs new file mode 100644 index 00000000..e70b8389 --- /dev/null +++ b/API/Choco/IChocoSearchService.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Chocolatey.Models; + +namespace Chocolatey; + +public interface IChocoSearchService +{ + Task> SearchAsync(string query, string targetFramework = "", bool includePrerelease = false, int top = 30, int skip = 0); + + Task GetPackageAsync(string id, Version version); + + Task GetPackagePropertyAsync(string id, Version version, string propertyName); +} + +public static class ChocoSearchServiceExtensions +{ + public static async Task GetPackagePropertyAsync(this IChocoSearchService service, string id, Version version, string propertyName, Parse parse) + { + string str = await service.GetPackagePropertyAsync(id, version, propertyName); + return parse(str); + } + + public static async Task GetPackageDatePropertyAsync(this IChocoSearchService service, string id, Version version, string propertyName) + { + return await service.GetPackagePropertyAsync(id, version, propertyName, DateTimeOffset.Parse); + } + public static async Task GetPackageBooleanPropertyAsync(this IChocoSearchService service, string id, Version version, string propertyName) + { + return await service.GetPackagePropertyAsync(id, version, propertyName, bool.Parse); + } + public static async Task GetPackageInt32PropertyAsync(this IChocoSearchService service, string id, Version version, string propertyName) + { + return await service.GetPackagePropertyAsync(id, version, propertyName, int.Parse); + } + public static async Task GetPackageInt64PropertyAsync(this IChocoSearchService service, string id, Version version, string propertyName) + { + return await service.GetPackagePropertyAsync(id, version, propertyName, long.Parse); + } +} diff --git a/Tests/ChocolateyTests/GetPackageProperty.cs b/Tests/ChocolateyTests/GetPackageProperty.cs index f6f27c5f..4f74ba32 100644 --- a/Tests/ChocolateyTests/GetPackageProperty.cs +++ b/Tests/ChocolateyTests/GetPackageProperty.cs @@ -8,6 +8,8 @@ namespace ChocolateyTests { public class GetPackageProperty { + private readonly IChocoSearchService _client = new ChocoCommunityWebClient(); + [Fact] public async Task GetPackagePropertyAsync() { @@ -15,13 +17,13 @@ public async Task GetPackagePropertyAsync() Version v = new(2, 35, 1, 2); string actual; - actual = await Choco.GetPackagePropertyAsync(id, v, "Title"); + actual = await _client.GetPackagePropertyAsync(id, v, "Title"); Assert.Equal("Git", actual); - actual = await Choco.GetPackagePropertyAsync(id, v, "Id"); + actual = await _client.GetPackagePropertyAsync(id, v, "Id"); Assert.Equal(id, actual); - actual = await Choco.GetPackagePropertyAsync(id, v, "GalleryDetailsUrl"); + actual = await _client.GetPackagePropertyAsync(id, v, "GalleryDetailsUrl"); Assert.Equal($"https://community.chocolatey.org/packages/{id}/{v}", actual); } @@ -32,13 +34,13 @@ public async Task GetPackageDatePropertyAsync() Version v = new(2, 35, 1, 2); DateTimeOffset actual; - actual = await Choco.GetPackageDatePropertyAsync(id, v, "Created"); + actual = await _client.GetPackageDatePropertyAsync(id, v, "Created"); Assert.Equal(DateTimeOffset.Parse("2022-02-01T18:09:34.013"), actual); - actual = await Choco.GetPackageDatePropertyAsync(id, v, "Published"); + actual = await _client.GetPackageDatePropertyAsync(id, v, "Published"); Assert.Equal(DateTimeOffset.Parse("2022-02-01T18:09:34.013"), actual); - actual = await Choco.GetPackageDatePropertyAsync(id, v, "PackageReviewedDate"); + actual = await _client.GetPackageDatePropertyAsync(id, v, "PackageReviewedDate"); Assert.Equal(DateTimeOffset.Parse("2022-02-02T01:46:13.997"), actual); } @@ -49,13 +51,13 @@ public async Task GetPackageBooleanPropertyAsync() Version v = new(2, 35, 1, 2); bool actual; - actual = await Choco.GetPackageBooleanPropertyAsync(id, v, "IsPrerelease"); + actual = await _client.GetPackageBooleanPropertyAsync(id, v, "IsPrerelease"); Assert.False(actual); - actual = await Choco.GetPackageBooleanPropertyAsync(id, v, "IsApproved"); + actual = await _client.GetPackageBooleanPropertyAsync(id, v, "IsApproved"); Assert.True(actual); - actual = await Choco.GetPackageBooleanPropertyAsync(id, v, "RequireLicenseAcceptance"); + actual = await _client.GetPackageBooleanPropertyAsync(id, v, "RequireLicenseAcceptance"); Assert.False(actual); } @@ -67,9 +69,9 @@ public async Task GetPackageInt32PropertyAsync() int actual; // Don't assert an exact value, download counts are subject to change - actual = await Choco.GetPackageInt32PropertyAsync(id, v, "DownloadCount"); + actual = await _client.GetPackageInt32PropertyAsync(id, v, "DownloadCount"); - actual = await Choco.GetPackageInt32PropertyAsync(id, v, "VersionDownloadCount"); + actual = await _client.GetPackageInt32PropertyAsync(id, v, "VersionDownloadCount"); } [Fact] @@ -79,7 +81,7 @@ public async Task GetPackageInt64PropertyAsync() Version v = new(2, 35, 1, 2); long actual; - actual = await Choco.GetPackageInt64PropertyAsync(id, v, "PackageSize"); + actual = await _client.GetPackageInt64PropertyAsync(id, v, "PackageSize"); Assert.Equal(8170L, actual); } @@ -90,7 +92,7 @@ public async Task GetPackagePropertyAsync_Enum() Version v = new(2, 35, 1, 2); PackageStatus actual; - actual = await Choco.GetPackagePropertyAsync(id, v, "PackageStatus", s => Enum.Parse(s)); + actual = await _client.GetPackagePropertyAsync(id, v, "PackageStatus", Enum.Parse); Assert.Equal(PackageStatus.Approved, actual); } } diff --git a/Tests/ChocolateyTests/Search.cs b/Tests/ChocolateyTests/Search.cs index f796bc58..83b8f912 100644 --- a/Tests/ChocolateyTests/Search.cs +++ b/Tests/ChocolateyTests/Search.cs @@ -7,10 +7,12 @@ namespace ChocolateyTests; public class Search(ITestOutputHelper output) { + private readonly IChocoSearchService _client = new ChocoCommunityWebClient(); + [Fact] public async Task SearchAsync_Minimum1() { - var actualResults = await Choco.SearchAsync("git"); + var actualResults = await _client.SearchAsync("git"); foreach (var result in actualResults) output.WriteLine($"{result.Title} {result.Version}"); @@ -25,7 +27,7 @@ public async Task SearchAsync_Minimum1() [Fact] public async Task SearchAsync_MinimumWithSpace() { - var actualResults = await Choco.SearchAsync("google chrome"); + var actualResults = await _client.SearchAsync("google chrome"); foreach (var result in actualResults) output.WriteLine($"{result.Title} {result.Version}"); @@ -43,7 +45,7 @@ public async Task SearchAsync_Minimum2Pages() string query = "python"; int pageSize = 10; - var results1 = await Choco.SearchAsync(query, top: pageSize, skip: 0); + var results1 = await _client.SearchAsync(query, top: pageSize, skip: 0); foreach (var result in results1) output.WriteLine($"{result.Title} {result.Version}"); @@ -54,7 +56,7 @@ public async Task SearchAsync_Minimum2Pages() Assert.NotNull(result.Title); } - var results2 = await Choco.SearchAsync(query, top: pageSize, skip: pageSize); + var results2 = await _client.SearchAsync(query, top: pageSize, skip: pageSize); foreach (var result in results2) output.WriteLine($"{result.Title} {result.Version}"); From 288d49944d677d9600ce093a1ee8e38f0214596d Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Mon, 17 Mar 2025 23:57:20 -0500 Subject: [PATCH 06/17] Use NuGetVersion and create IChocoSearchService --- API/Choco/ChocoCommunityWebClient.cs | 7 +++---- API/Choco/Chocolatey.csproj | 2 ++ API/Choco/IChocoSearchService.cs | 20 +++++++++++++------- Tests/ChocolateyTests/GetPackageProperty.cs | 13 +++++++------ 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/API/Choco/ChocoCommunityWebClient.cs b/API/Choco/ChocoCommunityWebClient.cs index 9f8c4e29..ac1eb62a 100644 --- a/API/Choco/ChocoCommunityWebClient.cs +++ b/API/Choco/ChocoCommunityWebClient.cs @@ -1,12 +1,11 @@ using Chocolatey.Models; using Flurl; -using Flurl.Http; using Flurl.Http.Xml; -using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Xml.Linq; +using NuGet.Versioning; namespace Chocolatey; @@ -26,14 +25,14 @@ public async Task> SearchAsync(string query, string targe return entries.Select(entry => new Package(entry)).ToList(); } - public async Task GetPackageAsync(string id, Version version) + public async Task GetPackageAsync(string id, NuGetVersion version) { var entry = await Constants.CHOCOLATEY_API_HOST.AppendPathSegment($"Packages(Id='{id}',Version='{version}')") .GetXDocumentAsync(); return new Package(entry.Root); } - public async Task GetPackagePropertyAsync(string id, Version version, string propertyName) + public async Task GetPackagePropertyAsync(string id, NuGetVersion version, string propertyName) { var entry = await Constants.CHOCOLATEY_API_HOST.AppendPathSegment($"Packages(Id='{id}',Version='{version}')") .AppendPathSegment(propertyName) diff --git a/API/Choco/Chocolatey.csproj b/API/Choco/Chocolatey.csproj index 4bce8806..08350cbb 100644 --- a/API/Choco/Chocolatey.csproj +++ b/API/Choco/Chocolatey.csproj @@ -8,8 +8,10 @@ + + diff --git a/API/Choco/IChocoSearchService.cs b/API/Choco/IChocoSearchService.cs index e70b8389..31a4193b 100644 --- a/API/Choco/IChocoSearchService.cs +++ b/API/Choco/IChocoSearchService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Chocolatey.Models; +using NuGet.Versioning; namespace Chocolatey; @@ -9,32 +10,37 @@ public interface IChocoSearchService { Task> SearchAsync(string query, string targetFramework = "", bool includePrerelease = false, int top = 30, int skip = 0); - Task GetPackageAsync(string id, Version version); + Task GetPackageAsync(string id, NuGetVersion version); - Task GetPackagePropertyAsync(string id, Version version, string propertyName); + Task GetPackagePropertyAsync(string id, NuGetVersion version, string propertyName); } public static class ChocoSearchServiceExtensions { - public static async Task GetPackagePropertyAsync(this IChocoSearchService service, string id, Version version, string propertyName, Parse parse) + public static async Task GetPackagePropertyAsync(this IChocoSearchService service, string id, + NuGetVersion version, string propertyName, Parse parse) { string str = await service.GetPackagePropertyAsync(id, version, propertyName); return parse(str); } - public static async Task GetPackageDatePropertyAsync(this IChocoSearchService service, string id, Version version, string propertyName) + public static async Task GetPackageDatePropertyAsync(this IChocoSearchService service, string id, + NuGetVersion version, string propertyName) { return await service.GetPackagePropertyAsync(id, version, propertyName, DateTimeOffset.Parse); } - public static async Task GetPackageBooleanPropertyAsync(this IChocoSearchService service, string id, Version version, string propertyName) + public static async Task GetPackageBooleanPropertyAsync(this IChocoSearchService service, string id, + NuGetVersion version, string propertyName) { return await service.GetPackagePropertyAsync(id, version, propertyName, bool.Parse); } - public static async Task GetPackageInt32PropertyAsync(this IChocoSearchService service, string id, Version version, string propertyName) + public static async Task GetPackageInt32PropertyAsync(this IChocoSearchService service, string id, + NuGetVersion version, string propertyName) { return await service.GetPackagePropertyAsync(id, version, propertyName, int.Parse); } - public static async Task GetPackageInt64PropertyAsync(this IChocoSearchService service, string id, Version version, string propertyName) + public static async Task GetPackageInt64PropertyAsync(this IChocoSearchService service, string id, + NuGetVersion version, string propertyName) { return await service.GetPackagePropertyAsync(id, version, propertyName, long.Parse); } diff --git a/Tests/ChocolateyTests/GetPackageProperty.cs b/Tests/ChocolateyTests/GetPackageProperty.cs index 4f74ba32..7719329f 100644 --- a/Tests/ChocolateyTests/GetPackageProperty.cs +++ b/Tests/ChocolateyTests/GetPackageProperty.cs @@ -2,6 +2,7 @@ using Chocolatey.Models; using System; using System.Threading.Tasks; +using NuGet.Versioning; using Xunit; namespace ChocolateyTests @@ -14,7 +15,7 @@ public class GetPackageProperty public async Task GetPackagePropertyAsync() { string id = "git"; - Version v = new(2, 35, 1, 2); + NuGetVersion v = new(2, 35, 1, 2); string actual; actual = await _client.GetPackagePropertyAsync(id, v, "Title"); @@ -31,7 +32,7 @@ public async Task GetPackagePropertyAsync() public async Task GetPackageDatePropertyAsync() { string id = "git"; - Version v = new(2, 35, 1, 2); + NuGetVersion v = new(2, 35, 1, 2); DateTimeOffset actual; actual = await _client.GetPackageDatePropertyAsync(id, v, "Created"); @@ -48,7 +49,7 @@ public async Task GetPackageDatePropertyAsync() public async Task GetPackageBooleanPropertyAsync() { string id = "git"; - Version v = new(2, 35, 1, 2); + NuGetVersion v = new(2, 35, 1, 2); bool actual; actual = await _client.GetPackageBooleanPropertyAsync(id, v, "IsPrerelease"); @@ -65,7 +66,7 @@ public async Task GetPackageBooleanPropertyAsync() public async Task GetPackageInt32PropertyAsync() { string id = "git"; - Version v = new(2, 35, 1, 2); + NuGetVersion v = new(2, 35, 1, 2); int actual; // Don't assert an exact value, download counts are subject to change @@ -78,7 +79,7 @@ public async Task GetPackageInt32PropertyAsync() public async Task GetPackageInt64PropertyAsync() { string id = "git"; - Version v = new(2, 35, 1, 2); + NuGetVersion v = new(2, 35, 1, 2); long actual; actual = await _client.GetPackageInt64PropertyAsync(id, v, "PackageSize"); @@ -89,7 +90,7 @@ public async Task GetPackageInt64PropertyAsync() public async Task GetPackagePropertyAsync_Enum() { string id = "git"; - Version v = new(2, 35, 1, 2); + NuGetVersion v = new(2, 35, 1, 2); PackageStatus actual; actual = await _client.GetPackagePropertyAsync(id, v, "PackageStatus", Enum.Parse); From 0f6ff3bc18ebf2485b1397716c38f4513274e850 Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Tue, 18 Mar 2025 01:12:49 -0500 Subject: [PATCH 07/17] [WIP] Add Choco package manager client --- API/Choco/ChocoCliClient.cs | 68 ++++++++++++++++++++++ API/Choco/IChocoPackageService.cs | 17 ++++++ API/Choco/Models/PackageProgress.cs | 14 +++++ Tests/ChocolateyTests/PackageManagement.cs | 24 ++++++++ 4 files changed, 123 insertions(+) create mode 100644 API/Choco/ChocoCliClient.cs create mode 100644 API/Choco/IChocoPackageService.cs create mode 100644 API/Choco/Models/PackageProgress.cs create mode 100644 Tests/ChocolateyTests/PackageManagement.cs diff --git a/API/Choco/ChocoCliClient.cs b/API/Choco/ChocoCliClient.cs new file mode 100644 index 00000000..89dccd79 --- /dev/null +++ b/API/Choco/ChocoCliClient.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Chocolatey.Models; +using CliWrap; +using NuGet.Versioning; + +namespace Chocolatey; + +public class ChocoCliClient : IChocoPackageService +{ + private const string CHOCO_EXE = "choco"; + private const string CHOCO_PARAM_YES = "-y"; + private const string CHOCO_PARAM_LIMITOUTPUT = "--limit-output"; + + private static readonly Regex _rxProgress = new(@"Progress: Downloading (?[\w.\-_]+) (?\d+(\.\d+){0,3}(-[\w\d]+)?)\.\.\. (?\d{1,3})%", RegexOptions.Compiled); + + public bool NoOp { get; set; } = false; + + public async Task InstallAsync(string id, NuGetVersion? version = null, IProgress? progress = null) + { + List args = ["install", id, CHOCO_PARAM_YES, CHOCO_PARAM_LIMITOUTPUT]; + + if (version is not null) + args.Add($"--version=\"'{version}'\""); + + if (NoOp) + args.Add("--noop"); + + var result = await Cli.Wrap(CHOCO_EXE) + .WithArguments(args) + .WithStandardOutputPipe(PipeTarget.ToDelegate(HandleStdOut)) + .ExecuteAsync(); + + return result.ExitCode is 0; + + void HandleStdOut(string line) + { + if (progress is null) + return; + + var match = _rxProgress.Match(line); + if (!match.Success) + return; + + var id = match.Groups["id"].Value; + var version = NuGetVersion.Parse(match.Groups["ver"].Value); + var percentage = int.Parse(match.Groups["prog"].Value); + progress.Report(new(id, version, percentage)); + } + } + + public async Task UninstallAsync(string id, NuGetVersion? version = null, IProgress? progress = null) + { + throw new NotImplementedException(); + } + + public async Task UpgradeAllAsync(IProgress? progress = null) + { + throw new NotImplementedException(); + } + + public async Task UpgradeAsync(string id, IProgress? progress = null) + { + throw new NotImplementedException(); + } +} diff --git a/API/Choco/IChocoPackageService.cs b/API/Choco/IChocoPackageService.cs new file mode 100644 index 00000000..7e75b2e4 --- /dev/null +++ b/API/Choco/IChocoPackageService.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; +using Chocolatey.Models; +using NuGet.Versioning; + +namespace Chocolatey; + +public interface IChocoPackageService +{ + Task InstallAsync(string id, NuGetVersion? version = null, IProgress? progress = null); + + Task UninstallAsync(string id, NuGetVersion? version = null, IProgress? progress = null); + + Task UpgradeAsync(string id, IProgress? progress = null); + + Task UpgradeAllAsync(IProgress? progress = null); +} \ No newline at end of file diff --git a/API/Choco/Models/PackageProgress.cs b/API/Choco/Models/PackageProgress.cs new file mode 100644 index 00000000..1f91357a --- /dev/null +++ b/API/Choco/Models/PackageProgress.cs @@ -0,0 +1,14 @@ +using NuGet.Versioning; + +namespace Chocolatey.Models; + +public class PackageProgress(string id, NuGetVersion version, int percentage) +{ + public string Id { get; } = id; + + public NuGetVersion Version { get; } = version; + + public int Percentage { get; } = percentage; + + public override string ToString() => $"{Id} {Version} {Percentage:##}%"; +} diff --git a/Tests/ChocolateyTests/PackageManagement.cs b/Tests/ChocolateyTests/PackageManagement.cs new file mode 100644 index 00000000..91a72e25 --- /dev/null +++ b/Tests/ChocolateyTests/PackageManagement.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; +using Chocolatey; +using Chocolatey.Models; +using Xunit; +using Xunit.Abstractions; + +namespace ChocolateyTests; + +public class PackageManagement(ITestOutputHelper output) +{ + private readonly IChocoPackageService _pkgMan = new ChocoCliClient + { + NoOp = true + }; + + [Theory] + [InlineData("git")] + public async Task InstallLatestAsync(string id) + { + Progress progress = new(p => output.WriteLine(p.ToString())); + await _pkgMan.InstallAsync(id, progress: progress); + } +} \ No newline at end of file From 15e9888267baa2e14e1cdf1fd3566254734de66b Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Thu, 20 Mar 2025 00:02:30 -0500 Subject: [PATCH 08/17] Add argument builder --- API/Choco/ChocoCliClient.cs | 83 +++++++++++++++++++--- Tests/ChocolateyTests/PackageManagement.cs | 11 +++ 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/API/Choco/ChocoCliClient.cs b/API/Choco/ChocoCliClient.cs index 89dccd79..550f5442 100644 --- a/API/Choco/ChocoCliClient.cs +++ b/API/Choco/ChocoCliClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Contracts; using System.Text.RegularExpressions; using System.Threading.Tasks; using Chocolatey.Models; @@ -11,8 +12,6 @@ namespace Chocolatey; public class ChocoCliClient : IChocoPackageService { private const string CHOCO_EXE = "choco"; - private const string CHOCO_PARAM_YES = "-y"; - private const string CHOCO_PARAM_LIMITOUTPUT = "--limit-output"; private static readonly Regex _rxProgress = new(@"Progress: Downloading (?[\w.\-_]+) (?\d+(\.\d+){0,3}(-[\w\d]+)?)\.\.\. (?\d{1,3})%", RegexOptions.Compiled); @@ -20,17 +19,22 @@ public class ChocoCliClient : IChocoPackageService public async Task InstallAsync(string id, NuGetVersion? version = null, IProgress? progress = null) { - List args = ["install", id, CHOCO_PARAM_YES, CHOCO_PARAM_LIMITOUTPUT]; - - if (version is not null) - args.Add($"--version=\"'{version}'\""); - - if (NoOp) - args.Add("--noop"); + List args = ["install", id]; + + ChocoArgumentsBuilder argBuilder = new() + { + Version = version, + Yes = true, + LimitOutput = true, + NoOp = NoOp + }; + + argBuilder.Build(args); var result = await Cli.Wrap(CHOCO_EXE) .WithArguments(args) .WithStandardOutputPipe(PipeTarget.ToDelegate(HandleStdOut)) + .WithValidation(CommandResultValidation.None) .ExecuteAsync(); return result.ExitCode is 0; @@ -53,7 +57,40 @@ void HandleStdOut(string line) public async Task UninstallAsync(string id, NuGetVersion? version = null, IProgress? progress = null) { - throw new NotImplementedException(); + List args = ["uninstall", id]; + + ChocoArgumentsBuilder argBuilder = new() + { + Version = version, + Yes = true, + LimitOutput = true, + NoOp = NoOp + }; + + argBuilder.Build(args); + + var result = await Cli.Wrap(CHOCO_EXE) + .WithArguments(args) + .WithStandardOutputPipe(PipeTarget.ToDelegate(HandleStdOut)) + .WithValidation(CommandResultValidation.None) + .ExecuteAsync(); + + return result.ExitCode is 0; + + void HandleStdOut(string line) + { + if (progress is null) + return; + + var match = _rxProgress.Match(line); + if (!match.Success) + return; + + var id = match.Groups["id"].Value; + var version = NuGetVersion.Parse(match.Groups["ver"].Value); + var percentage = int.Parse(match.Groups["prog"].Value); + progress.Report(new(id, version, percentage)); + } } public async Task UpgradeAllAsync(IProgress? progress = null) @@ -65,4 +102,30 @@ public async Task UpgradeAsync(string id, IProgress? prog { throw new NotImplementedException(); } + + private class ChocoArgumentsBuilder + { + public NuGetVersion? Version { get; set; } + public bool Yes { get; set; } + public bool LimitOutput { get; set; } + public bool NoOp { get; set; } + + public void Build(List args) => args.AddRange(Build()); + + [Pure] + public IEnumerable Build() + { + if (Version is not null) + yield return $"--version=\"'{Version}'\""; + + if (Yes) + yield return "-y"; + + if (LimitOutput) + yield return "--limit-output"; + + if (NoOp) + yield return "--noop"; + } + } } diff --git a/Tests/ChocolateyTests/PackageManagement.cs b/Tests/ChocolateyTests/PackageManagement.cs index 91a72e25..36ba9a45 100644 --- a/Tests/ChocolateyTests/PackageManagement.cs +++ b/Tests/ChocolateyTests/PackageManagement.cs @@ -16,9 +16,20 @@ public class PackageManagement(ITestOutputHelper output) [Theory] [InlineData("git")] + [InlineData("dotnet-windowshosting")] + [InlineData("notepadplusplus")] public async Task InstallLatestAsync(string id) { Progress progress = new(p => output.WriteLine(p.ToString())); await _pkgMan.InstallAsync(id, progress: progress); } + + [Theory] + [InlineData("git")] + [InlineData("dotnet-windowshosting")] + public async Task UninstallLatestAsync(string id) + { + Progress progress = new(p => output.WriteLine(p.ToString())); + await _pkgMan.UninstallAsync(id, progress: progress); + } } \ No newline at end of file From 4493d7c78b94bf9330656fc29a89b266e3b7a58b Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Thu, 20 Mar 2025 00:03:49 -0500 Subject: [PATCH 09/17] Fix Chocolatey source --- .../ChocolateyHandler.cs | 23 +++-- .../ChocolateyPackage.cs | 91 +++++++------------ 2 files changed, 44 insertions(+), 70 deletions(-) diff --git a/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs b/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs index e3edf505..a40924c9 100644 --- a/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs +++ b/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs @@ -5,6 +5,7 @@ using FluentStore.Services; using Flurl; using Garfoot.Utilities.FluentUrn; +using NuGet.Versioning; using System; using System.Collections.Generic; using System.Text.RegularExpressions; @@ -16,8 +17,13 @@ public partial class ChocolateyHandler : PackageHandlerBase { public const string NAMESPACE_CHOCO = "choco"; + private readonly IChocoSearchService _search; + private readonly IChocoPackageService _pkgMan; + public ChocolateyHandler(IPasswordVaultService passwordVaultService) : base(passwordVaultService) { + _search = new ChocoCommunityWebClient(); + _pkgMan = new ChocoCliClient(); } public override HashSet HandledNamespaces => new() @@ -36,27 +42,20 @@ public override async Task GetPackage(Urn packageUrn, PackageStatus int versionIdx = urnStr.LastIndexOf(':'); if (versionIdx <= 0) throw new NotSupportedException("The choco client library does not currently support fetching package info without specifying a version."); - var package = await Choco.GetPackageAsync(urnStr[..versionIdx], Version.Parse(urnStr[(versionIdx + 1)..])); + var package = await _search.GetPackageAsync(urnStr[..versionIdx], NuGetVersion.Parse(urnStr[(versionIdx + 1)..])); - return new ChocolateyPackage(this, package); + return new ChocolateyPackage(this, _pkgMan, package); } public override async IAsyncEnumerable SearchAsync(string query) { - var results = await Choco.SearchAsync(query); + var results = await _search.SearchAsync(query); foreach (var chocoPackage in results) - yield return new ChocolateyPackage(this, chocoPackage); + yield return new ChocolateyPackage(this, _pkgMan, chocoPackage); } - public override ImageBase GetImage() - { - return new TextImage - { - Text = "Ch", - FontFamily = "Segoe UI Variable Display" - }; - } + public override ImageBase GetImage() => new FileImage("https://github.com/chocolatey/choco-theme/blob/main/images/global-shared/logo.png?raw=true"); public override async Task GetPackageFromUrl(Url url) { diff --git a/Sources/FluentStore.Sources.Chocolatey/ChocolateyPackage.cs b/Sources/FluentStore.Sources.Chocolatey/ChocolateyPackage.cs index 57190da5..2affd3de 100644 --- a/Sources/FluentStore.Sources.Chocolatey/ChocolateyPackage.cs +++ b/Sources/FluentStore.Sources.Chocolatey/ChocolateyPackage.cs @@ -1,4 +1,5 @@ -using Chocolatey.Models; +using Chocolatey; +using Chocolatey.Models; using CommunityToolkit.Diagnostics; using CommunityToolkit.Mvvm.Messaging; using FluentStore.SDK; @@ -10,15 +11,18 @@ using System; using System.Collections.Generic; using System.IO; -using System.Management.Automation; using System.Threading.Tasks; namespace FluentStore.Sources.Chocolatey { public class ChocolateyPackage : PackageBase { - public ChocolateyPackage(PackageHandlerBase packageHandler, Package pack = null) : base(packageHandler) + private readonly IChocoPackageService _pkgMan; + + public ChocolateyPackage(PackageHandlerBase packageHandler, IChocoPackageService pkgMan, Package pack = null) : base(packageHandler) { + _pkgMan = pkgMan; + if (pack != null) Update(pack); } @@ -44,32 +48,31 @@ public void Update(Package pack) Website = Link.Create(pack.ProjectUrl, "Project website"); // Set Choco package properties - Links = new[] - { + Links = + [ Link.Create(pack.DocsUrl, ShortTitle + " docs"), Link.Create(pack.BugTrackerUrl, ShortTitle + " bug tracker"), Link.Create(pack.PackageSourceUrl, ShortTitle + " source"), Link.Create(pack.MailingListUrl, ShortTitle + " mailing list"), - }; + ]; } public override async Task DownloadAsync(DirectoryInfo folder = null) { // Find the package URI await PopulatePackageUri(); - if (!Status.IsAtLeast(SDK.PackageStatus.DownloadReady)) + if (PackageUri is null) return null; // Download package await StorageHelper.BackgroundDownloadPackage(this, PackageUri, folder); - if (!Status.IsAtLeast(SDK.PackageStatus.Downloaded)) + if (!IsDownloaded) return null; // Set the proper file name - DownloadItem = ((FileInfo)DownloadItem).CopyRename($"{Model.Id}.{Model.Version}.nupkg"); + DownloadItem = ((FileInfo)DownloadItem).CopyRename($"{Model.Id}_{Model.Version}.nupkg"); WeakReferenceMessenger.Default.Send(SuccessMessage.CreateForPackageDownloadCompleted(this)); - Status = SDK.PackageStatus.Downloaded; return DownloadItem; } @@ -78,11 +81,9 @@ private async Task PopulatePackageUri() WeakReferenceMessenger.Default.Send(new PackageFetchStartedMessage(this)); try { - if (PackageUri == null) - PackageUri = new(Model.DownloadUrl); + PackageUri = new(Model.DownloadUrl); WeakReferenceMessenger.Default.Send(new SuccessMessage(null, this, SuccessType.PackageFetchCompleted)); - Status = SDK.PackageStatus.DownloadReady; } catch (Exception ex) { @@ -105,67 +106,41 @@ public override async Task CacheAppIcon() return icon ?? TextImage.CreateFromName(Model.Title); } - public override async Task CacheHeroImage() - { - return null; - } + public override async Task CacheHeroImage() => null; - public override async Task> CacheScreenshots() - { - return new List(); - } + public override async Task> CacheScreenshots() => new List(); public override async Task InstallAsync() { - // Make sure installer is downloaded - Guard.IsTrue(Status.IsAtLeast(SDK.PackageStatus.Downloaded), nameof(Status)); - bool isSuccess = false; - try { - // Extract nupkg and locate PowerShell install script - var dir = StorageHelper.ExtractArchiveToDirectory((FileInfo)DownloadItem, true); - FileInfo installer = new(Path.Combine(dir.FullName, "tools", "chocolateyInstall.ps1")); - DownloadItem = installer; - - // Run install script - // Cannot find a provider with the name '$ErrorActionPreference = 'Stop'; - using PowerShell ps = PowerShell.Create(); - var results = await ps.AddScript(File.ReadAllText(installer.FullName)) - .InvokeAsync(); - + WeakReferenceMessenger.Default.Send(new PackageInstallStartedMessage(this)); + + Progress progress = new(p => + { + WeakReferenceMessenger.Default.Send( + new PackageDownloadProgressMessage(this, p.Percentage, 100)); + }); + + bool isSuccess = await _pkgMan.InstallAsync(PackageId, progress: progress); + if (!isSuccess) + throw new Exception(); + + WeakReferenceMessenger.Default.Send(SuccessMessage.CreateForPackageInstallCompleted(this)); + return true; } catch (Exception ex) { WeakReferenceMessenger.Default.Send(new ErrorMessage(ex, this, ErrorType.PackageInstallFailed)); return false; } - - if (isSuccess) - { - WeakReferenceMessenger.Default.Send(SuccessMessage.CreateForPackageInstallCompleted(this)); - Status = SDK.PackageStatus.Installed; - } - return isSuccess; } - public override Task CanLaunchAsync() - { - return Task.FromResult(false); - } + public override Task CanDownloadAsync() => Task.FromResult(Model?.DownloadUrl is not null); - public override Task LaunchAsync() - { - return Task.CompletedTask; - } + public override Task CanLaunchAsync() => Task.FromResult(false); - private InstallerType? _PackagedInstallerType; - public InstallerType? PackagedInstallerType - { - get => _PackagedInstallerType; - set => SetProperty(ref _PackagedInstallerType, value); - } - public bool HasPackagedInstallerType => PackagedInstallerType == null; + public override Task LaunchAsync() => Task.CompletedTask; private string _PackageId; public string PackageId From 62ad993adb64fc826ba20c7e9baf6696aa566b30 Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Thu, 20 Mar 2025 00:04:13 -0500 Subject: [PATCH 10/17] Prevent local-only plugins from crashing the auto-updater --- FluentStore.SDK/Plugins/PluginLoader.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/FluentStore.SDK/Plugins/PluginLoader.cs b/FluentStore.SDK/Plugins/PluginLoader.cs index fdb97d52..83ec3bd4 100644 --- a/FluentStore.SDK/Plugins/PluginLoader.cs +++ b/FluentStore.SDK/Plugins/PluginLoader.cs @@ -116,7 +116,13 @@ public async Task LoadPlugins(bool withAutoUpdate = false) var updateStatus = PluginInstallStatus.NoAction; if (withAutoUpdate) - updateStatus = await UpdatePlugin(plugin.PackageIdentity); + { + try + { + updateStatus = await UpdatePlugin(plugin.PackageIdentity); + } + catch { } + } if (updateStatus.IsLessThan(PluginInstallStatus.AppRestartRequired)) await LoadPlugin(plugin.PackageIdentity.Id); From 8142fb94b205e3859d51b41192df58632ac3912c Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Thu, 20 Mar 2025 00:04:19 -0500 Subject: [PATCH 11/17] Update NuGet deps --- FluentStore.SDK/FluentStore.SDK.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/FluentStore.SDK/FluentStore.SDK.csproj b/FluentStore.SDK/FluentStore.SDK.csproj index 3d185f3b..a8b3df21 100644 --- a/FluentStore.SDK/FluentStore.SDK.csproj +++ b/FluentStore.SDK/FluentStore.SDK.csproj @@ -21,9 +21,9 @@ - - - + + + From 0af229d003c8b3c15a6c9e54894fff1a7bcf12e9 Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Thu, 20 Mar 2025 00:22:47 -0500 Subject: [PATCH 12/17] Use admin client --- API/Choco/Cli/ChocoAdminCliClient.cs | 60 +++++++++++++++++++ API/Choco/Cli/ChocoArgumentsBuilder.cs | 33 ++++++++++ API/Choco/{ => Cli}/ChocoCliClient.cs | 37 ++---------- .../ChocolateyHandler.cs | 3 +- Tests/ChocolateyTests/PackageManagement.cs | 1 + 5 files changed, 100 insertions(+), 34 deletions(-) create mode 100644 API/Choco/Cli/ChocoAdminCliClient.cs create mode 100644 API/Choco/Cli/ChocoArgumentsBuilder.cs rename API/Choco/{ => Cli}/ChocoCliClient.cs (76%) diff --git a/API/Choco/Cli/ChocoAdminCliClient.cs b/API/Choco/Cli/ChocoAdminCliClient.cs new file mode 100644 index 00000000..d54399d9 --- /dev/null +++ b/API/Choco/Cli/ChocoAdminCliClient.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using Chocolatey.Models; +using NuGet.Versioning; + +namespace Chocolatey.Cli; + +public class ChocoAdminCliClient : IChocoPackageService +{ + public bool NoOp { get; set; } = false; + + public async Task InstallAsync(string id, NuGetVersion? version = null, IProgress? progress = null) + { + return await Task.Run(delegate + { + List args = ["install", id]; + + ChocoArgumentsBuilder argBuilder = new() + { + Version = version, + Yes = true, + LimitOutput = true, + NoOp = NoOp + }; + + argBuilder.Build(args); + + ProcessStartInfo info = new(ChocoArgumentsBuilder.CHOCO_EXE) + { + Arguments = string.Join(" ", args), + + // Required to run as admin + UseShellExecute = true, + Verb = "runas", + }; + + var process = Process.Start(info); + process.WaitForExit(); + + return process.ExitCode is 0; + }); + } + + public Task UninstallAsync(string id, NuGetVersion? version = null, IProgress? progress = null) + { + throw new NotImplementedException(); + } + + public Task UpgradeAllAsync(IProgress? progress = null) + { + throw new NotImplementedException(); + } + + public Task UpgradeAsync(string id, IProgress? progress = null) + { + throw new NotImplementedException(); + } +} diff --git a/API/Choco/Cli/ChocoArgumentsBuilder.cs b/API/Choco/Cli/ChocoArgumentsBuilder.cs new file mode 100644 index 00000000..1276eae7 --- /dev/null +++ b/API/Choco/Cli/ChocoArgumentsBuilder.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using NuGet.Versioning; + +namespace Chocolatey.Cli; + +internal class ChocoArgumentsBuilder +{ + public const string CHOCO_EXE = "choco"; + + public NuGetVersion? Version { get; set; } + public bool Yes { get; set; } + public bool LimitOutput { get; set; } + public bool NoOp { get; set; } + + public void Build(List args) => args.AddRange(Build()); + + [Pure] + public IEnumerable Build() + { + if (Version is not null) + yield return $"--version=\"'{Version}'\""; + + if (Yes) + yield return "-y"; + + if (LimitOutput) + yield return "--limit-output"; + + if (NoOp) + yield return "--noop"; + } +} \ No newline at end of file diff --git a/API/Choco/ChocoCliClient.cs b/API/Choco/Cli/ChocoCliClient.cs similarity index 76% rename from API/Choco/ChocoCliClient.cs rename to API/Choco/Cli/ChocoCliClient.cs index 550f5442..fbbb64a0 100644 --- a/API/Choco/ChocoCliClient.cs +++ b/API/Choco/Cli/ChocoCliClient.cs @@ -1,18 +1,15 @@ using System; using System.Collections.Generic; -using System.Diagnostics.Contracts; using System.Text.RegularExpressions; using System.Threading.Tasks; using Chocolatey.Models; using CliWrap; using NuGet.Versioning; -namespace Chocolatey; +namespace Chocolatey.Cli; -public class ChocoCliClient : IChocoPackageService +public partial class ChocoCliClient : IChocoPackageService { - private const string CHOCO_EXE = "choco"; - private static readonly Regex _rxProgress = new(@"Progress: Downloading (?[\w.\-_]+) (?\d+(\.\d+){0,3}(-[\w\d]+)?)\.\.\. (?\d{1,3})%", RegexOptions.Compiled); public bool NoOp { get; set; } = false; @@ -31,7 +28,7 @@ public async Task InstallAsync(string id, NuGetVersion? version = null, IP argBuilder.Build(args); - var result = await Cli.Wrap(CHOCO_EXE) + var result = await CliWrap.Cli.Wrap(ChocoArgumentsBuilder.CHOCO_EXE) .WithArguments(args) .WithStandardOutputPipe(PipeTarget.ToDelegate(HandleStdOut)) .WithValidation(CommandResultValidation.None) @@ -69,7 +66,7 @@ public async Task UninstallAsync(string id, NuGetVersion? version = null, argBuilder.Build(args); - var result = await Cli.Wrap(CHOCO_EXE) + var result = await CliWrap.Cli.Wrap(ChocoArgumentsBuilder.CHOCO_EXE) .WithArguments(args) .WithStandardOutputPipe(PipeTarget.ToDelegate(HandleStdOut)) .WithValidation(CommandResultValidation.None) @@ -102,30 +99,4 @@ public async Task UpgradeAsync(string id, IProgress? prog { throw new NotImplementedException(); } - - private class ChocoArgumentsBuilder - { - public NuGetVersion? Version { get; set; } - public bool Yes { get; set; } - public bool LimitOutput { get; set; } - public bool NoOp { get; set; } - - public void Build(List args) => args.AddRange(Build()); - - [Pure] - public IEnumerable Build() - { - if (Version is not null) - yield return $"--version=\"'{Version}'\""; - - if (Yes) - yield return "-y"; - - if (LimitOutput) - yield return "--limit-output"; - - if (NoOp) - yield return "--noop"; - } - } } diff --git a/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs b/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs index a40924c9..04ad6c13 100644 --- a/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs +++ b/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs @@ -1,4 +1,5 @@ using Chocolatey; +using Chocolatey.Cli; using CommunityToolkit.Diagnostics; using FluentStore.SDK; using FluentStore.SDK.Images; @@ -23,7 +24,7 @@ public partial class ChocolateyHandler : PackageHandlerBase public ChocolateyHandler(IPasswordVaultService passwordVaultService) : base(passwordVaultService) { _search = new ChocoCommunityWebClient(); - _pkgMan = new ChocoCliClient(); + _pkgMan = new ChocoAdminCliClient(); } public override HashSet HandledNamespaces => new() diff --git a/Tests/ChocolateyTests/PackageManagement.cs b/Tests/ChocolateyTests/PackageManagement.cs index 36ba9a45..1ae9c360 100644 --- a/Tests/ChocolateyTests/PackageManagement.cs +++ b/Tests/ChocolateyTests/PackageManagement.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Chocolatey; +using Chocolatey.Cli; using Chocolatey.Models; using Xunit; using Xunit.Abstractions; From ed28aa60b90578499f099efefa53f43592b7e640 Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Thu, 20 Mar 2025 00:34:27 -0500 Subject: [PATCH 13/17] Use Url object instead of Regex for Choco URLs --- .../ChocolateyHandler.cs | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs b/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs index 04ad6c13..c372329b 100644 --- a/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs +++ b/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs @@ -9,7 +9,6 @@ using NuGet.Versioning; using System; using System.Collections.Generic; -using System.Text.RegularExpressions; using System.Threading.Tasks; namespace FluentStore.Sources.Chocolatey @@ -27,10 +26,7 @@ public ChocolateyHandler(IPasswordVaultService passwordVaultService) : base(pass _pkgMan = new ChocoAdminCliClient(); } - public override HashSet HandledNamespaces => new() - { - NAMESPACE_CHOCO - }; + public override HashSet HandledNamespaces => [NAMESPACE_CHOCO]; public override string DisplayName => "Chocolatey"; @@ -38,12 +34,23 @@ public override async Task GetPackage(Urn packageUrn, PackageStatus { Guard.IsEqualTo(packageUrn.NamespaceIdentifier, NAMESPACE_CHOCO, nameof(packageUrn)); - // TODO: Support getting packages without specifying a version var urnStr = packageUrn.GetContent().UnEscapedValue; + + string id; + NuGetVersion version = null; + int versionIdx = urnStr.LastIndexOf(':'); - if (versionIdx <= 0) - throw new NotSupportedException("The choco client library does not currently support fetching package info without specifying a version."); - var package = await _search.GetPackageAsync(urnStr[..versionIdx], NuGetVersion.Parse(urnStr[(versionIdx + 1)..])); + if (versionIdx > 0) + { + _ = NuGetVersion.TryParse(urnStr[(versionIdx + 1)..], out version); + id = urnStr[..versionIdx]; + } + else + { + id = urnStr; + } + + var package = await _search.GetPackageAsync(id, version); return new ChocolateyPackage(this, _pkgMan, package); } @@ -60,15 +67,16 @@ public override async IAsyncEnumerable SearchAsync(string query) public override async Task GetPackageFromUrl(Url url) { - Regex rx = ChocoRx(); - Match m = rx.Match(url.ToString()); - if (!m.Success) + if (!url.Authority.Equals("community.chocolatey.org", StringComparison.InvariantCultureIgnoreCase) + || url.PathSegments.Count < 2 + || !url.PathSegments[0].Equals("packages", StringComparison.InvariantCultureIgnoreCase)) return null; - string urn = $"urn:{NAMESPACE_CHOCO}:{m.Groups["id"]}"; - var versionGroup = m.Groups["version"]; - if (versionGroup.Success) - urn += "." + versionGroup.Value; + string id = url.PathSegments[1]; + string urn = $"urn:{NAMESPACE_CHOCO}:{id}"; + + if (url.PathSegments.Count >= 3) + urn += $".{url.PathSegments[2]}"; return await GetPackage(Urn.Parse(urn)); } @@ -81,12 +89,9 @@ public override Url GetUrlFromPackage(PackageBase package) string url = $"https://community.chocolatey.org/packages/{chocoPackage.PackageId}"; if (chocoPackage.Version != null) - url += "/" + chocoPackage.Version; + url += $"/{chocoPackage.Version}"; return url; } - - [GeneratedRegex("^https?:\\/\\/community\\.chocolatey\\.org\\/packages\\/(?[^\\/\\s]+)(?:\\/(?[\\d.]+))?", RegexOptions.IgnoreCase, "en-US")] - private static partial Regex ChocoRx(); } } From d3491193103ebf70c48c378fbfd815020551f758 Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Thu, 20 Mar 2025 00:35:19 -0500 Subject: [PATCH 14/17] Bump Choco source version --- .../FluentStore.Sources.Chocolatey.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/FluentStore.Sources.Chocolatey/FluentStore.Sources.Chocolatey.csproj b/Sources/FluentStore.Sources.Chocolatey/FluentStore.Sources.Chocolatey.csproj index 67d73164..c189c4d1 100644 --- a/Sources/FluentStore.Sources.Chocolatey/FluentStore.Sources.Chocolatey.csproj +++ b/Sources/FluentStore.Sources.Chocolatey/FluentStore.Sources.Chocolatey.csproj @@ -7,7 +7,7 @@ Chocolatey - $(FluentStoreMajorMinorVersion).0.2 + $(FluentStoreMajorMinorVersion).1.0 $(Version)-alpha Provides support for the Chocolatey Community repository. Requires the Chocolatey CLI to be intalled. From 897dc85a6b512d7009ae41eca8dfa4888ea3e0c6 Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Thu, 20 Mar 2025 00:53:04 -0500 Subject: [PATCH 15/17] Add ListAsync --- API/Choco/Cli/ChocoAdminCliClient.cs | 53 +++++++++++++++++++++------- API/Choco/Cli/ChocoCliClient.cs | 29 +++++++++++++++ API/Choco/IChocoPackageService.cs | 3 ++ 3 files changed, 73 insertions(+), 12 deletions(-) diff --git a/API/Choco/Cli/ChocoAdminCliClient.cs b/API/Choco/Cli/ChocoAdminCliClient.cs index d54399d9..64e30787 100644 --- a/API/Choco/Cli/ChocoAdminCliClient.cs +++ b/API/Choco/Cli/ChocoAdminCliClient.cs @@ -27,27 +27,39 @@ public async Task InstallAsync(string id, NuGetVersion? version = null, IP argBuilder.Build(args); - ProcessStartInfo info = new(ChocoArgumentsBuilder.CHOCO_EXE) - { - Arguments = string.Join(" ", args), - - // Required to run as admin - UseShellExecute = true, - Verb = "runas", - }; - - var process = Process.Start(info); - process.WaitForExit(); + var process = RunChocoAsAdmin(args); return process.ExitCode is 0; }); } - public Task UninstallAsync(string id, NuGetVersion? version = null, IProgress? progress = null) + public IAsyncEnumerable<(string Id, NuGetVersion Version)> ListAsync() { throw new NotImplementedException(); } + public async Task UninstallAsync(string id, NuGetVersion? version = null, IProgress? progress = null) + { + return await Task.Run(delegate + { + List args = ["uninstall", id]; + + ChocoArgumentsBuilder argBuilder = new() + { + Version = version, + Yes = true, + LimitOutput = true, + NoOp = NoOp + }; + + argBuilder.Build(args); + + var process = RunChocoAsAdmin(args); + + return process.ExitCode is 0; + }); + } + public Task UpgradeAllAsync(IProgress? progress = null) { throw new NotImplementedException(); @@ -57,4 +69,21 @@ public Task UpgradeAsync(string id, IProgress? progress = { throw new NotImplementedException(); } + + private Process RunChocoAsAdmin(List args) + { + ProcessStartInfo info = new(ChocoArgumentsBuilder.CHOCO_EXE) + { + Arguments = string.Join(" ", args), + + // Required to run as admin + UseShellExecute = true, + Verb = "runas", + }; + + var process = Process.Start(info); + process.WaitForExit(); + + return process; + } } diff --git a/API/Choco/Cli/ChocoCliClient.cs b/API/Choco/Cli/ChocoCliClient.cs index fbbb64a0..64d3aa9e 100644 --- a/API/Choco/Cli/ChocoCliClient.cs +++ b/API/Choco/Cli/ChocoCliClient.cs @@ -99,4 +99,33 @@ public async Task UpgradeAsync(string id, IProgress? prog { throw new NotImplementedException(); } + + public async IAsyncEnumerable<(string Id, NuGetVersion Version)> ListAsync() + { + ChocoArgumentsBuilder argBuilder = new() + { + LimitOutput = true, + }; + + argBuilder.Build(["list"]); + + List<(string, NuGetVersion)> installedPackages = []; + + var result = await CliWrap.Cli.Wrap(ChocoArgumentsBuilder.CHOCO_EXE) + .WithArguments((List)["list"]) + .WithStandardOutputPipe(PipeTarget.ToDelegate(HandleStdOut)) + .WithValidation(CommandResultValidation.None) + .ExecuteAsync(); + + foreach (var p in installedPackages) + yield return p; + + void HandleStdOut(string line) + { + int separatorIdx = line.IndexOf('|'); + var id = line.Substring(0, separatorIdx).Trim(); + var version = NuGetVersion.Parse(line.Substring(separatorIdx + 1).Trim()); + installedPackages.Add((id, version)); + } + } } diff --git a/API/Choco/IChocoPackageService.cs b/API/Choco/IChocoPackageService.cs index 7e75b2e4..05cbf1ca 100644 --- a/API/Choco/IChocoPackageService.cs +++ b/API/Choco/IChocoPackageService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Chocolatey.Models; using NuGet.Versioning; @@ -14,4 +15,6 @@ public interface IChocoPackageService Task UpgradeAsync(string id, IProgress? progress = null); Task UpgradeAllAsync(IProgress? progress = null); + + IAsyncEnumerable<(string Id, NuGetVersion Version)> ListAsync(); } \ No newline at end of file From af82be8b51d137f16ddfac4f11eb40954c34ef7f Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Thu, 20 Mar 2025 01:16:04 -0500 Subject: [PATCH 16/17] Display Choco package metadata --- .../ChocolateyHandler.cs | 2 +- .../ChocolateyPackage.cs | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs b/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs index c372329b..77c07a57 100644 --- a/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs +++ b/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs @@ -76,7 +76,7 @@ public override async Task GetPackageFromUrl(Url url) string urn = $"urn:{NAMESPACE_CHOCO}:{id}"; if (url.PathSegments.Count >= 3) - urn += $".{url.PathSegments[2]}"; + urn += $":{url.PathSegments[2]}"; return await GetPackage(Urn.Parse(urn)); } diff --git a/Sources/FluentStore.Sources.Chocolatey/ChocolateyPackage.cs b/Sources/FluentStore.Sources.Chocolatey/ChocolateyPackage.cs index 2affd3de..baf723b1 100644 --- a/Sources/FluentStore.Sources.Chocolatey/ChocolateyPackage.cs +++ b/Sources/FluentStore.Sources.Chocolatey/ChocolateyPackage.cs @@ -3,14 +3,17 @@ using CommunityToolkit.Diagnostics; using CommunityToolkit.Mvvm.Messaging; using FluentStore.SDK; +using FluentStore.SDK.Attributes; using FluentStore.SDK.Helpers; using FluentStore.SDK.Images; using FluentStore.SDK.Messages; using FluentStore.SDK.Models; using Garfoot.Utilities.FluentUrn; +using Humanizer; using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; namespace FluentStore.Sources.Chocolatey @@ -48,6 +51,8 @@ public void Update(Package pack) Website = Link.Create(pack.ProjectUrl, "Project website"); // Set Choco package properties + DownloadCountDisplay = pack.DownloadCount.ToMetric(); + // TODO: Don't show broken links Links = [ Link.Create(pack.DocsUrl, ShortTitle + " docs"), @@ -55,6 +60,7 @@ public void Update(Package pack) Link.Create(pack.PackageSourceUrl, ShortTitle + " source"), Link.Create(pack.MailingListUrl, ShortTitle + " mailing list"), ]; + Tags = pack.Tags.ToList(); } public override async Task DownloadAsync(DirectoryInfo folder = null) @@ -143,17 +149,35 @@ public override async Task InstallAsync() public override Task LaunchAsync() => Task.CompletedTask; private string _PackageId; + [DisplayAdditionalInformation("Package ID", "\uE625")] public string PackageId { get => _PackageId; set => SetProperty(ref _PackageId, value); } + private string _DownloadCountDisplay; + [DisplayAdditionalInformation("Download count", "\uE896")] + public string DownloadCountDisplay + { + get => _DownloadCountDisplay; + set => SetProperty(ref _DownloadCountDisplay, value); + } + private Link[] _Links; + [DisplayAdditionalInformation("Links", "\uE71B")] public Link[] Links { get => _Links; set => SetProperty(ref _Links, value); } + + private List _Tags = []; + [DisplayAdditionalInformation(Icon = "\uE8EC")] + public List Tags + { + get => _Tags; + set => SetProperty(ref _Tags, value); + } } } From 20ab206c7717105423506a96d1ddd5aaeb9c4c35 Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Fri, 21 Mar 2025 00:23:24 -0500 Subject: [PATCH 17/17] Only show populated Choco links --- FluentStore.SDK/Models/Link.cs | 4 +++ .../ChocolateyPackage.cs | 27 ++++++++++++------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/FluentStore.SDK/Models/Link.cs b/FluentStore.SDK/Models/Link.cs index 847de154..d8d4c12c 100644 --- a/FluentStore.SDK/Models/Link.cs +++ b/FluentStore.SDK/Models/Link.cs @@ -1,5 +1,6 @@ using CommunityToolkit.Diagnostics; using System; +using System.Diagnostics.Contracts; namespace FluentStore.SDK.Models { @@ -38,6 +39,7 @@ public string TextContent set => _TextContent = value; } + [Pure] public override string ToString() => $"[{TextContent}]({Uri})"; /// @@ -48,6 +50,7 @@ public string TextContent /// to , or null if /// is null. /// + [Pure] public static Link Create(Uri uri, string textContent = null) => uri is null ? null : new(uri, textContent); @@ -59,6 +62,7 @@ public static Link Create(Uri uri, string textContent = null) /// to , or null if /// is null. /// + [Pure] public static Link Create(string url, string textContent = null) { if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out var uri)) diff --git a/Sources/FluentStore.Sources.Chocolatey/ChocolateyPackage.cs b/Sources/FluentStore.Sources.Chocolatey/ChocolateyPackage.cs index baf723b1..e06a9b73 100644 --- a/Sources/FluentStore.Sources.Chocolatey/ChocolateyPackage.cs +++ b/Sources/FluentStore.Sources.Chocolatey/ChocolateyPackage.cs @@ -52,15 +52,22 @@ public void Update(Package pack) // Set Choco package properties DownloadCountDisplay = pack.DownloadCount.ToMetric(); - // TODO: Don't show broken links - Links = - [ - Link.Create(pack.DocsUrl, ShortTitle + " docs"), - Link.Create(pack.BugTrackerUrl, ShortTitle + " bug tracker"), - Link.Create(pack.PackageSourceUrl, ShortTitle + " source"), - Link.Create(pack.MailingListUrl, ShortTitle + " mailing list"), - ]; Tags = pack.Tags.ToList(); + + Links.Clear(); + + if (!string.IsNullOrEmpty(pack.DocsUrl)) + Links.Add(Link.Create(pack.DocsUrl, "Docs")); + + if (!string.IsNullOrEmpty(pack.BugTrackerUrl)) + Links.Add(Link.Create(pack.BugTrackerUrl, "Bug tracker")); + + if (!string.IsNullOrEmpty(pack.PackageSourceUrl)) + Links.Add(Link.Create(pack.PackageSourceUrl, "Source")); + + if (!string.IsNullOrEmpty(pack.MailingListUrl)) + Links.Add(Link.Create(pack.MailingListUrl, "Mailing list")); + } public override async Task DownloadAsync(DirectoryInfo folder = null) @@ -164,9 +171,9 @@ public string DownloadCountDisplay set => SetProperty(ref _DownloadCountDisplay, value); } - private Link[] _Links; + private List _Links = []; [DisplayAdditionalInformation("Links", "\uE71B")] - public Link[] Links + public List Links { get => _Links; set => SetProperty(ref _Links, value);