diff --git a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/BackupCommands.cs b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/BackupCommands.cs
new file mode 100644
index 0000000..28d35f0
--- /dev/null
+++ b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/BackupCommands.cs
@@ -0,0 +1,347 @@
+using System.CommandLine;
+using JdeScoping.ConfigManager.Core.Services;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace JdeScoping.ConfigManager.Cli.Commands;
+
+///
+/// Backup management command implementations.
+///
+public static class BackupCommands
+{
+ ///
+ /// Creates the backup create command.
+ ///
+ public static Command CreateCreateCommand(
+ IServiceProvider serviceProvider,
+ Option configPathOption,
+ Option verboseOption,
+ Option quietOption)
+ {
+ var command = new Command("create", "Create backup of appsettings.json");
+
+ command.SetHandler(async (string? configPath, bool verbose, bool quiet) =>
+ {
+ var exitCode = await CreateBackupAsync(serviceProvider, configPath, verbose, quiet);
+ Environment.ExitCode = exitCode;
+ }, configPathOption, verboseOption, quietOption);
+
+ return command;
+ }
+
+ ///
+ /// Creates the backup list command.
+ ///
+ public static Command CreateListCommand(
+ IServiceProvider serviceProvider,
+ Option configPathOption,
+ Option verboseOption,
+ Option quietOption)
+ {
+ var command = new Command("list", "List available backups");
+
+ command.SetHandler(async (string? configPath, bool verbose, bool quiet) =>
+ {
+ var exitCode = await ListBackupsAsync(serviceProvider, configPath, verbose, quiet);
+ Environment.ExitCode = exitCode;
+ }, configPathOption, verboseOption, quietOption);
+
+ return command;
+ }
+
+ ///
+ /// Creates the backup restore command.
+ ///
+ public static Command CreateRestoreCommand(
+ IServiceProvider serviceProvider,
+ Option configPathOption,
+ Option verboseOption,
+ Option quietOption)
+ {
+ var command = new Command("restore", "Restore a specific backup");
+
+ var timestampArgument = new Argument("timestamp", "The backup timestamp (e.g., 2026-01-15_120000)");
+ command.AddArgument(timestampArgument);
+
+ command.SetHandler(async (string? configPath, bool verbose, bool quiet, string timestamp) =>
+ {
+ var exitCode = await RestoreBackupAsync(serviceProvider, configPath, verbose, quiet, timestamp);
+ Environment.ExitCode = exitCode;
+ }, configPathOption, verboseOption, quietOption, timestampArgument);
+
+ return command;
+ }
+
+ ///
+ /// Creates the backup cleanup command.
+ ///
+ public static Command CreateCleanupCommand(
+ IServiceProvider serviceProvider,
+ Option configPathOption,
+ Option verboseOption,
+ Option quietOption)
+ {
+ var command = new Command("cleanup", "Remove old backups");
+
+ var keepOption = new Option(
+ aliases: ["--keep", "-k"],
+ getDefaultValue: () => 10,
+ description: "Number of backups to keep");
+ command.AddOption(keepOption);
+
+ command.SetHandler(async (string? configPath, bool verbose, bool quiet, int keep) =>
+ {
+ var exitCode = await CleanupBackupsAsync(serviceProvider, configPath, verbose, quiet, keep);
+ Environment.ExitCode = exitCode;
+ }, configPathOption, verboseOption, quietOption, keepOption);
+
+ return command;
+ }
+
+ private static async Task CreateBackupAsync(
+ 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 backupService = serviceProvider.GetRequiredService();
+ 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 backupPath = await backupService.CreateBackupAsync(appSettingsPath);
+
+ if (!quiet)
+ {
+ Console.WriteLine("Backup created successfully");
+ Console.WriteLine($"Path: {backupPath}");
+ }
+
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return 1;
+ }
+ }
+
+ private static async Task ListBackupsAsync(
+ 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 backupService = serviceProvider.GetRequiredService();
+ var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
+
+ try
+ {
+ var backups = await backupService.GetBackupsAsync(appSettingsPath);
+
+ if (!quiet)
+ {
+ Console.WriteLine($"=== Backups ({backups.Count}) ===");
+ }
+
+ if (backups.Count == 0)
+ {
+ if (!quiet)
+ Console.WriteLine("No backups found");
+ return 0;
+ }
+
+ if (!quiet)
+ {
+ Console.WriteLine($"{"Timestamp",-20} {"Size",-12} {"Path"}");
+ Console.WriteLine(new string('-', 60));
+ }
+
+ foreach (var backup in backups)
+ {
+ var timestamp = backup.Timestamp.ToString("yyyy-MM-dd_HHmmss");
+ var size = FormatFileSize(backup.Size);
+ var path = verbose ? backup.Path : Path.GetFileName(backup.Path);
+
+ Console.WriteLine($"{timestamp,-20} {size,-12} {path}");
+ }
+
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return 1;
+ }
+ }
+
+ private static async Task RestoreBackupAsync(
+ IServiceProvider serviceProvider,
+ string? configPath,
+ bool verbose,
+ bool quiet,
+ string timestamp)
+ {
+ 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 backupService = serviceProvider.GetRequiredService();
+ var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
+
+ try
+ {
+ var backups = await backupService.GetBackupsAsync(appSettingsPath);
+ var backup = backups.FirstOrDefault(b => b.Path.Contains(timestamp));
+
+ if (backup == null)
+ {
+ Console.Error.WriteLine($"Error: Backup with timestamp '{timestamp}' not found");
+
+ if (backups.Count > 0)
+ {
+ Console.Error.WriteLine();
+ Console.Error.WriteLine("Available backups:");
+ foreach (var b in backups.Take(5))
+ {
+ Console.Error.WriteLine($" {b.Timestamp:yyyy-MM-dd_HHmmss}");
+ }
+ }
+
+ return 1;
+ }
+
+ // Create a backup of current config before restoring
+ if (File.Exists(appSettingsPath))
+ {
+ var currentBackup = await backupService.CreateBackupAsync(appSettingsPath);
+ if (!quiet)
+ {
+ Console.WriteLine($"Current config backed up to: {Path.GetFileName(currentBackup)}");
+ }
+ }
+
+ await backupService.RestoreBackupAsync(backup.Path, appSettingsPath);
+
+ if (!quiet)
+ {
+ Console.WriteLine("Backup restored successfully");
+ Console.WriteLine($"Restored from: {Path.GetFileName(backup.Path)}");
+ }
+
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return 1;
+ }
+ }
+
+ private static async Task CleanupBackupsAsync(
+ IServiceProvider serviceProvider,
+ string? configPath,
+ bool verbose,
+ bool quiet,
+ int keep)
+ {
+ 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;
+ }
+
+ if (keep < 1)
+ {
+ Console.Error.WriteLine("Error: --keep must be at least 1");
+ return 1;
+ }
+
+ var backupService = serviceProvider.GetRequiredService();
+ var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
+
+ try
+ {
+ var backupsBefore = await backupService.GetBackupsAsync(appSettingsPath);
+ var countBefore = backupsBefore.Count;
+
+ await backupService.CleanupOldBackupsAsync(appSettingsPath, keep);
+
+ var backupsAfter = await backupService.GetBackupsAsync(appSettingsPath);
+ var deleted = countBefore - backupsAfter.Count;
+
+ if (!quiet)
+ {
+ if (deleted > 0)
+ {
+ Console.WriteLine($"Deleted {deleted} old backup(s)");
+ Console.WriteLine($"Remaining: {backupsAfter.Count} backup(s)");
+ }
+ else
+ {
+ Console.WriteLine("No backups to delete");
+ Console.WriteLine($"Current count: {countBefore} (keeping {keep})");
+ }
+ }
+
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return 1;
+ }
+ }
+
+ private static string FormatFileSize(long bytes)
+ {
+ string[] sizes = ["B", "KB", "MB", "GB"];
+ var order = 0;
+ double size = bytes;
+
+ while (size >= 1024 && order < sizes.Length - 1)
+ {
+ order++;
+ size /= 1024;
+ }
+
+ return $"{size:0.##} {sizes[order]}";
+ }
+
+ private static async Task GetConfigFolderAsync(IServiceProvider serviceProvider, string? configPath)
+ {
+ if (!string.IsNullOrEmpty(configPath))
+ {
+ if (Directory.Exists(configPath))
+ return configPath;
+ return null;
+ }
+
+ var autoDiscoveryService = serviceProvider.GetRequiredService();
+ return await autoDiscoveryService.FindConfigFolderAsync();
+ }
+}
diff --git a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConfigCommands.cs b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConfigCommands.cs
new file mode 100644
index 0000000..9e412fd
--- /dev/null
+++ b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConfigCommands.cs
@@ -0,0 +1,435 @@
+using System.CommandLine;
+using System.Text.Json;
+using JdeScoping.ConfigManager.Core.Services;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace JdeScoping.ConfigManager.Cli.Commands;
+
+///
+/// Configuration viewing command implementations.
+///
+public static class ConfigCommands
+{
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ WriteIndented = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ ///
+ /// Creates the config show command with section subcommands.
+ ///
+ public static Command CreateShowCommand(
+ IServiceProvider serviceProvider,
+ Option configPathOption,
+ Option verboseOption,
+ Option quietOption)
+ {
+ var command = new Command("show", "View configuration settings");
+
+ var jsonOption = new Option(
+ aliases: ["--json", "-j"],
+ description: "Output in JSON format");
+
+ command.AddOption(jsonOption);
+
+ // Add section subcommands
+ command.AddCommand(CreateSectionCommand("datasync", "Show DataSync configuration",
+ serviceProvider, configPathOption, verboseOption, quietOption, jsonOption,
+ config => config.DataSync, FormatDataSync));
+
+ command.AddCommand(CreateSectionCommand("dataaccess", "Show DataAccess configuration",
+ serviceProvider, configPathOption, verboseOption, quietOption, jsonOption,
+ config => config.DataAccess, FormatDataAccess));
+
+ command.AddCommand(CreateSectionCommand("auth", "Show Auth configuration",
+ serviceProvider, configPathOption, verboseOption, quietOption, jsonOption,
+ config => config.Auth, FormatAuth));
+
+ command.AddCommand(CreateSectionCommand("ldap", "Show LDAP configuration",
+ serviceProvider, configPathOption, verboseOption, quietOption, jsonOption,
+ config => config.Ldap, FormatLdap));
+
+ command.AddCommand(CreateSectionCommand("search", "Show Search configuration",
+ serviceProvider, configPathOption, verboseOption, quietOption, jsonOption,
+ config => config.Search, FormatSearch));
+
+ command.AddCommand(CreateSectionCommand("excelexport", "Show ExcelExport configuration",
+ serviceProvider, configPathOption, verboseOption, quietOption, jsonOption,
+ config => config.ExcelExport, FormatExcelExport));
+
+ command.AddCommand(CreateSectionCommand("securestore", "Show SecureStore configuration",
+ serviceProvider, configPathOption, verboseOption, quietOption, jsonOption,
+ config => config.SecureStore, FormatSecureStore));
+
+ command.AddCommand(CreateConnectionsCommand(serviceProvider, configPathOption, verboseOption, quietOption, jsonOption));
+ command.AddCommand(CreateAllCommand(serviceProvider, configPathOption, verboseOption, quietOption, jsonOption));
+
+ return command;
+ }
+
+ private static Command CreateSectionCommand(
+ string name,
+ string description,
+ IServiceProvider serviceProvider,
+ Option configPathOption,
+ Option verboseOption,
+ Option quietOption,
+ Option jsonOption,
+ Func selector,
+ Action formatter)
+ {
+ var command = new Command(name, description);
+
+ command.SetHandler(async (string? configPath, bool verbose, bool quiet, bool json) =>
+ {
+ var exitCode = await ShowSectionAsync(serviceProvider, configPath, verbose, quiet, json, name, selector, formatter);
+ Environment.ExitCode = exitCode;
+ }, configPathOption, verboseOption, quietOption, jsonOption);
+
+ return command;
+ }
+
+ private static Command CreateConnectionsCommand(
+ IServiceProvider serviceProvider,
+ Option configPathOption,
+ Option verboseOption,
+ Option quietOption,
+ Option jsonOption)
+ {
+ var command = new Command("connections", "Show connection names (no secrets)");
+
+ command.SetHandler(async (string? configPath, bool verbose, bool quiet, bool json) =>
+ {
+ var exitCode = await ShowConnectionsAsync(serviceProvider, configPath, verbose, quiet, json);
+ Environment.ExitCode = exitCode;
+ }, configPathOption, verboseOption, quietOption, jsonOption);
+
+ return command;
+ }
+
+ private static Command CreateAllCommand(
+ IServiceProvider serviceProvider,
+ Option configPathOption,
+ Option verboseOption,
+ Option quietOption,
+ Option jsonOption)
+ {
+ var command = new Command("all", "Show all configuration sections");
+
+ command.SetHandler(async (string? configPath, bool verbose, bool quiet, bool json) =>
+ {
+ var exitCode = await ShowAllAsync(serviceProvider, configPath, verbose, quiet, json);
+ Environment.ExitCode = exitCode;
+ }, configPathOption, verboseOption, quietOption, jsonOption);
+
+ return command;
+ }
+
+ private static async Task ShowSectionAsync(
+ IServiceProvider serviceProvider,
+ string? configPath,
+ bool verbose,
+ bool quiet,
+ bool json,
+ string sectionName,
+ Func selector,
+ Action formatter)
+ {
+ 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();
+ 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 section = selector(config);
+
+ if (json)
+ {
+ Console.WriteLine(JsonSerializer.Serialize(section, JsonOptions));
+ }
+ else
+ {
+ if (!quiet)
+ {
+ Console.WriteLine($"=== {sectionName} ===");
+ }
+ formatter(section, verbose);
+ }
+
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return 1;
+ }
+ }
+
+ private static async Task ShowConnectionsAsync(
+ IServiceProvider serviceProvider,
+ string? configPath,
+ bool verbose,
+ bool quiet,
+ bool json)
+ {
+ 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();
+ 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);
+
+ if (json)
+ {
+ // Output only names and providers, no secrets
+ var safeConnections = config.ConnectionStrings.Entries.Select(e => new
+ {
+ e.Name,
+ Provider = e.Provider.ToString()
+ });
+ Console.WriteLine(JsonSerializer.Serialize(safeConnections, JsonOptions));
+ }
+ else
+ {
+ if (!quiet)
+ {
+ Console.WriteLine($"=== Connections ({config.ConnectionStrings.Entries.Count}) ===");
+ Console.WriteLine($"{"Name",-25} {"Provider",-15}");
+ Console.WriteLine(new string('-', 40));
+ }
+
+ foreach (var entry in config.ConnectionStrings.Entries.OrderBy(e => e.Name))
+ {
+ Console.WriteLine($"{entry.Name,-25} {entry.Provider,-15}");
+ }
+ }
+
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return 1;
+ }
+ }
+
+ private static async Task ShowAllAsync(
+ IServiceProvider serviceProvider,
+ string? configPath,
+ bool verbose,
+ bool quiet,
+ bool json)
+ {
+ 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();
+ 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);
+
+ if (json)
+ {
+ // Sanitize passwords before outputting
+ var sanitizedConfig = new
+ {
+ config.DataSync,
+ config.DataAccess,
+ config.Auth,
+ config.Ldap,
+ config.Search,
+ config.ExcelExport,
+ config.SecureStore,
+ config.Pipelines,
+ Connections = config.ConnectionStrings.Entries.Select(e => new
+ {
+ e.Name,
+ Provider = e.Provider.ToString()
+ })
+ };
+ Console.WriteLine(JsonSerializer.Serialize(sanitizedConfig, JsonOptions));
+ }
+ else
+ {
+ Console.WriteLine("=== DataSync ===");
+ FormatDataSync(config.DataSync, verbose);
+ Console.WriteLine();
+
+ Console.WriteLine("=== DataAccess ===");
+ FormatDataAccess(config.DataAccess, verbose);
+ Console.WriteLine();
+
+ Console.WriteLine("=== Auth ===");
+ FormatAuth(config.Auth, verbose);
+ Console.WriteLine();
+
+ Console.WriteLine("=== LDAP ===");
+ FormatLdap(config.Ldap, verbose);
+ Console.WriteLine();
+
+ Console.WriteLine("=== Search ===");
+ FormatSearch(config.Search, verbose);
+ Console.WriteLine();
+
+ Console.WriteLine("=== ExcelExport ===");
+ FormatExcelExport(config.ExcelExport, verbose);
+ Console.WriteLine();
+
+ Console.WriteLine("=== SecureStore ===");
+ FormatSecureStore(config.SecureStore, verbose);
+ Console.WriteLine();
+
+ Console.WriteLine($"=== Connections ({config.ConnectionStrings.Entries.Count}) ===");
+ Console.WriteLine($"{"Name",-25} {"Provider",-15}");
+ Console.WriteLine(new string('-', 40));
+ foreach (var entry in config.ConnectionStrings.Entries.OrderBy(e => e.Name))
+ {
+ Console.WriteLine($"{entry.Name,-25} {entry.Provider,-15}");
+ }
+ }
+
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return 1;
+ }
+ }
+
+ private static void FormatDataSync(Core.Models.DataSyncSection section, bool verbose)
+ {
+ Console.WriteLine($"Enabled: {section.Enabled}");
+ Console.WriteLine($"CheckInterval: {section.CheckInterval}");
+ Console.WriteLine($"MaxDegreeOfParallelism: {section.MaxDegreeOfParallelism}");
+ Console.WriteLine($"BatchSize: {section.BatchSize}");
+ Console.WriteLine($"BulkCopyBatchSize: {section.BulkCopyBatchSize}");
+
+ if (verbose)
+ {
+ Console.WriteLine($"LookbackMultiplier: {section.LookbackMultiplier}");
+ Console.WriteLine($"PurgeRetentionDays: {section.PurgeRetentionDays}");
+ Console.WriteLine($"SyncTimeoutSeconds: {section.SyncTimeoutSeconds}");
+ }
+ }
+
+ private static void FormatDataAccess(Core.Models.DataAccessSection section, bool verbose)
+ {
+ Console.WriteLine($"DefaultTimeoutSeconds: {section.DefaultTimeoutSeconds}");
+ Console.WriteLine($"LotUsageTimeoutSeconds: {section.LotUsageTimeoutSeconds}");
+ Console.WriteLine($"MisDataTimeoutSeconds: {section.MisDataTimeoutSeconds}");
+ Console.WriteLine($"EnableDetailedLogging: {section.EnableDetailedLogging}");
+
+ if (verbose)
+ {
+ Console.WriteLine($"ProductionSchema: {section.ProductionSchema}");
+ Console.WriteLine($"ArchiveSchema: {section.ArchiveSchema}");
+ Console.WriteLine($"StageSchema: {section.StageSchema}");
+ }
+ }
+
+ private static void FormatAuth(Core.Models.AuthSection section, bool verbose)
+ {
+ Console.WriteLine($"CookieName: {section.CookieName}");
+ Console.WriteLine($"CookieExpirationMinutes: {section.CookieExpirationMinutes}");
+ }
+
+ private static void FormatLdap(Core.Models.LdapSection section, bool verbose)
+ {
+ Console.WriteLine($"UseFakeAuth: {section.UseFakeAuth}");
+ Console.WriteLine($"ConnectionTimeoutSeconds: {section.ConnectionTimeoutSeconds}");
+ Console.WriteLine($"ServerUrls: {(section.ServerUrls.Length > 0 ? string.Join(", ", section.ServerUrls) : "(none)")}");
+ Console.WriteLine($"GroupDn: {(string.IsNullOrEmpty(section.GroupDn) ? "(not set)" : section.GroupDn)}");
+
+ if (verbose)
+ {
+ Console.WriteLine($"SearchBase: {(string.IsNullOrEmpty(section.SearchBase) ? "(not set)" : section.SearchBase)}");
+ Console.WriteLine($"AdminBypassUsers: {(section.AdminBypassUsers.Length > 0 ? string.Join(", ", section.AdminBypassUsers) : "(none)")}");
+ }
+ }
+
+ private static void FormatSearch(Core.Models.SearchSection section, bool verbose)
+ {
+ Console.WriteLine($"MaxResultRows: {section.MaxResultRows}");
+ Console.WriteLine($"TimeoutSeconds: {section.TimeoutSeconds}");
+ Console.WriteLine($"MaxConcurrentSearches: {section.MaxConcurrentSearches}");
+ }
+
+ private static void FormatExcelExport(Core.Models.ExcelExportSection section, bool verbose)
+ {
+ Console.WriteLine($"MaxRowsPerSheet: {section.MaxRowsPerSheet}");
+ Console.WriteLine($"DefaultDateFormat: {section.DefaultDateFormat}");
+ Console.WriteLine($"TimezoneId: {section.TimezoneId}");
+ Console.WriteLine($"TimezoneAbbreviation: {section.TimezoneAbbreviation}");
+ Console.WriteLine($"DebugWriteToFile: {section.DebugWriteToFile}");
+
+ if (verbose && section.DebugWriteToFile)
+ {
+ Console.WriteLine($"DebugOutputDirectory: {section.DebugOutputDirectory}");
+ }
+
+ // Note: passwords are not shown
+ Console.WriteLine("CriteriaSheetPassword: ********");
+ Console.WriteLine("DataSheetPassword: ********");
+ }
+
+ private static void FormatSecureStore(Core.Models.SecureStoreSection section, bool verbose)
+ {
+ Console.WriteLine($"StorePath: {section.StorePath}");
+ Console.WriteLine($"KeyFilePath: {section.KeyFilePath}");
+ Console.WriteLine($"AutoCreateStore: {section.AutoCreateStore}");
+ Console.WriteLine($"RequiredKeys: {(section.RequiredKeys.Count > 0 ? string.Join(", ", section.RequiredKeys) : "(none)")}");
+ }
+
+ private static async Task GetConfigFolderAsync(IServiceProvider serviceProvider, string? configPath)
+ {
+ if (!string.IsNullOrEmpty(configPath))
+ {
+ if (Directory.Exists(configPath))
+ return configPath;
+ return null;
+ }
+
+ var autoDiscoveryService = serviceProvider.GetRequiredService();
+ return await autoDiscoveryService.FindConfigFolderAsync();
+ }
+}
diff --git a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConnectionCommands.cs b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConnectionCommands.cs
new file mode 100644
index 0000000..eed3d30
--- /dev/null
+++ b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/ConnectionCommands.cs
@@ -0,0 +1,492 @@
+using System.CommandLine;
+using JdeScoping.ConfigManager.Core.Models;
+using JdeScoping.ConfigManager.Core.Services;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace JdeScoping.ConfigManager.Cli.Commands;
+
+///
+/// Connection string management command implementations.
+///
+public static class ConnectionCommands
+{
+ ///
+ /// Creates the connection list command.
+ ///
+ public static Command CreateListCommand(
+ IServiceProvider serviceProvider,
+ Option configPathOption,
+ Option verboseOption,
+ Option quietOption)
+ {
+ var command = new Command("list", "List all connection names and providers");
+
+ command.SetHandler(async (string? configPath, bool verbose, bool quiet) =>
+ {
+ var exitCode = await ListConnectionsAsync(serviceProvider, configPath, verbose, quiet);
+ Environment.ExitCode = exitCode;
+ }, configPathOption, verboseOption, quietOption);
+
+ return command;
+ }
+
+ ///
+ /// Creates the connection show command.
+ ///
+ public static Command CreateShowCommand(
+ IServiceProvider serviceProvider,
+ Option configPathOption,
+ Option verboseOption,
+ Option quietOption)
+ {
+ var command = new Command("show", "Show connection details (password masked)");
+
+ var nameArgument = new Argument("name", "The connection name");
+ command.AddArgument(nameArgument);
+
+ command.SetHandler(async (string? configPath, bool verbose, bool quiet, string name) =>
+ {
+ var exitCode = await ShowConnectionAsync(serviceProvider, configPath, verbose, quiet, name);
+ Environment.ExitCode = exitCode;
+ }, configPathOption, verboseOption, quietOption, nameArgument);
+
+ return command;
+ }
+
+ ///
+ /// Creates the connection add command.
+ ///
+ public static Command CreateAddCommand(
+ IServiceProvider serviceProvider,
+ Option configPathOption,
+ Option verboseOption,
+ Option quietOption)
+ {
+ var command = new Command("add", "Add new connection");
+
+ var nameArgument = new Argument("name", "The connection name");
+ command.AddArgument(nameArgument);
+
+ var providerOption = new Option(
+ aliases: ["--provider", "-p"],
+ getDefaultValue: () => ConnectionProvider.SqlServer,
+ description: "Database provider (SqlServer, Oracle, Generic)");
+ command.AddOption(providerOption);
+
+ var serverOption = new Option(
+ aliases: ["--server", "-s"],
+ description: "Server name or host");
+ command.AddOption(serverOption);
+
+ var databaseOption = new Option(
+ aliases: ["--database", "-d"],
+ description: "Database name");
+ command.AddOption(databaseOption);
+
+ var userOption = new Option(
+ aliases: ["--user", "-u"],
+ description: "User ID");
+ command.AddOption(userOption);
+
+ var passwordOption = new Option(
+ aliases: ["--password"],
+ description: "Password (will be stored in config)");
+ command.AddOption(passwordOption);
+
+ var portOption = new Option(
+ aliases: ["--port"],
+ description: "Port number");
+ command.AddOption(portOption);
+
+ var serviceNameOption = new Option(
+ aliases: ["--service-name"],
+ description: "Oracle service name");
+ command.AddOption(serviceNameOption);
+
+ var rawOption = new Option(
+ aliases: ["--raw"],
+ description: "Raw connection string (for Generic provider)");
+ command.AddOption(rawOption);
+
+ command.SetHandler(async (context) =>
+ {
+ var configPath = context.ParseResult.GetValueForOption(configPathOption);
+ var verbose = context.ParseResult.GetValueForOption(verboseOption);
+ var quiet = context.ParseResult.GetValueForOption(quietOption);
+ var name = context.ParseResult.GetValueForArgument(nameArgument);
+ var provider = context.ParseResult.GetValueForOption(providerOption);
+ var server = context.ParseResult.GetValueForOption(serverOption);
+ var database = context.ParseResult.GetValueForOption(databaseOption);
+ var user = context.ParseResult.GetValueForOption(userOption);
+ var password = context.ParseResult.GetValueForOption(passwordOption);
+ var port = context.ParseResult.GetValueForOption(portOption);
+ var serviceName = context.ParseResult.GetValueForOption(serviceNameOption);
+ var raw = context.ParseResult.GetValueForOption(rawOption);
+
+ var exitCode = await AddConnectionAsync(
+ serviceProvider, configPath, verbose, quiet,
+ name, provider, server, database, user, password, port, serviceName, raw);
+ Environment.ExitCode = exitCode;
+ });
+
+ return command;
+ }
+
+ ///
+ /// Creates the connection remove command.
+ ///
+ public static Command CreateRemoveCommand(
+ IServiceProvider serviceProvider,
+ Option configPathOption,
+ Option verboseOption,
+ Option quietOption)
+ {
+ var command = new Command("remove", "Remove a connection");
+
+ var nameArgument = new Argument("name", "The connection name to remove");
+ command.AddArgument(nameArgument);
+
+ command.SetHandler(async (string? configPath, bool verbose, bool quiet, string name) =>
+ {
+ var exitCode = await RemoveConnectionAsync(serviceProvider, configPath, verbose, quiet, name);
+ Environment.ExitCode = exitCode;
+ }, configPathOption, verboseOption, quietOption, nameArgument);
+
+ return command;
+ }
+
+ private static async Task ListConnectionsAsync(
+ 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();
+ 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);
+
+ if (!quiet)
+ {
+ Console.WriteLine($"=== Connections ({config.ConnectionStrings.Entries.Count}) ===");
+ Console.WriteLine($"{"Name",-25} {"Provider",-15} {"Server/Host",-30}");
+ Console.WriteLine(new string('-', 70));
+ }
+
+ foreach (var entry in config.ConnectionStrings.Entries.OrderBy(e => e.Name))
+ {
+ var serverOrHost = entry.Provider switch
+ {
+ ConnectionProvider.SqlServer => entry.Server ?? "-",
+ ConnectionProvider.Oracle => entry.Host ?? "-",
+ ConnectionProvider.Generic => "(raw)",
+ _ => "-"
+ };
+
+ Console.WriteLine($"{entry.Name,-25} {entry.Provider,-15} {serverOrHost,-30}");
+ }
+
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return 1;
+ }
+ }
+
+ private static async Task ShowConnectionAsync(
+ IServiceProvider serviceProvider,
+ string? configPath,
+ bool verbose,
+ bool quiet,
+ string name)
+ {
+ 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();
+ 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 = config.ConnectionStrings.Entries.FirstOrDefault(e =>
+ e.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
+
+ if (entry == null)
+ {
+ Console.Error.WriteLine($"Error: Connection '{name}' not found");
+ return 1;
+ }
+
+ if (!quiet)
+ {
+ Console.WriteLine($"=== Connection: {entry.Name} ===");
+ }
+
+ Console.WriteLine($"Name: {entry.Name}");
+ Console.WriteLine($"Provider: {entry.Provider}");
+
+ switch (entry.Provider)
+ {
+ case ConnectionProvider.SqlServer:
+ Console.WriteLine($"Server: {entry.Server ?? "(not set)"}");
+ if (entry.SqlServerPort.HasValue)
+ Console.WriteLine($"Port: {entry.SqlServerPort.Value}");
+ Console.WriteLine($"Database: {entry.Database ?? "(not set)"}");
+ Console.WriteLine($"User ID: {entry.UserId ?? "(not set)"}");
+ Console.WriteLine($"Password: {MaskPassword(entry.Password)}");
+ Console.WriteLine($"Encrypt: {entry.Encrypt}");
+ Console.WriteLine($"TrustServerCertificate: {entry.TrustServerCertificate}");
+ Console.WriteLine($"ConnectionTimeout: {entry.ConnectionTimeout}");
+ if (!string.IsNullOrEmpty(entry.ApplicationName))
+ Console.WriteLine($"ApplicationName: {entry.ApplicationName}");
+ break;
+
+ case ConnectionProvider.Oracle:
+ Console.WriteLine($"Host: {entry.Host ?? "(not set)"}");
+ Console.WriteLine($"Port: {entry.Port}");
+ Console.WriteLine($"ServiceName: {entry.ServiceName ?? "(not set)"}");
+ Console.WriteLine($"User ID: {entry.UserId ?? "(not set)"}");
+ Console.WriteLine($"Password: {MaskPassword(entry.Password)}");
+ break;
+
+ case ConnectionProvider.Generic:
+ Console.WriteLine($"RawConnectionString: {MaskConnectionString(entry.RawConnectionString)}");
+ break;
+ }
+
+ if (verbose)
+ {
+ Console.WriteLine();
+ Console.WriteLine("Generated connection string (masked):");
+ Console.WriteLine($" {MaskConnectionString(entry.GenerateConnectionString())}");
+ }
+
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return 1;
+ }
+ }
+
+ private static async Task AddConnectionAsync(
+ IServiceProvider serviceProvider,
+ string? configPath,
+ bool verbose,
+ bool quiet,
+ string name,
+ ConnectionProvider provider,
+ string? server,
+ string? database,
+ string? user,
+ string? password,
+ int? port,
+ string? serviceName,
+ string? raw)
+ {
+ 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();
+ 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);
+
+ // Check if connection already exists
+ if (config.ConnectionStrings.Entries.Any(e =>
+ e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
+ {
+ Console.Error.WriteLine($"Error: Connection '{name}' already exists");
+ return 1;
+ }
+
+ // Validate required fields based on provider
+ if (provider == ConnectionProvider.Generic && string.IsNullOrEmpty(raw))
+ {
+ Console.Error.WriteLine("Error: --raw is required for Generic provider");
+ return 1;
+ }
+
+ var entry = new ConnectionStringEntry
+ {
+ Name = name,
+ Provider = provider,
+ Server = server,
+ Database = database,
+ UserId = user,
+ Password = password,
+ Host = server, // For Oracle, use server as host
+ ServiceName = serviceName,
+ RawConnectionString = raw
+ };
+
+ if (port.HasValue)
+ {
+ if (provider == ConnectionProvider.SqlServer)
+ entry.SqlServerPort = port;
+ else if (provider == ConnectionProvider.Oracle)
+ entry.Port = port.Value;
+ }
+
+ config.ConnectionStrings.Entries.Add(entry);
+ await configFileService.SaveAppSettingsAsync(appSettingsPath, config);
+
+ if (!quiet)
+ {
+ Console.WriteLine($"Connection '{name}' added successfully");
+ }
+
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return 1;
+ }
+ }
+
+ private static async Task RemoveConnectionAsync(
+ IServiceProvider serviceProvider,
+ string? configPath,
+ bool verbose,
+ bool quiet,
+ string name)
+ {
+ 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();
+ 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 = config.ConnectionStrings.Entries.FirstOrDefault(e =>
+ e.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
+
+ if (entry == null)
+ {
+ Console.Error.WriteLine($"Error: Connection '{name}' not found");
+ return 1;
+ }
+
+ config.ConnectionStrings.Entries.Remove(entry);
+ await configFileService.SaveAppSettingsAsync(appSettingsPath, config);
+
+ if (!quiet)
+ {
+ Console.WriteLine($"Connection '{name}' removed successfully");
+ }
+
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return 1;
+ }
+ }
+
+ private static string MaskPassword(string? password)
+ {
+ if (string.IsNullOrEmpty(password))
+ return "(not set)";
+ return "********";
+ }
+
+ private static string MaskConnectionString(string? connectionString)
+ {
+ if (string.IsNullOrEmpty(connectionString))
+ return "(not set)";
+
+ // Mask password in connection string
+ var result = connectionString;
+
+ // Common patterns for password in connection strings
+ var patterns = new[]
+ {
+ "Password=",
+ "Pwd=",
+ "password="
+ };
+
+ foreach (var pattern in patterns)
+ {
+ var idx = result.IndexOf(pattern, StringComparison.OrdinalIgnoreCase);
+ if (idx >= 0)
+ {
+ var start = idx + pattern.Length;
+ var end = result.IndexOf(';', start);
+ if (end < 0) end = result.Length;
+
+ result = string.Concat(result.AsSpan(0, start), "********", result.AsSpan(end));
+ }
+ }
+
+ return result;
+ }
+
+ private static async Task GetConfigFolderAsync(IServiceProvider serviceProvider, string? configPath)
+ {
+ if (!string.IsNullOrEmpty(configPath))
+ {
+ if (Directory.Exists(configPath))
+ return configPath;
+ return null;
+ }
+
+ var autoDiscoveryService = serviceProvider.GetRequiredService();
+ return await autoDiscoveryService.FindConfigFolderAsync();
+ }
+}
diff --git a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/PipelineCommands.cs b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/PipelineCommands.cs
new file mode 100644
index 0000000..5d016e6
--- /dev/null
+++ b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Commands/PipelineCommands.cs
@@ -0,0 +1,487 @@
+using System.CommandLine;
+using JdeScoping.ConfigManager.Core.Services;
+using JdeScoping.DataSync.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace JdeScoping.ConfigManager.Cli.Commands;
+
+///
+/// Pipeline management command implementations.
+///
+public static class PipelineCommands
+{
+ ///
+ /// Creates the pipeline list command.
+ ///
+ public static Command CreateListCommand(
+ IServiceProvider serviceProvider,
+ Option configPathOption,
+ Option verboseOption,
+ Option quietOption)
+ {
+ var command = new Command("list", "List all pipelines with status");
+
+ command.SetHandler(async (string? configPath, bool verbose, bool quiet) =>
+ {
+ var exitCode = await ListPipelinesAsync(serviceProvider, configPath, verbose, quiet);
+ Environment.ExitCode = exitCode;
+ }, configPathOption, verboseOption, quietOption);
+
+ return command;
+ }
+
+ ///
+ /// Creates the pipeline show command.
+ ///
+ public static Command CreateShowCommand(
+ IServiceProvider serviceProvider,
+ Option configPathOption,
+ Option verboseOption,
+ Option quietOption)
+ {
+ var command = new Command("show", "Show detailed pipeline configuration");
+
+ var nameArgument = new Argument("name", "The pipeline name");
+ command.AddArgument(nameArgument);
+
+ command.SetHandler(async (string? configPath, bool verbose, bool quiet, string name) =>
+ {
+ var exitCode = await ShowPipelineAsync(serviceProvider, configPath, verbose, quiet, name);
+ Environment.ExitCode = exitCode;
+ }, configPathOption, verboseOption, quietOption, nameArgument);
+
+ return command;
+ }
+
+ ///
+ /// Creates the pipeline create command.
+ ///
+ public static Command CreateCreateCommand(
+ IServiceProvider serviceProvider,
+ Option configPathOption,
+ Option verboseOption,
+ Option quietOption)
+ {
+ var command = new Command("create", "Create new pipeline with defaults");
+
+ var nameArgument = new Argument("name", "The pipeline name");
+ command.AddArgument(nameArgument);
+
+ var enabledOption = new Option(
+ aliases: ["--enabled", "-e"],
+ getDefaultValue: () => true,
+ description: "Whether the pipeline is enabled");
+ command.AddOption(enabledOption);
+
+ var manualOnlyOption = new Option(
+ aliases: ["--manual-only", "-m"],
+ description: "Pipeline can only be triggered manually");
+ command.AddOption(manualOnlyOption);
+
+ command.SetHandler(async (string? configPath, bool verbose, bool quiet, string name, bool enabled, bool manualOnly) =>
+ {
+ var exitCode = await CreatePipelineAsync(serviceProvider, configPath, verbose, quiet, name, enabled, manualOnly);
+ Environment.ExitCode = exitCode;
+ }, configPathOption, verboseOption, quietOption, nameArgument, enabledOption, manualOnlyOption);
+
+ return command;
+ }
+
+ ///
+ /// Creates the pipeline delete command.
+ ///
+ public static Command CreateDeleteCommand(
+ IServiceProvider serviceProvider,
+ Option configPathOption,
+ Option verboseOption,
+ Option quietOption)
+ {
+ var command = new Command("delete", "Delete a pipeline");
+
+ var nameArgument = new Argument("name", "The pipeline name to delete");
+ command.AddArgument(nameArgument);
+
+ var forceOption = new Option(
+ aliases: ["--force", "-f"],
+ description: "Skip confirmation prompt");
+ command.AddOption(forceOption);
+
+ command.SetHandler(async (string? configPath, bool verbose, bool quiet, string name, bool force) =>
+ {
+ var exitCode = await DeletePipelineAsync(serviceProvider, configPath, verbose, quiet, name, force);
+ Environment.ExitCode = exitCode;
+ }, configPathOption, verboseOption, quietOption, nameArgument, forceOption);
+
+ return command;
+ }
+
+ ///
+ /// Creates the pipeline enable command.
+ ///
+ public static Command CreateEnableCommand(
+ IServiceProvider serviceProvider,
+ Option configPathOption,
+ Option verboseOption,
+ Option quietOption)
+ {
+ var command = new Command("enable", "Enable a pipeline");
+
+ var nameArgument = new Argument("name", "The pipeline name to enable");
+ command.AddArgument(nameArgument);
+
+ command.SetHandler(async (string? configPath, bool verbose, bool quiet, string name) =>
+ {
+ var exitCode = await SetPipelineEnabledAsync(serviceProvider, configPath, verbose, quiet, name, true);
+ Environment.ExitCode = exitCode;
+ }, configPathOption, verboseOption, quietOption, nameArgument);
+
+ return command;
+ }
+
+ ///
+ /// Creates the pipeline disable command.
+ ///
+ public static Command CreateDisableCommand(
+ IServiceProvider serviceProvider,
+ Option configPathOption,
+ Option verboseOption,
+ Option quietOption)
+ {
+ var command = new Command("disable", "Disable a pipeline");
+
+ var nameArgument = new Argument("name", "The pipeline name to disable");
+ command.AddArgument(nameArgument);
+
+ command.SetHandler(async (string? configPath, bool verbose, bool quiet, string name) =>
+ {
+ var exitCode = await SetPipelineEnabledAsync(serviceProvider, configPath, verbose, quiet, name, false);
+ Environment.ExitCode = exitCode;
+ }, configPathOption, verboseOption, quietOption, nameArgument);
+
+ return command;
+ }
+
+ private static async Task ListPipelinesAsync(
+ 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();
+ var pipelinesDir = Path.Combine(folderPath, "Pipelines");
+
+ try
+ {
+ var pipelines = await configFileService.LoadAllPipelinesAsync(pipelinesDir);
+
+ if (!quiet)
+ {
+ Console.WriteLine($"=== Pipelines ({pipelines.Count}) ===");
+ Console.WriteLine($"{"Name",-20} {"Enabled",-8} {"Mass",-8} {"Daily",-8} {"Hourly",-8}");
+ Console.WriteLine(new string('-', 52));
+ }
+
+ foreach (var kvp in pipelines.OrderBy(p => p.Key))
+ {
+ var p = kvp.Value;
+ var mass = FormatInterval(p.MassSyncIntervalMinutes);
+ var daily = FormatInterval(p.DailySyncIntervalMinutes);
+ var hourly = FormatInterval(p.HourlySyncIntervalMinutes);
+ var enabled = p.IsEnabled ? "Yes" : "No";
+
+ Console.WriteLine($"{p.Name,-20} {enabled,-8} {mass,-8} {daily,-8} {hourly,-8}");
+ }
+
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return 1;
+ }
+ }
+
+ private static async Task ShowPipelineAsync(
+ IServiceProvider serviceProvider,
+ string? configPath,
+ bool verbose,
+ bool quiet,
+ string name)
+ {
+ 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();
+ var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json");
+
+ if (!File.Exists(pipelinePath))
+ {
+ Console.Error.WriteLine($"Error: Pipeline '{name}' not found");
+ return 1;
+ }
+
+ try
+ {
+ var pipeline = await configFileService.LoadPipelineAsync(pipelinePath);
+
+ if (!quiet)
+ {
+ Console.WriteLine($"=== Pipeline: {pipeline.Name} ===");
+ }
+
+ Console.WriteLine($"Name: {pipeline.Name}");
+ Console.WriteLine($"Enabled: {pipeline.IsEnabled}");
+ Console.WriteLine($"Manual Only: {pipeline.IsManualOnly}");
+ Console.WriteLine();
+
+ Console.WriteLine("Sync Intervals:");
+ Console.WriteLine($" Mass: {FormatInterval(pipeline.MassSyncIntervalMinutes)}");
+ Console.WriteLine($" Daily: {FormatInterval(pipeline.DailySyncIntervalMinutes)}");
+ Console.WriteLine($" Hourly: {FormatInterval(pipeline.HourlySyncIntervalMinutes)}");
+
+ if (pipeline.Source != null)
+ {
+ Console.WriteLine();
+ Console.WriteLine("Source:");
+ Console.WriteLine($" Connection: {pipeline.Source.Connection}");
+ if (!string.IsNullOrEmpty(pipeline.Source.Query))
+ Console.WriteLine($" Query: {(pipeline.Source.Query.Length > 50 ? pipeline.Source.Query[..50] + "..." : pipeline.Source.Query)}");
+ }
+
+ if (pipeline.Destination != null)
+ {
+ Console.WriteLine();
+ Console.WriteLine("Destination:");
+ Console.WriteLine($" Table: {pipeline.Destination.Table}");
+ if (pipeline.Destination.MatchColumns.Count > 0)
+ Console.WriteLine($" Match Columns: {string.Join(", ", pipeline.Destination.MatchColumns)}");
+ }
+
+ if (verbose)
+ {
+ if (pipeline.PreScripts.Count > 0)
+ {
+ Console.WriteLine();
+ Console.WriteLine($"Pre-Scripts: {pipeline.PreScripts.Count}");
+ }
+
+ if (pipeline.Transforms.Count > 0)
+ {
+ Console.WriteLine();
+ Console.WriteLine($"Transforms: {pipeline.Transforms.Count}");
+ }
+
+ if (pipeline.PostScripts.Count > 0)
+ {
+ Console.WriteLine();
+ Console.WriteLine($"Post-Scripts: {pipeline.PostScripts.Count}");
+ }
+ }
+
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return 1;
+ }
+ }
+
+ private static async Task CreatePipelineAsync(
+ IServiceProvider serviceProvider,
+ string? configPath,
+ bool verbose,
+ bool quiet,
+ string name,
+ bool enabled,
+ bool manualOnly)
+ {
+ 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();
+ var pipelinesDir = Path.Combine(folderPath, "Pipelines");
+ var pipelinePath = Path.Combine(pipelinesDir, $"pipeline.{name}.json");
+
+ if (File.Exists(pipelinePath))
+ {
+ Console.Error.WriteLine($"Error: Pipeline '{name}' already exists");
+ return 1;
+ }
+
+ try
+ {
+ // Create Pipelines directory if it doesn't exist
+ if (!Directory.Exists(pipelinesDir))
+ {
+ Directory.CreateDirectory(pipelinesDir);
+ }
+
+ var pipeline = new EtlPipelineConfig
+ {
+ Name = name,
+ IsEnabled = enabled,
+ IsManualOnly = manualOnly,
+ Source = new SourceElement { Connection = "jde" },
+ Destination = new DestinationElement { Table = name }
+ };
+
+ await configFileService.SavePipelineAsync(pipelinePath, pipeline);
+
+ if (!quiet)
+ {
+ Console.WriteLine($"Pipeline '{name}' created successfully");
+ Console.WriteLine($"File: {pipelinePath}");
+ }
+
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return 1;
+ }
+ }
+
+ private static async Task DeletePipelineAsync(
+ IServiceProvider serviceProvider,
+ string? configPath,
+ bool verbose,
+ bool quiet,
+ string name,
+ bool force)
+ {
+ 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();
+ var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json");
+
+ if (!File.Exists(pipelinePath))
+ {
+ Console.Error.WriteLine($"Error: Pipeline '{name}' not found");
+ return 1;
+ }
+
+ if (!force)
+ {
+ Console.Write($"Delete pipeline '{name}'? [y/N] ");
+ var response = Console.ReadLine();
+ if (!string.Equals(response, "y", StringComparison.OrdinalIgnoreCase) &&
+ !string.Equals(response, "yes", StringComparison.OrdinalIgnoreCase))
+ {
+ if (!quiet)
+ Console.WriteLine("Cancelled");
+ return 0;
+ }
+ }
+
+ try
+ {
+ await configFileService.DeletePipelineFileAsync(pipelinePath);
+
+ if (!quiet)
+ {
+ Console.WriteLine($"Pipeline '{name}' deleted successfully");
+ }
+
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return 1;
+ }
+ }
+
+ private static async Task SetPipelineEnabledAsync(
+ IServiceProvider serviceProvider,
+ string? configPath,
+ bool verbose,
+ bool quiet,
+ string name,
+ bool enabled)
+ {
+ 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();
+ var pipelinePath = Path.Combine(folderPath, "Pipelines", $"pipeline.{name}.json");
+
+ if (!File.Exists(pipelinePath))
+ {
+ Console.Error.WriteLine($"Error: Pipeline '{name}' not found");
+ return 1;
+ }
+
+ try
+ {
+ var pipeline = await configFileService.LoadPipelineAsync(pipelinePath);
+ pipeline.IsEnabled = enabled;
+ await configFileService.SavePipelineAsync(pipelinePath, pipeline);
+
+ if (!quiet)
+ {
+ var action = enabled ? "enabled" : "disabled";
+ Console.WriteLine($"Pipeline '{name}' {action} successfully");
+ }
+
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return 1;
+ }
+ }
+
+ private static string FormatInterval(int? minutes)
+ {
+ if (!minutes.HasValue)
+ return "-";
+
+ if (minutes.Value >= 10080) // 7 days
+ return $"{minutes.Value / 10080}w";
+ if (minutes.Value >= 1440) // 1 day
+ return $"{minutes.Value / 1440}d";
+ if (minutes.Value >= 60)
+ return $"{minutes.Value / 60}h";
+ return $"{minutes.Value}m";
+ }
+
+ private static async Task GetConfigFolderAsync(IServiceProvider serviceProvider, string? configPath)
+ {
+ if (!string.IsNullOrEmpty(configPath))
+ {
+ if (Directory.Exists(configPath))
+ return configPath;
+ return null;
+ }
+
+ var autoDiscoveryService = serviceProvider.GetRequiredService();
+ return await autoDiscoveryService.FindConfigFolderAsync();
+ }
+}
diff --git a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Program.cs b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Program.cs
index 8fbc60a..6d1ffdf 100644
--- a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Program.cs
+++ b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/Program.cs
@@ -68,6 +68,37 @@ public static class Program
secretCommand.AddCommand(SecretCommands.CreateInitCommand(serviceProvider, configPathOption, verboseOption, quietOption));
rootCommand.AddCommand(secretCommand);
+ // Pipeline command group
+ var pipelineCommand = new Command("pipeline", "Manage ETL pipelines");
+ pipelineCommand.AddCommand(PipelineCommands.CreateListCommand(serviceProvider, configPathOption, verboseOption, quietOption));
+ pipelineCommand.AddCommand(PipelineCommands.CreateShowCommand(serviceProvider, configPathOption, verboseOption, quietOption));
+ pipelineCommand.AddCommand(PipelineCommands.CreateCreateCommand(serviceProvider, configPathOption, verboseOption, quietOption));
+ pipelineCommand.AddCommand(PipelineCommands.CreateDeleteCommand(serviceProvider, configPathOption, verboseOption, quietOption));
+ pipelineCommand.AddCommand(PipelineCommands.CreateEnableCommand(serviceProvider, configPathOption, verboseOption, quietOption));
+ pipelineCommand.AddCommand(PipelineCommands.CreateDisableCommand(serviceProvider, configPathOption, verboseOption, quietOption));
+ rootCommand.AddCommand(pipelineCommand);
+
+ // Config command group
+ var configCommand = new Command("config", "View configuration settings");
+ configCommand.AddCommand(ConfigCommands.CreateShowCommand(serviceProvider, configPathOption, verboseOption, quietOption));
+ rootCommand.AddCommand(configCommand);
+
+ // Backup command group
+ var backupCommand = new Command("backup", "Manage configuration backups");
+ backupCommand.AddCommand(BackupCommands.CreateCreateCommand(serviceProvider, configPathOption, verboseOption, quietOption));
+ backupCommand.AddCommand(BackupCommands.CreateListCommand(serviceProvider, configPathOption, verboseOption, quietOption));
+ backupCommand.AddCommand(BackupCommands.CreateRestoreCommand(serviceProvider, configPathOption, verboseOption, quietOption));
+ backupCommand.AddCommand(BackupCommands.CreateCleanupCommand(serviceProvider, configPathOption, verboseOption, quietOption));
+ rootCommand.AddCommand(backupCommand);
+
+ // Connection command group
+ var connectionCommand = new Command("connection", "Manage connection strings");
+ connectionCommand.AddCommand(ConnectionCommands.CreateListCommand(serviceProvider, configPathOption, verboseOption, quietOption));
+ connectionCommand.AddCommand(ConnectionCommands.CreateShowCommand(serviceProvider, configPathOption, verboseOption, quietOption));
+ connectionCommand.AddCommand(ConnectionCommands.CreateAddCommand(serviceProvider, configPathOption, verboseOption, quietOption));
+ connectionCommand.AddCommand(ConnectionCommands.CreateRemoveCommand(serviceProvider, configPathOption, verboseOption, quietOption));
+ rootCommand.AddCommand(connectionCommand);
+
return await rootCommand.InvokeAsync(args);
}
diff --git a/NEW/src/Utils/JdeScoping.ConfigManager.Cli/README.md b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/README.md
new file mode 100644
index 0000000..bb8edf5
--- /dev/null
+++ b/NEW/src/Utils/JdeScoping.ConfigManager.Cli/README.md
@@ -0,0 +1,273 @@
+# JdeScoping ConfigManager CLI
+
+Command-line tool for managing JDE Scoping Tool configuration files, ETL pipelines, connection strings, secrets, and backups.
+
+## Installation
+
+Build the tool from the solution:
+
+```bash
+dotnet build NEW/src/Utils/JdeScoping.ConfigManager.Cli/JdeScoping.ConfigManager.Cli.csproj
+```
+
+The executable is located at:
+- **Windows:** `bin/Debug/net10.0/jdescoping-config.exe`
+- **macOS/Linux:** `bin/Debug/net10.0/jdescoping-config`
+
+## Global Options
+
+These options apply to all commands:
+
+| Option | Alias | Description |
+|--------|-------|-------------|
+| `--config-path` | `-c` | Path to configuration folder (overrides auto-discovery) |
+| `--verbose` | `-v` | Enable verbose output |
+| `--quiet` | `-q` | Suppress non-error output |
+
+## Configuration Discovery
+
+If `--config-path` is not specified, the tool searches for configuration in this order:
+
+1. `JDESCOPING_CONFIG_PATH` environment variable
+2. Same directory as the executable
+3. `../JdeScoping.Host/` relative to executable
+4. User config directory (`~/.jdescoping` on Unix, `%LOCALAPPDATA%\JdeScoping` on Windows)
+
+## Commands Reference
+
+### Validation Commands
+
+Validate configuration files for errors and warnings.
+
+```bash
+# Validate appsettings.json
+jdescoping-config validate appsettings
+
+# Validate all pipeline files
+jdescoping-config validate pipelines
+
+# Validate all configuration files
+jdescoping-config validate all
+
+# Run runtime infrastructure validators
+jdescoping-config validate runtime
+```
+
+### Connection Testing Commands
+
+Test database connections defined in configuration.
+
+```bash
+# Test SQL Server connection
+jdescoping-config test-connection sql
+
+# Test a specific named connection
+jdescoping-config test-connection sql --name SqlServerCache
+
+# Test Oracle connection
+jdescoping-config test-connection oracle
+
+# Test all configured connections
+jdescoping-config test-connection all
+```
+
+### Secret Management Commands
+
+Manage encrypted secrets in SecureStore.
+
+```bash
+# Initialize a new SecureStore
+jdescoping-config secret init
+
+# List all secret keys
+jdescoping-config secret list
+
+# Get a secret value
+jdescoping-config secret get
+
+# Set or update a secret
+jdescoping-config secret set
+
+# Remove a secret
+jdescoping-config secret remove
+```
+
+### Pipeline Management Commands
+
+Manage ETL pipeline configurations.
+
+```bash
+# List all pipelines with status
+jdescoping-config pipeline list
+
+# Example output:
+# === Pipelines (3) ===
+# Name Enabled Mass Daily Hourly
+# -------------------- -------- -------- -------- --------
+# WorkOrder_Curr Yes 1w 1d 1h
+# LotUsage_Curr Yes 1w 1d -
+# Item No 1w - -
+
+# Show detailed pipeline configuration
+jdescoping-config pipeline show
+
+# Create a new pipeline with defaults
+jdescoping-config pipeline create
+jdescoping-config pipeline create --enabled false
+jdescoping-config pipeline create --manual-only
+
+# Delete a pipeline
+jdescoping-config pipeline delete
+jdescoping-config pipeline delete --force # Skip confirmation
+
+# Enable/disable a pipeline
+jdescoping-config pipeline enable
+jdescoping-config pipeline disable
+```
+
+### Configuration Viewing Commands
+
+View configuration settings (read-only).
+
+```bash
+# Show specific sections
+jdescoping-config config show datasync
+jdescoping-config config show dataaccess
+jdescoping-config config show auth
+jdescoping-config config show ldap
+jdescoping-config config show search
+jdescoping-config config show excelexport
+jdescoping-config config show securestore
+
+# Show connection names (no secrets)
+jdescoping-config config show connections
+
+# Show all configuration sections
+jdescoping-config config show all
+
+# Output as JSON
+jdescoping-config config show datasync --json
+jdescoping-config config show all --json
+```
+
+### Backup Management Commands
+
+Manage configuration file backups.
+
+```bash
+# Create a backup of appsettings.json
+jdescoping-config backup create
+
+# List available backups
+jdescoping-config backup list
+
+# Example output:
+# === Backups (3) ===
+# Timestamp Size Path
+# -------------------- ------------ --------------------------------
+# 2026-01-15_120000 1.2 KB appsettings.2026-01-15_120000.bak
+# 2026-01-14_090000 1.1 KB appsettings.2026-01-14_090000.bak
+
+# Restore a specific backup
+jdescoping-config backup restore
+jdescoping-config backup restore 2026-01-15_120000
+
+# Remove old backups (keep most recent N)
+jdescoping-config backup cleanup
+jdescoping-config backup cleanup --keep 5
+```
+
+### Connection String Management Commands
+
+Manage database connection strings.
+
+```bash
+# List all connections
+jdescoping-config connection list
+
+# Example output:
+# === Connections (3) ===
+# Name Provider Server/Host
+# ------------------------- --------------- ------------------------------
+# SqlServerCache SqlServer localhost
+# JdeOracle Oracle jde-prod-server
+# CmsSybase Generic (raw)
+
+# Show connection details (password masked)
+jdescoping-config connection show
+jdescoping-config connection show SqlServerCache
+
+# Add a new SQL Server connection
+jdescoping-config connection add MyConnection \
+ --provider SqlServer \
+ --server localhost \
+ --database MyDatabase \
+ --user sa \
+ --password secret123 \
+ --port 1434
+
+# Add a new Oracle connection
+jdescoping-config connection add JdeOracle \
+ --provider Oracle \
+ --server oracle-host \
+ --service-name JDEPRD \
+ --port 1521 \
+ --user jdeuser
+
+# Add a Generic connection (raw connection string)
+jdescoping-config connection add CustomDb \
+ --provider Generic \
+ --raw "Driver={...};Server=...;Database=..."
+
+# Remove a connection
+jdescoping-config connection remove
+```
+
+## Exit Codes
+
+| Code | Description |
+|------|-------------|
+| 0 | Success |
+| 1 | Error (validation failure, missing file, operation error) |
+
+## Examples
+
+### Validate and test before deployment
+
+```bash
+# Validate all configuration
+jdescoping-config validate all
+
+# Test all database connections
+jdescoping-config test-connection all
+
+# Create a backup before making changes
+jdescoping-config backup create
+```
+
+### Quick status check
+
+```bash
+# View pipelines
+jdescoping-config pipeline list --quiet
+
+# View connections
+jdescoping-config connection list --quiet
+```
+
+### Scripted configuration
+
+```bash
+# Disable a pipeline for maintenance
+jdescoping-config pipeline disable WorkOrder_Curr
+
+# Re-enable after maintenance
+jdescoping-config pipeline enable WorkOrder_Curr
+```
+
+### Export configuration as JSON
+
+```bash
+# Export all config to JSON for review
+jdescoping-config config show all --json > config-snapshot.json
+```
diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/BackupCommandsTests.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/BackupCommandsTests.cs
new file mode 100644
index 0000000..2fafe76
--- /dev/null
+++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/BackupCommandsTests.cs
@@ -0,0 +1,298 @@
+using System.CommandLine;
+using JdeScoping.ConfigManager.Cli.Commands;
+using JdeScoping.ConfigManager.Core.Services;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace JdeScoping.ConfigManager.Cli.Tests.Commands;
+
+public class BackupCommandsTests
+{
+ private readonly IServiceProvider _serviceProvider;
+ private readonly IBackupService _backupService;
+ private readonly IAutoDiscoveryService _autoDiscoveryService;
+ private readonly Option _configPathOption;
+ private readonly Option _verboseOption;
+ private readonly Option _quietOption;
+
+ public BackupCommandsTests()
+ {
+ _backupService = Substitute.For();
+ _autoDiscoveryService = Substitute.For();
+
+ var services = new ServiceCollection();
+ services.AddSingleton(_backupService);
+ services.AddSingleton(_autoDiscoveryService);
+ _serviceProvider = services.BuildServiceProvider();
+
+ _configPathOption = new Option(["--config-path", "-c"]);
+ _verboseOption = new Option(["--verbose", "-v"]);
+ _quietOption = new Option(["--quiet", "-q"]);
+ }
+
+ [Fact]
+ public void CreateCreateCommand_ReturnsCommand()
+ {
+ // Act
+ var command = BackupCommands.CreateCreateCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Assert
+ command.ShouldNotBeNull();
+ command.Name.ShouldBe("create");
+ }
+
+ [Fact]
+ public void CreateListCommand_ReturnsCommand()
+ {
+ // Act
+ var command = BackupCommands.CreateListCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Assert
+ command.ShouldNotBeNull();
+ command.Name.ShouldBe("list");
+ }
+
+ [Fact]
+ public void CreateRestoreCommand_ReturnsCommandWithTimestampArgument()
+ {
+ // Act
+ var command = BackupCommands.CreateRestoreCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Assert
+ command.ShouldNotBeNull();
+ command.Name.ShouldBe("restore");
+ command.Arguments.ShouldContain(a => a.Name == "timestamp");
+ }
+
+ [Fact]
+ public void CreateCleanupCommand_ReturnsCommandWithKeepOption()
+ {
+ // Act
+ var command = BackupCommands.CreateCleanupCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Assert
+ command.ShouldNotBeNull();
+ command.Name.ShouldBe("cleanup");
+ command.Options.ShouldContain(o => o.Name == "keep");
+ }
+
+ [Fact]
+ public async Task CreateCommand_WithNoConfigFolder_ReturnsError()
+ {
+ // Arrange
+ _autoDiscoveryService.FindConfigFolderAsync(Arg.Any())
+ .Returns(Task.FromResult(null));
+
+ var command = BackupCommands.CreateCreateCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+ var rootCommand = new RootCommand { command };
+ rootCommand.AddGlobalOption(_configPathOption);
+ rootCommand.AddGlobalOption(_verboseOption);
+ rootCommand.AddGlobalOption(_quietOption);
+
+ // Act
+ var exitCode = await rootCommand.InvokeAsync(["create"]);
+
+ // Assert - command completes without throwing
+ command.ShouldNotBeNull();
+ }
+
+ [Fact]
+ public async Task CreateCommand_WithValidConfig_CreatesBackup()
+ {
+ // Arrange
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+ var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
+ File.WriteAllText(appSettingsPath, "{}");
+ var backupPath = Path.Combine(tempDir, "appsettings.2026-01-15_120000.bak");
+
+ try
+ {
+ _autoDiscoveryService.FindConfigFolderAsync(Arg.Any())
+ .Returns(Task.FromResult(tempDir));
+
+ _backupService.CreateBackupAsync(appSettingsPath, Arg.Any())
+ .Returns(Task.FromResult(backupPath));
+
+ var command = BackupCommands.CreateCreateCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+ var rootCommand = new RootCommand { command };
+ rootCommand.AddGlobalOption(_configPathOption);
+ rootCommand.AddGlobalOption(_verboseOption);
+ rootCommand.AddGlobalOption(_quietOption);
+
+ // Capture console output
+ var originalOut = Console.Out;
+ using var writer = new StringWriter();
+ Console.SetOut(writer);
+
+ try
+ {
+ // Act
+ await rootCommand.InvokeAsync(["create"]);
+
+ // Assert
+ var output = writer.ToString();
+ output.ShouldContain("Backup created successfully");
+ await _backupService.Received(1).CreateBackupAsync(appSettingsPath, Arg.Any());
+ }
+ finally
+ {
+ Console.SetOut(originalOut);
+ }
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void ListCommand_HasCorrectDescription()
+ {
+ // Arrange & Act
+ var command = BackupCommands.CreateListCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Assert
+ command.Description.ShouldBe("List available backups");
+ }
+
+ [Fact]
+ public async Task ListCommand_WithNoBackups_ShowsEmptyMessage()
+ {
+ // Arrange
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+ var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
+ File.WriteAllText(appSettingsPath, "{}");
+
+ try
+ {
+ _autoDiscoveryService.FindConfigFolderAsync(Arg.Any())
+ .Returns(Task.FromResult(tempDir));
+
+ _backupService.GetBackupsAsync(appSettingsPath, Arg.Any())
+ .Returns(Task.FromResult>(new List()));
+
+ var command = BackupCommands.CreateListCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+ var rootCommand = new RootCommand { command };
+ rootCommand.AddGlobalOption(_configPathOption);
+ rootCommand.AddGlobalOption(_verboseOption);
+ rootCommand.AddGlobalOption(_quietOption);
+
+ // Capture console output
+ var originalOut = Console.Out;
+ using var writer = new StringWriter();
+ Console.SetOut(writer);
+
+ try
+ {
+ // Act
+ await rootCommand.InvokeAsync(["list"]);
+
+ // Assert
+ var output = writer.ToString();
+ output.ShouldContain("No backups found");
+ }
+ finally
+ {
+ Console.SetOut(originalOut);
+ }
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public async Task CleanupCommand_CallsCleanupService()
+ {
+ // Arrange
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+ var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
+ File.WriteAllText(appSettingsPath, "{}");
+
+ try
+ {
+ _autoDiscoveryService.FindConfigFolderAsync(Arg.Any())
+ .Returns(Task.FromResult(tempDir));
+
+ _backupService.GetBackupsAsync(appSettingsPath, Arg.Any())
+ .Returns(Task.FromResult>(new List()));
+
+ var command = BackupCommands.CreateCleanupCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+ var rootCommand = new RootCommand { command };
+ rootCommand.AddGlobalOption(_configPathOption);
+ rootCommand.AddGlobalOption(_verboseOption);
+ rootCommand.AddGlobalOption(_quietOption);
+
+ // Act
+ await rootCommand.InvokeAsync(["cleanup", "--keep", "5"]);
+
+ // Assert
+ await _backupService.Received(1).CleanupOldBackupsAsync(appSettingsPath, 5, Arg.Any());
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public async Task RestoreCommand_WithNonExistentBackup_ReturnsError()
+ {
+ // Arrange
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+ var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
+ File.WriteAllText(appSettingsPath, "{}");
+
+ try
+ {
+ _autoDiscoveryService.FindConfigFolderAsync(Arg.Any())
+ .Returns(Task.FromResult(tempDir));
+
+ _backupService.GetBackupsAsync(appSettingsPath, Arg.Any())
+ .Returns(Task.FromResult>(new List()));
+
+ var command = BackupCommands.CreateRestoreCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+ var rootCommand = new RootCommand { command };
+ rootCommand.AddGlobalOption(_configPathOption);
+ rootCommand.AddGlobalOption(_verboseOption);
+ rootCommand.AddGlobalOption(_quietOption);
+
+ // Capture console error output
+ var originalErr = Console.Error;
+ using var writer = new StringWriter();
+ Console.SetError(writer);
+
+ try
+ {
+ // Act
+ await rootCommand.InvokeAsync(["restore", "2026-01-99_999999"]);
+
+ // Assert
+ var output = writer.ToString();
+ output.ShouldContain("not found");
+ }
+ finally
+ {
+ Console.SetError(originalErr);
+ }
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+}
diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConfigCommandsTests.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConfigCommandsTests.cs
new file mode 100644
index 0000000..f944275
--- /dev/null
+++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConfigCommandsTests.cs
@@ -0,0 +1,183 @@
+using System.CommandLine;
+using JdeScoping.ConfigManager.Cli.Commands;
+using JdeScoping.ConfigManager.Core.Models;
+using JdeScoping.ConfigManager.Core.Services;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace JdeScoping.ConfigManager.Cli.Tests.Commands;
+
+public class ConfigCommandsTests
+{
+ private readonly IServiceProvider _serviceProvider;
+ private readonly IConfigFileService _configFileService;
+ private readonly IAutoDiscoveryService _autoDiscoveryService;
+ private readonly Option _configPathOption;
+ private readonly Option _verboseOption;
+ private readonly Option _quietOption;
+
+ public ConfigCommandsTests()
+ {
+ _configFileService = Substitute.For();
+ _autoDiscoveryService = Substitute.For();
+
+ var services = new ServiceCollection();
+ services.AddSingleton(_configFileService);
+ services.AddSingleton(_autoDiscoveryService);
+ _serviceProvider = services.BuildServiceProvider();
+
+ _configPathOption = new Option(["--config-path", "-c"]);
+ _verboseOption = new Option(["--verbose", "-v"]);
+ _quietOption = new Option(["--quiet", "-q"]);
+ }
+
+ [Fact]
+ public void CreateShowCommand_ReturnsCommandWithSubcommands()
+ {
+ // Act
+ var command = ConfigCommands.CreateShowCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Assert
+ command.ShouldNotBeNull();
+ command.Name.ShouldBe("show");
+ command.Subcommands.ShouldContain(c => c.Name == "datasync");
+ command.Subcommands.ShouldContain(c => c.Name == "dataaccess");
+ command.Subcommands.ShouldContain(c => c.Name == "auth");
+ command.Subcommands.ShouldContain(c => c.Name == "ldap");
+ command.Subcommands.ShouldContain(c => c.Name == "search");
+ command.Subcommands.ShouldContain(c => c.Name == "excelexport");
+ command.Subcommands.ShouldContain(c => c.Name == "securestore");
+ command.Subcommands.ShouldContain(c => c.Name == "connections");
+ command.Subcommands.ShouldContain(c => c.Name == "all");
+ }
+
+ [Fact]
+ public void CreateShowCommand_HasJsonOption()
+ {
+ // Act
+ var command = ConfigCommands.CreateShowCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Assert
+ command.Options.ShouldContain(o => o.Name == "json");
+ }
+
+ [Fact]
+ public async Task ShowDataSyncCommand_WithNoConfigFolder_ReturnsError()
+ {
+ // Arrange
+ _autoDiscoveryService.FindConfigFolderAsync(Arg.Any())
+ .Returns(Task.FromResult(null));
+
+ var command = ConfigCommands.CreateShowCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+ var rootCommand = new RootCommand { command };
+ rootCommand.AddGlobalOption(_configPathOption);
+ rootCommand.AddGlobalOption(_verboseOption);
+ rootCommand.AddGlobalOption(_quietOption);
+
+ // Act
+ var exitCode = await rootCommand.InvokeAsync(["show", "datasync"]);
+
+ // Assert - command completes without throwing
+ command.ShouldNotBeNull();
+ }
+
+ [Fact]
+ public void DataSyncSubcommand_Exists()
+ {
+ // Arrange
+ var command = ConfigCommands.CreateShowCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Act
+ var datasyncSubcommand = command.Subcommands.FirstOrDefault(c => c.Name == "datasync");
+
+ // Assert
+ datasyncSubcommand.ShouldNotBeNull();
+ datasyncSubcommand.Description.ShouldBe("Show DataSync configuration");
+ }
+
+ [Fact]
+ public async Task ShowConnectionsCommand_MasksNoPasswords()
+ {
+ // Arrange
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+ var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
+ File.WriteAllText(appSettingsPath, "{}");
+
+ try
+ {
+ _autoDiscoveryService.FindConfigFolderAsync(Arg.Any())
+ .Returns(Task.FromResult(tempDir));
+
+ var config = new ConfigModel
+ {
+ ConnectionStrings = new ConnectionStringsSection
+ {
+ Entries =
+ [
+ new ConnectionStringEntry
+ {
+ Name = "TestConnection",
+ Provider = ConnectionProvider.SqlServer,
+ Server = "localhost",
+ Password = "secret123"
+ }
+ ]
+ }
+ };
+
+ _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any())
+ .Returns(Task.FromResult(config));
+
+ var command = ConfigCommands.CreateShowCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+ var rootCommand = new RootCommand { command };
+ rootCommand.AddGlobalOption(_configPathOption);
+ rootCommand.AddGlobalOption(_verboseOption);
+ rootCommand.AddGlobalOption(_quietOption);
+
+ // Capture console output
+ var originalOut = Console.Out;
+ using var writer = new StringWriter();
+ Console.SetOut(writer);
+
+ try
+ {
+ // Act
+ await rootCommand.InvokeAsync(["show", "connections"]);
+
+ // Assert
+ var output = writer.ToString();
+ output.ShouldContain("TestConnection");
+ output.ShouldNotContain("secret123"); // Password should not be visible
+ }
+ finally
+ {
+ Console.SetOut(originalOut);
+ }
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void AllSubcommand_HasJsonOption()
+ {
+ // Arrange
+ var command = ConfigCommands.CreateShowCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Act
+ var allSubcommand = command.Subcommands.FirstOrDefault(c => c.Name == "all");
+
+ // Assert
+ allSubcommand.ShouldNotBeNull();
+ // The json option is on the parent show command
+ command.Options.ShouldContain(o => o.Name == "json");
+ }
+}
diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConnectionCommandsTests.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConnectionCommandsTests.cs
new file mode 100644
index 0000000..edc8cf3
--- /dev/null
+++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/ConnectionCommandsTests.cs
@@ -0,0 +1,419 @@
+using System.CommandLine;
+using JdeScoping.ConfigManager.Cli.Commands;
+using JdeScoping.ConfigManager.Core.Models;
+using JdeScoping.ConfigManager.Core.Services;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace JdeScoping.ConfigManager.Cli.Tests.Commands;
+
+public class ConnectionCommandsTests
+{
+ private readonly IServiceProvider _serviceProvider;
+ private readonly IConfigFileService _configFileService;
+ private readonly IAutoDiscoveryService _autoDiscoveryService;
+ private readonly Option _configPathOption;
+ private readonly Option _verboseOption;
+ private readonly Option _quietOption;
+
+ public ConnectionCommandsTests()
+ {
+ _configFileService = Substitute.For();
+ _autoDiscoveryService = Substitute.For();
+
+ var services = new ServiceCollection();
+ services.AddSingleton(_configFileService);
+ services.AddSingleton(_autoDiscoveryService);
+ _serviceProvider = services.BuildServiceProvider();
+
+ _configPathOption = new Option(["--config-path", "-c"]);
+ _verboseOption = new Option(["--verbose", "-v"]);
+ _quietOption = new Option(["--quiet", "-q"]);
+ }
+
+ [Fact]
+ public void CreateListCommand_ReturnsCommand()
+ {
+ // Act
+ var command = ConnectionCommands.CreateListCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Assert
+ command.ShouldNotBeNull();
+ command.Name.ShouldBe("list");
+ }
+
+ [Fact]
+ public void CreateShowCommand_ReturnsCommandWithNameArgument()
+ {
+ // Act
+ var command = ConnectionCommands.CreateShowCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Assert
+ command.ShouldNotBeNull();
+ command.Name.ShouldBe("show");
+ command.Arguments.ShouldContain(a => a.Name == "name");
+ }
+
+ [Fact]
+ public void CreateAddCommand_ReturnsCommandWithOptions()
+ {
+ // Act
+ var command = ConnectionCommands.CreateAddCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Assert
+ command.ShouldNotBeNull();
+ command.Name.ShouldBe("add");
+ command.Arguments.ShouldContain(a => a.Name == "name");
+ command.Options.ShouldContain(o => o.Name == "provider");
+ command.Options.ShouldContain(o => o.Name == "server");
+ command.Options.ShouldContain(o => o.Name == "database");
+ command.Options.ShouldContain(o => o.Name == "user");
+ command.Options.ShouldContain(o => o.Name == "password");
+ command.Options.ShouldContain(o => o.Name == "port");
+ }
+
+ [Fact]
+ public void CreateRemoveCommand_ReturnsCommandWithNameArgument()
+ {
+ // Act
+ var command = ConnectionCommands.CreateRemoveCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Assert
+ command.ShouldNotBeNull();
+ command.Name.ShouldBe("remove");
+ command.Arguments.ShouldContain(a => a.Name == "name");
+ }
+
+ [Fact]
+ public async Task ListCommand_WithNoConfigFolder_ReturnsError()
+ {
+ // Arrange
+ _autoDiscoveryService.FindConfigFolderAsync(Arg.Any())
+ .Returns(Task.FromResult(null));
+
+ var command = ConnectionCommands.CreateListCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+ var rootCommand = new RootCommand { command };
+ rootCommand.AddGlobalOption(_configPathOption);
+ rootCommand.AddGlobalOption(_verboseOption);
+ rootCommand.AddGlobalOption(_quietOption);
+
+ // Act
+ var exitCode = await rootCommand.InvokeAsync(["list"]);
+
+ // Assert - command completes without throwing
+ command.ShouldNotBeNull();
+ }
+
+ [Fact]
+ public void ListCommand_HasCorrectDescription()
+ {
+ // Arrange & Act
+ var command = ConnectionCommands.CreateListCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Assert
+ command.Description.ShouldBe("List all connection names and providers");
+ }
+
+ [Fact]
+ public async Task ShowCommand_WithValidConnection_ShowsDetailsWithMaskedPassword()
+ {
+ // Arrange
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+ var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
+ File.WriteAllText(appSettingsPath, "{}");
+
+ try
+ {
+ _autoDiscoveryService.FindConfigFolderAsync(Arg.Any())
+ .Returns(Task.FromResult(tempDir));
+
+ var config = new ConfigModel
+ {
+ ConnectionStrings = new ConnectionStringsSection
+ {
+ Entries =
+ [
+ new ConnectionStringEntry
+ {
+ Name = "TestConnection",
+ Provider = ConnectionProvider.SqlServer,
+ Server = "localhost",
+ Database = "TestDb",
+ UserId = "sa",
+ Password = "SuperSecretPassword123!"
+ }
+ ]
+ }
+ };
+
+ _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any())
+ .Returns(Task.FromResult(config));
+
+ var command = ConnectionCommands.CreateShowCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+ var rootCommand = new RootCommand { command };
+ rootCommand.AddGlobalOption(_configPathOption);
+ rootCommand.AddGlobalOption(_verboseOption);
+ rootCommand.AddGlobalOption(_quietOption);
+
+ // Capture console output
+ var originalOut = Console.Out;
+ using var writer = new StringWriter();
+ Console.SetOut(writer);
+
+ try
+ {
+ // Act
+ await rootCommand.InvokeAsync(["show", "TestConnection"]);
+
+ // Assert
+ var output = writer.ToString();
+ output.ShouldContain("TestConnection");
+ output.ShouldContain("SqlServer");
+ output.ShouldContain("localhost");
+ output.ShouldContain("TestDb");
+ output.ShouldContain("********"); // Password should be masked
+ output.ShouldNotContain("SuperSecretPassword123!"); // Actual password should not appear
+ }
+ finally
+ {
+ Console.SetOut(originalOut);
+ }
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public async Task ShowCommand_WithNonExistentConnection_ReturnsError()
+ {
+ // Arrange
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+ var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
+ File.WriteAllText(appSettingsPath, "{}");
+
+ try
+ {
+ _autoDiscoveryService.FindConfigFolderAsync(Arg.Any())
+ .Returns(Task.FromResult(tempDir));
+
+ var config = new ConfigModel();
+
+ _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any())
+ .Returns(Task.FromResult(config));
+
+ var command = ConnectionCommands.CreateShowCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+ var rootCommand = new RootCommand { command };
+ rootCommand.AddGlobalOption(_configPathOption);
+ rootCommand.AddGlobalOption(_verboseOption);
+ rootCommand.AddGlobalOption(_quietOption);
+
+ // Capture console error output
+ var originalErr = Console.Error;
+ using var writer = new StringWriter();
+ Console.SetError(writer);
+
+ try
+ {
+ // Act
+ await rootCommand.InvokeAsync(["show", "NonExistent"]);
+
+ // Assert
+ var output = writer.ToString();
+ output.ShouldContain("not found");
+ }
+ finally
+ {
+ Console.SetError(originalErr);
+ }
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public async Task AddCommand_WithNewConnection_AddsConnection()
+ {
+ // Arrange
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+ var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
+ File.WriteAllText(appSettingsPath, "{}");
+
+ try
+ {
+ _autoDiscoveryService.FindConfigFolderAsync(Arg.Any())
+ .Returns(Task.FromResult(tempDir));
+
+ var config = new ConfigModel();
+
+ _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any())
+ .Returns(Task.FromResult(config));
+
+ var command = ConnectionCommands.CreateAddCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+ var rootCommand = new RootCommand { command };
+ rootCommand.AddGlobalOption(_configPathOption);
+ rootCommand.AddGlobalOption(_verboseOption);
+ rootCommand.AddGlobalOption(_quietOption);
+
+ // Capture console output
+ var originalOut = Console.Out;
+ using var writer = new StringWriter();
+ Console.SetOut(writer);
+
+ try
+ {
+ // Act
+ await rootCommand.InvokeAsync(["add", "NewConnection", "--server", "localhost", "--database", "TestDb"]);
+
+ // Assert
+ var output = writer.ToString();
+ output.ShouldContain("added successfully");
+ await _configFileService.Received(1).SaveAppSettingsAsync(appSettingsPath, Arg.Any(), Arg.Any());
+ }
+ finally
+ {
+ Console.SetOut(originalOut);
+ }
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public async Task AddCommand_WithExistingConnection_ReturnsError()
+ {
+ // Arrange
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+ var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
+ File.WriteAllText(appSettingsPath, "{}");
+
+ try
+ {
+ _autoDiscoveryService.FindConfigFolderAsync(Arg.Any())
+ .Returns(Task.FromResult(tempDir));
+
+ var config = new ConfigModel
+ {
+ ConnectionStrings = new ConnectionStringsSection
+ {
+ Entries =
+ [
+ new ConnectionStringEntry { Name = "ExistingConnection" }
+ ]
+ }
+ };
+
+ _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any())
+ .Returns(Task.FromResult(config));
+
+ var command = ConnectionCommands.CreateAddCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+ var rootCommand = new RootCommand { command };
+ rootCommand.AddGlobalOption(_configPathOption);
+ rootCommand.AddGlobalOption(_verboseOption);
+ rootCommand.AddGlobalOption(_quietOption);
+
+ // Capture console error output
+ var originalErr = Console.Error;
+ using var writer = new StringWriter();
+ Console.SetError(writer);
+
+ try
+ {
+ // Act
+ await rootCommand.InvokeAsync(["add", "ExistingConnection", "--server", "localhost"]);
+
+ // Assert
+ var output = writer.ToString();
+ output.ShouldContain("already exists");
+ }
+ finally
+ {
+ Console.SetError(originalErr);
+ }
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+
+ [Fact]
+ public void RemoveCommand_HasCorrectDescription()
+ {
+ // Arrange & Act
+ var command = ConnectionCommands.CreateRemoveCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Assert
+ command.Description.ShouldBe("Remove a connection");
+ }
+
+ [Fact]
+ public async Task RemoveCommand_WithNonExistentConnection_ReturnsError()
+ {
+ // Arrange
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ Directory.CreateDirectory(tempDir);
+ var appSettingsPath = Path.Combine(tempDir, "appsettings.json");
+ File.WriteAllText(appSettingsPath, "{}");
+
+ try
+ {
+ _autoDiscoveryService.FindConfigFolderAsync(Arg.Any())
+ .Returns(Task.FromResult(tempDir));
+
+ var config = new ConfigModel();
+
+ _configFileService.LoadAppSettingsAsync(appSettingsPath, Arg.Any())
+ .Returns(Task.FromResult(config));
+
+ var command = ConnectionCommands.CreateRemoveCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+ var rootCommand = new RootCommand { command };
+ rootCommand.AddGlobalOption(_configPathOption);
+ rootCommand.AddGlobalOption(_verboseOption);
+ rootCommand.AddGlobalOption(_quietOption);
+
+ // Capture console error output
+ var originalErr = Console.Error;
+ using var writer = new StringWriter();
+ Console.SetError(writer);
+
+ try
+ {
+ // Act
+ await rootCommand.InvokeAsync(["remove", "NonExistent"]);
+
+ // Assert
+ var output = writer.ToString();
+ output.ShouldContain("not found");
+ }
+ finally
+ {
+ Console.SetError(originalErr);
+ }
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+}
diff --git a/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/PipelineCommandsTests.cs b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/PipelineCommandsTests.cs
new file mode 100644
index 0000000..ae0eafb
--- /dev/null
+++ b/NEW/tests/Utils/JdeScoping.ConfigManager.Cli.Tests/Commands/PipelineCommandsTests.cs
@@ -0,0 +1,193 @@
+using System.CommandLine;
+using JdeScoping.ConfigManager.Cli.Commands;
+using JdeScoping.ConfigManager.Core.Services;
+using JdeScoping.DataSync.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace JdeScoping.ConfigManager.Cli.Tests.Commands;
+
+public class PipelineCommandsTests
+{
+ private readonly IServiceProvider _serviceProvider;
+ private readonly IConfigFileService _configFileService;
+ private readonly IAutoDiscoveryService _autoDiscoveryService;
+ private readonly Option _configPathOption;
+ private readonly Option _verboseOption;
+ private readonly Option _quietOption;
+
+ public PipelineCommandsTests()
+ {
+ _configFileService = Substitute.For();
+ _autoDiscoveryService = Substitute.For();
+
+ var services = new ServiceCollection();
+ services.AddSingleton(_configFileService);
+ services.AddSingleton(_autoDiscoveryService);
+ _serviceProvider = services.BuildServiceProvider();
+
+ _configPathOption = new Option(["--config-path", "-c"]);
+ _verboseOption = new Option(["--verbose", "-v"]);
+ _quietOption = new Option(["--quiet", "-q"]);
+ }
+
+ [Fact]
+ public void CreateListCommand_ReturnsCommand()
+ {
+ // Act
+ var command = PipelineCommands.CreateListCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Assert
+ command.ShouldNotBeNull();
+ command.Name.ShouldBe("list");
+ }
+
+ [Fact]
+ public void CreateShowCommand_ReturnsCommandWithNameArgument()
+ {
+ // Act
+ var command = PipelineCommands.CreateShowCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Assert
+ command.ShouldNotBeNull();
+ command.Name.ShouldBe("show");
+ command.Arguments.ShouldContain(a => a.Name == "name");
+ }
+
+ [Fact]
+ public void CreateCreateCommand_ReturnsCommandWithOptions()
+ {
+ // Act
+ var command = PipelineCommands.CreateCreateCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Assert
+ command.ShouldNotBeNull();
+ command.Name.ShouldBe("create");
+ command.Arguments.ShouldContain(a => a.Name == "name");
+ command.Options.ShouldContain(o => o.Name == "enabled");
+ command.Options.ShouldContain(o => o.Name == "manual-only");
+ }
+
+ [Fact]
+ public void CreateDeleteCommand_ReturnsCommandWithForceOption()
+ {
+ // Act
+ var command = PipelineCommands.CreateDeleteCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Assert
+ command.ShouldNotBeNull();
+ command.Name.ShouldBe("delete");
+ command.Arguments.ShouldContain(a => a.Name == "name");
+ command.Options.ShouldContain(o => o.Name == "force");
+ }
+
+ [Fact]
+ public void CreateEnableCommand_ReturnsCommand()
+ {
+ // Act
+ var command = PipelineCommands.CreateEnableCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Assert
+ command.ShouldNotBeNull();
+ command.Name.ShouldBe("enable");
+ command.Arguments.ShouldContain(a => a.Name == "name");
+ }
+
+ [Fact]
+ public void CreateDisableCommand_ReturnsCommand()
+ {
+ // Act
+ var command = PipelineCommands.CreateDisableCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+
+ // Assert
+ command.ShouldNotBeNull();
+ command.Name.ShouldBe("disable");
+ command.Arguments.ShouldContain(a => a.Name == "name");
+ }
+
+ [Fact]
+ public async Task ListCommand_WithNoConfigFolder_ReturnsError()
+ {
+ // Arrange
+ _autoDiscoveryService.FindConfigFolderAsync(Arg.Any())
+ .Returns(Task.FromResult(null));
+
+ var command = PipelineCommands.CreateListCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+ var rootCommand = new RootCommand { command };
+ rootCommand.AddGlobalOption(_configPathOption);
+ rootCommand.AddGlobalOption(_verboseOption);
+ rootCommand.AddGlobalOption(_quietOption);
+
+ // Act
+ var exitCode = await rootCommand.InvokeAsync(["list"]);
+
+ // Assert - exit code is set via Environment.ExitCode
+ // The command should complete without throwing
+ command.ShouldNotBeNull();
+ }
+
+ [Fact]
+ public async Task ListCommand_WithValidPipelines_ReturnsSuccess()
+ {
+ // Arrange
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ var pipelinesDir = Path.Combine(tempDir, "Pipelines");
+ Directory.CreateDirectory(pipelinesDir);
+
+ try
+ {
+ _autoDiscoveryService.FindConfigFolderAsync(Arg.Any())
+ .Returns(Task.FromResult(tempDir));
+
+ var pipelines = new Dictionary
+ {
+ ["WorkOrder_Curr"] = new EtlPipelineConfig
+ {
+ Name = "WorkOrder_Curr",
+ IsEnabled = true,
+ MassSyncIntervalMinutes = 10080,
+ DailySyncIntervalMinutes = 1440
+ }
+ };
+
+ _configFileService.LoadAllPipelinesAsync(pipelinesDir, Arg.Any())
+ .Returns(Task.FromResult(pipelines));
+
+ var command = PipelineCommands.CreateListCommand(
+ _serviceProvider, _configPathOption, _verboseOption, _quietOption);
+ var rootCommand = new RootCommand { command };
+ rootCommand.AddGlobalOption(_configPathOption);
+ rootCommand.AddGlobalOption(_verboseOption);
+ rootCommand.AddGlobalOption(_quietOption);
+
+ // Capture console output
+ var originalOut = Console.Out;
+ using var writer = new StringWriter();
+ Console.SetOut(writer);
+
+ try
+ {
+ // Act
+ await rootCommand.InvokeAsync(["list"]);
+
+ // Assert
+ var output = writer.ToString();
+ output.ShouldContain("WorkOrder_Curr");
+ }
+ finally
+ {
+ Console.SetOut(originalOut);
+ }
+ }
+ finally
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+}