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;
}
}
}