1use clap::Args;
2use clap::{Parser, Subcommand};
3use color_eyre::Result;
4use color_eyre::eyre::eyre;
5use comfy_table::Attribute;
6use comfy_table::Cell;
7use comfy_table::Color;
8use comfy_table::ContentArrangement;
9use comfy_table::Table;
10use console::Term;
11use console::style;
12use envx_core::PathManager;
13use envx_core::profile_manager::ProfileManager;
14use envx_core::snapshot_manager::SnapshotManager;
15use envx_core::{Analyzer, EnvVarManager, ExportFormat, Exporter, ImportFormat, Importer};
16use std::io::Write;
17use std::path::Path;
18#[derive(Parser)]
19#[command(name = "envx")]
20#[command(about = "System Environment Variable Manager")]
21#[command(version)]
22pub struct Cli {
23 #[command(subcommand)]
24 pub command: Commands,
25}
26
27#[derive(Subcommand)]
28pub enum Commands {
29 List {
31 #[arg(short, long)]
33 source: Option<String>,
34
35 #[arg(short = 'q', long)]
37 query: Option<String>,
38
39 #[arg(short, long, default_value = "table")]
41 format: String,
42
43 #[arg(long, default_value = "name")]
45 sort: String,
46
47 #[arg(long)]
49 names_only: bool,
50
51 #[arg(short, long)]
53 limit: Option<usize>,
54
55 #[arg(long)]
57 stats: bool,
58 },
59
60 Get {
62 pattern: String,
71
72 #[arg(short, long, default_value = "simple")]
74 format: String,
75 },
76
77 Set {
79 name: String,
81
82 value: String,
84
85 #[arg(short, long)]
87 temporary: bool,
88 },
89
90 Delete {
92 pattern: String,
94
95 #[arg(short, long)]
97 force: bool,
98 },
99
100 Analyze {
102 #[arg(short, long, default_value = "all")]
104 analysis_type: String,
105 },
106
107 #[command(visible_alias = "ui")]
109 Tui,
110
111 Path {
113 #[command(subcommand)]
114 action: Option<PathAction>,
115
116 #[arg(short, long)]
118 check: bool,
119
120 #[arg(short = 'v', long, default_value = "PATH")]
122 var: String,
123
124 #[arg(short = 'p', long)]
126 permanent: bool,
127 },
128
129 Export {
131 file: String,
133
134 #[arg(short = 'v', long)]
136 vars: Vec<String>,
137
138 #[arg(short, long)]
140 format: Option<String>,
141
142 #[arg(short, long)]
144 source: Option<String>,
145
146 #[arg(short, long)]
148 metadata: bool,
149
150 #[arg(long)]
152 force: bool,
153 },
154
155 Import {
157 file: String,
159
160 #[arg(short = 'v', long)]
162 vars: Vec<String>,
163
164 #[arg(short, long)]
166 format: Option<String>,
167
168 #[arg(short, long)]
170 permanent: bool,
171
172 #[arg(long)]
174 prefix: Option<String>,
175
176 #[arg(long)]
178 overwrite: bool,
179
180 #[arg(short = 'n', long)]
182 dry_run: bool,
183 },
184
185 Snapshot(SnapshotArgs),
187
188 Profile(ProfileArgs),
190}
191
192#[derive(Subcommand)]
193pub enum PathAction {
194 Add {
196 directory: String,
198
199 #[arg(short, long)]
201 first: bool,
202
203 #[arg(short, long)]
205 create: bool,
206 },
207
208 Remove {
210 directory: String,
212
213 #[arg(short, long)]
215 all: bool,
216 },
217
218 Clean {
220 #[arg(short, long)]
222 dedupe: bool,
223
224 #[arg(short = 'n', long)]
226 dry_run: bool,
227 },
228
229 Dedupe {
231 #[arg(short, long)]
233 keep_first: bool,
234
235 #[arg(short = 'n', long)]
237 dry_run: bool,
238 },
239
240 Check {
242 #[arg(short, long)]
244 verbose: bool,
245 },
246
247 List {
249 #[arg(short, long)]
251 numbered: bool,
252
253 #[arg(short, long)]
255 check: bool,
256 },
257
258 Move {
260 from: String,
262
263 to: String,
265 },
266}
267
268#[derive(Args)]
269pub struct SnapshotArgs {
270 #[command(subcommand)]
271 pub command: SnapshotCommands,
272}
273
274#[derive(Subcommand)]
275pub enum SnapshotCommands {
276 Create {
278 name: String,
280 #[arg(short, long)]
282 description: Option<String>,
283 },
284 List,
286 Show {
288 snapshot: String,
290 },
291 Restore {
293 snapshot: String,
295 #[arg(short, long)]
297 force: bool,
298 },
299 Delete {
301 snapshot: String,
303 #[arg(short, long)]
305 force: bool,
306 },
307 Diff {
309 snapshot1: String,
311 snapshot2: String,
313 },
314}
315
316#[derive(Args)]
317pub struct ProfileArgs {
318 #[command(subcommand)]
319 pub command: ProfileCommands,
320}
321
322#[derive(Subcommand)]
323pub enum ProfileCommands {
324 Create {
326 name: String,
328 #[arg(short, long)]
330 description: Option<String>,
331 },
332 List,
334 Show {
336 name: Option<String>,
338 },
339 Switch {
341 name: String,
343 #[arg(short, long)]
345 apply: bool,
346 },
347 Add {
349 profile: String,
351 name: String,
353 value: String,
355 #[arg(short, long)]
357 override_system: bool,
358 },
359 Remove {
361 profile: String,
363 name: String,
365 },
366 Delete {
368 name: String,
370 #[arg(short, long)]
372 force: bool,
373 },
374 Export {
376 name: String,
378 #[arg(short, long)]
380 output: Option<String>,
381 },
382 Import {
384 file: String,
386 #[arg(short, long)]
388 name: Option<String>,
389 #[arg(short, long)]
391 overwrite: bool,
392 },
393 Apply {
395 name: String,
397 },
398}
399
400pub fn execute(cli: Cli) -> Result<()> {
411 match cli.command {
412 Commands::List {
413 source,
414 query,
415 format,
416 sort,
417 names_only,
418 limit,
419 stats,
420 } => {
421 handle_list_command(
422 source.as_deref(),
423 query.as_deref(),
424 &format,
425 &sort,
426 names_only,
427 limit,
428 stats,
429 )?;
430 }
431
432 Commands::Get { pattern, format } => {
433 handle_get_command(&pattern, &format)?;
434 }
435
436 Commands::Set { name, value, temporary } => {
437 handle_set_command(&name, &value, temporary)?;
438 }
439
440 Commands::Delete { pattern, force } => {
441 handle_delete_command(&pattern, force)?;
442 }
443
444 Commands::Analyze { analysis_type } => {
445 handle_analyze_command(&analysis_type)?;
446 }
447
448 Commands::Tui => {
449 envx_tui::run()?;
451 }
452
453 Commands::Path {
454 action,
455 check,
456 var,
457 permanent,
458 } => {
459 handle_path_command(action, check, &var, permanent)?;
460 }
461
462 Commands::Export {
463 file,
464 vars,
465 format,
466 source,
467 metadata,
468 force,
469 } => {
470 handle_export(&file, &vars, format, source, metadata, force)?;
471 }
472
473 Commands::Import {
474 file,
475 vars,
476 format,
477 permanent,
478 prefix,
479 overwrite,
480 dry_run,
481 } => {
482 handle_import(&file, &vars, format, permanent, prefix.as_ref(), overwrite, dry_run)?;
483 }
484
485 Commands::Snapshot(args) => {
486 handle_snapshot(args)?;
487 }
488 Commands::Profile(args) => {
489 handle_profile(args)?;
490 }
491 }
492
493 Ok(())
494}
495
496fn handle_get_command(pattern: &str, format: &str) -> Result<()> {
497 let mut manager = EnvVarManager::new();
498 manager.load_all()?;
499
500 let vars = manager.get_pattern(pattern);
501
502 if vars.is_empty() {
503 eprintln!("No variables found matching pattern: {pattern}");
504 return Ok(());
505 }
506
507 match format {
508 "json" => {
509 println!("{}", serde_json::to_string_pretty(&vars)?);
510 }
511 "detailed" => {
512 for var in vars {
513 println!("Name: {}", var.name);
514 println!("Value: {}", var.value);
515 println!("Source: {:?}", var.source);
516 println!("Modified: {}", var.modified.format("%Y-%m-%d %H:%M:%S"));
517 if let Some(orig) = &var.original_value {
518 println!("Original: {orig}");
519 }
520 println!("---");
521 }
522 }
523 _ => {
524 for var in vars {
525 println!("{} = {}", var.name, var.value);
526 }
527 }
528 }
529 Ok(())
530}
531
532fn handle_set_command(name: &str, value: &str, temporary: bool) -> Result<()> {
533 let mut manager = EnvVarManager::new();
534 manager.load_all()?;
535
536 let permanent = !temporary;
537
538 manager.set(name, value, permanent)?;
539 if permanent {
540 println!("ā
Set {name} = \"{value}\"");
541 #[cfg(windows)]
542 println!("š Note: You may need to restart your terminal for changes to take effect");
543 } else {
544 println!("ā” Set {name} = \"{value}\" (temporary - current session only)");
545 }
546 Ok(())
547}
548
549fn handle_delete_command(pattern: &str, force: bool) -> Result<()> {
550 let mut manager = EnvVarManager::new();
551 manager.load_all()?;
552
553 let vars_to_delete: Vec<String> = manager
555 .get_pattern(pattern)
556 .into_iter()
557 .map(|v| v.name.clone())
558 .collect();
559
560 if vars_to_delete.is_empty() {
561 eprintln!("No variables found matching pattern: {pattern}");
562 return Ok(());
563 }
564
565 if !force && vars_to_delete.len() > 1 {
566 println!("About to delete {} variables:", vars_to_delete.len());
567 for name in &vars_to_delete {
568 println!(" - {name}");
569 }
570 print!("Continue? [y/N]: ");
571 std::io::stdout().flush()?;
572
573 let mut input = String::new();
574 std::io::stdin().read_line(&mut input)?;
575
576 if !input.trim().eq_ignore_ascii_case("y") {
577 println!("Cancelled.");
578 return Ok(());
579 }
580 }
581
582 for name in vars_to_delete {
584 manager.delete(&name)?;
585 println!("Deleted: {name}");
586 }
587 Ok(())
588}
589
590fn handle_analyze_command(analysis_type: &str) -> Result<()> {
591 let mut manager = EnvVarManager::new();
592 manager.load_all()?;
593 let vars = manager.list().into_iter().cloned().collect();
594 let analyzer = Analyzer::new(vars);
595
596 match analysis_type {
597 "duplicates" | "all" => {
598 let duplicates = analyzer.find_duplicates();
599 if !duplicates.is_empty() {
600 println!("Duplicate variables found:");
601 for (name, vars) in duplicates {
602 println!(" {}: {} instances", name, vars.len());
603 }
604 }
605 }
606 "invalid" => {
607 let validation = analyzer.validate_all();
608 for (name, result) in validation {
609 if !result.valid {
610 println!("Invalid variable: {name}");
611 for error in result.errors {
612 println!(" Error: {error}");
613 }
614 }
615 }
616 }
617 _ => {}
618 }
619 Ok(())
620}
621
622#[allow(clippy::too_many_lines)]
623fn handle_path_command(action: Option<PathAction>, check: bool, var: &str, permanent: bool) -> Result<()> {
624 let mut manager = EnvVarManager::new();
625 manager.load_all()?;
626
627 let path_var = manager.get(var).ok_or_else(|| eyre!("Variable '{}' not found", var))?;
629
630 let mut path_mgr = PathManager::new(&path_var.value);
631
632 if action.is_none() {
634 if check {
635 handle_path_check(&path_mgr, true);
636 }
637 handle_path_list(&path_mgr, false, false);
638 }
639
640 let command = action.expect("Action should be Some if we reach here");
641 match command {
642 PathAction::Add {
643 directory,
644 first,
645 create,
646 } => {
647 let path = Path::new(&directory);
648
649 if !path.exists() {
651 if create {
652 std::fs::create_dir_all(path)?;
653 println!("Created directory: {directory}");
654 } else if !path.exists() {
655 eprintln!("Warning: Directory does not exist: {directory}");
656 print!("Add anyway? [y/N]: ");
657 std::io::stdout().flush()?;
658
659 let mut input = String::new();
660 std::io::stdin().read_line(&mut input)?;
661
662 if !input.trim().eq_ignore_ascii_case("y") {
663 return Ok(());
664 }
665 }
666 }
667
668 if path_mgr.contains(&directory) {
670 println!("Directory already in {var}: {directory}");
671 return Ok(());
672 }
673
674 if first {
676 path_mgr.add_first(directory.clone());
677 println!("Added to beginning of {var}: {directory}");
678 } else {
679 path_mgr.add_last(directory.clone());
680 println!("Added to end of {var}: {directory}");
681 }
682
683 let new_value = path_mgr.to_string();
685 manager.set(var, &new_value, permanent)?;
686 }
687
688 PathAction::Remove { directory, all } => {
689 let removed = if all {
690 path_mgr.remove_all(&directory)
691 } else {
692 path_mgr.remove_first(&directory)
693 };
694
695 if removed > 0 {
696 println!("Removed {removed} occurrence(s) of: {directory}");
697 let new_value = path_mgr.to_string();
698 manager.set(var, &new_value, permanent)?;
699 } else {
700 println!("Directory not found in {var}: {directory}");
701 }
702 }
703
704 PathAction::Clean { dedupe, dry_run } => {
705 let invalid = path_mgr.get_invalid();
706 let duplicates = if dedupe { path_mgr.get_duplicates() } else { vec![] };
707
708 if invalid.is_empty() && duplicates.is_empty() {
709 println!("No invalid or duplicate entries found in {var}");
710 return Ok(());
711 }
712
713 if !invalid.is_empty() {
714 println!("Invalid/non-existent paths to remove:");
715 for path in &invalid {
716 println!(" - {path}");
717 }
718 }
719
720 if !duplicates.is_empty() {
721 println!("Duplicate paths to remove:");
722 for path in &duplicates {
723 println!(" - {path}");
724 }
725 }
726
727 if dry_run {
728 println!("\n(Dry run - no changes made)");
729 } else {
730 let removed_invalid = path_mgr.remove_invalid();
731 let removed_dupes = if dedupe {
732 path_mgr.deduplicate(false) } else {
734 0
735 };
736
737 println!("Removed {removed_invalid} invalid and {removed_dupes} duplicate entries");
738 let new_value = path_mgr.to_string();
739 manager.set(var, &new_value, permanent)?;
740 }
741 }
742
743 PathAction::Dedupe { keep_first, dry_run } => {
744 let duplicates = path_mgr.get_duplicates();
745
746 if duplicates.is_empty() {
747 println!("No duplicate entries found in {var}");
748 return Ok(());
749 }
750
751 println!("Duplicate paths to remove:");
752 for path in &duplicates {
753 println!(" - {path}");
754 }
755 println!(
756 "Strategy: keep {} occurrence",
757 if keep_first { "first" } else { "last" }
758 );
759
760 if dry_run {
761 println!("\n(Dry run - no changes made)");
762 } else {
763 let removed = path_mgr.deduplicate(keep_first);
764 println!("Removed {removed} duplicate entries");
765 let new_value = path_mgr.to_string();
766 manager.set(var, &new_value, permanent)?;
767 }
768 }
769
770 PathAction::Check { verbose } => {
771 handle_path_check(&path_mgr, verbose);
772 }
773
774 PathAction::List { numbered, check } => {
775 handle_path_list(&path_mgr, numbered, check);
776 }
777
778 PathAction::Move { from, to } => {
779 let from_idx = if let Ok(idx) = from.parse::<usize>() {
781 idx
782 } else {
783 path_mgr
784 .find_index(&from)
785 .ok_or_else(|| eyre!("Path not found: {}", from))?
786 };
787
788 let to_idx = match to.as_str() {
790 "first" => 0,
791 "last" => path_mgr.len() - 1,
792 _ => to.parse::<usize>().map_err(|_| eyre!("Invalid position: {}", to))?,
793 };
794
795 path_mgr.move_entry(from_idx, to_idx)?;
796 println!("Moved entry from position {from_idx} to {to_idx}");
797
798 let new_value = path_mgr.to_string();
799 manager.set(var, &new_value, permanent)?;
800 }
801 }
802
803 Ok(())
804}
805
806fn handle_path_check(path_mgr: &PathManager, verbose: bool) {
807 let entries = path_mgr.entries();
808 let mut issues = Vec::new();
809 let mut valid_count = 0;
810
811 for (idx, entry) in entries.iter().enumerate() {
812 let path = Path::new(entry);
813 let exists = path.exists();
814 let is_dir = path.is_dir();
815
816 if verbose || !exists {
817 let status = if !exists {
818 issues.push(format!("Not found: {entry}"));
819 "ā NOT FOUND"
820 } else if !is_dir {
821 issues.push(format!("Not a directory: {entry}"));
822 "ā ļø NOT DIR"
823 } else {
824 valid_count += 1;
825 "ā OK"
826 };
827
828 if verbose {
829 println!("[{idx:3}] {status} - {entry}");
830 }
831 } else if exists && is_dir {
832 valid_count += 1;
833 }
834 }
835
836 println!("\nPATH Analysis:");
838 println!(" Total entries: {}", entries.len());
839 println!(" Valid entries: {valid_count}");
840
841 let duplicates = path_mgr.get_duplicates();
842 if !duplicates.is_empty() {
843 println!(" Duplicates: {} entries", duplicates.len());
844 if verbose {
845 for dup in &duplicates {
846 println!(" - {dup}");
847 }
848 }
849 }
850
851 let invalid = path_mgr.get_invalid();
852 if !invalid.is_empty() {
853 println!(" Invalid entries: {}", invalid.len());
854 if verbose {
855 for inv in &invalid {
856 println!(" - {inv}");
857 }
858 }
859 }
860
861 if issues.is_empty() {
862 println!("\nā
No issues found!");
863 } else {
864 println!("\nā ļø {} issue(s) found", issues.len());
865 if !verbose {
866 println!("Run with --verbose for details");
867 }
868 }
869}
870
871fn handle_path_list(path_mgr: &PathManager, numbered: bool, check: bool) {
872 let entries = path_mgr.entries();
873
874 if entries.is_empty() {
875 println!("PATH is empty");
876 }
877
878 for (idx, entry) in entries.iter().enumerate() {
879 let prefix = if numbered { format!("[{idx:3}] ") } else { String::new() };
880
881 let suffix = if check {
882 let path = Path::new(entry);
883 if !path.exists() {
884 " [NOT FOUND]"
885 } else if !path.is_dir() {
886 " [NOT A DIRECTORY]"
887 } else {
888 ""
889 }
890 } else {
891 ""
892 };
893
894 println!("{prefix}{entry}{suffix}");
895 }
896}
897
898fn handle_export(
899 file: &str,
900 vars: &[String],
901 format: Option<String>,
902 source: Option<String>,
903 metadata: bool,
904 force: bool,
905) -> Result<()> {
906 if Path::new(&file).exists() && !force {
908 print!("File '{file}' already exists. Overwrite? [y/N]: ");
909 std::io::stdout().flush()?;
910
911 let mut input = String::new();
912 std::io::stdin().read_line(&mut input)?;
913
914 if !input.trim().eq_ignore_ascii_case("y") {
915 println!("Export cancelled.");
916 return Ok(());
917 }
918 }
919
920 let mut manager = EnvVarManager::new();
922 manager.load_all()?;
923
924 let mut vars_to_export = if vars.is_empty() {
926 manager.list().into_iter().cloned().collect()
927 } else {
928 let mut selected = Vec::new();
929 for pattern in vars {
930 let matched = manager.get_pattern(pattern);
931 selected.extend(matched.into_iter().cloned());
932 }
933 selected
934 };
935
936 if let Some(src) = source {
938 let source_filter = match src.as_str() {
939 "system" => envx_core::EnvVarSource::System,
940 "user" => envx_core::EnvVarSource::User,
941 "process" => envx_core::EnvVarSource::Process,
942 "shell" => envx_core::EnvVarSource::Shell,
943 _ => return Err(eyre!("Invalid source: {}", src)),
944 };
945
946 vars_to_export.retain(|v| v.source == source_filter);
947 }
948
949 if vars_to_export.is_empty() {
950 println!("No variables to export.");
951 return Ok(());
952 }
953
954 let export_format = if let Some(fmt) = format {
956 match fmt.as_str() {
957 "env" => ExportFormat::DotEnv,
958 "json" => ExportFormat::Json,
959 "yaml" | "yml" => ExportFormat::Yaml,
960 "txt" | "text" => ExportFormat::Text,
961 "ps1" | "powershell" => ExportFormat::PowerShell,
962 "sh" | "bash" => ExportFormat::Shell,
963 _ => return Err(eyre!("Unsupported format: {}", fmt)),
964 }
965 } else {
966 ExportFormat::from_extension(file)?
968 };
969
970 let exporter = Exporter::new(vars_to_export, metadata);
972 exporter.export_to_file(file, export_format)?;
973
974 println!("Exported {} variables to '{}'", exporter.count(), file);
975
976 Ok(())
977}
978
979fn handle_import(
980 file: &str,
981 vars: &[String],
982 format: Option<String>,
983 permanent: bool,
984 prefix: Option<&String>,
985 overwrite: bool,
986 dry_run: bool,
987) -> Result<()> {
988 if !Path::new(&file).exists() {
990 return Err(eyre!("File not found: {}", file));
991 }
992
993 let import_format = if let Some(fmt) = format {
995 match fmt.as_str() {
996 "env" => ImportFormat::DotEnv,
997 "json" => ImportFormat::Json,
998 "yaml" | "yml" => ImportFormat::Yaml,
999 "txt" | "text" => ImportFormat::Text,
1000 _ => return Err(eyre!("Unsupported format: {}", fmt)),
1001 }
1002 } else {
1003 ImportFormat::from_extension(file)?
1005 };
1006
1007 let mut importer = Importer::new();
1009 importer.import_from_file(file, import_format)?;
1010
1011 if !vars.is_empty() {
1013 importer.filter_by_patterns(vars);
1014 }
1015
1016 if let Some(pfx) = &prefix {
1018 importer.add_prefix(pfx);
1019 }
1020
1021 let import_vars = importer.get_variables();
1023
1024 if import_vars.is_empty() {
1025 println!("No variables to import.");
1026 return Ok(());
1027 }
1028
1029 let mut manager = EnvVarManager::new();
1031 manager.load_all()?;
1032
1033 let mut conflicts = Vec::new();
1034 for (name, _) in &import_vars {
1035 if manager.get(name).is_some() {
1036 conflicts.push(name.clone());
1037 }
1038 }
1039
1040 if !conflicts.is_empty() && !overwrite && !dry_run {
1041 println!("The following variables already exist:");
1042 for name in &conflicts {
1043 println!(" - {name}");
1044 }
1045
1046 print!("Overwrite existing variables? [y/N]: ");
1047 std::io::stdout().flush()?;
1048
1049 let mut input = String::new();
1050 std::io::stdin().read_line(&mut input)?;
1051
1052 if !input.trim().eq_ignore_ascii_case("y") {
1053 println!("Import cancelled.");
1054 return Ok(());
1055 }
1056 }
1057
1058 if dry_run {
1060 println!("Would import {} variables:", import_vars.len());
1061 for (name, value) in &import_vars {
1062 let status = if conflicts.contains(name) {
1063 " [OVERWRITE]"
1064 } else {
1065 " [NEW]"
1066 };
1067 println!(
1068 " {} = {}{}",
1069 name,
1070 if value.len() > 50 {
1071 format!("{}...", &value[..50])
1072 } else {
1073 value.clone()
1074 },
1075 status
1076 );
1077 }
1078 println!("\n(Dry run - no changes made)");
1079 } else {
1080 let mut imported = 0;
1082 let mut failed = 0;
1083
1084 for (name, value) in import_vars {
1085 match manager.set(&name, &value, permanent) {
1086 Ok(()) => imported += 1,
1087 Err(e) => {
1088 eprintln!("Failed to import {name}: {e}");
1089 failed += 1;
1090 }
1091 }
1092 }
1093
1094 println!("Imported {imported} variables");
1095 if failed > 0 {
1096 println!("Failed to import {failed} variables");
1097 }
1098 }
1099
1100 Ok(())
1101}
1102
1103fn handle_list_command(
1104 source: Option<&str>,
1105 query: Option<&str>,
1106 format: &str,
1107 sort: &str,
1108 names_only: bool,
1109 limit: Option<usize>,
1110 stats: bool,
1111) -> Result<()> {
1112 let mut manager = EnvVarManager::new();
1113 manager.load_all()?;
1114
1115 let mut vars = if let Some(q) = &query {
1117 manager.search(q)
1118 } else if let Some(src) = source {
1119 let source_filter = match src {
1120 "system" => envx_core::EnvVarSource::System,
1121 "user" => envx_core::EnvVarSource::User,
1122 "process" => envx_core::EnvVarSource::Process,
1123 "shell" => envx_core::EnvVarSource::Shell,
1124 _ => return Err(eyre!("Invalid source: {}", src)),
1125 };
1126 manager.filter_by_source(&source_filter)
1127 } else {
1128 manager.list()
1129 };
1130
1131 match sort {
1133 "name" => vars.sort_by(|a, b| a.name.cmp(&b.name)),
1134 "value" => vars.sort_by(|a, b| a.value.cmp(&b.value)),
1135 "source" => vars.sort_by(|a, b| format!("{:?}", a.source).cmp(&format!("{:?}", b.source))),
1136 _ => {}
1137 }
1138
1139 let total_count = vars.len();
1141 if let Some(lim) = limit {
1142 vars.truncate(lim);
1143 }
1144
1145 if stats || (format == "table" && !names_only) {
1147 print_statistics(&manager, &vars, total_count, query, source);
1148 }
1149
1150 if names_only {
1152 for var in vars {
1153 println!("{}", var.name);
1154 }
1155 return Ok(());
1156 }
1157
1158 match format {
1160 "json" => {
1161 println!("{}", serde_json::to_string_pretty(&vars)?);
1162 }
1163 "simple" => {
1164 for var in vars {
1165 println!("{} = {}", style(&var.name).cyan(), var.value);
1166 }
1167 }
1168 "compact" => {
1169 for var in vars {
1170 let source_str = format_source_compact(&var.source);
1171 println!(
1172 "{} {} = {}",
1173 source_str,
1174 style(&var.name).bright(),
1175 style(truncate_value(&var.value, 60)).dim()
1176 );
1177 }
1178 }
1179 _ => {
1180 print_table(vars, limit.is_some());
1181 }
1182 }
1183
1184 if let Some(lim) = limit {
1186 if total_count > lim {
1187 println!(
1188 "\n{}",
1189 style(format!(
1190 "Showing {lim} of {total_count} total variables. Use --limit to see more."
1191 ))
1192 .yellow()
1193 );
1194 }
1195 }
1196
1197 Ok(())
1198}
1199
1200pub fn handle_snapshot(args: SnapshotArgs) -> Result<()> {
1212 let snapshot_manager = SnapshotManager::new()?;
1213 let mut env_manager = EnvVarManager::new();
1214 env_manager.load_all()?;
1215
1216 match args.command {
1217 SnapshotCommands::Create { name, description } => {
1218 let vars = env_manager.list().into_iter().cloned().collect();
1219 let snapshot = snapshot_manager.create(name, description, vars)?;
1220 println!("ā
Created snapshot: {} (ID: {})", snapshot.name, snapshot.id);
1221 }
1222 SnapshotCommands::List => {
1223 let snapshots = snapshot_manager.list()?;
1224 if snapshots.is_empty() {
1225 println!("No snapshots found.");
1226 return Ok(());
1227 }
1228
1229 let mut table = Table::new();
1230 table.set_header(vec!["Name", "ID", "Created", "Variables", "Description"]);
1231
1232 for snapshot in snapshots {
1233 table.add_row(vec![
1234 snapshot.name,
1235 snapshot.id[..8].to_string(),
1236 snapshot.created_at.format("%Y-%m-%d %H:%M").to_string(),
1237 snapshot.variables.len().to_string(),
1238 snapshot.description.unwrap_or_default(),
1239 ]);
1240 }
1241
1242 println!("{table}");
1243 }
1244 SnapshotCommands::Show { snapshot } => {
1245 let snap = snapshot_manager.get(&snapshot)?;
1246 println!("Snapshot: {}", snap.name);
1247 println!("ID: {}", snap.id);
1248 println!("Created: {}", snap.created_at.format("%Y-%m-%d %H:%M:%S"));
1249 println!("Description: {}", snap.description.unwrap_or_default());
1250 println!("Variables: {}", snap.variables.len());
1251
1252 println!("\nFirst 10 variables:");
1254 for (i, (name, var)) in snap.variables.iter().take(10).enumerate() {
1255 println!(" {}. {} = {}", i + 1, name, var.value);
1256 }
1257
1258 if snap.variables.len() > 10 {
1259 println!(" ... and {} more", snap.variables.len() - 10);
1260 }
1261 }
1262 SnapshotCommands::Restore { snapshot, force } => {
1263 if !force {
1264 print!("ā ļø This will replace all current environment variables. Continue? [y/N] ");
1265 std::io::Write::flush(&mut std::io::stdout())?;
1266
1267 let mut input = String::new();
1268 std::io::stdin().read_line(&mut input)?;
1269 if !input.trim().eq_ignore_ascii_case("y") {
1270 println!("Cancelled.");
1271 return Ok(());
1272 }
1273 }
1274
1275 snapshot_manager.restore(&snapshot, &mut env_manager)?;
1276 println!("ā
Restored from snapshot: {snapshot}");
1277 }
1278 SnapshotCommands::Delete { snapshot, force } => {
1279 if !force {
1280 print!("ā ļø Delete snapshot '{snapshot}'? [y/N] ");
1281 std::io::Write::flush(&mut std::io::stdout())?;
1282
1283 let mut input = String::new();
1284 std::io::stdin().read_line(&mut input)?;
1285 if !input.trim().eq_ignore_ascii_case("y") {
1286 println!("Cancelled.");
1287 return Ok(());
1288 }
1289 }
1290
1291 snapshot_manager.delete(&snapshot)?;
1292 println!("ā
Deleted snapshot: {snapshot}");
1293 }
1294 SnapshotCommands::Diff { snapshot1, snapshot2 } => {
1295 let diff = snapshot_manager.diff(&snapshot1, &snapshot2)?;
1296
1297 if diff.added.is_empty() && diff.removed.is_empty() && diff.modified.is_empty() {
1298 println!("No differences found between snapshots.");
1299 return Ok(());
1300 }
1301
1302 if !diff.added.is_empty() {
1303 println!("ā Added in {snapshot2}:");
1304 for (name, var) in &diff.added {
1305 println!(" {} = {}", name, var.value);
1306 }
1307 }
1308
1309 if !diff.removed.is_empty() {
1310 println!("\nā Removed in {snapshot2}:");
1311 for (name, var) in &diff.removed {
1312 println!(" {} = {}", name, var.value);
1313 }
1314 }
1315
1316 if !diff.modified.is_empty() {
1317 println!("\nš Modified:");
1318 for (name, (old, new)) in &diff.modified {
1319 println!(" {name}:");
1320 println!(" Old: {}", old.value);
1321 println!(" New: {}", new.value);
1322 }
1323 }
1324 }
1325 }
1326
1327 Ok(())
1328}
1329
1330pub fn handle_profile(args: ProfileArgs) -> Result<()> {
1343 let mut profile_manager = ProfileManager::new()?;
1344 let mut env_manager = EnvVarManager::new();
1345 env_manager.load_all()?;
1346
1347 match args.command {
1348 ProfileCommands::Create { name, description } => {
1349 handle_profile_create(&mut profile_manager, &name, description)?;
1350 }
1351 ProfileCommands::List => {
1352 handle_profile_list(&profile_manager);
1353 }
1354 ProfileCommands::Show { name } => {
1355 handle_profile_show(&profile_manager, name)?;
1356 }
1357 ProfileCommands::Switch { name, apply } => {
1358 handle_profile_switch(&mut profile_manager, &mut env_manager, &name, apply)?;
1359 }
1360 ProfileCommands::Add {
1361 profile,
1362 name,
1363 value,
1364 override_system,
1365 } => {
1366 handle_profile_add(&mut profile_manager, &profile, &name, &value, override_system)?;
1367 }
1368 ProfileCommands::Remove { profile, name } => {
1369 handle_profile_remove(&mut profile_manager, &profile, &name)?;
1370 }
1371 ProfileCommands::Delete { name, force } => {
1372 handle_profile_delete(&mut profile_manager, &name, force)?;
1373 }
1374 ProfileCommands::Export { name, output } => {
1375 handle_profile_export(&profile_manager, &name, output)?;
1376 }
1377 ProfileCommands::Import { file, name, overwrite } => {
1378 handle_profile_import(&mut profile_manager, &file, name, overwrite)?;
1379 }
1380 ProfileCommands::Apply { name } => {
1381 handle_profile_apply(&mut profile_manager, &mut env_manager, &name)?;
1382 }
1383 }
1384
1385 Ok(())
1386}
1387
1388fn handle_profile_create(profile_manager: &mut ProfileManager, name: &str, description: Option<String>) -> Result<()> {
1389 profile_manager.create(name.to_string(), description)?;
1390 println!("ā
Created profile: {name}");
1391 Ok(())
1392}
1393
1394fn handle_profile_list(profile_manager: &ProfileManager) {
1395 let profiles = profile_manager.list();
1396 if profiles.is_empty() {
1397 println!("No profiles found.");
1398 }
1399
1400 let active = profile_manager.active().map(|p| &p.name);
1401 let mut table = Table::new();
1402 table.set_header(vec!["Name", "Variables", "Created", "Description", "Status"]);
1403
1404 for profile in profiles {
1405 let status = if active == Some(&profile.name) {
1406 "ā Active"
1407 } else {
1408 ""
1409 };
1410
1411 table.add_row(vec![
1412 profile.name.clone(),
1413 profile.variables.len().to_string(),
1414 profile.created_at.format("%Y-%m-%d").to_string(),
1415 profile.description.clone().unwrap_or_default(),
1416 status.to_string(),
1417 ]);
1418 }
1419
1420 println!("{table}");
1421}
1422
1423fn handle_profile_show(profile_manager: &ProfileManager, name: Option<String>) -> Result<()> {
1424 let profile = if let Some(name) = name {
1425 profile_manager
1426 .get(&name)
1427 .ok_or_else(|| color_eyre::eyre::eyre!("Profile '{}' not found", name))?
1428 } else {
1429 profile_manager
1430 .active()
1431 .ok_or_else(|| color_eyre::eyre::eyre!("No active profile"))?
1432 };
1433
1434 println!("Profile: {}", profile.name);
1435 println!("Description: {}", profile.description.as_deref().unwrap_or(""));
1436 println!("Created: {}", profile.created_at.format("%Y-%m-%d %H:%M:%S"));
1437 println!("Updated: {}", profile.updated_at.format("%Y-%m-%d %H:%M:%S"));
1438 if let Some(parent) = &profile.parent {
1439 println!("Inherits from: {parent}");
1440 }
1441 println!("\nVariables:");
1442
1443 for (name, var) in &profile.variables {
1444 let status = if var.enabled { "ā" } else { "ā" };
1445 let override_flag = if var.override_system { " [override]" } else { "" };
1446 println!(" {} {} = {}{}", status, name, var.value, override_flag);
1447 }
1448 Ok(())
1449}
1450
1451fn handle_profile_switch(
1452 profile_manager: &mut ProfileManager,
1453 env_manager: &mut EnvVarManager,
1454 name: &str,
1455 apply: bool,
1456) -> Result<()> {
1457 profile_manager.switch(name)?;
1458 println!("ā
Switched to profile: {name}");
1459
1460 if apply {
1461 profile_manager.apply(name, env_manager)?;
1462 println!("ā
Applied profile variables");
1463 }
1464 Ok(())
1465}
1466
1467fn handle_profile_add(
1468 profile_manager: &mut ProfileManager,
1469 profile: &str,
1470 name: &str,
1471 value: &str,
1472 override_system: bool,
1473) -> Result<()> {
1474 let prof = profile_manager
1475 .get_mut(profile)
1476 .ok_or_else(|| color_eyre::eyre::eyre!("Profile '{}' not found", profile))?;
1477
1478 prof.add_var(name.to_string(), value.to_string(), override_system);
1479 profile_manager.save()?;
1480
1481 println!("ā
Added {name} to profile {profile}");
1482 Ok(())
1483}
1484
1485fn handle_profile_remove(profile_manager: &mut ProfileManager, profile: &str, name: &str) -> Result<()> {
1486 let prof = profile_manager
1487 .get_mut(profile)
1488 .ok_or_else(|| color_eyre::eyre::eyre!("Profile '{}' not found", profile))?;
1489
1490 prof.remove_var(name)
1491 .ok_or_else(|| color_eyre::eyre::eyre!("Variable '{}' not found in profile", name))?;
1492
1493 profile_manager.save()?;
1494 println!("ā
Removed {name} from profile {profile}");
1495 Ok(())
1496}
1497
1498fn handle_profile_delete(profile_manager: &mut ProfileManager, name: &str, force: bool) -> Result<()> {
1499 if !force {
1500 print!("ā ļø Delete profile '{name}'? [y/N] ");
1501 std::io::Write::flush(&mut std::io::stdout())?;
1502
1503 let mut input = String::new();
1504 std::io::stdin().read_line(&mut input)?;
1505 if !input.trim().eq_ignore_ascii_case("y") {
1506 println!("Cancelled.");
1507 return Ok(());
1508 }
1509 }
1510
1511 profile_manager.delete(name)?;
1512 println!("ā
Deleted profile: {name}");
1513 Ok(())
1514}
1515
1516fn handle_profile_export(profile_manager: &ProfileManager, name: &str, output: Option<String>) -> Result<()> {
1517 let json = profile_manager.export(name)?;
1518
1519 if let Some(path) = output {
1520 std::fs::write(path, json)?;
1521 println!("ā
Exported profile to file");
1522 } else {
1523 println!("{json}");
1524 }
1525 Ok(())
1526}
1527
1528fn handle_profile_import(
1529 profile_manager: &mut ProfileManager,
1530 file: &str,
1531 name: Option<String>,
1532 overwrite: bool,
1533) -> Result<()> {
1534 let content = std::fs::read_to_string(file)?;
1535 let import_name = name.unwrap_or_else(|| "imported".to_string());
1536
1537 profile_manager.import(import_name.clone(), &content, overwrite)?;
1538 println!("ā
Imported profile: {import_name}");
1539 Ok(())
1540}
1541
1542fn handle_profile_apply(
1543 profile_manager: &mut ProfileManager,
1544 env_manager: &mut EnvVarManager,
1545 name: &str,
1546) -> Result<()> {
1547 profile_manager.apply(name, env_manager)?;
1548 println!("ā
Applied profile: {name}");
1549 Ok(())
1550}
1551
1552fn print_statistics(
1553 manager: &EnvVarManager,
1554 filtered_vars: &[&envx_core::EnvVar],
1555 total_count: usize,
1556 query: Option<&str>,
1557 source: Option<&str>,
1558) {
1559 let _term = Term::stdout();
1560
1561 let system_count = manager.filter_by_source(&envx_core::EnvVarSource::System).len();
1563 let user_count = manager.filter_by_source(&envx_core::EnvVarSource::User).len();
1564 let process_count = manager.filter_by_source(&envx_core::EnvVarSource::Process).len();
1565 let shell_count = manager.filter_by_source(&envx_core::EnvVarSource::Shell).len();
1566
1567 println!("{}", style("ā".repeat(60)).blue().bold());
1569 println!("{}", style("Environment Variables Summary").cyan().bold());
1570 println!("{}", style("ā".repeat(60)).blue().bold());
1571
1572 if query.is_some() || source.is_some() {
1574 print!(" {} ", style("Filter:").yellow());
1575 if let Some(q) = query {
1576 print!("query='{}' ", style(q).green());
1577 }
1578 if let Some(s) = source {
1579 print!("source={} ", style(s).green());
1580 }
1581 println!();
1582 println!(
1583 " {} {}/{} variables",
1584 style("Showing:").yellow(),
1585 style(filtered_vars.len()).green().bold(),
1586 total_count
1587 );
1588 } else {
1589 println!(
1590 " {} {} variables",
1591 style("Total:").yellow(),
1592 style(total_count).green().bold()
1593 );
1594 }
1595
1596 println!();
1597 println!(" {} By Source:", style("āŗ").cyan());
1598
1599 let max_count = system_count.max(user_count).max(process_count).max(shell_count);
1601 let bar_width = 30;
1602
1603 print_source_bar("System", system_count, max_count, bar_width, "red");
1604 print_source_bar("User", user_count, max_count, bar_width, "yellow");
1605 print_source_bar("Process", process_count, max_count, bar_width, "green");
1606 print_source_bar("Shell", shell_count, max_count, bar_width, "cyan");
1607
1608 println!("{}", style("ā".repeat(60)).blue());
1609 println!();
1610}
1611
1612fn print_source_bar(label: &str, count: usize, max: usize, width: usize, color: &str) {
1613 let filled = if max > 0 { (count * width / max).max(1) } else { 0 };
1614
1615 let bar = "ā".repeat(filled);
1616 let empty = "ā".repeat(width - filled);
1617
1618 let colored_bar = match color {
1619 "red" => style(bar).red(),
1620 "yellow" => style(bar).yellow(),
1621 "green" => style(bar).green(),
1622 "cyan" => style(bar).cyan(),
1623 _ => style(bar).white(),
1624 };
1625
1626 println!(
1627 " {:10} {} {}{} {}",
1628 style(label).bold(),
1629 colored_bar,
1630 style(empty).dim(),
1631 style(format!(" {count:4}")).bold(),
1632 style("vars").dim()
1633 );
1634}
1635
1636fn print_table(vars: Vec<&envx_core::EnvVar>, _is_limited: bool) {
1637 if vars.is_empty() {
1638 println!("{}", style("No environment variables found.").yellow());
1639 }
1640
1641 let mut table = Table::new();
1642
1643 table
1645 .set_content_arrangement(ContentArrangement::Dynamic)
1646 .set_width(120)
1647 .set_header(vec![
1648 Cell::new("Source").add_attribute(Attribute::Bold).fg(Color::Cyan),
1649 Cell::new("Name").add_attribute(Attribute::Bold).fg(Color::Cyan),
1650 Cell::new("Value").add_attribute(Attribute::Bold).fg(Color::Cyan),
1651 ]);
1652
1653 for var in vars {
1655 let (source_str, source_color) = format_source(&var.source);
1656 let truncated_value = truncate_value(&var.value, 50);
1657
1658 table.add_row(vec![
1659 Cell::new(source_str).fg(source_color),
1660 Cell::new(&var.name).fg(Color::White),
1661 Cell::new(truncated_value).fg(Color::Grey),
1662 ]);
1663 }
1664
1665 println!("{table}");
1666}
1667
1668fn format_source(source: &envx_core::EnvVarSource) -> (String, Color) {
1669 match source {
1670 envx_core::EnvVarSource::System => ("System".to_string(), Color::Red),
1671 envx_core::EnvVarSource::User => ("User".to_string(), Color::Yellow),
1672 envx_core::EnvVarSource::Process => ("Process".to_string(), Color::Green),
1673 envx_core::EnvVarSource::Shell => ("Shell".to_string(), Color::Cyan),
1674 envx_core::EnvVarSource::Application(app) => (format!("App:{app}"), Color::Magenta),
1675 }
1676}
1677
1678fn format_source_compact(source: &envx_core::EnvVarSource) -> console::StyledObject<String> {
1679 match source {
1680 envx_core::EnvVarSource::System => style("[SYS]".to_string()).red().bold(),
1681 envx_core::EnvVarSource::User => style("[USR]".to_string()).yellow().bold(),
1682 envx_core::EnvVarSource::Process => style("[PRC]".to_string()).green().bold(),
1683 envx_core::EnvVarSource::Shell => style("[SHL]".to_string()).cyan().bold(),
1684 envx_core::EnvVarSource::Application(app) => style(format!("[{}]", &app[..3.min(app.len())].to_uppercase()))
1685 .magenta()
1686 .bold(),
1687 }
1688}
1689
1690fn truncate_value(value: &str, max_len: usize) -> String {
1691 if value.len() <= max_len {
1692 value.to_string()
1693 } else {
1694 format!("{}...", &value[..max_len - 3])
1695 }
1696}