From c473bfb0af81a8c745bae94a14a57dad55bc981b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:05:07 +0000 Subject: [PATCH 1/7] Initial plan From 46095a4c9788a375b1f9794d610da7849cdc57bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:09:11 +0000 Subject: [PATCH 2/7] Support creating YAML config files via devproxy config new Co-authored-by: garrytrinder <11563347+garrytrinder@users.noreply.github.com> --- DevProxy/Commands/ConfigCommand.cs | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs index a089494b..e2fef97d 100644 --- a/DevProxy/Commands/ConfigCommand.cs +++ b/DevProxy/Commands/ConfigCommand.cs @@ -10,6 +10,7 @@ using System.Net.Http.Headers; using System.Text.Json; using System.Text.RegularExpressions; +using YamlDotNet.Serialization; namespace DevProxy.Commands; @@ -326,6 +327,13 @@ private async Task CreateConfigFileAsync(string name) } var snippetBody = GetSnippetBody(snippet.Body); + + var extension = Path.GetExtension(name).ToLowerInvariant(); + if (extension is ".yaml" or ".yml") + { + snippetBody = ConvertJsonToYaml(snippetBody!); + } + var targetFileName = GetTargetFileName(name); await File.WriteAllTextAsync(targetFileName, snippetBody); _logger.LogInformation("Config file created at {TargetFileName}", targetFileName); @@ -401,4 +409,29 @@ private static string GetTargetFolderPath(string appFolder, string configId) body = Regex.Replace(body, @"\$[0-9]+", ""); return body; } + + private static string ConvertJsonToYaml(string json) + { + var jsonDocument = JsonDocument.Parse(json); + var obj = ConvertJsonElement(jsonDocument.RootElement); + var serializer = new SerializerBuilder().Build(); + return serializer.Serialize(obj); + } + + private static object? ConvertJsonElement(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.Object => element.EnumerateObject() + .ToDictionary(p => (object)p.Name, p => ConvertJsonElement(p.Value)), + JsonValueKind.Array => element.EnumerateArray() + .Select(ConvertJsonElement) + .ToList(), + JsonValueKind.String => element.GetString(), + JsonValueKind.Number => element.TryGetInt64(out var l) ? (object)l : element.GetDouble(), + JsonValueKind.True => true, + JsonValueKind.False => false, + _ => null + }; + } } \ No newline at end of file From e24a670f7cde1fe368076353b56437e3436c000b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:28:22 +0000 Subject: [PATCH 3/7] Add optional --format to config new command Co-authored-by: garrytrinder <11563347+garrytrinder@users.noreply.github.com> --- DevProxy/Commands/ConfigCommand.cs | 113 +++++++++++++++++------------ 1 file changed, 67 insertions(+), 46 deletions(-) diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs index 8d50d992..ac971117 100644 --- a/DevProxy/Commands/ConfigCommand.cs +++ b/DevProxy/Commands/ConfigCommand.cs @@ -37,16 +37,22 @@ sealed class GitHubTreeItem public string Type { get; set; } = string.Empty; } -sealed class VisualStudioCodeSnippet -{ - public string? Prefix { get; set; } - public string[]? Body { get; set; } - public string? Description { get; set; } -} - -sealed class ConfigCommand : Command -{ - private readonly ILogger _logger; +sealed class VisualStudioCodeSnippet +{ + public string? Prefix { get; set; } + public string[]? Body { get; set; } + public string? Description { get; set; } +} + +sealed class ConfigCommand : Command +{ + private enum ConfigFileFormat + { + Json, + Yaml + } + + private readonly ILogger _logger; private readonly IProxyConfiguration _proxyConfiguration; private readonly HttpClient _httpClient; private readonly string snippetsFileUrl = $"https://aka.ms/devproxy/snippets/v{ProxyUtils.NormalizeVersion(ProxyUtils.ProductVersion)}"; @@ -145,19 +151,24 @@ private void ConfigureCommand() }); var configNewCommand = new Command("new", "Create new Dev Proxy configuration file"); - var nameArgument = new Argument("name") - { - Arity = ArgumentArity.ZeroOrOne, - DefaultValueFactory = _ => "devproxyrc.json", - Description = "Name of the configuration file" - }; - configNewCommand.Add(nameArgument); - configNewCommand.SetAction(async (parseResult) => - { - var name = parseResult.GetValue(nameArgument) ?? "devproxyrc.json"; - var outputFormat = parseResult.GetValueOrDefault(DevProxyCommand.OutputOptionName) ?? OutputFormat.Text; - await CreateConfigFileAsync(name, outputFormat); - }); + var nameArgument = new Argument("name") + { + Arity = ArgumentArity.ZeroOrOne, + Description = "Name of the configuration file. Defaults to devproxyrc.json (or devproxyrc.yaml with --format yaml)." + }; + var formatOption = new Option("--format") + { + Description = "Configuration format to use (json or yaml)" + }; + configNewCommand.Add(nameArgument); + configNewCommand.Add(formatOption); + configNewCommand.SetAction(async (parseResult) => + { + var format = parseResult.GetValue(formatOption); + var name = parseResult.GetValue(nameArgument) ?? GetDefaultConfigFileName(format); + var outputFormat = parseResult.GetValueOrDefault(DevProxyCommand.OutputOptionName) ?? OutputFormat.Text; + await CreateConfigFileAsync(name, format, outputFormat); + }); var configOpenCommand = new Command("open", "Open devproxyrc.json"); configOpenCommand.SetAction(parseResult => @@ -192,13 +203,14 @@ private void ConfigureCommand() configValidateCommand }.OrderByName()); - HelpExamples.Add(this, [ - "devproxy config new Create default devproxyrc.json", - "devproxy config new myconfig.json Create named config file", - "devproxy config get Download config from gallery", - "devproxy config open Open config in default editor", - ]); - } + HelpExamples.Add(this, [ + "devproxy config new Create default devproxyrc.json", + "devproxy config new --format yaml Create default devproxyrc.yaml", + "devproxy config new myconfig.json Create named config file", + "devproxy config get Download config from gallery", + "devproxy config open Open config in default editor", + ]); + } private async Task DownloadConfigAsync(string configId, OutputFormat outputFormat) { @@ -439,10 +451,10 @@ private async Task DownloadFileAsync(string filePath, string targetFolderPath, s } } - private async Task CreateConfigFileAsync(string name, OutputFormat outputFormat) - { - try - { + private async Task CreateConfigFileAsync(string name, ConfigFileFormat? format, OutputFormat outputFormat) + { + try + { var snippets = await DownloadSnippetsAsync(); if (snippets is null) { @@ -465,15 +477,15 @@ private async Task CreateConfigFileAsync(string name, OutputFormat outputFormat) _logger.LogError("Snippet {SnippetName} is empty", configFileSnippetName); } return; - } - - var snippetBody = GetSnippetBody(snippet.Body); - - var extension = Path.GetExtension(name).ToLowerInvariant(); - if (extension is ".yaml" or ".yml") - { - snippetBody = ConvertJsonToYaml(snippetBody!); - } + } + + var snippetBody = GetSnippetBody(snippet.Body); + + var selectedFormat = format ?? GetConfigFileFormatFromFileName(name); + if (selectedFormat == ConfigFileFormat.Yaml) + { + snippetBody = ConvertJsonToYaml(snippetBody!); + } var targetFileName = GetTargetFileName(name); await File.WriteAllTextAsync(targetFileName, snippetBody); @@ -497,8 +509,17 @@ private async Task CreateConfigFileAsync(string name, OutputFormat outputFormat) { _logger.LogError(ex, "Error downloading config"); } - } - } + } + } + + private static string GetDefaultConfigFileName(ConfigFileFormat? format) => + format == ConfigFileFormat.Yaml ? "devproxyrc.yaml" : "devproxyrc.json"; + + private static ConfigFileFormat GetConfigFileFormatFromFileName(string name) + { + var extension = Path.GetExtension(name).ToLowerInvariant(); + return extension is ".yaml" or ".yml" ? ConfigFileFormat.Yaml : ConfigFileFormat.Json; + } private async Task?> DownloadSnippetsAsync() { @@ -966,4 +987,4 @@ private static void WriteTextResults( } private sealed record ValidationMessage(string Path, string Message); -} \ No newline at end of file +} From aabf572538d1d3449b78253b441dd07c15f5c790 Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Fri, 27 Feb 2026 09:13:07 +0100 Subject: [PATCH 4/7] Update DevProxy/Commands/ConfigCommand.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- DevProxy/Commands/ConfigCommand.cs | 140 +++++++++++++++-------------- 1 file changed, 72 insertions(+), 68 deletions(-) diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs index ac971117..0b626599 100644 --- a/DevProxy/Commands/ConfigCommand.cs +++ b/DevProxy/Commands/ConfigCommand.cs @@ -37,22 +37,22 @@ sealed class GitHubTreeItem public string Type { get; set; } = string.Empty; } -sealed class VisualStudioCodeSnippet -{ - public string? Prefix { get; set; } - public string[]? Body { get; set; } - public string? Description { get; set; } -} - -sealed class ConfigCommand : Command -{ - private enum ConfigFileFormat - { - Json, - Yaml - } - - private readonly ILogger _logger; +sealed class VisualStudioCodeSnippet +{ + public string? Prefix { get; set; } + public string[]? Body { get; set; } + public string? Description { get; set; } +} + +sealed class ConfigCommand : Command +{ + private enum ConfigFileFormat + { + Json, + Yaml + } + + private readonly ILogger _logger; private readonly IProxyConfiguration _proxyConfiguration; private readonly HttpClient _httpClient; private readonly string snippetsFileUrl = $"https://aka.ms/devproxy/snippets/v{ProxyUtils.NormalizeVersion(ProxyUtils.ProductVersion)}"; @@ -151,24 +151,24 @@ private void ConfigureCommand() }); var configNewCommand = new Command("new", "Create new Dev Proxy configuration file"); - var nameArgument = new Argument("name") - { - Arity = ArgumentArity.ZeroOrOne, - Description = "Name of the configuration file. Defaults to devproxyrc.json (or devproxyrc.yaml with --format yaml)." - }; - var formatOption = new Option("--format") - { - Description = "Configuration format to use (json or yaml)" - }; - configNewCommand.Add(nameArgument); - configNewCommand.Add(formatOption); - configNewCommand.SetAction(async (parseResult) => - { - var format = parseResult.GetValue(formatOption); - var name = parseResult.GetValue(nameArgument) ?? GetDefaultConfigFileName(format); - var outputFormat = parseResult.GetValueOrDefault(DevProxyCommand.OutputOptionName) ?? OutputFormat.Text; - await CreateConfigFileAsync(name, format, outputFormat); - }); + var nameArgument = new Argument("name") + { + Arity = ArgumentArity.ZeroOrOne, + Description = "Name of the configuration file. Defaults to devproxyrc.json (or devproxyrc.yaml with --format yaml)." + }; + var formatOption = new Option("--format") + { + Description = "Configuration format to use (json or yaml)" + }; + configNewCommand.Add(nameArgument); + configNewCommand.Add(formatOption); + configNewCommand.SetAction(async (parseResult) => + { + var format = parseResult.GetValue(formatOption); + var name = parseResult.GetValue(nameArgument) ?? GetDefaultConfigFileName(format); + var outputFormat = parseResult.GetValueOrDefault(DevProxyCommand.OutputOptionName) ?? OutputFormat.Text; + await CreateConfigFileAsync(name, format, outputFormat); + }); var configOpenCommand = new Command("open", "Open devproxyrc.json"); configOpenCommand.SetAction(parseResult => @@ -203,14 +203,14 @@ private void ConfigureCommand() configValidateCommand }.OrderByName()); - HelpExamples.Add(this, [ - "devproxy config new Create default devproxyrc.json", - "devproxy config new --format yaml Create default devproxyrc.yaml", - "devproxy config new myconfig.json Create named config file", - "devproxy config get Download config from gallery", - "devproxy config open Open config in default editor", - ]); - } + HelpExamples.Add(this, [ + "devproxy config new Create default devproxyrc.json", + "devproxy config new --format yaml Create default devproxyrc.yaml", + "devproxy config new myconfig.json Create named config file", + "devproxy config get Download config from gallery", + "devproxy config open Open config in default editor", + ]); + } private async Task DownloadConfigAsync(string configId, OutputFormat outputFormat) { @@ -451,10 +451,10 @@ private async Task DownloadFileAsync(string filePath, string targetFolderPath, s } } - private async Task CreateConfigFileAsync(string name, ConfigFileFormat? format, OutputFormat outputFormat) - { - try - { + private async Task CreateConfigFileAsync(string name, ConfigFileFormat? format, OutputFormat outputFormat) + { + try + { var snippets = await DownloadSnippetsAsync(); if (snippets is null) { @@ -477,15 +477,15 @@ private async Task CreateConfigFileAsync(string name, ConfigFileFormat? format, _logger.LogError("Snippet {SnippetName} is empty", configFileSnippetName); } return; - } - - var snippetBody = GetSnippetBody(snippet.Body); - - var selectedFormat = format ?? GetConfigFileFormatFromFileName(name); - if (selectedFormat == ConfigFileFormat.Yaml) - { - snippetBody = ConvertJsonToYaml(snippetBody!); - } + } + + var snippetBody = GetSnippetBody(snippet.Body); + + var selectedFormat = format ?? GetConfigFileFormatFromFileName(name); + if (selectedFormat == ConfigFileFormat.Yaml) + { + snippetBody = ConvertJsonToYaml(snippetBody!); + } var targetFileName = GetTargetFileName(name); await File.WriteAllTextAsync(targetFileName, snippetBody); @@ -509,17 +509,17 @@ private async Task CreateConfigFileAsync(string name, ConfigFileFormat? format, { _logger.LogError(ex, "Error downloading config"); } - } - } - - private static string GetDefaultConfigFileName(ConfigFileFormat? format) => - format == ConfigFileFormat.Yaml ? "devproxyrc.yaml" : "devproxyrc.json"; - - private static ConfigFileFormat GetConfigFileFormatFromFileName(string name) - { - var extension = Path.GetExtension(name).ToLowerInvariant(); - return extension is ".yaml" or ".yml" ? ConfigFileFormat.Yaml : ConfigFileFormat.Json; - } + } + } + + private static string GetDefaultConfigFileName(ConfigFileFormat? format) => + format == ConfigFileFormat.Yaml ? "devproxyrc.yaml" : "devproxyrc.json"; + + private static ConfigFileFormat GetConfigFileFormatFromFileName(string name) + { + var extension = Path.GetExtension(name).ToLowerInvariant(); + return extension is ".yaml" or ".yml" ? ConfigFileFormat.Yaml : ConfigFileFormat.Json; + } private async Task?> DownloadSnippetsAsync() { @@ -605,7 +605,11 @@ private static string ConvertJsonToYaml(string json) .Select(ConvertJsonElement) .ToList(), JsonValueKind.String => element.GetString(), - JsonValueKind.Number => element.TryGetInt64(out var l) ? (object)l : element.GetDouble(), + JsonValueKind.Number => element.TryGetInt64(out var l) + ? (object)l + : element.TryGetUInt64(out var ul) + ? (object)ul + : element.GetDouble(), JsonValueKind.True => true, JsonValueKind.False => false, _ => null @@ -987,4 +991,4 @@ private static void WriteTextResults( } private sealed record ValidationMessage(string Path, string Message); -} +} From f01c612d4d8202caf78208371ba849eba46487df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 08:14:44 +0000 Subject: [PATCH 5/7] Dispose JsonDocument in YAML conversion helper Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> --- DevProxy/Commands/ConfigCommand.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs index 0b626599..e4dc3b76 100644 --- a/DevProxy/Commands/ConfigCommand.cs +++ b/DevProxy/Commands/ConfigCommand.cs @@ -587,13 +587,13 @@ private static string GetTargetFolderPath(string appFolder, string configId) return body; } - private static string ConvertJsonToYaml(string json) - { - var jsonDocument = JsonDocument.Parse(json); - var obj = ConvertJsonElement(jsonDocument.RootElement); - var serializer = new SerializerBuilder().Build(); - return serializer.Serialize(obj); - } + private static string ConvertJsonToYaml(string json) + { + using var jsonDocument = JsonDocument.Parse(json); + var obj = ConvertJsonElement(jsonDocument.RootElement); + var serializer = new SerializerBuilder().Build(); + return serializer.Serialize(obj); + } private static object? ConvertJsonElement(JsonElement element) { From 0d9ecd2f2d4e9c2ff713ee0081ca30c1bf4e3adc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:28:04 +0000 Subject: [PATCH 6/7] Add missing blank line between ConfigCommand helper methods Co-authored-by: garrytrinder <11563347+garrytrinder@users.noreply.github.com> --- DevProxy/Commands/ConfigCommand.cs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs index e4dc3b76..ba3c3148 100644 --- a/DevProxy/Commands/ConfigCommand.cs +++ b/DevProxy/Commands/ConfigCommand.cs @@ -598,11 +598,11 @@ private static string ConvertJsonToYaml(string json) private static object? ConvertJsonElement(JsonElement element) { return element.ValueKind switch - { - JsonValueKind.Object => element.EnumerateObject() - .ToDictionary(p => (object)p.Name, p => ConvertJsonElement(p.Value)), - JsonValueKind.Array => element.EnumerateArray() - .Select(ConvertJsonElement) + { + JsonValueKind.Object => element.EnumerateObject() + .ToDictionary(p => (object)p.Name, p => ConvertJsonElement(p.Value)), + JsonValueKind.Array => element.EnumerateArray() + .Select(ConvertJsonElement) .ToList(), JsonValueKind.String => element.GetString(), JsonValueKind.Number => element.TryGetInt64(out var l) @@ -612,13 +612,14 @@ private static string ConvertJsonToYaml(string json) : element.GetDouble(), JsonValueKind.True => true, JsonValueKind.False => false, - _ => null - }; - } - private static string? ResolveConfigFile(string? configFilePath) - { - if (!string.IsNullOrEmpty(configFilePath)) - { + _ => null + }; + } + + private static string? ResolveConfigFile(string? configFilePath) + { + if (!string.IsNullOrEmpty(configFilePath)) + { var resolved = Path.GetFullPath(ProxyUtils.ReplacePathTokens(configFilePath)); return File.Exists(resolved) ? resolved : null; } From 23c3cf5c385f116527be26a08e3152021a61e198 Mon Sep 17 00:00:00 2001 From: Garry Trinder Date: Wed, 4 Mar 2026 09:36:12 +0000 Subject: [PATCH 7/7] Use format-specific snippets URLs for config new command Download snippets from /json or /yaml endpoint instead of a single URL. Removes client-side JSON-to-YAML conversion since YAML snippets are now served natively. Closes #1559 --- DevProxy/Commands/ConfigCommand.cs | 52 ++++++------------------------ 1 file changed, 10 insertions(+), 42 deletions(-) diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs index ba3c3148..c502bc8f 100644 --- a/DevProxy/Commands/ConfigCommand.cs +++ b/DevProxy/Commands/ConfigCommand.cs @@ -14,7 +14,6 @@ using System.Reflection; using System.Text.Json; using System.Text.RegularExpressions; -using YamlDotNet.Serialization; using YamlDotNet.Core; namespace DevProxy.Commands; @@ -55,7 +54,7 @@ private enum ConfigFileFormat private readonly ILogger _logger; private readonly IProxyConfiguration _proxyConfiguration; private readonly HttpClient _httpClient; - private readonly string snippetsFileUrl = $"https://aka.ms/devproxy/snippets/v{ProxyUtils.NormalizeVersion(ProxyUtils.ProductVersion)}"; + private readonly string snippetsBaseUrl = $"https://aka.ms/devproxy/snippets/v{ProxyUtils.NormalizeVersion(ProxyUtils.ProductVersion)}"; private readonly string configFileSnippetName = "ConfigFile"; public ConfigCommand( HttpClient httpClient, @@ -455,7 +454,9 @@ private async Task CreateConfigFileAsync(string name, ConfigFileFormat? format, { try { - var snippets = await DownloadSnippetsAsync(); + var selectedFormat = format ?? GetConfigFileFormatFromFileName(name); + + var snippets = await DownloadSnippetsAsync(selectedFormat); if (snippets is null) { return; @@ -481,12 +482,6 @@ private async Task CreateConfigFileAsync(string name, ConfigFileFormat? format, var snippetBody = GetSnippetBody(snippet.Body); - var selectedFormat = format ?? GetConfigFileFormatFromFileName(name); - if (selectedFormat == ConfigFileFormat.Yaml) - { - snippetBody = ConvertJsonToYaml(snippetBody!); - } - var targetFileName = GetTargetFileName(name); await File.WriteAllTextAsync(targetFileName, snippetBody); @@ -521,8 +516,10 @@ private static ConfigFileFormat GetConfigFileFormatFromFileName(string name) return extension is ".yaml" or ".yml" ? ConfigFileFormat.Yaml : ConfigFileFormat.Json; } - private async Task?> DownloadSnippetsAsync() + private async Task?> DownloadSnippetsAsync(ConfigFileFormat format) { + var formatSuffix = format == ConfigFileFormat.Yaml ? "yaml" : "json"; + var snippetsFileUrl = $"{snippetsBaseUrl}/{formatSuffix}"; _logger.LogDebug("Downloading snippets from {SnippetsFileUrl}...", snippetsFileUrl); var response = await _httpClient.GetAsync(new Uri(snippetsFileUrl)); if (response.IsSuccessStatusCode) @@ -587,39 +584,10 @@ private static string GetTargetFolderPath(string appFolder, string configId) return body; } - private static string ConvertJsonToYaml(string json) - { - using var jsonDocument = JsonDocument.Parse(json); - var obj = ConvertJsonElement(jsonDocument.RootElement); - var serializer = new SerializerBuilder().Build(); - return serializer.Serialize(obj); - } - - private static object? ConvertJsonElement(JsonElement element) + private static string? ResolveConfigFile(string? configFilePath) { - return element.ValueKind switch - { - JsonValueKind.Object => element.EnumerateObject() - .ToDictionary(p => (object)p.Name, p => ConvertJsonElement(p.Value)), - JsonValueKind.Array => element.EnumerateArray() - .Select(ConvertJsonElement) - .ToList(), - JsonValueKind.String => element.GetString(), - JsonValueKind.Number => element.TryGetInt64(out var l) - ? (object)l - : element.TryGetUInt64(out var ul) - ? (object)ul - : element.GetDouble(), - JsonValueKind.True => true, - JsonValueKind.False => false, - _ => null - }; - } - - private static string? ResolveConfigFile(string? configFilePath) - { - if (!string.IsNullOrEmpty(configFilePath)) - { + if (!string.IsNullOrEmpty(configFilePath)) + { var resolved = Path.GetFullPath(ProxyUtils.ReplacePathTokens(configFilePath)); return File.Exists(resolved) ? resolved : null; }