using System.CommandLine; using System.CommandLine.Parsing; using System.Text.Json; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; namespace ZB.MOM.WW.ScadaBridge.CLI.Commands; /// /// The flow-debug command group (Flow Debugger design 2026-09-05 §7.1): a Live run of /// an inbound API method or a value-change trigger with every routed site run traced, and a /// Replay of a past execution chain from the central audit store. All three are Deployer /// commands answered by the node the CLI is pointed at; a standby central refuses the two /// run commands with STANDBY_NODE (Replay is served from any node). /// public static class FlowDebugCommands { /// Client-side wait for the run commands — must exceed the server's run grace + trigger window. internal static readonly TimeSpan RunTimeout = TimeSpan.FromMinutes(2); /// Builds the flow-debug command with its subcommands using the given shared CLI options. /// Shared management URL option. /// Shared output format option. /// Shared username option for authentication. /// Shared password option for authentication. /// The configured flow-debug command. public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var command = new Command("flow-debug") { Description = "Flow Debugger: trace an inbound API method or value-change flow end to end, or replay a past one" }; command.Add(BuildRunMethod(urlOption, formatOption, usernameOption, passwordOption)); command.Add(BuildRunValueChange(urlOption, formatOption, usernameOption, passwordOption)); command.Add(BuildReplay(urlOption, formatOption, usernameOption, passwordOption)); return command; } private static Command BuildRunMethod(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var methodIdOption = new Option("--method-id") { Description = "Inbound API method ID", Required = true }; var paramsOption = new Option("--params") { Description = "Flat JSON object of method parameters (validated against the method's schema)" }; var cmd = new Command("run-method") { Description = "Run an inbound API method on the active central node with full tracing and print the flow's steps" }; cmd.Add(methodIdOption); cmd.Add(paramsOption); cmd.SetAction(async (ParseResult result) => { JsonElement? parameters; try { parameters = ParseParameters(result.GetValue(paramsOption)); } catch (JsonException ex) { OutputFormatter.WriteError($"--params is not valid JSON: {ex.Message}", "INVALID_PARAMS"); return 1; } return await CommandHelpers.ExecuteCommandAsync( result, urlOption, formatOption, usernameOption, passwordOption, new FlowDebugRunMethodCommand(result.GetValue(methodIdOption), parameters), timeout: RunTimeout, tableProjector: FlowDebugTableProjection.ProjectEvents); }); return cmd; } private static Command BuildRunValueChange(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var instanceIdOption = new Option("--instance-id") { Description = "Instance ID", Required = true }; var attributeOption = new Option("--attribute") { Description = "Canonical attribute name to write", Required = true }; var valueOption = new Option("--value") { Description = "Value to write (canonical string form)", Required = true }; var waitOption = new Option("--wait-seconds") { Description = "How long to wait for a triggered script run before reporting NO_TRIGGER_WITHIN_WINDOW", DefaultValueFactory = _ => 45 }; var cmd = new Command("run-value-change") { Description = "Write an attribute through the traced path and capture the script run(s) the change triggers" }; cmd.Add(instanceIdOption); cmd.Add(attributeOption); cmd.Add(valueOption); cmd.Add(waitOption); cmd.SetAction(async (ParseResult result) => { var waitSeconds = result.GetValue(waitOption); if (waitSeconds <= 0) { OutputFormatter.WriteError("--wait-seconds must be greater than zero.", "INVALID_ARGUMENT"); return 1; } return await CommandHelpers.ExecuteCommandAsync( result, urlOption, formatOption, usernameOption, passwordOption, new FlowDebugRunValueChangeCommand( result.GetValue(instanceIdOption), result.GetValue(attributeOption)!, result.GetValue(valueOption)!, waitSeconds), timeout: RunTimeout, tableProjector: FlowDebugTableProjection.ProjectEvents); }); return cmd; } private static Command BuildReplay(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var executionIdOption = new Option("--execution-id") { Description = "Any execution ID in the chain (from an audit row)", Required = true }; var cmd = new Command("replay") { Description = "Replay a past flow from the central audit store" }; cmd.Add(executionIdOption); cmd.SetAction(async (ParseResult result) => { return await CommandHelpers.ExecuteCommandAsync( result, urlOption, formatOption, usernameOption, passwordOption, new FlowDebugReplayCommand(result.GetValue(executionIdOption)), tableProjector: FlowDebugTableProjection.ProjectEvents); }); return cmd; } /// /// Parses the --params value into a detached ; null/blank /// means "no parameters". Throws on malformed input or a /// non-object root (the inbound API body is always a flat JSON object). /// internal static JsonElement? ParseParameters(string? json) { if (string.IsNullOrWhiteSpace(json)) return null; using var doc = JsonDocument.Parse(json); if (doc.RootElement.ValueKind != JsonValueKind.Object) throw new JsonException("expected a JSON object at the root"); return doc.RootElement.Clone(); } }