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