Tim Jones 1 месяц назад
Родитель
Сommit
fdb5fce1ad
100 измененных файлов с 3290 добавлено и 2 удалено
  1. 6 0
      RackPeek.Domain/IConsoleEmulator.cs
  2. 2 2
      RackPeek.Domain/Persistence/Yaml/YamlResourceCollection.cs
  3. 475 0
      RackPeek.Web.Viewer/CliBootstrap.cs
  4. 11 0
      RackPeek.Web.Viewer/Commands/AccessPoints/AccessCommands.cs
  5. 33 0
      RackPeek.Web.Viewer/Commands/AccessPoints/AccessPointAddCommand.cs
  6. 25 0
      RackPeek.Web.Viewer/Commands/AccessPoints/AccessPointDeleteCommand.cs
  7. 37 0
      RackPeek.Web.Viewer/Commands/AccessPoints/AccessPointDescribeCommand.cs
  8. 27 0
      RackPeek.Web.Viewer/Commands/AccessPoints/AccessPointGetByNameCommand.cs
  9. 44 0
      RackPeek.Web.Viewer/Commands/AccessPoints/AccessPointGetCommand.cs
  10. 43 0
      RackPeek.Web.Viewer/Commands/AccessPoints/AccessPointReportCommand.cs
  11. 42 0
      RackPeek.Web.Viewer/Commands/AccessPoints/AccessPointSetCommand.cs
  12. 24 0
      RackPeek.Web.Viewer/Commands/Desktops/Cpus/DesktopCpuAddCommand.cs
  13. 23 0
      RackPeek.Web.Viewer/Commands/Desktops/Cpus/DesktopCpuAddSettings.cs
  14. 24 0
      RackPeek.Web.Viewer/Commands/Desktops/Cpus/DesktopCpuRemoveCommand.cs
  15. 15 0
      RackPeek.Web.Viewer/Commands/Desktops/Cpus/DesktopCpuRemoveSettings.cs
  16. 25 0
      RackPeek.Web.Viewer/Commands/Desktops/Cpus/DesktopCpuSetCommand.cs
  17. 27 0
      RackPeek.Web.Viewer/Commands/Desktops/Cpus/DesktopCpuSetSettings.cs
  18. 24 0
      RackPeek.Web.Viewer/Commands/Desktops/DesktopAddCommand.cs
  19. 8 0
      RackPeek.Web.Viewer/Commands/Desktops/DesktopCommands.cs
  20. 24 0
      RackPeek.Web.Viewer/Commands/Desktops/DesktopDeleteCommand.cs
  21. 35 0
      RackPeek.Web.Viewer/Commands/Desktops/DesktopDescribeCommand.cs
  22. 24 0
      RackPeek.Web.Viewer/Commands/Desktops/DesktopGetByNameCommand.cs
  23. 50 0
      RackPeek.Web.Viewer/Commands/Desktops/DesktopGetCommand.cs
  24. 51 0
      RackPeek.Web.Viewer/Commands/Desktops/DesktopReportCommand.cs
  25. 29 0
      RackPeek.Web.Viewer/Commands/Desktops/DesktopSetCommand.cs
  26. 29 0
      RackPeek.Web.Viewer/Commands/Desktops/DesktopTreeCommand.cs
  27. 24 0
      RackPeek.Web.Viewer/Commands/Desktops/Drive/DesktopDriveAddCommand.cs
  28. 19 0
      RackPeek.Web.Viewer/Commands/Desktops/Drive/DesktopDriveAddSettings.cs
  29. 24 0
      RackPeek.Web.Viewer/Commands/Desktops/Drive/DesktopDriveRemoveCommand.cs
  30. 15 0
      RackPeek.Web.Viewer/Commands/Desktops/Drive/DesktopDriveRemoveSettings.cs
  31. 24 0
      RackPeek.Web.Viewer/Commands/Desktops/Drive/DesktopDriveSetCommand.cs
  32. 23 0
      RackPeek.Web.Viewer/Commands/Desktops/Drive/DesktopDriveSetSettings.cs
  33. 24 0
      RackPeek.Web.Viewer/Commands/Desktops/Gpus/DesktopGpuAddCommand.cs
  34. 19 0
      RackPeek.Web.Viewer/Commands/Desktops/Gpus/DesktopGpuAddSettings.cs
  35. 24 0
      RackPeek.Web.Viewer/Commands/Desktops/Gpus/DesktopGpuRemoveCommand.cs
  36. 15 0
      RackPeek.Web.Viewer/Commands/Desktops/Gpus/DesktopGpuRemoveSettings.cs
  37. 24 0
      RackPeek.Web.Viewer/Commands/Desktops/Gpus/DesktopGpuSetCommand.cs
  38. 23 0
      RackPeek.Web.Viewer/Commands/Desktops/Gpus/DesktopGpuSetSettings.cs
  39. 24 0
      RackPeek.Web.Viewer/Commands/Desktops/Nics/DesktopNicAddCommand.cs
  40. 23 0
      RackPeek.Web.Viewer/Commands/Desktops/Nics/DesktopNicAddSettings.cs
  41. 24 0
      RackPeek.Web.Viewer/Commands/Desktops/Nics/DesktopNicRemoveCommand.cs
  42. 15 0
      RackPeek.Web.Viewer/Commands/Desktops/Nics/DesktopNicRemoveSettings.cs
  43. 24 0
      RackPeek.Web.Viewer/Commands/Desktops/Nics/DesktopNicSetCommand.cs
  44. 27 0
      RackPeek.Web.Viewer/Commands/Desktops/Nics/DesktopNicSetSettings.cs
  45. 32 0
      RackPeek.Web.Viewer/Commands/Firewalls/FirewallAddCommand.cs
  46. 8 0
      RackPeek.Web.Viewer/Commands/Firewalls/FirewallCommands.cs
  47. 25 0
      RackPeek.Web.Viewer/Commands/Firewalls/FirewallDeleteCommand.cs
  48. 41 0
      RackPeek.Web.Viewer/Commands/Firewalls/FirewallDescribeCommand.cs
  49. 27 0
      RackPeek.Web.Viewer/Commands/Firewalls/FirewallGetByNameCommand.cs
  50. 49 0
      RackPeek.Web.Viewer/Commands/Firewalls/FirewallGetCommand.cs
  51. 51 0
      RackPeek.Web.Viewer/Commands/Firewalls/FirewallReportCommand.cs
  52. 39 0
      RackPeek.Web.Viewer/Commands/Firewalls/FirewallSetCommand.cs
  53. 29 0
      RackPeek.Web.Viewer/Commands/Firewalls/Ports/FirewallPortAddCommand.cs
  54. 27 0
      RackPeek.Web.Viewer/Commands/Firewalls/Ports/FirewallPortRemoveCommand.cs
  55. 30 0
      RackPeek.Web.Viewer/Commands/Firewalls/Ports/FirewallPortUpdateCommand.cs
  56. 139 0
      RackPeek.Web.Viewer/Commands/GetTotalSummaryCommand.cs
  57. 24 0
      RackPeek.Web.Viewer/Commands/Laptops/Cpus/LaptopCpuAddCommand.cs
  58. 23 0
      RackPeek.Web.Viewer/Commands/Laptops/Cpus/LaptopCpuAddSettings.cs
  59. 24 0
      RackPeek.Web.Viewer/Commands/Laptops/Cpus/LaptopCpuRemoveCommand.cs
  60. 15 0
      RackPeek.Web.Viewer/Commands/Laptops/Cpus/LaptopCpuRemoveSettings.cs
  61. 25 0
      RackPeek.Web.Viewer/Commands/Laptops/Cpus/LaptopCpuSetCommand.cs
  62. 27 0
      RackPeek.Web.Viewer/Commands/Laptops/Cpus/LaptopCpuSetSettings.cs
  63. 24 0
      RackPeek.Web.Viewer/Commands/Laptops/Drive/LaptopDriveAddCommand.cs
  64. 19 0
      RackPeek.Web.Viewer/Commands/Laptops/Drive/LaptopDriveAddSettings.cs
  65. 24 0
      RackPeek.Web.Viewer/Commands/Laptops/Drive/LaptopDriveRemoveCommand.cs
  66. 15 0
      RackPeek.Web.Viewer/Commands/Laptops/Drive/LaptopDriveRemoveSettings.cs
  67. 24 0
      RackPeek.Web.Viewer/Commands/Laptops/Drive/LaptopDriveSetCommand.cs
  68. 23 0
      RackPeek.Web.Viewer/Commands/Laptops/Drive/LaptopDriveSetSettings.cs
  69. 24 0
      RackPeek.Web.Viewer/Commands/Laptops/Gpus/LaptopGpuAddCommand.cs
  70. 19 0
      RackPeek.Web.Viewer/Commands/Laptops/Gpus/LaptopGpuAddSettings.cs
  71. 24 0
      RackPeek.Web.Viewer/Commands/Laptops/Gpus/LaptopGpuRemoveCommand.cs
  72. 15 0
      RackPeek.Web.Viewer/Commands/Laptops/Gpus/LaptopGpuRemoveSettings.cs
  73. 31 0
      RackPeek.Web.Viewer/Commands/Laptops/Gpus/LaptopGpuSetCommand.cs
  74. 23 0
      RackPeek.Web.Viewer/Commands/Laptops/Gpus/LaptopGpuSetSettings.cs
  75. 24 0
      RackPeek.Web.Viewer/Commands/Laptops/LaptopAddCommand.cs
  76. 8 0
      RackPeek.Web.Viewer/Commands/Laptops/LaptopCommands.cs
  77. 24 0
      RackPeek.Web.Viewer/Commands/Laptops/LaptopDeleteCommand.cs
  78. 33 0
      RackPeek.Web.Viewer/Commands/Laptops/LaptopDescribeCommand.cs
  79. 24 0
      RackPeek.Web.Viewer/Commands/Laptops/LaptopGetByNameCommand.cs
  80. 46 0
      RackPeek.Web.Viewer/Commands/Laptops/LaptopGetCommand.cs
  81. 49 0
      RackPeek.Web.Viewer/Commands/Laptops/LaptopReportCommand.cs
  82. 29 0
      RackPeek.Web.Viewer/Commands/Laptops/LaptopTreeCommand.cs
  83. 29 0
      RackPeek.Web.Viewer/Commands/Routers/Ports/RouterPortAddCommand.cs
  84. 27 0
      RackPeek.Web.Viewer/Commands/Routers/Ports/RouterPortRemoveCommand.cs
  85. 30 0
      RackPeek.Web.Viewer/Commands/Routers/Ports/RouterPortUpdateCommand.cs
  86. 32 0
      RackPeek.Web.Viewer/Commands/Routers/RouterAddCommand.cs
  87. 8 0
      RackPeek.Web.Viewer/Commands/Routers/RouterCommands.cs
  88. 25 0
      RackPeek.Web.Viewer/Commands/Routers/RouterDeleteCommand.cs
  89. 41 0
      RackPeek.Web.Viewer/Commands/Routers/RouterDescribeCommand.cs
  90. 27 0
      RackPeek.Web.Viewer/Commands/Routers/RouterGetByNameCommand.cs
  91. 49 0
      RackPeek.Web.Viewer/Commands/Routers/RouterGetCommand.cs
  92. 51 0
      RackPeek.Web.Viewer/Commands/Routers/RouterReportCommand.cs
  93. 39 0
      RackPeek.Web.Viewer/Commands/Routers/RouterSetCommand.cs
  94. 38 0
      RackPeek.Web.Viewer/Commands/Servers/Cpus/ServerCpuAddCommand.cs
  95. 30 0
      RackPeek.Web.Viewer/Commands/Servers/Cpus/ServerCpuRemoveCommand.cs
  96. 39 0
      RackPeek.Web.Viewer/Commands/Servers/Cpus/ServerCpuSetCommand.cs
  97. 34 0
      RackPeek.Web.Viewer/Commands/Servers/Drives/ServerDriveAddCommand.cs
  98. 31 0
      RackPeek.Web.Viewer/Commands/Servers/Drives/ServerDriveRemoveCommand.cs
  99. 37 0
      RackPeek.Web.Viewer/Commands/Servers/Drives/ServerDriveUpdateCommand.cs
  100. 34 0
      RackPeek.Web.Viewer/Commands/Servers/Gpus/AddGpuUseCaseCommand.cs

+ 6 - 0
RackPeek.Domain/IConsoleEmulator.cs

@@ -0,0 +1,6 @@
+namespace RackPeek.Domain;
+
+public interface IConsoleEmulator
+{
+    public Task<string> Execute(string input);
+}

+ 2 - 2
RackPeek.Domain/Persistence/Yaml/YamlResourceCollection.cs

@@ -14,8 +14,8 @@ public sealed class YamlResourceCollection : IResourceCollection
 {
     private readonly string _filePath;
     private readonly ITextFileStore _fileStore;
-    private readonly SemaphoreSlim _fileLock = new(1, 1);
-    private readonly List<Resource> _resources = new();
+    private static readonly SemaphoreSlim _fileLock = new(1, 1);
+    private static readonly List<Resource> _resources = new();
 
     public YamlResourceCollection(string filePath, ITextFileStore fileStore)
     {

+ 475 - 0
RackPeek.Web.Viewer/CliBootstrap.cs

@@ -0,0 +1,475 @@
+using System.ComponentModel.DataAnnotations;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Commands;
+using RackPeek.Commands.AccessPoints;
+using RackPeek.Commands.Desktops;
+using RackPeek.Commands.Desktops.Cpus;
+using RackPeek.Commands.Desktops.Drive;
+using RackPeek.Commands.Desktops.Gpus;
+using RackPeek.Commands.Desktops.Nics;
+using RackPeek.Commands.Firewalls;
+using RackPeek.Commands.Firewalls.Ports;
+using RackPeek.Commands.Laptops;
+using RackPeek.Commands.Laptops.Cpus;
+using RackPeek.Commands.Laptops.Drive;
+using RackPeek.Commands.Laptops.Gpus;
+using RackPeek.Commands.Routers;
+using RackPeek.Commands.Routers.Ports;
+using RackPeek.Commands.Servers;
+using RackPeek.Commands.Servers.Cpus;
+using RackPeek.Commands.Servers.Drives;
+using RackPeek.Commands.Servers.Gpus;
+using RackPeek.Commands.Servers.Nics;
+using RackPeek.Commands.Services;
+using RackPeek.Commands.Switches;
+using RackPeek.Commands.Switches.Ports;
+using RackPeek.Commands.Systems;
+using RackPeek.Commands.Ups;
+using RackPeek.Domain;
+using RackPeek.Domain.Helpers;
+using RackPeek.Domain.Persistence;
+using RackPeek.Domain.Persistence.Yaml;
+using RackPeek.Domain.Resources;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Services;
+using RackPeek.Domain.Resources.SystemResources;
+using RackPeek.Yaml;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek;
+
+public static class CliBootstrap
+{
+    public static void BuildApp(CommandApp app)
+    {
+
+        // Spectre bootstrap
+        app.Configure(config =>
+        {
+            config.SetApplicationName("rpk");
+            config.ValidateExamples();
+
+            config.SetExceptionHandler(HandleException);
+
+            // Global summary
+            config.AddCommand<GetTotalSummaryCommand>("summary")
+                .WithDescription("Show a summarized report of all resources in the system.");
+
+            // ----------------------------
+            // Server commands (CRUD-style)
+            // ----------------------------
+            config.AddBranch("servers", server =>
+            {
+                server.SetDescription("Manage servers and their components.");
+
+                server.AddCommand<ServerReportCommand>("summary")
+                    .WithDescription("Show a summarized hardware report for all servers.");
+
+                server.AddCommand<ServerAddCommand>("add").WithDescription("Add a new server to the inventory.");
+
+                server.AddCommand<ServerGetByNameCommand>("get")
+                    .WithDescription("List all servers or retrieve a specific server by name.");
+
+                server.AddCommand<ServerDescribeCommand>("describe")
+                    .WithDescription("Display detailed information about a specific server.");
+
+                server.AddCommand<ServerSetCommand>("set").WithDescription("Update properties of an existing server.");
+
+                server.AddCommand<ServerDeleteCommand>("del").WithDescription("Delete a server from the inventory.");
+
+                server.AddCommand<ServerTreeCommand>("tree")
+                    .WithDescription("Display the dependency tree of a server.");
+
+                // Server CPUs
+                server.AddBranch("cpu", cpu =>
+                {
+                    cpu.SetDescription("Manage CPUs attached to a server.");
+
+                    cpu.AddCommand<ServerCpuAddCommand>("add").WithDescription("Add a CPU to a specific server.");
+
+                    cpu.AddCommand<ServerCpuSetCommand>("set").WithDescription("Update configuration of a server CPU.");
+
+                    cpu.AddCommand<ServerCpuRemoveCommand>("del").WithDescription("Remove a CPU from a server.");
+                });
+
+                // Server Drives
+                server.AddBranch("drive", drive =>
+                {
+                    drive.SetDescription("Manage drives attached to a server.");
+
+                    drive.AddCommand<ServerDriveAddCommand>("add").WithDescription("Add a storage drive to a server.");
+
+                    drive.AddCommand<ServerDriveUpdateCommand>("set")
+                        .WithDescription("Update properties of a server drive.");
+
+                    drive.AddCommand<ServerDriveRemoveCommand>("del").WithDescription("Remove a drive from a server.");
+                });
+
+                // Server GPUs
+                server.AddBranch("gpu", gpu =>
+                {
+                    gpu.SetDescription("Manage GPUs attached to a server.");
+
+                    gpu.AddCommand<ServerGpuAddCommand>("add").WithDescription("Add a GPU to a server.");
+
+                    gpu.AddCommand<ServerGpuUpdateCommand>("set").WithDescription("Update properties of a server GPU.");
+
+                    gpu.AddCommand<ServerGpuRemoveCommand>("del").WithDescription("Remove a GPU from a server.");
+                });
+
+                // Server NICs
+                server.AddBranch("nic", nic =>
+                {
+                    nic.SetDescription("Manage network interface cards (NICs) for a server.");
+
+                    nic.AddCommand<ServerNicAddCommand>("add").WithDescription("Add a NIC to a server.");
+
+                    nic.AddCommand<ServerNicUpdateCommand>("set").WithDescription("Update properties of a server NIC.");
+
+                    nic.AddCommand<ServerNicRemoveCommand>("del").WithDescription("Remove a NIC from a server.");
+                });
+            });
+
+            // ----------------------------
+            // Switch commands
+            // ----------------------------
+            config.AddBranch("switches", switches =>
+            {
+                switches.SetDescription("Manage network switches.");
+
+                switches.AddCommand<SwitchReportCommand>("summary")
+                    .WithDescription("Show a hardware report for all switches.");
+
+                switches.AddCommand<SwitchAddCommand>("add")
+                    .WithDescription("Add a new network switch to the inventory.");
+
+                switches.AddCommand<SwitchGetCommand>("list").WithDescription("List all switches in the system.");
+
+                switches.AddCommand<SwitchGetByNameCommand>("get")
+                    .WithDescription("Retrieve details of a specific switch by name.");
+
+                switches.AddCommand<SwitchDescribeCommand>("describe")
+                    .WithDescription("Show detailed information about a switch.");
+
+                switches.AddCommand<SwitchSetCommand>("set").WithDescription("Update properties of a switch.");
+
+                switches.AddCommand<SwitchDeleteCommand>("del").WithDescription("Delete a switch from the inventory.");
+                switches.AddBranch("port", port =>
+                {
+                    port.SetDescription("Manage ports on a network switch.");
+
+                    port.AddCommand<SwitchPortAddCommand>("add").WithDescription("Add a port to a switch.");
+
+                    port.AddCommand<SwitchPortUpdateCommand>("set").WithDescription("Update a switch port.");
+
+                    port.AddCommand<SwitchPortRemoveCommand>("del").WithDescription("Remove a port from a switch.");
+                });
+            });
+
+            // ----------------------------
+            // Routers commands
+            // ----------------------------
+            config.AddBranch("routers", routers =>
+            {
+                routers.SetDescription("Manage network routers.");
+
+                routers.AddCommand<RouterReportCommand>("summary")
+                    .WithDescription("Show a hardware report for all routers.");
+
+                routers.AddCommand<RouterAddCommand>("add")
+                    .WithDescription("Add a new network router to the inventory.");
+
+                routers.AddCommand<RouterGetCommand>("list").WithDescription("List all routers in the system.");
+
+                routers.AddCommand<RouterGetByNameCommand>("get")
+                    .WithDescription("Retrieve details of a specific router by name.");
+
+                routers.AddCommand<RouterDescribeCommand>("describe")
+                    .WithDescription("Show detailed information about a router.");
+
+                routers.AddCommand<RouterSetCommand>("set").WithDescription("Update properties of a router.");
+
+                routers.AddCommand<RouterDeleteCommand>("del").WithDescription("Delete a router from the inventory.");
+                routers.AddBranch("port", port =>
+                {
+                    port.SetDescription("Manage ports on a router.");
+
+                    port.AddCommand<RouterPortAddCommand>("add").WithDescription("Add a port to a router.");
+
+                    port.AddCommand<RouterPortUpdateCommand>("set").WithDescription("Update a router port.");
+
+                    port.AddCommand<RouterPortRemoveCommand>("del").WithDescription("Remove a port from a router.");
+                });
+            });
+
+            // ----------------------------
+            // Firewalls commands
+            // ----------------------------
+            config.AddBranch("firewalls", firewalls =>
+            {
+                firewalls.SetDescription("Manage firewalls.");
+
+                firewalls.AddCommand<FirewallReportCommand>("summary")
+                    .WithDescription("Show a hardware report for all firewalls.");
+
+                firewalls.AddCommand<FirewallAddCommand>("add").WithDescription("Add a new firewall to the inventory.");
+
+                firewalls.AddCommand<FirewallGetCommand>("list").WithDescription("List all firewalls in the system.");
+
+                firewalls.AddCommand<FirewallGetByNameCommand>("get")
+                    .WithDescription("Retrieve details of a specific firewall by name.");
+
+                firewalls.AddCommand<FirewallDescribeCommand>("describe")
+                    .WithDescription("Show detailed information about a firewall.");
+
+                firewalls.AddCommand<FirewallSetCommand>("set").WithDescription("Update properties of a firewall.");
+
+                firewalls.AddCommand<FirewallDeleteCommand>("del")
+                    .WithDescription("Delete a firewall from the inventory.");
+                firewalls.AddBranch("port", port =>
+                {
+                    port.SetDescription("Manage ports on a firewall.");
+
+                    port.AddCommand<FirewallPortAddCommand>("add").WithDescription("Add a port to a firewall.");
+
+                    port.AddCommand<FirewallPortUpdateCommand>("set").WithDescription("Update a firewall port.");
+
+                    port.AddCommand<FirewallPortRemoveCommand>("del").WithDescription("Remove a port from a firewall.");
+                });
+            });
+
+            // ----------------------------
+            // System commands
+            // ----------------------------
+            config.AddBranch("systems", system =>
+            {
+                system.SetDescription("Manage systems and their dependencies.");
+
+                system.AddCommand<SystemReportCommand>("summary")
+                    .WithDescription("Show a summary report for all systems.");
+
+                system.AddCommand<SystemAddCommand>("add").WithDescription("Add a new system to the inventory.");
+
+                system.AddCommand<SystemGetCommand>("list").WithDescription("List all systems.");
+
+                system.AddCommand<SystemGetByNameCommand>("get").WithDescription("Retrieve a system by name.");
+
+                system.AddCommand<SystemDescribeCommand>("describe")
+                    .WithDescription("Display detailed information about a system.");
+
+                system.AddCommand<SystemSetCommand>("set").WithDescription("Update properties of a system.");
+
+                system.AddCommand<SystemDeleteCommand>("del").WithDescription("Delete a system from the inventory.");
+
+                system.AddCommand<SystemTreeCommand>("tree")
+                    .WithDescription("Display the dependency tree for a system.");
+            });
+
+            // ----------------------------
+            // Access Points
+            // ----------------------------
+            config.AddBranch("accesspoints", ap =>
+            {
+                ap.SetDescription("Manage access points.");
+
+                ap.AddCommand<AccessPointReportCommand>("summary")
+                    .WithDescription("Show a hardware report for all access points.");
+
+                ap.AddCommand<AccessPointAddCommand>("add").WithDescription("Add a new access point.");
+
+                ap.AddCommand<AccessPointGetCommand>("list").WithDescription("List all access points.");
+
+                ap.AddCommand<AccessPointGetByNameCommand>("get").WithDescription("Retrieve an access point by name.");
+
+                ap.AddCommand<AccessPointDescribeCommand>("describe")
+                    .WithDescription("Show detailed information about an access point.");
+
+                ap.AddCommand<AccessPointSetCommand>("set").WithDescription("Update properties of an access point.");
+
+                ap.AddCommand<AccessPointDeleteCommand>("del").WithDescription("Delete an access point.");
+            });
+
+            // ----------------------------
+            // UPS units
+            // ----------------------------
+            config.AddBranch("ups", ups =>
+            {
+                ups.SetDescription("Manage UPS units.");
+
+                ups.AddCommand<UpsReportCommand>("summary")
+                    .WithDescription("Show a hardware report for all UPS units.");
+
+                ups.AddCommand<UpsAddCommand>("add").WithDescription("Add a new UPS unit.");
+
+                ups.AddCommand<UpsGetCommand>("list").WithDescription("List all UPS units.");
+
+                ups.AddCommand<UpsGetByNameCommand>("get").WithDescription("Retrieve a UPS unit by name.");
+
+                ups.AddCommand<UpsDescribeCommand>("describe")
+                    .WithDescription("Show detailed information about a UPS unit.");
+
+                ups.AddCommand<UpsSetCommand>("set").WithDescription("Update properties of a UPS unit.");
+
+                ups.AddCommand<UpsDeleteCommand>("del").WithDescription("Delete a UPS unit.");
+            });
+
+            // ----------------------------
+            // Desktops
+            // ----------------------------
+            config.AddBranch("desktops", desktops =>
+            {
+                desktops.SetDescription("Manage desktop computers and their components.");
+
+                // CRUD
+                desktops.AddCommand<DesktopAddCommand>("add").WithDescription("Add a new desktop.");
+                desktops.AddCommand<DesktopGetCommand>("list").WithDescription("List all desktops.");
+                desktops.AddCommand<DesktopGetByNameCommand>("get").WithDescription("Retrieve a desktop by name.");
+                desktops.AddCommand<DesktopDescribeCommand>("describe")
+                    .WithDescription("Show detailed information about a desktop.");
+                desktops.AddCommand<DesktopSetCommand>("set").WithDescription("Update properties of a desktop.");
+                desktops.AddCommand<DesktopDeleteCommand>("del")
+                    .WithDescription("Delete a desktop from the inventory.");
+                desktops.AddCommand<DesktopReportCommand>("summary")
+                    .WithDescription("Show a summarized hardware report for all desktops.");
+                desktops.AddCommand<DesktopTreeCommand>("tree")
+                    .WithDescription("Display the dependency tree for a desktop.");
+
+                // CPU
+                desktops.AddBranch("cpu", cpu =>
+                {
+                    cpu.SetDescription("Manage CPUs attached to desktops.");
+                    cpu.AddCommand<DesktopCpuAddCommand>("add").WithDescription("Add a CPU to a desktop.");
+                    cpu.AddCommand<DesktopCpuSetCommand>("set").WithDescription("Update a desktop CPU.");
+                    cpu.AddCommand<DesktopCpuRemoveCommand>("del").WithDescription("Remove a CPU from a desktop.");
+                });
+
+                // Drives
+                desktops.AddBranch("drive", drive =>
+                {
+                    drive.SetDescription("Manage storage drives attached to desktops.");
+                    drive.AddCommand<DesktopDriveAddCommand>("add").WithDescription("Add a drive to a desktop.");
+                    drive.AddCommand<DesktopDriveSetCommand>("set").WithDescription("Update a desktop drive.");
+                    drive.AddCommand<DesktopDriveRemoveCommand>("del")
+                        .WithDescription("Remove a drive from a desktop.");
+                });
+
+                // GPUs
+                desktops.AddBranch("gpu", gpu =>
+                {
+                    gpu.SetDescription("Manage GPUs attached to desktops.");
+                    gpu.AddCommand<DesktopGpuAddCommand>("add").WithDescription("Add a GPU to a desktop.");
+                    gpu.AddCommand<DesktopGpuSetCommand>("set").WithDescription("Update a desktop GPU.");
+                    gpu.AddCommand<DesktopGpuRemoveCommand>("del").WithDescription("Remove a GPU from a desktop.");
+                });
+
+                // NICs
+                desktops.AddBranch("nic", nic =>
+                {
+                    nic.SetDescription("Manage network interface cards (NICs) for desktops.");
+                    nic.AddCommand<DesktopNicAddCommand>("add").WithDescription("Add a NIC to a desktop.");
+                    nic.AddCommand<DesktopNicSetCommand>("set").WithDescription("Update a desktop NIC.");
+                    nic.AddCommand<DesktopNicRemoveCommand>("del").WithDescription("Remove a NIC from a desktop.");
+                });
+            });
+
+            // ----------------------------
+            // Laptops
+            // ----------------------------
+            config.AddBranch("Laptops", Laptops =>
+            {
+                Laptops.SetDescription("Manage Laptop computers and their components.");
+
+                // CRUD
+                Laptops.AddCommand<LaptopAddCommand>("add").WithDescription("Add a new Laptop.");
+                Laptops.AddCommand<LaptopGetCommand>("list").WithDescription("List all Laptops.");
+                Laptops.AddCommand<LaptopGetByNameCommand>("get").WithDescription("Retrieve a Laptop by name.");
+                Laptops.AddCommand<LaptopDescribeCommand>("describe")
+                    .WithDescription("Show detailed information about a Laptop.");
+                Laptops.AddCommand<LaptopDeleteCommand>("del").WithDescription("Delete a Laptop from the inventory.");
+                Laptops.AddCommand<LaptopReportCommand>("summary")
+                    .WithDescription("Show a summarized hardware report for all Laptops.");
+                Laptops.AddCommand<LaptopTreeCommand>("tree")
+                    .WithDescription("Display the dependency tree for a Laptop.");
+
+                // CPU
+                Laptops.AddBranch("cpu", cpu =>
+                {
+                    cpu.SetDescription("Manage CPUs attached to Laptops.");
+                    cpu.AddCommand<LaptopCpuAddCommand>("add").WithDescription("Add a CPU to a Laptop.");
+                    cpu.AddCommand<LaptopCpuSetCommand>("set").WithDescription("Update a Laptop CPU.");
+                    cpu.AddCommand<LaptopCpuRemoveCommand>("del").WithDescription("Remove a CPU from a Laptop.");
+                });
+
+                // Drives
+                Laptops.AddBranch("drive", drive =>
+                {
+                    drive.SetDescription("Manage storage drives attached to Laptops.");
+                    drive.AddCommand<LaptopDriveAddCommand>("add").WithDescription("Add a drive to a Laptop.");
+                    drive.AddCommand<LaptopDriveSetCommand>("set").WithDescription("Update a Laptop drive.");
+                    drive.AddCommand<LaptopDriveRemoveCommand>("del").WithDescription("Remove a drive from a Laptop.");
+                });
+
+                // GPUs
+                Laptops.AddBranch("gpu", gpu =>
+                {
+                    gpu.SetDescription("Manage GPUs attached to Laptops.");
+                    gpu.AddCommand<LaptopGpuAddCommand>("add").WithDescription("Add a GPU to a Laptop.");
+                    gpu.AddCommand<LaptopGpuSetCommand>("set").WithDescription("Update a Laptop GPU.");
+                    gpu.AddCommand<LaptopGpuRemoveCommand>("del").WithDescription("Remove a GPU from a Laptop.");
+                });
+            });
+
+            // ----------------------------
+            // Services
+            // ----------------------------
+            config.AddBranch("services", service =>
+            {
+                service.SetDescription("Manage services and their configurations.");
+
+                service.AddCommand<ServiceReportCommand>("summary")
+                    .WithDescription("Show a summary report for all services.");
+
+                service.AddCommand<ServiceAddCommand>("add").WithDescription("Add a new service.");
+
+                service.AddCommand<ServiceGetCommand>("list").WithDescription("List all services.");
+
+                service.AddCommand<ServiceGetByNameCommand>("get").WithDescription("Retrieve a service by name.");
+
+                service.AddCommand<ServiceDescribeCommand>("describe")
+                    .WithDescription("Show detailed information about a service.");
+
+                service.AddCommand<ServiceSetCommand>("set").WithDescription("Update properties of a service.");
+
+                service.AddCommand<ServiceDeleteCommand>("del").WithDescription("Delete a service.");
+
+                service.AddCommand<ServiceSubnetsCommand>("subnets")
+                    .WithDescription("List subnets associated with a service, optionally filtered by CIDR.");
+            });
+        });
+    }
+
+    private static int HandleException(Exception ex, ITypeResolver? arg2)
+    {
+        switch (ex)
+        {
+            case ValidationException ve:
+                AnsiConsole.MarkupLine($"[yellow]Validation error:[/] {ve.Message}");
+                return 2;
+
+            case ConflictException ce:
+                AnsiConsole.MarkupLine($"[red]Conflict:[/] {ce.Message}");
+                return 3;
+
+            case NotFoundException ne:
+                AnsiConsole.MarkupLine($"[red]Not found:[/] {ne.Message}");
+                return 4;
+
+            default:
+                AnsiConsole.MarkupLine("[red]Unexpected error occurred.[/]");
+                AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything);
+                return 99;
+        }
+    }
+}

+ 11 - 0
RackPeek.Web.Viewer/Commands/AccessPoints/AccessCommands.cs

@@ -0,0 +1,11 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.AccessPoints;
+
+public class AccessPointNameSettings : CommandSettings
+{
+    [CommandArgument(0, "<name>")]
+    [Description("The access point name.")]
+    public string Name { get; set; } = default!;
+}

+ 33 - 0
RackPeek.Web.Viewer/Commands/AccessPoints/AccessPointAddCommand.cs

@@ -0,0 +1,33 @@
+using System.ComponentModel;
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.AccessPoints;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.AccessPoints;
+
+public class AccessPointAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<name>")]
+    [Description("The access point name.")]
+    public string Name { get; set; } = default!;
+}
+
+public class AccessPointAddCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<AccessPointAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        AccessPointAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddAccessPointUseCase>();
+
+        await useCase.ExecuteAsync(settings.Name);
+
+        AnsiConsole.MarkupLine($"[green]Access Point '{settings.Name}' added.[/]");
+        return 0;
+    }
+}

+ 25 - 0
RackPeek.Web.Viewer/Commands/AccessPoints/AccessPointDeleteCommand.cs

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

+ 37 - 0
RackPeek.Web.Viewer/Commands/AccessPoints/AccessPointDescribeCommand.cs

@@ -0,0 +1,37 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.AccessPoints;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.AccessPoints;
+
+public class AccessPointDescribeCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<AccessPointNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        AccessPointNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DescribeAccessPointUseCase>();
+
+        var ap = await useCase.ExecuteAsync(settings.Name);
+
+        var grid = new Grid()
+            .AddColumn(new GridColumn().NoWrap())
+            .AddColumn(new GridColumn().NoWrap());
+
+        grid.AddRow("Name:", ap.Name);
+        grid.AddRow("Model:", ap.Model ?? "Unknown");
+        grid.AddRow("Speed (Gbps):", ap.Speed?.ToString() ?? "Unknown");
+
+        AnsiConsole.Write(
+            new Panel(grid)
+                .Header("Access Point")
+                .Border(BoxBorder.Rounded));
+
+        return 0;
+    }
+}

+ 27 - 0
RackPeek.Web.Viewer/Commands/AccessPoints/AccessPointGetByNameCommand.cs

@@ -0,0 +1,27 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.AccessPoints;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.AccessPoints;
+
+public class AccessPointGetByNameCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<AccessPointNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        AccessPointNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DescribeAccessPointUseCase>();
+
+        var ap = await useCase.ExecuteAsync(settings.Name);
+
+        AnsiConsole.MarkupLine(
+            $"[green]{ap.Name}[/]  Model: {ap.Model ?? "Unknown"}, Speed: {ap.Speed?.ToString() ?? "Unknown"}Gbps");
+
+        return 0;
+    }
+}

+ 44 - 0
RackPeek.Web.Viewer/Commands/AccessPoints/AccessPointGetCommand.cs

@@ -0,0 +1,44 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.AccessPoints;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.AccessPoints;
+
+public class AccessPointGetCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AccessPointHardwareReportUseCase>();
+
+        var report = await useCase.ExecuteAsync();
+
+        if (report.AccessPoints.Count == 0)
+        {
+            AnsiConsole.MarkupLine("[yellow]No access points found.[/]");
+            return 0;
+        }
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .AddColumn("Name")
+            .AddColumn("Model")
+            .AddColumn("Speed (Gbps)");
+
+        foreach (var ap in report.AccessPoints)
+            table.AddRow(
+                ap.Name,
+                ap.Model,
+                ap.SpeedGb.ToString()
+            );
+
+
+        AnsiConsole.Write(table);
+        return 0;
+    }
+}

+ 43 - 0
RackPeek.Web.Viewer/Commands/AccessPoints/AccessPointReportCommand.cs

@@ -0,0 +1,43 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using RackPeek.Domain.Resources.Hardware.AccessPoints;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.AccessPoints;
+
+public class AccessPointReportCommand(
+    ILogger<AccessPointReportCommand> logger,
+    IServiceProvider serviceProvider
+) : AsyncCommand
+{
+    public override async Task<int> ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AccessPointHardwareReportUseCase>();
+
+        var report = await useCase.ExecuteAsync();
+
+        if (report.AccessPoints.Count == 0)
+        {
+            AnsiConsole.MarkupLine("[yellow]No access points found.[/]");
+            return 0;
+        }
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .AddColumn("Name")
+            .AddColumn("Model")
+            .AddColumn("Speed (Gbps)");
+
+        foreach (var ap in report.AccessPoints)
+            table.AddRow(
+                ap.Name,
+                ap.Model,
+                $"{ap.SpeedGb}"
+            );
+
+        AnsiConsole.Write(table);
+        return 0;
+    }
+}

+ 42 - 0
RackPeek.Web.Viewer/Commands/AccessPoints/AccessPointSetCommand.cs

@@ -0,0 +1,42 @@
+using System.ComponentModel;
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Commands.Servers;
+using RackPeek.Domain.Resources.Hardware.AccessPoints;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.AccessPoints;
+
+public class AccessPointSetSettings : ServerNameSettings
+{
+    [CommandOption("--model")]
+    [Description("The access point model name.")]
+    public string? Model { get; set; }
+
+    [CommandOption("--speed")]
+    [Description("The speed of the access point in Gb.")]
+    public double? Speed { get; set; }
+}
+
+public class AccessPointSetCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<AccessPointSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        AccessPointSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateAccessPointUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Model,
+            settings.Speed
+        );
+
+        AnsiConsole.MarkupLine($"[green]Access Point '{settings.Name}' updated.[/]");
+        return 0;
+    }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Desktops/Cpus/DesktopCpuAddCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktops.Cpus;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.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>();
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Model, settings.Cores, settings.Threads);
+
+        AnsiConsole.MarkupLine($"[green]CPU added to desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 23 - 0
RackPeek.Web.Viewer/Commands/Desktops/Cpus/DesktopCpuAddSettings.cs

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

+ 24 - 0
RackPeek.Web.Viewer/Commands/Desktops/Cpus/DesktopCpuRemoveCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktops.Cpus;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.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;
+    }
+}

+ 15 - 0
RackPeek.Web.Viewer/Commands/Desktops/Cpus/DesktopCpuRemoveSettings.cs

@@ -0,0 +1,15 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.Cpus;
+
+public class DesktopCpuRemoveSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    [Description("The name of the desktop.")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    [Description("The index of the desktop cpu to remove.")]
+    public int Index { get; set; }
+}

+ 25 - 0
RackPeek.Web.Viewer/Commands/Desktops/Cpus/DesktopCpuSetCommand.cs

@@ -0,0 +1,25 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktops.Cpus;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.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>();
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index, settings.Model, settings.Cores,
+            settings.Threads);
+
+        AnsiConsole.MarkupLine($"[green]CPU #{settings.Index} updated on desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 27 - 0
RackPeek.Web.Viewer/Commands/Desktops/Cpus/DesktopCpuSetSettings.cs

@@ -0,0 +1,27 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.Cpus;
+
+public class DesktopCpuSetSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    [Description("The desktop name.")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    [Description("The index of the desktop cpu.")]
+    public int Index { get; set; }
+
+    [CommandOption("--model")]
+    [Description("The cpu model.")]
+    public string? Model { get; set; }
+
+    [CommandOption("--cores")]
+    [Description("The number of cpu cores.")]
+    public int? Cores { get; set; }
+
+    [CommandOption("--threads")]
+    [Description("The number of cpu threads.")]
+    public int? Threads { get; set; }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Desktops/DesktopAddCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktops;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops;
+
+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;
+    }
+}

+ 8 - 0
RackPeek.Web.Viewer/Commands/Desktops/DesktopCommands.cs

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

+ 24 - 0
RackPeek.Web.Viewer/Commands/Desktops/DesktopDeleteCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktops;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops;
+
+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;
+    }
+}

+ 35 - 0
RackPeek.Web.Viewer/Commands/Desktops/DesktopDescribeCommand.cs

@@ -0,0 +1,35 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktops;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops;
+
+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);
+
+        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;
+    }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Desktops/DesktopGetByNameCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktops;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops;
+
+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);
+
+        AnsiConsole.MarkupLine($"[green]{desktop.Name}[/] (Model: {desktop.Model ?? "Unknown"})");
+        return 0;
+    }
+}

+ 50 - 0
RackPeek.Web.Viewer/Commands/Desktops/DesktopGetCommand.cs

@@ -0,0 +1,50 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktops;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops;
+
+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;
+    }
+}

+ 51 - 0
RackPeek.Web.Viewer/Commands/Desktops/DesktopReportCommand.cs

@@ -0,0 +1,51 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using RackPeek.Domain.Resources.Hardware.Desktops;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops;
+
+public class DesktopReportCommand(
+    ILogger<DesktopReportCommand> logger,
+    IServiceProvider serviceProvider
+) : AsyncCommand
+{
+    public override async Task<int> ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DesktopHardwareReportUseCase>();
+
+        var report = await useCase.ExecuteAsync();
+
+        if (report.Desktops.Count == 0)
+        {
+            AnsiConsole.MarkupLine("[yellow]No desktops found.[/]");
+            return 0;
+        }
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .AddColumn("Name")
+            .AddColumn("CPU")
+            .AddColumn("C/T")
+            .AddColumn("RAM")
+            .AddColumn("Storage")
+            .AddColumn("NICs")
+            .AddColumn("GPU");
+
+        foreach (var d in report.Desktops)
+            table.AddRow(
+                d.Name,
+                d.CpuSummary,
+                $"{d.TotalCores}/{d.TotalThreads}",
+                $"{d.RamGb} GB",
+                $"{d.TotalStorageGb} GB (SSD {d.SsdStorageGb} / HDD {d.HddStorageGb})",
+                d.NicSummary,
+                d.GpuSummary
+            );
+
+        AnsiConsole.Write(table);
+        return 0;
+    }
+}

+ 29 - 0
RackPeek.Web.Viewer/Commands/Desktops/DesktopSetCommand.cs

@@ -0,0 +1,29 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktops;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops;
+
+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;
+    }
+}

+ 29 - 0
RackPeek.Web.Viewer/Commands/Desktops/DesktopTreeCommand.cs

@@ -0,0 +1,29 @@
+using RackPeek.Domain.Resources.Hardware;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops;
+
+public sealed class DesktopTreeCommand(GetHardwareSystemTreeUseCase useCase)
+    : AsyncCommand<DesktopNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        DesktopNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        var tree = await useCase.ExecuteAsync(settings.Name);
+
+        var root = new Tree($"[bold]{tree.Hardware.Name}[/]");
+
+        foreach (var system in tree.Systems)
+        {
+            var systemNode = root.AddNode($"[green]System:[/] {system.System.Name}");
+            foreach (var service in system.Services)
+                systemNode.AddNode($"[green]Service:[/] {service.Name}");
+        }
+
+        AnsiConsole.Write(root);
+        return 0;
+    }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Desktops/Drive/DesktopDriveAddCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktops.Drives;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.Drive;
+
+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>();
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Type, settings.Size);
+
+        AnsiConsole.MarkupLine($"[green]Drive added to desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 19 - 0
RackPeek.Web.Viewer/Commands/Desktops/Drive/DesktopDriveAddSettings.cs

@@ -0,0 +1,19 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.Drive;
+
+public class DesktopDriveAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    [Description("The name of the desktop.")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandOption("--type")]
+    [Description("The drive type e.g hdd / ssd.")]
+    public string? Type { get; set; }
+
+    [CommandOption("--size")]
+    [Description("The drive capacity in Gb.")]
+    public int? Size { get; set; }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Desktops/Drive/DesktopDriveRemoveCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktops.Drives;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.Drive;
+
+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;
+    }
+}

+ 15 - 0
RackPeek.Web.Viewer/Commands/Desktops/Drive/DesktopDriveRemoveSettings.cs

@@ -0,0 +1,15 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.Drive;
+
+public class DesktopDriveRemoveSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    [Description("The name of the desktop.")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    [Description("The index of the drive to remove.")]
+    public int Index { get; set; }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Desktops/Drive/DesktopDriveSetCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktops.Drives;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.Drive;
+
+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>();
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index, settings.Type, settings.Size);
+
+        AnsiConsole.MarkupLine($"[green]Drive #{settings.Index} updated on desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 23 - 0
RackPeek.Web.Viewer/Commands/Desktops/Drive/DesktopDriveSetSettings.cs

@@ -0,0 +1,23 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.Drive;
+
+public class DesktopDriveSetSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    [Description("The desktop name.")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    [Description("The drive index to update.")]
+    public int Index { get; set; }
+
+    [CommandOption("--type")]
+    [Description("The drive type e.g hdd / ssd.")]
+    public string? Type { get; set; }
+
+    [CommandOption("--size")]
+    [Description("The drive capacity in Gb.")]
+    public int? Size { get; set; }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Desktops/Gpus/DesktopGpuAddCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktops.Gpus;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.Gpus;
+
+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>();
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Model, settings.Vram);
+
+        AnsiConsole.MarkupLine($"[green]GPU added to desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 19 - 0
RackPeek.Web.Viewer/Commands/Desktops/Gpus/DesktopGpuAddSettings.cs

@@ -0,0 +1,19 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.Gpus;
+
+public class DesktopGpuAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    [Description("The name of the desktop.")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandOption("--model")]
+    [Description("The Gpu model.")]
+    public string? Model { get; set; }
+
+    [CommandOption("--vram")]
+    [Description("The amount of gpu vram in Gb.")]
+    public int? Vram { get; set; }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Desktops/Gpus/DesktopGpuRemoveCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktops.Gpus;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.Gpus;
+
+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;
+    }
+}

+ 15 - 0
RackPeek.Web.Viewer/Commands/Desktops/Gpus/DesktopGpuRemoveSettings.cs

@@ -0,0 +1,15 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.Gpus;
+
+public class DesktopGpuRemoveSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    [Description("The desktop name.")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    [Description("The index of the Gpu to remove.")]
+    public int Index { get; set; }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Desktops/Gpus/DesktopGpuSetCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktops.Gpus;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.Gpus;
+
+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>();
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index, settings.Model, settings.Vram);
+
+        AnsiConsole.MarkupLine($"[green]GPU #{settings.Index} updated on desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 23 - 0
RackPeek.Web.Viewer/Commands/Desktops/Gpus/DesktopGpuSetSettings.cs

@@ -0,0 +1,23 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.Gpus;
+
+public class DesktopGpuSetSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    [Description("The desktop name.")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    [Description("The index of the gpu to update.")]
+    public int Index { get; set; }
+
+    [CommandOption("--model")]
+    [Description("The gpu model name.")]
+    public string? Model { get; set; }
+
+    [CommandOption("--vram")]
+    [Description("The amount of gpu vram in Gb.")]
+    public int? Vram { get; set; }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Desktops/Nics/DesktopNicAddCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktops.Nics;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.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>();
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Type, settings.Speed, settings.Ports);
+
+        AnsiConsole.MarkupLine($"[green]NIC added to desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 23 - 0
RackPeek.Web.Viewer/Commands/Desktops/Nics/DesktopNicAddSettings.cs

@@ -0,0 +1,23 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.Nics;
+
+public class DesktopNicAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    [Description("The desktop name.")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandOption("--type")]
+    [Description("The nic port type e.g rj45 / sfp+")]
+    public string? Type { get; set; }
+
+    [CommandOption("--speed")]
+    [Description("The port speed.")]
+    public int? Speed { get; set; }
+
+    [CommandOption("--ports")]
+    [Description("The number of ports.")]
+    public int? Ports { get; set; }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Desktops/Nics/DesktopNicRemoveCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktops.Nics;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.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;
+    }
+}

+ 15 - 0
RackPeek.Web.Viewer/Commands/Desktops/Nics/DesktopNicRemoveSettings.cs

@@ -0,0 +1,15 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.Nics;
+
+public class DesktopNicRemoveSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    [Description("The desktop name.")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    [Description("The index of the nic to remove.")]
+    public int Index { get; set; }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Desktops/Nics/DesktopNicSetCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Desktops.Nics;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.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>();
+
+        await useCase.ExecuteAsync(settings.DesktopName, settings.Index, settings.Type, settings.Speed, settings.Ports);
+
+        AnsiConsole.MarkupLine($"[green]NIC #{settings.Index} updated on desktop '{settings.DesktopName}'.[/]");
+        return 0;
+    }
+}

+ 27 - 0
RackPeek.Web.Viewer/Commands/Desktops/Nics/DesktopNicSetSettings.cs

@@ -0,0 +1,27 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Desktops.Nics;
+
+public class DesktopNicSetSettings : CommandSettings
+{
+    [CommandArgument(0, "<desktop>")]
+    [Description("The desktop name.")]
+    public string DesktopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    [Description("The index of the nic to remove.")]
+    public int Index { get; set; }
+
+    [CommandOption("--type")]
+    [Description("The nic port type e.g rj45 / sfp+")]
+    public string? Type { get; set; }
+
+    [CommandOption("--speed")]
+    [Description("The speed of the nic in Gb/s.")]
+    public int? Speed { get; set; }
+
+    [CommandOption("--ports")]
+    [Description("The number of ports.")]
+    public int? Ports { get; set; }
+}

+ 32 - 0
RackPeek.Web.Viewer/Commands/Firewalls/FirewallAddCommand.cs

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

+ 8 - 0
RackPeek.Web.Viewer/Commands/Firewalls/FirewallCommands.cs

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

+ 25 - 0
RackPeek.Web.Viewer/Commands/Firewalls/FirewallDeleteCommand.cs

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

+ 41 - 0
RackPeek.Web.Viewer/Commands/Firewalls/FirewallDescribeCommand.cs

@@ -0,0 +1,41 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Firewalls;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Firewalls;
+
+public class FirewallDescribeCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<FirewallNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        FirewallNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DescribeFirewallUseCase>();
+
+        var sw = await useCase.ExecuteAsync(settings.Name);
+
+        var grid = new Grid()
+            .AddColumn(new GridColumn().NoWrap())
+            .AddColumn(new GridColumn().NoWrap());
+
+        grid.AddRow("Name:", sw.Name);
+        grid.AddRow("Model:", sw.Model ?? "Unknown");
+        grid.AddRow("Managed:", sw.Managed.HasValue ? sw.Managed.Value ? "Yes" : "No" : "Unknown");
+        grid.AddRow("PoE:", sw.Poe.HasValue ? sw.Poe.Value ? "Yes" : "No" : "Unknown");
+        grid.AddRow("Total Ports:", sw.TotalPorts.ToString());
+        grid.AddRow("Total Speed (Gb):", sw.TotalSpeedGb.ToString());
+        grid.AddRow("Ports:", sw.PortSummary);
+
+        AnsiConsole.Write(
+            new Panel(grid)
+                .Header("Firewall")
+                .Border(BoxBorder.Rounded));
+
+        return 0;
+    }
+}

+ 27 - 0
RackPeek.Web.Viewer/Commands/Firewalls/FirewallGetByNameCommand.cs

@@ -0,0 +1,27 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Firewalls;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Firewalls;
+
+public class FirewallGetByNameCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<FirewallNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        FirewallNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DescribeFirewallUseCase>();
+
+        var sw = await useCase.ExecuteAsync(settings.Name);
+
+        AnsiConsole.MarkupLine(
+            $"[green]{sw.Name}[/]  Model: {sw.Model ?? "Unknown"}, Managed: {(sw.Managed == true ? "Yes" : "No")}, PoE: {(sw.Poe == true ? "Yes" : "No")}");
+
+        return 0;
+    }
+}

+ 49 - 0
RackPeek.Web.Viewer/Commands/Firewalls/FirewallGetCommand.cs

@@ -0,0 +1,49 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Firewalls;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Firewalls;
+
+public class FirewallGetCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<FirewallHardwareReportUseCase>();
+
+        var report = await useCase.ExecuteAsync();
+
+        if (report.Firewalls.Count == 0)
+        {
+            AnsiConsole.MarkupLine("[yellow]No Firewalls found.[/]");
+            return 0;
+        }
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .AddColumn("Name")
+            .AddColumn("Model")
+            .AddColumn("Managed")
+            .AddColumn("PoE")
+            .AddColumn("Ports")
+            .AddColumn("Port Summary");
+
+        foreach (var s in report.Firewalls)
+            table.AddRow(
+                s.Name,
+                s.Model ?? "Unknown",
+                s.Managed ? "[green]yes[/]" : "[red]no[/]",
+                s.Poe ? "[green]yes[/]" : "[red]no[/]",
+                s.TotalPorts.ToString(),
+                s.PortSummary
+            );
+
+        AnsiConsole.Write(table);
+        return 0;
+    }
+}

+ 51 - 0
RackPeek.Web.Viewer/Commands/Firewalls/FirewallReportCommand.cs

@@ -0,0 +1,51 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using RackPeek.Domain.Resources.Hardware.Firewalls;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Firewalls;
+
+public class FirewallReportCommand(
+    ILogger<FirewallReportCommand> logger,
+    IServiceProvider serviceProvider
+) : AsyncCommand
+{
+    public override async Task<int> ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<FirewallHardwareReportUseCase>();
+
+        var report = await useCase.ExecuteAsync();
+
+        if (report.Firewalls.Count == 0)
+        {
+            AnsiConsole.MarkupLine("[yellow]No Firewalls found.[/]");
+            return 0;
+        }
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .AddColumn("Name")
+            .AddColumn("Model")
+            .AddColumn("Managed")
+            .AddColumn("PoE")
+            .AddColumn("Ports")
+            .AddColumn("Max Speed")
+            .AddColumn("Port Summary");
+
+        foreach (var s in report.Firewalls)
+            table.AddRow(
+                s.Name,
+                s.Model,
+                s.Managed ? "[green]yes[/]" : "[red]no[/]",
+                s.Poe ? "[green]yes[/]" : "[red]no[/]",
+                s.TotalPorts.ToString(),
+                $"{s.MaxPortSpeedGb}G",
+                s.PortSummary
+            );
+
+        AnsiConsole.Write(table);
+        return 0;
+    }
+}

+ 39 - 0
RackPeek.Web.Viewer/Commands/Firewalls/FirewallSetCommand.cs

@@ -0,0 +1,39 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Commands.Servers;
+using RackPeek.Domain.Resources.Hardware.Firewalls;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Firewalls;
+
+public class FirewallSetSettings : ServerNameSettings
+{
+    [CommandOption("--Model")] public string Model { get; set; } = default!;
+
+    [CommandOption("--managed")] public bool Managed { get; set; }
+
+    [CommandOption("--poe")] public bool Poe { get; set; }
+}
+
+public class FirewallSetCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<FirewallSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        FirewallSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateFirewallUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Model,
+            settings.Managed,
+            settings.Poe);
+
+        AnsiConsole.MarkupLine($"[green]Firewall '{settings.Name}' updated.[/]");
+        return 0;
+    }
+}

+ 29 - 0
RackPeek.Web.Viewer/Commands/Firewalls/Ports/FirewallPortAddCommand.cs

@@ -0,0 +1,29 @@
+using Spectre.Console.Cli;
+using System.ComponentModel;
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Firewalls.Ports;
+using Spectre.Console;
+
+namespace RackPeek.Commands.Firewalls.Ports;
+
+public class FirewallPortAddSettings : FirewallNameSettings
+{
+    [CommandOption("--type")] public string? Type { get; set; }
+    [CommandOption("--speed")] public double? Speed { get; set; }
+    [CommandOption("--count")] public int? Count { get; set; }
+}
+
+public class FirewallPortAddCommand(IServiceProvider sp)
+    : AsyncCommand<FirewallPortAddSettings>
+{
+    public override async Task<int> ExecuteAsync(CommandContext ctx, FirewallPortAddSettings s, CancellationToken ct)
+    {
+        using var scope = sp.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddFirewallPortUseCase>();
+
+        await useCase.ExecuteAsync(s.Name, s.Type, s.Speed, s.Count);
+
+        AnsiConsole.MarkupLine($"[green]Port added to firewall '{s.Name}'.[/]");
+        return 0;
+    }
+}

+ 27 - 0
RackPeek.Web.Viewer/Commands/Firewalls/Ports/FirewallPortRemoveCommand.cs

@@ -0,0 +1,27 @@
+using Spectre.Console.Cli;
+using System.ComponentModel;
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Firewalls.Ports;
+using Spectre.Console;
+
+namespace RackPeek.Commands.Firewalls.Ports;
+
+public class FirewallPortRemoveSettings : FirewallNameSettings
+{
+    [CommandOption("--index <INDEX>")] public int Index { get; set; }
+}
+
+public class FirewallPortRemoveCommand(IServiceProvider sp)
+    : AsyncCommand<FirewallPortRemoveSettings>
+{
+    public override async Task<int> ExecuteAsync(CommandContext ctx, FirewallPortRemoveSettings s, CancellationToken ct)
+    {
+        using var scope = sp.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<RemoveFirewallPortUseCase>();
+
+        await useCase.ExecuteAsync(s.Name, s.Index);
+
+        AnsiConsole.MarkupLine($"[green]Port {s.Index} removed from firewall '{s.Name}'.[/]");
+        return 0;
+    }
+}

+ 30 - 0
RackPeek.Web.Viewer/Commands/Firewalls/Ports/FirewallPortUpdateCommand.cs

@@ -0,0 +1,30 @@
+using Spectre.Console.Cli;
+using System.ComponentModel;
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Firewalls.Ports;
+using Spectre.Console;
+
+namespace RackPeek.Commands.Firewalls.Ports;
+
+public class FirewallPortUpdateSettings : FirewallNameSettings
+{
+    [CommandOption("--index <INDEX>")] public int Index { get; set; }
+    [CommandOption("--type")] public string? Type { get; set; }
+    [CommandOption("--speed")] public double? Speed { get; set; }
+    [CommandOption("--count")] public int? Count { get; set; }
+}
+
+public class FirewallPortUpdateCommand(IServiceProvider sp)
+    : AsyncCommand<FirewallPortUpdateSettings>
+{
+    public override async Task<int> ExecuteAsync(CommandContext ctx, FirewallPortUpdateSettings s, CancellationToken ct)
+    {
+        using var scope = sp.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateFirewallPortUseCase>();
+
+        await useCase.ExecuteAsync(s.Name, s.Index, s.Type, s.Speed, s.Count);
+
+        AnsiConsole.MarkupLine($"[green]Port {s.Index} updated on firewall '{s.Name}'.[/]");
+        return 0;
+    }
+}

+ 139 - 0
RackPeek.Web.Viewer/Commands/GetTotalSummaryCommand.cs

@@ -0,0 +1,139 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Services.UseCases;
+using RackPeek.Domain.Resources.SystemResources.UseCases;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands;
+
+public class GetTotalSummaryCommand(IServiceProvider provider) : AsyncCommand
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+
+        var systemUseCase =
+            scope.ServiceProvider.GetRequiredService<GetSystemSummaryUseCase>();
+        var serviceUseCase =
+            scope.ServiceProvider.GetRequiredService<GetServiceSummaryUseCase>();
+        var hardwareUseCase =
+            scope.ServiceProvider.GetRequiredService<GetHardwareUseCaseSummary>();
+
+        // Execute all summaries in parallel
+        var systemTask = systemUseCase.ExecuteAsync();
+        var serviceTask = serviceUseCase.ExecuteAsync();
+        var hardwareTask = hardwareUseCase.ExecuteAsync();
+
+        await Task.WhenAll(systemTask, serviceTask, hardwareTask);
+
+        var systemSummary = systemTask.Result;
+        var serviceSummary = serviceTask.Result;
+        var hardwareSummary = hardwareTask.Result;
+
+        RenderSummaryTree(systemSummary, serviceSummary, hardwareSummary);
+
+        return 0;
+    }
+
+    private static void RenderSummaryTree(
+        SystemSummary systemSummary,
+        AllServicesSummary serviceSummary,
+        HardwareSummary hardwareSummary)
+    {
+        var tree = new Tree("[bold]Breakdown[/]");
+
+        var hardwareNode = tree.AddNode(
+            $"[bold]Hardware[/] ({hardwareSummary.TotalHardware})");
+
+        foreach (var (kind, count) in hardwareSummary.HardwareByKind.OrderByDescending(h => h.Value).ThenBy(h => h.Key))
+            hardwareNode.AddNode($"{kind}: {count}");
+
+        var systemsNode = tree.AddNode(
+            $"[bold]Systems[/] ({systemSummary.TotalSystems})");
+
+        if (systemSummary.SystemsByType.Count > 0)
+        {
+            var typesNode = systemsNode.AddNode("[bold]Types[/]");
+            foreach (var (type, count) in systemSummary.SystemsByType.OrderByDescending(h => h.Value)
+                         .ThenBy(h => h.Key))
+                typesNode.AddNode($"{type}: {count}");
+        }
+
+        if (systemSummary.SystemsByOs.Count > 0)
+        {
+            var osNode = systemsNode.AddNode("[bold]Operating Systems[/]");
+            foreach (var (os, count) in systemSummary.SystemsByOs.OrderByDescending(h => h.Value).ThenBy(h => h.Key))
+                osNode.AddNode($"{os}: {count}");
+        }
+
+        var servicesNode = tree.AddNode(
+            $"[bold]Services[/] ({serviceSummary.TotalServices})");
+
+        servicesNode.AddNode(
+            $"IP Addresses: {serviceSummary.TotalIpAddresses}");
+
+        AnsiConsole.Write(tree);
+    }
+
+    private static void RenderTotals(
+        SystemSummary systemSummary,
+        AllServicesSummary serviceSummary,
+        HardwareSummary hardwareSummary)
+    {
+        var grid = new Grid()
+            .AddColumn()
+            .AddColumn();
+
+        grid.AddRow("[bold]Systems[/]", systemSummary.TotalSystems.ToString());
+        grid.AddRow("[bold]Services[/]", serviceSummary.TotalServices.ToString());
+        grid.AddRow("[bold]Service IPs[/]", serviceSummary.TotalIpAddresses.ToString());
+        grid.AddRow("[bold]Hardware[/]", hardwareSummary.TotalHardware.ToString());
+
+        AnsiConsole.Write(
+            new Panel(grid)
+                .Header("[bold]Totals[/]")
+                .Border(BoxBorder.Rounded));
+    }
+
+    private static void RenderSystemBreakdown(SystemSummary systemSummary)
+    {
+        if (systemSummary.SystemsByType.Count == 0 &&
+            systemSummary.SystemsByOs.Count == 0)
+            return;
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .Title("[bold]Systems Breakdown[/]")
+            .AddColumn("Category")
+            .AddColumn("Name")
+            .AddColumn("Count");
+
+        foreach (var (type, count) in systemSummary.SystemsByType)
+            table.AddRow("Type", type, count.ToString());
+
+        foreach (var (os, count) in systemSummary.SystemsByOs)
+            table.AddRow("OS", os, count.ToString());
+
+        AnsiConsole.Write(table);
+    }
+
+    private static void RenderHardwareBreakdown(HardwareSummary hardwareSummary)
+    {
+        if (hardwareSummary.HardwareByKind.Count == 0)
+            return;
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .Title("[bold]Hardware Breakdown[/]")
+            .AddColumn("Kind")
+            .AddColumn("Count");
+
+        foreach (var (kind, count) in hardwareSummary.HardwareByKind)
+            table.AddRow(kind, count.ToString());
+
+        AnsiConsole.Write(table);
+    }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Laptops/Cpus/LaptopCpuAddCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Laptops.Cpus;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Laptops.Cpus;
+
+public class LaptopCpuAddCommand(IServiceProvider provider)
+    : AsyncCommand<LaptopCpuAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        LaptopCpuAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddLaptopCpuUseCase>();
+
+        await useCase.ExecuteAsync(settings.LaptopName, settings.Model, settings.Cores, settings.Threads);
+
+        AnsiConsole.MarkupLine($"[green]CPU added to Laptop '{settings.LaptopName}'.[/]");
+        return 0;
+    }
+}

+ 23 - 0
RackPeek.Web.Viewer/Commands/Laptops/Cpus/LaptopCpuAddSettings.cs

@@ -0,0 +1,23 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Laptops.Cpus;
+
+public class LaptopCpuAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<Laptop>")]
+    [Description("The Laptop name.")]
+    public string LaptopName { get; set; } = default!;
+
+    [CommandOption("--model")]
+    [Description("The model name.")]
+    public string? Model { get; set; }
+
+    [CommandOption("--cores")]
+    [Description("The number of cpu cores.")]
+    public int? Cores { get; set; }
+
+    [CommandOption("--threads")]
+    [Description("The number of cpu threads.")]
+    public int? Threads { get; set; }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Laptops/Cpus/LaptopCpuRemoveCommand.cs

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

+ 15 - 0
RackPeek.Web.Viewer/Commands/Laptops/Cpus/LaptopCpuRemoveSettings.cs

@@ -0,0 +1,15 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Laptops.Cpus;
+
+public class LaptopCpuRemoveSettings : CommandSettings
+{
+    [CommandArgument(0, "<Laptop>")]
+    [Description("The name of the Laptop.")]
+    public string LaptopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    [Description("The index of the Laptop cpu to remove.")]
+    public int Index { get; set; }
+}

+ 25 - 0
RackPeek.Web.Viewer/Commands/Laptops/Cpus/LaptopCpuSetCommand.cs

@@ -0,0 +1,25 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Laptops.Cpus;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Laptops.Cpus;
+
+public class LaptopCpuSetCommand(IServiceProvider provider)
+    : AsyncCommand<LaptopCpuSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        LaptopCpuSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateLaptopCpuUseCase>();
+
+        await useCase.ExecuteAsync(settings.LaptopName, settings.Index, settings.Model, settings.Cores,
+            settings.Threads);
+
+        AnsiConsole.MarkupLine($"[green]CPU #{settings.Index} updated on Laptop '{settings.LaptopName}'.[/]");
+        return 0;
+    }
+}

+ 27 - 0
RackPeek.Web.Viewer/Commands/Laptops/Cpus/LaptopCpuSetSettings.cs

@@ -0,0 +1,27 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Laptops.Cpus;
+
+public class LaptopCpuSetSettings : CommandSettings
+{
+    [CommandArgument(0, "<Laptop>")]
+    [Description("The Laptop name.")]
+    public string LaptopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    [Description("The index of the Laptop cpu.")]
+    public int Index { get; set; }
+
+    [CommandOption("--model")]
+    [Description("The cpu model.")]
+    public string? Model { get; set; }
+
+    [CommandOption("--cores")]
+    [Description("The number of cpu cores.")]
+    public int? Cores { get; set; }
+
+    [CommandOption("--threads")]
+    [Description("The number of cpu threads.")]
+    public int? Threads { get; set; }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Laptops/Drive/LaptopDriveAddCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Laptops.Drives;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Laptops.Drive;
+
+public class LaptopDriveAddCommand(IServiceProvider provider)
+    : AsyncCommand<LaptopDriveAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        LaptopDriveAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddLaptopDriveUseCase>();
+
+        await useCase.ExecuteAsync(settings.LaptopName, settings.Type, settings.Size);
+
+        AnsiConsole.MarkupLine($"[green]Drive added to Laptop '{settings.LaptopName}'.[/]");
+        return 0;
+    }
+}

+ 19 - 0
RackPeek.Web.Viewer/Commands/Laptops/Drive/LaptopDriveAddSettings.cs

@@ -0,0 +1,19 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Laptops.Drive;
+
+public class LaptopDriveAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<Laptop>")]
+    [Description("The name of the Laptop.")]
+    public string LaptopName { get; set; } = default!;
+
+    [CommandOption("--type")]
+    [Description("The drive type e.g hdd / ssd.")]
+    public string? Type { get; set; }
+
+    [CommandOption("--size")]
+    [Description("The drive capacity in Gb.")]
+    public int? Size { get; set; }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Laptops/Drive/LaptopDriveRemoveCommand.cs

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

+ 15 - 0
RackPeek.Web.Viewer/Commands/Laptops/Drive/LaptopDriveRemoveSettings.cs

@@ -0,0 +1,15 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Laptops.Drive;
+
+public class LaptopDriveRemoveSettings : CommandSettings
+{
+    [CommandArgument(0, "<Laptop>")]
+    [Description("The name of the Laptop.")]
+    public string LaptopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    [Description("The index of the drive to remove.")]
+    public int Index { get; set; }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Laptops/Drive/LaptopDriveSetCommand.cs

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

+ 23 - 0
RackPeek.Web.Viewer/Commands/Laptops/Drive/LaptopDriveSetSettings.cs

@@ -0,0 +1,23 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Laptops.Drive;
+
+public class LaptopDriveSetSettings : CommandSettings
+{
+    [CommandArgument(0, "<Laptop>")]
+    [Description("The Laptop name.")]
+    public string LaptopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    [Description("The drive index to update.")]
+    public int Index { get; set; }
+
+    [CommandOption("--type")]
+    [Description("The drive type e.g hdd / ssd.")]
+    public string? Type { get; set; }
+
+    [CommandOption("--size")]
+    [Description("The drive capacity in Gb.")]
+    public int? Size { get; set; }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Laptops/Gpus/LaptopGpuAddCommand.cs

@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Laptops.Gpus;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Laptops.Gpus;
+
+public class LaptopGpuAddCommand(IServiceProvider provider)
+    : AsyncCommand<LaptopGpuAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        LaptopGpuAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddLaptopGpuUseCase>();
+
+        await useCase.ExecuteAsync(settings.LaptopName, settings.Model, settings.Vram);
+
+        AnsiConsole.MarkupLine($"[green]GPU added to Laptop '{settings.LaptopName}'.[/]");
+        return 0;
+    }
+}

+ 19 - 0
RackPeek.Web.Viewer/Commands/Laptops/Gpus/LaptopGpuAddSettings.cs

@@ -0,0 +1,19 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Laptops.Gpus;
+
+public class LaptopGpuAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<Laptop>")]
+    [Description("The name of the Laptop.")]
+    public string LaptopName { get; set; } = default!;
+
+    [CommandOption("--model")]
+    [Description("The Gpu model.")]
+    public string? Model { get; set; }
+
+    [CommandOption("--vram")]
+    [Description("The amount of gpu vram in Gb.")]
+    public int? Vram { get; set; }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Laptops/Gpus/LaptopGpuRemoveCommand.cs

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

+ 15 - 0
RackPeek.Web.Viewer/Commands/Laptops/Gpus/LaptopGpuRemoveSettings.cs

@@ -0,0 +1,15 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Laptops.Gpus;
+
+public class LaptopGpuRemoveSettings : CommandSettings
+{
+    [CommandArgument(0, "<Laptop>")]
+    [Description("The Laptop name.")]
+    public string LaptopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    [Description("The index of the Gpu to remove.")]
+    public int Index { get; set; }
+}

+ 31 - 0
RackPeek.Web.Viewer/Commands/Laptops/Gpus/LaptopGpuSetCommand.cs

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

+ 23 - 0
RackPeek.Web.Viewer/Commands/Laptops/Gpus/LaptopGpuSetSettings.cs

@@ -0,0 +1,23 @@
+using System.ComponentModel;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Laptops.Gpus;
+
+public class LaptopGpuSetSettings : CommandSettings
+{
+    [CommandArgument(0, "<Laptop>")]
+    [Description("The Laptop name.")]
+    public string LaptopName { get; set; } = default!;
+
+    [CommandArgument(1, "<index>")]
+    [Description("The index of the gpu to update.")]
+    public int Index { get; set; }
+
+    [CommandOption("--model")]
+    [Description("The gpu model name.")]
+    public string? Model { get; set; }
+
+    [CommandOption("--vram")]
+    [Description("The amount of gpu vram in Gb.")]
+    public int? Vram { get; set; }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Laptops/LaptopAddCommand.cs

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

+ 8 - 0
RackPeek.Web.Viewer/Commands/Laptops/LaptopCommands.cs

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

+ 24 - 0
RackPeek.Web.Viewer/Commands/Laptops/LaptopDeleteCommand.cs

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

+ 33 - 0
RackPeek.Web.Viewer/Commands/Laptops/LaptopDescribeCommand.cs

@@ -0,0 +1,33 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Laptops;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Laptops;
+
+public class LaptopDescribeCommand(IServiceProvider provider)
+    : AsyncCommand<LaptopNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        LaptopNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DescribeLaptopUseCase>();
+
+        var result = await useCase.ExecuteAsync(settings.Name);
+
+        var grid = new Grid().AddColumn().AddColumn();
+
+        grid.AddRow("Name:", result.Name);
+        grid.AddRow("CPUs:", result.CpuCount.ToString());
+        grid.AddRow("RAM:", result.RamSummary ?? "None");
+        grid.AddRow("Drives:", result.DriveCount.ToString());
+        grid.AddRow("GPUs:", result.GpuCount.ToString());
+
+        AnsiConsole.Write(new Panel(grid).Header("Laptop").Border(BoxBorder.Rounded));
+
+        return 0;
+    }
+}

+ 24 - 0
RackPeek.Web.Viewer/Commands/Laptops/LaptopGetByNameCommand.cs

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

+ 46 - 0
RackPeek.Web.Viewer/Commands/Laptops/LaptopGetCommand.cs

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

+ 49 - 0
RackPeek.Web.Viewer/Commands/Laptops/LaptopReportCommand.cs

@@ -0,0 +1,49 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using RackPeek.Domain.Resources.Hardware.Laptops;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Laptops;
+
+public class LaptopReportCommand(
+    ILogger<LaptopReportCommand> logger,
+    IServiceProvider serviceProvider
+) : AsyncCommand
+{
+    public override async Task<int> ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<LaptopHardwareReportUseCase>();
+
+        var report = await useCase.ExecuteAsync();
+
+        if (report.Laptops.Count == 0)
+        {
+            AnsiConsole.MarkupLine("[yellow]No Laptops found.[/]");
+            return 0;
+        }
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .AddColumn("Name")
+            .AddColumn("CPU")
+            .AddColumn("C/T")
+            .AddColumn("RAM")
+            .AddColumn("Storage")
+            .AddColumn("GPU");
+
+        foreach (var d in report.Laptops)
+            table.AddRow(
+                d.Name,
+                d.CpuSummary,
+                $"{d.TotalCores}/{d.TotalThreads}",
+                $"{d.RamGb} GB",
+                $"{d.TotalStorageGb} GB (SSD {d.SsdStorageGb} / HDD {d.HddStorageGb})",
+                d.GpuSummary
+            );
+
+        AnsiConsole.Write(table);
+        return 0;
+    }
+}

+ 29 - 0
RackPeek.Web.Viewer/Commands/Laptops/LaptopTreeCommand.cs

@@ -0,0 +1,29 @@
+using RackPeek.Domain.Resources.Hardware;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Laptops;
+
+public sealed class LaptopTreeCommand(GetHardwareSystemTreeUseCase useCase)
+    : AsyncCommand<LaptopNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        LaptopNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        var tree = await useCase.ExecuteAsync(settings.Name);
+
+        var root = new Tree($"[bold]{tree.Hardware.Name}[/]");
+
+        foreach (var system in tree.Systems)
+        {
+            var systemNode = root.AddNode($"[green]System:[/] {system.System.Name}");
+            foreach (var service in system.Services)
+                systemNode.AddNode($"[green]Service:[/] {service.Name}");
+        }
+
+        AnsiConsole.Write(root);
+        return 0;
+    }
+}

+ 29 - 0
RackPeek.Web.Viewer/Commands/Routers/Ports/RouterPortAddCommand.cs

@@ -0,0 +1,29 @@
+using Spectre.Console.Cli;
+using System.ComponentModel;
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Routers.Ports;
+using Spectre.Console;
+
+namespace RackPeek.Commands.Routers.Ports;
+
+public class RouterPortAddSettings : RouterNameSettings
+{
+    [CommandOption("--type")] public string? Type { get; set; }
+    [CommandOption("--speed")] public double? Speed { get; set; }
+    [CommandOption("--count")] public int? Count { get; set; }
+}
+
+public class RouterPortAddCommand(IServiceProvider sp)
+    : AsyncCommand<RouterPortAddSettings>
+{
+    public override async Task<int> ExecuteAsync(CommandContext ctx, RouterPortAddSettings s, CancellationToken ct)
+    {
+        using var scope = sp.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddRouterPortUseCase>();
+
+        await useCase.ExecuteAsync(s.Name, s.Type, s.Speed, s.Count);
+
+        AnsiConsole.MarkupLine($"[green]Port added to router '{s.Name}'.[/]");
+        return 0;
+    }
+}

+ 27 - 0
RackPeek.Web.Viewer/Commands/Routers/Ports/RouterPortRemoveCommand.cs

@@ -0,0 +1,27 @@
+using Spectre.Console.Cli;
+using System.ComponentModel;
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Routers.Ports;
+using Spectre.Console;
+
+namespace RackPeek.Commands.Routers.Ports;
+
+public class RouterPortRemoveSettings : RouterNameSettings
+{
+    [CommandOption("--index <INDEX>")] public int Index { get; set; }
+}
+
+public class RouterPortRemoveCommand(IServiceProvider sp)
+    : AsyncCommand<RouterPortRemoveSettings>
+{
+    public override async Task<int> ExecuteAsync(CommandContext ctx, RouterPortRemoveSettings s, CancellationToken ct)
+    {
+        using var scope = sp.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<RemoveRouterPortUseCase>();
+
+        await useCase.ExecuteAsync(s.Name, s.Index);
+
+        AnsiConsole.MarkupLine($"[green]Port {s.Index} removed from router '{s.Name}'.[/]");
+        return 0;
+    }
+}

+ 30 - 0
RackPeek.Web.Viewer/Commands/Routers/Ports/RouterPortUpdateCommand.cs

@@ -0,0 +1,30 @@
+using Spectre.Console.Cli;
+using System.ComponentModel;
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Routers.Ports;
+using Spectre.Console;
+
+namespace RackPeek.Commands.Routers.Ports;
+
+public class RouterPortUpdateSettings : RouterNameSettings
+{
+    [CommandOption("--index <INDEX>")] public int Index { get; set; }
+    [CommandOption("--type")] public string? Type { get; set; }
+    [CommandOption("--speed")] public double? Speed { get; set; }
+    [CommandOption("--count")] public int? Count { get; set; }
+}
+
+public class RouterPortUpdateCommand(IServiceProvider sp)
+    : AsyncCommand<RouterPortUpdateSettings>
+{
+    public override async Task<int> ExecuteAsync(CommandContext ctx, RouterPortUpdateSettings s, CancellationToken ct)
+    {
+        using var scope = sp.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateRouterPortUseCase>();
+
+        await useCase.ExecuteAsync(s.Name, s.Index, s.Type, s.Speed, s.Count);
+
+        AnsiConsole.MarkupLine($"[green]Port {s.Index} updated on router '{s.Name}'.[/]");
+        return 0;
+    }
+}

+ 32 - 0
RackPeek.Web.Viewer/Commands/Routers/RouterAddCommand.cs

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

+ 8 - 0
RackPeek.Web.Viewer/Commands/Routers/RouterCommands.cs

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

+ 25 - 0
RackPeek.Web.Viewer/Commands/Routers/RouterDeleteCommand.cs

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

+ 41 - 0
RackPeek.Web.Viewer/Commands/Routers/RouterDescribeCommand.cs

@@ -0,0 +1,41 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Routers;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Routers;
+
+public class RouterDescribeCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<RouterNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        RouterNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DescribeRouterUseCase>();
+
+        var sw = await useCase.ExecuteAsync(settings.Name);
+
+        var grid = new Grid()
+            .AddColumn(new GridColumn().NoWrap())
+            .AddColumn(new GridColumn().NoWrap());
+
+        grid.AddRow("Name:", sw.Name);
+        grid.AddRow("Model:", sw.Model ?? "Unknown");
+        grid.AddRow("Managed:", sw.Managed.HasValue ? sw.Managed.Value ? "Yes" : "No" : "Unknown");
+        grid.AddRow("PoE:", sw.Poe.HasValue ? sw.Poe.Value ? "Yes" : "No" : "Unknown");
+        grid.AddRow("Total Ports:", sw.TotalPorts.ToString());
+        grid.AddRow("Total Speed (Gb):", sw.TotalSpeedGb.ToString());
+        grid.AddRow("Ports:", sw.PortSummary);
+
+        AnsiConsole.Write(
+            new Panel(grid)
+                .Header("Router")
+                .Border(BoxBorder.Rounded));
+
+        return 0;
+    }
+}

+ 27 - 0
RackPeek.Web.Viewer/Commands/Routers/RouterGetByNameCommand.cs

@@ -0,0 +1,27 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Routers;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Routers;
+
+public class RouterGetByNameCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<RouterNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        RouterNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DescribeRouterUseCase>();
+
+        var sw = await useCase.ExecuteAsync(settings.Name);
+
+        AnsiConsole.MarkupLine(
+            $"[green]{sw.Name}[/]  Model: {sw.Model ?? "Unknown"}, Managed: {(sw.Managed == true ? "Yes" : "No")}, PoE: {(sw.Poe == true ? "Yes" : "No")}");
+
+        return 0;
+    }
+}

+ 49 - 0
RackPeek.Web.Viewer/Commands/Routers/RouterGetCommand.cs

@@ -0,0 +1,49 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Routers;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Routers;
+
+public class RouterGetCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<RouterHardwareReportUseCase>();
+
+        var report = await useCase.ExecuteAsync();
+
+        if (report.Routers.Count == 0)
+        {
+            AnsiConsole.MarkupLine("[yellow]No routers found.[/]");
+            return 0;
+        }
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .AddColumn("Name")
+            .AddColumn("Model")
+            .AddColumn("Managed")
+            .AddColumn("PoE")
+            .AddColumn("Ports")
+            .AddColumn("Port Summary");
+
+        foreach (var s in report.Routers)
+            table.AddRow(
+                s.Name,
+                s.Model ?? "Unknown",
+                s.Managed ? "[green]yes[/]" : "[red]no[/]",
+                s.Poe ? "[green]yes[/]" : "[red]no[/]",
+                s.TotalPorts.ToString(),
+                s.PortSummary
+            );
+
+        AnsiConsole.Write(table);
+        return 0;
+    }
+}

+ 51 - 0
RackPeek.Web.Viewer/Commands/Routers/RouterReportCommand.cs

@@ -0,0 +1,51 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using RackPeek.Domain.Resources.Hardware.Routers;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Routers;
+
+public class RouterReportCommand(
+    ILogger<RouterReportCommand> logger,
+    IServiceProvider serviceProvider
+) : AsyncCommand
+{
+    public override async Task<int> ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<RouterHardwareReportUseCase>();
+
+        var report = await useCase.ExecuteAsync();
+
+        if (report.Routers.Count == 0)
+        {
+            AnsiConsole.MarkupLine("[yellow]No Routers found.[/]");
+            return 0;
+        }
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .AddColumn("Name")
+            .AddColumn("Model")
+            .AddColumn("Managed")
+            .AddColumn("PoE")
+            .AddColumn("Ports")
+            .AddColumn("Max Speed")
+            .AddColumn("Port Summary");
+
+        foreach (var s in report.Routers)
+            table.AddRow(
+                s.Name,
+                s.Model,
+                s.Managed ? "[green]yes[/]" : "[red]no[/]",
+                s.Poe ? "[green]yes[/]" : "[red]no[/]",
+                s.TotalPorts.ToString(),
+                $"{s.MaxPortSpeedGb}G",
+                s.PortSummary
+            );
+
+        AnsiConsole.Write(table);
+        return 0;
+    }
+}

+ 39 - 0
RackPeek.Web.Viewer/Commands/Routers/RouterSetCommand.cs

@@ -0,0 +1,39 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Commands.Servers;
+using RackPeek.Domain.Resources.Hardware.Routers;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Routers;
+
+public class RouterSetSettings : ServerNameSettings
+{
+    [CommandOption("--Model")] public string Model { get; set; } = default!;
+
+    [CommandOption("--managed")] public bool Managed { get; set; }
+
+    [CommandOption("--poe")] public bool Poe { get; set; }
+}
+
+public class RouterSetCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<RouterSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        RouterSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateRouterUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Model,
+            settings.Managed,
+            settings.Poe);
+
+        AnsiConsole.MarkupLine($"[green]Server '{settings.Name}' updated.[/]");
+        return 0;
+    }
+}

+ 38 - 0
RackPeek.Web.Viewer/Commands/Servers/Cpus/ServerCpuAddCommand.cs

@@ -0,0 +1,38 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Servers.Cpus;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Servers.Cpus;
+
+public class ServerCpuAddSettings : ServerNameSettings
+{
+    [CommandOption("--model <MODEL>")] public string Model { get; set; }
+
+    [CommandOption("--cores <CORES>")] public int Cores { get; set; }
+
+    [CommandOption("--threads <THREADS>")] public int Threads { get; set; }
+}
+
+public class ServerCpuAddCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<ServerCpuAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerCpuAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddCpuUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Model,
+            settings.Cores,
+            settings.Threads);
+
+        AnsiConsole.MarkupLine($"[green]CPU added to '{settings.Name}'.[/]");
+        return 0;
+    }
+}

+ 30 - 0
RackPeek.Web.Viewer/Commands/Servers/Cpus/ServerCpuRemoveCommand.cs

@@ -0,0 +1,30 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Servers.Cpus;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Servers.Cpus;
+
+public class ServerCpuRemoveSettings : ServerNameSettings
+{
+    [CommandOption("--index <INDEX>")] public int Index { get; set; }
+}
+
+public class ServerCpuRemoveCommand(IServiceProvider serviceProvider) : AsyncCommand<ServerCpuRemoveSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerCpuRemoveSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<RemoveCpuUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Index);
+
+        AnsiConsole.MarkupLine($"[green]CPU {settings.Index} removed from '{settings.Name}'.[/]");
+        return 0;
+    }
+}

+ 39 - 0
RackPeek.Web.Viewer/Commands/Servers/Cpus/ServerCpuSetCommand.cs

@@ -0,0 +1,39 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Servers.Cpus;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Servers.Cpus;
+
+public class ServerCpuSetSettings : ServerNameSettings
+{
+    [CommandOption("--index <INDEX>")] public int Index { get; set; }
+
+    [CommandOption("--model <MODEL>")] public string Model { get; set; }
+
+    [CommandOption("--cores <CORES>")] public int Cores { get; set; }
+
+    [CommandOption("--threads <THREADS>")] public int Threads { get; set; }
+}
+
+public class ServerCpuSetCommand(IServiceProvider serviceProvider) : AsyncCommand<ServerCpuSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerCpuSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateCpuUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Index,
+            settings.Model,
+            settings.Cores,
+            settings.Threads);
+
+        AnsiConsole.MarkupLine($"[green]CPU {settings.Index} updated on '{settings.Name}'.[/]");
+        return 0;
+    }
+}

+ 34 - 0
RackPeek.Web.Viewer/Commands/Servers/Drives/ServerDriveAddCommand.cs

@@ -0,0 +1,34 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Servers.Drives;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Servers.Drives;
+
+public class ServerDriveAddSettings : ServerNameSettings
+{
+    [CommandOption("--type <TYPE>")] public string Type { get; set; }
+
+    [CommandOption("--size <SIZE>")] public int Size { get; set; }
+}
+
+public class ServerDriveAddCommand(IServiceProvider serviceProvider)
+    : AsyncCommand<ServerDriveAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerDriveAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddDrivesUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Type,
+            settings.Size);
+
+        AnsiConsole.MarkupLine($"[green]Drive added to '{settings.Name}'.[/]");
+        return 0;
+    }
+}

+ 31 - 0
RackPeek.Web.Viewer/Commands/Servers/Drives/ServerDriveRemoveCommand.cs

@@ -0,0 +1,31 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Servers.Drives;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Servers.Drives;
+
+public class ServerDriveRemoveSettings : ServerNameSettings
+{
+    [CommandOption("--index <INDEX>")] public int Index { get; set; }
+}
+
+public class ServerDriveRemoveCommand(IServiceProvider serviceProvider)
+    : AsyncCommand<ServerDriveRemoveSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerDriveRemoveSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<RemoveDriveUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Index);
+
+        AnsiConsole.MarkupLine($"[green]Drive {settings.Index} removed from '{settings.Name}'.[/]");
+        return 0;
+    }
+}

+ 37 - 0
RackPeek.Web.Viewer/Commands/Servers/Drives/ServerDriveUpdateCommand.cs

@@ -0,0 +1,37 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Servers.Drives;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Servers.Drives;
+
+public class ServerDriveUpdateSettings : ServerNameSettings
+{
+    [CommandOption("--index <INDEX>")] public int Index { get; set; }
+
+    [CommandOption("--type <TYPE>")] public string Type { get; set; }
+
+    [CommandOption("--size <SIZE>")] public int Size { get; set; }
+}
+
+public class ServerDriveUpdateCommand(IServiceProvider serviceProvider)
+    : AsyncCommand<ServerDriveUpdateSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerDriveUpdateSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateDriveUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Index,
+            settings.Type,
+            settings.Size);
+
+        AnsiConsole.MarkupLine($"[green]Drive {settings.Index} updated on '{settings.Name}'.[/]");
+        return 0;
+    }
+}

+ 34 - 0
RackPeek.Web.Viewer/Commands/Servers/Gpus/AddGpuUseCaseCommand.cs

@@ -0,0 +1,34 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Servers.Gpus;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Servers.Gpus;
+
+public class ServerGpuAddSettings : ServerNameSettings
+{
+    [CommandOption("--model <MODEL>")] public string Model { get; set; }
+
+    [CommandOption("--vram <VRAM>")] public int Vram { get; set; }
+}
+
+public class ServerGpuAddCommand(IServiceProvider serviceProvider)
+    : AsyncCommand<ServerGpuAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerGpuAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddGpuUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Model,
+            settings.Vram);
+
+        AnsiConsole.MarkupLine($"[green]GPU added to '{settings.Name}'.[/]");
+        return 0;
+    }
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов