using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using GitHub.Runner.Common;
namespace GitHub.Runner.Worker.Dap.StepCommands
{
///
/// Output format for step command responses.
///
public enum OutputFormat
{
/// Human-readable text output (default)
Text,
/// JSON output for programmatic use
Json
}
///
/// Interface for parsing step commands from REPL strings.
///
[ServiceLocator(Default = typeof(StepCommandParser))]
public interface IStepCommandParser : IRunnerService
{
///
/// Parses a command string into a structured StepCommand.
///
/// The input string (e.g., "steps list --verbose")
/// Parsed StepCommand
/// If parsing fails
StepCommand Parse(string input);
///
/// Checks if the input is a step command.
///
/// The input string to check
/// True if this is a step command (starts with "steps")
bool IsStepCommand(string input);
}
#region Command Classes
///
/// Base class for all step commands.
///
public abstract class StepCommand
{
///
/// Output format for the command response.
///
public OutputFormat Output { get; set; } = OutputFormat.Text;
}
///
/// steps list [--verbose]
///
public class ListCommand : StepCommand
{
public bool Verbose { get; set; }
}
///
/// steps add run "script" [options]
///
public class AddRunCommand : StepCommand
{
public string Script { get; set; }
public string Name { get; set; }
public string Shell { get; set; }
public string WorkingDirectory { get; set; }
public Dictionary Env { get; set; }
public string Condition { get; set; }
public bool ContinueOnError { get; set; }
public int? Timeout { get; set; }
public StepPosition Position { get; set; } = StepPosition.Last();
}
///
/// steps add uses "action@ref" [options]
///
public class AddUsesCommand : StepCommand
{
public string Action { get; set; }
public string Name { get; set; }
public Dictionary With { get; set; }
public Dictionary Env { get; set; }
public string Condition { get; set; }
public bool ContinueOnError { get; set; }
public int? Timeout { get; set; }
public StepPosition Position { get; set; } = StepPosition.Last();
}
///
/// steps edit <index> [modifications]
///
public class EditCommand : StepCommand
{
public int Index { get; set; }
public string Name { get; set; }
public string Script { get; set; }
public string Action { get; set; }
public string Shell { get; set; }
public string WorkingDirectory { get; set; }
public string Condition { get; set; }
public Dictionary With { get; set; }
public Dictionary Env { get; set; }
public List RemoveWith { get; set; }
public List RemoveEnv { get; set; }
public bool? ContinueOnError { get; set; }
public int? Timeout { get; set; }
}
///
/// steps remove <index>
///
public class RemoveCommand : StepCommand
{
public int Index { get; set; }
}
///
/// steps move <from> [position options]
///
public class MoveCommand : StepCommand
{
public int FromIndex { get; set; }
public StepPosition Position { get; set; }
}
///
/// steps export [--changes-only] [--with-comments]
///
public class ExportCommand : StepCommand
{
public bool ChangesOnly { get; set; }
public bool WithComments { get; set; }
}
#endregion
#region Position Types
///
/// Types of position specifications for inserting/moving steps.
///
public enum PositionType
{
/// Insert at specific index (1-based)
At,
/// Insert after specific index (1-based)
After,
/// Insert before specific index (1-based)
Before,
/// Insert at first pending position
First,
/// Insert at end (default)
Last
}
///
/// Represents a position for inserting or moving steps.
///
public class StepPosition
{
public PositionType Type { get; set; }
public int? Index { get; set; }
public static StepPosition At(int index) => new StepPosition { Type = PositionType.At, Index = index };
public static StepPosition After(int index) => new StepPosition { Type = PositionType.After, Index = index };
public static StepPosition Before(int index) => new StepPosition { Type = PositionType.Before, Index = index };
public static StepPosition First() => new StepPosition { Type = PositionType.First };
public static StepPosition Last() => new StepPosition { Type = PositionType.Last };
public override string ToString()
{
return Type switch
{
PositionType.At => $"at {Index}",
PositionType.After => $"after {Index}",
PositionType.Before => $"before {Index}",
PositionType.First => "first",
PositionType.Last => "last",
_ => "unknown"
};
}
}
#endregion
///
/// Parser implementation for step commands.
///
public sealed class StepCommandParser : RunnerService, IStepCommandParser
{
// Regex to match quoted strings (handles escaped quotes)
private static readonly Regex QuotedStringRegex = new Regex(
@"""(?:[^""\\]|\\.)*""|'(?:[^'\\]|\\.)*'",
RegexOptions.Compiled);
public bool IsStepCommand(string input)
{
if (string.IsNullOrWhiteSpace(input))
return false;
var trimmed = input.Trim();
// Command format: steps ...
if (trimmed.StartsWith("steps ", StringComparison.OrdinalIgnoreCase) ||
trimmed.Equals("steps", StringComparison.OrdinalIgnoreCase))
return true;
return false;
}
public StepCommand Parse(string input)
{
var trimmed = input?.Trim() ?? "";
return ParseReplCommand(trimmed);
}
#region REPL Parsing
private StepCommand ParseReplCommand(string input)
{
// Tokenize the input, respecting quoted strings
var tokens = Tokenize(input);
if (tokens.Count < 2 || !tokens[0].Equals("steps", StringComparison.OrdinalIgnoreCase))
{
throw new StepCommandException(StepCommandErrors.ParseError,
"Invalid command format. Expected: steps [args...]");
}
var subCommand = tokens[1].ToLower();
return subCommand switch
{
"list" => ParseReplListCommand(tokens),
"add" => ParseReplAddCommand(tokens),
"edit" => ParseReplEditCommand(tokens),
"remove" => ParseReplRemoveCommand(tokens),
"move" => ParseReplMoveCommand(tokens),
"export" => ParseReplExportCommand(tokens),
_ => throw new StepCommandException(StepCommandErrors.InvalidCommand, $"Unknown sub-command: {subCommand}")
};
}
private List Tokenize(string input)
{
var tokens = new List();
var remaining = input;
while (!string.IsNullOrEmpty(remaining))
{
remaining = remaining.TrimStart();
if (string.IsNullOrEmpty(remaining))
break;
// Check for quoted string
var match = QuotedStringRegex.Match(remaining);
if (match.Success && match.Index == 0)
{
// Extract the quoted content (without quotes)
var quoted = match.Value;
var content = quoted.Substring(1, quoted.Length - 2);
// Unescape
content = content.Replace("\\\"", "\"").Replace("\\'", "'").Replace("\\\\", "\\");
tokens.Add(content);
remaining = remaining.Substring(match.Length);
}
else
{
// Non-quoted token
var spaceIndex = remaining.IndexOfAny(new[] { ' ', '\t' });
if (spaceIndex == -1)
{
tokens.Add(remaining);
break;
}
tokens.Add(remaining.Substring(0, spaceIndex));
remaining = remaining.Substring(spaceIndex);
}
}
return tokens;
}
private ListCommand ParseReplListCommand(List tokens)
{
// Extract --output flag before processing other options
var outputFormat = ExtractOutputFlag(tokens);
var cmd = new ListCommand { Output = outputFormat };
for (int i = 2; i < tokens.Count; i++)
{
var token = tokens[i].ToLower();
if (token == "--verbose" || token == "-v")
{
cmd.Verbose = true;
}
else
{
throw new StepCommandException(StepCommandErrors.InvalidOption,
$"Unknown option for list: {tokens[i]}");
}
}
return cmd;
}
private StepCommand ParseReplAddCommand(List tokens)
{
// Extract --output flag before processing other options
var outputFormat = ExtractOutputFlag(tokens);
if (tokens.Count < 3)
{
throw new StepCommandException(StepCommandErrors.ParseError,
"Usage: steps add