CliBootstrap.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. using System.ComponentModel.DataAnnotations;
  2. using Microsoft.Extensions.Configuration;
  3. using Microsoft.Extensions.DependencyInjection;
  4. using RackPeek.Domain;
  5. using RackPeek.Domain.Helpers;
  6. using RackPeek.Domain.Persistence;
  7. using RackPeek.Domain.Persistence.Yaml;
  8. using Shared.Rcl.Commands;
  9. using Shared.Rcl.Commands.AccessPoints;
  10. using Shared.Rcl.Commands.Desktops;
  11. using Shared.Rcl.Commands.Desktops.Cpus;
  12. using Shared.Rcl.Commands.Desktops.Drive;
  13. using Shared.Rcl.Commands.Desktops.Gpus;
  14. using Shared.Rcl.Commands.Desktops.Nics;
  15. using Shared.Rcl.Commands.Firewalls;
  16. using Shared.Rcl.Commands.Firewalls.Ports;
  17. using Shared.Rcl.Commands.Laptops;
  18. using Shared.Rcl.Commands.Laptops.Cpus;
  19. using Shared.Rcl.Commands.Laptops.Drive;
  20. using Shared.Rcl.Commands.Laptops.Gpus;
  21. using Shared.Rcl.Commands.Routers;
  22. using Shared.Rcl.Commands.Routers.Ports;
  23. using Shared.Rcl.Commands.Servers;
  24. using Shared.Rcl.Commands.Servers.Cpus;
  25. using Shared.Rcl.Commands.Servers.Drives;
  26. using Shared.Rcl.Commands.Servers.Gpus;
  27. using Shared.Rcl.Commands.Servers.Nics;
  28. using Shared.Rcl.Commands.Services;
  29. using Shared.Rcl.Commands.Switches;
  30. using Shared.Rcl.Commands.Switches.Ports;
  31. using Shared.Rcl.Commands.Systems;
  32. using Shared.Rcl.Commands.Ups;
  33. using Spectre.Console;
  34. using Spectre.Console.Cli;
  35. namespace Shared.Rcl;
  36. public static class CliBootstrap
  37. {
  38. private static string[]? _lastArgs;
  39. private static CommandApp? _app;
  40. private static bool _showingHelp;
  41. public static void SetContext(string[] args, CommandApp app)
  42. {
  43. _lastArgs = args;
  44. _app = app;
  45. }
  46. public static async Task RegisterInternals(
  47. IServiceCollection services,
  48. IConfiguration configuration,
  49. string yamlDir,
  50. string yamlFile)
  51. {
  52. services.AddSingleton(configuration);
  53. var appBasePath = AppContext.BaseDirectory;
  54. var resolvedYamlDir = Path.IsPathRooted(yamlDir)
  55. ? yamlDir
  56. : Path.Combine(appBasePath, yamlDir);
  57. Directory.CreateDirectory(resolvedYamlDir);
  58. var fullYamlPath = Path.Combine(resolvedYamlDir, yamlFile);
  59. if (!File.Exists(fullYamlPath)) await File.WriteAllTextAsync(fullYamlPath, "");
  60. var collection = new YamlResourceCollection(
  61. fullYamlPath,
  62. new PhysicalTextFileStore(),
  63. new ResourceCollection());
  64. await collection.LoadAsync();
  65. services.AddSingleton<IResourceCollection>(collection);
  66. // Infrastructure
  67. services.AddYamlRepos();
  68. // Application
  69. services.AddUseCases();
  70. services.AddCommands();
  71. }
  72. public static void BuildApp(CommandApp app)
  73. {
  74. // Spectre bootstrap
  75. app.Configure(config =>
  76. {
  77. config.SetApplicationName("rpk");
  78. config.ValidateExamples();
  79. config.SetApplicationVersion("0.0.3");
  80. config.SetExceptionHandler(HandleException);
  81. // Global summary
  82. config.AddCommand<GetTotalSummaryCommand>("summary")
  83. .WithDescription("Show a summarized report of all resources in the system.");
  84. // ----------------------------
  85. // Server commands (CRUD-style)
  86. // ----------------------------
  87. config.AddBranch("servers", server =>
  88. {
  89. server.SetDescription("Manage servers and their components.");
  90. server.AddCommand<ServerReportCommand>("summary")
  91. .WithDescription("Show a summarized hardware report for all servers.");
  92. server.AddCommand<ServerAddCommand>("add").WithDescription("Add a new server to the inventory.");
  93. server.AddCommand<ServerGetByNameCommand>("get")
  94. .WithDescription("List all servers or retrieve a specific server by name.");
  95. server.AddCommand<ServerDescribeCommand>("describe")
  96. .WithDescription("Display detailed information about a specific server.");
  97. server.AddCommand<ServerSetCommand>("set").WithDescription("Update properties of an existing server.");
  98. server.AddCommand<ServerDeleteCommand>("del").WithDescription("Delete a server from the inventory.");
  99. server.AddCommand<ServerTreeCommand>("tree")
  100. .WithDescription("Display the dependency tree of a server.");
  101. // Server CPUs
  102. server.AddBranch("cpu", cpu =>
  103. {
  104. cpu.SetDescription("Manage CPUs attached to a server.");
  105. cpu.AddCommand<ServerCpuAddCommand>("add").WithDescription("Add a CPU to a specific server.");
  106. cpu.AddCommand<ServerCpuSetCommand>("set").WithDescription("Update configuration of a server CPU.");
  107. cpu.AddCommand<ServerCpuRemoveCommand>("del").WithDescription("Remove a CPU from a server.");
  108. });
  109. // Server Drives
  110. server.AddBranch("drive", drive =>
  111. {
  112. drive.SetDescription("Manage drives attached to a server.");
  113. drive.AddCommand<ServerDriveAddCommand>("add").WithDescription("Add a storage drive to a server.");
  114. drive.AddCommand<ServerDriveUpdateCommand>("set")
  115. .WithDescription("Update properties of a server drive.");
  116. drive.AddCommand<ServerDriveRemoveCommand>("del").WithDescription("Remove a drive from a server.");
  117. });
  118. // Server GPUs
  119. server.AddBranch("gpu", gpu =>
  120. {
  121. gpu.SetDescription("Manage GPUs attached to a server.");
  122. gpu.AddCommand<ServerGpuAddCommand>("add").WithDescription("Add a GPU to a server.");
  123. gpu.AddCommand<ServerGpuUpdateCommand>("set").WithDescription("Update properties of a server GPU.");
  124. gpu.AddCommand<ServerGpuRemoveCommand>("del").WithDescription("Remove a GPU from a server.");
  125. });
  126. // Server NICs
  127. server.AddBranch("nic", nic =>
  128. {
  129. nic.SetDescription("Manage network interface cards (NICs) for a server.");
  130. nic.AddCommand<ServerNicAddCommand>("add").WithDescription("Add a NIC to a server.");
  131. nic.AddCommand<ServerNicUpdateCommand>("set").WithDescription("Update properties of a server NIC.");
  132. nic.AddCommand<ServerNicRemoveCommand>("del").WithDescription("Remove a NIC from a server.");
  133. });
  134. });
  135. // ----------------------------
  136. // Switch commands
  137. // ----------------------------
  138. config.AddBranch("switches", switches =>
  139. {
  140. switches.SetDescription("Manage network switches.");
  141. switches.AddCommand<SwitchReportCommand>("summary")
  142. .WithDescription("Show a hardware report for all switches.");
  143. switches.AddCommand<SwitchAddCommand>("add")
  144. .WithDescription("Add a new network switch to the inventory.");
  145. switches.AddCommand<SwitchGetCommand>("list").WithDescription("List all switches in the system.");
  146. switches.AddCommand<SwitchGetByNameCommand>("get")
  147. .WithDescription("Retrieve details of a specific switch by name.");
  148. switches.AddCommand<SwitchDescribeCommand>("describe")
  149. .WithDescription("Show detailed information about a switch.");
  150. switches.AddCommand<SwitchSetCommand>("set").WithDescription("Update properties of a switch.");
  151. switches.AddCommand<SwitchDeleteCommand>("del").WithDescription("Delete a switch from the inventory.");
  152. switches.AddBranch("port", port =>
  153. {
  154. port.SetDescription("Manage ports on a network switch.");
  155. port.AddCommand<SwitchPortAddCommand>("add").WithDescription("Add a port to a switch.");
  156. port.AddCommand<SwitchPortUpdateCommand>("set").WithDescription("Update a switch port.");
  157. port.AddCommand<SwitchPortRemoveCommand>("del").WithDescription("Remove a port from a switch.");
  158. });
  159. });
  160. // ----------------------------
  161. // Routers commands
  162. // ----------------------------
  163. config.AddBranch("routers", routers =>
  164. {
  165. routers.SetDescription("Manage network routers.");
  166. routers.AddCommand<RouterReportCommand>("summary")
  167. .WithDescription("Show a hardware report for all routers.");
  168. routers.AddCommand<RouterAddCommand>("add")
  169. .WithDescription("Add a new network router to the inventory.");
  170. routers.AddCommand<RouterGetCommand>("list").WithDescription("List all routers in the system.");
  171. routers.AddCommand<RouterGetByNameCommand>("get")
  172. .WithDescription("Retrieve details of a specific router by name.");
  173. routers.AddCommand<RouterDescribeCommand>("describe")
  174. .WithDescription("Show detailed information about a router.");
  175. routers.AddCommand<RouterSetCommand>("set").WithDescription("Update properties of a router.");
  176. routers.AddCommand<RouterDeleteCommand>("del").WithDescription("Delete a router from the inventory.");
  177. routers.AddBranch("port", port =>
  178. {
  179. port.SetDescription("Manage ports on a router.");
  180. port.AddCommand<RouterPortAddCommand>("add").WithDescription("Add a port to a router.");
  181. port.AddCommand<RouterPortUpdateCommand>("set").WithDescription("Update a router port.");
  182. port.AddCommand<RouterPortRemoveCommand>("del").WithDescription("Remove a port from a router.");
  183. });
  184. });
  185. // ----------------------------
  186. // Firewalls commands
  187. // ----------------------------
  188. config.AddBranch("firewalls", firewalls =>
  189. {
  190. firewalls.SetDescription("Manage firewalls.");
  191. firewalls.AddCommand<FirewallReportCommand>("summary")
  192. .WithDescription("Show a hardware report for all firewalls.");
  193. firewalls.AddCommand<FirewallAddCommand>("add").WithDescription("Add a new firewall to the inventory.");
  194. firewalls.AddCommand<FirewallGetCommand>("list").WithDescription("List all firewalls in the system.");
  195. firewalls.AddCommand<FirewallGetByNameCommand>("get")
  196. .WithDescription("Retrieve details of a specific firewall by name.");
  197. firewalls.AddCommand<FirewallDescribeCommand>("describe")
  198. .WithDescription("Show detailed information about a firewall.");
  199. firewalls.AddCommand<FirewallSetCommand>("set").WithDescription("Update properties of a firewall.");
  200. firewalls.AddCommand<FirewallDeleteCommand>("del")
  201. .WithDescription("Delete a firewall from the inventory.");
  202. firewalls.AddBranch("port", port =>
  203. {
  204. port.SetDescription("Manage ports on a firewall.");
  205. port.AddCommand<FirewallPortAddCommand>("add").WithDescription("Add a port to a firewall.");
  206. port.AddCommand<FirewallPortUpdateCommand>("set").WithDescription("Update a firewall port.");
  207. port.AddCommand<FirewallPortRemoveCommand>("del").WithDescription("Remove a port from a firewall.");
  208. });
  209. });
  210. // ----------------------------
  211. // System commands
  212. // ----------------------------
  213. config.AddBranch("systems", system =>
  214. {
  215. system.SetDescription("Manage systems and their dependencies.");
  216. system.AddCommand<SystemReportCommand>("summary")
  217. .WithDescription("Show a summary report for all systems.");
  218. system.AddCommand<SystemAddCommand>("add").WithDescription("Add a new system to the inventory.");
  219. system.AddCommand<SystemGetCommand>("list").WithDescription("List all systems.");
  220. system.AddCommand<SystemGetByNameCommand>("get").WithDescription("Retrieve a system by name.");
  221. system.AddCommand<SystemDescribeCommand>("describe")
  222. .WithDescription("Display detailed information about a system.");
  223. system.AddCommand<SystemSetCommand>("set").WithDescription("Update properties of a system.");
  224. system.AddCommand<SystemDeleteCommand>("del").WithDescription("Delete a system from the inventory.");
  225. system.AddCommand<SystemTreeCommand>("tree")
  226. .WithDescription("Display the dependency tree for a system.");
  227. });
  228. // ----------------------------
  229. // Access Points
  230. // ----------------------------
  231. config.AddBranch("accesspoints", ap =>
  232. {
  233. ap.SetDescription("Manage access points.");
  234. ap.AddCommand<AccessPointReportCommand>("summary")
  235. .WithDescription("Show a hardware report for all access points.");
  236. ap.AddCommand<AccessPointAddCommand>("add").WithDescription("Add a new access point.");
  237. ap.AddCommand<AccessPointGetCommand>("list").WithDescription("List all access points.");
  238. ap.AddCommand<AccessPointGetByNameCommand>("get").WithDescription("Retrieve an access point by name.");
  239. ap.AddCommand<AccessPointDescribeCommand>("describe")
  240. .WithDescription("Show detailed information about an access point.");
  241. ap.AddCommand<AccessPointSetCommand>("set").WithDescription("Update properties of an access point.");
  242. ap.AddCommand<AccessPointDeleteCommand>("del").WithDescription("Delete an access point.");
  243. });
  244. // ----------------------------
  245. // UPS units
  246. // ----------------------------
  247. config.AddBranch("ups", ups =>
  248. {
  249. ups.SetDescription("Manage UPS units.");
  250. ups.AddCommand<UpsReportCommand>("summary")
  251. .WithDescription("Show a hardware report for all UPS units.");
  252. ups.AddCommand<UpsAddCommand>("add").WithDescription("Add a new UPS unit.");
  253. ups.AddCommand<UpsGetCommand>("list").WithDescription("List all UPS units.");
  254. ups.AddCommand<UpsGetByNameCommand>("get").WithDescription("Retrieve a UPS unit by name.");
  255. ups.AddCommand<UpsDescribeCommand>("describe")
  256. .WithDescription("Show detailed information about a UPS unit.");
  257. ups.AddCommand<UpsSetCommand>("set").WithDescription("Update properties of a UPS unit.");
  258. ups.AddCommand<UpsDeleteCommand>("del").WithDescription("Delete a UPS unit.");
  259. });
  260. // ----------------------------
  261. // Desktops
  262. // ----------------------------
  263. config.AddBranch("desktops", desktops =>
  264. {
  265. desktops.SetDescription("Manage desktop computers and their components.");
  266. // CRUD
  267. desktops.AddCommand<DesktopAddCommand>("add").WithDescription("Add a new desktop.");
  268. desktops.AddCommand<DesktopGetCommand>("list").WithDescription("List all desktops.");
  269. desktops.AddCommand<DesktopGetByNameCommand>("get").WithDescription("Retrieve a desktop by name.");
  270. desktops.AddCommand<DesktopDescribeCommand>("describe")
  271. .WithDescription("Show detailed information about a desktop.");
  272. desktops.AddCommand<DesktopSetCommand>("set").WithDescription("Update properties of a desktop.");
  273. desktops.AddCommand<DesktopDeleteCommand>("del")
  274. .WithDescription("Delete a desktop from the inventory.");
  275. desktops.AddCommand<DesktopReportCommand>("summary")
  276. .WithDescription("Show a summarized hardware report for all desktops.");
  277. desktops.AddCommand<DesktopTreeCommand>("tree")
  278. .WithDescription("Display the dependency tree for a desktop.");
  279. // CPU
  280. desktops.AddBranch("cpu", cpu =>
  281. {
  282. cpu.SetDescription("Manage CPUs attached to desktops.");
  283. cpu.AddCommand<DesktopCpuAddCommand>("add").WithDescription("Add a CPU to a desktop.");
  284. cpu.AddCommand<DesktopCpuSetCommand>("set").WithDescription("Update a desktop CPU.");
  285. cpu.AddCommand<DesktopCpuRemoveCommand>("del").WithDescription("Remove a CPU from a desktop.");
  286. });
  287. // Drives
  288. desktops.AddBranch("drive", drive =>
  289. {
  290. drive.SetDescription("Manage storage drives attached to desktops.");
  291. drive.AddCommand<DesktopDriveAddCommand>("add").WithDescription("Add a drive to a desktop.");
  292. drive.AddCommand<DesktopDriveSetCommand>("set").WithDescription("Update a desktop drive.");
  293. drive.AddCommand<DesktopDriveRemoveCommand>("del")
  294. .WithDescription("Remove a drive from a desktop.");
  295. });
  296. // GPUs
  297. desktops.AddBranch("gpu", gpu =>
  298. {
  299. gpu.SetDescription("Manage GPUs attached to desktops.");
  300. gpu.AddCommand<DesktopGpuAddCommand>("add").WithDescription("Add a GPU to a desktop.");
  301. gpu.AddCommand<DesktopGpuSetCommand>("set").WithDescription("Update a desktop GPU.");
  302. gpu.AddCommand<DesktopGpuRemoveCommand>("del").WithDescription("Remove a GPU from a desktop.");
  303. });
  304. // NICs
  305. desktops.AddBranch("nic", nic =>
  306. {
  307. nic.SetDescription("Manage network interface cards (NICs) for desktops.");
  308. nic.AddCommand<DesktopNicAddCommand>("add").WithDescription("Add a NIC to a desktop.");
  309. nic.AddCommand<DesktopNicSetCommand>("set").WithDescription("Update a desktop NIC.");
  310. nic.AddCommand<DesktopNicRemoveCommand>("del").WithDescription("Remove a NIC from a desktop.");
  311. });
  312. });
  313. // ----------------------------
  314. // Laptops
  315. // ----------------------------
  316. config.AddBranch("laptops", laptops =>
  317. {
  318. laptops.SetDescription("Manage Laptop computers and their components.");
  319. // CRUD
  320. laptops.AddCommand<LaptopAddCommand>("add").WithDescription("Add a new Laptop.");
  321. laptops.AddCommand<LaptopGetCommand>("list").WithDescription("List all Laptops.");
  322. laptops.AddCommand<LaptopGetByNameCommand>("get").WithDescription("Retrieve a Laptop by name.");
  323. laptops.AddCommand<LaptopDescribeCommand>("describe")
  324. .WithDescription("Show detailed information about a Laptop.");
  325. laptops.AddCommand<LaptopDeleteCommand>("del").WithDescription("Delete a Laptop from the inventory.");
  326. laptops.AddCommand<LaptopReportCommand>("summary")
  327. .WithDescription("Show a summarized hardware report for all Laptops.");
  328. laptops.AddCommand<LaptopTreeCommand>("tree")
  329. .WithDescription("Display the dependency tree for a Laptop.");
  330. // CPU
  331. laptops.AddBranch("cpu", cpu =>
  332. {
  333. cpu.SetDescription("Manage CPUs attached to Laptops.");
  334. cpu.AddCommand<LaptopCpuAddCommand>("add").WithDescription("Add a CPU to a Laptop.");
  335. cpu.AddCommand<LaptopCpuSetCommand>("set").WithDescription("Update a Laptop CPU.");
  336. cpu.AddCommand<LaptopCpuRemoveCommand>("del").WithDescription("Remove a CPU from a Laptop.");
  337. });
  338. // Drives
  339. laptops.AddBranch("drives", drives =>
  340. {
  341. drives.SetDescription("Manage storage drives attached to Laptops.");
  342. drives.AddCommand<LaptopDriveAddCommand>("add").WithDescription("Add a drive to a Laptop.");
  343. drives.AddCommand<LaptopDriveSetCommand>("set").WithDescription("Update a Laptop drive.");
  344. drives.AddCommand<LaptopDriveRemoveCommand>("del").WithDescription("Remove a drive from a Laptop.");
  345. });
  346. // GPUs
  347. laptops.AddBranch("gpu", gpu =>
  348. {
  349. gpu.SetDescription("Manage GPUs attached to Laptops.");
  350. gpu.AddCommand<LaptopGpuAddCommand>("add").WithDescription("Add a GPU to a Laptop.");
  351. gpu.AddCommand<LaptopGpuSetCommand>("set").WithDescription("Update a Laptop GPU.");
  352. gpu.AddCommand<LaptopGpuRemoveCommand>("del").WithDescription("Remove a GPU from a Laptop.");
  353. });
  354. });
  355. // ----------------------------
  356. // Services
  357. // ----------------------------
  358. config.AddBranch("services", service =>
  359. {
  360. service.SetDescription("Manage services and their configurations.");
  361. service.AddCommand<ServiceReportCommand>("summary")
  362. .WithDescription("Show a summary report for all services.");
  363. service.AddCommand<ServiceAddCommand>("add").WithDescription("Add a new service.");
  364. service.AddCommand<ServiceGetCommand>("list").WithDescription("List all services.");
  365. service.AddCommand<ServiceGetByNameCommand>("get").WithDescription("Retrieve a service by name.");
  366. service.AddCommand<ServiceDescribeCommand>("describe")
  367. .WithDescription("Show detailed information about a service.");
  368. service.AddCommand<ServiceSetCommand>("set").WithDescription("Update properties of a service.");
  369. service.AddCommand<ServiceDeleteCommand>("del").WithDescription("Delete a service.");
  370. service.AddCommand<ServiceSubnetsCommand>("subnets")
  371. .WithDescription("List subnets associated with a service, optionally filtered by CIDR.");
  372. });
  373. });
  374. }
  375. private static int HandleException(Exception ex, ITypeResolver? arg2)
  376. {
  377. switch (ex)
  378. {
  379. case ValidationException ve:
  380. AnsiConsole.MarkupLine($"[yellow]Validation error:[/] {ve.Message}");
  381. return 2;
  382. case ConflictException ce:
  383. AnsiConsole.MarkupLine($"[red]Conflict:[/] {ce.Message}");
  384. return 3;
  385. case NotFoundException ne:
  386. AnsiConsole.MarkupLine($"[red]Not found:[/] {ne.Message}");
  387. return 4;
  388. case CommandParseException pe:
  389. if (_showingHelp) return 1; // suppress errors during help lookup
  390. AnsiConsole.MarkupLine($"[red]Invalid command:[/] {pe.Message}");
  391. if (pe.Pretty != null) AnsiConsole.Write(pe.Pretty);
  392. ShowContextualHelp();
  393. return 1;
  394. case CommandRuntimeException re:
  395. if (_showingHelp) return 1;
  396. AnsiConsole.MarkupLine($"[red]Error:[/] {re.Message}");
  397. if (re.Pretty != null) AnsiConsole.Write(re.Pretty);
  398. ShowContextualHelp();
  399. return 1;
  400. default:
  401. AnsiConsole.MarkupLine("[red]Unexpected error occurred.[/]");
  402. AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything);
  403. return 99;
  404. }
  405. }
  406. private static void ShowContextualHelp()
  407. {
  408. if (_lastArgs == null || _app == null || _showingHelp) return;
  409. _showingHelp = true;
  410. try
  411. {
  412. // Extract command path (args before any --flags)
  413. var commandPath = _lastArgs.TakeWhile(a => !a.StartsWith("-")).ToList();
  414. // Try progressively shorter command paths until --help succeeds
  415. while (commandPath.Count > 0)
  416. {
  417. var helpArgs = commandPath.Append("--help").ToArray();
  418. AnsiConsole.WriteLine();
  419. var result = _app.Run(helpArgs);
  420. if (result == 0) return;
  421. commandPath.RemoveAt(commandPath.Count - 1);
  422. }
  423. }
  424. finally
  425. {
  426. _showingHelp = false;
  427. }
  428. }
  429. }