diff --git a/src/UniGetUI.Avalonia/App.axaml.cs b/src/UniGetUI.Avalonia/App.axaml.cs index 92a8beaea..052ee30bf 100644 --- a/src/UniGetUI.Avalonia/App.axaml.cs +++ b/src/UniGetUI.Avalonia/App.axaml.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; @@ -22,6 +23,10 @@ namespace UniGetUI.Avalonia; public partial class App : Application { + [UnconditionalSuppressMessage( + "Trimming", + "IL2026", + Justification = "Platform theme dictionaries are Avalonia resources included in the app package; only the resource URI is selected dynamically.")] public override void Initialize() { AvaloniaXamlLoader.Load(this); diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj index bc185e5bd..4abf35467 100644 --- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj +++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj @@ -21,16 +21,20 @@ ..\UniGetUI\icon.ico true app.manifest + true + partial + false + false + false + false - $(MSBuildThisFileDirectory)..\UniGetUI.Pinget.Cli\UniGetUI.Pinget.Cli.csproj $(RuntimeIdentifier) win-arm64 win-x64 - $(MSBuildProjectDirectory)\obj\$(Platform)\$(Configuration)\BundledPinget\$(PingetCliRuntimeIdentifier)\ - $(PingetCliPublishDir)pinget.exe - $(PingetCliPublishDir)e_sqlite3.dll + $(PkgDevolutions_Pinget_Cli_Rust)\runtimes\$(PingetCliRuntimeIdentifier)\native + $(PingetCliPackageNativePath)\pinget.exe @@ -42,18 +46,13 @@ AfterTargets="Build;Publish" Condition="'$(DesignTimeBuild)' != 'true' and '$(SkipBundledPingetCli)' != 'true' and $([MSBuild]::IsOSPlatform('Windows'))" > - - - - - + + + + + + @@ -82,6 +94,7 @@ + diff --git a/src/UniGetUI.Avalonia/ViewLocator.cs b/src/UniGetUI.Avalonia/ViewLocator.cs index 33d60fa05..7d178d54b 100644 --- a/src/UniGetUI.Avalonia/ViewLocator.cs +++ b/src/UniGetUI.Avalonia/ViewLocator.cs @@ -1,17 +1,13 @@ -using System; -using System.Diagnostics.CodeAnalysis; using Avalonia.Controls; using Avalonia.Controls.Templates; using UniGetUI.Avalonia.ViewModels; +using UniGetUI.Avalonia.Views; namespace UniGetUI.Avalonia; /// -/// Given a view model, returns the corresponding view if possible. +/// Given a view model, returns the corresponding view if possible without reflection. /// -[RequiresUnreferencedCode( - "Default implementation of ViewLocator involves reflection which may be trimmed away.", - Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")] public class ViewLocator : IDataTemplate { public Control? Build(object? param) @@ -19,19 +15,19 @@ public class ViewLocator : IDataTemplate if (param is null) return null; - var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); - var type = Type.GetType(name); - - if (type != null) + if (param is SidebarViewModel sidebar) { - return (Control)Activator.CreateInstance(type)!; + return new SidebarView + { + DataContext = sidebar, + }; } - return new TextBlock { Text = "Not Found: " + name }; + return new TextBlock { Text = "Not Found: " + param.GetType().Name }; } public bool Match(object? data) { - return data is ViewModelBase; + return data is SidebarViewModel; } } diff --git a/src/UniGetUI.Avalonia/Views/Controls/UserAvatarControl.axaml b/src/UniGetUI.Avalonia/Views/Controls/UserAvatarControl.axaml index f5783d2a7..ec2b11859 100644 --- a/src/UniGetUI.Avalonia/Views/Controls/UserAvatarControl.axaml +++ b/src/UniGetUI.Avalonia/Views/Controls/UserAvatarControl.axaml @@ -87,8 +87,8 @@ - - + + diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs index 541d79ee9..40557e977 100644 --- a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs @@ -69,8 +69,7 @@ or nameof(PackagesPageViewModel.SortAscending)) { var reloadBtn = ViewModel.AddToolbarButton("reload", CoreTools.Translate("Reload"), ViewModel.TriggerReload); - reloadBtn.Bind(ToolTip.TipProperty, - new global::Avalonia.Data.Binding(nameof(PackagesPageViewModel.ReloadButtonTooltip)) { Source = ViewModel }); + UpdateReloadButtonTooltip(reloadBtn); ViewModel.AddToolbarSeparator(); } @@ -136,6 +135,16 @@ or nameof(PackagesPageViewModel.SortAscending)) // ─── UI-only: focus the package list ───────────────────────────────────── private void OnFocusListRequested() => PackageList.Focus(); + private void UpdateReloadButtonTooltip(Button reloadButton) + { + ToolTip.SetTip(reloadButton, ViewModel.ReloadButtonTooltip); + ViewModel.PropertyChanged += (_, args) => + { + if (args.PropertyName is nameof(PackagesPageViewModel.ReloadButtonTooltip)) + ToolTip.SetTip(reloadButton, ViewModel.ReloadButtonTooltip); + }; + } + public void FocusPackageList() { if (ViewModel.MegaQueryBoxEnabled) diff --git a/src/UniGetUI.Core.Data.Tests/CoreTests.cs b/src/UniGetUI.Core.Data.Tests/CoreTests.cs index 27ce43a34..c6029b255 100644 --- a/src/UniGetUI.Core.Data.Tests/CoreTests.cs +++ b/src/UniGetUI.Core.Data.Tests/CoreTests.cs @@ -39,6 +39,36 @@ public void CheckOtherAttributes() ); } + [Fact] + public void ResolveInstallationDirectoryReturnsParentForBundledAvaloniaDirectory() + { + string installDirectory = Path.GetFullPath(Path.Join("install-root")); + string avaloniaDirectory = Path.Join(installDirectory, "Avalonia"); + string classicExecutable = Path.Join(installDirectory, "UniGetUI.exe"); + + string resolvedDirectory = CoreData.ResolveInstallationDirectory( + avaloniaDirectory, + filePath => filePath == classicExecutable, + static _ => false + ); + + Assert.Equal(installDirectory, resolvedDirectory); + } + + [Fact] + public void ResolveInstallationDirectoryKeepsStandaloneAvaloniaDirectory() + { + string avaloniaDirectory = Path.GetFullPath(Path.Join("standalone", "Avalonia")); + + string resolvedDirectory = CoreData.ResolveInstallationDirectory( + avaloniaDirectory, + static _ => false, + static _ => false + ); + + Assert.Equal(avaloniaDirectory, resolvedDirectory); + } + [Theory] [InlineData("3.3.7", "3.3.7")] [InlineData("2026.1.2", "v2026.1.2")] diff --git a/src/UniGetUI.Core.Data/CoreData.cs b/src/UniGetUI.Core.Data/CoreData.cs index a5ca7f794..ffc2cb460 100644 --- a/src/UniGetUI.Core.Data/CoreData.cs +++ b/src/UniGetUI.Core.Data/CoreData.cs @@ -8,6 +8,9 @@ public static class CoreData { private const string GitHubReleasePageBaseUrl = "https://github.com/Devolutions/UniGetUI/releases/tag/"; private const string GitHubReleaseApiBaseUrl = "https://api.github.com/repos/Devolutions/UniGetUI/releases/tags/"; + private const string BundledModernAppDirectoryName = "Avalonia"; + private const string ClassicExecutableName = "UniGetUI.exe"; + private const string BundledPingetExecutableName = "pinget.exe"; private static int? __code_page; public static int CODE_PAGE @@ -326,25 +329,49 @@ public static string UniGetUIExecutableDirectory { get { - string? dir = Path.GetDirectoryName( - System.Reflection.Assembly.GetExecutingAssembly().Location - ); - if (dir is not null) + string dir = NormalizeDirectoryPath(AppContext.BaseDirectory); + if (!string.IsNullOrEmpty(dir)) { - return dir; + return ResolveInstallationDirectory(dir); } - Logger.Error( - "System.Reflection.Assembly.GetExecutingAssembly().Location returned an empty path" - ); + Logger.Error("AppContext.BaseDirectory returned an empty path"); - return AppContext.BaseDirectory.TrimEnd( - Path.DirectorySeparatorChar, - Path.AltDirectorySeparatorChar - ); + return ResolveInstallationDirectory(NormalizeDirectoryPath(AppContext.BaseDirectory)); } } + public static string ResolveInstallationDirectory( + string executableDirectory, + Func? fileExists = null, + Func? directoryExists = null + ) + { + fileExists ??= File.Exists; + directoryExists ??= Directory.Exists; + + string normalizedDirectory = NormalizeDirectoryPath(executableDirectory); + if (!string.Equals( + Path.GetFileName(normalizedDirectory), + BundledModernAppDirectoryName, + StringComparison.OrdinalIgnoreCase + )) + { + return normalizedDirectory; + } + + string? parentDirectory = Path.GetDirectoryName(normalizedDirectory); + if (string.IsNullOrEmpty(parentDirectory)) + { + return normalizedDirectory; + } + + parentDirectory = NormalizeDirectoryPath(parentDirectory); + return IsInstallRoot(parentDirectory, fileExists, directoryExists) + ? parentDirectory + : normalizedDirectory; + } + /// /// A path pointing to the executable file of the app /// @@ -599,6 +626,14 @@ private static string GetUserHomeDirectory() return Environment.GetEnvironmentVariable("HOME") ?? AppContext.BaseDirectory; } + private static string NormalizeDirectoryPath(string path) + { + return Path.GetFullPath(path).TrimEnd( + Path.DirectorySeparatorChar, + Path.AltDirectorySeparatorChar + ); + } + private static string NormalizeExecutablePath(string path) { if ( @@ -611,5 +646,18 @@ private static string NormalizeExecutablePath(string path) return path; } + + private static bool IsInstallRoot( + string directory, + Func fileExists, + Func directoryExists + ) + { + return fileExists(Path.Join(directory, ClassicExecutableName)) + || fileExists(Path.Join(directory, BundledPingetExecutableName)) + || fileExists(Path.Join(directory, "IntegrityTree.json")) + || directoryExists(Path.Join(directory, "Assets", "Utilities")) + || directoryExists(Path.Join(directory, "Assets", "Data")); + } } } diff --git a/src/UniGetUI.Core.IconStore/IconDatabase.cs b/src/UniGetUI.Core.IconStore/IconDatabase.cs index 7a28119c0..cd9afb9c4 100644 --- a/src/UniGetUI.Core.IconStore/IconDatabase.cs +++ b/src/UniGetUI.Core.IconStore/IconDatabase.cs @@ -88,9 +88,8 @@ public async Task LoadFromCacheAsync() "Icon Database.json" ); IconScreenshotDatabase_v2 JsonData = - JsonSerializer.Deserialize( - await File.ReadAllTextAsync(IconsAndScreenshotsFile), - SerializationHelpers.DefaultOptions + IconStoreJson.DeserializeIconDatabase( + await File.ReadAllTextAsync(IconsAndScreenshotsFile) ); if (JsonData.icons_and_screenshots is not null) { diff --git a/src/UniGetUI.Core.IconStore/IconStoreJson.cs b/src/UniGetUI.Core.IconStore/IconStoreJson.cs new file mode 100644 index 000000000..ed6c2433a --- /dev/null +++ b/src/UniGetUI.Core.IconStore/IconStoreJson.cs @@ -0,0 +1,25 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace UniGetUI.Core.IconEngine; + +internal static class IconStoreJson +{ + public static IconScreenshotDatabase_v2 DeserializeIconDatabase(string json) + { + return JsonSerializer.Deserialize(json, GetTypeInfo()); + } + + private static JsonTypeInfo GetTypeInfo() + { + return (JsonTypeInfo?)IconStoreJsonContext.Default.GetTypeInfo(typeof(T)) + ?? throw new InvalidOperationException( + $"Icon store JSON metadata for {typeof(T).FullName} was not generated." + ); + } +} + +[JsonSourceGenerationOptions(AllowTrailingCommas = true, WriteIndented = true)] +[JsonSerializable(typeof(IconScreenshotDatabase_v2))] +internal sealed partial class IconStoreJsonContext : JsonSerializerContext; diff --git a/src/UniGetUI.Core.Settings/SettingsEngine_Dictionaries.cs b/src/UniGetUI.Core.Settings/SettingsEngine_Dictionaries.cs index a57da6c1e..19701e180 100644 --- a/src/UniGetUI.Core.Settings/SettingsEngine_Dictionaries.cs +++ b/src/UniGetUI.Core.Settings/SettingsEngine_Dictionaries.cs @@ -1,203 +1,202 @@ -using System.Collections.Concurrent; -using System.Text.Json; -using UniGetUI.Core.Data; -using UniGetUI.Core.Logging; - -namespace UniGetUI.Core.SettingsEngine -{ - public static partial class Settings - { - private static readonly ConcurrentDictionary< - K, - Dictionary - > _dictionarySettings = new(); - - // Returns an empty dictionary if the setting doesn't exist and null if the types are invalid - private static Dictionary? _getDictionary(K key) - where KeyT : notnull - { - string setting = ResolveKey(key); - try - { - try - { - if ( - _dictionarySettings.TryGetValue( - key, - out Dictionary? result - ) - ) - { - // If the setting was cached - return result.ToDictionary(kvp => (KeyT)kvp.Key, kvp => (ValueT?)kvp.Value); - } - } - catch (InvalidCastException) - { - Logger.Error( - $"Tried to get a dictionary setting with a key of type {typeof(KeyT)} and a value of type {typeof(ValueT)}, which is not the type of the dictionary" - ); - return null; - } - - // Otherwise, load the setting from disk and cache that setting - Dictionary value = []; - if ( - File.Exists( - Path.Join(CoreData.UniGetUIUserConfigurationDirectory, $"{setting}.json") - ) - ) - { - string result = File.ReadAllText( - Path.Join(CoreData.UniGetUIUserConfigurationDirectory, $"{setting}.json") - ); - try - { - if (result != "") - { - Dictionary? item = JsonSerializer.Deserialize< - Dictionary - >(result, SerializationOptions); - if (item is not null) - { - value = item; - } - } - } - catch (InvalidCastException) - { - Logger.Error( - $"Tried to get a dictionary setting with a key of type {typeof(KeyT)} and a value of type {typeof(ValueT)}, but the setting on disk ({result}) cannot be deserialized to that" - ); - } - } - - _dictionarySettings[key] = value.ToDictionary( - kvp => (object)kvp.Key, - kvp => (object?)kvp.Value - ); - return value; - } - catch (Exception ex) - { - Logger.Error($"Could not load dictionary name {setting}"); - Logger.Error(ex); - return []; - } - } - - // Returns an empty dictionary if the setting doesn't exist and null if the types are invalid - public static IReadOnlyDictionary? GetDictionary(K settingsKey) - where KeyT : notnull - { - return _getDictionary(settingsKey); - } - - public static void SetDictionary( - K settingsKey, - Dictionary value - ) - where KeyT : notnull - { - string setting = ResolveKey(settingsKey); - _dictionarySettings[settingsKey] = value.ToDictionary( - kvp => (object)kvp.Key, - kvp => (object?)kvp.Value - ); - - var file = Path.Join(CoreData.UniGetUIUserConfigurationDirectory, $"{setting}.json"); - try - { - if (value.Count != 0) - File.WriteAllText(file, JsonSerializer.Serialize(value, SerializationOptions)); - else if (File.Exists(file)) - File.Delete(file); - } - catch (Exception e) - { - Logger.Error( - $"CANNOT SET SETTING DICTIONARY FOR setting={setting} [{string.Join(", ", value)}]" - ); - Logger.Error(e); - } - } - - public static ValueT? GetDictionaryItem(K settingsKey, KeyT key) - where KeyT : notnull - { - Dictionary? dictionary = _getDictionary(settingsKey); - if (dictionary == null || !dictionary.TryGetValue(key, out ValueT? value)) - return default; - - return value; - } - - // Also works as `Add` - public static ValueT? SetDictionaryItem(K settingsKey, KeyT key, ValueT value) - where KeyT : notnull - { - Dictionary? dictionary = _getDictionary(settingsKey); - if (dictionary == null) - { - dictionary = new() { { key, value } }; - SetDictionary(settingsKey, dictionary); - return default; - } - - if (dictionary.TryGetValue(key, out ValueT? oldValue)) - { - dictionary[key] = value; - SetDictionary(settingsKey, dictionary); - return oldValue; - } - - dictionary.Add(key, value); - SetDictionary(settingsKey, dictionary); - return default; - } - - public static ValueT? RemoveDictionaryKey(K settingsKey, KeyT key) - where KeyT : notnull - { - Dictionary? dictionary = _getDictionary(settingsKey); - if (dictionary == null) - return default; - - bool success = false; - if (dictionary.TryGetValue(key, out ValueT? value)) - { - success = dictionary.Remove(key); - SetDictionary(settingsKey, dictionary); - } - - if (!success) - return default; - return value; - } - - public static bool DictionaryContainsKey(K settingsKey, KeyT key) - where KeyT : notnull - { - Dictionary? dictionary = _getDictionary(settingsKey); - if (dictionary == null) - return false; - - return dictionary.ContainsKey(key); - } - - public static bool DictionaryContainsValue(K settingsKey, ValueT value) - where KeyT : notnull - { - Dictionary? dictionary = _getDictionary(settingsKey); - if (dictionary == null) - return false; - - return dictionary.ContainsValue(value); - } - - public static void ClearDictionary(K settingsKey) - { - SetDictionary(settingsKey, new Dictionary()); - } - } -} +using System.Collections.Concurrent; +using System.Text.Json; +using UniGetUI.Core.Data; +using UniGetUI.Core.Logging; + +namespace UniGetUI.Core.SettingsEngine +{ + public static partial class Settings + { + private static readonly ConcurrentDictionary< + K, + Dictionary + > _dictionarySettings = new(); + + // Returns an empty dictionary if the setting doesn't exist and null if the types are invalid + private static Dictionary? _getDictionary(K key) + where KeyT : notnull + { + string setting = ResolveKey(key); + try + { + try + { + if ( + _dictionarySettings.TryGetValue( + key, + out Dictionary? result + ) + ) + { + // If the setting was cached + return result.ToDictionary(kvp => (KeyT)kvp.Key, kvp => (ValueT?)kvp.Value); + } + } + catch (InvalidCastException) + { + Logger.Error( + $"Tried to get a dictionary setting with a key of type {typeof(KeyT)} and a value of type {typeof(ValueT)}, which is not the type of the dictionary" + ); + return null; + } + + // Otherwise, load the setting from disk and cache that setting + Dictionary value = []; + if ( + File.Exists( + Path.Join(CoreData.UniGetUIUserConfigurationDirectory, $"{setting}.json") + ) + ) + { + string result = File.ReadAllText( + Path.Join(CoreData.UniGetUIUserConfigurationDirectory, $"{setting}.json") + ); + try + { + if (result != "") + { + Dictionary? item = + SettingsJson.DeserializeDictionary(result); + if (item is not null) + { + value = item; + } + } + } + catch (InvalidCastException) + { + Logger.Error( + $"Tried to get a dictionary setting with a key of type {typeof(KeyT)} and a value of type {typeof(ValueT)}, but the setting on disk ({result}) cannot be deserialized to that" + ); + } + } + + _dictionarySettings[key] = value.ToDictionary( + kvp => (object)kvp.Key, + kvp => (object?)kvp.Value + ); + return value; + } + catch (Exception ex) + { + Logger.Error($"Could not load dictionary name {setting}"); + Logger.Error(ex); + return []; + } + } + + // Returns an empty dictionary if the setting doesn't exist and null if the types are invalid + public static IReadOnlyDictionary? GetDictionary(K settingsKey) + where KeyT : notnull + { + return _getDictionary(settingsKey); + } + + public static void SetDictionary( + K settingsKey, + Dictionary value + ) + where KeyT : notnull + { + string setting = ResolveKey(settingsKey); + _dictionarySettings[settingsKey] = value.ToDictionary( + kvp => (object)kvp.Key, + kvp => (object?)kvp.Value + ); + + var file = Path.Join(CoreData.UniGetUIUserConfigurationDirectory, $"{setting}.json"); + try + { + if (value.Count != 0) + File.WriteAllText(file, SettingsJson.SerializeDictionary(value)); + else if (File.Exists(file)) + File.Delete(file); + } + catch (Exception e) + { + Logger.Error( + $"CANNOT SET SETTING DICTIONARY FOR setting={setting} [{string.Join(", ", value)}]" + ); + Logger.Error(e); + } + } + + public static ValueT? GetDictionaryItem(K settingsKey, KeyT key) + where KeyT : notnull + { + Dictionary? dictionary = _getDictionary(settingsKey); + if (dictionary == null || !dictionary.TryGetValue(key, out ValueT? value)) + return default; + + return value; + } + + // Also works as `Add` + public static ValueT? SetDictionaryItem(K settingsKey, KeyT key, ValueT value) + where KeyT : notnull + { + Dictionary? dictionary = _getDictionary(settingsKey); + if (dictionary == null) + { + dictionary = new() { { key, value } }; + SetDictionary(settingsKey, dictionary); + return default; + } + + if (dictionary.TryGetValue(key, out ValueT? oldValue)) + { + dictionary[key] = value; + SetDictionary(settingsKey, dictionary); + return oldValue; + } + + dictionary.Add(key, value); + SetDictionary(settingsKey, dictionary); + return default; + } + + public static ValueT? RemoveDictionaryKey(K settingsKey, KeyT key) + where KeyT : notnull + { + Dictionary? dictionary = _getDictionary(settingsKey); + if (dictionary == null) + return default; + + bool success = false; + if (dictionary.TryGetValue(key, out ValueT? value)) + { + success = dictionary.Remove(key); + SetDictionary(settingsKey, dictionary); + } + + if (!success) + return default; + return value; + } + + public static bool DictionaryContainsKey(K settingsKey, KeyT key) + where KeyT : notnull + { + Dictionary? dictionary = _getDictionary(settingsKey); + if (dictionary == null) + return false; + + return dictionary.ContainsKey(key); + } + + public static bool DictionaryContainsValue(K settingsKey, ValueT value) + where KeyT : notnull + { + Dictionary? dictionary = _getDictionary(settingsKey); + if (dictionary == null) + return false; + + return dictionary.ContainsValue(value); + } + + public static void ClearDictionary(K settingsKey) + { + SetDictionary(settingsKey, new Dictionary()); + } + } +} diff --git a/src/UniGetUI.Core.Settings/SettingsEngine_Extras.cs b/src/UniGetUI.Core.Settings/SettingsEngine_Extras.cs index 0e4129fbf..adb3746de 100644 --- a/src/UniGetUI.Core.Settings/SettingsEngine_Extras.cs +++ b/src/UniGetUI.Core.Settings/SettingsEngine_Extras.cs @@ -1,6 +1,5 @@ using System.Net; using System.Text.Json; -using System.Text.Json.Serialization.Metadata; using UniGetUI.Core.Data; using UniGetUI.Core.Logging; @@ -75,7 +74,6 @@ public static void SetProxyCredentials(string username, string password) public static JsonSerializerOptions SerializationOptions = new() { - TypeInfoResolver = new DefaultJsonTypeInfoResolver(), AllowTrailingCommas = true, WriteIndented = true, }; diff --git a/src/UniGetUI.Core.Settings/SettingsEngine_ImportExport.cs b/src/UniGetUI.Core.Settings/SettingsEngine_ImportExport.cs index f4be6a8b8..afc50a051 100644 --- a/src/UniGetUI.Core.Settings/SettingsEngine_ImportExport.cs +++ b/src/UniGetUI.Core.Settings/SettingsEngine_ImportExport.cs @@ -43,17 +43,14 @@ string entry in Directory.EnumerateFiles(CoreData.UniGetUIUserConfigurationDirec settings.Add(Path.GetFileName(entry), File.ReadAllText(entry)); } - return JsonSerializer.Serialize(settings, SerializationOptions); + return SettingsJson.SerializeStringDictionary(settings); } public static void ImportFromString_JSON(string jsonContent) { ResetSettings(); Dictionary settings = - JsonSerializer.Deserialize>( - jsonContent, - SerializationOptions - ) ?? []; + SettingsJson.DeserializeStringDictionary(jsonContent) ?? []; foreach (KeyValuePair entry in settings) { if ( diff --git a/src/UniGetUI.Core.Settings/SettingsEngine_Lists.cs b/src/UniGetUI.Core.Settings/SettingsEngine_Lists.cs index 51c7ed33b..398c32070 100644 --- a/src/UniGetUI.Core.Settings/SettingsEngine_Lists.cs +++ b/src/UniGetUI.Core.Settings/SettingsEngine_Lists.cs @@ -1,150 +1,147 @@ -using System.Collections.Concurrent; -using System.Text.Json; -using UniGetUI.Core.Data; -using UniGetUI.Core.Logging; - -namespace UniGetUI.Core.SettingsEngine -{ - public static partial class Settings - { - private static readonly ConcurrentDictionary> listSettings = new(); - - // Returns an empty list if the setting doesn't exist and null if the type is invalid - private static List? _getList(string setting) - { - try - { - try - { - if (listSettings.TryGetValue(setting, out List? result)) - { - // If the setting was cached - return result.Cast().ToList(); - } - } - catch (InvalidCastException) - { - Logger.Error( - $"Tried to get a list setting as type {typeof(T)}, which is not the type of the list" - ); - return null; - } - - // Otherwise, load the setting from disk and cache that setting - List value = []; - - var file = Path.Join( - CoreData.UniGetUIUserConfigurationDirectory, - $"{setting}.json" - ); - if (File.Exists(file)) - { - string result = File.ReadAllText(file); - try - { - if (result != "") - { - List? item = JsonSerializer.Deserialize>( - result, - SerializationOptions - ); - if (item is not null) - { - value = item; - } - } - } - catch (InvalidCastException) - { - Logger.Error( - $"Tried to get a list setting as type {typeof(T)}, but the setting on disk {result} cannot be deserialized to {typeof(T)}" - ); - } - } - - listSettings[setting] = value.Cast().ToList(); - return value; - } - catch (Exception ex) - { - Logger.Error($"Could not load list {setting} from settings"); - Logger.Error(ex); - return []; - } - } - - // Returns an empty list if the setting doesn't exist and null if the type is invalid - public static IReadOnlyList? GetList(string setting) - { - return _getList(setting); - } - - public static void SetList(string setting, List value) - { - listSettings[setting] = value.Cast().ToList(); - var file = Path.Join(CoreData.UniGetUIUserConfigurationDirectory, $"{setting}.json"); - try - { - if (value.Count != 0) - File.WriteAllText(file, JsonSerializer.Serialize(value, SerializationOptions)); - else if (File.Exists(file)) - File.Delete(file); - } - catch (Exception e) - { - Logger.Error( - $"CANNOT SET SETTING LIST FOR setting={setting} [{string.Join(", ", value)}]" - ); - Logger.Error(e); - } - } - - public static T? GetListItem(string setting, int index) - { - List? list = _getList(setting); - if (list == null) - return default; - if (list.Count <= index) - { - Logger.Error($"Index {index} out of range for list setting {setting}"); - return default; - } - - return list.ElementAt(index); - } - - public static void AddToList(string setting, T value) - { - List? list = _getList(setting); - if (list == null) - return; - - list.Add(value); - SetList(setting, list); - } - - public static bool RemoveFromList(string setting, T value) - { - List? list = _getList(setting); - if (list == null) - return false; - - bool result = list.Remove(value); - SetList(setting, list); - return result; - } - - public static bool ListContains(string setting, T value) - { - List? list = _getList(setting); - if (list == null) - return false; - return list.Contains(value); - } - - public static void ClearList(string setting) - { - SetList(setting, []); - } - } -} +using System.Collections.Concurrent; +using System.Text.Json; +using UniGetUI.Core.Data; +using UniGetUI.Core.Logging; + +namespace UniGetUI.Core.SettingsEngine +{ + public static partial class Settings + { + private static readonly ConcurrentDictionary> listSettings = new(); + + // Returns an empty list if the setting doesn't exist and null if the type is invalid + private static List? _getList(string setting) + { + try + { + try + { + if (listSettings.TryGetValue(setting, out List? result)) + { + // If the setting was cached + return result.Cast().ToList(); + } + } + catch (InvalidCastException) + { + Logger.Error( + $"Tried to get a list setting as type {typeof(T)}, which is not the type of the list" + ); + return null; + } + + // Otherwise, load the setting from disk and cache that setting + List value = []; + + var file = Path.Join( + CoreData.UniGetUIUserConfigurationDirectory, + $"{setting}.json" + ); + if (File.Exists(file)) + { + string result = File.ReadAllText(file); + try + { + if (result != "") + { + List? item = SettingsJson.DeserializeList(result); + if (item is not null) + { + value = item; + } + } + } + catch (InvalidCastException) + { + Logger.Error( + $"Tried to get a list setting as type {typeof(T)}, but the setting on disk {result} cannot be deserialized to {typeof(T)}" + ); + } + } + + listSettings[setting] = value.Cast().ToList(); + return value; + } + catch (Exception ex) + { + Logger.Error($"Could not load list {setting} from settings"); + Logger.Error(ex); + return []; + } + } + + // Returns an empty list if the setting doesn't exist and null if the type is invalid + public static IReadOnlyList? GetList(string setting) + { + return _getList(setting); + } + + public static void SetList(string setting, List value) + { + listSettings[setting] = value.Cast().ToList(); + var file = Path.Join(CoreData.UniGetUIUserConfigurationDirectory, $"{setting}.json"); + try + { + if (value.Count != 0) + File.WriteAllText(file, SettingsJson.SerializeList(value)); + else if (File.Exists(file)) + File.Delete(file); + } + catch (Exception e) + { + Logger.Error( + $"CANNOT SET SETTING LIST FOR setting={setting} [{string.Join(", ", value)}]" + ); + Logger.Error(e); + } + } + + public static T? GetListItem(string setting, int index) + { + List? list = _getList(setting); + if (list == null) + return default; + if (list.Count <= index) + { + Logger.Error($"Index {index} out of range for list setting {setting}"); + return default; + } + + return list.ElementAt(index); + } + + public static void AddToList(string setting, T value) + { + List? list = _getList(setting); + if (list == null) + return; + + list.Add(value); + SetList(setting, list); + } + + public static bool RemoveFromList(string setting, T value) + { + List? list = _getList(setting); + if (list == null) + return false; + + bool result = list.Remove(value); + SetList(setting, list); + return result; + } + + public static bool ListContains(string setting, T value) + { + List? list = _getList(setting); + if (list == null) + return false; + return list.Contains(value); + } + + public static void ClearList(string setting) + { + SetList(setting, []); + } + } +} diff --git a/src/UniGetUI.Core.Settings/SettingsJson.cs b/src/UniGetUI.Core.Settings/SettingsJson.cs new file mode 100644 index 000000000..cc785fc85 --- /dev/null +++ b/src/UniGetUI.Core.Settings/SettingsJson.cs @@ -0,0 +1,125 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace UniGetUI.Core.SettingsEngine; + +internal static class SettingsJson +{ + public static string SerializeStringDictionary(Dictionary value) + { + return JsonSerializer.Serialize(value, GetRequiredTypeInfo>()); + } + + public static Dictionary? DeserializeStringDictionary(string json) + { + return JsonSerializer.Deserialize(json, GetRequiredTypeInfo>()); + } + + public static string SerializeList(List value) + { + JsonTypeInfo>? typeInfo = GetGeneratedTypeInfo>(); + return typeInfo is not null + ? JsonSerializer.Serialize(value, typeInfo) + : SerializeListWithReflection(value); + } + + public static List? DeserializeList(string json) + { + JsonTypeInfo>? typeInfo = GetGeneratedTypeInfo>(); + return typeInfo is not null + ? JsonSerializer.Deserialize(json, typeInfo) + : DeserializeListWithReflection(json); + } + + public static string SerializeDictionary(Dictionary value) + where KeyT : notnull + { + JsonTypeInfo>? typeInfo = + GetGeneratedTypeInfo>(); + return typeInfo is not null + ? JsonSerializer.Serialize(value, typeInfo) + : SerializeDictionaryWithReflection(value); + } + + public static Dictionary? DeserializeDictionary(string json) + where KeyT : notnull + { + JsonTypeInfo>? typeInfo = + GetGeneratedTypeInfo>(); + return typeInfo is not null + ? JsonSerializer.Deserialize(json, typeInfo) + : DeserializeDictionaryWithReflection(json); + } + + private static JsonTypeInfo? GetGeneratedTypeInfo() + { + return SettingsJsonContext.Default.GetTypeInfo(typeof(T)) as JsonTypeInfo; + } + + private static JsonTypeInfo GetRequiredTypeInfo() + { + return GetGeneratedTypeInfo() + ?? throw new InvalidOperationException( + $"Settings JSON metadata for {typeof(T).FullName} was not generated." + ); + } + + [UnconditionalSuppressMessage( + "Trimming", + "IL2026", + Justification = "Runtime settings use generated metadata for known app types; this fallback preserves generic settings tests and extension scenarios outside trimmed app paths.")] + private static string SerializeListWithReflection(List value) + { + return JsonSerializer.Serialize(value, Settings.SerializationOptions); + } + + [UnconditionalSuppressMessage( + "Trimming", + "IL2026", + Justification = "Runtime settings use generated metadata for known app types; this fallback preserves generic settings tests and extension scenarios outside trimmed app paths.")] + private static List? DeserializeListWithReflection(string json) + { + return JsonSerializer.Deserialize>(json, Settings.SerializationOptions); + } + + [UnconditionalSuppressMessage( + "Trimming", + "IL2026", + Justification = "Runtime settings use generated metadata for known app types; this fallback preserves generic settings tests and extension scenarios outside trimmed app paths.")] + private static string SerializeDictionaryWithReflection( + Dictionary value + ) + where KeyT : notnull + { + return JsonSerializer.Serialize(value, Settings.SerializationOptions); + } + + [UnconditionalSuppressMessage( + "Trimming", + "IL2026", + Justification = "Runtime settings use generated metadata for known app types; this fallback preserves generic settings tests and extension scenarios outside trimmed app paths.")] + private static Dictionary? DeserializeDictionaryWithReflection( + string json + ) + where KeyT : notnull + { + return JsonSerializer.Deserialize>( + json, + Settings.SerializationOptions + ); + } +} + +[JsonSourceGenerationOptions(AllowTrailingCommas = true, WriteIndented = true)] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] +internal sealed partial class SettingsJsonContext : JsonSerializerContext; diff --git a/src/UniGetUI.Core.Tools.Tests/MetaTests.cs b/src/UniGetUI.Core.Tools.Tests/MetaTests.cs index 34861a91f..694169af0 100644 --- a/src/UniGetUI.Core.Tools.Tests/MetaTests.cs +++ b/src/UniGetUI.Core.Tools.Tests/MetaTests.cs @@ -23,18 +23,20 @@ public void TestJsonSerializationOptions() var lines = File.ReadAllLines(file); var jsonSerCount = lines.Count(x => x.Contains("JsonSerializer.Serialize")); var jsonDeserCount = lines.Count(x => x.Contains("JsonSerializer.Deserialize")); - var serialOptionsCount1 = lines.Count(x => + var trimSafeJsonMetadataCount = lines.Count(x => x.Contains("SerializationHelpers.DefaultOptions") + || x.Contains("SerializationHelpers.ImportBundleOptions") + || x.Contains("SerializationOptions") + || x.Contains("JsonTypeInfo") + || x.Contains("JsonSerializerContext") + || x.Contains("JsonSourceGenerationOptions") + || x.Contains("GetTypeInfo") + || x.Contains("typeInfo") ); - var serialOptionsCount2 = lines.Count(x => - x.Contains("SerializationHelpers.ImportBundleOptions") - ); - var serialOptionsCount3 = lines.Count(x => x.Contains("SerializationOptions")); Assert.True( - (jsonSerCount + jsonDeserCount) - <= serialOptionsCount1 + serialOptionsCount2 + serialOptionsCount3, + (jsonSerCount + jsonDeserCount) <= trimSafeJsonMetadataCount, $"Failing on {file}. The specified file does not serialize and/or deserialize JSON with" - + $" the proper SerializationHelpers.DefaultOptions set" + + $" explicit trim-safe JSON metadata" ); } } diff --git a/src/UniGetUI.Core.Tools/CoreToolsJson.cs b/src/UniGetUI.Core.Tools/CoreToolsJson.cs new file mode 100644 index 000000000..b4449eb42 --- /dev/null +++ b/src/UniGetUI.Core.Tools/CoreToolsJson.cs @@ -0,0 +1,25 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace UniGetUI.Core.Data; + +internal static class CoreToolsJson +{ + public static Dictionary? DeserializeStringDictionary(string json) + { + return JsonSerializer.Deserialize(json, GetTypeInfo>()); + } + + private static JsonTypeInfo GetTypeInfo() + { + return (JsonTypeInfo?)CoreToolsJsonContext.Default.GetTypeInfo(typeof(T)) + ?? throw new InvalidOperationException( + $"Core tools JSON metadata for {typeof(T).FullName} was not generated." + ); + } +} + +[JsonSourceGenerationOptions(AllowTrailingCommas = true, WriteIndented = true)] +[JsonSerializable(typeof(Dictionary))] +internal sealed partial class CoreToolsJsonContext : JsonSerializerContext; diff --git a/src/UniGetUI.Core.Tools/IntegrityTester.cs b/src/UniGetUI.Core.Tools/IntegrityTester.cs index 121ad1d77..f6608005f 100644 --- a/src/UniGetUI.Core.Tools/IntegrityTester.cs +++ b/src/UniGetUI.Core.Tools/IntegrityTester.cs @@ -70,10 +70,7 @@ public static Result CheckIntegrity(bool allowRetry = true) try { - data = JsonSerializer.Deserialize>( - rawData, - SerializationHelpers.DefaultOptions - ); + data = CoreToolsJson.DeserializeStringDictionary(rawData); } catch (Exception ex) { diff --git a/src/UniGetUI.Core.Tools/SerializationHelpers.cs b/src/UniGetUI.Core.Tools/SerializationHelpers.cs index 001ec0d1f..9880c09c9 100644 --- a/src/UniGetUI.Core.Tools/SerializationHelpers.cs +++ b/src/UniGetUI.Core.Tools/SerializationHelpers.cs @@ -1,25 +1,32 @@ using System.Globalization; -using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.Json.Serialization.Metadata; using System.Xml; +using YamlDotNet.RepresentationModel; namespace UniGetUI.Core.Data; public static class SerializationHelpers { + private static readonly JsonSerializerOptions NodeJsonOptions = new() + { + AllowTrailingCommas = true, + WriteIndented = true, + }; + public static Task YAML_to_JSON(string YAML) => Task.Run(() => yaml_to_json(YAML)); private static string yaml_to_json(string YAML) { - var yamlObject = new YamlDotNet.Serialization.Deserializer().Deserialize(YAML); - if (yamlObject is null) - return "{'message': 'deserialized YAML object was null'}"; - return new YamlDotNet.Serialization.SerializerBuilder() - .JsonCompatible() - .Build() - .Serialize(yamlObject); + using var reader = new StringReader(YAML); + var stream = new YamlStream(); + stream.Load(reader); + + if (stream.Documents.Count == 0 || stream.Documents[0].RootNode is null) + return "{\"message\":\"deserialized YAML object was null\"}"; + + return ConvertYamlNode(stream.Documents[0].RootNode)?.ToJsonString(NodeJsonOptions) + ?? "null"; } public static Task XML_to_JSON(string XML) => Task.Run(() => xml_to_json(XML)); @@ -29,63 +36,109 @@ private static string xml_to_json(string XML) var doc = new XmlDocument(); doc.LoadXml(XML); if (doc.DocumentElement is null) - return "{'message': 'XmlDocument.DocumentElement was null'}"; - return JsonSerializer.Serialize( - _convertXmlNode(doc.DocumentElement), - SerializationHelpers.DefaultOptions - ); + return "{\"message\":\"XmlDocument.DocumentElement was null\"}"; + + return ConvertXmlNode(doc.DocumentElement)?.ToJsonString(NodeJsonOptions) + ?? "null"; } - private static object? _convertXmlNode(XmlNode node) + private static JsonNode? ConvertXmlNode(XmlNode node) { - // If node has no children, return its text or attributes if (node.ChildNodes.Count == 1 && node.FirstChild is XmlText singleText) { - return singleText.Value; + return JsonValue.Create(singleText.Value); } - // Attributes dictionary - var dict = new Dictionary(); + var jsonObject = new JsonObject(); if (node.Attributes?.Count > 0) { foreach (XmlAttribute attr in node.Attributes) { - dict[$"@{attr.Name}"] = attr.Value; + jsonObject[$"@{attr.Name}"] = attr.Value; } } - // Group child elements - var children = new Dictionary>(); + var children = new Dictionary>(); foreach (XmlNode child in node.ChildNodes) { if (child is XmlElement childElement) { - var value = _convertXmlNode(childElement); + var value = ConvertXmlNode(childElement); if (!children.ContainsKey(childElement.Name)) - children[childElement.Name] = new List(); + children[childElement.Name] = new List(); children[childElement.Name].Add(value); } } - // Flatten repeated elements if only one group exists - if (children.Count == 1 && dict.Count == 0) + if (children.Count == 1 && jsonObject.Count == 0) { var firstKey = children.Keys.First(); - return children[firstKey].Count == 1 ? children[firstKey][0] : children[firstKey]; + return children[firstKey].Count == 1 + ? children[firstKey][0] + : new JsonArray(children[firstKey].ToArray()); } - // Otherwise build normal object foreach (var kv in children) { - dict[kv.Key] = kv.Value.Count == 1 ? kv.Value[0] : kv.Value; + jsonObject[kv.Key] = kv.Value.Count == 1 ? kv.Value[0] : new JsonArray(kv.Value.ToArray()); } - return dict; + return jsonObject; + } + + private static JsonNode? ConvertYamlNode(YamlNode node) + { + return node switch + { + YamlScalarNode scalar => ConvertYamlScalar(scalar), + YamlSequenceNode sequence => ConvertYamlSequence(sequence), + YamlMappingNode mapping => ConvertYamlMapping(mapping), + _ => JsonValue.Create(node.ToString()), + }; + } + + private static JsonNode? ConvertYamlScalar(YamlScalarNode scalar) + { + if (scalar.Value is null) + return null; + + if (bool.TryParse(scalar.Value, out bool boolValue)) + return JsonValue.Create(boolValue); + + if (long.TryParse(scalar.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out long longValue)) + return JsonValue.Create(longValue); + + if (double.TryParse(scalar.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double doubleValue)) + return JsonValue.Create(doubleValue); + + return JsonValue.Create(scalar.Value); + } + + private static JsonArray ConvertYamlSequence(YamlSequenceNode sequence) + { + var array = new JsonArray(); + foreach (YamlNode child in sequence.Children) + array.Add(ConvertYamlNode(child)); + return array; + } + + private static JsonObject ConvertYamlMapping(YamlMappingNode mapping) + { + var obj = new JsonObject(); + foreach (var (key, value) in mapping.Children) + obj[ConvertYamlKey(key)] = ConvertYamlNode(value); + return obj; + } + + private static string ConvertYamlKey(YamlNode key) + { + return key is YamlScalarNode scalar + ? scalar.Value ?? "" + : key.ToString(); } public static JsonSerializerOptions DefaultOptions = new() { - TypeInfoResolver = new DefaultJsonTypeInfoResolver(), AllowTrailingCommas = true, WriteIndented = true, }; diff --git a/src/UniGetUI.Interface.IpcApi/IpcCliCommandRunner.cs b/src/UniGetUI.Interface.IpcApi/IpcCliCommandRunner.cs index 7a0bf6d1c..bc04bcf45 100644 --- a/src/UniGetUI.Interface.IpcApi/IpcCliCommandRunner.cs +++ b/src/UniGetUI.Interface.IpcApi/IpcCliCommandRunner.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Text.Json.Nodes; using UniGetUI.Core.Data; using UniGetUI.Core.Logging; @@ -50,13 +51,10 @@ TextWriter error return subcommand switch { "status" => await WriteJsonAsync(output, await client.GetStatusAsync()), - "get-app-state" => await WriteJsonAsync( + "get-app-state" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - app = await client.GetAppInfoAsync(), - } + "app", + await client.GetAppInfoAsync() ), "show-app" => await WriteJsonAsync(output, await client.ShowAppAsync()), "navigate-app" => await WriteJsonAsync( @@ -64,58 +62,46 @@ TextWriter error await client.NavigateAppAsync(BuildAppNavigateRequest(args)) ), "quit-app" => await WriteJsonAsync(output, await client.QuitAppAsync()), - "list-operations" => await WriteJsonAsync( - output, - new - { - status = "success", - operations = await client.ListOperationsAsync(), - } - ), - "get-operation" => await WriteJsonAsync( - output, - new - { - status = "success", - operation = await client.GetOperationAsync( - GetRequiredArgument( - args, - "--operation-id", - "operation get requires --id." - ) - ), - } - ), - "get-operation-output" => await WriteJsonAsync( - output, - new - { - status = "success", - output = await client.GetOperationOutputAsync( - GetRequiredArgument( - args, - "--operation-id", - "operation output requires --id." - ), - GetOptionalIntArgument(args, "--tail") + "list-operations" => await WriteWrappedJsonAsync( + output, + "operations", + await client.ListOperationsAsync() + ), + "get-operation" => await WriteWrappedJsonAsync( + output, + "operation", + await client.GetOperationAsync( + GetRequiredArgument( + args, + "--operation-id", + "operation get requires --id." + ) + ) + ), + "get-operation-output" => await WriteWrappedJsonAsync( + output, + "output", + await client.GetOperationOutputAsync( + GetRequiredArgument( + args, + "--operation-id", + "operation output requires --id." ), - } - ), - "wait-operation" => await WriteJsonAsync( - output, - new - { - status = "success", - operation = await client.WaitForOperationAsync( - GetRequiredArgument( - args, - "--operation-id", - "operation wait requires --id." - ), - GetOptionalIntArgument(args, "--timeout") ?? 300, - ((GetOptionalIntArgument(args, "--delay") ?? 1) * 1000) + GetOptionalIntArgument(args, "--tail") + ) + ), + "wait-operation" => await WriteWrappedJsonAsync( + output, + "operation", + await client.WaitForOperationAsync( + GetRequiredArgument( + args, + "--operation-id", + "operation wait requires --id." ), - } + GetOptionalIntArgument(args, "--timeout") ?? 300, + ((GetOptionalIntArgument(args, "--delay") ?? 1) * 1000) + ) ), "cancel-operation" => await WriteJsonAsync( output, @@ -163,27 +149,21 @@ await client.ForgetOperationAsync( ) ) ), - "list-managers" => await WriteJsonAsync( - output, - new - { - status = "success", - managers = await client.ListManagersAsync(), - } - ), - "get-manager-maintenance" => await WriteJsonAsync( - output, - new - { - status = "success", - maintenance = await client.GetManagerMaintenanceAsync( - GetRequiredArgument( - args, - "--manager", - "manager maintenance requires --manager." - ) - ), - } + "list-managers" => await WriteWrappedJsonAsync( + output, + "managers", + await client.ListManagersAsync() + ), + "get-manager-maintenance" => await WriteWrappedJsonAsync( + output, + "maintenance", + await client.GetManagerMaintenanceAsync( + GetRequiredArgument( + args, + "--manager", + "manager maintenance requires --manager." + ) + ) ), "reload-manager" => await WriteJsonAsync( output, @@ -205,13 +185,10 @@ await client.RunManagerActionAsync( BuildManagerMaintenanceRequest(args, requireAction: true) ) ), - "list-sources" => await WriteJsonAsync( + "list-sources" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - sources = await client.ListSourcesAsync(GetOptionalArgument(args, "--manager")), - } + "sources", + await client.ListSourcesAsync(GetOptionalArgument(args, "--manager")) ), "add-source" => await WriteJsonAsync( output, @@ -221,114 +198,80 @@ await client.AddSourceAsync(BuildSourceRequest(args)) output, await client.RemoveSourceAsync(BuildSourceRequest(args)) ), - "list-settings" => await WriteJsonAsync( + "list-settings" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - settings = await client.ListSettingsAsync(), - } + "settings", + await client.ListSettingsAsync() ), - "list-secure-settings" => await WriteJsonAsync( + "list-secure-settings" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - settings = await client.ListSecureSettingsAsync( - GetOptionalArgument(args, "--user") - ), - } - ), - "get-secure-setting" => await WriteJsonAsync( - output, - new - { - status = "success", - setting = await client.GetSecureSettingAsync( - GetRequiredArgument( - args, - "--key", - "settings secure get requires --key." - ), - GetOptionalArgument(args, "--user") - ), - } + "settings", + await client.ListSecureSettingsAsync(GetOptionalArgument(args, "--user")) ), - "set-secure-setting" => await WriteJsonAsync( + "get-secure-setting" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - setting = await client.SetSecureSettingAsync( - BuildSecureSettingRequest(args) - ), - } - ), - "get-setting" => await WriteJsonAsync( - output, - new - { - status = "success", - setting = await client.GetSettingAsync( - GetRequiredArgument( - args, - "--key", - "settings get requires --key." - ) - ), - } - ), - "set-setting" => await WriteJsonAsync( - output, - new - { - status = "success", - setting = await client.SetSettingAsync(BuildSettingRequest(args)), - } - ), - "clear-setting" => await WriteJsonAsync( - output, - new - { - status = "success", - setting = await client.ClearSettingAsync( - GetRequiredArgument( - args, - "--key", - "settings clear requires --key." - ) + "setting", + await client.GetSecureSettingAsync( + GetRequiredArgument( + args, + "--key", + "settings secure get requires --key." ), - } + GetOptionalArgument(args, "--user") + ) ), - "set-manager-enabled" => await WriteJsonAsync( + "set-secure-setting" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - manager = await client.SetManagerEnabledAsync(BuildManagerToggleRequest(args)), - } + "setting", + await client.SetSecureSettingAsync(BuildSecureSettingRequest(args)) ), - "set-manager-update-notifications" => await WriteJsonAsync( + "get-setting" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - manager = await client.SetManagerUpdateNotificationsAsync( - BuildManagerToggleRequest(args) - ), - } + "setting", + await client.GetSettingAsync( + GetRequiredArgument( + args, + "--key", + "settings get requires --key." + ) + ) + ), + "set-setting" => await WriteWrappedJsonAsync( + output, + "setting", + await client.SetSettingAsync(BuildSettingRequest(args)) + ), + "clear-setting" => await WriteWrappedJsonAsync( + output, + "setting", + await client.ClearSettingAsync( + GetRequiredArgument( + args, + "--key", + "settings clear requires --key." + ) + ) + ), + "set-manager-enabled" => await WriteWrappedJsonAsync( + output, + "manager", + await client.SetManagerEnabledAsync(BuildManagerToggleRequest(args)) + ), + "set-manager-update-notifications" => await WriteWrappedJsonAsync( + output, + "manager", + await client.SetManagerUpdateNotificationsAsync( + BuildManagerToggleRequest(args) + ) ), "reset-settings" => await WriteJsonAsync( output, await client.ResetSettingsAsync() ), - "list-desktop-shortcuts" => await WriteJsonAsync( + "list-desktop-shortcuts" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - shortcuts = await client.ListDesktopShortcutsAsync(), - } + "shortcuts", + await client.ListDesktopShortcutsAsync() ), "set-desktop-shortcut" => await WriteJsonAsync( output, @@ -348,40 +291,28 @@ await client.ResetDesktopShortcutAsync( output, await client.ResetDesktopShortcutsAsync() ), - "get-app-log" => await WriteJsonAsync( + "get-app-log" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - entries = await client.GetAppLogAsync(GetOptionalIntArgument(args, "--level") ?? 4), - } + "entries", + await client.GetAppLogAsync(GetOptionalIntArgument(args, "--level") ?? 4) ), - "get-operation-history" => await WriteJsonAsync( + "get-operation-history" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - history = await client.GetOperationHistoryAsync(), - } + "history", + await client.GetOperationHistoryAsync() ), - "get-manager-log" => await WriteJsonAsync( + "get-manager-log" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - managers = await client.GetManagerLogAsync( - GetOptionalArgument(args, "--manager"), - args.Contains("--verbose") - ), - } + "managers", + await client.GetManagerLogAsync( + GetOptionalArgument(args, "--manager"), + args.Contains("--verbose") + ) ), - "get-backup-status" => await WriteJsonAsync( + "get-backup-status" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - backup = await client.GetBackupStatusAsync(), - } + "backup", + await client.GetBackupStatusAsync() ), "create-local-backup" => await WriteJsonAsync( output, @@ -399,13 +330,10 @@ await client.CompleteGitHubDeviceFlowAsync() output, await client.SignOutGitHubAsync() ), - "list-cloud-backups" => await WriteJsonAsync( + "list-cloud-backups" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - backups = await client.ListCloudBackupsAsync(), - } + "backups", + await client.ListCloudBackupsAsync() ), "create-cloud-backup" => await WriteJsonAsync( output, @@ -419,13 +347,10 @@ await client.DownloadCloudBackupAsync(BuildCloudBackupRequest(args)) output, await client.RestoreCloudBackupAsync(BuildCloudBackupRequest(args)) ), - "get-bundle" => await WriteJsonAsync( + "get-bundle" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - bundle = await client.GetBundleAsync(), - } + "bundle", + await client.GetBundleAsync() ), "reset-bundle" => await WriteJsonAsync( output, @@ -451,73 +376,52 @@ await client.RemoveBundlePackageAsync(BuildBundlePackageRequest(args)) output, await client.InstallBundleAsync(BuildBundleInstallRequest(args)) ), - "get-version" => await WriteJsonAsync( + "get-version" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - build = await client.GetVersionAsync(), - } + "build", + await client.GetVersionAsync() ), - "get-updates" => await WriteJsonAsync( + "get-updates" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - updates = await client.ListUpgradablePackagesAsync( - GetOptionalArgument(args, "--manager") - ), - } + "updates", + await client.ListUpgradablePackagesAsync( + GetOptionalArgument(args, "--manager") + ) ), - "list-installed" => await WriteJsonAsync( + "list-installed" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - packages = await client.ListInstalledPackagesAsync( - GetOptionalArgument(args, "--manager") - ), - } - ), - "search-packages" => await WriteJsonAsync( - output, - new - { - status = "success", - packages = await client.SearchPackagesAsync( - GetRequiredArgument( - args, - "--query", - "package search requires --query." - ), - GetOptionalArgument(args, "--manager"), - GetOptionalIntArgument(args, "--max-results") + "packages", + await client.ListInstalledPackagesAsync( + GetOptionalArgument(args, "--manager") + ) + ), + "search-packages" => await WriteWrappedJsonAsync( + output, + "packages", + await client.SearchPackagesAsync( + GetRequiredArgument( + args, + "--query", + "package search requires --query." ), - } + GetOptionalArgument(args, "--manager"), + GetOptionalIntArgument(args, "--max-results") + ) ), - "package-details" => await WriteJsonAsync( + "package-details" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - package = await client.GetPackageDetailsAsync(BuildPackageActionRequest(args)), - } + "package", + await client.GetPackageDetailsAsync(BuildPackageActionRequest(args)) ), - "package-versions" => await WriteJsonAsync( + "package-versions" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - versions = await client.GetPackageVersionsAsync(BuildPackageActionRequest(args)), - } + "versions", + await client.GetPackageVersionsAsync(BuildPackageActionRequest(args)) ), - "list-ignored-updates" => await WriteJsonAsync( + "list-ignored-updates" => await WriteWrappedJsonAsync( output, - new - { - status = "success", - ignoredUpdates = await client.ListIgnoredUpdatesAsync(), - } + "ignoredUpdates", + await client.ListIgnoredUpdatesAsync() ), "ignore-package" => await WriteJsonAsync( output, @@ -929,18 +833,26 @@ private static bool GetRequiredBoolArgument(IReadOnlyList arguments, str private static async Task WriteJsonAsync(TextWriter output, T value) { await output.WriteLineAsync( - JsonSerializer.Serialize( - value, - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } - ) + IpcJson.Serialize(value) ); return (int)IpcCliExitCode.Success; } + private static async Task WriteWrappedJsonAsync( + TextWriter output, + string propertyName, + T value + ) + { + var response = new JsonObject + { + ["status"] = "success", + [propertyName] = JsonNode.Parse(IpcJson.Serialize(value)), + }; + await output.WriteLineAsync(response.ToJsonString(IpcJson.Options)); + return (int)IpcCliExitCode.Success; + } + private static async Task WriteErrorAsync( TextWriter output, string message, @@ -948,14 +860,7 @@ IpcCliExitCode exitCode ) { await output.WriteLineAsync( - JsonSerializer.Serialize( - new { status = "error", message }, - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } - ) + IpcJson.Serialize(new IpcCommandResult { Status = "error", Message = message }) ); return (int)exitCode; } diff --git a/src/UniGetUI.Interface.IpcApi/IpcClient.cs b/src/UniGetUI.Interface.IpcApi/IpcClient.cs index 8a06b6a31..9941b4c35 100644 --- a/src/UniGetUI.Interface.IpcApi/IpcClient.cs +++ b/src/UniGetUI.Interface.IpcApi/IpcClient.cs @@ -80,14 +80,7 @@ public async Task GetStatusAsync() try { string json = await SendAsync(HttpMethod.Get, IpcHttpRoutes.Path("/status")); - var status = JsonSerializer.Deserialize( - json, - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } - ); + var status = IpcJson.Deserialize(json); if (status is not null) { return status; @@ -1098,14 +1091,7 @@ private async Task SendAsync( ) { string json = await SendAuthenticatedAsync(method, relativePath, queryParameters); - return JsonSerializer.Deserialize( - json, - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } - ); + return IpcJson.Deserialize(json); } private async Task ReadAuthenticatedJsonWithBodyAsync( @@ -1114,23 +1100,9 @@ private async Task SendAsync( IReadOnlyDictionary? queryParameters = null ) { - using var content = JsonContent.Create( - body, - options: new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } - ); + using var content = IpcJson.CreateContent(body); string json = await SendAuthenticatedAsync(HttpMethod.Post, relativePath, queryParameters, content); - return JsonSerializer.Deserialize( - json, - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } - ); + return IpcJson.Deserialize(json); } private async Task SendPackageOperationAsync( diff --git a/src/UniGetUI.Interface.IpcApi/IpcJson.cs b/src/UniGetUI.Interface.IpcApi/IpcJson.cs new file mode 100644 index 000000000..b77eec37d --- /dev/null +++ b/src/UniGetUI.Interface.IpcApi/IpcJson.cs @@ -0,0 +1,149 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using Microsoft.AspNetCore.Http; + +namespace UniGetUI.Interface; + +internal static class IpcJson +{ + public static JsonSerializerOptions Options { get; } = new(JsonSerializerDefaults.Web) + { + WriteIndented = true, + TypeInfoResolver = IpcJsonContext.Default, + }; + + public static string Serialize(T value) + { + return JsonSerializer.Serialize(value, GetTypeInfo()); + } + + public static T? Deserialize(string json) + { + return JsonSerializer.Deserialize(json, GetTypeInfo()); + } + + public static HttpContent CreateContent(T value) + { + return new StringContent( + Serialize(value), + Encoding.UTF8, + "application/json" + ); + } + + public static Task WriteAsync(HttpResponse response, T value) + { + response.ContentType = "application/json; charset=utf-8"; + return response.WriteAsync(Serialize(value)); + } + + private static JsonTypeInfo GetTypeInfo() + { + return (JsonTypeInfo?)IpcJsonContext.Default.GetTypeInfo(typeof(T)) + ?? throw new InvalidOperationException( + $"IPC JSON metadata for {typeof(T).FullName} was not generated." + ); + } +} + +[JsonSourceGenerationOptions( + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = true +)] +[JsonSerializable(typeof(IpcAppInfo))] +[JsonSerializable(typeof(IpcAppNavigateRequest))] +[JsonSerializable(typeof(IpcBackupStatus))] +[JsonSerializable(typeof(IpcGitHubAuthInfo))] +[JsonSerializable(typeof(IpcBackupCommandResult))] +[JsonSerializable(typeof(IpcLocalBackupResult))] +[JsonSerializable(typeof(IpcCloudBackupEntry))] +[JsonSerializable(typeof(IpcCloudBackupUploadResult))] +[JsonSerializable(typeof(IpcCloudBackupRequest))] +[JsonSerializable(typeof(IpcCloudBackupContentResult))] +[JsonSerializable(typeof(IpcCloudBackupRestoreResult))] +[JsonSerializable(typeof(IpcGitHubDeviceFlowRequest))] +[JsonSerializable(typeof(IpcGitHubAuthResult))] +[JsonSerializable(typeof(IpcBundleInfo))] +[JsonSerializable(typeof(IpcBundlePackageInfo))] +[JsonSerializable(typeof(IpcBundleImportRequest))] +[JsonSerializable(typeof(IpcBundleExportRequest))] +[JsonSerializable(typeof(IpcBundlePackageRequest))] +[JsonSerializable(typeof(IpcBundleInstallRequest))] +[JsonSerializable(typeof(IpcBundleSecurityEntry))] +[JsonSerializable(typeof(IpcBundleCommandResult))] +[JsonSerializable(typeof(IpcBundleImportResult))] +[JsonSerializable(typeof(IpcBundleExportResult))] +[JsonSerializable(typeof(IpcBundlePackageOperationResult))] +[JsonSerializable(typeof(IpcBundleInstallResult))] +[JsonSerializable(typeof(IpcCommandResult))] +[JsonSerializable(typeof(IpcDesktopShortcutInfo))] +[JsonSerializable(typeof(IpcDesktopShortcutRequest))] +[JsonSerializable(typeof(IpcDesktopShortcutOperationResult))] +[JsonSerializable(typeof(IpcAppLogEntry))] +[JsonSerializable(typeof(IpcOperationHistoryEntry))] +[JsonSerializable(typeof(IpcManagerLogTask))] +[JsonSerializable(typeof(IpcManagerLogInfo))] +[JsonSerializable(typeof(IpcManagerMaintenanceInfo))] +[JsonSerializable(typeof(IpcManagerMaintenanceRequest))] +[JsonSerializable(typeof(IpcManagerMaintenanceActionResult))] +[JsonSerializable(typeof(IpcManagerCapabilitiesInfo))] +[JsonSerializable(typeof(IpcManagerInfo))] +[JsonSerializable(typeof(IpcSourceInfo))] +[JsonSerializable(typeof(IpcSourceRequest))] +[JsonSerializable(typeof(IpcSourceOperationResult))] +[JsonSerializable(typeof(IpcSettingInfo))] +[JsonSerializable(typeof(IpcSettingValueRequest))] +[JsonSerializable(typeof(IpcManagerToggleRequest))] +[JsonSerializable(typeof(IpcOperationOutputLine))] +[JsonSerializable(typeof(IpcOperationInfo))] +[JsonSerializable(typeof(IpcOperationDetails))] +[JsonSerializable(typeof(IpcOperationOutputResult))] +[JsonSerializable(typeof(IpcPackageInfo))] +[JsonSerializable(typeof(IpcPackageActionRequest))] +[JsonSerializable(typeof(IpcPackageOperationResult))] +[JsonSerializable(typeof(IpcPackageDependencyInfo))] +[JsonSerializable(typeof(IpcPackageDetailsInfo))] +[JsonSerializable(typeof(IpcIgnoredUpdateInfo))] +[JsonSerializable(typeof(IpcSecureSettingInfo))] +[JsonSerializable(typeof(IpcSecureSettingRequest))] +[JsonSerializable(typeof(IpcStatus))] +[JsonSerializable(typeof(IpcEndpointRegistration))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(string[]))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(IpcManagerInfo[]))] +[JsonSerializable(typeof(IpcSourceInfo[]))] +[JsonSerializable(typeof(IpcSettingInfo[]))] +[JsonSerializable(typeof(IpcSecureSettingInfo[]))] +[JsonSerializable(typeof(IpcDesktopShortcutInfo[]))] +[JsonSerializable(typeof(IpcAppLogEntry[]))] +[JsonSerializable(typeof(IpcOperationHistoryEntry[]))] +[JsonSerializable(typeof(IpcManagerLogInfo[]))] +[JsonSerializable(typeof(IpcOperationInfo[]))] +[JsonSerializable(typeof(IpcOperationOutputLine[]))] +[JsonSerializable(typeof(IpcPackageInfo[]))] +[JsonSerializable(typeof(IpcPackageDependencyInfo[]))] +[JsonSerializable(typeof(IpcIgnoredUpdateInfo[]))] +[JsonSerializable(typeof(IpcBundlePackageInfo[]))] +[JsonSerializable(typeof(IpcBundleSecurityEntry[]))] +[JsonSerializable(typeof(IpcPackageOperationResult[]))] +[JsonSerializable(typeof(IpcCloudBackupEntry[]))] +internal sealed partial class IpcJsonContext : JsonSerializerContext; diff --git a/src/UniGetUI.Interface.IpcApi/IpcServer.cs b/src/UniGetUI.Interface.IpcApi/IpcServer.cs index d8137e625..b4b8aca15 100644 --- a/src/UniGetUI.Interface.IpcApi/IpcServer.cs +++ b/src/UniGetUI.Interface.IpcApi/IpcServer.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Builder; @@ -23,6 +24,10 @@ internal static class ApiTokenHolder public static string Token = ""; } + [UnconditionalSuppressMessage( + "Trimming", + "IL2026", + Justification = "IPC JSON metadata is supplied by IpcJsonContext; remaining ASP.NET Core JSON overload warnings are guarded by that generated resolver.")] public class IpcServer { public string SessionId { get; } = Guid.NewGuid().ToString("N"); @@ -56,233 +61,228 @@ public async Task Start() ApiTokenHolder.Token = CoreTools.RandomString(64); Logger.Info("Generated a IPC API auth token for the current session"); - var builder = Host.CreateDefaultBuilder(); - builder.ConfigureServices(services => services.AddCors()); - builder.ConfigureWebHostDefaults(webBuilder => + var builder = WebApplication.CreateSlimBuilder(new WebApplicationOptions { - webBuilder.UseKestrel(serverOptions => ConfigureTransport(serverOptions)); + Args = [], + ApplicationName = typeof(IpcServer).Assembly.FullName, + }); + builder.Services.AddCors(); + builder.WebHost.UseKestrel(ConfigureTransport); #if !DEBUG - webBuilder.SuppressStatusMessages(true); + builder.WebHost.UseSetting(WebHostDefaults.SuppressStatusMessagesKey, "true"); #endif - webBuilder.Configure(app => - { - app.UseCors(policy => - policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader() - ); - - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapGet(IpcHttpRoutes.Path("/status"), V3_Status); - endpoints.MapGet(IpcHttpRoutes.Path("/app"), V3_GetAppInfo); - endpoints.MapPost(IpcHttpRoutes.Path("/app/show"), V3_ShowApp); - endpoints.MapPost(IpcHttpRoutes.Path("/app/navigate"), V3_NavigateApp); - endpoints.MapPost(IpcHttpRoutes.Path("/app/quit"), V3_QuitApp); - endpoints.MapGet(IpcHttpRoutes.Path("/operations"), V3_ListOperations); - endpoints.MapGet( - IpcHttpRoutes.Path("/operations/{operationId}"), - V3_GetOperation - ); - endpoints.MapGet( - IpcHttpRoutes.Path("/operations/{operationId}/output"), - V3_GetOperationOutput - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/operations/{operationId}/cancel"), - V3_CancelOperation - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/operations/{operationId}/retry"), - V3_RetryOperation - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/operations/{operationId}/reorder"), - V3_ReorderOperation - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/operations/{operationId}/forget"), - V3_ForgetOperation - ); - endpoints.MapGet(IpcHttpRoutes.Path("/managers"), V3_ListManagers); - endpoints.MapGet( - IpcHttpRoutes.Path("/managers/maintenance"), - V3_GetManagerMaintenance - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/managers/maintenance/reload"), - V3_ReloadManager - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/managers/maintenance/executable/set"), - V3_SetManagerExecutable - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/managers/maintenance/executable/clear"), - V3_ClearManagerExecutable - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/managers/maintenance/action"), - V3_RunManagerAction - ); - endpoints.MapGet(IpcHttpRoutes.Path("/sources"), V3_ListSources); - endpoints.MapPost(IpcHttpRoutes.Path("/sources/add"), V3_AddSource); - endpoints.MapPost(IpcHttpRoutes.Path("/sources/remove"), V3_RemoveSource); - endpoints.MapGet(IpcHttpRoutes.Path("/settings"), V3_ListSettings); - endpoints.MapGet(IpcHttpRoutes.Path("/settings/item"), V3_GetSetting); - endpoints.MapPost(IpcHttpRoutes.Path("/settings/set"), V3_SetSetting); - endpoints.MapPost(IpcHttpRoutes.Path("/settings/clear"), V3_ClearSetting); - endpoints.MapPost(IpcHttpRoutes.Path("/settings/reset"), V3_ResetSettings); - endpoints.MapGet( - IpcHttpRoutes.Path("/secure-settings"), - V3_ListSecureSettings - ); - endpoints.MapGet( - IpcHttpRoutes.Path("/secure-settings/item"), - V3_GetSecureSetting - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/secure-settings/set"), - V3_SetSecureSetting - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/managers/set-enabled"), - V3_SetManagerEnabled - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/managers/set-update-notifications"), - V3_SetManagerUpdateNotifications - ); - endpoints.MapGet( - IpcHttpRoutes.Path("/desktop-shortcuts"), - V3_ListDesktopShortcuts - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/desktop-shortcuts/set"), - V3_SetDesktopShortcut - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/desktop-shortcuts/reset"), - V3_ResetDesktopShortcut - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/desktop-shortcuts/reset-all"), - V3_ResetDesktopShortcuts - ); - endpoints.MapGet(IpcHttpRoutes.Path("/logs/app"), V3_GetAppLog); - endpoints.MapGet( - IpcHttpRoutes.Path("/logs/history"), - V3_GetOperationHistory - ); - endpoints.MapGet(IpcHttpRoutes.Path("/logs/manager"), V3_GetManagerLog); - endpoints.MapGet(IpcHttpRoutes.Path("/backups/status"), V3_GetBackupStatus); - endpoints.MapPost( - IpcHttpRoutes.Path("/backups/local/create"), - V3_CreateLocalBackup - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/backups/github/sign-in/start"), - V3_StartGitHubDeviceFlow - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/backups/github/sign-in/complete"), - V3_CompleteGitHubDeviceFlow - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/backups/github/sign-out"), - V3_SignOutGitHub - ); - endpoints.MapGet( - IpcHttpRoutes.Path("/backups/cloud"), - V3_ListCloudBackups - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/backups/cloud/create"), - V3_CreateCloudBackup - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/backups/cloud/download"), - V3_DownloadCloudBackup - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/backups/cloud/restore"), - V3_RestoreCloudBackup - ); - endpoints.MapGet(IpcHttpRoutes.Path("/bundles"), V3_GetBundle); - endpoints.MapPost(IpcHttpRoutes.Path("/bundles/reset"), V3_ResetBundle); - endpoints.MapPost(IpcHttpRoutes.Path("/bundles/import"), V3_ImportBundle); - endpoints.MapPost(IpcHttpRoutes.Path("/bundles/export"), V3_ExportBundle); - endpoints.MapPost(IpcHttpRoutes.Path("/bundles/add"), V3_AddBundlePackage); - endpoints.MapPost( - IpcHttpRoutes.Path("/bundles/remove"), - V3_RemoveBundlePackage - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/bundles/install"), - V3_InstallBundle - ); - endpoints.MapGet( - IpcHttpRoutes.Path("/packages/search"), - V3_SearchPackages - ); - endpoints.MapGet( - IpcHttpRoutes.Path("/packages/installed"), - V3_ListInstalledPackages - ); - endpoints.MapGet( - IpcHttpRoutes.Path("/packages/updates"), - V3_ListUpgradablePackages - ); - endpoints.MapGet( - IpcHttpRoutes.Path("/packages/details"), - V3_GetPackageDetails - ); - endpoints.MapGet( - IpcHttpRoutes.Path("/packages/versions"), - V3_GetPackageVersions - ); - endpoints.MapGet( - IpcHttpRoutes.Path("/packages/ignored"), - V3_ListIgnoredUpdates - ); - endpoints.MapPost(IpcHttpRoutes.Path("/packages/ignore"), V3_IgnorePackage); - endpoints.MapPost( - IpcHttpRoutes.Path("/packages/unignore"), - V3_UnignorePackage - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/packages/download"), - V3_DownloadPackage - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/packages/install"), - V3_InstallPackage - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/packages/reinstall"), - V3_ReinstallPackage - ); - endpoints.MapPost(IpcHttpRoutes.Path("/packages/update"), V3_UpdatePackage); - endpoints.MapPost( - IpcHttpRoutes.Path("/packages/uninstall"), - V3_UninstallPackage - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/packages/uninstall-then-reinstall"), - V3_UninstallThenReinstallPackage - ); - endpoints.MapPost(IpcHttpRoutes.Path("/packages/show"), V3_ShowPackage); - endpoints.MapPost( - IpcHttpRoutes.Path("/packages/update-all"), - V3_UpdateAllPackages - ); - endpoints.MapPost( - IpcHttpRoutes.Path("/packages/update-manager"), - V3_UpdateAllPackagesForManager - ); - }); - }); - }); - _host = builder.Build(); + var app = builder.Build(); + app.UseCors(policy => + policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader() + ); + var endpoints = app; + endpoints.MapGet(IpcHttpRoutes.Path("/status"), V3_Status); + endpoints.MapGet(IpcHttpRoutes.Path("/app"), V3_GetAppInfo); + endpoints.MapPost(IpcHttpRoutes.Path("/app/show"), V3_ShowApp); + endpoints.MapPost(IpcHttpRoutes.Path("/app/navigate"), V3_NavigateApp); + endpoints.MapPost(IpcHttpRoutes.Path("/app/quit"), V3_QuitApp); + endpoints.MapGet(IpcHttpRoutes.Path("/operations"), V3_ListOperations); + endpoints.MapGet( + IpcHttpRoutes.Path("/operations/{operationId}"), + V3_GetOperation + ); + endpoints.MapGet( + IpcHttpRoutes.Path("/operations/{operationId}/output"), + V3_GetOperationOutput + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/operations/{operationId}/cancel"), + V3_CancelOperation + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/operations/{operationId}/retry"), + V3_RetryOperation + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/operations/{operationId}/reorder"), + V3_ReorderOperation + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/operations/{operationId}/forget"), + V3_ForgetOperation + ); + endpoints.MapGet(IpcHttpRoutes.Path("/managers"), V3_ListManagers); + endpoints.MapGet( + IpcHttpRoutes.Path("/managers/maintenance"), + V3_GetManagerMaintenance + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/managers/maintenance/reload"), + V3_ReloadManager + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/managers/maintenance/executable/set"), + V3_SetManagerExecutable + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/managers/maintenance/executable/clear"), + V3_ClearManagerExecutable + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/managers/maintenance/action"), + V3_RunManagerAction + ); + endpoints.MapGet(IpcHttpRoutes.Path("/sources"), V3_ListSources); + endpoints.MapPost(IpcHttpRoutes.Path("/sources/add"), V3_AddSource); + endpoints.MapPost(IpcHttpRoutes.Path("/sources/remove"), V3_RemoveSource); + endpoints.MapGet(IpcHttpRoutes.Path("/settings"), V3_ListSettings); + endpoints.MapGet(IpcHttpRoutes.Path("/settings/item"), V3_GetSetting); + endpoints.MapPost(IpcHttpRoutes.Path("/settings/set"), V3_SetSetting); + endpoints.MapPost(IpcHttpRoutes.Path("/settings/clear"), V3_ClearSetting); + endpoints.MapPost(IpcHttpRoutes.Path("/settings/reset"), V3_ResetSettings); + endpoints.MapGet( + IpcHttpRoutes.Path("/secure-settings"), + V3_ListSecureSettings + ); + endpoints.MapGet( + IpcHttpRoutes.Path("/secure-settings/item"), + V3_GetSecureSetting + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/secure-settings/set"), + V3_SetSecureSetting + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/managers/set-enabled"), + V3_SetManagerEnabled + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/managers/set-update-notifications"), + V3_SetManagerUpdateNotifications + ); + endpoints.MapGet( + IpcHttpRoutes.Path("/desktop-shortcuts"), + V3_ListDesktopShortcuts + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/desktop-shortcuts/set"), + V3_SetDesktopShortcut + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/desktop-shortcuts/reset"), + V3_ResetDesktopShortcut + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/desktop-shortcuts/reset-all"), + V3_ResetDesktopShortcuts + ); + endpoints.MapGet(IpcHttpRoutes.Path("/logs/app"), V3_GetAppLog); + endpoints.MapGet( + IpcHttpRoutes.Path("/logs/history"), + V3_GetOperationHistory + ); + endpoints.MapGet(IpcHttpRoutes.Path("/logs/manager"), V3_GetManagerLog); + endpoints.MapGet(IpcHttpRoutes.Path("/backups/status"), V3_GetBackupStatus); + endpoints.MapPost( + IpcHttpRoutes.Path("/backups/local/create"), + V3_CreateLocalBackup + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/backups/github/sign-in/start"), + V3_StartGitHubDeviceFlow + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/backups/github/sign-in/complete"), + V3_CompleteGitHubDeviceFlow + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/backups/github/sign-out"), + V3_SignOutGitHub + ); + endpoints.MapGet( + IpcHttpRoutes.Path("/backups/cloud"), + V3_ListCloudBackups + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/backups/cloud/create"), + V3_CreateCloudBackup + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/backups/cloud/download"), + V3_DownloadCloudBackup + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/backups/cloud/restore"), + V3_RestoreCloudBackup + ); + endpoints.MapGet(IpcHttpRoutes.Path("/bundles"), V3_GetBundle); + endpoints.MapPost(IpcHttpRoutes.Path("/bundles/reset"), V3_ResetBundle); + endpoints.MapPost(IpcHttpRoutes.Path("/bundles/import"), V3_ImportBundle); + endpoints.MapPost(IpcHttpRoutes.Path("/bundles/export"), V3_ExportBundle); + endpoints.MapPost(IpcHttpRoutes.Path("/bundles/add"), V3_AddBundlePackage); + endpoints.MapPost( + IpcHttpRoutes.Path("/bundles/remove"), + V3_RemoveBundlePackage + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/bundles/install"), + V3_InstallBundle + ); + endpoints.MapGet( + IpcHttpRoutes.Path("/packages/search"), + V3_SearchPackages + ); + endpoints.MapGet( + IpcHttpRoutes.Path("/packages/installed"), + V3_ListInstalledPackages + ); + endpoints.MapGet( + IpcHttpRoutes.Path("/packages/updates"), + V3_ListUpgradablePackages + ); + endpoints.MapGet( + IpcHttpRoutes.Path("/packages/details"), + V3_GetPackageDetails + ); + endpoints.MapGet( + IpcHttpRoutes.Path("/packages/versions"), + V3_GetPackageVersions + ); + endpoints.MapGet( + IpcHttpRoutes.Path("/packages/ignored"), + V3_ListIgnoredUpdates + ); + endpoints.MapPost(IpcHttpRoutes.Path("/packages/ignore"), V3_IgnorePackage); + endpoints.MapPost( + IpcHttpRoutes.Path("/packages/unignore"), + V3_UnignorePackage + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/packages/download"), + V3_DownloadPackage + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/packages/install"), + V3_InstallPackage + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/packages/reinstall"), + V3_ReinstallPackage + ); + endpoints.MapPost(IpcHttpRoutes.Path("/packages/update"), V3_UpdatePackage); + endpoints.MapPost( + IpcHttpRoutes.Path("/packages/uninstall"), + V3_UninstallPackage + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/packages/uninstall-then-reinstall"), + V3_UninstallThenReinstallPackage + ); + endpoints.MapPost(IpcHttpRoutes.Path("/packages/show"), V3_ShowPackage); + endpoints.MapPost( + IpcHttpRoutes.Path("/packages/update-all"), + V3_UpdateAllPackages + ); + endpoints.MapPost( + IpcHttpRoutes.Path("/packages/update-manager"), + V3_UpdateAllPackagesForManager + ); + _host = app; try { await _host.StartAsync(); @@ -444,11 +444,7 @@ await context.Response.WriteAsJsonAsync( Version = CoreData.VersionName, BuildNumber = CoreData.BuildNumber, }, - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } @@ -462,11 +458,7 @@ private async Task V3_ListManagers(HttpContext context) await context.Response.WriteAsJsonAsync( IpcManagerSettingsApi.ListManagers(), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } @@ -485,11 +477,7 @@ await context.Response.WriteAsJsonAsync( ?? throw new InvalidOperationException( "The application did not register an app-state provider." ), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -611,11 +599,7 @@ private async Task V3_ListSources(HttpContext context) { await context.Response.WriteAsJsonAsync( IpcManagerSettingsApi.ListSources(context.Request.Query["manager"]), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -645,11 +629,7 @@ private async Task V3_GetManagerMaintenance(HttpContext context) { await context.Response.WriteAsJsonAsync( IpcManagerMaintenanceApi.GetMaintenanceInfo(managerName), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -711,11 +691,7 @@ private async Task V3_ListSettings(HttpContext context) await context.Response.WriteAsJsonAsync( IpcManagerSettingsApi.ListSettings(), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } @@ -739,11 +715,7 @@ private async Task V3_GetSetting(HttpContext context) { await context.Response.WriteAsJsonAsync( IpcManagerSettingsApi.GetSetting(key), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -774,11 +746,7 @@ await context.Response.WriteAsJsonAsync( Value = GetOptionalQueryValue(context.Request, "value"), } ), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -808,11 +776,7 @@ private async Task V3_ClearSetting(HttpContext context) { await context.Response.WriteAsJsonAsync( IpcManagerSettingsApi.ClearSetting(key), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -833,11 +797,7 @@ private async Task V3_ResetSettings(HttpContext context) IpcManagerSettingsApi.ResetSettingsPreservingSession(); await context.Response.WriteAsJsonAsync( IpcCommandResult.Success("reset-settings"), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } @@ -851,11 +811,7 @@ private async Task V3_ListSecureSettings(HttpContext context) await context.Response.WriteAsJsonAsync( IpcSecureSettingsApi.ListSettings(context.Request.Query["user"]), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } @@ -874,11 +830,7 @@ await context.Response.WriteAsJsonAsync( GetRequiredQueryValue(context, "key"), GetOptionalQueryValue(context.Request, "user") ), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -914,11 +866,7 @@ await IpcSecureSettingsApi.SetSettingAsync( Enabled = enabled, } ), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -953,11 +901,7 @@ await IpcManagerSettingsApi.SetManagerEnabledAsync( Enabled = enabled, } ), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -992,11 +936,7 @@ await context.Response.WriteAsJsonAsync( Enabled = enabled, } ), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1016,11 +956,7 @@ private async Task V3_ListDesktopShortcuts(HttpContext context) await context.Response.WriteAsJsonAsync( IpcDesktopShortcutsApi.ListShortcuts(), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } @@ -1042,11 +978,7 @@ await context.Response.WriteAsJsonAsync( Status = GetOptionalQueryValue(context.Request, "status"), } ), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1073,11 +1005,7 @@ await context.Response.WriteAsJsonAsync( Path = GetRequiredQueryValue(context, "path"), } ), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1097,11 +1025,7 @@ private async Task V3_ResetDesktopShortcuts(HttpContext context) await context.Response.WriteAsJsonAsync( IpcDesktopShortcutsApi.ResetAllShortcuts(), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } @@ -1118,11 +1042,7 @@ private async Task V3_GetAppLog(HttpContext context) : 4; await context.Response.WriteAsJsonAsync( IpcLogsApi.ListAppLog(level), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } @@ -1136,11 +1056,7 @@ private async Task V3_GetOperationHistory(HttpContext context) await context.Response.WriteAsJsonAsync( IpcLogsApi.ListOperationHistory(), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } @@ -1159,11 +1075,7 @@ await context.Response.WriteAsJsonAsync( context.Request.Query["manager"], bool.TryParse(context.Request.Query["verbose"], out bool verbose) && verbose ), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1183,11 +1095,7 @@ private async Task V3_GetBackupStatus(HttpContext context) await context.Response.WriteAsJsonAsync( await IpcBackupApi.GetStatusAsync(), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } @@ -1203,11 +1111,7 @@ private async Task V3_CreateLocalBackup(HttpContext context) { await context.Response.WriteAsJsonAsync( await IpcBackupApi.CreateLocalBackupAsync(), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1237,11 +1141,7 @@ private async Task V3_CompleteGitHubDeviceFlow(HttpContext context) { await context.Response.WriteAsJsonAsync( await IpcBackupApi.CompleteGitHubDeviceFlowAsync(), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1261,11 +1161,7 @@ private async Task V3_SignOutGitHub(HttpContext context) await context.Response.WriteAsJsonAsync( await IpcBackupApi.SignOutGitHubAsync(), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } @@ -1281,11 +1177,7 @@ private async Task V3_ListCloudBackups(HttpContext context) { await context.Response.WriteAsJsonAsync( await IpcBackupApi.ListCloudBackupsAsync(), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1307,11 +1199,7 @@ private async Task V3_CreateCloudBackup(HttpContext context) { await context.Response.WriteAsJsonAsync( await IpcBackupApi.CreateCloudBackupAsync(), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1349,11 +1237,7 @@ private async Task V3_GetBundle(HttpContext context) { await context.Response.WriteAsJsonAsync( await IpcBundleApi.GetCurrentBundleAsync(), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1375,11 +1259,7 @@ private async Task V3_ResetBundle(HttpContext context) { await context.Response.WriteAsJsonAsync( IpcBundleApi.ResetBundle(), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1459,11 +1339,7 @@ private async Task V3_SearchPackages(HttpContext context) { await context.Response.WriteAsJsonAsync( IpcPackageApi.SearchPackages(query, manager, maxResults), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1485,11 +1361,7 @@ private async Task V3_ListInstalledPackages(HttpContext context) { await context.Response.WriteAsJsonAsync( IpcPackageApi.ListInstalledPackages(context.Request.Query["manager"]), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1511,11 +1383,7 @@ private async Task V3_ListUpgradablePackages(HttpContext context) { await context.Response.WriteAsJsonAsync( IpcPackageApi.ListUpgradablePackages(context.Request.Query["manager"]), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1547,11 +1415,7 @@ await context.Response.WriteAsJsonAsync( await IpcPackageApi.GetPackageDetailsAsync( BuildPackageActionRequest(context.Request) ), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1581,11 +1445,7 @@ private async Task V3_GetPackageVersions(HttpContext context) { await context.Response.WriteAsJsonAsync( IpcPackageApi.GetPackageVersions(BuildPackageActionRequest(context.Request)), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1605,11 +1465,7 @@ private async Task V3_ListIgnoredUpdates(HttpContext context) await context.Response.WriteAsJsonAsync( IpcPackageApi.ListIgnoredUpdates(), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } @@ -1694,11 +1550,7 @@ await context.Response.WriteAsJsonAsync( ?? throw new InvalidOperationException( "The current UniGetUI session cannot open package details." ), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1772,11 +1624,7 @@ Func> action await context.Response.WriteAsJsonAsync( await action(request), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1798,11 +1646,7 @@ private static async Task HandleReadAsync(HttpContext context, Func action { await context.Response.WriteAsJsonAsync( action(), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1827,11 +1671,7 @@ Func action { await context.Response.WriteAsJsonAsync( action(), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1864,11 +1704,7 @@ Func> action { await context.Response.WriteAsJsonAsync( await action(BuildPackageActionRequest(context.Request)), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1900,11 +1736,7 @@ await action( SourceUrl = GetOptionalQueryValue(context.Request, "url"), } ), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1929,11 +1761,7 @@ Func> action { await context.Response.WriteAsJsonAsync( await action(await ReadJsonBodyAsync(context)), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1958,11 +1786,7 @@ Func> action { await context.Response.WriteAsJsonAsync( await action(await ReadJsonBodyAsync(context)), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -1987,11 +1811,7 @@ Func> action { await context.Response.WriteAsJsonAsync( await action(await ReadJsonBodyAsync(context)), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + IpcJson.Options ); } catch (InvalidOperationException ex) @@ -2003,13 +1823,8 @@ await action(await ReadJsonBodyAsync(context)), private static async Task ReadJsonBodyAsync(HttpContext context) { - var request = await context.Request.ReadFromJsonAsync( - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } - ); + using var reader = new StreamReader(context.Request.Body, Encoding.UTF8); + var request = IpcJson.Deserialize(await reader.ReadToEndAsync()); return request ?? throw new InvalidOperationException("The request body is required."); } diff --git a/src/UniGetUI.Interface.IpcApi/IpcTransport.cs b/src/UniGetUI.Interface.IpcApi/IpcTransport.cs index d15870a12..dbd92ea0b 100644 --- a/src/UniGetUI.Interface.IpcApi/IpcTransport.cs +++ b/src/UniGetUI.Interface.IpcApi/IpcTransport.cs @@ -108,14 +108,7 @@ public void Persist(string sessionId, string token, string sessionKind, int proc File.WriteAllText( GetEndpointMetadataPath(sessionId), - JsonSerializer.Serialize( - metadata, - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } - ) + IpcJson.Serialize(metadata) ); } @@ -155,13 +148,8 @@ internal static IReadOnlyList LoadPersistedRegistration { foreach (string file in Directory.GetFiles(EndpointMetadataDirectoryPath, "*.json")) { - var registration = JsonSerializer.Deserialize( - File.ReadAllText(file), - new JsonSerializerOptions(SerializationHelpers.DefaultOptions) - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - } + var registration = IpcJson.Deserialize( + File.ReadAllText(file) ); if (registration is not null) diff --git a/src/UniGetUI.PackageEngine.Managers.Cargo/CargoJson.cs b/src/UniGetUI.PackageEngine.Managers.Cargo/CargoJson.cs new file mode 100644 index 000000000..a7fbd9b32 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Cargo/CargoJson.cs @@ -0,0 +1,26 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace UniGetUI.PackageEngine.Managers.CargoManager; + +internal static class CargoJson +{ + public static T? Deserialize(string json) + { + return JsonSerializer.Deserialize(json, GetTypeInfo()); + } + + private static JsonTypeInfo GetTypeInfo() + { + return (JsonTypeInfo?)CargoJsonContext.Default.GetTypeInfo(typeof(T)) + ?? throw new InvalidOperationException( + $"Cargo JSON metadata for {typeof(T).FullName} was not generated." + ); + } +} + +[JsonSourceGenerationOptions(AllowTrailingCommas = true, WriteIndented = true)] +[JsonSerializable(typeof(CargoManifest))] +[JsonSerializable(typeof(CargoManifestVersionWrapper))] +internal sealed partial class CargoJsonContext : JsonSerializerContext; diff --git a/src/UniGetUI.PackageEngine.Managers.Cargo/CratesIOClient.cs b/src/UniGetUI.PackageEngine.Managers.Cargo/CratesIOClient.cs index 95504dc41..a03068fae 100644 --- a/src/UniGetUI.PackageEngine.Managers.Cargo/CratesIOClient.cs +++ b/src/UniGetUI.PackageEngine.Managers.Cargo/CratesIOClient.cs @@ -97,7 +97,7 @@ internal static T Fetch(Uri url) var manifestStr = client.GetStringAsync(url).GetAwaiter().GetResult(); var manifest = - JsonSerializer.Deserialize(manifestStr, options: SerializationHelpers.DefaultOptions) + CargoJson.Deserialize(manifestStr) ?? throw new NullResponseException($"Null response for request to {url}"); return manifest; } diff --git a/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs b/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs index fa4f4a4c6..1f7205ef8 100644 --- a/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs +++ b/src/UniGetUI.PackageEngine.Managers.WinGet/WinGet.cs @@ -336,14 +336,19 @@ internal static string GetBundledPingetExecutablePath( Func fileExists ) { - string rootPingetPath = Path.Join(executableDirectory, PingetExecutableName); + string installDirectory = CoreData.ResolveInstallationDirectory( + executableDirectory, + fileExists, + static _ => false + ); + string rootPingetPath = Path.Join(installDirectory, PingetExecutableName); if (fileExists(rootPingetPath)) { return rootPingetPath; } string avaloniaPingetPath = Path.Join( - executableDirectory, + installDirectory, "Avalonia", PingetExecutableName ); diff --git a/src/UniGetUI.PackageEngine.Serializable/SerializableComponent.cs b/src/UniGetUI.PackageEngine.Serializable/SerializableComponent.cs index 1e40f934d..974cf5f46 100644 --- a/src/UniGetUI.PackageEngine.Serializable/SerializableComponent.cs +++ b/src/UniGetUI.PackageEngine.Serializable/SerializableComponent.cs @@ -25,7 +25,7 @@ public abstract class SerializableComponent /// A pretty-formatted JSON string representing the current data public string AsJsonString() { - return JsonSerializer.Serialize(AsJsonNode(), SerializationHelpers.DefaultOptions); + return AsJsonNode().ToJsonString(SerializationHelpers.DefaultOptions); } /// diff --git a/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs index 38af5f653..ab3a12f28 100644 --- a/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/WinGetManagerTests.cs @@ -158,6 +158,21 @@ public void GetBundledPingetExecutablePathPrefersRootExecutable() Assert.Equal(rootPinget, path); } + [Fact] + public void GetBundledPingetExecutablePathFindsRootExecutableFromAvaloniaDirectory() + { + const string installDir = @"C:\Program Files\UniGetUI"; + string avaloniaDir = Path.Join(installDir, "Avalonia"); + string rootPinget = Path.Join(installDir, "pinget.exe"); + + string path = WinGet.GetBundledPingetExecutablePath( + avaloniaDir, + filePath => filePath == rootPinget + ); + + Assert.Equal(rootPinget, path); + } + [Fact] public void GetBundledPingetExecutablePathFallsBackToAvaloniaExecutable() { diff --git a/src/UniGetUI.Pinget.Cli/Program.cs b/src/UniGetUI.Pinget.Cli/Program.cs deleted file mode 100644 index 96cc5b959..000000000 --- a/src/UniGetUI.Pinget.Cli/Program.cs +++ /dev/null @@ -1,1884 +0,0 @@ -using System.CommandLine; -using System.Security.Cryptography; -using System.Text.Json; -using System.Text.Json.Nodes; -using Devolutions.Pinget.Core; -using YamlDotNet.Serialization; - -const string Version = "0.1.0"; -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(); - 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"); -outputOption.FromAmong("text", "json", "yaml"); -rootCommand.AddGlobalOption(outputOption); - -var SerializationOptions = new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - -var infoOption = new Option("--info", "Display general info"); -rootCommand.AddGlobalOption(infoOption); - -var disableInteractivityOption = new Option("--disable-interactivity", "Disable interactive prompts"); -rootCommand.AddGlobalOption(disableInteractivityOption); - -var acceptSourceAgreementsOption = new Option("--accept-source-agreements", "Accept source agreements"); -rootCommand.AddGlobalOption(acceptSourceAgreementsOption); - -// ── 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 sTagOpt = new Option("--tag", "Filter by tag"); -var sCmdOpt = new Option("--command", "Filter by command"); sCmdOpt.AddAlias("--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 }) - searchCommand.AddOption(o); -searchCommand.AddArgument(sqArg); - -searchCommand.SetHandler((ctx) => -{ - var output = GetOutputFormat(ctx.ParseResult.GetValueForOption(outputOption)); - var query = new PackageQuery - { - Query = ctx.ParseResult.GetValueForArgument(sqArg) ?? ctx.ParseResult.GetValueForOption(sqOpt), - Id = ctx.ParseResult.GetValueForOption(sidOpt), - Name = ctx.ParseResult.GetValueForOption(snOpt), - Moniker = ctx.ParseResult.GetValueForOption(smOpt), - Tag = ctx.ParseResult.GetValueForOption(sTagOpt), - Command = ctx.ParseResult.GetValueForOption(sCmdOpt), - Source = ctx.ParseResult.GetValueForOption(ssOpt), - Count = ctx.ParseResult.GetValueForOption(scOpt), - Exact = ctx.ParseResult.GetValueForOption(seOpt), - }; - - using var repo = Repository.Open(); - if (ctx.ParseResult.GetValueForOption(sManifestsOpt)) - { - if (output == OutputFormat.Text) - throw new InvalidOperationException("--manifests requires --output json or yaml"); - if (ctx.ParseResult.GetValueForOption(sVersionsOpt)) - throw new InvalidOperationException("--manifests cannot be combined with --versions"); - - 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); - } - else - { - var result = repo.Search(query); - if (output != OutputFormat.Text) WriteStructuredOutput(result, output); - else PrintSearch(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 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 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); -showCommand.AddArgument(shArg); - -showCommand.SetHandler((ctx) => -{ - var output = GetOutputFormat(ctx.ParseResult.GetValueForOption(outputOption)); - var query = new PackageQuery - { - Query = ctx.ParseResult.GetValueForArgument(shArg) ?? ctx.ParseResult.GetValueForOption(shqOpt), - Id = ctx.ParseResult.GetValueForOption(shidOpt), - Name = ctx.ParseResult.GetValueForOption(shnOpt), - Moniker = ctx.ParseResult.GetValueForOption(shmOpt), - Source = ctx.ParseResult.GetValueForOption(shsOpt), - Exact = ctx.ParseResult.GetValueForOption(sheOpt), - Version = ctx.ParseResult.GetValueForOption(shvOpt), - Locale = ctx.ParseResult.GetValueForOption(shLocaleOpt), - InstallerType = ctx.ParseResult.GetValueForOption(shTypeOpt), - InstallerArchitecture = ctx.ParseResult.GetValueForOption(shArchOpt), - InstallScope = ctx.ParseResult.GetValueForOption(shScopeOpt), - }; - - using var repo = Repository.Open(); - if (ctx.ParseResult.GetValueForOption(shVerOpt)) - { - var result = repo.ShowVersions(query); - if (output != OutputFormat.Text) WriteStructuredOutput(result, output); - else PrintVersions(result); - } - else - { - var result = repo.Show(query); - if (output != OutputFormat.Text) WriteManifestStructuredOutput(result.ToStructuredDocument(), output); - else PrintShow(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 lTagOpt = new Option("--tag", "Filter by tag"); -var lCmdOpt = new Option("--command", "Filter by command"); lCmdOpt.AddAlias("--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 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 }) - listCommand.AddOption(o); -listCommand.AddArgument(lArg); - -listCommand.SetHandler((ctx) => -{ - var output = GetOutputFormat(ctx.ParseResult.GetValueForOption(outputOption)); - var details = ctx.ParseResult.GetValueForOption(lDetailsOpt); - var upgrade = ctx.ParseResult.GetValueForOption(lUpgradeOpt); - var query = new ListQuery - { - Query = ctx.ParseResult.GetValueForArgument(lArg) ?? ctx.ParseResult.GetValueForOption(lqOpt), - Id = ctx.ParseResult.GetValueForOption(lidOpt), - Name = ctx.ParseResult.GetValueForOption(lnOpt), - Moniker = ctx.ParseResult.GetValueForOption(lmOpt), - Tag = ctx.ParseResult.GetValueForOption(lTagOpt), - Command = ctx.ParseResult.GetValueForOption(lCmdOpt), - Source = ctx.ParseResult.GetValueForOption(lsOpt), - Count = ctx.ParseResult.GetValueForOption(lcOpt), - Exact = ctx.ParseResult.GetValueForOption(leOpt), - InstallScope = ctx.ParseResult.GetValueForOption(lScopeOpt), - UpgradeOnly = upgrade, - IncludeUnknown = ctx.ParseResult.GetValueForOption(lUnknownOpt), - IncludePinned = ctx.ParseResult.GetValueForOption(lPinnedOpt), - }; - - using var repo = Repository.Open(); - var result = repo.List(query); - if (output != OutputFormat.Text) WriteStructuredOutput(result, output); - else PrintListResult(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 uLocaleOpt = new Option("--locale", "Installer locale"); -var uTypeOpt = new Option("--installer-type", "Installer type"); -var uArchOpt = new Option("--architecture", "Architecture"); uArchOpt.AddAlias("-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 uPinnedOpt = new Option("--include-pinned", "Include pinned"); -var uAllOpt = new Option("--all", "Upgrade all"); uAllOpt.AddAlias("-r"); uAllOpt.AddAlias("--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 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"); -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 manifestPath = ctx.ParseResult.GetValueForOption(uManifestOpt); - var interactive = ctx.ParseResult.GetValueForOption(uInteractiveOpt); - var silent = ctx.ParseResult.GetValueForOption(uSilentOpt); - var hasExplicitUpgradeSelector = - ctx.ParseResult.GetValueForArgument(uArg) is not null || - ctx.ParseResult.GetValueForOption(uqOpt) is not null || - ctx.ParseResult.GetValueForOption(uidOpt) is not null || - ctx.ParseResult.GetValueForOption(unOpt) is not null || - ctx.ParseResult.GetValueForOption(umOpt) is not null; - if (silent && interactive) - throw new InvalidOperationException("--silent and --interactive cannot be used together."); - - var doInstall = !string.IsNullOrWhiteSpace(manifestPath) - || ctx.ParseResult.GetValueForOption(uAllOpt) - || ctx.ParseResult.GetValueForArgument(uArg) is not null - || ctx.ParseResult.GetValueForOption(uqOpt) is not null - || ctx.ParseResult.GetValueForOption(uidOpt) is not null - || ctx.ParseResult.GetValueForOption(unOpt) is not null; - - var query = new ListQuery - { - Query = ctx.ParseResult.GetValueForArgument(uArg) ?? ctx.ParseResult.GetValueForOption(uqOpt), - Id = ctx.ParseResult.GetValueForOption(uidOpt), - Name = ctx.ParseResult.GetValueForOption(unOpt), - Moniker = ctx.ParseResult.GetValueForOption(umOpt), - Source = ctx.ParseResult.GetValueForOption(usOpt), - Count = ctx.ParseResult.GetValueForOption(ucOpt), - Exact = ctx.ParseResult.GetValueForOption(ueOpt), - Version = ctx.ParseResult.GetValueForOption(uvOpt), - InstallScope = ctx.ParseResult.GetValueForOption(uScopeOpt), - UpgradeOnly = true, - IncludeUnknown = ctx.ParseResult.GetValueForOption(uUnknownOpt), - IncludePinned = ctx.ParseResult.GetValueForOption(uPinnedOpt) || hasExplicitUpgradeSelector, - }; - - var installQuery = new PackageQuery - { - Query = query.Query, - Id = query.Id, - Name = query.Name, - Moniker = query.Moniker, - Source = query.Source, - Exact = query.Exact, - Version = query.Version, - Locale = ctx.ParseResult.GetValueForOption(uLocaleOpt), - InstallerType = ctx.ParseResult.GetValueForOption(uTypeOpt), - InstallerArchitecture = ctx.ParseResult.GetValueForOption(uArchOpt), - Platform = ctx.ParseResult.GetValueForOption(uPlatformOpt), - OsVersion = ctx.ParseResult.GetValueForOption(uOsVersionOpt), - InstallScope = query.InstallScope, - }; - - using var repo = Repository.Open(); - if (doInstall && !OperatingSystem.IsWindows()) - { - PrintWarnings([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( - installQuery, - manifestPath, - mode, - ctx.ParseResult.GetValueForOption(uLogOpt), - ctx.ParseResult.GetValueForOption(uCustomOpt), - ctx.ParseResult.GetValueForOption(uOverrideOpt), - ctx.ParseResult.GetValueForOption(uLocationOpt), - ctx.ParseResult.GetValueForOption(uSkipDependenciesOpt), - false, - ctx.ParseResult.GetValueForOption(uAcceptPkgAgreementsOpt), - ctx.ParseResult.GetValueForOption(uForceOpt), - null, - ctx.ParseResult.GetValueForOption(uUninstallPreviousOpt), - ctx.ParseResult.GetValueForOption(uIgnoreSecurityHashOpt), - ctx.ParseResult.GetValueForOption(uDependencySourceOpt), - false); - - if (!doInstall) - { - if (output != OutputFormat.Text) WriteStructuredOutput(result, output); - else PrintListResult(result, false, true); - } - else if (!string.IsNullOrWhiteSpace(manifestPath)) - { - var installResult = repo.Install(baseInstallRequest); - PrintPackageActionResult(installResult, "upgrade", "upgraded"); - } - else - { - var upgradeable = result.Matches.Where(m => m.AvailableVersion is not null).ToList(); - var pins = repo.ListPins(); - var upgradedCount = 0; - if (upgradeable.Count == 0) - { - Console.WriteLine("No applicable upgrade found."); - } - else - { - foreach (var m in upgradeable) - { - Console.WriteLine($"Upgrading {m.Id} from {m.InstalledVersion} to {m.AvailableVersion ?? "?"} ..."); - try - { - var pin = FindMatchingPin(m, pins); - if (pin?.PinType == PinType.Blocking) - { - Console.WriteLine($" Package is blocked by pin {pin.Version}; remove the pin before upgrading."); - continue; - } - - var r = repo.Install(baseInstallRequest with - { - Query = new PackageQuery - { - Id = m.Id, - Source = installQuery.Source ?? m.SourceName, - Exact = true, - Version = installQuery.Version, - Locale = installQuery.Locale, - InstallerType = installQuery.InstallerType, - InstallerArchitecture = installQuery.InstallerArchitecture, - Platform = installQuery.Platform, - OsVersion = installQuery.OsVersion, - InstallScope = installQuery.InstallScope, - }, - ManifestPath = null, - }); - PrintWarnings(r.Warnings); - Console.WriteLine(r.NoOp - ? $" No changes were made for {m.Id}" - : r.Success - ? $" Successfully upgraded {m.Id}" - : $" Failed to upgrade {m.Id} (exit code: {r.ExitCode})"); - if (r.Success && !r.NoOp) - upgradedCount++; - } - catch (Exception ex) - { - Console.Error.WriteLine($" Error upgrading {m.Id}: {ex.Message}"); - } - } - Console.WriteLine($"{upgradedCount} package(s) upgraded."); - } - } -}); - -// ── Source commands ── -var sourceCommand = new Command("source", "Manage sources"); -var sourceListCmd = new Command("list", "List sources"); -var sourceUpdateCmd = new Command("update", "Update sources"); -var suSourceArg = new Argument("source", () => null, "Source name"); -sourceUpdateCmd.AddArgument(suSourceArg); -var sourceExportCmd = new Command("export", "Export sources"); -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 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"); -sourceEditCmd.AddOption(seNameOpt); -sourceEditCmd.AddOption(seExplicitOpt); -var sourceRemoveCmd = new Command("remove", "Remove source"); -var srNameArg = new Argument("name", () => null, "Source name"); -var srRemoveNameOpt = new Option("--name", "Source name"); srRemoveNameOpt.AddAlias("-n"); -sourceRemoveCmd.AddArgument(srNameArg); -sourceRemoveCmd.AddOption(srRemoveNameOpt); -var sourceResetCmd = new Command("reset", "Reset sources"); -var srNameOpt = new Option("--name", "Source name"); srNameOpt.AddAlias("-n"); -var srForceOpt = new Option("--force", "Force reset"); -sourceResetCmd.AddOption(srNameOpt); -sourceResetCmd.AddOption(srForceOpt); -sourceCommand.AddCommand(sourceListCmd); -sourceCommand.AddCommand(sourceUpdateCmd); -sourceCommand.AddCommand(sourceExportCmd); -sourceCommand.AddCommand(sourceAddCmd); -sourceCommand.AddCommand(sourceEditCmd); -sourceCommand.AddCommand(sourceRemoveCmd); -sourceCommand.AddCommand(sourceResetCmd); - -sourceListCmd.SetHandler((ctx) => -{ - var output = GetOutputFormat(ctx.ParseResult.GetValueForOption(outputOption)); - using var repo = Repository.Open(); - var sources = repo.ListSources(); - if (output != OutputFormat.Text) WriteStructuredOutput(new { Sources = sources }, output); - else PrintSources(sources); -}); - -sourceUpdateCmd.SetHandler((source) => -{ - using var repo = Repository.Open(); - foreach (var r in repo.UpdateSources(source)) - Console.WriteLine($"{r.Name} [{r.Kind}]: {r.Detail}"); -}, suSourceArg); - -sourceExportCmd.SetHandler(() => -{ - using var repo = Repository.Open(); - var sources = repo.ListSources().Select(s => new - { - Name = s.Name, - Type = FormatSourceType(s.Kind), - Arg = s.Arg, - Data = s.Identifier, - Identifier = s.Identifier, - TrustLevel = s.TrustLevel, - Explicit = s.Explicit, - Priority = s.Priority, - }); - Console.WriteLine(JsonSerializer.Serialize(new { Sources = sources }, SerializationOptions)); -}); - -sourceAddCmd.SetHandler((ctx) => -{ - var name = ResolveSourceAddValue( - ctx.ParseResult.GetValueForArgument(saNameArg), - ctx.ParseResult.GetValueForOption(saNameOpt), - "name"); - var arg = ResolveSourceAddValue( - ctx.ParseResult.GetValueForArgument(saArgArg), - ctx.ParseResult.GetValueForOption(saArgOpt), - "argument"); - var type = ctx.ParseResult.GetValueForOption(saTypeOpt); - var trustLevel = ctx.ParseResult.GetValueForOption(saTrustLevelOpt); - var explicitSource = ctx.ParseResult.GetValueForOption(saExplicitOpt); - using var repo = Repository.Open(); - var kind = ParseSourceKind(type); - repo.AddSource(name, arg, kind, trustLevel ?? "None", explicitSource); - Console.WriteLine("Done"); -}); - -sourceEditCmd.SetHandler((name, explicitSource) => -{ - if (string.IsNullOrWhiteSpace(name)) - throw new InvalidOperationException("source edit requires --name."); - if (!explicitSource.HasValue) - throw new InvalidOperationException("source edit requires --explicit true|false."); - - using var repo = Repository.Open(); - repo.EditSource(name, explicitSource: explicitSource.Value); - Console.WriteLine("Done"); -}, seNameOpt, seExplicitOpt); - -sourceRemoveCmd.SetHandler((ctx) => -{ - var name = ResolveSourceAddValue( - ctx.ParseResult.GetValueForArgument(srNameArg), - ctx.ParseResult.GetValueForOption(srRemoveNameOpt), - "name"); - using var repo = Repository.Open(); - repo.RemoveSource(name); - Console.WriteLine("Done"); -}); - -sourceResetCmd.SetHandler((name, force) => -{ - using var repo = Repository.Open(); - if (!string.IsNullOrWhiteSpace(name)) - repo.ResetSource(name); - else - { - if (!force) { Console.Error.WriteLine("error: Resetting all sources requires --force"); return; } - repo.ResetSources(); - } - Console.WriteLine("Done"); -}, srNameOpt, srForceOpt); - -// ── 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(); -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 query = new PackageQuery - { - Query = ctx.ParseResult.GetValueForArgument(cwArg) ?? ctx.ParseResult.GetValueForOption(cwqOpt), - Id = ctx.ParseResult.GetValueForOption(cwidOpt), - Source = ctx.ParseResult.GetValueForOption(cwsOpt), - Exact = ctx.ParseResult.GetValueForOption(cweOpt), - }; - using var repo = Repository.Open(); - var result = repo.WarmCache(query); - if (output != OutputFormat.Text) WriteStructuredOutput(result, output); - else - { - Console.WriteLine($"Warmed cache for {result.Package.Name} [{result.Package.Id}]"); - foreach (var f in result.CachedFiles) Console.WriteLine($" {f}"); - } -}); - -// ── Hash ── -var hashCommand = new Command("hash", "Hash a file"); -var hashFileArg = new Argument("file", "File path"); -var hashMsixOpt = new Option("--msix", "MSIX signature hash"); -hashCommand.AddArgument(hashFileArg); hashCommand.AddOption(hashMsixOpt); - -hashCommand.SetHandler((file, msix) => -{ - var bytes = File.ReadAllBytes(file); - var hash = SHA256.HashData(bytes); - Console.WriteLine($"SHA256: {Convert.ToHexString(hash)}"); - if (msix) - { - try - { - using var archive = System.IO.Compression.ZipFile.OpenRead(file); - var sig = archive.GetEntry("AppxSignature.p7x"); - if (sig is not null) - { - using var stream = sig.Open(); - using var ms = new MemoryStream(); - stream.CopyTo(ms); - var sigHash = SHA256.HashData(ms.ToArray()); - Console.WriteLine($"SignatureSha256: {Convert.ToHexString(sigHash)}"); - } - } - catch { Console.Error.WriteLine("Not a valid MSIX/Appx package"); } - } -}, hashFileArg, hashMsixOpt); - -// ── 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 exVersionsOpt = new Option("--include-versions", "Include versions"); -exportCommand.AddOption(exOutputOpt); exportCommand.AddOption(exSourceOpt); exportCommand.AddOption(exVersionsOpt); - -exportCommand.SetHandler((output, source, includeVersions) => -{ - using var repo = Repository.Open(); - var listResult = repo.List(new ListQuery { Source = source is not null ? source : null, Query = source is not null ? " " : null }); - var packages = listResult.Matches.Select(m => - { - var pkg = new Dictionary { ["PackageIdentifier"] = m.Id }; - if (includeVersions && m.InstalledVersion is not null) pkg["Version"] = m.InstalledVersion; - return pkg; - }).ToList(); - - var export = new - { - Schema = "https://aka.ms/winget-packages.schema.2.0.json", - Sources = new[] - { - new - { - SourceDetails = new { Name = source ?? "winget", Argument = "https://cdn.winget.microsoft.com/cache", Type = "Microsoft.PreIndexed" }, - Packages = packages - } - } - }; - File.WriteAllText(output, JsonSerializer.Serialize(export, SerializationOptions)); - Console.WriteLine($"Exported {packages.Count} packages to {output}"); -}, exOutputOpt, exSourceOpt, exVersionsOpt); - -// ── Error ── -var errorCommand = new Command("error", "Look up error codes"); -var errInputArg = new Argument("input", "Error code"); -errorCommand.AddArgument(errInputArg); - -errorCommand.SetHandler(PrintErrorLookup, errInputArg); - -// ── Settings ── -var settingsCommand = new Command("settings", "Settings"); -settingsCommand.AddAlias("config"); -var settingsEnableOpt = new Option("--enable", "Enables the specific administrator setting"); -var settingsDisableOpt = new Option("--disable", "Disables the specific administrator setting"); -settingsCommand.AddOption(settingsEnableOpt); -settingsCommand.AddOption(settingsDisableOpt); -var settingsExportCmd = new Command("export", "Export settings"); -var settingsSetCmd = new Command("set", "Sets the value of an admin setting."); -var settingsSetNameOpt = new Option("--setting", "Name of the setting to modify") { IsRequired = true }; -var settingsSetValueOpt = new Option("--value", "Value to set for the setting.") { IsRequired = true }; -settingsSetCmd.AddOption(settingsSetNameOpt); -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"); -settingsResetCmd.AddOption(settingsResetNameOpt); -settingsResetCmd.AddOption(settingsResetAllOpt); -settingsCommand.AddCommand(settingsExportCmd); -settingsCommand.AddCommand(settingsSetCmd); -settingsCommand.AddCommand(settingsResetCmd); - -settingsCommand.SetHandler((ctx) => -{ - var enable = ctx.ParseResult.GetValueForOption(settingsEnableOpt); - var disable = ctx.ParseResult.GetValueForOption(settingsDisableOpt); - if (!string.IsNullOrWhiteSpace(enable) && !string.IsNullOrWhiteSpace(disable)) - throw new InvalidOperationException("--enable and --disable cannot be used together."); - - using var repo = Repository.Open(); - if (!string.IsNullOrWhiteSpace(enable)) - { - repo.SetAdminSetting(enable, true); - Console.WriteLine($"Enabled admin setting '{enable}'."); - return; - } - - if (!string.IsNullOrWhiteSpace(disable)) - { - repo.SetAdminSetting(disable, false); - Console.WriteLine($"Disabled admin setting '{disable}'."); - return; - } - - WriteJsonNode(repo.GetUserSettings(), GetOutputFormat(ctx.ParseResult.GetValueForOption(outputOption))); -}); - -settingsExportCmd.SetHandler((ctx) => -{ - using var repo = Repository.Open(); - WriteJsonNode(repo.GetUserSettings(), GetOutputFormat(ctx.ParseResult.GetValueForOption(outputOption))); -}); - -settingsSetCmd.SetHandler((ctx) => -{ - using var repo = Repository.Open(); - var name = ctx.ParseResult.GetValueForOption(settingsSetNameOpt) - ?? throw new InvalidOperationException("settings set requires --setting."); - var rawValue = ctx.ParseResult.GetValueForOption(settingsSetValueOpt) - ?? throw new InvalidOperationException("settings set requires --value."); - var value = ParseBooleanSettingValue(rawValue); - repo.SetAdminSetting(name, value); - var output = GetOutputFormat(ctx.ParseResult.GetValueForOption(outputOption)); - if (output != OutputFormat.Text) - WriteJsonNode(repo.GetAdminSettings(), output); - else - Console.WriteLine($"Set admin setting '{name}' to {value.ToString().ToLowerInvariant()}."); -}); - -settingsResetCmd.SetHandler((ctx) => -{ - using var repo = Repository.Open(); - var name = ctx.ParseResult.GetValueForOption(settingsResetNameOpt); - var resetAll = ctx.ParseResult.GetValueForOption(settingsResetAllOpt); - if (string.IsNullOrWhiteSpace(name) && !resetAll) - throw new InvalidOperationException("settings reset requires --setting or --all."); - - repo.ResetAdminSetting(name, resetAll); - var output = GetOutputFormat(ctx.ParseResult.GetValueForOption(outputOption)); - if (output != OutputFormat.Text) - WriteJsonNode(repo.GetAdminSettings(), output); - else if (resetAll) - Console.WriteLine("Reset all admin settings."); - else - Console.WriteLine($"Reset admin setting '{name}'."); -}); - -// ── Features ── -var featuresCommand = new Command("features", "Show features"); -featuresCommand.SetHandler(() => -{ - Console.WriteLine("Feature Status"); - Console.WriteLine("-".PadRight(50, '-')); - Console.WriteLine("Pure C# implementation Enabled"); - Console.WriteLine("Preindexed source support Enabled"); - Console.WriteLine("REST source support Enabled"); - Console.WriteLine("Installed package discovery Enabled"); - Console.WriteLine("Install/uninstall Enabled"); -}); - -// ── Validate ── -var validateCommand = new Command("validate", "Validate manifest"); -var valManifestArg = new Argument("manifest", "Manifest file"); -validateCommand.AddArgument(valManifestArg); -validateCommand.SetHandler((manifest) => -{ - if (!File.Exists(manifest)) { Console.Error.WriteLine($"error: File not found: {manifest}"); return; } - Console.WriteLine("Manifest validation succeeded."); -}, 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 dlLocaleOpt = new Option("--locale", "Installer locale"); -var dlTypeOpt = new Option("--installer-type", "Installer type"); -var dlArchOpt = new Option("--architecture", "Architecture"); dlArchOpt.AddAlias("-a"); -var dlPlatformOpt = new Option("--platform", "Target platform"); -var dlOsVersionOpt = new Option("--os-version", "Target OS version"); -var dlScopeOpt = new Option("--scope", "Install scope"); -var dlIgnoreSecurityHashOpt = new Option("--ignore-security-hash", "Ignore installer hash mismatches"); -var dlSkipDependenciesOpt = new Option("--skip-dependencies", "Skip package dependencies"); -var dlAcceptPkgAgreementsOpt = new Option("--accept-package-agreements", "Accept package agreements"); -downloadCommand.AddArgument(dlArg); -foreach (var o in new Option[] { dlqOpt, dlidOpt, dlnOpt, dlmOpt, dlsOpt, dleOpt, dlvOpt, dlDirOpt, dlManifestOpt, dlLocaleOpt, dlTypeOpt, dlArchOpt, dlPlatformOpt, dlOsVersionOpt, dlScopeOpt, dlIgnoreSecurityHashOpt, dlSkipDependenciesOpt, dlAcceptPkgAgreementsOpt }) downloadCommand.AddOption(o); - -downloadCommand.SetHandler((ctx) => -{ - var query = new PackageQuery - { - Query = ctx.ParseResult.GetValueForArgument(dlArg) ?? ctx.ParseResult.GetValueForOption(dlqOpt), - Id = ctx.ParseResult.GetValueForOption(dlidOpt), - Name = ctx.ParseResult.GetValueForOption(dlnOpt), - Moniker = ctx.ParseResult.GetValueForOption(dlmOpt), - Source = ctx.ParseResult.GetValueForOption(dlsOpt), - Exact = ctx.ParseResult.GetValueForOption(dleOpt), - Version = ctx.ParseResult.GetValueForOption(dlvOpt), - Locale = ctx.ParseResult.GetValueForOption(dlLocaleOpt), - InstallerType = ctx.ParseResult.GetValueForOption(dlTypeOpt), - InstallerArchitecture = ctx.ParseResult.GetValueForOption(dlArchOpt), - Platform = ctx.ParseResult.GetValueForOption(dlPlatformOpt), - OsVersion = ctx.ParseResult.GetValueForOption(dlOsVersionOpt), - InstallScope = ctx.ParseResult.GetValueForOption(dlScopeOpt), - }; - var dir = ctx.ParseResult.GetValueForOption(dlDirOpt) - ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads"); - using var repo = Repository.Open(); - var request = CreateInstallRequest( - query, - ctx.ParseResult.GetValueForOption(dlManifestOpt), - InstallerMode.SilentWithProgress, - null, - null, - null, - null, - ctx.ParseResult.GetValueForOption(dlSkipDependenciesOpt), - false, - ctx.ParseResult.GetValueForOption(dlAcceptPkgAgreementsOpt), - false, - null, - false, - ctx.ParseResult.GetValueForOption(dlIgnoreSecurityHashOpt), - null, - false); - var (manifest, path) = repo.DownloadInstaller(request, dir); - Console.WriteLine($"Downloaded {manifest.Name} v{manifest.Version}"); - Console.WriteLine($" Path: {path}"); -}); - -// ── Pin commands ── -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 plTagOpt = new Option("--tag", "Filter by tag"); -var plCmdOpt = new Option("--command", "Filter by command"); plCmdOpt.AddAlias("--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 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 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"); -pinAddCmd.AddArgument(paArg); -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 prTagOpt = new Option("--tag", "Filter by tag"); -var prCmdOpt = new Option("--command", "Filter by command"); prCmdOpt.AddAlias("--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(); -pinResetCmd.AddOption(prForceOpt); -pinResetCmd.AddOption(prResetSourceOpt); -pinCommand.AddCommand(pinListCmd); pinCommand.AddCommand(pinAddCmd); pinCommand.AddCommand(pinRemoveCmd); pinCommand.AddCommand(pinResetCmd); - -pinListCmd.SetHandler((ctx) => -{ - using var repo = Repository.Open(); - var query = CreatePinQuery( - ctx.ParseResult.GetValueForArgument(plArg) ?? ctx.ParseResult.GetValueForOption(plqOpt), - ctx.ParseResult.GetValueForOption(plIdOpt), - ctx.ParseResult.GetValueForOption(plNameOpt), - ctx.ParseResult.GetValueForOption(plMonikerOpt), - ctx.ParseResult.GetValueForOption(plTagOpt), - ctx.ParseResult.GetValueForOption(plCmdOpt), - ctx.ParseResult.GetValueForOption(plSourceOpt), - ctx.ParseResult.GetValueForOption(plExactOpt)); - var pins = FilterPins(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)); - foreach (var p in pins) Console.WriteLine($"{p.PackageId,-40} {p.Version,-20} {p.SourceId,-15} {p.PinType}"); -}); - -pinAddCmd.SetHandler((ctx) => -{ - using var repo = Repository.Open(); - var query = CreatePinQuery( - ctx.ParseResult.GetValueForArgument(paArg) ?? ctx.ParseResult.GetValueForOption(paqOpt), - ctx.ParseResult.GetValueForOption(paIdOpt), - ctx.ParseResult.GetValueForOption(paNameOpt), - ctx.ParseResult.GetValueForOption(paMonikerOpt), - ctx.ParseResult.GetValueForOption(paTagOpt), - ctx.ParseResult.GetValueForOption(paCmdOpt), - ctx.ParseResult.GetValueForOption(paSourceOpt), - ctx.ParseResult.GetValueForOption(paExactOpt)); - EnsurePinQueryProvided(query, "pin add"); - - var blocking = ctx.ParseResult.GetValueForOption(paBlockingOpt); - var installed = ctx.ParseResult.GetValueForOption(paInstalledOpt); - var force = ctx.ParseResult.GetValueForOption(paForceOpt); - var requestedVersion = ctx.ParseResult.GetValueForOption(paVersionOpt); - - string packageId; - string sourceId; - string? resolvedVersion; - if (installed) - { - var target = ResolveSingleInstalledPinTarget(repo, query); - packageId = target.Id; - sourceId = target.SourceName ?? query.Source ?? ""; - resolvedVersion = target.InstalledVersion; - } - else - { - var target = ResolveSingleAvailablePinTarget(repo, query); - packageId = target.Id; - sourceId = target.SourceName; - resolvedVersion = target.Version; - } - - if (repo.ListPins(sourceId).Any(pin => pin.PackageId.Equals(packageId, StringComparison.OrdinalIgnoreCase)) && !force) - throw new InvalidOperationException("A pin for the selected package already exists. Rerun with --force to replace it."); - - var pinVersion = !string.IsNullOrWhiteSpace(requestedVersion) - ? requestedVersion - : blocking - ? "*" - : resolvedVersion ?? "*"; - repo.AddPin(packageId, pinVersion, sourceId, blocking ? PinType.Blocking : PinType.Pinning); - Console.WriteLine($"Pin added for {packageId}"); -}); - -pinRemoveCmd.SetHandler((ctx) => -{ - using var repo = Repository.Open(); - var query = CreatePinQuery( - ctx.ParseResult.GetValueForArgument(prArg) ?? ctx.ParseResult.GetValueForOption(prqOpt), - ctx.ParseResult.GetValueForOption(prIdOpt), - ctx.ParseResult.GetValueForOption(prNameOpt), - ctx.ParseResult.GetValueForOption(prMonikerOpt), - ctx.ParseResult.GetValueForOption(prTagOpt), - ctx.ParseResult.GetValueForOption(prCmdOpt), - ctx.ParseResult.GetValueForOption(prSourceOpt), - ctx.ParseResult.GetValueForOption(prExactOpt)); - EnsurePinQueryProvided(query, "pin remove"); - - PinRecord? pin; - if (ctx.ParseResult.GetValueForOption(prInstalledOpt)) - { - var target = ResolveSingleInstalledPinTarget(repo, query); - pin = repo.ListPins(target.SourceName ?? query.Source ?? "") - .FirstOrDefault(candidate => candidate.PackageId.Equals(target.Id, StringComparison.OrdinalIgnoreCase)); - } - else - { - var pins = FilterPins(repo, query); - if (pins.Count == 0) - { - Console.WriteLine("No pin found matching the query."); - return; - } - - if (pins.Count > 1) - throw new InvalidOperationException("Multiple pins matched the query; refine the query."); - - pin = pins[0]; - } - - if (pin is null) - { - Console.WriteLine("No pin found matching the query."); - return; - } - - Console.WriteLine(repo.RemovePin(pin.PackageId, pin.SourceId) ? $"Pin removed for {pin.PackageId}" : $"No pin found for {pin.PackageId}"); -}); - -pinResetCmd.SetHandler((force, source) => -{ - if (!force) { Console.Error.WriteLine("error: Resetting all pins requires --force"); return; } - using var repo = Repository.Open(); - repo.ResetPins(source); - Console.WriteLine(string.IsNullOrWhiteSpace(source) ? "All pins have been reset." : $"All pins for source '{source}' have been reset."); -}, 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 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 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 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 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 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 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"); -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); - -installCommand.SetHandler((ctx) => -{ - var query = new PackageQuery - { - Query = ctx.ParseResult.GetValueForArgument(iArg) ?? ctx.ParseResult.GetValueForOption(iqOpt), - Id = ctx.ParseResult.GetValueForOption(iidOpt), - Name = ctx.ParseResult.GetValueForOption(inameOpt), - Moniker = ctx.ParseResult.GetValueForOption(imonikerOpt), - Source = ctx.ParseResult.GetValueForOption(isrcOpt), - Exact = ctx.ParseResult.GetValueForOption(ieOpt), - Version = ctx.ParseResult.GetValueForOption(ivOpt), - Channel = ctx.ParseResult.GetValueForOption(ichannelOpt), - Locale = ctx.ParseResult.GetValueForOption(ilocaleOpt), - InstallerType = ctx.ParseResult.GetValueForOption(itypeOpt), - InstallerArchitecture = ctx.ParseResult.GetValueForOption(iarchOpt), - Platform = ctx.ParseResult.GetValueForOption(iplatformOpt), - OsVersion = ctx.ParseResult.GetValueForOption(iosVersionOpt), - InstallScope = ctx.ParseResult.GetValueForOption(iscopeOpt), - }; - var silent = ctx.ParseResult.GetValueForOption(iSilentOpt); - var interactive = ctx.ParseResult.GetValueForOption(iInteractiveOpt); - if (silent && interactive) - 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( - query, - ctx.ParseResult.GetValueForOption(imanifestOpt), - mode, - ctx.ParseResult.GetValueForOption(ilogOpt), - ctx.ParseResult.GetValueForOption(icustomOpt), - ctx.ParseResult.GetValueForOption(ioverrideOpt), - ctx.ParseResult.GetValueForOption(ilocationOpt), - ctx.ParseResult.GetValueForOption(iskipDepsOpt), - ctx.ParseResult.GetValueForOption(idepsOnlyOpt), - ctx.ParseResult.GetValueForOption(iacceptPkgAgreementsOpt), - ctx.ParseResult.GetValueForOption(iforceOpt), - ctx.ParseResult.GetValueForOption(irenameOpt), - ctx.ParseResult.GetValueForOption(iuninstallPreviousOpt), - ctx.ParseResult.GetValueForOption(iignoreSecurityHashOpt), - ctx.ParseResult.GetValueForOption(idependencySourceOpt), - ctx.ParseResult.GetValueForOption(inoUpgradeOpt))); - PrintPackageActionResult(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 uiscopeOpt = new Option("--scope", "Install scope"); -var uimanifestOpt = new Option("--manifest", "Local manifest file or directory"); uimanifestOpt.AddAlias("-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 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"); -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); - -uninstallCommand.SetHandler((ctx) => -{ - var query = new PackageQuery - { - Query = ctx.ParseResult.GetValueForArgument(uiArg) ?? ctx.ParseResult.GetValueForOption(uiqOpt), - Id = ctx.ParseResult.GetValueForOption(uiidOpt), - Name = ctx.ParseResult.GetValueForOption(uinameOpt), - Moniker = ctx.ParseResult.GetValueForOption(uimonikerOpt), - Source = ctx.ParseResult.GetValueForOption(uisOpt), - Exact = ctx.ParseResult.GetValueForOption(uieOpt), - Version = ctx.ParseResult.GetValueForOption(uivOpt), - InstallScope = ctx.ParseResult.GetValueForOption(uiscopeOpt), - }; - var silent = ctx.ParseResult.GetValueForOption(uiSilentOpt); - var interactive = ctx.ParseResult.GetValueForOption(uiInteractiveOpt); - if (silent && interactive) - throw new InvalidOperationException("--silent and --interactive cannot be used together."); - using var repo = Repository.Open(); - var result = repo.Uninstall(new UninstallRequest - { - Query = query, - ManifestPath = ctx.ParseResult.GetValueForOption(uimanifestOpt), - ProductCode = ctx.ParseResult.GetValueForOption(uiproductCodeOpt), - Mode = interactive ? InstallerMode.Interactive : silent ? InstallerMode.Silent : InstallerMode.SilentWithProgress, - AllVersions = ctx.ParseResult.GetValueForOption(uiallVersionsOpt), - Force = ctx.ParseResult.GetValueForOption(uiForceOpt), - Purge = ctx.ParseResult.GetValueForOption(uiPurgeOpt), - Preserve = ctx.ParseResult.GetValueForOption(uiPreserveOpt), - LogPath = ctx.ParseResult.GetValueForOption(uiLogOpt), - }); - PrintPackageActionResult(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 rproductCodeOpt = new Option("--product-code", "Installed product code"); -var rarchOpt = new Option("--architecture", "Architecture"); rarchOpt.AddAlias("-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 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"); -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); - -repairCommand.SetHandler((ctx) => -{ - var silent = ctx.ParseResult.GetValueForOption(rSilentOpt); - var interactive = ctx.ParseResult.GetValueForOption(rInteractiveOpt); - if (silent && interactive) - 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.Repair(CreateRepairRequest( - new PackageQuery - { - Query = ctx.ParseResult.GetValueForArgument(rArg) ?? ctx.ParseResult.GetValueForOption(rqOpt), - Id = ctx.ParseResult.GetValueForOption(ridOpt), - Name = ctx.ParseResult.GetValueForOption(rnameOpt), - Moniker = ctx.ParseResult.GetValueForOption(rmonikerOpt), - Source = ctx.ParseResult.GetValueForOption(rsrcOpt), - Exact = ctx.ParseResult.GetValueForOption(reOpt), - Version = ctx.ParseResult.GetValueForOption(rvOpt), - Locale = ctx.ParseResult.GetValueForOption(rlocaleOpt), - InstallerArchitecture = ctx.ParseResult.GetValueForOption(rarchOpt), - InstallScope = ctx.ParseResult.GetValueForOption(rscopeOpt), - }, - ctx.ParseResult.GetValueForOption(rmanifestOpt), - ctx.ParseResult.GetValueForOption(rproductCodeOpt), - mode, - ctx.ParseResult.GetValueForOption(rlogOpt), - ctx.ParseResult.GetValueForOption(racceptPkgAgreementsOpt), - ctx.ParseResult.GetValueForOption(rforceOpt), - ctx.ParseResult.GetValueForOption(rignoreSecurityHashOpt))); - PrintPackageActionResult(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 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"); -var imNoUpgradeOpt = new Option("--no-upgrade", "Skip packages that are already installed"); -var imAcceptPkgAgreementsOpt = new Option("--accept-package-agreements", "Accept package agreements"); -importCommand.AddOption(imFileOpt); importCommand.AddOption(imDryRunOpt); importCommand.AddOption(imIgnoreUnavailableOpt); importCommand.AddOption(imIgnoreVersionsOpt); importCommand.AddOption(imNoUpgradeOpt); importCommand.AddOption(imAcceptPkgAgreementsOpt); - -importCommand.SetHandler((ctx) => -{ - var file = ctx.ParseResult.GetValueForOption(imFileOpt) - ?? throw new InvalidOperationException("import requires --import-file."); - var dryRun = ctx.ParseResult.GetValueForOption(imDryRunOpt); - var ignoreUnavailable = ctx.ParseResult.GetValueForOption(imIgnoreUnavailableOpt); - var ignoreVersions = ctx.ParseResult.GetValueForOption(imIgnoreVersionsOpt); - var noUpgrade = ctx.ParseResult.GetValueForOption(imNoUpgradeOpt); - var acceptPackageAgreements = ctx.ParseResult.GetValueForOption(imAcceptPkgAgreementsOpt); - - if (!File.Exists(file)) { Console.Error.WriteLine($"error: File not found: {file}"); return; } - var jsonText = File.ReadAllText(file); - var doc = JsonSerializer.Deserialize(jsonText, SerializationOptions); - var sources = doc.GetProperty("Sources").EnumerateArray().ToList(); - - using var repo = Repository.Open(); - int total = 0; - int skipped = 0; - foreach (var source in sources) - { - var sourceName = source.TryGetProperty("SourceDetails", out var sourceDetails) - ? GetJsonString(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"); - if (dryRun) - { - Console.WriteLine($"[dry-run] Would install: {pkgId}"); - } - else if (noUpgrade && IsInstalledPackagePresent(repo, pkgId, sourceName)) - { - Console.WriteLine($"[no-upgrade] Skipping already installed package: {pkgId}"); - skipped++; - } - else - { - try - { - Console.Write($"Installing {pkgId}..."); - var result = repo.Install(CreateInstallRequest( - new PackageQuery - { - Id = pkgId, - Source = sourceName, - Exact = true, - Version = pkgVersion, - }, - null, - InstallerMode.SilentWithProgress, - null, - null, - null, - null, - false, - false, - acceptPackageAgreements, - false, - null, - false, - false, - null, - noUpgrade)); - if (result.NoOp) - { - Console.WriteLine(" no-op"); - PrintWarnings(result.Warnings); - skipped++; - } - else - { - PrintWarnings(result.Warnings); - Console.WriteLine(result.Success ? " done" : $" failed (exit {result.ExitCode})"); - } - } - catch (Exception ex) when (ignoreUnavailable && CanIgnoreUnavailableImportFailure(ex)) - { - Console.WriteLine(" unavailable"); - Console.Error.WriteLine($"warning: Skipping unavailable package '{pkgId}': {ex.Message}"); - skipped++; - } - catch (Exception ex) { Console.Error.WriteLine($" error: {ex.Message}"); } - } - total++; - } - } - if (!dryRun && skipped > 0) - Console.WriteLine($"Skipped {skipped} package(s)."); - Console.WriteLine($"{total} package(s) {(dryRun ? "would be installed" : "processed")}."); -}); - -// ── Add all commands to root ── -rootCommand.AddCommand(searchCommand); -rootCommand.AddCommand(showCommand); -rootCommand.AddCommand(listCommand); -rootCommand.AddCommand(upgradeCommand); -rootCommand.AddCommand(sourceCommand); -rootCommand.AddCommand(cacheCommand); -rootCommand.AddCommand(hashCommand); -rootCommand.AddCommand(exportCommand); -rootCommand.AddCommand(errorCommand); -rootCommand.AddCommand(settingsCommand); -rootCommand.AddCommand(featuresCommand); -rootCommand.AddCommand(validateCommand); -rootCommand.AddCommand(downloadCommand); -rootCommand.AddCommand(pinCommand); -rootCommand.AddCommand(installCommand); -rootCommand.AddCommand(uninstallCommand); -rootCommand.AddCommand(repairCommand); -rootCommand.AddCommand(importCommand); - -rootCommand.SetHandler((ctx) => -{ - if (ctx.ParseResult.GetValueForOption(infoOption)) - { - PrintInfo(); - 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: - Console.WriteLine(JsonSerializer.Serialize(value, SerializationOptions)); - 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(), SerializationOptions) ?? new object(); - Console.Write(new SerializerBuilder().Build().Serialize(structured)); - break; - default: - Console.WriteLine(value.ToJsonString(SerializationOptions)); - 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/src/UniGetUI.Pinget.Cli/UniGetUI.Pinget.Cli.csproj b/src/UniGetUI.Pinget.Cli/UniGetUI.Pinget.Cli.csproj deleted file mode 100644 index 1f24addea..000000000 --- a/src/UniGetUI.Pinget.Cli/UniGetUI.Pinget.Cli.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - Exe - net10.0 - enable - enable - pinget - UniGetUI.Pinget.Cli - x64;arm64 - win-x64;win-arm64 - win-$(Platform) - false - - - - - - - - diff --git a/src/UniGetUI.Windows.slnx b/src/UniGetUI.Windows.slnx index 2b18d78c8..0cb1eb039 100644 --- a/src/UniGetUI.Windows.slnx +++ b/src/UniGetUI.Windows.slnx @@ -221,10 +221,6 @@ - - - - diff --git a/src/UniGetUI/UniGetUI.csproj b/src/UniGetUI/UniGetUI.csproj index 7f1bb695f..79bd880ea 100644 --- a/src/UniGetUI/UniGetUI.csproj +++ b/src/UniGetUI/UniGetUI.csproj @@ -40,24 +40,22 @@ - $(MSBuildThisFileDirectory)..\UniGetUI.Pinget.Cli\UniGetUI.Pinget.Cli.csproj $(RuntimeIdentifier) win-arm64 win-x64 - $(MSBuildProjectDirectory)\obj\$(Platform)\$(Configuration)\BundledPinget\$(PingetCliRuntimeIdentifier)\ - $(PingetCliPublishDir)pinget.exe + $(PkgDevolutions_Pinget_Cli_Rust)\runtimes\$(PingetCliRuntimeIdentifier)\native + $(PingetCliPackageNativePath)\pinget.exe true - false + Avalonia $(MSBuildThisFileDirectory)..\UniGetUI.Avalonia\UniGetUI.Avalonia.csproj $(RuntimeIdentifier) win-arm64 win-x64 $(MSBuildProjectDirectory)\obj\$(Platform)\$(Configuration)\BundledAvalonia\$(AvaloniaRuntimeIdentifier)\ $(AvaloniaPublishDir)UniGetUI.Avalonia.exe - $(MSBuildProjectDirectory)\..\..\scripts\merge-publish-output.ps1 @@ -90,22 +88,21 @@ BeforeTargets="PostBuildGenerateIntegrityTree;PostPublishGenerateIntegrityTree" Condition="'$(DesignTimeBuild)' != 'true'" > - @@ -114,30 +111,19 @@ BeforeTargets="PostPublishGenerateIntegrityTree" Condition="'$(DesignTimeBuild)' != 'true' and '$(BundleModernApp)' == 'true' and '$(PublishDir)' != '' and Exists('$(PublishDir)')" > + + + $(PublishDir)$(ModernAppDirectoryName)\ + $(BundledAvaloniaPublishDir)UniGetUI.Avalonia.exe + - - - - @@ -234,6 +220,7 @@ + diff --git a/src/WindowsPackageManager.Interop/ExternalLibraries.WindowsPackageManager.Interop.csproj b/src/WindowsPackageManager.Interop/ExternalLibraries.WindowsPackageManager.Interop.csproj index dfbf54951..8f2f83b59 100644 --- a/src/WindowsPackageManager.Interop/ExternalLibraries.WindowsPackageManager.Interop.csproj +++ b/src/WindowsPackageManager.Interop/ExternalLibraries.WindowsPackageManager.Interop.csproj @@ -5,6 +5,8 @@ true WindowsPackageManager.Interop + + $(NoWarn);IL2050 diff --git a/src/WindowsPackageManager.Interop/WindowsPackageManager/WindowsPackageManagerStandardFactory.cs b/src/WindowsPackageManager.Interop/WindowsPackageManager/WindowsPackageManagerStandardFactory.cs index 18a1b557f..20ac87a9d 100644 --- a/src/WindowsPackageManager.Interop/WindowsPackageManager/WindowsPackageManagerStandardFactory.cs +++ b/src/WindowsPackageManager.Interop/WindowsPackageManager/WindowsPackageManagerStandardFactory.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Runtime.Versioning; using Windows.Win32.System.Com; @@ -17,6 +18,10 @@ public WindowsPackageManagerStandardFactory( ) : base(clsidContext, allowLowerTrustRegistration) { } + [UnconditionalSuppressMessage( + "Trimming", + "IL2072", + Justification = "WinGet COM projected activation types come from the Windows Package Manager WinMD and registered COM server; this path does not depend on app-owned trimmed constructors.")] protected override T CreateInstance(Guid clsid, Guid iid) { if (!_allowLowerTrustRegistration)