using System; using System.CommandLine; using System.Linq; using System.Threading; using Microsoft.Extensions.DependencyInjection; using StellaOps.Cli.Services; using StellaOps.Cli.Extensions; using StellaOps.Infrastructure.Postgres.Migrations; namespace StellaOps.Cli.Commands; internal static class SystemCommandBuilder { private static MigrationCategory? ParseCategory(string? value) { if (string.IsNullOrWhiteSpace(value)) { return null; } return value.ToLowerInvariant() switch { "startup" => MigrationCategory.Startup, "release" => MigrationCategory.Release, "seed" => MigrationCategory.Seed, "data" => MigrationCategory.Data, _ => throw new CommandLineException("Unknown category. Expected: startup|release|seed|data") }; } internal static Command BuildSystemCommand( IServiceProvider services, Option verboseOption, CancellationToken cancellationToken) { var moduleOption = new Option("--module") { Description = "Module name (Authority, Scheduler, Concelier, Policy, Notify, Excititor, all)" }; var categoryOption = new Option("--category") { Description = "Migration category (startup, release, seed, data)" }; var dryRunOption = new Option("--dry-run") { Description = "List migrations without executing" }; var connectionOption = new Option("--connection") { Description = "PostgreSQL connection string override (otherwise uses STELLAOPS_POSTGRES_* env vars)" }; var timeoutOption = new Option("--timeout") { Description = "Command timeout in seconds for each migration (default 300)." }; var forceOption = new Option("--force") { Description = "Allow execution of release migrations without --dry-run." }; var run = new Command("migrations-run", "Run migrations for the selected module(s)."); run.Add(moduleOption); run.Add(categoryOption); run.Add(dryRunOption); run.Add(connectionOption); run.Add(timeoutOption); run.Add(forceOption); run.SetAction(async parseResult => { var modules = MigrationModuleRegistry.GetModules(parseResult.GetValue(moduleOption)).ToList(); if (!modules.Any()) { throw new CommandLineException( "No modules matched the filter; available: " + string.Join(", ", MigrationModuleRegistry.ModuleNames)); } var category = ParseCategory(parseResult.GetValue(categoryOption)); var dryRun = parseResult.GetValue(dryRunOption); var force = parseResult.GetValue(forceOption); if (category == MigrationCategory.Release && !dryRun && !force) { throw new CommandLineException( "Release migrations require explicit approval; use --dry-run to preview or --force to execute."); } var connection = parseResult.GetValue(connectionOption); var timeoutSeconds = parseResult.GetValue(timeoutOption); var verbose = parseResult.GetValue(verboseOption); var migrationService = services.GetRequiredService(); foreach (var module in modules) { var result = await migrationService .RunAsync(module, connection, category, dryRun, timeoutSeconds, cancellationToken) .ConfigureAwait(false); WriteRunResult(module, result, verbose); } }); var status = new Command("migrations-status", "Show migration status for the selected module(s)."); status.Add(moduleOption); status.Add(connectionOption); status.SetAction(async parseResult => { var modules = MigrationModuleRegistry.GetModules(parseResult.GetValue(moduleOption)).ToList(); if (!modules.Any()) { throw new CommandLineException( "No modules matched the filter; available: " + string.Join(", ", MigrationModuleRegistry.ModuleNames)); } var connection = parseResult.GetValue(connectionOption); var verbose = parseResult.GetValue(verboseOption); var migrationService = services.GetRequiredService(); foreach (var module in modules) { var statusResult = await migrationService .GetStatusAsync(module, connection, cancellationToken) .ConfigureAwait(false); WriteStatusResult(module, statusResult, verbose); } }); var verify = new Command("migrations-verify", "Verify migration checksums for the selected module(s)."); verify.Add(moduleOption); verify.Add(connectionOption); verify.SetAction(async parseResult => { var modules = MigrationModuleRegistry.GetModules(parseResult.GetValue(moduleOption)).ToList(); if (!modules.Any()) { throw new CommandLineException( "No modules matched the filter; available: " + string.Join(", ", MigrationModuleRegistry.ModuleNames)); } var connection = parseResult.GetValue(connectionOption); var migrationService = services.GetRequiredService(); foreach (var module in modules) { var errors = await migrationService .VerifyAsync(module, connection, cancellationToken) .ConfigureAwait(false); WriteVerifyResult(module, errors); } }); var system = new Command("system", "System operations (migrations)."); system.Add(run); system.Add(status); system.Add(verify); return system; } private static void WriteRunResult(MigrationModuleInfo module, MigrationResult result, bool verbose) { var prefix = $"[{module.Name}]"; if (!result.Success) { Console.Error.WriteLine($"{prefix} FAILED: {result.ErrorMessage}"); foreach (var error in result.ChecksumErrors) { Console.Error.WriteLine($"{prefix} checksum: {error}"); } if (Environment.ExitCode == 0) { Environment.ExitCode = 1; } return; } Console.WriteLine( $"{prefix} applied={result.AppliedCount} skipped={result.SkippedCount} filtered={result.FilteredCount} duration_ms={result.DurationMs}"); if (verbose && result.AppliedMigrations.Count > 0) { foreach (var migration in result.AppliedMigrations.OrderBy(m => m.Name)) { var mode = migration.WasDryRun ? "DRY-RUN" : "APPLIED"; Console.WriteLine($"{prefix} {mode}: {migration.Name} ({migration.Category}) {migration.DurationMs}ms"); } } } private static void WriteStatusResult(MigrationModuleInfo module, MigrationStatus status, bool verbose) { var prefix = $"[{module.Name}]"; Console.WriteLine( $"{prefix} applied={status.AppliedCount} pending_startup={status.PendingStartupCount} pending_release={status.PendingReleaseCount} checksum_errors={status.ChecksumErrors.Count}"); if (verbose) { foreach (var pending in status.PendingMigrations.OrderBy(p => p.Name)) { Console.WriteLine($"{prefix} pending {pending.Category}: {pending.Name}"); } foreach (var error in status.ChecksumErrors) { Console.WriteLine($"{prefix} checksum: {error}"); } } if (status.HasBlockingIssues && Environment.ExitCode == 0) { Environment.ExitCode = 1; } } private static void WriteVerifyResult(MigrationModuleInfo module, IReadOnlyList errors) { var prefix = $"[{module.Name}]"; if (errors.Count == 0) { Console.WriteLine($"{prefix} checksum verification passed."); return; } Console.Error.WriteLine($"{prefix} checksum verification failed ({errors.Count})."); foreach (var error in errors) { Console.Error.WriteLine($"{prefix} {error}"); } if (Environment.ExitCode == 0) { Environment.ExitCode = 1; } } }