From 47566df0525ba64cd277b3eb0e8347b78c49c1e0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 16 May 2026 07:09:40 +0000
Subject: [PATCH 1/3] Initial plan
From 9e688cc1f27cf7b353399ab7d4f260bf6f2e5173 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 16 May 2026 07:18:19 +0000
Subject: [PATCH 2/3] Implement smarter CLI update flow and global update hints
Agent-Logs-Url: https://github.com/Cratis/cli/sessions/64a26cda-1d9a-43de-8af8-4929a5567467
Co-authored-by: einari <134365+einari@users.noreply.github.com>
---
.../for_CliUpdate/when_detecting_strategy.cs | 54 +++++
Source/Cli/Cli.csproj | 4 +
Source/Cli/CliUpdate.cs | 216 ++++++++++++++++++
.../Commands/Chronicle/ChronicleCommand.cs | 34 ---
.../Cli/Commands/Version/SelfUpdateCommand.cs | 55 +++--
Source/Cli/Commands/Version/VersionCommand.cs | 2 +-
Source/Cli/Program.cs | 34 ++-
7 files changed, 344 insertions(+), 55 deletions(-)
create mode 100644 Source/Cli.Specs/for_CliUpdate/when_detecting_strategy.cs
create mode 100644 Source/Cli/CliUpdate.cs
diff --git a/Source/Cli.Specs/for_CliUpdate/when_detecting_strategy.cs b/Source/Cli.Specs/for_CliUpdate/when_detecting_strategy.cs
new file mode 100644
index 0000000..68e7b71
--- /dev/null
+++ b/Source/Cli.Specs/for_CliUpdate/when_detecting_strategy.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Cratis.Cli.for_CliUpdate;
+
+public class when_detecting_strategy : Specification
+{
+ [Fact]
+ void should_detect_dotnet_tool_from_path()
+ {
+ var strategy = CliUpdate.DetectStrategy(
+ "/home/user/.dotnet/tools/cratis",
+ "/home/user/.dotnet/tools/.store/cratis.cli/",
+ isNativeBuild: false,
+ isMacOS: false,
+ isLinux: true);
+
+ strategy.ShouldEqual(CliUpdateStrategy.DotNetTool);
+ }
+
+ [Fact]
+ void should_detect_homebrew_when_native_on_macos()
+ {
+ var strategy = CliUpdate.DetectStrategy(
+ "/opt/homebrew/Cellar/cratis/1.2.3/bin/cratis",
+ "/opt/homebrew/Cellar/cratis/1.2.3/bin/",
+ isNativeBuild: true,
+ isMacOS: true,
+ isLinux: false);
+
+ strategy.ShouldEqual(CliUpdateStrategy.Homebrew);
+ }
+
+ [Fact]
+ void should_detect_manual_linux_when_native_on_linux()
+ {
+ var strategy = CliUpdate.DetectStrategy(
+ "/usr/local/bin/cratis",
+ "/usr/local/bin/",
+ isNativeBuild: true,
+ isMacOS: false,
+ isLinux: true);
+
+ strategy.ShouldEqual(CliUpdateStrategy.ManualLinux);
+ }
+
+ [Fact]
+ void should_use_cratis_update_hint_for_auto_update_strategies()
+ {
+ var hint = CliUpdate.GetUpdateHint(CliUpdateStrategy.Homebrew, "1.0.0", "1.1.0");
+ hint.ShouldContain("run 'cratis update'");
+ hint.ShouldContain("1.0.0 -> 1.1.0");
+ }
+}
diff --git a/Source/Cli/Cli.csproj b/Source/Cli/Cli.csproj
index 1bac8ee..008d56a 100644
--- a/Source/Cli/Cli.csproj
+++ b/Source/Cli/Cli.csproj
@@ -14,6 +14,10 @@
README.md
+
+ $(DefineConstants);CRATIS_NATIVE
+
+
diff --git a/Source/Cli/CliUpdate.cs b/Source/Cli/CliUpdate.cs
new file mode 100644
index 0000000..c36b8aa
--- /dev/null
+++ b/Source/Cli/CliUpdate.cs
@@ -0,0 +1,216 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+
+namespace Cratis.Cli;
+
+///
+/// Represents the update strategy for the current CLI installation.
+///
+public enum CliUpdateStrategy
+{
+ ///
+ /// Update by running dotnet tool update -g Cratis.Cli.
+ ///
+ DotNetTool,
+
+ ///
+ /// Update by running brew upgrade cratis.
+ ///
+ Homebrew,
+
+ ///
+ /// Update manually by downloading and replacing the Linux native binary.
+ ///
+ ManualLinux,
+
+ ///
+ /// Update manually using the installation mechanism used by the user.
+ ///
+ Manual
+}
+
+///
+/// Provides update-strategy detection and update-related command guidance.
+///
+public static class CliUpdate
+{
+#if CRATIS_NATIVE
+ const bool IsNativeBuild = true;
+#else
+ const bool IsNativeBuild = false;
+#endif
+
+ const string PackageId = "Cratis.Cli";
+
+ ///
+ /// Detects how this CLI instance should be updated.
+ ///
+ /// The update strategy.
+ public static CliUpdateStrategy DetectStrategy()
+ {
+ var processPath = GetEffectiveProcessPath();
+ var baseDirectory = AppContext.BaseDirectory;
+ return DetectStrategy(
+ processPath,
+ baseDirectory,
+ IsNativeBuild,
+ OperatingSystem.IsMacOS(),
+ OperatingSystem.IsLinux());
+ }
+
+ ///
+ /// Creates the process launch settings for automatic update, when available.
+ ///
+ /// The detected update strategy.
+ /// Optional target version.
+ /// A process start info for supported auto-update paths, otherwise null.
+ public static ProcessStartInfo? CreateUpdateProcessStartInfo(CliUpdateStrategy strategy, string? targetVersion = null)
+ {
+ if (strategy == CliUpdateStrategy.DotNetTool)
+ {
+ var arguments = $"tool update -g {PackageId}";
+ if (!string.IsNullOrWhiteSpace(targetVersion))
+ {
+ arguments += $" --version {targetVersion}";
+ }
+
+ return new ProcessStartInfo
+ {
+ FileName = "dotnet",
+ Arguments = arguments,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+ }
+
+ if (strategy == CliUpdateStrategy.Homebrew)
+ {
+ if (!string.IsNullOrWhiteSpace(targetVersion))
+ {
+ return null;
+ }
+
+ return new ProcessStartInfo
+ {
+ FileName = "brew",
+ Arguments = "upgrade cratis",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+ }
+
+ return null;
+ }
+
+ ///
+ /// Gets manual update guidance for non-automatic update strategies.
+ ///
+ /// The detected strategy.
+ /// Manual guidance text, or null when auto-update is supported.
+ public static string? GetManualUpdateInstructions(CliUpdateStrategy strategy) =>
+ strategy switch
+ {
+ CliUpdateStrategy.ManualLinux =>
+ "Manual update (Linux):\n" +
+ "curl -Lo cratis.tar.gz https://github.com/Cratis/cli/releases/latest/download/cratis-linux-x64.tar.gz\n" +
+ "# arm64: curl -Lo cratis.tar.gz https://github.com/Cratis/cli/releases/latest/download/cratis-linux-arm64.tar.gz\n" +
+ "tar -xzf cratis.tar.gz\n" +
+ "sudo mv cratis /usr/local/bin/cratis",
+ CliUpdateStrategy.Manual => "This installation method cannot be auto-updated by the CLI. Please upgrade using the same method you used to install it.",
+ _ => null
+ };
+
+ ///
+ /// Gets update hint text for interactive output when a newer version is available.
+ ///
+ /// The detected strategy.
+ /// Current version.
+ /// Latest available version.
+ /// A user-facing hint message.
+ public static string GetUpdateHint(CliUpdateStrategy strategy, string currentVersion, string latestVersion)
+ {
+ var baseMessage = $"Update available: {currentVersion} -> {latestVersion}";
+ return strategy switch
+ {
+ CliUpdateStrategy.ManualLinux => $"{baseMessage} — manual update required (download the latest Linux tarball and replace the 'cratis' binary)",
+ CliUpdateStrategy.Manual => $"{baseMessage} — manual update required (use your original installation method)",
+ _ => $"{baseMessage} — run 'cratis update'"
+ };
+ }
+
+ internal static CliUpdateStrategy DetectStrategy(
+ string? processPath,
+ string? baseDirectory,
+ bool isNativeBuild,
+ bool isMacOS,
+ bool isLinux)
+ {
+ if (IsDotNetToolPath(processPath) || IsDotNetToolPath(baseDirectory))
+ {
+ return CliUpdateStrategy.DotNetTool;
+ }
+
+ if (!isNativeBuild)
+ {
+ return CliUpdateStrategy.DotNetTool;
+ }
+
+ if (isMacOS && IsHomebrewPath(processPath))
+ {
+ return CliUpdateStrategy.Homebrew;
+ }
+
+ if (isLinux)
+ {
+ return CliUpdateStrategy.ManualLinux;
+ }
+
+ return CliUpdateStrategy.Manual;
+ }
+
+ static bool IsDotNetToolPath(string? path) =>
+ !string.IsNullOrWhiteSpace(path) &&
+ (path.Contains(".dotnet/tools", StringComparison.OrdinalIgnoreCase) ||
+ path.Contains(".dotnet\\tools", StringComparison.OrdinalIgnoreCase));
+
+ static bool IsHomebrewPath(string? path) =>
+ !string.IsNullOrWhiteSpace(path) &&
+ (path.Contains("/Cellar/cratis/", StringComparison.OrdinalIgnoreCase) ||
+ path.Contains("/Homebrew/Cellar/cratis/", StringComparison.OrdinalIgnoreCase));
+
+ static string? GetEffectiveProcessPath()
+ {
+ var path = Environment.ProcessPath;
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return path;
+ }
+
+ var currentPath = path;
+ for (var i = 0; i < 8; i++)
+ {
+ try
+ {
+ var linkTarget = File.ResolveLinkTarget(currentPath, false);
+ if (linkTarget is null)
+ {
+ return currentPath;
+ }
+
+ currentPath = linkTarget.FullName;
+ }
+ catch
+ {
+ return currentPath;
+ }
+ }
+
+ return currentPath;
+ }
+}
diff --git a/Source/Cli/Commands/Chronicle/ChronicleCommand.cs b/Source/Cli/Commands/Chronicle/ChronicleCommand.cs
index 779d351..2a14aa4 100644
--- a/Source/Cli/Commands/Chronicle/ChronicleCommand.cs
+++ b/Source/Cli/Commands/Chronicle/ChronicleCommand.cs
@@ -4,7 +4,6 @@
using System.Diagnostics;
using System.Net.Sockets;
using System.Text.RegularExpressions;
-using Cratis.Cli.Commands.Version;
using Grpc.Core;
namespace Cratis.Cli.Commands.Chronicle;
@@ -29,11 +28,6 @@ protected sealed override async Task ExecuteAsync(CommandContext context, T
WriteDebugInfo(settings);
}
- // Start update check in the background — never blocks the command.
- using var updateCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
- updateCts.CancelAfter(TimeSpan.FromSeconds(5));
- var updateCheckTask = UpdateChecker.CheckForUpdate(VersionCommand.GetCliVersion(), updateCts.Token);
-
var tokenRefreshAttempted = false;
while (true)
{
@@ -65,7 +59,6 @@ protected sealed override async Task ExecuteAsync(CommandContext context, T
await Console.Error.WriteLineAsync($"[debug] command completed in {sw.ElapsedMilliseconds}ms, exit code {exitCode}");
}
- await ShowUpdateHint(updateCheckTask, format);
return exitCode;
}
catch (RpcException ex) when (!tokenRefreshAttempted && IsHttpUnauthorized(ex))
@@ -179,31 +172,4 @@ static bool IsNetworkException(Exception? ex)
return false;
}
-
- static async Task ShowUpdateHint(Task updateCheckTask, string format)
- {
- try
- {
- if (!updateCheckTask.IsCompleted)
- {
- return;
- }
-
- var latestVersion = await updateCheckTask;
- if (latestVersion is null)
- {
- return;
- }
-
- if (string.Equals(format, OutputFormats.Table, StringComparison.Ordinal))
- {
- AnsiConsole.WriteLine();
- AnsiConsole.MarkupLine($" [{OutputFormatter.Warning.ToMarkup()}]\u2191 Update available:[/] {latestVersion.EscapeMarkup()} \u2014 run [bold]cratis update[/] to upgrade");
- }
- }
- catch
- {
- // Update check failures are non-critical.
- }
- }
}
diff --git a/Source/Cli/Commands/Version/SelfUpdateCommand.cs b/Source/Cli/Commands/Version/SelfUpdateCommand.cs
index ad8e9bb..33c0ce4 100644
--- a/Source/Cli/Commands/Version/SelfUpdateCommand.cs
+++ b/Source/Cli/Commands/Version/SelfUpdateCommand.cs
@@ -6,9 +6,9 @@
namespace Cratis.Cli.Commands.Version;
///
-/// Updates the Cratis CLI to the latest (or a specific) version using dotnet tool update.
+/// Updates the Cratis CLI to the latest version using the detected installation method.
///
-[LlmDescription("Updates the cratis CLI to the latest version from NuGet. Use instead of remembering the NuGet package name.")]
+[LlmDescription("Updates the cratis CLI to the latest version using the appropriate installation method (dotnet tool or Homebrew).")]
[CliCommand("update", "Update the Cratis CLI to the latest version")]
[CliExample("update")]
[CliExample("update", "--version", "1.2.3")]
@@ -16,39 +16,56 @@ namespace Cratis.Cli.Commands.Version;
[LlmOption("--version", "string", "Specific version to install (default: latest)")]
public class SelfUpdateCommand : AsyncCommand
{
- const string PackageId = "Cratis.Cli";
-
///
protected override async Task ExecuteAsync(CommandContext context, SelfUpdateSettings settings, CancellationToken cancellationToken)
{
var format = ResolveFormat(settings.Output);
var currentVersion = VersionCommand.GetCliVersion();
-
- var arguments = $"tool update -g {PackageId}";
- if (!string.IsNullOrWhiteSpace(settings.TargetVersion))
- {
- arguments += $" --version {settings.TargetVersion}";
- }
+ var strategy = CliUpdate.DetectStrategy();
if (string.Equals(format, OutputFormats.Table, StringComparison.Ordinal))
{
AnsiConsole.MarkupLine($"[bold]Updating Cratis CLI...[/] (current: {currentVersion.EscapeMarkup()})");
}
- var startInfo = new ProcessStartInfo
+ var startInfo = CliUpdate.CreateUpdateProcessStartInfo(strategy, settings.TargetVersion);
+ if (startInfo is null)
{
- FileName = "dotnet",
- Arguments = arguments,
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- UseShellExecute = false,
- CreateNoWindow = true
- };
+ if (!string.IsNullOrWhiteSpace(settings.TargetVersion) && strategy == CliUpdateStrategy.Homebrew)
+ {
+ OutputFormatter.WriteError(format, "Target version is not supported for Homebrew updates", "Run 'cratis update' to upgrade to the latest Homebrew version", ExitCodes.ValidationErrorCode);
+ return ExitCodes.ValidationError;
+ }
+
+ var instructions = CliUpdate.GetManualUpdateInstructions(strategy) ?? "Manual update required.";
+ if (string.Equals(format, OutputFormats.Json, StringComparison.Ordinal) || string.Equals(format, OutputFormats.JsonCompact, StringComparison.Ordinal))
+ {
+ OutputFormatter.WriteObject(format, new
+ {
+ PreviousVersion = currentVersion,
+ CurrentVersion = currentVersion,
+ Updated = false,
+ Strategy = strategy.ToString(),
+ Message = instructions
+ });
+ }
+ else
+ {
+ AnsiConsole.MarkupLine($"[yellow]{instructions.EscapeMarkup()}[/]");
+ }
+ return ExitCodes.Success;
+ }
using var process = Process.Start(startInfo);
if (process is null)
{
- OutputFormatter.WriteError(format, "Failed to start dotnet process", "Ensure the .NET SDK is installed and 'dotnet' is on your PATH", ExitCodes.ServerErrorCode);
+ var hint = strategy switch
+ {
+ CliUpdateStrategy.DotNetTool => "Ensure the .NET SDK is installed and 'dotnet' is on your PATH",
+ CliUpdateStrategy.Homebrew => "Ensure Homebrew is installed and 'brew' is on your PATH",
+ _ => null
+ };
+ OutputFormatter.WriteError(format, "Failed to start update process", hint, ExitCodes.ServerErrorCode);
return ExitCodes.ServerError;
}
diff --git a/Source/Cli/Commands/Version/VersionCommand.cs b/Source/Cli/Commands/Version/VersionCommand.cs
index 33cf5e5..a28159a 100644
--- a/Source/Cli/Commands/Version/VersionCommand.cs
+++ b/Source/Cli/Commands/Version/VersionCommand.cs
@@ -113,7 +113,7 @@ protected override async Task ExecuteAsync(CommandContext context, Chronicl
if (latestCli is not null)
{
- AnsiConsole.MarkupLine($"[yellow]Update available:[/] {latestCli.EscapeMarkup()} (run 'cratis update' to upgrade)");
+ AnsiConsole.MarkupLine($"[yellow]Update available:[/] {cliVersion.EscapeMarkup()} -> {latestCli.EscapeMarkup()} (run 'cratis update' to upgrade)");
}
if (serverInfo is not null)
diff --git a/Source/Cli/Program.cs b/Source/Cli/Program.cs
index dc439a5..b2f8a1d 100644
--- a/Source/Cli/Program.cs
+++ b/Source/Cli/Program.cs
@@ -2,6 +2,11 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using Cratis.Cli;
+using Cratis.Cli.Commands.Version;
+
+using var updateCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
+var currentVersion = VersionCommand.GetCliVersion();
+var updateCheckTask = UpdateChecker.CheckForUpdate(currentVersion, updateCts.Token);
if (args.Length == 0 && !Console.IsOutputRedirected && !GlobalSettings.IsAiAgentEnvironment())
{
@@ -19,5 +24,32 @@
AnsiConsole.WriteLine();
}
-return await CliApp.Create().RunAsync(args);
+var exitCode = await CliApp.Create().RunAsync(args);
+
+if (!ShouldSkipUpdateHint(args) &&
+ !Console.IsOutputRedirected &&
+ !GlobalSettings.IsAiAgentEnvironment() &&
+ updateCheckTask.IsCompleted)
+{
+ try
+ {
+ var latestVersion = await updateCheckTask;
+ if (latestVersion is not null)
+ {
+ var strategy = CliUpdate.DetectStrategy();
+ var hint = CliUpdate.GetUpdateHint(strategy, currentVersion, latestVersion);
+ AnsiConsole.WriteLine();
+ AnsiConsole.MarkupLine($" [{OutputFormatter.Warning.ToMarkup()}]\u2191 {hint.EscapeMarkup()}[/]");
+ }
+ }
+ catch
+ {
+ // Update check failures are non-critical.
+ }
+}
+
+return exitCode;
+static bool ShouldSkipUpdateHint(string[] args) =>
+ args.Length > 0 && (string.Equals(args[0], "update", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(args[0], "version", StringComparison.OrdinalIgnoreCase));
From db70895e41de11ee1b74d7702f61ee5e1454ec4d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 16 May 2026 07:29:01 +0000
Subject: [PATCH 3/3] Run brew update before brew upgrade in self-update flow
Agent-Logs-Url: https://github.com/Cratis/cli/sessions/e47b4e89-a0ff-45e8-bc1f-16ac8fd16cc2
Co-authored-by: einari <134365+einari@users.noreply.github.com>
---
.../for_CliUpdate/when_detecting_strategy.cs | 16 +++++
Source/Cli/CliUpdate.cs | 24 +++++++
.../Cli/Commands/Version/SelfUpdateCommand.cs | 63 ++++++++++++-------
3 files changed, 82 insertions(+), 21 deletions(-)
diff --git a/Source/Cli.Specs/for_CliUpdate/when_detecting_strategy.cs b/Source/Cli.Specs/for_CliUpdate/when_detecting_strategy.cs
index 68e7b71..a25967b 100644
--- a/Source/Cli.Specs/for_CliUpdate/when_detecting_strategy.cs
+++ b/Source/Cli.Specs/for_CliUpdate/when_detecting_strategy.cs
@@ -51,4 +51,20 @@ void should_use_cratis_update_hint_for_auto_update_strategies()
hint.ShouldContain("run 'cratis update'");
hint.ShouldContain("1.0.0 -> 1.1.0");
}
+
+ [Fact]
+ void should_prepare_brew_update_before_upgrade_for_homebrew()
+ {
+ var startInfo = CliUpdate.CreatePreUpdateProcessStartInfo(CliUpdateStrategy.Homebrew);
+ startInfo.ShouldNotBeNull();
+ startInfo!.FileName.ShouldEqual("brew");
+ startInfo.Arguments.ShouldEqual("update");
+ }
+
+ [Fact]
+ void should_not_prepare_brew_update_when_target_version_is_set()
+ {
+ var startInfo = CliUpdate.CreatePreUpdateProcessStartInfo(CliUpdateStrategy.Homebrew, "1.2.3");
+ startInfo.ShouldBeNull();
+ }
}
diff --git a/Source/Cli/CliUpdate.cs b/Source/Cli/CliUpdate.cs
index c36b8aa..faf37c4 100644
--- a/Source/Cli/CliUpdate.cs
+++ b/Source/Cli/CliUpdate.cs
@@ -60,6 +60,30 @@ public static CliUpdateStrategy DetectStrategy()
OperatingSystem.IsLinux());
}
+ ///
+ /// Creates process launch settings that should run before the main update command.
+ ///
+ /// The detected update strategy.
+ /// Optional target version.
+ /// A process start info for supported auto-update paths, otherwise null.
+ public static ProcessStartInfo? CreatePreUpdateProcessStartInfo(CliUpdateStrategy strategy, string? targetVersion = null)
+ {
+ if (strategy != CliUpdateStrategy.Homebrew || !string.IsNullOrWhiteSpace(targetVersion))
+ {
+ return null;
+ }
+
+ return new ProcessStartInfo
+ {
+ FileName = "brew",
+ Arguments = "update",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+ }
+
///
/// Creates the process launch settings for automatic update, when available.
///
diff --git a/Source/Cli/Commands/Version/SelfUpdateCommand.cs b/Source/Cli/Commands/Version/SelfUpdateCommand.cs
index 33c0ce4..c6543df 100644
--- a/Source/Cli/Commands/Version/SelfUpdateCommand.cs
+++ b/Source/Cli/Commands/Version/SelfUpdateCommand.cs
@@ -28,6 +28,16 @@ protected override async Task ExecuteAsync(CommandContext context, SelfUpda
AnsiConsole.MarkupLine($"[bold]Updating Cratis CLI...[/] (current: {currentVersion.EscapeMarkup()})");
}
+ var preUpdateStartInfo = CliUpdate.CreatePreUpdateProcessStartInfo(strategy, settings.TargetVersion);
+ if (preUpdateStartInfo is not null)
+ {
+ var preUpdateResult = await RunProcess(preUpdateStartInfo);
+ if (preUpdateResult is not null)
+ {
+ return preUpdateResult.Value;
+ }
+ }
+
var startInfo = CliUpdate.CreateUpdateProcessStartInfo(strategy, settings.TargetVersion);
if (startInfo is null)
{
@@ -56,28 +66,10 @@ protected override async Task ExecuteAsync(CommandContext context, SelfUpda
return ExitCodes.Success;
}
- using var process = Process.Start(startInfo);
- if (process is null)
+ var updateResult = await RunProcess(startInfo);
+ if (updateResult is not null)
{
- var hint = strategy switch
- {
- CliUpdateStrategy.DotNetTool => "Ensure the .NET SDK is installed and 'dotnet' is on your PATH",
- CliUpdateStrategy.Homebrew => "Ensure Homebrew is installed and 'brew' is on your PATH",
- _ => null
- };
- OutputFormatter.WriteError(format, "Failed to start update process", hint, ExitCodes.ServerErrorCode);
- return ExitCodes.ServerError;
- }
-
- var stdout = await process.StandardOutput.ReadToEndAsync();
- var stderr = await process.StandardError.ReadToEndAsync();
- await process.WaitForExitAsync();
-
- if (process.ExitCode != 0)
- {
- var errorMessage = !string.IsNullOrWhiteSpace(stderr) ? stderr.Trim() : stdout.Trim();
- OutputFormatter.WriteError(format, $"Update failed: {errorMessage}", errorCode: ExitCodes.ServerErrorCode);
- return ExitCodes.ServerError;
+ return updateResult.Value;
}
var newVersion = VersionCommand.GetCliVersion();
@@ -101,6 +93,35 @@ protected override async Task ExecuteAsync(CommandContext context, SelfUpda
}
return ExitCodes.Success;
+
+ async Task RunProcess(ProcessStartInfo processStartInfo)
+ {
+ using var process = Process.Start(processStartInfo);
+ if (process is null)
+ {
+ var hint = strategy switch
+ {
+ CliUpdateStrategy.DotNetTool => "Ensure the .NET SDK is installed and 'dotnet' is on your PATH",
+ CliUpdateStrategy.Homebrew => "Ensure Homebrew is installed and 'brew' is on your PATH",
+ _ => null
+ };
+ OutputFormatter.WriteError(format, "Failed to start update process", hint, ExitCodes.ServerErrorCode);
+ return ExitCodes.ServerError;
+ }
+
+ var stdout = await process.StandardOutput.ReadToEndAsync();
+ var stderr = await process.StandardError.ReadToEndAsync();
+ await process.WaitForExitAsync();
+
+ if (process.ExitCode != 0)
+ {
+ var errorMessage = !string.IsNullOrWhiteSpace(stderr) ? stderr.Trim() : stdout.Trim();
+ OutputFormatter.WriteError(format, $"Update failed: {errorMessage}", errorCode: ExitCodes.ServerErrorCode);
+ return ExitCodes.ServerError;
+ }
+
+ return null;
+ }
}
static string ResolveFormat(string output)