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