save dev progress

This commit is contained in:
StellaOps Bot
2025-12-26 00:32:35 +02:00
parent aa70af062e
commit ed3079543c
142 changed files with 23771 additions and 232 deletions

View File

@@ -1,7 +1,7 @@
// -----------------------------------------------------------------------------
// CommandHandlers.Federation.cs
// Sprint: SPRINT_8200_0014_0002 (Delta Bundle Export)
// Description: Command handlers for federation bundle operations.
// Sprint: SPRINT_8200_0014_0002 (Delta Bundle Export), SPRINT_8200_0014_0003 (Bundle Import)
// Description: Command handlers for federation bundle export and import operations.
// -----------------------------------------------------------------------------
using System.Net.Http.Headers;
@@ -253,4 +253,566 @@ internal static partial class CommandHandlers
public long EstimatedSizeBytes { get; set; }
public double EstimatedSizeMb { get; set; }
}
internal static async Task<int> HandleFederationBundleImportAsync(
IServiceProvider services,
string inputPath,
bool dryRun,
bool skipSignature,
string? onConflict,
bool force,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
if (verbose)
{
AnsiConsole.MarkupLine("[blue]Importing federation bundle...[/]");
AnsiConsole.MarkupLine($" File: [bold]{Markup.Escape(inputPath)}[/]");
AnsiConsole.MarkupLine($" Dry Run: {dryRun}");
AnsiConsole.MarkupLine($" Skip Signature: {skipSignature}");
AnsiConsole.MarkupLine($" On Conflict: {onConflict ?? "PreferRemote"}");
AnsiConsole.MarkupLine($" Force: {force}");
}
if (!File.Exists(inputPath))
{
AnsiConsole.MarkupLine($"[red]Error: File not found: {Markup.Escape(inputPath)}[/]");
return 1;
}
try
{
var httpClientFactory = services.GetService<IHttpClientFactory>();
if (httpClientFactory == null)
{
AnsiConsole.MarkupLine("[red]Error: HTTP client factory not available.[/]");
return 1;
}
var client = httpClientFactory.CreateClient("Concelier");
// Build query string
var queryParams = new List<string>();
if (dryRun)
queryParams.Add("dry_run=true");
if (skipSignature)
queryParams.Add("skip_signature=true");
if (!string.IsNullOrEmpty(onConflict))
queryParams.Add($"on_conflict={Uri.EscapeDataString(onConflict)}");
if (force)
queryParams.Add("force=true");
var url = "/api/v1/federation/import";
if (queryParams.Count > 0)
url += $"?{string.Join("&", queryParams)}";
await using var fileStream = File.OpenRead(inputPath);
using var content = new StreamContent(fileStream);
content.Headers.ContentType = new MediaTypeHeaderValue("application/zstd");
using var response = await client.PostAsync(url, content, cancellationToken);
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
if (!response.IsSuccessStatusCode)
{
if (json)
{
AnsiConsole.WriteLine(responseContent);
}
else
{
AnsiConsole.MarkupLine($"[red]Import failed: {response.StatusCode}[/]");
try
{
var errorResponse = JsonSerializer.Deserialize<ImportErrorResponse>(responseContent, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (errorResponse?.FailureReason != null)
{
AnsiConsole.MarkupLine($" Reason: [yellow]{Markup.Escape(errorResponse.FailureReason)}[/]");
}
}
catch
{
if (verbose)
AnsiConsole.MarkupLine($"[grey]{Markup.Escape(responseContent)}[/]");
}
}
return 1;
}
if (json)
{
AnsiConsole.WriteLine(responseContent);
}
else
{
var result = JsonSerializer.Deserialize<ImportSuccessResponse>(responseContent, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (result != null)
{
var status = dryRun ? "[yellow]DRY RUN[/]" : "[green]SUCCESS[/]";
AnsiConsole.MarkupLine($"{status} Bundle import completed.");
AnsiConsole.MarkupLine($" Bundle Hash: [dim]{result.BundleHash}[/]");
AnsiConsole.MarkupLine($" Cursor: [bold]{result.ImportedCursor}[/]");
if (result.Counts != null)
{
AnsiConsole.MarkupLine($" Created: [green]{result.Counts.CanonicalCreated:N0}[/]");
AnsiConsole.MarkupLine($" Updated: [blue]{result.Counts.CanonicalUpdated:N0}[/]");
AnsiConsole.MarkupLine($" Skipped: [dim]{result.Counts.CanonicalSkipped:N0}[/]");
AnsiConsole.MarkupLine($" Edges: [blue]{result.Counts.EdgesAdded:N0}[/]");
AnsiConsole.MarkupLine($" Deletions: [yellow]{result.Counts.DeletionsProcessed:N0}[/]");
}
if (result.Conflicts?.Count > 0)
{
AnsiConsole.MarkupLine($" Conflicts: [yellow]{result.Conflicts.Count}[/]");
}
AnsiConsole.MarkupLine($" Duration: {result.DurationMs:F0}ms");
}
}
return 0;
}
catch (HttpRequestException ex)
{
AnsiConsole.MarkupLine($"[red]Connection error: {Markup.Escape(ex.Message)}[/]");
return 1;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error: {Markup.Escape(ex.Message)}[/]");
if (verbose)
{
AnsiConsole.WriteException(ex);
}
return 1;
}
}
internal static async Task<int> HandleFederationBundleValidateAsync(
IServiceProvider services,
string inputPath,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
if (verbose)
{
AnsiConsole.MarkupLine("[blue]Validating federation bundle...[/]");
AnsiConsole.MarkupLine($" File: [bold]{Markup.Escape(inputPath)}[/]");
}
if (!File.Exists(inputPath))
{
AnsiConsole.MarkupLine($"[red]Error: File not found: {Markup.Escape(inputPath)}[/]");
return 1;
}
try
{
var httpClientFactory = services.GetService<IHttpClientFactory>();
if (httpClientFactory == null)
{
AnsiConsole.MarkupLine("[red]Error: HTTP client factory not available.[/]");
return 1;
}
var client = httpClientFactory.CreateClient("Concelier");
await using var fileStream = File.OpenRead(inputPath);
using var content = new StreamContent(fileStream);
content.Headers.ContentType = new MediaTypeHeaderValue("application/zstd");
using var response = await client.PostAsync("/api/v1/federation/import/validate", content, cancellationToken);
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
if (json)
{
AnsiConsole.WriteLine(responseContent);
}
else
{
var result = JsonSerializer.Deserialize<ValidateResponse>(responseContent, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (result != null)
{
var status = result.IsValid ? "[green]VALID[/]" : "[red]INVALID[/]";
AnsiConsole.MarkupLine($"{status} Bundle validation result");
AnsiConsole.MarkupLine($" Hash Valid: {(result.HashValid ? "[green]Yes[/]" : "[red]No[/]")}");
AnsiConsole.MarkupLine($" Signature Valid: {(result.SignatureValid ? "[green]Yes[/]" : "[yellow]No/Skipped[/]")}");
AnsiConsole.MarkupLine($" Cursor Valid: {(result.CursorValid ? "[green]Yes[/]" : "[yellow]No[/]")}");
if (result.Errors?.Count > 0)
{
AnsiConsole.MarkupLine("[red]Errors:[/]");
foreach (var error in result.Errors)
{
AnsiConsole.MarkupLine($" - {Markup.Escape(error)}");
}
}
if (result.Warnings?.Count > 0)
{
AnsiConsole.MarkupLine("[yellow]Warnings:[/]");
foreach (var warning in result.Warnings)
{
AnsiConsole.MarkupLine($" - {Markup.Escape(warning)}");
}
}
}
}
return response.IsSuccessStatusCode ? 0 : 1;
}
catch (HttpRequestException ex)
{
AnsiConsole.MarkupLine($"[red]Connection error: {Markup.Escape(ex.Message)}[/]");
return 1;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error: {Markup.Escape(ex.Message)}[/]");
if (verbose)
{
AnsiConsole.WriteException(ex);
}
return 1;
}
}
private sealed class ImportErrorResponse
{
public bool Success { get; set; }
public string? BundleHash { get; set; }
public string? FailureReason { get; set; }
public double DurationMs { get; set; }
}
private sealed class ImportSuccessResponse
{
public bool Success { get; set; }
public string? BundleHash { get; set; }
public string? ImportedCursor { get; set; }
public ImportCountsResponse? Counts { get; set; }
public List<object>? Conflicts { get; set; }
public double DurationMs { get; set; }
public bool DryRun { get; set; }
}
private sealed class ImportCountsResponse
{
public int CanonicalCreated { get; set; }
public int CanonicalUpdated { get; set; }
public int CanonicalSkipped { get; set; }
public int EdgesAdded { get; set; }
public int DeletionsProcessed { get; set; }
public int Total { get; set; }
}
private sealed class ValidateResponse
{
public bool IsValid { get; set; }
public List<string>? Errors { get; set; }
public List<string>? Warnings { get; set; }
public bool HashValid { get; set; }
public bool SignatureValid { get; set; }
public bool CursorValid { get; set; }
}
internal static async Task<int> HandleFederationSitesListAsync(
IServiceProvider services,
bool enabledOnly,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
if (verbose)
{
AnsiConsole.MarkupLine("[blue]Listing federation sites...[/]");
}
try
{
var httpClientFactory = services.GetService<IHttpClientFactory>();
if (httpClientFactory == null)
{
AnsiConsole.MarkupLine("[red]Error: HTTP client factory not available.[/]");
return 1;
}
var client = httpClientFactory.CreateClient("Concelier");
var url = "/api/v1/federation/sites";
if (enabledOnly)
url += "?enabled_only=true";
using var response = await client.GetAsync(url, cancellationToken);
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
if (!response.IsSuccessStatusCode)
{
AnsiConsole.MarkupLine($"[red]Error: {response.StatusCode}[/]");
if (verbose)
AnsiConsole.MarkupLine($"[grey]{Markup.Escape(responseContent)}[/]");
return 1;
}
if (json)
{
AnsiConsole.WriteLine(responseContent);
}
else
{
var result = JsonSerializer.Deserialize<SitesListResponse>(responseContent, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (result?.Sites != null && result.Sites.Count > 0)
{
var table = new Table();
table.AddColumn("Site ID");
table.AddColumn("Display Name");
table.AddColumn("Enabled");
table.AddColumn("Last Sync");
table.AddColumn("Imports");
foreach (var site in result.Sites)
{
var enabledMark = site.Enabled ? "[green]Yes[/]" : "[red]No[/]";
var lastSync = site.LastSyncAt?.ToString("g") ?? "-";
table.AddRow(
site.SiteId ?? "-",
site.DisplayName ?? "-",
enabledMark,
lastSync,
site.TotalImports.ToString());
}
AnsiConsole.Write(table);
AnsiConsole.MarkupLine($"\n[dim]{result.Count} site(s)[/]");
}
else
{
AnsiConsole.MarkupLine("[dim]No sites found.[/]");
}
}
return 0;
}
catch (HttpRequestException ex)
{
AnsiConsole.MarkupLine($"[red]Connection error: {Markup.Escape(ex.Message)}[/]");
return 1;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error: {Markup.Escape(ex.Message)}[/]");
if (verbose)
AnsiConsole.WriteException(ex);
return 1;
}
}
internal static async Task<int> HandleFederationSitesShowAsync(
IServiceProvider services,
string siteId,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
if (verbose)
{
AnsiConsole.MarkupLine($"[blue]Fetching site details for: {Markup.Escape(siteId)}[/]");
}
try
{
var httpClientFactory = services.GetService<IHttpClientFactory>();
if (httpClientFactory == null)
{
AnsiConsole.MarkupLine("[red]Error: HTTP client factory not available.[/]");
return 1;
}
var client = httpClientFactory.CreateClient("Concelier");
using var response = await client.GetAsync($"/api/v1/federation/sites/{Uri.EscapeDataString(siteId)}", cancellationToken);
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
if (!response.IsSuccessStatusCode)
{
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
AnsiConsole.MarkupLine($"[yellow]Site '{Markup.Escape(siteId)}' not found.[/]");
}
else
{
AnsiConsole.MarkupLine($"[red]Error: {response.StatusCode}[/]");
}
return 1;
}
if (json)
{
AnsiConsole.WriteLine(responseContent);
}
else
{
var site = JsonSerializer.Deserialize<SiteDetailsResponse>(responseContent, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (site != null)
{
AnsiConsole.MarkupLine($"[bold]Site: {Markup.Escape(site.SiteId ?? "")}[/]");
AnsiConsole.MarkupLine($" Display Name: {site.DisplayName ?? "(none)"}");
AnsiConsole.MarkupLine($" Enabled: {(site.Enabled ? "[green]Yes[/]" : "[red]No[/]")}");
AnsiConsole.MarkupLine($" Last Sync: {site.LastSyncAt?.ToString("g") ?? "(never)"}");
AnsiConsole.MarkupLine($" Last Cursor: [dim]{site.LastCursor ?? "(none)"}[/]");
AnsiConsole.MarkupLine($" Total Imports: {site.TotalImports}");
if (site.RecentHistory?.Count > 0)
{
AnsiConsole.MarkupLine("\n[bold]Recent Sync History:[/]");
var table = new Table();
table.AddColumn("Imported At");
table.AddColumn("Items");
table.AddColumn("Bundle Hash");
foreach (var entry in site.RecentHistory)
{
table.AddRow(
entry.ImportedAt.ToString("g"),
entry.ItemCount.ToString(),
entry.BundleHash?.Length > 16 ? entry.BundleHash[..16] + "..." : entry.BundleHash ?? "-"
);
}
AnsiConsole.Write(table);
}
}
}
return 0;
}
catch (HttpRequestException ex)
{
AnsiConsole.MarkupLine($"[red]Connection error: {Markup.Escape(ex.Message)}[/]");
return 1;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error: {Markup.Escape(ex.Message)}[/]");
if (verbose)
AnsiConsole.WriteException(ex);
return 1;
}
}
internal static async Task<int> HandleFederationSitesSetEnabledAsync(
IServiceProvider services,
string siteId,
bool enabled,
bool verbose,
CancellationToken cancellationToken)
{
var action = enabled ? "Enabling" : "Disabling";
if (verbose)
{
AnsiConsole.MarkupLine($"[blue]{action} site: {Markup.Escape(siteId)}[/]");
}
try
{
var httpClientFactory = services.GetService<IHttpClientFactory>();
if (httpClientFactory == null)
{
AnsiConsole.MarkupLine("[red]Error: HTTP client factory not available.[/]");
return 1;
}
var client = httpClientFactory.CreateClient("Concelier");
var payload = new { enabled };
var content = new StringContent(
JsonSerializer.Serialize(payload),
System.Text.Encoding.UTF8,
"application/json");
using var response = await client.PutAsync(
$"/api/v1/federation/sites/{Uri.EscapeDataString(siteId)}/policy",
content,
cancellationToken);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
AnsiConsole.MarkupLine($"[red]Error: {response.StatusCode}[/]");
if (verbose)
AnsiConsole.MarkupLine($"[grey]{Markup.Escape(errorContent)}[/]");
return 1;
}
var result = enabled ? "[green]enabled[/]" : "[yellow]disabled[/]";
AnsiConsole.MarkupLine($"Site '{Markup.Escape(siteId)}' {result}.");
return 0;
}
catch (HttpRequestException ex)
{
AnsiConsole.MarkupLine($"[red]Connection error: {Markup.Escape(ex.Message)}[/]");
return 1;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error: {Markup.Escape(ex.Message)}[/]");
if (verbose)
AnsiConsole.WriteException(ex);
return 1;
}
}
private sealed class SitesListResponse
{
public List<SiteInfo>? Sites { get; set; }
public int Count { get; set; }
}
private class SiteInfo
{
public string? SiteId { get; set; }
public string? DisplayName { get; set; }
public bool Enabled { get; set; }
public DateTimeOffset? LastSyncAt { get; set; }
public string? LastCursor { get; set; }
public int TotalImports { get; set; }
}
private sealed class SiteDetailsResponse : SiteInfo
{
public List<SyncHistoryEntry>? RecentHistory { get; set; }
}
private sealed class SyncHistoryEntry
{
public string? Cursor { get; set; }
public string? BundleHash { get; set; }
public int ItemCount { get; set; }
public DateTimeOffset ExportedAt { get; set; }
public DateTimeOffset ImportedAt { get; set; }
}
}

View File

@@ -1,8 +1,8 @@
// -----------------------------------------------------------------------------
// FederationCommandGroup.cs
// Sprint: SPRINT_8200_0014_0002 (Delta Bundle Export)
// Tasks: EXPORT-8200-025, EXPORT-8200-026 - CLI commands for federation bundle export.
// Description: CLI commands for federation bundle export to support air-gapped sync.
// Sprint: SPRINT_8200_0014_0002 (Delta Bundle Export), SPRINT_8200_0014_0003 (Bundle Import)
// Tasks: EXPORT-8200-025, EXPORT-8200-026, IMPORT-8200-027, IMPORT-8200-028
// Description: CLI commands for federation bundle export and import for air-gapped sync.
// -----------------------------------------------------------------------------
using System.CommandLine;
@@ -20,6 +20,7 @@ internal static class FederationCommandGroup
var feedser = new Command("feedser", "Federation bundle operations for multi-site sync.");
feedser.Add(BuildBundleCommand(services, verboseOption, cancellationToken));
feedser.Add(BuildSitesCommand(services, verboseOption, cancellationToken));
return feedser;
}
@@ -33,6 +34,8 @@ internal static class FederationCommandGroup
bundle.Add(BuildExportCommand(services, verboseOption, cancellationToken));
bundle.Add(BuildPreviewCommand(services, verboseOption, cancellationToken));
bundle.Add(BuildImportCommand(services, verboseOption, cancellationToken));
bundle.Add(BuildValidateCommand(services, verboseOption, cancellationToken));
return bundle;
}
@@ -149,4 +152,272 @@ internal static class FederationCommandGroup
return command;
}
private static Command BuildImportCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var inputArg = new Argument<string>("file")
{
Description = "Bundle file path to import."
};
var dryRunOption = new Option<bool>("--dry-run", new[] { "-n" })
{
Description = "Validate and preview without importing."
};
var skipSignatureOption = new Option<bool>("--skip-signature")
{
Description = "Skip signature verification (DANGEROUS)."
};
var onConflictOption = new Option<string>("--on-conflict")
{
Description = "Conflict resolution: PreferRemote (default), PreferLocal, Fail."
};
onConflictOption.SetDefaultValue("PreferRemote");
var forceOption = new Option<bool>("--force", new[] { "-f" })
{
Description = "Force import even if cursor validation fails."
};
var jsonOption = new Option<bool>("--json")
{
Description = "Output results as JSON."
};
var command = new Command("import", "Import federation bundle from file.")
{
inputArg,
dryRunOption,
skipSignatureOption,
onConflictOption,
forceOption,
jsonOption,
verboseOption
};
command.SetAction(parseResult =>
{
var input = parseResult.GetValue(inputArg)!;
var dryRun = parseResult.GetValue(dryRunOption);
var skipSignature = parseResult.GetValue(skipSignatureOption);
var onConflict = parseResult.GetValue(onConflictOption);
var force = parseResult.GetValue(forceOption);
var json = parseResult.GetValue(jsonOption);
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleFederationBundleImportAsync(
services,
input,
dryRun,
skipSignature,
onConflict,
force,
json,
verbose,
cancellationToken);
});
return command;
}
private static Command BuildValidateCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var inputArg = new Argument<string>("file")
{
Description = "Bundle file path to validate."
};
var jsonOption = new Option<bool>("--json")
{
Description = "Output results as JSON."
};
var command = new Command("validate", "Validate bundle without importing.")
{
inputArg,
jsonOption,
verboseOption
};
command.SetAction(parseResult =>
{
var input = parseResult.GetValue(inputArg)!;
var json = parseResult.GetValue(jsonOption);
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleFederationBundleValidateAsync(
services,
input,
json,
verbose,
cancellationToken);
});
return command;
}
private static Command BuildSitesCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var sites = new Command("sites", "Federation site management.");
sites.Add(BuildSitesListCommand(services, verboseOption, cancellationToken));
sites.Add(BuildSitesShowCommand(services, verboseOption, cancellationToken));
sites.Add(BuildSitesEnableCommand(services, verboseOption, cancellationToken));
sites.Add(BuildSitesDisableCommand(services, verboseOption, cancellationToken));
return sites;
}
private static Command BuildSitesListCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var enabledOnlyOption = new Option<bool>("--enabled-only", new[] { "-e" })
{
Description = "Show only enabled sites."
};
var jsonOption = new Option<bool>("--json")
{
Description = "Output as JSON."
};
var command = new Command("list", "List all federation sites.")
{
enabledOnlyOption,
jsonOption,
verboseOption
};
command.SetAction(parseResult =>
{
var enabledOnly = parseResult.GetValue(enabledOnlyOption);
var json = parseResult.GetValue(jsonOption);
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleFederationSitesListAsync(
services,
enabledOnly,
json,
verbose,
cancellationToken);
});
return command;
}
private static Command BuildSitesShowCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var siteIdArg = new Argument<string>("site-id")
{
Description = "Site identifier."
};
var jsonOption = new Option<bool>("--json")
{
Description = "Output as JSON."
};
var command = new Command("show", "Show site details and sync history.")
{
siteIdArg,
jsonOption,
verboseOption
};
command.SetAction(parseResult =>
{
var siteId = parseResult.GetValue(siteIdArg)!;
var json = parseResult.GetValue(jsonOption);
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleFederationSitesShowAsync(
services,
siteId,
json,
verbose,
cancellationToken);
});
return command;
}
private static Command BuildSitesEnableCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var siteIdArg = new Argument<string>("site-id")
{
Description = "Site identifier."
};
var command = new Command("enable", "Enable federation sync for a site.")
{
siteIdArg,
verboseOption
};
command.SetAction(parseResult =>
{
var siteId = parseResult.GetValue(siteIdArg)!;
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleFederationSitesSetEnabledAsync(
services,
siteId,
enabled: true,
verbose,
cancellationToken);
});
return command;
}
private static Command BuildSitesDisableCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var siteIdArg = new Argument<string>("site-id")
{
Description = "Site identifier."
};
var command = new Command("disable", "Disable federation sync for a site.")
{
siteIdArg,
verboseOption
};
command.SetAction(parseResult =>
{
var siteId = parseResult.GetValue(siteIdArg)!;
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleFederationSitesSetEnabledAsync(
services,
siteId,
enabled: false,
verbose,
cancellationToken);
});
return command;
}
}