using System.CommandLine;
using System.CommandLine.Parsing;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
public static class TemplateCommands
{
/// Builds the template 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 fully configured template command with all its subcommands.
public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var command = new Command("template") { Description = "Manage templates" };
command.Add(BuildList(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildGet(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUpdate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildValidate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildAttribute(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildAlarm(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildNativeAlarmSource(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildScript(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildComposition(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildResyncMembers(urlOption, formatOption, usernameOption, passwordOption));
return command;
}
private static Command BuildResyncMembers(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var idOption = new Option("--id") { Description = "Template ID (its derived subtree is included)", Required = true };
var cmd = new Command("resync-members")
{
Description = "Resync inherited members onto a template and its derived subtree " +
"(materialize missing, re-sync drifted, remove orphaned)"
};
cmd.Add(idOption);
cmd.SetAction(async (ParseResult result) =>
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption, new ResyncInheritedMembersCommand(id));
});
return cmd;
}
private static Command BuildList(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var detailOption = new Option("--detail")
{
Description = "Include full template definitions (all attributes/alarms/scripts) in table output. "
+ "Without it, table output is a compact summary (counts only). JSON output is always full."
};
var cmd = new Command("list") { Description = "List all templates (compact table summary; use --detail for the full dump)" };
cmd.Add(detailOption);
cmd.SetAction(async (ParseResult result) =>
{
var detail = result.GetValue(detailOption);
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption, new ListTemplatesCommand(),
tableProjector: detail ? null : TemplateTableProjection.ProjectSummary);
});
return cmd;
}
private static Command BuildGet(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var idOption = new Option("--id") { Description = "Template ID", Required = true };
var detailOption = new Option("--detail")
{
Description = "Include full template definitions (all attributes/alarms/scripts) in table output. "
+ "Without it, table output is a compact summary (counts only). JSON output is always full."
};
var cmd = new Command("get") { Description = "Get a template by ID (compact table summary; use --detail for the full dump)" };
cmd.Add(idOption);
cmd.Add(detailOption);
cmd.SetAction(async (ParseResult result) =>
{
var id = result.GetValue(idOption);
var detail = result.GetValue(detailOption);
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption, new GetTemplateCommand(id),
tableProjector: detail ? null : TemplateTableProjection.ProjectSummary);
});
return cmd;
}
private static Command BuildCreate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var nameOption = new Option("--name") { Description = "Template name", Required = true };
var descOption = new Option("--description") { Description = "Template description" };
var parentOption = new Option("--parent-id") { Description = "Parent template ID" };
var cmd = new Command("create") { Description = "Create a new template" };
cmd.Add(nameOption);
cmd.Add(descOption);
cmd.Add(parentOption);
cmd.SetAction(async (ParseResult result) =>
{
var name = result.GetValue(nameOption)!;
var desc = result.GetValue(descOption);
var parentId = result.GetValue(parentOption);
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new CreateTemplateCommand(name, desc, parentId));
});
return cmd;
}
private static Command BuildUpdate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var idOption = new Option("--id") { Description = "Template ID", Required = true };
var nameOption = new Option("--name") { Description = "Template name", Required = true };
var descOption = new Option("--description") { Description = "Template description. Omit to leave unchanged; pass an empty string (\"\") to clear it." };
var parentOption = new Option("--parent-id") { Description = "Parent template ID. Immutable after creation; omit to leave unchanged." };
var cmd = new Command("update") { Description = "Update a template (omitted optional fields are left unchanged)" };
cmd.Add(idOption);
cmd.Add(nameOption);
cmd.Add(descOption);
cmd.Add(parentOption);
cmd.SetAction(async (ParseResult result) =>
{
var id = result.GetValue(idOption);
var name = result.GetValue(nameOption)!;
var desc = result.GetValue(descOption);
var parentId = result.GetValue(parentOption);
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new UpdateTemplateCommand(id, name, desc, parentId));
});
return cmd;
}
private static Command BuildValidate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var idOption = new Option("--id") { Description = "Template ID", Required = true };
var cmd = new Command("validate") { Description = "Validate a template" };
cmd.Add(idOption);
cmd.SetAction(async (ParseResult result) =>
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption, new ValidateTemplateCommand(id));
});
return cmd;
}
private static Command BuildDelete(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var idOption = new Option("--id") { Description = "Template ID", Required = true };
var cmd = new Command("delete") { Description = "Delete a template" };
cmd.Add(idOption);
cmd.SetAction(async (ParseResult result) =>
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption, new DeleteTemplateCommand(id));
});
return cmd;
}
private static Command BuildAttribute(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var group = new Command("attribute") { Description = "Manage template attributes" };
var templateIdOption = new Option("--template-id") { Description = "Template ID", Required = true };
var nameOption = new Option("--name") { Description = "Attribute name", Required = true };
var dataTypeOption = new Option("--data-type") { Description = "Data type", Required = true };
var valueOption = new Option("--value") { Description = "Default value. For a List attribute, supply a JSON array in native form: numeric/boolean elements unquoted (e.g. an Int32 list '[10,20,30]'), string elements quoted (e.g. '[\"WO-1\",\"WO-2\"]')." };
var descOption = new Option("--description") { Description = "Description" };
var sourceOption = new Option("--data-source") { Description = "Data source reference" };
var elementTypeOption = new Option("--element-type") { Description = ElementTypeOptionDescription };
var lockedOption = new Option("--locked") { Description = "Lock status" };
lockedOption.DefaultValueFactory = _ => false;
var addCmd = new Command("add") { Description = "Add an attribute to a template" };
addCmd.Add(templateIdOption);
addCmd.Add(nameOption);
addCmd.Add(dataTypeOption);
addCmd.Add(valueOption);
addCmd.Add(descOption);
addCmd.Add(sourceOption);
addCmd.Add(elementTypeOption);
addCmd.Add(lockedOption);
addCmd.SetAction(async (ParseResult result) =>
{
var dataType = result.GetValue(dataTypeOption)!;
var elementType = result.GetValue(elementTypeOption);
if (!TryValidateElementType(dataType, elementType, out var error))
{
OutputFormatter.WriteError(error!, "INVALID_ARGUMENT");
return 1;
}
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
BuildAddAttributeCommand(
result.GetValue(templateIdOption),
result.GetValue(nameOption)!,
dataType,
result.GetValue(valueOption),
result.GetValue(descOption),
result.GetValue(sourceOption),
result.GetValue(lockedOption),
elementType));
});
group.Add(addCmd);
var updateIdOption = new Option("--id") { Description = "Attribute ID", Required = true };
var updateNameOption = new Option("--name") { Description = "Attribute name", Required = true };
var updateDataTypeOption = new Option("--data-type") { Description = "Data type", Required = true };
var updateValueOption = new Option("--value") { Description = "Default value. For a List attribute, supply a JSON array in native form: numeric/boolean elements unquoted (e.g. an Int32 list '[10,20,30]'), string elements quoted (e.g. '[\"WO-1\",\"WO-2\"]')." };
var updateDescOption = new Option("--description") { Description = "Description" };
var updateSourceOption = new Option("--data-source") { Description = "Data source reference" };
var updateElementTypeOption = new Option("--element-type") { Description = ElementTypeOptionDescription };
var updateLockedOption = new Option("--locked") { Description = "Lock status" };
updateLockedOption.DefaultValueFactory = _ => false;
var updateCmd = new Command("update") { Description = "Update a template attribute" };
updateCmd.Add(updateIdOption);
updateCmd.Add(updateNameOption);
updateCmd.Add(updateDataTypeOption);
updateCmd.Add(updateValueOption);
updateCmd.Add(updateDescOption);
updateCmd.Add(updateSourceOption);
updateCmd.Add(updateElementTypeOption);
updateCmd.Add(updateLockedOption);
updateCmd.SetAction(async (ParseResult result) =>
{
var dataType = result.GetValue(updateDataTypeOption)!;
var elementType = result.GetValue(updateElementTypeOption);
if (!TryValidateElementType(dataType, elementType, out var error))
{
OutputFormatter.WriteError(error!, "INVALID_ARGUMENT");
return 1;
}
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
BuildUpdateAttributeCommand(
result.GetValue(updateIdOption),
result.GetValue(updateNameOption)!,
dataType,
result.GetValue(updateValueOption),
result.GetValue(updateDescOption),
result.GetValue(updateSourceOption),
result.GetValue(updateLockedOption),
elementType));
});
group.Add(updateCmd);
var deleteIdOption = new Option("--id") { Description = "Attribute ID", Required = true };
var deleteCmd = new Command("delete") { Description = "Delete a template attribute" };
deleteCmd.Add(deleteIdOption);
deleteCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new DeleteTemplateAttributeCommand(result.GetValue(deleteIdOption)));
});
group.Add(deleteCmd);
return group;
}
/// Shared description for the --element-type option on attribute add/update.
internal const string ElementTypeOptionDescription =
"Element scalar type for a List attribute (String, Int32, Float, Double, Boolean, DateTime). Required when --data-type is List.";
/// Shared description for the --min-time-between-runs option on script add/update.
internal const string MinTimeBetweenRunsOptionDescription =
"Minimum time between script runs (a throttle for triggered scripts; the re-fire interval for a WhileTrue trigger). "
+ "Accepts a value with a unit suffix: ms, s/sec, or m/min (e.g. 500ms, 5s, 2min); a bare number is seconds. "
+ "Omit, or pass 0 / a blank value, to leave it unset (no throttle).";
/// Shared description for the --execution-timeout-seconds option on script add/update.
internal const string ExecutionTimeoutOptionDescription =
"Per-script execution timeout in seconds. Omit (or pass a non-positive value) to use the site's global "
+ "default (SiteRuntimeOptions.ScriptExecutionTimeoutSeconds).";
/// Shared description for the script --trigger-kind option on script add/update.
internal const string ScriptTriggerKindOptionDescription =
"Expression trigger analysis kind: advisory (default) or strict. "
+ "Strict escalates a blank expression from a warning to a deploy-blocking error. "
+ "Requires --trigger-config: with no config the kind is ignored (a warning is printed to stderr).";
/// Shared description for the alarm --trigger-kind option on alarm update.
internal const string AlarmTriggerKindOptionDescription =
"Expression trigger analysis kind: advisory (default) or strict. "
+ "Requires --trigger-config: with no config the kind is ignored (a warning is printed to stderr).";
///
/// The element scalar types permitted for a List attribute — derived from the
/// single source of truth, ,
/// so the CLI never drifts from the codec/Management API.
///
private static readonly string[] ValidElementScalars =
Enum.GetValues()
.Where(AttributeValueCodec.IsValidElementType)
.Select(t => t.ToString())
.ToArray();
///
/// Validates the --data-type / --element-type combination client-side so
/// the CLI fails fast with a clear message before contacting the Management API (the
/// server validates independently). A List attribute requires a valid element scalar;
/// a non-List attribute must not carry an element type. Comparison is case-insensitive.
///
/// The raw --data-type value.
/// The raw --element-type value, or null if absent.
/// A descriptive error message when validation fails; otherwise null.
/// true when the combination is valid; otherwise false.
internal static bool TryValidateElementType(string dataType, string? elementType, out string? error)
{
error = null;
var isList = string.Equals(dataType, "List", StringComparison.OrdinalIgnoreCase);
var hasElementType = !string.IsNullOrWhiteSpace(elementType);
if (isList)
{
if (!hasElementType)
{
error = "--element-type is required when --data-type is List "
+ "(one of: String, Int32, Float, Double, Boolean, DateTime).";
return false;
}
if (!Enum.TryParse(elementType!.Trim(), ignoreCase: true, out var parsed)
|| !AttributeValueCodec.IsValidElementType(parsed))
{
error = $"Invalid --element-type '{elementType}'. Valid List element scalars are: "
+ string.Join(", ", ValidElementScalars) + ".";
return false;
}
return true;
}
if (hasElementType)
{
error = "--element-type is only valid when --data-type is List.";
return false;
}
return true;
}
///
/// Builds the payload sent to the Management API.
/// The raw string is forwarded unchanged — for a List attribute it
/// is a JSON array, which the API/codec parses; the CLI does not reshape it.
///
internal static AddTemplateAttributeCommand BuildAddAttributeCommand(
int templateId, string name, string dataType, string? value,
string? description, string? dataSource, bool isLocked, string? elementType)
=> new(templateId, name, dataType, value, description, dataSource, isLocked, NormalizeElementType(elementType));
///
/// Builds the payload sent to the Management API.
/// The raw string is forwarded unchanged — for a List attribute it
/// is a JSON array, which the API/codec parses; the CLI does not reshape it.
///
internal static UpdateTemplateAttributeCommand BuildUpdateAttributeCommand(
int attributeId, string name, string dataType, string? value,
string? description, string? dataSource, bool isLocked, string? elementType)
=> new(attributeId, name, dataType, value, description, dataSource, isLocked, NormalizeElementType(elementType));
/// Trims a non-empty element type; an empty/whitespace value becomes null (no element type).
private static string? NormalizeElementType(string? elementType)
=> string.IsNullOrWhiteSpace(elementType) ? null : elementType.Trim();
///
/// Parses the raw --min-time-between-runs value into a .
/// A blank/absent value means "unset" ( null → null, no error).
/// Otherwise the value must be a positive integer with an optional unit suffix:
/// ms, s/sec, or m/min; a bare number is interpreted as
/// seconds (matching the UI duration input's default unit). A value of 0 resolves to
/// null (unset), mirroring DurationInput.Compose. The unit set and "bare = seconds"
/// default deliberately mirror the Central UI duration input so the two authoring surfaces agree.
///
/// The raw flag value, or null when the flag was omitted.
/// The parsed duration, or null for an absent/unset value.
/// A descriptive error message when parsing fails; otherwise null.
/// true when the value is absent or parses cleanly; false on a malformed value.
internal static bool TryParseMinTimeBetweenRuns(string? value, out TimeSpan? duration, out string? error)
{
duration = null;
error = null;
var trimmed = value?.Trim();
if (string.IsNullOrEmpty(trimmed))
return true; // omitted → unset
// Split a trailing alphabetic unit suffix from the leading numeric part.
var splitIndex = trimmed.Length;
while (splitIndex > 0 && char.IsLetter(trimmed[splitIndex - 1]))
splitIndex--;
var numberPart = trimmed[..splitIndex];
var unitPart = trimmed[splitIndex..].ToLowerInvariant();
if (!long.TryParse(numberPart, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var amount)
|| amount < 0)
{
error = $"Invalid --min-time-between-runs '{value}'. Expected a non-negative number with an optional "
+ "unit suffix (ms, s/sec, m/min); a bare number is seconds (e.g. 500ms, 5s, 2min).";
return false;
}
var factorMs = unitPart switch
{
"ms" => 1L,
"" or "s" or "sec" => 1000L,
"m" or "min" => 60000L,
_ => -1L,
};
if (factorMs < 0)
{
error = $"Invalid --min-time-between-runs unit in '{value}'. Valid units are: ms, s/sec, m/min.";
return false;
}
// Guard against overflow: reject if amount * factorMs would exceed TimeSpan.MaxValue (in ms).
// The divide is safe because factorMs is always >= 1.
if (amount > TimeSpan.MaxValue.TotalMilliseconds / factorMs)
{
error = $"Invalid --min-time-between-runs '{value}': value is too large.";
return false;
}
// 0 (with or without a unit) → unset, mirroring DurationInput.Compose's non-positive handling.
duration = amount == 0 ? null : TimeSpan.FromMilliseconds(amount * factorMs);
return true;
}
///
/// Determines whether --trigger-kind was supplied without a --trigger-config.
/// In that case has no JSON object to inject
/// into and silently drops the kind (#257) — the caller should warn the user.
///
/// The raw --trigger-config JSON value, or null when absent.
/// The raw --trigger-kind value, or null when absent.
/// true when a kind was given but no config — i.e. the kind will be ignored.
internal static bool TriggerKindWillBeIgnored(string? triggerConfig, string? triggerKind)
=> !string.IsNullOrWhiteSpace(triggerKind) && string.IsNullOrWhiteSpace(triggerConfig);
///
/// Warns (to stderr) when --trigger-kind is supplied without a --trigger-config
/// (see ). The command still proceeds — the kind is
/// advisory metadata, not a required field — but the user is told it had no effect so they are
/// not surprised that a strict request did not take. Warn-and-continue (rather than a hard
/// error) keeps the flag combination non-fatal: a script/alarm is still created, just without the
/// requested analysis kind.
///
/// The raw --trigger-config JSON value, or null when absent.
/// The raw --trigger-kind value, or null when absent.
private static void WarnIfTriggerKindIgnored(string? triggerConfig, string? triggerKind)
{
if (TriggerKindWillBeIgnored(triggerConfig, triggerKind))
{
Console.Error.WriteLine(
$"warning: --trigger-kind '{triggerKind!.Trim()}' was ignored because no --trigger-config was supplied. "
+ "The analysis kind is written into the trigger-config JSON, so it has no effect without one.");
}
}
private static Command BuildAlarm(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var group = new Command("alarm") { Description = "Manage template alarms" };
var templateIdOption = new Option("--template-id") { Description = "Template ID", Required = true };
var nameOption = new Option("--name") { Description = "Alarm name", Required = true };
var triggerTypeOption = new Option("--trigger-type") { Description = "Trigger type", Required = true };
var priorityOption = new Option("--priority") { Description = "Alarm priority", Required = true };
var descOption = new Option("--description") { Description = "Description" };
var triggerConfigOption = new Option("--trigger-config") { Description = "Trigger configuration JSON" };
var lockedOption = new Option("--locked") { Description = "Lock status" };
lockedOption.DefaultValueFactory = _ => false;
// Typed setpoint flags (alternative to raw --trigger-config; raw wins when both supplied).
var attributeOption = new Option("--attribute") { Description = "Attribute name the trigger watches (all trigger types except Expression)" };
var matchValueOption = new Option("--match-value") { Description = "ValueMatch: value to compare against" };
var notEqualsOption = new Option("--not-equals") { Description = "ValueMatch: match when the value is NOT equal (emits !=)" };
notEqualsOption.DefaultValueFactory = _ => false;
var minOption = new Option("--min") { Description = "RangeViolation: minimum allowed value" };
var maxOption = new Option("--max") { Description = "RangeViolation: maximum allowed value" };
var thresholdOption = new Option("--threshold-per-second") { Description = "RateOfChange: rate threshold per second" };
var windowOption = new Option("--window-seconds") { Description = "RateOfChange: sliding window in seconds" };
var directionOption = new Option("--direction") { Description = "RateOfChange: direction (rising|falling|either)" };
var loLoOption = new Option("--lolo") { Description = "HiLo: low-low setpoint" };
var loOption = new Option("--lo") { Description = "HiLo: low setpoint" };
var hiOption = new Option("--hi") { Description = "HiLo: high setpoint" };
var hiHiOption = new Option("--hihi") { Description = "HiLo: high-high setpoint" };
var expressionOption = new Option("--expression") { Description = "Expression: boolean trigger expression" };
// M9-T28b: analysis kind for Expression triggers (advisory|strict; default advisory).
// Writes "analysisKind":"Strict" into the trigger config when set to strict.
var triggerKindOption = new Option("--trigger-kind")
{
Description = "Expression trigger analysis kind: advisory (default) or strict. " +
"Strict escalates a blank expression from a warning to a deploy-blocking error."
};
var addCmd = new Command("add") { Description = "Add an alarm to a template" };
addCmd.Add(templateIdOption);
addCmd.Add(nameOption);
addCmd.Add(triggerTypeOption);
addCmd.Add(priorityOption);
addCmd.Add(descOption);
addCmd.Add(triggerConfigOption);
addCmd.Add(lockedOption);
addCmd.Add(attributeOption);
addCmd.Add(matchValueOption);
addCmd.Add(notEqualsOption);
addCmd.Add(minOption);
addCmd.Add(maxOption);
addCmd.Add(thresholdOption);
addCmd.Add(windowOption);
addCmd.Add(directionOption);
addCmd.Add(loLoOption);
addCmd.Add(loOption);
addCmd.Add(hiOption);
addCmd.Add(hiHiOption);
addCmd.Add(expressionOption);
addCmd.Add(triggerKindOption);
addCmd.SetAction(async (ParseResult result) =>
{
var triggerType = result.GetValue(triggerTypeOption)!;
var rawConfig = result.GetValue(triggerConfigOption);
var triggerConfig = rawConfig ?? AlarmTriggerConfigJson.Build(
triggerType,
result.GetValue(attributeOption),
result.GetValue(matchValueOption), result.GetValue(notEqualsOption),
result.GetValue(minOption), result.GetValue(maxOption),
result.GetValue(thresholdOption), result.GetValue(windowOption), result.GetValue(directionOption),
result.GetValue(loLoOption), result.GetValue(loOption), result.GetValue(hiOption), result.GetValue(hiHiOption),
result.GetValue(expressionOption),
result.GetValue(triggerKindOption));
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new AddTemplateAlarmCommand(
result.GetValue(templateIdOption),
result.GetValue(nameOption)!,
triggerType,
result.GetValue(priorityOption)!,
result.GetValue(descOption),
triggerConfig,
result.GetValue(lockedOption)));
});
group.Add(addCmd);
var updateIdOption = new Option("--id") { Description = "Alarm ID", Required = true };
var updateNameOption = new Option("--name") { Description = "Alarm name", Required = true };
var updateTriggerTypeOption = new Option("--trigger-type") { Description = "Trigger type", Required = true };
var updatePriorityOption = new Option("--priority") { Description = "Alarm priority", Required = true };
var updateDescOption = new Option("--description") { Description = "Description" };
var updateTriggerConfigOption = new Option("--trigger-config") { Description = "Trigger configuration JSON" };
var updateLockedOption = new Option("--locked") { Description = "Lock status" };
updateLockedOption.DefaultValueFactory = _ => false;
// M9-T28b: --trigger-kind for update (same semantics as add)
var updateTriggerKindOption = new Option("--trigger-kind")
{
Description = AlarmTriggerKindOptionDescription
};
var updateCmd = new Command("update") { Description = "Update a template alarm" };
updateCmd.Add(updateIdOption);
updateCmd.Add(updateNameOption);
updateCmd.Add(updateTriggerTypeOption);
updateCmd.Add(updatePriorityOption);
updateCmd.Add(updateDescOption);
updateCmd.Add(updateTriggerConfigOption);
updateCmd.Add(updateLockedOption);
updateCmd.Add(updateTriggerKindOption);
updateCmd.SetAction(async (ParseResult result) =>
{
var rawConfig = result.GetValue(updateTriggerConfigOption);
WarnIfTriggerKindIgnored(rawConfig, result.GetValue(updateTriggerKindOption));
var triggerConfig = TriggerConfigJson.InjectAnalysisKind(
rawConfig,
result.GetValue(updateTriggerKindOption));
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new UpdateTemplateAlarmCommand(
result.GetValue(updateIdOption),
result.GetValue(updateNameOption)!,
result.GetValue(updateTriggerTypeOption)!,
result.GetValue(updatePriorityOption)!,
result.GetValue(updateDescOption),
triggerConfig,
result.GetValue(updateLockedOption)));
});
group.Add(updateCmd);
var deleteIdOption = new Option("--id") { Description = "Alarm ID", Required = true };
var deleteCmd = new Command("delete") { Description = "Delete a template alarm" };
deleteCmd.Add(deleteIdOption);
deleteCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new DeleteTemplateAlarmCommand(result.GetValue(deleteIdOption)));
});
group.Add(deleteCmd);
return group;
}
private static Command BuildNativeAlarmSource(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var group = new Command("native-alarm-source")
{
Description = "Manage template native alarm source bindings (read-only mirror of OPC UA A&C / MxGateway alarms)"
};
// add
var templateIdOption = new Option("--template-id") { Description = "Template ID", Required = true };
var nameOption = new Option("--name") { Description = "Source binding name", Required = true };
var connectionOption = new Option("--connection") { Description = "Alarm-capable data connection name", Required = true };
var sourceRefOption = new Option("--source-ref") { Description = "Source reference (OPC UA SourceNode nodeId, or MxAccess object/area)", Required = true };
var filterOption = new Option("--filter") { Description = "Optional condition filter (null = mirror all conditions under the source)" };
var descOption = new Option("--description") { Description = "Description" };
var lockedOption = new Option("--locked") { Description = "Lock status" };
lockedOption.DefaultValueFactory = _ => false;
var addCmd = new Command("add") { Description = "Add a native alarm source binding to a template" };
addCmd.Add(templateIdOption);
addCmd.Add(nameOption);
addCmd.Add(connectionOption);
addCmd.Add(sourceRefOption);
addCmd.Add(filterOption);
addCmd.Add(descOption);
addCmd.Add(lockedOption);
addCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new AddTemplateNativeAlarmSourceCommand(
result.GetValue(templateIdOption),
result.GetValue(nameOption)!,
result.GetValue(connectionOption)!,
result.GetValue(sourceRefOption)!,
result.GetValue(filterOption),
result.GetValue(descOption),
result.GetValue(lockedOption)));
});
group.Add(addCmd);
// list
var listIdOption = new Option("--template-id") { Description = "Template ID", Required = true };
var listCmd = new Command("list") { Description = "List native alarm source bindings on a template" };
listCmd.Add(listIdOption);
listCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new ListTemplateNativeAlarmSourcesCommand(result.GetValue(listIdOption)));
});
group.Add(listCmd);
// remove
var removeIdOption = new Option("--id") { Description = "Native alarm source ID", Required = true };
var removeCmd = new Command("remove") { Description = "Remove a native alarm source binding from a template" };
removeCmd.Add(removeIdOption);
removeCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new DeleteTemplateNativeAlarmSourceCommand(result.GetValue(removeIdOption)));
});
group.Add(removeCmd);
return group;
}
private static Command BuildScript(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var group = new Command("script") { Description = "Manage template scripts" };
var templateIdOption = new Option("--template-id") { Description = "Template ID", Required = true };
var nameOption = new Option("--name") { Description = "Script name", Required = true };
var codeOption = new Option("--code") { Description = "Script code", Required = true };
var triggerTypeOption = new Option("--trigger-type") { Description = "Trigger type", Required = true };
var triggerConfigOption = new Option("--trigger-config") { Description = "Trigger configuration JSON" };
var lockedOption = new Option("--locked") { Description = "Lock status" };
lockedOption.DefaultValueFactory = _ => false;
var paramsOption = new Option("--parameters") { Description = "Parameter definitions JSON" };
var returnOption = new Option("--return-def") { Description = "Return definition JSON" };
var minTimeOption = new Option("--min-time-between-runs") { Description = MinTimeBetweenRunsOptionDescription };
var execTimeoutOption = new Option("--execution-timeout-seconds") { Description = ExecutionTimeoutOptionDescription };
// M9-T28b: analysis kind for Expression triggers (advisory|strict; default advisory).
var scriptTriggerKindOption = new Option("--trigger-kind")
{
Description = ScriptTriggerKindOptionDescription
};
var addCmd = new Command("add") { Description = "Add a script to a template" };
addCmd.Add(templateIdOption);
addCmd.Add(nameOption);
addCmd.Add(codeOption);
addCmd.Add(triggerTypeOption);
addCmd.Add(triggerConfigOption);
addCmd.Add(lockedOption);
addCmd.Add(paramsOption);
addCmd.Add(returnOption);
addCmd.Add(minTimeOption);
addCmd.Add(execTimeoutOption);
addCmd.Add(scriptTriggerKindOption);
addCmd.SetAction(async (ParseResult result) =>
{
if (!TryParseMinTimeBetweenRuns(result.GetValue(minTimeOption), out var minTime, out var minTimeError))
{
OutputFormatter.WriteError(minTimeError!, "INVALID_ARGUMENT");
return 1;
}
var rawConfig = result.GetValue(triggerConfigOption);
WarnIfTriggerKindIgnored(rawConfig, result.GetValue(scriptTriggerKindOption));
var triggerConfig = TriggerConfigJson.InjectAnalysisKind(
rawConfig,
result.GetValue(scriptTriggerKindOption));
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new AddTemplateScriptCommand(
result.GetValue(templateIdOption),
result.GetValue(nameOption)!,
result.GetValue(codeOption)!,
result.GetValue(triggerTypeOption)!,
triggerConfig,
result.GetValue(lockedOption),
result.GetValue(paramsOption),
result.GetValue(returnOption),
minTime,
result.GetValue(execTimeoutOption)));
});
group.Add(addCmd);
var updateIdOption = new Option("--id") { Description = "Script ID", Required = true };
var updateNameOption = new Option("--name") { Description = "Script name", Required = true };
var updateCodeOption = new Option("--code") { Description = "Script code", Required = true };
var updateTriggerTypeOption = new Option("--trigger-type") { Description = "Trigger type", Required = true };
var updateTriggerConfigOption = new Option("--trigger-config") { Description = "Trigger configuration JSON" };
var updateLockedOption = new Option("--locked") { Description = "Lock status" };
updateLockedOption.DefaultValueFactory = _ => false;
var updateParamsOption = new Option("--parameters") { Description = "Parameter definitions JSON" };
var updateReturnOption = new Option("--return-def") { Description = "Return definition JSON" };
var updateMinTimeOption = new Option("--min-time-between-runs") { Description = MinTimeBetweenRunsOptionDescription };
var updateExecTimeoutOption = new Option("--execution-timeout-seconds") { Description = ExecutionTimeoutOptionDescription };
// M9-T28b: --trigger-kind for update (same semantics as add)
var updateScriptTriggerKindOption = new Option("--trigger-kind")
{
Description = ScriptTriggerKindOptionDescription
};
var updateCmd = new Command("update") { Description = "Update a template script" };
updateCmd.Add(updateIdOption);
updateCmd.Add(updateNameOption);
updateCmd.Add(updateCodeOption);
updateCmd.Add(updateTriggerTypeOption);
updateCmd.Add(updateTriggerConfigOption);
updateCmd.Add(updateLockedOption);
updateCmd.Add(updateParamsOption);
updateCmd.Add(updateReturnOption);
updateCmd.Add(updateMinTimeOption);
updateCmd.Add(updateExecTimeoutOption);
updateCmd.Add(updateScriptTriggerKindOption);
updateCmd.SetAction(async (ParseResult result) =>
{
if (!TryParseMinTimeBetweenRuns(result.GetValue(updateMinTimeOption), out var minTime, out var minTimeError))
{
OutputFormatter.WriteError(minTimeError!, "INVALID_ARGUMENT");
return 1;
}
var rawConfig = result.GetValue(updateTriggerConfigOption);
WarnIfTriggerKindIgnored(rawConfig, result.GetValue(updateScriptTriggerKindOption));
var triggerConfig = TriggerConfigJson.InjectAnalysisKind(
rawConfig,
result.GetValue(updateScriptTriggerKindOption));
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new UpdateTemplateScriptCommand(
result.GetValue(updateIdOption),
result.GetValue(updateNameOption)!,
result.GetValue(updateCodeOption)!,
result.GetValue(updateTriggerTypeOption)!,
triggerConfig,
result.GetValue(updateLockedOption),
result.GetValue(updateParamsOption),
result.GetValue(updateReturnOption),
minTime,
result.GetValue(updateExecTimeoutOption)));
});
group.Add(updateCmd);
var deleteIdOption = new Option("--id") { Description = "Script ID", Required = true };
var deleteCmd = new Command("delete") { Description = "Delete a template script" };
deleteCmd.Add(deleteIdOption);
deleteCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new DeleteTemplateScriptCommand(result.GetValue(deleteIdOption)));
});
group.Add(deleteCmd);
return group;
}
private static Command BuildComposition(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption)
{
var group = new Command("composition") { Description = "Manage template compositions" };
var templateIdOption = new Option("--template-id") { Description = "Template ID", Required = true };
var instanceNameOption = new Option("--instance-name") { Description = "Composed instance name", Required = true };
var composedTemplateIdOption = new Option("--composed-template-id") { Description = "Composed template ID", Required = true };
var addCmd = new Command("add") { Description = "Add a composition to a template" };
addCmd.Add(templateIdOption);
addCmd.Add(instanceNameOption);
addCmd.Add(composedTemplateIdOption);
addCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new AddTemplateCompositionCommand(
result.GetValue(templateIdOption),
result.GetValue(instanceNameOption)!,
result.GetValue(composedTemplateIdOption)));
});
group.Add(addCmd);
var deleteIdOption = new Option("--id") { Description = "Composition ID", Required = true };
var deleteCmd = new Command("delete") { Description = "Delete a template composition" };
deleteCmd.Add(deleteIdOption);
deleteCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new DeleteTemplateCompositionCommand(result.GetValue(deleteIdOption)));
});
group.Add(deleteCmd);
return group;
}
}