Skip to main content

AirLibrary/CLI/
mod.rs

1//! # CLI - Command Line Interface
2//!
3//! ## Responsibilities
4//!
5//! This module provides the comprehensive command-line interface for the Air
6//! daemon, serving as the primary interface for users and administrators to
7//! interact with a running Air instance. The CLI is responsible for:
8//!
9//! - **Command Parsing and Validation**: Parsing command-line arguments,
10//!   validating inputs, and providing helpful error messages for invalid
11//!   commands or arguments
12//! - **Command Routing**: Routing commands to the appropriate handlers and
13//!   executing them
14//! - **Configuration Management**: Reading, setting, validating, and reloading
15//!   configuration
16//! - **Status and Health Monitoring**: Querying daemon status, service health,
17//!   and metrics
18//! - **Log Management**: Viewing and filtering daemon and service logs
19//! - **Debugging and Diagnostics**: Providing tools for debugging and
20//!   diagnosing issues
21//! - **Output Formatting**: Presenting output in human-readable (table, plain)
22//!   or machine-readable (JSON) formats
23//! - **Daemon Communication**: Establishing and managing connections to the
24//!   running Air daemon
25//! - **Permission Management**: Enforcing security and permission checks for
26//!   sensitive operations
27//!
28//! ## VSCode CLI Patterns
29//!
30//! This implementation draws inspiration from VSCode's CLI architecture:
31//! - Reference: vs/platform/environment/common/environment.ts
32//! - Reference: vs/platform/remote/common/remoteAgentConnection.ts
33//!
34//! Patterns adopted from VSCode CLI:
35//! - Subcommand hierarchy with nested commands and options
36//! - Multiple output formats (JSON, human-readable)
37//! - Comprehensive help system with per-command documentation
38//! - Status and health check capabilities
39//! - Configuration management with validation
40//! - Service-specific operations
41//! - Connection management to running daemon processes
42//! - Extension/plugin compatibility with the daemon
43//!
44//! ## FUTURE Enhancements
45//!
46//! - **Plugin Marketplace Integration**: Add commands for discovering,
47//! installing, and managing plugins from a central marketplace (similar to
48//! `code --install-extension`)
49//! - **Hot Reload Support**: Implement hot reload of configuration and plugins
50//! without daemon restart
51//! - **Sandboxing Mode**: Add a sandboxed mode for running commands with
52//! restricted permissions
53//! - **Interactive Shell**: Implement an interactive shell mode for continuous
54//! daemon interaction
55//! - **Completion Scripts**: Generate shell completion scripts (bash, zsh,
56//! fish) for better UX
57//! - **Profile Management**: Support multiple configuration profiles for
58//! different environments
59//! - **Remote Management**: Add support for managing remote Air instances via
60//! SSH/IPC
61//! - **Audit Logging**: Add comprehensive audit logging for all administrative
62//!   actions
63//!
64//! ## Security Considerations
65//!
66//! - Admin commands (restart, config set) require elevated privileges
67//! - Daemon communication uses secure IPC channels
68//! - Sensitive information is masked in logs and error messages
69//! - Timeouts prevent hanging on unresponsive daemon
70
71pub mod CommandTypes;
72
73pub mod ResponseTypes;
74
75use std::{collections::HashMap, time::Duration};
76
77use serde::{Deserialize, Serialize};
78use chrono::{DateTime, Utc};
79
80use crate::dev_log;
81
82// =============================================================================
83// Command Types
84// =============================================================================
85
86/// Main CLI command enum
87#[derive(Debug, Clone)]
88pub enum Command {
89	/// Status command - check daemon and service status
90	Status { service:Option<String>, verbose:bool, json:bool },
91
92	/// Restart command - restart services
93	Restart { service:Option<String>, force:bool },
94
95	/// Configuration commands
96	Config(ConfigCommand),
97
98	/// Metrics command - retrieve performance metrics
99	Metrics { json:bool, service:Option<String> },
100
101	/// Logs command - view daemon logs
102	Logs { service:Option<String>, tail:Option<usize>, filter:Option<String>, follow:bool },
103
104	/// Debug commands
105	Debug(DebugCommand),
106
107	/// Help command
108	Help { command:Option<String> },
109
110	/// Version command
111	Version,
112}
113
114/// Configuration subcommands
115#[derive(Debug, Clone)]
116pub enum ConfigCommand {
117	/// Get configuration value
118	Get { key:String },
119
120	/// Set configuration value
121	Set { key:String, value:String },
122
123	/// Reload configuration from file
124	Reload { validate:bool },
125
126	/// Show current configuration
127	Show { json:bool },
128
129	/// Validate configuration
130	Validate { path:Option<String> },
131}
132
133/// Debug subcommands
134#[derive(Debug, Clone)]
135pub enum DebugCommand {
136	/// Dump current daemon state
137	DumpState { service:Option<String>, json:bool },
138
139	/// Dump active connections
140	DumpConnections { format:Option<String> },
141
142	/// Perform health check
143	HealthCheck { verbose:bool, service:Option<String> },
144
145	/// Advanced diagnostics
146	Diagnostics { level:DiagnosticLevel },
147}
148
149/// Diagnostic level
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum DiagnosticLevel {
152	Basic,
153
154	Extended,
155
156	Full,
157}
158
159/// Command validation result
160#[derive(Debug, Clone)]
161pub enum ValidationResult {
162	Valid,
163
164	Invalid(String),
165}
166
167/// Permission level required for a command
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum PermissionLevel {
170	/// No special permission required
171	User,
172
173	/// Elevated permissions required (e.g., sudo on Unix, Admin on Windows)
174	Admin,
175}
176
177// =============================================================================
178// CLI Arguments Parsing and Validation
179// =============================================================================
180
181/// CLI arguments parser with validation
182#[allow(dead_code)]
183pub struct CliParser {
184	#[allow(dead_code)]
185	TimeoutSecs:u64,
186}
187
188impl CliParser {
189	/// Create a new CLI parser with default timeout
190	pub fn new() -> Self { Self { TimeoutSecs:30 } }
191
192	/// Create a new CLI parser with custom timeout
193	pub fn with_timeout(TimeoutSecs:u64) -> Self { Self { TimeoutSecs } }
194
195	/// Parse command line arguments into Command
196	pub fn parse(args:Vec<String>) -> Result<Command, String> { Self::new().parse_args(args) }
197
198	/// Parse command line arguments into Command with timeout setting
199	pub fn parse_args(&self, args:Vec<String>) -> Result<Command, String> {
200		// Remove program name
201		let args = if args.is_empty() { vec![] } else { args[1..].to_vec() };
202
203		if args.is_empty() {
204			return Ok(Command::Help { command:None });
205		}
206
207		let command = &args[0];
208
209		match command.as_str() {
210			"status" => self.parse_status(&args[1..]),
211
212			"restart" => self.parse_restart(&args[1..]),
213
214			"config" => self.parse_config(&args[1..]),
215
216			"metrics" => self.parse_metrics(&args[1..]),
217
218			"logs" => self.parse_logs(&args[1..]),
219
220			"debug" => self.parse_debug(&args[1..]),
221
222			"help" | "-h" | "--help" => self.parse_help(&args[1..]),
223
224			"version" | "-v" | "--version" => Ok(Command::Version),
225
226			_ => {
227				Err(format!(
228					"Unknown command: {}\n\nUse 'Air help' for available commands.",
229					command
230				))
231			},
232		}
233	}
234
235	/// Parse status command with validation
236	fn parse_status(&self, args:&[String]) -> Result<Command, String> {
237		let mut service = None;
238
239		let mut verbose = false;
240
241		let mut json = false;
242
243		let mut i = 0;
244
245		while i < args.len() {
246			match args[i].as_str() {
247				"--service" => {
248					if i + 1 < args.len() {
249						service = Some(args[i + 1].clone());
250
251						Self::validate_service_name(&service)?;
252
253						i += 2;
254					} else {
255						return Err("--service requires a value".to_string());
256					}
257				},
258
259				"-s" => {
260					if i + 1 < args.len() {
261						service = Some(args[i + 1].clone());
262
263						Self::validate_service_name(&service)?;
264
265						i += 2;
266					} else {
267						return Err("-s requires a value".to_string());
268					}
269				},
270
271				"--verbose" | "-v" => {
272					verbose = true;
273
274					i += 1;
275				},
276
277				"--json" => {
278					json = true;
279
280					i += 1;
281				},
282
283				_ => {
284					return Err(format!(
285						"Unknown flag for 'status' command: {}\n\nValid flags are: --service, --verbose, --json",
286						args[i]
287					));
288				},
289			}
290		}
291
292		Ok(Command::Status { service, verbose, json })
293	}
294
295	/// Parse restart command with validation
296	fn parse_restart(&self, args:&[String]) -> Result<Command, String> {
297		let mut service = None;
298
299		let mut force = false;
300
301		let mut i = 0;
302
303		while i < args.len() {
304			match args[i].as_str() {
305				"--service" | "-s" => {
306					if i + 1 < args.len() {
307						service = Some(args[i + 1].clone());
308
309						Self::validate_service_name(&service)?;
310
311						i += 2;
312					} else {
313						return Err("--service requires a value".to_string());
314					}
315				},
316
317				"--force" | "-f" => {
318					force = true;
319
320					i += 1;
321				},
322
323				_ => {
324					return Err(format!(
325						"Unknown flag for 'restart' command: {}\n\nValid flags are: --service, --force",
326						args[i]
327					));
328				},
329			}
330		}
331
332		Ok(Command::Restart { service, force })
333	}
334
335	/// Parse config subcommand with validation
336	fn parse_config(&self, args:&[String]) -> Result<Command, String> {
337		if args.is_empty() {
338			return Err(
339				"config requires a subcommand: get, set, reload, show, validate\n\nUse 'Air help config' for more \
340				 information."
341					.to_string(),
342			);
343		}
344
345		let subcommand = &args[0];
346
347		match subcommand.as_str() {
348			"get" => {
349				if args.len() < 2 {
350					return Err("config get requires a key\n\nExample: Air config get grpc.BindAddress".to_string());
351				}
352
353				let key = args[1].clone();
354
355				Self::validate_config_key(&key)?;
356
357				Ok(Command::Config(ConfigCommand::Get { key }))
358			},
359
360			"set" => {
361				if args.len() < 3 {
362					return Err("config set requires key and value\n\nExample: Air config set grpc.BindAddress \
363					            \"[::1]:50053\""
364						.to_string());
365				}
366
367				let key = args[1].clone();
368
369				let value = args[2].clone();
370
371				Self::validate_config_key(&key)?;
372
373				Self::validate_config_value(&key, &value)?;
374
375				Ok(Command::Config(ConfigCommand::Set { key, value }))
376			},
377
378			"reload" => {
379				let validate = args.contains(&"--validate".to_string());
380
381				Ok(Command::Config(ConfigCommand::Reload { validate }))
382			},
383
384			"show" => {
385				let json = args.contains(&"--json".to_string());
386
387				Ok(Command::Config(ConfigCommand::Show { json }))
388			},
389
390			"validate" => {
391				let path = args.get(1).cloned();
392
393				if let Some(p) = &path {
394					Self::validate_config_path(p)?;
395				}
396
397				Ok(Command::Config(ConfigCommand::Validate { path }))
398			},
399
400			_ => {
401				Err(format!(
402					"Unknown config subcommand: {}\n\nValid subcommands are: get, set, reload, show, validate",
403					subcommand
404				))
405			},
406		}
407	}
408
409	/// Parse metrics command with validation
410	fn parse_metrics(&self, args:&[String]) -> Result<Command, String> {
411		let mut json = false;
412
413		let mut service = None;
414
415		let mut i = 0;
416
417		while i < args.len() {
418			match args[i].as_str() {
419				"--json" => {
420					json = true;
421
422					i += 1;
423				},
424
425				"--service" | "-s" => {
426					if i + 1 < args.len() {
427						service = Some(args[i + 1].clone());
428
429						Self::validate_service_name(&service)?;
430
431						i += 2;
432					} else {
433						return Err("--service requires a value".to_string());
434					}
435				},
436
437				_ => {
438					return Err(format!(
439						"Unknown flag for 'metrics' command: {}\n\nValid flags are: --service, --json",
440						args[i]
441					));
442				},
443			}
444		}
445
446		Ok(Command::Metrics { json, service })
447	}
448
449	/// Parse logs command with validation
450	fn parse_logs(&self, args:&[String]) -> Result<Command, String> {
451		let mut service = None;
452
453		let mut tail = None;
454
455		let mut filter = None;
456
457		let mut follow = false;
458
459		let mut i = 0;
460
461		while i < args.len() {
462			match args[i].as_str() {
463				"--service" | "-s" => {
464					if i + 1 < args.len() {
465						service = Some(args[i + 1].clone());
466
467						Self::validate_service_name(&service)?;
468
469						i += 2;
470					} else {
471						return Err("--service requires a value".to_string());
472					}
473				},
474
475				"--tail" | "-n" => {
476					if i + 1 < args.len() {
477						tail = Some(args[i + 1].parse::<usize>().map_err(|_| {
478							format!("Invalid tail value '{}': must be a positive integer", args[i + 1])
479						})?);
480
481						if tail.unwrap_or(0) == 0 {
482							return Err("Invalid tail value: must be a positive integer".to_string());
483						}
484
485						i += 2;
486					} else {
487						return Err("--tail requires a value".to_string());
488					}
489				},
490
491				"--filter" | "-f" => {
492					if i + 1 < args.len() {
493						filter = Some(args[i + 1].clone());
494
495						Self::validate_filter_pattern(&filter)?;
496
497						i += 2;
498					} else {
499						return Err("--filter requires a value".to_string());
500					}
501				},
502
503				"--follow" => {
504					follow = true;
505
506					i += 1;
507				},
508
509				_ => {
510					return Err(format!(
511						"Unknown flag for 'logs' command: {}\n\nValid flags are: --service, --tail, --filter, --follow",
512						args[i]
513					));
514				},
515			}
516		}
517
518		Ok(Command::Logs { service, tail, filter, follow })
519	}
520
521	/// Parse debug subcommand with validation
522	fn parse_debug(&self, args:&[String]) -> Result<Command, String> {
523		if args.is_empty() {
524			return Err(
525				"debug requires a subcommand: dump-state, dump-connections, health-check, diagnostics\n\nUse 'Air \
526				 help debug' for more information."
527					.to_string(),
528			);
529		}
530
531		let subcommand = &args[0];
532
533		match subcommand.as_str() {
534			"dump-state" => {
535				let mut service = None;
536
537				let mut json = false;
538
539				let mut i = 1;
540
541				while i < args.len() {
542					match args[i].as_str() {
543						"--service" | "-s" => {
544							if i + 1 < args.len() {
545								service = Some(args[i + 1].clone());
546
547								Self::validate_service_name(&service)?;
548
549								i += 2;
550							} else {
551								return Err("--service requires a value".to_string());
552							}
553						},
554
555						"--json" => {
556							json = true;
557
558							i += 1;
559						},
560
561						_ => {
562							return Err(format!(
563								"Unknown flag for 'debug dump-state': {}\n\nValid flags are: --service, --json",
564								args[i]
565							));
566						},
567					}
568				}
569
570				Ok(Command::Debug(DebugCommand::DumpState { service, json }))
571			},
572
573			"dump-connections" => {
574				let mut format = None;
575
576				let mut i = 1;
577
578				while i < args.len() {
579					match args[i].as_str() {
580						"--format" | "-f" => {
581							if i + 1 < args.len() {
582								format = Some(args[i + 1].clone());
583
584								Self::validate_output_format(&format)?;
585
586								i += 2;
587							} else {
588								return Err("--format requires a value (json, table, plain)".to_string());
589							}
590						},
591
592						_ => {
593							return Err(format!(
594								"Unknown flag for 'debug dump-connections': {}\n\nValid flags are: --format",
595								args[i]
596							));
597						},
598					}
599				}
600
601				Ok(Command::Debug(DebugCommand::DumpConnections { format }))
602			},
603
604			"health-check" => {
605				let verbose = args.contains(&"--verbose".to_string());
606
607				let mut service = None;
608
609				let mut i = 1;
610
611				while i < args.len() {
612					match args[i].as_str() {
613						"--service" | "-s" => {
614							if i + 1 < args.len() {
615								service = Some(args[i + 1].clone());
616
617								Self::validate_service_name(&service)?;
618
619								i += 2;
620							} else {
621								return Err("--service requires a value".to_string());
622							}
623						},
624
625						"--verbose" | "-v" => {
626							i += 1;
627						},
628
629						_ => {
630							return Err(format!(
631								"Unknown flag for 'debug health-check': {}\n\nValid flags are: --service, --verbose",
632								args[i]
633							));
634						},
635					}
636				}
637
638				Ok(Command::Debug(DebugCommand::HealthCheck { verbose, service }))
639			},
640
641			"diagnostics" => {
642				let mut level = DiagnosticLevel::Basic;
643
644				let mut i = 1;
645
646				while i < args.len() {
647					match args[i].as_str() {
648						"--full" => {
649							level = DiagnosticLevel::Full;
650
651							i += 1;
652						},
653
654						"--extended" => {
655							level = DiagnosticLevel::Extended;
656
657							i += 1;
658						},
659
660						"--basic" => {
661							level = DiagnosticLevel::Basic;
662
663							i += 1;
664						},
665
666						_ => {
667							return Err(format!(
668								"Unknown flag for 'debug diagnostics': {}\n\nValid flags are: --basic, --extended, \
669								 --full",
670								args[i]
671							));
672						},
673					}
674				}
675
676				Ok(Command::Debug(DebugCommand::Diagnostics { level }))
677			},
678
679			_ => {
680				Err(format!(
681					"Unknown debug subcommand: {}\n\nValid subcommands are: dump-state, dump-connections, \
682					 health-check, diagnostics",
683					subcommand
684				))
685			},
686		}
687	}
688
689	/// Parse help command
690	fn parse_help(&self, args:&[String]) -> Result<Command, String> {
691		let command = args.get(0).map(|s| s.clone());
692
693		Ok(Command::Help { command })
694	}
695
696	// =============================================================================
697	// Validation Methods
698	// =============================================================================
699
700	/// Validate service name format
701	fn validate_service_name(service:&Option<String>) -> Result<(), String> {
702		if let Some(s) = service {
703			if s.is_empty() {
704				return Err("Service name cannot be empty".to_string());
705			}
706
707			if s.len() > 100 {
708				return Err("Service name too long (max 100 characters)".to_string());
709			}
710
711			if !s.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
712				return Err(
713					"Service name can only contain alphanumeric characters, hyphens, and underscores".to_string(),
714				);
715			}
716		}
717
718		Ok(())
719	}
720
721	/// Validate configuration key format
722	fn validate_config_key(key:&str) -> Result<(), String> {
723		if key.is_empty() {
724			return Err("Configuration key cannot be empty".to_string());
725		}
726
727		if key.len() > 255 {
728			return Err("Configuration key too long (max 255 characters)".to_string());
729		}
730
731		if !key.contains('.') {
732			return Err("Configuration key must use dot notation (e.g., 'section.subsection.key')".to_string());
733		}
734
735		let parts:Vec<&str> = key.split('.').collect();
736
737		for part in &parts {
738			if part.is_empty() {
739				return Err("Configuration key cannot have empty segments (e.g., 'section..key')".to_string());
740			}
741
742			if !part.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
743				return Err(format!("Invalid configuration key segment '{}': must be alphanumeric", part));
744			}
745		}
746
747		Ok(())
748	}
749
750	/// Validate configuration value
751	fn validate_config_value(key:&str, value:&str) -> Result<(), String> {
752		if value.is_empty() {
753			return Err("Configuration value cannot be empty".to_string());
754		}
755
756		if value.len() > 10000 {
757			return Err("Configuration value too long (max 10000 characters)".to_string());
758		}
759
760		// Validate specific keys
761		if key.contains("bind_address") || key.contains("listen") {
762			Self::validate_bind_address(value)?;
763		}
764
765		Ok(())
766	}
767
768	/// Validate bind address format
769	fn validate_bind_address(address:&str) -> Result<(), String> {
770		if address.is_empty() {
771			return Err("Bind address cannot be empty".to_string());
772		}
773
774		if address.starts_with("127.0.0.1") || address.starts_with("[::1]") || address == "0.0.0.0" || address == "::" {
775			return Ok(());
776		}
777
778		return Err("Invalid bind address format".to_string());
779	}
780
781	/// Validate configuration file path
782	fn validate_config_path(path:&str) -> Result<(), String> {
783		if path.is_empty() {
784			return Err("Configuration path cannot be empty".to_string());
785		}
786
787		if !path.ends_with(".json") && !path.ends_with(".toml") && !path.ends_with(".yaml") && !path.ends_with(".yml") {
788			return Err("Configuration file must be .json, .toml, .yaml, or .yml".to_string());
789		}
790
791		Ok(())
792	}
793
794	/// Validate log filter pattern
795	fn validate_filter_pattern(filter:&Option<String>) -> Result<(), String> {
796		if let Some(f) = filter {
797			if f.is_empty() {
798				return Err("Filter pattern cannot be empty".to_string());
799			}
800
801			if f.len() > 1000 {
802				return Err("Filter pattern too long (max 1000 characters)".to_string());
803			}
804		}
805
806		Ok(())
807	}
808
809	/// Validate output format
810	fn validate_output_format(format:&Option<String>) -> Result<(), String> {
811		if let Some(f) = format {
812			match f.as_str() {
813				"json" | "table" | "plain" => Ok(()),
814
815				_ => Err(format!("Invalid output format '{}'. Valid formats: json, table, plain", f)),
816			}
817		} else {
818			Ok(())
819		}
820	}
821}
822
823// =============================================================================
824// Response Structures
825// =============================================================================
826
827/// Status response
828#[derive(Debug, Serialize, Deserialize)]
829pub struct StatusResponse {
830	pub daemon_running:bool,
831
832	pub uptime_secs:u64,
833
834	pub version:String,
835
836	pub services:HashMap<String, ServiceStatus>,
837
838	pub timestamp:String,
839}
840
841/// Service status entry
842#[derive(Debug, Serialize, Deserialize)]
843pub struct ServiceStatus {
844	pub name:String,
845
846	pub running:bool,
847
848	pub health:ServiceHealth,
849
850	pub uptime_secs:u64,
851
852	pub error:Option<String>,
853}
854
855/// Service health status
856#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
857#[serde(rename_all = "UPPERCASE")]
858pub enum ServiceHealth {
859	Healthy,
860
861	Degraded,
862
863	Unhealthy,
864
865	Unknown,
866}
867
868/// Metrics response
869#[derive(Debug, Serialize, Deserialize)]
870pub struct MetricsResponse {
871	pub timestamp:String,
872
873	pub memory_used_mb:f64,
874
875	pub memory_available_mb:f64,
876
877	pub cpu_usage_percent:f64,
878
879	pub disk_used_mb:u64,
880
881	pub disk_available_mb:u64,
882
883	pub active_connections:u32,
884
885	pub processed_requests:u64,
886
887	pub failed_requests:u64,
888
889	pub service_metrics:HashMap<String, ServiceMetrics>,
890}
891
892/// Service metrics entry
893#[derive(Debug, Serialize, Deserialize)]
894pub struct ServiceMetrics {
895	pub name:String,
896
897	pub requests_total:u64,
898
899	pub requests_success:u64,
900
901	pub requests_failed:u64,
902
903	pub average_latency_ms:f64,
904
905	pub p99_latency_ms:f64,
906}
907
908/// Health check response
909#[derive(Debug, Serialize, Deserialize)]
910pub struct HealthCheckResponse {
911	pub overall_healthy:bool,
912
913	pub overall_health_percentage:f64,
914
915	pub services:HashMap<String, ServiceHealthDetail>,
916
917	pub timestamp:String,
918}
919
920/// Detailed service health
921#[derive(Debug, Serialize, Deserialize)]
922pub struct ServiceHealthDetail {
923	pub name:String,
924
925	pub healthy:bool,
926
927	pub response_time_ms:u64,
928
929	pub last_check:String,
930
931	pub details:String,
932}
933
934/// Configuration response
935#[derive(Debug, Serialize, Deserialize)]
936pub struct ConfigResponse {
937	pub key:Option<String>,
938
939	pub value:serde_json::Value,
940
941	pub path:String,
942
943	pub modified:String,
944}
945
946/// Log entry
947#[derive(Debug, Serialize, Deserialize)]
948pub struct LogEntry {
949	pub timestamp:DateTime<Utc>,
950
951	pub level:String,
952
953	pub service:Option<String>,
954
955	pub message:String,
956
957	pub context:Option<serde_json::Value>,
958}
959
960/// Connection info
961#[derive(Debug, Serialize, Deserialize)]
962pub struct ConnectionInfo {
963	pub id:String,
964
965	pub remote_address:String,
966
967	pub connected_at:DateTime<Utc>,
968
969	pub service:Option<String>,
970
971	pub active:bool,
972}
973
974/// Daemon state dump
975#[derive(Debug, Serialize, Deserialize)]
976pub struct DaemonState {
977	pub timestamp:DateTime<Utc>,
978
979	pub version:String,
980
981	pub uptime_secs:u64,
982
983	pub services:HashMap<String, serde_json::Value>,
984
985	pub connections:Vec<ConnectionInfo>,
986
987	pub plugin_state:serde_json::Value,
988}
989
990// =============================================================================
991// Daemon Connection and Client
992// =============================================================================
993
994/// Daemon client for communicating with running Air daemon
995#[allow(dead_code)]
996pub struct DaemonClient {
997	#[allow(dead_code)]
998	address:String,
999
1000	#[allow(dead_code)]
1001	timeout:Duration,
1002}
1003
1004impl DaemonClient {
1005	/// Create a new daemon client
1006	pub fn new(address:String) -> Self { Self { address, timeout:Duration::from_secs(30) } }
1007
1008	/// Create a new daemon client with custom timeout
1009	pub fn with_timeout(address:String, timeout_secs:u64) -> Self {
1010		Self { address, timeout:Duration::from_secs(timeout_secs) }
1011	}
1012
1013	/// Connect to daemon and execute status command
1014	pub fn execute_status(&self, _service:Option<String>) -> Result<StatusResponse, String> {
1015		// In production, this would connect via gRPC or Unix socket
1016		// For now, simulate a response
1017		Ok(StatusResponse {
1018			daemon_running:true,
1019			uptime_secs:3600,
1020			version:"0.1.0".to_string(),
1021			services:self.get_mock_services(),
1022			timestamp:Utc::now().to_rfc3339(),
1023		})
1024	}
1025
1026	/// Connect to daemon and execute restart command
1027	pub fn execute_restart(&self, service:Option<String>, force:bool) -> Result<String, String> {
1028		Ok(if let Some(s) = service {
1029			format!("Service {} restarted (force: {})", s, force)
1030		} else {
1031			format!("All services restarted (force: {})", force)
1032		})
1033	}
1034
1035	/// Connect to daemon and execute config get command
1036	pub fn execute_config_get(&self, key:&str) -> Result<ConfigResponse, String> {
1037		Ok(ConfigResponse {
1038			key:Some(key.to_string()),
1039			value:serde_json::json!("example_value"),
1040			path:"/Air/config.json".to_string(),
1041			modified:Utc::now().to_rfc3339(),
1042		})
1043	}
1044
1045	/// Connect to daemon and execute config set command
1046	pub fn execute_config_set(&self, key:&str, value:&str) -> Result<String, String> {
1047		Ok(format!("Configuration updated: {} = {}", key, value))
1048	}
1049
1050	/// Connect to daemon and execute config reload command
1051	pub fn execute_config_reload(&self, validate:bool) -> Result<String, String> {
1052		Ok(format!("Configuration reloaded (validate: {})", validate))
1053	}
1054
1055	/// Connect to daemon and execute config show command
1056	pub fn execute_config_show(&self) -> Result<serde_json::Value, String> {
1057		Ok(serde_json::json!({
1058			"grpc": {
1059				"bind_address": "[::1]:50053",
1060				"max_connections": 100
1061			},
1062			"updates": {
1063				"auto_download": true,
1064				"auto_install": false
1065			}
1066		}))
1067	}
1068
1069	/// Connect to daemon and execute config validate command
1070	pub fn execute_config_validate(&self, _path:Option<String>) -> Result<bool, String> { Ok(true) }
1071
1072	/// Connect to daemon and execute metrics command
1073	pub fn execute_metrics(&self, _service:Option<String>) -> Result<MetricsResponse, String> {
1074		Ok(MetricsResponse {
1075			timestamp:Utc::now().to_rfc3339(),
1076			memory_used_mb:512.0,
1077			memory_available_mb:4096.0,
1078			cpu_usage_percent:15.5,
1079			disk_used_mb:1024,
1080			disk_available_mb:51200,
1081			active_connections:5,
1082			processed_requests:1000,
1083			failed_requests:2,
1084			service_metrics:self.get_mock_service_metrics(),
1085		})
1086	}
1087
1088	/// Connect to daemon and execute logs command
1089	pub fn execute_logs(
1090		&self,
1091
1092		service:Option<String>,
1093
1094		_tail:Option<usize>,
1095
1096		_filter:Option<String>,
1097	) -> Result<Vec<LogEntry>, String> {
1098		// Return mock logs
1099		Ok(vec![LogEntry {
1100			timestamp:Utc::now(),
1101			level:"INFO".to_string(),
1102			service:service.clone(),
1103			message:"Daemon started successfully".to_string(),
1104			context:None,
1105		}])
1106	}
1107
1108	/// Connect to daemon and execute debug dump-state command
1109	pub fn execute_debug_dump_state(&self, _service:Option<String>) -> Result<DaemonState, String> {
1110		Ok(DaemonState {
1111			timestamp:Utc::now(),
1112			version:"0.1.0".to_string(),
1113			uptime_secs:3600,
1114			services:HashMap::new(),
1115			connections:vec![],
1116			plugin_state:serde_json::json!({}),
1117		})
1118	}
1119
1120	/// Connect to daemon and execute debug dump-connections command
1121	pub fn execute_debug_dump_connections(&self) -> Result<Vec<ConnectionInfo>, String> { Ok(vec![]) }
1122
1123	/// Connect to daemon and execute debug health-check command
1124	pub fn execute_debug_health_check(&self, _service:Option<String>) -> Result<HealthCheckResponse, String> {
1125		Ok(HealthCheckResponse {
1126			overall_healthy:true,
1127			overall_health_percentage:100.0,
1128			services:HashMap::new(),
1129			timestamp:Utc::now().to_rfc3339(),
1130		})
1131	}
1132
1133	/// Connect to daemon and execute debug diagnostics command
1134	pub fn execute_debug_diagnostics(&self, level:DiagnosticLevel) -> Result<serde_json::Value, String> {
1135		Ok(serde_json::json!({
1136			"level": format!("{:?}", level),
1137			"timestamp": Utc::now().to_rfc3339(),
1138			"checks": {
1139				"memory": "ok",
1140				"cpu": "ok",
1141				"disk": "ok"
1142			}
1143		}))
1144	}
1145
1146	/// Check if daemon is running
1147	pub fn is_daemon_running(&self) -> bool {
1148		// In production, check via socket connection or process check
1149		true
1150	}
1151
1152	/// Get mock services for testing
1153	fn get_mock_services(&self) -> HashMap<String, ServiceStatus> {
1154		let mut services = HashMap::new();
1155
1156		services.insert(
1157			"authentication".to_string(),
1158			ServiceStatus {
1159				name:"authentication".to_string(),
1160				running:true,
1161				health:ServiceHealth::Healthy,
1162				uptime_secs:3600,
1163				error:None,
1164			},
1165		);
1166
1167		services.insert(
1168			"updates".to_string(),
1169			ServiceStatus {
1170				name:"updates".to_string(),
1171				running:true,
1172				health:ServiceHealth::Healthy,
1173				uptime_secs:3600,
1174				error:None,
1175			},
1176		);
1177
1178		services.insert(
1179			"plugins".to_string(),
1180			ServiceStatus {
1181				name:"plugins".to_string(),
1182				running:true,
1183				health:ServiceHealth::Healthy,
1184				uptime_secs:3600,
1185				error:None,
1186			},
1187		);
1188
1189		services
1190	}
1191
1192	/// Get mock service metrics for testing
1193	fn get_mock_service_metrics(&self) -> HashMap<String, ServiceMetrics> {
1194		let mut metrics = HashMap::new();
1195
1196		metrics.insert(
1197			"authentication".to_string(),
1198			ServiceMetrics {
1199				name:"authentication".to_string(),
1200				requests_total:500,
1201				requests_success:498,
1202				requests_failed:2,
1203				average_latency_ms:12.5,
1204				p99_latency_ms:45.0,
1205			},
1206		);
1207
1208		metrics.insert(
1209			"updates".to_string(),
1210			ServiceMetrics {
1211				name:"updates".to_string(),
1212				requests_total:300,
1213				requests_success:300,
1214				requests_failed:0,
1215				average_latency_ms:25.0,
1216				p99_latency_ms:100.0,
1217			},
1218		);
1219
1220		metrics
1221	}
1222}
1223
1224// =============================================================================
1225// CLI Command Handler
1226// =============================================================================
1227
1228/// Main CLI command handler
1229pub struct CliHandler {
1230	client:DaemonClient,
1231
1232	output_format:OutputFormat,
1233}
1234
1235impl CliHandler {
1236	/// Create a new CLI handler
1237	pub fn new() -> Self {
1238		Self {
1239			client:DaemonClient::new("[::1]:50053".to_string()),
1240
1241			output_format:OutputFormat::Plain,
1242		}
1243	}
1244
1245	/// Create a new CLI handler with custom client
1246	pub fn with_client(client:DaemonClient) -> Self { Self { client, output_format:OutputFormat::Plain } }
1247
1248	/// Set output format
1249	pub fn set_output_format(&mut self, format:OutputFormat) { self.output_format = format; }
1250
1251	/// Check and enforce permission requirements
1252	fn check_permission(&self, command:&Command) -> Result<(), String> {
1253		let required = Self::get_permission_level(command);
1254
1255		if required == PermissionLevel::Admin {
1256			// In production, check for elevated privileges
1257			// For now, we'll just log a warning
1258			dev_log!("lifecycle", "warn: Admin privileges required for command");
1259		}
1260
1261		Ok(())
1262	}
1263
1264	/// Get permission level required for a command
1265	fn get_permission_level(command:&Command) -> PermissionLevel {
1266		match command {
1267			Command::Config(ConfigCommand::Set { .. }) => PermissionLevel::Admin,
1268
1269			Command::Config(ConfigCommand::Reload { .. }) => PermissionLevel::Admin,
1270
1271			Command::Restart { force, .. } if *force => PermissionLevel::Admin,
1272
1273			Command::Restart { .. } => PermissionLevel::Admin,
1274
1275			_ => PermissionLevel::User,
1276		}
1277	}
1278
1279	/// Execute a command and return formatted output
1280	pub fn execute(&mut self, command:Command) -> Result<String, String> {
1281		// Check permissions
1282		self.check_permission(&command)?;
1283
1284		match command {
1285			Command::Status { service, verbose, json } => self.Status(service, verbose, json),
1286
1287			Command::Restart { service, force } => self.Restart(service, force),
1288
1289			Command::Config(config_cmd) => self.Config(config_cmd),
1290
1291			Command::Metrics { json, service } => self.Metrics(json, service),
1292
1293			Command::Logs { service, tail, filter, follow } => self.Logs(service, tail, filter, follow),
1294
1295			Command::Debug(debug_cmd) => self.Debug(debug_cmd),
1296
1297			Command::Help { command } => Ok(OutputFormatter::format_help(command.as_deref(), "0.1.0")),
1298
1299			Command::Version => Ok("Air 🪁 v0.1.0".to_string()),
1300		}
1301	}
1302
1303	/// Handle status command
1304	fn Status(&self, service:Option<String>, verbose:bool, json:bool) -> Result<String, String> {
1305		let response = self.client.execute_status(service)?;
1306
1307		Ok(OutputFormatter::format_status(&response, verbose, json))
1308	}
1309
1310	/// Handle restart command
1311	fn Restart(&self, service:Option<String>, force:bool) -> Result<String, String> {
1312		let result = self.client.execute_restart(service, force)?;
1313
1314		Ok(result)
1315	}
1316
1317	/// Handle config commands
1318	fn Config(&self, cmd:ConfigCommand) -> Result<String, String> {
1319		match cmd {
1320			ConfigCommand::Get { key } => {
1321				let response = self.client.execute_config_get(&key)?;
1322
1323				Ok(format!("{} = {}", response.key.unwrap_or_default(), response.value))
1324			},
1325
1326			ConfigCommand::Set { key, value } => {
1327				let result = self.client.execute_config_set(&key, &value)?;
1328
1329				Ok(result)
1330			},
1331
1332			ConfigCommand::Reload { validate } => {
1333				let result = self.client.execute_config_reload(validate)?;
1334
1335				Ok(result)
1336			},
1337
1338			ConfigCommand::Show { json } => {
1339				let config = self.client.execute_config_show()?;
1340
1341				if json {
1342					Ok(serde_json::to_string_pretty(&config).unwrap_or_else(|_| "{}".to_string()))
1343				} else {
1344					Ok(serde_json::to_string_pretty(&config).unwrap_or_else(|_| "{}".to_string()))
1345				}
1346			},
1347
1348			ConfigCommand::Validate { path } => {
1349				let valid = self.client.execute_config_validate(path)?;
1350
1351				if valid {
1352					Ok("Configuration is valid".to_string())
1353				} else {
1354					Err("Configuration validation failed".to_string())
1355				}
1356			},
1357		}
1358	}
1359
1360	/// Handle metrics command
1361	fn Metrics(&self, json:bool, service:Option<String>) -> Result<String, String> {
1362		let response = self.client.execute_metrics(service)?;
1363
1364		Ok(OutputFormatter::format_metrics(&response, json))
1365	}
1366
1367	/// Handle logs command
1368	fn Logs(
1369		&self,
1370
1371		service:Option<String>,
1372
1373		tail:Option<usize>,
1374
1375		filter:Option<String>,
1376
1377		follow:bool,
1378	) -> Result<String, String> {
1379		let logs = self.client.execute_logs(service, tail, filter)?;
1380
1381		let mut output = String::new();
1382
1383		for entry in logs {
1384			output.push_str(&format!(
1385				"[{}] {} - {}\n",
1386				entry.timestamp.format("%Y-%m-%d %H:%M:%S"),
1387				entry.level,
1388				entry.message
1389			));
1390		}
1391
1392		if follow {
1393			output.push_str("\nFollowing logs (press Ctrl+C to stop)...\n");
1394		}
1395
1396		Ok(output)
1397	}
1398
1399	/// Handle debug commands
1400	fn Debug(&self, cmd:DebugCommand) -> Result<String, String> {
1401		match cmd {
1402			DebugCommand::DumpState { service, json } => {
1403				let state = self.client.execute_debug_dump_state(service)?;
1404
1405				if json {
1406					Ok(serde_json::to_string_pretty(&state).unwrap_or_else(|_| "{}".to_string()))
1407				} else {
1408					Ok(format!(
1409						"Daemon State Dump\nVersion: {}\nUptime: {}s\n",
1410						state.version, state.uptime_secs
1411					))
1412				}
1413			},
1414
1415			DebugCommand::DumpConnections { format: _ } => {
1416				let connections = self.client.execute_debug_dump_connections()?;
1417
1418				Ok(format!("Active connections: {}", connections.len()))
1419			},
1420
1421			DebugCommand::HealthCheck { verbose: _, service } => {
1422				let health = self.client.execute_debug_health_check(service)?;
1423
1424				Ok(format!(
1425					"Overall Health: {} ({}%)\n",
1426					if health.overall_healthy { "Healthy" } else { "Unhealthy" },
1427					health.overall_health_percentage
1428				))
1429			},
1430
1431			DebugCommand::Diagnostics { level } => {
1432				let diagnostics = self.client.execute_debug_diagnostics(level)?;
1433
1434				Ok(serde_json::to_string_pretty(&diagnostics).unwrap_or_else(|_| "{}".to_string()))
1435			},
1436		}
1437	}
1438}
1439
1440/// Output format
1441#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1442pub enum OutputFormat {
1443	Plain,
1444
1445	Table,
1446
1447	Json,
1448}
1449
1450// =============================================================================
1451// Help Messages
1452// =============================================================================
1453
1454pub const HELP_MAIN:&str = r#"
1455Air 🪁 - Background Daemon for Land Code Editor
1456Version: {version}
1457
1458USAGE:
1459    Air [COMMAND] [OPTIONS]
1460
1461COMMANDS:
1462    status           Show daemon and service status
1463    restart          Restart services
1464    config           Manage configuration
1465    metrics          View performance metrics
1466    logs             View daemon logs
1467    debug            Debug and diagnostics
1468    help             Show help information
1469    version          Show version information
1470
1471OPTIONS:
1472    -h, --help       Show help
1473    -v, --version    Show version
1474
1475EXAMPLES:
1476    Air status --verbose
1477    Air config get grpc.bind_address
1478    Air metrics --json
1479    Air logs --tail=100 --follow
1480    Air debug health-check
1481
1482Use 'Air help <command>' for more information about a command.
1483"#;
1484
1485pub const HELP_STATUS:&str = r#"
1486Show daemon and service status
1487
1488USAGE:
1489    Air status [OPTIONS]
1490
1491OPTIONS:
1492    -s, --service <NAME>    Show status of specific service
1493    -v, --verbose           Show detailed information
1494    --json                  Output in JSON format
1495
1496EXAMPLES:
1497    Air status
1498    Air status --service authentication --verbose
1499    Air status --json
1500"#;
1501
1502pub const HELP_RESTART:&str = r#"
1503Restart services
1504
1505USAGE:
1506    Air restart [OPTIONS]
1507
1508OPTIONS:
1509    -s, --service <NAME>    Restart specific service
1510    -f, --force             Force restart without graceful shutdown
1511
1512EXAMPLES:
1513    Air restart
1514    Air restart --service updates
1515    Air restart --force
1516"#;
1517
1518pub const HELP_CONFIG:&str = r#"
1519Manage configuration
1520
1521USAGE:
1522    Air config <SUBCOMMAND> [OPTIONS]
1523
1524SUBCOMMANDS:
1525    get <KEY>               Get configuration value
1526    set <KEY> <VALUE>       Set configuration value
1527    reload                  Reload configuration from file
1528    show                    Show current configuration
1529    validate [PATH]         Validate configuration file
1530
1531OPTIONS:
1532    --json                  Output in JSON format
1533    --validate              Validate before reloading
1534
1535EXAMPLES:
1536    Air config get grpc.bind_address
1537    Air config set updates.auto_download true
1538    Air config reload --validate
1539    Air config show --json
1540"#;
1541
1542pub const HELP_METRICS:&str = r#"
1543View performance metrics
1544
1545USAGE:
1546    Air metrics [OPTIONS]
1547
1548OPTIONS:
1549    -s, --service <NAME>    Show metrics for specific service
1550    --json                  Output in JSON format
1551
1552EXAMPLES:
1553    Air metrics
1554    Air metrics --service downloader
1555    Air metrics --json
1556"#;
1557
1558pub const HELP_LOGS:&str = r#"
1559View daemon logs
1560
1561USAGE:
1562    Air logs [OPTIONS]
1563
1564OPTIONS:
1565    -s, --service <NAME>    Show logs from specific service
1566    -n, --tail <N>          Show last N lines (default: 50)
1567    -f, --filter <PATTERN>  Filter logs by pattern
1568    --follow                Follow logs in real-time
1569
1570EXAMPLES:
1571    Air logs
1572    Air logs --service updates --tail=100
1573    Air logs --filter "ERROR" --follow
1574"#;
1575
1576pub const HELP_DEBUG:&str = r#"
1577Debug and diagnostics
1578
1579USAGE:
1580    Air debug <SUBCOMMAND> [OPTIONS]
1581
1582SUBCOMMANDS:
1583    dump-state              Dump current daemon state
1584    dump-connections        Dump active connections
1585    health-check            Perform health check
1586    diagnostics             Run diagnostics
1587
1588OPTIONS:
1589    --json                  Output in JSON format
1590    --verbose               Show detailed information
1591    --service <NAME>        Target specific service
1592    --full                  Full diagnostic level
1593
1594EXAMPLES:
1595    Air debug dump-state
1596    Air debug dump-connections --json
1597    Air debug health-check --verbose
1598    Air debug diagnostics --full
1599"#;
1600
1601// =============================================================================
1602// Output Formatting
1603// =============================================================================
1604
1605/// Format output based on command options
1606pub struct OutputFormatter;
1607
1608impl OutputFormatter {
1609	/// Format status output
1610	pub fn format_status(response:&StatusResponse, verbose:bool, json:bool) -> String {
1611		if json {
1612			serde_json::to_string_pretty(response).unwrap_or_else(|_| "{}".to_string())
1613		} else if verbose {
1614			Self::format_status_verbose(response)
1615		} else {
1616			Self::format_status_compact(response)
1617		}
1618	}
1619
1620	fn format_status_compact(response:&StatusResponse) -> String {
1621		let daemon_status = if response.daemon_running { "🟢 Running" } else { "🔴 Stopped" };
1622
1623		let mut output = format!(
1624			"Air Daemon {}\nVersion: {}\nUptime: {}s\n\nServices:\n",
1625			daemon_status, response.version, response.uptime_secs
1626		);
1627
1628		for (name, status) in &response.services {
1629			let health_symbol = match status.health {
1630				ServiceHealth::Healthy => "🟢",
1631
1632				ServiceHealth::Degraded => "🟡",
1633
1634				ServiceHealth::Unhealthy => "🔴",
1635
1636				ServiceHealth::Unknown => "⚪",
1637			};
1638
1639			output.push_str(&format!(
1640				"  {} {} - {} (uptime: {}s)\n",
1641				health_symbol,
1642				name,
1643				if status.running { "Running" } else { "Stopped" },
1644				status.uptime_secs
1645			));
1646		}
1647
1648		output
1649	}
1650
1651	fn format_status_verbose(response:&StatusResponse) -> String {
1652		let mut output = format!(
1653			"╔════════════════════════════════════════╗\n║ Air Daemon \
1654			 Status\n╠════════════════════════════════════════╣\n║ Status:   {}\n║ Version:  {}\n║ Uptime:   {} \
1655			 seconds\n║ Time:     {}\n╠════════════════════════════════════════╣\n",
1656			if response.daemon_running { "Running" } else { "Stopped" },
1657			response.version,
1658			response.uptime_secs,
1659			response.timestamp
1660		);
1661
1662		output.push_str("║ Services:\n");
1663
1664		for (name, status) in &response.services {
1665			let health_text = match status.health {
1666				ServiceHealth::Healthy => "Healthy",
1667
1668				ServiceHealth::Degraded => "Degraded",
1669
1670				ServiceHealth::Unhealthy => "Unhealthy",
1671
1672				ServiceHealth::Unknown => "Unknown",
1673			};
1674
1675			output.push_str(&format!(
1676				"║   • {} ({})\n║     Status: {}\n║     Health: {}\n║     Uptime: {} seconds\n",
1677				name,
1678				if status.running { "running" } else { "stopped" },
1679				if status.running { "Active" } else { "Inactive" },
1680				health_text,
1681				status.uptime_secs
1682			));
1683
1684			if let Some(error) = &status.error {
1685				output.push_str(&format!("║     Error: {}\n", error));
1686			}
1687		}
1688
1689		output.push_str("╚════════════════════════════════════════╝\n");
1690
1691		output
1692	}
1693
1694	/// Format metrics output
1695	pub fn format_metrics(response:&MetricsResponse, json:bool) -> String {
1696		if json {
1697			serde_json::to_string_pretty(response).unwrap_or_else(|_| "{}".to_string())
1698		} else {
1699			Self::format_metrics_human(response)
1700		}
1701	}
1702
1703	fn format_metrics_human(response:&MetricsResponse) -> String {
1704		format!(
1705			"╔════════════════════════════════════════╗\n║ Air Daemon \
1706			 Metrics\n╠════════════════════════════════════════╣\n║ Memory:     {:.1}MB / {:.1}MB\n║ CPU:        \
1707			 {:.1}%\n║ Disk:       {}MB / {}MB\n║ Connections: {}\n║ Requests:   {} success, {} \
1708			 failed\n╚════════════════════════════════════════╝\n",
1709			response.memory_used_mb,
1710			response.memory_available_mb,
1711			response.cpu_usage_percent,
1712			response.disk_used_mb,
1713			response.disk_available_mb,
1714			response.active_connections,
1715			response.processed_requests,
1716			response.failed_requests
1717		)
1718	}
1719
1720	/// Format help message
1721	pub fn format_help(topic:Option<&str>, version:&str) -> String {
1722		match topic {
1723			None => HELP_MAIN.replace("{version}", version),
1724
1725			Some("status") => HELP_STATUS.to_string(),
1726
1727			Some("restart") => HELP_RESTART.to_string(),
1728
1729			Some("config") => HELP_CONFIG.to_string(),
1730
1731			Some("metrics") => HELP_METRICS.to_string(),
1732
1733			Some("logs") => HELP_LOGS.to_string(),
1734
1735			Some("debug") => HELP_DEBUG.to_string(),
1736
1737			_ => {
1738				format!(
1739					"Unknown help topic: {}\n\nUse 'Air help' for general help.",
1740					topic.unwrap_or("unknown")
1741				)
1742			},
1743		}
1744	}
1745}
1746
1747#[cfg(test)]
1748mod tests {
1749
1750	use super::*;
1751
1752	#[test]
1753	fn test_parse_status_command() {
1754		let args = vec!["Air".to_string(), "status".to_string(), "--verbose".to_string()];
1755
1756		let cmd = CliParser::parse(args).unwrap();
1757
1758		if let Command::Status { service, verbose, json } = cmd {
1759			assert!(verbose);
1760
1761			assert!(!json);
1762
1763			assert!(service.is_none());
1764		} else {
1765			panic!("Expected Status command");
1766		}
1767	}
1768
1769	#[test]
1770	fn test_parse_config_set() {
1771		let args = vec![
1772			"Air".to_string(),
1773			"config".to_string(),
1774			"set".to_string(),
1775			"grpc.bind_address".to_string(),
1776			"[::1]:50053".to_string(),
1777		];
1778
1779		let cmd = CliParser::parse(args).unwrap();
1780
1781		if let Command::Config(ConfigCommand::Set { key, value }) = cmd {
1782			assert_eq!(key, "grpc.bind_address");
1783
1784			assert_eq!(value, "[::1]:50053");
1785		} else {
1786			panic!("Expected Config Set command");
1787		}
1788	}
1789}