diff --git a/README.md b/README.md
index 8871431..afd598d 100644
--- a/README.md
+++ b/README.md
@@ -50,6 +50,8 @@ dotnet run -- --help
- ✅ Bill of Materials (BOM) generation
- ✅ NuGet package vulnerability scanning
- ✅ Multiple output formats (console, markdown)
+- ✅ Path argument support (`-p` / `--path`) for all analysis commands
+- ✅ Command-specific help with argument documentation
## 🎯 Current Commands
@@ -61,13 +63,13 @@ codemedic --version # Show version
# Analysis commands
codemedic health # Repository health dashboard
-codemedic health --format markdown
+codemedic health -p /path/to/repo --format markdown
codemedic bom # Bill of Materials
-codemedic bom --format md > bom.md
+codemedic bom --path /path/to/repo --format md > bom.md
codemedic vulnerabilities # Scan for NuGet vulnerabilities
-codemedic vulnerabilities --format markdown > vulns.md
+codemedic vulnerabilities -p /path/to/repo --format markdown > vulns.md
```
## 🔧 Technology Stack
diff --git a/doc/plugin_architecture.md b/doc/plugin_architecture.md
index 0e21c66..bc2a69d 100644
--- a/doc/plugin_architecture.md
+++ b/doc/plugin_architecture.md
@@ -231,6 +231,123 @@ public class PluginMetadata
---
+## Command Registration System
+
+Analysis engine plugins can register CLI commands with arguments using the `RegisterCommands()` method. This enables plugins to expose their functionality through dedicated CLI commands with proper argument parsing and help integration.
+
+### CommandRegistration & CommandArgument
+
+```csharp
+namespace CodeMedic.Abstractions.Plugins;
+
+///
+/// Represents a command that can be registered with the CLI.
+///
+public class CommandRegistration
+{
+ public required string Name { get; init; } // Command name (e.g., "health")
+ public required string Description { get; init; } // Command description for help
+ public required Func> Handler { get; init; } // Command handler
+ public string[]? Examples { get; init; } // Usage examples
+ public CommandArgument[]? Arguments { get; init; } // Command arguments
+}
+
+///
+/// Represents a command-line argument specification.
+///
+public record CommandArgument(
+ string Description, // Required: what this argument does
+ string? ShortName = null, // Short form: "p" for "-p"
+ string? LongName = null, // Long form: "path" for "--path"
+ bool IsRequired = false, // Whether argument is mandatory
+ bool HasValue = true, // Whether argument takes a value
+ string? DefaultValue = null, // Default value description
+ string? ValueName = null); // Value type for help display
+```
+
+### Command Registration Features
+
+- **Automatic Help Integration**: Commands with arguments automatically appear in `--help` output
+- **Command-Specific Help**: `codemedic mycommand --help` shows detailed argument information
+- **Argument Parsing**: Use built-in utilities like `IdentifyTargetPathFromArgs()` for common patterns
+- **Rich Help Display**: Arguments show descriptions, default values, and usage examples
+
+### Built-in Path Argument Support
+
+All current analysis plugins support the standard path argument pattern:
+
+```bash
+# Current directory (default)
+codemedic health
+
+# Specific path (short form)
+codemedic health -p /path/to/repo
+
+# Specific path (long form)
+codemedic health --path /path/to/repo
+
+# Combined with format
+codemedic bom -p ../other-project --format markdown
+```
+
+### Example: Plugin with Command Registration
+
+```csharp
+public class MyAnalysisPlugin : IAnalysisEnginePlugin
+{
+ // ... other plugin implementation ...
+
+ public CommandRegistration[]? RegisterCommands()
+ {
+ return
+ [
+ new CommandRegistration
+ {
+ Name = "myanalysis",
+ Description = "Run my custom analysis",
+ Handler = ExecuteMyAnalysisAsync,
+ Arguments =
+ [
+ new CommandArgument(
+ Description: "Path to the repository to analyze",
+ ShortName: "p",
+ LongName: "path",
+ ValueName: "path",
+ DefaultValue: "current directory"),
+ new CommandArgument(
+ Description: "Enable verbose output",
+ ShortName: "v",
+ LongName: "verbose",
+ HasValue: false) // Flag argument
+ ],
+ Examples =
+ [
+ "codemedic myanalysis",
+ "codemedic myanalysis -p /path/to/repo",
+ "codemedic myanalysis --path /path/to/repo --verbose",
+ "codemedic myanalysis -p . -v --format markdown"
+ ]
+ }
+ ];
+ }
+
+ private async Task ExecuteMyAnalysisAsync(string[] args, IRenderer renderer)
+ {
+ // Parse path argument using built-in extension
+ var targetPath = args.IdentifyTargetPathFromArgs();
+
+ // Run analysis on the specified path
+ var result = await AnalyzeAsync(targetPath);
+
+ // Render results
+ renderer.RenderReport(result);
+ return 0;
+ }
+}
+```
+
+---
+
## Plugin Development Workflow
### Step 1: Create the Plugin Project
diff --git a/src/CodeMedic.Abstractions/Plugins/CommandRegistration.cs b/src/CodeMedic.Abstractions/Plugins/CommandRegistration.cs
index 4f4246f..e305538 100644
--- a/src/CodeMedic.Abstractions/Plugins/CommandRegistration.cs
+++ b/src/CodeMedic.Abstractions/Plugins/CommandRegistration.cs
@@ -1,3 +1,5 @@
+using System.Collections.Generic;
+
namespace CodeMedic.Abstractions.Plugins;
///
@@ -24,4 +26,28 @@ public class CommandRegistration
/// Gets or sets example usage strings for help text.
///
public string[]? Examples { get; init; }
+
+ ///
+ /// Gets or sets the command arguments specification.
+ ///
+ public CommandArgument[]? Arguments { get; init; }
}
+
+///
+/// Represents a command-line argument specification.
+///
+/// The description of what this argument does.
+/// The short name of the argument (e.g., "p" for "-p").
+/// The long name of the argument (e.g., "path" for "--path").
+/// Whether this argument is required.
+/// Whether this argument takes a value.
+/// The default value for this argument.
+/// The value type name for help display (e.g., "path", "format", "count").
+public record CommandArgument(
+ string Description,
+ string? ShortName = null,
+ string? LongName = null,
+ bool IsRequired = false,
+ bool HasValue = true,
+ string? DefaultValue = null,
+ string? ValueName = null);
diff --git a/src/CodeMedic/Commands/RootCommandHandler.cs b/src/CodeMedic/Commands/RootCommandHandler.cs
index 87abaaf..d8acecb 100644
--- a/src/CodeMedic/Commands/RootCommandHandler.cs
+++ b/src/CodeMedic/Commands/RootCommandHandler.cs
@@ -29,8 +29,8 @@ public static async Task ProcessArguments(string[] args)
_pluginLoader = new PluginLoader();
await _pluginLoader.LoadInternalPluginsAsync();
- // No arguments or help requested
- if (args.Length == 0 || args.Contains("--help") || args.Contains("-h") || args.Contains("help"))
+ // No arguments or general help requested
+ if (args.Length == 0 || args[0] == "--help" || args[0] == "-h" || args[0] == "help")
{
console.RenderBanner(version);
RenderHelp();
@@ -51,6 +51,15 @@ public static async Task ProcessArguments(string[] args)
if (commandRegistration != null)
{
+ // Check for command-specific help
+ var commandArgs = args.Skip(1).ToArray();
+ if (commandArgs.Contains("--help") || commandArgs.Contains("-h"))
+ {
+ console.RenderBanner(version);
+ RenderCommandHelp(commandRegistration);
+ return 0;
+ }
+
// Parse --format argument (default: console)
string format = "console";
var commandArgsList = args.Skip(1).ToList();
@@ -114,10 +123,36 @@ private static void RenderHelp()
AnsiConsole.MarkupLine(" [green]codemedic[/] [cyan]--version[/]");
AnsiConsole.WriteLine();
- AnsiConsole.MarkupLine("[dim]Options:[/]");
+ AnsiConsole.MarkupLine("[dim]Global Options:[/]");
AnsiConsole.MarkupLine(" [yellow]--format [/] Output format: [cyan]console[/] (default), [cyan]markdown[/] (or [cyan]md[/])");
AnsiConsole.WriteLine();
+ // Show command-specific arguments
+ if (_pluginLoader != null)
+ {
+ foreach (var command in _pluginLoader.Commands.Values.OrderBy(c => c.Name))
+ {
+ if (command.Arguments != null && command.Arguments.Length > 0)
+ {
+ AnsiConsole.MarkupLine($"[dim]{command.Name} Command Options:[/]");
+
+ foreach (var arg in command.Arguments)
+ {
+ var shortName = !string.IsNullOrEmpty(arg.ShortName) ? $"-{arg.ShortName}" : "";
+ var longName = !string.IsNullOrEmpty(arg.LongName) ? $"--{arg.LongName}" : "";
+ var names = string.Join(", ", new[] { shortName, longName }.Where(s => !string.IsNullOrEmpty(s)));
+
+ var valuePart = arg.HasValue && !string.IsNullOrEmpty(arg.ValueName) ? $" <{arg.ValueName}>" : "";
+ var requiredIndicator = arg.IsRequired ? " [red](required)[/]" : "";
+ var defaultPart = !string.IsNullOrEmpty(arg.DefaultValue) ? $" (default: {arg.DefaultValue})" : "";
+
+ AnsiConsole.MarkupLine($" [yellow]{names}{valuePart}[/] {arg.Description}{requiredIndicator}{defaultPart}");
+ }
+ AnsiConsole.WriteLine();
+ }
+ }
+ }
+
AnsiConsole.MarkupLine("[dim]Examples:[/]");
// Display examples from plugins
@@ -138,6 +173,77 @@ private static void RenderHelp()
AnsiConsole.MarkupLine(" [green]codemedic --version[/]");
}
+ ///
+ /// Renders help text for a specific command.
+ ///
+ private static void RenderCommandHelp(CodeMedic.Abstractions.Plugins.CommandRegistration command)
+ {
+ AnsiConsole.MarkupLine($"[bold]Command: {command.Name}[/]");
+ AnsiConsole.WriteLine();
+ AnsiConsole.MarkupLine($"{command.Description}");
+ AnsiConsole.WriteLine();
+
+ AnsiConsole.MarkupLine("[dim]Usage:[/]");
+ var usage = $"codemedic {command.Name}";
+
+ if (command.Arguments != null && command.Arguments.Length > 0)
+ {
+ foreach (var arg in command.Arguments)
+ {
+ var argName = !string.IsNullOrEmpty(arg.LongName) ? $"--{arg.LongName}" : $"-{arg.ShortName}";
+ var valuePart = arg.HasValue && !string.IsNullOrEmpty(arg.ValueName) ? $" <{arg.ValueName}>" : "";
+ var optionalWrapper = arg.IsRequired ? "" : "[ ]";
+
+ if (arg.IsRequired)
+ {
+ usage += $" {argName}{valuePart}";
+ }
+ else
+ {
+ usage += $" [{argName}{valuePart}]";
+ }
+ }
+ }
+
+ usage += " [--format ]";
+ AnsiConsole.MarkupLine($" [green]{usage.EscapeMarkup()}[/]");
+ AnsiConsole.WriteLine();
+
+ if (command.Arguments != null && command.Arguments.Length > 0)
+ {
+ AnsiConsole.MarkupLine("[dim]Command Options:[/]");
+
+ foreach (var arg in command.Arguments)
+ {
+ var shortName = !string.IsNullOrEmpty(arg.ShortName) ? $"-{arg.ShortName}" : "";
+ var longName = !string.IsNullOrEmpty(arg.LongName) ? $"--{arg.LongName}" : "";
+ var names = string.Join(", ", new[] { shortName, longName }.Where(s => !string.IsNullOrEmpty(s)));
+
+ var valuePart = arg.HasValue && !string.IsNullOrEmpty(arg.ValueName) ? $" <{arg.ValueName}>" : "";
+ var requiredIndicator = arg.IsRequired ? " [red](required)[/]" : "";
+ var defaultPart = !string.IsNullOrEmpty(arg.DefaultValue) ? $" (default: {arg.DefaultValue})" : "";
+
+ AnsiConsole.MarkupLine($" [yellow]{(names + valuePart).EscapeMarkup()}[/] {arg.Description.EscapeMarkup()}{requiredIndicator}{defaultPart.EscapeMarkup()}");
+ }
+ AnsiConsole.WriteLine();
+ }
+
+ AnsiConsole.MarkupLine("[dim]Global Options:[/]");
+ AnsiConsole.MarkupLine(" [yellow]--format [/] Output format: [cyan]console[/] (default), [cyan]markdown[/] (or [cyan]md[/])");
+ AnsiConsole.MarkupLine(" [yellow]-h, --help[/] Show this help message");
+ AnsiConsole.WriteLine();
+
+ if (command.Examples != null && command.Examples.Length > 0)
+ {
+ AnsiConsole.MarkupLine("[dim]Examples:[/]");
+ foreach (var example in command.Examples)
+ {
+ AnsiConsole.MarkupLine($" [green]{example.EscapeMarkup()}[/]");
+ }
+ AnsiConsole.WriteLine();
+ }
+ }
+
///
/// Renders information about loaded plugins.
///
diff --git a/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs b/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs
index 79f0703..f73e217 100644
--- a/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs
+++ b/src/CodeMedic/Plugins/BomAnalysis/BomAnalysisPlugin.cs
@@ -60,10 +60,21 @@ public async Task