Skip to content

Reorders CLI options and subcommands to show alphabetically #1171

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions dev-proxy-abstractions/CommandLineExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,37 @@ public static class CommandLineExtensions

return parseResult.GetValueForOption(option);
}

private static string ByName<T>(T symbol) where T : Symbol => symbol.Name;

public static IEnumerable<T> OrderByName<T>(this IEnumerable<T> symbols) where T : Symbol
{
ArgumentNullException.ThrowIfNull(symbols);

return symbols.OrderBy(ByName, StringComparer.Ordinal);
}

public static Command AddCommands(this Command command, IEnumerable<Command> subcommands)
{
ArgumentNullException.ThrowIfNull(command);
ArgumentNullException.ThrowIfNull(subcommands);

foreach (var subcommand in subcommands)
{
command.AddCommand(subcommand);
}
return command;
}

public static Command AddOptions(this Command command, IEnumerable<Option> options)
{
ArgumentNullException.ThrowIfNull(command);
ArgumentNullException.ThrowIfNull(options);

foreach (var option in options)
{
command.AddOption(option);
}
return command;
}
}
23 changes: 11 additions & 12 deletions dev-proxy/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@
IProxyContext context = new ProxyContext(ProxyCommandHandler.Configuration, ProxyEngine.Certificate, lmClient);
ProxyHost proxyHost = new();

// this is where the root command is created which contains all commands and subcommands
RootCommand rootCommand = proxyHost.GetRootCommand(logger);

// store the global options that are created automatically for us
// rootCommand doesn't return the global options, so we have to store them manually
string[] globalOptions = ["--version"];
Expand All @@ -62,20 +59,22 @@
// load plugins to get their options and commands
var pluginLoader = new PluginLoader(isDiscover, logger, loggerFactory);
PluginLoaderResult loaderResults = await pluginLoader.LoadPluginsAsync(pluginEvents, context);
var options = loaderResults.ProxyPlugins

var pluginOptions = loaderResults.ProxyPlugins
.SelectMany(p => p.GetOptions())
// remove duplicates by comparing the option names
.GroupBy(o => o.Name)
.Select(g => g.First())
.ToList();
options.ForEach(rootCommand.AddOption);
// register all plugin commands
loaderResults.ProxyPlugins
.ToArray();

var pluginCommands = loaderResults.ProxyPlugins
.SelectMany(p => p.GetCommands())
.ToList()
.ForEach(rootCommand.AddCommand);
.ToArray();

// this is where the root command is created which contains all commands and subcommands
RootCommand rootCommand = proxyHost.CreateRootCommand(logger, pluginOptions, pluginCommands);

// get the list of available subcommands
// get the list of available subcommand's names
var subCommands = rootCommand.Children.OfType<Command>().Select(c => c.Name).ToArray();

// check if any of the subcommands are present
Expand Down Expand Up @@ -132,7 +131,7 @@
pluginEvents.RaiseInit(new InitArgs());
}

rootCommand.Handler = proxyHost.GetCommandHandler(pluginEvents, [.. options], loaderResults.UrlsToWatch, logger);
rootCommand.Handler = proxyHost.GetCommandHandler(pluginEvents, [.. pluginOptions], loaderResults.UrlsToWatch, logger);
var exitCode = await rootCommand.InvokeAsync(args);
loggerFactory.Dispose();
Environment.Exit(exitCode);
Expand Down
193 changes: 136 additions & 57 deletions dev-proxy/ProxyHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -333,9 +333,10 @@ public ProxyHost()
ProxyCommandHandler.Configuration.ConfigFile = ConfigFile;
}

public RootCommand GetRootCommand(ILogger logger)
public RootCommand CreateRootCommand(ILogger logger, Option[] pluginOptions, Command[] pluginCommands)
{
var command = new RootCommand {
var command = new RootCommand("Dev Proxy is a command line tool for testing Microsoft Graph, SharePoint Online and any other HTTP APIs.");
var options = (Option[])[
_portOption,
_ipAddressOption,
_recordOption,
Expand All @@ -352,86 +353,89 @@ public RootCommand GetRootCommand(ILogger logger)
_urlsToWatchOption!,
_timeoutOption,
_discoverOption,
_envOption
};
command.Description = "Dev Proxy is a command line tool for testing Microsoft Graph, SharePoint Online and any other HTTP APIs.";
_envOption,
..pluginOptions
];

command.AddOptions(options.OrderByName());

// _logLevelOption is set while initializing the Program
// As such, it's always set here
command.AddGlobalOption(_logLevelOption!);

var msGraphDbCommand = new Command("msgraphdb", "Generate a local SQLite database with Microsoft Graph API metadata")
{
Handler = new MSGraphDbCommandHandler(logger)
};
command.Add(msGraphDbCommand);
var commands = (Command[])[
CreateMsGraphDbCommand(logger),
CreateConfigCommand(logger),
CreateOutdatedCommand(logger),
CreateJwtCommand(),
CreateCertCommand(logger),
..pluginCommands
];

var configCommand = new Command("config", "Manage Dev Proxy configs");

var configGetCommand = new Command("get", "Download the specified config from the Sample Solution Gallery");
var configIdArgument = new Argument<string>("config-id", "The ID of the config to download");
configGetCommand.AddArgument(configIdArgument);
configGetCommand.SetHandler(async configId => await ConfigGetCommandHandler.DownloadConfigAsync(configId, logger), configIdArgument);
configCommand.Add(configGetCommand);
command.AddCommands(commands.OrderByName());
return command;
}

var configNewCommand = new Command("new", "Create new Dev Proxy configuration file");
var nameArgument = new Argument<string>("name", "Name of the configuration file")
{
Arity = ArgumentArity.ZeroOrOne
};
nameArgument.SetDefaultValue("devproxyrc.json");
configNewCommand.AddArgument(nameArgument);
configNewCommand.SetHandler(async name => await ConfigNewCommandHandler.CreateConfigFileAsync(name, logger), nameArgument);
configCommand.Add(configNewCommand);
private static Command CreateCertCommand(ILogger logger)
{
var certCommand = new Command("cert", "Manage the Dev Proxy certificate");

var configOpenCommand = new Command("open", "Open devproxyrc.json");
configOpenCommand.SetHandler(() =>
var sortedCommands = new[]
{
var cfgPsi = new ProcessStartInfo(ConfigFile)
{
UseShellExecute = true
};
Process.Start(cfgPsi);
});
configCommand.Add(configOpenCommand);
CreateCertEnsureCommand(logger)
}.OrderByName();

command.Add(configCommand);
certCommand.AddCommands(sortedCommands);
return certCommand;
}

var outdatedCommand = new Command("outdated", "Check for new version");
var outdatedShortOption = new Option<bool>("--short", "Return version only");
outdatedCommand.AddOption(outdatedShortOption);
outdatedCommand.SetHandler(async versionOnly => await OutdatedCommandHandler.CheckVersionAsync(versionOnly, logger), outdatedShortOption);
command.Add(outdatedCommand);
private static Command CreateCertEnsureCommand(ILogger logger)
{
var certEnsureCommand = new Command("ensure", "Ensure certificates are setup (creates root if required). Also makes root certificate trusted.");
certEnsureCommand.SetHandler(async () => await CertEnsureCommandHandler.EnsureCertAsync(logger));
return certEnsureCommand;
}

private static Command CreateJwtCommand()
{
var jwtCommand = new Command("jwt", "Manage JSON Web Tokens");

var sortedCommands = new[]
{
CreateJwtCreateCommand()
}.OrderByName();

jwtCommand.AddCommands(sortedCommands);
return jwtCommand;
}

private static Command CreateJwtCreateCommand()
{
var jwtCreateCommand = new Command("create", "Create a new JWT token");

var jwtNameOption = new Option<string>("--name", "The name of the user to create the token for.");
jwtNameOption.AddAlias("-n");
jwtCreateCommand.AddOption(jwtNameOption);

var jwtAudienceOption = new Option<IEnumerable<string>>("--audience", "The audiences to create the token for. Specify once for each audience")
{
AllowMultipleArgumentsPerToken = true
};
jwtAudienceOption.AddAlias("-a");
jwtCreateCommand.AddOption(jwtAudienceOption);

var jwtIssuerOption = new Option<string>("--issuer", "The issuer of the token.");
jwtIssuerOption.AddAlias("-i");
jwtCreateCommand.AddOption(jwtIssuerOption);

var jwtRolesOption = new Option<IEnumerable<string>>("--roles", "A role claim to add to the token. Specify once for each role.")
{
AllowMultipleArgumentsPerToken = true
};
jwtRolesOption.AddAlias("-r");
jwtCreateCommand.AddOption(jwtRolesOption);

var jwtScopesOption = new Option<IEnumerable<string>>("--scopes", "A scope claim to add to the token. Specify once for each scope.")
{
AllowMultipleArgumentsPerToken = true
};
jwtScopesOption.AddAlias("-s");
jwtCreateCommand.AddOption(jwtScopesOption);

var jwtClaimsOption = new Option<Dictionary<string, string>>("--claims",
description: "Claims to add to the token. Specify once for each claim in the format \"name:value\".",
Expand Down Expand Up @@ -464,11 +468,9 @@ public RootCommand GetRootCommand(ILogger logger)
{
AllowMultipleArgumentsPerToken = true,
};
jwtCreateCommand.AddOption(jwtClaimsOption);

var jwtValidForOption = new Option<double>("--valid-for", "The duration for which the token is valid. Duration is set in minutes.");
jwtValidForOption.AddAlias("-v");
jwtCreateCommand.AddOption(jwtValidForOption);

var jwtSigningKeyOption = new Option<string>("--signing-key", "The signing key to sign the token. Minimum length is 32 characters.");
jwtSigningKeyOption.AddAlias("-k");
Expand All @@ -487,7 +489,6 @@ public RootCommand GetRootCommand(ILogger logger)
input.ErrorMessage = ex.Message;
}
});
jwtCreateCommand.AddOption(jwtSigningKeyOption);

jwtCreateCommand.SetHandler(
JwtCommandHandler.GetToken,
Expand All @@ -502,17 +503,95 @@ public RootCommand GetRootCommand(ILogger logger)
jwtSigningKeyOption
)
);
jwtCommand.Add(jwtCreateCommand);

command.Add(jwtCommand);
var sortedOptions = new Option[]
{
jwtNameOption,
jwtAudienceOption,
jwtIssuerOption,
jwtRolesOption,
jwtScopesOption,
jwtClaimsOption,
jwtValidForOption,
jwtSigningKeyOption
}.OrderByName();

jwtCreateCommand.AddOptions(sortedOptions);
return jwtCreateCommand;
}

var certCommand = new Command("cert", "Manage the Dev Proxy certificate");
var certEnsureCommand = new Command("ensure", "Ensure certificates are setup (creates root if required). Also makes root certificate trusted.");
certEnsureCommand.SetHandler(async () => await CertEnsureCommandHandler.EnsureCertAsync(logger));
certCommand.Add(certEnsureCommand);
command.Add(certCommand);
private static Command CreateOutdatedCommand(ILogger logger)
{
var outdatedCommand = new Command("outdated", "Check for new version");
var outdatedShortOption = new Option<bool>("--short", "Return version only");
outdatedCommand.SetHandler(async versionOnly => await OutdatedCommandHandler.CheckVersionAsync(versionOnly, logger), outdatedShortOption);

return command;
var sortedOptions = new[]
{
outdatedShortOption
}.OrderByName();

outdatedCommand.AddOptions(sortedOptions);
return outdatedCommand;
}

private static Command CreateConfigCommand(ILogger logger)
{
var configCommand = new Command("config", "Manage Dev Proxy configs");

var sortedCommands = new[]
{
CreateConfigGetCommand(logger),
CreateConfigNewCommand(logger),
CreateConfigOpenCommand()
}.OrderByName();

configCommand.AddCommands(sortedCommands);
return configCommand;
}

private static Command CreateConfigGetCommand(ILogger logger)
{
var configGetCommand = new Command("get", "Download the specified config from the Sample Solution Gallery");
var configIdArgument = new Argument<string>("config-id", "The ID of the config to download");
configGetCommand.AddArgument(configIdArgument);
configGetCommand.SetHandler(async configId => await ConfigGetCommandHandler.DownloadConfigAsync(configId, logger), configIdArgument);
return configGetCommand;
}

private static Command CreateConfigNewCommand(ILogger logger)
{
var configNewCommand = new Command("new", "Create new Dev Proxy configuration file");
var nameArgument = new Argument<string>("name", "Name of the configuration file")
{
Arity = ArgumentArity.ZeroOrOne
};
nameArgument.SetDefaultValue("devproxyrc.json");
configNewCommand.AddArgument(nameArgument);
configNewCommand.SetHandler(async name => await ConfigNewCommandHandler.CreateConfigFileAsync(name, logger), nameArgument);
return configNewCommand;
}

private static Command CreateConfigOpenCommand()
{
var configOpenCommand = new Command("open", "Open devproxyrc.json");
configOpenCommand.SetHandler(() =>
{
var cfgPsi = new ProcessStartInfo(ConfigFile)
{
UseShellExecute = true
};
Process.Start(cfgPsi);
});
return configOpenCommand;
}

private static Command CreateMsGraphDbCommand(ILogger logger)
{
return new Command("msgraphdb", "Generate a local SQLite database with Microsoft Graph API metadata")
{
Handler = new MSGraphDbCommandHandler(logger)
};
}

public ProxyCommandHandler GetCommandHandler(PluginEvents pluginEvents, Option[] optionsFromPlugins, ISet<UrlToWatch> urlsToWatch, ILogger logger) => new(
Expand Down