diff --git a/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs b/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs index 491372278c..24204326f6 100644 --- a/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs +++ b/src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs @@ -54,32 +54,29 @@ public Bun() 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); - logger.AddToStdErr(p.StandardError.ReadToEnd()); - p.WaitForExit(); - logger.Close(p.ExitCode); + // Bun has no `search` subcommand; it resolves packages from the npm registry. + // Query the registry's search endpoint directly (the same data npm search uses). + INativeTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.FindPackages); + string url = "https://registry.npmjs.org/-/v1/search?size=100&text=" + Uri.EscapeDataString(query); + logger.Log($"Querying the npm registry: {url}"); - return ParseSearchOutput(strContents, DefaultSource, this); + try + { + using HttpClient client = new(CoreTools.GenericHttpClientParameters); + client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreData.UserAgentString); + string strContents = client.GetStringAsync(url).GetAwaiter().GetResult(); + logger.Log(strContents); + + var packages = ParseSearchOutput(strContents, DefaultSource, this); + logger.Close(0); + return packages; + } + catch (Exception e) + { + logger.Error(e); + logger.Close(1); + return []; + } } protected override IReadOnlyList GetAvailableUpdates_UnSafe() @@ -146,6 +143,18 @@ protected override IReadOnlyList GetAvailableUpdates_UnSafe() protected override IReadOnlyList GetInstalledPackages_UnSafe() { + // `bun pm ls --global` is a no-op for the --global flag: Bun ignores it and walks + // up from the working directory to the nearest lockfile. List the dedicated global + // package.json by running from that directory instead, mirroring GetAvailableUpdates_UnSafe. + string globalDir = GetGlobalPackagesDirectory( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); + + if (!HasGlobalPackageManifest(globalDir)) + { + Logger.Info($"Bun: Skipping installed package detection because {globalDir} is missing package.json"); + return []; + } + List Packages = []; using Process p = new() @@ -153,13 +162,13 @@ protected override IReadOnlyList GetInstalledPackages_UnSafe() StartInfo = new ProcessStartInfo { FileName = Status.ExecutablePath, - Arguments = Status.ExecutableCallArgs + " pm ls --global", + Arguments = Status.ExecutableCallArgs + " pm ls", RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = true, UseShellExecute = false, CreateNoWindow = true, - WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + WorkingDirectory = globalDir, StandardOutputEncoding = System.Text.Encoding.UTF8 } }; @@ -186,7 +195,16 @@ protected override IReadOnlyList GetInstalledPackages_UnSafe() } public override IReadOnlyList FindCandidateExecutableFiles() - => CoreTools.WhichMultiple(OperatingSystem.IsWindows() ? "bun.exe" : "bun"); + { + if (!OperatingSystem.IsWindows()) + return CoreTools.WhichMultiple("bun"); + + // The official installer ships bun.exe, but `npm i -g bun` only puts a bun.cmd + // wrapper on the PATH (the real bun.exe stays inside node_modules). Search both. + List candidates = [.. CoreTools.WhichMultiple("bun.exe")]; + candidates.AddRange(CoreTools.WhichMultiple("bun.cmd")); + return candidates; + } internal static string GetGlobalPackagesDirectory(string userProfile) => Path.Combine(userProfile, ".bun", "install", "global"); @@ -209,7 +227,7 @@ protected override void _loadManagerVersion(out string version) StartInfo = new ProcessStartInfo { FileName = Status.ExecutablePath, - Arguments = Status.ExecutableCallArgs + "--version", + Arguments = Status.ExecutableCallArgs + " --version", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, @@ -225,8 +243,8 @@ protected override void _loadManagerVersion(out string version) } /// - /// Parses JSON search results from 'bun search <query> --json'. - /// Each result object contains 'name' and 'version' fields. + /// Parses the npm registry search response (https://registry.npmjs.org/-/v1/search). + /// The payload is { "objects": [ { "package": { "name", "version" } } ] }. /// internal static IReadOnlyList ParseSearchOutput( string output, @@ -239,11 +257,12 @@ internal static IReadOnlyList ParseSearchOutput( try { - JsonArray? results = JsonNode.Parse(output) as JsonArray; + JsonArray? results = (JsonNode.Parse(output) as JsonObject)?["objects"] as JsonArray; foreach (JsonNode? entry in results ?? []) { - string? id = entry?["name"]?.ToString(); - string? version = entry?["version"]?.ToString(); + JsonNode? pkg = entry?["package"]; + string? id = pkg?["name"]?.ToString(); + string? version = pkg?["version"]?.ToString(); if (id is not null && version is not null) { packages.Add(new Package(CoreTools.FormatAsName(id), id, version, source, manager)); diff --git a/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs index 660228c1f4..913b231cef 100644 --- a/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/BunManagerTests.cs @@ -23,11 +23,11 @@ private sealed class TestableBun : Bun } /// - /// Tests parsing of JSON search results from 'bun search <query> --json'. + /// Tests parsing of the npm registry search response (registry.npmjs.org/-/v1/search). /// Verifies that multiple packages with different names and versions are correctly parsed. /// [Fact] - public void ParseSearchOutputParsesJsonArray() + public void ParseSearchOutputParsesRegistryResponse() { var manager = new Bun(); @@ -297,13 +297,13 @@ Invalid line without marker } /// - /// Tests that search returns empty list for empty JSON array input. + /// Tests that search returns empty list for an empty registry response. /// [Fact] - public void ParseSearchOutputReturnsEmptyForEmptyArray() + public void ParseSearchOutputReturnsEmptyForEmptyResults() { var manager = new Bun(); - var packages = Bun.ParseSearchOutput("[]", manager.DefaultSource, manager); + var packages = Bun.ParseSearchOutput("""{"objects":[],"total":0}""", manager.DefaultSource, manager); Assert.Empty(packages); } diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/search-results.json b/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/search-results.json index 4e36229770..fee6deb195 100644 --- a/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/search-results.json +++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Bun/search-results.json @@ -1,17 +1,27 @@ -[ - { - "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" - } -] +{ + "objects": [ + { + "package": { + "name": "typescript", + "version": "5.3.3", + "description": "TypeScript is a language for application scale JavaScript development" + } + }, + { + "package": { + "name": "lodash", + "version": "4.17.21", + "description": "Lodash modular utilities." + } + }, + { + "package": { + "name": "@types/node", + "version": "20.10.6", + "description": "TypeScript definitions for Node.js" + } + } + ], + "total": 3, + "time": "Mon Jun 04 2026 00:00:00 GMT+0000" +}