Der Button, der aus „Problem erkannt" ein „Problem gelöst" macht: startet eine interaktive Claude-Session, die dem Nutzer beim Einrichten hilft. ## Warum externes Terminal Der ConPTY-Stack (`PtyTerminalSession`, `ConPtyPaneView`) liegt in `ClaudeDo.Ui` und ist Avalonia — der Installer ist WPF und referenziert nur Data/Releases/Localization. Beim Fresh Install sind `app\`/`worker\` außerdem noch n ClaudeDo-Task: 4e196058-38a3-404f-9862-4cb0e90195da
183 lines
6.9 KiB
C#
183 lines
6.9 KiB
C#
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);
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<string> 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<ClaudeHelpLaunchResult> 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<string> 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('\\', '/'));
|
|
}
|