diff --git a/API/Choco/Choco.cs b/API/Choco/Choco.cs
deleted file mode 100644
index 9a79d94b..00000000
--- a/API/Choco/Choco.cs
+++ /dev/null
@@ -1,75 +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
- {
- ///
- /// 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();
- }
-
- 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..ac1eb62a
--- /dev/null
+++ b/API/Choco/ChocoCommunityWebClient.cs
@@ -0,0 +1,42 @@
+using Chocolatey.Models;
+using Flurl;
+using Flurl.Http.Xml;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using NuGet.Versioning;
+
+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, 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, NuGetVersion 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/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/Cli/ChocoAdminCliClient.cs b/API/Choco/Cli/ChocoAdminCliClient.cs
new file mode 100644
index 00000000..64e30787
--- /dev/null
+++ b/API/Choco/Cli/ChocoAdminCliClient.cs
@@ -0,0 +1,89 @@
+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);
+
+ var process = RunChocoAsAdmin(args);
+
+ return process.ExitCode is 0;
+ });
+ }
+
+ 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();
+ }
+
+ public Task UpgradeAsync(string id, IProgress? progress = null)
+ {
+ 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/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/Cli/ChocoCliClient.cs b/API/Choco/Cli/ChocoCliClient.cs
new file mode 100644
index 00000000..64d3aa9e
--- /dev/null
+++ b/API/Choco/Cli/ChocoCliClient.cs
@@ -0,0 +1,131 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Chocolatey.Models;
+using CliWrap;
+using NuGet.Versioning;
+
+namespace Chocolatey.Cli;
+
+public partial class ChocoCliClient : IChocoPackageService
+{
+ 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];
+
+ ChocoArgumentsBuilder argBuilder = new()
+ {
+ Version = version,
+ Yes = true,
+ LimitOutput = true,
+ NoOp = NoOp
+ };
+
+ argBuilder.Build(args);
+
+ var result = await CliWrap.Cli.Wrap(ChocoArgumentsBuilder.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 UninstallAsync(string id, NuGetVersion? version = null, IProgress? progress = null)
+ {
+ List args = ["uninstall", id];
+
+ ChocoArgumentsBuilder argBuilder = new()
+ {
+ Version = version,
+ Yes = true,
+ LimitOutput = true,
+ NoOp = NoOp
+ };
+
+ argBuilder.Build(args);
+
+ var result = await CliWrap.Cli.Wrap(ChocoArgumentsBuilder.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)
+ {
+ throw new NotImplementedException();
+ }
+
+ public async Task UpgradeAsync(string id, IProgress? progress = null)
+ {
+ 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/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";
diff --git a/API/Choco/IChocoPackageService.cs b/API/Choco/IChocoPackageService.cs
new file mode 100644
index 00000000..05cbf1ca
--- /dev/null
+++ b/API/Choco/IChocoPackageService.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+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);
+
+ IAsyncEnumerable<(string Id, NuGetVersion Version)> ListAsync();
+}
\ No newline at end of file
diff --git a/API/Choco/IChocoSearchService.cs b/API/Choco/IChocoSearchService.cs
new file mode 100644
index 00000000..31a4193b
--- /dev/null
+++ b/API/Choco/IChocoSearchService.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Chocolatey.Models;
+using NuGet.Versioning;
+
+namespace Chocolatey;
+
+public interface IChocoSearchService
+{
+ Task> SearchAsync(string query, string targetFramework = "", bool includePrerelease = false, int top = 30, int skip = 0);
+
+ Task GetPackageAsync(string id, NuGetVersion version);
+
+ Task GetPackagePropertyAsync(string id, NuGetVersion version, string propertyName);
+}
+
+public static class ChocoSearchServiceExtensions
+{
+ 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,
+ NuGetVersion version, string propertyName)
+ {
+ return await service.GetPackagePropertyAsync(id, version, propertyName, DateTimeOffset.Parse);
+ }
+ 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,
+ NuGetVersion version, string propertyName)
+ {
+ return await service.GetPackagePropertyAsync(id, version, propertyName, int.Parse);
+ }
+ 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/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/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);
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/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);
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/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs b/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs
index e3edf505..77c07a57 100644
--- a/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs
+++ b/Sources/FluentStore.Sources.Chocolatey/ChocolateyHandler.cs
@@ -1,13 +1,14 @@
using Chocolatey;
+using Chocolatey.Cli;
using CommunityToolkit.Diagnostics;
using FluentStore.SDK;
using FluentStore.SDK.Images;
using FluentStore.Services;
using Flurl;
using Garfoot.Utilities.FluentUrn;
+using NuGet.Versioning;
using System;
using System.Collections.Generic;
-using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace FluentStore.Sources.Chocolatey
@@ -16,14 +17,16 @@ 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 ChocoAdminCliClient();
}
- public override HashSet HandledNamespaces => new()
- {
- NAMESPACE_CHOCO
- };
+ public override HashSet HandledNamespaces => [NAMESPACE_CHOCO];
public override string DisplayName => "Chocolatey";
@@ -31,44 +34,49 @@ 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 Choco.GetPackageAsync(urnStr[..versionIdx], Version.Parse(urnStr[(versionIdx + 1)..]));
+ if (versionIdx > 0)
+ {
+ _ = NuGetVersion.TryParse(urnStr[(versionIdx + 1)..], out version);
+ id = urnStr[..versionIdx];
+ }
+ else
+ {
+ id = urnStr;
+ }
- return new ChocolateyPackage(this, package);
+ var package = await _search.GetPackageAsync(id, version);
+
+ 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)
{
- 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();
}
}
diff --git a/Sources/FluentStore.Sources.Chocolatey/ChocolateyPackage.cs b/Sources/FluentStore.Sources.Chocolatey/ChocolateyPackage.cs
index 57190da5..e06a9b73 100644
--- a/Sources/FluentStore.Sources.Chocolatey/ChocolateyPackage.cs
+++ b/Sources/FluentStore.Sources.Chocolatey/ChocolateyPackage.cs
@@ -1,24 +1,31 @@
-using Chocolatey.Models;
+using Chocolatey;
+using Chocolatey.Models;
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.Management.Automation;
+using System.Linq;
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 +51,41 @@ public void Update(Package pack)
Website = Link.Create(pack.ProjectUrl, "Project website");
// Set Choco package properties
- Links = new[]
- {
- 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"),
- };
+ DownloadCountDisplay = pack.DownloadCount.ToMetric();
+ 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)
{
// 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 +94,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,80 +119,72 @@ 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;
+ [DisplayAdditionalInformation("Package ID", "\uE625")]
public string PackageId
{
get => _PackageId;
set => SetProperty(ref _PackageId, value);
}
- private Link[] _Links;
- public Link[] Links
+ private string _DownloadCountDisplay;
+ [DisplayAdditionalInformation("Download count", "\uE896")]
+ public string DownloadCountDisplay
+ {
+ get => _DownloadCountDisplay;
+ set => SetProperty(ref _DownloadCountDisplay, value);
+ }
+
+ private List _Links = [];
+ [DisplayAdditionalInformation("Links", "\uE71B")]
+ public List Links
{
get => _Links;
set => SetProperty(ref _Links, value);
}
+
+ private List _Tags = [];
+ [DisplayAdditionalInformation(Icon = "\uE8EC")]
+ public List Tags
+ {
+ get => _Tags;
+ set => SetProperty(ref _Tags, value);
+ }
}
}
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.
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
+
+
-
-
-
+
+
+
diff --git a/Tests/ChocolateyTests/GetPackageProperty.cs b/Tests/ChocolateyTests/GetPackageProperty.cs
index f6f27c5f..7719329f 100644
--- a/Tests/ChocolateyTests/GetPackageProperty.cs
+++ b/Tests/ChocolateyTests/GetPackageProperty.cs
@@ -2,26 +2,29 @@
using Chocolatey.Models;
using System;
using System.Threading.Tasks;
+using NuGet.Versioning;
using Xunit;
namespace ChocolateyTests
{
public class GetPackageProperty
{
+ private readonly IChocoSearchService _client = new ChocoCommunityWebClient();
+
[Fact]
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 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);
}
@@ -29,16 +32,16 @@ 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 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);
}
@@ -46,16 +49,16 @@ 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 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);
}
@@ -63,23 +66,23 @@ 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
- 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]
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 Choco.GetPackageInt64PropertyAsync(id, v, "PackageSize");
+ actual = await _client.GetPackageInt64PropertyAsync(id, v, "PackageSize");
Assert.Equal(8170L, actual);
}
@@ -87,10 +90,10 @@ 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 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/PackageManagement.cs b/Tests/ChocolateyTests/PackageManagement.cs
new file mode 100644
index 00000000..1ae9c360
--- /dev/null
+++ b/Tests/ChocolateyTests/PackageManagement.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Threading.Tasks;
+using Chocolatey;
+using Chocolatey.Cli;
+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")]
+ [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
diff --git a/Tests/ChocolateyTests/Search.cs b/Tests/ChocolateyTests/Search.cs
index ba131e88..83b8f912 100644
--- a/Tests/ChocolateyTests/Search.cs
+++ b/Tests/ChocolateyTests/Search.cs
@@ -1,57 +1,71 @@
using Chocolatey;
using System.Threading.Tasks;
using Xunit;
+using Xunit.Abstractions;
-namespace ChocolateyTests
+namespace ChocolateyTests;
+
+public class Search(ITestOutputHelper output)
{
- public class Search
+ private readonly IChocoSearchService _client = new ChocoCommunityWebClient();
+
+ [Fact]
+ public async Task SearchAsync_Minimum1()
+ {
+ var actualResults = await _client.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 _client.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;
+
+ var results1 = await _client.SearchAsync(query, top: pageSize, skip: 0);
+ foreach (var result in results1)
+ output.WriteLine($"{result.Title} {result.Version}");
- [Fact]
- public async Task SearchAsync_MinimumWithSpace()
+ 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 _client.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);
}
}
}