From 1bd85879fbc0de80a50375c9640a02a24b2bd625 Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Tue, 1 Jul 2025 16:51:06 +0200 Subject: [PATCH] Upgrades to System.CommandLine beta 5. Closes #1265 --- .../DevProxy.Abstractions.csproj | 2 +- .../Extensions/CommandLineExtensions.cs | 22 +- DevProxy.Abstractions/Proxy/ProxyEvents.cs | 9 +- DevProxy.Abstractions/packages.lock.json | 6 +- .../Behavior/GenericRandomErrorPlugin.cs | 20 +- .../Behavior/GraphRandomErrorPlugin.cs | 30 +- DevProxy.Plugins/DevProxy.Plugins.csproj | 2 +- .../Mocking/MockResponsePlugin.cs | 17 +- .../Reporting/ExecutionSummaryPlugin.cs | 13 +- DevProxy.Plugins/packages.lock.json | 8 +- DevProxy/Commands/CertCommand.cs | 14 +- DevProxy/Commands/ConfigCommand.cs | 33 +- DevProxy/Commands/DevProxyCommand.cs | 563 ++++++------------ DevProxy/Commands/DevProxyConfigOptions.cs | 131 ++++ DevProxy/Commands/JwtBinder.cs | 36 -- DevProxy/Commands/JwtCommand.cs | 94 +-- DevProxy/Commands/MsGraphDbCommand.cs | 5 +- DevProxy/Commands/OutdatedCommand.cs | 13 +- DevProxy/DevProxy.csproj | 2 +- .../ConfigurationManagerExtensions.cs | 4 +- .../Extensions/ILoggingBuilderExtensions.cs | 5 +- .../IServiceCollectionExtensions.cs | 9 +- DevProxy/Plugins/PluginServiceExtensions.cs | 6 +- DevProxy/Program.cs | 18 +- DevProxy/packages.lock.json | 8 +- 25 files changed, 521 insertions(+), 549 deletions(-) create mode 100644 DevProxy/Commands/DevProxyConfigOptions.cs delete mode 100644 DevProxy/Commands/JwtBinder.cs diff --git a/DevProxy.Abstractions/DevProxy.Abstractions.csproj b/DevProxy.Abstractions/DevProxy.Abstractions.csproj index a614e4b0..88db4c91 100644 --- a/DevProxy.Abstractions/DevProxy.Abstractions.csproj +++ b/DevProxy.Abstractions/DevProxy.Abstractions.csproj @@ -22,7 +22,7 @@ - + diff --git a/DevProxy.Abstractions/Extensions/CommandLineExtensions.cs b/DevProxy.Abstractions/Extensions/CommandLineExtensions.cs index 5671c9c1..3a78fa8b 100644 --- a/DevProxy.Abstractions/Extensions/CommandLineExtensions.cs +++ b/DevProxy.Abstractions/Extensions/CommandLineExtensions.cs @@ -8,7 +8,7 @@ namespace System.CommandLine.Parsing; public static class CommandLineExtensions { - public static T? GetValueForOption(this ParseResult parseResult, string optionName, IReadOnlyList - + false runtime diff --git a/DevProxy.Plugins/Mocking/MockResponsePlugin.cs b/DevProxy.Plugins/Mocking/MockResponsePlugin.cs index 83fb0515..00abeeaa 100644 --- a/DevProxy.Plugins/Mocking/MockResponsePlugin.cs +++ b/DevProxy.Plugins/Mocking/MockResponsePlugin.cs @@ -72,15 +72,16 @@ public override async Task InitializeAsync(InitArgs e) public override Option[] GetOptions() { - var _noMocks = new Option(_noMocksOptionName, "Disable loading mock requests") + var _noMocks = new Option(_noMocksOptionName, "-n") { - ArgumentHelpName = "no mocks" + Description = "Disable loading mock requests", + HelpName = "no-mocks" }; - _noMocks.AddAlias("-n"); - var _mocksFile = new Option(_mocksFileOptionName, "Provide a file populated with mock responses") + var _mocksFile = new Option(_mocksFileOptionName) { - ArgumentHelpName = "mocks file" + Description = "Provide a file populated with mock responses", + HelpName = "mocks-file" }; return [_noMocks, _mocksFile]; @@ -92,10 +93,10 @@ public override void OptionsLoaded(OptionsLoadedArgs e) base.OptionsLoaded(e); - var context = e.Context; + var parseResult = e.ParseResult; // allow disabling of mocks as a command line option - var noMocks = context.ParseResult.GetValueForOption(_noMocksOptionName, e.Options); + var noMocks = parseResult.GetValueOrDefault(_noMocksOptionName); if (noMocks.HasValue) { Configuration.NoMocks = noMocks.Value; @@ -107,7 +108,7 @@ public override void OptionsLoaded(OptionsLoadedArgs e) } // update the name of the mocks file to load from if supplied - var mocksFile = context.ParseResult.GetValueForOption(_mocksFileOptionName, e.Options); + var mocksFile = parseResult.GetValueOrDefault(_mocksFileOptionName); if (mocksFile is not null) { Configuration.MocksFile = mocksFile; diff --git a/DevProxy.Plugins/Reporting/ExecutionSummaryPlugin.cs b/DevProxy.Plugins/Reporting/ExecutionSummaryPlugin.cs index a65a95ee..5cde5f0c 100644 --- a/DevProxy.Plugins/Reporting/ExecutionSummaryPlugin.cs +++ b/DevProxy.Plugins/Reporting/ExecutionSummaryPlugin.cs @@ -42,15 +42,16 @@ public sealed class ExecutionSummaryPlugin( public override Option[] GetOptions() { - var groupBy = new Option(_groupByOptionName, "Specifies how the information should be grouped in the summary. Available options: `url` (default), `messageType`.") + var groupBy = new Option(_groupByOptionName) { - ArgumentHelpName = "summary-group-by" + Description = "Specifies how the information should be grouped in the summary. Available options: `url` (default), `messageType`.", + HelpName = "summary-group-by" }; - groupBy.AddValidator(input => + groupBy.Validators.Add(input => { if (!Enum.TryParse(input.Tokens[0].Value, true, out var groupBy)) { - input.ErrorMessage = $"{input.Tokens[0].Value} is not a valid option to group by. Allowed values are: {string.Join(", ", Enum.GetNames())}"; + input.AddError($"{input.Tokens[0].Value} is not a valid option to group by. Allowed values are: {string.Join(", ", Enum.GetNames())}"); } }); @@ -63,9 +64,9 @@ public override void OptionsLoaded(OptionsLoadedArgs e) base.OptionsLoaded(e); - var context = e.Context; + var parseResult = e.ParseResult; - var groupBy = context.ParseResult.GetValueForOption(_groupByOptionName, e.Options); + var groupBy = parseResult.GetValueOrDefault(_groupByOptionName); if (groupBy is not null) { Configuration.GroupBy = groupBy.Value; diff --git a/DevProxy.Plugins/packages.lock.json b/DevProxy.Plugins/packages.lock.json index 1de6afe8..7e1dcbf5 100644 --- a/DevProxy.Plugins/packages.lock.json +++ b/DevProxy.Plugins/packages.lock.json @@ -85,9 +85,9 @@ }, "System.CommandLine": { "type": "Direct", - "requested": "[2.0.0-beta4.22272.1, )", - "resolved": "2.0.0-beta4.22272.1", - "contentHash": "1uqED/q2H0kKoLJ4+hI2iPSBSEdTuhfCYADeJrAqERmiGQ2NNacYKRNEQ+gFbU4glgVyK8rxI+ZOe1onEtr/Pg==" + "requested": "[2.0.0-beta5.25306.1, )", + "resolved": "2.0.0-beta5.25306.1", + "contentHash": "ce0wuowuh13Cd7GXqLCq77/YWlxQMxrVCMIO/2/QUP6CdP/JWnlYSN/N3/55wwGsUwa9CvPuT8ddjgyypUr5ag==" }, "System.IdentityModel.Tokens.Jwt": { "type": "Direct", @@ -603,7 +603,7 @@ "Microsoft.OpenApi.Readers": "[1.6.24, )", "Newtonsoft.Json.Schema": "[4.0.1, )", "Prompty.Core": "[0.2.2-beta, )", - "System.CommandLine": "[2.0.0-beta4.22272.1, )", + "System.CommandLine": "[2.0.0-beta5.25306.1, )", "Unobtanium.Web.Proxy": "[0.1.5, )" } } diff --git a/DevProxy/Commands/CertCommand.cs b/DevProxy/Commands/CertCommand.cs index fe4607bf..06c05240 100644 --- a/DevProxy/Commands/CertCommand.cs +++ b/DevProxy/Commands/CertCommand.cs @@ -5,7 +5,6 @@ using DevProxy.Abstractions.Utils; using DevProxy.Proxy; using System.CommandLine; -using System.CommandLine.Invocation; using System.CommandLine.Parsing; using System.Diagnostics; using Titanium.Web.Proxy.Helpers; @@ -15,7 +14,10 @@ namespace DevProxy.Commands; sealed class CertCommand : Command { private readonly ILogger _logger; - private readonly Option _forceOption = new(["--force", "-f"], "Don't prompt for confirmation when removing the certificate"); + private readonly Option _forceOption = new("--force", "-f") + { + Description = "Don't prompt for confirmation when removing the certificate" + }; public CertCommand(ILogger logger) : base("cert", "Manage the Dev Proxy certificate") @@ -28,10 +30,10 @@ public CertCommand(ILogger logger) : private void ConfigureCommand() { var certEnsureCommand = new Command("ensure", "Ensure certificates are setup (creates root if required). Also makes root certificate trusted."); - certEnsureCommand.SetHandler(EnsureCertAsync); + certEnsureCommand.SetAction(async _ => await EnsureCertAsync()); var certRemoveCommand = new Command("remove", "Remove the certificate from Root Store"); - certRemoveCommand.SetHandler(RemoveCert); + certRemoveCommand.SetAction(RemoveCert); certRemoveCommand.AddOptions(new[] { _forceOption }.OrderByName()); this.AddCommands(new List @@ -59,13 +61,13 @@ private async Task EnsureCertAsync() _logger.LogTrace("EnsureCertAsync() finished"); } - public void RemoveCert(InvocationContext invocationContext) + public void RemoveCert(ParseResult parseResult) { _logger.LogTrace("RemoveCert() called"); try { - var isForced = invocationContext.ParseResult.GetValueForOption(_forceOption); + var isForced = parseResult.GetValue(_forceOption); if (!isForced) { var isConfirmed = PromptConfirmation("Do you want to remove the root certificate", acceptByDefault: false); diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs index a37a075a..478c68bc 100644 --- a/DevProxy/Commands/ConfigCommand.cs +++ b/DevProxy/Commands/ConfigCommand.cs @@ -62,21 +62,36 @@ public ConfigCommand( private void ConfigureCommand() { var configGetCommand = new Command("get", "Download the specified config from the Sample Solution Gallery"); - var configIdArgument = new Argument("config-id", "The ID of the config to download"); - configGetCommand.AddArgument(configIdArgument); - configGetCommand.SetHandler(DownloadConfigAsync, configIdArgument); + var configIdArgument = new Argument("config-id") + { + Description = "The ID of the config to download" + }; + configGetCommand.Add(configIdArgument); + configGetCommand.SetAction(async (parseResult) => + { + var configId = parseResult.GetValue(configIdArgument); + if (configId != null) + { + await DownloadConfigAsync(configId); + } + }); var configNewCommand = new Command("new", "Create new Dev Proxy configuration file"); - var nameArgument = new Argument("name", "Name of the configuration file") + var nameArgument = new Argument("name") { - Arity = ArgumentArity.ZeroOrOne + Arity = ArgumentArity.ZeroOrOne, + DefaultValueFactory = _ => "devproxyrc.json", + Description = "Name of the configuration file" }; - nameArgument.SetDefaultValue("devproxyrc.json"); - configNewCommand.AddArgument(nameArgument); - configNewCommand.SetHandler(CreateConfigFileAsync, nameArgument); + configNewCommand.Add(nameArgument); + configNewCommand.SetAction(async (parseResult) => + { + var name = parseResult.GetValue(nameArgument) ?? "devproxyrc.json"; + await CreateConfigFileAsync(name); + }); var configOpenCommand = new Command("open", "Open devproxyrc.json"); - configOpenCommand.SetHandler(() => + configOpenCommand.SetAction(parseResult => { var cfgPsi = new ProcessStartInfo(_proxyConfiguration.ConfigFile) { diff --git a/DevProxy/Commands/DevProxyCommand.cs b/DevProxy/Commands/DevProxyCommand.cs index 0d658eb6..aca23368 100644 --- a/DevProxy/Commands/DevProxyCommand.cs +++ b/DevProxy/Commands/DevProxyCommand.cs @@ -2,9 +2,7 @@ using DevProxy.Abstractions.Proxy; using DevProxy.Abstractions.Utils; using System.CommandLine; -using System.CommandLine.Invocation; using System.CommandLine.Parsing; -using Microsoft.VisualStudio.Threading; namespace DevProxy.Commands; @@ -19,249 +17,19 @@ sealed class DevProxyCommand : RootCommand private WebApplication? _app; internal const string PortOptionName = "--port"; - private Option? _portOption; internal const string IpAddressOptionName = "--ip-address"; - private static Option? _ipAddressOption; internal const string LogLevelOptionName = "--log-level"; - private static Option? _logLevelOption; internal const string RecordOptionName = "--record"; - private Option? _recordOption; internal const string WatchPidsOptionName = "--watch-pids"; - private Option>? _watchPidsOption; internal const string WatchProcessNamesOptionName = "--watch-process-names"; - private Option>? _watchProcessNamesOption; internal const string ConfigFileOptionName = "--config-file"; - private static Option? _configFileOption; internal const string NoFirstRunOptionName = "--no-first-run"; - private Option? _noFirstRunOption; internal const string AsSystemProxyOptionName = "--as-system-proxy"; - private Option? _asSystemProxyOption; internal const string InstallCertOptionName = "--install-cert"; - private Option? _installCertOption; internal const string UrlsToWatchOptionName = "--urls-to-watch"; - private static Option?>? _urlsToWatchOption; internal const string TimeoutOptionName = "--timeout"; - private Option? _timeoutOption; internal const string DiscoverOptionName = "--discover"; - private Option? _discoverOption; internal const string EnvOptionName = "--env"; - private Option? _envOption; - - public static string? ConfigFile - { - get - { - if (_configFileOption is null) - { - _configFileOption = new Option(ConfigFileOptionName, "The path to the configuration file"); - _configFileOption.AddAlias("-c"); - _configFileOption.ArgumentHelpName = "configFile"; - _configFileOption.AddValidator(input => - { - var filePath = ProxyUtils.ReplacePathTokens(input.Tokens[0].Value); - if (string.IsNullOrEmpty(filePath)) - { - return; - } - - if (!File.Exists(filePath)) - { - input.ErrorMessage = $"Configuration file {filePath} does not exist"; - } - }); - } - - var result = _configFileOption.Parse(Environment.GetCommandLineArgs()); - // since we're parsing all args, and other options are not instantiated yet - // we're getting here a bunch of other errors, so we only need to look for - // errors related to the config file option - var error = result.Errors.FirstOrDefault(e => e.SymbolResult?.Symbol == _configFileOption); - if (error is not null) - { - // Logger is not available here yet so we need to fallback to Console - var color = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.Red; - Console.Error.WriteLine(error.Message); - Console.ForegroundColor = color; - Environment.Exit(1); - } - - var configFile = result.GetValueForOption(_configFileOption); - return configFile is not null ? - Path.GetFullPath(ProxyUtils.ReplacePathTokens(configFile)) : - null; - } - } - - private static bool _logLevelResolved; - private static LogLevel? _logLevel; - public static LogLevel? LogLevel - { - get - { - if (_logLevelResolved) - { - return _logLevel; - } - - if (_logLevelOption is null) - { - _logLevelOption = new Option( - LogLevelOptionName, - $"Level of messages to log. Allowed values: {string.Join(", ", Enum.GetNames())}" - ) - { - ArgumentHelpName = "logLevel" - }; - _logLevelOption.AddValidator(input => - { - if (!Enum.TryParse(input.Tokens[0].Value, true, out _)) - { - input.ErrorMessage = $"{input.Tokens[0].Value} is not a valid log level. Allowed values are: {string.Join(", ", Enum.GetNames())}"; - } - }); - } - - var result = _logLevelOption.Parse(Environment.GetCommandLineArgs()); - // since we're parsing all args, and other options are not instantiated yet - // we're getting here a bunch of other errors, so we only need to look for - // errors related to the log level option - var error = result.Errors.FirstOrDefault(e => e.SymbolResult?.Symbol == _logLevelOption); - if (error is not null) - { - // Logger is not available here yet so we need to fallback to Console - var color = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.Red; - Console.Error.WriteLine(error.Message); - Console.ForegroundColor = color; - Environment.Exit(1); - } - - _logLevel = result.GetValueForOption(_logLevelOption); - _logLevelResolved = true; - - return _logLevel; - } - } - - private static bool _ipAddressResolved; - private static string? _ipAddress; - public static string? IPAddress - { - get - { - if (_ipAddressResolved) - { - return _ipAddress; - } - - if (_ipAddressOption is null) - { - _ipAddressOption = new(IpAddressOptionName, "The IP address for the proxy to bind to") - { - ArgumentHelpName = "ipAddress" - }; - _ipAddressOption.AddValidator(input => - { - if (!System.Net.IPAddress.TryParse(input.Tokens[0].Value, out _)) - { - input.ErrorMessage = $"{input.Tokens[0].Value} is not a valid IP address"; - } - }); - } - - var result = _ipAddressOption.Parse(Environment.GetCommandLineArgs()); - // since we're parsing all args, and other options are not instantiated yet - // we're getting here a bunch of other errors, so we only need to look for - // errors related to the log level option - var error = result.Errors.FirstOrDefault(e => e.SymbolResult?.Symbol == _ipAddressOption); - if (error is not null) - { - // Logger is not available here yet so we need to fallback to Console - var color = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.Red; - Console.Error.WriteLine(error.Message); - Console.ForegroundColor = color; - Environment.Exit(1); - } - - _ipAddress = result.GetValueForOption(_ipAddressOption); - _ipAddressResolved = true; - - return _ipAddress; - } - } - - private static bool urlsToWatchResolved; - private static List? urlsToWatch; - public static List? UrlsToWatch - { - get - { - if (urlsToWatchResolved) - { - return urlsToWatch; - } - - if (_urlsToWatchOption is null) - { - _urlsToWatchOption = new Option?>( - UrlsToWatchOptionName, - "The list of URLs to watch for requests" - ) - { - ArgumentHelpName = "urlsToWatch", - AllowMultipleArgumentsPerToken = true, - Arity = ArgumentArity.ZeroOrMore - }; - _urlsToWatchOption.AddAlias("-u"); - } - - var result = _urlsToWatchOption!.Parse(Environment.GetCommandLineArgs()); - // since we're parsing all args, and other options are not instantiated yet - // we're getting here a bunch of other errors, so we only need to look for - // errors related to the log level option - var error = result.Errors.FirstOrDefault(e => e.SymbolResult?.Symbol == _urlsToWatchOption); - if (error is not null) - { - // Logger is not available here yet so we need to fallback to Console - var color = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.Red; - Console.Error.WriteLine(error.Message); - Console.ForegroundColor = color; - Environment.Exit(1); - } - - urlsToWatch = result.GetValueForOption(_urlsToWatchOption!); - if (urlsToWatch is not null && urlsToWatch.Count == 0) - { - urlsToWatch = null; - } - urlsToWatchResolved = true; - - return urlsToWatch; - } - } - - private static bool _isRootCommandResolved; - private static bool _isRootCommand; - public static bool IsRootCommand - { - get - { - if (_isRootCommandResolved) - { - return _isRootCommand; - } - - // Check if the command is being invoked as the root command - // by checking if the second argument is an option - var args = Environment.GetCommandLineArgs(); - _isRootCommand = args.Length == 1 || args[1].StartsWith('-'); - _isRootCommandResolved = true; - return _isRootCommand; - } - } private static readonly string[] globalOptions = ["--version"]; private static readonly string[] helpOptions = ["--help", "-h", "/h", "-?", "/?"]; @@ -291,105 +59,229 @@ public DevProxyCommand( IProxyConfiguration proxyConfiguration, IServiceProvider serviceProvider, UpdateNotification updateNotification, - ILogger logger) + ILogger logger) : base("Start Dev Proxy") { + _serviceProvider = serviceProvider; _plugins = plugins; _urlsToWatch = urlsToWatch; _proxyConfiguration = proxyConfiguration; - _serviceProvider = serviceProvider; - _logger = logger; _updateNotification = updateNotification; + _logger = logger; ConfigureCommand(); } - private void ConfigureCommand() + public async Task InvokeAsync(string[] args, WebApplication app) { - _portOption = new(PortOptionName, "The port for the proxy to listen on"); - _portOption.AddAlias("-p"); - _portOption.ArgumentHelpName = "port"; + _app = app; + var parseResult = Parse(args); + return await parseResult.InvokeAsync(app.Lifetime.ApplicationStopping); + } - _recordOption = new(RecordOptionName, "Use this option to record all request logs"); + private async Task InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken) + { + if (_app is null) + { + throw new InvalidOperationException("WebApplication instance is not set. Please provide it when invoking the command."); + } + if (!_plugins.Any()) + { + _logger.LogError("You haven't configured any plugins. Please add plugins to your configuration file. Dev Proxy will exit."); + return 1; + } + if (_urlsToWatch.Count == 0) + { + _logger.LogError("You haven't configured any URLs to watch. Please add URLs to your configuration file or use the --urls-to-watch option. Dev Proxy will exit."); + return 1; + } - _watchPidsOption = new(WatchPidsOptionName, "The IDs of processes to watch for requests") + ConfigureFromOptions(parseResult); + var optionsLoadedArgs = new OptionsLoadedArgs(parseResult); + foreach (var plugin in _plugins.Where(p => p.Enabled)) { - ArgumentHelpName = "pids", - AllowMultipleArgumentsPerToken = true - }; + plugin.OptionsLoaded(optionsLoadedArgs); + } + + await CheckForNewVersionAsync(); - _watchProcessNamesOption = new(WatchProcessNamesOptionName, "The names of processes to watch for requests") + try { - ArgumentHelpName = "processNames", - AllowMultipleArgumentsPerToken = true - }; + var ipAddress = parseResult.GetValue(IpAddressOptionName) ?? _proxyConfiguration.IPAddress; + _logger.LogInformation("Dev Proxy API listening on http://{IPAddress}:{Port}...", ipAddress, _proxyConfiguration.ApiPort); + await _app.RunAsync(cancellationToken); - _noFirstRunOption = new(NoFirstRunOptionName, "Skip the first run experience"); + return 0; + } + catch (TaskCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while running Dev Proxy"); + var inner = ex.InnerException; - _discoverOption = new(DiscoverOptionName, "Run Dev Proxy in discovery mode"); + while (inner is not null) + { + _logger.LogError(inner, "============ Inner exception ============"); + inner = inner.InnerException; + } +#if DEBUG + throw; // so debug tools go straight to the source of the exception when attached +#else + return 1; +#endif + } + } - _asSystemProxyOption = new(AsSystemProxyOptionName, "Set Dev Proxy as the system proxy"); - _asSystemProxyOption.AddValidator(input => + private void ConfigureCommand() + { + var configFileOption = new Option(ConfigFileOptionName, "-c") { - try + HelpName = "config-file", + Description = "The path to the configuration file" + }; + configFileOption.Validators.Add(input => + { + var filePath = ProxyUtils.ReplacePathTokens(input.Tokens[0].Value); + if (string.IsNullOrEmpty(filePath)) { - _ = input.GetValueForOption(_asSystemProxyOption); + return; } - catch (InvalidOperationException ex) + + if (!File.Exists(filePath)) + { + input.AddError($"Configuration file {filePath} does not exist"); + } + }); + + var ipAddressOption = new Option(IpAddressOptionName) + { + Description = "The IP address for the proxy to bind to", + HelpName = "ip-address" + }; + ipAddressOption.Validators.Add(input => + { + if (!System.Net.IPAddress.TryParse(input.Tokens[0].Value, out _)) { - input.ErrorMessage = ex.Message; + input.AddError($"{input.Tokens[0].Value} is not a valid IP address"); } }); - _installCertOption = new(InstallCertOptionName, "Install self-signed certificate"); - _installCertOption.AddValidator(input => + var urlsToWatchOption = new Option?>(UrlsToWatchOptionName, "-u") + { + AllowMultipleArgumentsPerToken = true, + Arity = ArgumentArity.ZeroOrMore, + Description = "The list of URLs to watch for requests", + HelpName = "urls-to-watch", + }; + + var logLevelOption = new Option(LogLevelOptionName) + { + Description = $"Level of messages to log. Allowed values: {string.Join(", ", Enum.GetNames())}", + HelpName = "log-level" + }; + logLevelOption.Validators.Add(input => + { + if (!Enum.TryParse(input.Tokens[0].Value, true, out _)) + { + input.AddError($"{input.Tokens[0].Value} is not a valid log level. Allowed values are: {string.Join(", ", Enum.GetNames())}"); + } + }); + + var portOption = new Option(PortOptionName, "-p") + { + Description = "The port for the proxy to listen on", + HelpName = "port" + }; + + var recordOption = new Option(RecordOptionName) + { + Description = "Use this option to record all request logs" + }; + + var watchPidsOption = new Option>(WatchPidsOptionName) + { + AllowMultipleArgumentsPerToken = true, + Description = "The IDs of processes to watch for requests", + HelpName = "pids" + }; + + var watchProcessNamesOption = new Option>(WatchProcessNamesOptionName) + { + AllowMultipleArgumentsPerToken = true, + Description = "The names of processes to watch for requests", + HelpName = "process-names", + }; + + var noFirstRunOption = new Option(NoFirstRunOptionName) + { + Description = "Skip the first run experience" + }; + + var discoverOption = new Option(DiscoverOptionName) + { + Description = "Run Dev Proxy in discovery mode" + }; + + var asSystemProxyOption = new Option(AsSystemProxyOptionName) + { + Description = "Set Dev Proxy as the system proxy" + }; + + var installCertOption = new Option(InstallCertOptionName) + { + Description = "Install self-signed certificate" + }; + installCertOption.Validators.Add(input => { try { - var asSystemProxy = input.GetValueForOption(_asSystemProxyOption) ?? true; - var installCert = input.GetValueForOption(_installCertOption) ?? true; + var asSystemProxy = input.GetValue(asSystemProxyOption) ?? true; + var installCert = input.GetValue(installCertOption) ?? true; if (asSystemProxy && !installCert) { - input.ErrorMessage = $"Requires option '--{_asSystemProxyOption.Name}' to be 'false'"; + input.AddError($"Requires option '{AsSystemProxyOptionName}' to be 'false'"); } } catch (InvalidOperationException ex) { - input.ErrorMessage = ex.Message; + input.AddError(ex.Message); } }); - _timeoutOption = new(TimeoutOptionName, "Time in seconds after which Dev Proxy exits. Resets when Dev Proxy intercepts a request.") + var timeoutOption = new Option(TimeoutOptionName, "-t") { - ArgumentHelpName = "timeout", + Description = "Time in seconds after which Dev Proxy exits. Resets when Dev Proxy intercepts a request.", + HelpName = "timeout" }; - _timeoutOption.AddValidator(input => + timeoutOption.Validators.Add(input => { try { if (!long.TryParse(input.Tokens[0].Value, out var timeoutInput) || timeoutInput < 1) { - input.ErrorMessage = $"{input.Tokens[0].Value} is not valid as a timeout value"; + input.AddError($"{input.Tokens[0].Value} is not valid as a timeout value"); } } catch (InvalidOperationException ex) { - input.ErrorMessage = ex.Message; + input.AddError(ex.Message); } }); - _timeoutOption.AddAlias("-t"); - _envOption = new(EnvOptionName, "Variables to set for the Dev Proxy process") + var envOption = new Option(EnvOptionName, "-e") { - ArgumentHelpName = "env", AllowMultipleArgumentsPerToken = true, - Arity = ArgumentArity.ZeroOrMore + Arity = ArgumentArity.ZeroOrMore, + Description = "Variables to set for the Dev Proxy process", + HelpName = "env", }; - _envOption.AddAlias("-e"); - _envOption.AddValidator(input => + envOption.Validators.Add(input => { try { - var envVars = input.GetValueForOption(_envOption); + var envVars = input.GetValue(envOption); if (envVars is null || envVars.Length == 0) { return; @@ -401,34 +293,33 @@ private void ConfigureCommand() var parts = envVar.Split('=', 2); if (parts.Length != 2) { - input.ErrorMessage = $"Invalid environment variable format: '{envVar}'. Expected format is 'name=value'."; + input.AddError($"Invalid environment variable format: '{envVar}'. Expected format is 'name=value'."); return; } } } catch (InvalidOperationException ex) { - input.ErrorMessage = ex.Message; + input.AddError(ex.Message); } }); var options = new List