From 532d2b62594510c635bfeeec6d3df2090d7c0662 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:02:58 +0000 Subject: [PATCH 1/9] Initial plan From d6805aa9c77df9ae5ba64c13a7bb65da3e88502d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:18:26 +0000 Subject: [PATCH 2/9] Add config validate subcommand for validating Dev Proxy configuration files Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> --- DevProxy/Commands/ConfigCommand.cs | 441 ++++++++++++++++++++++++++- DevProxy/Commands/DevProxyCommand.cs | 20 ++ DevProxy/Program.cs | 9 + 3 files changed, 469 insertions(+), 1 deletion(-) diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs index a089494b..5957297c 100644 --- a/DevProxy/Commands/ConfigCommand.cs +++ b/DevProxy/Commands/ConfigCommand.cs @@ -2,12 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using DevProxy.Abstractions.Plugins; using DevProxy.Abstractions.Proxy; using DevProxy.Abstractions.Utils; +using DevProxy.Plugins; using System.CommandLine; using System.CommandLine.Parsing; using System.Diagnostics; using System.Net.Http.Headers; +using System.Reflection; using System.Text.Json; using System.Text.RegularExpressions; @@ -45,6 +48,12 @@ sealed class ConfigCommand : Command private readonly HttpClient _httpClient; private readonly string snippetsFileUrl = $"https://aka.ms/devproxy/snippets/v{ProxyUtils.NormalizeVersion(ProxyUtils.ProductVersion)}"; private readonly string configFileSnippetName = "ConfigFile"; + private static readonly JsonSerializerOptions _jsonOutputOptions = new() + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; public ConfigCommand( HttpClient httpClient, @@ -59,6 +68,46 @@ public ConfigCommand( ConfigureCommand(); } + /// + /// Runs config validation standalone, without requiring the full DI container. + /// Used when the proxy is invoked with 'config validate' to allow validating + /// even broken config files. + /// + internal static async Task RunValidateStandaloneAsync(string[] args) + { + string? configFile = null; + string output = "text"; + + for (var i = 0; i < args.Length; i++) + { + if ((string.Equals(args[i], "--config-file", StringComparison.OrdinalIgnoreCase) || + string.Equals(args[i], "-c", StringComparison.OrdinalIgnoreCase)) && + i + 1 < args.Length) + { + configFile = args[i + 1]; + i++; + } + else if (string.Equals(args[i], "--output", StringComparison.OrdinalIgnoreCase) && + i + 1 < args.Length) + { + output = args[i + 1]; + i++; + } + } + + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.UserAgent.Add( + new ProductInfoHeaderValue("dev-proxy", ProxyUtils.ProductVersion)); + + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(LogLevel.Debug); + }); + var logger = loggerFactory.CreateLogger(); + + return await ValidateConfigCoreAsync(configFile, output, httpClient, logger, CancellationToken.None); + } + private void ConfigureCommand() { var configGetCommand = new Command("get", "Download the specified config from the Sample Solution Gallery"); @@ -100,11 +149,33 @@ private void ConfigureCommand() _ = Process.Start(cfgPsi); }); + var configValidateCommand = new Command("validate", "Validate a Dev Proxy configuration file"); + var validateConfigFileOption = new Option("--config-file", "-c") + { + Description = "The path to the configuration file to validate", + HelpName = "config-file" + }; + var outputOption = new Option("--output") + { + Description = "Output format (text or json)", + HelpName = "output", + DefaultValueFactory = _ => "text" + }; + configValidateCommand.Add(validateConfigFileOption); + configValidateCommand.Add(outputOption); + configValidateCommand.SetAction(async (parseResult, cancellationToken) => + { + var configFile = parseResult.GetValue(validateConfigFileOption); + var output = parseResult.GetValue(outputOption) ?? "text"; + return await ValidateConfigAsync(configFile, output, cancellationToken); + }); + this.AddCommands(new List { configGetCommand, configNewCommand, - configOpenCommand + configOpenCommand, + configValidateCommand }.OrderByName()); } @@ -401,4 +472,372 @@ private static string GetTargetFolderPath(string appFolder, string configId) body = Regex.Replace(body, @"\$[0-9]+", ""); return body; } + + private static string? ResolveConfigFile(string? configFilePath) + { + if (!string.IsNullOrEmpty(configFilePath)) + { + var resolved = Path.GetFullPath(ProxyUtils.ReplacePathTokens(configFilePath)); + return File.Exists(resolved) ? resolved : null; + } + + string?[] configFiles = [ + "devproxyrc.jsonc", + "devproxyrc.json", + Path.Combine(".devproxy", "devproxyrc.jsonc"), + Path.Combine(".devproxy", "devproxyrc.json"), + Path.Combine(ProxyUtils.AppFolder ?? "", "devproxyrc.jsonc"), + Path.Combine(ProxyUtils.AppFolder ?? "", "devproxyrc.json") + ]; + + foreach (var configFile in configFiles) + { + if (!string.IsNullOrEmpty(configFile) && File.Exists(configFile)) + { + return Path.GetFullPath(configFile); + } + } + + return null; + } + + private async Task ValidateConfigAsync(string? configFilePath, string output, CancellationToken cancellationToken) + { + return await ValidateConfigCoreAsync(configFilePath, output, _httpClient, _logger, cancellationToken); + } + + private static async Task ValidateConfigCoreAsync( + string? configFilePath, + string output, + HttpClient httpClient, + ILogger logger, + CancellationToken cancellationToken) + { + var errors = new List(); + var warnings = new List(); + var pluginNames = new List(); + var urlPatterns = new List(); + + var resolvedConfigFile = ResolveConfigFile(configFilePath); + if (resolvedConfigFile is null) + { + errors.Add(new("configFile", configFilePath is not null + ? $"Configuration file '{configFilePath}' not found" + : "No configuration file found")); + WriteResults(output, null, errors, warnings, pluginNames, urlPatterns); + return 1; + } + + string configJson; + JsonDocument configDoc; + try + { + configJson = await File.ReadAllTextAsync(resolvedConfigFile, cancellationToken); + configDoc = JsonDocument.Parse(configJson, new JsonDocumentOptions + { + CommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true + }); + } + catch (JsonException ex) + { + errors.Add(new("configFile", $"Invalid JSON: {ex.Message}")); + WriteResults(output, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns); + return 1; + } + + using (configDoc) + { + var schemaUrl = configDoc.RootElement.TryGetProperty("$schema", out var schemaProp) + ? schemaProp.GetString() + : null; + if (!string.IsNullOrEmpty(schemaUrl)) + { + try + { + var (isValid, validationErrors) = await ProxyUtils.ValidateJsonAsync( + configJson, schemaUrl, httpClient, logger, cancellationToken); + if (!isValid) + { + foreach (var error in validationErrors) + { + errors.Add(new("schema", error)); + } + } + } + catch (Exception ex) + { + warnings.Add(new("$schema", $"Could not validate schema: {ex.Message}")); + } + + ValidateSchemaVersion(schemaUrl, warnings); + } + else + { + warnings.Add(new("$schema", "No schema URL found, skipping schema validation")); + } + + var configFileDirectory = Path.GetDirectoryName(resolvedConfigFile) ?? "."; + + if (configDoc.RootElement.TryGetProperty("plugins", out var pluginsElement) && + pluginsElement.ValueKind == JsonValueKind.Array) + { + ValidatePlugins(pluginsElement, configFileDirectory, errors, warnings, pluginNames); + } + else + { + errors.Add(new("plugins", "No plugins configured")); + } + + if (configDoc.RootElement.TryGetProperty("urlsToWatch", out var urlsElement) && + urlsElement.ValueKind == JsonValueKind.Array) + { + ValidateUrls(urlsElement, errors, warnings, urlPatterns); + } + else + { + warnings.Add(new("urlsToWatch", "No URLs to watch configured")); + } + } + + var isConfigValid = errors.Count == 0; + WriteResults(output, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns); + return isConfigValid ? 0 : 1; + } + + private static void ValidateSchemaVersion(string schemaUrl, List warnings) + { + try + { + var uri = new Uri(schemaUrl); + if (uri.Segments.Length > 2) + { + var schemaVersion = uri.Segments[^2] + .TrimStart('v') + .TrimEnd('/'); + var currentVersion = ProxyUtils.NormalizeVersion(ProxyUtils.ProductVersion); + if (ProxyUtils.CompareSemVer(currentVersion, schemaVersion) != 0) + { + var currentSchemaUrl = uri.ToString().Replace( + $"/v{schemaVersion}/", + $"/v{currentVersion}/", + StringComparison.OrdinalIgnoreCase); + warnings.Add(new("$schema", + $"Schema version does not match Dev Proxy version, expected schema: {currentSchemaUrl}")); + } + } + } + catch + { + // Ignore schema version parsing errors + } + } + + private static void ValidatePlugins( + JsonElement pluginsElement, + string configFileDirectory, + List errors, + List warnings, + List pluginNames) + { + var hasEnabledPlugins = false; + var i = 0; + + foreach (var plugin in pluginsElement.EnumerateArray()) + { + var name = plugin.TryGetProperty("name", out var nameProp) ? nameProp.GetString() : null; + var enabled = !plugin.TryGetProperty("enabled", out var enabledProp) || enabledProp.GetBoolean(); + var pluginPath = plugin.TryGetProperty("pluginPath", out var pathProp) ? pathProp.GetString() : null; + + if (string.IsNullOrEmpty(name)) + { + errors.Add(new($"plugins[{i}].name", "Plugin name is required")); + i++; + continue; + } + + if (!enabled) + { + i++; + continue; + } + + hasEnabledPlugins = true; + pluginNames.Add(name); + + if (string.IsNullOrEmpty(pluginPath)) + { + errors.Add(new($"plugins[{i}].pluginPath", $"Plugin path is required for '{name}'")); + i++; + continue; + } + + var resolvedPluginPath = Path.GetFullPath( + Path.Combine( + configFileDirectory, + ProxyUtils.ReplacePathTokens(pluginPath.Replace('\\', Path.DirectorySeparatorChar)))); + + if (!File.Exists(resolvedPluginPath)) + { + errors.Add(new($"plugins[{i}].pluginPath", + $"Plugin assembly '{resolvedPluginPath}' not found")); + i++; + continue; + } + + try + { + var pluginLoadContext = new PluginLoadContext(resolvedPluginPath); + var assembly = pluginLoadContext.LoadFromAssemblyName( + new AssemblyName(Path.GetFileNameWithoutExtension(resolvedPluginPath))); + var pluginType = assembly.GetTypes() + .FirstOrDefault(t => t.Name == name && typeof(IPlugin).IsAssignableFrom(t)); + if (pluginType is null) + { + errors.Add(new($"plugins[{i}].name", + $"Plugin '{name}' not found in assembly '{Path.GetFileName(resolvedPluginPath)}'")); + } + } + catch (Exception ex) + { + errors.Add(new($"plugins[{i}]", + $"Failed to load plugin assembly: {ex.Message}")); + } + + i++; + } + + if (!hasEnabledPlugins) + { + errors.Add(new("plugins", "No enabled plugins found")); + } + } + + private static void ValidateUrls( + JsonElement urlsElement, + List errors, + List warnings, + List urlPatterns) + { + var i = 0; + foreach (var url in urlsElement.EnumerateArray()) + { + var pattern = url.GetString(); + if (string.IsNullOrEmpty(pattern)) + { + warnings.Add(new($"urlsToWatch[{i}]", "Empty URL pattern")); + } + else + { + urlPatterns.Add(pattern); + try + { + var cleanPattern = pattern.StartsWith('!') ? pattern[1..] : pattern; + _ = new Regex( + $"^{Regex.Escape(cleanPattern).Replace("\\*", ".*", StringComparison.OrdinalIgnoreCase)}$"); + } + catch (ArgumentException ex) + { + errors.Add(new($"urlsToWatch[{i}]", + $"Invalid URL pattern '{pattern}': {ex.Message}")); + } + } + i++; + } + } + + private static void WriteResults( + string output, + string? configFile, + List errors, + List warnings, + List pluginNames, + List urlPatterns) + { + var isValid = errors.Count == 0; + + if (string.Equals(output, "json", StringComparison.OrdinalIgnoreCase)) + { + WriteJsonResults(configFile, errors, warnings, pluginNames, urlPatterns, isValid); + } + else + { + WriteTextResults(configFile, errors, warnings, pluginNames, urlPatterns, isValid); + } + } + + private static void WriteJsonResults( + string? configFile, + List errors, + List warnings, + List pluginNames, + List urlPatterns, + bool isValid) + { + var result = new + { + valid = isValid, + configFile, + plugins = pluginNames, + urlsToWatch = urlPatterns, + errors = errors.Select(e => new { path = e.Path, message = e.Message }), + warnings = warnings.Select(w => new { path = w.Path, message = w.Message }) + }; + + var json = JsonSerializer.Serialize(result, _jsonOutputOptions); + Console.WriteLine(json); + } + + private static void WriteTextResults( + string? configFile, + List errors, + List warnings, + List pluginNames, + List urlPatterns, + bool isValid) + { + if (isValid) + { + Console.WriteLine("\u2713 Configuration is valid"); + } + else + { + Console.WriteLine("\u2717 Configuration is invalid"); + } + + if (configFile is not null) + { + Console.WriteLine($" Config file: {configFile}"); + } + if (pluginNames.Count > 0) + { + Console.WriteLine($" Plugins: {pluginNames.Count} loaded"); + } + if (urlPatterns.Count > 0) + { + Console.WriteLine($" URLs to watch: {urlPatterns.Count} pattern{(urlPatterns.Count != 1 ? "s" : "")}"); + } + + if (errors.Count > 0) + { + Console.WriteLine(); + Console.WriteLine("Errors:"); + foreach (var error in errors) + { + Console.WriteLine($" - {error.Path}: {error.Message}"); + } + } + + if (warnings.Count > 0) + { + Console.WriteLine(); + Console.WriteLine("Warnings:"); + foreach (var warning in warnings) + { + Console.WriteLine($" - {warning.Path}: {warning.Message}"); + } + } + } + + private sealed record ValidationMessage(string Path, string Message); } \ No newline at end of file diff --git a/DevProxy/Commands/DevProxyCommand.cs b/DevProxy/Commands/DevProxyCommand.cs index c5c4b8b3..aa8f3fdd 100644 --- a/DevProxy/Commands/DevProxyCommand.cs +++ b/DevProxy/Commands/DevProxyCommand.cs @@ -46,6 +46,7 @@ sealed class DevProxyCommand : RootCommand private static bool _isStdioCommandResolved; private static bool _isJwtCommandResolved; private static bool _isRootCommandResolved; + private static bool _isConfigValidateCommandResolved; private static bool _stdioLogFilePathResolved; public static bool HasGlobalOptions @@ -97,6 +98,25 @@ public static bool IsJwtCommand } } + public static bool IsConfigValidateCommand + { + get + { + if (_isConfigValidateCommandResolved) + { + return field; + } + + var args = Environment.GetCommandLineArgs(); + field = args.Length > 2 && + string.Equals(args[1], "config", StringComparison.OrdinalIgnoreCase) && + string.Equals(args[2], "validate", StringComparison.OrdinalIgnoreCase) && + !args.Any(arg => helpOptions.Contains(arg)); + _isConfigValidateCommandResolved = true; + return field; + } + } + /// /// Determines if the root command (proxy itself) is being invoked. /// Returns true when no subcommand is specified (only options or no args). diff --git a/DevProxy/Program.cs b/DevProxy/Program.cs index 435288c9..14deaf7f 100644 --- a/DevProxy/Program.cs +++ b/DevProxy/Program.cs @@ -57,6 +57,15 @@ static async Task RunProxyAsync(string[] args, DevProxyConfigOptions option _ = Announcement.ShowAsync(); +// Handle 'config validate' early to allow validation of broken configs +// without requiring the full DI container to be built +if (DevProxyCommand.IsConfigValidateCommand) +{ + var validateExitCode = await ConfigCommand.RunValidateStandaloneAsync(args); + Environment.Exit(validateExitCode); + return; +} + var options = new DevProxyConfigOptions(); options.ParseOptions(args); From b8793d477d947756118c03e96c2bbf5d49763e07 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:14:41 +0000 Subject: [PATCH 3/9] Use ProxyUtils.JsonSerializerOptions and JsonDocumentOptions instead of custom duplicates Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> --- DevProxy/Commands/ConfigCommand.cs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs index 5957297c..79eeda14 100644 --- a/DevProxy/Commands/ConfigCommand.cs +++ b/DevProxy/Commands/ConfigCommand.cs @@ -48,13 +48,6 @@ sealed class ConfigCommand : Command private readonly HttpClient _httpClient; private readonly string snippetsFileUrl = $"https://aka.ms/devproxy/snippets/v{ProxyUtils.NormalizeVersion(ProxyUtils.ProductVersion)}"; private readonly string configFileSnippetName = "ConfigFile"; - private static readonly JsonSerializerOptions _jsonOutputOptions = new() - { - WriteIndented = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; - public ConfigCommand( HttpClient httpClient, IProxyConfiguration proxyConfiguration, @@ -533,11 +526,7 @@ private static async Task ValidateConfigCoreAsync( try { configJson = await File.ReadAllTextAsync(resolvedConfigFile, cancellationToken); - configDoc = JsonDocument.Parse(configJson, new JsonDocumentOptions - { - CommentHandling = JsonCommentHandling.Skip, - AllowTrailingCommas = true - }); + configDoc = JsonDocument.Parse(configJson, ProxyUtils.JsonDocumentOptions); } catch (JsonException ex) { @@ -784,7 +773,7 @@ private static void WriteJsonResults( warnings = warnings.Select(w => new { path = w.Path, message = w.Message }) }; - var json = JsonSerializer.Serialize(result, _jsonOutputOptions); + var json = JsonSerializer.Serialize(result, ProxyUtils.JsonSerializerOptions); Console.WriteLine(json); } From 4e809cdbea0f2af98ae4f33478cf253b7a08efd1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:20:46 +0000 Subject: [PATCH 4/9] Switch from Console.WriteLine to ILogger in config validate output Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> --- DevProxy/Commands/ConfigCommand.cs | 56 +++++++++++++++++++----------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs index 79eeda14..5885b12e 100644 --- a/DevProxy/Commands/ConfigCommand.cs +++ b/DevProxy/Commands/ConfigCommand.cs @@ -5,6 +5,7 @@ using DevProxy.Abstractions.Plugins; using DevProxy.Abstractions.Proxy; using DevProxy.Abstractions.Utils; +using DevProxy.Logging; using DevProxy.Plugins; using System.CommandLine; using System.CommandLine.Parsing; @@ -94,7 +95,19 @@ internal static async Task RunValidateStandaloneAsync(string[] args) using var loggerFactory = LoggerFactory.Create(builder => { - builder.SetMinimumLevel(LogLevel.Debug); + builder + .SetMinimumLevel(LogLevel.Information) + .AddConsole(consoleOptions => + { + consoleOptions.FormatterName = ProxyConsoleFormatter.DefaultCategoryName; + consoleOptions.LogToStandardErrorThreshold = LogLevel.Warning; + }) + .AddConsoleFormatter(formatterOptions => + { + formatterOptions.IncludeScopes = false; + formatterOptions.ShowSkipMessages = true; + formatterOptions.ShowTimestamps = false; + }); }); var logger = loggerFactory.CreateLogger(); @@ -517,7 +530,7 @@ private static async Task ValidateConfigCoreAsync( errors.Add(new("configFile", configFilePath is not null ? $"Configuration file '{configFilePath}' not found" : "No configuration file found")); - WriteResults(output, null, errors, warnings, pluginNames, urlPatterns); + WriteResults(output, null, errors, warnings, pluginNames, urlPatterns, logger); return 1; } @@ -531,7 +544,7 @@ private static async Task ValidateConfigCoreAsync( catch (JsonException ex) { errors.Add(new("configFile", $"Invalid JSON: {ex.Message}")); - WriteResults(output, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns); + WriteResults(output, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger); return 1; } @@ -590,7 +603,7 @@ private static async Task ValidateConfigCoreAsync( } var isConfigValid = errors.Count == 0; - WriteResults(output, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns); + WriteResults(output, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger); return isConfigValid ? 0 : 1; } @@ -741,17 +754,18 @@ private static void WriteResults( List errors, List warnings, List pluginNames, - List urlPatterns) + List urlPatterns, + ILogger logger) { var isValid = errors.Count == 0; if (string.Equals(output, "json", StringComparison.OrdinalIgnoreCase)) { - WriteJsonResults(configFile, errors, warnings, pluginNames, urlPatterns, isValid); + WriteJsonResults(configFile, errors, warnings, pluginNames, urlPatterns, isValid, logger); } else { - WriteTextResults(configFile, errors, warnings, pluginNames, urlPatterns, isValid); + WriteTextResults(configFile, errors, warnings, pluginNames, urlPatterns, isValid, logger); } } @@ -761,7 +775,8 @@ private static void WriteJsonResults( List warnings, List pluginNames, List urlPatterns, - bool isValid) + bool isValid, + ILogger logger) { var result = new { @@ -774,7 +789,7 @@ private static void WriteJsonResults( }; var json = JsonSerializer.Serialize(result, ProxyUtils.JsonSerializerOptions); - Console.WriteLine(json); + logger.LogInformation("{Result}", json); } private static void WriteTextResults( @@ -783,47 +798,46 @@ private static void WriteTextResults( List warnings, List pluginNames, List urlPatterns, - bool isValid) + bool isValid, + ILogger logger) { if (isValid) { - Console.WriteLine("\u2713 Configuration is valid"); + logger.LogInformation("\u2713 Configuration is valid"); } else { - Console.WriteLine("\u2717 Configuration is invalid"); + logger.LogError("\u2717 Configuration is invalid"); } if (configFile is not null) { - Console.WriteLine($" Config file: {configFile}"); + logger.LogInformation(" Config file: {ConfigFile}", configFile); } if (pluginNames.Count > 0) { - Console.WriteLine($" Plugins: {pluginNames.Count} loaded"); + logger.LogInformation(" Plugins: {Count} loaded", pluginNames.Count); } if (urlPatterns.Count > 0) { - Console.WriteLine($" URLs to watch: {urlPatterns.Count} pattern{(urlPatterns.Count != 1 ? "s" : "")}"); + logger.LogInformation(" URLs to watch: {Count} pattern{Plural}", urlPatterns.Count, urlPatterns.Count != 1 ? "s" : ""); } if (errors.Count > 0) { - Console.WriteLine(); - Console.WriteLine("Errors:"); + logger.LogInformation(""); foreach (var error in errors) { - Console.WriteLine($" - {error.Path}: {error.Message}"); + logger.LogError(" - {Path}: {Message}", error.Path, error.Message); } } if (warnings.Count > 0) { - Console.WriteLine(); - Console.WriteLine("Warnings:"); + logger.LogInformation(""); foreach (var warning in warnings) { - Console.WriteLine($" - {warning.Path}: {warning.Message}"); + logger.LogWarning(" - {Path}: {Message}", warning.Path, warning.Message); } } } From fb4b5d283e7f9d42110f84eb4ee14c0c6b831e9f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:29:11 +0000 Subject: [PATCH 5/9] Centralize config file search and schema version validation to avoid duplication Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> --- DevProxy.Abstractions/Utils/ProxyUtils.cs | 62 ++++++++++++++++--- DevProxy/Commands/ConfigCommand.cs | 36 ++--------- .../ConfigurationManagerExtensions.cs | 26 +------- 3 files changed, 60 insertions(+), 64 deletions(-) diff --git a/DevProxy.Abstractions/Utils/ProxyUtils.cs b/DevProxy.Abstractions/Utils/ProxyUtils.cs index ec21a389..c9a40402 100644 --- a/DevProxy.Abstractions/Utils/ProxyUtils.cs +++ b/DevProxy.Abstractions/Utils/ProxyUtils.cs @@ -273,6 +273,25 @@ public static void ValidateSchemaVersion(string schemaUrl, ILogger logger) return; } + var warning = GetSchemaVersionMismatchWarning(schemaUrl); + if (warning is not null) + { + logger.LogWarning("{Warning}", warning); + } + } + + /// + /// Checks if the schema URL version matches the current Dev Proxy version. + /// Returns a warning message if versions don't match, or null if they match + /// or the schema URL cannot be parsed. + /// + public static string? GetSchemaVersionMismatchWarning(string schemaUrl) + { + if (string.IsNullOrWhiteSpace(schemaUrl)) + { + return null; + } + try { var uri = new Uri(schemaUrl); @@ -285,18 +304,47 @@ public static void ValidateSchemaVersion(string schemaUrl, ILogger logger) if (CompareSemVer(currentVersion, schemaVersion) != 0) { var currentSchemaUrl = uri.ToString().Replace($"/v{schemaVersion}/", $"/v{currentVersion}/", StringComparison.OrdinalIgnoreCase); - logger.LogWarning("The version of schema does not match the installed Dev Proxy version, the expected schema is {Schema}", currentSchemaUrl); + return $"The version of schema does not match the installed Dev Proxy version, the expected schema is {currentSchemaUrl}"; } } - else - { - logger.LogDebug("Invalid schema {SchemaUrl}, skipping schema version validation.", schemaUrl); - } } - catch (Exception ex) + catch { - logger.LogWarning("Invalid schema {SchemaUrl}, skipping schema version validation. Error: {Error}", schemaUrl, ex.Message); + // Ignore schema URL parsing errors } + + return null; + } + + /// + /// Returns the ordered list of config file paths to search. + /// The first existing file in the list should be used. + /// + public static IEnumerable GetConfigFileCandidates(string? userConfigFile) + { + return [ + // config file specified by the user takes precedence + // null if not specified + userConfigFile, + // current directory - JSON/JSONC files + "devproxyrc.jsonc", + "devproxyrc.json", + // current directory - YAML files + "devproxyrc.yaml", + "devproxyrc.yml", + // .devproxy subdirectory - JSON/JSONC files + Path.Combine(".devproxy", "devproxyrc.jsonc"), + Path.Combine(".devproxy", "devproxyrc.json"), + // .devproxy subdirectory - YAML files + Path.Combine(".devproxy", "devproxyrc.yaml"), + Path.Combine(".devproxy", "devproxyrc.yml"), + // app folder - JSON/JSONC files + Path.Combine(AppFolder ?? "", "devproxyrc.jsonc"), + Path.Combine(AppFolder ?? "", "devproxyrc.json"), + // app folder - YAML files + Path.Combine(AppFolder ?? "", "devproxyrc.yaml"), + Path.Combine(AppFolder ?? "", "devproxyrc.yml") + ]; } /// diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs index 332ce890..e5a9d8f4 100644 --- a/DevProxy/Commands/ConfigCommand.cs +++ b/DevProxy/Commands/ConfigCommand.cs @@ -504,16 +504,7 @@ private static string GetTargetFolderPath(string appFolder, string configId) return File.Exists(resolved) ? resolved : null; } - string?[] configFiles = [ - "devproxyrc.jsonc", - "devproxyrc.json", - Path.Combine(".devproxy", "devproxyrc.jsonc"), - Path.Combine(".devproxy", "devproxyrc.json"), - Path.Combine(ProxyUtils.AppFolder ?? "", "devproxyrc.jsonc"), - Path.Combine(ProxyUtils.AppFolder ?? "", "devproxyrc.json") - ]; - - foreach (var configFile in configFiles) + foreach (var configFile in ProxyUtils.GetConfigFileCandidates(null)) { if (!string.IsNullOrEmpty(configFile) && File.Exists(configFile)) { @@ -626,29 +617,10 @@ private static async Task ValidateConfigCoreAsync( private static void ValidateSchemaVersion(string schemaUrl, List warnings) { - try - { - var uri = new Uri(schemaUrl); - if (uri.Segments.Length > 2) - { - var schemaVersion = uri.Segments[^2] - .TrimStart('v') - .TrimEnd('/'); - var currentVersion = ProxyUtils.NormalizeVersion(ProxyUtils.ProductVersion); - if (ProxyUtils.CompareSemVer(currentVersion, schemaVersion) != 0) - { - var currentSchemaUrl = uri.ToString().Replace( - $"/v{schemaVersion}/", - $"/v{currentVersion}/", - StringComparison.OrdinalIgnoreCase); - warnings.Add(new("$schema", - $"Schema version does not match Dev Proxy version, expected schema: {currentSchemaUrl}")); - } - } - } - catch + var warning = ProxyUtils.GetSchemaVersionMismatchWarning(schemaUrl); + if (warning is not null) { - // Ignore schema version parsing errors + warnings.Add(new("$schema", warning)); } } diff --git a/DevProxy/Extensions/ConfigurationManagerExtensions.cs b/DevProxy/Extensions/ConfigurationManagerExtensions.cs index cac373b9..d37e514c 100644 --- a/DevProxy/Extensions/ConfigurationManagerExtensions.cs +++ b/DevProxy/Extensions/ConfigurationManagerExtensions.cs @@ -16,31 +16,7 @@ public static ConfigurationManager ConfigureDevProxyConfig(this ConfigurationMan configuration.Sources.Clear(); _ = configuration.SetBasePath(Directory.GetCurrentDirectory()); - string?[] configFiles = [ - // config file specified by the user takes precedence - // null if not specified - options.ConfigFile, - // current directory - JSON/JSONC files - "devproxyrc.jsonc", - "devproxyrc.json", - // current directory - YAML files - "devproxyrc.yaml", - "devproxyrc.yml", - // .devproxy subdirectory - JSON/JSONC files - Path.Combine(".devproxy", "devproxyrc.jsonc"), - Path.Combine(".devproxy", "devproxyrc.json"), - // .devproxy subdirectory - YAML files - Path.Combine(".devproxy", "devproxyrc.yaml"), - Path.Combine(".devproxy", "devproxyrc.yml"), - // app folder - JSON/JSONC files - Path.Combine(ProxyUtils.AppFolder ?? "", "devproxyrc.jsonc"), - Path.Combine(ProxyUtils.AppFolder ?? "", "devproxyrc.json"), - // app folder - YAML files - Path.Combine(ProxyUtils.AppFolder ?? "", "devproxyrc.yaml"), - Path.Combine(ProxyUtils.AppFolder ?? "", "devproxyrc.yml") - ]; - - foreach (var configFile in configFiles) + foreach (var configFile in ProxyUtils.GetConfigFileCandidates(options.ConfigFile)) { if (!string.IsNullOrEmpty(configFile) && File.Exists(configFile)) { From 60d4acafedb38ff9d32aa10c86375e1d64bd057e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:44:43 +0000 Subject: [PATCH 6/9] Remove local --output option from validate command, use global --log-for instead Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> --- DevProxy/Commands/ConfigCommand.cs | 36 +++++++++++++----------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs index e5a9d8f4..21360303 100644 --- a/DevProxy/Commands/ConfigCommand.cs +++ b/DevProxy/Commands/ConfigCommand.cs @@ -70,7 +70,7 @@ public ConfigCommand( internal static async Task RunValidateStandaloneAsync(string[] args) { string? configFile = null; - string output = "text"; + var isJsonOutput = false; for (var i = 0; i < args.Length; i++) { @@ -81,10 +81,10 @@ internal static async Task RunValidateStandaloneAsync(string[] args) configFile = args[i + 1]; i++; } - else if (string.Equals(args[i], "--output", StringComparison.OrdinalIgnoreCase) && + else if (string.Equals(args[i], DevProxyCommand.LogForOptionName, StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length) { - output = args[i + 1]; + isJsonOutput = string.Equals(args[i + 1], "machine", StringComparison.OrdinalIgnoreCase); i++; } } @@ -111,7 +111,7 @@ internal static async Task RunValidateStandaloneAsync(string[] args) }); var logger = loggerFactory.CreateLogger(); - return await ValidateConfigCoreAsync(configFile, output, httpClient, logger, CancellationToken.None); + return await ValidateConfigCoreAsync(configFile, isJsonOutput, httpClient, logger, CancellationToken.None); } private void ConfigureCommand() @@ -161,19 +161,13 @@ private void ConfigureCommand() Description = "The path to the configuration file to validate", HelpName = "config-file" }; - var outputOption = new Option("--output") - { - Description = "Output format (text or json)", - HelpName = "output", - DefaultValueFactory = _ => "text" - }; configValidateCommand.Add(validateConfigFileOption); - configValidateCommand.Add(outputOption); configValidateCommand.SetAction(async (parseResult, cancellationToken) => { var configFile = parseResult.GetValue(validateConfigFileOption); - var output = parseResult.GetValue(outputOption) ?? "text"; - return await ValidateConfigAsync(configFile, output, cancellationToken); + var logFor = parseResult.GetValueOrDefault(DevProxyCommand.LogForOptionName); + var isJsonOutput = logFor == LogFor.Machine; + return await ValidateConfigAsync(configFile, isJsonOutput, cancellationToken); }); this.AddCommands(new List @@ -515,14 +509,14 @@ private static string GetTargetFolderPath(string appFolder, string configId) return null; } - private async Task ValidateConfigAsync(string? configFilePath, string output, CancellationToken cancellationToken) + private async Task ValidateConfigAsync(string? configFilePath, bool isJsonOutput, CancellationToken cancellationToken) { - return await ValidateConfigCoreAsync(configFilePath, output, _httpClient, _logger, cancellationToken); + return await ValidateConfigCoreAsync(configFilePath, isJsonOutput, _httpClient, _logger, cancellationToken); } private static async Task ValidateConfigCoreAsync( string? configFilePath, - string output, + bool isJsonOutput, HttpClient httpClient, ILogger logger, CancellationToken cancellationToken) @@ -538,7 +532,7 @@ private static async Task ValidateConfigCoreAsync( errors.Add(new("configFile", configFilePath is not null ? $"Configuration file '{configFilePath}' not found" : "No configuration file found")); - WriteResults(output, null, errors, warnings, pluginNames, urlPatterns, logger); + WriteResults(isJsonOutput, null, errors, warnings, pluginNames, urlPatterns, logger); return 1; } @@ -552,7 +546,7 @@ private static async Task ValidateConfigCoreAsync( catch (JsonException ex) { errors.Add(new("configFile", $"Invalid JSON: {ex.Message}")); - WriteResults(output, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger); + WriteResults(isJsonOutput, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger); return 1; } @@ -611,7 +605,7 @@ private static async Task ValidateConfigCoreAsync( } var isConfigValid = errors.Count == 0; - WriteResults(output, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger); + WriteResults(isJsonOutput, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger); return isConfigValid ? 0 : 1; } @@ -738,7 +732,7 @@ private static void ValidateUrls( } private static void WriteResults( - string output, + bool isJsonOutput, string? configFile, List errors, List warnings, @@ -748,7 +742,7 @@ private static void WriteResults( { var isValid = errors.Count == 0; - if (string.Equals(output, "json", StringComparison.OrdinalIgnoreCase)) + if (isJsonOutput) { WriteJsonResults(configFile, errors, warnings, pluginNames, urlPatterns, isValid, logger); } From 57f99375c3525840b61a148394a4b0abedf0848e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:51:28 +0000 Subject: [PATCH 7/9] Remove ValidateConfigAsync wrapper; use exit code 2 for validation errors Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> --- DevProxy/Commands/ConfigCommand.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs index 21360303..ba448924 100644 --- a/DevProxy/Commands/ConfigCommand.cs +++ b/DevProxy/Commands/ConfigCommand.cs @@ -167,7 +167,7 @@ private void ConfigureCommand() var configFile = parseResult.GetValue(validateConfigFileOption); var logFor = parseResult.GetValueOrDefault(DevProxyCommand.LogForOptionName); var isJsonOutput = logFor == LogFor.Machine; - return await ValidateConfigAsync(configFile, isJsonOutput, cancellationToken); + return await ValidateConfigCoreAsync(configFile, isJsonOutput, _httpClient, _logger, cancellationToken); }); this.AddCommands(new List @@ -509,11 +509,6 @@ private static string GetTargetFolderPath(string appFolder, string configId) return null; } - private async Task ValidateConfigAsync(string? configFilePath, bool isJsonOutput, CancellationToken cancellationToken) - { - return await ValidateConfigCoreAsync(configFilePath, isJsonOutput, _httpClient, _logger, cancellationToken); - } - private static async Task ValidateConfigCoreAsync( string? configFilePath, bool isJsonOutput, @@ -533,7 +528,7 @@ private static async Task ValidateConfigCoreAsync( ? $"Configuration file '{configFilePath}' not found" : "No configuration file found")); WriteResults(isJsonOutput, null, errors, warnings, pluginNames, urlPatterns, logger); - return 1; + return 2; } string configJson; @@ -547,7 +542,7 @@ private static async Task ValidateConfigCoreAsync( { errors.Add(new("configFile", $"Invalid JSON: {ex.Message}")); WriteResults(isJsonOutput, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger); - return 1; + return 2; } using (configDoc) @@ -606,7 +601,7 @@ private static async Task ValidateConfigCoreAsync( var isConfigValid = errors.Count == 0; WriteResults(isJsonOutput, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger); - return isConfigValid ? 0 : 1; + return isConfigValid ? 0 : 2; } private static void ValidateSchemaVersion(string schemaUrl, List warnings) From fe35faeebc60e4e51715e18fc02430ca4f1e24f4 Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Thu, 26 Feb 2026 08:53:48 +0100 Subject: [PATCH 8/9] Address review feedback: YAML validation, type safety, error handling, machine output formatting - Add YAML-to-JSON conversion in ValidateConfigCoreAsync so YAML configs can be validated - Wrap plugin property reads in try-catch for malformed type handling - Check url.ValueKind before calling GetString() in ValidateUrls - Catch IOException/UnauthorizedAccessException when reading config files - Register MachineConsoleFormatter and select formatter based on isJsonOutput - Return warning message for unparseable schema URLs instead of silently ignoring --- DevProxy.Abstractions/Utils/ProxyUtils.cs | 2 +- DevProxy/Commands/ConfigCommand.cs | 68 +++++++++++++++++++++-- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/DevProxy.Abstractions/Utils/ProxyUtils.cs b/DevProxy.Abstractions/Utils/ProxyUtils.cs index c9a40402..536f5a45 100644 --- a/DevProxy.Abstractions/Utils/ProxyUtils.cs +++ b/DevProxy.Abstractions/Utils/ProxyUtils.cs @@ -310,7 +310,7 @@ public static void ValidateSchemaVersion(string schemaUrl, ILogger logger) } catch { - // Ignore schema URL parsing errors + return $"The $schema value '{schemaUrl}' is not a valid URL. Schema version could not be validated."; } return null; diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs index ba448924..5c482e9e 100644 --- a/DevProxy/Commands/ConfigCommand.cs +++ b/DevProxy/Commands/ConfigCommand.cs @@ -14,6 +14,7 @@ using System.Reflection; using System.Text.Json; using System.Text.RegularExpressions; +using YamlDotNet.Core; namespace DevProxy.Commands; @@ -93,16 +94,26 @@ internal static async Task RunValidateStandaloneAsync(string[] args) httpClient.DefaultRequestHeaders.UserAgent.Add( new ProductInfoHeaderValue("dev-proxy", ProxyUtils.ProductVersion)); + var formatterName = isJsonOutput + ? MachineConsoleFormatter.FormatterName + : ProxyConsoleFormatter.DefaultCategoryName; + using var loggerFactory = LoggerFactory.Create(builder => { builder .SetMinimumLevel(LogLevel.Information) .AddConsole(consoleOptions => { - consoleOptions.FormatterName = ProxyConsoleFormatter.DefaultCategoryName; + consoleOptions.FormatterName = formatterName; consoleOptions.LogToStandardErrorThreshold = LogLevel.Warning; }) .AddConsoleFormatter(formatterOptions => + { + formatterOptions.IncludeScopes = false; + formatterOptions.ShowSkipMessages = true; + formatterOptions.ShowTimestamps = false; + }) + .AddConsoleFormatter(formatterOptions => { formatterOptions.IncludeScopes = false; formatterOptions.ShowSkipMessages = true; @@ -535,12 +546,38 @@ private static async Task ValidateConfigCoreAsync( JsonDocument configDoc; try { - configJson = await File.ReadAllTextAsync(resolvedConfigFile, cancellationToken); + var configText = await File.ReadAllTextAsync(resolvedConfigFile, cancellationToken); + + if (ProxyYaml.IsYamlFile(resolvedConfigFile)) + { + if (!ProxyYaml.TryConvertYamlToJson(configText, out var converted, out var yamlError)) + { + throw new JsonException($"Could not convert YAML configuration to JSON: {yamlError}"); + } + configJson = converted!; + } + else + { + configJson = configText; + } + configDoc = JsonDocument.Parse(configJson, ProxyUtils.JsonDocumentOptions); } catch (JsonException ex) { - errors.Add(new("configFile", $"Invalid JSON: {ex.Message}")); + errors.Add(new("configFile", $"Invalid configuration: {ex.Message}")); + WriteResults(isJsonOutput, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger); + return 2; + } + catch (YamlException ex) + { + errors.Add(new("configFile", $"Invalid YAML: {ex.Message}")); + WriteResults(isJsonOutput, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger); + return 2; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + errors.Add(new("configFile", $"Could not read configuration file: {ex.Message}")); WriteResults(isJsonOutput, resolvedConfigFile, errors, warnings, pluginNames, urlPatterns, logger); return 2; } @@ -625,9 +662,21 @@ private static void ValidatePlugins( foreach (var plugin in pluginsElement.EnumerateArray()) { - var name = plugin.TryGetProperty("name", out var nameProp) ? nameProp.GetString() : null; - var enabled = !plugin.TryGetProperty("enabled", out var enabledProp) || enabledProp.GetBoolean(); - var pluginPath = plugin.TryGetProperty("pluginPath", out var pathProp) ? pathProp.GetString() : null; + string? name; + bool enabled; + string? pluginPath; + try + { + name = plugin.TryGetProperty("name", out var nameProp) ? nameProp.GetString() : null; + enabled = !plugin.TryGetProperty("enabled", out var enabledProp) || enabledProp.GetBoolean(); + pluginPath = plugin.TryGetProperty("pluginPath", out var pathProp) ? pathProp.GetString() : null; + } + catch (InvalidOperationException ex) + { + errors.Add(new($"plugins[{i}]", $"Invalid plugin definition: {ex.Message}")); + i++; + continue; + } if (string.IsNullOrEmpty(name)) { @@ -702,6 +751,13 @@ private static void ValidateUrls( var i = 0; foreach (var url in urlsElement.EnumerateArray()) { + if (url.ValueKind != JsonValueKind.String) + { + errors.Add(new($"urlsToWatch[{i}]", $"Expected a string but got {url.ValueKind}")); + i++; + continue; + } + var pattern = url.GetString(); if (string.IsNullOrEmpty(pattern)) { From 1071b1fa1d5dbf077100d029dea3128fb2e175d9 Mon Sep 17 00:00:00 2001 From: Garry Trinder Date: Thu, 26 Feb 2026 12:15:25 +0000 Subject: [PATCH 9/9] Fix references to renamed OutputFormat/JsonConsoleFormatter after merge with main --- DevProxy/Commands/ConfigCommand.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs index a3ea15c1..976294b8 100644 --- a/DevProxy/Commands/ConfigCommand.cs +++ b/DevProxy/Commands/ConfigCommand.cs @@ -82,10 +82,10 @@ internal static async Task RunValidateStandaloneAsync(string[] args) configFile = args[i + 1]; i++; } - else if (string.Equals(args[i], DevProxyCommand.LogForOptionName, StringComparison.OrdinalIgnoreCase) && + else if (string.Equals(args[i], DevProxyCommand.OutputOptionName, StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length) { - isJsonOutput = string.Equals(args[i + 1], "machine", StringComparison.OrdinalIgnoreCase); + isJsonOutput = string.Equals(args[i + 1], "json", StringComparison.OrdinalIgnoreCase); i++; } } @@ -95,7 +95,7 @@ internal static async Task RunValidateStandaloneAsync(string[] args) new ProductInfoHeaderValue("dev-proxy", ProxyUtils.ProductVersion)); var formatterName = isJsonOutput - ? MachineConsoleFormatter.FormatterName + ? JsonConsoleFormatter.FormatterName : ProxyConsoleFormatter.DefaultCategoryName; using var loggerFactory = LoggerFactory.Create(builder => @@ -113,7 +113,7 @@ internal static async Task RunValidateStandaloneAsync(string[] args) formatterOptions.ShowSkipMessages = true; formatterOptions.ShowTimestamps = false; }) - .AddConsoleFormatter(formatterOptions => + .AddConsoleFormatter(formatterOptions => { formatterOptions.IncludeScopes = false; formatterOptions.ShowSkipMessages = true; @@ -178,8 +178,8 @@ private void ConfigureCommand() configValidateCommand.SetAction(async (parseResult, cancellationToken) => { var configFile = parseResult.GetValue(validateConfigFileOption); - var logFor = parseResult.GetValueOrDefault(DevProxyCommand.LogForOptionName); - var isJsonOutput = logFor == LogFor.Machine; + var output = parseResult.GetValueOrDefault(DevProxyCommand.OutputOptionName); + var isJsonOutput = output == OutputFormat.Json; return await ValidateConfigCoreAsync(configFile, isJsonOutput, _httpClient, _logger, cancellationToken); });