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