From c896fe89c35e5346fda19c3f28f76dace117723c Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Fri, 21 Mar 2025 17:09:12 -0500 Subject: [PATCH 01/11] Add old WIP Scoop library --- API/Scoop/Requests/SearchRequest.cs | 40 +++++ API/Scoop/Responses/Manifest.cs | 166 ++++++++++++++++++++ API/Scoop/Responses/SearchResponse.cs | 16 ++ API/Scoop/Responses/SearchResult.cs | 19 +++ API/Scoop/Responses/SearchResultMetadata.cs | 17 ++ API/Scoop/Scoop.csproj | 18 +++ API/Scoop/ScoopApi.cs | 8 + API/Scoop/ScoopSearch.cs | 53 +++++++ FluentStore.sln | 23 +++ 9 files changed, 360 insertions(+) create mode 100644 API/Scoop/Requests/SearchRequest.cs create mode 100644 API/Scoop/Responses/Manifest.cs create mode 100644 API/Scoop/Responses/SearchResponse.cs create mode 100644 API/Scoop/Responses/SearchResult.cs create mode 100644 API/Scoop/Responses/SearchResultMetadata.cs create mode 100644 API/Scoop/Scoop.csproj create mode 100644 API/Scoop/ScoopApi.cs create mode 100644 API/Scoop/ScoopSearch.cs diff --git a/API/Scoop/Requests/SearchRequest.cs b/API/Scoop/Requests/SearchRequest.cs new file mode 100644 index 0000000..7280a28 --- /dev/null +++ b/API/Scoop/Requests/SearchRequest.cs @@ -0,0 +1,40 @@ +using System.Text.Json.Serialization; + +namespace Scoop.Requests; + +internal class SearchRequest +{ + [JsonPropertyName("count")] + public bool Count { get; set; } + + [JsonPropertyName("search")] + public string Search { get; set; } + + [JsonPropertyName("searchMode")] + public string SearchMode { get; set; } + + [JsonPropertyName("filter")] + public string Filter { get; set; } + + [JsonPropertyName("orderby")] + public string OrderBy { get; set; } + + [JsonPropertyName("skip")] + public int Skip { get; set; } + + [JsonPropertyName("top")] + public int Top { get; set; } + + [JsonPropertyName("select")] + public string Select { get; set; } + + [JsonPropertyName("highlight")] + public string Highlight { get; set; } + + [JsonPropertyName("highlightPreTag")] + public string HighlightPreTag { get; set; } + + [JsonPropertyName("highlightPostTag")] + public string HighlightPostTag { get; set; } +} + diff --git a/API/Scoop/Responses/Manifest.cs b/API/Scoop/Responses/Manifest.cs new file mode 100644 index 0000000..7c5def8 --- /dev/null +++ b/API/Scoop/Responses/Manifest.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json.Serialization; + +namespace Scoop.Responses; + +public class Manifest +{ + /// + /// The version of the app that this manifest installs. + /// + [JsonPropertyName("version")] + public string Version { get; set; } + + /// + /// A one line string containing a short description of the program. + /// Don’t include the name of the program, if it’s the same + /// as the app’s filename. + /// + [JsonPropertyName("description")] + public string Description { get; set; } + + /// + /// The home page for the program. + /// + [JsonPropertyName("homepage")] + public string Homepage { get; set; } + + /// + /// A string or hash of the software license for the program. + /// + /// + /// For well-known licenses, please use the identifier found + /// at For other licenses, use the + /// URL of the license, if available. Otherwise, use + /// “Freeware”, “Proprietary”, “Public Domain”, “Shareware”, + /// or “Unknown”, as appropriate. If different files have + /// different licenses, separate licenses with a comma (,). + /// If the entire application is dual licensed, separate + /// licenses with a pipe symbol (|). + /// + [JsonPropertyName("license")] + public string License { get; set; } + + /// + /// If the app has 32- and 64-bit versions, architecture can + /// be used to wrap the differences. + /// + [JsonPropertyName("architecture")] + public Architecture Architecture { get; set; } + + /// + /// Definition of how the manifest can be updated automatically. + /// + /// + /// + /// + [JsonPropertyName("autoupdate")] + public Architecture Autoupdate { get; set; } + + /// + /// A string or array of strings of programs (executables or scripts) + /// to make available on the user's path. + /// + [JsonPropertyName("bin")] + public object Bin { get; set; } + + /// + /// App maintainers and developers can use the bin/checkver tool + /// to check for updated versions of apps. The property + /// in a manifest is a regular expression that can be used to + /// match the current stable version of an app from the app's + /// homepage. For an example, see the go manifest. If the homepage + /// doesn't have a reliable indication of the current version, you + /// can also specify a different URL to check—for an example see + /// the ruby manifest. + /// + [JsonPropertyName("checkver")] + public string CheckVer { get; set; } + + /// + /// Runtime dependencies for the app which will be installed automatically. + /// + /// + /// See also for an alternative to . + /// + [JsonPropertyName("depends")] + public List Depends { get; set; } + + /// + /// Add this directory to the user's path (or system path if --global is used). + /// The directory is relative to the install directory and must be inside the + /// install directory. + /// + [JsonPropertyName("env_add_path")] + public string EnvAddPath { get; set; } + + /// + /// Sets one or more environment variables for the user + /// (or system if --global is used). + /// + [JsonPropertyName("env_set")] + public Dictionary EnvSet { get; set; } + + /// + /// If points to a compressed file (.zip, .7z, .tar, .gz, + /// .lzma, and .lzh are supported), Scoop will extract just the directory + /// specified from it. + /// + [JsonPropertyName("extract_dir")] + public string ExtractDir { get; set; } + + /// + /// If points to a compressed file (.zip, .7z, .tar, .gz, + /// .lzma, and .lzh are supported), Scoop will extract all content to the + /// directory specified. + /// + [JsonPropertyName("extract_to")] + public string ExtractTo { get; set; } + + /// + /// A string or array of strings with a file hash for each URL in url. + /// Hashes are SHA256 by default, but you can use SHA512, SHA1 or MD5 by + /// prefixing the hash string with 'sha512:', 'sha1:' or 'md5:'. + /// + [JsonPropertyName("hash")] + public object Hash { get; set; } + + /// + /// Set to if the installer is InnoSetup based. + /// + [JsonPropertyName("innosetup")] + public bool IsInnoSetup { get; set; } + + public +} + +public class Architecture +{ + [JsonPropertyName("64bit")] + public ManifestDifference X64 { get; set; } + + [JsonPropertyName("32bit")] + public ManifestDifference X86 { get; set; } +} + +public class ManifestDifference +{ + +} + +public class Installer +{ + /// + /// The installer executable file. For + /// this defaults to the last URL downloaded. Must be specified for + /// . + /// + [JsonPropertyName("file")] + public string File { get; set; } + + [JsonPropertyName("script")] + public string Script { get; set; } +} + diff --git a/API/Scoop/Responses/SearchResponse.cs b/API/Scoop/Responses/SearchResponse.cs new file mode 100644 index 0000000..48d71e7 --- /dev/null +++ b/API/Scoop/Responses/SearchResponse.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Scoop.Responses; + +public class SearchResponse +{ + [JsonPropertyName("@odata.context")] + public string ODataContext { get; set; } + + [JsonPropertyName("@odata.count")] + public int ODataCount { get; set; } + + [JsonPropertyName("value")] + public List Results { get; set; } +} diff --git a/API/Scoop/Responses/SearchResult.cs b/API/Scoop/Responses/SearchResult.cs new file mode 100644 index 0000000..433201a --- /dev/null +++ b/API/Scoop/Responses/SearchResult.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace Scoop.Responses; + +public class SearchResult +{ + [JsonPropertyName("@search.score")] + public double SearchScore { get; set; } + + public string Id { get; set; } + public string Name { get; set; } + public string NamePartial { get; set; } + public string NameSuffix { get; set; } + public string Description { get; set; } + public string Homepage { get; set; } + public string License { get; set; } + public string Version { get; set; } + public SearchResultMetadata Metadata { get; set; } +} diff --git a/API/Scoop/Responses/SearchResultMetadata.cs b/API/Scoop/Responses/SearchResultMetadata.cs new file mode 100644 index 0000000..7863d95 --- /dev/null +++ b/API/Scoop/Responses/SearchResultMetadata.cs @@ -0,0 +1,17 @@ +using Flurl; +using System; + +namespace Scoop.Responses; + +public class SearchResultMetadata +{ + public string Repository { get; set; } + public bool OfficialRepository { get; set; } + public int RepositoryStars { get; set; } + public string FilePath { get; set; } + public string AuthorName { get; set; } + public DateTimeOffset Committed { get; set; } + public string Sha { get; set; } + + public Url GetManifestUrl() => $"{Repository}/raw/master/{FilePath}"; +} diff --git a/API/Scoop/Scoop.csproj b/API/Scoop/Scoop.csproj new file mode 100644 index 0000000..8cdecfa --- /dev/null +++ b/API/Scoop/Scoop.csproj @@ -0,0 +1,18 @@ + + + + netstandard2.0 + latest + enable + AnyCPU;x64;x86;ARM;ARM64 + + + + + + + + + + + diff --git a/API/Scoop/ScoopApi.cs b/API/Scoop/ScoopApi.cs new file mode 100644 index 0000000..fc2231d --- /dev/null +++ b/API/Scoop/ScoopApi.cs @@ -0,0 +1,8 @@ +using System; + +namespace Scoop; + +public class ScoopApi +{ + +} diff --git a/API/Scoop/ScoopSearch.cs b/API/Scoop/ScoopSearch.cs new file mode 100644 index 0000000..736295d --- /dev/null +++ b/API/Scoop/ScoopSearch.cs @@ -0,0 +1,53 @@ +using Flurl.Http; +using Flurl; +using System.Threading.Tasks; +using System.Threading; +using Scoop.Requests; +using Scoop.Responses; + +namespace Scoop; + +public static class ScoopSearch +{ + private const string SEARCH_URL = "https://scoopsearch.search.windows.net/indexes/apps/docs/search"; + private const string API_VERSION = "2020-06-30"; + private const string API_KEY = "DC6D2BBE65FC7313F2C52BBD2B0286ED"; + + public static async Task SearchAsync(string query, int count = 20, int skip = 0, CancellationToken token = default) + { + SearchRequest request = new() + { + Count = true, + Top = count, + Skip = skip, + Filter = string.Empty, + Highlight = "Name,NamePartial,NameSuffix,Description,Version,License,Metadata/Repository,Metadata/AuthorName", + HighlightPreTag = string.Empty, + HighlightPostTag = string.Empty, + OrderBy = "search.score() desc, Metadata/OfficialRepositoryNumber desc, NameSortable asc", + Search = query, + SearchMode = "all", + Select = "Id,Name,NamePartial,NameSuffix,Description,Homepage,License,Version,Metadata/Repository,Metadata/FilePath,Metadata/AuthorName,Metadata/OfficialRepository,Metadata/RepositoryStars,Metadata/Committed,Metadata/Sha" + }; + + var http = await SEARCH_URL + .WithHeader("api-key", API_KEY) + .SetQueryParam("api-version", API_VERSION) + .PostJsonAsync(request, cancellationToken: token); + + return await http.GetJsonAsync(); + } + + public static Task GetManifestAsync(SearchResultMetadata metadata, CancellationToken token = default) + { + return GetManifestAsync(metadata.GetManifestUrl(), token); + } + + public static async Task GetManifestAsync(Url metadataUrl, CancellationToken token = default) + { + var manifest = await metadataUrl + .WithHeader("User-Agent", "fluent-store") + .GetJsonAsync(cancellationToken: token); + return manifest; + } +} diff --git a/FluentStore.sln b/FluentStore.sln index 281b8b2..f3136bf 100644 --- a/FluentStore.sln +++ b/FluentStore.sln @@ -72,6 +72,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnifiedUpdatePlatform.Servi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentStore.Sources.Microsoft", "Sources\FluentStore.Sources.Microsoft\FluentStore.Sources.Microsoft.csproj", "{54E80C11-BF74-4A71-8620-1E110C9529C2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scoop", "API\Scoop\Scoop.csproj", "{7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -618,6 +620,26 @@ Global {54E80C11-BF74-4A71-8620-1E110C9529C2}.Release|x64.Build.0 = Release|x64 {54E80C11-BF74-4A71-8620-1E110C9529C2}.Release|x86.ActiveCfg = Release|x86 {54E80C11-BF74-4A71-8620-1E110C9529C2}.Release|x86.Build.0 = Release|x86 + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Debug|ARM.ActiveCfg = Debug|Any CPU + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Debug|ARM.Build.0 = Debug|Any CPU + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Debug|ARM64.Build.0 = Debug|Any CPU + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Debug|x64.ActiveCfg = Debug|Any CPU + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Debug|x64.Build.0 = Debug|Any CPU + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Debug|x86.ActiveCfg = Debug|Any CPU + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Debug|x86.Build.0 = Debug|Any CPU + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Release|Any CPU.Build.0 = Release|Any CPU + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Release|ARM.ActiveCfg = Release|Any CPU + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Release|ARM.Build.0 = Release|Any CPU + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Release|ARM64.ActiveCfg = Release|Any CPU + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Release|ARM64.Build.0 = Release|Any CPU + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Release|x64.ActiveCfg = Release|Any CPU + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Release|x64.Build.0 = Release|Any CPU + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Release|x86.ActiveCfg = Release|Any CPU + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -641,6 +663,7 @@ Global {0611BAD5-C405-45EF-BAEC-C52E50E9791D} = {042B0595-6134-4558-9725-157181F8D185} {3BC41048-0234-4996-82BB-3C98163241D2} = {042B0595-6134-4558-9725-157181F8D185} {54E80C11-BF74-4A71-8620-1E110C9529C2} = {5AEB24F0-A15D-4E57-8498-AE6F8C5A04D0} + {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248} = {A6FEC513-6706-44AF-B200-D40836E32C19} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DAB9771E-34BB-4E5E-95EA-D7538346B7C4} From 0a1832021ce8d66fc2a2aa4d59cdaea616a290fe Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Sun, 23 Mar 2025 10:27:34 -0500 Subject: [PATCH 02/11] Add ScoopTests --- FluentStore.sln | 23 +++++++++++++++++++++++ Tests/ScoopTests/ScoopTests.csproj | 27 +++++++++++++++++++++++++++ Tests/ScoopTests/Search.cs | 24 ++++++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 Tests/ScoopTests/ScoopTests.csproj create mode 100644 Tests/ScoopTests/Search.cs diff --git a/FluentStore.sln b/FluentStore.sln index f3136bf..a1a6fe9 100644 --- a/FluentStore.sln +++ b/FluentStore.sln @@ -74,6 +74,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentStore.Sources.Microso EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scoop", "API\Scoop\Scoop.csproj", "{7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScoopTests", "Tests\ScoopTests\ScoopTests.csproj", "{7180E2A8-0441-4F74-AB91-D1B962A033A4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -640,6 +642,26 @@ Global {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Release|x64.Build.0 = Release|Any CPU {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Release|x86.ActiveCfg = Release|Any CPU {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248}.Release|x86.Build.0 = Release|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Debug|ARM.ActiveCfg = Debug|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Debug|ARM.Build.0 = Debug|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Debug|ARM64.Build.0 = Debug|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Debug|x64.ActiveCfg = Debug|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Debug|x64.Build.0 = Debug|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Debug|x86.ActiveCfg = Debug|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Debug|x86.Build.0 = Debug|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Release|Any CPU.Build.0 = Release|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Release|ARM.ActiveCfg = Release|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Release|ARM.Build.0 = Release|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Release|ARM64.ActiveCfg = Release|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Release|ARM64.Build.0 = Release|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Release|x64.ActiveCfg = Release|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Release|x64.Build.0 = Release|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Release|x86.ActiveCfg = Release|Any CPU + {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -664,6 +686,7 @@ Global {3BC41048-0234-4996-82BB-3C98163241D2} = {042B0595-6134-4558-9725-157181F8D185} {54E80C11-BF74-4A71-8620-1E110C9529C2} = {5AEB24F0-A15D-4E57-8498-AE6F8C5A04D0} {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248} = {A6FEC513-6706-44AF-B200-D40836E32C19} + {7180E2A8-0441-4F74-AB91-D1B962A033A4} = {56C24CCE-3389-42E0-95F0-30B154BF421B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DAB9771E-34BB-4E5E-95EA-D7538346B7C4} diff --git a/Tests/ScoopTests/ScoopTests.csproj b/Tests/ScoopTests/ScoopTests.csproj new file mode 100644 index 0000000..c588e87 --- /dev/null +++ b/Tests/ScoopTests/ScoopTests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/Tests/ScoopTests/Search.cs b/Tests/ScoopTests/Search.cs new file mode 100644 index 0000000..476b266 --- /dev/null +++ b/Tests/ScoopTests/Search.cs @@ -0,0 +1,24 @@ +using Scoop; +using Xunit.Abstractions; + +namespace ScoopTests; + +public class Search(ITestOutputHelper output) +{ + [Fact] + public async Task SearchAsync_Minimum1() + { + var response = await ScoopSearch.SearchAsync("vscode"); + var actualResults = response.Results; + Assert.Equal(3, actualResults.Count); + + var firstResult = actualResults[0]; + Assert.Equal("8706b3f90529133e6d1450c4e363645a1b24d4cf", firstResult.Id); + Assert.Equal("vscode", firstResult.Name); + Assert.Equal("https://code.visualstudio.com/", firstResult.Homepage); + + Assert.NotNull(firstResult.Metadata); + Assert.Equal("https://github.com/ScoopInstaller/Extras", firstResult.Metadata.Repository); + Assert.Equal("bucket/vscode.json", firstResult.Metadata.FilePath); + } +} \ No newline at end of file From e4c778fa2471907f9d0a8cf5ba5f0e8a880671d2 Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Sun, 23 Mar 2025 10:27:56 -0500 Subject: [PATCH 03/11] Complete Manifest model --- .../Converters/StringOrArrayJsonConverter.cs | 58 ++++++++ API/Scoop/Responses/Manifest.cs | 135 +++++++++++++++--- 2 files changed, 177 insertions(+), 16 deletions(-) create mode 100644 API/Scoop/Converters/StringOrArrayJsonConverter.cs diff --git a/API/Scoop/Converters/StringOrArrayJsonConverter.cs b/API/Scoop/Converters/StringOrArrayJsonConverter.cs new file mode 100644 index 0000000..66a6931 --- /dev/null +++ b/API/Scoop/Converters/StringOrArrayJsonConverter.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Scoop.Converters; + +internal class StringOrArrayJsonConverter : JsonConverter> +{ + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(string) || base.CanConvert(typeToConvert); + + public override List? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType is JsonTokenType.Null) + return []; + + var singleStr = reader.GetString(); + if (singleStr is not null) + return [singleStr]; + + if (reader.TokenType != JsonTokenType.StartArray) + throw new JsonException($"Expected the start of a string array."); + + List list = []; + + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + var str = JsonSerializer.Deserialize(ref reader, options) + ?? throw new JsonException($"Unexpected null value could not be converted to List."); + + list.Add(str); + } + + return list; + } + + public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + if (value.Count == 1) + { + writer.WriteStringValue(value[0]); + return; + } + + writer.WriteStartArray(); + + foreach (var str in value) + writer.WriteStringValue(str); + + writer.WriteEndArray(); + } +} diff --git a/API/Scoop/Responses/Manifest.cs b/API/Scoop/Responses/Manifest.cs index 7c5def8..ad69b86 100644 --- a/API/Scoop/Responses/Manifest.cs +++ b/API/Scoop/Responses/Manifest.cs @@ -1,10 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System.Collections.Generic; +using System.Text.Json; using System.Text.Json.Serialization; +using Scoop.Converters; namespace Scoop.Responses; +// https://github.com/ScoopInstaller/Scoop/wiki/App-Manifests + public class Manifest { /// @@ -40,6 +42,7 @@ public class Manifest /// If the entire application is dual licensed, separate /// licenses with a pipe symbol (|). /// + // TODO: Deserialize non-standard licenses e.g. extras/sourcetree [JsonPropertyName("license")] public string License { get; set; } @@ -48,7 +51,7 @@ public class Manifest /// be used to wrap the differences. /// [JsonPropertyName("architecture")] - public Architecture Architecture { get; set; } + public Architecture? Architecture { get; set; } /// /// Definition of how the manifest can be updated automatically. @@ -63,8 +66,8 @@ public class Manifest /// A string or array of strings of programs (executables or scripts) /// to make available on the user's path. /// - [JsonPropertyName("bin")] - public object Bin { get; set; } + // TODO: Deserialize complex bin + public JsonElement? Bin { get; set; } /// /// App maintainers and developers can use the bin/checkver tool @@ -76,8 +79,9 @@ public class Manifest /// can also specify a different URL to check—for an example see /// the ruby manifest. /// + // TODO: Deserialize complex checkver [JsonPropertyName("checkver")] - public string CheckVer { get; set; } + public JsonElement? CheckVer { get; set; } /// /// Runtime dependencies for the app which will be installed automatically. @@ -86,7 +90,7 @@ public class Manifest /// See also for an alternative to . /// [JsonPropertyName("depends")] - public List Depends { get; set; } + public List? Depends { get; set; } /// /// Add this directory to the user's path (or system path if --global is used). @@ -94,14 +98,14 @@ public class Manifest /// install directory. /// [JsonPropertyName("env_add_path")] - public string EnvAddPath { get; set; } + public string? EnvAddPath { get; set; } /// /// Sets one or more environment variables for the user /// (or system if --global is used). /// [JsonPropertyName("env_set")] - public Dictionary EnvSet { get; set; } + public Dictionary? EnvSet { get; set; } /// /// If points to a compressed file (.zip, .7z, .tar, .gz, @@ -109,7 +113,7 @@ public class Manifest /// specified from it. /// [JsonPropertyName("extract_dir")] - public string ExtractDir { get; set; } + public string? ExtractDir { get; set; } /// /// If points to a compressed file (.zip, .7z, .tar, .gz, @@ -117,15 +121,15 @@ public class Manifest /// directory specified. /// [JsonPropertyName("extract_to")] - public string ExtractTo { get; set; } + public string? ExtractTo { get; set; } /// /// A string or array of strings with a file hash for each URL in url. /// Hashes are SHA256 by default, but you can use SHA512, SHA1 or MD5 by /// prefixing the hash string with 'sha512:', 'sha1:' or 'md5:'. /// - [JsonPropertyName("hash")] - public object Hash { get; set; } + [JsonConverter(typeof(StringOrArrayJsonConverter))] + public List Hash { get; set; } /// /// Set to if the installer is InnoSetup based. @@ -133,7 +137,77 @@ public class Manifest [JsonPropertyName("innosetup")] public bool IsInnoSetup { get; set; } - public + /// + /// Instructions for running a non-MSI installer. + /// + public Installer? Installer { get; set; } + + /// + /// A one-line string, or array of strings, with a message to be displayed after installing the app. + /// + [JsonConverter(typeof(StringOrArrayJsonConverter))] + public List Notes { get; set; } + + /// + /// A string or array of strings of directories and files to persist inside the data directory for the app. + /// + [JsonConverter(typeof(StringOrArrayJsonConverter))] + public List Persist { get; set; } + + /// + /// A one-line string, or array of strings, of the commands to be executed after an application is installed. + /// These can use variables like $dir, $persist_dir, and $version. + /// + [JsonConverter(typeof(StringOrArrayJsonConverter))] + public List PostInstall { get; set; } + + /// + /// Same options as , but executed before an application is installed. + /// + [JsonConverter(typeof(StringOrArrayJsonConverter))] + public List PreInstall { get; set; } + + /// + /// Same options as , but executed before an application is installed. + /// + [JsonConverter(typeof(StringOrArrayJsonConverter))] + public List PreUninstall { get; set; } + + /// + /// Same options as , but executed before an application is installed. + /// + [JsonConverter(typeof(StringOrArrayJsonConverter))] + public List PostUninstall { get; set; } + + /// + /// Install as a PowerShell module in ~/scoop/modules. + /// + [JsonPropertyName("psmodule")] + public PowerShellModuleDeclaration? PowerShellModule { get; set; } + + /// + /// Specifies the shortcut values to make available in the startmenu. + /// The array has to contain a executable/label pair. The third and fourth element are optional. + /// + public List>? Shortcuts { get; set; } + + /// + /// Display a message suggesting optional apps that provide complementary features. + /// + public Dictionary>? Suggest { get; set; } + + /// + /// Same options as , but the file/script is run to uninstall the application. + /// + public Installer? Uninstaller { get; set; } + + /// + /// The URL or URLs of files to download. If there's more than one URL, you can use a JSON - array, e.g. + /// "url": [ "http://example.org/program.zip", "http://example.org/dependencies.zip" ]. + /// URLs can be HTTP, HTTPS or FTP. + /// + [JsonConverter(typeof(StringOrArrayJsonConverter))] + public List Url { get; set; } } public class Architecture @@ -143,6 +217,9 @@ public class Architecture [JsonPropertyName("32bit")] public ManifestDifference X86 { get; set; } + + [JsonPropertyName("arm64")] + public ManifestDifference Arm64 { get; set; } } public class ManifestDifference @@ -160,7 +237,33 @@ public class Installer [JsonPropertyName("file")] public string File { get; set; } + /// + /// A one-line string, or array of strings, of commands to be executed as + /// an installer/uninstaller instead of . + /// [JsonPropertyName("script")] - public string Script { get; set; } + [JsonConverter(typeof(StringOrArrayJsonConverter))] + public List Script { get; set; } + + /// + /// An array of arguments to pass to the installer. Optional. + /// + public List Args { get; set; } + + /// + /// if the installer should be kept after running (for future uninstallation, as an example). + /// If omitted or set to any other value, the installer will be deleted after running. + /// See java/oraclejdk for an example. + /// This option will be ignored when used in an directive. + /// + public bool Keep { get; set; } +} + +public class PowerShellModuleDeclaration +{ + /// + /// The name of the module, which should match at least one file in the extracted directory for PowerShell to recognize this as a module. + /// + public string Name { get; set; } } From f02c6991ccbda31de369a24dba7761fc525b1414 Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Sun, 23 Mar 2025 10:28:04 -0500 Subject: [PATCH 04/11] Fix search requests --- API/Scoop/Requests/SearchRequest.cs | 10 +++++----- API/Scoop/ScoopSearch.cs | 12 +++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/API/Scoop/Requests/SearchRequest.cs b/API/Scoop/Requests/SearchRequest.cs index 7280a28..83e69fd 100644 --- a/API/Scoop/Requests/SearchRequest.cs +++ b/API/Scoop/Requests/SearchRequest.cs @@ -2,7 +2,7 @@ namespace Scoop.Requests; -internal class SearchRequest +public class SearchRequest { [JsonPropertyName("count")] public bool Count { get; set; } @@ -23,18 +23,18 @@ internal class SearchRequest public int Skip { get; set; } [JsonPropertyName("top")] - public int Top { get; set; } + public int Top { get; set; } = 20; [JsonPropertyName("select")] public string Select { get; set; } [JsonPropertyName("highlight")] - public string Highlight { get; set; } + public string Highlight { get; set; } = string.Empty; [JsonPropertyName("highlightPreTag")] - public string HighlightPreTag { get; set; } + public string HighlightPreTag { get; set; } = string.Empty; [JsonPropertyName("highlightPostTag")] - public string HighlightPostTag { get; set; } + public string HighlightPostTag { get; set; } = string.Empty; } diff --git a/API/Scoop/ScoopSearch.cs b/API/Scoop/ScoopSearch.cs index 736295d..df9b0e4 100644 --- a/API/Scoop/ScoopSearch.cs +++ b/API/Scoop/ScoopSearch.cs @@ -20,16 +20,18 @@ public static async Task SearchAsync(string query, int count = 2 Count = true, Top = count, Skip = skip, - Filter = string.Empty, - Highlight = "Name,NamePartial,NameSuffix,Description,Version,License,Metadata/Repository,Metadata/AuthorName", - HighlightPreTag = string.Empty, - HighlightPostTag = string.Empty, + Filter = "Metadata/OfficialRepositoryNumber eq 1 and Metadata/DuplicateOf eq null", OrderBy = "search.score() desc, Metadata/OfficialRepositoryNumber desc, NameSortable asc", Search = query, SearchMode = "all", - Select = "Id,Name,NamePartial,NameSuffix,Description,Homepage,License,Version,Metadata/Repository,Metadata/FilePath,Metadata/AuthorName,Metadata/OfficialRepository,Metadata/RepositoryStars,Metadata/Committed,Metadata/Sha" + Select = "Id,Name,NamePartial,NameSuffix,Description,Notes,Homepage,License,Version,Metadata/Repository,Metadata/FilePath,Metadata/OfficialRepository,Metadata/RepositoryStars,Metadata/Committed,Metadata/Sha" }; + return await SearchAsync(request, token); + } + + public static async Task SearchAsync(SearchRequest request, CancellationToken token = default) + { var http = await SEARCH_URL .WithHeader("api-key", API_KEY) .SetQueryParam("api-version", API_VERSION) From 9ec46efb2a1f7fefd23b00b2571c8f7ecd95fea1 Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Mon, 24 Mar 2025 17:17:50 -0500 Subject: [PATCH 05/11] Add service interfaces --- API/Scoop/IScoopMetadataService.cs | 13 +++++++++++++ API/Scoop/IScoopPackageManager.cs | 22 ++++++++++++++++++++++ API/Scoop/IScoopSearchService.cs | 10 ++++++++++ API/Scoop/ScoopSearch.cs | 21 +++++++++++++-------- Tests/ScoopTests/Search.cs | 4 +++- 5 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 API/Scoop/IScoopMetadataService.cs create mode 100644 API/Scoop/IScoopPackageManager.cs create mode 100644 API/Scoop/IScoopSearchService.cs diff --git a/API/Scoop/IScoopMetadataService.cs b/API/Scoop/IScoopMetadataService.cs new file mode 100644 index 0000000..269fb93 --- /dev/null +++ b/API/Scoop/IScoopMetadataService.cs @@ -0,0 +1,13 @@ +using Scoop.Responses; +using System.Threading.Tasks; +using System.Threading; +using Flurl; + +namespace Scoop; + +public interface IScoopMetadataService +{ + Task GetManifestAsync(SearchResultMetadata metadata, CancellationToken token = default); + + Task GetManifestAsync(Url metadataUrl, CancellationToken token = default); +} diff --git a/API/Scoop/IScoopPackageManager.cs b/API/Scoop/IScoopPackageManager.cs new file mode 100644 index 0000000..4437618 --- /dev/null +++ b/API/Scoop/IScoopPackageManager.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Scoop; + +public interface IScoopPackageManager +{ + Task InstallAsync(string name, CancellationToken token = default); + + Task DownloadAsync(string name, CancellationToken token = default); + + Task UpdateAsync(string name, CancellationToken token = default); + + Task UninstallAsync(string name, CancellationToken token = default); + + IAsyncEnumerable ListInstalledAppsAsync(string name, CancellationToken token = default); + + Task GetAppInfoAsync(string name, CancellationToken token = default); + + Task AddBucketAsync(string name, CancellationToken token = default); +} diff --git a/API/Scoop/IScoopSearchService.cs b/API/Scoop/IScoopSearchService.cs new file mode 100644 index 0000000..02bd70e --- /dev/null +++ b/API/Scoop/IScoopSearchService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using System.Threading; +using Scoop.Responses; + +namespace Scoop; + +public interface IScoopSearchService +{ + Task SearchAsync(string query, int count = 20, int skip = 0, CancellationToken token = default); +} diff --git a/API/Scoop/ScoopSearch.cs b/API/Scoop/ScoopSearch.cs index df9b0e4..a200d1d 100644 --- a/API/Scoop/ScoopSearch.cs +++ b/API/Scoop/ScoopSearch.cs @@ -7,13 +7,17 @@ namespace Scoop; -public static class ScoopSearch +public class ScoopSearch : IScoopSearchService, IScoopMetadataService { private const string SEARCH_URL = "https://scoopsearch.search.windows.net/indexes/apps/docs/search"; private const string API_VERSION = "2020-06-30"; - private const string API_KEY = "DC6D2BBE65FC7313F2C52BBD2B0286ED"; + private const string PUBLIC_API_KEY = "DC6D2BBE65FC7313F2C52BBD2B0286ED"; - public static async Task SearchAsync(string query, int count = 20, int skip = 0, CancellationToken token = default) + public string UserAgent { get; set; } = "Scoop/.NET"; + + public string ApiKey { get; set; } = PUBLIC_API_KEY; + + public async Task SearchAsync(string query, int count = 20, int skip = 0, CancellationToken token = default) { SearchRequest request = new() { @@ -30,25 +34,26 @@ public static async Task SearchAsync(string query, int count = 2 return await SearchAsync(request, token); } - public static async Task SearchAsync(SearchRequest request, CancellationToken token = default) + public async Task SearchAsync(SearchRequest request, CancellationToken token = default) { var http = await SEARCH_URL - .WithHeader("api-key", API_KEY) + .WithHeader("User-Agent", UserAgent) + .WithHeader("api-key", ApiKey) .SetQueryParam("api-version", API_VERSION) .PostJsonAsync(request, cancellationToken: token); return await http.GetJsonAsync(); } - public static Task GetManifestAsync(SearchResultMetadata metadata, CancellationToken token = default) + public Task GetManifestAsync(SearchResultMetadata metadata, CancellationToken token = default) { return GetManifestAsync(metadata.GetManifestUrl(), token); } - public static async Task GetManifestAsync(Url metadataUrl, CancellationToken token = default) + public async Task GetManifestAsync(Url metadataUrl, CancellationToken token = default) { var manifest = await metadataUrl - .WithHeader("User-Agent", "fluent-store") + .WithHeader("User-Agent", UserAgent) .GetJsonAsync(cancellationToken: token); return manifest; } diff --git a/Tests/ScoopTests/Search.cs b/Tests/ScoopTests/Search.cs index 476b266..74fa0a9 100644 --- a/Tests/ScoopTests/Search.cs +++ b/Tests/ScoopTests/Search.cs @@ -5,10 +5,12 @@ namespace ScoopTests; public class Search(ITestOutputHelper output) { + public readonly ScoopSearch _search = new(); + [Fact] public async Task SearchAsync_Minimum1() { - var response = await ScoopSearch.SearchAsync("vscode"); + var response = await _search.SearchAsync("vscode"); var actualResults = response.Results; Assert.Equal(3, actualResults.Count); From 11446ba10d9b7c5e7cf8e6db55f6ffb97c2c0a02 Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Mon, 24 Mar 2025 17:18:22 -0500 Subject: [PATCH 06/11] Add deserializers for complex types --- API/Scoop/Converters/LicenseJsonConverter.cs | 80 +++++++++++++++++++ .../Converters/StringOrArrayJsonConverter.cs | 19 +++-- API/Scoop/Requests/SearchRequest.cs | 6 +- API/Scoop/Responses/Manifest.cs | 57 +++++++------ API/Scoop/Responses/StringOrArray.cs | 15 ++++ API/Scoop/Scoop.csproj | 4 - Tests/ScoopTests/Search.cs | 38 +++++++++ 7 files changed, 180 insertions(+), 39 deletions(-) create mode 100644 API/Scoop/Converters/LicenseJsonConverter.cs create mode 100644 API/Scoop/Responses/StringOrArray.cs diff --git a/API/Scoop/Converters/LicenseJsonConverter.cs b/API/Scoop/Converters/LicenseJsonConverter.cs new file mode 100644 index 0000000..6ef77a6 --- /dev/null +++ b/API/Scoop/Converters/LicenseJsonConverter.cs @@ -0,0 +1,80 @@ +using System; +using System.Text.Json.Serialization; +using System.Text.Json; +using Scoop.Responses; +using System.Linq; +using System.Collections.Generic; + +namespace Scoop.Converters; + +internal class LicenseJsonConverter : JsonConverter +{ + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(string) || base.CanConvert(typeToConvert); + + public override LicenseInfo? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType is JsonTokenType.Null) + return null; + + if (reader.TokenType is JsonTokenType.String) + { + var singleString = reader.GetString(); + if (Uri.TryCreate(singleString, UriKind.Absolute, out var licenseUri)) + { + return new() + { + Url = licenseUri + }; + } + + return new() + { + MultiLicenses = ParseIdentifiers(singleString!) + }; + } + + if (reader.TokenType is not JsonTokenType.StartObject) + throw new JsonException($"Expected the start of an object."); + + Dictionary properties = []; + + while (reader.TokenType is not JsonTokenType.EndObject) + { + if (!reader.Read() || reader.TokenType is not JsonTokenType.PropertyName) + break; + var propertyName = reader.GetString(); + + if (!reader.Read() || reader.TokenType is not JsonTokenType.String) + throw new JsonException("Expected property value"); + var propertyValue = reader.GetString(); + + properties[propertyName!] = propertyValue!; + } + + LicenseInfo license = new(); + + if (properties.TryGetValue("identifier", out var identifierStr)) + license.MultiLicenses = ParseIdentifiers(identifierStr); + + if (properties.TryGetValue("url", out var urlStr) && Uri.TryCreate(urlStr, UriKind.Absolute, out var url)) + license.Url = url; + + return license; + } + + public override void Write(Utf8JsonWriter writer, LicenseInfo value, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + private static List> ParseIdentifiers(string value) + { + // Apps can be dual-licensed, and their different files within them can have different licenses. + // Dual-licenses are split by '|', and internal licenses by ','. + + return value + .Split('|') + .Select(d => d.Split(',').ToList()) + .ToList(); + } +} diff --git a/API/Scoop/Converters/StringOrArrayJsonConverter.cs b/API/Scoop/Converters/StringOrArrayJsonConverter.cs index 66a6931..4b7d921 100644 --- a/API/Scoop/Converters/StringOrArrayJsonConverter.cs +++ b/API/Scoop/Converters/StringOrArrayJsonConverter.cs @@ -1,29 +1,28 @@ using System; -using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; +using Scoop.Responses; namespace Scoop.Converters; -internal class StringOrArrayJsonConverter : JsonConverter> +internal class StringOrArrayJsonConverter : JsonConverter { public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(string) || base.CanConvert(typeToConvert); - public override List? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override StringOrArray? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType is JsonTokenType.Null) return []; - var singleStr = reader.GetString(); - if (singleStr is not null) - return [singleStr]; + if (reader.TokenType is JsonTokenType.String) + return [reader.GetString()]; - if (reader.TokenType != JsonTokenType.StartArray) + if (reader.TokenType is not JsonTokenType.StartArray) throw new JsonException($"Expected the start of a string array."); - List list = []; + StringOrArray list = []; - while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + while (reader.Read() && reader.TokenType is not JsonTokenType.EndArray) { var str = JsonSerializer.Deserialize(ref reader, options) ?? throw new JsonException($"Unexpected null value could not be converted to List."); @@ -34,7 +33,7 @@ internal class StringOrArrayJsonConverter : JsonConverter> return list; } - public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, StringOrArray value, JsonSerializerOptions options) { if (value is null) { diff --git a/API/Scoop/Requests/SearchRequest.cs b/API/Scoop/Requests/SearchRequest.cs index 83e69fd..3c04f61 100644 --- a/API/Scoop/Requests/SearchRequest.cs +++ b/API/Scoop/Requests/SearchRequest.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.Collections.Generic; +using System.Text.Json.Serialization; namespace Scoop.Requests; @@ -28,6 +29,9 @@ public class SearchRequest [JsonPropertyName("select")] public string Select { get; set; } + [JsonPropertyName("facets")] + public List Facets { get; set; } = []; + [JsonPropertyName("highlight")] public string Highlight { get; set; } = string.Empty; diff --git a/API/Scoop/Responses/Manifest.cs b/API/Scoop/Responses/Manifest.cs index ad69b86..d938951 100644 --- a/API/Scoop/Responses/Manifest.cs +++ b/API/Scoop/Responses/Manifest.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using Scoop.Converters; @@ -36,15 +37,15 @@ public class Manifest /// For well-known licenses, please use the identifier found /// at For other licenses, use the /// URL of the license, if available. Otherwise, use - /// “Freeware”, “Proprietary”, “Public Domain”, “Shareware”, - /// or “Unknown”, as appropriate. If different files have + /// "Freeware", "Proprietary", "Public Domain", "Shareware", + /// or "Unknown", as appropriate. If different files have /// different licenses, separate licenses with a comma (,). /// If the entire application is dual licensed, separate /// licenses with a pipe symbol (|). /// // TODO: Deserialize non-standard licenses e.g. extras/sourcetree [JsonPropertyName("license")] - public string License { get; set; } + public LicenseInfo? License { get; set; } /// /// If the app has 32- and 64-bit versions, architecture can @@ -128,8 +129,7 @@ public class Manifest /// Hashes are SHA256 by default, but you can use SHA512, SHA1 or MD5 by /// prefixing the hash string with 'sha512:', 'sha1:' or 'md5:'. /// - [JsonConverter(typeof(StringOrArrayJsonConverter))] - public List Hash { get; set; } + public StringOrArray Hash { get; set; } /// /// Set to if the installer is InnoSetup based. @@ -145,39 +145,33 @@ public class Manifest /// /// A one-line string, or array of strings, with a message to be displayed after installing the app. /// - [JsonConverter(typeof(StringOrArrayJsonConverter))] - public List Notes { get; set; } + public StringOrArray Notes { get; set; } /// /// A string or array of strings of directories and files to persist inside the data directory for the app. /// - [JsonConverter(typeof(StringOrArrayJsonConverter))] - public List Persist { get; set; } + public StringOrArray Persist { get; set; } /// /// A one-line string, or array of strings, of the commands to be executed after an application is installed. /// These can use variables like $dir, $persist_dir, and $version. /// - [JsonConverter(typeof(StringOrArrayJsonConverter))] - public List PostInstall { get; set; } + public StringOrArray PostInstall { get; set; } /// /// Same options as , but executed before an application is installed. /// - [JsonConverter(typeof(StringOrArrayJsonConverter))] - public List PreInstall { get; set; } + public StringOrArray PreInstall { get; set; } /// /// Same options as , but executed before an application is installed. /// - [JsonConverter(typeof(StringOrArrayJsonConverter))] - public List PreUninstall { get; set; } + public StringOrArray PreUninstall { get; set; } /// /// Same options as , but executed before an application is installed. /// - [JsonConverter(typeof(StringOrArrayJsonConverter))] - public List PostUninstall { get; set; } + public StringOrArray PostUninstall { get; set; } /// /// Install as a PowerShell module in ~/scoop/modules. @@ -194,7 +188,7 @@ public class Manifest /// /// Display a message suggesting optional apps that provide complementary features. /// - public Dictionary>? Suggest { get; set; } + public Dictionary? Suggest { get; set; } /// /// Same options as , but the file/script is run to uninstall the application. @@ -206,8 +200,7 @@ public class Manifest /// "url": [ "http://example.org/program.zip", "http://example.org/dependencies.zip" ]. /// URLs can be HTTP, HTTPS or FTP. /// - [JsonConverter(typeof(StringOrArrayJsonConverter))] - public List Url { get; set; } + public StringOrArray Url { get; set; } } public class Architecture @@ -242,13 +235,12 @@ public class Installer /// an installer/uninstaller instead of . /// [JsonPropertyName("script")] - [JsonConverter(typeof(StringOrArrayJsonConverter))] - public List Script { get; set; } + public StringOrArray Script { get; set; } /// /// An array of arguments to pass to the installer. Optional. /// - public List Args { get; set; } + public List? Args { get; set; } /// /// if the installer should be kept after running (for future uninstallation, as an example). @@ -267,3 +259,20 @@ public class PowerShellModuleDeclaration public string Name { get; set; } } +[JsonConverter(typeof(LicenseJsonConverter))] +public class LicenseInfo +{ + /// + /// The SPDX identifier, or "Freeware" (free to use forever), "Proprietary" (must pay to use), + /// "Public Domain", "Shareware" (free to try, must pay eventually), or "Unknown" + /// (unable to determine the license), as appropriate. + /// + public List> MultiLicenses { get; set; } = [["Unknown"]]; + + /// + /// For non-SPDX licenses, include a link to the license. It is acceptable to include links + /// to SPDX licenses, as well. + /// + public Uri? Url { get; set; } +} + diff --git a/API/Scoop/Responses/StringOrArray.cs b/API/Scoop/Responses/StringOrArray.cs new file mode 100644 index 0000000..6ad3e72 --- /dev/null +++ b/API/Scoop/Responses/StringOrArray.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Scoop.Converters; + +namespace Scoop.Responses; + +[JsonConverter(typeof(StringOrArrayJsonConverter))] +public class StringOrArray : List +{ + public StringOrArray() : base() { } + + public StringOrArray(IEnumerable values) : base(values) { } + + public StringOrArray(string value) : base([value]) { } +} diff --git a/API/Scoop/Scoop.csproj b/API/Scoop/Scoop.csproj index 8cdecfa..583d120 100644 --- a/API/Scoop/Scoop.csproj +++ b/API/Scoop/Scoop.csproj @@ -11,8 +11,4 @@ - - - - diff --git a/Tests/ScoopTests/Search.cs b/Tests/ScoopTests/Search.cs index 74fa0a9..7cc55ce 100644 --- a/Tests/ScoopTests/Search.cs +++ b/Tests/ScoopTests/Search.cs @@ -23,4 +23,42 @@ public async Task SearchAsync_Minimum1() Assert.Equal("https://github.com/ScoopInstaller/Extras", firstResult.Metadata.Repository); Assert.Equal("bucket/vscode.json", firstResult.Metadata.FilePath); } + + [Theory] + [InlineData("stack", "https://github.com/ScoopInstaller/Main/raw/489bc5445ae47a5504cb0fb1c32a43fa34d9a8f8/bucket/stack.json", "Add-Path -Path \"$env:APPDATA\\local\\bin\" -Global:$global")] + [InlineData("cuda", "https://github.com/ScoopInstaller/Main/raw/489bc5445ae47a5504cb0fb1c32a43fa34d9a8f8/bucket/cuda.json", "$names = @('bin', 'extras', 'include', 'lib', 'libnvvp', 'nvml', 'nvvm', 'compute-sanitizer')\r\nforeach ($name in $names) {\r\n Copy-Item \"$dir\\cuda_*\\*\\$name\" \"$dir\" -Recurse -Force\r\n Copy-Item \"$dir\\lib*\\*\\$name\" \"$dir\" -Recurse -Force\r\n}\r\nGet-ChildItem \"$dir\" -Exclude $names | Remove-Item -Recurse -Force")] + [InlineData("go", "https://github.com/ScoopInstaller/Main/raw/489bc5445ae47a5504cb0fb1c32a43fa34d9a8f8/bucket/go.json", "$envgopath = \"$env:USERPROFILE\\go\"\r\nif ($env:GOPATH) { $envgopath = $env:GOPATH }\r\ninfo \"Adding '$envgopath\\bin' to PATH...\"\r\nAdd-Path -Path \"$envgopath\\bin\" -Global:$global -Force")] + public async Task DeserializeInstallerScript(string name, string manifestUrl, string fullScriptEx) + { + var manifest = await _search.GetManifestAsync(manifestUrl); + + Assert.NotNull(manifest); + Assert.NotNull(manifest.Installer); + Assert.NotNull(manifest.Installer.Script); + + var fullScriptAc = string.Join("\r\n", manifest.Installer.Script); + output.WriteLine(fullScriptAc); + + Assert.Equal(fullScriptEx, fullScriptAc); + } + + [Theory] + [InlineData("gpg", "https://github.com/ScoopInstaller/Main/raw/489bc5445ae47a5504cb0fb1c32a43fa34d9a8f8/bucket/gpg.json", "GPL-3.0-or-later", null)] + [InlineData("cuda", "https://github.com/ScoopInstaller/Main/raw/489bc5445ae47a5504cb0fb1c32a43fa34d9a8f8/bucket/cuda.json", "Freeware", "https://docs.nvidia.com/cuda/eula/index.html")] + [InlineData("xz", "https://github.com/ScoopInstaller/Main/raw/489bc5445ae47a5504cb0fb1c32a43fa34d9a8f8/bucket/xz.json", "LGPL-2.1-or-later,GPL-2.0-or-later,GPL-3.0-or-later,Public Domain", "https://git.tukaani.org/?p=xz.git;a=blob;f=COPYING")] + [InlineData("scc", "https://github.com/ScoopInstaller/Main/raw/489bc5445ae47a5504cb0fb1c32a43fa34d9a8f8/bucket/scc.json", "MIT|Unlicense", null)] + [InlineData("mongodb", "https://github.com/ScoopInstaller/Main/raw/489bc5445ae47a5504cb0fb1c32a43fa34d9a8f8/bucket/mongodb.json", "SSPL-1.0", "https://www.mongodb.com/licensing/server-side-public-license")] + public async Task DeserializeAppLicense(string name, string manifestUrl, string identifierEx, string? urlEx) + { + var manifest = await _search.GetManifestAsync(manifestUrl); + + Assert.NotNull(manifest); + Assert.NotNull(manifest.License); + + var identifierAc = string.Join("|", manifest.License.MultiLicenses.Select(d => string.Join(",", d))); + Assert.Equal(identifierEx, identifierAc); + + var urlAc = manifest.License.Url?.ToString(); + Assert.Equal(urlEx, urlAc); + } } \ No newline at end of file From 0133bd10a4a288e8ca72bf8b529c3a69cbe491a4 Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Mon, 24 Mar 2025 17:29:57 -0500 Subject: [PATCH 07/11] Remove outdated TODO --- API/Scoop/Responses/Manifest.cs | 1 - PublishTool/Properties/launchSettings.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/API/Scoop/Responses/Manifest.cs b/API/Scoop/Responses/Manifest.cs index d938951..5e1ca77 100644 --- a/API/Scoop/Responses/Manifest.cs +++ b/API/Scoop/Responses/Manifest.cs @@ -43,7 +43,6 @@ public class Manifest /// If the entire application is dual licensed, separate /// licenses with a pipe symbol (|). /// - // TODO: Deserialize non-standard licenses e.g. extras/sourcetree [JsonPropertyName("license")] public LicenseInfo? License { get; set; } diff --git a/PublishTool/Properties/launchSettings.json b/PublishTool/Properties/launchSettings.json index 32568ce..adfd1f9 100644 --- a/PublishTool/Properties/launchSettings.json +++ b/PublishTool/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "PublishTool": { "commandName": "Project", - "commandLineArgs": "--repo=E:\\Repos\\yoshiask\\FluentStore --id=FluentStore.Sources.Microsoft -v -f --install" + "commandLineArgs": "--repo=E:\\Repos\\yoshiask\\FluentStore --id=FluentStore.Sources.Scoop -v -f --install" } } } \ No newline at end of file From 36ee61117f6625e859448a00325486376155ad1b Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Mon, 24 Mar 2025 17:38:36 -0500 Subject: [PATCH 08/11] Add GetManifestAsync tests --- Tests/ScoopTests/Search.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Tests/ScoopTests/Search.cs b/Tests/ScoopTests/Search.cs index 7cc55ce..905c23d 100644 --- a/Tests/ScoopTests/Search.cs +++ b/Tests/ScoopTests/Search.cs @@ -24,6 +24,29 @@ public async Task SearchAsync_Minimum1() Assert.Equal("bucket/vscode.json", firstResult.Metadata.FilePath); } + [Theory] + [InlineData("vscode")] + [InlineData("notepadplusplus")] + [InlineData("notepadplusplus-np")] + [InlineData("jetbrains-mono")] + [InlineData("7zip")] + [InlineData("7zip-beta")] + public async Task GetManifestAsync_FromSearchResult(string name) + { + var response = await _search.SearchAsync(name); + var searchResult = response.Results.FirstOrDefault(); + + Assert.NotNull(searchResult); + Assert.Equal(name, searchResult.Name, ignoreCase: true); + Assert.NotNull(searchResult.Metadata); + + var manifest = await _search.GetManifestAsync(searchResult.Metadata); + + Assert.NotNull(manifest); + + output.WriteLine(manifest.Description ?? "{null}"); + } + [Theory] [InlineData("stack", "https://github.com/ScoopInstaller/Main/raw/489bc5445ae47a5504cb0fb1c32a43fa34d9a8f8/bucket/stack.json", "Add-Path -Path \"$env:APPDATA\\local\\bin\" -Global:$global")] [InlineData("cuda", "https://github.com/ScoopInstaller/Main/raw/489bc5445ae47a5504cb0fb1c32a43fa34d9a8f8/bucket/cuda.json", "$names = @('bin', 'extras', 'include', 'lib', 'libnvvp', 'nvml', 'nvvm', 'compute-sanitizer')\r\nforeach ($name in $names) {\r\n Copy-Item \"$dir\\cuda_*\\*\\$name\" \"$dir\" -Recurse -Force\r\n Copy-Item \"$dir\\lib*\\*\\$name\" \"$dir\" -Recurse -Force\r\n}\r\nGet-ChildItem \"$dir\" -Exclude $names | Remove-Item -Recurse -Force")] From 76c44b3914a3dfc39e92d7a847dcb3b6d1bac482 Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Tue, 25 Mar 2025 01:50:46 -0500 Subject: [PATCH 09/11] Add more managers --- ...oopPackageManager.cs => IScoopAppManager.cs} | 6 ++---- API/Scoop/IScoopBucketManager.cs | 17 +++++++++++++++++ API/Scoop/Responses/Bucket.cs | 14 ++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) rename API/Scoop/{IScoopPackageManager.cs => IScoopAppManager.cs} (69%) create mode 100644 API/Scoop/IScoopBucketManager.cs create mode 100644 API/Scoop/Responses/Bucket.cs diff --git a/API/Scoop/IScoopPackageManager.cs b/API/Scoop/IScoopAppManager.cs similarity index 69% rename from API/Scoop/IScoopPackageManager.cs rename to API/Scoop/IScoopAppManager.cs index 4437618..68664b1 100644 --- a/API/Scoop/IScoopPackageManager.cs +++ b/API/Scoop/IScoopAppManager.cs @@ -4,7 +4,7 @@ namespace Scoop; -public interface IScoopPackageManager +public interface IScoopAppManager { Task InstallAsync(string name, CancellationToken token = default); @@ -14,9 +14,7 @@ public interface IScoopPackageManager Task UninstallAsync(string name, CancellationToken token = default); - IAsyncEnumerable ListInstalledAppsAsync(string name, CancellationToken token = default); + IAsyncEnumerable GetInstalledAppsAsync(string name, CancellationToken token = default); Task GetAppInfoAsync(string name, CancellationToken token = default); - - Task AddBucketAsync(string name, CancellationToken token = default); } diff --git a/API/Scoop/IScoopBucketManager.cs b/API/Scoop/IScoopBucketManager.cs new file mode 100644 index 0000000..ca7782c --- /dev/null +++ b/API/Scoop/IScoopBucketManager.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Scoop.Responses; + +namespace Scoop; + +public interface IScoopBucketManager +{ + Task AddBucketAsync(string name, string? repo = null, CancellationToken token = default); + + Task RemoveBucketAsync(string name, CancellationToken token = default); + + IAsyncEnumerable GetKnownBucketsAsync(CancellationToken token = default); + + IAsyncEnumerable GetBucketsAsync(CancellationToken token = default); +} diff --git a/API/Scoop/Responses/Bucket.cs b/API/Scoop/Responses/Bucket.cs new file mode 100644 index 0000000..c47474e --- /dev/null +++ b/API/Scoop/Responses/Bucket.cs @@ -0,0 +1,14 @@ +using System; + +namespace Scoop.Responses; + +public class Bucket +{ + public string Name { get; set; } + + public string Source { get; set; } + + public DateTime Updated { get; set; } + + public int Manifests { get; set; } +} From c19292537733c9cd2e95dc20ee4f035f31a501a6 Mon Sep 17 00:00:00 2001 From: "Joshua \"Yoshi\" Askharoun" Date: Tue, 25 Mar 2025 01:51:01 -0500 Subject: [PATCH 10/11] Start Scoop PowerShell impl --- Tests/ScoopTests/PwshHost.cs | 23 ++++++ Tests/ScoopTests/ScoopPwsh.cs | 117 +++++++++++++++++++++++++++++ Tests/ScoopTests/ScoopTests.csproj | 1 + 3 files changed, 141 insertions(+) create mode 100644 Tests/ScoopTests/PwshHost.cs create mode 100644 Tests/ScoopTests/ScoopPwsh.cs diff --git a/Tests/ScoopTests/PwshHost.cs b/Tests/ScoopTests/PwshHost.cs new file mode 100644 index 0000000..69e8cfd --- /dev/null +++ b/Tests/ScoopTests/PwshHost.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentStore.Sources.Scoop; +using Xunit.Abstractions; + +namespace ScoopTests; + +public class PwshHost(ITestOutputHelper output) +{ + private readonly ScoopPwsh _scoop = new(); + + [Fact] + public async Task GetBucketsAsync() + { + await foreach (var bucket in _scoop.GetBucketsAsync()) + { + output.WriteLine($"{bucket.Name} {bucket.Updated} {bucket.Manifests} {bucket.Source}"); + } + } +} diff --git a/Tests/ScoopTests/ScoopPwsh.cs b/Tests/ScoopTests/ScoopPwsh.cs new file mode 100644 index 0000000..d336589 --- /dev/null +++ b/Tests/ScoopTests/ScoopPwsh.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.PowerShell; +using Scoop; +using Scoop.Responses; + +namespace FluentStore.Sources.Scoop; + +public class ScoopPwsh : IScoopAppManager, IScoopBucketManager +{ + private const string CMDLET_SCOOP = "scoop"; + + public async Task AddBucketAsync(string name, string repo = null, CancellationToken token = default) + { + var ps = PowerShell.Create(); + ps.AddCommand(CMDLET_SCOOP).AddArgument("bucket").AddArgument("add").AddArgument(name); + + if (repo is not null) + ps.AddCommand(repo); + + await ps.InvokeAsync(); + } + + public Task DownloadAsync(string name, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public Task GetAppInfoAsync(string name, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public async IAsyncEnumerable GetBucketsAsync(CancellationToken token = default) + { + using var ps = CreatePwsh(); + ps.AddCommand(CMDLET_SCOOP).AddArgument("bucket").AddArgument("list"); + + var psResults = await ps.InvokeAsync(); + + foreach (var psResult in psResults) + { + Bucket bucket = new() + { + Name = GetValue(psResult, "Name"), + Source = GetValue(psResult, "Source"), + Updated = GetValue(psResult, "Updated"), + Manifests = GetValue(psResult, "Manifests"), + }; + + yield return bucket; + } + + yield break; + throw new NotImplementedException(); + } + + public IAsyncEnumerable GetInstalledAppsAsync(string name, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable GetKnownBucketsAsync(CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public Task InstallAsync(string name, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public Task RemoveBucketAsync(string name, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public Task UninstallAsync(string name, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public Task UpdateAsync(string name, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + /// + /// Creates a PowerShell console with an unrestricted execution policy + /// + /// + private PowerShell CreatePwsh() + { + // Create a default initial session state and set the execution policy. + InitialSessionState initialSessionState = InitialSessionState.CreateDefault(); + initialSessionState.ExecutionPolicy = ExecutionPolicy.Unrestricted; + + // Create a runspace and open it. This example uses C#8 simplified using statements + Runspace runspace = RunspaceFactory.CreateRunspace(initialSessionState); + runspace.Open(); + + // Create a PowerShell object + return PowerShell.Create(runspace); + } + + private static T GetValue(PSObject psObj, string propertyName) + { + var psProp = psObj.Properties[propertyName]; + return (T)((PSObject)psProp.Value).BaseObject; + } +} diff --git a/Tests/ScoopTests/ScoopTests.csproj b/Tests/ScoopTests/ScoopTests.csproj index c588e87..15ef7d0 100644 --- a/Tests/ScoopTests/ScoopTests.csproj +++ b/Tests/ScoopTests/ScoopTests.csproj @@ -14,6 +14,7 @@ + From 0e352a34f652f55d16af7b8d00fac054c1b813ad Mon Sep 17 00:00:00 2001 From: Yoshi Askharoun Date: Mon, 28 Jul 2025 20:23:32 -0500 Subject: [PATCH 11/11] [WIP] Scoop source project --- FluentStore.sln | 23 +++++++++++++++++++ .../FluentStore.Sources.Scoop.csproj | 15 ++++++++++++ Tests/ScoopTests/ScoopPwsh.cs | 14 +++++++---- 3 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 Sources/FluentStore.Sources.Scoop/FluentStore.Sources.Scoop.csproj diff --git a/FluentStore.sln b/FluentStore.sln index a1a6fe9..80ee5f6 100644 --- a/FluentStore.sln +++ b/FluentStore.sln @@ -76,6 +76,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scoop", "API\Scoop\Scoop.cs EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScoopTests", "Tests\ScoopTests\ScoopTests.csproj", "{7180E2A8-0441-4F74-AB91-D1B962A033A4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentStore.Sources.Scoop", "Sources\FluentStore.Sources.Scoop\FluentStore.Sources.Scoop.csproj", "{A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -662,6 +664,26 @@ Global {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Release|x64.Build.0 = Release|Any CPU {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Release|x86.ActiveCfg = Release|Any CPU {7180E2A8-0441-4F74-AB91-D1B962A033A4}.Release|x86.Build.0 = Release|Any CPU + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Debug|Any CPU.ActiveCfg = Debug|x64 + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Debug|Any CPU.Build.0 = Debug|x64 + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Debug|ARM.ActiveCfg = Debug|x64 + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Debug|ARM.Build.0 = Debug|x64 + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Debug|ARM64.Build.0 = Debug|ARM64 + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Debug|x64.ActiveCfg = Debug|x64 + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Debug|x64.Build.0 = Debug|x64 + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Debug|x86.ActiveCfg = Debug|x86 + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Debug|x86.Build.0 = Debug|x86 + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Release|Any CPU.ActiveCfg = Release|x64 + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Release|Any CPU.Build.0 = Release|x64 + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Release|ARM.ActiveCfg = Release|x64 + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Release|ARM.Build.0 = Release|x64 + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Release|ARM64.ActiveCfg = Release|ARM64 + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Release|ARM64.Build.0 = Release|ARM64 + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Release|x64.ActiveCfg = Release|x64 + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Release|x64.Build.0 = Release|x64 + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Release|x86.ActiveCfg = Release|x86 + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -687,6 +709,7 @@ Global {54E80C11-BF74-4A71-8620-1E110C9529C2} = {5AEB24F0-A15D-4E57-8498-AE6F8C5A04D0} {7F2DEB6A-C57F-4A2C-A5C5-F0DF96F2C248} = {A6FEC513-6706-44AF-B200-D40836E32C19} {7180E2A8-0441-4F74-AB91-D1B962A033A4} = {56C24CCE-3389-42E0-95F0-30B154BF421B} + {A999C2F9-8C07-4B2C-A1AC-6FCB504DF6D1} = {5AEB24F0-A15D-4E57-8498-AE6F8C5A04D0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DAB9771E-34BB-4E5E-95EA-D7538346B7C4} diff --git a/Sources/FluentStore.Sources.Scoop/FluentStore.Sources.Scoop.csproj b/Sources/FluentStore.Sources.Scoop/FluentStore.Sources.Scoop.csproj new file mode 100644 index 0000000..63c1353 --- /dev/null +++ b/Sources/FluentStore.Sources.Scoop/FluentStore.Sources.Scoop.csproj @@ -0,0 +1,15 @@ + + + + + + + + + Scoop + $(FluentStoreMajorMinorVersion).0.1 + $(Version)-alpha + Provides support for Scoop. Requires the Scoop CLI to be intalled. + + + diff --git a/Tests/ScoopTests/ScoopPwsh.cs b/Tests/ScoopTests/ScoopPwsh.cs index d336589..b547c48 100644 --- a/Tests/ScoopTests/ScoopPwsh.cs +++ b/Tests/ScoopTests/ScoopPwsh.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; @@ -16,7 +17,7 @@ public class ScoopPwsh : IScoopAppManager, IScoopBucketManager { private const string CMDLET_SCOOP = "scoop"; - public async Task AddBucketAsync(string name, string repo = null, CancellationToken token = default) + public async Task AddBucketAsync(string name, string? repo = null, CancellationToken token = default) { var ps = PowerShell.Create(); ps.AddCommand(CMDLET_SCOOP).AddArgument("bucket").AddArgument("add").AddArgument(name); @@ -37,15 +38,21 @@ public Task GetAppInfoAsync(string name, CancellationToken token = defau throw new NotImplementedException(); } - public async IAsyncEnumerable GetBucketsAsync(CancellationToken token = default) + public async IAsyncEnumerable GetBucketsAsync([EnumeratorCancellation] CancellationToken token = default) { using var ps = CreatePwsh(); ps.AddCommand(CMDLET_SCOOP).AddArgument("bucket").AddArgument("list"); + token.ThrowIfCancellationRequested(); + var psResults = await ps.InvokeAsync(); + token.ThrowIfCancellationRequested(); + foreach (var psResult in psResults) { + token.ThrowIfCancellationRequested(); + Bucket bucket = new() { Name = GetValue(psResult, "Name"), @@ -56,9 +63,6 @@ public async IAsyncEnumerable GetBucketsAsync(CancellationToken token = yield return bucket; } - - yield break; - throw new NotImplementedException(); } public IAsyncEnumerable GetInstalledAppsAsync(string name, CancellationToken token = default)