using System.CommandLine; using System.CommandLine.Parsing; using System.Text.Json; using ScadaLink.Commons.Messages.Management; namespace ScadaLink.CLI.Commands; internal static class CommandHelpers { internal static async Task ExecuteCommandAsync( ParseResult result, Option urlOption, Option formatOption, Option usernameOption, Option passwordOption, object command) { var config = CliConfig.Load(); var format = ResolveFormat(result, formatOption, config); // Resolve management URL var url = result.GetValue(urlOption); if (string.IsNullOrWhiteSpace(url)) url = config.ManagementUrl; if (string.IsNullOrWhiteSpace(url)) { OutputFormatter.WriteError( "No management URL specified. Use --url, set SCADALINK_MANAGEMENT_URL, or add 'managementUrl' to ~/.scadalink/config.json.", "NO_URL"); return 1; } if (!IsValidManagementUrl(url)) { OutputFormatter.WriteError( $"Invalid management URL '{url}'. Expected an absolute http/https URL (e.g. http://localhost:9001).", "INVALID_URL"); return 1; } // Resolve credentials: command-line options take precedence, then the // SCADALINK_USERNAME / SCADALINK_PASSWORD environment variables. var username = ResolveCredential(result.GetValue(usernameOption), config.Username); var password = ResolveCredential(result.GetValue(passwordOption), config.Password); if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) { OutputFormatter.WriteError( "Credentials required. Use --username/--password or set SCADALINK_USERNAME/SCADALINK_PASSWORD.", "NO_CREDENTIALS"); return 1; } // Derive command name from type var commandName = ManagementCommandRegistry.GetCommandName(command.GetType()); // Send via HTTP using var client = new ManagementHttpClient(url, username, password); var response = await client.SendCommandAsync(commandName, command, TimeSpan.FromSeconds(30)); return HandleResponse(response, format); } /// /// Resolves the output format using the documented precedence chain: /// an explicitly supplied --format option wins, otherwise the /// config-file / environment-variable default () /// is used, otherwise json. The --format option must not declare a /// DefaultValueFactory — that would mask whether the flag was supplied. /// internal static string ResolveFormat(ParseResult result, Option formatOption, CliConfig config) { // GetResult returns non-null only when the option was actually present on the // command line, letting an explicit --format override the config default. if (result.GetResult(formatOption) != null) { var explicitValue = result.GetValue(formatOption); if (!string.IsNullOrWhiteSpace(explicitValue)) return explicitValue; } return string.IsNullOrWhiteSpace(config.DefaultFormat) ? "json" : config.DefaultFormat; } /// /// Resolves a single credential: an explicit command-line value wins, otherwise the /// environment-variable fallback (from ) is used. /// internal static string? ResolveCredential(string? commandLineValue, string? envValue) => string.IsNullOrWhiteSpace(commandLineValue) ? envValue : commandLineValue; /// /// Validates that a management URL is an absolute http/https URL. A malformed URL /// (missing scheme, empty, or a non-http scheme) would otherwise reach /// new Uri(...) in the constructor and throw /// an unhandled . /// internal static bool IsValidManagementUrl(string? url) { if (string.IsNullOrWhiteSpace(url)) return false; return Uri.TryCreate(url, UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps); } internal static int HandleResponse(ManagementResponse response, string format) { if (response.JsonData != null) { // A success status with an empty/whitespace body (e.g. a 204 from a delete) // is a "command succeeded, no output" case — do not attempt to parse it. if (string.IsNullOrWhiteSpace(response.JsonData)) { Console.WriteLine("(ok)"); return 0; } if (string.Equals(format, "table", StringComparison.OrdinalIgnoreCase)) { WriteAsTable(response.JsonData); } else { Console.WriteLine(response.JsonData); } return 0; } var errorCode = response.ErrorCode ?? "ERROR"; var error = response.Error ?? "Unknown error"; OutputFormatter.WriteError(error, errorCode); return response.StatusCode == 403 ? 2 : 1; } private static void WriteAsTable(string json) { JsonDocument doc; try { doc = JsonDocument.Parse(json); } catch (JsonException) { // The server returned a success status but a non-JSON body (e.g. a proxy // HTML error page, or a plain-text message). Print it verbatim rather than // crashing — mirrors the raw-body fallback on the JSON path. Console.WriteLine(json); return; } using (doc) { var root = doc.RootElement; if (root.ValueKind == JsonValueKind.Array) { var items = root.EnumerateArray().ToList(); if (items.Count == 0) { Console.WriteLine("(no results)"); return; } var headers = items[0].ValueKind == JsonValueKind.Object ? items[0].EnumerateObject().Select(p => p.Name).ToArray() : new[] { "Value" }; var rows = items.Select(item => { if (item.ValueKind == JsonValueKind.Object) { return headers.Select(h => item.TryGetProperty(h, out var val) ? val.ValueKind == JsonValueKind.Null ? "" : val.ToString() : "").ToArray(); } return new[] { item.ToString() }; }); OutputFormatter.WriteTable(rows, headers); } else if (root.ValueKind == JsonValueKind.Object) { var headers = new[] { "Property", "Value" }; var rows = root.EnumerateObject().Select(p => new[] { p.Name, p.Value.ValueKind == JsonValueKind.Null ? "" : p.Value.ToString() }); OutputFormatter.WriteTable(rows, headers); } else { Console.WriteLine(root.ToString()); } } } }