src/Skills/Commands/ follows the System.CommandLine conventions summarized
below.
- Command class:
internal sealed class {Verb}Command : Command, a parameterless constructor. Constructor order: base(name, description),Arguments.Add,Options.Add,Validators.Add(rare, for cross-option checks System.CommandLine can't express on a single option),AddExamples, andSetActionWithExceptionHandlinglast. The handler isprivate static async Task<int> ExecuteAsync(ICommandServices services, ParseResult parseResult, CancellationToken cancellationToken). - Options and arguments: one class per option/argument, each exposing
public const string OptionName(orArgumentName). Options shared verbatim across commands live undersrc/Skills/Options/; options that are merely similarly-named but command-specific (different help text, different default) get their own class undersrc/Skills/Commands/<Command>/Options/(see the four separateGlobalOptionclasses foradd,list,remove,update). Always register and read throughOpt<T>.Instance, nevernew: System.CommandLine matches options/arguments by reference, so the registered instance and the one read back inExecuteAsyncmust be the same object. - Errors: throw
ExitExceptionfor CLI-layer aborts; an empty-messageExitExceptionmeans the command already reported detail throughIInteractionService. ThrowCliExceptionfor domain errors that carry anExitCodeplus optionalTitle/Hintrendered as an error panel. Return codes fromExitCodeConstants(Success,Failure,Cancelled). Never callEnvironment.Exit;SetActionWithExceptionHandlingowns the catch ladder that turns exceptions into exit codes. - Error streams: human-mode errors go to stderr through the interaction
service's stderr-bound
IAnsiConsole; machine-mode (--format json) errors are plain text on stderr too, never mixed into the JSON on stdout. update's exit code:updatereturnsExitCodeConstants.Successeven when some skills fail their update check or updates are found but not applied; a non-zero exit is reserved for the command itself failing to run, not for what it reports.- Services: resolve everything the handler needs from
ICommandServicesinsideExecuteAsync. Commands take no constructor dependencies. - Services initialization: standalone execution stores the provider in
CommandExecutionContextbefore parsing. An embedding host passes its provider toSkillsCommand, which initializes the same context.
namespace Skills.Commands;
internal sealed class GreetCommand : Command
{
public GreetCommand() : base("greet", "Print a greeting.")
{
Arguments.Add(Opt<NameArgument>.Instance);
Options.Add(Opt<LoudOption>.Instance);
this.AddExamples("greet world", "greet world --loud");
this.SetActionWithExceptionHandling(ExecuteAsync);
}
private static async Task<int> ExecuteAsync(
ICommandServices services,
ParseResult parseResult,
CancellationToken cancellationToken)
{
var interaction = services.GetRequiredService<IInteractionService>();
var name = parseResult.GetValue(Opt<NameArgument>.Instance);
if (string.IsNullOrWhiteSpace(name))
{
throw new ExitException($"Missing required argument '{NameArgument.ArgumentName}'.");
}
var loud = parseResult.GetValue(Opt<LoudOption>.Instance);
interaction.WriteLine(loud ? $"HELLO, {name.ToUpperInvariant()}!" : $"Hello, {name}.");
await Task.CompletedTask;
return ExitCodeConstants.Success;
}
}