Quellcode durchsuchen

Desktop commands and tests added

James vor 2 Monaten
Ursprung
Commit
e39aab1aae
61 geänderte Dateien mit 1419 neuen und 13 gelöschten Zeilen
  1. 0 4
      RackPeek.Domain/RackPeek.Domain.csproj
  2. 32 0
      RackPeek.Domain/Resources/Hardware/Desktop/AddDesktopUseCase.cs
  3. 17 0
      RackPeek.Domain/Resources/Hardware/Desktop/Cpu/AddDesktopCpuUseCase.cs
  4. 19 0
      RackPeek.Domain/Resources/Hardware/Desktop/Cpu/RemoveDesktopCpuUseCase.cs
  5. 19 0
      RackPeek.Domain/Resources/Hardware/Desktop/Cpu/UpdateDesktopCpuUseCase.cs
  6. 22 0
      RackPeek.Domain/Resources/Hardware/Desktop/DeleteDesktopUseCase.cs
  7. 37 0
      RackPeek.Domain/Resources/Hardware/Desktop/DescribeDesktopUseCase.cs
  8. 17 0
      RackPeek.Domain/Resources/Hardware/Desktop/Drive/AddDesktopDriveUseCase.cs
  9. 19 0
      RackPeek.Domain/Resources/Hardware/Desktop/Drive/RemoveDesktopDriveUseCase.cs
  10. 19 0
      RackPeek.Domain/Resources/Hardware/Desktop/Drive/UpdateDesktopDriveUseCase.cs
  11. 12 0
      RackPeek.Domain/Resources/Hardware/Desktop/GetDesktopUseCase.cs
  12. 12 0
      RackPeek.Domain/Resources/Hardware/Desktop/GetDesktopsUseCase.cs
  13. 17 0
      RackPeek.Domain/Resources/Hardware/Desktop/Gpu/AddDesktopGpuUseCase.cs
  14. 19 0
      RackPeek.Domain/Resources/Hardware/Desktop/Gpu/RemoveDesktopGpuUseCase.cs
  15. 19 0
      RackPeek.Domain/Resources/Hardware/Desktop/Gpu/UpdateDesktopGpuUseCase.cs
  16. 17 0
      RackPeek.Domain/Resources/Hardware/Desktop/Nic/AddDesktopNicUseCase.cs
  17. 19 0
      RackPeek.Domain/Resources/Hardware/Desktop/Nic/RemoveDesktopNicUseCase.cs
  18. 19 0
      RackPeek.Domain/Resources/Hardware/Desktop/Nic/UpdateDesktopNicUseCase.cs
  19. 21 0
      RackPeek.Domain/Resources/Hardware/Desktop/UpdateDesktopUseCase.cs
  20. 2 0
      RackPeek.Domain/Resources/Hardware/Models/Desktop.cs
  21. 1 1
      RackPeek.Domain/Resources/Hardware/Reports/DesktopHardwareReport.cs
  22. 32 0
      RackPeek/Commands/Desktop/Cpu/DesktopCpuAddCommand.cs
  23. 18 0
      RackPeek/Commands/Desktop/Cpu/DesktopCpuAddSettings.cs
  24. 24 0
      RackPeek/Commands/Desktop/Cpu/DesktopCpuRemoveCommand.cs
  25. 12 0
      RackPeek/Commands/Desktop/Cpu/DesktopCpuRemoveSettings.cs
  26. 32 0
      RackPeek/Commands/Desktop/Cpu/DesktopCpuSetCommand.cs
  27. 21 0
      RackPeek/Commands/Desktop/Cpu/DesktopCpuSetSettings.cs
  28. 24 0
      RackPeek/Commands/Desktop/DesktopAddCommand.cs
  29. 9 0
      RackPeek/Commands/Desktop/DesktopCommands.cs
  30. 24 0
      RackPeek/Commands/Desktop/DesktopDeleteCommand.cs
  31. 41 0
      RackPeek/Commands/Desktop/DesktopDescribeCommand.cs
  32. 30 0
      RackPeek/Commands/Desktop/DesktopGetByNameCommand.cs
  33. 52 0
      RackPeek/Commands/Desktop/DesktopGetCommand.cs
  34. 30 0
      RackPeek/Commands/Desktop/DesktopSetCommand.cs
  35. 31 0
      RackPeek/Commands/Desktop/Drive/DesktopDriveAddCommand.cs
  36. 15 0
      RackPeek/Commands/Desktop/Drive/DesktopDriveAddSettings.cs
  37. 24 0
      RackPeek/Commands/Desktop/Drive/DesktopDriveRemoveCommand.cs
  38. 12 0
      RackPeek/Commands/Desktop/Drive/DesktopDriveRemoveSettings.cs
  39. 31 0
      RackPeek/Commands/Desktop/Drive/DesktopDriveSetCommand.cs
  40. 18 0
      RackPeek/Commands/Desktop/Drive/DesktopDriveSetSettings.cs
  41. 31 0
      RackPeek/Commands/Desktop/Gpu/DesktopGpuAddCommand.cs
  42. 15 0
      RackPeek/Commands/Desktop/Gpu/DesktopGpuAddSettings.cs
  43. 24 0
      RackPeek/Commands/Desktop/Gpu/DesktopGpuRemoveCommand.cs
  44. 12 0
      RackPeek/Commands/Desktop/Gpu/DesktopGpuRemoveSettings.cs
  45. 31 0
      RackPeek/Commands/Desktop/Gpu/DesktopGpuSetCommand.cs
  46. 18 0
      RackPeek/Commands/Desktop/Gpu/DesktopGpuSetSettings.cs
  47. 32 0
      RackPeek/Commands/Desktop/Nic/DesktopNicAddCommand.cs
  48. 18 0
      RackPeek/Commands/Desktop/Nic/DesktopNicAddSettings.cs
  49. 24 0
      RackPeek/Commands/Desktop/Nic/DesktopNicRemoveCommand.cs
  50. 12 0
      RackPeek/Commands/Desktop/Nic/DesktopNicRemoveSettings.cs
  51. 32 0
      RackPeek/Commands/Desktop/Nic/DesktopNicSetCommand.cs
  52. 21 0
      RackPeek/Commands/Desktop/Nic/DesktopNicSetSettings.cs
  53. 109 0
      RackPeek/Program.cs
  54. 0 4
      RackPeek/RackPeek.csproj
  55. 31 0
      Tests/Hardware/Desktop/AddDesktopUseCaseTests.cs
  56. 31 0
      Tests/Hardware/Desktop/DeleteDesktopUseCaseTests.cs
  57. 47 0
      Tests/Hardware/Desktop/DescribeDesktopUseCaseTests.cs
  58. 34 0
      Tests/Hardware/Desktop/GetDesktopUseCaseTests.cs
  59. 24 0
      Tests/Hardware/Desktop/GetDesktopsUseCaseTests.cs
  60. 34 0
      Tests/Hardware/Desktop/UpdateDesktopUseCaseTests.cs
  61. 0 4
      Tests/Tests.csproj

+ 0 - 4
RackPeek.Domain/RackPeek.Domain.csproj

@@ -6,8 +6,4 @@
         <Nullable>enable</Nullable>
     </PropertyGroup>
 
-    <ItemGroup>
-      <Folder Include="Resources\Hardware\Desktop\" />
-    </ItemGroup>
-
 </Project>

+ 32 - 0
RackPeek.Domain/Resources/Hardware/Desktop/AddDesktopUseCase.cs

@@ -0,0 +1,32 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class AddDesktopUseCase
+{
+    private readonly IHardwareRepository _repository;
+
+    public AddDesktopUseCase(IHardwareRepository repository)
+    {
+        _repository = repository;
+    }
+
+    public async Task ExecuteAsync(string name)
+    {
+        var existing = await _repository.GetByNameAsync(name);
+        if (existing != null)
+            throw new InvalidOperationException($"Desktop '{name}' already exists.");
+
+        var desktop = new Models.Desktop
+        {
+            Name = name,
+            Cpus = new List<Cpu>(),
+            Drives = new List<Drive>(),
+            Nics = new List<Nic>(),
+            Gpus = new List<Gpu>(),
+            Ram = null
+        };
+
+        await _repository.AddAsync(desktop);
+    }
+}

+ 17 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Cpu/AddDesktopCpuUseCase.cs

@@ -0,0 +1,17 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class AddDesktopCpuUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, Cpu cpu)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        desktop.Cpus ??= new List<Cpu>();
+        desktop.Cpus.Add(cpu);
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 19 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Cpu/RemoveDesktopCpuUseCase.cs

@@ -0,0 +1,19 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class RemoveDesktopCpuUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, int index)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        if (desktop.Cpus == null || index < 0 || index >= desktop.Cpus.Count)
+            throw new InvalidOperationException($"CPU index {index} not found on desktop '{desktopName}'.");
+
+        desktop.Cpus.RemoveAt(index);
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 19 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Cpu/UpdateDesktopCpuUseCase.cs

@@ -0,0 +1,19 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class UpdateDesktopCpuUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, int index, Cpu updated)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        if (desktop.Cpus == null || index < 0 || index >= desktop.Cpus.Count)
+            throw new InvalidOperationException($"CPU index {index} not found on desktop '{desktopName}'.");
+
+        desktop.Cpus[index] = updated;
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 22 - 0
RackPeek.Domain/Resources/Hardware/Desktop/DeleteDesktopUseCase.cs

@@ -0,0 +1,22 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class DeleteDesktopUseCase
+{
+    private readonly IHardwareRepository _repository;
+
+    public DeleteDesktopUseCase(IHardwareRepository repository)
+    {
+        _repository = repository;
+    }
+
+    public async Task ExecuteAsync(string name)
+    {
+        var hardware = await _repository.GetByNameAsync(name);
+        if (hardware == null)
+            throw new InvalidOperationException($"Desktop '{name}' not found.");
+
+        await _repository.DeleteAsync(name);
+    }
+}

+ 37 - 0
RackPeek.Domain/Resources/Hardware/Desktop/DescribeDesktopUseCase.cs

@@ -0,0 +1,37 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public record DesktopDescription(
+    string Name,
+    string? Model,
+    int CpuCount,
+    string? RamSummary,
+    int DriveCount,
+    int NicCount,
+    int GpuCount
+);
+
+public class DescribeDesktopUseCase(IHardwareRepository repository)
+{
+    public async Task<DesktopDescription?> ExecuteAsync(string name)
+    {
+        var desktop = await repository.GetByNameAsync(name) as Models.Desktop;
+        if (desktop == null)
+            return null;
+
+        var ramSummary = desktop.Ram == null
+            ? "None"
+            : $"{desktop.Ram.Size} GB @ {desktop.Ram.Mts} MT/s";
+
+        return new DesktopDescription(
+            desktop.Name,
+            desktop.Model,
+            desktop.Cpus?.Count ?? 0,
+            ramSummary,
+            desktop.Drives?.Count ?? 0,
+            desktop.Nics?.Count ?? 0,
+            desktop.Gpus?.Count ?? 0
+        );
+    }
+}

+ 17 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Drive/AddDesktopDriveUseCase.cs

@@ -0,0 +1,17 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class AddDesktopDriveUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, Drive drive)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        desktop.Drives ??= new List<Drive>();
+        desktop.Drives.Add(drive);
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 19 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Drive/RemoveDesktopDriveUseCase.cs

@@ -0,0 +1,19 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class RemoveDesktopDriveUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, int index)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        if (desktop.Drives == null || index < 0 || index >= desktop.Drives.Count)
+            throw new InvalidOperationException($"Drive index {index} not found on desktop '{desktopName}'.");
+
+        desktop.Drives.RemoveAt(index);
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 19 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Drive/UpdateDesktopDriveUseCase.cs

@@ -0,0 +1,19 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class UpdateDesktopDriveUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, int index, Drive updated)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        if (desktop.Drives == null || index < 0 || index >= desktop.Drives.Count)
+            throw new InvalidOperationException($"Drive index {index} not found on desktop '{desktopName}'.");
+
+        desktop.Drives[index] = updated;
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 12 - 0
RackPeek.Domain/Resources/Hardware/Desktop/GetDesktopUseCase.cs

@@ -0,0 +1,12 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class GetDesktopUseCase(IHardwareRepository repository)
+{
+    public async Task<Models.Desktop?> ExecuteAsync(string name)
+    {
+        var hardware = await repository.GetByNameAsync(name);
+        return hardware as Models.Desktop;
+    }
+}

+ 12 - 0
RackPeek.Domain/Resources/Hardware/Desktop/GetDesktopsUseCase.cs

@@ -0,0 +1,12 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class GetDesktopsUseCase(IHardwareRepository repository)
+{
+    public async Task<IReadOnlyList<Models.Desktop>> ExecuteAsync()
+    {
+        var hardware = await repository.GetAllAsync();
+        return hardware.OfType<Models.Desktop>().ToList();
+    }
+}

+ 17 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Gpu/AddDesktopGpuUseCase.cs

@@ -0,0 +1,17 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class AddDesktopGpuUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, Gpu gpu)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        desktop.Gpus ??= new List<Gpu>();
+        desktop.Gpus.Add(gpu);
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 19 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Gpu/RemoveDesktopGpuUseCase.cs

@@ -0,0 +1,19 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class RemoveDesktopGpuUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, int index)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        if (desktop.Gpus == null || index < 0 || index >= desktop.Gpus.Count)
+            throw new InvalidOperationException($"GPU index {index} not found on desktop '{desktopName}'.");
+
+        desktop.Gpus.RemoveAt(index);
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 19 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Gpu/UpdateDesktopGpuUseCase.cs

@@ -0,0 +1,19 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class UpdateDesktopGpuUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, int index, Gpu updated)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        if (desktop.Gpus == null || index < 0 || index >= desktop.Gpus.Count)
+            throw new InvalidOperationException($"GPU index {index} not found on desktop '{desktopName}'.");
+
+        desktop.Gpus[index] = updated;
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 17 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Nic/AddDesktopNicUseCase.cs

@@ -0,0 +1,17 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class AddDesktopNicUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, Nic nic)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        desktop.Nics ??= new List<Nic>();
+        desktop.Nics.Add(nic);
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 19 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Nic/RemoveDesktopNicUseCase.cs

@@ -0,0 +1,19 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class RemoveDesktopNicUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, int index)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        if (desktop.Nics == null || index < 0 || index >= desktop.Nics.Count)
+            throw new InvalidOperationException($"NIC index {index} not found on desktop '{desktopName}'.");
+
+        desktop.Nics.RemoveAt(index);
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 19 - 0
RackPeek.Domain/Resources/Hardware/Desktop/Nic/UpdateDesktopNicUseCase.cs

@@ -0,0 +1,19 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class UpdateDesktopNicUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string desktopName, int index, Nic updated)
+    {
+        var desktop = await repository.GetByNameAsync(desktopName) as Models.Desktop
+                      ?? throw new InvalidOperationException($"Desktop '{desktopName}' not found.");
+
+        if (desktop.Nics == null || index < 0 || index >= desktop.Nics.Count)
+            throw new InvalidOperationException($"NIC index {index} not found on desktop '{desktopName}'.");
+
+        desktop.Nics[index] = updated;
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 21 - 0
RackPeek.Domain/Resources/Hardware/Desktop/UpdateDesktopUseCase.cs

@@ -0,0 +1,21 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktop;
+
+public class UpdateDesktopUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(
+        string name,
+        string? model = null
+    )
+    {
+        var desktop = await repository.GetByNameAsync(name) as Models.Desktop;
+        if (desktop == null)
+            throw new InvalidOperationException($"Desktop '{name}' not found.");
+
+        if (!string.IsNullOrWhiteSpace(model))
+            desktop.Model = model;
+
+        await repository.UpdateAsync(desktop);
+    }
+}

+ 2 - 0
RackPeek.Domain/Resources/Hardware/Models/Desktop.cs

@@ -7,4 +7,6 @@ public class Desktop : Hardware
     public List<Drive>? Drives { get; set; }
     public List<Nic>? Nics { get; set; }
     public List<Gpu>? Gpus { get; set; }
+    public string Model { get; set; }
+
 }

+ 1 - 1
RackPeek.Domain/Resources/Hardware/Reports/DesktopHardwareReport.cs

@@ -24,7 +24,7 @@ public class DesktopHardwareReportUseCase(IHardwareRepository repository)
     public async Task<DesktopHardwareReport> ExecuteAsync()
     {
         var hardware = await repository.GetAllAsync();
-        var desktops = hardware.OfType<Desktop>();
+        var desktops = hardware.OfType<Models.Desktop>();
 
         var rows = desktops.Select(desktop =>
         {

+ 32 - 0
RackPeek/Commands/Desktop/Cpu/DesktopCpuAddCommand.cs

@@ -0,0 +1,32 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Cpus;
+
+public class DesktopCpuAddCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopCpuAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopCpuAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddDesktopCpuUseCase>();
+
+        var cpu = new Cpu
+        {
+            Model = settings.Model,
+            Cores = settings.Cores,
+            Threads = settings.Threads
+        };
+
+        await useCase.ExecuteAsync(settings.DesktopName, cpu);
+
+        AnsiConsole.MarkupLine($"[green]CPU added to desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 18 - 0
RackPeek/Commands/Desktop/Cpu/DesktopCpuAddSettings.cs

@@ -0,0 +1,18 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Cpus;
+
+public class DesktopCpuAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandOption("--model")]
+    public string? Model { get; set; }
+
+    [CommandOption("--cores")]
+    public int? Cores { get; set; }
+
+    [CommandOption("--threads")]
+    public int? Threads { get; set; }
+}

+ 24 - 0
RackPeek/Commands/Desktop/Cpu/DesktopCpuRemoveCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Cpus;
+
+public class DesktopCpuRemoveCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopCpuRemoveSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopCpuRemoveSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<RemoveDesktopCpuUseCase>();
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index);
+
+        AnsiConsole.MarkupLine($"[green]CPU #{settings.Index} removed from desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 12 - 0
RackPeek/Commands/Desktop/Cpu/DesktopCpuRemoveSettings.cs

@@ -0,0 +1,12 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Cpus;
+
+public class DesktopCpuRemoveSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    public int Index { get; set; }
+}

+ 32 - 0
RackPeek/Commands/Desktop/Cpu/DesktopCpuSetCommand.cs

@@ -0,0 +1,32 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Cpus;
+
+public class DesktopCpuSetCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopCpuSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopCpuSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateDesktopCpuUseCase>();
+
+        var cpu = new Cpu
+        {
+            Model = settings.Model,
+            Cores = settings.Cores,
+            Threads = settings.Threads
+        };
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index, cpu);
+
+        AnsiConsole.MarkupLine($"[green]CPU #{settings.Index} updated on desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 21 - 0
RackPeek/Commands/Desktop/Cpu/DesktopCpuSetSettings.cs

@@ -0,0 +1,21 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Cpus;
+
+public class DesktopCpuSetSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    public int Index { get; set; }
+
+    [CommandOption("--model")]
+    public string? Model { get; set; }
+
+    [CommandOption("--cores")]
+    public int? Cores { get; set; }
+
+    [CommandOption("--threads")]
+    public int? Threads { get; set; }
+}

+ 24 - 0
RackPeek/Commands/Desktop/DesktopAddCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop;
+
+public class DesktopAddCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddDesktopUseCase>();
+
+        await useCase.ExecuteAsync(settings.Name);
+
+        AnsiConsole.MarkupLine($"[green]Desktop '{settings.Name}' added.[/]");
+        return 0;
+    }
+}

+ 9 - 0
RackPeek/Commands/Desktop/DesktopCommands.cs

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

+ 24 - 0
RackPeek/Commands/Desktop/DesktopDeleteCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop;
+
+public class DesktopDeleteCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DeleteDesktopUseCase>();
+
+        await useCase.ExecuteAsync(settings.Name);
+
+        AnsiConsole.MarkupLine($"[green]Desktop '{settings.Name}' deleted.[/]");
+        return 0;
+    }
+}

+ 41 - 0
RackPeek/Commands/Desktop/DesktopDescribeCommand.cs

@@ -0,0 +1,41 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop;
+
+public class DesktopDescribeCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DescribeDesktopUseCase>();
+
+        var result = await useCase.ExecuteAsync(settings.Name);
+
+        if (result == null)
+        {
+            AnsiConsole.MarkupLine($"[red]Desktop '{settings.Name}' not found.[/]");
+            return 1;
+        }
+
+        var grid = new Grid().AddColumn().AddColumn();
+
+        grid.AddRow("Name:", result.Name);
+        grid.AddRow("Model:", result.Model ?? "Unknown");
+        grid.AddRow("CPUs:", result.CpuCount.ToString());
+        grid.AddRow("RAM:", result.RamSummary ?? "None");
+        grid.AddRow("Drives:", result.DriveCount.ToString());
+        grid.AddRow("NICs:", result.NicCount.ToString());
+        grid.AddRow("GPUs:", result.GpuCount.ToString());
+
+        AnsiConsole.Write(new Panel(grid).Header("Desktop").Border(BoxBorder.Rounded));
+
+        return 0;
+    }
+}

+ 30 - 0
RackPeek/Commands/Desktop/DesktopGetByNameCommand.cs

@@ -0,0 +1,30 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop;
+
+public class DesktopGetByNameCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<GetDesktopUseCase>();
+
+        var desktop = await useCase.ExecuteAsync(settings.Name);
+
+        if (desktop == null)
+        {
+            AnsiConsole.MarkupLine($"[red]Desktop '{settings.Name}' not found.[/]");
+            return 1;
+        }
+
+        AnsiConsole.MarkupLine($"[green]{desktop.Name}[/] (Model: {desktop.Model ?? "Unknown"})");
+        return 0;
+    }
+}

+ 52 - 0
RackPeek/Commands/Desktop/DesktopGetCommand.cs

@@ -0,0 +1,52 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop;
+
+public class DesktopGetCommand(IServiceProvider provider)
+    : AsyncCommand
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<GetDesktopsUseCase>();
+
+        var desktops = await useCase.ExecuteAsync();
+
+        if (desktops.Count == 0)
+        {
+            AnsiConsole.MarkupLine("[yellow]No desktops found.[/]");
+            return 0;
+        }
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .AddColumn("Name")
+            .AddColumn("Model")
+            .AddColumn("CPUs")
+            .AddColumn("RAM")
+            .AddColumn("Drives")
+            .AddColumn("NICs")
+            .AddColumn("GPUs");
+
+        foreach (var d in desktops)
+        {
+            table.AddRow(
+                d.Name,
+                d.Model ?? "Unknown",
+                (d.Cpus?.Count ?? 0).ToString(),
+                d.Ram == null ? "None" : $"{d.Ram.Size}GB",
+                (d.Drives?.Count ?? 0).ToString(),
+                (d.Nics?.Count ?? 0).ToString(),
+                (d.Gpus?.Count ?? 0).ToString()
+            );
+        }
+
+        AnsiConsole.Write(table);
+        return 0;
+    }
+}

+ 30 - 0
RackPeek/Commands/Desktop/DesktopSetCommand.cs

@@ -0,0 +1,30 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop;
+
+public class DesktopSetSettings : DesktopNameSettings
+{
+    [CommandOption("--model")]
+    public string? Model { get; set; }
+}
+
+public class DesktopSetCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateDesktopUseCase>();
+
+        await useCase.ExecuteAsync(settings.Name, settings.Model);
+
+        AnsiConsole.MarkupLine($"[green]Desktop '{settings.Name}' updated.[/]");
+        return 0;
+    }
+}

+ 31 - 0
RackPeek/Commands/Desktop/Drive/DesktopDriveAddCommand.cs

@@ -0,0 +1,31 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Drives;
+
+public class DesktopDriveAddCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopDriveAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopDriveAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddDesktopDriveUseCase>();
+
+        var drive = new Drive
+        {
+            Type = settings.Type,
+            Size = settings.Size
+        };
+
+        await useCase.ExecuteAsync(settings.DesktopName, drive);
+
+        AnsiConsole.MarkupLine($"[green]Drive added to desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 15 - 0
RackPeek/Commands/Desktop/Drive/DesktopDriveAddSettings.cs

@@ -0,0 +1,15 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Drives;
+
+public class DesktopDriveAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandOption("--type")]
+    public string? Type { get; set; }
+
+    [CommandOption("--size")]
+    public int? Size { get; set; }
+}

+ 24 - 0
RackPeek/Commands/Desktop/Drive/DesktopDriveRemoveCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Drives;
+
+public class DesktopDriveRemoveCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopDriveRemoveSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopDriveRemoveSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<RemoveDesktopDriveUseCase>();
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index);
+
+        AnsiConsole.MarkupLine($"[green]Drive #{settings.Index} removed from desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 12 - 0
RackPeek/Commands/Desktop/Drive/DesktopDriveRemoveSettings.cs

@@ -0,0 +1,12 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Drives;
+
+public class DesktopDriveRemoveSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    public int Index { get; set; }
+}

+ 31 - 0
RackPeek/Commands/Desktop/Drive/DesktopDriveSetCommand.cs

@@ -0,0 +1,31 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Drives;
+
+public class DesktopDriveSetCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopDriveSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopDriveSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateDesktopDriveUseCase>();
+
+        var drive = new Drive
+        {
+            Type = settings.Type,
+            Size = settings.Size
+        };
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index, drive);
+
+        AnsiConsole.MarkupLine($"[green]Drive #{settings.Index} updated on desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 18 - 0
RackPeek/Commands/Desktop/Drive/DesktopDriveSetSettings.cs

@@ -0,0 +1,18 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Drives;
+
+public class DesktopDriveSetSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    public int Index { get; set; }
+
+    [CommandOption("--type")]
+    public string? Type { get; set; }
+
+    [CommandOption("--size")]
+    public int? Size { get; set; }
+}

+ 31 - 0
RackPeek/Commands/Desktop/Gpu/DesktopGpuAddCommand.cs

@@ -0,0 +1,31 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Gpu;
+
+public class DesktopGpuAddCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopGpuAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopGpuAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddDesktopGpuUseCase>();
+
+        var gpu = new Domain.Resources.Hardware.Models.Gpu
+        {
+            Model = settings.Model,
+            Vram = settings.Vram
+        };
+
+        await useCase.ExecuteAsync(settings.DesktopName, gpu);
+
+        AnsiConsole.MarkupLine($"[green]GPU added to desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 15 - 0
RackPeek/Commands/Desktop/Gpu/DesktopGpuAddSettings.cs

@@ -0,0 +1,15 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Gpu;
+
+public class DesktopGpuAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandOption("--model")]
+    public string? Model { get; set; }
+
+    [CommandOption("--vram")]
+    public int? Vram { get; set; }
+}

+ 24 - 0
RackPeek/Commands/Desktop/Gpu/DesktopGpuRemoveCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Gpu;
+
+public class DesktopGpuRemoveCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopGpuRemoveSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopGpuRemoveSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<RemoveDesktopGpuUseCase>();
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index);
+
+        AnsiConsole.MarkupLine($"[green]GPU #{settings.Index} removed from desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 12 - 0
RackPeek/Commands/Desktop/Gpu/DesktopGpuRemoveSettings.cs

@@ -0,0 +1,12 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Gpu;
+
+public class DesktopGpuRemoveSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    public int Index { get; set; }
+}

+ 31 - 0
RackPeek/Commands/Desktop/Gpu/DesktopGpuSetCommand.cs

@@ -0,0 +1,31 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Gpu;
+
+public class DesktopGpuSetCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopGpuSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopGpuSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateDesktopGpuUseCase>();
+
+        var gpu = new Domain.Resources.Hardware.Models.Gpu
+        {
+            Model = settings.Model,
+            Vram = settings.Vram
+        };
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index, gpu);
+
+        AnsiConsole.MarkupLine($"[green]GPU #{settings.Index} updated on desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 18 - 0
RackPeek/Commands/Desktop/Gpu/DesktopGpuSetSettings.cs

@@ -0,0 +1,18 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Gpu;
+
+public class DesktopGpuSetSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    public int Index { get; set; }
+
+    [CommandOption("--model")]
+    public string? Model { get; set; }
+
+    [CommandOption("--vram")]
+    public int? Vram { get; set; }
+}

+ 32 - 0
RackPeek/Commands/Desktop/Nic/DesktopNicAddCommand.cs

@@ -0,0 +1,32 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Nics;
+
+public class DesktopNicAddCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopNicAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopNicAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddDesktopNicUseCase>();
+
+        var nic = new Nic
+        {
+            Type = settings.Type,
+            Speed = settings.Speed,
+            Ports = settings.Ports
+        };
+
+        await useCase.ExecuteAsync(settings.DesktopName, nic);
+
+        AnsiConsole.MarkupLine($"[green]NIC added to desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 18 - 0
RackPeek/Commands/Desktop/Nic/DesktopNicAddSettings.cs

@@ -0,0 +1,18 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Nics;
+
+public class DesktopNicAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandOption("--type")]
+    public string? Type { get; set; }
+
+    [CommandOption("--speed")]
+    public int? Speed { get; set; }
+
+    [CommandOption("--ports")]
+    public int? Ports { get; set; }
+}

+ 24 - 0
RackPeek/Commands/Desktop/Nic/DesktopNicRemoveCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Nics;
+
+public class DesktopNicRemoveCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopNicRemoveSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopNicRemoveSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<RemoveDesktopNicUseCase>();
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index);
+
+        AnsiConsole.MarkupLine($"[green]NIC #{settings.Index} removed from desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 12 - 0
RackPeek/Commands/Desktop/Nic/DesktopNicRemoveSettings.cs

@@ -0,0 +1,12 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Nics;
+
+public class DesktopNicRemoveSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    public int Index { get; set; }
+}

+ 32 - 0
RackPeek/Commands/Desktop/Nic/DesktopNicSetCommand.cs

@@ -0,0 +1,32 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Nics;
+
+public class DesktopNicSetCommand(IServiceProvider provider)
+    : AsyncCommand<DesktopNicSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopNicSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateDesktopNicUseCase>();
+
+        var nic = new Nic
+        {
+            Type = settings.Type,
+            Speed = settings.Speed,
+            Ports = settings.Ports
+        };
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index, nic);
+
+        AnsiConsole.MarkupLine($"[green]NIC #{settings.Index} updated on desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 21 - 0
RackPeek/Commands/Desktop/Nic/DesktopNicSetSettings.cs

@@ -0,0 +1,21 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktop.Nics;
+
+public class DesktopNicSetSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    public int Index { get; set; }
+
+    [CommandOption("--type")]
+    public string? Type { get; set; }
+
+    [CommandOption("--speed")]
+    public int? Speed { get; set; }
+
+    [CommandOption("--ports")]
+    public int? Ports { get; set; }
+}

+ 109 - 0
RackPeek/Program.cs

@@ -3,6 +3,11 @@ using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using RackPeek.Commands;
 using RackPeek.Commands.AccessPoints;
+using RackPeek.Commands.Desktop;
+using RackPeek.Commands.Desktop.Cpus;
+using RackPeek.Commands.Desktop.Drives;
+using RackPeek.Commands.Desktop.Gpu;
+using RackPeek.Commands.Desktop.Nics;
 using RackPeek.Commands.Server;
 using RackPeek.Commands.Server.Cpus;
 using RackPeek.Commands.Server.Drives;
@@ -13,6 +18,7 @@ using RackPeek.Commands.Systems;
 using RackPeek.Commands.Ups;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware.AccessPoints;
+using RackPeek.Domain.Resources.Hardware.Desktop;
 using RackPeek.Domain.Resources.Hardware.Reports;
 using RackPeek.Domain.Resources.Hardware.Server;
 using RackPeek.Domain.Resources.Hardware.Server.Cpu;
@@ -219,6 +225,65 @@ public static class CliBootstrap
         services.AddScoped<UpsGetByNameCommand>();
         services.AddScoped<UpsGetCommand>();
         services.AddScoped<UpsSetCommand>();
+        
+        // Desktop use cases
+        services.AddScoped<AddDesktopUseCase>();
+        services.AddScoped<DeleteDesktopUseCase>();
+        services.AddScoped<DescribeDesktopUseCase>();
+        services.AddScoped<GetDesktopUseCase>();
+        services.AddScoped<GetDesktopsUseCase>();
+        services.AddScoped<UpdateDesktopUseCase>();
+
+// Desktop CPU use cases
+        services.AddScoped<AddDesktopCpuUseCase>();
+        services.AddScoped<UpdateDesktopCpuUseCase>();
+        services.AddScoped<RemoveDesktopCpuUseCase>();
+
+// Desktop Drive use cases
+        services.AddScoped<AddDesktopDriveUseCase>();
+        services.AddScoped<UpdateDesktopDriveUseCase>();
+        services.AddScoped<RemoveDesktopDriveUseCase>();
+
+// Desktop GPU use cases
+        services.AddScoped<AddDesktopGpuUseCase>();
+        services.AddScoped<UpdateDesktopGpuUseCase>();
+        services.AddScoped<RemoveDesktopGpuUseCase>();
+
+// Desktop NIC use cases
+        services.AddScoped<AddDesktopNicUseCase>();
+        services.AddScoped<UpdateDesktopNicUseCase>();
+        services.AddScoped<RemoveDesktopNicUseCase>();
+
+// Desktop CRUD commands
+        services.AddScoped<DesktopAddCommand>();
+        services.AddScoped<DesktopDeleteCommand>();
+        services.AddScoped<DesktopDescribeCommand>();
+        services.AddScoped<DesktopGetByNameCommand>();
+        services.AddScoped<DesktopGetCommand>();
+        services.AddScoped<DesktopSetCommand>();
+
+// Desktop CPU commands
+        services.AddScoped<DesktopCpuAddCommand>();
+        services.AddScoped<DesktopCpuSetCommand>();
+        services.AddScoped<DesktopCpuRemoveCommand>();
+
+// Desktop Drive commands
+        services.AddScoped<DesktopDriveAddCommand>();
+        services.AddScoped<DesktopDriveSetCommand>();
+        services.AddScoped<DesktopDriveRemoveCommand>();
+
+// Desktop GPU commands
+        services.AddScoped<DesktopGpuAddCommand>();
+        services.AddScoped<DesktopGpuSetCommand>();
+        services.AddScoped<DesktopGpuRemoveCommand>();
+
+// Desktop NIC commands
+        services.AddScoped<DesktopNicAddCommand>();
+        services.AddScoped<DesktopNicSetCommand>();
+        services.AddScoped<DesktopNicRemoveCommand>();
+        
+
+
 
         
 
@@ -416,6 +481,50 @@ public static class CliBootstrap
                         .WithDescription("Delete a UPS");
                 });
 
+                config.AddBranch("desktops", desktops =>
+                {
+                    // CRUD
+                    desktops.AddCommand<DesktopAddCommand>("add");
+                    desktops.AddCommand<DesktopGetCommand>("list");
+                    desktops.AddCommand<DesktopGetByNameCommand>("get");
+                    desktops.AddCommand<DesktopDescribeCommand>("describe");
+                    desktops.AddCommand<DesktopSetCommand>("set");
+                    desktops.AddCommand<DesktopDeleteCommand>("del");
+
+                    // CPU
+                    desktops.AddBranch("cpu", cpu =>
+                    {
+                        cpu.AddCommand<DesktopCpuAddCommand>("add");
+                        cpu.AddCommand<DesktopCpuSetCommand>("set");
+                        cpu.AddCommand<DesktopCpuRemoveCommand>("del");
+                    });
+
+                    // Drives
+                    desktops.AddBranch("drive", drive =>
+                    {
+                        drive.AddCommand<DesktopDriveAddCommand>("add");
+                        drive.AddCommand<DesktopDriveSetCommand>("set");
+                        drive.AddCommand<DesktopDriveRemoveCommand>("del");
+                    });
+
+                    // GPUs
+                    desktops.AddBranch("gpu", gpu =>
+                    {
+                        gpu.AddCommand<DesktopGpuAddCommand>("add");
+                        gpu.AddCommand<DesktopGpuSetCommand>("set");
+                        gpu.AddCommand<DesktopGpuRemoveCommand>("del");
+                    });
+
+                    // NICs
+                    desktops.AddBranch("nic", nic =>
+                    {
+                        nic.AddCommand<DesktopNicAddCommand>("add");
+                        nic.AddCommand<DesktopNicSetCommand>("set");
+                        nic.AddCommand<DesktopNicRemoveCommand>("del");
+                    });
+                    
+                });
+
                 
                 // ----------------------------
                 // Reports (read-only summaries)

+ 0 - 4
RackPeek/RackPeek.csproj

@@ -23,8 +23,4 @@
         <ProjectReference Include="..\RackPeek.Domain\RackPeek.Domain.csproj"/>
     </ItemGroup>
 
-    <ItemGroup>
-      <Folder Include="Commands\Desktop\" />
-    </ItemGroup>
-
 </Project>

+ 31 - 0
Tests/Hardware/Desktop/AddDesktopUseCaseTests.cs

@@ -0,0 +1,31 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+
+public class AddDesktopUseCaseTests
+{
+    [Fact]
+    public async Task Adds_New_Desktop()
+    {
+        var repo = Substitute.For<IHardwareR epository>();
+        repo.GetByNameAsync("desk1").Returns((Hardware?)null);
+
+        var useCase = new AddDesktopUseCase(repo);
+
+        await useCase.ExecuteAsync("desk1");
+
+        await repo.Received().AddAsync(Arg.Is<Desktop>(d => d.Name == "desk1"));
+    }
+
+    [Fact]
+    public async Task Throws_If_Desktop_Exists()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("desk1").Returns(new Desktop { Name = "desk1" });
+
+        var useCase = new AddDesktopUseCase(repo);
+
+        await Assert.ThrowsAsync<InvalidOperationException>(() => useCase.ExecuteAsync("desk1"));
+    }
+}

+ 31 - 0
Tests/Hardware/Desktop/DeleteDesktopUseCaseTests.cs

@@ -0,0 +1,31 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+
+public class DeleteDesktopUseCaseTests
+{
+    [Fact]
+    public async Task Deletes_Desktop()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("desk1").Returns(new Desktop { Name = "desk1" });
+
+        var useCase = new DeleteDesktopUseCase(repo);
+
+        await useCase.ExecuteAsync("desk1");
+
+        await repo.Received().DeleteAsync("desk1");
+    }
+
+    [Fact]
+    public async Task Throws_If_Not_Found()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("desk1").Returns((Hardware?)null);
+
+        var useCase = new DeleteDesktopUseCase(repo);
+
+        await Assert.ThrowsAsync<InvalidOperationException>(() => useCase.ExecuteAsync("desk1"));
+    }
+}

+ 47 - 0
Tests/Hardware/Desktop/DescribeDesktopUseCaseTests.cs

@@ -0,0 +1,47 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+
+public class DescribeDesktopUseCaseTests
+{
+    [Fact]
+    public async Task Returns_Description()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("desk1").Returns(new Desktop
+        {
+            Name = "desk1",
+            Model = "Optiplex",
+            Cpus = new() { new Cpu() },
+            Ram = new Ram { Size = 16, Mts = 2666 },
+            Drives = new() { new Drive() },
+            Nics = new() { new Nic() },
+            Gpus = new() { new Gpu() }
+        });
+
+        var useCase = new DescribeDesktopUseCase(repo);
+
+        var result = await useCase.ExecuteAsync("desk1");
+
+        Assert.NotNull(result);
+        Assert.Equal("desk1", result!.Name);
+        Assert.Equal(1, result.CpuCount);
+        Assert.Equal(1, result.DriveCount);
+        Assert.Equal(1, result.NicCount);
+        Assert.Equal(1, result.GpuCount);
+    }
+
+    [Fact]
+    public async Task Returns_Null_If_Not_Found()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("desk1").Returns((Hardware?)null);
+
+        var useCase = new DescribeDesktopUseCase(repo);
+
+        var result = await useCase.ExecuteAsync("desk1");
+
+        Assert.Null(result);
+    }
+}

+ 34 - 0
Tests/Hardware/Desktop/GetDesktopUseCaseTests.cs

@@ -0,0 +1,34 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+
+public class GetDesktopUseCaseTests
+{
+    [Fact]
+    public async Task Returns_Desktop()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("desk1").Returns(new Desktop { Name = "desk1" });
+
+        var useCase = new GetDesktopUseCase(repo);
+
+        var result = await useCase.ExecuteAsync("desk1");
+
+        Assert.NotNull(result);
+        Assert.Equal("desk1", result!.Name);
+    }
+
+    [Fact]
+    public async Task Returns_Null_If_Not_Found()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("desk1").Returns((Hardware?)null);
+
+        var useCase = new GetDesktopUseCase(repo);
+
+        var result = await useCase.ExecuteAsync("desk1");
+
+        Assert.Null(result);
+    }
+}

+ 24 - 0
Tests/Hardware/Desktop/GetDesktopsUseCaseTests.cs

@@ -0,0 +1,24 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+
+public class GetDesktopsUseCaseTests
+{
+    [Fact]
+    public async Task Returns_All_Desktops()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetAllAsync().Returns(new Hardware[]
+        {
+            new Desktop { Name = "desk1" },
+            new Desktop { Name = "desk2" }
+        });
+
+        var useCase = new GetDesktopsUseCase(repo);
+
+        var result = await useCase.ExecuteAsync();
+
+        Assert.Equal(2, result.Count);
+    }
+}

+ 34 - 0
Tests/Hardware/Desktop/UpdateDesktopUseCaseTests.cs

@@ -0,0 +1,34 @@
+using NSubstitute;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Hardware.Desktop;
+using RackPeek.Domain.Resources.Hardware.Models;
+
+public class UpdateDesktopUseCaseTests
+{
+    [Fact]
+    public async Task Updates_Model()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        var desktop = new Desktop { Name = "desk1" };
+        repo.GetByNameAsync("desk1").Returns(desktop);
+
+        var useCase = new UpdateDesktopUseCase(repo);
+
+        await useCase.ExecuteAsync("desk1", "Optiplex");
+
+        Assert.Equal("Optiplex", desktop.Model);
+        await repo.Received().UpdateAsync(desktop);
+    }
+
+    [Fact]
+    public async Task Throws_If_Not_Found()
+    {
+        var repo = Substitute.For<IHardwareRepository>();
+        repo.GetByNameAsync("desk1").Returns((Hardware?)null);
+
+        var useCase = new UpdateDesktopUseCase(repo);
+
+        await Assert.ThrowsAsync<InvalidOperationException>(() =>
+            useCase.ExecuteAsync("desk1", "Optiplex"));
+    }
+}

+ 0 - 4
Tests/Tests.csproj

@@ -24,8 +24,4 @@
         <ProjectReference Include="..\RackPeek\RackPeek.csproj"/>
     </ItemGroup>
 
-    <ItemGroup>
-      <Folder Include="Hardware\Desktop\" />
-    </ItemGroup>
-
 </Project>