save dev progress
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user