System Commandline CLI
GitHubOfficialFirst committed by massimobonanniDistributed by GitHub
- Skill
- Guidelines
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a structured approach to developing command-line interfaces in .NET using the System.CommandLine library. It emphasizes clean architecture by promoting the use of a base class for commands, dependency injection for business logic, and consistent naming conventions. The guidelines cover the entire lifecycle of a command, from registration and option definition to handler implementation and global option management, ensuring that CLI tools remain maintainable and follow established best practices.
What it does
- Defines a consistent project structure for CLI commands and groups.
- Provides patterns for implementing command handlers and managing dependencies.
- Outlines rules for defining options, arguments, and global configuration.
- Establishes naming conventions for classes, files, and CLI verbs.
- Details the process for registering commands and subcommands within the application.
- Specifies validation patterns for global options to ensure robust input handling.
When to use it
- When creating a new command or subcommand in a .NET CLI project.
- When refactoring existing CLI code to improve structure and maintainability.
- When adding new options or arguments to an existing command.
- When implementing shared logic or global options across multiple commands.
- When deciding how to wire command handlers or integrate service logic.
Advantages
- Ensures architectural consistency across the CLI command tree.
- Promotes thin command handlers by offloading business logic to injected services.
- Simplifies the addition of new commands through standardized templates.
- Centralizes shared behavior and global configuration management.
- Improves code readability through strict naming and visibility rules.
Usage examples
- “How should I structure a new command group for managing user resources in my CLI?”
- The agent will guide you to create a parent class inheriting from CommandBase, register subcommands in its constructor, and place the file in a dedicated folder following the established namespace conventions.
- “I need to add a new option to my CLI command, what is the standard way to do this?”
- The agent will provide the syntax for defining an Option object, setting its description and required status, adding aliases, and reading the value within the command handler.
- “How do I implement a handler for a new command?”
- The agent will show you how to wire an async method using SetAction, ensuring the signature accepts ParseResult and CancellationToken, and keeping the logic thin by calling injected services.
- “What is the best way to handle global options that apply to all commands?”
- The agent will explain how to define these in a static class, register them once in the root command, and use protected helper methods in the base class to access them consistently.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/github/system-commandline-cli/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip system-commandline-cli.zip -d .agents/skills
Documentation by massimobonanni
System.CommandLine CLI Developer Skill
You are working on a .NET CLI application built with System.CommandLine v2.x.x, targeting .NET 8 or later or any .NET Standard 2.0 implementation, including .NET Framework 4.6.1 or later and .NET Core 2.0 or later. Follow these rules and patterns strictly when creating or modifying CLI commands.
Architecture Overview
<CLI Project>/
├── Program.cs # Entry point and command invocation
└── Commands/
├── CommandBase.cs # Base class for all commands
├── GlobalOptions.cs # Defines global options for the CLI
├── RootCommand.cs # Registers top-level commands
└── <Group>/ # One folder per command group
├── <Group>Command.cs # Parent command that registers its children
└── <Group><Verb>Command.cs # Leaf command with its handlerRULE 1 — Prefer a Project-Specific Command Base Class
Prefer defining a project-specific abstract CommandBase that inherits from System.CommandLine.Command. Concrete commands should inherit from this base class so shared behavior and conventions remain centralized.
internal abstract class CommandBase : Command
{
protected CommandBase(string name, string? description = null)
: base(name, description)
{
}
}
internal sealed class MyCommand : CommandBase
{
public MyCommand()
: base("command-name", "Help text shown in --help")
{
this.SetAction(CommandHandler);
}
private async Task<int> CommandHandler(
ParseResult parseResult,
CancellationToken cancellationToken)
{
// implementation
return 0;
}
}When the project already has a command base class, preserve its established conventions. Otherwise, introduce one when commands need shared behavior; simple applications may inherit from Command directly when a base class adds no meaningful value.
RULE 2 — Options and Arguments
Defining Options
private readonly Option<string> _myOption;
// In constructor:
_myOption = new Option<string>("--my-option")
{
Description = "Clear description of what this option does",
Required = true, // or false
};
_myOption.Aliases.Add("-m"); // Add a short alias
this.Options.Add(_myOption);Defining Arguments (positional)
private readonly Argument<string> _fileArgument;
// In constructor:
_fileArgument = new Argument<string>("file")
{
Description = "Path to the input file"
};
this.Arguments.Add(_fileArgument);Reading Values in Handlers
// Required option/argument — use GetValue:
var value = parseResult.GetValue(_myOption);RULE 3 — Command Handler Pattern
Handlers are async methods wired via SetAction:
this.SetAction(CommandHandler);
private async Task<int> CommandHandler(ParseResult parseResult, CancellationToken cancellationToken)
{
// 1. Read option/argument values
// 2. Load session settings (if needed)
// 3. Validate configuration early — fail fast with clear error
// 4. Execute business logic
// 5. Output results with Console
return 0; // or non-zero exit code
}RULE 4 — Command Group (Parent with Subcommands)
A group command registers children but does not call SetAction:
internal class MyGroupCommand : CommandBase
{
public MyGroupCommand()
: base("mygroup", "Manages my-group resources")
{
this.Subcommands.Add(new MyGroupListCommand());
this.Subcommands.Add(new MyGroupCreateCommand());
this.Subcommands.Add(new MyGroupDeleteCommand());
}
}A command may define both an action and subcommands when the direct invocation has meaningful behavior.
RULE 5 — Registration
Top-level commands → register in
RootCommand.cs:this.Subcommands.Add(new MyGroupCommand());Subcommands → register inside the parent command's constructor:
this.Subcommands.Add(new MyGroupCreateCommand());
RULE 6 — User Confirmation for Destructive Operations
Console.WriteLine("Are you sure you want to delete X? This action cannot be undone. (yes/no)");
var confirmation = Console.ReadLine();
if (confirmation?.ToLower() != "yes" && confirmation?.ToLower() != "y")
{
Console.WriteLine("Operation cancelled.");
return 0;
}RULE 7 — Command Logic
The logic of each command should be in one or more service classes that implement interfaces. The command receives interfaces through dependency injection (DI), not concrete implementations. The command handler should not contain business logic. The command handler should be thin, responsible only for:
Parsing input
Validating configuration
Calling the service method
Outputting results
Service class should be injected in the command constructor via DI, not instantiated directly.
RULE 8 — Dependency Injection
Services are registered in Program.cs:
serviceCollection.TryAddSingleton<IMyService, MyServiceImpl>();Add a convenience extension in ServiceProviderExtensions.cs:
public static IMyService GetMyService(this ServiceProvider provider)
=> provider.GetRequiredService<IMyService>();RULE 9 — Naming Conventions
| Element | Convention | Example |
|---|---|---|
| CLI command name | lowercase kebab-case | agent create, set show |
| Command class | PascalCase + Command suffix | AgentCreateCommand |
| Option field | _camelCaseOption (private readonly) | _projectNameOption |
| Option long name | --kebab-case | --project-name |
| Option short alias | -x (1-2 chars) | -p, -id, -md |
| Argument field | _camelCaseArgument | _fileArgument |
| Namespace | MyProject.Commands.<Group> | MyProject.Commands.Agent |
| Folder | Commands/<Group>/ | Commands/Agent/ |
RULE 10 — Visibility
All command classes are
internal.
RULE 11 — Global Options and Validation
Define options shared by the entire command tree once in GlobalOptions.cs. Reuse the same
Option<T> instance when registering, validating, and reading the option.
internal static class GlobalOptions
{
public static readonly Option<string> EndpointOption = CreateEndpointOption();
private static Option<string> CreateEndpointOption()
{
var option = new Option<string>(...);
// add option description, aliases, and Required flag
// Add validation to the option's Validators collection
return option;
}
}Using Global Options in a Command
Expose repeated parsing or conversion through protected CommandBase helpers:
/// <summary>Resolves the validated endpoint from the global option.</summary>
protected Uri GetEndpoint(ParseResult parseResult)
{
var baseUrl = parseResult.GetValue(GlobalOptions.EndpointOption)!;
return new Uri(baseUrl);
}
/// <summary>Resolves the optional key from the global option.</summary>
protected string? GetKey(ParseResult parseResult)
=> parseResult.GetValue(GlobalOptions.KeyOption);Consume those helpers from the leaf command's handler. The command must not add the global options to its own
Options collection; recursive registration on the root already makes them available in its ParseResult.
private async Task<int> CommandHandler(
ParseResult parseResult,
CancellationToken cancellationToken)
{
var endpoint = GetEndpoint(parseResult);
var key = GetKey(parseResult);
...
return 0;
}Read a global option directly in a leaf handler only when no shared conversion or fallback logic is needed.
Always use the static GlobalOptions symbol; never create a second Option<T> with the same aliases.
Follow these requirements:
Set
Recursive = trueso the option is accepted for every descendant command.Add each global option exactly once to
RootCommand.Options; do not duplicate it on leaf commands.Read values through the shared symbol, for example
parseResult.GetValue(GlobalOptions.Endpoint), preferably behind aCommandBasehelper.Add validation to the option's
Validatorscollection so invalid input becomes a parse error and the command handler is not invoked. Do not rely on exceptions fromnew Uri(...)or downstream services.Validate endpoint options as nonblank absolute
httporhttpsURIs. Reject unsupported schemes, relative URIs, query strings, and fragments because appending a fixed endpoint path would change their meaning.For optional secret options such as
--key, allow omission but reject an explicitly supplied blank or whitespace-only value. Validate the value without logging, displaying, trimming, or otherwise mutating it.Keep validation separate from derivation.
Use stable, actionable validation messages that name the option and the accepted format.
Test global options through the root parser, including the default, explicit valid values, invalid values, and placement before and after a representative subcommand. Verify invalid input prevents handler execution.
RULE 12 — Checklist for New Commands
When creating a new command, verify:
✅ Inherits from the project’s command base class when one exists or provides meaningful shared behavior
✅ Constructor passes
name,descriptionto base✅ All options have
Description,Required✅ Handler wired via
this.SetAction(CommandHandler)✅ Handler signature:
async Task<int> CommandHandler(ParseResult, CancellationToken)✅ Command registered in parent (RootCommand or group command)
✅ Class is
internal✅ File placed in
Commands/<Group>/folder✅ Namespace matches folder:
MyProject.CLI.Commands.<Group>
Files
- ATTRIBUTION.mdAdded by AutoGineer889 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md10.4 KiB
SKILL.md
System.CommandLine CLI Developer Skill
You are working on a .NET CLI application built with System.CommandLine v2.x.x, targeting .NET 8 or later or any .NET Standard 2.0 implementation, including .NET Framework 4.6.1 or later and .NET Core 2.0 or later. Follow these rules and patterns strictly when creating or modifying CLI commands.
Architecture Overview
<CLI Project>/
├── Program.cs # Entry point and command invocation
└── Commands/
├── CommandBase.cs # Base class for all commands
├── GlobalOptions.cs # Defines global options for the CLI
├── RootCommand.cs # Registers top-level commands
└── <Group>/ # One folder per command group
├── <Group>Command.cs # Parent command that registers its children
└── <Group><Verb>Command.cs # Leaf command with its handlerRULE 1 — Prefer a Project-Specific Command Base Class
Prefer defining a project-specific abstract CommandBase that inherits from System.CommandLine.Command. Concrete commands should inherit from this base class so shared behavior and conventions remain centralized.
internal abstract class CommandBase : Command
{
protected CommandBase(string name, string? description = null)
: base(name, description)
{
}
}
internal sealed class MyCommand : CommandBase
{
public MyCommand()
: base("command-name", "Help text shown in --help")
{
this.SetAction(CommandHandler);
}
private async Task<int> CommandHandler(
ParseResult parseResult,
CancellationToken cancellationToken)
{
// implementation
return 0;
}
}When the project already has a command base class, preserve its established conventions. Otherwise, introduce one when commands need shared behavior; simple applications may inherit from Command directly when a base class adds no meaningful value.
RULE 2 — Options and Arguments
Defining Options
private readonly Option<string> _myOption;
// In constructor:
_myOption = new Option<string>("--my-option")
{
Description = "Clear description of what this option does",
Required = true, // or false
};
_myOption.Aliases.Add("-m"); // Add a short alias
this.Options.Add(_myOption);Defining Arguments (positional)
private readonly Argument<string> _fileArgument;
// In constructor:
_fileArgument = new Argument<string>("file")
{
Description = "Path to the input file"
};
this.Arguments.Add(_fileArgument);Reading Values in Handlers
// Required option/argument — use GetValue:
var value = parseResult.GetValue(_myOption);RULE 3 — Command Handler Pattern
Handlers are async methods wired via SetAction:
this.SetAction(CommandHandler);
private async Task<int> CommandHandler(ParseResult parseResult, CancellationToken cancellationToken)
{
// 1. Read option/argument values
// 2. Load session settings (if needed)
// 3. Validate configuration early — fail fast with clear error
// 4. Execute business logic
// 5. Output results with Console
return 0; // or non-zero exit code
}RULE 4 — Command Group (Parent with Subcommands)
A group command registers children but does not call SetAction:
internal class MyGroupCommand : CommandBase
{
public MyGroupCommand()
: base("mygroup", "Manages my-group resources")
{
this.Subcommands.Add(new MyGroupListCommand());
this.Subcommands.Add(new MyGroupCreateCommand());
this.Subcommands.Add(new MyGroupDeleteCommand());
}
}A command may define both an action and subcommands when the direct invocation has meaningful behavior.
RULE 5 — Registration
Top-level commands → register in
RootCommand.cs:this.Subcommands.Add(new MyGroupCommand());Subcommands → register inside the parent command's constructor:
this.Subcommands.Add(new MyGroupCreateCommand());
RULE 6 — User Confirmation for Destructive Operations
Console.WriteLine("Are you sure you want to delete X? This action cannot be undone. (yes/no)");
var confirmation = Console.ReadLine();
if (confirmation?.ToLower() != "yes" && confirmation?.ToLower() != "y")
{
Console.WriteLine("Operation cancelled.");
return 0;
}RULE 7 — Command Logic
The logic of each command should be in one or more service classes that implement interfaces. The command receives interfaces through dependency injection (DI), not concrete implementations. The command handler should not contain business logic. The command handler should be thin, responsible only for:
Parsing input
Validating configuration
Calling the service method
Outputting results
Service class should be injected in the command constructor via DI, not instantiated directly.
RULE 8 — Dependency Injection
Services are registered in Program.cs:
serviceCollection.TryAddSingleton<IMyService, MyServiceImpl>();Add a convenience extension in ServiceProviderExtensions.cs:
public static IMyService GetMyService(this ServiceProvider provider)
=> provider.GetRequiredService<IMyService>();RULE 9 — Naming Conventions
| Element | Convention | Example |
|---|---|---|
| CLI command name | lowercase kebab-case | agent create, set show |
| Command class | PascalCase + Command suffix | AgentCreateCommand |
| Option field | _camelCaseOption (private readonly) | _projectNameOption |
| Option long name | --kebab-case | --project-name |
| Option short alias | -x (1-2 chars) | -p, -id, -md |
| Argument field | _camelCaseArgument | _fileArgument |
| Namespace | MyProject.Commands.<Group> | MyProject.Commands.Agent |
| Folder | Commands/<Group>/ | Commands/Agent/ |
RULE 10 — Visibility
All command classes are
internal.
RULE 11 — Global Options and Validation
Define options shared by the entire command tree once in GlobalOptions.cs. Reuse the same
Option<T> instance when registering, validating, and reading the option.
internal static class GlobalOptions
{
public static readonly Option<string> EndpointOption = CreateEndpointOption();
private static Option<string> CreateEndpointOption()
{
var option = new Option<string>(...);
// add option description, aliases, and Required flag
// Add validation to the option's Validators collection
return option;
}
}Using Global Options in a Command
Expose repeated parsing or conversion through protected CommandBase helpers:
/// <summary>Resolves the validated endpoint from the global option.</summary>
protected Uri GetEndpoint(ParseResult parseResult)
{
var baseUrl = parseResult.GetValue(GlobalOptions.EndpointOption)!;
return new Uri(baseUrl);
}
/// <summary>Resolves the optional key from the global option.</summary>
protected string? GetKey(ParseResult parseResult)
=> parseResult.GetValue(GlobalOptions.KeyOption);Consume those helpers from the leaf command's handler. The command must not add the global options to its own
Options collection; recursive registration on the root already makes them available in its ParseResult.
private async Task<int> CommandHandler(
ParseResult parseResult,
CancellationToken cancellationToken)
{
var endpoint = GetEndpoint(parseResult);
var key = GetKey(parseResult);
...
return 0;
}Read a global option directly in a leaf handler only when no shared conversion or fallback logic is needed.
Always use the static GlobalOptions symbol; never create a second Option<T> with the same aliases.
Follow these requirements:
Set
Recursive = trueso the option is accepted for every descendant command.Add each global option exactly once to
RootCommand.Options; do not duplicate it on leaf commands.Read values through the shared symbol, for example
parseResult.GetValue(GlobalOptions.Endpoint), preferably behind aCommandBasehelper.Add validation to the option's
Validatorscollection so invalid input becomes a parse error and the command handler is not invoked. Do not rely on exceptions fromnew Uri(...)or downstream services.Validate endpoint options as nonblank absolute
httporhttpsURIs. Reject unsupported schemes, relative URIs, query strings, and fragments because appending a fixed endpoint path would change their meaning.For optional secret options such as
--key, allow omission but reject an explicitly supplied blank or whitespace-only value. Validate the value without logging, displaying, trimming, or otherwise mutating it.Keep validation separate from derivation.
Use stable, actionable validation messages that name the option and the accepted format.
Test global options through the root parser, including the default, explicit valid values, invalid values, and placement before and after a representative subcommand. Verify invalid input prevents handler execution.
RULE 12 — Checklist for New Commands
When creating a new command, verify:
✅ Inherits from the project’s command base class when one exists or provides meaningful shared behavior
✅ Constructor passes
name,descriptionto base✅ All options have
Description,Required✅ Handler wired via
this.SetAction(CommandHandler)✅ Handler signature:
async Task<int> CommandHandler(ParseResult, CancellationToken)✅ Command registered in parent (RootCommand or group command)
✅ Class is
internal✅ File placed in
Commands/<Group>/folder✅ Namespace matches folder:
MyProject.CLI.Commands.<Group>
Requirements and compatibility
Written for: GitHub Copilot
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- First committed by
massimobonanni
- Publisher
- GitHub (Official)
- Distributed by
- GitHub
- Source
- github/awesome-copilot · skills/system-commandline-cli · commit 797337f
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Contributors
- massimobonanni
AutoGineer is not affiliated with or endorsed by massimobonanni or GitHub.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download system-commandline-cli.zip