using Sdt.Config; namespace Sdt.Core; public sealed class WorkflowExecutor( IWorkflowPlanner planner, IToolProbe toolProbe, IPrereqInstaller installer, IActionRunner actionRunner, IRequirementResolver requirementResolver) { private readonly IWorkflowPlanner _planner = planner; private readonly IToolProbe _toolProbe = toolProbe; private readonly IPrereqInstaller _installer = installer; private readonly IActionRunner _actionRunner = actionRunner; private readonly IRequirementResolver _requirementResolver = requirementResolver; public async Task ExecuteAsync( WorkflowDefinition rootWorkflow, IReadOnlyDictionary allWorkflows, DevToolConfig config, string projectRoot, Func> confirmInstallAsync, Action onOutput, Action? onEvent = null, IReadOnlyDictionary? envOverrides = null, CancellationToken cancellationToken = default) { var results = new List(); var plan = _planner.ResolvePlan(rootWorkflow, allWorkflows); onEvent?.Invoke(new RunEvent( Category: "workflow", Type: RunEventType.WorkflowStarted, Message: $"Workflow '{rootWorkflow.Id}' started.", WorkflowId: rootWorkflow.Id)); onEvent?.Invoke(new RunEvent( Category: "workflow", Type: RunEventType.WorkflowPlanned, Message: $"Execution plan contains {plan.Count} workflow(s).", WorkflowId: rootWorkflow.Id)); if (plan.Count == 0) { onEvent?.Invoke(new RunEvent( Category: "workflow", Type: RunEventType.WorkflowCompleted, Message: "No executable workflow steps were found.", WorkflowId: rootWorkflow.Id, Success: false)); return new WorkflowExecutionResult( Success: false, StopReason: ExecutionStopReason.ValidationFailed, Message: "This workflow has no executable steps.", Steps: results); } foreach (var workflow in plan) { foreach (var step in workflow.Steps) { onEvent?.Invoke(new RunEvent( Category: "workflow", Type: RunEventType.WorkflowStepStarted, Message: $"Step '{step.Label}' started.", WorkflowId: workflow.Id, StepId: step.Id)); var requires = _requirementResolver.Resolve(step); foreach (var req in requires) { var probe = await _toolProbe.ProbeAsync(req.Tool, projectRoot, config, envOverrides, cancellationToken).ConfigureAwait(false); onEvent?.Invoke(new RunEvent( Category: "workflow", Type: RunEventType.ProbeChecked, Message: probe.IsAvailable ? $"Tool '{req.Tool}' is available." : $"Tool '{req.Tool}' is missing.", WorkflowId: workflow.Id, StepId: step.Id, Tool: req.Tool, Success: probe.IsAvailable)); if (probe.IsAvailable) continue; if (!string.IsNullOrWhiteSpace(probe.Details)) { onOutput($"Probe detail [{req.Tool}]: {probe.Details}", false); onEvent?.Invoke(new RunEvent( Category: "workflow", Type: RunEventType.ProbeFailed, Message: probe.Details, WorkflowId: workflow.Id, StepId: step.Id, Tool: req.Tool, Success: false)); } if (req.InstallPolicy == InstallPolicy.Never) { onEvent?.Invoke(new RunEvent( Category: "workflow", Type: RunEventType.WorkflowCompleted, Message: $"Missing prerequisite '{req.Tool}'.", WorkflowId: rootWorkflow.Id, Tool: req.Tool, Success: false)); return new WorkflowExecutionResult( Success: false, StopReason: ExecutionStopReason.MissingPrereq, Message: $"Missing prerequisite '{req.Tool}' for step '{step.Label}'.", Steps: results); } var installPlan = await _installer.GetInstallPlanAsync( req.Tool, projectRoot, config, cancellationToken).ConfigureAwait(false); onEvent?.Invoke(new RunEvent( Category: "workflow", Type: RunEventType.InstallPlanPrepared, Message: installPlan.Summary, WorkflowId: workflow.Id, StepId: step.Id, Tool: req.Tool, Success: installPlan.Supported)); if (!installPlan.Supported || installPlan.Commands.Count == 0) { onEvent?.Invoke(new RunEvent( Category: "workflow", Type: RunEventType.WorkflowCompleted, Message: $"No installer plan available for '{req.Tool}'.", WorkflowId: rootWorkflow.Id, Tool: req.Tool, Success: false)); return new WorkflowExecutionResult( Success: false, StopReason: ExecutionStopReason.MissingPrereq, Message: $"Missing prerequisite '{req.Tool}' and no installer plan is available.", Steps: results); } var approved = req.InstallPolicy == InstallPolicy.Auto ? true : await confirmInstallAsync(req.Tool, installPlan).ConfigureAwait(false); if (!approved) { onEvent?.Invoke(new RunEvent( Category: "workflow", Type: RunEventType.InstallDeclined, Message: $"Install declined for '{req.Tool}'.", WorkflowId: workflow.Id, StepId: step.Id, Tool: req.Tool, Success: false)); return new WorkflowExecutionResult( Success: false, StopReason: ExecutionStopReason.UserDeclined, Message: $"Install declined for missing prerequisite '{req.Tool}'.", Steps: results); } foreach (var installCommand in installPlan.Commands) { onEvent?.Invoke(new RunEvent( Category: "workflow", Type: RunEventType.InstallCommandStarted, Message: $"{installCommand.Command} {string.Join(" ", installCommand.Args)}", WorkflowId: workflow.Id, StepId: step.Id, Tool: req.Tool)); var installResult = await _installer.RunInstallAsync( installCommand, projectRoot, onOutput, envOverrides, cancellationToken).ConfigureAwait(false); onEvent?.Invoke(new RunEvent( Category: "workflow", Type: RunEventType.InstallCommandCompleted, Message: $"Install command exited {installResult.ExitCode}.", WorkflowId: workflow.Id, StepId: step.Id, Tool: req.Tool, Success: installResult.Success, ExitCode: installResult.ExitCode)); if (!installResult.Success) { onEvent?.Invoke(new RunEvent( Category: "workflow", Type: RunEventType.WorkflowCompleted, Message: $"Install failed for '{req.Tool}'.", WorkflowId: rootWorkflow.Id, Tool: req.Tool, Success: false)); return new WorkflowExecutionResult( Success: false, StopReason: ExecutionStopReason.InstallFailed, Message: $"Failed to install prerequisite '{req.Tool}'.", Steps: results); } } var verifyProbe = await _toolProbe.ProbeAsync(req.Tool, projectRoot, config, envOverrides, cancellationToken).ConfigureAwait(false); if (!verifyProbe.IsAvailable) { onEvent?.Invoke(new RunEvent( Category: "workflow", Type: RunEventType.WorkflowCompleted, Message: $"Tool '{req.Tool}' still missing after install.", WorkflowId: rootWorkflow.Id, Tool: req.Tool, Success: false)); return new WorkflowExecutionResult( Success: false, StopReason: ExecutionStopReason.InstallFailed, Message: $"Prerequisite '{req.Tool}' is still missing after install attempt.", Steps: results); } } var runResult = await _actionRunner.RunStepAsync( step, projectRoot, onOutput, envOverrides, cancellationToken).ConfigureAwait(false); results.Add(new WorkflowStepResult( workflow.Id, step.Id, string.IsNullOrWhiteSpace(step.Label) ? step.Id : step.Label, runResult)); onEvent?.Invoke(new RunEvent( Category: "workflow", Type: RunEventType.WorkflowStepCompleted, Message: $"Step '{step.Label}' exited {runResult.ExitCode}.", WorkflowId: workflow.Id, StepId: step.Id, Success: runResult.Success, ExitCode: runResult.ExitCode)); if (!runResult.Success) { onEvent?.Invoke(new RunEvent( Category: "workflow", Type: RunEventType.WorkflowCompleted, Message: $"Step '{step.Label}' failed.", WorkflowId: rootWorkflow.Id, Success: false, ExitCode: runResult.ExitCode)); return new WorkflowExecutionResult( Success: false, StopReason: ExecutionStopReason.CommandFailed, Message: $"Step '{step.Label}' failed with exit code {runResult.ExitCode}.", Steps: results); } } } onEvent?.Invoke(new RunEvent( Category: "workflow", Type: RunEventType.WorkflowCompleted, Message: "Workflow completed successfully.", WorkflowId: rootWorkflow.Id, Success: true)); return new WorkflowExecutionResult( Success: true, StopReason: null, Message: "Workflow completed successfully.", Steps: results); } }