diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..b93733d --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,60 @@ +name: Deploy Docs + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: docs/package-lock.json + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Install dependencies + run: npm ci + working-directory: docs + + - name: Build with VitePress + run: npm run docs:build + working-directory: docs + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + name: Deploy + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index b6df5bc..31eeaa0 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ [![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE) A powerful and flexible command-line parser and command executor framework for .NET applications. Build beautiful CLI tools with minimal boilerplate code. + + **[๐Ÿ“š Documentation](https://stho01.github.io/promty/)** ยท **[๐Ÿš€ Getting Started](https://stho01.github.io/promty/guide/getting-started)** ยท **[๐Ÿ’ก Examples](https://stho01.github.io/promty/examples/basic)** ## Features diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..6a968a7 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.vitepress/cache/ +.vitepress/dist/ +package-lock.json diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts new file mode 100644 index 0000000..3e9c9d1 --- /dev/null +++ b/docs/.vitepress/config.mts @@ -0,0 +1,63 @@ +import { defineConfig } from 'vitepress' + +export default defineConfig({ + title: "Promty", + description: "A powerful command-line parser and command executor framework for .NET", + base: '/promty/', + + themeConfig: { + logo: '/logo.png', + + nav: [ + { text: 'Home', link: '/' }, + { text: 'Guide', link: '/guide/getting-started' }, + { text: 'API Reference', link: '/api/commands' }, + { text: 'Examples', link: '/examples/basic' } + ], + + sidebar: [ + { + text: 'Guide', + items: [ + { text: 'Getting Started', link: '/guide/getting-started' }, + { text: 'Commands', link: '/guide/commands' }, + { text: 'Arguments & Flags', link: '/guide/arguments' }, + { text: 'Flags Enums', link: '/guide/flags-enums' }, + { text: 'Process Commands', link: '/guide/process-commands' }, + { text: 'Help Text', link: '/guide/help-text' } + ] + }, + { + text: 'API Reference', + items: [ + { text: 'Commands', link: '/api/commands' }, + { text: 'Attributes', link: '/api/attributes' }, + { text: 'Command Executor', link: '/api/executor' } + ] + }, + { + text: 'Examples', + items: [ + { text: 'Basic Command', link: '/examples/basic' }, + { text: 'File Operations', link: '/examples/file-operations' }, + { text: 'Flags Enums', link: '/examples/flags-enums' }, + { text: 'Process Wrapper', link: '/examples/process-wrapper' } + ] + } + ], + + socialLinks: [ + { icon: 'github', link: 'https://github.com/stho01/promty' }, + { icon: 'npm', link: 'https://www.nuget.org/packages/Promty' } + ], + + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright ยฉ 2024-present STHO' + }, + + search: { + provider: 'local' + } + } +}) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..ee6a712 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,31 @@ +# Promty Documentation + +This directory contains the VitePress documentation site for Promty. + +## Local Development + +Install dependencies: +```bash +npm install +``` + +Start the development server: +```bash +npm run docs:dev +``` + +Build for production: +```bash +npm run docs:build +``` + +Preview production build: +```bash +npm run docs:preview +``` + +## Deployment + +The documentation is automatically deployed to GitHub Pages when changes are pushed to the `main` branch. + +Visit: https://stho01.github.io/promty/ diff --git a/docs/api/attributes.md b/docs/api/attributes.md new file mode 100644 index 0000000..60bd1bb --- /dev/null +++ b/docs/api/attributes.md @@ -0,0 +1,298 @@ +# Attributes API + +## DescriptionAttribute + +Provides descriptions for commands, arguments, and enum fields. + +### Definition + +```csharp +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)] +public class DescriptionAttribute : Attribute +``` + +### Constructors + +#### For Commands + +```csharp +public DescriptionAttribute(string name, string description) +``` + +**Parameters:** +- `name` - The command name +- `description` - The command description + +**Example:** +```csharp +[Description("greet", "Greets a person by name")] +public class GreetCommand : Command +``` + +#### For Arguments and Enum Fields + +```csharp +public DescriptionAttribute(string description) +``` + +**Parameters:** +- `description` - The description text + +**Example:** +```csharp +[Description("Enable verbose output")] +public bool Verbose { get; set; } +``` + +#### For Positional Arguments + +```csharp +public DescriptionAttribute(string name, string description) +``` + +**Parameters:** +- `name` - The argument name (used in help text) +- `description` - The argument description + +**Example:** +```csharp +[Description("name", "The name of the person")] +public string Name { get; set; } = string.Empty; +``` + +### Properties + +#### Description + +The description text. + +```csharp +public string Description { get; } +``` + +#### Name + +The name (optional, for commands and positional arguments). + +```csharp +public string? Name { get; set; } +``` + +### Usage Examples + +#### On Commands + +```csharp +[Description("build", "Build the project")] +public class BuildCommand : Command +``` + +#### On Positional Arguments + +```csharp +[Description("source", "The source file path")] +public string Source { get; set; } = string.Empty; +``` + +#### On Flags + +```csharp +[FlagAlias("verbose", 'v')] +[Description("Enable verbose output")] +public bool Verbose { get; set; } +``` + +#### On Enum Fields + +```csharp +[Flags] +public enum BuildOptions +{ + [Description("Enable verbose output")] + Verbose = 1, + + [Description("Build in debug mode")] + Debug = 2 +} +``` + +## FlagAliasAttribute + +Defines long and short aliases for flag arguments and enum fields. + +### Definition + +```csharp +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +public class FlagAliasAttribute : Attribute +``` + +### Constructors + +#### Long and Short Alias + +```csharp +public FlagAliasAttribute(string longAlias, char shortAlias) +``` + +**Parameters:** +- `longAlias` - The long flag name (used as `--long-alias`) +- `shortAlias` - The short flag character (used as `-s`) + +**Example:** +```csharp +[FlagAlias("verbose", 'v')] +public bool Verbose { get; set; } +``` + +Command line: `--verbose` or `-v` + +#### Long Alias Only + +```csharp +public FlagAliasAttribute(string longAlias) +``` + +**Parameters:** +- `longAlias` - The long flag name + +**Example:** +```csharp +[FlagAlias("verbose")] +public bool Verbose { get; set; } +``` + +Command line: `--verbose` + +#### Short Alias Only + +```csharp +public FlagAliasAttribute(char shortAlias) +``` + +**Parameters:** +- `shortAlias` - The short flag character + +**Example:** +```csharp +[FlagAlias('v')] +public bool Verbose { get; set; } +``` + +Command line: `-v` + +### Properties + +#### LongAlias + +The long form of the flag. + +```csharp +public string LongAlias { get; } +``` + +#### ShortAlias + +The short form of the flag (optional). + +```csharp +public char? ShortAlias { get; } +``` + +### Usage Examples + +#### On Properties + +```csharp +public class Args +{ + [FlagAlias("verbose", 'v')] + [Description("Enable verbose output")] + public bool Verbose { get; set; } + + [FlagAlias("port", 'p')] + [Description("Port number")] + public int? Port { get; set; } +} +``` + +#### On Enum Fields + +```csharp +[Flags] +public enum BuildOptions +{ + [FlagAlias("verbose", 'v')] + [Description("Enable verbose output")] + Verbose = 1, + + [FlagAlias("debug", 'd')] + [Description("Build in debug mode")] + Debug = 2, + + // No FlagAlias - uses kebab-case: --no-cache + NoCache = 4 +} +``` + +### Automatic Kebab-Case + +For `[Flags]` enum fields without `[FlagAlias]`, the field name is automatically converted to kebab-case: + +| Field Name | Command Line Flag | +|------------|-------------------| +| `Verbose` | `--verbose` | +| `NoCache` | `--no-cache` | +| `SkipTests` | `--skip-tests` | +| `EnableFeatureX` | `--enable-feature-x` | + +## Best Practices + +### Naming Conventions + +- Use lowercase for long aliases: `verbose`, `no-cache` +- Use single lowercase letter for short aliases: `v`, `d` +- Make names descriptive and clear + +### Description Guidelines + +- Start with a verb for commands: "Greets a person", "Builds the project" +- Be concise but clear +- Explain what the flag does, not how to use it + +### Example + +```csharp +[Description("build", "Build the project with specified options")] +public class BuildCommand : Command +{ + [Flags] + public enum BuildOptions + { + None = 0, + + [FlagAlias("verbose", 'v')] + [Description("Enable verbose output")] + Verbose = 1, + + [FlagAlias("debug", 'd')] + [Description("Build in debug mode")] + Debug = 2, + + [Description("Disable build cache")] + NoCache = 4 + } + + public class Args + { + [Description("project", "Path to project file")] + public string Project { get; set; } = string.Empty; + + [FlagAlias("output", 'o')] + [Description("Output directory")] + public string? Output { get; set; } + + public BuildOptions Options { get; set; } + } +} +``` diff --git a/docs/api/commands.md b/docs/api/commands.md new file mode 100644 index 0000000..91f8111 --- /dev/null +++ b/docs/api/commands.md @@ -0,0 +1,188 @@ +# Commands API + +## Command\ + +Base class for standard commands with typed argument binding. + +### Definition + +```csharp +public abstract class Command where TArgs : new() +``` + +### Methods + +#### ExecuteAsync + +Execute the command with bound arguments. + +```csharp +public abstract Task ExecuteAsync(TArgs args) +``` + +**Parameters:** +- `args` - The bound arguments instance + +**Returns:** +- `Task` - Exit code (0 for success, non-zero for error) + +**Example:** +```csharp +public override Task ExecuteAsync(Args args) +{ + Console.WriteLine($"Hello, {args.Name}!"); + return Task.FromResult(0); +} +``` + +### Usage + +Create a command by extending `Command` (using generic type parameter): + +```csharp +[Description("mycommand", "My command description")] +public class MyCommand : Command +{ + public class Args + { + // Define your arguments here + } + + public override Task ExecuteAsync(Args args) + { + // Your command logic here + return Task.FromResult(0); + } +} +``` + +## ProcessCommand + +Base class for commands that forward arguments to external executables. + +### Definition + +```csharp +public abstract class ProcessCommand +``` + +### Properties + +#### ExecutablePath + +The path to the executable to run. + +```csharp +protected abstract string ExecutablePath { get; } +``` + +**Example:** +```csharp +protected override string ExecutablePath => "git"; +``` + +### Usage + +Create a process command by extending `ProcessCommand`: + +```csharp +[Description("git", "Execute git commands")] +public class GitCommand : ProcessCommand +{ + protected override string ExecutablePath => "git"; +} +``` + +All arguments are automatically forwarded to the executable. + +## Exit Codes + +Commands should return appropriate exit codes: + +| Code | Meaning | +|------|---------| +| `0` | Success | +| `1` | General error | +| `2+` | Application-specific errors | + +**Example:** +```csharp +public override Task ExecuteAsync(Args args) +{ + if (!File.Exists(args.FilePath)) + { + Console.WriteLine("Error: File not found"); + return Task.FromResult(1); // Error + } + + // Process file... + return Task.FromResult(0); // Success +} +``` + +## Async Operations + +Commands support asynchronous operations: + +```csharp +public override async Task ExecuteAsync(Args args) +{ + await SomeAsyncOperation(); + + using var client = new HttpClient(); + var response = await client.GetAsync(args.Url); + + if (!response.IsSuccessStatusCode) + { + return 1; // Error + } + + return 0; // Success +} +``` + +## Error Handling + +Handle errors gracefully and return appropriate exit codes: + +```csharp +public override async Task ExecuteAsync(Args args) +{ + try + { + await ProcessAsync(args); + return 0; + } + catch (FileNotFoundException ex) + { + Console.WriteLine($"Error: File not found - {ex.Message}"); + return 1; + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + return 2; + } +} +``` + +## Type Parameters + +### TArgs + +The arguments class for your command. Must: +- Have a parameterless constructor +- Be a nested class within your command +- Have public properties for arguments + +**Example:** +```csharp +public class Args +{ + [Description("name", "The name")] + public string Name { get; set; } = string.Empty; + + [FlagAlias("verbose", 'v')] + public bool Verbose { get; set; } +} +``` diff --git a/docs/api/executor.md b/docs/api/executor.md new file mode 100644 index 0000000..84f0a70 --- /dev/null +++ b/docs/api/executor.md @@ -0,0 +1,263 @@ +# Command Executor API + +## CommandExecutor + +The main class responsible for registering and executing commands. + +### Definition + +```csharp +public class CommandExecutor +``` + +### Constructor + +```csharp +public CommandExecutor() +``` + +Creates a new instance of the command executor. + +**Example:** +```csharp +var executor = new CommandExecutor(); +``` + +### Methods + +#### RegisterCommandsFromAssembly + +Registers all commands from the specified assembly. + +```csharp +public void RegisterCommandsFromAssembly(Assembly assembly) +``` + +**Parameters:** +- `assembly` - The assembly to scan for commands + +**Example:** +```csharp +using System.Reflection; + +var executor = new CommandExecutor(); +executor.RegisterCommandsFromAssembly(Assembly.GetExecutingAssembly()); +``` + +#### ExecuteAsync + +Executes a command based on the provided command-line arguments. + +```csharp +public Task ExecuteAsync(string[] args) +``` + +**Parameters:** +- `args` - Command-line arguments (typically from `Main`) + +**Returns:** +- `Task` - Exit code from the executed command + +**Example:** +```csharp +public static async Task Main(string[] args) +{ + var executor = new CommandExecutor(); + executor.RegisterCommandsFromAssembly(Assembly.GetExecutingAssembly()); + + return await executor.ExecuteAsync(args); +} +``` + +## Usage Patterns + +### Basic Setup + +```csharp +using System.Reflection; +using Promty; + +var executor = new CommandExecutor(); +executor.RegisterCommandsFromAssembly(Assembly.GetExecutingAssembly()); + +return await executor.ExecuteAsync(args); +``` + +### Multiple Assemblies + +Register commands from multiple assemblies: + +```csharp +var executor = new CommandExecutor(); + +// Register commands from main assembly +executor.RegisterCommandsFromAssembly(Assembly.GetExecutingAssembly()); + +// Register commands from plugin assembly +executor.RegisterCommandsFromAssembly(typeof(PluginCommand).Assembly); + +// Register commands from external library +executor.RegisterCommandsFromAssembly( + Assembly.LoadFrom("MyCommands.dll") +); + +return await executor.ExecuteAsync(args); +``` + +### With Error Handling + +```csharp +var executor = new CommandExecutor(); +executor.RegisterCommandsFromAssembly(Assembly.GetExecutingAssembly()); + +try +{ + return await executor.ExecuteAsync(args); +} +catch (Exception ex) +{ + Console.WriteLine($"Fatal error: {ex.Message}"); + return 1; +} +``` + +### In ASP.NET Core + +```csharp +public class Program +{ + public static async Task Main(string[] args) + { + // Check if running as CLI + if (args.Length > 0 && args[0] == "cli") + { + var executor = new CommandExecutor(); + executor.RegisterCommandsFromAssembly(Assembly.GetExecutingAssembly()); + return await executor.ExecuteAsync(args.Skip(1).ToArray()); + } + + // Otherwise start web host + var host = CreateHostBuilder(args).Build(); + await host.RunAsync(); + return 0; + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); +} +``` + +## Behavior + +### Command Discovery + +The executor automatically discovers all classes that: +- Inherit from `Command` +- Inherit from `ProcessCommand` +- Are in the registered assemblies +- Have a `[Description]` attribute + +### Command Name Resolution + +Command names are determined by the `[Description]` attribute: + +```csharp +[Description("my-command", "Description")] +public class MyCommand : Command +``` + +The first parameter (`"my-command"`) becomes the command name. + +### No Command Provided + +When no command is provided: +```bash +dotnet run +``` + +The executor displays a list of all available commands. + +### Unknown Command + +When an unknown command is provided: +```bash +dotnet run -- unknown-command +``` + +The executor displays an error and the command list. + +### Missing Arguments + +When required arguments are missing: +```bash +dotnet run -- mycommand +``` + +The executor displays command-specific help text. + +## Return Values + +The `ExecuteAsync` method returns the exit code: + +| Code | Meaning | +|------|---------| +| `0` | Success | +| `1` | General error (command not found, missing arguments, etc.) | +| Other | Command-specific exit code | + +## Example Program + +Complete example `Program.cs`: + +```csharp +using System.Reflection; +using Promty; + +// Create and configure executor +var executor = new CommandExecutor(); + +// Register all commands in this assembly +executor.RegisterCommandsFromAssembly(Assembly.GetExecutingAssembly()); + +// Execute command and return exit code +return await executor.ExecuteAsync(args); +``` + +## Top-Level Statements + +Promty works great with C# top-level statements: + +```csharp +using System.Reflection; +using Promty; + +var executor = new CommandExecutor(); +executor.RegisterCommandsFromAssembly(Assembly.GetExecutingAssembly()); +return await executor.ExecuteAsync(args); +``` + +## Dependency Injection + +For advanced scenarios with dependency injection: + +```csharp +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); +services.AddSingleton(); +var serviceProvider = services.BuildServiceProvider(); + +// Commands can resolve services via constructor injection +var executor = new CommandExecutor(); +executor.RegisterCommandsFromAssembly(Assembly.GetExecutingAssembly()); + +return await executor.ExecuteAsync(args); +``` + +::: warning +Built-in dependency injection support is not currently available. Commands must manage their own dependencies. +::: diff --git a/docs/examples/basic.md b/docs/examples/basic.md new file mode 100644 index 0000000..fdc14ed --- /dev/null +++ b/docs/examples/basic.md @@ -0,0 +1,190 @@ +# Basic Command Example + +A simple command that demonstrates the core features of Promty. + +## Echo Command + +Create a command that echoes text with optional transformations: + +```csharp +using Promty; +using Promty.Attributes; + +[Description("echo", "Print a message to the console")] +public class EchoCommand : Command +{ + public class Args + { + [Description("message", "The message to print")] + public string Message { get; set; } = string.Empty; + + [FlagAlias("uppercase", 'u')] + [Description("Convert message to uppercase")] + public bool Uppercase { get; set; } + + [FlagAlias("repeat", 'r')] + [Description("Number of times to repeat the message")] + public int? Repeat { get; set; } + + [FlagAlias("prefix", 'p')] + [Description("Prefix to add before the message")] + public string? Prefix { get; set; } + } + + public override Task ExecuteAsync(Args args) + { + var message = args.Message; + + // Apply transformations + if (args.Uppercase) + { + message = message.ToUpper(); + } + + if (!string.IsNullOrEmpty(args.Prefix)) + { + message = $"{args.Prefix}: {message}"; + } + + // Repeat the message + var repeat = args.Repeat ?? 1; + for (int i = 0; i < repeat; i++) + { + Console.WriteLine(message); + } + + return Task.FromResult(0); + } +} +``` + +## Usage Examples + +### Basic usage +```bash +dotnet run -- echo "Hello, World!" +``` +Output: +``` +Hello, World! +``` + +### With uppercase flag +```bash +dotnet run -- echo "Hello, World!" --uppercase +``` +Output: +``` +HELLO, WORLD! +``` + +### With repeat +```bash +dotnet run -- echo "Hello!" -r 3 +``` +Output: +``` +Hello! +Hello! +Hello! +``` + +### With prefix +```bash +dotnet run -- echo "Something went wrong" -p "ERROR" +``` +Output: +``` +ERROR: Something went wrong +``` + +### Combined flags +```bash +dotnet run -- echo "warning" -u -p "ALERT" -r 2 +``` +Output: +``` +ALERT: WARNING +ALERT: WARNING +``` + +## Help Text + +```bash +dotnet run -- echo --help +``` + +Output: +``` +Usage: echo [options] + +Print a message to the console + +Arguments: + The message to print + +Options: + -u, --uppercase Convert message to uppercase + -r, --repeat Number of times to repeat the message + -p, --prefix Prefix to add before the message +``` + +## Key Features Demonstrated + +- โœ… Positional argument (`message`) +- โœ… Boolean flag (`uppercase`) +- โœ… Nullable int flag (`repeat`) +- โœ… String flag with value (`prefix`) +- โœ… Long and short aliases +- โœ… Default values handling +- โœ… Simple logic implementation + +## Complete Program + +```csharp +using System.Reflection; +using Promty; + +var executor = new CommandExecutor(); +executor.RegisterCommandsFromAssembly(Assembly.GetExecutingAssembly()); +return await executor.ExecuteAsync(args); + +[Description("echo", "Print a message to the console")] +public class EchoCommand : Command +{ + public class Args + { + [Description("message", "The message to print")] + public string Message { get; set; } = string.Empty; + + [FlagAlias("uppercase", 'u')] + [Description("Convert message to uppercase")] + public bool Uppercase { get; set; } + + [FlagAlias("repeat", 'r')] + [Description("Number of times to repeat the message")] + public int? Repeat { get; set; } + + [FlagAlias("prefix", 'p')] + [Description("Prefix to add before the message")] + public string? Prefix { get; set; } + } + + public override Task ExecuteAsync(Args args) + { + var message = args.Message; + + if (args.Uppercase) + message = message.ToUpper(); + + if (!string.IsNullOrEmpty(args.Prefix)) + message = $"{args.Prefix}: {message}"; + + var repeat = args.Repeat ?? 1; + for (int i = 0; i < repeat; i++) + Console.WriteLine(message); + + return Task.FromResult(0); + } +} +``` diff --git a/docs/examples/file-operations.md b/docs/examples/file-operations.md new file mode 100644 index 0000000..887493f --- /dev/null +++ b/docs/examples/file-operations.md @@ -0,0 +1,259 @@ +# File Operations Example + +A real-world example demonstrating file operations with proper validation and error handling. + +## File Copy Command + +```csharp +using Promty; +using Promty.Attributes; + +[Description("copy", "Copy a file from source to destination")] +public class CopyCommand : Command +{ + public class Args + { + [Description("source", "The source file path")] + public string Source { get; set; } = string.Empty; + + [Description("destination", "The destination file path")] + public string Destination { get; set; } = string.Empty; + + [FlagAlias("overwrite", 'o')] + [Description("Overwrite destination if it exists")] + public bool Overwrite { get; set; } + + [FlagAlias("verbose", 'v')] + [Description("Show detailed output")] + public bool Verbose { get; set; } + + [FlagAlias("create-dirs", 'c')] + [Description("Create destination directories if they don't exist")] + public bool CreateDirectories { get; set; } + } + + public override Task ExecuteAsync(Args args) + { + try + { + // Validate source file exists + if (!File.Exists(args.Source)) + { + Console.WriteLine($"Error: Source file '{args.Source}' does not exist"); + return Task.FromResult(1); + } + + // Check if destination exists + if (File.Exists(args.Destination) && !args.Overwrite) + { + Console.WriteLine($"Error: Destination file '{args.Destination}' already exists"); + Console.WriteLine("Use --overwrite to replace it"); + return Task.FromResult(1); + } + + // Create destination directory if needed + var destDir = Path.GetDirectoryName(args.Destination); + if (!string.IsNullOrEmpty(destDir) && !Directory.Exists(destDir)) + { + if (args.CreateDirectories) + { + if (args.Verbose) + Console.WriteLine($"Creating directory: {destDir}"); + + Directory.CreateDirectory(destDir); + } + else + { + Console.WriteLine($"Error: Destination directory '{destDir}' does not exist"); + Console.WriteLine("Use --create-dirs to create it automatically"); + return Task.FromResult(1); + } + } + + // Perform the copy + if (args.Verbose) + { + Console.WriteLine($"Copying: {args.Source}"); + Console.WriteLine($"To: {args.Destination}"); + + if (args.Overwrite && File.Exists(args.Destination)) + Console.WriteLine("Overwriting existing file"); + } + + File.Copy(args.Source, args.Destination, args.Overwrite); + + // Success message + if (args.Verbose) + { + var fileInfo = new FileInfo(args.Destination); + Console.WriteLine($"Success! Copied {fileInfo.Length:N0} bytes"); + } + else + { + Console.WriteLine($"Copied: {Path.GetFileName(args.Source)} -> {Path.GetFileName(args.Destination)}"); + } + + return Task.FromResult(0); + } + catch (UnauthorizedAccessException) + { + Console.WriteLine("Error: Access denied. Check file permissions."); + return Task.FromResult(1); + } + catch (IOException ex) + { + Console.WriteLine($"Error: {ex.Message}"); + return Task.FromResult(1); + } + catch (Exception ex) + { + Console.WriteLine($"Unexpected error: {ex.Message}"); + return Task.FromResult(2); + } + } +} +``` + +## Usage Examples + +### Basic copy +```bash +dotnet run -- copy input.txt output.txt +``` + +### Copy with overwrite +```bash +dotnet run -- copy source.txt destination.txt --overwrite +``` + +### Verbose output +```bash +dotnet run -- copy file.txt backup.txt -v +``` +Output: +``` +Copying: file.txt +To: backup.txt +Success! Copied 1,234 bytes +``` + +### Create destination directories +```bash +dotnet run -- copy data.csv backups/2024/data.csv --create-dirs -v +``` +Output: +``` +Creating directory: backups/2024 +Copying: data.csv +To: backups/2024/data.csv +Success! Copied 5,678 bytes +``` + +### Combined flags +```bash +dotnet run -- copy app.log logs/$(date +%Y-%m-%d)/app.log -o -c -v +``` + +## Error Handling + +### Source file doesn't exist +```bash +dotnet run -- copy missing.txt output.txt +``` +Output: +``` +Error: Source file 'missing.txt' does not exist +``` +Exit code: `1` + +### Destination already exists +```bash +dotnet run -- copy input.txt existing.txt +``` +Output: +``` +Error: Destination file 'existing.txt' already exists +Use --overwrite to replace it +``` +Exit code: `1` + +### Destination directory doesn't exist +```bash +dotnet run -- copy file.txt folder/file.txt +``` +Output: +``` +Error: Destination directory 'folder' does not exist +Use --create-dirs to create it automatically +``` +Exit code: `1` + +### Permission denied +```bash +dotnet run -- copy file.txt /root/protected.txt +``` +Output: +``` +Error: Access denied. Check file permissions. +``` +Exit code: `1` + +## Help Text + +```bash +dotnet run -- copy --help +``` + +Output: +``` +Usage: copy [options] + +Copy a file from source to destination + +Arguments: + The source file path + The destination file path + +Options: + -o, --overwrite Overwrite destination if it exists + -v, --verbose Show detailed output + -c, --create-dirs Create destination directories if they don't exist +``` + +## Key Features Demonstrated + +- โœ… Multiple positional arguments +- โœ… Input validation +- โœ… Comprehensive error handling +- โœ… User-friendly error messages +- โœ… Verbose mode for debugging +- โœ… Proper exit codes +- โœ… File system operations +- โœ… Helpful usage hints + +## Extended Features + +You could extend this command with: + +- Progress reporting for large files +- Recursive directory copying +- Pattern matching (wildcards) +- Preserve file attributes +- Dry-run mode +- Backup existing files + +Example with dry-run: + +```csharp +[FlagAlias("dry-run", 'n')] +[Description("Show what would be copied without actually copying")] +public bool DryRun { get; set; } + +// In ExecuteAsync: +if (args.DryRun) +{ + Console.WriteLine("DRY RUN - No files will be copied"); + Console.WriteLine($"Would copy: {args.Source} -> {args.Destination}"); + return Task.FromResult(0); +} +``` diff --git a/docs/examples/flags-enums.md b/docs/examples/flags-enums.md new file mode 100644 index 0000000..15f65b2 --- /dev/null +++ b/docs/examples/flags-enums.md @@ -0,0 +1,340 @@ +# Flags Enums Example + +A complete example showing how to use `[Flags]` enums to group related command-line flags. + +## Build Command with Flags Enum + +```csharp +using Promty; +using Promty.Attributes; + +[Description("build", "Build a project with various options")] +public class BuildCommand : Command +{ + [Flags] + public enum BuildOptions + { + None = 0, + + [FlagAlias("verbose", 'v')] + [Description("Enable verbose output")] + Verbose = 1, + + [FlagAlias("debug", 'd')] + [Description("Build in debug mode")] + Debug = 2, + + [Description("Disable build cache")] + NoCache = 4, + + [Description("Skip running tests")] + SkipTests = 8, + + [Description("Generate documentation")] + GenerateDocs = 16, + + [FlagAlias("watch", 'w')] + [Description("Watch for file changes and rebuild")] + Watch = 32 + } + + public class Args + { + [Description("project", "Project file or directory to build")] + public string Project { get; set; } = string.Empty; + + [FlagAlias("output", 'o')] + [Description("Output directory")] + public string? Output { get; set; } + + public BuildOptions Options { get; set; } + } + + public override async Task ExecuteAsync(Args args) + { + // Display what we're building + Console.WriteLine($"Building: {args.Project}"); + + if (!string.IsNullOrEmpty(args.Output)) + Console.WriteLine($"Output directory: {args.Output}"); + + Console.WriteLine(); + + // Show selected options + if (args.Options != BuildOptions.None) + { + Console.WriteLine("Build options:"); + + if (args.Options.HasFlag(BuildOptions.Verbose)) + Console.WriteLine(" โœ“ Verbose output enabled"); + + if (args.Options.HasFlag(BuildOptions.Debug)) + Console.WriteLine(" โœ“ Debug mode"); + + if (args.Options.HasFlag(BuildOptions.NoCache)) + Console.WriteLine(" โœ“ Cache disabled"); + + if (args.Options.HasFlag(BuildOptions.SkipTests)) + Console.WriteLine(" โœ“ Tests will be skipped"); + + if (args.Options.HasFlag(BuildOptions.GenerateDocs)) + Console.WriteLine(" โœ“ Documentation will be generated"); + + if (args.Options.HasFlag(BuildOptions.Watch)) + Console.WriteLine(" โœ“ Watch mode enabled"); + + Console.WriteLine(); + } + + // Simulate build process + Console.WriteLine("Starting build..."); + + if (args.Options.HasFlag(BuildOptions.Verbose)) + { + Console.WriteLine(" Restoring packages..."); + await Task.Delay(500); + Console.WriteLine(" Compiling source files..."); + await Task.Delay(500); + Console.WriteLine(" Linking assemblies..."); + await Task.Delay(500); + } + else + { + await Task.Delay(1500); + } + + Console.WriteLine("Build completed successfully!"); + + // Run tests unless skipped + if (!args.Options.HasFlag(BuildOptions.SkipTests)) + { + Console.WriteLine(); + Console.WriteLine("Running tests..."); + await Task.Delay(500); + Console.WriteLine("All tests passed!"); + } + + // Generate docs if requested + if (args.Options.HasFlag(BuildOptions.GenerateDocs)) + { + Console.WriteLine(); + Console.WriteLine("Generating documentation..."); + await Task.Delay(500); + Console.WriteLine("Documentation generated!"); + } + + // Watch mode + if (args.Options.HasFlag(BuildOptions.Watch)) + { + Console.WriteLine(); + Console.WriteLine("Watching for changes... (Press Ctrl+C to stop)"); + // In real implementation, set up file watcher here + } + + return 0; + } +} +``` + +## Usage Examples + +### Basic build +```bash +dotnet run -- build MyProject.csproj +``` +Output: +``` +Building: MyProject.csproj + +Starting build... +Build completed successfully! + +Running tests... +All tests passed! +``` + +### With verbose flag +```bash +dotnet run -- build MyProject.csproj --verbose +``` +Output: +``` +Building: MyProject.csproj + +Build options: + โœ“ Verbose output enabled + +Starting build... + Restoring packages... + Compiling source files... + Linking assemblies... +Build completed successfully! + +Running tests... +All tests passed! +``` + +### Multiple flags using aliases +```bash +dotnet run -- build MyProject.csproj -v -d --skip-tests +``` +Output: +``` +Building: MyProject.csproj + +Build options: + โœ“ Verbose output enabled + โœ“ Debug mode + โœ“ Tests will be skipped + +Starting build... + Restoring packages... + Compiling source files... + Linking assemblies... +Build completed successfully! +``` + +### Kebab-case flags +```bash +dotnet run -- build MyProject.csproj --no-cache --generate-docs +``` +Output: +``` +Building: MyProject.csproj + +Build options: + โœ“ Cache disabled + โœ“ Documentation will be generated + +Starting build... +Build completed successfully! + +Running tests... +All tests passed! + +Generating documentation... +Documentation generated! +``` + +### All options +```bash +dotnet run -- build MyProject.csproj -v -d --no-cache --skip-tests --generate-docs --watch -o ./dist +``` +Output: +``` +Building: MyProject.csproj +Output directory: ./dist + +Build options: + โœ“ Verbose output enabled + โœ“ Debug mode + โœ“ Cache disabled + โœ“ Tests will be skipped + โœ“ Documentation will be generated + โœ“ Watch mode enabled + +Starting build... + Restoring packages... + Compiling source files... + Linking assemblies... +Build completed successfully! + +Generating documentation... +Documentation generated! + +Watching for changes... (Press Ctrl+C to stop) +``` + +## Help Text + +```bash +dotnet run -- build --help +``` + +Output: +``` +Usage: build [options] + +Build a project with various options + +Arguments: + Project file or directory to build + +Options: + -o, --output Output directory + -v, --verbose Enable verbose output + -d, --debug Build in debug mode + --no-cache Disable build cache + --skip-tests Skip running tests + --generate-docs Generate documentation + -w, --watch Watch for file changes and rebuild +``` + +## Checking Multiple Flags + +You can check for multiple flags at once: + +```csharp +// Check if either verbose or debug is set +if (args.Options.HasFlag(BuildOptions.Verbose) || + args.Options.HasFlag(BuildOptions.Debug)) +{ + Console.WriteLine("Detailed logging enabled"); +} + +// Check if both verbose and debug are set +if (args.Options.HasFlag(BuildOptions.Verbose | BuildOptions.Debug)) +{ + Console.WriteLine("Maximum verbosity"); +} + +// Get raw value +var optionsValue = (int)args.Options; +Console.WriteLine($"Raw options value: {optionsValue}"); +``` + +## Key Features Demonstrated + +- โœ… `[Flags]` enum with multiple values +- โœ… Mix of `[FlagAlias]` and auto-kebab-case +- โœ… Descriptions on enum fields +- โœ… Checking individual flags with `HasFlag()` +- โœ… Combining short and long aliases +- โœ… None value handling +- โœ… Async command execution +- โœ… Conditional logic based on flags + +## Real-World Use Cases + +This pattern is perfect for: + +1. **Build Tools** - Compilation options, optimization levels +2. **Server Commands** - Feature flags, protocol options +3. **Deploy Tools** - Deployment strategies, rollout options +4. **Test Runners** - Test categories, reporting options +5. **File Processors** - Processing modes, output formats + +## Advantages Over Multiple Booleans + +**Before (8 boolean properties):** +```csharp +public bool Verbose { get; set; } +public bool Debug { get; set; } +public bool NoCache { get; set; } +public bool SkipTests { get; set; } +public bool GenerateDocs { get; set; } +public bool Watch { get; set; } +// ... more flags +``` + +**After (1 enum property):** +```csharp +public BuildOptions Options { get; set; } +``` + +Benefits: +- Less code to write +- Related options grouped together +- Easy to add new options +- Automatic help text +- Type-safe combinations diff --git a/docs/examples/process-wrapper.md b/docs/examples/process-wrapper.md new file mode 100644 index 0000000..52dcfaf --- /dev/null +++ b/docs/examples/process-wrapper.md @@ -0,0 +1,308 @@ +# Process Wrapper Example + +Examples of wrapping external CLI tools using `ProcessCommand`. + +## Git Wrapper + +```csharp +using Promty; +using Promty.Attributes; + +[Description("git", "Execute git commands")] +public class GitCommand : ProcessCommand +{ + protected override string ExecutablePath => "git"; +} +``` + +Usage: +```bash +dotnet run -- git status +dotnet run -- git add . +dotnet run -- git commit -m "Initial commit" +dotnet run -- git push origin main +``` + +## Docker Wrapper + +```csharp +[Description("docker", "Execute docker commands")] +public class DockerCommand : ProcessCommand +{ + protected override string ExecutablePath => "docker"; +} +``` + +Usage: +```bash +dotnet run -- docker ps +dotnet run -- docker build -t myapp . +dotnet run -- docker run -p 8080:80 myapp +dotnet run -- docker logs mycontainer +``` + +## Custom Build Script Wrapper + +```csharp +[Description("build-legacy", "Run the legacy build script")] +public class BuildLegacyCommand : ProcessCommand +{ + protected override string ExecutablePath => "./scripts/build.sh"; +} +``` + +Usage: +```bash +dotnet run -- build-legacy --target production +``` + +## Cross-Platform Python Wrapper + +```csharp +[Description("python", "Execute Python scripts")] +public class PythonCommand : ProcessCommand +{ + protected override string ExecutablePath => + Environment.OSVersion.Platform == PlatformID.Win32NT + ? "python.exe" + : "python3"; +} +``` + +Usage: +```bash +dotnet run -- python script.py --arg value +dotnet run -- python -m pip install requests +``` + +## NPM Wrapper with Default Arguments + +Override `GetArguments` to add default arguments: + +```csharp +[Description("npm-install", "Install npm packages with preferred settings")] +public class NpmInstallCommand : ProcessCommand +{ + protected override string ExecutablePath => "npm"; + + protected override string[] GetArguments(string[] args) + { + // Always use "npm install" with --save flag + var defaultArgs = new[] { "install", "--save" }; + return defaultArgs.Concat(args).ToArray(); + } +} +``` + +Usage: +```bash +# Runs: npm install --save express +dotnet run -- npm-install express + +# Runs: npm install --save express body-parser +dotnet run -- npm-install express body-parser +``` + +## Kubectl Wrapper with Namespace + +```csharp +[Description("k8s", "Execute kubectl commands with default namespace")] +public class KubectlCommand : ProcessCommand +{ + protected override string ExecutablePath => "kubectl"; + + protected override string[] GetArguments(string[] args) + { + // Add default namespace unless already specified + if (!args.Any(a => a == "-n" || a == "--namespace")) + { + return new[] { "--namespace", "production" } + .Concat(args) + .ToArray(); + } + return args; + } +} +``` + +Usage: +```bash +# Uses production namespace by default +dotnet run -- k8s get pods + +# Override with custom namespace +dotnet run -- k8s get pods -n staging +``` + +## Terraform Wrapper with Working Directory + +```csharp +[Description("tf", "Execute Terraform commands in infrastructure directory")] +public class TerraformCommand : ProcessCommand +{ + protected override string ExecutablePath => "terraform"; + + public override async Task ExecuteAsync(string[] args) + { + // Change to infrastructure directory + var originalDir = Directory.GetCurrentDirectory(); + var infraDir = Path.Combine(originalDir, "infrastructure"); + + if (!Directory.Exists(infraDir)) + { + Console.WriteLine($"Error: Infrastructure directory not found: {infraDir}"); + return 1; + } + + try + { + Directory.SetCurrentDirectory(infraDir); + Console.WriteLine($"Working directory: {infraDir}"); + return await base.ExecuteAsync(args); + } + finally + { + Directory.SetCurrentDirectory(originalDir); + } + } +} +``` + +Usage: +```bash +dotnet run -- tf init +dotnet run -- tf plan +dotnet run -- tf apply -auto-approve +``` + +## Composite Command (Multiple Tools) + +For complex workflows, use a standard command that runs multiple processes: + +```csharp +[Description("deploy", "Build, test, and deploy the application")] +public class DeployCommand : Command +{ + public class Args + { + [FlagAlias("environment", 'e')] + [Description("Target environment (dev, staging, prod)")] + public string Environment { get; set; } = "dev"; + + [FlagAlias("skip-tests", 't')] + [Description("Skip running tests")] + public bool SkipTests { get; set; } + } + + public override async Task ExecuteAsync(Args args) + { + Console.WriteLine($"Deploying to: {args.Environment}"); + Console.WriteLine(); + + // Step 1: Build + Console.WriteLine("Step 1: Building..."); + var buildResult = await RunProcess("dotnet", "build -c Release"); + if (buildResult != 0) + { + Console.WriteLine("Build failed!"); + return buildResult; + } + Console.WriteLine("โœ“ Build successful"); + Console.WriteLine(); + + // Step 2: Test (optional) + if (!args.SkipTests) + { + Console.WriteLine("Step 2: Running tests..."); + var testResult = await RunProcess("dotnet", "test -c Release"); + if (testResult != 0) + { + Console.WriteLine("Tests failed!"); + return testResult; + } + Console.WriteLine("โœ“ Tests passed"); + Console.WriteLine(); + } + + // Step 3: Deploy + Console.WriteLine($"Step 3: Deploying to {args.Environment}..."); + var deployResult = await RunProcess( + "./deploy.sh", + args.Environment + ); + if (deployResult != 0) + { + Console.WriteLine("Deployment failed!"); + return deployResult; + } + Console.WriteLine("โœ“ Deployment successful"); + + Console.WriteLine(); + Console.WriteLine($"Successfully deployed to {args.Environment}!"); + return 0; + } + + private async Task RunProcess(string executable, string arguments) + { + var process = new System.Diagnostics.Process + { + StartInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = executable, + Arguments = arguments, + UseShellExecute = false, + RedirectStandardOutput = false, + RedirectStandardError = false + } + }; + + process.Start(); + await process.WaitForExitAsync(); + return process.ExitCode; + } +} +``` + +Usage: +```bash +# Deploy to dev +dotnet run -- deploy + +# Deploy to production, skip tests +dotnet run -- deploy -e prod --skip-tests +``` + +Output: +``` +Deploying to: prod + +Step 1: Building... +โœ“ Build successful + +Step 3: Deploying to prod... +โœ“ Deployment successful + +Successfully deployed to prod! +``` + +## When to Use Process Commands + +โœ… **Use ProcessCommand when:** +- Wrapping existing CLI tools +- Forwarding all arguments unchanged +- Creating simple shortcuts +- No argument parsing needed + +โœ… **Use Standard Command when:** +- You need typed argument binding +- Custom validation required +- Running multiple processes +- Complex business logic + +## Best Practices + +1. **Check if executable exists** - Provide helpful error messages +2. **Handle exit codes** - Forward them appropriately +3. **Use full paths** - When executable not in PATH +4. **Cross-platform support** - Check OS when needed +5. **Add default arguments** - Override `GetArguments()` for convenience diff --git a/docs/guide/arguments.md b/docs/guide/arguments.md new file mode 100644 index 0000000..703da85 --- /dev/null +++ b/docs/guide/arguments.md @@ -0,0 +1,248 @@ +# Arguments & Flags + +Promty supports two types of command-line arguments: **positional arguments** and **flag arguments**. + +## Positional Arguments + +Positional arguments are required and must be provided in order. They don't have a `[FlagAlias]` attribute. + +```csharp +public class Args +{ + [Description("name", "The name argument")] + public string Name { get; set; } = string.Empty; + + [Description("age", "The age argument")] + public int Age { get; set; } +} +``` + +Usage: +```bash +dotnet run -- mycommand John 25 +``` + +### Rules for Positional Arguments + +1. Always **required** - must be provided +2. Order **matters** - must be provided in the order they're defined +3. Must come **before** any flags +4. Cannot be nullable + +## Flag Arguments + +Flag arguments are optional and can be provided in any order. They have a `[FlagAlias]` attribute. + +```csharp +public class Args +{ + [FlagAlias("verbose", 'v')] + [Description("Enable verbose output")] + public bool Verbose { get; set; } + + [FlagAlias("port", 'p')] + [Description("Port number")] + public int? Port { get; set; } +} +``` + +Usage: +```bash +# Long form +dotnet run -- mycommand --verbose --port 8080 + +# Short form +dotnet run -- mycommand -v -p 8080 + +# Mixed +dotnet run -- mycommand --verbose -p 8080 +``` + +## Supported Types + +Promty automatically converts argument strings to the following types: + +### Primitive Types +- `string` +- `int` +- `long` +- `double` +- `bool` + +### Nullable Types +- `int?` +- `long?` +- `double?` +- `bool?` + +### Enums +- Standard enums +- `[Flags]` enums (see [Flags Enums](/guide/flags-enums)) + +## Boolean Flags + +Boolean flags have special behavior - they don't require a value: + +```csharp +[FlagAlias("force", 'f')] +public bool Force { get; set; } +``` + +Both of these work: +```bash +dotnet run -- mycommand --force # Implicitly true +dotnet run -- mycommand --force true # Explicitly true +``` + +## Nullable Arguments + +Use nullable types for optional flags with values: + +```csharp +public class Args +{ + [FlagAlias("timeout", 't')] + [Description("Timeout in seconds")] + public int? Timeout { get; set; } +} +``` + +If not provided, the value will be `null`: +```csharp +public override Task ExecuteAsync(Args args) +{ + var timeout = args.Timeout ?? 30; // Use 30 if not provided + Console.WriteLine($"Timeout: {timeout}s"); + return Task.FromResult(0); +} +``` + +## Description Attribute + +Use `[Description]` to provide help text: + +### For Commands +```csharp +[Description("greet", "Greets a person by name")] +public class GreetCommand : Command +``` + +### For Positional Arguments +```csharp +[Description("name", "The name of the person to greet")] +public string Name { get; set; } +``` + +### For Flags +```csharp +[FlagAlias("verbose", 'v')] +[Description("Enable verbose output")] +public bool Verbose { get; set; } +``` + +## FlagAlias Attribute + +Define long and/or short aliases for flags: + +### Both Long and Short +```csharp +[FlagAlias("verbose", 'v')] +``` + +### Long Only +```csharp +[FlagAlias("verbose")] +``` + +### Short Only +```csharp +[FlagAlias('v')] +``` + +## Examples + +### Simple Command +```csharp +public class Args +{ + [Description("message", "The message to display")] + public string Message { get; set; } = string.Empty; + + [FlagAlias("uppercase", 'u')] + [Description("Convert to uppercase")] + public bool Uppercase { get; set; } +} +``` + +```bash +dotnet run -- mycommand "Hello World" --uppercase +``` + +### Multiple Positional Arguments +```csharp +public class Args +{ + [Description("source", "Source file")] + public string Source { get; set; } = string.Empty; + + [Description("destination", "Destination file")] + public string Destination { get; set; } = string.Empty; + + [FlagAlias("overwrite", 'o')] + [Description("Overwrite if exists")] + public bool Overwrite { get; set; } +} +``` + +```bash +dotnet run -- copy input.txt output.txt --overwrite +``` + +### Mixed Types +```csharp +public class Args +{ + [Description("name", "Server name")] + public string Name { get; set; } = string.Empty; + + [FlagAlias("port", 'p')] + [Description("Port number")] + public int? Port { get; set; } + + [FlagAlias("timeout", 't')] + [Description("Timeout in seconds")] + public double? Timeout { get; set; } + + [FlagAlias("verbose", 'v')] + [Description("Verbose output")] + public bool Verbose { get; set; } +} +``` + +```bash +dotnet run -- server MyServer -p 8080 -t 30.5 --verbose +``` + +## Validation + +Add custom validation in your command: + +```csharp +public override Task ExecuteAsync(Args args) +{ + if (args.Port < 1 || args.Port > 65535) + { + Console.WriteLine("Error: Port must be between 1 and 65535"); + return Task.FromResult(1); + } + + if (!File.Exists(args.Source)) + { + Console.WriteLine($"Error: Source file '{args.Source}' not found"); + return Task.FromResult(1); + } + + // Continue with valid arguments... + return Task.FromResult(0); +} +``` diff --git a/docs/guide/commands.md b/docs/guide/commands.md new file mode 100644 index 0000000..7ffd91d --- /dev/null +++ b/docs/guide/commands.md @@ -0,0 +1,183 @@ +# Commands + +Commands are the core building blocks of your CLI application. Promty provides two types of commands: + +## Standard Commands + +Standard commands extend `Command` and provide full type-safe argument binding. + +### Basic Structure + +```csharp +[Description("command-name", "Command description")] +public class MyCommand : Command +{ + public class Args + { + // Define your arguments here + } + + public override Task ExecuteAsync(Args args) + { + // Your command logic here + return Task.FromResult(0); // Return exit code + } +} +``` + +### Example: File Copy Command + +```csharp +using Promty; +using Promty.Attributes; + +[Description("copy", "Copies a file from source to destination")] +public class CopyCommand : Command +{ + public class Args + { + [Description("source", "The source file path")] + public string Source { get; set; } = string.Empty; + + [Description("destination", "The destination file path")] + public string Destination { get; set; } = string.Empty; + + [FlagAlias("verbose", 'v')] + [Description("Show detailed output")] + public bool Verbose { get; set; } + + [FlagAlias("overwrite", 'o')] + [Description("Overwrite existing files")] + public bool Overwrite { get; set; } + } + + public override Task ExecuteAsync(Args args) + { + if (args.Verbose) + { + Console.WriteLine($"Copying {args.Source} to {args.Destination}"); + } + + try + { + File.Copy(args.Source, args.Destination, args.Overwrite); + + if (args.Verbose) + { + Console.WriteLine("Copy completed successfully"); + } + + return Task.FromResult(0); // Success + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + return Task.FromResult(1); // Error + } + } +} +``` + +Usage: +```bash +dotnet run -- copy input.txt output.txt --verbose --overwrite +``` + +## Process Commands + +Process commands extend `ProcessCommand` and forward all arguments to an external executable. + +### Basic Structure + +```csharp +[Description("command-name", "Command description")] +public class MyProcessCommand : ProcessCommand +{ + protected override string ExecutablePath => "executable-name"; +} +``` + +### Example: Git Wrapper + +```csharp +using Promty; +using Promty.Attributes; + +[Description("git", "Execute git commands")] +public class GitCommand : ProcessCommand +{ + protected override string ExecutablePath => "git"; +} +``` + +Usage: +```bash +dotnet run -- git status +dotnet run -- git commit -m "Initial commit" +dotnet run -- git --version +``` + +### Example: Docker Wrapper + +```csharp +[Description("docker", "Execute docker commands")] +public class DockerCommand : ProcessCommand +{ + protected override string ExecutablePath => "docker"; +} +``` + +Usage: +```bash +dotnet run -- docker ps +dotnet run -- docker build -t myapp . +``` + +## Command Registration + +Register your commands in `Program.cs`: + +### Register from Assembly + +```csharp +var executor = new CommandExecutor(); +executor.RegisterCommandsFromAssembly(Assembly.GetExecutingAssembly()); +``` + +### Register Multiple Assemblies + +```csharp +var executor = new CommandExecutor(); +executor.RegisterCommandsFromAssembly(Assembly.GetExecutingAssembly()); +executor.RegisterCommandsFromAssembly(typeof(PluginCommand).Assembly); +``` + +## Exit Codes + +Return appropriate exit codes from your commands: + +- `0` - Success +- `1` - General error +- Other codes - Application-specific errors + +```csharp +public override Task ExecuteAsync(Args args) +{ + if (!File.Exists(args.Source)) + { + Console.WriteLine($"Error: Source file '{args.Source}' not found"); + return Task.FromResult(1); // Error exit code + } + + // Success logic... + return Task.FromResult(0); // Success exit code +} +``` + +## Best Practices + +1. **Keep Commands Focused** - Each command should do one thing well +2. **Use Descriptive Names** - Make command names clear and intuitive +3. **Provide Good Descriptions** - Help users understand what each command does +4. **Validate Input** - Check for invalid arguments and provide helpful error messages +5. **Return Proper Exit Codes** - Follow standard conventions for exit codes diff --git a/docs/guide/flags-enums.md b/docs/guide/flags-enums.md new file mode 100644 index 0000000..6168354 --- /dev/null +++ b/docs/guide/flags-enums.md @@ -0,0 +1,302 @@ +# Flags Enums + +Instead of defining multiple boolean properties, you can use a `[Flags]` enum to group related flags together. Each enum value becomes an individual command-line flag that can be combined. + +## Why Use Flags Enums? + +**Before (Multiple Booleans):** +```csharp +public class Args +{ + [FlagAlias("verbose", 'v')] + public bool Verbose { get; set; } + + [FlagAlias("debug", 'd')] + public bool Debug { get; set; } + + [FlagAlias("no-cache")] + public bool NoCache { get; set; } + + [FlagAlias("skip-tests")] + public bool SkipTests { get; set; } +} +``` + +**After (Flags Enum):** +```csharp +[Flags] +public enum BuildOptions +{ + None = 0, + [FlagAlias("verbose", 'v')] + Verbose = 1, + [FlagAlias("debug", 'd')] + Debug = 2, + NoCache = 4, + SkipTests = 8 +} + +public class Args +{ + public BuildOptions Options { get; set; } +} +``` + +## Basic Usage + +### Define a Flags Enum + +```csharp +using Promty; +using Promty.Attributes; + +[Description("build", "Build a project with options")] +public class BuildCommand : Command +{ + [Flags] + public enum BuildOptions + { + None = 0, + [FlagAlias("verbose", 'v')] + [Description("Enable verbose output")] + Verbose = 1, + [FlagAlias("debug", 'd')] + [Description("Build in debug mode")] + Debug = 2, + [Description("Disable build cache")] + NoCache = 4, + [Description("Skip running tests")] + SkipTests = 8 + } + + public class Args + { + [Description("project", "Project name")] + public string Project { get; set; } = string.Empty; + + // No [FlagAlias] needed on the property! + public BuildOptions Options { get; set; } + } + + public override Task ExecuteAsync(Args args) + { + Console.WriteLine($"Building {args.Project}"); + + if (args.Options.HasFlag(BuildOptions.Verbose)) + Console.WriteLine("Verbose mode enabled"); + + if (args.Options.HasFlag(BuildOptions.Debug)) + Console.WriteLine("Debug mode enabled"); + + if (args.Options.HasFlag(BuildOptions.NoCache)) + Console.WriteLine("Cache disabled"); + + if (args.Options.HasFlag(BuildOptions.SkipTests)) + Console.WriteLine("Tests skipped"); + + return Task.FromResult(0); + } +} +``` + +### Command-Line Usage + +```bash +# Combine multiple flags +dotnet run -- build MyProject --verbose --debug --skip-tests + +# Use short aliases +dotnet run -- build MyProject -v -d + +# Mix aliases with kebab-case names +dotnet run -- build MyProject -v --no-cache + +# Use no flags +dotnet run -- build MyProject +``` + +## Features + +### 1. Custom Aliases with FlagAlias + +Define custom long and short aliases: + +```csharp +[FlagAlias("verbose", 'v')] +[Description("Enable verbose output")] +Verbose = 1, +``` + +Command line: +```bash +--verbose # or -v +``` + +### 2. Automatic Kebab-Case Conversion + +Enum fields without `[FlagAlias]` automatically convert to kebab-case: + +```csharp +NoCache = 4, // Becomes: --no-cache +SkipTests = 8, // Becomes: --skip-tests +EnableFeatureX = 16 // Becomes: --enable-feature-x +``` + +### 3. Descriptions on Enum Fields + +Add `[Description]` to enum fields for help text: + +```csharp +[Description("Enable verbose output")] +Verbose = 1, + +[Description("Disable build cache")] +NoCache = 4, +``` + +These appear in the help text: +``` +Options: + -v, --verbose Enable verbose output + --no-cache Disable build cache +``` + +### 4. None Value Excluded + +The `None = 0` value is automatically excluded from help text and command-line parsing. + +## Checking Flags in Code + +Use `HasFlag()` to check if a flag is set: + +```csharp +public override Task ExecuteAsync(Args args) +{ + if (args.Options.HasFlag(BuildOptions.Verbose)) + { + Console.WriteLine("Verbose mode enabled"); + } + + if (args.Options.HasFlag(BuildOptions.Debug)) + { + Console.WriteLine("Debug mode enabled"); + } + + // Check for multiple flags + if (args.Options.HasFlag(BuildOptions.Debug | BuildOptions.NoCache)) + { + Console.WriteLine("Debug mode with no cache"); + } + + // Check if no flags are set + if (args.Options == BuildOptions.None) + { + Console.WriteLine("No options specified"); + } + + return Task.FromResult(0); +} +``` + +## Complete Example + +```csharp +using Promty; +using Promty.Attributes; + +[Description("serve", "Start a development server")] +public class ServeCommand : Command +{ + [Flags] + public enum ServerOptions + { + None = 0, + [FlagAlias("watch", 'w')] + [Description("Watch for file changes")] + Watch = 1, + [FlagAlias("open", 'o')] + [Description("Open browser automatically")] + OpenBrowser = 2, + [Description("Enable HTTPS")] + Https = 4, + [Description("Enable hot module replacement")] + HotReload = 8, + [Description("Show debug information")] + Debug = 16 + } + + public class Args + { + [Description("port", "Port number")] + public int Port { get; set; } = 3000; + + public ServerOptions Options { get; set; } + } + + public override Task ExecuteAsync(Args args) + { + Console.WriteLine($"Starting server on port {args.Port}"); + + if (args.Options.HasFlag(ServerOptions.Watch)) + Console.WriteLine("File watching enabled"); + + if (args.Options.HasFlag(ServerOptions.OpenBrowser)) + Console.WriteLine("Opening browser..."); + + if (args.Options.HasFlag(ServerOptions.Https)) + Console.WriteLine("HTTPS enabled"); + + if (args.Options.HasFlag(ServerOptions.HotReload)) + Console.WriteLine("Hot reload enabled"); + + if (args.Options.HasFlag(ServerOptions.Debug)) + Console.WriteLine("Debug mode enabled"); + + // Start server logic... + return Task.FromResult(0); + } +} +``` + +Usage: +```bash +dotnet run -- serve 8080 --watch --open --https +dotnet run -- serve 3000 -w -o --hot-reload --debug +``` + +## Help Text Generation + +The help text automatically displays each flag individually: + +``` +Usage: build [options] + +Build a project with options + +Arguments: + Project name + +Options: + -v, --verbose Enable verbose output + -d, --debug Build in debug mode + --no-cache Disable build cache + --skip-tests Skip running tests +``` + +## Best Practices + +1. **Always Start with None = 0** - Required for flags enums +2. **Use Powers of 2** - Values should be 1, 2, 4, 8, 16, 32, etc. +3. **Add Descriptions** - Help users understand each flag +4. **Use FlagAlias for Common Flags** - Provide short aliases for frequently used flags +5. **Group Related Flags** - Keep related functionality together in one enum +6. **Use Descriptive Names** - Make it clear what each flag does + +## Advantages + +โœ… **Less Boilerplate** - One enum vs multiple boolean properties +โœ… **Better Organization** - Related flags grouped together +โœ… **Type Safety** - Enum values are compile-time checked +โœ… **Easy to Extend** - Add new flags by adding enum values +โœ… **Automatic Help Text** - Each flag appears individually +โœ… **Flexible Naming** - Use FlagAlias or auto-kebab-case diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md new file mode 100644 index 0000000..b9f4ba4 --- /dev/null +++ b/docs/guide/getting-started.md @@ -0,0 +1,95 @@ +# Getting Started + +## Installation + +Install Promty via NuGet: + +```bash +dotnet add package Promty +``` + +## Your First Command + +Create a simple command that greets a user: + +```csharp +using Promty; +using Promty.Attributes; + +[Description("greet", "Greets a person by name")] +public class GreetCommand : Command +{ + public class Args + { + [Description("name", "The name of the person to greet")] + public string Name { get; set; } = string.Empty; + + [FlagAlias("uppercase", 'u')] + [Description("Print the greeting in uppercase")] + public bool Uppercase { get; set; } + + [FlagAlias("repeat", 'r')] + [Description("Number of times to repeat the greeting")] + public int? Repeat { get; set; } + } + + public override Task ExecuteAsync(Args args) + { + var greeting = $"Hello, {args.Name}!"; + + if (args.Uppercase) + { + greeting = greeting.ToUpper(); + } + + var repeat = args.Repeat ?? 1; + for (int i = 0; i < repeat; i++) + { + Console.WriteLine(greeting); + } + + return Task.FromResult(0); + } +} +``` + +## Set Up the Executor + +In your `Program.cs`: + +```csharp +using System.Reflection; +using Promty; + +var executor = new CommandExecutor(); +executor.RegisterCommandsFromAssembly(Assembly.GetExecutingAssembly()); + +return await executor.ExecuteAsync(args); +``` + +## Run Your CLI + +```bash +# Show available commands +dotnet run + +# Run with arguments +dotnet run -- greet Alice + +# Use flags +dotnet run -- greet Bob --uppercase -r 3 +``` + +Output: +``` +HELLO, BOB! +HELLO, BOB! +HELLO, BOB! +``` + +## Next Steps + +- Learn about [Commands](/guide/commands) +- Explore [Arguments & Flags](/guide/arguments) +- Check out [Flags Enums](/guide/flags-enums) for grouping related flags +- See more [Examples](/examples/basic) diff --git a/docs/guide/help-text.md b/docs/guide/help-text.md new file mode 100644 index 0000000..33dcfcf --- /dev/null +++ b/docs/guide/help-text.md @@ -0,0 +1,303 @@ +# Help Text + +Promty automatically generates beautiful help text from your command and argument attributes. + +## Automatic Generation + +Help text is displayed when: +- No commands are provided (shows command list) +- An unknown command is entered +- A required argument is missing +- The `--help` or `-h` flag is used (if implemented) + +## Command List + +When no command is provided, Promty shows all available commands: + +``` +Available commands: + + build Build a project with options + copy Copies a file from source to destination + git Execute git commands + greet Greets a person by name +``` + +### Customizing Command Descriptions + +Use the `[Description]` attribute on your command class: + +```csharp +[Description("greet", "Greets a person by name")] +public class GreetCommand : Command +{ + // ... +} +``` + +The first parameter is the command name, the second is the description. + +## Command Help + +When a required argument is missing, Promty shows command-specific help: + +``` +Usage: greet [options] + +Greets a person by name + +Arguments: + The name of the person to greet + +Options: + -u, --uppercase Print the greeting in uppercase + -r, --repeat Number of times to repeat the greeting +``` + +### Structure + +The help text includes: + +1. **Usage Line** - Shows command structure +2. **Description** - From the command's `[Description]` attribute +3. **Arguments** - Required positional arguments +4. **Options** - Optional flags + +## Argument Help + +### Positional Arguments + +```csharp +[Description("source", "The source file path")] +public string Source { get; set; } = string.Empty; +``` + +Appears in help as: +``` +Arguments: + The source file path +``` + +### Flag Arguments + +```csharp +[FlagAlias("verbose", 'v')] +[Description("Enable verbose output")] +public bool Verbose { get; set; } +``` + +Appears in help as: +``` +Options: + -v, --verbose Enable verbose output +``` + +### Flags with Values + +Non-boolean flags show a value placeholder: + +```csharp +[FlagAlias("port", 'p')] +[Description("Port number")] +public int? Port { get; set; } +``` + +Appears in help as: +``` +Options: + -p, --port Port number +``` + +## Flags Enum Help + +Flags enums display each flag individually: + +```csharp +[Flags] +public enum BuildOptions +{ + None = 0, + [FlagAlias("verbose", 'v')] + [Description("Enable verbose output")] + Verbose = 1, + [FlagAlias("debug", 'd')] + [Description("Build in debug mode")] + Debug = 2, + [Description("Disable build cache")] + NoCache = 4, + [Description("Skip running tests")] + SkipTests = 8 +} + +public class Args +{ + public BuildOptions Options { get; set; } +} +``` + +Appears in help as: +``` +Options: + -v, --verbose Enable verbose output + -d, --debug Build in debug mode + --no-cache Disable build cache + --skip-tests Skip running tests +``` + +Note: The `None = 0` value is automatically excluded. + +## Examples + +### Simple Command + +```csharp +[Description("echo", "Print a message")] +public class EchoCommand : Command +{ + public class Args + { + [Description("message", "The message to print")] + public string Message { get; set; } = string.Empty; + + [FlagAlias("uppercase", 'u')] + [Description("Convert to uppercase")] + public bool Uppercase { get; set; } + } +} +``` + +Help output: +``` +Usage: echo [options] + +Print a message + +Arguments: + The message to print + +Options: + -u, --uppercase Convert to uppercase +``` + +### Multiple Arguments + +```csharp +[Description("copy", "Copy a file")] +public class CopyCommand : Command +{ + public class Args + { + [Description("source", "Source file path")] + public string Source { get; set; } = string.Empty; + + [Description("destination", "Destination file path")] + public string Destination { get; set; } = string.Empty; + + [FlagAlias("overwrite", 'o')] + [Description("Overwrite if exists")] + public bool Overwrite { get; set; } + + [FlagAlias("verbose", 'v')] + [Description("Show detailed output")] + public bool Verbose { get; set; } + } +} +``` + +Help output: +``` +Usage: copy [options] + +Copy a file + +Arguments: + Source file path + Destination file path + +Options: + -o, --overwrite Overwrite if exists + -v, --verbose Show detailed output +``` + +### With Flags Enum + +```csharp +[Description("build", "Build a project")] +public class BuildCommand : Command +{ + [Flags] + public enum BuildOptions + { + None = 0, + [FlagAlias("verbose", 'v')] + [Description("Enable verbose output")] + Verbose = 1, + [Description("Skip tests")] + SkipTests = 2 + } + + public class Args + { + [Description("project", "Project name")] + public string Project { get; set; } = string.Empty; + + public BuildOptions Options { get; set; } + } +} +``` + +Help output: +``` +Usage: build [options] + +Build a project + +Arguments: + Project name + +Options: + -v, --verbose Enable verbose output + --skip-tests Skip tests +``` + +## Best Practices + +1. **Write Clear Descriptions** - Make it obvious what each command/argument does +2. **Be Consistent** - Use similar language across all commands +3. **Keep It Concise** - Help text should be scannable +4. **Use Examples** - Show typical usage patterns +5. **Group Related Flags** - Use flags enums for related options + +## Custom Help Command + +You can implement a custom help command if needed: + +```csharp +[Description("help", "Show help information")] +public class HelpCommand : Command +{ + public class Args + { + [Description("command", "Command to get help for")] + public string? CommandName { get; set; } + } + + public override Task ExecuteAsync(Args args) + { + if (string.IsNullOrEmpty(args.CommandName)) + { + // Show all commands + Console.WriteLine("Available commands:"); + // ... list commands + } + else + { + // Show specific command help + Console.WriteLine($"Help for: {args.CommandName}"); + // ... show command details + } + + return Task.FromResult(0); + } +} +``` diff --git a/docs/guide/process-commands.md b/docs/guide/process-commands.md new file mode 100644 index 0000000..7732ed6 --- /dev/null +++ b/docs/guide/process-commands.md @@ -0,0 +1,234 @@ +# Process Commands + +Process commands allow you to easily wrap external CLI tools and forward all arguments to them. This is perfect for creating shortcuts, wrappers, or composite tools. + +## Basic Usage + +Extend `ProcessCommand` and specify the executable path: + +```csharp +using Promty; +using Promty.Attributes; + +[Description("git", "Execute git commands")] +public class GitCommand : ProcessCommand +{ + protected override string ExecutablePath => "git"; +} +``` + +Usage: +```bash +dotnet run -- git status +dotnet run -- git commit -m "Initial commit" +dotnet run -- git push origin main +``` + +All arguments after `git` are forwarded directly to the git executable. + +## Examples + +### Docker Wrapper + +```csharp +[Description("docker", "Execute docker commands")] +public class DockerCommand : ProcessCommand +{ + protected override string ExecutablePath => "docker"; +} +``` + +Usage: +```bash +dotnet run -- docker ps +dotnet run -- docker build -t myapp . +dotnet run -- docker run -p 8080:80 myapp +``` + +### .NET CLI Wrapper + +```csharp +[Description("dotnet", "Execute dotnet CLI commands")] +public class DotNetCommand : ProcessCommand +{ + protected override string ExecutablePath => "dotnet"; +} +``` + +Usage: +```bash +dotnet run -- dotnet build +dotnet run -- dotnet test +dotnet run -- dotnet pack +``` + +### NPM Wrapper + +```csharp +[Description("npm", "Execute npm commands")] +public class NpmCommand : ProcessCommand +{ + protected override string ExecutablePath => "npm"; +} +``` + +Usage: +```bash +dotnet run -- npm install +dotnet run -- npm run build +dotnet run -- npm test +``` + +## With Full Paths + +If the executable is not in the system PATH, specify the full path: + +```csharp +[Description("custom", "Execute custom tool")] +public class CustomCommand : ProcessCommand +{ + protected override string ExecutablePath => + "/usr/local/bin/custom-tool"; // Unix + // or + // @"C:\Tools\custom-tool.exe"; // Windows +} +``` + +## Cross-Platform Paths + +For cross-platform support: + +```csharp +[Description("python", "Execute Python scripts")] +public class PythonCommand : ProcessCommand +{ + protected override string ExecutablePath => + Environment.OSVersion.Platform == PlatformID.Win32NT + ? "python.exe" + : "python3"; +} +``` + +## Use Cases + +### 1. Create Shortcuts + +Simplify complex commands: + +```csharp +[Description("build-all", "Build all projects")] +public class BuildAllCommand : ProcessCommand +{ + protected override string ExecutablePath => "dotnet"; + + // Override to add default arguments + protected override string[] GetArguments(string[] args) + { + return new[] { "build", "-c", "Release" } + .Concat(args) + .ToArray(); + } +} +``` + +### 2. Wrap External Tools + +Make external tools part of your CLI: + +```csharp +[Description("format", "Format code with Prettier")] +public class FormatCommand : ProcessCommand +{ + protected override string ExecutablePath => "npx"; + + protected override string[] GetArguments(string[] args) + { + return new[] { "prettier", "--write", "." } + .Concat(args) + .ToArray(); + } +} +``` + +### 3. Create Composite Commands + +Chain multiple operations: + +```csharp +[Description("deploy", "Build and deploy the application")] +public class DeployCommand : Command +{ + public class Args { } + + public override async Task ExecuteAsync(Args args) + { + // Build + Console.WriteLine("Building..."); + var buildResult = await RunProcessAsync("dotnet", "build -c Release"); + if (buildResult != 0) return buildResult; + + // Test + Console.WriteLine("Testing..."); + var testResult = await RunProcessAsync("dotnet", "test"); + if (testResult != 0) return testResult; + + // Deploy + Console.WriteLine("Deploying..."); + var deployResult = await RunProcessAsync("./deploy.sh"); + return deployResult; + } + + private async Task RunProcessAsync(string executable, string args = "") + { + var process = new System.Diagnostics.Process + { + StartInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = executable, + Arguments = args, + UseShellExecute = false + } + }; + process.Start(); + await process.WaitForExitAsync(); + return process.ExitCode; + } +} +``` + +## Exit Codes + +Process commands automatically return the exit code from the external process: + +- `0` - Process succeeded +- Non-zero - Process failed (specific meaning depends on the tool) + +## Standard Output/Error + +By default, process commands: +- Forward stdout to your console +- Forward stderr to your console +- Wait for the process to complete +- Return the exit code + +## Advantages + +โœ… **No Argument Parsing** - All arguments forwarded as-is +โœ… **Simple Setup** - Just specify the executable path +โœ… **Exit Code Propagation** - Exit codes automatically forwarded +โœ… **Console Integration** - Output appears directly in your console +โœ… **Quick Wrappers** - Perfect for creating shortcuts and aliases + +## When to Use + +**Use Process Commands when:** +- Wrapping existing CLI tools +- Creating shortcuts for complex commands +- Building composite workflows +- Forwarding all arguments to external tools + +**Use Standard Commands when:** +- You need typed argument binding +- You want validation and help text +- Building custom business logic +- Processing arguments in code diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..2974f37 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,99 @@ +--- +layout: home + +hero: + name: "Promty" + text: "Build Beautiful CLI Tools" + tagline: A powerful and flexible command-line parser and command executor framework for .NET + image: + src: /logo.png + alt: Promty + actions: + - theme: brand + text: Get Started + link: /guide/getting-started + - theme: alt + text: View on GitHub + link: https://github.com/stho01/promty + +features: + - icon: ๐ŸŽฏ + title: Type-Safe Argument Binding + details: Automatically bind command-line arguments to strongly-typed classes with full IntelliSense support. + + - icon: ๐Ÿšฉ + title: Long & Short Flags + details: Support for both --verbose and -v style flags with automatic help text generation. + + - icon: ๐Ÿ“ + title: Automatic Help Generation + details: Beautiful, table-formatted help text generated automatically from attributes. + + - icon: โšก + title: Process Command Execution + details: Easily wrap external CLI tools and forward arguments seamlessly. + + - icon: ๐ŸŽจ + title: Attribute-Based Configuration + details: Use simple attributes to configure commands and arguments - minimal boilerplate. + + - icon: โœ… + title: Built-in Validation + details: Automatic validation for required arguments with helpful error messages. + + - icon: ๐Ÿ”„ + title: Flags Enums + details: Group related boolean flags into a single [Flags] enum for cleaner code. + + - icon: ๐ŸŽ + title: Flexible Architecture + details: Extend Command or ProcessCommand base classes to suit your needs. +--- + +## Quick Example + +```csharp +using Promty; + +[Description("greet", "Greets a person by name")] +public class GreetCommand : Command +{ + public class Args + { + [Description("name", "The name of the person to greet")] + public string Name { get; set; } = string.Empty; + + [FlagAlias("uppercase", 'u')] + [Description("Print the greeting in uppercase")] + public bool Uppercase { get; set; } + } + + public override Task ExecuteAsync(Args args) + { + var greeting = $"Hello, {args.Name}!"; + if (args.Uppercase) greeting = greeting.ToUpper(); + Console.WriteLine(greeting); + return Task.FromResult(0); + } +} +``` + +```bash +# Run your command +dotnet run -- greet Alice --uppercase +# Output: HELLO, ALICE! +``` + +## Installation + +::: code-group + +```bash [.NET CLI] +dotnet add package Promty +``` + +```xml [PackageReference] + +``` + +::: diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..bb61d7c --- /dev/null +++ b/docs/package.json @@ -0,0 +1,13 @@ +{ + "name": "promty-docs", + "version": "1.0.0", + "description": "Documentation for Promty", + "scripts": { + "docs:dev": "vitepress dev", + "docs:build": "vitepress build", + "docs:preview": "vitepress preview" + }, + "devDependencies": { + "vitepress": "^1.0.0" + } +} diff --git a/docs/public/logo.png b/docs/public/logo.png new file mode 100644 index 0000000..7e5fe34 Binary files /dev/null and b/docs/public/logo.png differ