From 04b24467c8e9b2cab5025c72106b9f23cf2358c3 Mon Sep 17 00:00:00 2001 From: socrat3z Date: Thu, 14 May 2026 11:33:11 +0300 Subject: [PATCH 01/10] Add Bun package manager implementation and related files --- global.json | 8 +- src/UniGetUI.Avalonia.slnx | 6 + .../Bun.cs | 272 ++++++++++++++++++ .../Helpers/BunPkgDetailsHelper.cs | 175 +++++++++++ .../Helpers/BunPkgOperationHelper.cs | 43 +++ .../InternalsVisibleTo.cs | 3 + ...UniGetUI.PackageEngine.Managers.Bun.csproj | 29 ++ .../PEInterface.cs | 4 +- .../UniGetUI.PackageEngine.PEInterface.csproj | 1 + src/UniGetUI.Windows.slnx | 4 + 10 files changed, 540 insertions(+), 5 deletions(-) create mode 100644 src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs create mode 100644 src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs create mode 100644 src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgOperationHelper.cs create mode 100644 src/UniGetUI.PackageEngine.Managers.Bun/InternalsVisibleTo.cs create mode 100644 src/UniGetUI.PackageEngine.Managers.Bun/UniGetUI.PackageEngine.Managers.Bun.csproj diff --git a/global.json b/global.json index c358071f08..50baaf1a03 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { - "sdk": { - "version": "10.0.103", - "rollForward": "latestPatch" - } + "sdk": { + "version": "10.0.300", + "rollForward": "latestPatch" + } } diff --git a/src/UniGetUI.Avalonia.slnx b/src/UniGetUI.Avalonia.slnx index da15199754..9d0b8ab277 100644 --- a/src/UniGetUI.Avalonia.slnx +++ b/src/UniGetUI.Avalonia.slnx @@ -135,6 +135,12 @@ + + + + + + diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs b/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs new file mode 100644 index 0000000000..067dbd3ba6 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs @@ -0,0 +1,272 @@ +using System.Diagnostics; +using System.Text.Json.Nodes; +using UniGetUI.Core.Data; +using UniGetUI.Core.Logging; +using UniGetUI.Core.Tools; +using UniGetUI.Interface.Enums; +using UniGetUI.PackageEngine.Classes.Manager; +using UniGetUI.PackageEngine.Classes.Manager.ManagerHelpers; +using UniGetUI.PackageEngine.Enums; +using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.ManagerClasses.Classes; +using UniGetUI.PackageEngine.ManagerClasses.Manager; +using UniGetUI.PackageEngine.PackageClasses; +using UniGetUI.PackageEngine.Structs; + +namespace UniGetUI.PackageEngine.Managers.BunManager +{ + public class Bun : PackageManager + { + public Bun() + { + Capabilities = new ManagerCapabilities + { + CanRunAsAdmin = true, + SupportsCustomVersions = true, + CanDownloadInstaller = true, + SupportsCustomScopes = true, + CanListDependencies = true, + SupportsPreRelease = true, + SupportsProxy = ProxySupport.No, + SupportsProxyAuth = false + }; + + Properties = new ManagerProperties + { + Id = "bun", + Name = "Bun", + Description = CoreTools.Translate("A npmjs package manager written in Zig. Full of libraries and other utilities that orbit the javascript world
Contains: Node javascript libraries and other related utilities"), + IconId = IconType.Node, + ColorIconId = "node_color", + ExecutableFriendlyName = "bun", + InstallVerb = "add", + UninstallVerb = "remove", + UpdateVerb = "add", + DefaultSource = new ManagerSource(this, "Bun", new Uri("https://www.npmjs.com/")), + KnownSources = [new ManagerSource(this, "Bun", new Uri("https://www.npmjs.com/"))], + }; + + DetailsHelper = new BunPkgDetailsHelper(this); + OperationHelper = new BunPkgOperationHelper(this); + } + + protected override IReadOnlyList FindPackages_UnSafe(string query) + { + using Process p = new() + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = Status.ExecutableCallArgs + " search \"" + query + "\" --json", + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true, + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + StandardOutputEncoding = System.Text.Encoding.UTF8 + } + }; + + IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.FindPackages, p); + p.Start(); + + string strContents = p.StandardOutput.ReadToEnd(); + logger.AddToStdOut(strContents); + List Packages = []; + + if (strContents.Any()) + { + try + { + JsonArray? results = JsonNode.Parse(strContents) as JsonArray; + foreach (JsonNode? entry in results ?? []) + { + string? id = entry?["name"]?.ToString(); + string? version = entry?["version"]?.ToString(); + if (id is not null && version is not null) + { + Packages.Add(new Package(CoreTools.FormatAsName(id), id, version, DefaultSource, this)); + } + } + } + catch (Exception e) + { + logger.AddToStdErr($"Failed to parse search results: {e.Message}"); + } + } + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + + return Packages; + } + + protected override IReadOnlyList GetAvailableUpdates_UnSafe() + { + List Packages = []; + + // bun outdated checks the project in the current directory, not a --global flag. + // Global packages live in ~/.bun/install/global which has its own package.json. + string globalDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".bun", "install", "global"); + + using Process p = new() + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = Status.ExecutableCallArgs + " outdated", + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true, + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = Directory.Exists(globalDir) ? globalDir + : Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + StandardOutputEncoding = System.Text.Encoding.UTF8 + } + }; + + IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.ListUpdates, p); + p.Start(); + + // Read both streams concurrently to avoid deadlock when the process writes + // to both. Bun may write the table to stderr when stdout is not a TTY. + var stdoutTask = p.StandardOutput.ReadToEndAsync(); + var stderrTask = p.StandardError.ReadToEndAsync(); + Task.WaitAll(stdoutTask, stderrTask); + + string strOut = stdoutTask.Result; + string strErr = stderrTask.Result; + logger.AddToStdOut(strOut); + logger.AddToStdErr(strErr); + + // Parse stdout first; fall back to stderr if stdout has no table rows. + string tableSrc = ParseBunOutdatedTable(strOut).Any() ? strOut : strErr; + foreach (var (packageId, version, newVersion) in ParseBunOutdatedTable(tableSrc)) + { + Packages.Add(new Package(CoreTools.FormatAsName(packageId), packageId, version, newVersion, + DefaultSource, this, new(PackageScope.Global))); + } + + p.WaitForExit(); + logger.Close(p.ExitCode); + return Packages; + } + + protected override IReadOnlyList GetInstalledPackages_UnSafe() + { + List Packages = []; + + using Process p = new() + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = Status.ExecutableCallArgs + " pm ls --global", + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true, + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + StandardOutputEncoding = System.Text.Encoding.UTF8 + } + }; + + IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.ListInstalledPackages, p); + p.Start(); + + string strContents = p.StandardOutput.ReadToEnd(); + logger.AddToStdOut(strContents); + + // bun pm ls --global outputs a tree: + // /home/user/.bun/install/global node_modules (3) + // ├── @devcontainers/cli@0.81.1 + // └── typescript@5.7.3 + foreach (string line in strContents.Split('\n')) + { + if (!line.Contains("──")) continue; + string entry = line[(line.IndexOf("──") + 2)..].Trim(); + + // Use LastIndexOf to handle scoped packages: @scope/name@version + int atIdx = entry.LastIndexOf('@'); + if (atIdx <= 0) continue; + + string packageName = entry[..atIdx]; + string version = entry[(atIdx + 1)..]; + + if (string.IsNullOrWhiteSpace(packageName) || string.IsNullOrWhiteSpace(version)) continue; + + Packages.Add(new Package(CoreTools.FormatAsName(packageName), packageName, version, + DefaultSource, this, new(PackageScope.Global))); + } + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + + return Packages; + } + + public override IReadOnlyList FindCandidateExecutableFiles() + => CoreTools.WhichMultiple(OperatingSystem.IsWindows() ? "bun.exe" : "bun"); + + protected override void _loadManagerExecutableFile(out bool found, out string path, out string callArguments) + { + var (_found, _executablePath) = GetExecutableFile(); + found = _found; + path = _executablePath; + callArguments = ""; + } + + protected override void _loadManagerVersion(out string version) + { + Process process = new() + { + StartInfo = new ProcessStartInfo + { + FileName = Status.ExecutablePath, + Arguments = Status.ExecutableCallArgs + "--version", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true, + CreateNoWindow = true, + WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + StandardOutputEncoding = System.Text.Encoding.UTF8 + } + }; + process.Start(); + version = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(); + } + + /// + /// Parses the Unicode box-drawing table produced by bun outdated. + /// Each yielded tuple contains (packageId, currentVersion, latestVersion). + /// Columns: │ Package │ Current │ Update │ Latest │ + /// + private static IEnumerable<(string Id, string Version, string NewVersion)> ParseBunOutdatedTable(string output) + { + foreach (string line in output.Split('\n')) + { + if (!line.TrimStart().StartsWith('│')) continue; + string[] parts = line.Split('│'); + if (parts.Length < 5) continue; + + string id = parts[1].Trim(); + string version = parts[2].Trim(); + string newVersion = parts[4].Trim(); + + if (id is "Package" || string.IsNullOrWhiteSpace(id) + || string.IsNullOrWhiteSpace(version) || string.IsNullOrWhiteSpace(newVersion)) continue; + + yield return (id, version, newVersion); + } + } + } +} diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs b/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs new file mode 100644 index 0000000000..cc6c3351b4 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs @@ -0,0 +1,175 @@ +using System.Diagnostics; +using System.Globalization; +using System.Text.Json.Nodes; +using UniGetUI.Core.IconEngine; +using UniGetUI.Core.Logging; +using UniGetUI.PackageEngine.Classes.Manager.BaseProviders; +using UniGetUI.PackageEngine.Enums; +using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.ManagerClasses.Classes; + +namespace UniGetUI.PackageEngine.Managers.BunManager +{ + internal sealed class BunPkgDetailsHelper : BasePkgDetailsHelper + { + public BunPkgDetailsHelper(Bun manager) : base(manager) { } + + protected override void GetDetails_UnSafe(IPackageDetails details) + { + try + { + details.InstallerType = "Tarball"; + details.ManifestUrl = new Uri($"https://www.npmjs.com/package/{details.Package.Id}"); + details.ReleaseNotesUrl = new Uri($"https://www.npmjs.com/package/{details.Package.Id}?activeTab=versions"); + + using Process p = new(); + p.StartInfo = new ProcessStartInfo + { + FileName = Manager.Status.ExecutablePath, + Arguments = Manager.Status.ExecutableCallArgs + " show " + details.Package.Id + " --json", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true, + CreateNoWindow = true, + WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + StandardOutputEncoding = System.Text.Encoding.UTF8 + }; + + IProcessTaskLogger logger = Manager.TaskLogger.CreateNew(LoggableTaskType.LoadPackageDetails, p); + p.Start(); + + string strContents = p.StandardOutput.ReadToEnd(); + logger.AddToStdOut(strContents); + JsonObject? contents = JsonNode.Parse(strContents) as JsonObject; + + details.License = contents?["license"]?.ToString(); + details.Description = contents?["description"]?.ToString(); + + if (Uri.TryCreate(contents?["homepage"]?.ToString() ?? "", UriKind.RelativeOrAbsolute, out var homepageUrl)) + details.HomepageUrl = homepageUrl; + + details.Publisher = (contents?["maintainers"] as JsonArray)?[0]?.ToString(); + details.Author = contents?["author"]?.ToString(); + details.UpdateDate = contents?["time"]?[contents?["dist-tags"]?["latest"]?.ToString() ?? details.Package.VersionString]?.ToString(); + + if (Uri.TryCreate(contents?["dist"]?["tarball"]?.ToString() ?? "", UriKind.RelativeOrAbsolute, out var installerUrl)) + details.InstallerUrl = installerUrl; + + if (int.TryParse(contents?["dist"]?["unpackedSize"]?.ToString() ?? "", NumberStyles.Any, CultureInfo.InvariantCulture, out int installerSize)) + details.InstallerSize = installerSize; + + details.InstallerHash = contents?["dist"]?["integrity"]?.ToString(); + + details.Dependencies.Clear(); + HashSet addedDeps = new(); + foreach (var rawDep in (contents?["dependencies"]?.AsObject() ?? [])) + { + if(addedDeps.Contains(rawDep.Key)) continue; + addedDeps.Add(rawDep.Key); + + details.Dependencies.Add(new() + { + Name = rawDep.Key, + Version = rawDep.Value?.GetValue() ?? "", + Mandatory = true, + }); + } + + foreach (var rawDep in (contents?["devDependencies"]?.AsObject() ?? [])) + { + if(addedDeps.Contains(rawDep.Key)) continue; + addedDeps.Add(rawDep.Key); + + details.Dependencies.Add(new() + { + Name = rawDep.Key, + Version = rawDep.Value?.GetValue() ?? "", + Mandatory = false, + }); + } + + foreach (var rawDep in (contents?["peerDependencies"]?.AsObject() ?? [])) + { + if(addedDeps.Contains(rawDep.Key)) continue; + addedDeps.Add(rawDep.Key); + + details.Dependencies.Add(new() + { + Name = rawDep.Key, + Version = rawDep.Value?.GetValue() ?? "", + Mandatory = false, + }); + } + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + } + catch (Exception e) + { + Logger.Error(e); + } + + return; + } + + protected override CacheableIcon? GetIcon_UnSafe(IPackage package) + { + throw new NotImplementedException(); + } + + protected override IReadOnlyList GetScreenshots_UnSafe(IPackage package) + { + throw new NotImplementedException(); + } + + protected override string? GetInstallLocation_UnSafe(IPackage package) + { + if (package.OverridenOptions.Scope is PackageScope.Local) + return Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "node_modules", package.Id); + return Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Roaming", "npm", + "node_modules", package.Id); + } + + protected override IReadOnlyList GetInstallableVersions_UnSafe(IPackage package) + { + using Process p = new() + { + StartInfo = new ProcessStartInfo + { + FileName = Manager.Status.ExecutablePath, + Arguments = + Manager.Status.ExecutableCallArgs + " show " + package.Id + " versions --json", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true, + CreateNoWindow = true, + WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + StandardOutputEncoding = System.Text.Encoding.UTF8 + } + }; + + IProcessTaskLogger logger = Manager.TaskLogger.CreateNew(LoggableTaskType.LoadPackageVersions, p); + p.Start(); + + string strContents = p.StandardOutput.ReadToEnd(); + logger.AddToStdOut(strContents); + JsonArray? rawVersions = JsonNode.Parse(strContents) as JsonArray; + + List versions = []; + foreach(JsonNode? raw_ver in rawVersions ?? []) + { + if (raw_ver is not null) + versions.Add(raw_ver.ToString()); + } + + logger.AddToStdErr(p.StandardError.ReadToEnd()); + p.WaitForExit(); + logger.Close(p.ExitCode); + + return versions; + } + } +} diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgOperationHelper.cs new file mode 100644 index 0000000000..7329d88e78 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgOperationHelper.cs @@ -0,0 +1,43 @@ +using UniGetUI.PackageEngine.Classes.Manager.BaseProviders; +using UniGetUI.PackageEngine.Enums; +using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.Serializable; + +namespace UniGetUI.PackageEngine.Managers.BunManager; +internal sealed class BunPkgOperationHelper : BasePkgOperationHelper +{ + public BunPkgOperationHelper(Bun manager) : base(manager) { } + + protected override IReadOnlyList _getOperationParameters(IPackage package, + InstallOptions options, OperationType operation) + { + List parameters = operation switch { + OperationType.Install => [Manager.Properties.InstallVerb, $"'{package.Id}@{(options.Version == string.Empty? package.VersionString: options.Version)}'"], + OperationType.Update => [Manager.Properties.UpdateVerb, $"'{package.Id}@{package.NewVersionString}'"], + OperationType.Uninstall => [Manager.Properties.UninstallVerb, package.Id], + _ => throw new InvalidDataException("Invalid package operation") + }; + + if (package.OverridenOptions.Scope == PackageScope.Global || + (package.OverridenOptions.Scope is null && options.InstallationScope == PackageScope.Global)) + parameters.Add("--global"); + + parameters.AddRange(operation switch + { + OperationType.Update => options.CustomParameters_Update, + OperationType.Uninstall => options.CustomParameters_Uninstall, + _ => options.CustomParameters_Install, + }); + + return parameters; + } + + protected override OperationVeredict _getOperationResult( + IPackage package, + OperationType operation, + IReadOnlyList processOutput, + int returnCode) + { + return returnCode == 0 ? OperationVeredict.Success : OperationVeredict.Failure; + } +} diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/InternalsVisibleTo.cs b/src/UniGetUI.PackageEngine.Managers.Bun/InternalsVisibleTo.cs new file mode 100644 index 0000000000..eeb63dad19 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Bun/InternalsVisibleTo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("UniGetUI.PackageEngine.Tests")] diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/UniGetUI.PackageEngine.Managers.Bun.csproj b/src/UniGetUI.PackageEngine.Managers.Bun/UniGetUI.PackageEngine.Managers.Bun.csproj new file mode 100644 index 0000000000..74aff90943 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Managers.Bun/UniGetUI.PackageEngine.Managers.Bun.csproj @@ -0,0 +1,29 @@ + + + $(SharedTargetFrameworks) + UniGetUI.PackageEngine.Managers.Bun + UniGetUI.PackageEngine.Managers.BunManager + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.PackageEngine.PackageEngine/PEInterface.cs b/src/UniGetUI.PackageEngine.PackageEngine/PEInterface.cs index 16af27c483..b114fc305b 100644 --- a/src/UniGetUI.PackageEngine.PackageEngine/PEInterface.cs +++ b/src/UniGetUI.PackageEngine.PackageEngine/PEInterface.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using UniGetUI.Core.Logging; using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.Managers.BunManager; using UniGetUI.PackageEngine.Managers.CargoManager; using UniGetUI.PackageEngine.Managers.DotNetManager; using UniGetUI.PackageEngine.Managers.NpmManager; @@ -37,6 +38,7 @@ public static class PEInterface public static readonly Chocolatey Chocolatey = new(); #endif public static readonly Npm Npm = new(); + public static readonly Bun Bun = new(); public static readonly Pip Pip = new(); public static readonly DotNet DotNet = new(); public static readonly PowerShell7 PowerShell7 = new(); @@ -58,7 +60,7 @@ public static class PEInterface private static IPackageManager[] CreateManagers() { - List managers = [Npm, Pip, Cargo, Vcpkg, DotNet, PowerShell7]; + List managers = [Npm, Bun, Pip, Cargo, Vcpkg, DotNet, PowerShell7]; #if WINDOWS managers.InsertRange(0, [WinGet, Scoop, Chocolatey]); managers.Add(PowerShell); diff --git a/src/UniGetUI.PackageEngine.PackageEngine/UniGetUI.PackageEngine.PEInterface.csproj b/src/UniGetUI.PackageEngine.PackageEngine/UniGetUI.PackageEngine.PEInterface.csproj index 9a8df247cc..ccfcb25277 100644 --- a/src/UniGetUI.PackageEngine.PackageEngine/UniGetUI.PackageEngine.PEInterface.csproj +++ b/src/UniGetUI.PackageEngine.PackageEngine/UniGetUI.PackageEngine.PEInterface.csproj @@ -11,6 +11,7 @@ + diff --git a/src/UniGetUI.Windows.slnx b/src/UniGetUI.Windows.slnx index 0cb1eb039c..d3ecc29b0e 100644 --- a/src/UniGetUI.Windows.slnx +++ b/src/UniGetUI.Windows.slnx @@ -140,6 +140,10 @@
+ + + + From 5def32f3681282ae2090a183332c2b26b3f0d848 Mon Sep 17 00:00:00 2001 From: socrat3z Date: Thu, 14 May 2026 12:10:09 +0300 Subject: [PATCH 02/10] Implement Bun package manager support with settings and tests --- .../SettingsPages/PackageManagerPage.axaml.cs | 14 + .../SettingsEngine_Names.cs | 2 + .../Bun.cs | 209 +++++-- .../Helpers/BunPkgOperationHelper.cs | 19 +- .../BunManagerTests.cs | 541 ++++++++++++++++++ .../Fixtures/Bun/installed.txt | 6 + .../Fixtures/Bun/outdated-table.txt | 10 + .../Fixtures/Bun/package-details.json | 13 + .../Fixtures/Bun/search-results.json | 17 + .../UniGetUI.PackageEngine.Tests.csproj | 1 + .../ManagersPages/PackageManager.xaml.cs | 20 + 11 files changed, 788 insertions(+), 64 deletions(-) create mode 100644 src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs create mode 100644 src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/installed.txt create mode 100644 src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/outdated-table.txt create mode 100644 src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/package-details.json create mode 100644 src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/search-results.json diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs index bbb2ced2ff..8b732980b9 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs @@ -459,6 +459,20 @@ private void BuildExtraControls(CheckboxCard_Dict disableNotifsCard) }); break; + case "Bun": + disableNotifsCard.CornerRadius = new CornerRadius(8, 8, 0, 0); + disableNotifsCard.BorderThickness = new Thickness(1, 1, 1, 0); + ExtraControls.Children.Add(disableNotifsCard); + + ExtraControls.Children.Add(new CheckboxCard + { + CornerRadius = new CornerRadius(0, 0, 8, 8), + BorderThickness = new Thickness(1, 0, 1, 1), + SettingName = CoreSettings.K.PreferLatestVersionsForBun, + Text = CoreTools.AutoTranslated("Prefer latest versions (may include breaking changes) instead of recommended safe updates"), + }); + break; + case "vcpkg": disableNotifsCard.CornerRadius = new CornerRadius(8, 8, 0, 0); disableNotifsCard.BorderThickness = new Thickness(1, 1, 1, 0); diff --git a/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs b/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs index c5839853cc..071e744291 100644 --- a/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs +++ b/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs @@ -91,6 +91,7 @@ public enum K WinGetComApiPolicy, DisableClassicMode, DisableInstallerHostChangeWarning, + PreferLatestVersionsForBun, Test1, Test2, @@ -193,6 +194,7 @@ public static string ResolveKey(K key) K.WinGetComApiPolicy => "WinGetComApiPolicy", K.DisableClassicMode => "DisableClassicMode", K.DisableInstallerHostChangeWarning => "DisableInstallerHostChangeWarning", + K.PreferLatestVersionsForBun => "PreferLatestVersionsForBun", K.Test1 => "TestSetting1", K.Test2 => "TestSetting2", diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs b/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs index 067dbd3ba6..89ccbe39e2 100644 --- a/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs +++ b/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs @@ -2,6 +2,7 @@ using System.Text.Json.Nodes; using UniGetUI.Core.Data; using UniGetUI.Core.Logging; +using UniGetUI.Core.SettingsEngine; using UniGetUI.Core.Tools; using UniGetUI.Interface.Enums; using UniGetUI.PackageEngine.Classes.Manager; @@ -73,40 +74,15 @@ protected override IReadOnlyList FindPackages_UnSafe(string query) string strContents = p.StandardOutput.ReadToEnd(); logger.AddToStdOut(strContents); - List Packages = []; - - if (strContents.Any()) - { - try - { - JsonArray? results = JsonNode.Parse(strContents) as JsonArray; - foreach (JsonNode? entry in results ?? []) - { - string? id = entry?["name"]?.ToString(); - string? version = entry?["version"]?.ToString(); - if (id is not null && version is not null) - { - Packages.Add(new Package(CoreTools.FormatAsName(id), id, version, DefaultSource, this)); - } - } - } - catch (Exception e) - { - logger.AddToStdErr($"Failed to parse search results: {e.Message}"); - } - } - logger.AddToStdErr(p.StandardError.ReadToEnd()); p.WaitForExit(); logger.Close(p.ExitCode); - return Packages; + return ParseSearchOutput(strContents, DefaultSource, this); } protected override IReadOnlyList GetAvailableUpdates_UnSafe() { - List Packages = []; - // bun outdated checks the project in the current directory, not a --global flag. // Global packages live in ~/.bun/install/global which has its own package.json. string globalDir = Path.Combine( @@ -146,15 +122,18 @@ protected override IReadOnlyList GetAvailableUpdates_UnSafe() // Parse stdout first; fall back to stderr if stdout has no table rows. string tableSrc = ParseBunOutdatedTable(strOut).Any() ? strOut : strErr; - foreach (var (packageId, version, newVersion) in ParseBunOutdatedTable(tableSrc)) + bool preferLatest = Settings.Get(Settings.K.PreferLatestVersionsForBun); + var result = ParseAvailableUpdates(tableSrc, DefaultSource, this, preferLatest); + + Logger.Info($"Bun: Found {result.Count} packages with available updates (preferLatest={preferLatest})"); + foreach (var pkg in result) { - Packages.Add(new Package(CoreTools.FormatAsName(packageId), packageId, version, newVersion, - DefaultSource, this, new(PackageScope.Global))); + Logger.Info($" - {pkg.Id}: {pkg.VersionString} → {pkg.NewVersionString}"); } p.WaitForExit(); logger.Close(p.ExitCode); - return Packages; + return result; } protected override IReadOnlyList GetInstalledPackages_UnSafe() @@ -183,27 +162,7 @@ protected override IReadOnlyList GetInstalledPackages_UnSafe() string strContents = p.StandardOutput.ReadToEnd(); logger.AddToStdOut(strContents); - // bun pm ls --global outputs a tree: - // /home/user/.bun/install/global node_modules (3) - // ├── @devcontainers/cli@0.81.1 - // └── typescript@5.7.3 - foreach (string line in strContents.Split('\n')) - { - if (!line.Contains("──")) continue; - string entry = line[(line.IndexOf("──") + 2)..].Trim(); - - // Use LastIndexOf to handle scoped packages: @scope/name@version - int atIdx = entry.LastIndexOf('@'); - if (atIdx <= 0) continue; - - string packageName = entry[..atIdx]; - string version = entry[(atIdx + 1)..]; - - if (string.IsNullOrWhiteSpace(packageName) || string.IsNullOrWhiteSpace(version)) continue; - - Packages.Add(new Package(CoreTools.FormatAsName(packageName), packageName, version, - DefaultSource, this, new(PackageScope.Global))); - } + Packages.AddRange(ParseInstalledPackages(strContents, DefaultSource, this, new OverridenInstallationOptions(PackageScope.Global))); logger.AddToStdErr(p.StandardError.ReadToEnd()); p.WaitForExit(); @@ -246,26 +205,154 @@ protected override void _loadManagerVersion(out string version) } /// - /// Parses the Unicode box-drawing table produced by bun outdated. - /// Each yielded tuple contains (packageId, currentVersion, latestVersion). - /// Columns: │ Package │ Current │ Update │ Latest │ + /// Parses JSON search results from 'bun search <query> --json'. + /// Each result object contains 'name' and 'version' fields. /// - private static IEnumerable<(string Id, string Version, string NewVersion)> ParseBunOutdatedTable(string output) + internal static IReadOnlyList ParseSearchOutput( + string output, + IManagerSource source, + IPackageManager manager) { + List packages = []; + + if (!output.Any()) return packages; + + try + { + JsonArray? results = JsonNode.Parse(output) as JsonArray; + foreach (JsonNode? entry in results ?? []) + { + string? id = entry?["name"]?.ToString(); + string? version = entry?["version"]?.ToString(); + if (id is not null && version is not null) + { + packages.Add(new Package(CoreTools.FormatAsName(id), id, version, source, manager)); + } + } + } + catch (Exception e) + { + Logger.Warn($"Failed to parse Bun search results: {e.Message}"); + } + + return packages; + } + + /// + /// Parses the outdated packages table from 'bun outdated'. + /// Creates packages with current and available versions and sets scope to Global. + /// + internal static IReadOnlyList ParseAvailableUpdates( + string output, + IManagerSource source, + IPackageManager manager, + bool preferLatest = false) + { + List packages = []; + + foreach (var (packageId, version, newVersion) in ParseBunOutdatedTable(output, preferLatest)) + { + packages.Add(new Package(CoreTools.FormatAsName(packageId), packageId, version, newVersion, + source, manager, new(PackageScope.Global))); + } + + return packages; + } + + /// + /// Parses the installed packages tree from 'bun pm ls --global'. + /// Each package entry is formatted as: [├/└]── [@scope/]name@version + /// + internal static IReadOnlyList ParseInstalledPackages( + string output, + IManagerSource source, + IPackageManager manager, + OverridenInstallationOptions options) + { + List packages = []; + + // bun pm ls --global outputs a tree: + // /home/user/.bun/install/global node_modules (3) + // ├── @devcontainers/cli@0.81.1 + // └── typescript@5.7.3 + foreach (string line in output.Split('\n')) + { + if (!line.Contains("──")) continue; + string entry = line[(line.IndexOf("──") + 2)..].Trim(); + + // Use LastIndexOf to handle scoped packages: @scope/name@version + int atIdx = entry.LastIndexOf('@'); + if (atIdx <= 0) continue; + + string packageName = entry[..atIdx]; + string version = entry[(atIdx + 1)..]; + + if (string.IsNullOrWhiteSpace(packageName) || string.IsNullOrWhiteSpace(version)) continue; + + packages.Add(new Package(CoreTools.FormatAsName(packageName), packageName, version, + source, manager, options)); + } + + return packages; + } + + /// + /// Parses the outdated packages table from 'bun outdated'. + /// Supports both ASCII pipe format (|) and Unicode box-drawing format (│). + /// Each yielded tuple contains (packageId, currentVersion, recommendedUpdateVersion). + /// Columns: [Package | Current | Update | Latest] + /// If preferLatest is true, uses "Latest" (parts[4], may have breaking changes). + /// If preferLatest is false (default), uses "Update" (parts[3], safe semantic version). + /// + internal static IEnumerable<(string Id, string Version, string NewVersion)> ParseBunOutdatedTable( + string output, + bool preferLatest = false) + { + int columnIndex = preferLatest ? 4 : 3; // 4 = Latest, 3 = Update + string columnName = preferLatest ? "Latest" : "Update"; + + int lineNum = 0; foreach (string line in output.Split('\n')) { - if (!line.TrimStart().StartsWith('│')) continue; - string[] parts = line.Split('│'); - if (parts.Length < 5) continue; + lineNum++; + string trimmed = line.TrimStart(); + // Skip lines that don't contain package data (headers, separators, etc.) + if (!trimmed.StartsWith('│') && !trimmed.StartsWith('|')) + { + Logger.Debug($"Bun: Skipping line {lineNum} (no pipe): {line.Substring(0, Math.Min(50, line.Length))}"); + continue; + } + + // Split by either Unicode box-drawing or ASCII pipe characters + string[] parts = line.Split(new[] { '│', '|' }, StringSplitOptions.None); + if (parts.Length < columnIndex + 1) + { + Logger.Debug($"Bun: Skipping line {lineNum} (insufficient parts for {columnName}): {line.Substring(0, Math.Min(50, line.Length))}"); + continue; + } string id = parts[1].Trim(); string version = parts[2].Trim(); - string newVersion = parts[4].Trim(); + string recommendedUpdate = parts[columnIndex].Trim(); + // Skip header row and empty rows if (id is "Package" || string.IsNullOrWhiteSpace(id) - || string.IsNullOrWhiteSpace(version) || string.IsNullOrWhiteSpace(newVersion)) continue; + || string.IsNullOrWhiteSpace(version) || string.IsNullOrWhiteSpace(recommendedUpdate)) + { + Logger.Debug($"Bun: Skipping line {lineNum} (header/empty): {id}/{version}/{recommendedUpdate}"); + continue; + } - yield return (id, version, newVersion); + // Only include packages that have a different update version + if (version != recommendedUpdate) + { + Logger.Debug($"Bun: Found update for {id}: {version} → {recommendedUpdate} (using {columnName} column)"); + yield return (id, version, recommendedUpdate); + } + else + { + Logger.Debug($"Bun: Skipping {id} (no update available: {version} == {recommendedUpdate})"); + } } } } diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgOperationHelper.cs index 7329d88e78..ee65db2f76 100644 --- a/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgOperationHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgOperationHelper.cs @@ -11,9 +11,22 @@ public BunPkgOperationHelper(Bun manager) : base(manager) { } protected override IReadOnlyList _getOperationParameters(IPackage package, InstallOptions options, OperationType operation) { - List parameters = operation switch { - OperationType.Install => [Manager.Properties.InstallVerb, $"'{package.Id}@{(options.Version == string.Empty? package.VersionString: options.Version)}'"], - OperationType.Update => [Manager.Properties.UpdateVerb, $"'{package.Id}@{package.NewVersionString}'"], + // Bun is called directly (not through PowerShell), so we do NOT use quotes. + // Quotes would be passed literally to bun.exe, which doesn't understand them. + // Unlike Npm which goes through PowerShell on Windows, Bun always executes directly. + + List parameters = operation switch + { + OperationType.Install => + [ + Manager.Properties.InstallVerb, + $"{package.Id}@{(options.Version == string.Empty ? package.VersionString : options.Version)}", + ], + OperationType.Update => + [ + Manager.Properties.UpdateVerb, + $"{package.Id}@{package.NewVersionString}", + ], OperationType.Uninstall => [Manager.Properties.UninstallVerb, package.Id], _ => throw new InvalidDataException("Invalid package operation") }; diff --git a/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs new file mode 100644 index 0000000000..9cac3eed46 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs @@ -0,0 +1,541 @@ +using UniGetUI.PackageEngine.Enums; +using UniGetUI.PackageEngine.Managers.BunManager; +using UniGetUI.PackageEngine.ManagerClasses.Manager; +using UniGetUI.PackageEngine.Serializable; +using UniGetUI.PackageEngine.Structs; +using UniGetUI.PackageEngine.Tests.Infrastructure.Assertions; +using UniGetUI.PackageEngine.Tests.Infrastructure.Builders; +using UniGetUI.PackageEngine.Tests.Infrastructure.Helpers; +using UniGetUI.Interface.Enums; + +namespace UniGetUI.PackageEngine.Tests; + +/// +/// Unit and integration tests for the Bun package manager. +/// Tests cover search, package listing, updates detection, and package operations. +/// +public sealed class BunManagerTests +{ + /// + /// Tests parsing of JSON search results from 'bun search <query> --json'. + /// Verifies that multiple packages with different names and versions are correctly parsed. + /// + [Fact] + public void ParseSearchOutputParsesJsonArray() + { + var manager = new Bun(); + + var searchOutput = PackageEngineFixtureFiles.ReadAllText(Path.Combine("Bun", "search-results.json")); + var packages = Bun.ParseSearchOutput(searchOutput, manager.DefaultSource, manager); + + var packageList = packages.ToArray(); + Assert.Equal(3, packageList.Length); + + // Verify first package + PackageAssert.BelongsTo(packageList[0], manager, manager.DefaultSource); + Assert.Equal("typescript", packageList[0].Id); + Assert.Equal("5.3.3", packageList[0].VersionString); + + // Verify second package + PackageAssert.BelongsTo(packageList[1], manager, manager.DefaultSource); + Assert.Equal("lodash", packageList[1].Id); + Assert.Equal("4.17.21", packageList[1].VersionString); + + // Verify scoped package + PackageAssert.BelongsTo(packageList[2], manager, manager.DefaultSource); + Assert.Equal("@types/node", packageList[2].Id); + Assert.Equal("20.10.6", packageList[2].VersionString); + } + + /// + /// Tests parsing of the outdated packages table output from 'bun outdated'. + /// Verifies that the Unicode box-drawing table format is correctly parsed. + /// + [Fact] + public void ParseBunOutdatedTableParsesUnicodeTable() + { + var outdatedOutput = PackageEngineFixtureFiles.ReadAllText(Path.Combine("Bun", "outdated-table.txt")); + + var results = Bun.ParseBunOutdatedTable(outdatedOutput).ToList(); + + Assert.Equal(3, results.Count); + + // Verify first package (typescript) + Assert.Equal("typescript", results[0].Id); + Assert.Equal("5.2.0", results[0].Version); + Assert.Equal("5.3.0", results[0].NewVersion); + + // Verify scoped package (@types/node) + Assert.Equal("@types/node", results[1].Id); + Assert.Equal("20.8.0", results[1].Version); + Assert.Equal("20.9.0", results[1].NewVersion); + + // Verify third package (vite) + Assert.Equal("vite", results[2].Id); + Assert.Equal("4.5.0", results[2].Version); + Assert.Equal("4.5.1", results[2].NewVersion); + } + + /// + /// Tests that ParseBunOutdatedTable skips the header row and empty lines. + /// + [Fact] + public void ParseBunOutdatedTableSkipsHeaderAndEmptyLines() + { + var output = """ + |-------------------------------------------------| + | Package | Current | Update | Latest | + |--------------------------------------------------| + | typescript | 5.2.0 | 5.3.0 | 5.4.0 | + |-------------------------------------------------| + """; + + var results = Bun.ParseBunOutdatedTable(output).ToList(); + + Assert.Single(results); + Assert.Equal("typescript", results[0].Id); + } + + /// + /// Tests that ParseBunOutdatedTable returns empty list for invalid input. + /// + [Fact] + public void ParseBunOutdatedTableReturnsEmptyForInvalidInput() + { + var output = "Invalid output format\nNo table here"; + + var results = Bun.ParseBunOutdatedTable(output).ToList(); + + Assert.Empty(results); + } + + /// + /// Tests that ParseBunOutdatedTable uses "Update" column by default (preferLatest=false). + /// This provides safe, semantic-versioning compatible updates. + /// + [Fact] + public void ParseBunOutdatedTableUsesUpdateColumnByDefault() + { + var output = """ + |-------------------------------------------------| + | Package | Current | Update | Latest | + |--------------------------------------------------| + | typescript | 5.2.0 | 5.3.0 | 6.0.0 | + | lodash | 4.17.0 | 4.17.21 | 4.17.21 | + |-------------------------------------------------| + """; + + var results = Bun.ParseBunOutdatedTable(output, preferLatest: false).ToList(); + + Assert.Equal(2, results.Count); + // typescript: uses Update column (5.3.0), not Latest (6.0.0) + Assert.Equal("typescript", results[0].Id); + Assert.Equal("5.3.0", results[0].NewVersion); + // lodash: Update == Latest, so only one entry + Assert.Equal("lodash", results[1].Id); + Assert.Equal("4.17.21", results[1].NewVersion); + } + + /// + /// Tests that ParseBunOutdatedTable uses "Latest" column when preferLatest=true. + /// This allows users to upgrade to the absolute latest, even with breaking changes. + /// + [Fact] + public void ParseBunOutdatedTableUsesLatestColumnWhenPreferLatest() + { + var output = """ + |-------------------------------------------------| + | Package | Current | Update | Latest | + |--------------------------------------------------| + | typescript | 5.2.0 | 5.3.0 | 6.0.0 | + | lodash | 4.17.0 | 4.17.21 | 4.17.21 | + |-------------------------------------------------| + """; + + var results = Bun.ParseBunOutdatedTable(output, preferLatest: true).ToList(); + + Assert.Equal(2, results.Count); + // typescript: uses Latest column (6.0.0), not Update (5.3.0) + Assert.Equal("typescript", results[0].Id); + Assert.Equal("6.0.0", results[0].NewVersion); + // lodash: Update == Latest, so included + Assert.Equal("lodash", results[1].Id); + Assert.Equal("4.17.21", results[1].NewVersion); + } + + /// + /// Tests parsing of the tree output from 'bun pm ls --global'. + /// Verifies that globally installed packages are correctly extracted from the tree structure. + /// + [Fact] + public void ParseInstalledPackagesOutputParsesTreeFormat() + { + var manager = new Bun(); + var installedOutput = PackageEngineFixtureFiles.ReadAllText(Path.Combine("Bun", "installed.txt")); + + var packages = Bun.ParseInstalledPackages(installedOutput, manager.DefaultSource, manager, + new OverridenInstallationOptions(PackageScope.Global)); + + var packageList = packages.ToArray(); + Assert.Equal(5, packageList.Length); + + // Verify first package + Assert.Equal("typescript", packageList[0].Id); + Assert.Equal("5.7.3", packageList[0].VersionString); + Assert.Equal(PackageScope.Global, packageList[0].OverridenOptions.Scope); + + // Verify scoped package + Assert.Equal("@devcontainers/cli", packageList[1].Id); + Assert.Equal("0.81.1", packageList[1].VersionString); + + // Verify last package + Assert.Equal("bunx", packageList[4].Id); + Assert.Equal("1.0.24", packageList[4].VersionString); + } + + /// + /// Tests that scoped packages (with @ prefix) are correctly handled. + /// + [Fact] + public void ParseInstalledPackagesHandlesScopedPackagesCorrectly() + { + var manager = new Bun(); + var output = """ + /home/user/.bun/install/global node_modules (2) + ├── @scope/package@1.0.0 + └── @another-scope/tool@2.5.3 + """; + + var packages = Bun.ParseInstalledPackages(output, manager.DefaultSource, manager, + new OverridenInstallationOptions(PackageScope.Global)); + + var packageList = packages.ToArray(); + Assert.Equal(2, packageList.Length); + Assert.Equal("@scope/package", packageList[0].Id); + Assert.Equal("1.0.0", packageList[0].VersionString); + Assert.Equal("@another-scope/tool", packageList[1].Id); + Assert.Equal("2.5.3", packageList[1].VersionString); + } + + /// + /// Tests that invalid or malformed tree lines are skipped during parsing. + /// + [Fact] + public void ParseInstalledPackagesSkipsInvalidLines() + { + var manager = new Bun(); + var output = """ + /home/user/.bun/install/global node_modules + Invalid line without marker + ├── valid-package@1.0.0 + └── another@2.0.0 + └── @invalid-version + """; + + var packages = Bun.ParseInstalledPackages(output, manager.DefaultSource, manager, + new OverridenInstallationOptions(PackageScope.Global)); + + // Should only parse the two valid packages + Assert.Equal(2, packages.Count); + } + + /// + /// Tests that search returns empty list for empty JSON array input. + /// + [Fact] + public void ParseSearchOutputReturnsEmptyForEmptyArray() + { + var manager = new Bun(); + var packages = Bun.ParseSearchOutput("[]", manager.DefaultSource, manager); + + Assert.Empty(packages); + } + + /// + /// Tests OperationHelper builds correct install parameters for basic install. + /// + [Fact] + public void OperationHelperBuildsInstallParameters() + { + var manager = new Bun(); + var package = new PackageBuilder() + .WithManager(manager) + .WithId("typescript") + .WithVersion("5.3.3") + .Build(); + + var parameters = manager.OperationHelper.GetParameters(package, new InstallOptions(), OperationType.Install); + + Assert.Contains("add", parameters); + Assert.Contains("typescript@5.3.3", parameters); + } + + /// + /// Tests OperationHelper correctly adds --global flag for global scope. + /// + [Fact] + public void OperationHelperAddsGlobalFlagForGlobalScope() + { + var manager = new Bun(); + var package = new PackageBuilder() + .WithManager(manager) + .WithId("typescript") + .WithVersion("5.3.3") + .Build(); + + var options = new InstallOptions { InstallationScope = PackageScope.Global }; + var parameters = manager.OperationHelper.GetParameters(package, options, OperationType.Install); + + Assert.Contains("--global", parameters); + } + + /// + /// Tests OperationHelper uses custom version when specified in options. + /// + [Fact] + public void OperationHelperUsesCustomVersionFromOptions() + { + var manager = new Bun(); + var package = new PackageBuilder() + .WithManager(manager) + .WithId("lodash") + .WithVersion("4.17.20") + .Build(); + + var options = new InstallOptions { Version = "4.17.21" }; + var parameters = manager.OperationHelper.GetParameters(package, options, OperationType.Install); + + Assert.Contains("lodash@4.17.21", parameters); + } + + /// + /// Tests OperationHelper builds correct uninstall parameters. + /// + [Fact] + public void OperationHelperBuildsUninstallParameters() + { + var manager = new Bun(); + var package = new PackageBuilder() + .WithManager(manager) + .WithId("typescript") + .WithVersion("5.3.3") + .Build(); + + var parameters = manager.OperationHelper.GetParameters(package, new InstallOptions(), OperationType.Uninstall); + + Assert.Contains("remove", parameters); + Assert.Contains("typescript", parameters); + } + + /// + /// Tests OperationHelper builds correct update parameters. + /// + [Fact] + public void OperationHelperBuildsUpdateParameters() + { + var manager = new Bun(); + var package = new PackageBuilder() + .WithManager(manager) + .WithId("typescript") + .WithVersion("5.2.0") + .WithNewVersion("5.4.0") + .Build(); + + var parameters = manager.OperationHelper.GetParameters(package, new InstallOptions(), OperationType.Update); + + Assert.Contains("add", parameters); + Assert.Contains("typescript@5.4.0", parameters); + } + + /// + /// Tests OperationHelper respects package's overridden scope over options scope. + /// + [Fact] + public void OperationHelperRespectsPackageScopeOverOption() + { + var manager = new Bun(); + var package = new PackageBuilder() + .WithManager(manager) + .WithId("typescript") + .WithVersion("5.3.3") + .WithOptions(new OverridenInstallationOptions(PackageScope.Global)) + .Build(); + + var options = new InstallOptions { InstallationScope = PackageScope.Local }; + var parameters = manager.OperationHelper.GetParameters(package, options, OperationType.Install); + + Assert.Contains("--global", parameters); + } + + /// + /// Tests OperationHelper includes custom install parameters. + /// + [Fact] + public void OperationHelperIncludesCustomParameters() + { + var manager = new Bun(); + var package = new PackageBuilder() + .WithManager(manager) + .WithId("typescript") + .WithVersion("5.3.3") + .Build(); + + var options = new InstallOptions(); + options.CustomParameters_Install.Add("--save-dev"); + options.CustomParameters_Install.Add("--no-save"); + + var parameters = manager.OperationHelper.GetParameters(package, options, OperationType.Install); + + Assert.Contains("--save-dev", parameters); + Assert.Contains("--no-save", parameters); + } + + /// + /// Tests OperationHelper returns Success for zero exit code. + /// + [Fact] + public void OperationHelperReturnsSuccessForZeroExitCode() + { + var manager = new Bun(); + var package = new PackageBuilder().WithManager(manager).Build(); + + var result = manager.OperationHelper.GetResult(package, OperationType.Install, [], 0); + + Assert.Equal(OperationVeredict.Success, result); + } + + /// + /// Tests OperationHelper returns Failure for non-zero exit code. + /// + [Fact] + public void OperationHelperReturnsFailureForNonZeroExitCode() + { + var manager = new Bun(); + var package = new PackageBuilder().WithManager(manager).Build(); + + var result = manager.OperationHelper.GetResult(package, OperationType.Install, [], 1); + + Assert.Equal(OperationVeredict.Failure, result); + } + + /// + /// Tests that manager has correct capabilities configured. + /// + [Fact] + public void BunManagerHasCorrectCapabilities() + { + var manager = new Bun(); + + Assert.True(manager.Capabilities.CanRunAsAdmin); + Assert.True(manager.Capabilities.SupportsCustomVersions); + Assert.True(manager.Capabilities.CanDownloadInstaller); + Assert.True(manager.Capabilities.SupportsCustomScopes); + Assert.True(manager.Capabilities.CanListDependencies); + Assert.True(manager.Capabilities.SupportsPreRelease); + Assert.Equal(ProxySupport.No, manager.Capabilities.SupportsProxy); + Assert.False(manager.Capabilities.SupportsProxyAuth); + } + + /// + /// Tests that manager properties are correctly configured. + /// + [Fact] + public void BunManagerHasCorrectProperties() + { + var manager = new Bun(); + + Assert.Equal("bun", manager.Properties.Id); + Assert.Equal("Bun", manager.Properties.Name); + Assert.Equal("add", manager.Properties.InstallVerb); + Assert.Equal("remove", manager.Properties.UninstallVerb); + Assert.Equal("add", manager.Properties.UpdateVerb); + Assert.NotNull(manager.Properties.DefaultSource); + Assert.Equal("https://www.npmjs.com/", manager.Properties.DefaultSource.Url.ToString()); + } + + /// + /// Tests that scoped packages with multiple @ symbols are handled correctly. + /// This is a regression test for packages like @scope/name@version. + /// + [Fact] + public void ParseInstalledPackagesHandlesMultipleAtSymbols() + { + var manager = new Bun(); + var output = """ + /home/user/.bun/install/global node_modules (1) + └── @babel/core@7.23.5 + """; + + var packages = Bun.ParseInstalledPackages(output, manager.DefaultSource, manager, + new OverridenInstallationOptions(PackageScope.Global)); + + Assert.Single(packages); + Assert.Equal("@babel/core", packages[0].Id); + Assert.Equal("7.23.5", packages[0].VersionString); + } + + /// + /// Tests that ParseBunOutdatedTable correctly identifies and skips the Package header. + /// + [Fact] + public void ParseBunOutdatedTableSkipsPackageHeader() + { + var output = """ + bun outdated v1.3.9 + |-------------------------------------------------| + | Package | Current | Update | Latest | + |--------------------------------------------------| + | typescript | 5.2.0 | 5.3.0 | 5.4.0 | + |-------------------------------------------------| + """; + + var results = Bun.ParseBunOutdatedTable(output).ToList(); + + Assert.Single(results); + Assert.Equal("typescript", results[0].Id); + Assert.Equal("5.2.0", results[0].Version); + Assert.Equal("5.3.0", results[0].NewVersion); + } + + /// + /// Tests that manager returns public interface correctly (IPackageManager compliance). + /// + [Fact] + public void ParseAvailableUpdatesRespectsPreferlatesParameter() + { + var manager = new Bun(); + var output = """ + |-------------------------------------------------| + | Package | Current | Update | Latest | + |--------------------------------------------------| + | typescript | 5.2.0 | 5.3.0 | 6.0.0 | + |-------------------------------------------------| + """; + + // Test with preferLatest=false (default): should use Update column (5.3.0) + var resultsUpdate = Bun.ParseAvailableUpdates(output, manager.DefaultSource, manager, preferLatest: false); + Assert.Single(resultsUpdate); + Assert.Equal("typescript", resultsUpdate[0].Id); + Assert.Equal("5.3.0", resultsUpdate[0].NewVersionString); + + // Test with preferLatest=true: should use Latest column (6.0.0) + var resultsLatest = Bun.ParseAvailableUpdates(output, manager.DefaultSource, manager, preferLatest: true); + Assert.Single(resultsLatest); + Assert.Equal("typescript", resultsLatest[0].Id); + Assert.Equal("6.0.0", resultsLatest[0].NewVersionString); + } + + /// + /// Tests that manager returns public interface correctly (IPackageManager compliance). + /// + [Fact] + public void BunImplementsIPackageManager() + { + var manager = new Bun(); + + // Verify that the manager implements IPackageManager interface + Assert.NotNull(manager); + Assert.NotNull(manager.DefaultSource); + Assert.NotNull(manager.OperationHelper); + } +} diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/installed.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/installed.txt new file mode 100644 index 0000000000..c785bb5c4c --- /dev/null +++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/installed.txt @@ -0,0 +1,6 @@ +/home/user/.bun/install/global node_modules (5) +├── typescript@5.7.3 +├── @devcontainers/cli@0.81.1 +├── esbuild@0.19.11 +├── @swc/cli@0.1.62 +└── bunx@1.0.24 diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/outdated-table.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/outdated-table.txt new file mode 100644 index 0000000000..bdb50eaa3e --- /dev/null +++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/outdated-table.txt @@ -0,0 +1,10 @@ +bun outdated v1.3.9 (cf6cdbbb) +|-------------------------------------------------| +| Package | Current | Update | Latest | +|--------------------------------------------------| +| typescript | 5.2.0 | 5.3.0 | 5.4.0 | +|--------------------------------------------------| +| @types/node | 20.8.0 | 20.9.0 | 20.10.6 | +|--------------------------------------------------| +| vite | 4.5.0 | 4.5.1 | 5.0.0 | +|-------------------------------------------------| diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/package-details.json b/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/package-details.json new file mode 100644 index 0000000000..126dc229d7 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/package-details.json @@ -0,0 +1,13 @@ +{ + "name": "typescript", + "version": "5.7.3", + "description": "TypeScript is a language for application scale JavaScript development", + "license": "Apache-2.0", + "homepage": "https://www.typescriptlang.org/", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/TypeScript.git" + }, + "author": "Microsoft Corp.", + "keywords": ["typescript", "language", "compiler"] +} diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/search-results.json b/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/search-results.json new file mode 100644 index 0000000000..4e36229770 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/search-results.json @@ -0,0 +1,17 @@ +[ + { + "name": "typescript", + "version": "5.3.3", + "description": "TypeScript is a language for application scale JavaScript development" + }, + { + "name": "lodash", + "version": "4.17.21", + "description": "Lodash modular utilities." + }, + { + "name": "@types/node", + "version": "20.10.6", + "description": "TypeScript definitions for Node.js" + } +] diff --git a/src/UniGetUI.PackageEngine.Tests/UniGetUI.PackageEngine.Tests.csproj b/src/UniGetUI.PackageEngine.Tests/UniGetUI.PackageEngine.Tests.csproj index 4d3f0352d5..e004b3de4e 100644 --- a/src/UniGetUI.PackageEngine.Tests/UniGetUI.PackageEngine.Tests.csproj +++ b/src/UniGetUI.PackageEngine.Tests/UniGetUI.PackageEngine.Tests.csproj @@ -24,6 +24,7 @@ + diff --git a/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs b/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs index 41ea995b56..d8564be3c9 100644 --- a/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs +++ b/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs @@ -11,6 +11,7 @@ using UniGetUI.Interface.Widgets; using UniGetUI.PackageEngine; using UniGetUI.PackageEngine.Interfaces; +using UniGetUI.PackageEngine.Managers.BunManager; using UniGetUI.PackageEngine.Managers.CargoManager; using UniGetUI.PackageEngine.Managers.ChocolateyManager; using UniGetUI.PackageEngine.Managers.DotNetManager; @@ -77,6 +78,8 @@ protected override void OnNavigatedTo(NavigationEventArgs e) Manager = PEInterface.PowerShell7; else if (Manager_T == typeof(Cargo)) Manager = PEInterface.Cargo; + else if (Manager_T == typeof(Bun)) + Manager = PEInterface.Bun; else if (Manager_T == typeof(Vcpkg)) Manager = PEInterface.Vcpkg; else if (Manager_T == typeof(DotNet)) @@ -325,6 +328,23 @@ protected override void OnNavigatedTo(NavigationEventArgs e) }; ExtraControls.Children.Add(Scoop_CleanupOnStart); } + // -------------------------------- BUN EXTRA SETTINGS ---------------------------------- + + else if (Manager is Bun) + { + DisableNotifsCard.CornerRadius = new CornerRadius(8, 8, 0, 0); + DisableNotifsCard.BorderThickness = new Thickness(1, 1, 1, 0); + ExtraControls.Children.Add(DisableNotifsCard); + + CheckboxCard Bun_PreferLatestVersions = new() + { + CornerRadius = new CornerRadius(0, 0, 8, 8), + BorderThickness = new Thickness(1, 0, 1, 1), + SettingName = Settings.K.PreferLatestVersionsForBun, + Text = CoreTools.Translate("Prefer latest versions (may include breaking changes) instead of recommended safe updates"), + }; + ExtraControls.Children.Add(Bun_PreferLatestVersions); + } // -------------------------------- VCPKG EXTRA SETTINGS -------------------------------------- else if (Manager is Vcpkg) From f071c8dadb319fc9c7e57394634b0ad1f2313216 Mon Sep 17 00:00:00 2001 From: socrat3z Date: Thu, 14 May 2026 12:38:21 +0300 Subject: [PATCH 03/10] Add Bun package manager support with updated descriptions, tests, and assets --- src/UniGetUI.Interface.Enums/Enums.cs | 1 + .../Bun.cs | 12 +- .../Helpers/BunPkgDetailsHelper.cs | 11 +- .../BunManagerTests.cs | 10 + .../Fixtures/Bun/package-info.json | 185 ++++++++++++++++++ src/UniGetUI/Assets/Symbols/bun.svg | 1 + 6 files changed, 211 insertions(+), 9 deletions(-) create mode 100644 src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/package-info.json create mode 100644 src/UniGetUI/Assets/Symbols/bun.svg diff --git a/src/UniGetUI.Interface.Enums/Enums.cs b/src/UniGetUI.Interface.Enums/Enums.cs index 7d813080c8..09e25a4e91 100644 --- a/src/UniGetUI.Interface.Enums/Enums.cs +++ b/src/UniGetUI.Interface.Enums/Enums.cs @@ -90,6 +90,7 @@ public enum IconType Pacman = '\uE946', Snap = '\uE947', Flatpak = '\uE948', + Bun = '\uE949', } public class NotificationArguments diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs b/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs index 89ccbe39e2..8abd14eecd 100644 --- a/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs +++ b/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs @@ -36,9 +36,9 @@ public Bun() { Id = "bun", Name = "Bun", - Description = CoreTools.Translate("A npmjs package manager written in Zig. Full of libraries and other utilities that orbit the javascript world
Contains: Node javascript libraries and other related utilities"), - IconId = IconType.Node, - ColorIconId = "node_color", + Description = CoreTools.Translate("Fast JavaScript runtime, bundler, and package manager"), + IconId = IconType.Bun, + ColorIconId = "bun_color", ExecutableFriendlyName = "bun", InstallVerb = "add", UninstallVerb = "remove", @@ -120,9 +120,11 @@ protected override IReadOnlyList GetAvailableUpdates_UnSafe() logger.AddToStdOut(strOut); logger.AddToStdErr(strErr); - // Parse stdout first; fall back to stderr if stdout has no table rows. - string tableSrc = ParseBunOutdatedTable(strOut).Any() ? strOut : strErr; + // Read the preference first bool preferLatest = Settings.Get(Settings.K.PreferLatestVersionsForBun); + + // Parse stdout first; fall back to stderr if stdout has no table rows. + string tableSrc = ParseBunOutdatedTable(strOut, preferLatest).Any() ? strOut : strErr; var result = ParseAvailableUpdates(tableSrc, DefaultSource, this, preferLatest); Logger.Info($"Bun: Found {result.Count} packages with available updates (preferLatest={preferLatest})"); diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs b/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs index cc6c3351b4..23c3ab23ff 100644 --- a/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs @@ -26,7 +26,7 @@ protected override void GetDetails_UnSafe(IPackageDetails details) p.StartInfo = new ProcessStartInfo { FileName = Manager.Status.ExecutablePath, - Arguments = Manager.Status.ExecutableCallArgs + " show " + details.Package.Id + " --json", + Arguments = Manager.Status.ExecutableCallArgs + " info " + details.Package.Id + " --json --global", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, @@ -50,7 +50,9 @@ protected override void GetDetails_UnSafe(IPackageDetails details) details.HomepageUrl = homepageUrl; details.Publisher = (contents?["maintainers"] as JsonArray)?[0]?.ToString(); - details.Author = contents?["author"]?.ToString(); + // Handle author which can be string or object with "name" property + var authorNode = contents?["author"]; + details.Author = authorNode is JsonObject authorObj ? authorObj["name"]?.ToString() : authorNode?.ToString(); details.UpdateDate = contents?["time"]?[contents?["dist-tags"]?["latest"]?.ToString() ?? details.Package.VersionString]?.ToString(); if (Uri.TryCreate(contents?["dist"]?["tarball"]?.ToString() ?? "", UriKind.RelativeOrAbsolute, out var installerUrl)) @@ -140,7 +142,7 @@ protected override IReadOnlyList GetInstallableVersions_UnSafe(IPackage { FileName = Manager.Status.ExecutablePath, Arguments = - Manager.Status.ExecutableCallArgs + " show " + package.Id + " versions --json", + Manager.Status.ExecutableCallArgs + " info " + package.Id + " --json --global", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, @@ -156,7 +158,8 @@ protected override IReadOnlyList GetInstallableVersions_UnSafe(IPackage string strContents = p.StandardOutput.ReadToEnd(); logger.AddToStdOut(strContents); - JsonArray? rawVersions = JsonNode.Parse(strContents) as JsonArray; + JsonObject? contents = JsonNode.Parse(strContents) as JsonObject; + JsonArray? rawVersions = contents?["versions"] as JsonArray; List versions = []; foreach(JsonNode? raw_ver in rawVersions ?? []) diff --git a/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs index 9cac3eed46..d2cc58fe97 100644 --- a/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs @@ -538,4 +538,14 @@ public void BunImplementsIPackageManager() Assert.NotNull(manager.DefaultSource); Assert.NotNull(manager.OperationHelper); } + + /// + /// Tests that package info fixture exists for details helper testing. + /// + [Fact] + public void PackageInfoFixtureExists() + { + string fixturePath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "Bun", "package-info.json"); + Assert.True(File.Exists(fixturePath), $"Fixture file not found at {fixturePath}"); + } } diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/package-info.json b/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/package-info.json new file mode 100644 index 0000000000..9215ddecf0 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/package-info.json @@ -0,0 +1,185 @@ +{ + "name": "lodash", + "version": "4.18.1", + "keywords": ["modules", "stdlib", "util"], + "author": { + "name": "John-David Dalton", + "email": "john.david.dalton@gmail.com" + }, + "license": "MIT", + "_id": "lodash@4.18.1", + "maintainers": [{ + "name": "mathias", + "email": "mathias@qiwi.be" +}, { +"name": "jdalton", +"email": "john.david.dalton@gmail.com" +}, { +"name": "bnjmnt4n", +"email": "benjamin@dev.ofcr.se" +}], +"contributors": [{ +"name": "John-David Dalton", +"email": "john.david.dalton@gmail.com" +}, { +"name": "Mathias Bynens", +"email": "mathias@qiwi.be" +}], +"homepage": "https://lodash.com/", +"bugs": { +"url": "https://github.com/lodash/lodash/issues" +}, +"dist": { +"shasum": "ff2b66c1f6326d59513de2407bf881439812771c", +"tarball": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", +"fileCount": 1051, +"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", +"signatures": [{ +"sig": "MEUCIQDZo+W4JJ5T/9RkS8Wz1uN7Gw+CBvP16yGeT8H9lnb+sAIgKM5Nqj636zyRL2YOUuhbSK2e1DhK2VBaMXiVNIYMq1M=", +"keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U" +}], +"unpackedSize": 1413741 +}, +"icon": "https://lodash.com/icon.svg", +"main": "lodash.js", +"gitHead": "4f0b76e2eca13de1c1fe8b4305abc1f7d63f4b86", +"scripts": { +"test": "echo \"See https://travis-ci.org/lodash-archive/lodash-cli for testing details.\"" +}, +"_npmUser": { +"name": "jdalton", +"email": "john.david.dalton@gmail.com" +}, +"repository": { +"url": "git+https://github.com/lodash/lodash.git", +"type": "git" +}, +"_npmVersion": "10.9.4", +"description": "Lodash modular utilities.", +"directories": {}, +"_nodeVersion": "22.21.1", +"_hasShrinkwrap": false, +"_npmOperationalInternal": { +"tmp": "tmp/lodash_4.18.1_1775077280191_0.6534118914634555", +"host": "s3://npm-registry-packages-npm-production" +}, +"versions": [ + "0.1.0", + "0.2.0", + "0.2.1", + "0.2.2", + "0.3.0", + "0.3.1", + "0.3.2", + "0.4.0", + "0.4.1", + "0.4.2", + "0.5.0-rc.1", + "0.5.0", + "0.5.1", + "0.5.2", + "0.6.0", + "0.6.1", + "0.7.0", + "0.8.0", + "0.8.1", + "0.8.2", + "0.9.0", + "0.9.1", + "0.9.2", + "0.10.0", + "1.0.0-rc.1", + "1.0.0-rc.2", + "1.0.0-rc.3", + "1.0.0", + "1.0.1", + "1.1.0", + "1.1.1", + "1.2.0", + "1.2.1", + "1.3.0", + "1.3.1", + "2.0.0", + "2.1.0", + "2.2.0", + "2.2.1", + "2.3.0", + "2.4.0", + "2.4.1", + "3.0.0", + "3.0.1", + "3.1.0", + "3.2.0", + "3.3.0", + "3.3.1", + "3.4.0", + "3.5.0", + "3.6.0", + "1.0.2", + "3.7.0", + "2.4.2", + "3.8.0", + "3.9.0", + "3.9.1", + "3.9.2", + "3.9.3", + "3.10.0", + "3.10.1", + "4.0.0", + "4.0.1", + "4.1.0", + "4.2.0", + "4.2.1", + "4.3.0", + "4.4.0", + "4.5.0", + "4.5.1", + "4.6.0", + "4.6.1", + "4.7.0", + "4.8.0", + "4.8.1", + "4.8.2", + "4.9.0", + "4.10.0", + "4.11.0", + "4.11.1", + "4.11.2", + "4.12.0", + "4.13.0", + "4.13.1", + "4.14.0", + "4.14.1", + "4.14.2", + "4.15.0", + "4.16.0", + "4.16.1", + "4.16.2", + "4.16.3", + "4.16.4", + "4.16.5", + "4.16.6", + "4.17.0", + "4.17.1", + "4.17.2", + "4.17.3", + "4.17.4", + "4.17.5", + "4.17.9", + "4.17.10", + "4.17.11", + "4.17.12", + "4.17.13", + "4.17.14", + "4.17.15", + "4.17.16", + "4.17.17", + "4.17.18", + "4.17.19", + "4.17.20", + "4.17.21", + "4.17.23", + "4.18.0", + "4.18.1" +] +} diff --git a/src/UniGetUI/Assets/Symbols/bun.svg b/src/UniGetUI/Assets/Symbols/bun.svg new file mode 100644 index 0000000000..278b63c529 --- /dev/null +++ b/src/UniGetUI/Assets/Symbols/bun.svg @@ -0,0 +1 @@ +Bun \ No newline at end of file From 0e3c7d92206515572b767bf86b86a7a05b605619 Mon Sep 17 00:00:00 2001 From: socrat3z Date: Thu, 14 May 2026 12:50:30 +0300 Subject: [PATCH 04/10] Refactor Bun package manager to enforce global-only installation and update detection, add tests for local scope handling and global package manifest requirements --- .../Bun.cs | 25 +++- .../Helpers/BunPkgDetailsHelper.cs | 16 ++- .../Helpers/BunPkgOperationHelper.cs | 4 +- .../BunManagerTests.cs | 122 +++++++++++++++++- 4 files changed, 153 insertions(+), 14 deletions(-) diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs b/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs index 8abd14eecd..c4a932bf8e 100644 --- a/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs +++ b/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs @@ -25,7 +25,7 @@ public Bun() CanRunAsAdmin = true, SupportsCustomVersions = true, CanDownloadInstaller = true, - SupportsCustomScopes = true, + SupportsCustomScopes = false, CanListDependencies = true, SupportsPreRelease = true, SupportsProxy = ProxySupport.No, @@ -84,10 +84,16 @@ protected override IReadOnlyList FindPackages_UnSafe(string query) protected override IReadOnlyList GetAvailableUpdates_UnSafe() { // bun outdated checks the project in the current directory, not a --global flag. - // Global packages live in ~/.bun/install/global which has its own package.json. - string globalDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".bun", "install", "global"); + // Until Bun supports per-project working directories in UniGetUI, expose Bun as + // a global-only manager and query the dedicated global package.json. + string globalDir = GetGlobalPackagesDirectory( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); + + if (!HasGlobalPackageManifest(globalDir)) + { + Logger.Info($"Bun: Skipping global update detection because {globalDir} is missing package.json"); + return []; + } using Process p = new() { @@ -100,8 +106,7 @@ protected override IReadOnlyList GetAvailableUpdates_UnSafe() RedirectStandardInput = true, UseShellExecute = false, CreateNoWindow = true, - WorkingDirectory = Directory.Exists(globalDir) ? globalDir - : Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + WorkingDirectory = globalDir, StandardOutputEncoding = System.Text.Encoding.UTF8 } }; @@ -176,6 +181,12 @@ protected override IReadOnlyList GetInstalledPackages_UnSafe() public override IReadOnlyList FindCandidateExecutableFiles() => CoreTools.WhichMultiple(OperatingSystem.IsWindows() ? "bun.exe" : "bun"); + internal static string GetGlobalPackagesDirectory(string userProfile) + => Path.Combine(userProfile, ".bun", "install", "global"); + + internal static bool HasGlobalPackageManifest(string globalDir) + => Directory.Exists(globalDir) && File.Exists(Path.Combine(globalDir, "package.json")); + protected override void _loadManagerExecutableFile(out bool found, out string path, out string callArguments) { var (_found, _executablePath) = GetExecutableFile(); diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs b/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs index 23c3ab23ff..a5c67cb226 100644 --- a/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs @@ -128,10 +128,18 @@ protected override IReadOnlyList GetScreenshots_UnSafe(IPackage package) protected override string? GetInstallLocation_UnSafe(IPackage package) { - if (package.OverridenOptions.Scope is PackageScope.Local) - return Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "node_modules", package.Id); - return Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Roaming", "npm", - "node_modules", package.Id); + return GetInstallLocation( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + package.OverridenOptions.Scope, + package.Id); + } + + internal static string GetInstallLocation(string userProfile, string? scope, string packageId) + { + if (scope is PackageScope.Local) + return Path.Join(userProfile, "node_modules", packageId); + + return Path.Join(Bun.GetGlobalPackagesDirectory(userProfile), "node_modules", packageId); } protected override IReadOnlyList GetInstallableVersions_UnSafe(IPackage package) diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgOperationHelper.cs index ee65db2f76..6fd9496bdc 100644 --- a/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgOperationHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgOperationHelper.cs @@ -31,8 +31,8 @@ protected override IReadOnlyList _getOperationParameters(IPackage packag _ => throw new InvalidDataException("Invalid package operation") }; - if (package.OverridenOptions.Scope == PackageScope.Global || - (package.OverridenOptions.Scope is null && options.InstallationScope == PackageScope.Global)) + string effectiveScope = package.OverridenOptions.Scope ?? options.InstallationScope; + if (effectiveScope is not PackageScope.Local) parameters.Add("--global"); parameters.AddRange(operation switch diff --git a/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs index d2cc58fe97..87d9612a2d 100644 --- a/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs @@ -1,6 +1,7 @@ using UniGetUI.PackageEngine.Enums; using UniGetUI.PackageEngine.Managers.BunManager; using UniGetUI.PackageEngine.ManagerClasses.Manager; +using UniGetUI.PackageEngine.PackageClasses; using UniGetUI.PackageEngine.Serializable; using UniGetUI.PackageEngine.Structs; using UniGetUI.PackageEngine.Tests.Infrastructure.Assertions; @@ -16,6 +17,11 @@ namespace UniGetUI.PackageEngine.Tests; /// public sealed class BunManagerTests { + private sealed class TestableBun : Bun + { + public IReadOnlyList GetAvailableUpdatesUnsafe() => base.GetAvailableUpdates_UnSafe(); + } + /// /// Tests parsing of JSON search results from 'bun search <query> --json'. /// Verifies that multiple packages with different names and versions are correctly parsed. @@ -268,6 +274,7 @@ public void OperationHelperBuildsInstallParameters() Assert.Contains("add", parameters); Assert.Contains("typescript@5.3.3", parameters); + Assert.Contains("--global", parameters); } /// @@ -367,6 +374,25 @@ public void OperationHelperRespectsPackageScopeOverOption() Assert.Contains("--global", parameters); } + /// + /// Tests OperationHelper allows explicit local installs when scope is overridden. + /// + [Fact] + public void OperationHelperDoesNotAddGlobalFlagForExplicitLocalScope() + { + var manager = new Bun(); + var package = new PackageBuilder() + .WithManager(manager) + .WithId("typescript") + .WithVersion("5.3.3") + .WithOptions(new OverridenInstallationOptions(PackageScope.Local)) + .Build(); + + var parameters = manager.OperationHelper.GetParameters(package, new InstallOptions(), OperationType.Install); + + Assert.DoesNotContain("--global", parameters); + } + /// /// Tests OperationHelper includes custom install parameters. /// @@ -429,7 +455,7 @@ public void BunManagerHasCorrectCapabilities() Assert.True(manager.Capabilities.CanRunAsAdmin); Assert.True(manager.Capabilities.SupportsCustomVersions); Assert.True(manager.Capabilities.CanDownloadInstaller); - Assert.True(manager.Capabilities.SupportsCustomScopes); + Assert.False(manager.Capabilities.SupportsCustomScopes); Assert.True(manager.Capabilities.CanListDependencies); Assert.True(manager.Capabilities.SupportsPreRelease); Assert.Equal(ProxySupport.No, manager.Capabilities.SupportsProxy); @@ -525,6 +551,100 @@ public void ParseAvailableUpdatesRespectsPreferlatesParameter() Assert.Equal("6.0.0", resultsLatest[0].NewVersionString); } + /// + /// Tests Bun global updates are skipped when there is no global package manifest. + /// + [Fact] + public void HasGlobalPackageManifestRequiresPackageJson() + { + string globalDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + + try + { + Directory.CreateDirectory(globalDir); + + Assert.False(Bun.HasGlobalPackageManifest(globalDir)); + + File.WriteAllText(Path.Combine(globalDir, "package.json"), "{}"); + + Assert.True(Bun.HasGlobalPackageManifest(globalDir)); + } + finally + { + if (Directory.Exists(globalDir)) + Directory.Delete(globalDir, recursive: true); + } + } + + /// + /// Tests Bun update detection returns no results when the global manifest is missing. + /// + [Fact] + public void GetAvailableUpdatesReturnsEmptyWhenGlobalManifestIsMissing() + { + string userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + string globalDir = Bun.GetGlobalPackagesDirectory(userProfile); + string manifestPath = Path.Combine(globalDir, "package.json"); + string backupManifestPath = manifestPath + ".bun-test-backup"; + + if (!Directory.Exists(globalDir)) + { + Assert.Empty(new TestableBun().GetAvailableUpdatesUnsafe()); + return; + } + + bool restoreManifest = false; + + try + { + if (File.Exists(backupManifestPath)) + File.Delete(backupManifestPath); + + if (File.Exists(manifestPath)) + { + File.Move(manifestPath, backupManifestPath); + restoreManifest = true; + } + + Assert.Empty(new TestableBun().GetAvailableUpdatesUnsafe()); + } + finally + { + if (restoreManifest) + { + if (File.Exists(manifestPath)) + File.Delete(manifestPath); + File.Move(backupManifestPath, manifestPath); + } + } + } + + /// + /// Tests Bun install location defaults to the global Bun node_modules directory. + /// + [Fact] + public void GetInstallLocationDefaultsToBunGlobalNodeModules() + { + string userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + string location = BunPkgDetailsHelper.GetInstallLocation(userProfile, null, "typescript"); + + Assert.Equal( + Path.Join(userProfile, ".bun", "install", "global", "node_modules", "typescript"), + location); + } + + /// + /// Tests Bun install location honors explicit local scope. + /// + [Fact] + public void GetInstallLocationUsesLocalNodeModulesForLocalScope() + { + string userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + string location = BunPkgDetailsHelper.GetInstallLocation(userProfile, PackageScope.Local, "typescript"); + + Assert.Equal(Path.Join(userProfile, "node_modules", "typescript"), location); + } + /// /// Tests that manager returns public interface correctly (IPackageManager compliance). /// From f94db63baaee9e3a626ddf266eefe35631fbbf69 Mon Sep 17 00:00:00 2001 From: socrat3z Date: Thu, 14 May 2026 13:26:58 +0300 Subject: [PATCH 05/10] Enhance Bun package manager to skip border lines in outdated table parsing, add tests for border line handling and Unicode formats --- .../Bun.cs | 8 +- .../BunManagerTests.cs | 138 ++++++++++++++++++ 2 files changed, 143 insertions(+), 3 deletions(-) diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs b/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs index c4a932bf8e..45d06e1141 100644 --- a/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs +++ b/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs @@ -348,11 +348,13 @@ internal static IReadOnlyList ParseInstalledPackages( string version = parts[2].Trim(); string recommendedUpdate = parts[columnIndex].Trim(); - // Skip header row and empty rows + // Skip header row, empty rows, and border lines (which contain only dashes or box-drawing chars) if (id is "Package" || string.IsNullOrWhiteSpace(id) - || string.IsNullOrWhiteSpace(version) || string.IsNullOrWhiteSpace(recommendedUpdate)) + || string.IsNullOrWhiteSpace(version) || string.IsNullOrWhiteSpace(recommendedUpdate) + || id.All(c => c == '-' || c == '─' || c == '┬' || c == '┼' || c == '┴') + || version.All(c => c == '-' || c == '─' || c == '┬' || c == '┼' || c == '┴')) { - Logger.Debug($"Bun: Skipping line {lineNum} (header/empty): {id}/{version}/{recommendedUpdate}"); + Logger.Debug($"Bun: Skipping line {lineNum} (header/empty/border): {id}/{version}/{recommendedUpdate}"); continue; } diff --git a/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs index 87d9612a2d..460c9e2fa1 100644 --- a/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs @@ -115,6 +115,57 @@ public void ParseBunOutdatedTableReturnsEmptyForInvalidInput() Assert.Empty(results); } + /// + /// Tests that ParseBunOutdatedTable skips border lines (all dashes or box-drawing chars). + /// Verifies that lines like |---| or ├─┤ are not parsed as packages. + /// + [Fact] + public void ParseBunOutdatedTableSkipsBorderLines() + { + var output = """ + bun outdated v1.3.9 + |-------------------------------------------------| + | Package | Current | Update | Latest | + |--------------------------------------------------| + | typescript | 5.2.0 | 5.3.0 | 5.4.0 | + |-------------------------------------------------| + | lodash | 4.17.0 | 4.17.21 | 4.17.21 | + |-------------------------------------------------| + """; + + var results = Bun.ParseBunOutdatedTable(output).ToList(); + + // Should find both typescript and lodash with updates, and skip all border lines + Assert.Equal(2, results.Count); + Assert.Equal("typescript", results[0].Id); + Assert.Equal("lodash", results[1].Id); + } + + /// + /// Tests that ParseBunOutdatedTable skips Unicode box-drawing borders. + /// + [Fact] + public void ParseBunOutdatedTableSkipsUnicodeBorders() + { + var output = """ + bun outdated v1.3.10 + ┌────────────────┬─────────┬────────┬────────┐ + │ Package │ Current │ Update │ Latest │ + ├────────────────┼─────────┼────────┼────────┤ + │ typescript │ 5.2.0 │ 5.3.0 │ 5.4.0 │ + ├────────────────┼─────────┼────────┼────────┤ + │ lodash │ 4.17.0 │ 4.17.21│ 4.17.21│ + └────────────────┴─────────┴────────┴────────┘ + """; + + var results = Bun.ParseBunOutdatedTable(output).ToList(); + + // Should find both packages with updates, and skip Unicode border lines + Assert.Equal(2, results.Count); + Assert.Equal("typescript", results[0].Id); + Assert.Equal("lodash", results[1].Id); + } + /// /// Tests that ParseBunOutdatedTable uses "Update" column by default (preferLatest=false). /// This provides safe, semantic-versioning compatible updates. @@ -523,6 +574,93 @@ bun outdated v1.3.9 Assert.Equal("5.3.0", results[0].NewVersion); } + /// + /// Tests parsing of Unicode box-drawing table format from 'bun outdated' (v1.3.10+). + /// Real-world output uses Unicode box-drawing characters instead of ASCII pipes. + /// Verifies that the parser correctly handles both formats. + /// + [Fact] + public void ParseBunOutdatedTableHandlesUnicodeBoxDrawingFormat() + { + var output = """ + bun outdated v1.3.10 (30e609e0) + ┌───────────────────────────┬─────────┬────────┬────────┐ + │ Package │ Current │ Update │ Latest │ + ├───────────────────────────┼─────────┼────────┼────────┤ + │ @google/gemini-cli │ 0.32.1 │ 0.32.1 │ 0.42.0 │ + ├───────────────────────────┼─────────┼────────┼────────┤ + │ @oh-my-pi/pi-coding-agent │ 15.0.0 │ 15.0.0 │ 15.0.1 │ + └───────────────────────────┴─────────┴────────┴────────┘ + """; + + var results = Bun.ParseBunOutdatedTable(output).ToList(); + + // Both packages have Current == Update, so they should NOT be included (no updates available) + Assert.Empty(results); + } + + /// + /// Tests that ParseBunOutdatedTable correctly handles Unicode format with actual updates available. + /// + [Fact] + public void ParseBunOutdatedTableHandlesUnicodeBoxDrawingWithUpdates() + { + var output = """ + bun outdated v1.3.10 + ┌────────────────┬─────────┬────────┬────────┐ + │ Package │ Current │ Update │ Latest │ + ├────────────────┼─────────┼────────┼────────┤ + │ typescript │ 5.2.0 │ 5.3.0 │ 5.4.0 │ + ├────────────────┼─────────┼────────┼────────┤ + │ @types/node │ 20.8.0 │ 20.9.0 │ 20.10.0│ + └────────────────┴─────────┴────────┴────────┘ + """; + + var results = Bun.ParseBunOutdatedTable(output).ToList(); + + Assert.Equal(2, results.Count); + Assert.Equal("typescript", results[0].Id); + Assert.Equal("5.2.0", results[0].Version); + Assert.Equal("5.3.0", results[0].NewVersion); + Assert.Equal("@types/node", results[1].Id); + Assert.Equal("20.8.0", results[1].Version); + Assert.Equal("20.9.0", results[1].NewVersion); + } + + /// + /// Tests that Unicode box-drawing format works with preferLatest=true. + /// This scenario matches when a user enables PreferLatestVersionsForBun setting + /// and runs bun outdated with the new Unicode table format. + /// + [Fact] + public void ParseBunOutdatedTableHandlesUnicodeWithPreferLatest() + { + var output = """ + bun outdated v1.3.10 (30e609e0) + ┌───────────────────────────┬─────────┬────────┬────────┐ + │ Package │ Current │ Update │ Latest │ + ├───────────────────────────┼─────────┼────────┼────────┤ + │ @google/gemini-cli │ 0.32.1 │ 0.32.1 │ 0.42.0 │ + ├───────────────────────────┼─────────┼────────┼────────┤ + │ @oh-my-pi/pi-coding-agent │ 15.0.0 │ 15.0.0 │ 15.0.1 │ + └───────────────────────────┴─────────┴────────┴────────┘ + """; + + // With preferLatest=false (default), no updates because Update column = Current + var resultsDefault = Bun.ParseBunOutdatedTable(output, preferLatest: false).ToList(); + Assert.Empty(resultsDefault); + + // With preferLatest=true, both packages should show as having updates from Latest column + var resultsPreferLatest = Bun.ParseBunOutdatedTable(output, preferLatest: true).ToList(); + Assert.Equal(2, resultsPreferLatest.Count); + Assert.Equal("@google/gemini-cli", resultsPreferLatest[0].Id); + Assert.Equal("0.32.1", resultsPreferLatest[0].Version); + Assert.Equal("0.42.0", resultsPreferLatest[0].NewVersion); + Assert.Equal("@oh-my-pi/pi-coding-agent", resultsPreferLatest[1].Id); + Assert.Equal("15.0.0", resultsPreferLatest[1].Version); + Assert.Equal("15.0.1", resultsPreferLatest[1].NewVersion); + } + /// /// Tests that manager returns public interface correctly (IPackageManager compliance). /// From 1f4ecd2ed653b1997dc6204e39a3d3fc7c6398d5 Mon Sep 17 00:00:00 2001 From: socrat3z Date: Thu, 14 May 2026 16:06:21 +0300 Subject: [PATCH 06/10] Refactor Bun package manager to read process output asynchronously and enhance border line handling in package parsing --- .../SettingsPages/PackageManagerPage.axaml.cs | 2 +- src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs | 14 ++++++++++---- .../Helpers/BunPkgDetailsHelper.cs | 12 ++++++++---- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs index 8b732980b9..72db779ec4 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs @@ -469,7 +469,7 @@ private void BuildExtraControls(CheckboxCard_Dict disableNotifsCard) CornerRadius = new CornerRadius(0, 0, 8, 8), BorderThickness = new Thickness(1, 0, 1, 1), SettingName = CoreSettings.K.PreferLatestVersionsForBun, - Text = CoreTools.AutoTranslated("Prefer latest versions (may include breaking changes) instead of recommended safe updates"), + Text = CoreTools.Translate("Prefer latest versions instead of recommended safe (minor or patch) updates"), }); break; diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs b/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs index 45d06e1141..814f94ff23 100644 --- a/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs +++ b/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs @@ -166,12 +166,18 @@ protected override IReadOnlyList GetInstalledPackages_UnSafe() IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.ListInstalledPackages, p); p.Start(); - string strContents = p.StandardOutput.ReadToEnd(); + // Read both streams concurrently to avoid deadlock when the process writes + // to both. Bun may write to stderr when stdout is not a TTY. + var stdoutTask = p.StandardOutput.ReadToEndAsync(); + var stderrTask = p.StandardError.ReadToEndAsync(); + Task.WaitAll(stdoutTask, stderrTask); + + string strContents = stdoutTask.Result; logger.AddToStdOut(strContents); Packages.AddRange(ParseInstalledPackages(strContents, DefaultSource, this, new OverridenInstallationOptions(PackageScope.Global))); - logger.AddToStdErr(p.StandardError.ReadToEnd()); + logger.AddToStdErr(stderrTask.Result); p.WaitForExit(); logger.Close(p.ExitCode); @@ -351,8 +357,8 @@ internal static IReadOnlyList ParseInstalledPackages( // Skip header row, empty rows, and border lines (which contain only dashes or box-drawing chars) if (id is "Package" || string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(version) || string.IsNullOrWhiteSpace(recommendedUpdate) - || id.All(c => c == '-' || c == '─' || c == '┬' || c == '┼' || c == '┴') - || version.All(c => c == '-' || c == '─' || c == '┬' || c == '┼' || c == '┴')) + || id.All(c => c == '-' || c == '─' || c == '┬' || c == '┼' || c == '┴' || c == '├' || c == '┤' || c == '┌' || c == '└' || c == '┘' || c == '┐') + || version.All(c => c == '-' || c == '─' || c == '┬' || c == '┼' || c == '┴' || c == '├' || c == '┤' || c == '┌' || c == '└' || c == '┘' || c == '┐')) { Logger.Debug($"Bun: Skipping line {lineNum} (header/empty/border): {id}/{version}/{recommendedUpdate}"); continue; diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs b/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs index a5c67cb226..f56b561ef1 100644 --- a/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs @@ -39,7 +39,11 @@ protected override void GetDetails_UnSafe(IPackageDetails details) IProcessTaskLogger logger = Manager.TaskLogger.CreateNew(LoggableTaskType.LoadPackageDetails, p); p.Start(); - string strContents = p.StandardOutput.ReadToEnd(); + var stdoutTask = p.StandardOutput.ReadToEndAsync(); + var stderrTask = p.StandardError.ReadToEndAsync(); + Task.WaitAll(stdoutTask, stderrTask); + + string strContents = stdoutTask.Result; logger.AddToStdOut(strContents); JsonObject? contents = JsonNode.Parse(strContents) as JsonObject; @@ -104,7 +108,7 @@ protected override void GetDetails_UnSafe(IPackageDetails details) }); } - logger.AddToStdErr(p.StandardError.ReadToEnd()); + logger.AddToStdErr(stderrTask.Result); p.WaitForExit(); logger.Close(p.ExitCode); } @@ -118,12 +122,12 @@ protected override void GetDetails_UnSafe(IPackageDetails details) protected override CacheableIcon? GetIcon_UnSafe(IPackage package) { - throw new NotImplementedException(); + return null; } protected override IReadOnlyList GetScreenshots_UnSafe(IPackage package) { - throw new NotImplementedException(); + return []; } protected override string? GetInstallLocation_UnSafe(IPackage package) From 4dbcba355e381ce5183fe7b772e53b584cdbaa95 Mon Sep 17 00:00:00 2001 From: socrat3z Date: Thu, 14 May 2026 16:20:48 +0300 Subject: [PATCH 07/10] Update README and CLI documentation to include Bun package manager; refactor settings for Bun version preferences --- README.md | 4 +- docs/CLI.md | 2 +- .../SettingsPages/PackageManagerPage.axaml.cs | 4 +- .../SettingsEngine_Names.cs | 4 +- .../Bun.cs | 9 +- .../BunManagerTests.cs | 22 +- .../Fixtures/Bun/package-details.json | 13 -- .../Fixtures/Bun/package-info.json | 209 +++--------------- .../ManagersPages/PackageManager.xaml.cs | 2 +- 9 files changed, 47 insertions(+), 222 deletions(-) delete mode 100644 src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/package-details.json diff --git a/README.md b/README.md index d53d95b7ed..ac3bc26b1f 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ [![Release Version Badge](https://img.shields.io/github/v/release/Devolutions/UniGetUI?style=for-the-badge)](https://github.com/Devolutions/UniGetUI/releases) [![Issues Badge](https://img.shields.io/github/issues/Devolutions/UniGetUI?style=for-the-badge)](https://github.com/Devolutions/UniGetUI/issues) [![Closed Issues Badge](https://img.shields.io/github/issues-closed/Devolutions/UniGetUI?color=%238256d0&style=for-the-badge)](https://github.com/Devolutions/UniGetUI/issues?q=is%3Aissue+is%3Aclosed)
-UniGetUI is an intuitive GUI for the most common CLI package managers on Windows 10 and 11, including [WinGet](https://learn.microsoft.com/en-us/windows/package-manager/), [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/), [pip](https://pypi.org/), [npm](https://www.npmjs.com/), [.NET Tool](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-tool-install), [PowerShell Gallery](https://www.powershellgallery.com/), and more. +UniGetUI is an intuitive GUI for the most common CLI package managers on Windows 10 and 11, including [WinGet](https://learn.microsoft.com/en-us/windows/package-manager/), [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/), [pip](https://pypi.org/), [npm](https://www.npmjs.com/), [Bun](https://bun.sh/), [.NET Tool](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-tool-install), [PowerShell Gallery](https://www.powershellgallery.com/), and more. With UniGetUI, you can discover, install, update, and uninstall software from multiple package managers through one interface. ![image](https://github.com/user-attachments/assets/7cb447ca-ee8b-4bce-8561-b9332fb0139a) @@ -92,7 +92,7 @@ UniGetUI has a built-in autoupdater. However, it can also be updated like any ot ## Features - - Install, update, and remove software from your system easily at one click: UniGetUI combines the packages from the most used package managers for windows: Winget, Chocolatey, Scoop, Pip, Npm and .NET Tool. + - Install, update, and remove software from your system easily at one click: UniGetUI combines the packages from the most used package managers for windows: Winget, Chocolatey, Scoop, Pip, Npm, Bun and .NET Tool. - Discover new packages and filter them to easily find the package you want. - View detailed metadata about any package before installing it. Get the direct download URL or the name of the publisher, as well as the size of the download. - Easily bulk-install, update, or uninstall multiple packages at once selecting multiple packages before performing an operation diff --git a/docs/CLI.md b/docs/CLI.md index d58c05f164..b7c9211b57 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -51,7 +51,7 @@ Related environment variables: - `--source` maps to `--package-source` - Boolean options use explicit values such as `--enabled true` or `--wait false`. - `--detach` is shorthand for asynchronous package operations (`--wait false`). -- `--manager` uses stable manager ids, not GUI labels. Current ids: `apt`, `cargo`, `chocolatey`, `dnf`, `dotnet-tool`, `flatpak`, `homebrew`, `npm`, `pacman`, `pip`, `pwsh`, `scoop`, `snap`, `vcpkg`, `winget`, and `winps`. +- `--manager` uses stable manager ids, not GUI labels. Current ids: `apt`, `bun`, `cargo`, `chocolatey`, `dnf`, `dotnet-tool`, `flatpak`, `homebrew`, `npm`, `pacman`, `pip`, `pwsh`, `scoop`, `snap`, `vcpkg`, `winget`, and `winps`. ## Command reference diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs index 72db779ec4..8840c5b266 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml.cs @@ -468,8 +468,8 @@ private void BuildExtraControls(CheckboxCard_Dict disableNotifsCard) { CornerRadius = new CornerRadius(0, 0, 8, 8), BorderThickness = new Thickness(1, 0, 1, 1), - SettingName = CoreSettings.K.PreferLatestVersionsForBun, - Text = CoreTools.Translate("Prefer latest versions instead of recommended safe (minor or patch) updates"), + SettingName = CoreSettings.K.BunPreferLatestVersions, + Text = CoreTools.Translate("Prefer latest versions (may include breaking changes) instead of recommended safe updates"), }); break; diff --git a/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs b/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs index 071e744291..601c1865f6 100644 --- a/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs +++ b/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs @@ -91,7 +91,7 @@ public enum K WinGetComApiPolicy, DisableClassicMode, DisableInstallerHostChangeWarning, - PreferLatestVersionsForBun, + BunPreferLatestVersions, Test1, Test2, @@ -194,7 +194,7 @@ public static string ResolveKey(K key) K.WinGetComApiPolicy => "WinGetComApiPolicy", K.DisableClassicMode => "DisableClassicMode", K.DisableInstallerHostChangeWarning => "DisableInstallerHostChangeWarning", - K.PreferLatestVersionsForBun => "PreferLatestVersionsForBun", + K.BunPreferLatestVersions => "BunPreferLatestVersions", K.Test1 => "TestSetting1", K.Test2 => "TestSetting2", diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs b/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs index 814f94ff23..c9ab969555 100644 --- a/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs +++ b/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs @@ -126,7 +126,7 @@ protected override IReadOnlyList GetAvailableUpdates_UnSafe() logger.AddToStdErr(strErr); // Read the preference first - bool preferLatest = Settings.Get(Settings.K.PreferLatestVersionsForBun); + bool preferLatest = Settings.Get(Settings.K.BunPreferLatestVersions); // Parse stdout first; fall back to stderr if stdout has no table rows. string tableSrc = ParseBunOutdatedTable(strOut, preferLatest).Any() ? strOut : strErr; @@ -330,23 +330,19 @@ internal static IReadOnlyList ParseInstalledPackages( int columnIndex = preferLatest ? 4 : 3; // 4 = Latest, 3 = Update string columnName = preferLatest ? "Latest" : "Update"; - int lineNum = 0; foreach (string line in output.Split('\n')) { - lineNum++; string trimmed = line.TrimStart(); // Skip lines that don't contain package data (headers, separators, etc.) if (!trimmed.StartsWith('│') && !trimmed.StartsWith('|')) { - Logger.Debug($"Bun: Skipping line {lineNum} (no pipe): {line.Substring(0, Math.Min(50, line.Length))}"); continue; - } + } // Split by either Unicode box-drawing or ASCII pipe characters string[] parts = line.Split(new[] { '│', '|' }, StringSplitOptions.None); if (parts.Length < columnIndex + 1) { - Logger.Debug($"Bun: Skipping line {lineNum} (insufficient parts for {columnName}): {line.Substring(0, Math.Min(50, line.Length))}"); continue; } @@ -360,7 +356,6 @@ internal static IReadOnlyList ParseInstalledPackages( || id.All(c => c == '-' || c == '─' || c == '┬' || c == '┼' || c == '┴' || c == '├' || c == '┤' || c == '┌' || c == '└' || c == '┘' || c == '┐') || version.All(c => c == '-' || c == '─' || c == '┬' || c == '┼' || c == '┴' || c == '├' || c == '┤' || c == '┌' || c == '└' || c == '┘' || c == '┐')) { - Logger.Debug($"Bun: Skipping line {lineNum} (header/empty/border): {id}/{version}/{recommendedUpdate}"); continue; } diff --git a/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs index 460c9e2fa1..e524a3e093 100644 --- a/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs @@ -587,9 +587,9 @@ bun outdated v1.3.10 (30e609e0) ┌───────────────────────────┬─────────┬────────┬────────┐ │ Package │ Current │ Update │ Latest │ ├───────────────────────────┼─────────┼────────┼────────┤ - │ @google/gemini-cli │ 0.32.1 │ 0.32.1 │ 0.42.0 │ + │ @types/jest │ 29.6.0 │ 29.6.0 │ 30.0.0 │ ├───────────────────────────┼─────────┼────────┼────────┤ - │ @oh-my-pi/pi-coding-agent │ 15.0.0 │ 15.0.0 │ 15.0.1 │ + │ @org/shared-lib │ 3.2.0 │ 3.2.0 │ 4.0.0 │ └───────────────────────────┴─────────┴────────┴────────┘ """; @@ -629,7 +629,7 @@ bun outdated v1.3.10 /// /// Tests that Unicode box-drawing format works with preferLatest=true. - /// This scenario matches when a user enables PreferLatestVersionsForBun setting + /// This scenario matches when a user enables BunPreferLatestVersions setting /// and runs bun outdated with the new Unicode table format. /// [Fact] @@ -640,9 +640,9 @@ bun outdated v1.3.10 (30e609e0) ┌───────────────────────────┬─────────┬────────┬────────┐ │ Package │ Current │ Update │ Latest │ ├───────────────────────────┼─────────┼────────┼────────┤ - │ @google/gemini-cli │ 0.32.1 │ 0.32.1 │ 0.42.0 │ + │ @types/jest │ 29.6.0 │ 29.6.0 │ 30.0.0 │ ├───────────────────────────┼─────────┼────────┼────────┤ - │ @oh-my-pi/pi-coding-agent │ 15.0.0 │ 15.0.0 │ 15.0.1 │ + │ @org/shared-lib │ 3.2.0 │ 3.2.0 │ 4.0.0 │ └───────────────────────────┴─────────┴────────┴────────┘ """; @@ -653,12 +653,12 @@ bun outdated v1.3.10 (30e609e0) // With preferLatest=true, both packages should show as having updates from Latest column var resultsPreferLatest = Bun.ParseBunOutdatedTable(output, preferLatest: true).ToList(); Assert.Equal(2, resultsPreferLatest.Count); - Assert.Equal("@google/gemini-cli", resultsPreferLatest[0].Id); - Assert.Equal("0.32.1", resultsPreferLatest[0].Version); - Assert.Equal("0.42.0", resultsPreferLatest[0].NewVersion); - Assert.Equal("@oh-my-pi/pi-coding-agent", resultsPreferLatest[1].Id); - Assert.Equal("15.0.0", resultsPreferLatest[1].Version); - Assert.Equal("15.0.1", resultsPreferLatest[1].NewVersion); + Assert.Equal("@types/jest", resultsPreferLatest[0].Id); + Assert.Equal("29.6.0", resultsPreferLatest[0].Version); + Assert.Equal("30.0.0", resultsPreferLatest[0].NewVersion); + Assert.Equal("@org/shared-lib", resultsPreferLatest[1].Id); + Assert.Equal("3.2.0", resultsPreferLatest[1].Version); + Assert.Equal("4.0.0", resultsPreferLatest[1].NewVersion); } /// diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/package-details.json b/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/package-details.json deleted file mode 100644 index 126dc229d7..0000000000 --- a/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/package-details.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "typescript", - "version": "5.7.3", - "description": "TypeScript is a language for application scale JavaScript development", - "license": "Apache-2.0", - "homepage": "https://www.typescriptlang.org/", - "repository": { - "type": "git", - "url": "https://github.com/microsoft/TypeScript.git" - }, - "author": "Microsoft Corp.", - "keywords": ["typescript", "language", "compiler"] -} diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/package-info.json b/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/package-info.json index 9215ddecf0..5f5e464845 100644 --- a/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/package-info.json +++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/package-info.json @@ -1,185 +1,28 @@ { - "name": "lodash", - "version": "4.18.1", - "keywords": ["modules", "stdlib", "util"], - "author": { - "name": "John-David Dalton", - "email": "john.david.dalton@gmail.com" - }, - "license": "MIT", - "_id": "lodash@4.18.1", - "maintainers": [{ - "name": "mathias", - "email": "mathias@qiwi.be" -}, { -"name": "jdalton", -"email": "john.david.dalton@gmail.com" -}, { -"name": "bnjmnt4n", -"email": "benjamin@dev.ofcr.se" -}], -"contributors": [{ -"name": "John-David Dalton", -"email": "john.david.dalton@gmail.com" -}, { -"name": "Mathias Bynens", -"email": "mathias@qiwi.be" -}], -"homepage": "https://lodash.com/", -"bugs": { -"url": "https://github.com/lodash/lodash/issues" -}, -"dist": { -"shasum": "ff2b66c1f6326d59513de2407bf881439812771c", -"tarball": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", -"fileCount": 1051, -"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", -"signatures": [{ -"sig": "MEUCIQDZo+W4JJ5T/9RkS8Wz1uN7Gw+CBvP16yGeT8H9lnb+sAIgKM5Nqj636zyRL2YOUuhbSK2e1DhK2VBaMXiVNIYMq1M=", -"keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U" -}], -"unpackedSize": 1413741 -}, -"icon": "https://lodash.com/icon.svg", -"main": "lodash.js", -"gitHead": "4f0b76e2eca13de1c1fe8b4305abc1f7d63f4b86", -"scripts": { -"test": "echo \"See https://travis-ci.org/lodash-archive/lodash-cli for testing details.\"" -}, -"_npmUser": { -"name": "jdalton", -"email": "john.david.dalton@gmail.com" -}, -"repository": { -"url": "git+https://github.com/lodash/lodash.git", -"type": "git" -}, -"_npmVersion": "10.9.4", -"description": "Lodash modular utilities.", -"directories": {}, -"_nodeVersion": "22.21.1", -"_hasShrinkwrap": false, -"_npmOperationalInternal": { -"tmp": "tmp/lodash_4.18.1_1775077280191_0.6534118914634555", -"host": "s3://npm-registry-packages-npm-production" -}, -"versions": [ - "0.1.0", - "0.2.0", - "0.2.1", - "0.2.2", - "0.3.0", - "0.3.1", - "0.3.2", - "0.4.0", - "0.4.1", - "0.4.2", - "0.5.0-rc.1", - "0.5.0", - "0.5.1", - "0.5.2", - "0.6.0", - "0.6.1", - "0.7.0", - "0.8.0", - "0.8.1", - "0.8.2", - "0.9.0", - "0.9.1", - "0.9.2", - "0.10.0", - "1.0.0-rc.1", - "1.0.0-rc.2", - "1.0.0-rc.3", - "1.0.0", - "1.0.1", - "1.1.0", - "1.1.1", - "1.2.0", - "1.2.1", - "1.3.0", - "1.3.1", - "2.0.0", - "2.1.0", - "2.2.0", - "2.2.1", - "2.3.0", - "2.4.0", - "2.4.1", - "3.0.0", - "3.0.1", - "3.1.0", - "3.2.0", - "3.3.0", - "3.3.1", - "3.4.0", - "3.5.0", - "3.6.0", - "1.0.2", - "3.7.0", - "2.4.2", - "3.8.0", - "3.9.0", - "3.9.1", - "3.9.2", - "3.9.3", - "3.10.0", - "3.10.1", - "4.0.0", - "4.0.1", - "4.1.0", - "4.2.0", - "4.2.1", - "4.3.0", - "4.4.0", - "4.5.0", - "4.5.1", - "4.6.0", - "4.6.1", - "4.7.0", - "4.8.0", - "4.8.1", - "4.8.2", - "4.9.0", - "4.10.0", - "4.11.0", - "4.11.1", - "4.11.2", - "4.12.0", - "4.13.0", - "4.13.1", - "4.14.0", - "4.14.1", - "4.14.2", - "4.15.0", - "4.16.0", - "4.16.1", - "4.16.2", - "4.16.3", - "4.16.4", - "4.16.5", - "4.16.6", - "4.17.0", - "4.17.1", - "4.17.2", - "4.17.3", - "4.17.4", - "4.17.5", - "4.17.9", - "4.17.10", - "4.17.11", - "4.17.12", - "4.17.13", - "4.17.14", - "4.17.15", - "4.17.16", - "4.17.17", - "4.17.18", - "4.17.19", - "4.17.20", - "4.17.21", - "4.17.23", - "4.18.0", - "4.18.1" -] + "name": "example-lib", + "version": "2.0.0", + "description": "An example library for testing", + "license": "MIT", + "homepage": "https://example.com/lib", + "author": "Example Author", + "maintainers": ["maintainer1"], + "time": { + "2.0.0": "2025-01-15T00:00:00.000Z" + }, + "dist-tags": { + "latest": "2.0.0" + }, + "dist": { + "tarball": "https://registry.example.com/example-lib/-/example-lib-2.0.0.tgz", + "unpackedSize": 12345, + "integrity": "sha512-exampleIntegrityHash==" + }, + "dependencies": { + "helper-lib": "^1.0.0" + }, + "versions": [ + "1.0.0", + "1.1.0", + "2.0.0" + ] } diff --git a/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs b/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs index d8564be3c9..eddb4a0a59 100644 --- a/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs +++ b/src/UniGetUI/Pages/SettingsPages/ManagersPages/PackageManager.xaml.cs @@ -340,7 +340,7 @@ protected override void OnNavigatedTo(NavigationEventArgs e) { CornerRadius = new CornerRadius(0, 0, 8, 8), BorderThickness = new Thickness(1, 0, 1, 1), - SettingName = Settings.K.PreferLatestVersionsForBun, + SettingName = Settings.K.BunPreferLatestVersions, Text = CoreTools.Translate("Prefer latest versions (may include breaking changes) instead of recommended safe updates"), }; ExtraControls.Children.Add(Bun_PreferLatestVersions); From 1d59ee668435198c0f8e28edc1e887282ac5d248 Mon Sep 17 00:00:00 2001 From: socrat3z Date: Thu, 14 May 2026 16:23:26 +0300 Subject: [PATCH 08/10] Refactor Bun package manager helpers for improved code consistency and readability --- .../Helpers/BunPkgDetailsHelper.cs | 8 ++++---- .../Helpers/BunPkgOperationHelper.cs | 1 + src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs b/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs index f56b561ef1..59de3d4ddb 100644 --- a/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgDetailsHelper.cs @@ -71,7 +71,7 @@ protected override void GetDetails_UnSafe(IPackageDetails details) HashSet addedDeps = new(); foreach (var rawDep in (contents?["dependencies"]?.AsObject() ?? [])) { - if(addedDeps.Contains(rawDep.Key)) continue; + if (addedDeps.Contains(rawDep.Key)) continue; addedDeps.Add(rawDep.Key); details.Dependencies.Add(new() @@ -84,7 +84,7 @@ protected override void GetDetails_UnSafe(IPackageDetails details) foreach (var rawDep in (contents?["devDependencies"]?.AsObject() ?? [])) { - if(addedDeps.Contains(rawDep.Key)) continue; + if (addedDeps.Contains(rawDep.Key)) continue; addedDeps.Add(rawDep.Key); details.Dependencies.Add(new() @@ -97,7 +97,7 @@ protected override void GetDetails_UnSafe(IPackageDetails details) foreach (var rawDep in (contents?["peerDependencies"]?.AsObject() ?? [])) { - if(addedDeps.Contains(rawDep.Key)) continue; + if (addedDeps.Contains(rawDep.Key)) continue; addedDeps.Add(rawDep.Key); details.Dependencies.Add(new() @@ -174,7 +174,7 @@ protected override IReadOnlyList GetInstallableVersions_UnSafe(IPackage JsonArray? rawVersions = contents?["versions"] as JsonArray; List versions = []; - foreach(JsonNode? raw_ver in rawVersions ?? []) + foreach (JsonNode? raw_ver in rawVersions ?? []) { if (raw_ver is not null) versions.Add(raw_ver.ToString()); diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgOperationHelper.cs index 6fd9496bdc..82ec061abd 100644 --- a/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgOperationHelper.cs +++ b/src/UniGetUI.PackageEngine.Managers.Bun/Helpers/BunPkgOperationHelper.cs @@ -4,6 +4,7 @@ using UniGetUI.PackageEngine.Serializable; namespace UniGetUI.PackageEngine.Managers.BunManager; + internal sealed class BunPkgOperationHelper : BasePkgOperationHelper { public BunPkgOperationHelper(Bun manager) : base(manager) { } diff --git a/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs index e524a3e093..660228c1f4 100644 --- a/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs @@ -1,13 +1,13 @@ +using UniGetUI.Interface.Enums; using UniGetUI.PackageEngine.Enums; -using UniGetUI.PackageEngine.Managers.BunManager; using UniGetUI.PackageEngine.ManagerClasses.Manager; +using UniGetUI.PackageEngine.Managers.BunManager; using UniGetUI.PackageEngine.PackageClasses; using UniGetUI.PackageEngine.Serializable; using UniGetUI.PackageEngine.Structs; using UniGetUI.PackageEngine.Tests.Infrastructure.Assertions; using UniGetUI.PackageEngine.Tests.Infrastructure.Builders; using UniGetUI.PackageEngine.Tests.Infrastructure.Helpers; -using UniGetUI.Interface.Enums; namespace UniGetUI.PackageEngine.Tests; From 937522c70401c7731448e454c1934fe0ade416b7 Mon Sep 17 00:00:00 2001 From: socrat3z Date: Thu, 14 May 2026 16:28:51 +0300 Subject: [PATCH 09/10] Revert SDK version in global.json to 10.0.103 for consistency --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 50baaf1a03..1ea831d6cd 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.300", + "version": "10.0.103", "rollForward": "latestPatch" } } From 5547bd9e69c8cfc356daadfdde1a2c7f0be1bffa Mon Sep 17 00:00:00 2001 From: socrat3z Date: Thu, 14 May 2026 16:34:16 +0300 Subject: [PATCH 10/10] Refactor global.json formatting for consistency; add TODO for JSON deserialization in Bun package manager --- global.json | 8 ++++---- src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/global.json b/global.json index 1ea831d6cd..c358071f08 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { - "sdk": { - "version": "10.0.103", - "rollForward": "latestPatch" - } + "sdk": { + "version": "10.0.103", + "rollForward": "latestPatch" + } } diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs b/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs index c9ab969555..8e5f90f7ac 100644 --- a/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs +++ b/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs @@ -323,6 +323,9 @@ internal static IReadOnlyList ParseInstalledPackages( /// If preferLatest is true, uses "Latest" (parts[4], may have breaking changes). /// If preferLatest is false (default), uses "Update" (parts[3], safe semantic version). /// + // TODO: Replace table parsing with JSON deserialization when bun outdated adds --json flag. + // Track: https://github.com/oven-sh/bun/issues — once --json is available, this entire + // method should be swapped for a simple JsonNode.Parse() call. internal static IEnumerable<(string Id, string Version, string NewVersion)> ParseBunOutdatedTable( string output, bool preferLatest = false)