using System; using System.CommandLine; using System.Globalization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using StellaOps.Cli.Extensions; using StellaOps.Doctor.Engine; using StellaOps.Doctor.Models; using StellaOps.Doctor.Output; namespace StellaOps.Cli.Commands; /// /// Builds the stella doctor command for diagnostic checks. /// internal static class DoctorCommandGroup { internal static Command BuildDoctorCommand( IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var doctor = new Command("doctor", "Run diagnostic checks on Stella Ops installation and environment."); // Sub-commands doctor.Add(BuildRunCommand(services, verboseOption, cancellationToken)); doctor.Add(BuildListCommand(services, verboseOption, cancellationToken)); return doctor; } private static Command BuildRunCommand( IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var formatOption = new Option("--format", new[] { "-f" }) { Description = "Output format: text (default), json, markdown" }; formatOption.SetDefaultValue("text"); var modeOption = new Option("--mode", new[] { "-m" }) { Description = "Run mode: quick (fast checks only), normal (default), full (all checks including slow ones)" }; var categoryOption = new Option("--category", new[] { "-c" }) { Description = "Filter checks by category (e.g., Core, Database, Security)" }; var tagOption = new Option("--tag", new[] { "-t" }) { Description = "Filter checks by tag (e.g., quick, connectivity). Can be specified multiple times.", Arity = ArgumentArity.ZeroOrMore }; var checkOption = new Option("--check") { Description = "Run a specific check by ID (e.g., check.core.disk)" }; var parallelOption = new Option("--parallel", new[] { "-p" }) { Description = "Maximum parallel check executions (default: 4)" }; var timeoutOption = new Option("--timeout") { Description = "Per-check timeout in seconds (default: 30)" }; var outputOption = new Option("--output", new[] { "-o" }) { Description = "Write output to file instead of stdout" }; var failOnWarnOption = new Option("--fail-on-warn") { Description = "Exit with non-zero code on warnings (default: only fail on errors)" }; var run = new Command("run", "Execute diagnostic checks.") { formatOption, modeOption, categoryOption, tagOption, checkOption, parallelOption, timeoutOption, outputOption, failOnWarnOption, verboseOption }; run.SetAction(async (parseResult, ct) => { var format = parseResult.GetValue(formatOption) ?? "text"; var mode = parseResult.GetValue(modeOption); var category = parseResult.GetValue(categoryOption); var tags = parseResult.GetValue(tagOption) ?? []; var checkId = parseResult.GetValue(checkOption); var parallel = parseResult.GetValue(parallelOption) ?? 4; var timeout = parseResult.GetValue(timeoutOption) ?? 30; var output = parseResult.GetValue(outputOption); var failOnWarn = parseResult.GetValue(failOnWarnOption); var verbose = parseResult.GetValue(verboseOption); await RunDoctorAsync( services, format, mode, category, tags, checkId, parallel, timeout, output, failOnWarn, verbose, cancellationToken); }); return run; } private static Command BuildListCommand( IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var categoryOption = new Option("--category", new[] { "-c" }) { Description = "Filter by category" }; var tagOption = new Option("--tag", new[] { "-t" }) { Description = "Filter by tag", Arity = ArgumentArity.ZeroOrMore }; var list = new Command("list", "List available diagnostic checks.") { categoryOption, tagOption, verboseOption }; list.SetAction((parseResult, ct) => { var category = parseResult.GetValue(categoryOption); var tags = parseResult.GetValue(tagOption) ?? []; var verbose = parseResult.GetValue(verboseOption); return ListChecksAsync(services, category, tags, verbose, cancellationToken); }); return list; } private static async Task RunDoctorAsync( IServiceProvider services, string format, string? mode, string? category, string[] tags, string? checkId, int parallel, int timeout, string? outputPath, bool failOnWarn, bool verbose, CancellationToken ct) { var engine = services.GetRequiredService(); var runMode = ParseRunMode(mode); var options = new DoctorRunOptions { Mode = runMode, Categories = category != null ? [category] : null, Tags = tags.Length > 0 ? tags : null, CheckIds = checkId != null ? [checkId] : null, Parallelism = parallel, Timeout = TimeSpan.FromSeconds(timeout) }; // Progress reporting for verbose mode IProgress? progress = null; if (verbose) { progress = new Progress(p => { Console.WriteLine($"[{p.Completed}/{p.Total}] {p.CheckId} - {p.Severity}"); }); } var report = await engine.RunAsync(options, progress, ct); // Format output var formatter = GetFormatter(format); var output = formatter.Format(report); // Write output if (!string.IsNullOrEmpty(outputPath)) { await File.WriteAllTextAsync(outputPath, output, ct); Console.WriteLine($"Report written to: {outputPath}"); } else { Console.WriteLine(output); } // Set exit code SetExitCode(report, failOnWarn); } private static async Task ListChecksAsync( IServiceProvider services, string? category, string[] tags, bool verbose, CancellationToken ct) { var engine = services.GetRequiredService(); var options = new DoctorRunOptions { Categories = category != null ? [category] : null, Tags = tags.Length > 0 ? tags : null }; var checks = engine.ListChecks(options); Console.WriteLine($"Available diagnostic checks ({checks.Count}):"); Console.WriteLine(); string? currentCategory = null; foreach (var check in checks.OrderBy(c => c.Category).ThenBy(c => c.CheckId)) { if (check.Category != currentCategory) { currentCategory = check.Category; Console.WriteLine($"## {currentCategory ?? "Uncategorized"}"); Console.WriteLine(); } Console.WriteLine($" {check.CheckId}"); Console.WriteLine($" Name: {check.Name}"); if (verbose) { Console.WriteLine($" Description: {check.Description}"); Console.WriteLine($" Plugin: {check.PluginId}"); Console.WriteLine($" Tags: {string.Join(", ", check.Tags)}"); Console.WriteLine($" Estimated: {check.EstimatedDuration.TotalSeconds:F1}s"); } Console.WriteLine(); } await Task.CompletedTask; } private static DoctorRunMode ParseRunMode(string? mode) { return mode?.ToLowerInvariant() switch { "quick" => DoctorRunMode.Quick, "full" => DoctorRunMode.Full, _ => DoctorRunMode.Normal }; } private static IDoctorReportFormatter GetFormatter(string format) { return format.ToLowerInvariant() switch { "json" => new JsonReportFormatter(), "markdown" or "md" => new MarkdownReportFormatter(), _ => new TextReportFormatter() }; } private static void SetExitCode(DoctorReport report, bool failOnWarn) { var exitCode = report.OverallSeverity switch { DoctorSeverity.Fail => CliExitCodes.DoctorFailed, DoctorSeverity.Warn when failOnWarn => CliExitCodes.DoctorWarning, _ => 0 }; if (exitCode != 0) { Environment.ExitCode = exitCode; } } }