Browse Source

Added System UseCases

Tim Jones 2 tháng trước cách đây
mục cha
commit
0c40204178
36 tập tin đã thay đổi với 704 bổ sung30 xóa
  1. 1 1
      RackPeek.Domain/Resources/Hardware/Switches/AddSwitchUseCase.cs
  2. 1 1
      RackPeek.Domain/Resources/Hardware/Switches/DeleteSwitchUseCase.cs
  3. 1 1
      RackPeek.Domain/Resources/Hardware/Switches/DescribeSwitchUseCase.cs
  4. 1 1
      RackPeek.Domain/Resources/Hardware/Switches/GetSwitchUseCase.cs
  5. 1 1
      RackPeek.Domain/Resources/Hardware/Switches/GetSwitchesUseCase.cs
  6. 1 1
      RackPeek.Domain/Resources/Hardware/Switches/UpdateSwitchUseCase.cs
  7. 6 1
      RackPeek.Domain/Resources/SystemResources/ISystemRepository.cs
  8. 19 0
      RackPeek.Domain/Resources/SystemResources/UseCases/AddSwitchUseCase.cs
  9. 12 0
      RackPeek.Domain/Resources/SystemResources/UseCases/DeleteSystemUseCase.cs
  10. 31 0
      RackPeek.Domain/Resources/SystemResources/UseCases/DescribeSystemUseCase.cs
  11. 9 0
      RackPeek.Domain/Resources/SystemResources/UseCases/GetSystemUseCase.cs
  12. 9 0
      RackPeek.Domain/Resources/SystemResources/UseCases/GetSystemsUseCase.cs
  13. 40 0
      RackPeek.Domain/Resources/SystemResources/UseCases/SystemReport.cs
  14. 34 0
      RackPeek.Domain/Resources/SystemResources/UseCases/UpdateSystemUseCase.cs
  15. 1 1
      RackPeek/Commands/Switches/SwitchAddCommand.cs
  16. 1 1
      RackPeek/Commands/Switches/SwitchDeleteCommand.cs
  17. 1 1
      RackPeek/Commands/Switches/SwitchDescribeCommand.cs
  18. 1 1
      RackPeek/Commands/Switches/SwitchGetByNameCommand.cs
  19. 1 1
      RackPeek/Commands/Switches/SwitchSetCommand.cs
  20. 32 0
      RackPeek/Commands/Systems/SystemAddCommand.cs
  21. 25 0
      RackPeek/Commands/Systems/SystemDeleteCommand.cs
  22. 48 0
      RackPeek/Commands/Systems/SystemDescribeCommand.cs
  23. 35 0
      RackPeek/Commands/Systems/SystemGetByNameCommand.cs
  24. 51 0
      RackPeek/Commands/Systems/SystemGetCommand.cs
  25. 8 0
      RackPeek/Commands/Systems/SystemNameSettings.cs
  26. 51 0
      RackPeek/Commands/Systems/SystemReportCommand.cs
  27. 51 0
      RackPeek/Commands/Systems/SystemSetCommand.cs
  28. 57 10
      RackPeek/Program.cs
  29. 52 2
      RackPeek/Yaml/YamlSystemRepository.cs
  30. 117 0
      Tests/EndToEnd/SystemYamlE2ETests.cs
  31. 1 1
      Tests/Hardware/Switches/AddSwitchUseCaseTests.cs
  32. 1 1
      Tests/Hardware/Switches/DeleteSwitchUseCaseTests.cs
  33. 1 1
      Tests/Hardware/Switches/DescribeSwitchUseCaseTests.cs
  34. 1 1
      Tests/Hardware/Switches/GetSwitchUseCaseTests.cs
  35. 1 1
      Tests/Hardware/Switches/GetSwitchesUseCaseTests.cs
  36. 1 1
      Tests/Hardware/Switches/UpdateSwitchUseCaseTests.cs

+ 1 - 1
RackPeek.Domain/Resources/Hardware/Switchs/AddSwitchUseCase.cs → RackPeek.Domain/Resources/Hardware/Switches/AddSwitchUseCase.cs

@@ -1,6 +1,6 @@
 using RackPeek.Domain.Resources.Hardware.Models;
 
-namespace RackPeek.Domain.Resources.Hardware.Switchs;
+namespace RackPeek.Domain.Resources.Hardware.Switches;
 
 public class AddSwitchUseCase(IHardwareRepository repository)
 {

+ 1 - 1
RackPeek.Domain/Resources/Hardware/Switchs/DeleteSwitchUseCase.cs → RackPeek.Domain/Resources/Hardware/Switches/DeleteSwitchUseCase.cs

@@ -1,6 +1,6 @@
 using RackPeek.Domain.Resources.Hardware.Models;
 
-namespace RackPeek.Domain.Resources.Hardware.Switchs;
+namespace RackPeek.Domain.Resources.Hardware.Switches;
 
 public class DeleteSwitchUseCase(IHardwareRepository repository)
 {

+ 1 - 1
RackPeek.Domain/Resources/Hardware/Switchs/DescribeSwitchUseCase.cs → RackPeek.Domain/Resources/Hardware/Switches/DescribeSwitchUseCase.cs

@@ -1,6 +1,6 @@
 using RackPeek.Domain.Resources.Hardware.Models;
 
-namespace RackPeek.Domain.Resources.Hardware.Switchs;
+namespace RackPeek.Domain.Resources.Hardware.Switches;
 
 public record SwitchDescription(
     string Name,

+ 1 - 1
RackPeek.Domain/Resources/Hardware/Switchs/GetSwitchUseCase.cs → RackPeek.Domain/Resources/Hardware/Switches/GetSwitchUseCase.cs

@@ -1,6 +1,6 @@
 using RackPeek.Domain.Resources.Hardware.Models;
 
-namespace RackPeek.Domain.Resources.Hardware.Switchs;
+namespace RackPeek.Domain.Resources.Hardware.Switches;
 
 public class GetSwitchUseCase(IHardwareRepository repository)
 {

+ 1 - 1
RackPeek.Domain/Resources/Hardware/Switchs/GetSwitchesUseCase.cs → RackPeek.Domain/Resources/Hardware/Switches/GetSwitchesUseCase.cs

@@ -1,6 +1,6 @@
 using RackPeek.Domain.Resources.Hardware.Models;
 
-namespace RackPeek.Domain.Resources.Hardware.Switchs;
+namespace RackPeek.Domain.Resources.Hardware.Switches;
 
 public class GetSwitchesUseCase(IHardwareRepository repository)
 {

+ 1 - 1
RackPeek.Domain/Resources/Hardware/Switchs/UpdateSwitchUseCase.cs → RackPeek.Domain/Resources/Hardware/Switches/UpdateSwitchUseCase.cs

@@ -1,6 +1,6 @@
 using RackPeek.Domain.Resources.Hardware.Models;
 
-namespace RackPeek.Domain.Resources.Hardware.Switchs;
+namespace RackPeek.Domain.Resources.Hardware.Switches;
 
 public class UpdateSwitchUseCase(IHardwareRepository repository)
 {

+ 6 - 1
RackPeek.Domain/Resources/SystemResources/ISystemRepository.cs

@@ -3,4 +3,9 @@ namespace RackPeek.Domain.Resources.SystemResources;
 public interface ISystemRepository
 {
     Task<IReadOnlyList<SystemResource>> GetAllAsync();
-}
+    Task AddAsync(SystemResource systemResource);
+    Task UpdateAsync(SystemResource systemResource);
+    Task DeleteAsync(string name);
+    Task<SystemResource?> GetByNameAsync(string name);
+}
+

+ 19 - 0
RackPeek.Domain/Resources/SystemResources/UseCases/AddSwitchUseCase.cs

@@ -0,0 +1,19 @@
+namespace RackPeek.Domain.Resources.SystemResources.UseCases;
+
+public class AddSystemUseCase(ISystemRepository repository)
+{
+    public async Task ExecuteAsync(string name)
+    {
+        // basic guard rails
+        var existing = await repository.GetByNameAsync(name);
+        if (existing != null)
+            throw new InvalidOperationException($"System '{name}' already exists.");
+
+        var system = new SystemResource
+        {
+            Name = name
+        };
+
+        await repository.AddAsync(system);
+    }
+}

+ 12 - 0
RackPeek.Domain/Resources/SystemResources/UseCases/DeleteSystemUseCase.cs

@@ -0,0 +1,12 @@
+namespace RackPeek.Domain.Resources.SystemResources.UseCases;
+
+public class DeleteSystemUseCase(ISystemRepository repository)
+{
+    public async Task ExecuteAsync(string name)
+    {
+        if (await repository.GetByNameAsync(name) is not SystemResource)
+            throw new InvalidOperationException($"System '{name}' not found.");
+
+        await repository.DeleteAsync(name);
+    }
+}

+ 31 - 0
RackPeek.Domain/Resources/SystemResources/UseCases/DescribeSystemUseCase.cs

@@ -0,0 +1,31 @@
+namespace RackPeek.Domain.Resources.SystemResources.UseCases;
+
+public record SystemDescription(
+    string Name,
+    string? Type,
+    string? Os,
+    int Cores,
+    int RamGb,
+    int TotalStorageGb,
+    string? RunsOn
+);
+
+public class DescribeSystemUseCase(ISystemRepository repository)
+{
+    public async Task<SystemDescription?> ExecuteAsync(string name)
+    {
+        var system = await repository.GetByNameAsync(name);
+        if (system is null)
+            return null;
+
+        return new SystemDescription(
+            system.Name,
+            system.Type,
+            system.Os,
+            system.Cores ?? 0,
+            system.Ram ?? 0,
+            system.Drives?.Sum(d => d.Size) ?? 0,
+            system.RunsOn
+        );
+    }
+}

+ 9 - 0
RackPeek.Domain/Resources/SystemResources/UseCases/GetSystemUseCase.cs

@@ -0,0 +1,9 @@
+namespace RackPeek.Domain.Resources.SystemResources.UseCases;
+
+public class GetSystemUseCase(ISystemRepository repository)
+{
+    public async Task<SystemResource?> ExecuteAsync(string name)
+    {
+        return await repository.GetByNameAsync(name);
+    }
+}

+ 9 - 0
RackPeek.Domain/Resources/SystemResources/UseCases/GetSystemsUseCase.cs

@@ -0,0 +1,9 @@
+namespace RackPeek.Domain.Resources.SystemResources.UseCases;
+
+public class GetSystemsUseCase(ISystemRepository repository)
+{
+    public async Task<IReadOnlyList<SystemResource>> ExecuteAsync()
+    {
+        return await repository.GetAllAsync();
+    }
+}

+ 40 - 0
RackPeek.Domain/Resources/SystemResources/UseCases/SystemReport.cs

@@ -0,0 +1,40 @@
+namespace RackPeek.Domain.Resources.SystemResources.UseCases;
+
+public record SystemReport(
+    IReadOnlyList<SystemHardwareRow> Systems
+);
+
+public record SystemHardwareRow(
+    string Name,
+    string? Type,
+    string? Os,
+    int Cores,
+    int RamGb,
+    int TotalStorageGb,
+    string? RunsOn
+);
+
+public class SystemReportUseCase(ISystemRepository repository)
+{
+    public async Task<SystemReport> ExecuteAsync()
+    {
+        var systems = await repository.GetAllAsync();
+
+        var rows = systems.Select(system =>
+        {
+            var totalStorage = system.Drives?.Sum(d => d.Size) ?? 0;
+
+            return new SystemHardwareRow(
+                system.Name,
+                system.Type,
+                system.Os,
+                system.Cores ?? 0,
+                system.Ram ?? 0,
+                totalStorage,
+                system.RunsOn
+            );
+        }).ToList();
+
+        return new SystemReport(rows);
+    }
+}

+ 34 - 0
RackPeek.Domain/Resources/SystemResources/UseCases/UpdateSystemUseCase.cs

@@ -0,0 +1,34 @@
+namespace RackPeek.Domain.Resources.SystemResources.UseCases;
+public class UpdateSystemUseCase(ISystemRepository repository)
+{
+    public async Task ExecuteAsync(
+        string name,
+        string? type = null,
+        string? os = null,
+        int? cores = null,
+        int? ram = null,
+        string? runsOn = null
+    )
+    {
+        var system = await repository.GetByNameAsync(name);
+        if (system is null)
+            throw new InvalidOperationException($"System '{name}' not found.");
+
+        if (!string.IsNullOrWhiteSpace(type))
+            system.Type = type;
+
+        if (!string.IsNullOrWhiteSpace(os))
+            system.Os = os;
+
+        if (cores.HasValue)
+            system.Cores = cores.Value;
+
+        if (ram.HasValue)
+            system.Ram = ram.Value;
+
+        if (!string.IsNullOrWhiteSpace(runsOn))
+            system.RunsOn = runsOn;
+
+        await repository.UpdateAsync(system);
+    }
+}

+ 1 - 1
RackPeek/Commands/Switches/SwitchAddCommand.cs

@@ -1,5 +1,5 @@
 using Microsoft.Extensions.DependencyInjection;
-using RackPeek.Domain.Resources.Hardware.Switchs;
+using RackPeek.Domain.Resources.Hardware.Switches;
 using Spectre.Console;
 using Spectre.Console.Cli;
 

+ 1 - 1
RackPeek/Commands/Switches/SwitchDeleteCommand.cs

@@ -1,5 +1,5 @@
 using Microsoft.Extensions.DependencyInjection;
-using RackPeek.Domain.Resources.Hardware.Switchs;
+using RackPeek.Domain.Resources.Hardware.Switches;
 using Spectre.Console;
 using Spectre.Console.Cli;
 

+ 1 - 1
RackPeek/Commands/Switches/SwitchDescribeCommand.cs

@@ -1,5 +1,5 @@
 using Microsoft.Extensions.DependencyInjection;
-using RackPeek.Domain.Resources.Hardware.Switchs;
+using RackPeek.Domain.Resources.Hardware.Switches;
 using Spectre.Console;
 using Spectre.Console.Cli;
 

+ 1 - 1
RackPeek/Commands/Switches/SwitchGetByNameCommand.cs

@@ -1,5 +1,5 @@
 using Microsoft.Extensions.DependencyInjection;
-using RackPeek.Domain.Resources.Hardware.Switchs;
+using RackPeek.Domain.Resources.Hardware.Switches;
 using Spectre.Console;
 using Spectre.Console.Cli;
 

+ 1 - 1
RackPeek/Commands/Switches/SwitchSetCommand.cs

@@ -1,6 +1,6 @@
 using Microsoft.Extensions.DependencyInjection;
 using RackPeek.Commands.Server;
-using RackPeek.Domain.Resources.Hardware.Switchs;
+using RackPeek.Domain.Resources.Hardware.Switches;
 using Spectre.Console;
 using Spectre.Console.Cli;
 

+ 32 - 0
RackPeek/Commands/Systems/SystemAddCommand.cs

@@ -0,0 +1,32 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.SystemResources.UseCases;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Systems;
+
+public class SystemAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<name>")] public string Name { get; set; } = default!;
+}
+
+public class SystemAddCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<SystemAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        SystemAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddSystemUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name
+        );
+
+        AnsiConsole.MarkupLine($"[green]System '{settings.Name}' added.[/]");
+        return 0;
+    }
+}

+ 25 - 0
RackPeek/Commands/Systems/SystemDeleteCommand.cs

@@ -0,0 +1,25 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.SystemResources.UseCases;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Systems;
+
+public class SystemDeleteCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<SystemNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        SystemNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DeleteSystemUseCase>();
+
+        await useCase.ExecuteAsync(settings.Name);
+
+        AnsiConsole.MarkupLine($"[green]System '{settings.Name}' deleted.[/]");
+        return 0;
+    }
+}

+ 48 - 0
RackPeek/Commands/Systems/SystemDescribeCommand.cs

@@ -0,0 +1,48 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Commands.Switches;
+using RackPeek.Domain.Resources.SystemResources.UseCases;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Systems;
+
+public class SystemDescribeCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<SystemNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        SystemNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DescribeSystemUseCase>();
+
+        var system = await useCase.ExecuteAsync(settings.Name);
+
+        if (system == null)
+        {
+            AnsiConsole.MarkupLine($"[red]System '{settings.Name}' not found.[/]");
+            return 1;
+        }
+
+        var grid = new Grid()
+            .AddColumn(new GridColumn().NoWrap())
+            .AddColumn(new GridColumn().NoWrap());
+
+        grid.AddRow("Name:", system.Name);
+        grid.AddRow("Type:", system.Type ?? "Unknown");
+        grid.AddRow("OS:", system.Os ?? "Unknown");
+        grid.AddRow("Cores:", system.Cores.ToString());
+        grid.AddRow("RAM (GB):", system.RamGb.ToString());
+        grid.AddRow("Total Storage (GB):", system.TotalStorageGb.ToString());
+        grid.AddRow("Runs On:", system.RunsOn ?? "Unknown");
+
+        AnsiConsole.Write(
+            new Panel(grid)
+                .Header("System")
+                .Border(BoxBorder.Rounded));
+
+        return 0;
+    }
+}

+ 35 - 0
RackPeek/Commands/Systems/SystemGetByNameCommand.cs

@@ -0,0 +1,35 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Commands.Switches;
+using RackPeek.Domain.Resources.SystemResources.UseCases;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Systems;
+
+public class SystemGetByNameCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<SystemNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        SystemNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DescribeSystemUseCase>();
+
+        var system = await useCase.ExecuteAsync(settings.Name);
+
+        if (system == null)
+        {
+            AnsiConsole.MarkupLine($"[red]System '{settings.Name}' not found.[/]");
+            return 1;
+        }
+
+        AnsiConsole.MarkupLine(
+            $"[green]{system.Name}[/]  Type: {system.Type ?? "Unknown"}, OS: {system.Os ?? "Unknown"}, " +
+            $"Cores: {system.Cores}, RAM: {system.RamGb}GB, Storage: {system.TotalStorageGb}GB, RunsOn: {system.RunsOn ?? "Unknown"}");
+
+        return 0;
+    }
+}

+ 51 - 0
RackPeek/Commands/Systems/SystemGetCommand.cs

@@ -0,0 +1,51 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.SystemResources.UseCases;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Systems;
+
+public class SystemGetCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<SystemReportUseCase>();
+
+        var report = await useCase.ExecuteAsync();
+
+        if (report.Systems.Count == 0)
+        {
+            AnsiConsole.MarkupLine("[yellow]No systems found.[/]");
+            return 0;
+        }
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .AddColumn("Name")
+            .AddColumn("Type")
+            .AddColumn("OS")
+            .AddColumn("Cores")
+            .AddColumn("RAM (GB)")
+            .AddColumn("Storage (GB)")
+            .AddColumn("Runs On");
+
+        foreach (var s in report.Systems)
+            table.AddRow(
+                s.Name,
+                s.Type ?? "Unknown",
+                s.Os ?? "Unknown",
+                s.Cores.ToString(),
+                s.RamGb.ToString(),
+                s.TotalStorageGb.ToString(),
+                s.RunsOn ?? "Unknown"
+            );
+
+        AnsiConsole.Write(table);
+        return 0;
+    }
+}

+ 8 - 0
RackPeek/Commands/Systems/SystemNameSettings.cs

@@ -0,0 +1,8 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Systems;
+
+public class SystemNameSettings : CommandSettings
+{
+    [CommandArgument(0, "<name>")] public string Name { get; set; } = default!;
+}

+ 51 - 0
RackPeek/Commands/Systems/SystemReportCommand.cs

@@ -0,0 +1,51 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using RackPeek.Domain.Resources.SystemResources.UseCases;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Systems;
+
+public class SystemReportCommand(
+    ILogger<SystemReportCommand> logger,
+    IServiceProvider serviceProvider
+) : AsyncCommand
+{
+    public override async Task<int> ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<SystemReportUseCase>();
+
+        var report = await useCase.ExecuteAsync();
+
+        if (report.Systems.Count == 0)
+        {
+            AnsiConsole.MarkupLine("[yellow]No systems found.[/]");
+            return 0;
+        }
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .AddColumn("Name")
+            .AddColumn("Type")
+            .AddColumn("OS")
+            .AddColumn("Cores")
+            .AddColumn("RAM (GB)")
+            .AddColumn("Storage (GB)")
+            .AddColumn("Runs On");
+
+        foreach (var s in report.Systems)
+            table.AddRow(
+                s.Name,
+                s.Type ?? "Unknown",
+                s.Os ?? "Unknown",
+                s.Cores.ToString(),
+                s.RamGb.ToString(),
+                s.TotalStorageGb.ToString(),
+                s.RunsOn ?? "Unknown"
+            );
+
+        AnsiConsole.Write(table);
+        return 0;
+    }
+}

+ 51 - 0
RackPeek/Commands/Systems/SystemSetCommand.cs

@@ -0,0 +1,51 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Commands.Server;
+using RackPeek.Domain.Resources.SystemResources.UseCases;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Systems;
+
+public class SystemSetSettings : ServerNameSettings
+{
+    [CommandOption("--type")]
+    public string? Type { get; set; }
+
+    [CommandOption("--os")]
+    public string? Os { get; set; }
+
+    [CommandOption("--cores")]
+    public int? Cores { get; set; }
+
+    [CommandOption("--ram")]
+    public int? Ram { get; set; }
+
+    [CommandOption("--runs-on")]
+    public string? RunsOn { get; set; }
+}
+
+public class SystemSetCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<SystemSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        SystemSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateSystemUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Type,
+            settings.Os,
+            settings.Cores,
+            settings.Ram,
+            settings.RunsOn
+        );
+
+        AnsiConsole.MarkupLine($"[green]System '{settings.Name}' updated.[/]");
+        return 0;
+    }
+}

+ 57 - 10
RackPeek/Program.cs

@@ -8,6 +8,7 @@ using RackPeek.Commands.Server.Drives;
 using RackPeek.Commands.Server.Gpu;
 using RackPeek.Commands.Server.Nics;
 using RackPeek.Commands.Switches;
+using RackPeek.Commands.Systems;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware.Reports;
 using RackPeek.Domain.Resources.Hardware.Server;
@@ -15,7 +16,9 @@ using RackPeek.Domain.Resources.Hardware.Server.Cpu;
 using RackPeek.Domain.Resources.Hardware.Server.Drive;
 using RackPeek.Domain.Resources.Hardware.Server.Gpu;
 using RackPeek.Domain.Resources.Hardware.Server.Nic;
-using RackPeek.Domain.Resources.Hardware.Switchs;
+using RackPeek.Domain.Resources.Hardware.Switches;
+using RackPeek.Domain.Resources.SystemResources;
+using RackPeek.Domain.Resources.SystemResources.UseCases;
 using RackPeek.Spectre;
 using RackPeek.Yaml;
 using Spectre.Console.Cli;
@@ -67,15 +70,14 @@ public static class CliBootstrap
     {
         services.AddSingleton<IConfiguration>(configuration);
 
-        // Infrastructure
-        services.AddScoped<IHardwareRepository>(_ =>
-        {
-            var collection = new YamlResourceCollection();
-            var basePath = configuration["HardwarePath"] ?? Directory.GetCurrentDirectory();
+        var collection = new YamlResourceCollection();
+        var basePath = configuration["HardwarePath"] ?? Directory.GetCurrentDirectory();
 
-            collection.LoadFiles(yamlFiles.Select(f => Path.Combine(basePath, f)));
-            return new YamlHardwareRepository(collection);
-        });
+        collection.LoadFiles(yamlFiles.Select(f => Path.Combine(basePath, f)));
+        
+        // Infrastructure
+        services.AddScoped<IHardwareRepository>(_ => new YamlHardwareRepository(collection));
+        services.AddScoped<ISystemRepository>(_ => new YamlSystemRepository(collection));
 
         // Application
         services.AddScoped<ServerHardwareReportUseCase>();
@@ -161,8 +163,27 @@ public static class CliBootstrap
         services.AddScoped<ServerGpuUpdateCommand>();
         services.AddScoped<ServerGpuRemoveCommand>();
 
+        // System use cases
+        services.AddScoped<AddSystemUseCase>();
+        services.AddScoped<DeleteSystemUseCase>();
+        services.AddScoped<DescribeSystemUseCase>();
+        services.AddScoped<GetSystemsUseCase>();
+        services.AddScoped<GetSystemUseCase>();
+        services.AddScoped<UpdateSystemUseCase>();
+        services.AddScoped<SystemReportUseCase>();
+
+
+        // System commands
+        services.AddScoped<SystemSetCommand>();
+        services.AddScoped<SystemGetCommand>();
+        services.AddScoped<SystemGetByNameCommand>();
+        services.AddScoped<SystemDescribeCommand>();
+        services.AddScoped<SystemDeleteCommand>();
+        services.AddScoped<SystemAddCommand>();
+        services.AddScoped<SystemReportCommand>();
 
-
+        
+        
         // Spectre bootstrap
         app.Configure(config =>
         {
@@ -274,6 +295,32 @@ public static class CliBootstrap
                         .WithDescription("Delete a switch");
                 });
 
+                config.AddBranch("systems", system =>
+                {
+                    system.SetDescription("Manage systems");
+
+                    system.AddCommand<SystemReportCommand>("summary")
+                        .WithDescription("Show system report");
+
+                    system.AddCommand<SystemAddCommand>("add")
+                        .WithDescription("Add a new system");
+
+                    system.AddCommand<SystemGetCommand>("list")
+                        .WithDescription("List systems");
+                    
+                    system.AddCommand<SystemGetByNameCommand>("get")
+                        .WithDescription("Get a system by name");
+
+                    system.AddCommand<SystemDescribeCommand>("describe")
+                        .WithDescription("Show detailed information about a system");
+
+                    system.AddCommand<SystemSetCommand>("set")
+                        .WithDescription("Update system properties");
+
+                    system.AddCommand<SystemDeleteCommand>("del")
+                        .WithDescription("Delete a system");
+                });
+                
                 // ----------------------------
                 // Reports (read-only summaries)
                 // ----------------------------

+ 52 - 2
RackPeek/Yaml/YamlSystemRepository.cs

@@ -2,10 +2,60 @@ using RackPeek.Domain.Resources.SystemResources;
 
 namespace RackPeek.Yaml;
 
-public class YamlSystemRepository(YamlResourceCollection resourceCollection) : ISystemRepository
+public class YamlSystemRepository(YamlResourceCollection resources) : ISystemRepository
 {
     public Task<IReadOnlyList<SystemResource>> GetAllAsync()
     {
-        return Task.FromResult(resourceCollection.SystemResources);
+        return Task.FromResult(resources.SystemResources);
+    }
+
+    public Task<SystemResource?> GetByNameAsync(string name)
+    {
+        return Task.FromResult(resources.GetByName(name) as SystemResource);
+    }
+
+    public Task AddAsync(SystemResource systemResource)
+    {
+        if (resources.SystemResources.Any(r =>
+                r.Name.Equals(systemResource.Name, StringComparison.OrdinalIgnoreCase)))
+            throw new InvalidOperationException(
+                $"System with name '{systemResource.Name}' already exists.");
+
+        // Use first file as default for new resources
+        var targetFile = resources.SourceFiles.FirstOrDefault()
+                         ?? throw new InvalidOperationException("No YAML file loaded.");
+
+        resources.Add(systemResource, targetFile);
+        resources.SaveAll();
+
+        return Task.CompletedTask;
+    }
+
+    public Task UpdateAsync(SystemResource systemResource)
+    {
+        var existing = resources.SystemResources
+            .FirstOrDefault(r => r.Name.Equals(systemResource.Name, StringComparison.OrdinalIgnoreCase));
+
+        if (existing == null)
+            throw new InvalidOperationException($"System '{systemResource.Name}' not found.");
+
+        resources.Update(systemResource);
+        resources.SaveAll();
+
+        return Task.CompletedTask;
+    }
+
+    public Task DeleteAsync(string name)
+    {
+        var existing = resources.SystemResources
+            .FirstOrDefault(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
+
+        if (existing == null)
+            throw new InvalidOperationException($"System '{name}' not found.");
+
+        resources.Delete(name);
+        resources.SaveAll();
+
+        return Task.CompletedTask;
     }
 }

+ 117 - 0
Tests/EndToEnd/SystemYamlE2ETests.cs

@@ -0,0 +1,117 @@
+using Tests.EndToEnd.Infra;
+using Xunit.Abstractions;
+
+namespace Tests.EndToEnd;
+
+[Collection("Yaml CLI tests")]
+public class SystemYamlE2ETests(TempYamlCliFixture fs, ITestOutputHelper outputHelper) : IClassFixture<TempYamlCliFixture>
+{
+    private async Task<(string, string)> ExecuteAsync(params string[] args)
+    {
+        outputHelper.WriteLine($"rpk {string.Join(" ", args)}");
+
+        var inputArgs = args.ToArray();
+        var output = await YamlCliTestHost.RunAsync(
+            inputArgs,
+            fs.Root,
+            outputHelper,
+            "config.yaml"
+        );
+
+        outputHelper.WriteLine(output);
+        
+        var yaml = await File.ReadAllTextAsync(Path.Combine(fs.Root, "config.yaml"));
+        return (output, yaml);
+    }
+    
+[Fact]
+public async Task systems_cli_workflow_test()
+{
+    // Add system
+    var (output, yaml) = await ExecuteAsync("systems", "add", "host01");
+    Assert.Equal("System 'host01' added.\n", output);
+    Assert.Equal("""
+                    resources:
+                    - kind: System
+                      type: 
+                      os: 
+                      cores: 
+                      ram: 
+                      drives: 
+                      runsOn: 
+                      name: host01
+                      tags: 
+                    
+                    """, yaml);
+
+    // Update system
+    (output, yaml) = await ExecuteAsync(
+        "systems", "set", "host01",
+        "--type", "server",
+        "--os", "ubuntu-22.04",
+        "--cores", "4",
+        "--ram", "8192",
+        "--runs-on", "hypervisor01"
+    );
+
+    Assert.Equal("System 'host01' updated.\n", output);
+    Assert.Equal("""
+                 resources:
+                 - kind: System
+                   type: server
+                   os: ubuntu-22.04
+                   cores: 4
+                   ram: 8192
+                   drives: 
+                   runsOn: hypervisor01
+                   name: host01
+                   tags: 
+                 
+                 """, yaml);
+
+    // Get system by name
+    (output, yaml) = await ExecuteAsync("systems", "get", "host01");
+    Assert.Equal("host01  Type: server, OS: ubuntu-22.04, Cores: 4, RAM: 8192GB, Storage: 0GB, \nRunsOn: hypervisor01\n", output);
+
+    // List systems
+    (output, yaml) = await ExecuteAsync("systems", "list");
+    Assert.Equal("""
+                 ╭────────┬────────┬─────────────┬───────┬──────────┬─────────────┬─────────────╮
+                 │ Name   │ Type   │ OS          │ Cores │ RAM (GB) │ Storage     │ Runs On     │
+                 │        │        │             │       │          │ (GB)        │             │
+                 ├────────┼────────┼─────────────┼───────┼──────────┼─────────────┼─────────────┤
+                 │ host01 │ server │ ubuntu-22.0 │ 4     │ 8192     │ 0           │ hypervisor0 │
+                 │        │        │ 4           │       │          │             │ 1           │
+                 ╰────────┴────────┴─────────────┴───────┴──────────┴─────────────┴─────────────╯
+                 
+                 """, output);
+
+    // Report systems
+    (output, yaml) = await ExecuteAsync("systems", "summary");
+    Assert.Equal("""
+                 ╭────────┬────────┬─────────────┬───────┬──────────┬─────────────┬─────────────╮
+                 │ Name   │ Type   │ OS          │ Cores │ RAM (GB) │ Storage     │ Runs On     │
+                 │        │        │             │       │          │ (GB)        │             │
+                 ├────────┼────────┼─────────────┼───────┼──────────┼─────────────┼─────────────┤
+                 │ host01 │ server │ ubuntu-22.0 │ 4     │ 8192     │ 0           │ hypervisor0 │
+                 │        │        │ 4           │       │          │             │ 1           │
+                 ╰────────┴────────┴─────────────┴───────┴──────────┴─────────────┴─────────────╯
+
+                 """, output);
+
+    // Delete system
+    (output, yaml) = await ExecuteAsync("systems", "del", "host01");
+    Assert.Equal("""
+                    System 'host01' deleted.
+                    
+                    """, output);
+
+    // Ensure list is empty
+    (output, yaml) = await ExecuteAsync("systems", "list");
+    Assert.Equal("""
+                 No systems found.
+                 
+                 """, output);
+}
+
+}

+ 1 - 1
Tests/Hardware/Switches/AddSwitchUseCaseTests.cs

@@ -1,7 +1,7 @@
 using NSubstitute;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware.Models;
-using RackPeek.Domain.Resources.Hardware.Switchs;
+using RackPeek.Domain.Resources.Hardware.Switches;
 
 namespace Tests.Hardware.Switches;
 

+ 1 - 1
Tests/Hardware/Switches/DeleteSwitchUseCaseTests.cs

@@ -1,7 +1,7 @@
 using NSubstitute;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware.Models;
-using RackPeek.Domain.Resources.Hardware.Switchs;
+using RackPeek.Domain.Resources.Hardware.Switches;
 
 namespace Tests.Hardware.Switches;
 

+ 1 - 1
Tests/Hardware/Switches/DescribeSwitchUseCaseTests.cs

@@ -1,7 +1,7 @@
 using NSubstitute;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware.Models;
-using RackPeek.Domain.Resources.Hardware.Switchs;
+using RackPeek.Domain.Resources.Hardware.Switches;
 
 namespace Tests.Hardware.Switches;
 

+ 1 - 1
Tests/Hardware/Switches/GetSwitchUseCaseTests.cs

@@ -1,7 +1,7 @@
 using NSubstitute;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware.Models;
-using RackPeek.Domain.Resources.Hardware.Switchs;
+using RackPeek.Domain.Resources.Hardware.Switches;
 
 namespace Tests.Hardware.Switches;
 

+ 1 - 1
Tests/Hardware/Switches/GetSwitchesUseCaseTests.cs

@@ -1,7 +1,7 @@
 using NSubstitute;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware.Models;
-using RackPeek.Domain.Resources.Hardware.Switchs;
+using RackPeek.Domain.Resources.Hardware.Switches;
 
 namespace Tests.Hardware.Switches;
 

+ 1 - 1
Tests/Hardware/Switches/UpdateSwitchUseCaseTests.cs

@@ -1,7 +1,7 @@
 using NSubstitute;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware.Models;
-using RackPeek.Domain.Resources.Hardware.Switchs;
+using RackPeek.Domain.Resources.Hardware.Switches;
 
 namespace Tests.Hardware.Switches;