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