refactor(configmanager): split into Core, CLI, and UI projects

Extract shared models, services, and application logic into
JdeScoping.ConfigManager.Core library. Add JdeScoping.ConfigManager.Cli
console app with validate, test-connection, and secret commands using
System.CommandLine. UI project now references Core for platform-agnostic
functionality while retaining Avalonia-specific dialog and clipboard services.
This commit is contained in:
Joseph Doherty
2026-01-28 10:01:48 -05:00
parent 5ee710d330
commit 7c4781dfe3
91 changed files with 1314 additions and 251 deletions
+2
View File
@@ -12,6 +12,8 @@
<Project Path="src/JdeScoping.Infrastructure/JdeScoping.Infrastructure.csproj" />
</Folder>
<Folder Name="/utils/">
<Project Path="src/Utils/JdeScoping.ConfigManager.Core/JdeScoping.ConfigManager.Core.csproj" />
<Project Path="src/Utils/JdeScoping.ConfigManager.Cli/JdeScoping.ConfigManager.Cli.csproj" />
<Project Path="src/Utils/JdeScoping.ConfigManager/JdeScoping.ConfigManager.csproj" />
</Folder>
<Folder Name="/tests/">
@@ -0,0 +1,389 @@
using System.CommandLine;
using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.ConfigManager.Core.Services.SecureStore;
using Microsoft.Extensions.DependencyInjection;
namespace JdeScoping.ConfigManager.Cli.Commands;
/// <summary>
/// Secret management command implementations.
/// </summary>
public static class SecretCommands
{
/// <summary>
/// Creates the secret list command.
/// </summary>
public static Command CreateListCommand(
IServiceProvider serviceProvider,
Option<string?> configPathOption,
Option<bool> verboseOption,
Option<bool> quietOption)
{
var command = new Command("list", "List all secret keys");
command.SetHandler(async (string? configPath, bool verbose, bool quiet) =>
{
var exitCode = await ListSecretsAsync(serviceProvider, configPath, verbose, quiet);
Environment.ExitCode = exitCode;
}, configPathOption, verboseOption, quietOption);
return command;
}
/// <summary>
/// Creates the secret get command.
/// </summary>
public static Command CreateGetCommand(
IServiceProvider serviceProvider,
Option<string?> configPathOption,
Option<bool> verboseOption,
Option<bool> quietOption)
{
var command = new Command("get", "Get a secret value");
var keyArgument = new Argument<string>("key", "The secret key to retrieve");
command.AddArgument(keyArgument);
command.SetHandler(async (string? configPath, bool verbose, bool quiet, string key) =>
{
var exitCode = await GetSecretAsync(serviceProvider, configPath, verbose, quiet, key);
Environment.ExitCode = exitCode;
}, configPathOption, verboseOption, quietOption, keyArgument);
return command;
}
/// <summary>
/// Creates the secret set command.
/// </summary>
public static Command CreateSetCommand(
IServiceProvider serviceProvider,
Option<string?> configPathOption,
Option<bool> verboseOption,
Option<bool> quietOption)
{
var command = new Command("set", "Set or update a secret");
var keyArgument = new Argument<string>("key", "The secret key");
var valueArgument = new Argument<string>("value", "The secret value");
command.AddArgument(keyArgument);
command.AddArgument(valueArgument);
command.SetHandler(async (string? configPath, bool verbose, bool quiet, string key, string value) =>
{
var exitCode = await SetSecretAsync(serviceProvider, configPath, verbose, quiet, key, value);
Environment.ExitCode = exitCode;
}, configPathOption, verboseOption, quietOption, keyArgument, valueArgument);
return command;
}
/// <summary>
/// Creates the secret remove command.
/// </summary>
public static Command CreateRemoveCommand(
IServiceProvider serviceProvider,
Option<string?> configPathOption,
Option<bool> verboseOption,
Option<bool> quietOption)
{
var command = new Command("remove", "Remove a secret");
var keyArgument = new Argument<string>("key", "The secret key to remove");
command.AddArgument(keyArgument);
command.SetHandler(async (string? configPath, bool verbose, bool quiet, string key) =>
{
var exitCode = await RemoveSecretAsync(serviceProvider, configPath, verbose, quiet, key);
Environment.ExitCode = exitCode;
}, configPathOption, verboseOption, quietOption, keyArgument);
return command;
}
/// <summary>
/// Creates the secret init command.
/// </summary>
public static Command CreateInitCommand(
IServiceProvider serviceProvider,
Option<string?> configPathOption,
Option<bool> verboseOption,
Option<bool> quietOption)
{
var command = new Command("init", "Initialize a new SecureStore");
var storePathOption = new Option<string?>(
aliases: ["--store", "-s"],
description: "Path for the store file");
var keyPathOption = new Option<string?>(
aliases: ["--key", "-k"],
description: "Path for the key file");
command.AddOption(storePathOption);
command.AddOption(keyPathOption);
command.SetHandler(async (string? configPath, bool verbose, bool quiet, string? storePath, string? keyPath) =>
{
var exitCode = await InitStoreAsync(serviceProvider, configPath, verbose, quiet, storePath, keyPath);
Environment.ExitCode = exitCode;
}, configPathOption, verboseOption, quietOption, storePathOption, keyPathOption);
return command;
}
private static async Task<int> ListSecretsAsync(
IServiceProvider serviceProvider,
string? configPath,
bool verbose,
bool quiet)
{
var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath);
if (manager == null)
return 1;
try
{
var keys = manager.GetKeys();
if (!quiet)
{
Console.WriteLine($"=== SecureStore Keys ({keys.Count}) ===");
}
foreach (var key in keys.OrderBy(k => k))
{
Console.WriteLine(key);
}
return 0;
}
finally
{
manager.CloseStore();
}
}
private static async Task<int> GetSecretAsync(
IServiceProvider serviceProvider,
string? configPath,
bool verbose,
bool quiet,
string key)
{
var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath);
if (manager == null)
return 1;
try
{
var value = manager.GetSecret(key);
Console.WriteLine(value);
return 0;
}
catch (KeyNotFoundException)
{
Console.Error.WriteLine($"Error: Secret '{key}' not found");
return 1;
}
finally
{
manager.CloseStore();
}
}
private static async Task<int> SetSecretAsync(
IServiceProvider serviceProvider,
string? configPath,
bool verbose,
bool quiet,
string key,
string value)
{
var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath);
if (manager == null)
return 1;
try
{
manager.SetSecret(key, value);
manager.Save();
if (!quiet)
{
Console.WriteLine($"Secret '{key}' set successfully");
}
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
return 1;
}
finally
{
manager.CloseStore();
}
}
private static async Task<int> RemoveSecretAsync(
IServiceProvider serviceProvider,
string? configPath,
bool verbose,
bool quiet,
string key)
{
var (manager, folderPath) = await OpenStoreAsync(serviceProvider, configPath);
if (manager == null)
return 1;
try
{
manager.RemoveSecret(key);
manager.Save();
if (!quiet)
{
Console.WriteLine($"Secret '{key}' removed successfully");
}
return 0;
}
catch (KeyNotFoundException)
{
Console.Error.WriteLine($"Error: Secret '{key}' not found");
return 1;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
return 1;
}
finally
{
manager.CloseStore();
}
}
private static async Task<int> InitStoreAsync(
IServiceProvider serviceProvider,
string? configPath,
bool verbose,
bool quiet,
string? storePath,
string? keyPath)
{
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
if (folderPath == null)
{
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
return 1;
}
var effectiveStorePath = storePath ?? Path.Combine(folderPath, "data", "secrets.json");
var effectiveKeyPath = keyPath ?? Path.Combine(folderPath, "data", "secrets.key");
if (File.Exists(effectiveStorePath))
{
Console.Error.WriteLine($"Error: Store already exists at {effectiveStorePath}");
return 1;
}
var manager = serviceProvider.GetRequiredService<ISecureStoreManager>();
try
{
manager.CreateStore(effectiveStorePath, effectiveKeyPath);
if (!quiet)
{
Console.WriteLine("SecureStore initialized successfully");
Console.WriteLine($"Store: {effectiveStorePath}");
Console.WriteLine($"Key: {effectiveKeyPath}");
}
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error creating store: {ex.Message}");
return 1;
}
finally
{
manager.CloseStore();
}
}
private static async Task<(ISecureStoreManager?, string?)> OpenStoreAsync(
IServiceProvider serviceProvider,
string? configPath)
{
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
if (folderPath == null)
{
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
return (null, null);
}
// Load config to get SecureStore paths
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}");
return (null, null);
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
var storePath = Path.IsPathRooted(config.SecureStore.StorePath)
? config.SecureStore.StorePath
: Path.Combine(folderPath, config.SecureStore.StorePath);
var keyPath = Path.IsPathRooted(config.SecureStore.KeyFilePath)
? config.SecureStore.KeyFilePath
: Path.Combine(folderPath, config.SecureStore.KeyFilePath);
if (!File.Exists(storePath))
{
Console.Error.WriteLine($"Error: SecureStore not found at {storePath}");
Console.Error.WriteLine("Use 'secret init' to create a new store.");
return (null, null);
}
if (!File.Exists(keyPath))
{
Console.Error.WriteLine($"Error: Key file not found at {keyPath}");
return (null, null);
}
var manager = serviceProvider.GetRequiredService<ISecureStoreManager>();
manager.OpenStore(storePath, keyPath);
return (manager, folderPath);
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error opening store: {ex.Message}");
return (null, null);
}
}
private static async Task<string?> GetConfigFolderAsync(IServiceProvider serviceProvider, string? configPath)
{
if (!string.IsNullOrEmpty(configPath))
{
if (Directory.Exists(configPath))
return configPath;
return null;
}
var autoDiscoveryService = serviceProvider.GetRequiredService<IAutoDiscoveryService>();
return await autoDiscoveryService.FindConfigFolderAsync();
}
}
@@ -0,0 +1,243 @@
using System.CommandLine;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Core.Services;
using Microsoft.Extensions.DependencyInjection;
namespace JdeScoping.ConfigManager.Cli.Commands;
/// <summary>
/// Test connection command implementations.
/// </summary>
public static class TestConnectionCommand
{
/// <summary>
/// Creates the test-connection sql command.
/// </summary>
public static Command CreateSqlCommand(
IServiceProvider serviceProvider,
Option<string?> configPathOption,
Option<bool> verboseOption,
Option<bool> quietOption)
{
var command = new Command("sql", "Test SQL Server connection");
var connectionNameOption = new Option<string?>(
aliases: ["--name", "-n"],
description: "Name of the connection string to test");
command.AddOption(connectionNameOption);
command.SetHandler(async (string? configPath, bool verbose, bool quiet, string? connectionName) =>
{
var exitCode = await TestConnectionAsync(serviceProvider, configPath, verbose, quiet, connectionName, ConnectionProvider.SqlServer);
Environment.ExitCode = exitCode;
}, configPathOption, verboseOption, quietOption, connectionNameOption);
return command;
}
/// <summary>
/// Creates the test-connection oracle command.
/// </summary>
public static Command CreateOracleCommand(
IServiceProvider serviceProvider,
Option<string?> configPathOption,
Option<bool> verboseOption,
Option<bool> quietOption)
{
var command = new Command("oracle", "Test Oracle connection");
var connectionNameOption = new Option<string?>(
aliases: ["--name", "-n"],
description: "Name of the connection string to test");
command.AddOption(connectionNameOption);
command.SetHandler(async (string? configPath, bool verbose, bool quiet, string? connectionName) =>
{
var exitCode = await TestConnectionAsync(serviceProvider, configPath, verbose, quiet, connectionName, ConnectionProvider.Oracle);
Environment.ExitCode = exitCode;
}, configPathOption, verboseOption, quietOption, connectionNameOption);
return command;
}
/// <summary>
/// Creates the test-connection all command.
/// </summary>
public static Command CreateAllCommand(
IServiceProvider serviceProvider,
Option<string?> configPathOption,
Option<bool> verboseOption,
Option<bool> quietOption)
{
var command = new Command("all", "Test all configured connections");
command.SetHandler(async (string? configPath, bool verbose, bool quiet) =>
{
var exitCode = await TestAllConnectionsAsync(serviceProvider, configPath, verbose, quiet);
Environment.ExitCode = exitCode;
}, configPathOption, verboseOption, quietOption);
return command;
}
private static async Task<int> TestConnectionAsync(
IServiceProvider serviceProvider,
string? configPath,
bool verbose,
bool quiet,
string? connectionName,
ConnectionProvider provider)
{
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
if (folderPath == null)
{
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
return 1;
}
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var connectionTestService = serviceProvider.GetRequiredService<IConnectionTestService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}");
return 1;
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
var entry = connectionName != null
? config.ConnectionStrings.Entries.FirstOrDefault(e =>
e.Name.Equals(connectionName, StringComparison.OrdinalIgnoreCase))
: config.ConnectionStrings.Entries.FirstOrDefault(e => e.Provider == provider);
if (entry == null)
{
var message = connectionName != null
? $"Connection '{connectionName}' not found"
: $"No {provider} connection found";
Console.Error.WriteLine($"Error: {message}");
return 1;
}
if (!quiet)
{
Console.WriteLine($"Testing connection: {entry.Name}");
}
var connectionString = entry.GenerateConnectionString();
var result = await connectionTestService.TestConnectionAsync(connectionString, entry.Provider);
if (result.Success)
{
if (!quiet)
{
Console.WriteLine($"Status: Success");
if (result.Duration.HasValue)
Console.WriteLine($"Duration: {result.Duration.Value.TotalMilliseconds:F0}ms");
}
return 0;
}
Console.Error.WriteLine($"Status: Failed");
Console.Error.WriteLine($"Message: {result.Message}");
return 1;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
return 1;
}
}
private static async Task<int> TestAllConnectionsAsync(
IServiceProvider serviceProvider,
string? configPath,
bool verbose,
bool quiet)
{
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
if (folderPath == null)
{
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
return 1;
}
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var connectionTestService = serviceProvider.GetRequiredService<IConnectionTestService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}");
return 1;
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
var hasFailures = false;
if (!quiet)
{
Console.WriteLine("=== Testing All Connections ===");
Console.WriteLine($"Found {config.ConnectionStrings.Entries.Count} connection(s)");
Console.WriteLine();
}
foreach (var entry in config.ConnectionStrings.Entries)
{
if (entry.Provider == ConnectionProvider.Generic)
{
if (!quiet)
Console.WriteLine($"{entry.Name}: Skipped (generic provider)");
continue;
}
var connectionString = entry.GenerateConnectionString();
var result = await connectionTestService.TestConnectionAsync(connectionString, entry.Provider);
if (result.Success)
{
if (!quiet)
{
var duration = result.Duration.HasValue
? $" ({result.Duration.Value.TotalMilliseconds:F0}ms)"
: "";
Console.WriteLine($"{entry.Name}: OK{duration}");
}
}
else
{
hasFailures = true;
Console.Error.WriteLine($"{entry.Name}: FAILED - {result.Message}");
}
}
return hasFailures ? 1 : 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
return 1;
}
}
private static async Task<string?> GetConfigFolderAsync(IServiceProvider serviceProvider, string? configPath)
{
if (!string.IsNullOrEmpty(configPath))
{
if (Directory.Exists(configPath))
return configPath;
return null;
}
var autoDiscoveryService = serviceProvider.GetRequiredService<IAutoDiscoveryService>();
return await autoDiscoveryService.FindConfigFolderAsync();
}
}
@@ -0,0 +1,273 @@
using System.CommandLine;
using JdeScoping.ConfigManager.Core.Services;
using Microsoft.Extensions.DependencyInjection;
namespace JdeScoping.ConfigManager.Cli.Commands;
/// <summary>
/// Validation command implementations.
/// </summary>
public static class ValidateCommand
{
/// <summary>
/// Creates the validate appsettings command.
/// </summary>
public static Command CreateAppSettingsCommand(
IServiceProvider serviceProvider,
Option<string?> configPathOption,
Option<bool> verboseOption,
Option<bool> quietOption)
{
var command = new Command("appsettings", "Validate appsettings.json");
command.SetHandler(async (string? configPath, bool verbose, bool quiet) =>
{
var exitCode = await ValidateAppSettingsAsync(serviceProvider, configPath, verbose, quiet);
Environment.ExitCode = exitCode;
}, configPathOption, verboseOption, quietOption);
return command;
}
/// <summary>
/// Creates the validate pipelines command.
/// </summary>
public static Command CreatePipelinesCommand(
IServiceProvider serviceProvider,
Option<string?> configPathOption,
Option<bool> verboseOption,
Option<bool> quietOption)
{
var command = new Command("pipelines", "Validate pipeline configuration files");
command.SetHandler(async (string? configPath, bool verbose, bool quiet) =>
{
var exitCode = await ValidatePipelinesAsync(serviceProvider, configPath, verbose, quiet);
Environment.ExitCode = exitCode;
}, configPathOption, verboseOption, quietOption);
return command;
}
/// <summary>
/// Creates the validate all command.
/// </summary>
public static Command CreateAllCommand(
IServiceProvider serviceProvider,
Option<string?> configPathOption,
Option<bool> verboseOption,
Option<bool> quietOption)
{
var command = new Command("all", "Validate all configuration files");
command.SetHandler(async (string? configPath, bool verbose, bool quiet) =>
{
var exitCode1 = await ValidateAppSettingsAsync(serviceProvider, configPath, verbose, quiet);
var exitCode2 = await ValidatePipelinesAsync(serviceProvider, configPath, verbose, quiet);
Environment.ExitCode = Math.Max(exitCode1, exitCode2);
}, configPathOption, verboseOption, quietOption);
return command;
}
/// <summary>
/// Creates the validate runtime command.
/// </summary>
public static Command CreateRuntimeCommand(
IServiceProvider serviceProvider,
Option<string?> configPathOption,
Option<bool> verboseOption,
Option<bool> quietOption)
{
var command = new Command("runtime", "Run Infrastructure validators");
command.SetHandler(async (string? configPath, bool verbose, bool quiet) =>
{
var exitCode = await ValidateRuntimeAsync(serviceProvider, configPath, verbose, quiet);
Environment.ExitCode = exitCode;
}, configPathOption, verboseOption, quietOption);
return command;
}
private static async Task<int> ValidateAppSettingsAsync(
IServiceProvider serviceProvider,
string? configPath,
bool verbose,
bool quiet)
{
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
if (folderPath == null)
{
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
return 1;
}
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var validationService = serviceProvider.GetRequiredService<IValidationService>();
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
if (!File.Exists(appSettingsPath))
{
Console.Error.WriteLine($"Error: appsettings.json not found at {appSettingsPath}");
return 1;
}
try
{
var config = await configFileService.LoadAppSettingsAsync(appSettingsPath);
var result = validationService.ValidateAppSettings(config);
if (!quiet)
{
Console.WriteLine("=== AppSettings Validation ===");
Console.WriteLine($"File: {appSettingsPath}");
}
if (result.IsValid)
{
if (!quiet)
Console.WriteLine("Status: Valid");
return 0;
}
foreach (var error in result.Errors)
{
Console.Error.WriteLine($"Error: {error}");
}
foreach (var warning in result.Warnings)
{
if (!quiet)
Console.WriteLine($"Warning: {warning}");
}
return 1;
}
catch (ConfigLoadException ex)
{
Console.Error.WriteLine($"Error loading configuration: {ex.Message}");
return 1;
}
}
private static async Task<int> ValidatePipelinesAsync(
IServiceProvider serviceProvider,
string? configPath,
bool verbose,
bool quiet)
{
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
if (folderPath == null)
{
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
return 1;
}
var configFileService = serviceProvider.GetRequiredService<IConfigFileService>();
var validationService = serviceProvider.GetRequiredService<IValidationService>();
var pipelinesDir = Path.Combine(folderPath, "Pipelines");
try
{
var pipelines = await configFileService.LoadAllPipelinesAsync(pipelinesDir);
var result = validationService.ValidatePipelines(pipelines);
if (!quiet)
{
Console.WriteLine("=== Pipelines Validation ===");
Console.WriteLine($"Directory: {pipelinesDir}");
Console.WriteLine($"Pipelines found: {pipelines.Count}");
}
if (result.IsValid)
{
if (!quiet)
Console.WriteLine("Status: Valid");
return 0;
}
foreach (var error in result.Errors)
{
Console.Error.WriteLine($"Error: {error}");
}
foreach (var warning in result.Warnings)
{
if (!quiet)
Console.WriteLine($"Warning: {warning}");
}
return result.Errors.Count > 0 ? 1 : 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error validating pipelines: {ex.Message}");
return 1;
}
}
private static async Task<int> ValidateRuntimeAsync(
IServiceProvider serviceProvider,
string? configPath,
bool verbose,
bool quiet)
{
var folderPath = await GetConfigFolderAsync(serviceProvider, configPath);
if (folderPath == null)
{
Console.Error.WriteLine("Error: Could not find configuration folder. Use --config-path to specify.");
return 1;
}
var runtimeValidationService = serviceProvider.GetRequiredService<IRuntimeConfigValidationService>();
if (!quiet)
{
Console.WriteLine("=== Runtime Configuration Validation ===");
Console.WriteLine($"Folder: {folderPath}");
}
var results = runtimeValidationService.ValidateRuntimeConfig(folderPath);
var hasErrors = false;
foreach (var result in results)
{
if (result.Errors.Count > 0)
{
hasErrors = true;
Console.Error.WriteLine($"\n[{result.ValidatorName}]");
foreach (var error in result.Errors)
{
Console.Error.WriteLine($" Error: {error}");
}
}
else if (!quiet)
{
Console.WriteLine($"[{result.ValidatorName}]: OK");
}
foreach (var warning in result.Warnings)
{
if (!quiet)
Console.WriteLine($" Warning: {warning}");
}
}
return hasErrors ? 1 : 0;
}
private static async Task<string?> GetConfigFolderAsync(IServiceProvider serviceProvider, string? configPath)
{
if (!string.IsNullOrEmpty(configPath))
{
if (Directory.Exists(configPath))
return configPath;
return null;
}
var autoDiscoveryService = serviceProvider.GetRequiredService<IAutoDiscoveryService>();
return await autoDiscoveryService.FindConfigFolderAsync();
}
}
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>jdescoping-config</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.*" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.*" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.*" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\JdeScoping.ConfigManager.Core\JdeScoping.ConfigManager.Core.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,82 @@
using System.CommandLine;
using JdeScoping.ConfigManager.Cli.Commands;
using JdeScoping.ConfigManager.Core.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace JdeScoping.ConfigManager.Cli;
/// <summary>
/// Main entry point for the jdescoping-config CLI tool.
/// </summary>
public static class Program
{
/// <summary>
/// Main entry point.
/// </summary>
/// <param name="args">Command-line arguments.</param>
/// <returns>Exit code: 0 for success, non-zero for errors.</returns>
public static async Task<int> Main(string[] args)
{
var services = new ServiceCollection();
ConfigureServices(services);
using var serviceProvider = services.BuildServiceProvider();
var rootCommand = new RootCommand("JDE Scoping Tool configuration management CLI")
{
Name = "jdescoping-config"
};
// Global options
var configPathOption = new Option<string?>(
aliases: ["--config-path", "-c"],
description: "Path to configuration folder");
var verboseOption = new Option<bool>(
aliases: ["--verbose", "-v"],
description: "Enable verbose output");
var quietOption = new Option<bool>(
aliases: ["--quiet", "-q"],
description: "Suppress non-error output");
rootCommand.AddGlobalOption(configPathOption);
rootCommand.AddGlobalOption(verboseOption);
rootCommand.AddGlobalOption(quietOption);
// Validate command group
var validateCommand = new Command("validate", "Validate configuration files");
validateCommand.AddCommand(ValidateCommand.CreateAppSettingsCommand(serviceProvider, configPathOption, verboseOption, quietOption));
validateCommand.AddCommand(ValidateCommand.CreatePipelinesCommand(serviceProvider, configPathOption, verboseOption, quietOption));
validateCommand.AddCommand(ValidateCommand.CreateAllCommand(serviceProvider, configPathOption, verboseOption, quietOption));
validateCommand.AddCommand(ValidateCommand.CreateRuntimeCommand(serviceProvider, configPathOption, verboseOption, quietOption));
rootCommand.AddCommand(validateCommand);
// Test-connection command group
var testConnectionCommand = new Command("test-connection", "Test database connections");
testConnectionCommand.AddCommand(TestConnectionCommand.CreateSqlCommand(serviceProvider, configPathOption, verboseOption, quietOption));
testConnectionCommand.AddCommand(TestConnectionCommand.CreateOracleCommand(serviceProvider, configPathOption, verboseOption, quietOption));
testConnectionCommand.AddCommand(TestConnectionCommand.CreateAllCommand(serviceProvider, configPathOption, verboseOption, quietOption));
rootCommand.AddCommand(testConnectionCommand);
// Secret command group
var secretCommand = new Command("secret", "Manage SecureStore secrets");
secretCommand.AddCommand(SecretCommands.CreateListCommand(serviceProvider, configPathOption, verboseOption, quietOption));
secretCommand.AddCommand(SecretCommands.CreateGetCommand(serviceProvider, configPathOption, verboseOption, quietOption));
secretCommand.AddCommand(SecretCommands.CreateSetCommand(serviceProvider, configPathOption, verboseOption, quietOption));
secretCommand.AddCommand(SecretCommands.CreateRemoveCommand(serviceProvider, configPathOption, verboseOption, quietOption));
secretCommand.AddCommand(SecretCommands.CreateInitCommand(serviceProvider, configPathOption, verboseOption, quietOption));
rootCommand.AddCommand(secretCommand);
return await rootCommand.InvokeAsync(args);
}
private static void ConfigureServices(IServiceCollection services)
{
services.AddLogging(builder => builder
.AddConsole()
.SetMinimumLevel(LogLevel.Warning));
services.AddConfigManagerCore();
}
}
@@ -1,7 +1,7 @@
using Microsoft.Extensions.Logging;
using JdeScoping.ConfigManager.Services.SecureStore;
using JdeScoping.ConfigManager.Core.Services.SecureStore;
namespace JdeScoping.ConfigManager.Application;
namespace JdeScoping.ConfigManager.Core.Application;
/// <summary>
/// Secret CRUD use-case operations with logging.
@@ -1,7 +1,7 @@
using Microsoft.Extensions.Logging;
using JdeScoping.ConfigManager.Services.SecureStore;
using JdeScoping.ConfigManager.Core.Services.SecureStore;
namespace JdeScoping.ConfigManager.Application;
namespace JdeScoping.ConfigManager.Core.Application;
/// <summary>
/// Store lifecycle use-case operations with logging.
@@ -1,4 +1,4 @@
namespace JdeScoping.ConfigManager.Constants;
namespace JdeScoping.ConfigManager.Core.Constants;
/// <summary>
/// Centralized constants for secure store file extensions and patterns used in file dialogs.
@@ -1,4 +1,4 @@
namespace JdeScoping.ConfigManager.Constants;
namespace JdeScoping.ConfigManager.Core.Constants;
/// <summary>
/// Centralized string constants for secure store dialog titles, messages, and validation errors.
@@ -0,0 +1,43 @@
using JdeScoping.ConfigManager.Core.Application;
using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.ConfigManager.Core.Services.SecureStore;
using Microsoft.Extensions.DependencyInjection;
namespace JdeScoping.ConfigManager.Core.DependencyInjection;
/// <summary>
/// Extension methods for registering ConfigManager.Core services with dependency injection.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds ConfigManager.Core services to the service collection.
/// </summary>
/// <param name="services">The service collection to add services to.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddConfigManagerCore(this IServiceCollection services)
{
// File system abstraction
services.AddSingleton<IFileSystem, FileSystem>();
// Configuration management services
services.AddSingleton<IAutoDiscoveryService, AutoDiscoveryService>();
services.AddSingleton<IBackupService, BackupService>();
services.AddSingleton<IDiffService, DiffService>();
services.AddSingleton<IValidationService, ValidationService>();
services.AddScoped<IConfigFileService, ConfigFileService>();
// SecureStore services
services.AddSingleton<ISecureStoreManager, SecureStoreManager>();
services.AddSingleton<StoreUseCases>();
services.AddSingleton<SecretUseCases>();
// Runtime validation
services.AddSingleton<IRuntimeConfigValidationService, RuntimeConfigValidationService>();
// Connection testing
services.AddSingleton<IConnectionTestService, ConnectionTestService>();
return services;
}
}
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DiffPlex" Version="1.7.*" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.*" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.*" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.*" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.*" />
<PackageReference Include="SecureStore" Version="1.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\JdeScoping.Core\JdeScoping.Core.csproj" />
<ProjectReference Include="..\..\JdeScoping.DataSync\JdeScoping.DataSync.csproj" />
<ProjectReference Include="..\..\JdeScoping.Infrastructure\JdeScoping.Infrastructure.csproj" />
</ItemGroup>
</Project>
@@ -1,6 +1,4 @@
using System.Text.Json.Serialization;
namespace JdeScoping.ConfigManager.Models;
namespace JdeScoping.ConfigManager.Core.Models;
/// <summary>
/// Root model for appsettings.json configuration.
@@ -1,4 +1,4 @@
namespace JdeScoping.ConfigManager.Models;
namespace JdeScoping.ConfigManager.Core.Models;
/// <summary>
/// Database provider types supported by the ConnectionStrings editor.
@@ -1,4 +1,4 @@
namespace JdeScoping.ConfigManager.Models;
namespace JdeScoping.ConfigManager.Core.Models;
/// <summary>
/// Represents a single connection string entry with provider-specific fields.
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace JdeScoping.ConfigManager.Models;
namespace JdeScoping.ConfigManager.Core.Models;
/// <summary>
/// Configuration section for connection strings.
@@ -1,7 +1,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace JdeScoping.ConfigManager.Models;
namespace JdeScoping.ConfigManager.Core.Models;
/// <summary>
/// Custom JSON converter that handles the standard .NET ConnectionStrings dictionary format
@@ -215,7 +215,12 @@ public class ConnectionStringsSectionConverter : JsonConverter<ConnectionStrings
return entry;
}
internal static void ApplyConnectionString(ConnectionStringEntry entry, string connectionString)
/// <summary>
/// Applies a connection string to an existing entry, parsing it according to the detected provider.
/// </summary>
/// <param name="entry">The entry to update.</param>
/// <param name="connectionString">The connection string to parse and apply.</param>
public static void ApplyConnectionString(ConnectionStringEntry entry, string connectionString)
{
var parsed = ParseConnectionString(entry.Name, connectionString);
@@ -1,6 +1,6 @@
using Microsoft.Extensions.Logging;
namespace JdeScoping.ConfigManager.Services;
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Service for auto-discovering configuration file locations.
@@ -0,0 +1,22 @@
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Represents backup file information.
/// </summary>
public class BackupInfo
{
/// <summary>
/// Gets the full path to the backup file.
/// </summary>
public required string Path { get; init; }
/// <summary>
/// Gets the timestamp when the backup was created.
/// </summary>
public required DateTime Timestamp { get; init; }
/// <summary>
/// Gets the file size in bytes.
/// </summary>
public required long Size { get; init; }
}
@@ -1,7 +1,7 @@
using System.Globalization;
using Microsoft.Extensions.Logging;
namespace JdeScoping.ConfigManager.Services;
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Service for managing configuration file backups.
@@ -1,10 +1,10 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.DataSync.Configuration;
using Microsoft.Extensions.Logging;
namespace JdeScoping.ConfigManager.Services;
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Service for loading and saving configuration files.
@@ -1,4 +1,4 @@
namespace JdeScoping.ConfigManager.Services;
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Exception thrown when configuration file loading fails.
@@ -0,0 +1,22 @@
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Result of testing a database connection.
/// </summary>
public class ConnectionTestResult
{
/// <summary>
/// Gets a value indicating whether the connection test was successful.
/// </summary>
public bool Success { get; init; }
/// <summary>
/// Gets the message describing the result of the connection test.
/// </summary>
public string Message { get; init; } = string.Empty;
/// <summary>
/// Gets the elapsed time of the connection test operation.
/// </summary>
public TimeSpan? Duration { get; init; }
}
@@ -1,8 +1,8 @@
using System.Diagnostics;
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
using Microsoft.Data.SqlClient;
namespace JdeScoping.ConfigManager.Services;
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Service for testing database connections.
@@ -1,4 +1,4 @@
namespace JdeScoping.ConfigManager.Services;
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Represents a line in a diff output.
@@ -58,17 +58,3 @@ public class DiffResult
/// </summary>
public int Deletions { get; init; }
}
/// <summary>
/// Service for generating diffs between text content.
/// </summary>
public interface IDiffService
{
/// <summary>
/// Generates a diff between original and modified text content.
/// </summary>
/// <param name="original">The original text content.</param>
/// <param name="modified">The modified text content.</param>
/// <returns>A diff result containing added, removed, and unchanged lines with counts.</returns>
DiffResult GenerateDiff(string original, string modified);
}
@@ -2,7 +2,7 @@ using DiffPlex;
using DiffPlex.DiffBuilder;
using DiffPlex.DiffBuilder.Model;
namespace JdeScoping.ConfigManager.Services;
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Service for generating diffs between text content.
@@ -1,4 +1,4 @@
namespace JdeScoping.ConfigManager.Services;
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Real file system implementation.
@@ -1,4 +1,4 @@
namespace JdeScoping.ConfigManager.Services;
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Service for auto-discovering configuration file locations.
@@ -1,25 +1,4 @@
namespace JdeScoping.ConfigManager.Services;
/// <summary>
/// Represents backup file information.
/// </summary>
public class BackupInfo
{
/// <summary>
/// Gets the full path to the backup file.
/// </summary>
public required string Path { get; init; }
/// <summary>
/// Gets the timestamp when the backup was created.
/// </summary>
public required DateTime Timestamp { get; init; }
/// <summary>
/// Gets the file size in bytes.
/// </summary>
public required long Size { get; init; }
}
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Service for managing configuration file backups.
@@ -1,7 +1,7 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.DataSync.Configuration;
namespace JdeScoping.ConfigManager.Services;
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Service for loading and saving configuration files.
@@ -0,0 +1,18 @@
using JdeScoping.ConfigManager.Core.Models;
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Service for testing database connections.
/// </summary>
public interface IConnectionTestService
{
/// <summary>
/// Tests a database connection asynchronously.
/// </summary>
/// <param name="connectionString">The connection string to test.</param>
/// <param name="provider">The database provider type.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A ConnectionTestResult indicating success or failure of the test.</returns>
Task<ConnectionTestResult> TestConnectionAsync(string connectionString, ConnectionProvider provider, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,15 @@
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Service for generating diffs between text content.
/// </summary>
public interface IDiffService
{
/// <summary>
/// Generates a diff between original and modified text content.
/// </summary>
/// <param name="original">The original text content.</param>
/// <param name="modified">The modified text content.</param>
/// <returns>A diff result containing added, removed, and unchanged lines with counts.</returns>
DiffResult GenerateDiff(string original, string modified);
}
@@ -1,4 +1,4 @@
namespace JdeScoping.ConfigManager.Services;
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Abstraction for file system operations to enable testing.
@@ -0,0 +1,14 @@
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Service for validating runtime configuration using Infrastructure validators.
/// </summary>
public interface IRuntimeConfigValidationService
{
/// <summary>
/// Validates the configuration in the specified folder using Infrastructure validators.
/// </summary>
/// <param name="configFolderPath">Path to the configuration folder.</param>
/// <returns>List of validation results from each validator.</returns>
List<RuntimeValidationResult> ValidateRuntimeConfig(string configFolderPath);
}
@@ -1,40 +1,7 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.DataSync.Configuration;
namespace JdeScoping.ConfigManager.Services;
/// <summary>
/// Result of a validation operation.
/// </summary>
public class ValidationResult
{
/// <summary>
/// Gets a value indicating whether the validation succeeded (no errors).
/// </summary>
public bool IsValid => Errors.Count == 0;
/// <summary>
/// Gets the list of validation errors encountered.
/// </summary>
public List<string> Errors { get; } = [];
/// <summary>
/// Gets the list of validation warnings encountered.
/// </summary>
public List<string> Warnings { get; } = [];
/// <summary>
/// Adds an error message to the validation result.
/// </summary>
/// <param name="message">The error message to add.</param>
public void AddError(string message) => Errors.Add(message);
/// <summary>
/// Adds a warning message to the validation result.
/// </summary>
/// <param name="message">The warning message to add.</param>
public void AddWarning(string message) => Warnings.Add(message);
}
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Service for validating configuration files.
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Services.SecureStore;
using JdeScoping.ConfigManager.Core.Services.SecureStore;
using JdeScoping.Core.Interfaces;
using JdeScoping.Core.Validation;
using Microsoft.Extensions.Configuration;
@@ -6,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace JdeScoping.ConfigManager.Services;
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Service that validates configuration using Infrastructure validators.
@@ -1,4 +1,4 @@
namespace JdeScoping.ConfigManager.Services;
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Result of runtime configuration validation.
@@ -25,16 +25,3 @@ public class RuntimeValidationResult
/// </summary>
public List<string> Warnings { get; } = [];
}
/// <summary>
/// Service for validating runtime configuration using Infrastructure validators.
/// </summary>
public interface IRuntimeConfigValidationService
{
/// <summary>
/// Validates the configuration in the specified folder using Infrastructure validators.
/// </summary>
/// <param name="configFolderPath">Path to the configuration folder.</param>
/// <returns>List of validation results from each validator.</returns>
List<RuntimeValidationResult> ValidateRuntimeConfig(string configFolderPath);
}
@@ -1,4 +1,4 @@
namespace JdeScoping.ConfigManager.Services.SecureStore;
namespace JdeScoping.ConfigManager.Core.Services.SecureStore;
/// <summary>
/// Interface for managing SecureStore encrypted secret stores.
@@ -1,13 +1,12 @@
using System.IO;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NeoSmart.SecureStore;
namespace JdeScoping.ConfigManager.Services.SecureStore;
namespace JdeScoping.ConfigManager.Core.Services.SecureStore;
/// <summary>
/// Manages SecureStore encrypted secret stores for the Avalonia application.
/// Manages SecureStore encrypted secret stores.
/// </summary>
public class SecureStoreManager : ISecureStoreManager, IDisposable
{
@@ -1,7 +1,7 @@
using JdeScoping.ConfigManager.Services.SecureStore;
using JdeScoping.ConfigManager.Core.Services.SecureStore;
using JdeScoping.Core.Interfaces;
namespace JdeScoping.ConfigManager.Services;
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Adapts ISecureStoreManager to ISecureStoreService for use by Infrastructure validators.
@@ -0,0 +1,34 @@
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Result of a validation operation.
/// </summary>
public class ValidationResult
{
/// <summary>
/// Gets a value indicating whether the validation succeeded (no errors).
/// </summary>
public bool IsValid => Errors.Count == 0;
/// <summary>
/// Gets the list of validation errors encountered.
/// </summary>
public List<string> Errors { get; } = [];
/// <summary>
/// Gets the list of validation warnings encountered.
/// </summary>
public List<string> Warnings { get; } = [];
/// <summary>
/// Adds an error message to the validation result.
/// </summary>
/// <param name="message">The error message to add.</param>
public void AddError(string message) => Errors.Add(message);
/// <summary>
/// Adds a warning message to the validation result.
/// </summary>
/// <param name="message">The warning message to add.</param>
public void AddWarning(string message) => Warnings.Add(message);
}
@@ -1,7 +1,7 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.DataSync.Configuration;
namespace JdeScoping.ConfigManager.Services;
namespace JdeScoping.ConfigManager.Core.Services;
/// <summary>
/// Service for validating configuration files.
@@ -3,9 +3,8 @@ using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input.Platform;
using Avalonia.Markup.Xaml;
using JdeScoping.ConfigManager.Application;
using JdeScoping.ConfigManager.Core.DependencyInjection;
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.Services.SecureStore;
using JdeScoping.ConfigManager.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -50,33 +49,15 @@ public partial class App : Avalonia.Application
.AddConsole()
.SetMinimumLevel(LogLevel.Debug));
// Services - File system abstraction
services.AddSingleton<IFileSystem, FileSystem>();
// Add all ConfigManager.Core services
services.AddConfigManagerCore();
// Services - Configuration management
services.AddSingleton<IAutoDiscoveryService, AutoDiscoveryService>();
services.AddSingleton<IBackupService, BackupService>();
services.AddSingleton<IDiffService, DiffService>();
services.AddSingleton<IValidationService, ValidationService>();
services.AddScoped<IConfigFileService, ConfigFileService>();
// Platform Services
// Platform Services (Avalonia-specific)
services.AddSingleton<IDialogService>(sp =>
new AvaloniaDialogService(GetMainWindow));
services.AddSingleton<IClipboardService>(sp =>
new AvaloniaClipboardService(GetClipboard));
// SecureStore Services
services.AddSingleton<ISecureStoreManager, SecureStoreManager>();
services.AddSingleton<StoreUseCases>();
services.AddSingleton<SecretUseCases>();
// Runtime Validation Services
services.AddSingleton<IRuntimeConfigValidationService, RuntimeConfigValidationService>();
// Connection Testing
services.AddSingleton<IConnectionTestService, ConnectionTestService>();
// ViewModels
services.AddTransient<MainWindowViewModel>();
}
@@ -1,6 +1,6 @@
using System.Globalization;
using Avalonia.Data.Converters;
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
namespace JdeScoping.ConfigManager.Converters;
@@ -16,22 +16,15 @@
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.*" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.*" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.*" Condition="'$(Configuration)' == 'Debug'" />
<PackageReference Include="DiffPlex" Version="1.7.*" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.*" />
<PackageReference Include="MessageBox.Avalonia" Version="3.1.*" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.*" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.*" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.*" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.*" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.*" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.*" />
<PackageReference Include="SecureStore" Version="1.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.*" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\JdeScoping.Core\JdeScoping.Core.csproj" />
<ProjectReference Include="..\..\JdeScoping.DataSync\JdeScoping.DataSync.csproj" />
<ProjectReference Include="..\..\JdeScoping.Infrastructure\JdeScoping.Infrastructure.csproj" />
<ProjectReference Include="..\JdeScoping.ConfigManager.Core\JdeScoping.ConfigManager.Core.csproj" />
</ItemGroup>
</Project>
@@ -1,6 +1,7 @@
using System.Text;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.ConfigManager.Views.Dialogs;
using MsBox.Avalonia;
using MsBox.Avalonia.Enums;
@@ -1,39 +0,0 @@
using JdeScoping.ConfigManager.Models;
namespace JdeScoping.ConfigManager.Services;
/// <summary>
/// Result of testing a database connection.
/// </summary>
public class ConnectionTestResult
{
/// <summary>
/// Gets a value indicating whether the connection test was successful.
/// </summary>
public bool Success { get; init; }
/// <summary>
/// Gets the message describing the result of the connection test.
/// </summary>
public string Message { get; init; } = string.Empty;
/// <summary>
/// Gets the elapsed time of the connection test operation.
/// </summary>
public TimeSpan? Duration { get; init; }
}
/// <summary>
/// Service for testing database connections.
/// </summary>
public interface IConnectionTestService
{
/// <summary>
/// Tests a database connection asynchronously.
/// </summary>
/// <param name="connectionString">The connection string to test.</param>
/// <param name="provider">The database provider type.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A ConnectionTestResult indicating success or failure of the test.</returns>
Task<ConnectionTestResult> TestConnectionAsync(string connectionString, ConnectionProvider provider, CancellationToken cancellationToken = default);
}
@@ -1,3 +1,5 @@
using JdeScoping.ConfigManager.Core.Services;
namespace JdeScoping.ConfigManager.Services;
/// <summary>
@@ -1,6 +1,6 @@
using System.Collections.ObjectModel;
using System.Windows.Input;
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.Core.Services;
namespace JdeScoping.ConfigManager.ViewModels.Dialogs;
@@ -1,5 +1,5 @@
using System.Windows.Input;
using JdeScoping.ConfigManager.Constants;
using JdeScoping.ConfigManager.Core.Constants;
namespace JdeScoping.ConfigManager.ViewModels.Dialogs;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Constants;
using JdeScoping.ConfigManager.Core.Constants;
namespace JdeScoping.ConfigManager.ViewModels.Dialogs;
@@ -1,5 +1,5 @@
using System.Windows.Input;
using JdeScoping.ConfigManager.Constants;
using JdeScoping.ConfigManager.Core.Constants;
namespace JdeScoping.ConfigManager.ViewModels.Dialogs;
@@ -1,6 +1,6 @@
using System.Collections.ObjectModel;
using System.Windows.Input;
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.Core.Services;
namespace JdeScoping.ConfigManager.ViewModels.Dialogs;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
namespace JdeScoping.ConfigManager.ViewModels.Forms;
@@ -1,5 +1,5 @@
using System.Windows.Input;
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
namespace JdeScoping.ConfigManager.ViewModels.Forms;
@@ -1,8 +1,9 @@
using System.Collections.ObjectModel;
using System.Windows.Input;
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.ConfigManager.Core.Services.SecureStore;
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.Services.SecureStore;
namespace JdeScoping.ConfigManager.ViewModels.Forms;
@@ -52,10 +53,10 @@ public class ConnectionStringsFormViewModel : ViewModelBase
: null;
// Update entry's RawConnectionString with SecureStore value if available
if (!string.IsNullOrEmpty(secureStoreValue))
{
ConnectionStringsSectionConverter.ApplyConnectionString(entry, secureStoreValue);
}
if (!string.IsNullOrEmpty(secureStoreValue))
{
ConnectionStringsSectionConverter.ApplyConnectionString(entry, secureStoreValue);
}
Connections.Add(new ConnectionStringEntryViewModel(entry, OnEntryChanged));
}
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
namespace JdeScoping.ConfigManager.ViewModels.Forms;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
namespace JdeScoping.ConfigManager.ViewModels.Forms;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
namespace JdeScoping.ConfigManager.ViewModels.Forms;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
namespace JdeScoping.ConfigManager.ViewModels.Forms;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
namespace JdeScoping.ConfigManager.ViewModels.Forms;
@@ -1,10 +1,11 @@
using System.Collections.ObjectModel;
using System.Windows.Input;
using Avalonia.Media;
using JdeScoping.ConfigManager.Constants;
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Constants;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.ConfigManager.Core.Services.SecureStore;
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.Services.SecureStore;
using JdeScoping.ConfigManager.ViewModels.Dialogs;
using JdeScoping.ConfigManager.ViewModels.Forms;
using JdeScoping.DataSync.Configuration;
@@ -1,8 +1,8 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using JdeScoping.ConfigManager.Constants;
using JdeScoping.ConfigManager.Services.SecureStore;
using JdeScoping.ConfigManager.Core.Constants;
using JdeScoping.ConfigManager.Core.Services.SecureStore;
using JdeScoping.ConfigManager.ViewModels.Dialogs;
using MsBox.Avalonia;
using MsBox.Avalonia.Enums;
@@ -1,6 +1,6 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using JdeScoping.ConfigManager.Constants;
using JdeScoping.ConfigManager.Core.Constants;
using JdeScoping.ConfigManager.ViewModels.Dialogs;
using MsBox.Avalonia;
using MsBox.Avalonia.Enums;
@@ -1,7 +1,7 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using JdeScoping.ConfigManager.Constants;
using JdeScoping.ConfigManager.Core.Constants;
using JdeScoping.ConfigManager.ViewModels.Dialogs;
using MsBox.Avalonia;
using MsBox.Avalonia.Enums;
@@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Utils\JdeScoping.ConfigManager.Core\JdeScoping.ConfigManager.Core.csproj" />
<ProjectReference Include="..\..\src\Utils\JdeScoping.ConfigManager\JdeScoping.ConfigManager.csproj" />
</ItemGroup>
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
namespace JdeScoping.ConfigManager.Tests.Models;
@@ -1,5 +1,5 @@
using System.Text.Json;
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
using Shouldly;
using Xunit;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.Core.Services;
namespace JdeScoping.ConfigManager.Tests.Services;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.Core.Services;
namespace JdeScoping.ConfigManager.Tests.Services;
@@ -1,5 +1,5 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.DataSync.Configuration;
namespace JdeScoping.ConfigManager.Tests.Services;
@@ -1,5 +1,5 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Core.Services;
namespace JdeScoping.ConfigManager.Tests.Services;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.Core.Services;
namespace JdeScoping.ConfigManager.Tests.Services;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.Core.Services;
namespace JdeScoping.ConfigManager.Tests.Services;
@@ -1,5 +1,5 @@
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.Services.SecureStore;
using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.ConfigManager.Core.Services.SecureStore;
namespace JdeScoping.ConfigManager.Tests.Services;
@@ -1,5 +1,5 @@
using System.IO;
using JdeScoping.ConfigManager.Services.SecureStore;
using JdeScoping.ConfigManager.Core.Services.SecureStore;
namespace JdeScoping.ConfigManager.Tests.Services.SecureStore;
@@ -1,5 +1,5 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.DataSync.Configuration;
namespace JdeScoping.ConfigManager.Tests.Services;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.ConfigManager.ViewModels.Dialogs;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Dialogs;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Constants;
using JdeScoping.ConfigManager.Core.Constants;
using JdeScoping.ConfigManager.ViewModels.Dialogs;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Dialogs;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Constants;
using JdeScoping.ConfigManager.Core.Constants;
using JdeScoping.ConfigManager.ViewModels.Dialogs;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Dialogs;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Constants;
using JdeScoping.ConfigManager.Core.Constants;
using JdeScoping.ConfigManager.ViewModels.Dialogs;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Dialogs;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.ConfigManager.ViewModels.Dialogs;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Dialogs;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
@@ -1,6 +1,7 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.ConfigManager.Core.Services.SecureStore;
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.Services.SecureStore;
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
@@ -1,4 +1,4 @@
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.ViewModels.Forms;
namespace JdeScoping.ConfigManager.Tests.ViewModels.Forms;
@@ -1,7 +1,8 @@
using JdeScoping.ConfigManager.Constants;
using JdeScoping.ConfigManager.Models;
using JdeScoping.ConfigManager.Core.Constants;
using JdeScoping.ConfigManager.Core.Models;
using JdeScoping.ConfigManager.Core.Services;
using JdeScoping.ConfigManager.Core.Services.SecureStore;
using JdeScoping.ConfigManager.Services;
using JdeScoping.ConfigManager.Services.SecureStore;
using JdeScoping.ConfigManager.ViewModels;
using JdeScoping.ConfigManager.ViewModels.Forms;
using JdeScoping.DataSync.Configuration;