# ConfigManager Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Build an Avalonia desktop application that provides a GUI for editing `appsettings.json` and `pipelines.json` configuration files.
**Architecture:** MVVM pattern with services layer. Tree view for navigation, dynamic forms for editing. File operations through abstracted `IFileSystem` for testability. Validation at schema and business-rule levels.
**Tech Stack:** Avalonia 11.2, .NET 10, Microsoft.Extensions.DependencyInjection, DiffPlex, Serilog
---
## Phase 1: Project Setup & Infrastructure
### Task 1: Create Project Structure
**Files:**
- Create: `NEW/src/Utils/JdeScoping.ConfigManager/JdeScoping.ConfigManager.csproj`
- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Program.cs`
- Create: `NEW/src/Utils/JdeScoping.ConfigManager/App.axaml`
- Create: `NEW/src/Utils/JdeScoping.ConfigManager/App.axaml.cs`
**Step 1: Create project file**
```xml
WinExe
net10.0
enable
enable
true
app.manifest
true
```
**Step 2: Create Program.cs**
```csharp
using Avalonia;
namespace JdeScoping.ConfigManager;
class Program
{
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}
```
**Step 3: Create App.axaml**
```xml
```
**Step 4: Create App.axaml.cs**
```csharp
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace JdeScoping.ConfigManager;
public partial class App : Application
{
public static IServiceProvider Services { get; private set; } = null!;
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
var services = new ServiceCollection();
ConfigureServices(services);
Services = services.BuildServiceProvider();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new Views.MainWindow();
}
base.OnFrameworkInitializationCompleted();
}
private void ConfigureServices(IServiceCollection services)
{
services.AddLogging(builder => builder
.AddConsole()
.SetMinimumLevel(LogLevel.Debug));
}
}
```
**Step 5: Create app.manifest**
```xml
```
**Step 6: Verify project builds**
Run: `dotnet build NEW/src/Utils/JdeScoping.ConfigManager/`
Expected: Build succeeded
**Step 7: Commit**
```bash
git add NEW/src/Utils/JdeScoping.ConfigManager/
git commit -m "feat(configmanager): create initial project structure"
```
---
### Task 2: Create Test Project
**Files:**
- Create: `NEW/tests/JdeScoping.ConfigManager.Tests/JdeScoping.ConfigManager.Tests.csproj`
- Create: `NEW/tests/JdeScoping.ConfigManager.Tests/GlobalUsings.cs`
**Step 1: Create test project file**
```xml
net10.0
enable
enable
false
true
```
**Step 2: Create GlobalUsings.cs**
```csharp
global using Xunit;
global using Shouldly;
global using NSubstitute;
```
**Step 3: Verify test project builds**
Run: `dotnet build NEW/tests/JdeScoping.ConfigManager.Tests/`
Expected: Build succeeded
**Step 4: Commit**
```bash
git add NEW/tests/JdeScoping.ConfigManager.Tests/
git commit -m "feat(configmanager): add test project"
```
---
### Task 3: Create IFileSystem Abstraction
**Files:**
- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Services/IFileSystem.cs`
- Create: `NEW/src/Utils/JdeScoping.ConfigManager/Services/FileSystem.cs`
- Create: `NEW/tests/JdeScoping.ConfigManager.Tests/Services/FileSystemTests.cs`
**Step 1: Write the failing test**
```csharp
namespace JdeScoping.ConfigManager.Tests.Services;
public class FileSystemTests
{
[Fact]
public void FileExists_WithExistingFile_ReturnsTrue()
{
// Arrange
var sut = new FileSystem();
var tempFile = Path.GetTempFileName();
try
{
// Act
var result = sut.FileExists(tempFile);
// Assert
result.ShouldBeTrue();
}
finally
{
File.Delete(tempFile);
}
}
[Fact]
public void FileExists_WithNonExistingFile_ReturnsFalse()
{
// Arrange
var sut = new FileSystem();
// Act
var result = sut.FileExists("/nonexistent/path/file.txt");
// Assert
result.ShouldBeFalse();
}
}
```
**Step 2: Run test to verify it fails**
Run: `dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "FileSystemTests"`
Expected: FAIL with "FileSystem not found"
**Step 3: Create IFileSystem interface**
```csharp
namespace JdeScoping.ConfigManager.Services;
///
/// Abstraction for file system operations to enable testing.
///
public interface IFileSystem
{
bool FileExists(string path);
bool DirectoryExists(string path);
Task ReadAllTextAsync(string path, CancellationToken ct = default);
Task WriteAllTextAsync(string path, string content, CancellationToken ct = default);
Task GetFilesAsync(string directory, string pattern, CancellationToken ct = default);
Task CopyFileAsync(string source, string destination, CancellationToken ct = default);
Task DeleteFileAsync(string path, CancellationToken ct = default);
string GetDirectoryName(string path);
string GetFileName(string path);
string GetFileNameWithoutExtension(string path);
string Combine(params string[] paths);
}
```
**Step 4: Create FileSystem implementation**
```csharp
namespace JdeScoping.ConfigManager.Services;
///
/// Real file system implementation.
///
public class FileSystem : IFileSystem
{
public bool FileExists(string path) => File.Exists(path);
public bool DirectoryExists(string path) => Directory.Exists(path);
public async Task ReadAllTextAsync(string path, CancellationToken ct = default)
=> await File.ReadAllTextAsync(path, ct);
public async Task WriteAllTextAsync(string path, string content, CancellationToken ct = default)
=> await File.WriteAllTextAsync(path, content, ct);
public Task GetFilesAsync(string directory, string pattern, CancellationToken ct = default)
=> Task.FromResult(Directory.GetFiles(directory, pattern));
public async Task CopyFileAsync(string source, string destination, CancellationToken ct = default)
{
var content = await File.ReadAllBytesAsync(source, ct);
await File.WriteAllBytesAsync(destination, content, ct);
}
public Task DeleteFileAsync(string path, CancellationToken ct = default)
{
File.Delete(path);
return Task.CompletedTask;
}
public string GetDirectoryName(string path) => Path.GetDirectoryName(path) ?? string.Empty;
public string GetFileName(string path) => Path.GetFileName(path);
public string GetFileNameWithoutExtension(string path) => Path.GetFileNameWithoutExtension(path);
public string Combine(params string[] paths) => Path.Combine(paths);
}
```
**Step 5: Run test to verify it passes**
Run: `dotnet test NEW/tests/JdeScoping.ConfigManager.Tests/ --filter "FileSystemTests"`
Expected: PASS
**Step 6: Commit**
```bash
git add NEW/src/Utils/JdeScoping.ConfigManager/Services/
git add NEW/tests/JdeScoping.ConfigManager.Tests/Services/
git commit -m "feat(configmanager): add IFileSystem abstraction"
```
---
### Task 4: Create ViewModelBase and Command Classes
**Files:**
- Create: `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/ViewModelBase.cs`
- Create: `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/RelayCommand.cs`
- Create: `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/AsyncRelayCommand.cs`
**Step 1: Create ViewModelBase**
```csharp
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace JdeScoping.ConfigManager.ViewModels;
///
/// Base class for all view models providing INotifyPropertyChanged implementation.
///
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer.Default.Equals(field, value))
return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
```
**Step 2: Create RelayCommand**
```csharp
using System.Windows.Input;
namespace JdeScoping.ConfigManager.ViewModels;
///
/// A command implementation that delegates to action methods.
///
public class RelayCommand : ICommand
{
private readonly Action