256 lines
8.1 KiB
C#
256 lines
8.1 KiB
C#
using System.Text.Json;
|
|
using Spectre.Console;
|
|
|
|
namespace StellaOps.Cli.Output;
|
|
|
|
/// <summary>
|
|
/// Helper for rendering CLI errors consistently.
|
|
/// CLI-SDK-62-002: Provides standardized error output with error.code and trace_id.
|
|
/// </summary>
|
|
internal static class CliErrorRenderer
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
WriteIndented = true,
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
};
|
|
|
|
/// <summary>
|
|
/// Renders an error to the console.
|
|
/// </summary>
|
|
public static void Render(CliError error, bool verbose = false, bool asJson = false)
|
|
{
|
|
if (asJson)
|
|
{
|
|
RenderJson(error);
|
|
}
|
|
else
|
|
{
|
|
RenderConsole(error, verbose);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders an error as JSON.
|
|
/// </summary>
|
|
public static void RenderJson(CliError error)
|
|
{
|
|
var output = new
|
|
{
|
|
error = new
|
|
{
|
|
code = error.Code,
|
|
message = error.Message,
|
|
detail = error.Detail,
|
|
target = error.Target,
|
|
help_url = error.HelpUrl,
|
|
retry_after = error.RetryAfter,
|
|
metadata = error.Metadata
|
|
},
|
|
trace_id = error.TraceId,
|
|
request_id = error.RequestId,
|
|
exit_code = error.ExitCode
|
|
};
|
|
|
|
var json = JsonSerializer.Serialize(output, JsonOptions);
|
|
AnsiConsole.WriteLine(json);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders an error to the console with formatting.
|
|
/// </summary>
|
|
public static void RenderConsole(CliError error, bool verbose = false)
|
|
{
|
|
// Main error message
|
|
AnsiConsole.MarkupLine($"[red]Error:[/] {Markup.Escape(error.Message)}");
|
|
|
|
// Error code
|
|
AnsiConsole.MarkupLine($"[grey]Code:[/] {Markup.Escape(error.Code)}");
|
|
|
|
if (TryGetReasonCode(error, out var reasonCode))
|
|
{
|
|
AnsiConsole.MarkupLine($"[grey]Reason:[/] {Markup.Escape(reasonCode)}");
|
|
}
|
|
|
|
// Detail (if present)
|
|
if (!string.IsNullOrWhiteSpace(error.Detail))
|
|
{
|
|
AnsiConsole.MarkupLine($"[grey]Detail:[/] {Markup.Escape(error.Detail)}");
|
|
}
|
|
|
|
// Target (if present)
|
|
if (!string.IsNullOrWhiteSpace(error.Target))
|
|
{
|
|
AnsiConsole.MarkupLine($"[grey]Target:[/] {Markup.Escape(error.Target)}");
|
|
}
|
|
|
|
// Help URL (if present)
|
|
if (!string.IsNullOrWhiteSpace(error.HelpUrl))
|
|
{
|
|
AnsiConsole.MarkupLine($"[grey]Help:[/] [link]{Markup.Escape(error.HelpUrl)}[/]");
|
|
}
|
|
|
|
// Retry-after (if present)
|
|
if (error.RetryAfter.HasValue)
|
|
{
|
|
AnsiConsole.MarkupLine($"[yellow]Retry after:[/] {error.RetryAfter} seconds");
|
|
}
|
|
|
|
// Trace/Request IDs (shown in verbose mode or always for debugging)
|
|
if (verbose || !string.IsNullOrWhiteSpace(error.TraceId))
|
|
{
|
|
AnsiConsole.WriteLine();
|
|
if (!string.IsNullOrWhiteSpace(error.TraceId))
|
|
{
|
|
AnsiConsole.MarkupLine($"[grey]Trace ID:[/] {Markup.Escape(error.TraceId)}");
|
|
}
|
|
if (!string.IsNullOrWhiteSpace(error.RequestId))
|
|
{
|
|
AnsiConsole.MarkupLine($"[grey]Request ID:[/] {Markup.Escape(error.RequestId)}");
|
|
}
|
|
}
|
|
|
|
// Metadata (shown in verbose mode)
|
|
if (verbose && error.Metadata is not null && error.Metadata.Count > 0)
|
|
{
|
|
AnsiConsole.WriteLine();
|
|
AnsiConsole.MarkupLine("[grey]Metadata:[/]");
|
|
foreach (var (key, value) in error.Metadata)
|
|
{
|
|
AnsiConsole.MarkupLine($" [grey]{Markup.Escape(key)}:[/] {Markup.Escape(value)}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders a simple error message.
|
|
/// </summary>
|
|
public static void RenderSimple(string message, string? code = null)
|
|
{
|
|
AnsiConsole.MarkupLine($"[red]Error:[/] {Markup.Escape(message)}");
|
|
if (!string.IsNullOrWhiteSpace(code))
|
|
{
|
|
AnsiConsole.MarkupLine($"[grey]Code:[/] {Markup.Escape(code)}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders a warning message.
|
|
/// </summary>
|
|
public static void RenderWarning(string message)
|
|
{
|
|
AnsiConsole.MarkupLine($"[yellow]Warning:[/] {Markup.Escape(message)}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders scope guidance for invalid_scope errors.
|
|
/// </summary>
|
|
public static void RenderScopeGuidance(CliError error)
|
|
{
|
|
if (!error.Code.Contains("SCOPE", StringComparison.OrdinalIgnoreCase))
|
|
return;
|
|
|
|
AnsiConsole.WriteLine();
|
|
AnsiConsole.MarkupLine("[yellow]The requested operation requires additional OAuth scopes.[/]");
|
|
AnsiConsole.WriteLine();
|
|
AnsiConsole.MarkupLine("To resolve this issue:");
|
|
AnsiConsole.MarkupLine(" 1. Check your CLI profile configuration: [cyan]stella profile show[/]");
|
|
AnsiConsole.MarkupLine(" 2. Update your profile with required scopes: [cyan]stella profile edit <name>[/]");
|
|
AnsiConsole.MarkupLine(" 3. Request additional scopes from your administrator");
|
|
AnsiConsole.MarkupLine(" 4. Re-authenticate with the updated scopes: [cyan]stella auth login[/]");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders rate limit guidance.
|
|
/// </summary>
|
|
public static void RenderRateLimitGuidance(CliError error)
|
|
{
|
|
if (!error.Code.Contains("RATE_LIMIT", StringComparison.OrdinalIgnoreCase))
|
|
return;
|
|
|
|
AnsiConsole.WriteLine();
|
|
AnsiConsole.MarkupLine("[yellow]Rate limit exceeded.[/]");
|
|
|
|
if (error.RetryAfter.HasValue)
|
|
{
|
|
AnsiConsole.MarkupLine($"Please wait [cyan]{error.RetryAfter}[/] seconds before retrying.");
|
|
}
|
|
else
|
|
{
|
|
AnsiConsole.MarkupLine("Please wait before retrying your request.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders authentication guidance.
|
|
/// </summary>
|
|
public static void RenderAuthGuidance(CliError error)
|
|
{
|
|
if (!error.Code.StartsWith("ERR_AUTH_", StringComparison.OrdinalIgnoreCase))
|
|
return;
|
|
|
|
AnsiConsole.WriteLine();
|
|
|
|
if (error.Code == CliErrorCodes.Unauthorized)
|
|
{
|
|
AnsiConsole.MarkupLine("[yellow]Authentication required.[/]");
|
|
AnsiConsole.MarkupLine("Please authenticate using: [cyan]stella auth login[/]");
|
|
}
|
|
else if (error.Code == CliErrorCodes.Forbidden)
|
|
{
|
|
AnsiConsole.MarkupLine("[yellow]Access denied.[/]");
|
|
AnsiConsole.MarkupLine("You do not have permission to perform this operation.");
|
|
AnsiConsole.MarkupLine("Contact your administrator to request access.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders contextual guidance based on error code.
|
|
/// </summary>
|
|
public static void RenderGuidance(CliError error)
|
|
{
|
|
RenderScopeGuidance(error);
|
|
RenderRateLimitGuidance(error);
|
|
RenderAuthGuidance(error);
|
|
RenderOfflineKitGuidance(error);
|
|
}
|
|
|
|
private static bool TryGetReasonCode(CliError error, out string reasonCode)
|
|
{
|
|
reasonCode = "";
|
|
if (error.Metadata is null || error.Metadata.Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string? code1 = null;
|
|
string? code2 = null;
|
|
|
|
if ((!error.Metadata.TryGetValue("reason_code", out code1) || string.IsNullOrWhiteSpace(code1)) &&
|
|
(!error.Metadata.TryGetValue("reasonCode", out code2) || string.IsNullOrWhiteSpace(code2)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
reasonCode = OfflineKitReasonCodes.Normalize(code1 ?? code2 ?? "") ?? "";
|
|
return reasonCode.Length > 0;
|
|
}
|
|
|
|
private static void RenderOfflineKitGuidance(CliError error)
|
|
{
|
|
if (!TryGetReasonCode(error, out var reasonCode))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var remediation = OfflineKitReasonCodes.GetRemediation(reasonCode);
|
|
if (string.IsNullOrWhiteSpace(remediation))
|
|
{
|
|
return;
|
|
}
|
|
|
|
AnsiConsole.WriteLine();
|
|
AnsiConsole.MarkupLine($"[yellow]Remediation:[/] {Markup.Escape(remediation)}");
|
|
}
|
|
}
|