using Sdt.Config; using Sdt.Runner; namespace Sdt.Core; public sealed class ActionRunner : IActionRunner { public async Task RunStepAsync( WorkflowStep step, string projectRoot, Action onOutput, CancellationToken cancellationToken = default) { if (!string.IsNullOrWhiteSpace(step.Action)) { var scriptPath = ScriptLocator.FindHelperScript(projectRoot, "build.py"); if (scriptPath is null) throw new InvalidOperationException("build.py not found in bundled scripts or project scripts directory."); var actionArgs = new List { scriptPath, step.Action, "--project-root", projectRoot, }; actionArgs.AddRange(step.ActionArgs); return await ProcessRunner.RunAsync( PythonResolver.ResolveExecutable(), actionArgs, projectRoot, onOutput, cancellationToken: cancellationToken).ConfigureAwait(false); } if (string.IsNullOrWhiteSpace(step.Command)) return new RunResult(0, TimeSpan.Zero); var workingDir = Path.GetFullPath(Path.Combine(projectRoot, step.WorkingDir)); var pwshReroute = await TryRunLegacyPwshScriptViaPythonAsync( step, projectRoot, workingDir, onOutput, cancellationToken).ConfigureAwait(false); if (pwshReroute is not null) return pwshReroute; return await ProcessRunner.RunAsync( step.Command, step.Args, workingDir, onOutput, cancellationToken: cancellationToken).ConfigureAwait(false); } private static async Task TryRunLegacyPwshScriptViaPythonAsync( WorkflowStep step, string projectRoot, string workingDir, Action onOutput, CancellationToken cancellationToken) { if (!IsPowerShellCommand(step.Command)) return null; var args = step.Args; var fileIndex = FindArgIndex(args, "-File"); if (fileIndex < 0 || fileIndex + 1 >= args.Count) return null; var psScriptArg = args[fileIndex + 1]; if (!psScriptArg.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase)) return null; var pyScriptPath = ResolvePythonScriptPath(projectRoot, workingDir, psScriptArg); if (pyScriptPath is null) return null; var translated = TranslatePowerShellArgsToPython(args.Skip(fileIndex + 2)); var pythonArgs = new List { pyScriptPath }; pythonArgs.AddRange(translated); onOutput($"Legacy PowerShell target detected. Trying Python script first: {Path.GetFileName(pyScriptPath)}", false); var pyRun = await ProcessRunner.RunAsync( PythonResolver.ResolveExecutable(), pythonArgs, workingDir, onOutput, cancellationToken: cancellationToken).ConfigureAwait(false); if (pyRun.Success) return pyRun; var psScriptPath = ResolveScriptPath(workingDir, psScriptArg); if (psScriptPath is null || !File.Exists(psScriptPath)) return pyRun; onOutput( $"Python script failed (exit {pyRun.ExitCode}). Falling back to legacy PowerShell script: {psScriptArg}", true); return null; } private static string? ResolvePythonScriptPath(string projectRoot, string workingDir, string psScriptArg) { var pyArg = Path.ChangeExtension(psScriptArg, ".py"); var candidate = ResolveScriptPath(workingDir, pyArg); if (candidate is not null && File.Exists(candidate)) return candidate; var fileName = Path.GetFileName(pyArg); return ScriptLocator.FindHelperScript(projectRoot, fileName); } private static string? ResolveScriptPath(string workingDir, string scriptArg) { if (Path.IsPathRooted(scriptArg)) return scriptArg; return Path.GetFullPath(Path.Combine(workingDir, scriptArg)); } private static bool IsPowerShellCommand(string? command) { if (string.IsNullOrWhiteSpace(command)) return false; var normalized = Path.GetFileNameWithoutExtension(command).ToLowerInvariant(); return normalized is "pwsh" or "powershell"; } private static int FindArgIndex(IReadOnlyList args, string name) { for (var i = 0; i < args.Count; i++) { if (string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase)) return i; } return -1; } private static List TranslatePowerShellArgsToPython(IEnumerable inputArgs) { var result = new List(); var list = inputArgs.ToList(); for (var i = 0; i < list.Count; i++) { var token = list[i]; if (!token.StartsWith("-", StringComparison.Ordinal) || token == "-") { result.Add(token); continue; } var key = token.TrimStart('-'); if (key.Length == 0) continue; var mapped = MapPowerShellParameter(key); var nextIsValue = (i + 1) < list.Count && !list[i + 1].StartsWith("-", StringComparison.Ordinal); result.Add(mapped); if (nextIsValue) { result.Add(list[i + 1]); i++; } } return result; } private static string MapPowerShellParameter(string key) { return key.ToLowerInvariant() switch { "tauribundles" => "--tauri-bundles", "projectroot" => "--project-root", "reporoot" => "--repo-root", "outputzip" => "--output-zip", "inputzip" => "--input-zip", "workingdir" => "--working-dir", "outputdir" => "--output-dir", _ => "--" + ToKebabCase(key) }; } private static string ToKebabCase(string value) { if (string.IsNullOrWhiteSpace(value)) return value.ToLowerInvariant(); var chars = new List(value.Length + 4); for (var i = 0; i < value.Length; i++) { var c = value[i]; if (char.IsUpper(c) && i > 0 && value[i - 1] != '-') chars.Add('-'); chars.Add(char.ToLowerInvariant(c)); } return new string(chars.ToArray()); } }