using System.Diagnostics; using System.Text.Json; using Sdt.Config; namespace Sdt.Core; public sealed class ToolProbeService : IToolProbe { public async Task ProbeAsync( string tool, string projectRoot, DevToolConfig? config = null, CancellationToken cancellationToken = default) { var direct = await ProbeDirectAsync(tool, config, cancellationToken).ConfigureAwait(false); if (direct.IsAvailable) return direct; var scriptPath = ScriptLocator.FindHelperScript(projectRoot, "diag.py"); if (scriptPath is null) return direct; if (!(await ProbeDirectAsync("python", config, cancellationToken).ConfigureAwait(false)).IsAvailable) return direct; try { var psi = new ProcessStartInfo { FileName = PythonResolver.ResolveExecutable(), RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, WorkingDirectory = projectRoot, }; psi.ArgumentList.Add(scriptPath); psi.ArgumentList.Add("probe"); psi.ArgumentList.Add("--tool"); psi.ArgumentList.Add(tool); psi.ArgumentList.Add("--json"); using var process = new Process { StartInfo = psi }; process.Start(); var stdout = await process.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false); var stderr = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false); await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); if (process.ExitCode != 0) return new ProbeResult(tool, false, Details: stderr.Trim()); var parsed = JsonSerializer.Deserialize(stdout); if (parsed is null) return new ProbeResult(tool, false, Details: "diag.py returned invalid JSON"); return new ProbeResult(parsed.Tool ?? tool, parsed.Available, parsed.Version, parsed.Details); } catch (Exception ex) { return new ProbeResult(tool, false, Details: ex.Message); } } private static async Task ProbeDirectAsync(string tool, DevToolConfig? config, CancellationToken cancellationToken) { var command = tool.ToLowerInvariant() switch { "python" => PythonResolver.ResolveExecutable(), "dotnet" => "dotnet", "node" => "node", "npm" => "npm", "cargo" => "cargo", "tauri" => "tauri", "git" => "git", "docker" => "docker", _ => tool, }; var resolution = CommandResolver.ResolveWithTrace(command, config, tool); command = resolution.Resolved; var versionArg = command is "python" ? "--version" : "--version"; try { var psi = new ProcessStartInfo { FileName = command, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, }; psi.ArgumentList.Add(versionArg); using var process = new Process { StartInfo = psi }; process.Start(); var stdout = await process.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false); var stderr = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false); await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); if (process.ExitCode != 0) { var failDetails = string.IsNullOrWhiteSpace(stderr) ? stdout.Trim() : stderr.Trim(); var trace = $"{resolution.Source}: {resolution.Resolved}"; return new ProbeResult(tool, false, Details: string.IsNullOrWhiteSpace(failDetails) ? trace : $"{trace} | {failDetails}"); } var version = string.IsNullOrWhiteSpace(stdout) ? stderr.Trim() : stdout.Trim(); return new ProbeResult(tool, true, Version: version, Details: $"{resolution.Source}: {resolution.Resolved}"); } catch (Exception ex) { return new ProbeResult(tool, false, Details: $"{resolution.Source}: {resolution.Resolved} | {ex.Message}"); } } private sealed class DiagProbeJson { public string? Tool { get; init; } public bool Available { get; init; } public string? Version { get; init; } public string? Details { get; init; } } }