305 lines
9.3 KiB
C#
305 lines
9.3 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Builds the stella doctor command for diagnostic checks.
|
|
/// </summary>
|
|
internal static class DoctorCommandGroup
|
|
{
|
|
internal static Command BuildDoctorCommand(
|
|
IServiceProvider services,
|
|
Option<bool> 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<bool> verboseOption,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var formatOption = new Option<string>("--format", new[] { "-f" })
|
|
{
|
|
Description = "Output format: text (default), json, markdown"
|
|
};
|
|
formatOption.SetDefaultValue("text");
|
|
|
|
var modeOption = new Option<string?>("--mode", new[] { "-m" })
|
|
{
|
|
Description = "Run mode: quick (fast checks only), normal (default), full (all checks including slow ones)"
|
|
};
|
|
|
|
var categoryOption = new Option<string?>("--category", new[] { "-c" })
|
|
{
|
|
Description = "Filter checks by category (e.g., Core, Database, Security)"
|
|
};
|
|
|
|
var tagOption = new Option<string[]>("--tag", new[] { "-t" })
|
|
{
|
|
Description = "Filter checks by tag (e.g., quick, connectivity). Can be specified multiple times.",
|
|
Arity = ArgumentArity.ZeroOrMore
|
|
};
|
|
|
|
var checkOption = new Option<string?>("--check")
|
|
{
|
|
Description = "Run a specific check by ID (e.g., check.core.disk)"
|
|
};
|
|
|
|
var parallelOption = new Option<int?>("--parallel", new[] { "-p" })
|
|
{
|
|
Description = "Maximum parallel check executions (default: 4)"
|
|
};
|
|
|
|
var timeoutOption = new Option<int?>("--timeout")
|
|
{
|
|
Description = "Per-check timeout in seconds (default: 30)"
|
|
};
|
|
|
|
var outputOption = new Option<string?>("--output", new[] { "-o" })
|
|
{
|
|
Description = "Write output to file instead of stdout"
|
|
};
|
|
|
|
var failOnWarnOption = new Option<bool>("--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<bool> verboseOption,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var categoryOption = new Option<string?>("--category", new[] { "-c" })
|
|
{
|
|
Description = "Filter by category"
|
|
};
|
|
|
|
var tagOption = new Option<string[]>("--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<DoctorEngine>();
|
|
|
|
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<DoctorCheckProgress>? progress = null;
|
|
if (verbose)
|
|
{
|
|
progress = new Progress<DoctorCheckProgress>(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<DoctorEngine>();
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|