using System.Security.Cryptography; using System.Text; using System.Text.Json; using Sdt.Config; namespace Sdt.Core.Debug; public sealed class DiagnosticsBundleService : IDiagnosticsBundleService { private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; public async Task WriteBundleAsync( DiagnosticsBundleRequest request, CancellationToken cancellationToken = default) { try { var timestamp = DateTimeOffset.Now.ToString("yyyyMMdd-HHmmss"); var root = Path.GetFullPath(Path.Combine(request.ProjectRoot, request.DiagnosticsOptions.OutputDir)); var bundleDir = Path.Combine(root, $"{request.Category}-{timestamp}"); Directory.CreateDirectory(bundleDir); var stepsPath = Path.Combine(bundleDir, "steps.json"); var toolsPath = Path.Combine(bundleDir, "tools.json"); var envPath = Path.Combine(bundleDir, "env.json"); var outputPath = Path.Combine(bundleDir, "output.log"); var summaryPath = Path.Combine(bundleDir, "summary.json"); await File.WriteAllTextAsync(stepsPath, JsonSerializer.Serialize(request.WorkflowSteps, JsonOptions), cancellationToken); await File.WriteAllTextAsync(toolsPath, JsonSerializer.Serialize(request.Probes, JsonOptions), cancellationToken); await File.WriteAllTextAsync(envPath, JsonSerializer.Serialize(CaptureEnvironment(request.DiagnosticsOptions), JsonOptions), cancellationToken); await File.WriteAllLinesAsync(outputPath, request.OutputLines, cancellationToken); var summary = new { category = request.Category, stopReason = request.StopReason?.ToString(), message = request.SummaryMessage, createdAt = DateTimeOffset.Now, configHash = HashConfig(request.Config), debugProfile = request.DebugProfile?.Id, debugExitCode = request.DebugRun?.ExitCode, envCapture = BuildEnvCaptureSummary(request.DiagnosticsOptions), }; await File.WriteAllTextAsync(summaryPath, JsonSerializer.Serialize(summary, JsonOptions), cancellationToken); return new DiagnosticsBundleResult( Success: true, BundleDirectory: bundleDir, ZipPath: null, Message: "Diagnostics bundle generated."); } catch (Exception ex) { return new DiagnosticsBundleResult( Success: false, BundleDirectory: string.Empty, ZipPath: null, Message: ex.Message); } } private static Dictionary CaptureEnvironment(DebugDiagnosticsOptions options) { if (options.IncludeAllEnv) { return Environment.GetEnvironmentVariables() .Cast() .ToDictionary(e => e.Key?.ToString() ?? "", e => e.Value?.ToString() ?? "", StringComparer.OrdinalIgnoreCase); } if (options.CaptureEnvKeys.Count == 0) return new Dictionary(StringComparer.OrdinalIgnoreCase); var captured = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var key in options.CaptureEnvKeys) captured[key] = Environment.GetEnvironmentVariable(key) ?? ""; return captured; } private static string BuildEnvCaptureSummary(DebugDiagnosticsOptions options) { if (options.IncludeAllEnv) return "full"; if (options.CaptureEnvKeys.Count == 0) return "allowlist-empty (intentional minimal capture)"; return $"allowlist ({options.CaptureEnvKeys.Count} keys)"; } private static string HashConfig(object config) { var json = JsonSerializer.Serialize(config); var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(json)); return Convert.ToHexString(bytes); } }