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