diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs index e591e061..3ad9bc56 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Microsoft.Agents.A365.DevTools.Cli.Constants; +using Microsoft.Agents.A365.DevTools.Cli.Exceptions; using Microsoft.Agents.A365.DevTools.Cli.Helpers; using Microsoft.Agents.A365.DevTools.Cli.Models; using Microsoft.Agents.A365.DevTools.Cli.Services; @@ -315,6 +316,10 @@ await ProjectSettingsSyncHelper.ExecuteAsync( // Display comprehensive setup summary DisplaySetupSummary(setupResults, logger); } + catch (Agent365Exception ex) + { + ExceptionHandler.HandleAgent365Exception(ex); + } catch (Exception ex) { logger.LogError(ex, "Setup failed: {Message}", ex.Message); diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/AzureExceptions.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/AzureExceptions.cs index d8445684..49abe58b 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/AzureExceptions.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/AzureExceptions.cs @@ -60,7 +60,7 @@ private static List BuildMitigation(string resourceType, bool isPermissi return new List { "Check your Azure subscription permissions", - $"Ensure you have Contributor or Owner role on the subscription", + $"Ensure you have Contributor or Owner role on the subscription or at least the Resource Group", "Contact your Azure administrator if needed", "Run 'az account show' to verify your account" }; diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/ExceptionHandler.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/ExceptionHandler.cs new file mode 100644 index 00000000..763d6fd9 --- /dev/null +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/ExceptionHandler.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Serilog; + +namespace Microsoft.Agents.A365.DevTools.Cli.Exceptions; + +/// +/// Centralized exception handling utility for Agent365 CLI. +/// Provides consistent error display and logging. +/// Follows Microsoft CLI best practices (Azure CLI, dotnet CLI patterns). +/// +public static class ExceptionHandler +{ + /// + /// Handles Agent365Exception with user-friendly output (no stack traces for user errors). + /// Displays formatted error messages to console and logs to Serilog for diagnostics. + /// + /// The Agent365Exception to handle + public static void HandleAgent365Exception(Agent365Exception ex) + { + // Display formatted error message + Console.Error.Write(ex.GetFormattedMessage()); + + // For system errors (not user errors), suggest reporting as bug + if (!ex.IsUserError) + { + Console.Error.WriteLine("If this error persists, please report it at:"); + Console.Error.WriteLine("https://github.com/microsoft/Agent365-devTools/issues"); + Console.Error.WriteLine(); + } + + // Log for diagnostics (but don't show stack trace to user) + Log.Error("Operation failed. ErrorCode={ErrorCode}, IssueDescription={IssueDescription}", + ex.ErrorCode, ex.IssueDescription); + } +} \ No newline at end of file diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Program.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Program.cs index f4615a4a..3ea22587 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Program.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Program.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Serilog; +using Serilog.Events; using System.CommandLine; using System.CommandLine.Builder; using System.CommandLine.Parsing; @@ -28,7 +29,9 @@ static async Task Main(string[] args) // Configure Serilog with both console and file output Log.Logger = new LoggerConfiguration() .MinimumLevel.Is(isVerbose ? Serilog.Events.LogEventLevel.Debug : Serilog.Events.LogEventLevel.Information) - .WriteTo.Console() // Console output (user-facing) + .WriteTo.Logger(lc => lc + .Filter.ByExcluding(e => e.Level >= LogEventLevel.Error) // ✅ Exclude Error/Fatal from console + .WriteTo.Console()) .WriteTo.File( // File output (for debugging) path: logFilePath, rollingInterval: RollingInterval.Infinite, @@ -108,7 +111,7 @@ static async Task Main(string[] args) { if (exception is Agent365Exception myEx) { - HandleAgent365Exception(myEx); + ExceptionHandler.HandleAgent365Exception(myEx); context.ExitCode = myEx.ExitCode; } else @@ -132,28 +135,6 @@ static async Task Main(string[] args) } } - - /// - /// Handles Agent365Exception with user-friendly output (no stack traces for user errors). - /// Follows Microsoft CLI best practices (Azure CLI, dotnet CLI patterns). - /// - private static void HandleAgent365Exception(Agent365Exception ex) - { - // Display formatted error message - Console.Error.WriteLine(ex.GetFormattedMessage()); - - // For system errors (not user errors), suggest reporting as bug - if (!ex.IsUserError) - { - Console.Error.WriteLine("If this error persists, please report it at:"); - Console.Error.WriteLine("https://github.com/microsoft/Agent365-devTools/issues"); - } - - // Log for diagnostics (but don't show stack trace to user) - Log.Error("Operation failed. ErrorCode={ErrorCode}, IssueDescription={IssueDescription}", - ex.ErrorCode, ex.IssueDescription); - } - private static void ConfigureServices(IServiceCollection services) { // Add logging diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs index 4e1c3770..09949983 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs @@ -11,6 +11,7 @@ using Azure.Identity; using Azure.Core; using Microsoft.Agents.A365.DevTools.Cli.Constants; +using Microsoft.Agents.A365.DevTools.Cli.Exceptions; namespace Microsoft.Agents.A365.DevTools.Cli.Services; @@ -275,16 +276,23 @@ public async Task RunAsync(string configPath, string generatedConfigPath, } // Web App - var webShow = await _executor.ExecuteAsync("az", $"webapp show -g {resourceGroup} -n {webAppName} --subscription {subscriptionId}", captureOutput: true, suppressErrorLogging: true); + var webShow = await _executor.ExecuteAsync("az", $"webapp show -g {resourceGroup} -n {webAppName} --subscription {subscriptionId}", captureOutput: true); if (!webShow.Success) { var runtime = GetRuntimeForPlatform(platform); _logger.LogInformation("Creating web app {App} with runtime {Runtime}", webAppName, runtime); - var createResult = await _executor.ExecuteAsync("az", $"webapp create -g {resourceGroup} -p {planName} -n {webAppName} --runtime \"{runtime}\" --subscription {subscriptionId}"); + var createResult = await _executor.ExecuteAsync("az", $"webapp create -g {resourceGroup} -p {planName} -n {webAppName} --runtime \"{runtime}\" --subscription {subscriptionId}", captureOutput: true, suppressErrorLogging: true); if (!createResult.Success) { - _logger.LogError("ERROR: Web app creation failed: {Err}", createResult.StandardError); - throw new InvalidOperationException($"Failed to create web app '{webAppName}'. Setup cannot continue."); + if (createResult.StandardError.Contains("AuthorizationFailed", StringComparison.OrdinalIgnoreCase)) + { + throw new AzureResourceException("WebApp", webAppName, createResult.StandardError, true); + } + else + { + _logger.LogError("ERROR: Web app creation failed: {Err}", createResult.StandardError); + throw new InvalidOperationException($"Failed to create web app '{webAppName}'. Setup cannot continue."); + } } } else @@ -1198,13 +1206,18 @@ private string ProtectSecret(string plaintext) private async Task AzWarnAsync(string args, string description) { - var result = await _executor.ExecuteAsync("az", args); + var result = await _executor.ExecuteAsync("az", args, suppressErrorLogging: true); if (!result.Success) { if (result.StandardError.Contains("already exists", StringComparison.OrdinalIgnoreCase)) { _logger.LogInformation("{Description} already exists (skipping creation)", description); } + else if (result.StandardError.Contains("AuthorizationFailed", StringComparison.OrdinalIgnoreCase)) + { + var exception = new AzureResourceException(description, string.Empty, result.StandardError, true); + ExceptionHandler.HandleAgent365Exception(exception); + } else { _logger.LogWarning("az {Description} returned non-success (exit code {Code}). Error: {Err}",