Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"editor.insertSpaces": false,
"editor.tabSize": 4,
"editor.detectIndentation": false,
"[csharp]": {
"editor.insertSpaces": false,
"editor.tabSize": 4
},
"[json]": {
"editor.insertSpaces": false,
"editor.tabSize": 2
},
"[xml]": {
"editor.insertSpaces": false,
"editor.tabSize": 2
},
"[yaml]": {
"editor.insertSpaces": true,
"editor.tabSize": 2
},
"[markdown]": {
"editor.insertSpaces": true,
"editor.tabSize": 2
}
}
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,15 @@ dotnet run -- --help

## 📖 Documentation

- **User Guide:** `user-docs/cli_quick_reference.md`
- **Docker Guide:** `user-docs/docker_usage.md`
- **Technical Guide:** `doc/cli_skeleton_implementation.md`
- **Architecture & Extension:** `doc/cli_architecture.md`
### User Documentation
- **CLI Quick Reference:** `user-docs/cli_quick_reference.md`
- **Configuration Files:** `user-docs/configuration-files.md` - Batch analysis with JSON/YAML
- **Docker Usage:** `user-docs/docker_usage.md`
- **Vulnerability Scanning:** `user-docs/vulnerability-scanning.md`

### Technical Documentation
- **CLI Architecture:** `doc/cli_architecture.md`
- **Implementation Guide:** `doc/cli_skeleton_implementation.md`
- **Docker Implementation:** `doc/docker_implementation.md`
- **Test Results:** `doc/cli_skeleton_test_results.md`

Expand All @@ -52,6 +57,7 @@ dotnet run -- --help
- ✅ Multiple output formats (console, markdown)
- ✅ Path argument support (`-p` / `--path`) for all analysis commands
- ✅ Command-specific help with argument documentation
- ✅ **Configuration file support (JSON & YAML) for batch analysis**

## 🎯 Current Commands

Expand All @@ -61,6 +67,11 @@ codemedic # Show help (default)
codemedic --help # Explicit help
codemedic --version # Show version

# Configuration-based batch analysis
codemedic config <config-file> # Run multiple analyses from config file
codemedic config config.json
codemedic config config.yaml

# Analysis commands
codemedic health # Repository health dashboard
codemedic health -p /path/to/repo --format markdown
Expand Down
17 changes: 17 additions & 0 deletions run-config.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@echo off
REM Run CodeMedic with a configuration file

if "%1"=="" (
set CONFIG_FILE=sample-config.yaml
) else (
set CONFIG_FILE=%1
)

if not exist "%CONFIG_FILE%" (
echo Configuration file not found: %CONFIG_FILE%
exit /b 1
)

echo Running CodeMedic with configuration: %CONFIG_FILE%

dotnet run --project src\CodeMedic config %CONFIG_FILE%
16 changes: 16 additions & 0 deletions run-config.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env pwsh
# Run CodeMedic with a configuration file

param(
[Parameter(Mandatory=$false)]
[string]$ConfigFile = "sample-config.yaml"
)

if (-not (Test-Path $ConfigFile)) {
Write-Host "Configuration file not found: $ConfigFile" -ForegroundColor Red
exit 1
}

Write-Host "Running CodeMedic with configuration: $ConfigFile" -ForegroundColor Cyan

dotnet run --project src\CodeMedic config $ConfigFile
13 changes: 13 additions & 0 deletions run-config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Run CodeMedic with a configuration file

CONFIG_FILE="${1:-sample-config.yaml}"

if [ ! -f "$CONFIG_FILE" ]; then
echo "Configuration file not found: $CONFIG_FILE"
exit 1
fi

echo "Running CodeMedic with configuration: $CONFIG_FILE"

dotnet run --project src/CodeMedic config "$CONFIG_FILE"
23 changes: 23 additions & 0 deletions sample-config-multi-repo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"global": {
"format": "markdown",
"output-dir": "./reports/multi"
},
"repositories": [
{
"name": "MainProject",
"path": "./src",
"commands": ["health", "bom"]
},
{
"name": "TestSuite",
"path": "./test",
"commands": ["health"]
},
{
"name": "Documentation",
"path": "./docs",
"commands": ["bom"]
}
]
}
20 changes: 20 additions & 0 deletions sample-config-multi-repo.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
global:
format: markdown
output-dir: ./reports/multi

repositories:
- name: MainProject
path: ./src
commands:
- health
- bom

- name: TestSuite
path: ./test
commands:
- health

- name: Documentation
path: ./docs
commands:
- bom
13 changes: 13 additions & 0 deletions sample-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"global": {
"format": "markdown",
"output-dir": "./reports"
},
"repositories": [
{
"name": "CodeMedic",
"path": ".",
"commands": ["health", "bom", "vulnerabilities"]
}
]
}
11 changes: 11 additions & 0 deletions sample-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
global:
format: markdown
output-dir: ./reports

repositories:
- name: CodeMedic
path: .
commands:
- health
- bom
- vulnerabilities
1 change: 1 addition & 0 deletions src/CodeMedic/CodeMedic.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
</PackageReference>
<PackageReference Include="System.CommandLine" Version="2.0.0" />
<PackageReference Include="Spectre.Console" Version="0.49.1" />
<PackageReference Include="YamlDotNet" Version="16.3.0" />
</ItemGroup>

<ItemGroup>
Expand Down
145 changes: 145 additions & 0 deletions src/CodeMedic/Commands/ConfigurationCommandHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using CodeMedic.Utilities;
using CodeMedic.Commands;
using CodeMedic.Output;

namespace CodeMedic.Commands;

/// <summary>
/// Handles configuration-related commands.
/// </summary>
public class ConfigurationCommandHandler
{
private PluginLoader _PluginLoader;

/// <summary>
/// Initializes a new instance of the <see cref="ConfigurationCommandHandler"/> class.
/// </summary>
/// <param name="pluginLoader">The plugin loader instance used to manage plugins.</param>
public ConfigurationCommandHandler(PluginLoader pluginLoader)
{
_PluginLoader = pluginLoader;
}

/// <summary>
/// Handles the configuration file command.
/// </summary>
/// <param name="configFilePath">The path to the configuration file.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the exit code.</returns>
internal async Task<int> HandleConfigurationFileAsync(string configFilePath)
{

// Check if the configuration file exists
if (!File.Exists(configFilePath))
{
RootCommandHandler.Console.RenderError($"Configuration file not found: {configFilePath}");
return 1; // Return a non-zero exit code to indicate failure
}

// Load the specified configuration file - we need to identify the file type and load accordingly
CodeMedicRunConfiguration config;
try {
config = LoadConfigurationFromFile(configFilePath);
if (config == null)
{
RootCommandHandler.Console.RenderError($"Failed to load configuration from file: {configFilePath}");
return 1; // Return a non-zero exit code to indicate failure
}
} catch (Exception ex)
{
RootCommandHandler.Console.RenderError($"Error loading configuration file: {ex.Message}");
return 1; // Return a non-zero exit code to indicate failure
}

await RunCommandsForConfigurationAsync(config);

return 0; // Return zero to indicate success

}

private async Task RunCommandsForConfigurationAsync(CodeMedicRunConfiguration config)
{

// For each repository in the configuration, run the specified commands
foreach (var repoConfig in config.Repositories)
{
RootCommandHandler.Console.RenderInfo($"Processing repository: {repoConfig.Name} at {repoConfig.Path}");

foreach (var commandName in repoConfig.Commands)
{

// Check that the command is registered with the plugin loader
if (!_PluginLoader.Commands.ContainsKey(commandName))
{
RootCommandHandler.Console.RenderError($" Command not registered: {commandName}");
continue;
}

RootCommandHandler.Console.RenderInfo($" Running command: {commandName}");

// Load the command plugin
var commandPlugin = _PluginLoader.GetCommand(commandName);
if (commandPlugin == null)
{
RootCommandHandler.Console.RenderError($" Command plugin not found: {commandName}");
continue;
}

// Get the Formatter plugin for output formatting - we only support markdown for now
// Ensure output directory exists
Directory.CreateDirectory(config.Global.OutputDirectory);

var reportPath = Path.Combine(config.Global.OutputDirectory, $"{repoConfig.Name}_{commandName}.md");

// Execute the command with proper disposal of the StreamWriter
using (var writer = new StreamWriter(reportPath))
{
var formatter = new MarkdownRenderer(writer);

// Build arguments array to pass the repository path to the command
var commandArgs = new[] { "--path", repoConfig.Path };

await commandPlugin.Handler(commandArgs, formatter);
}

}
// For now, just simulate with a delay
await Task.Delay(500);

RootCommandHandler.Console.RenderInfo($"Completed processing repository: {repoConfig.Name}");
}

}

private CodeMedicRunConfiguration LoadConfigurationFromFile(string configFilePath)
{
// detect if the file is json or yaml based on extension
var extension = Path.GetExtension(configFilePath).ToLower();
var fileContents = File.ReadAllText(configFilePath);
if (extension == ".json")
{
var config = System.Text.Json.JsonSerializer.Deserialize<CodeMedicRunConfiguration>(fileContents);
if (config == null)
{
throw new InvalidOperationException("Failed to deserialize JSON configuration file.");
}
return config;
}
else if (extension == ".yaml" || extension == ".yml")
{
// Configure YAML deserializer - we use explicit YamlMember aliases on properties
var deserializer = new YamlDotNet.Serialization.DeserializerBuilder()
.Build();
var config = deserializer.Deserialize<CodeMedicRunConfiguration>(fileContents);
if (config == null)
{
throw new InvalidOperationException("Failed to deserialize YAML configuration file.");
}
return config;
}
else
{
throw new InvalidOperationException("Unsupported configuration file format. Only JSON and YAML are supported.");
}
}

}
Loading