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