using Sdt.Config; using Sdt.Runner; namespace Sdt.Core.Debug; public sealed class DebugProfileRunner( IToolProbe toolProbe, IPrereqInstaller installer) : IDebugProfileRunner { private readonly IToolProbe _toolProbe = toolProbe; private readonly IPrereqInstaller _installer = installer; public async Task RunAsync( DebugProfileDefinition profile, DevToolConfig config, string projectRoot, bool verbose, Func> confirmInstallAsync, Action onOutput, Action? onEvent = null, CancellationToken cancellationToken = default) { var probes = new List(); var output = new List(); onEvent?.Invoke(new RunEvent( Category: "debug", Type: RunEventType.DebugStarted, Message: $"Debug profile '{profile.Id}' started.")); var requires = profile.Requires.Count > 0 ? profile.Requires : InferRequirements(profile); foreach (var req in requires) { var probe = await _toolProbe.ProbeAsync(req.Tool, projectRoot, config, cancellationToken).ConfigureAwait(false); probes.Add(probe); if (probe.IsAvailable) continue; if (!string.IsNullOrWhiteSpace(probe.Details)) { var line = $"Probe detail [{req.Tool}]: {probe.Details}"; output.Add("OUT: " + line); if (verbose) onOutput(line, false); } if (req.InstallPolicy == InstallPolicy.Never) { onEvent?.Invoke(new RunEvent( Category: "debug", Type: RunEventType.DebugCompleted, Message: $"Missing prerequisite '{req.Tool}'.", Tool: req.Tool, Success: false)); return new DebugRunResult( Success: false, StopReason: ExecutionStopReason.MissingPrereq, Message: $"Missing prerequisite '{req.Tool}' for debug profile '{profile.Label}'.", Profile: profile, RunResult: null, OutputLines: output, Probes: probes); } var installPlan = await _installer.GetInstallPlanAsync(req.Tool, projectRoot, config, cancellationToken).ConfigureAwait(false); if (!installPlan.Supported || installPlan.Commands.Count == 0) { onEvent?.Invoke(new RunEvent( Category: "debug", Type: RunEventType.DebugCompleted, Message: $"No installer plan available for '{req.Tool}'.", Tool: req.Tool, Success: false)); return new DebugRunResult( Success: false, StopReason: ExecutionStopReason.MissingPrereq, Message: $"Missing prerequisite '{req.Tool}' and no installer plan is available.", Profile: profile, RunResult: null, OutputLines: output, Probes: probes); } var approved = req.InstallPolicy == InstallPolicy.Auto ? true : await confirmInstallAsync(req.Tool, installPlan).ConfigureAwait(false); if (!approved) { onEvent?.Invoke(new RunEvent( Category: "debug", Type: RunEventType.InstallDeclined, Message: $"Install declined for '{req.Tool}'.", Tool: req.Tool, Success: false)); return new DebugRunResult( Success: false, StopReason: ExecutionStopReason.UserDeclined, Message: $"Install declined for missing prerequisite '{req.Tool}'.", Profile: profile, RunResult: null, OutputLines: output, Probes: probes); } foreach (var cmd in installPlan.Commands) { var installResult = await _installer.RunInstallAsync(cmd, projectRoot, onOutput, cancellationToken).ConfigureAwait(false); if (!installResult.Success) { onEvent?.Invoke(new RunEvent( Category: "debug", Type: RunEventType.DebugCompleted, Message: $"Install failed for '{req.Tool}'.", Tool: req.Tool, Success: false, ExitCode: installResult.ExitCode)); return new DebugRunResult( Success: false, StopReason: ExecutionStopReason.InstallFailed, Message: $"Failed to install prerequisite '{req.Tool}'.", Profile: profile, RunResult: installResult, OutputLines: output, Probes: probes); } } } var cwd = Path.GetFullPath(Path.Combine(projectRoot, profile.WorkingDir)); var mergedEnv = profile.Env.Count > 0 ? profile.Env : null; onEvent?.Invoke(new RunEvent( Category: "debug", Type: RunEventType.DebugCommandStarted, Message: $"{profile.Command} {string.Join(" ", profile.Args)}")); var run = await ProcessRunner.RunAsync( profile.Command, profile.Args, cwd, (line, isErr) => { output.Add((isErr ? "ERR: " : "OUT: ") + line); if (verbose) onOutput(line, isErr); }, mergedEnv, cancellationToken).ConfigureAwait(false); onEvent?.Invoke(new RunEvent( Category: "debug", Type: RunEventType.DebugCommandCompleted, Message: $"Debug command exited {run.ExitCode}.", Success: run.Success, ExitCode: run.ExitCode)); onEvent?.Invoke(new RunEvent( Category: "debug", Type: RunEventType.DebugCompleted, Message: run.Success ? "Debug run completed." : "Debug run failed.", Success: run.Success, ExitCode: run.ExitCode)); return new DebugRunResult( Success: run.Success, StopReason: run.Success ? null : ExecutionStopReason.CommandFailed, Message: run.Success ? $"Debug profile '{profile.Label}' completed." : $"Debug profile '{profile.Label}' exited with code {run.ExitCode}.", Profile: profile, RunResult: run, OutputLines: output, Probes: probes); } private static List InferRequirements(DebugProfileDefinition profile) { return profile.Type.ToLowerInvariant() switch { "dotnet" => [new ToolRequirement { Tool = "dotnet" }], "node" => [new ToolRequirement { Tool = "node" }, new ToolRequirement { Tool = "npm" }], "python" => [new ToolRequirement { Tool = "python" }], _ => string.IsNullOrWhiteSpace(profile.Command) ? [] : [new ToolRequirement { Tool = profile.Command }], }; } }