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