journal/Journal.DevTool/Core/ActionRunner.cs

208 lines
6.6 KiB
C#

using Sdt.Config;
using Sdt.Runner;
namespace Sdt.Core;
public sealed class ActionRunner : IActionRunner
{
public async Task<RunResult> RunStepAsync(
WorkflowStep step,
string projectRoot,
Action<string, bool> 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<string>
{
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<RunResult?> TryRunLegacyPwshScriptViaPythonAsync(
WorkflowStep step,
string projectRoot,
string workingDir,
Action<string, bool> 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<string> { 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<string> 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<string> TranslatePowerShellArgsToPython(IEnumerable<string> inputArgs)
{
var result = new List<string>();
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<char>(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());
}
}