From 740e3273c716efaab48f9a0788fae79c272810d6 Mon Sep 17 00:00:00 2001 From: daumast Date: Tue, 19 May 2026 13:40:42 +0300 Subject: [PATCH 1/3] Refactoring. Common elements and initial Program cleanup --- .../Devolutions.Pinget.Cli/CommonOptions.cs | 23 + dotnet/src/Devolutions.Pinget.Cli/Consts.cs | 7 + .../Devolutions.Pinget.Cli.csproj | 6 +- dotnet/src/Devolutions.Pinget.Cli/Enums.cs | 8 + .../Extensions/CommandExtensions.cs | 18 + .../Extensions/ExceptionExtensions.cs | 11 + .../Extensions/OptionExtensions.cs | 18 + .../Extensions/StringExtensions.cs | 15 + .../Helpers/InstalledPackageChecker.cs | 15 + .../Devolutions.Pinget.Cli/Helpers/Json.cs | 29 + .../Devolutions.Pinget.Cli/Helpers/Output.cs | 50 + .../src/Devolutions.Pinget.Cli/Helpers/Pin.cs | 66 ++ .../Helpers/PinQuery.cs | 41 + .../Helpers/PinTarget.cs | 37 + .../Devolutions.Pinget.Cli/Helpers/Print.cs | 315 ++++++ .../Helpers/RequestCreator.cs | 64 ++ .../Devolutions.Pinget.Cli/Helpers/Source.cs | 50 + .../Devolutions.Pinget.Cli/Helpers/Text.cs | 9 + dotnet/src/Devolutions.Pinget.Cli/Program.cs | 970 +++--------------- .../Devolutions.Pinget.Cli/QueryArguments.cs | 9 + 20 files changed, 942 insertions(+), 819 deletions(-) create mode 100644 dotnet/src/Devolutions.Pinget.Cli/CommonOptions.cs create mode 100644 dotnet/src/Devolutions.Pinget.Cli/Consts.cs create mode 100644 dotnet/src/Devolutions.Pinget.Cli/Enums.cs create mode 100644 dotnet/src/Devolutions.Pinget.Cli/Extensions/CommandExtensions.cs create mode 100644 dotnet/src/Devolutions.Pinget.Cli/Extensions/ExceptionExtensions.cs create mode 100644 dotnet/src/Devolutions.Pinget.Cli/Extensions/OptionExtensions.cs create mode 100644 dotnet/src/Devolutions.Pinget.Cli/Extensions/StringExtensions.cs create mode 100644 dotnet/src/Devolutions.Pinget.Cli/Helpers/InstalledPackageChecker.cs create mode 100644 dotnet/src/Devolutions.Pinget.Cli/Helpers/Json.cs create mode 100644 dotnet/src/Devolutions.Pinget.Cli/Helpers/Output.cs create mode 100644 dotnet/src/Devolutions.Pinget.Cli/Helpers/Pin.cs create mode 100644 dotnet/src/Devolutions.Pinget.Cli/Helpers/PinQuery.cs create mode 100644 dotnet/src/Devolutions.Pinget.Cli/Helpers/PinTarget.cs create mode 100644 dotnet/src/Devolutions.Pinget.Cli/Helpers/Print.cs create mode 100644 dotnet/src/Devolutions.Pinget.Cli/Helpers/RequestCreator.cs create mode 100644 dotnet/src/Devolutions.Pinget.Cli/Helpers/Source.cs create mode 100644 dotnet/src/Devolutions.Pinget.Cli/Helpers/Text.cs create mode 100644 dotnet/src/Devolutions.Pinget.Cli/QueryArguments.cs diff --git a/dotnet/src/Devolutions.Pinget.Cli/CommonOptions.cs b/dotnet/src/Devolutions.Pinget.Cli/CommonOptions.cs new file mode 100644 index 0000000..4052a4e --- /dev/null +++ b/dotnet/src/Devolutions.Pinget.Cli/CommonOptions.cs @@ -0,0 +1,23 @@ +using System.CommandLine; +using Devolutions.Pinget.Cli.Extensions; + +namespace Devolutions.Pinget.Cli; + +internal static class CommonOptions +{ + internal static Option Query => new Option("--query", "Query").WithAliases("-q"); + + internal static Option Id => new("--id", "Filter by id"); + + internal static Option Name => new("--name", "Filter by name"); + + internal static Option Moniker => new("--moniker", "Filter by moniker"); + + internal static Option Source => new Option("--source", "Source name").WithAliases("-s"); + + internal static Option Exact => new Option("--exact", "Exact match").WithAliases("-e"); + + internal static Option Count => new Option("--count", "Max results").WithAliases("-n"); + + internal static Option Version => new Option("--version", "Version").WithAliases("-v"); +} diff --git a/dotnet/src/Devolutions.Pinget.Cli/Consts.cs b/dotnet/src/Devolutions.Pinget.Cli/Consts.cs new file mode 100644 index 0000000..991e664 --- /dev/null +++ b/dotnet/src/Devolutions.Pinget.Cli/Consts.cs @@ -0,0 +1,7 @@ +namespace Devolutions.Pinget.Cli; + +internal static class Consts +{ + internal const string Version = "0.4.2"; + internal const string UpgradeUnsupportedWarning = "Upgrading packages is not supported on this platform; no changes were made."; +} diff --git a/dotnet/src/Devolutions.Pinget.Cli/Devolutions.Pinget.Cli.csproj b/dotnet/src/Devolutions.Pinget.Cli/Devolutions.Pinget.Cli.csproj index e98a569..534d4d0 100644 --- a/dotnet/src/Devolutions.Pinget.Cli/Devolutions.Pinget.Cli.csproj +++ b/dotnet/src/Devolutions.Pinget.Cli/Devolutions.Pinget.Cli.csproj @@ -31,11 +31,7 @@ - + diff --git a/dotnet/src/Devolutions.Pinget.Cli/Enums.cs b/dotnet/src/Devolutions.Pinget.Cli/Enums.cs new file mode 100644 index 0000000..c9bf59c --- /dev/null +++ b/dotnet/src/Devolutions.Pinget.Cli/Enums.cs @@ -0,0 +1,8 @@ +namespace Devolutions.Pinget.Cli; + +internal enum OutputFormat +{ + Text, + Json, + Yaml, +} diff --git a/dotnet/src/Devolutions.Pinget.Cli/Extensions/CommandExtensions.cs b/dotnet/src/Devolutions.Pinget.Cli/Extensions/CommandExtensions.cs new file mode 100644 index 0000000..54a2f57 --- /dev/null +++ b/dotnet/src/Devolutions.Pinget.Cli/Extensions/CommandExtensions.cs @@ -0,0 +1,18 @@ +using System.CommandLine; + +namespace Devolutions.Pinget.Cli.Extensions; + +internal static partial class Extensions +{ + extension(Command command) + { + public Command WithAliases(params string[] aliases) + { + foreach (var alias in aliases) + { + command.AddAlias(alias); + } + return command; + } + } +} diff --git a/dotnet/src/Devolutions.Pinget.Cli/Extensions/ExceptionExtensions.cs b/dotnet/src/Devolutions.Pinget.Cli/Extensions/ExceptionExtensions.cs new file mode 100644 index 0000000..bb6f6bc --- /dev/null +++ b/dotnet/src/Devolutions.Pinget.Cli/Extensions/ExceptionExtensions.cs @@ -0,0 +1,11 @@ +namespace Devolutions.Pinget.Cli.Extensions; + +internal static class ExceptionExtensions +{ + extension(Exception ex) + { + internal bool CanIgnoreUnavailableImportFailure => ex is InvalidOperationException && + (ex.Message.Contains("No package matched the query", StringComparison.OrdinalIgnoreCase) || + ex.Message.Contains("No applicable installer found", StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/dotnet/src/Devolutions.Pinget.Cli/Extensions/OptionExtensions.cs b/dotnet/src/Devolutions.Pinget.Cli/Extensions/OptionExtensions.cs new file mode 100644 index 0000000..7e1a78d --- /dev/null +++ b/dotnet/src/Devolutions.Pinget.Cli/Extensions/OptionExtensions.cs @@ -0,0 +1,18 @@ +using System.CommandLine; + +namespace Devolutions.Pinget.Cli.Extensions; + +internal static partial class Extensions +{ + extension(Option option) + { + public Option WithAliases(params string[] aliases) + { + foreach (var alias in aliases) + { + option.AddAlias(alias); + } + return option; + } + } +} diff --git a/dotnet/src/Devolutions.Pinget.Cli/Extensions/StringExtensions.cs b/dotnet/src/Devolutions.Pinget.Cli/Extensions/StringExtensions.cs new file mode 100644 index 0000000..9d2d51a --- /dev/null +++ b/dotnet/src/Devolutions.Pinget.Cli/Extensions/StringExtensions.cs @@ -0,0 +1,15 @@ +namespace Devolutions.Pinget.Cli.Extensions; + +internal static class StringExtensions +{ + extension(string value) + { + public bool BooleanSetting => + value.Trim().ToLowerInvariant() switch + { + "true" or "1" or "on" or "yes" or "enabled" => true, + "false" or "0" or "off" or "no" or "disabled" => false, + _ => throw new InvalidOperationException($"Unsupported admin setting value: {value}") + }; + } +} diff --git a/dotnet/src/Devolutions.Pinget.Cli/Helpers/InstalledPackageChecker.cs b/dotnet/src/Devolutions.Pinget.Cli/Helpers/InstalledPackageChecker.cs new file mode 100644 index 0000000..3147549 --- /dev/null +++ b/dotnet/src/Devolutions.Pinget.Cli/Helpers/InstalledPackageChecker.cs @@ -0,0 +1,15 @@ +using Devolutions.Pinget.Core; + +namespace Devolutions.Pinget.Cli.Helpers; + +internal static class InstalledPackageChecker +{ + internal static bool IsPresent(Repository repo, string packageId, string? sourceName) => + repo.List(new ListQuery + { + Id = packageId, + Source = sourceName, + Exact = true, + Count = 1, + }).Matches.Count > 0; +} diff --git a/dotnet/src/Devolutions.Pinget.Cli/Helpers/Json.cs b/dotnet/src/Devolutions.Pinget.Cli/Helpers/Json.cs new file mode 100644 index 0000000..80a0e72 --- /dev/null +++ b/dotnet/src/Devolutions.Pinget.Cli/Helpers/Json.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using YamlDotNet.Serialization; + +namespace Devolutions.Pinget.Cli.Helpers; + +internal static class Json +{ + internal static JsonSerializerOptions Options = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + internal static void WriteNode(JsonNode value, OutputFormat output) + { + switch (output) + { + case OutputFormat.Yaml: + var structured = JsonSerializer.Deserialize(value.ToJsonString()) ?? new object(); + Console.Write(new SerializerBuilder().Build().Serialize(structured)); + break; + default: + Console.WriteLine(value.ToJsonString(new JsonSerializerOptions { WriteIndented = true })); + break; + } + } + + internal static string? GetString(JsonElement element, string propertyName) => + element.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String + ? value.GetString() + : null; +} diff --git a/dotnet/src/Devolutions.Pinget.Cli/Helpers/Output.cs b/dotnet/src/Devolutions.Pinget.Cli/Helpers/Output.cs new file mode 100644 index 0000000..3219de4 --- /dev/null +++ b/dotnet/src/Devolutions.Pinget.Cli/Helpers/Output.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using Devolutions.Pinget.Core; +using YamlDotNet.Serialization; + +namespace Devolutions.Pinget.Cli.Helpers; + +internal static class Output +{ + internal static OutputFormat GetFormat(string? value) => + value?.ToLowerInvariant() switch + { + "json" => OutputFormat.Json, + "yaml" => OutputFormat.Yaml, + _ => OutputFormat.Text, + }; + + internal static void WriteStructuredOutput(object value, OutputFormat output) + { + switch (output) + { + case OutputFormat.Json: + if (value is SerializableShowManifest showManifest) + Console.WriteLine(JsonSerializer.Serialize(showManifest, PingetJsonContext.Default.SerializableShowManifest)); + else + Console.WriteLine(JsonSerializer.Serialize(value, Json.Options)); + break; + case OutputFormat.Yaml: + Console.Write(new SerializerBuilder().Build().Serialize(value)); + break; + default: + throw new InvalidOperationException("Text output should be handled separately."); + } + } + + internal static void WriteManifestStructuredOutput(object value, OutputFormat output) + { + if (output == OutputFormat.Yaml && value is List> documents) + { + var serializer = new SerializerBuilder().Build(); + foreach (var document in documents) + { + Console.Write("---\n"); + Console.Write(serializer.Serialize(document)); + } + return; + } + + WriteStructuredOutput(value, output); + } +} diff --git a/dotnet/src/Devolutions.Pinget.Cli/Helpers/Pin.cs b/dotnet/src/Devolutions.Pinget.Cli/Helpers/Pin.cs new file mode 100644 index 0000000..1b8ddf1 --- /dev/null +++ b/dotnet/src/Devolutions.Pinget.Cli/Helpers/Pin.cs @@ -0,0 +1,66 @@ +using Devolutions.Pinget.Core; + +namespace Devolutions.Pinget.Cli.Helpers; + +internal static class Pin +{ + internal static List Filter(Repository repo, PackageQuery query) + { + IEnumerable pins = repo.ListPins(query.Source); + if (!string.IsNullOrWhiteSpace(query.Id)) + pins = pins.Where(pin => Text.Matches(pin.PackageId, query.Id, query.Exact)); + + var needsCatalogResolution = + !string.IsNullOrWhiteSpace(query.Query) || + !string.IsNullOrWhiteSpace(query.Name) || + !string.IsNullOrWhiteSpace(query.Moniker) || + !string.IsNullOrWhiteSpace(query.Tag) || + !string.IsNullOrWhiteSpace(query.Command); + if (!needsCatalogResolution) + return pins.ToList(); + + var searchResult = repo.Search(query); + var keys = searchResult.Matches + .Select(match => $"{match.Id}|{match.SourceName ?? ""}") + .ToHashSet(StringComparer.OrdinalIgnoreCase); + var ids = searchResult.Matches + .Select(match => match.Id) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + return pins + .Where(pin => keys.Contains($"{pin.PackageId}|{pin.SourceId}") || + (string.IsNullOrWhiteSpace(pin.SourceId) && ids.Contains(pin.PackageId))) + .ToList(); + } + + internal static PinRecord? FindMatching(ListMatch match, IReadOnlyList pins) + { + PinRecord? sourceSpecific = null; + PinRecord? sourceAgnostic = null; + foreach (var pin in pins) + { + if (!pin.PackageId.Equals(match.Id, StringComparison.OrdinalIgnoreCase) && + !pin.PackageId.Equals(match.LocalId, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (!string.IsNullOrWhiteSpace(pin.SourceId)) + { + if (!string.IsNullOrWhiteSpace(match.SourceName) && + pin.SourceId.Equals(match.SourceName, StringComparison.OrdinalIgnoreCase)) + { + sourceSpecific = pin; + break; + } + } + else if (sourceAgnostic is null) + { + sourceAgnostic = pin; + } + } + + return sourceSpecific ?? sourceAgnostic; + } + +} diff --git a/dotnet/src/Devolutions.Pinget.Cli/Helpers/PinQuery.cs b/dotnet/src/Devolutions.Pinget.Cli/Helpers/PinQuery.cs new file mode 100644 index 0000000..6679b92 --- /dev/null +++ b/dotnet/src/Devolutions.Pinget.Cli/Helpers/PinQuery.cs @@ -0,0 +1,41 @@ +using Devolutions.Pinget.Core; + +namespace Devolutions.Pinget.Cli.Helpers; + +internal static class PinQuery +{ + internal static PackageQuery Create( + string? query, + string? id, + string? name, + string? moniker, + string? tag, + string? command, + string? source, + bool exact) => + new() + { + Query = query, + Id = id, + Name = name, + Moniker = moniker, + Tag = tag, + Command = command, + Source = source, + Exact = exact, + Count = 200, + }; + + internal static void EnsureProvided(PackageQuery query, string commandName) + { + if (string.IsNullOrWhiteSpace(query.Query) && + string.IsNullOrWhiteSpace(query.Id) && + string.IsNullOrWhiteSpace(query.Name) && + string.IsNullOrWhiteSpace(query.Moniker) && + string.IsNullOrWhiteSpace(query.Tag) && + string.IsNullOrWhiteSpace(query.Command)) + { + throw new InvalidOperationException($"{commandName} requires a query or explicit filter."); + } + } +} diff --git a/dotnet/src/Devolutions.Pinget.Cli/Helpers/PinTarget.cs b/dotnet/src/Devolutions.Pinget.Cli/Helpers/PinTarget.cs new file mode 100644 index 0000000..9a393aa --- /dev/null +++ b/dotnet/src/Devolutions.Pinget.Cli/Helpers/PinTarget.cs @@ -0,0 +1,37 @@ +using Devolutions.Pinget.Core; + +namespace Devolutions.Pinget.Cli.Helpers; + +internal static class PinTarget +{ + internal static SearchMatch ResolveSingleAvailable(Repository repo, PackageQuery query) + { + var result = repo.Search(query); + if (result.Matches.Count == 0) + throw new InvalidOperationException("No package matched the query."); + if (result.Matches.Count > 1) + throw new InvalidOperationException("Multiple packages matched the query; refine the query."); + return result.Matches[0]; + } + + internal static ListMatch ResolveSingleInstalled(Repository repo, PackageQuery query) + { + var result = repo.List(new ListQuery + { + Query = query.Query, + Id = query.Id, + Name = query.Name, + Moniker = query.Moniker, + Tag = query.Tag, + Command = query.Command, + Source = query.Source, + Exact = query.Exact, + Count = 200, + }); + if (result.Matches.Count == 0) + throw new InvalidOperationException("No installed package matched the query."); + if (result.Matches.Count > 1) + throw new InvalidOperationException("Multiple installed packages matched the query; refine the query."); + return result.Matches[0]; + } +} diff --git a/dotnet/src/Devolutions.Pinget.Cli/Helpers/Print.cs b/dotnet/src/Devolutions.Pinget.Cli/Helpers/Print.cs new file mode 100644 index 0000000..007b332 --- /dev/null +++ b/dotnet/src/Devolutions.Pinget.Cli/Helpers/Print.cs @@ -0,0 +1,315 @@ +using Devolutions.Pinget.Core; + +namespace Devolutions.Pinget.Cli.Helpers; + +internal static class Print +{ + internal static void Info() + { + Version(); + Console.WriteLine("Pure C# subset of Pinget (portable winget)"); + Console.WriteLine($"Runtime: {System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}"); + Console.WriteLine($"OS: {System.Runtime.InteropServices.RuntimeInformation.OSDescription}"); + } + + internal static void Version() => Console.WriteLine($"pinget v{Consts.Version}"); + + internal static void Search(SearchResponse result) + { + Warnings(result.Warnings); + if (result.Matches.Count == 0) { Console.WriteLine("No package matched the supplied query."); return; } + + bool showMatch = result.Matches.Any(m => m.MatchCriteria is not null); + if (showMatch) + { + Console.WriteLine("{0,-32} {1,-40} {2,-18} {3,-24} Source", "Name", "Id", "Version", "Match"); + foreach (var m in result.Matches) + { + Console.WriteLine( + "{0,-32} {1,-40} {2,-18} {3,-24} {4}", + Trunc(m.Name, 32), + Trunc(m.Id, 40), + m.Version ?? "Unknown", + Trunc(m.MatchCriteria ?? "", 24), + m.SourceName); + } + } + else + { + Console.WriteLine("{0,-36} {1,-42} {2,-18} Source", "Name", "Id", "Version"); + foreach (var m in result.Matches) + { + Console.WriteLine( + "{0,-36} {1,-42} {2,-18} {3}", + Trunc(m.Name, 36), + Trunc(m.Id, 42), + m.Version ?? "Unknown", + m.SourceName); + } + } + + if (result.Truncated) Console.WriteLine($""); + } + + internal static void Warnings(List warnings) { foreach (var w in warnings) Console.Error.WriteLine($"warning: {w}"); } + + internal static void Table(string[] headers, List rows) + { + if (headers.Length == 0) return; + int cols = headers.Length; + var widths = headers.Select(h => h.Length).ToArray(); + var hasData = new bool[cols]; + + foreach (var row in rows) + for (int i = 0; i < Math.Min(cols, row.Length); i++) + if (!string.IsNullOrEmpty(row[i])) + { + hasData[i] = true; + widths[i] = Math.Max(widths[i], row[i].Length); + } + + for (int i = 0; i < cols; i++) + if (!hasData[i]) widths[i] = 0; + + var spaceAfter = Enumerable.Repeat(true, cols).ToArray(); + spaceAfter[^1] = false; + for (int i = cols - 1; i >= 1; i--) + { + if (widths[i] == 0) spaceAfter[i - 1] = false; + else break; + } + + int totalWidth = widths.Zip(spaceAfter, (w, s) => w + (s ? 1 : 0)).Sum(); + int consoleWidth = 119; + try { consoleWidth = Math.Max(1, Console.WindowWidth - 2); } catch { } + if (totalWidth >= consoleWidth) + { + int extra = totalWidth - consoleWidth + 1; + while (extra > 0) + { + int target = 0; + for (int i = 1; i < cols; i++) + if (widths[i] > widths[target]) target = i; + if (widths[target] > 1) widths[target]--; + extra--; + } + totalWidth = Math.Max(0, consoleWidth - 1); + } + + TableLine(headers, widths, spaceAfter); + Console.WriteLine(new string('-', totalWidth)); + foreach (var row in rows) + TableLine(row, widths, spaceAfter); + } + + internal static void Versions(VersionsResult result) + { + Warnings(result.Warnings); + Console.WriteLine($"Found {result.Package.Name} [{result.Package.Id}]"); + Console.WriteLine("Version"); + Console.WriteLine(new string('-', 40)); + foreach (var v in result.Versions) + { + Console.Write(v.Version); + if (!string.IsNullOrEmpty(v.Channel)) Console.Write($" [{v.Channel}]"); + Console.WriteLine(); + } + } + + internal static void Show(ShowResult result) + { + Warnings(result.Warnings); + Console.WriteLine($"Found {result.Package.Name} [{result.Package.Id}]"); + var m = result.Manifest; + Console.WriteLine($"Version: {m.Version}"); + PrintOpt("Publisher", m.Publisher); + PrintOpt("Publisher Url", m.PublisherUrl); + PrintOpt("Publisher Support Url", m.PublisherSupportUrl); + PrintOpt("Author", m.Author); + PrintOpt("Moniker", m.Moniker); + if (m.Description is not null) + { + Console.WriteLine("Description:"); + foreach (var line in m.Description.Split('\n')) + Console.WriteLine($" {line.TrimEnd()}"); + } + PrintOpt("Homepage", m.PackageUrl); + PrintOpt("License", m.License); + PrintOpt("License Url", m.LicenseUrl); + PrintOpt("Privacy Url", m.PrivacyUrl); + PrintOpt("Copyright", m.Copyright); + PrintOpt("Copyright Url", m.CopyrightUrl); + PrintOpt("Release Notes Url", m.ReleaseNotesUrl); + if (m.Documentation.Count > 0) + { + Console.WriteLine("Documentation:"); + foreach (var doc in m.Documentation) + Console.WriteLine($" {doc.Label ?? "Link"}: {doc.Url}"); + } + + if (result.Manifest.PackageDependencies.Count > 0) + { + Console.Write("Dependencies:"); + Console.WriteLine($" {string.Join(", ", result.Manifest.PackageDependencies)}"); + } + + if (m.Tags.Count > 0) { Console.WriteLine("Tags:"); foreach (var t in m.Tags) Console.WriteLine($" {t}"); } + + if (result.SelectedInstaller is Installer inst) + { + Console.WriteLine("Installer:"); + PrintOpt(" Type", inst.InstallerType); + PrintOpt(" Architecture", inst.Architecture); + PrintOpt(" Locale", inst.Locale); + PrintOpt(" Scope", inst.Scope); + PrintOpt(" Url", inst.Url); + PrintOpt(" Sha256", inst.Sha256); + PrintOpt(" ProductCode", inst.ProductCode); + PrintOpt(" ReleaseDate", inst.ReleaseDate); + } + else if (m.Installers.Count > 0) + { + Console.WriteLine("Installer:"); + Console.WriteLine(" No applicable installer found; see logs for more details."); + } + } + + internal static void ListResult(ListResponse result, bool details, bool upgrade) + { + Warnings(result.Warnings); + if (result.Matches.Count == 0) { Console.WriteLine("No installed package found matching input criteria."); return; } + + if (details) + { + int total = result.Matches.Count; + for (int idx = 0; idx < total; idx++) + { + var m = result.Matches[idx]; + if (total > 1) + Console.WriteLine($"({idx + 1}/{total}) {m.Name} [{m.Id}]"); + else + Console.WriteLine($"{m.Name} [{m.Id}]"); + PrintOpt("Version", m.InstalledVersion); + PrintOpt("Publisher", m.Publisher); + if (m.LocalId != m.Id) PrintOpt("Local Identifier", m.LocalId); + PrintOpt("Source", m.SourceName); + PrintOpt("Available", m.AvailableVersion); + } + } + else + { + bool showAvailable = result.Matches.Any(m => !string.IsNullOrEmpty(m.AvailableVersion)); + string[] headers = showAvailable + ? ["Name", "Id", "Version", "Available", "Source"] + : ["Name", "Id", "Version", "Source"]; + var rows = result.Matches.Select(m => showAvailable + ? new[] { m.Name, m.Id, m.InstalledVersion, m.AvailableVersion ?? "", m.SourceName ?? "" } + : new[] { m.Name, m.Id, m.InstalledVersion, m.SourceName ?? "" }).ToList(); + Table(headers, rows); + } + + if (result.Truncated) Console.WriteLine($""); + if (upgrade) Console.WriteLine($"{result.Matches.Count} upgrades available."); + } + + internal static void Sources(List sources) + { + Console.WriteLine($"{"Name",-12} {"Trust",-8} {"Explicit",-8} Argument"); + foreach (var s in sources) + Console.WriteLine($"{s.Name,-12} {s.TrustLevel,-8} {s.Explicit.ToString().ToLowerInvariant(),-8} {s.Arg}"); + } + + internal static void PackageActionResult(InstallResult result, string action, string actionPastTense) + { + Warnings(result.Warnings); + var target = string.IsNullOrWhiteSpace(result.Version) + ? result.PackageId + : $"{result.PackageId} v{result.Version}"; + if (result.NoOp) + Console.WriteLine($"No changes were made for {target}."); + else if (result.Success) + Console.WriteLine($"Successfully {actionPastTense} {target}"); + else + Console.Error.WriteLine($"Failed to {action} {target} (exit code: {result.ExitCode})"); + } + + internal static void ErrorLookup(string input) + { + if (!long.TryParse(input.StartsWith("0x", StringComparison.OrdinalIgnoreCase) + ? input[2..] : input, System.Globalization.NumberStyles.HexNumber, null, out var code)) + { + Console.Error.WriteLine($"error: Could not parse '{input}' as an error code"); + return; + } + + var lookup = LookupHresult(code); + if (lookup is not null) + { + // APPINSTALLER codes (0x8A15xxxx): show symbol on same line + if ((code & 0xFFFF0000L) == unchecked((long)0x8A150000)) + Console.WriteLine($"0x{code:x8} : {lookup.Value.Symbol}"); + else + Console.WriteLine($"0x{code:x8}"); + Console.WriteLine(lookup.Value.Description); + } + else + { + Console.WriteLine($"0x{code:x8}"); + Console.WriteLine(" Unknown error code"); + } + } + + static void TableLine(string[] values, int[] widths, bool[] spaceAfter) + { + var sb = new System.Text.StringBuilder(); + for (int i = 0; i < Math.Min(values.Length, widths.Length); i++) + { + if (widths[i] == 0) continue; + var val = values[i] ?? ""; + if (val.Length > widths[i]) + { + sb.Append(Trunc(val, widths[i])); + if (spaceAfter[i]) sb.Append(' '); + } + else + { + sb.Append(val); + if (spaceAfter[i]) sb.Append(' ', widths[i] - val.Length + 1); + } + } + Console.WriteLine(sb.ToString().TrimEnd()); + } + + static string Trunc(string s, int max) => s.Length <= max ? s : s[..(max - 1)] + "."; + + static void PrintOpt(string label, string? value) { if (value is not null) Console.WriteLine($"{label}: {value}"); } + + static (string Symbol, string Description)? LookupHresult(long code) + { + return (code & 0xFFFF0000L) switch + { + 0x8A150000 => (code & 0xFFFF) switch + { + 0x0001 => ("APPINSTALLER_CLI_ERROR_INTERNAL_ERROR", "An unexpected error occurred."), + 0x0002 => ("APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS", "Invalid command line arguments."), + 0x0003 => ("APPINSTALLER_CLI_ERROR_COMMAND_FAILED", "The command failed."), + 0x0004 => ("APPINSTALLER_CLI_ERROR_MANIFEST_FAILED", "Opening the manifest failed."), + 0x0007 => ("APPINSTALLER_CLI_ERROR_NO_APPLICABLE_INSTALLER", "No applicable installer found."), + 0x000E => ("APPINSTALLER_CLI_ERROR_PACKAGE_NOT_FOUND", "No package matched the query."), + 0x0010 => ("APPINSTALLER_CLI_ERROR_SOURCE_NAME_ALREADY_EXISTS", "A source with the given name already exists."), + 0x0012 => ("APPINSTALLER_CLI_ERROR_NO_SOURCES_DEFINED", "No sources are configured."), + 0x0013 => ("APPINSTALLER_CLI_ERROR_MULTIPLE_APPLICATIONS_FOUND", "Multiple packages matched the query."), + 0x0016 => ("APPINSTALLER_CLI_ERROR_NO_APPLICABLE_UPGRADE", "No applicable upgrade found."), + _ => null, + }, + _ => code switch + { + unchecked(0x80004005) => ("E_FAIL", "Unspecified error"), + unchecked(0x80070005) => ("E_ACCESSDENIED", "General access denied error"), + unchecked(0x80070057) => ("E_INVALIDARG", "One or more arguments are not valid"), + unchecked(0x8007000E) => ("E_OUTOFMEMORY", "Failed to allocate necessary memory"), + _ => null, + } + }; + } +} diff --git a/dotnet/src/Devolutions.Pinget.Cli/Helpers/RequestCreator.cs b/dotnet/src/Devolutions.Pinget.Cli/Helpers/RequestCreator.cs new file mode 100644 index 0000000..ef7736d --- /dev/null +++ b/dotnet/src/Devolutions.Pinget.Cli/Helpers/RequestCreator.cs @@ -0,0 +1,64 @@ +using Devolutions.Pinget.Core; + +namespace Devolutions.Pinget.Cli.Helpers; + +internal static class RequestCreator +{ + internal static InstallRequest Install( + PackageQuery query, + string? manifestPath, + InstallerMode mode, + string? logPath, + string? custom, + string? overrideArgs, + string? installLocation, + bool skipDependencies, + bool dependenciesOnly, + bool acceptPackageAgreements, + bool force, + string? rename, + bool uninstallPrevious, + bool ignoreSecurityHash, + string? dependencySource, + bool noUpgrade) => + new() + { + Query = query, + ManifestPath = manifestPath, + Mode = mode, + LogPath = logPath, + Custom = custom, + Override = overrideArgs, + InstallLocation = installLocation, + SkipDependencies = skipDependencies, + DependenciesOnly = dependenciesOnly, + AcceptPackageAgreements = acceptPackageAgreements, + Force = force, + Rename = rename, + UninstallPrevious = uninstallPrevious, + IgnoreSecurityHash = ignoreSecurityHash, + DependencySource = dependencySource, + NoUpgrade = noUpgrade, + }; + + internal static RepairRequest Repair( + PackageQuery query, + string? manifestPath, + string? productCode, + InstallerMode mode, + string? logPath, + bool acceptPackageAgreements, + bool force, + bool ignoreSecurityHash) => + new() + { + Query = query, + ManifestPath = manifestPath, + ProductCode = productCode, + Mode = mode, + LogPath = logPath, + AcceptPackageAgreements = acceptPackageAgreements, + Force = force, + IgnoreSecurityHash = ignoreSecurityHash, + }; +} diff --git a/dotnet/src/Devolutions.Pinget.Cli/Helpers/Source.cs b/dotnet/src/Devolutions.Pinget.Cli/Helpers/Source.cs new file mode 100644 index 0000000..a13e586 --- /dev/null +++ b/dotnet/src/Devolutions.Pinget.Cli/Helpers/Source.cs @@ -0,0 +1,50 @@ +using Devolutions.Pinget.Core; + +namespace Devolutions.Pinget.Cli.Helpers; + +internal static class Source +{ + internal static string ResolveAddValue(string? positionalValue, string? optionValue, string label) + { + if (!string.IsNullOrWhiteSpace(positionalValue) && + !string.IsNullOrWhiteSpace(optionValue) && + !string.Equals(positionalValue, optionValue, StringComparison.Ordinal)) + { + throw new InvalidOperationException($"Conflicting source {label} values were provided."); + } + + if (!string.IsNullOrWhiteSpace(optionValue)) + return optionValue; + + if (!string.IsNullOrWhiteSpace(positionalValue)) + return positionalValue; + + throw new InvalidOperationException($"source add requires a {label}."); + } + + internal static SourceKind ParseKind(string? value) + { + if (string.IsNullOrWhiteSpace(value) || + string.Equals(value, "rest", StringComparison.OrdinalIgnoreCase) || + string.Equals(value, "Microsoft.Rest", StringComparison.OrdinalIgnoreCase)) + { + return SourceKind.Rest; + } + + if (string.Equals(value, "preindexed", StringComparison.OrdinalIgnoreCase) || + string.Equals(value, "Microsoft.PreIndexed.Package", StringComparison.OrdinalIgnoreCase)) + { + return SourceKind.PreIndexed; + } + + throw new InvalidOperationException($"Unsupported source type: {value}"); + } + + internal static string FormatType(SourceKind kind) => kind switch + { + SourceKind.Rest => "Microsoft.Rest", + SourceKind.PreIndexed => "Microsoft.PreIndexed.Package", + _ => kind.ToString(), + }; + +} diff --git a/dotnet/src/Devolutions.Pinget.Cli/Helpers/Text.cs b/dotnet/src/Devolutions.Pinget.Cli/Helpers/Text.cs new file mode 100644 index 0000000..409445f --- /dev/null +++ b/dotnet/src/Devolutions.Pinget.Cli/Helpers/Text.cs @@ -0,0 +1,9 @@ +namespace Devolutions.Pinget.Cli.Helpers; + +internal static class Text +{ + internal static bool Matches(string value, string query, bool exact) => + exact + ? value.Equals(query, StringComparison.OrdinalIgnoreCase) + : value.Contains(query, StringComparison.OrdinalIgnoreCase); +} diff --git a/dotnet/src/Devolutions.Pinget.Cli/Program.cs b/dotnet/src/Devolutions.Pinget.Cli/Program.cs index f3ec3c7..6d52725 100644 --- a/dotnet/src/Devolutions.Pinget.Cli/Program.cs +++ b/dotnet/src/Devolutions.Pinget.Cli/Program.cs @@ -2,22 +2,21 @@ using System.Security.Cryptography; using System.Text.Json; using System.Text.Json.Nodes; +using Devolutions.Pinget.Cli; +using Devolutions.Pinget.Cli.Extensions; +using Devolutions.Pinget.Cli.Helpers; using Devolutions.Pinget.Core; using YamlDotNet.Serialization; -const string Version = "0.4.2"; -const string UpgradeUnsupportedWarning = "Upgrading packages is not supported on this platform; no changes were made."; - if (args.Length == 1 && (string.Equals(args[0], "--version", StringComparison.OrdinalIgnoreCase) || string.Equals(args[0], "-v", StringComparison.OrdinalIgnoreCase))) { - PrintVersion(); + Print.Version(); return 0; } var rootCommand = new RootCommand("Pinget: portable winget in pure C#"); -var outputOption = new Option("--output", "Output format: text, json, or yaml"); -outputOption.AddAlias("-o"); +var outputOption = new Option("--output", "Output format: text, json, or yaml").WithAliases("-o"); outputOption.FromAmong("text", "json", "yaml"); rootCommand.AddGlobalOption(outputOption); @@ -26,28 +25,13 @@ var infoOption = new Option("--info", "Display general info"); rootCommand.AddGlobalOption(infoOption); -// ── Common options ── -Option QueryArg(string description = "Query") -{ - var o = new Option("--query", description); - o.AddAlias("-q"); - return o; -} -Option IdOpt() => new("--id", "Filter by id"); -Option NameOpt() => new("--name", "Filter by name"); -Option MonikerOpt() => new("--moniker", "Filter by moniker"); -Option SourceOpt() { var o = new Option("--source", "Source name"); o.AddAlias("-s"); return o; } -Option ExactOpt() { var o = new Option("--exact", "Exact match"); o.AddAlias("-e"); return o; } -Option CountOpt() { var o = new Option("--count", "Max results"); o.AddAlias("-n"); return o; } -Option VersionOpt() { var o = new Option("--version", "Version"); o.AddAlias("-v"); return o; } - // ── Search command ── var searchCommand = new Command("search", "Search for packages"); -var sqArg = new Argument("query", () => null, "Search query"); -var sqOpt = QueryArg(); var sidOpt = IdOpt(); var snOpt = NameOpt(); var smOpt = MonikerOpt(); -var ssOpt = SourceOpt(); var seOpt = ExactOpt(); var scOpt = CountOpt(); +var sqArg = QueryArguments.Search; +var sqOpt = CommonOptions.Query; var sidOpt = CommonOptions.Id; var snOpt = CommonOptions.Name; var smOpt = CommonOptions.Moniker; +var ssOpt = CommonOptions.Source; var seOpt = CommonOptions.Exact; var scOpt = CommonOptions.Count; var sTagOpt = new Option("--tag", "Filter by tag"); -var sCmdOpt = new Option("--command", "Filter by command"); sCmdOpt.AddAlias("--cmd"); +var sCmdOpt = new Option("--command", "Filter by command").WithAliases("--cmd"); var sVersionsOpt = new Option("--versions", "Show versions"); var sManifestsOpt = new Option("--manifests", "Return show-style manifests"); foreach (var o in new Option[] { sqOpt, sidOpt, snOpt, smOpt, ssOpt, seOpt, scOpt, sTagOpt, sCmdOpt, sVersionsOpt, sManifestsOpt }) @@ -56,7 +40,7 @@ searchCommand.SetHandler((ctx) => { - var output = GetOutputFormat(ctx.ParseResult.GetValueForOption(outputOption)); + var output = Output.GetFormat(ctx.ParseResult.GetValueForOption(outputOption)); var query = new PackageQuery { Query = ctx.ParseResult.GetValueForArgument(sqArg) ?? ctx.ParseResult.GetValueForOption(sqOpt), @@ -78,31 +62,31 @@ if (ctx.ParseResult.GetValueForOption(sVersionsOpt)) throw new InvalidOperationException("--manifests cannot be combined with --versions"); - WriteStructuredOutput(repo.SearchManifests(query), output); + Output.WriteStructuredOutput(repo.SearchManifests(query), output); } else if (ctx.ParseResult.GetValueForOption(sVersionsOpt)) { var result = repo.SearchVersions(query); - if (output != OutputFormat.Text) WriteStructuredOutput(result, output); - else PrintVersions(result); + if (output != OutputFormat.Text) Output.WriteStructuredOutput(result, output); + else Print.Versions(result); } else { var result = repo.Search(query); - if (output != OutputFormat.Text) WriteStructuredOutput(result, output); - else PrintSearch(result); + if (output != OutputFormat.Text) Output.WriteStructuredOutput(result, output); + else Print.Search(result); } }); // ── Show command ── var showCommand = new Command("show", "Show package info"); -var shArg = new Argument("query", () => null, "Package query"); -var shqOpt = QueryArg(); var shidOpt = IdOpt(); var shnOpt = NameOpt(); var shmOpt = MonikerOpt(); -var shsOpt = SourceOpt(); var sheOpt = ExactOpt(); var shvOpt = VersionOpt(); +var shArg = QueryArguments.Package; +var shqOpt = CommonOptions.Query; var shidOpt = CommonOptions.Id; var shnOpt = CommonOptions.Name; var shmOpt = CommonOptions.Moniker; +var shsOpt = CommonOptions.Source; var sheOpt = CommonOptions.Exact; var shvOpt = CommonOptions.Version; var shVerOpt = new Option("--versions", "Show available versions"); var shLocaleOpt = new Option("--locale", "Installer locale"); var shTypeOpt = new Option("--installer-type", "Installer type"); -var shArchOpt = new Option("--architecture", "Architecture"); shArchOpt.AddAlias("-a"); +var shArchOpt = new Option("--architecture", "Architecture").WithAliases("-a"); var shScopeOpt = new Option("--scope", "Install scope"); foreach (var o in new Option[] { shqOpt, shidOpt, shnOpt, shmOpt, shsOpt, sheOpt, shvOpt, shVerOpt, shLocaleOpt, shTypeOpt, shArchOpt, shScopeOpt }) showCommand.AddOption(o); @@ -110,7 +94,7 @@ showCommand.SetHandler((ctx) => { - var output = GetOutputFormat(ctx.ParseResult.GetValueForOption(outputOption)); + var output = Output.GetFormat(ctx.ParseResult.GetValueForOption(outputOption)); var query = new PackageQuery { Query = ctx.ParseResult.GetValueForArgument(shArg) ?? ctx.ParseResult.GetValueForOption(shqOpt), @@ -130,28 +114,27 @@ if (ctx.ParseResult.GetValueForOption(shVerOpt)) { var result = repo.ShowVersions(query); - if (output != OutputFormat.Text) WriteStructuredOutput(result, output); - else PrintVersions(result); + if (output != OutputFormat.Text) Output.WriteStructuredOutput(result, output); + else Print.Versions(result); } else { var result = repo.Show(query); - if (output != OutputFormat.Text) WriteManifestStructuredOutput(result.ToSerializableManifest(), output); - else PrintShow(result); + if (output != OutputFormat.Text) Output.WriteManifestStructuredOutput(result.ToSerializableManifest(), output); + else Print.Show(result); } }); // ── List command ── -var listCommand = new Command("list", "List installed packages"); -listCommand.AddAlias("ls"); -var lArg = new Argument("query", () => null, "Package query"); -var lqOpt = QueryArg(); var lidOpt = IdOpt(); var lnOpt = NameOpt(); var lmOpt = MonikerOpt(); -var lsOpt = SourceOpt(); var leOpt = ExactOpt(); var lcOpt = CountOpt(); +var listCommand = new Command("list", "List installed packages").WithAliases("ls"); +var lArg = QueryArguments.Package; +var lqOpt = CommonOptions.Query; var lidOpt = CommonOptions.Id; var lnOpt = CommonOptions.Name; var lmOpt = CommonOptions.Moniker; +var lsOpt = CommonOptions.Source; var leOpt = CommonOptions.Exact; var lcOpt = CommonOptions.Count; var lTagOpt = new Option("--tag", "Filter by tag"); -var lCmdOpt = new Option("--command", "Filter by command"); lCmdOpt.AddAlias("--cmd"); +var lCmdOpt = new Option("--command", "Filter by command").WithAliases("--cmd"); var lScopeOpt = new Option("--scope", "Install scope"); var lUpgradeOpt = new Option("--upgrade-available", "Show upgradeable only"); -var lUnknownOpt = new Option("--include-unknown", "Include unknown versions"); lUnknownOpt.AddAlias("-u"); +var lUnknownOpt = new Option("--include-unknown", "Include unknown versions").WithAliases("-u"); var lPinnedOpt = new Option("--include-pinned", "Include pinned packages"); var lDetailsOpt = new Option("--details", "Show details"); foreach (var o in new Option[] { lqOpt, lidOpt, lnOpt, lmOpt, lsOpt, leOpt, lcOpt, lTagOpt, lCmdOpt, lScopeOpt, lUpgradeOpt, lUnknownOpt, lPinnedOpt, lDetailsOpt }) @@ -160,7 +143,7 @@ listCommand.SetHandler((ctx) => { - var output = GetOutputFormat(ctx.ParseResult.GetValueForOption(outputOption)); + var output = Output.GetFormat(ctx.ParseResult.GetValueForOption(outputOption)); var details = ctx.ParseResult.GetValueForOption(lDetailsOpt); var upgrade = ctx.ParseResult.GetValueForOption(lUpgradeOpt); var query = new ListQuery @@ -182,45 +165,44 @@ using var repo = Repository.Open(); var result = repo.List(query); - if (output != OutputFormat.Text) WriteStructuredOutput(result, output); - else PrintListResult(result, details, upgrade); + if (output != OutputFormat.Text) Output.WriteStructuredOutput(result, output); + else Print.ListResult(result, details, upgrade); }); // ── Upgrade command ── -var upgradeCommand = new Command("upgrade", "Upgrade packages"); -upgradeCommand.AddAlias("update"); -var uArg = new Argument("query", () => null, "Package query"); -var uqOpt = QueryArg(); var uidOpt = IdOpt(); var unOpt = NameOpt(); var umOpt = MonikerOpt(); -var usOpt = SourceOpt(); var ueOpt = ExactOpt(); var ucOpt = CountOpt(); var uvOpt = VersionOpt(); -var uManifestOpt = new Option("--manifest", "Local manifest file or directory"); uManifestOpt.AddAlias("-m"); +var upgradeCommand = new Command("upgrade", "Upgrade packages").WithAliases("update"); +var uArg = QueryArguments.Package; +var uqOpt = CommonOptions.Query; var uidOpt = CommonOptions.Id; var unOpt = CommonOptions.Name; var umOpt = CommonOptions.Moniker; +var usOpt = CommonOptions.Source; var ueOpt = CommonOptions.Exact; var ucOpt = CommonOptions.Count; var uvOpt = CommonOptions.Version; +var uManifestOpt = new Option("--manifest", "Local manifest file or directory").WithAliases("-m"); var uLocaleOpt = new Option("--locale", "Installer locale"); var uTypeOpt = new Option("--installer-type", "Installer type"); -var uArchOpt = new Option("--architecture", "Architecture"); uArchOpt.AddAlias("-a"); +var uArchOpt = new Option("--architecture", "Architecture").WithAliases("-a"); var uPlatformOpt = new Option("--platform", "Target platform"); var uOsVersionOpt = new Option("--os-version", "Target OS version"); var uScopeOpt = new Option("--scope", "Install scope"); -var uUnknownOpt = new Option("--include-unknown", "Include unknown"); uUnknownOpt.AddAlias("-u"); +var uUnknownOpt = new Option("--include-unknown", "Include unknown").WithAliases("-u"); var uPinnedOpt = new Option("--include-pinned", "Include pinned"); -var uAllOpt = new Option("--all", "Upgrade all"); uAllOpt.AddAlias("-r"); uAllOpt.AddAlias("--recurse"); +var uAllOpt = new Option("--all", "Upgrade all").WithAliases("-r", "--recurse"); var uLogOpt = new Option("--log", "Installer log path"); var uCustomOpt = new Option("--custom", "Additional installer switches"); var uOverrideOpt = new Option("--override", "Override installer arguments"); -var uLocationOpt = new Option("--location", "Install location"); uLocationOpt.AddAlias("-l"); +var uLocationOpt = new Option("--location", "Install location").WithAliases("-l"); var uIgnoreSecurityHashOpt = new Option("--ignore-security-hash", "Ignore installer hash mismatches"); var uSkipDependenciesOpt = new Option("--skip-dependencies", "Skip package dependencies"); var uDependencySourceOpt = new Option("--dependency-source", "Source to use when resolving dependencies"); var uAcceptPkgAgreementsOpt = new Option("--accept-package-agreements", "Accept package agreements"); var uForceOpt = new Option("--force", "Force install behavior"); var uUninstallPreviousOpt = new Option("--uninstall-previous", "Uninstall previous versions before installing"); -var uSilentOpt = new Option("--silent", "Silent install"); uSilentOpt.AddAlias("-h"); -var uInteractiveOpt = new Option("--interactive", "Interactive install"); uInteractiveOpt.AddAlias("-i"); +var uSilentOpt = new Option("--silent", "Silent install").WithAliases("-h"); +var uInteractiveOpt = new Option("--interactive", "Interactive install").WithAliases("-i"); foreach (var o in new Option[] { uqOpt, uidOpt, unOpt, umOpt, usOpt, ueOpt, ucOpt, uvOpt, uManifestOpt, uLocaleOpt, uTypeOpt, uArchOpt, uPlatformOpt, uOsVersionOpt, uScopeOpt, uUnknownOpt, uPinnedOpt, uAllOpt, uLogOpt, uCustomOpt, uOverrideOpt, uLocationOpt, uIgnoreSecurityHashOpt, uSkipDependenciesOpt, uDependencySourceOpt, uAcceptPkgAgreementsOpt, uForceOpt, uUninstallPreviousOpt, uSilentOpt, uInteractiveOpt }) upgradeCommand.AddOption(o); upgradeCommand.AddArgument(uArg); upgradeCommand.SetHandler((ctx) => { - var output = GetOutputFormat(ctx.ParseResult.GetValueForOption(outputOption)); + var output = Output.GetFormat(ctx.ParseResult.GetValueForOption(outputOption)); var manifestPath = ctx.ParseResult.GetValueForOption(uManifestOpt); var interactive = ctx.ParseResult.GetValueForOption(uInteractiveOpt); var silent = ctx.ParseResult.GetValueForOption(uSilentOpt); @@ -276,14 +258,14 @@ using var repo = Repository.Open(); if (doInstall && !OperatingSystem.IsWindows()) { - PrintWarnings([UpgradeUnsupportedWarning]); + Print.Warnings([Consts.UpgradeUnsupportedWarning]); Console.WriteLine("No changes were made."); return; } var result = repo.List(query); var mode = interactive ? InstallerMode.Interactive : silent ? InstallerMode.Silent : InstallerMode.SilentWithProgress; - var baseInstallRequest = CreateInstallRequest( + var baseInstallRequest = RequestCreator.Install( installQuery, manifestPath, mode, @@ -303,13 +285,13 @@ if (!doInstall) { - if (output != OutputFormat.Text) WriteStructuredOutput(result, output); - else PrintListResult(result, false, true); + if (output != OutputFormat.Text) Output.WriteStructuredOutput(result, output); + else Print.ListResult(result, false, true); } else if (!string.IsNullOrWhiteSpace(manifestPath)) { var installResult = repo.Install(baseInstallRequest); - PrintPackageActionResult(installResult, "upgrade", "upgraded"); + Print.PackageActionResult(installResult, "upgrade", "upgraded"); } else { @@ -327,7 +309,7 @@ Console.WriteLine($"Upgrading {m.Id} from {m.InstalledVersion} to {m.AvailableVersion ?? "?"} ..."); try { - var pin = FindMatchingPin(m, pins); + var pin = Pin.FindMatching(m, pins); if (pin?.PinType == PinType.Blocking) { Console.WriteLine($" Package is blocked by pin {pin.Version}; remove the pin before upgrading."); @@ -351,7 +333,7 @@ }, ManifestPath = null, }); - PrintWarnings(r.Warnings); + Print.Warnings(r.Warnings); Console.WriteLine(r.NoOp ? $" No changes were made for {m.Id}" : r.Success @@ -380,25 +362,23 @@ var sourceAddCmd = new Command("add", "Add source"); var saNameArg = new Argument("name", () => null, "Source name"); var saArgArg = new Argument("arg", () => null, "Source URL"); -var saNameOpt = new Option("--name", "Source name"); saNameOpt.AddAlias("-n"); -var saArgOpt = new Option("--arg", "Source URL"); saArgOpt.AddAlias("-a"); -var saTypeOpt = new Option("--type", "Source type"); saTypeOpt.AddAlias("-t"); +var saNameOpt = new Option("--name", "Source name").WithAliases("-n"); +var saArgOpt = new Option("--arg", "Source URL").WithAliases("-a"); +var saTypeOpt = new Option("--type", "Source type").WithAliases("-t"); var saTrustLevelOpt = new Option("--trust-level", "Source trust level"); var saExplicitOpt = new Option("--explicit", "Exclude source from discovery unless specified"); sourceAddCmd.AddArgument(saNameArg); sourceAddCmd.AddArgument(saArgArg); foreach (var o in new Option[] { saNameOpt, saArgOpt, saTypeOpt, saTrustLevelOpt, saExplicitOpt }) sourceAddCmd.AddOption(o); -var sourceEditCmd = new Command("edit", "Edit source"); -sourceEditCmd.AddAlias("config"); -sourceEditCmd.AddAlias("set"); -var seNameOpt = new Option("--name", "Source name"); seNameOpt.AddAlias("-n"); -var seExplicitOpt = new Option("--explicit", "Excludes a source from discovery (true or false)"); seExplicitOpt.AddAlias("-e"); +var sourceEditCmd = new Command("edit", "Edit source").WithAliases("config", "set"); +var seNameOpt = new Option("--name", "Source name").WithAliases("-n"); +var seExplicitOpt = new Option("--explicit", "Excludes a source from discovery (true or false)").WithAliases("-e"); sourceEditCmd.AddOption(seNameOpt); sourceEditCmd.AddOption(seExplicitOpt); var sourceRemoveCmd = new Command("remove", "Remove source"); var srNameArg = new Argument("name", "Source name"); sourceRemoveCmd.AddArgument(srNameArg); var sourceResetCmd = new Command("reset", "Reset sources"); -var srNameOpt = new Option("--name", "Source name"); srNameOpt.AddAlias("-n"); +var srNameOpt = new Option("--name", "Source name").WithAliases("-n"); var srForceOpt = new Option("--force", "Force reset"); sourceResetCmd.AddOption(srNameOpt); sourceResetCmd.AddOption(srForceOpt); @@ -413,7 +393,7 @@ sourceListCmd.SetHandler(() => { using var repo = Repository.Open(); - PrintSources(repo.ListSources()); + Print.Sources(repo.ListSources()); }); sourceUpdateCmd.SetHandler((source) => @@ -428,25 +408,25 @@ using var repo = Repository.Open(); var sources = repo.ListSources().Select(s => new { - Name = s.Name, - Type = FormatSourceType(s.Kind), - Arg = s.Arg, + s.Name, + Type = Source.FormatType(s.Kind), + s.Arg, Data = s.Identifier, - Identifier = s.Identifier, - TrustLevel = s.TrustLevel, - Explicit = s.Explicit, - Priority = s.Priority, + s.Identifier, + s.TrustLevel, + s.Explicit, + s.Priority, }); Console.WriteLine(JsonSerializer.Serialize(new { Sources = sources }, JsonOpts)); }); sourceAddCmd.SetHandler((ctx) => { - var name = ResolveSourceAddValue( + var name = Source.ResolveAddValue( ctx.ParseResult.GetValueForArgument(saNameArg), ctx.ParseResult.GetValueForOption(saNameOpt), "name"); - var arg = ResolveSourceAddValue( + var arg = Source.ResolveAddValue( ctx.ParseResult.GetValueForArgument(saArgArg), ctx.ParseResult.GetValueForOption(saArgOpt), "argument"); @@ -454,7 +434,7 @@ var trustLevel = ctx.ParseResult.GetValueForOption(saTrustLevelOpt); var explicitSource = ctx.ParseResult.GetValueForOption(saExplicitOpt); using var repo = Repository.Open(); - var kind = ParseSourceKind(type); + var kind = Source.ParseKind(type); repo.AddSource(name, arg, kind, trustLevel ?? "None", explicitSource); Console.WriteLine("Done"); }); @@ -494,15 +474,15 @@ // ── Cache warm ── var cacheCommand = new Command("cache", "Cache management"); var cacheWarmCmd = new Command("warm", "Warm manifest cache"); -var cwArg = new Argument("query", () => null, "Package query"); -var cwqOpt = QueryArg(); var cwidOpt = IdOpt(); var cwsOpt = SourceOpt(); var cweOpt = ExactOpt(); +var cwArg = QueryArguments.Package; +var cwqOpt = CommonOptions.Query; var cwidOpt = CommonOptions.Id; var cwsOpt = CommonOptions.Source; var cweOpt = CommonOptions.Exact; cacheWarmCmd.AddArgument(cwArg); foreach (var o in new Option[] { cwqOpt, cwidOpt, cwsOpt, cweOpt }) cacheWarmCmd.AddOption(o); cacheCommand.AddCommand(cacheWarmCmd); cacheWarmCmd.SetHandler((ctx) => { - var output = GetOutputFormat(ctx.ParseResult.GetValueForOption(outputOption)); + var output = Output.GetFormat(ctx.ParseResult.GetValueForOption(outputOption)); var query = new PackageQuery { Query = ctx.ParseResult.GetValueForArgument(cwArg) ?? ctx.ParseResult.GetValueForOption(cwqOpt), @@ -512,7 +492,7 @@ }; using var repo = Repository.Open(); var result = repo.WarmCache(query); - if (output != OutputFormat.Text) WriteStructuredOutput(result, output); + if (output != OutputFormat.Text) Output.WriteStructuredOutput(result, output); else { Console.WriteLine($"Warmed cache for {result.Package.Name} [{result.Package.Id}]"); @@ -552,8 +532,8 @@ // ── Export ── var exportCommand = new Command("export", "Export installed packages"); -var exOutputOpt = new Option("--output", "Output file") { IsRequired = true }; exOutputOpt.AddAlias("-o"); -var exSourceOpt = SourceOpt(); +var exOutputOpt = new Option("--output", "Output file") { IsRequired = true }.WithAliases("-o"); +var exSourceOpt = CommonOptions.Source; var exVersionsOpt = new Option("--include-versions", "Include versions"); exportCommand.AddOption(exOutputOpt); exportCommand.AddOption(exSourceOpt); exportCommand.AddOption(exVersionsOpt); @@ -589,11 +569,10 @@ var errInputArg = new Argument("input", "Error code"); errorCommand.AddArgument(errInputArg); -errorCommand.SetHandler(PrintErrorLookup, errInputArg); +errorCommand.SetHandler(Print.ErrorLookup, errInputArg); // ── Settings ── -var settingsCommand = new Command("settings", "Settings"); -settingsCommand.AddAlias("config"); +var settingsCommand = new Command("settings", "Settings").WithAliases("config"); var settingsEnableOpt = new Option("--enable", "Enables the specific administrator setting"); var settingsDisableOpt = new Option("--disable", "Disables the specific administrator setting"); settingsCommand.AddOption(settingsEnableOpt); @@ -606,9 +585,7 @@ settingsSetCmd.AddOption(settingsSetValueOpt); var settingsResetCmd = new Command("reset", "Resets an admin setting to its default value."); var settingsResetNameOpt = new Option("--setting", "Name of the setting to modify"); -var settingsResetAllOpt = new Option("--recurse", "Resets all admin settings"); -settingsResetAllOpt.AddAlias("-r"); -settingsResetAllOpt.AddAlias("--all"); +var settingsResetAllOpt = new Option("--recurse", "Resets all admin settings").WithAliases("-r", "--all"); settingsResetCmd.AddOption(settingsResetNameOpt); settingsResetCmd.AddOption(settingsResetAllOpt); settingsCommand.AddCommand(settingsExportCmd); @@ -637,13 +614,13 @@ return; } - WriteJsonNode(repo.GetUserSettings(), GetOutputFormat(ctx.ParseResult.GetValueForOption(outputOption))); + Json.WriteNode(repo.GetUserSettings(), Output.GetFormat(ctx.ParseResult.GetValueForOption(outputOption))); }); settingsExportCmd.SetHandler((ctx) => { using var repo = Repository.Open(); - WriteJsonNode(repo.GetUserSettings(), GetOutputFormat(ctx.ParseResult.GetValueForOption(outputOption))); + Json.WriteNode(repo.GetUserSettings(), Output.GetFormat(ctx.ParseResult.GetValueForOption(outputOption))); }); settingsSetCmd.SetHandler((ctx) => @@ -653,11 +630,11 @@ ?? throw new InvalidOperationException("settings set requires --setting."); var rawValue = ctx.ParseResult.GetValueForOption(settingsSetValueOpt) ?? throw new InvalidOperationException("settings set requires --value."); - var value = ParseBooleanSettingValue(rawValue); + var value = rawValue.BooleanSetting; repo.SetAdminSetting(name, value); - var output = GetOutputFormat(ctx.ParseResult.GetValueForOption(outputOption)); + var output = Output.GetFormat(ctx.ParseResult.GetValueForOption(outputOption)); if (output != OutputFormat.Text) - WriteJsonNode(repo.GetAdminSettings(), output); + Json.WriteNode(repo.GetAdminSettings(), output); else Console.WriteLine($"Set admin setting '{name}' to {value.ToString().ToLowerInvariant()}."); }); @@ -671,9 +648,9 @@ throw new InvalidOperationException("settings reset requires --setting or --all."); repo.ResetAdminSetting(name, resetAll); - var output = GetOutputFormat(ctx.ParseResult.GetValueForOption(outputOption)); + var output = Output.GetFormat(ctx.ParseResult.GetValueForOption(outputOption)); if (output != OutputFormat.Text) - WriteJsonNode(repo.GetAdminSettings(), output); + Json.WriteNode(repo.GetAdminSettings(), output); else if (resetAll) Console.WriteLine("Reset all admin settings."); else @@ -704,15 +681,14 @@ }, valManifestArg); // ── Download ── -var downloadCommand = new Command("download", "Download installer"); -downloadCommand.AddAlias("dl"); -var dlArg = new Argument("query", () => null, "Package query"); -var dlqOpt = QueryArg(); var dlidOpt = IdOpt(); var dlnOpt = NameOpt(); var dlmOpt = MonikerOpt(); var dlsOpt = SourceOpt(); var dleOpt = ExactOpt(); var dlvOpt = VersionOpt(); -var dlDirOpt = new Option("--download-directory", "Download directory"); dlDirOpt.AddAlias("-d"); -var dlManifestOpt = new Option("--manifest", "Local manifest file or directory"); dlManifestOpt.AddAlias("-m"); +var downloadCommand = new Command("download", "Download installer").WithAliases("dl"); +var dlArg = QueryArguments.Package; +var dlqOpt = CommonOptions.Query; var dlidOpt = CommonOptions.Id; var dlnOpt = CommonOptions.Name; var dlmOpt = CommonOptions.Moniker; var dlsOpt = CommonOptions.Source; var dleOpt = CommonOptions.Exact; var dlvOpt = CommonOptions.Version; +var dlDirOpt = new Option("--download-directory", "Download directory").WithAliases("-d"); +var dlManifestOpt = new Option("--manifest", "Local manifest file or directory").WithAliases("-m"); var dlLocaleOpt = new Option("--locale", "Installer locale"); var dlTypeOpt = new Option("--installer-type", "Installer type"); -var dlArchOpt = new Option("--architecture", "Architecture"); dlArchOpt.AddAlias("-a"); +var dlArchOpt = new Option("--architecture", "Architecture").WithAliases("-a"); var dlPlatformOpt = new Option("--platform", "Target platform"); var dlOsVersionOpt = new Option("--os-version", "Target OS version"); var dlScopeOpt = new Option("--scope", "Install scope"); @@ -743,7 +719,7 @@ var dir = ctx.ParseResult.GetValueForOption(dlDirOpt) ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads"); using var repo = Repository.Open(); - var request = CreateInstallRequest( + var request = RequestCreator.Install( query, ctx.ParseResult.GetValueForOption(dlManifestOpt), InstallerMode.SilentWithProgress, @@ -769,18 +745,18 @@ var pinCommand = new Command("pin", "Manage pins"); var pinListCmd = new Command("list", "List pins"); var pinAddCmd = new Command("add", "Add pin"); -var plArg = new Argument("query", () => null, "Package query"); -var plqOpt = QueryArg(); var plIdOpt = IdOpt(); var plNameOpt = NameOpt(); var plMonikerOpt = MonikerOpt(); var plSourceOpt = SourceOpt(); var plExactOpt = ExactOpt(); +var plArg = QueryArguments.Package; +var plqOpt = CommonOptions.Query; var plIdOpt = CommonOptions.Id; var plNameOpt = CommonOptions.Name; var plMonikerOpt = CommonOptions.Moniker; var plSourceOpt = CommonOptions.Source; var plExactOpt = CommonOptions.Exact; var plTagOpt = new Option("--tag", "Filter by tag"); -var plCmdOpt = new Option("--command", "Filter by command"); plCmdOpt.AddAlias("--cmd"); +var plCmdOpt = new Option("--command", "Filter by command").WithAliases("--cmd"); pinListCmd.AddArgument(plArg); foreach (var o in new Option[] { plqOpt, plIdOpt, plNameOpt, plMonikerOpt, plSourceOpt, plExactOpt, plTagOpt, plCmdOpt }) pinListCmd.AddOption(o); -var paArg = new Argument("query", () => null, "Package query"); -var paqOpt = QueryArg(); var paIdOpt = IdOpt(); var paNameOpt = NameOpt(); var paMonikerOpt = MonikerOpt(); var paSourceOpt = SourceOpt(); var paExactOpt = ExactOpt(); +var paArg = QueryArguments.Package; +var paqOpt = CommonOptions.Query; var paIdOpt = CommonOptions.Id; var paNameOpt = CommonOptions.Name; var paMonikerOpt = CommonOptions.Moniker; var paSourceOpt = CommonOptions.Source; var paExactOpt = CommonOptions.Exact; var paTagOpt = new Option("--tag", "Filter by tag"); -var paCmdOpt = new Option("--command", "Filter by command"); paCmdOpt.AddAlias("--cmd"); -var paVersionOpt = new Option("--version", "Pin version"); paVersionOpt.AddAlias("-v"); +var paCmdOpt = new Option("--command", "Filter by command").WithAliases("--cmd"); +var paVersionOpt = new Option("--version", "Pin version").WithAliases("-v"); var paBlockingOpt = new Option("--blocking", "Blocking pin"); var paInstalledOpt = new Option("--installed", "Pin a specific installed version"); var paForceOpt = new Option("--force", "Replace an existing pin"); @@ -788,17 +764,17 @@ foreach (var o in new Option[] { paqOpt, paIdOpt, paNameOpt, paMonikerOpt, paSourceOpt, paExactOpt, paTagOpt, paCmdOpt, paVersionOpt, paBlockingOpt, paInstalledOpt, paForceOpt }) pinAddCmd.AddOption(o); var pinRemoveCmd = new Command("remove", "Remove pin"); -var prArg = new Argument("query", () => null, "Package query"); -var prqOpt = QueryArg(); var prIdOpt = IdOpt(); var prNameOpt = NameOpt(); var prMonikerOpt = MonikerOpt(); var prSourceOpt = SourceOpt(); var prExactOpt = ExactOpt(); +var prArg = QueryArguments.Package; +var prqOpt = CommonOptions.Query; var prIdOpt = CommonOptions.Id; var prNameOpt = CommonOptions.Name; var prMonikerOpt = CommonOptions.Moniker; var prSourceOpt = CommonOptions.Source; var prExactOpt = CommonOptions.Exact; var prTagOpt = new Option("--tag", "Filter by tag"); -var prCmdOpt = new Option("--command", "Filter by command"); prCmdOpt.AddAlias("--cmd"); +var prCmdOpt = new Option("--command", "Filter by command").WithAliases("--cmd"); var prInstalledOpt = new Option("--installed", "Remove the pin for a specific installed version"); pinRemoveCmd.AddArgument(prArg); foreach (var o in new Option[] { prqOpt, prIdOpt, prNameOpt, prMonikerOpt, prSourceOpt, prExactOpt, prTagOpt, prCmdOpt, prInstalledOpt }) pinRemoveCmd.AddOption(o); var pinResetCmd = new Command("reset", "Reset pins"); var prForceOpt = new Option("--force", "Force reset"); -var prResetSourceOpt = SourceOpt(); +var prResetSourceOpt = CommonOptions.Source; pinResetCmd.AddOption(prForceOpt); pinResetCmd.AddOption(prResetSourceOpt); pinCommand.AddCommand(pinListCmd); pinCommand.AddCommand(pinAddCmd); pinCommand.AddCommand(pinRemoveCmd); pinCommand.AddCommand(pinResetCmd); @@ -806,7 +782,7 @@ pinListCmd.SetHandler((ctx) => { using var repo = Repository.Open(); - var query = CreatePinQuery( + var query = PinQuery.Create( ctx.ParseResult.GetValueForArgument(plArg) ?? ctx.ParseResult.GetValueForOption(plqOpt), ctx.ParseResult.GetValueForOption(plIdOpt), ctx.ParseResult.GetValueForOption(plNameOpt), @@ -815,7 +791,7 @@ ctx.ParseResult.GetValueForOption(plCmdOpt), ctx.ParseResult.GetValueForOption(plSourceOpt), ctx.ParseResult.GetValueForOption(plExactOpt)); - var pins = FilterPins(repo, query); + var pins = Pin.Filter(repo, query); if (pins.Count == 0) { Console.WriteLine("No pins found."); return; } Console.WriteLine($"{"Package Id",-40} {"Version",-20} {"Source",-15} Pin Type"); Console.WriteLine(new string('-', 85)); @@ -825,7 +801,7 @@ pinAddCmd.SetHandler((ctx) => { using var repo = Repository.Open(); - var query = CreatePinQuery( + var query = PinQuery.Create( ctx.ParseResult.GetValueForArgument(paArg) ?? ctx.ParseResult.GetValueForOption(paqOpt), ctx.ParseResult.GetValueForOption(paIdOpt), ctx.ParseResult.GetValueForOption(paNameOpt), @@ -834,7 +810,7 @@ ctx.ParseResult.GetValueForOption(paCmdOpt), ctx.ParseResult.GetValueForOption(paSourceOpt), ctx.ParseResult.GetValueForOption(paExactOpt)); - EnsurePinQueryProvided(query, "pin add"); + PinQuery.EnsureProvided(query, "pin add"); var blocking = ctx.ParseResult.GetValueForOption(paBlockingOpt); var installed = ctx.ParseResult.GetValueForOption(paInstalledOpt); @@ -846,14 +822,14 @@ string? resolvedVersion; if (installed) { - var target = ResolveSingleInstalledPinTarget(repo, query); + var target = PinTarget.ResolveSingleInstalled(repo, query); packageId = target.Id; sourceId = target.SourceName ?? query.Source ?? ""; resolvedVersion = target.InstalledVersion; } else { - var target = ResolveSingleAvailablePinTarget(repo, query); + var target = PinTarget.ResolveSingleAvailable(repo, query); packageId = target.Id; sourceId = target.SourceName; resolvedVersion = target.Version; @@ -874,7 +850,7 @@ pinRemoveCmd.SetHandler((ctx) => { using var repo = Repository.Open(); - var query = CreatePinQuery( + var query = PinQuery.Create( ctx.ParseResult.GetValueForArgument(prArg) ?? ctx.ParseResult.GetValueForOption(prqOpt), ctx.ParseResult.GetValueForOption(prIdOpt), ctx.ParseResult.GetValueForOption(prNameOpt), @@ -883,18 +859,18 @@ ctx.ParseResult.GetValueForOption(prCmdOpt), ctx.ParseResult.GetValueForOption(prSourceOpt), ctx.ParseResult.GetValueForOption(prExactOpt)); - EnsurePinQueryProvided(query, "pin remove"); + PinQuery.EnsureProvided(query, "pin remove"); PinRecord? pin; if (ctx.ParseResult.GetValueForOption(prInstalledOpt)) { - var target = ResolveSingleInstalledPinTarget(repo, query); + var target = PinTarget.ResolveSingleInstalled(repo, query); pin = repo.ListPins(target.SourceName ?? query.Source ?? "") .FirstOrDefault(candidate => candidate.PackageId.Equals(target.Id, StringComparison.OrdinalIgnoreCase)); } else { - var pins = FilterPins(repo, query); + var pins = Pin.Filter(repo, query); if (pins.Count == 0) { Console.WriteLine("No pin found matching the query."); @@ -925,34 +901,33 @@ }, prForceOpt, prResetSourceOpt); // ── Install ── -var installCommand = new Command("install", "Install a package"); -installCommand.AddAlias("add"); -var iArg = new Argument("query", () => null, "Package query"); -var iqOpt = QueryArg(); var iidOpt = IdOpt(); var inameOpt = NameOpt(); var imonikerOpt = MonikerOpt(); var isrcOpt = SourceOpt(); -var ieOpt = ExactOpt(); var ivOpt = VersionOpt(); +var installCommand = new Command("install", "Install a package").WithAliases("add"); +var iArg = QueryArguments.Package; +var iqOpt = CommonOptions.Query; var iidOpt = CommonOptions.Id; var inameOpt = CommonOptions.Name; var imonikerOpt = CommonOptions.Moniker; var isrcOpt = CommonOptions.Source; +var ieOpt = CommonOptions.Exact; var ivOpt = CommonOptions.Version; var ichannelOpt = new Option("--channel", "Channel"); var ilocaleOpt = new Option("--locale", "Installer locale"); var itypeOpt = new Option("--installer-type", "Installer type"); -var iarchOpt = new Option("--architecture", "Architecture"); iarchOpt.AddAlias("-a"); +var iarchOpt = new Option("--architecture", "Architecture").WithAliases("-a"); var iplatformOpt = new Option("--platform", "Target platform"); var iosVersionOpt = new Option("--os-version", "Target OS version"); var iscopeOpt = new Option("--scope", "Install scope"); -var imanifestOpt = new Option("--manifest", "Local manifest file or directory"); imanifestOpt.AddAlias("-m"); +var imanifestOpt = new Option("--manifest", "Local manifest file or directory").WithAliases("-m"); var ilogOpt = new Option("--log", "Installer log path"); var icustomOpt = new Option("--custom", "Additional installer switches"); var ioverrideOpt = new Option("--override", "Override installer arguments"); -var ilocationOpt = new Option("--location", "Install location"); ilocationOpt.AddAlias("-l"); +var ilocationOpt = new Option("--location", "Install location").WithAliases("-l"); var iignoreSecurityHashOpt = new Option("--ignore-security-hash", "Ignore installer hash mismatches"); var iskipDepsOpt = new Option("--skip-dependencies", "Skip package dependencies"); -var idepsOnlyOpt = new Option("--dependencies-only", "Install dependencies only"); idepsOnlyOpt.AddAlias("--dependencies"); +var idepsOnlyOpt = new Option("--dependencies-only", "Install dependencies only").WithAliases("--dependencies"); var idependencySourceOpt = new Option("--dependency-source", "Source to use when resolving dependencies"); var iacceptPkgAgreementsOpt = new Option("--accept-package-agreements", "Accept package agreements"); var inoUpgradeOpt = new Option("--no-upgrade", "Skip upgrade if the package is already installed"); var iforceOpt = new Option("--force", "Force install behavior"); -var irenameOpt = new Option("--rename", "Rename the installer or target payload"); irenameOpt.AddAlias("-r"); +var irenameOpt = new Option("--rename", "Rename the installer or target payload").WithAliases("-r"); var iuninstallPreviousOpt = new Option("--uninstall-previous", "Uninstall previous versions before installing"); -var iSilentOpt = new Option("--silent", "Silent install"); iSilentOpt.AddAlias("-h"); -var iInteractiveOpt = new Option("--interactive", "Interactive install"); iInteractiveOpt.AddAlias("-i"); +var iSilentOpt = new Option("--silent", "Silent install").WithAliases("-h"); +var iInteractiveOpt = new Option("--interactive", "Interactive install").WithAliases("-i"); installCommand.AddArgument(iArg); foreach (var o in new Option[] { iqOpt, iidOpt, inameOpt, imonikerOpt, isrcOpt, ieOpt, ivOpt, ichannelOpt, ilocaleOpt, itypeOpt, iarchOpt, iplatformOpt, iosVersionOpt, iscopeOpt, imanifestOpt, ilogOpt, icustomOpt, ioverrideOpt, ilocationOpt, iignoreSecurityHashOpt, iskipDepsOpt, idepsOnlyOpt, idependencySourceOpt, iacceptPkgAgreementsOpt, inoUpgradeOpt, iforceOpt, irenameOpt, iuninstallPreviousOpt, iSilentOpt, iInteractiveOpt }) installCommand.AddOption(o); @@ -981,7 +956,7 @@ throw new InvalidOperationException("--silent and --interactive cannot be used together."); using var repo = Repository.Open(); var mode = interactive ? InstallerMode.Interactive : silent ? InstallerMode.Silent : InstallerMode.SilentWithProgress; - var result = repo.Install(CreateInstallRequest( + var result = repo.Install(RequestCreator.Install( query, ctx.ParseResult.GetValueForOption(imanifestOpt), mode, @@ -998,26 +973,24 @@ ctx.ParseResult.GetValueForOption(iignoreSecurityHashOpt), ctx.ParseResult.GetValueForOption(idependencySourceOpt), ctx.ParseResult.GetValueForOption(inoUpgradeOpt))); - PrintPackageActionResult(result, "install", "installed"); + Print.PackageActionResult(result, "install", "installed"); }); // ── Uninstall ── -var uninstallCommand = new Command("uninstall", "Uninstall a package"); -uninstallCommand.AddAlias("remove"); -uninstallCommand.AddAlias("rm"); -var uiArg = new Argument("query", () => null, "Package query"); -var uiqOpt = QueryArg(); var uiidOpt = IdOpt(); var uinameOpt = NameOpt(); var uimonikerOpt = MonikerOpt(); var uisOpt = SourceOpt(); -var uieOpt = ExactOpt(); var uivOpt = VersionOpt(); +var uninstallCommand = new Command("uninstall", "Uninstall a package").WithAliases("remove", "rm"); +var uiArg = QueryArguments.Package; +var uiqOpt = CommonOptions.Query; var uiidOpt = CommonOptions.Id; var uinameOpt = CommonOptions.Name; var uimonikerOpt = CommonOptions.Moniker; var uisOpt = CommonOptions.Source; +var uieOpt = CommonOptions.Exact; var uivOpt = CommonOptions.Version; var uiscopeOpt = new Option("--scope", "Install scope"); -var uimanifestOpt = new Option("--manifest", "Local manifest file or directory"); uimanifestOpt.AddAlias("-m"); +var uimanifestOpt = new Option("--manifest", "Local manifest file or directory").WithAliases("-m"); var uiproductCodeOpt = new Option("--product-code", "Installed product code"); -var uiallVersionsOpt = new Option("--all-versions", "Uninstall all matching versions"); uiallVersionsOpt.AddAlias("--all"); -var uiInteractiveOpt = new Option("--interactive", "Interactive uninstall"); uiInteractiveOpt.AddAlias("-i"); +var uiallVersionsOpt = new Option("--all-versions", "Uninstall all matching versions").WithAliases("--all"); +var uiInteractiveOpt = new Option("--interactive", "Interactive uninstall").WithAliases("-i"); var uiForceOpt = new Option("--force", "Force uninstall behavior"); var uiPurgeOpt = new Option("--purge", "Purge portable package contents"); var uiPreserveOpt = new Option("--preserve", "Preserve portable package contents"); var uiLogOpt = new Option("--log", "Uninstaller log path"); -var uiSilentOpt = new Option("--silent", "Silent uninstall"); uiSilentOpt.AddAlias("-h"); +var uiSilentOpt = new Option("--silent", "Silent uninstall").WithAliases("-h"); uninstallCommand.AddArgument(uiArg); foreach (var o in new Option[] { uiqOpt, uiidOpt, uinameOpt, uimonikerOpt, uisOpt, uieOpt, uivOpt, uiscopeOpt, uimanifestOpt, uiproductCodeOpt, uiallVersionsOpt, uiInteractiveOpt, uiForceOpt, uiPurgeOpt, uiPreserveOpt, uiLogOpt, uiSilentOpt }) uninstallCommand.AddOption(o); @@ -1051,26 +1024,25 @@ Preserve = ctx.ParseResult.GetValueForOption(uiPreserveOpt), LogPath = ctx.ParseResult.GetValueForOption(uiLogOpt), }); - PrintPackageActionResult(result, "uninstall", "uninstalled"); + Print.PackageActionResult(result, "uninstall", "uninstalled"); }); // ── Repair ── -var repairCommand = new Command("repair", "Repair a package"); -repairCommand.AddAlias("fix"); -var rArg = new Argument("query", () => null, "Package query"); -var rqOpt = QueryArg(); var ridOpt = IdOpt(); var rnameOpt = NameOpt(); var rmonikerOpt = MonikerOpt(); var rsrcOpt = SourceOpt(); -var reOpt = ExactOpt(); var rvOpt = VersionOpt(); -var rmanifestOpt = new Option("--manifest", "Local manifest file or directory"); rmanifestOpt.AddAlias("-m"); +var repairCommand = new Command("repair", "Repair a package").WithAliases("fix"); +var rArg = QueryArguments.Package; +var rqOpt = CommonOptions.Query; var ridOpt = CommonOptions.Id; var rnameOpt = CommonOptions.Name; var rmonikerOpt = CommonOptions.Moniker; var rsrcOpt = CommonOptions.Source; +var reOpt = CommonOptions.Exact; var rvOpt = CommonOptions.Version; +var rmanifestOpt = new Option("--manifest", "Local manifest file or directory").WithAliases("-m"); var rproductCodeOpt = new Option("--product-code", "Installed product code"); -var rarchOpt = new Option("--architecture", "Architecture"); rarchOpt.AddAlias("-a"); +var rarchOpt = new Option("--architecture", "Architecture").WithAliases("-a"); var rscopeOpt = new Option("--scope", "Install scope"); var rlocaleOpt = new Option("--locale", "Installer locale"); -var rlogOpt = new Option("--log", "Installer log path"); rlogOpt.AddAlias("-o"); +var rlogOpt = new Option("--log", "Installer log path").WithAliases("-o"); var racceptPkgAgreementsOpt = new Option("--accept-package-agreements", "Accept package agreements"); var rignoreSecurityHashOpt = new Option("--ignore-security-hash", "Ignore installer hash mismatches"); var rforceOpt = new Option("--force", "Force repair behavior"); -var rSilentOpt = new Option("--silent", "Silent install"); rSilentOpt.AddAlias("-h"); -var rInteractiveOpt = new Option("--interactive", "Interactive install"); rInteractiveOpt.AddAlias("-i"); +var rSilentOpt = new Option("--silent", "Silent install").WithAliases("-h"); +var rInteractiveOpt = new Option("--interactive", "Interactive install").WithAliases("-i"); repairCommand.AddArgument(rArg); foreach (var o in new Option[] { rqOpt, ridOpt, rnameOpt, rmonikerOpt, rsrcOpt, reOpt, rvOpt, rmanifestOpt, rproductCodeOpt, rarchOpt, rscopeOpt, rlocaleOpt, rlogOpt, racceptPkgAgreementsOpt, rignoreSecurityHashOpt, rforceOpt, rSilentOpt, rInteractiveOpt }) repairCommand.AddOption(o); @@ -1083,7 +1055,7 @@ using var repo = Repository.Open(); var mode = interactive ? InstallerMode.Interactive : silent ? InstallerMode.Silent : InstallerMode.SilentWithProgress; - var result = repo.Repair(CreateRepairRequest( + var result = repo.Repair(RequestCreator.Repair( new PackageQuery { Query = ctx.ParseResult.GetValueForArgument(rArg) ?? ctx.ParseResult.GetValueForOption(rqOpt), @@ -1104,12 +1076,12 @@ ctx.ParseResult.GetValueForOption(racceptPkgAgreementsOpt), ctx.ParseResult.GetValueForOption(rforceOpt), ctx.ParseResult.GetValueForOption(rignoreSecurityHashOpt))); - PrintPackageActionResult(result, "repair", "repaired"); + Print.PackageActionResult(result, "repair", "repaired"); }); // ── Import ── var importCommand = new Command("import", "Import packages"); -var imFileOpt = new Option("--import-file", "Import file") { IsRequired = true }; imFileOpt.AddAlias("-i"); +var imFileOpt = new Option("--import-file", "Import file") { IsRequired = true }.WithAliases("-i"); var imDryRunOpt = new Option("--dry-run", "Dry run only"); var imIgnoreUnavailableOpt = new Option("--ignore-unavailable", "Ignore unavailable packages"); var imIgnoreVersionsOpt = new Option("--ignore-versions", "Ignore package versions in the import file"); @@ -1138,18 +1110,18 @@ foreach (var source in sources) { var sourceName = source.TryGetProperty("SourceDetails", out var sourceDetails) - ? GetJsonString(sourceDetails, "Name") + ? Json.GetString(sourceDetails, "Name") : null; var packages = source.GetProperty("Packages").EnumerateArray().ToList(); foreach (var pkg in packages) { var pkgId = pkg.GetProperty("PackageIdentifier").GetString()!; - var pkgVersion = ignoreVersions ? null : GetJsonString(pkg, "Version"); + var pkgVersion = ignoreVersions ? null : Json.GetString(pkg, "Version"); if (dryRun) { Console.WriteLine($"[dry-run] Would install: {pkgId}"); } - else if (noUpgrade && IsInstalledPackagePresent(repo, pkgId, sourceName)) + else if (noUpgrade && InstalledPackageChecker.IsPresent(repo, pkgId, sourceName)) { Console.WriteLine($"[no-upgrade] Skipping already installed package: {pkgId}"); skipped++; @@ -1159,7 +1131,7 @@ try { Console.Write($"Installing {pkgId}..."); - var result = repo.Install(CreateInstallRequest( + var result = repo.Install(RequestCreator.Install( new PackageQuery { Id = pkgId, @@ -1185,16 +1157,16 @@ if (result.NoOp) { Console.WriteLine(" no-op"); - PrintWarnings(result.Warnings); + Print.Warnings(result.Warnings); skipped++; } else { - PrintWarnings(result.Warnings); + Print.Warnings(result.Warnings); Console.WriteLine(result.Success ? " done" : $" failed (exit {result.ExitCode})"); } } - catch (Exception ex) when (ignoreUnavailable && CanIgnoreUnavailableImportFailure(ex)) + catch (Exception ex) when (ignoreUnavailable && ex.CanIgnoreUnavailableImportFailure) { Console.WriteLine(" unavailable"); Console.Error.WriteLine($"warning: Skipping unavailable package '{pkgId}': {ex.Message}"); @@ -1234,639 +1206,9 @@ { if (ctx.ParseResult.GetValueForOption(infoOption)) { - PrintInfo(); + Print.Info(); return; } }); return rootCommand.Invoke(args); - -// ═══════════════ Output helpers ═══════════════ - -static void PrintInfo() -{ - PrintVersion(); - Console.WriteLine("Pure C# subset of Pinget (portable winget)"); - Console.WriteLine($"Runtime: {System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}"); - Console.WriteLine($"OS: {System.Runtime.InteropServices.RuntimeInformation.OSDescription}"); -} - -static void PrintVersion() => Console.WriteLine($"pinget v{Version}"); - -static OutputFormat GetOutputFormat(string? value) => - value?.ToLowerInvariant() switch - { - "json" => OutputFormat.Json, - "yaml" => OutputFormat.Yaml, - _ => OutputFormat.Text, - }; - -void WriteStructuredOutput(object value, OutputFormat output) -{ - switch (output) - { - case OutputFormat.Json: - if (value is SerializableShowManifest showManifest) - Console.WriteLine(JsonSerializer.Serialize(showManifest, PingetJsonContext.Default.SerializableShowManifest)); - else - Console.WriteLine(JsonSerializer.Serialize(value, JsonOpts)); - break; - case OutputFormat.Yaml: - Console.Write(new SerializerBuilder().Build().Serialize(value)); - break; - default: - throw new InvalidOperationException("Text output should be handled separately."); - } -} - -void WriteManifestStructuredOutput(object value, OutputFormat output) -{ - if (output == OutputFormat.Yaml && value is List> documents) - { - var serializer = new SerializerBuilder().Build(); - foreach (var document in documents) - { - Console.Write("---\n"); - Console.Write(serializer.Serialize(document)); - } - return; - } - - WriteStructuredOutput(value, output); -} - -static void PrintSearch(SearchResponse result) -{ - PrintWarnings(result.Warnings); - if (result.Matches.Count == 0) { Console.WriteLine("No package matched the supplied query."); return; } - - bool showMatch = result.Matches.Any(m => m.MatchCriteria is not null); - if (showMatch) - { - Console.WriteLine("{0,-32} {1,-40} {2,-18} {3,-24} Source", "Name", "Id", "Version", "Match"); - foreach (var m in result.Matches) - { - Console.WriteLine( - "{0,-32} {1,-40} {2,-18} {3,-24} {4}", - Trunc(m.Name, 32), - Trunc(m.Id, 40), - m.Version ?? "Unknown", - Trunc(m.MatchCriteria ?? "", 24), - m.SourceName); - } - } - else - { - Console.WriteLine("{0,-36} {1,-42} {2,-18} Source", "Name", "Id", "Version"); - foreach (var m in result.Matches) - { - Console.WriteLine( - "{0,-36} {1,-42} {2,-18} {3}", - Trunc(m.Name, 36), - Trunc(m.Id, 42), - m.Version ?? "Unknown", - m.SourceName); - } - } - - if (result.Truncated) Console.WriteLine($""); -} - -static void PrintVersions(VersionsResult result) -{ - PrintWarnings(result.Warnings); - Console.WriteLine($"Found {result.Package.Name} [{result.Package.Id}]"); - Console.WriteLine("Version"); - Console.WriteLine(new string('-', 40)); - foreach (var v in result.Versions) - { - Console.Write(v.Version); - if (!string.IsNullOrEmpty(v.Channel)) Console.Write($" [{v.Channel}]"); - Console.WriteLine(); - } -} - -static void PrintShow(ShowResult result) -{ - PrintWarnings(result.Warnings); - Console.WriteLine($"Found {result.Package.Name} [{result.Package.Id}]"); - var m = result.Manifest; - Console.WriteLine($"Version: {m.Version}"); - PrintOpt("Publisher", m.Publisher); - PrintOpt("Publisher Url", m.PublisherUrl); - PrintOpt("Publisher Support Url", m.PublisherSupportUrl); - PrintOpt("Author", m.Author); - PrintOpt("Moniker", m.Moniker); - if (m.Description is not null) - { - Console.WriteLine("Description:"); - foreach (var line in m.Description.Split('\n')) - Console.WriteLine($" {line.TrimEnd()}"); - } - PrintOpt("Homepage", m.PackageUrl); - PrintOpt("License", m.License); - PrintOpt("License Url", m.LicenseUrl); - PrintOpt("Privacy Url", m.PrivacyUrl); - PrintOpt("Copyright", m.Copyright); - PrintOpt("Copyright Url", m.CopyrightUrl); - PrintOpt("Release Notes Url", m.ReleaseNotesUrl); - if (m.Documentation.Count > 0) - { - Console.WriteLine("Documentation:"); - foreach (var doc in m.Documentation) - Console.WriteLine($" {doc.Label ?? "Link"}: {doc.Url}"); - } - - if (result.Manifest.PackageDependencies.Count > 0) - { - Console.Write("Dependencies:"); - Console.WriteLine($" {string.Join(", ", result.Manifest.PackageDependencies)}"); - } - - if (m.Tags.Count > 0) { Console.WriteLine("Tags:"); foreach (var t in m.Tags) Console.WriteLine($" {t}"); } - - if (result.SelectedInstaller is Installer inst) - { - Console.WriteLine("Installer:"); - PrintOpt(" Type", inst.InstallerType); - PrintOpt(" Architecture", inst.Architecture); - PrintOpt(" Locale", inst.Locale); - PrintOpt(" Scope", inst.Scope); - PrintOpt(" Url", inst.Url); - PrintOpt(" Sha256", inst.Sha256); - PrintOpt(" ProductCode", inst.ProductCode); - PrintOpt(" ReleaseDate", inst.ReleaseDate); - } - else if (m.Installers.Count > 0) - { - Console.WriteLine("Installer:"); - Console.WriteLine(" No applicable installer found; see logs for more details."); - } -} - -static void PrintListResult(ListResponse result, bool details, bool upgrade) -{ - PrintWarnings(result.Warnings); - if (result.Matches.Count == 0) { Console.WriteLine("No installed package found matching input criteria."); return; } - - if (details) - { - int total = result.Matches.Count; - for (int idx = 0; idx < total; idx++) - { - var m = result.Matches[idx]; - if (total > 1) - Console.WriteLine($"({idx + 1}/{total}) {m.Name} [{m.Id}]"); - else - Console.WriteLine($"{m.Name} [{m.Id}]"); - PrintOpt("Version", m.InstalledVersion); - PrintOpt("Publisher", m.Publisher); - if (m.LocalId != m.Id) PrintOpt("Local Identifier", m.LocalId); - PrintOpt("Source", m.SourceName); - PrintOpt("Available", m.AvailableVersion); - } - } - else - { - bool showAvailable = result.Matches.Any(m => !string.IsNullOrEmpty(m.AvailableVersion)); - string[] headers = showAvailable - ? ["Name", "Id", "Version", "Available", "Source"] - : ["Name", "Id", "Version", "Source"]; - var rows = result.Matches.Select(m => showAvailable - ? new[] { m.Name, m.Id, m.InstalledVersion, m.AvailableVersion ?? "", m.SourceName ?? "" } - : new[] { m.Name, m.Id, m.InstalledVersion, m.SourceName ?? "" }).ToList(); - PrintTable(headers, rows); - } - - if (result.Truncated) Console.WriteLine($""); - if (upgrade) Console.WriteLine($"{result.Matches.Count} upgrades available."); -} - -static void PrintSources(List sources) -{ - Console.WriteLine($"{"Name",-12} {"Trust",-8} {"Explicit",-8} Argument"); - foreach (var s in sources) - Console.WriteLine($"{s.Name,-12} {s.TrustLevel,-8} {s.Explicit.ToString().ToLowerInvariant(),-8} {s.Arg}"); -} - -static void PrintPackageActionResult(InstallResult result, string action, string actionPastTense) -{ - PrintWarnings(result.Warnings); - var target = string.IsNullOrWhiteSpace(result.Version) - ? result.PackageId - : $"{result.PackageId} v{result.Version}"; - if (result.NoOp) - Console.WriteLine($"No changes were made for {target}."); - else if (result.Success) - Console.WriteLine($"Successfully {actionPastTense} {target}"); - else - Console.Error.WriteLine($"Failed to {action} {target} (exit code: {result.ExitCode})"); -} - -static void PrintErrorLookup(string input) -{ - if (!long.TryParse(input.StartsWith("0x", StringComparison.OrdinalIgnoreCase) - ? input[2..] : input, System.Globalization.NumberStyles.HexNumber, null, out var code)) - { - Console.Error.WriteLine($"error: Could not parse '{input}' as an error code"); - return; - } - - var lookup = LookupHresult(code); - if (lookup is not null) - { - // APPINSTALLER codes (0x8A15xxxx): show symbol on same line - if ((code & 0xFFFF0000L) == unchecked((long)0x8A150000)) - Console.WriteLine($"0x{code:x8} : {lookup.Value.Symbol}"); - else - Console.WriteLine($"0x{code:x8}"); - Console.WriteLine(lookup.Value.Description); - } - else - { - Console.WriteLine($"0x{code:x8}"); - Console.WriteLine(" Unknown error code"); - } -} - -static (string Symbol, string Description)? LookupHresult(long code) -{ - return (code & 0xFFFF0000L) switch - { - 0x8A150000 => (code & 0xFFFF) switch - { - 0x0001 => ("APPINSTALLER_CLI_ERROR_INTERNAL_ERROR", "An unexpected error occurred."), - 0x0002 => ("APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS", "Invalid command line arguments."), - 0x0003 => ("APPINSTALLER_CLI_ERROR_COMMAND_FAILED", "The command failed."), - 0x0004 => ("APPINSTALLER_CLI_ERROR_MANIFEST_FAILED", "Opening the manifest failed."), - 0x0007 => ("APPINSTALLER_CLI_ERROR_NO_APPLICABLE_INSTALLER", "No applicable installer found."), - 0x000E => ("APPINSTALLER_CLI_ERROR_PACKAGE_NOT_FOUND", "No package matched the query."), - 0x0010 => ("APPINSTALLER_CLI_ERROR_SOURCE_NAME_ALREADY_EXISTS", "A source with the given name already exists."), - 0x0012 => ("APPINSTALLER_CLI_ERROR_NO_SOURCES_DEFINED", "No sources are configured."), - 0x0013 => ("APPINSTALLER_CLI_ERROR_MULTIPLE_APPLICATIONS_FOUND", "Multiple packages matched the query."), - 0x0016 => ("APPINSTALLER_CLI_ERROR_NO_APPLICABLE_UPGRADE", "No applicable upgrade found."), - _ => null, - }, - _ => code switch - { - unchecked((long)0x80004005) => ("E_FAIL", "Unspecified error"), - unchecked((long)0x80070005) => ("E_ACCESSDENIED", "General access denied error"), - unchecked((long)0x80070057) => ("E_INVALIDARG", "One or more arguments are not valid"), - unchecked((long)0x8007000E) => ("E_OUTOFMEMORY", "Failed to allocate necessary memory"), - _ => null, - } - }; -} - -static void PrintWarnings(List warnings) { foreach (var w in warnings) Console.Error.WriteLine($"warning: {w}"); } -static void PrintOpt(string label, string? value) { if (value is not null) Console.WriteLine($"{label}: {value}"); } -static string Trunc(string s, int max) => s.Length <= max ? s : s[..(max - 1)] + "."; - -static string ResolveSourceAddValue(string? positionalValue, string? optionValue, string label) -{ - if (!string.IsNullOrWhiteSpace(positionalValue) && - !string.IsNullOrWhiteSpace(optionValue) && - !string.Equals(positionalValue, optionValue, StringComparison.Ordinal)) - { - throw new InvalidOperationException($"Conflicting source {label} values were provided."); - } - - if (!string.IsNullOrWhiteSpace(optionValue)) - return optionValue; - - if (!string.IsNullOrWhiteSpace(positionalValue)) - return positionalValue; - - throw new InvalidOperationException($"source add requires a {label}."); -} - -static SourceKind ParseSourceKind(string? value) -{ - if (string.IsNullOrWhiteSpace(value) || - string.Equals(value, "rest", StringComparison.OrdinalIgnoreCase) || - string.Equals(value, "Microsoft.Rest", StringComparison.OrdinalIgnoreCase)) - { - return SourceKind.Rest; - } - - if (string.Equals(value, "preindexed", StringComparison.OrdinalIgnoreCase) || - string.Equals(value, "Microsoft.PreIndexed.Package", StringComparison.OrdinalIgnoreCase)) - { - return SourceKind.PreIndexed; - } - - throw new InvalidOperationException($"Unsupported source type: {value}"); -} - -static string FormatSourceType(SourceKind kind) => kind switch -{ - SourceKind.Rest => "Microsoft.Rest", - SourceKind.PreIndexed => "Microsoft.PreIndexed.Package", - _ => kind.ToString(), -}; - -static bool ParseBooleanSettingValue(string value) => - value.Trim().ToLowerInvariant() switch - { - "true" or "1" or "on" or "yes" or "enabled" => true, - "false" or "0" or "off" or "no" or "disabled" => false, - _ => throw new InvalidOperationException($"Unsupported admin setting value: {value}") - }; - -static PackageQuery CreatePinQuery( - string? query, - string? id, - string? name, - string? moniker, - string? tag, - string? command, - string? source, - bool exact) => - new() - { - Query = query, - Id = id, - Name = name, - Moniker = moniker, - Tag = tag, - Command = command, - Source = source, - Exact = exact, - Count = 200, - }; - -static void EnsurePinQueryProvided(PackageQuery query, string commandName) -{ - if (string.IsNullOrWhiteSpace(query.Query) && - string.IsNullOrWhiteSpace(query.Id) && - string.IsNullOrWhiteSpace(query.Name) && - string.IsNullOrWhiteSpace(query.Moniker) && - string.IsNullOrWhiteSpace(query.Tag) && - string.IsNullOrWhiteSpace(query.Command)) - { - throw new InvalidOperationException($"{commandName} requires a query or explicit filter."); - } -} - -static SearchMatch ResolveSingleAvailablePinTarget(Repository repo, PackageQuery query) -{ - var result = repo.Search(query); - if (result.Matches.Count == 0) - throw new InvalidOperationException("No package matched the query."); - if (result.Matches.Count > 1) - throw new InvalidOperationException("Multiple packages matched the query; refine the query."); - return result.Matches[0]; -} - -static ListMatch ResolveSingleInstalledPinTarget(Repository repo, PackageQuery query) -{ - var result = repo.List(new ListQuery - { - Query = query.Query, - Id = query.Id, - Name = query.Name, - Moniker = query.Moniker, - Tag = query.Tag, - Command = query.Command, - Source = query.Source, - Exact = query.Exact, - Count = 200, - }); - if (result.Matches.Count == 0) - throw new InvalidOperationException("No installed package matched the query."); - if (result.Matches.Count > 1) - throw new InvalidOperationException("Multiple installed packages matched the query; refine the query."); - return result.Matches[0]; -} - -static List FilterPins(Repository repo, PackageQuery query) -{ - IEnumerable pins = repo.ListPins(query.Source); - if (!string.IsNullOrWhiteSpace(query.Id)) - pins = pins.Where(pin => MatchesText(pin.PackageId, query.Id, query.Exact)); - - var needsCatalogResolution = - !string.IsNullOrWhiteSpace(query.Query) || - !string.IsNullOrWhiteSpace(query.Name) || - !string.IsNullOrWhiteSpace(query.Moniker) || - !string.IsNullOrWhiteSpace(query.Tag) || - !string.IsNullOrWhiteSpace(query.Command); - if (!needsCatalogResolution) - return pins.ToList(); - - var searchResult = repo.Search(query); - var keys = searchResult.Matches - .Select(match => $"{match.Id}|{match.SourceName ?? ""}") - .ToHashSet(StringComparer.OrdinalIgnoreCase); - var ids = searchResult.Matches - .Select(match => match.Id) - .ToHashSet(StringComparer.OrdinalIgnoreCase); - - return pins - .Where(pin => keys.Contains($"{pin.PackageId}|{pin.SourceId}") || - (string.IsNullOrWhiteSpace(pin.SourceId) && ids.Contains(pin.PackageId))) - .ToList(); -} - -static bool MatchesText(string value, string query, bool exact) => - exact - ? value.Equals(query, StringComparison.OrdinalIgnoreCase) - : value.Contains(query, StringComparison.OrdinalIgnoreCase); - -static PinRecord? FindMatchingPin(ListMatch match, IReadOnlyList pins) -{ - PinRecord? sourceSpecific = null; - PinRecord? sourceAgnostic = null; - foreach (var pin in pins) - { - if (!pin.PackageId.Equals(match.Id, StringComparison.OrdinalIgnoreCase) && - !pin.PackageId.Equals(match.LocalId, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - if (!string.IsNullOrWhiteSpace(pin.SourceId)) - { - if (!string.IsNullOrWhiteSpace(match.SourceName) && - pin.SourceId.Equals(match.SourceName, StringComparison.OrdinalIgnoreCase)) - { - sourceSpecific = pin; - break; - } - } - else if (sourceAgnostic is null) - { - sourceAgnostic = pin; - } - } - - return sourceSpecific ?? sourceAgnostic; -} - -static InstallRequest CreateInstallRequest( - PackageQuery query, - string? manifestPath, - InstallerMode mode, - string? logPath, - string? custom, - string? overrideArgs, - string? installLocation, - bool skipDependencies, - bool dependenciesOnly, - bool acceptPackageAgreements, - bool force, - string? rename, - bool uninstallPrevious, - bool ignoreSecurityHash, - string? dependencySource, - bool noUpgrade) => - new() - { - Query = query, - ManifestPath = manifestPath, - Mode = mode, - LogPath = logPath, - Custom = custom, - Override = overrideArgs, - InstallLocation = installLocation, - SkipDependencies = skipDependencies, - DependenciesOnly = dependenciesOnly, - AcceptPackageAgreements = acceptPackageAgreements, - Force = force, - Rename = rename, - UninstallPrevious = uninstallPrevious, - IgnoreSecurityHash = ignoreSecurityHash, - DependencySource = dependencySource, - NoUpgrade = noUpgrade, - }; - -static RepairRequest CreateRepairRequest( - PackageQuery query, - string? manifestPath, - string? productCode, - InstallerMode mode, - string? logPath, - bool acceptPackageAgreements, - bool force, - bool ignoreSecurityHash) => - new() - { - Query = query, - ManifestPath = manifestPath, - ProductCode = productCode, - Mode = mode, - LogPath = logPath, - AcceptPackageAgreements = acceptPackageAgreements, - Force = force, - IgnoreSecurityHash = ignoreSecurityHash, - }; - -static string? GetJsonString(JsonElement element, string propertyName) => - element.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String - ? value.GetString() - : null; - -static bool IsInstalledPackagePresent(Repository repo, string packageId, string? sourceName) => - repo.List(new ListQuery - { - Id = packageId, - Source = sourceName, - Exact = true, - Count = 1, - }).Matches.Count > 0; - -static bool CanIgnoreUnavailableImportFailure(Exception ex) => - ex is InvalidOperationException && - (ex.Message.Contains("No package matched the query", StringComparison.OrdinalIgnoreCase) || - ex.Message.Contains("No applicable installer found", StringComparison.OrdinalIgnoreCase)); - -void WriteJsonNode(JsonNode value, OutputFormat output) -{ - switch (output) - { - case OutputFormat.Yaml: - var structured = JsonSerializer.Deserialize(value.ToJsonString()) ?? new object(); - Console.Write(new SerializerBuilder().Build().Serialize(structured)); - break; - default: - Console.WriteLine(value.ToJsonString(new JsonSerializerOptions { WriteIndented = true })); - break; - } -} - -static void PrintTable(string[] headers, List rows) -{ - if (headers.Length == 0) return; - int cols = headers.Length; - var widths = headers.Select(h => h.Length).ToArray(); - var hasData = new bool[cols]; - - foreach (var row in rows) - for (int i = 0; i < Math.Min(cols, row.Length); i++) - if (!string.IsNullOrEmpty(row[i])) - { - hasData[i] = true; - widths[i] = Math.Max(widths[i], row[i].Length); - } - - for (int i = 0; i < cols; i++) - if (!hasData[i]) widths[i] = 0; - - var spaceAfter = Enumerable.Repeat(true, cols).ToArray(); - spaceAfter[^1] = false; - for (int i = cols - 1; i >= 1; i--) - { - if (widths[i] == 0) spaceAfter[i - 1] = false; - else break; - } - - int totalWidth = widths.Zip(spaceAfter, (w, s) => w + (s ? 1 : 0)).Sum(); - int consoleWidth = 119; - try { consoleWidth = Math.Max(1, Console.WindowWidth - 2); } catch { } - if (totalWidth >= consoleWidth) - { - int extra = totalWidth - consoleWidth + 1; - while (extra > 0) - { - int target = 0; - for (int i = 1; i < cols; i++) - if (widths[i] > widths[target]) target = i; - if (widths[target] > 1) widths[target]--; - extra--; - } - totalWidth = Math.Max(0, consoleWidth - 1); - } - - PrintTableLine(headers, widths, spaceAfter); - Console.WriteLine(new string('-', totalWidth)); - foreach (var row in rows) - PrintTableLine(row, widths, spaceAfter); -} - -static void PrintTableLine(string[] values, int[] widths, bool[] spaceAfter) -{ - var sb = new System.Text.StringBuilder(); - for (int i = 0; i < Math.Min(values.Length, widths.Length); i++) - { - if (widths[i] == 0) continue; - var val = values[i] ?? ""; - if (val.Length > widths[i]) - { - sb.Append(Trunc(val, widths[i])); - if (spaceAfter[i]) sb.Append(' '); - } - else - { - sb.Append(val); - if (spaceAfter[i]) sb.Append(' ', widths[i] - val.Length + 1); - } - } - Console.WriteLine(sb.ToString().TrimEnd()); -} - -enum OutputFormat -{ - Text, - Json, - Yaml, -} diff --git a/dotnet/src/Devolutions.Pinget.Cli/QueryArguments.cs b/dotnet/src/Devolutions.Pinget.Cli/QueryArguments.cs new file mode 100644 index 0000000..fcb7031 --- /dev/null +++ b/dotnet/src/Devolutions.Pinget.Cli/QueryArguments.cs @@ -0,0 +1,9 @@ +using System.CommandLine; + +namespace Devolutions.Pinget.Cli; + +internal static class QueryArguments +{ + internal static Argument Package => new("query", () => null, "Package query"); + internal static Argument Search => new("query", () => null, "Search query"); +} From 959e77b6e5079d0d750441f31021ef8b258433d9 Mon Sep 17 00:00:00 2001 From: daumast Date: Tue, 19 May 2026 13:44:37 +0300 Subject: [PATCH 2/3] Quickfix: Json format --- dotnet/src/Devolutions.Pinget.Cli/Program.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Devolutions.Pinget.Cli/Program.cs b/dotnet/src/Devolutions.Pinget.Cli/Program.cs index 6d52725..6cca613 100644 --- a/dotnet/src/Devolutions.Pinget.Cli/Program.cs +++ b/dotnet/src/Devolutions.Pinget.Cli/Program.cs @@ -1,12 +1,10 @@ using System.CommandLine; using System.Security.Cryptography; using System.Text.Json; -using System.Text.Json.Nodes; using Devolutions.Pinget.Cli; using Devolutions.Pinget.Cli.Extensions; using Devolutions.Pinget.Cli.Helpers; using Devolutions.Pinget.Core; -using YamlDotNet.Serialization; if (args.Length == 1 && (string.Equals(args[0], "--version", StringComparison.OrdinalIgnoreCase) || string.Equals(args[0], "-v", StringComparison.OrdinalIgnoreCase))) { @@ -20,8 +18,6 @@ outputOption.FromAmong("text", "json", "yaml"); rootCommand.AddGlobalOption(outputOption); -var JsonOpts = new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - var infoOption = new Option("--info", "Display general info"); rootCommand.AddGlobalOption(infoOption); @@ -417,7 +413,7 @@ s.Explicit, s.Priority, }); - Console.WriteLine(JsonSerializer.Serialize(new { Sources = sources }, JsonOpts)); + Console.WriteLine(JsonSerializer.Serialize(new { Sources = sources }, Json.Options)); }); sourceAddCmd.SetHandler((ctx) => @@ -560,7 +556,7 @@ } } }; - File.WriteAllText(output, JsonSerializer.Serialize(export, JsonOpts)); + File.WriteAllText(output, JsonSerializer.Serialize(export, Json.Options)); Console.WriteLine($"Exported {packages.Count} packages to {output}"); }, exOutputOpt, exSourceOpt, exVersionsOpt); From 4ee4af81b8a4ffefbe7c677a81a2d53be0d6877c Mon Sep 17 00:00:00 2001 From: daumast Date: Tue, 19 May 2026 19:58:46 +0300 Subject: [PATCH 3/3] Version moved to Consts.cs --- scripts/Build-CliNativeNuGetPackages.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/Build-CliNativeNuGetPackages.ps1 b/scripts/Build-CliNativeNuGetPackages.ps1 index 52e88ac..270c1df 100644 --- a/scripts/Build-CliNativeNuGetPackages.ps1 +++ b/scripts/Build-CliNativeNuGetPackages.ps1 @@ -301,10 +301,10 @@ function Assert-WindowsExecutableMetadata { function Get-SourceVersion { $rustCliManifest = Join-Path $repoRoot 'rust\crates\pinget-cli\Cargo.toml' - $dotNetCliProgram = Join-Path $repoRoot 'dotnet\src\Devolutions.Pinget.Cli\Program.cs' + $dotNetCliProgram = Join-Path $repoRoot 'dotnet\src\Devolutions.Pinget.Cli\Consts.cs' $rustMatch = Select-String -Path $rustCliManifest -Pattern '^version = "([^"]+)"$' | Select-Object -First 1 - $dotNetMatch = Select-String -Path $dotNetCliProgram -Pattern '^const string Version = "([^"]+)";$' | Select-Object -First 1 + $dotNetMatch = Select-String -Path $dotNetCliProgram -Pattern '^\s*internal const string Version = "([^"]+)";$' | Select-Object -First 1 if (($null -eq $rustMatch) -or ($null -eq $dotNetMatch)) { throw 'Unable to detect CLI package version from source files.'