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