using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Text; using ClaudeDo.Data.Environment; using ClaudeDo.Installer.Checks; using ClaudeDo.Installer.Core.Interfaces; namespace ClaudeDo.Installer.Core; public sealed record ClaudeHelpLaunchResult(bool Success, string? ErrorMessage); /// /// Builds a setup-diagnose report from the environment check results and opens an interactive /// claude session pointed at it, so a stuck user can get live help finishing setup. /// public sealed class ClaudeHelpLauncher { public const string ReportFileName = "claudedo-setup-diagnose.md"; private readonly IProcessRunner _processRunner; private readonly IProcessLauncher _processLauncher; private readonly string? _pathOverride; private readonly string? _pathExtOverride; public ClaudeHelpLauncher( IProcessRunner processRunner, IProcessLauncher processLauncher, string? pathOverride = null, string? pathExtOverride = null) { _processRunner = processRunner; _processLauncher = processLauncher; _pathOverride = pathOverride; _pathExtOverride = pathExtOverride; } public async Task BuildReportAsync(EnvironmentCheckReport report, InstallContext ctx, CancellationToken ct) { var sb = new StringBuilder(); sb.AppendLine("# ClaudeDo setup diagnose"); sb.AppendLine(); sb.AppendLine( "ClaudeDo is a desktop task manager that runs the `claude` CLI autonomously in git " + "worktrees. The person you're talking to is stuck partway through the setup wizard. " + "Explain the likely cause of each failing check below in plain language, propose the " + "concrete command(s) to fix it, and ask a clarifying question if something here is " + "ambiguous."); sb.AppendLine(); sb.AppendLine("## Check results"); sb.AppendLine(); sb.AppendLine("| Id | Severity | Status | Message |"); sb.AppendLine("|---|---|---|---|"); foreach (var result in report.Results) { sb.AppendLine($"| {result.Id} | {result.Severity} | {result.Status} | {EscapeCell(result.Message)} |"); } sb.AppendLine(); var failedDetails = report.Results .Where(r => r.Status == CheckStatus.Failed && !string.IsNullOrWhiteSpace(r.Detail)) .ToList(); if (failedDetails.Count > 0) { sb.AppendLine("## Details of failed checks"); foreach (var result in failedDetails) { sb.AppendLine(); sb.AppendLine($"### {result.Id}"); sb.AppendLine("```"); sb.AppendLine(result.Detail); sb.AppendLine("```"); } sb.AppendLine(); } sb.AppendLine("## System info"); sb.AppendLine(); sb.AppendLine($"- Windows: {RuntimeInformation.OSDescription}"); sb.AppendLine($"- Architecture: {RuntimeInformation.OSArchitecture}"); sb.AppendLine($"- git: {FindMessage(report, GitCheck.CheckId)}"); sb.AppendLine($"- claude: {FindMessage(report, ClaudeCliCheck.CheckId)}"); sb.AppendLine($"- Planned install directory: {ctx.InstallDirectory}"); sb.AppendLine($"- Planned ports: SignalR {ctx.SignalRPort}, MCP {ctx.ExternalMcpPort}"); sb.AppendLine("- `dotnet --list-runtimes`:"); sb.AppendLine("```"); sb.AppendLine(await RunDotnetListRuntimesAsync(ct)); sb.AppendLine("```"); sb.AppendLine(); // No secrets: never include credentials.json content, tokens, or a raw environment // variable dump here — only resolved paths and version numbers, since this file exists // to be read by an AI assistant, not to stay private. sb.AppendLine( "No secrets are included above (no credentials file contents, no tokens, no raw " + "environment variable dump) — only resolved paths and version numbers."); sb.AppendLine(); sb.AppendLine("Please start with the blocking errors above."); return sb.ToString(); } public async Task LaunchAsync(EnvironmentCheckReport report, InstallContext ctx, CancellationToken ct) { string reportPath; try { var content = await BuildReportAsync(report, ctx, ct); reportPath = Path.Combine(Path.GetTempPath(), ReportFileName); await File.WriteAllTextAsync(reportPath, content, ct); } catch (Exception ex) { return new ClaudeHelpLaunchResult(false, ex.Message); } return LaunchTerminal(reportPath, ctx); } public ClaudeHelpLaunchResult LaunchTerminal(string reportPath, InstallContext ctx) { var claude = ExecutableResolver.Resolve(ctx.ClaudeBin, _pathOverride, _pathExtOverride); if (claude is null) { return new ClaudeHelpLaunchResult(false, $"'{ctx.ClaudeBin}' was not found on PATH."); } var tempDir = Path.GetTempPath(); var promptText = $"Lies {reportPath} und hilf mir, mein ClaudeDo-Setup zum Laufen zu bringen."; var claudeCommand = $"{Quote(claude.Path)} {Quote(promptText)}"; var wt = ExecutableResolver.Resolve("wt", _pathOverride, _pathExtOverride); var startInfo = wt is not null ? new ProcessStartInfo(wt.Path) { Arguments = $"-d {QuoteDirectory(tempDir)} cmd.exe /k {claudeCommand}", WorkingDirectory = tempDir, UseShellExecute = false, } : new ProcessStartInfo("cmd.exe") { Arguments = $"/k {claudeCommand}", WorkingDirectory = tempDir, UseShellExecute = false, }; try { _processLauncher.Start(startInfo); return new ClaudeHelpLaunchResult(true, null); } catch (Exception ex) { return new ClaudeHelpLaunchResult(false, ex.Message); } } private async Task RunDotnetListRuntimesAsync(CancellationToken ct) { try { var (_, output) = await _processRunner.RunAsync("dotnet", "--list-runtimes", null, ct); return output.Trim(); } catch (Exception ex) { return $"(could not run 'dotnet --list-runtimes': {ex.Message})"; } } private static string? FindMessage(EnvironmentCheckReport report, string checkId) => report.Results.FirstOrDefault(r => r.Id == checkId)?.Message; private static string EscapeCell(string value) => value.Replace("|", "\\|").Replace("\r", " ").Replace("\n", " "); private static string Quote(string value) => value.Contains(' ') ? $"\"{value}\"" : value; private static string QuoteDirectory(string directory) => Quote(directory.TrimEnd('\\', '/')); }