save development progress

This commit is contained in:
StellaOps Bot
2025-12-25 23:09:58 +02:00
parent d71853ad7e
commit aa70af062e
351 changed files with 37683 additions and 150156 deletions

View File

@@ -0,0 +1,256 @@
// -----------------------------------------------------------------------------
// CommandHandlers.Federation.cs
// Sprint: SPRINT_8200_0014_0002 (Delta Bundle Export)
// Description: Command handlers for federation bundle operations.
// -----------------------------------------------------------------------------
using System.Net.Http.Headers;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Spectre.Console;
namespace StellaOps.Cli.Commands;
internal static partial class CommandHandlers
{
internal static async Task<int> HandleFederationBundleExportAsync(
IServiceProvider services,
string? sinceCursor,
string? output,
bool sign,
int compressLevel,
int maxItems,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
if (verbose)
{
AnsiConsole.MarkupLine("[blue]Exporting federation bundle...[/]");
AnsiConsole.MarkupLine($" Since Cursor: [bold]{Markup.Escape(sinceCursor ?? "(none - full export)")}[/]");
AnsiConsole.MarkupLine($" Sign: {sign}");
AnsiConsole.MarkupLine($" Compression Level: {compressLevel}");
AnsiConsole.MarkupLine($" Max Items: {maxItems}");
}
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 (!string.IsNullOrEmpty(sinceCursor))
queryParams.Add($"since_cursor={Uri.EscapeDataString(sinceCursor)}");
queryParams.Add($"sign={sign.ToString().ToLowerInvariant()}");
queryParams.Add($"max_items={maxItems}");
queryParams.Add($"compress_level={compressLevel}");
var url = $"/api/v1/federation/export?{string.Join("&", queryParams)}";
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync(cancellationToken);
AnsiConsole.MarkupLine($"[red]Error: {response.StatusCode}[/]");
if (verbose)
{
AnsiConsole.MarkupLine($"[grey]{Markup.Escape(error)}[/]");
}
return 1;
}
// Extract metadata from headers
var bundleHash = response.Headers.TryGetValues("X-Bundle-Hash", out var hashValues)
? hashValues.FirstOrDefault()
: null;
var exportCursor = response.Headers.TryGetValues("X-Export-Cursor", out var cursorValues)
? cursorValues.FirstOrDefault()
: null;
var itemsCount = response.Headers.TryGetValues("X-Items-Count", out var countValues)
? countValues.FirstOrDefault()
: null;
// Determine output stream
Stream outputStream;
bool disposeStream;
if (string.IsNullOrEmpty(output))
{
outputStream = Console.OpenStandardOutput();
disposeStream = false;
}
else
{
outputStream = File.Create(output);
disposeStream = true;
}
try
{
await using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);
await contentStream.CopyToAsync(outputStream, cancellationToken);
}
finally
{
if (disposeStream)
{
await outputStream.DisposeAsync();
}
}
// Output metadata
if (!string.IsNullOrEmpty(output))
{
if (json)
{
var metadata = new
{
bundle_hash = bundleHash,
export_cursor = exportCursor,
since_cursor = sinceCursor,
items_count = int.TryParse(itemsCount, out var count) ? count : 0,
output_path = output
};
AnsiConsole.WriteLine(JsonSerializer.Serialize(metadata, new JsonSerializerOptions
{
WriteIndented = true
}));
}
else
{
AnsiConsole.MarkupLine("[green]Bundle exported successfully.[/]");
AnsiConsole.MarkupLine($" Output: [bold]{Markup.Escape(output)}[/]");
if (bundleHash != null)
AnsiConsole.MarkupLine($" Hash: [dim]{bundleHash}[/]");
if (exportCursor != null)
AnsiConsole.MarkupLine($" New Cursor: [bold]{exportCursor}[/]");
if (itemsCount != null)
AnsiConsole.MarkupLine($" Items: {itemsCount}");
}
}
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> HandleFederationBundlePreviewAsync(
IServiceProvider services,
string? sinceCursor,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
if (verbose)
{
AnsiConsole.MarkupLine("[blue]Previewing federation export...[/]");
AnsiConsole.MarkupLine($" Since Cursor: [bold]{Markup.Escape(sinceCursor ?? "(none - full export)")}[/]");
}
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/export/preview";
if (!string.IsNullOrEmpty(sinceCursor))
{
url += $"?since_cursor={Uri.EscapeDataString(sinceCursor)}";
}
using var response = await client.GetAsync(url, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync(cancellationToken);
AnsiConsole.MarkupLine($"[red]Error: {response.StatusCode}[/]");
if (verbose)
{
AnsiConsole.MarkupLine($"[grey]{Markup.Escape(error)}[/]");
}
return 1;
}
var content = await response.Content.ReadAsStringAsync(cancellationToken);
if (json)
{
AnsiConsole.WriteLine(content);
}
else
{
var preview = JsonSerializer.Deserialize<PreviewResponse>(content, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (preview != null)
{
AnsiConsole.MarkupLine("[green]Export Preview[/]");
AnsiConsole.MarkupLine($" Since Cursor: [dim]{sinceCursor ?? "(full export)"}[/]");
AnsiConsole.MarkupLine($" Estimated Canonicals: [bold]{preview.EstimatedCanonicals:N0}[/]");
AnsiConsole.MarkupLine($" Estimated Edges: [bold]{preview.EstimatedEdges:N0}[/]");
AnsiConsole.MarkupLine($" Estimated Deletions: [bold]{preview.EstimatedDeletions:N0}[/]");
AnsiConsole.MarkupLine($" Estimated Size: [bold]{preview.EstimatedSizeMb:F2} MB[/]");
}
else
{
AnsiConsole.WriteLine(content);
}
}
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 PreviewResponse
{
public string? SinceCursor { get; set; }
public int EstimatedCanonicals { get; set; }
public int EstimatedEdges { get; set; }
public int EstimatedDeletions { get; set; }
public long EstimatedSizeBytes { get; set; }
public double EstimatedSizeMb { get; set; }
}
}