Skip to content

Plan: CLI Restructure — Command Groups, JMESPath --query, --output Format#

Context#

The Orchestrator CLI has 9 flat commands with inconsistent categorization. All output is Spectre.Console tables only — no machine-readable format for scripting. The user wants Azure CLI-style organization with JMESPath query support and output format options.


Step 1: Add JMESPath NuGet package#

File: src/BenefitManager.Orchestrator/BenefitManager.Orchestrator.csproj

<PackageReference Include="JmesPath.Net" Version="1.1.0" />
  • [x] Package added (JmesPath.Net v1.1.0 — DevLab.JmesPath namespace)

Step 2: Create QuerySettings and ManageSettings base classes#

New file: src/BenefitManager.Orchestrator/Commands/QuerySettings.cs

public enum OutputFormat { Table, Json, Md }

public class QuerySettings : CommandSettings
{
    [CommandOption("--output|-o")]
    [Description("Output format: table, json, or md (default: table)")]
    [DefaultValue(OutputFormat.Table)]
    public OutputFormat Output { get; init; } = OutputFormat.Table;

    [CommandOption("--query")]
    [Description("JMESPath expression to filter/reshape output (uses camelCase property names)")]
    public string? Query { get; init; }
}

New file: src/BenefitManager.Orchestrator/Commands/ManageSettings.cs

public class ManageSettings : CommandSettings { }
  • [x] QuerySettings created with --output and --query
  • [x] ManageSettings created as empty marker

Step 3: Create OutputFormatter#

New file: src/BenefitManager.Orchestrator/Infrastructure/OutputFormatter.cs

Static helper. Flow: 1. Serialize IReadOnlyList<T> to JSON (camelCase, JsonStringEnumConverter) 2. If --query set, apply JMESPath via new JmesPath().Transform(json, query) 3. Render based on --output: - json → pretty-print to TextWriter output (defaults to Console.Out) - table → parse JsonElement, auto-detect columns from first object's properties, build Spectre.Console.Table - md → pipe-delimited markdown table to TextWriter output

Key signatures:

public static class OutputFormatter
{
    public static void Render<T>(IReadOnlyList<T> data, OutputFormat format, string? query = null, IAnsiConsole? console = null, TextWriter? output = null);
}

IAnsiConsole and TextWriter parameters enable test injection.

Dynamic table rendering handles JMESPath-reshaped data (columns from property names of first JSON object). Booleans render as / in table/md modes. Empty collections print "No results." via the console. Invalid JMESPath is caught and printed as an error.

  • [x] OutputFormatter created with json/table/md rendering
  • [x] JMESPath integration working
  • [x] Dynamic column detection from JsonElement

Step 4: Reparent Settings classes#

Query commands — base changed to QuerySettings: - CustomersSettings in CustomersCommand.cs - StagesSettings in StagesCommand.cs - VersionsSettings in VersionsCommand.cs - StatusSettings in StatusCommand.cs

Manage commands — base changed to ManageSettings: - RemoveSettings in RemoveCommand.cs - ServiceSettings in ServiceCommand.cs - IisSettings in IisCommand.cs - StageRemoveSettings in StageRemoveCommand.cs

  • [x] Query settings reparented
  • [x] Manage settings reparented

Step 5: Refactor query commands to use OutputFormatter#

Replace manual Table construction with OutputFormatter.Render(data, settings.Output, settings.Query).

CustomersCommand, StagesCommand, VersionsCommand — each reduced to ~5 lines.

StatusCommand — special case: - Dual path: legacy per-server grouped visual for format=table && query=null, OutputFormatter otherwise. - ServerStatusResult record added to ManagementModels.cs:

public record ServerStatusResult(string Server, List<InstallationSummary> Installations, string? Error);
  • [x] CustomersCommand uses OutputFormatter
  • [x] StagesCommand uses OutputFormatter
  • [x] VersionsCommand uses OutputFormatter
  • [x] StatusCommand refactored with ServerStatusResult + dual rendering path

Step 6: Restructure Program.cs with AddBranch#

app.Configure(cli =>
{
    cli.AddCommand<DeployCommand>("deploy")...

    cli.AddBranch<QuerySettings>("query", query =>
    {
        query.SetDescription("Query data from environments and agents.");
        query.AddCommand<CustomersCommand>("customers")...
        query.AddCommand<StagesCommand>("stages")...
        query.AddCommand<VersionsCommand>("versions")...
        query.AddCommand<StatusCommand>("status")...
    });

    cli.AddBranch<ManageSettings>("manage", manage =>
    {
        manage.SetDescription("Manage deployments, services, IIS pools, and stages.");
        manage.AddCommand<RemoveCommand>("remove")...
        manage.AddCommand<ServiceCommand>("service")...
        manage.AddCommand<IisCommand>("iis")...
        manage.AddCommand<StageRemoveCommand>("remove-stage")...
    });
});
  • [x] query branch with 4 subcommands
  • [x] manage branch with 4 subcommands
  • [x] deploy at root
  • [x] Examples updated with group prefix

Step 7: Tests#

New file: tests/BenefitManager.Orchestrator.Tests/OutputFormatterTests.cs

  • [x] Render json: verify camelCase output
  • [x] Render table: verify columns detected from record properties
  • [x] Render md: verify pipe-delimited markdown with header separator
  • [x] JMESPath filter: [?stageCount > \2`]` on CustomerSummary list
  • [x] JMESPath projection: [].{n: name} reshapes columns
  • [x] Empty collection: "No results." message
  • [x] Invalid JMESPath: error message, no exception
  • [x] Existing 27 tests still pass (34 total pass)

Files Summary#

Action File
Modify src/BenefitManager.Orchestrator/BenefitManager.Orchestrator.csproj
Modify src/BenefitManager.Orchestrator/Program.cs
Modify src/BenefitManager.Orchestrator/Commands/CustomersCommand.cs
Modify src/BenefitManager.Orchestrator/Commands/StagesCommand.cs
Modify src/BenefitManager.Orchestrator/Commands/VersionsCommand.cs
Modify src/BenefitManager.Orchestrator/Commands/StatusCommand.cs
Modify src/BenefitManager.Orchestrator/Commands/RemoveCommand.cs
Modify src/BenefitManager.Orchestrator/Commands/ServiceCommand.cs
Modify src/BenefitManager.Orchestrator/Commands/IisCommand.cs
Modify src/BenefitManager.Orchestrator/Commands/StageRemoveCommand.cs
Modify src/BenefitManager.Contracts/Models/ManagementModels.cs
New src/BenefitManager.Orchestrator/Commands/QuerySettings.cs
New src/BenefitManager.Orchestrator/Commands/ManageSettings.cs
New src/BenefitManager.Orchestrator/Infrastructure/OutputFormatter.cs
New tests/BenefitManager.Orchestrator.Tests/OutputFormatterTests.cs