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